writeonce/docs/examples/writeonce-framework
shoney.arickathil fb86156d04 feat(framework): auth in core — Bearer/Basic mechanism + principal slot
- http/auth.wo: auth_header (scheme split, case-insensitive, RFC 9110),
  bearer_token, basic_credentials (first-colon split, RFC 7617),
  pure-.wo base64_decode (RFC 4648, strict padding), ct_eq constant-time
  compare (no early exit, both Basic fields always compared)
- req.principal: the blessed "who is this" slot, "" until authenticated;
  Middleware.before now takes mut req so auth can write it
- BearerAuth { token, principal } and BasicAuth { user, pass, realm }
  middlewares; BasicAuth answers the WWW-Authenticate challenge; policy
  (routes/users/secrets) stays app-side on the exposed fns
- web-app dogfoods BearerAuth; its hand-rolled Auth class deleted
- probe matrix 26/26 (RFC 4648 vectors, rfc7617 pair, pass-with-colon,
  bad padding/chars/length, deny paths, challenge header) release+ASan
- gate grows 16 -> 17: wrong bearer token answers 401 over the wire
- README: auth bullet + the core CHECKLIST (done / candidate / parked
  behind 8-11 by design); story 16 + board record the landing
- all gates green: web-app 17/0, oop-e2e 89/0, deps-accept 8/0,
  log-watcher 7/0, employee 8/0, woc-test green

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 03:15:16 +02:00
..
http feat(framework): auth in core — Bearer/Basic mechanism + principal slot 2026-08-20 03:15:16 +02:00
router feat(framework): auth in core — Bearer/Basic mechanism + principal slot 2026-08-20 03:15:16 +02:00
app.wo feat(framework): v1 polish — helpers, 405+Allow, HEAD, Logging, set_header 2026-08-20 03:04:57 +02:00
README.md feat(framework): auth in core — Bearer/Basic mechanism + principal slot 2026-08-20 03:15:16 +02:00
wo.toml feat(examples): framework skeleton + web-app scaffold (iter 16 Task 1) 2026-08-19 19:43:19 +02:00

writeonce-framework

A web framework written in writeonce, consumed as a [deps] dependency (iteration 15). Spec: docs/superpowers/specs/2026-08-18-web-framework-design.md §B.

[deps]
writeonce-framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }

What it is

  • HTTP/1.1 server core (http/): request parsing (Content-Length bodies, %-decoded paths and query strings), response serialization, a blocking serve loop that answers 400 to malformed requests, 500 to trapping handlers (and survives), closes every fd, and honors SIGTERM. Connection policy: pipelined requests are served on one connection; idle connections close after the response — on a single-threaded server a parked keep-alive connection would block accept and starve every other client, so closing is the correct shape until shards/fibers (8/11). A proxy in front simply reconnects.
  • Router (router/): method + path table with :param captures into req.params; first match wins; a known path with the wrong method is 405 with the Allow header (registration order); no matching path is the framework's 404. HEAD is served free: routed as GET, body suppressed, Content-Length still names the body a GET would carry.
  • Registration helpers: app.get/post/put/delete_(pattern, handler) push the route for you (delete_ because delete is the query keyword); app.add(Route { ... }) stays for anything else. A request-line Logging middleware ships in router/, and set_header(resp, name, value) is the escape hatch for headers the builders don't set.
  • Handlers without closures: the language has no function values by doctrine, so a route handler is a class satisfying the Handler interface (fn handle(req: Req) -> Resp), dispatched structurally — a non-conforming handler is a compile error (WO-E205). Middleware is its own interface (fn before(req: Req) -> ?Resp; nil = continue, a Resp short-circuits).
  • Auth mechanism in core (http/auth.wo): Authorization header parsing (scheme split, case-insensitive), pure-.wo base64, a constant-time comparator (ct_eq, no early exit), and the blessed principal slot — req.principal is "" until an auth middleware authenticates, then downstream handlers read who it is. BearerAuth and BasicAuth (with the WWW-Authenticate challenge) ship as middlewares; POLICY — which routes, which users, where secrets live — stays in the app, on top of bearer_token/basic_credentials/ct_eq.
  • Data layer for free: handlers use @table + the query surface directly — durable, compiler-checked persistence in the same binary. No ORM, no database server.

Honest limits (v1, all deliberate)

  • Single-threaded, blocking — one request at a time. Concurrency arrives underneath this same surface with the shard/fiber iterations (8/11).
  • TLS: none, anywhere. Deploy behind nginx/caddy; the proxy terminates TLS+ALPN and gives browsers HTTP/2 while this backend speaks HTTP/1.1 keep-alive. See the web-app sample's README for the nginx sketch.
  • Content-Length bodies only (no chunked encoding), no WebSockets/SSE, JSON-first (no templates).

The core checklist (what a framework core owes, and where this one is)

Core concern State
HTTP parsing + connection lifecycle ✅ http/parse.wo, http/serve.wo (keep-alive, 400-and-survive, fd-clean, SIGTERM)
Routing: path params, method dispatch, precedence ✅ :param captures, first-match-wins, wrong-method = 405 + Allow
Middleware chain, ordering guarantee ✅ registration order, ?Resp short-circuits
Request/response types ✅ Req/Resp + builders + set_header
Bearer/Basic auth mechanism + principal ✅ http/auth.wo, req.principal
Body parsing hooks: JSON ✅ the language's checked json.decode
Body parsing hooks: form-encoded, multipart ⬜ candidate next slices (form first — parse_query already decodes the encoding)
Error handling → status mapping 🔶 trap = 500, builders per status; a per-error mapping hook is a candidate slice
Body streaming, backpressure ⏸ needs fibers/shards (iterations 8/11) — whole bodies until then, by design
Cancellation propagation ⏸ process-level only (env.stopping()); per-request cancel needs fibers (11)
Configuration + graceful shutdown 🔶 SIGTERM drains and closes clean; config is ctor fields — a config record is a candidate slice

The consuming sample

docs/examples/web-app — a small storefront importing this framework through [deps]. Its acceptance (just web-app) exercises the whole chain: fetch → lock → build → serve → durable restart.