host: live writes via signed sessions + kernel multi-Set-Cookie (193/193)
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>
This commit is contained in:
@@ -73,9 +73,12 @@ MODULES=(
|
||||
"lib/dream/auth.sx"
|
||||
"lib/dream/error.sx"
|
||||
"lib/dream/form.sx"
|
||||
"lib/dream/session.sx"
|
||||
"lib/dream/router.sx"
|
||||
"lib/host/handler.sx"
|
||||
"lib/host/middleware.sx"
|
||||
"lib/host/session.sx"
|
||||
"lib/host/auth.sx"
|
||||
"lib/host/sxtp.sx"
|
||||
"lib/host/router.sx"
|
||||
"lib/host/feed.sx"
|
||||
@@ -84,6 +87,13 @@ MODULES=(
|
||||
"lib/host/server.sx"
|
||||
)
|
||||
|
||||
# Admin login credentials + session signing secret. Override via the container
|
||||
# env; the in-source defaults are dev-only. The blog write routes are now GUARDED
|
||||
# (session login or Bearer), so these gate publishing on blog.rose-ash.com.
|
||||
ADMIN_USER="${SX_ADMIN_USER:-admin}"
|
||||
ADMIN_PASS="${SX_ADMIN_PASSWORD:-letmein}"
|
||||
SESSION_SECRET="${SX_SESSION_SECRET:-rose-ash-host-dev-secret-change-me}"
|
||||
|
||||
EPOCH=1
|
||||
{
|
||||
for M in "${MODULES[@]}"; do
|
||||
@@ -95,16 +105,32 @@ EPOCH=1
|
||||
echo "(epoch $EPOCH)"
|
||||
echo "(eval \"(host/blog-use-store! (persist/durable-backend))\")"
|
||||
EPOCH=$((EPOCH+1))
|
||||
# Session signing secret + admin login credentials, then grant the admin
|
||||
# principal "edit" on "blog" so a logged-in session passes the ACL gate on the
|
||||
# write routes. Sessions stay IN-MEMORY (default store) — logins reset on
|
||||
# restart but the durable KV isn't spammed by anonymous/ crawler sessions
|
||||
# (lazy session creation is a future lib/dream/session.sx improvement).
|
||||
echo "(epoch $EPOCH)"
|
||||
echo "(eval \"(host/session-set-secret! \\\"$SESSION_SECRET\\\")\")"
|
||||
EPOCH=$((EPOCH+1))
|
||||
echo "(epoch $EPOCH)"
|
||||
echo "(eval \"(host/auth-set-admin! \\\"$ADMIN_USER\\\" \\\"$ADMIN_PASS\\\")\")"
|
||||
EPOCH=$((EPOCH+1))
|
||||
echo "(epoch $EPOCH)"
|
||||
echo "(eval \"(acl/load! (list (acl-grant \\\"$ADMIN_USER\\\" \\\"edit\\\" \\\"blog\\\")))\")"
|
||||
EPOCH=$((EPOCH+1))
|
||||
# Idempotently seed a welcome post (sx_content = SX element markup, the editor's
|
||||
# content model). Re-seeding is a no-op if the slug already exists.
|
||||
echo "(epoch $EPOCH)"
|
||||
echo "(eval \"(host/blog-seed! \\\"welcome\\\" \\\"Welcome to the SX host\\\" \\\"(article (h1 \\\\\\\"Welcome to the SX host\\\\\\\") (p \\\\\\\"Rendered by lib/host via render-to-html, from the durable SX store.\\\\\\\"))\\\" \\\"published\\\")\")"
|
||||
EPOCH=$((EPOCH+1))
|
||||
echo "(epoch $EPOCH)"
|
||||
# Anonymous read endpoints: feed timeline + relations container reads + blog
|
||||
# post detail (blog-routes LAST — the :slug catch-all must not shadow the rest).
|
||||
# Guarded write groups (auth/ACL or internal-HMAC) are added here once their
|
||||
# injected policy is supplied at wiring time.
|
||||
# EXPERIMENTAL: host/blog-open-create-routes mounts POST /new UNGUARDED (no
|
||||
# auth) so the editor can publish end-to-end on the experimental subdomain.
|
||||
# Create-only (no PUT/DELETE). GATE (Caddy basicauth / sessions) before real use.
|
||||
echo "(eval \"(host/serve $PORT (list host/feed-routes host/relations-routes host/blog-open-create-routes host/blog-routes))\")"
|
||||
# Anonymous reads (feed timeline + relations container reads + blog post detail)
|
||||
# plus the GUARDED blog write routes: POST /new (editor form ingest), POST/PUT/
|
||||
# DELETE /posts behind host/require-user (session login OR Bearer) + ACL. make-app
|
||||
# auto-mounts /login + /logout and wraps everything in the signed-session
|
||||
# middleware, so a browser logs in then publishes. The bearer resolver is a stub
|
||||
# (no API tokens configured) — browser session is the live auth path for now.
|
||||
# blog-routes LAST — its GET /:slug catch-all must not shadow the rest.
|
||||
echo "(eval \"(host/serve $PORT (list host/feed-routes host/relations-routes (host/blog-write-routes (fn (tok) nil)) host/blog-routes))\")"
|
||||
} | exec "$SX_SERVER"
|
||||
|
||||
Reference in New Issue
Block a user