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_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_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 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
- 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
- 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
- shared claim semantics

View file

@ -118,6 +118,7 @@ Supported providers in the skeleton:
- `twitch`
- `youtube`
- `spotify`
- `joystick`
Request body:
@ -430,9 +431,11 @@ Important behavior:
- 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
- if a stored provider access token is expired or near expiry, Heimdall uses
the stored provider refresh token and persists any rotation before
evaluating entitlements
- if a stored provider access token is expired or near expiry, Heimdall takes
a row lock on the linked identity, re-reads it, uses the stored provider
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
send `entitlementPolicies`; Heimdall matches each stored identity to the
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
- when a stored managed credential is expired or within the refresh window,
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-owned