Unblock the guarded blog write routes for browsers: a login form sets a signed session cookie that the same routes accept (alongside Bearer), so publishing works end-to-end on blog.rose-ash.com without Quart. - kernel: http-listen emit serialises a response :set-cookies LIST as one Set-Cookie header each (a headers dict can't hold more than one). Purely additive — responses without :set-cookies are unchanged. - server.sx: host/-dream->native forwards :set-cookies to the native resp. - lib/host/session.sx: durable, signed sessions on the persist KV (session/create|exists|get|set|clear), wired via dream-sessions-signed. - lib/host/auth.sx: GET/POST /login + POST /logout; host/require-user accepts a session principal OR a Bearer token. - router.sx: host/make-app wraps the whole app in the session middleware and auto-mounts /login + /logout — the front door always has sessions. - blog.sx: write routes use host/require-user; serve.sh flips POST /new from the experimental UNGUARDED route to the guarded write routes, with admin creds + signing secret + ACL grant from the container env. - session conformance suite (12): login->cookie->guarded write 201; no cookie/forged/logged-out -> 401; Bearer fallback still works. Verified live on blog.rose-ash.com: 401 unauthenticated, 303 login, 303 publish, anonymous read renders, post persists across container recreate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
3.6 KiB
Plaintext
71 lines
3.6 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-counter-key "session:-counter")
|
|
|
|
;; mint the next sid from a persisted counter (signature guards guessability)
|
|
(define host/-sess-next-sid
|
|
(fn ()
|
|
(let ((n (+ 1 (or (persist/backend-kv-get host/session-store host/-sess-counter-key) 0))))
|
|
(begin
|
|
(persist/backend-kv-put host/session-store host/-sess-counter-key n)
|
|
(str "s" n)))))
|
|
|
|
;; ── backend io fn: dispatch session/* ops onto the persist KV ───────
|
|
(define host/session-backend
|
|
(fn (op)
|
|
(let ((kind (get op :op)))
|
|
(cond
|
|
((= kind "session/create")
|
|
(let ((sid (host/-sess-next-sid)))
|
|
(begin
|
|
(persist/backend-kv-put host/session-store (host/-sess-key sid) {})
|
|
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)))
|