SessionVerifier now holds a REGISTRY of trusted OIDC issuers instead of a single
Supabase project. A token is routed by its `iss` claim to that issuer's entry,
verified against that issuer's JWKS (ES256, no secret), and stamped with that
entry's appId/orgId — so two Supabase projects / IdPs map to two isolated app
scopes on one IIOS (chat vs a future support app). App A's tokens can't reach B.
- Config: `AUTH_ISSUERS` (JSON array of { url, appId, orgId? }); the single
`SUPABASE_URL` (+ SUPABASE_APP_ID) still works as a one-entry shorthand.
- Per-issuer JWKS cache; untrusted issuer → reject; forgery (right issuer claim,
wrong key) → reject.
- HS256 app-token path (dev/tests) unchanged.
Tests: new session.verifier.spec (4) — routes to correct appId, second issuer →
different appId, untrusted issuer rejected, cross-issuer forgery rejected.
Note: outbox `replay.spec` has a pre-existing clock-skew flake (nextAttemptAt vs
Postgres now()) that surfaces under certain suite orderings — unrelated to this
change (fails in isolation on a clean tree; passes when another spec runs first).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SessionVerifier gains a Supabase mode (set SUPABASE_URL): user SATs are ES256,
verified against the project's public JWKS — no shared secret. Keys are fetched at
startup (onModuleInit) and cached as PEM so verify() stays synchronous; a rotated
kid triggers a background refresh. Claims map to MessagePrincipal with userId=email
(stable, human-readable → mentions/directory keep working; RealMDM canonicalises
later). The legacy HS256 app-token path is unchanged (dev/tests untouched).
senderName now prefers the actor display name over the raw handle, so real names
show on bubbles when identity is an email.
Verified end-to-end against a live project: signup → real ES256 SAT → GET /v1/threads
200 → thread created + owned by the resolved email principal. Full suite 182 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>