refreshStreamToken
refreshStreamToken(
options,claims,expiringToken,now):Promise<RefreshResult>
Defined in: packages/server/src/circuits/subscribe.ts:511
Re-mint a stream token from an existing one (ADR-0055 decision 6).
The expiring token IS the request: its signature proves this control plane issued those grants, so nothing has to be stored server-side to know what the subject held, and the edge stays stateless on both halves. Expiry is deliberately not enforced here — a token is presented for re-mint precisely because it is at or past its TTL — but the signature is, and every grant is re-authorized. That re-check is what makes the TTL a revocation bound rather than a formality.
BOTH TIERS are re-authorized, by the question each tier’s authorization actually turns on.
- Shared: the live entitlement set. Its predicate is generated from the scope values, so it cannot drift with the subject; what can change is whether the subject still holds the scope.
- Private: recompile the shape with the subject’s CURRENT claims and compare the fingerprint
the grant carries. A private predicate may read ANY verified claim, not only
sub, so a subject demoted mid-session — a role dropped, a tenant changed — is still the same authenticated subject and would sail through a check that only re-verified the JWT. Re-verifying the JWT proves the bearer is still who they were; it says nothing about whether the shape they hold is still the shape their claims compile to, and only recompiling answers that.
A fingerprint mismatch is a REVOCATION by design, including when the recompiled predicate would still permit the subject something. The grant names one stream, that stream serves the old predicate, and this route may not hand out a new one — the client’s re-subscribe is where a new shape is created, and this route is not that. So the grant is revoked, the client re-subscribes, the control plane creates the shape its current claims compile to, and because the handle differs the client re-snapshots through ADR-0056 decision 7’s must-refetch.
This is also where every surviving grant’s engine claim is RENEWED (fork ADR-0008). A claim is a
lease: the engine releases one not renewed within leaseSeconds, because native reads terminate on
durable-streams and the renewal is the only liveness signal it has. Renewing here rather than on a
timer is what makes the cadence honest — this route already runs once per subject per TTL window and
already decides which grants are still authorized, so the renewal covers exactly the grants that
survived and costs no extra round trip. A revoked grant is deliberately NOT renewed: its lease
lapses and the engine reclaims the shape.
The renewal is the same create, repeated with the grant’s own claim id. Its outcomes are in
renewGrantClaim; a renewal that comes back a different handle, or 409s, revokes the grant
with a re-subscribe reason rather than failing the whole re-mint.
registry is therefore REQUIRED rather than optional: a refresh route that cannot recompile cannot
re-authorize the private tier — and now cannot renew either, since the renewal IS the compiled
create. engine is required for the same reason.
Throws EntitlementsUnavailableError rather than revoking when the set cannot be consulted —
the same outage-is-not-a-denial rule subscribeToShapes follows, and it matters more here:
revoked is the wire’s clear-this-scope instruction, and this route runs every few minutes for
the life of every subscription. An engine that cannot answer a renewal joins it, as does a lease
window this deployment’s TTL does not fit inside (CircuitsLeaseConfigError).
Parameters
Section titled “Parameters”options
Section titled “options”Pick<SubscribeOptions, "registry" | "engine" | "entitlements" | "key" | "ttlSeconds" | "params">
claims
Section titled “claims”{[key: string]: unknown; app_metadata?: {[key: string]: unknown; roles?: string[]; }; sub?: string; } | null
expiringToken
Section titled “expiringToken”string
number
Returns
Section titled “Returns”Promise<RefreshResult>