oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1// Command calendar wires the app: resolve the platform's activation
2// argv, build the app, serve.
3//
4// The platform activation contract has no $PORT. The platform — or a
5// developer — invokes this binary as
6//
7// calendar --socket <path> --db <path>
8//
9// and rastrillo.Resolve/Serve own that contract end to end: activation
10// argv, LISTEN_FDS, $STATE_DIRECTORY, GET /healthz, GET /api/version,
11// SIGTERM drain and the baseline security headers. None of it is
12// hand-rolled here.
13package main
14
15import (
16 "amadan.net/oficina/home/connect"
17 "log/slog"
18 "os"
19 "strings"
20
21 "amadan.net/rastrillo/rastrillo"
22 "amadan.net/rastrillo/rastrillo/mail"
23
24 calendar "amadan.net/oficina/calendar/internal/calendar"
25)
26
27func main() {
28 logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
29
30 // -env is stripped from argv before Resolve sees it: Resolve's own
31 // flag set knows only -socket, -addr and -db, and errors on
32 // anything else.
33 envFlag, rest := extractEnvFlag(os.Args[1:])
34 os.Args = append(os.Args[:1], rest...)
35
36 // Resolve, not Run: this app opens its own database handle inside
37 // calendar.New, so Options.DBPath must be empty again by the Serve
38 // call below or Serve would open a second connection to the same
39 // file. Run re-parses argv and would repopulate it.
40 opts, err := rastrillo.Resolve(rastrillo.Options{DBPath: "calendar.db", Logger: logger})
41 if err != nil {
42 logger.Error("resolve activation", "err", err)
43 os.Exit(1)
44 }
45
46 origin := os.Getenv("CALENDAR_ORIGIN")
47 if origin == "" && opts.Addr != "" {
48 origin = "http://" + opts.Addr
49 }
50
51 // A mailer only when both halves of some prefix are configured.
52 // calendar.New's own fail-closed rule turns "none were" into a
53 // startup refusal outside development rather than a silent
54 // no-mail instance.
55 var mailer mail.Sender
56 if prefix, ok := smtpPrefix(); ok {
57 mailer = mail.FromEnv(prefix, logger)
58 }
59
60 app, err := calendar.New(calendar.Config{
61 Home: connect.CredentialsFromEnv("CALENDAR"),
62 DBPath: opts.DBPath,
63 Env: chooseEnv(envFlag, os.Getenv("CALENDAR_ENV")),
64 Origin: origin,
65 InstanceKey: os.Getenv("CALENDAR_INSTANCE_KEY"),
66 DEK: os.Getenv("CALENDAR_DEK"),
67 Mailer: mailer,
68 AllowedAddresses: os.Getenv("CALENDAR_ALLOWED_ADDRESSES"),
69 Logger: logger,
70 })
71 if err != nil {
72 logger.Error("build app", "err", err)
73 os.Exit(1)
74 }
75 defer app.Close()
76
77 // The router, the error page and the CSP (calendar.ServeOptions).
78 opts = app.ServeOptions(opts)
79 opts.DBPath = ""
80 opts.NextDue = app.NextDue
81 if err := rastrillo.Serve(opts); err != nil {
82 logger.Error("serve failed", "err", err)
83 os.Exit(1)
84 }
85}
86
87// smtpPrefix picks which <PREFIX>_SMTP_* set to build the mailer from,
88// preferring the app's own over the platform's.
89//
90// The platform delivers CARLOS_SMTP_* — that is what `carlos email
91// enable` writes, and an app that reads only its own prefix boots into
92// a fail-closed refusal on an instance whose mail is configured
93// perfectly well. This app did exactly that on its first deploy: eleven
94// consecutive wake failures, every one of them
95//
96// mail is not configured (set $CALENDAR_SMTP_HOST and
97// $CALENDAR_SMTP_FROM) ... refusing to start
98//
99// on an instance where CARLOS_SMTP_HOST and CARLOS_SMTP_FROM were both
100// set. The refusal was right and the prefix was wrong.
101//
102// CALENDAR_ still wins where it is set, so an operator can point this
103// one app at a different relay without touching the platform's own
104// settings — which is how docs is configured today.
105func smtpPrefix() (string, bool) {
106 for _, prefix := range []string{"CALENDAR", "CARLOS"} {
107 host := strings.TrimSpace(os.Getenv(prefix + "_SMTP_HOST"))
108 from := strings.TrimSpace(os.Getenv(prefix + "_SMTP_FROM"))
109 if host != "" && from != "" {
110 return prefix, true
111 }
112 }
113 return "", false
114}
115
116// chooseEnv decides the environment, and is deliberately asymmetric.
117//
118// "development" is honoured ONLY from the literal -env flag, never from
119// the process environment. Platform instance configuration IS
120// environment variables — $CALENDAR_ORIGIN, $CALENDAR_INSTANCE_KEY, the
121// SMTP set — so a stray $CALENDAR_ENV=development sitting one line away
122// from its neighbours in the same config block would be a
123// production-reachable path to the public development instance key, to
124// an allowlist that admits everyone, and to every sign-in link landing
125// in the instance's own log with its token intact.
126//
127// Anything else from the environment is taken at face value; the
128// default, and the answer for an empty environment, is production.
129func chooseEnv(flagEnv, osEnv string) string {
130 if flagEnv != "" {
131 return flagEnv
132 }
133 if osEnv != "" && osEnv != "development" {
134 return osEnv
135 }
136 return "production"
137}
138
139// extractEnvFlag pulls -env/--env (space- or =-separated) out of args,
140// returning its value and everything else in order.
141func extractEnvFlag(args []string) (string, []string) {
142 var env string
143 rest := make([]string, 0, len(args))
144 for i := 0; i < len(args); i++ {
145 a := args[i]
146 switch {
147 case a == "-env" || a == "--env":
148 if i+1 < len(args) {
149 env = args[i+1]
150 i++
151 }
152 case strings.HasPrefix(a, "-env="):
153 env = strings.TrimPrefix(a, "-env=")
154 case strings.HasPrefix(a, "--env="):
155 env = strings.TrimPrefix(a, "--env=")
156 default:
157 rest = append(rest, a)
158 }
159 }
160 return env, rest
161}
162