;; lib/host/middleware.sx — Host middleware: composable handler->handler layers ;; for the cross-cutting concerns every write endpoint shares — error trapping ;; (JSON 500), authentication (bearer token -> principal), and authorisation ;; (ACL permit?). Middleware is plain function composition; host/pipeline threads a ;; list onto a handler, FIRST middleware outermost (so it runs first). Auth and ;; permission policy are INJECTED — the token resolver and the resource extractor — ;; so this layer carries no hardcoded policy. Reuses Dream's bearer/error helpers ;; and lib/acl's public acl/permit?. ;; Depends on lib/dream/{auth,error,router}.sx + lib/acl/api.sx + lib/host/handler.sx. ;; Compose a list of middlewares onto a handler (first = outermost). (define host/pipeline (fn (middlewares handler) (dr/apply-middlewares middlewares handler))) ;; The authenticated principal attached by host/require-auth. (define host/principal (fn (req) (dream-principal req))) ;; ── error trapping ───────────────────────────────────────────────── ;; Any error thrown downstream becomes a JSON 500 envelope. (define host/-on-error (fn (req e) (host/error 500 "internal error"))) (define host/wrap-errors (dream-catch-with host/-on-error)) ;; ── authentication ───────────────────────────────────────────────── ;; resolve : token -> principal | nil. Missing/invalid token -> JSON 401 with a ;; WWW-Authenticate: Bearer challenge; success attaches :dream-principal so ;; downstream layers (and host/principal) can read it. (define host/require-auth (fn (resolve) (fn (next) (fn (req) (let ((tok (dream-bearer-token req))) (let ((principal (if tok (resolve tok) nil))) (if (nil? principal) (dream-add-header (host/error 401 "unauthorized") "www-authenticate" "Bearer") (next (assoc req :dream-principal principal))))))))) ;; ── authorisation ────────────────────────────────────────────────── ;; Gate on ACL: the authed principal must be permitted `action` on the resource ;; computed by res-fn from the request. Denied -> JSON 403. Assumes the ACL fact ;; db was loaded (acl/load!) at startup. Place AFTER host/require-auth. (define host/require-permission (fn (action res-fn) (fn (next) (fn (req) (let ((subject (host/principal req)) (resource (res-fn req))) (if (acl/permit? subject action resource) (next req) (host/error 403 "forbidden")))))))