Skip to content

block-form catch, a jwt claims builder, and route groups - #633

Merged
kacy merged 4 commits into
mainfrom
elegant-auth
Jul 31, 2026
Merged

block-form catch, a jwt claims builder, and route groups#633
kacy merged 4 commits into
mainfrom
elegant-auth

Conversation

@kacy

@kacy kacy commented Jul 31, 2026

Copy link
Copy Markdown
Owner

summary

the auth example worked but read badly, and each rough patch traced to a missing piece one level down. this adds the pieces at their right layers and rewrites the example on top of them — its output is unchanged, byte for byte.

the language: catch takes a block. expr catch fallback recovers with a value, but a server handler's recovery is control flow — answer a 401, skip the row, stop the loop — and that shape had no spelling, so handlers grew helper functions whose only job was to route an error somewhere returnable. expr catch: opens an indented block that runs on the error path; the checker requires it to leave with return, fail, continue or break (E253), since the block produces no value for the binding. the lowering reuses the inline catch's arms and ownership rules, and the regression case — heap payloads, a borrowed operand, loops — is valgrind-clean.

the library: jwt.claims(). building a claims object meant escaped quotes, doubled braces, interpolation and epoch arithmetic in one string literal. the builder replaces the literal: chainable setters, custom claims, expires_in stamping iat/exp at signing time, and a signing method per algorithm. reading back is session.subject() / .claim(name) instead of three nested json calls. around it: time.unix(), json.of/of_map, req.bearer_token() and response body_json_or.

the framework: group() and request values. use_mw wraps every route, so a guard meant an allowlist of public paths inside the middleware. group(prefix, mw) binds the guard to a prefix — the public/protected split is spelled once, where the routes are declared — and with_value/value hand what the guard learned to handlers on the request itself, replacing a threadlocal.

and one real bug the builder's tests caught: std.json's parse never decoded string escapes. any value with a quote, backslash or \u in it survived encode and came back wrong — a five-byte string returned as seven. both parse paths now decode the rfc 8259 escapes, surrogate pairs included, and reject malformed ones; the no-escape fast path is untouched. every http server reading a json body was exposed to this.

what was tested

make test, make bootstrap-verify (seed regenerated — parser, checker and emitter changed), make run-examples, make fuzz-check and make leak-check, all clean. new cases: the catch-block corpus case (valgrind-clean with the struct freelist off, E253 and E224 rejections verified), builder round trip through a real verifier, escape round trips including surrogate pairs, group scoping and request values through the served example.

notes

the block form does not bind the error value; catch err: is a natural follow-up once wanted. the rewritten examples/web_auth.pith is the before/after argument for the whole change — same behavior, no escaped-json literal, no threadlocal, no allowlist, no error-routing helpers.

kacy added 4 commits July 31, 2026 20:02
 recovers with a value, but the recovery a server
handler wants is usually control flow: answer a 401, skip this item, stop
the loop. that shape had no direct spelling, so every handler grew a helper
function whose only job was to route the error somewhere returnable.

catch: now opens an indented block that runs on the error path. the
block produces no value, so the checker requires it to leave — return,
fail, continue or break (E253) — rather than fall through to a binding
nothing wrote. the colon must open a block, so the inline form is untouched
anywhere it appears.

the lowering reuses the inline catch's arms: the ok path extracts the
payload with the same borrowed-vs-fresh ownership rules, and the error path
releases the failed result before the user's block runs. the regression
case covers heap payloads, a borrowed operand, continue and break inside
loops, and statement position; it is valgrind-clean with the struct
freelist off.
building a claims object meant writing json by hand inside a pith string —
escaped quotes, doubled braces, interpolation and epoch arithmetic in one
literal. jwt.claims() replaces that: chainable setters for the registered
claims, custom string and int claims, and expires_in that stamps iat and
exp at signing time, so callers never touch epoch math. a custom claim
spelling a registered name is refused rather than silently overriding the
builder. reading came down to three nested json calls per field; Verified
now answers subject(), claim(name) and claim_int(name) directly.

the builder's escape test caught a real std.json bug: parse never decoded
string escapes, so any value with a quote, backslash or \u in it survived
encode and came back wrong — len 7 for a 5-byte string. both parse paths
now decode the rfc 8259 escapes, including surrogate pairs, and reject
malformed ones; the fast no-escape path is unchanged.

smaller pieces in the same spirit: time.unix() (seconds, the unit protocols
mean — the jwt clock bug was exactly a /1000 nobody wrote), json.of and
json.of_map for small object bodies, req.bearer_token() to mirror the
client's .bearer(), and body_json_or on responses to mirror requests.
…itten

use_mw wraps every route, so a guard that belongs to some routes and not
others meant an allowlist of public paths inside the middleware. group()
binds a middleware to a path prefix instead: the public/protected split is
spelled once, at the site that declares the routes, and the group guard
runs inside the app-wide middleware. with_value and value give middleware a
way to hand what they learned — who the caller is — to handlers on the
request itself, rather than through module state.

the auth example is the point of all of it. rewritten on the claims
builder, catch blocks, bearer_token, group and request values, it has no
escaped-json literal, no threadlocal, no path allowlist and no helper
functions that existed only to route errors — and its output is unchanged,
byte for byte. docs updated to the same shapes.
inserting the Scoped struct landed between App and its doc comment, and the
claims() constructor shipped without one.
@kacy
kacy merged commit 8780d5e into main Jul 31, 2026
2 checks passed
@kacy
kacy deleted the elegant-auth branch July 31, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant