Declare Joystick in the Idunn recipe and document the locked refresh

Recipe optional_environment gains the Joystick client id and secret file;
service-contract documents the row lock, rotation fail-closed and the 410/502
answers; the StreamPixels profile names Heimdall as sole holder of provider
refresh tokens (F5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-19 11:15:42 +02:00
commit 3eaa9ef265
3 changed files with 30 additions and 6 deletions

View file

@ -157,6 +157,8 @@ optional_environment = [
"GC_ACCESS_PROVIDER_DISCORD_CLIENT_ID", "GC_ACCESS_PROVIDER_DISCORD_CLIENT_ID",
"GC_ACCESS_PROVIDER_DISCORD_CLIENT_SECRET_FILE", "GC_ACCESS_PROVIDER_DISCORD_CLIENT_SECRET_FILE",
"GC_ACCESS_PROVIDER_GITHUB_CLIENT_SECRET_FILE", "GC_ACCESS_PROVIDER_GITHUB_CLIENT_SECRET_FILE",
"GC_ACCESS_PROVIDER_JOYSTICK_CLIENT_ID",
"GC_ACCESS_PROVIDER_JOYSTICK_CLIENT_SECRET_FILE",
"GC_ACCESS_PROVIDER_PATREON_CLIENT_ID", "GC_ACCESS_PROVIDER_PATREON_CLIENT_ID",
"GC_ACCESS_PROVIDER_PATREON_CLIENT_SECRET_FILE", "GC_ACCESS_PROVIDER_PATREON_CLIENT_SECRET_FILE",
"GC_ACCESS_PROVIDER_SPOTIFY_CLIENT_SECRET_FILE", "GC_ACCESS_PROVIDER_SPOTIFY_CLIENT_SECRET_FILE",

View file

@ -20,7 +20,11 @@ Corroborated by `gamecult-ops/nginx/streampixels.gamecult.org.conf:14`
The current StreamPixels architecture already makes useful splits: The current StreamPixels architecture already makes useful splits:
- the web app owns the remembered viewer session - the web app owns the remembered viewer session
- the service owns provider OAuth exchange and identity resolution - Heimdall owns provider OAuth exchange, identity resolution and refresh-token
custody for Twitch, YouTube and Joystick; StreamPixels never calls a provider
token endpoint and holds no provider refresh token. It asks Heimdall for a
short-lived access token (`managed-credentials/resolve`) and treats `410
managed_credential_revoked` as "the creator must reconnect"
- the overlay remains auth-free - the overlay remains auth-free
- creator memberships and operator roles are separate from general viewer state - creator memberships and operator roles are separate from general viewer state
@ -38,7 +42,7 @@ In the service-first Heimdall version, the most likely reusable slice is:
- provider OAuth authority - provider OAuth authority
- identity-linking authority - identity-linking authority
- managed Twitch/YouTube credential custody for creator connections - managed Twitch/YouTube/Joystick credential custody for creator connections (Joystick is a managed connection only, not a viewer sign-in provider)
- entitlement refresh - entitlement refresh
- shared claim semantics - shared claim semantics

View file

@ -118,6 +118,7 @@ Supported providers in the skeleton:
- `twitch` - `twitch`
- `youtube` - `youtube`
- `spotify` - `spotify`
- `joystick`
Request body: Request body:
@ -430,9 +431,11 @@ Important behavior:
- the refresh token is signed by Heimdall and scoped to one app session - the refresh token is signed by Heimdall and scoped to one app session
- the access token remains short-lived; app backends still verify it locally - the access token remains short-lived; app backends still verify it locally
- if a stored provider access token is expired or near expiry, Heimdall uses - if a stored provider access token is expired or near expiry, Heimdall takes
the stored provider refresh token and persists any rotation before a row lock on the linked identity, re-reads it, uses the stored provider
evaluating entitlements refresh token and persists any rotation before evaluating entitlements; an
identity whose grant the provider has revoked (no stored access token)
contributes no entitlement
- any app authenticated via `x-heimdall-app-secret` (`resolveAppCaller`) may - any app authenticated via `x-heimdall-app-secret` (`resolveAppCaller`) may
send `entitlementPolicies`; Heimdall matches each stored identity to the send `entitlementPolicies`; Heimdall matches each stored identity to the
policy for that provider. Sending policy without authenticating is policy for that provider. Sending policy without authenticating is
@ -484,7 +487,22 @@ Important behavior:
- it returns access tokens only; refresh tokens remain in Heimdall custody - it returns access tokens only; refresh tokens remain in Heimdall custody
- when a stored managed credential is expired or within the refresh window, - when a stored managed credential is expired or within the refresh window,
Heimdall refreshes it before returning the access token and persists rotated Heimdall refreshes it before returning the access token and persists rotated
access/refresh tokens access/refresh tokens. The refresh runs under a row lock on the linked
identity (`SELECT ... FOR UPDATE`), so a given refresh token is presented to
the provider once across concurrent requests and across two Heimdall
processes sharing one database. The provider call is bounded at 10 seconds
- a provider whose refresh tokens rotate (Joystick) fails closed: a refresh
response without a new refresh token revokes the credential instead of
keeping the old token
- `410 {"error":"managed_credential_revoked","provider":...}`: the provider
rejected the grant (`invalid_grant`), or the identity holds no access token.
The stored tokens are cleared and an audit event `managed_credential_revoked`
is written; the owner must reconnect. `404 managed_credential_not_found`
means no linked identity exists
- `502 {"error":"managed_credential_refresh_failed","provider":...}`: the
refresh failed for any reason that is not a grant verdict (invalid client,
provider 5xx, timeout, malformed body). Stored tokens are untouched and a
later request may succeed
- app-local binding, diagnostics, subscription setup, and runtime behavior stay - app-local binding, diagnostics, subscription setup, and runtime behavior stay
app-owned app-owned