oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Command sheets is the Sheets instance binary.
2//
3// The platform activation contract has no $PORT: the platform (or a
4// developer) invokes this binary as
5//
6// sheets --socket <path> --db <path>
7//
8// and rastrillo.Resolve/Serve own that contract end to end — activation
9// argv, LISTEN_FDS, $STATE_DIRECTORY, GET /healthz, GET /api/version,
10// SIGTERM drain, baseline security headers. This file only wires the
11// app's own *gorm.DB handle (via rastrillo's db package) into
12// sheets.New, because the app needs GORM, not the raw *sql.DB
13// rastrillo.Options.Router would hand back. It also runs migrate.Apply
14// against sheets.BootSchema before anything else touches the database —
15// see internal/sheets/migrations.go.
16//
17// rastrillo.Resolve/Run's shape at v0.21.0 differs from a zero-argument
18// Resolve()/Run(handler) pair: Resolve takes and returns an Options, and
19// an app that opens its own database handle (as this one does, for
20// gorm) uses Resolve+Serve directly rather than Run — see
21// $(go env GOMODCACHE)/github.com/carlosframework/rastrillo@v0.21.0/SKILL.md
22// and run.go's doc comment on Resolve.
23package main
24
25import (
26 "context"
27 "log/slog"
28 "os"
29
30 "amadan.net/rastrillo/rastrillo"
31 rdb "amadan.net/rastrillo/rastrillo/db"
32 "amadan.net/rastrillo/rastrillo/migrate"
33
34 "amadan.net/oficina/sheets/internal/sheets"
35)
36
37func main() {
38 logger := slog.Default()
39
40 opts, err := rastrillo.Resolve(rastrillo.Options{Logger: logger})
41 if err != nil {
42 logger.Error("resolve", "err", err)
43 os.Exit(1)
44 }
45
46 d, err := rdb.Open(opts.DBPath, logger)
47 if err != nil {
48 logger.Error("open db", "err", err)
49 os.Exit(1)
50 }
51 defer d.Close()
52
53 // Ledgered, once-at-boot, never re-run or reversed — see
54 // internal/sheets/migrations.go's Schema/BootSchema/Models and
55 // AGENTS.md's additive-only constraint. A boot that can't reconcile
56 // its schema (a structural diff migrate.Apply doesn't recognise as
57 // its own pending migrations) refuses rather than guessing, which
58 // is why this is a hard exit, same as a failed db.Open above.
59 if _, err := migrate.Apply(context.Background(), d, sheets.BootSchema); err != nil {
60 logger.Error("migrate", "err", err)
61 os.Exit(1)
62 }
63
64 // Sign-in configuration is validated before anything serves. Every
65 // failure here is one where continuing would produce an instance
66 // that looks like it works: no mailer means magic links get written
67 // to platform-held logs instead of emailed, and an empty allowlist
68 // means anyone who can receive mail can hold a session. Both answer
69 // /healthz perfectly well, which is why this is a hard exit and not
70 // a warning.
71 authEnv, err := sheets.LoadAuthEnv(logger)
72 if err != nil {
73 logger.Error("sign-in", "err", err)
74 os.Exit(1)
75 }
76 // Same shape as the sign-in refusals above, and for the same
77 // reason: an instance with no DEK answers /healthz perfectly well
78 // and then fails on the first cell anybody types. The key arrives
79 // as platform config and is never written to the database — see
80 // internal/sheets/keys.go and spec §2.1.
81 sealer, err := sheets.LoadSealer()
82 if err != nil {
83 logger.Error("sealing", "err", err)
84 os.Exit(1)
85 }
86
87 // The app is built first and auth attached second, because the two
88 // are mutually dependent: auth's admission gate has to read this
89 // app's grants table so a guest somebody shared a sheet with can
90 // sign in at all. See App.Admits.
91 app := sheets.New(sheets.Config{
92 DB: d.G,
93 Sealer: sealer,
94 Origin: authEnv.Origin,
95 Admit: sheets.Admits(authEnv.Owners),
96 })
97
98 signin, err := sheets.NewAuth(d.Writer(), authEnv, logger, app)
99 if err != nil {
100 logger.Error("sign-in", "err", err)
101 os.Exit(1)
102 }
103 app.UseAuth(signin)
104 if err := app.ConfigureHome(authEnv.Home); err != nil {
105 logger.Error("home configuration unavailable")
106 os.Exit(1)
107 }
108
109 if err := app.ConfigureNative(); err != nil {
110 logger.Error("native configuration unavailable")
111 os.Exit(1)
112 }
113
114 // The router, the error page and the CSP (sheets.ServeOptions).
115 opts = app.ServeOptions(opts)
116
117 // The app opened its own database handle above (db.Open already
118 // Pings eagerly, which satisfies Serve's boot-materialization duty
119 // for a hibernate route's activator), so DBPath is blanked here —
120 // otherwise Serve would open a second connection to the same file.
121 opts.DBPath = ""
122
123 err = rastrillo.Serve(opts)
124 // Sign-in links are sent after the response (mailqueue.go); send
125 // what is still queued before the process goes.
126 sheets.DrainSigninMail()
127 if err != nil {
128 logger.Error("serve", "err", err)
129 os.Exit(1)
130 }
131}
132