Booking stream gains :hold/:confirm/:release; fold tracks per-actor seat state
(:held/:confirmed). A held seat counts toward capacity so a pending payment
can't be oversold. ev/hold! (capacity-safe), ev/confirm!, ev/release!,
ev/seat-state. Holds race test mirrors the booking race. 144/144 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
delegation.sx makes the loop's central rule concrete: check() introspects
the token first — inactive → {error, unauthenticated} (401), acl never
consulted — and only an authenticated subject's request is delegated to
acl, which returns permit/deny ({error, forbidden} = 403). 401 strictly
precedes 403. acl-on-sx (Datalog) is a different SX guest wired at the
integration layer, so the decider here is a labelled stub (permits when
Action in Scope); swap the pid and the boundary is unchanged. New
tests/delegation.sx. 185/185 — extensions backlog clear.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The identity coordinator now owns an audit ledger and a membership registry
alongside its token table (started with the ledger) and session registry.
login/logout are audited; new ops history/enroll/member_status/member_project
surface the audit and membership axes through the one `identity` door.
Identity proves who and reports membership; acl still decides permission.
Existing api behaviour unchanged. New tests/facade.sx. 177/177.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
device.sx — for input-constrained devices. authorize → {device_code,
user_code}; the human approves/denies out-of-band by user_code; the device
polls by device_code through the §3.5 status machine (authorization_pending
→ access_denied / {ok, Token}). Device code is single-use once a token
issues; approve-after-deny is rejected. Tokens grant-backed via token.sx.
Device-code expiry + slow_down deferred (no wall clock). New
tests/device.sx. 168/168.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
oauth.sx now owns a client registry (loop/6) with register_client and the
client_credentials grant. A confidential client authenticates and gets a
token acting on its own behalf (subject = the client), no refresh token
(§4.4.3). A public client is unauthorized_client; any auth failure (unknown
client or wrong secret) is invalid_client — no client-existence oracle
(§5.2). identity-load-oauth! now pulls its deps. New tests/grants.sx.
158/158.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Booking stream carries :booking/:cancel events; live roster is the folded
replay so cancelling frees a seat and capacity reopens. ev/cancel! (retrying
append-expect), no-op on unbooked, cancelled actor may re-book. Capacity count
is folded roster size. 110/110 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
clients.sx (RFC 6749 §2) — confidential clients must present the correct
secret at the token endpoint (wrong → invalid_client); public clients are
identified but not authenticated; redirect_uris are pre-registered and
checked by exact-match valid_redirect (§3.1.2.2 + Security BCP). Standalone
module for now; wiring confidential-client auth into oauth exchange is a
follow-up. New tests/clients.sx. 149/149.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The token registry holds a logical clock (advance/now; the substrate has no
wall clock). Grants carry a Ttl; each access token carries an Expires
(Now-at-issue + Ttl, or infinity); introspect returns inactive once Now
reaches it. Refresh mints a fresh short-lived access token — short access
tokens, long refresh tokens. issue/4 and issue_grant/4 default to infinity so
all prior behaviour is unchanged. New tests/expiry.sx. token loop/6. 138/138.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
booking.sx: per-occurrence append-only stream, roster = replay. Booking
decided against an observed (roster, last-seq) snapshot, committed via
persist/append-expect — atomic check+append, no overbooking, no lock.
Explicit last-seat race test: two bookers, one booked, one conflict, roster
capped. Idempotent per actor. 97/97 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each access token now carries its own effective scope (<= the grant's max).
refresh/3 requests a narrower scope; the request must be a subset of the
grant scope, else {error, invalid_scope} and the refresh token is NOT
consumed (client may retry, §5.2). refresh/2 keeps full scope; scope stays
opaque (atom or list) for issue so all prior atom-scope tests are unchanged.
Also files a Blocker: PKCE S256 is blocked on erlang substrate bugs (binary
=:= always true; crypto:hash ignores binary content). token 24/24, 130/130.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All four phases done. Records an extensions queue (PKCE S256, token TTL,
scope sets/narrowing, client registry, client-credentials/device grants,
acl delegation, state/nonce, unified facade) to keep deepening the engine.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
federation.sx — peer-asserted subjects, advisory and trust-gated. An
assertion is accepted only from an explicitly trusted peer (else
{error, untrusted}) and is flagged {peer_asserted, Peer}, never promoted to
local authority; acl decides what a peer-asserted identity may do. Cross-
instance subject mapping namespaces remote subjects by peer
({federated, Peer, Remote}) so two peers' "alice" never collide, with
optional explicit aliasing. Adds an audit-completeness test. New
tests/federation.sx. All four phases done — 124/124.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
audit.sx is an append-only ledger process. token.sx gains start/1(Audit)
and emits an event on every grant transition (issue, refresh, revoke —
including reuse-triggered revoke); start/0 stays unaudited so existing use
is unchanged (token.sx has no compile-time dep on the audit module, it just
sends to a pid). The ledger answers (identity/audit subject) via
audit/actions/count/all, chronological. In-memory event stream; persist
backing is a later Erlang<->persist bridge, out of scope. 111/111.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cache.sx — a process wrapping the token registry, memoising introspect.
Revocation stays real via generation invalidation: any revoke/refresh bumps
a generation counter, so every cached positive instantly becomes a miss and
re-validates against the live registry. A revoked token never reads valid
out of cache, not for a millisecond. stats() exposes hits/misses. New
tests/cache.sx. 101/101.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
membership.sx — coop membership as a guarded state machine
(none→pending→active→lapsed⇄active, any→revoked terminal); invalid
transitions return explicit {error, CurrentStatus}, never silent no-ops.
project(Subject, App) renders the one canonical state into a per-app claim
({member,Tier,App} / {pending,App} / {lapsed,App} / {denied,App} /
{non_member,App}) — identity reports what the membership is; acl decides
whether the app should honour it. New tests/membership.sx. 92/92.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ev-next-free finds the earliest free slot >= after for a duration within a
horizon, probing 'after' + busy-interval ends via the busy_in rule (ev-free?).
Finds gaps, skips too-short gaps, half-open at edges. 59/59 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
suggest/suggestN rank indexed terms by edit distance to a (misspelled) query
term, alphabetical tiebreak. 234/234.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
oauth.sx now owns a session registry. establish creates a subject session;
silent_authorize (OIDC prompt=none §3.1.2.1) asks "does this subject have a
live session?" — if yes it mints a code skipping consent, bound to client +
redirect_uri + PKCE exactly like a consented code; if no it returns
login_required (a negative state, not a login redirect). One session serves
many clients; end_session closes the fast-path. New tests/sso.sx. 75/75.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
exchange now issues an access+refresh pair (RFC 6749 §4.1.4/§5.1) via
token.sx issue_grant; added the refresh grant (§6) delegating to token
rotation. End-to-end: code-exchange → refresh → introspect (active),
refresh-token reuse rejected (invalid_grant), and revoke-then-refresh
blocked by grant cascade. oauth 17/17, 65/65.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The grant {Subject,Client,Scope,Status} becomes the unit of authorization
and cascade; access + refresh tokens reference it. issue_grant returns an
access+refresh pair; refresh (RFC 6749 §6) supersedes the presented refresh
token and mints a fresh pair; reusing a superseded refresh token is treated
as theft (RFC 6819 §5.2.2.3) and revokes the whole family, killing the live
descendant. revoke of any token cascades to the grant. All prior token
behaviour preserved. token 18/18, 62/62.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>