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>
26 lines
1.4 KiB
Plaintext
26 lines
1.4 KiB
Plaintext
;; lib/host/router.sx — Host application assembly. A host app is a single Dream
|
|
;; router built from per-domain route groups, with a built-in health endpoint and
|
|
;; a JSON 404 fallback so the native OCaml HTTP server has one entry point:
|
|
;; request -> response. Each subsystem contributes a list of Dream routes (see
|
|
;; lib/host/feed.sx); host/make-app concatenates them under one router.
|
|
;; dr/flatten-routes (Dream) flattens the nested groups, so a group is just a list
|
|
;; of routes. Depends on lib/dream/router.sx + lib/host/handler.sx + the host
|
|
;; session middleware (lib/host/session.sx) and login routes (lib/host/auth.sx).
|
|
|
|
;; Liveness probe — GET /health -> 200 {"ok":true,"data":"healthy"}.
|
|
(define host/health-route
|
|
(dream-get "/health" (fn (req) (host/ok "healthy"))))
|
|
|
|
;; Build the host app from a list of route groups (each a list of Dream routes).
|
|
;; The health route + login routes are always mounted; Dream's router returns a
|
|
;; JSON 404 for unmatched paths, which host endpoints override per-domain as
|
|
;; needed. The WHOLE app is wrapped in the signed-session middleware so every
|
|
;; request carries a session and any handler can log a principal in/out — this is
|
|
;; the front door, so sessions are not optional.
|
|
(define host/make-app
|
|
(fn (groups)
|
|
(let ((router (dream-router
|
|
(cons host/health-route
|
|
(cons host/auth-routes groups)))))
|
|
((host/sessions) router))))
|