host: Phase 2 — middleware (auth+ACL+error) + guarded POST /feed, 43/43

Composable handler->handler layers over Dream's primitives, with auth and
permission POLICY injected so the layer is policy-free and testable:

- middleware.sx: host/wrap-errors (JSON 500 via dream-catch-with),
  host/require-auth (bearer->principal via dream-bearer-token, JSON 401,
  injected token resolver), host/require-permission (lib/acl acl/permit? gate,
  JSON 403, injected resource extractor), host/pipeline (first = outermost)
- feed.sx: POST /feed via host/feed-write-routes — auth ∘ ACL(post,feed) ∘
  wrap-errors over host/feed-create (parse JSON body -> feed/post -> 201;
  non-object -> 400). Created activity reads back via GET /feed.
- middleware suite (9) + feed write tests (6 new); conformance preloads now
  include the Datalog engine + ACL subsystem + Dream auth/error.

ACL works with string atoms (no symbol coercion). Mute/prefs layer and sxtp.sx
deferred to the next tick.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 19:48:18 +00:00
parent d5a1c8370c
commit 2ffdd6f078
6 changed files with 291 additions and 20 deletions

View File

@@ -36,7 +36,8 @@ host — no `ocaml-on-sx` dependency.
## Status (rolling)
`bash lib/host/conformance.sh`**28/28** (3 suites: handler, router, feed). Phase 1 DONE.
`bash lib/host/conformance.sh`**43/43** (4 suites: handler, middleware, router,
feed). Phase 1 DONE; Phase 2 in progress (middleware + write endpoint DONE, SXTP next).
## Ground rules
@@ -84,9 +85,18 @@ lib/host/sxtp.sx subsystem APIs (feed/search/commerce/…
- [x] `conformance.sh` (mirrors `lib/dream`'s runner) — 28/28
## Phase 2 — Middleware + SXTP
- [ ] `middleware.sx` — composable auth/acl/mute/error layers
- [ ] `sxtp.sx` — host↔subsystem wire format (align with existing spec)
- [ ] migrate a write endpoint (auth + permission + action)
- [x] `middleware.sx` — composable layers as `handler->handler`: `host/wrap-errors`
(JSON 500), `host/require-auth` (bearer -> principal, JSON 401, INJECTED token
resolver), `host/require-permission` (ACL `acl/permit?` gate, JSON 403,
INJECTED resource extractor), `host/pipeline` (first = outermost). Reuses
Dream's `dream-bearer-token` + `dream-catch-with`; calls lib/acl public API.
Mute/prefs layer deferred (no blocker, add when a domain needs it).
- [ ] `sxtp.sx` — host↔subsystem wire format (align with existing spec at
`applications/sxtp/spec.sx`)
- [x] migrate a write endpoint (auth + permission + action): `POST /feed`
(`host/feed-write-routes resolve`) — auth ∘ ACL("post","feed") ∘ wrap-errors
over `host/feed-create`, which parses the JSON body and `feed/post`s it (201);
non-object body -> 400. Created activity is readable back via `GET /feed`.
## Phase 3 — Strangler migration ledger
- [ ] enumerate Quart endpoints; track migrated vs proxied
@@ -116,6 +126,16 @@ lib/host/sxtp.sx subsystem APIs (feed/search/commerce/…
`hosts/` change (out of scope) — tracked under Blockers as the eventual
live-wiring step. For now the host layer is exercised purely via conformance.
- **Phase 2 (middleware + write endpoint DONE, 43/43).** `lib/host/middleware.sx`
+ a guarded `POST /feed`. Middleware is plain function composition over Dream's
primitives; auth/permission *policy* is injected (token resolver, resource
extractor) so the layer is policy-free and testable. ACL authorisation runs
against lib/acl's public `acl/permit?` (string atoms work — no symbol coercion
needed). The write path proves the auth ∘ permission ∘ action stack end-to-end:
401 unauth, 403 unpermitted, 201 + readback on success, 400 on bad body.
- **Remaining for Phase 2: `sxtp.sx`** — the host↔subsystem wire format. Align
with the existing spec at `applications/sxtp/spec.sx`. This is the next tick.
## Blockers
- **Live wiring to the native OCaml HTTP server** (Phase 3/4): the prod server in