Sessions were in-memory, so a restart logged everyone out (same class as the relation wipe). Move them to the durable store, but LAZILY so anonymous/crawler traffic doesn't spam it: session/create mints a sid with no row; the row appears on the first session/set (a login). A per-boot epoch (one durable write at startup, host/session-init!) keeps sids unique across restarts without a write per request. - lib/host/session.sx: lazy backend (create = no row, set = create row, exists = row written) + epoch/in-memory-counter sid generation. - serve.sh: point the session store at the durable backend + host/session-init!. - blog.sx: host/current-principal is now a durable read, so host/auth-footer (home + post footers) had to move OUT of the quasiquote into let bindings — a perform during page-tree build raises VmSuspended (the whole site 500'd for a beat). Principal computed once per page. - 2 session tests: create writes no row, set creates the row. 249/249. Verified live: site renders (anon + authed), login + footer survive a container force-recreate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
4.3 KiB
Plaintext
82 lines
4.3 KiB
Plaintext
;; lib/host/session.sx — durable, signed sessions for the host.
|
|
;; Backs Dream's session middleware ops (session/create|exists|get|set|clear)
|
|
;; with the SAME durable persist KV the blog uses, so a login survives restarts.
|
|
;; The session cookie carries only a signed sid (dream-sessions-signed): the sid
|
|
;; itself is a persisted monotonic counter ("s1", "s2", …) — cheap and ordered —
|
|
;; and the HMAC signature (dr/sess-hash, keyed by host/session-secret) makes a
|
|
;; guessed or forged cookie unusable. http-listen serialises handler calls under a
|
|
;; mutex, so the counter increment is race-free.
|
|
;;
|
|
;; Depends on lib/dream/session.sx (dream-sessions-signed + cookie helpers) and
|
|
;; lib/persist/* (the KV backend). Wired into host/make-app via host/sessions.
|
|
|
|
;; ── store (durable persist KV, injectable; mirrors host/blog-store) ──
|
|
(define host/session-store (persist/open))
|
|
(define host/session-use-store! (fn (b) (set! host/session-store b)))
|
|
|
|
;; ── signing secret (override from $SX_SESSION_SECRET in serve.sh) ────
|
|
(define host/session-secret "rose-ash-host-dev-secret-change-me")
|
|
(define host/session-set-secret! (fn (s) (set! host/session-secret s)))
|
|
|
|
;; ── keys ────────────────────────────────────────────────────────────
|
|
(define host/-sess-key (fn (sid) (str "session:" sid)))
|
|
(define host/-sess-epoch-key "session:-epoch")
|
|
|
|
;; sid generation: a per-BOOT epoch (one durable write at startup) + an in-memory
|
|
;; counter. The epoch keeps sids unique across restarts WITHOUT a write per
|
|
;; request, so anonymous traffic costs no disk. host/session-init! bumps the epoch
|
|
;; on boot (serve.sh); without it (e.g. tests) epoch 0 is fine within one process.
|
|
(define host/session-epoch 0)
|
|
(define host/session-ctr 0)
|
|
(define host/session-init!
|
|
(fn ()
|
|
(let ((e (+ 1 (or (persist/backend-kv-get host/session-store host/-sess-epoch-key) 0))))
|
|
(begin
|
|
(persist/backend-kv-put host/session-store host/-sess-epoch-key e)
|
|
(set! host/session-epoch e)
|
|
(set! host/session-ctr 0)))))
|
|
(define host/-sess-next-sid
|
|
(fn ()
|
|
(begin
|
|
(set! host/session-ctr (+ host/session-ctr 1))
|
|
(str "s" host/session-epoch "-" host/session-ctr))))
|
|
|
|
;; ── backend io fn: dispatch session/* ops onto the persist KV ───────
|
|
;; LAZY: session/create mints a sid but writes NO row, so an anonymous request
|
|
;; (which never sets a field) leaves no durable trace — the store isn't spammed by
|
|
;; crawlers. The row appears on the first session/set (i.e. login), so a logged-in
|
|
;; session persists and survives a restart; session/exists is "has a written row".
|
|
(define host/session-backend
|
|
(fn (op)
|
|
(let ((kind (get op :op)))
|
|
(cond
|
|
((= kind "session/create") (host/-sess-next-sid))
|
|
((= kind "session/exists")
|
|
(persist/backend-kv-has? host/session-store (host/-sess-key (get op :sid))))
|
|
((= kind "session/get")
|
|
(get
|
|
(or (persist/backend-kv-get host/session-store (host/-sess-key (get op :sid))) {})
|
|
(get op :key)))
|
|
((= kind "session/set")
|
|
(let ((sid (get op :sid)))
|
|
(persist/backend-kv-put host/session-store (host/-sess-key sid)
|
|
(assoc
|
|
(or (persist/backend-kv-get host/session-store (host/-sess-key sid)) {})
|
|
(get op :key)
|
|
(get op :val)))))
|
|
((= kind "session/load")
|
|
(or (persist/backend-kv-get host/session-store (host/-sess-key (get op :sid))) {}))
|
|
((= kind "session/clear")
|
|
(persist/backend-kv-delete host/session-store (host/-sess-key (get op :sid))))
|
|
(else nil)))))
|
|
|
|
;; ── middleware for the host pipeline: signed cookie + durable backend ─
|
|
(define host/sessions
|
|
(fn () (dream-sessions-signed host/session-backend host/session-secret)))
|
|
|
|
;; ── handler-facing helpers ──────────────────────────────────────────
|
|
;; The logged-in principal (or nil), and login/logout writing the session field.
|
|
(define host/current-principal (fn (req) (dream-session-field req :principal)))
|
|
(define host/login! (fn (req principal) (dream-set-session-field req :principal principal)))
|
|
(define host/logout! (fn (req) (dream-invalidate-session req)))
|