Files
rose-ash/lib/host/serve.sh
giles 3b8e1dfe2e 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>
2026-06-25 21:51:41 +00:00

137 lines
5.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# host-on-sx live server launcher. Loads the kernel stdlib, the subsystem
# libraries, and the host modules into one sx_server process, then calls
# (host/serve PORT ...) which binds the native http-listen server to the
# Dream-shaped host app. Runs in the FOREGROUND (http-listen blocks), so this
# doubles as a container entrypoint and a local launcher.
#
# Usage:
# bash lib/host/serve.sh # serve on $HOST_PORT (default 8910)
# HOST_PORT=8920 bash lib/host/serve.sh # pick a port
#
# The module list is kept identical to lib/host/conformance.sh so what serves is
# exactly what the suites verify.
set -uo pipefail
# Project root: SX_PROJECT_DIR in containers (set to /app by the compose stack),
# else the git toplevel for local runs.
cd "${SX_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo .)}"
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
if [ ! -x "$SX_SERVER" ]; then
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
fi
if [ ! -x "$SX_SERVER" ]; then
echo "ERROR: sx_server.exe not found." >&2
exit 1
fi
PORT="${HOST_PORT:-8910}"
# Modules: every load line from conformance.sh's MODULES list, minus the ledger
# (not needed to serve). server.sx supplies host/serve.
MODULES=(
"spec/stdlib.sx"
"lib/r7rs.sx"
"lib/apl/runtime.sx"
"lib/datalog/tokenizer.sx"
"lib/datalog/parser.sx"
"lib/datalog/unify.sx"
"lib/datalog/db.sx"
"lib/datalog/builtins.sx"
"lib/datalog/aggregates.sx"
"lib/datalog/strata.sx"
"lib/datalog/eval.sx"
"lib/datalog/api.sx"
"lib/datalog/magic.sx"
"lib/acl/schema.sx"
"lib/acl/facts.sx"
"lib/acl/engine.sx"
"lib/acl/explain.sx"
"lib/acl/audit.sx"
"lib/acl/federation.sx"
"lib/acl/api.sx"
"lib/relations/schema.sx"
"lib/relations/engine.sx"
"lib/relations/api.sx"
"lib/relations/explain.sx"
"lib/relations/federation.sx"
"lib/relations/tree.sx"
"lib/feed/normalize.sx"
"lib/feed/stream.sx"
"lib/feed/api.sx"
"lib/persist/event.sx"
"lib/persist/backend.sx"
"lib/persist/log.sx"
"lib/persist/kv.sx"
"lib/persist/api.sx"
"lib/persist/durable.sx"
"spec/render.sx"
"web/adapter-html.sx"
"lib/dream/types.sx"
"lib/dream/json.sx"
"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"
"lib/host/relations.sx"
"lib/host/blog.sx"
"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
echo "(epoch $EPOCH)"; echo "(load \"$M\")"; EPOCH=$((EPOCH+1))
done
# Point the blog at the DURABLE file backend (persists under $SX_PERSIST_DIR),
# then 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-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 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"