From 3eaa9ef2654d77b7e3e2d007c34cc46614a5db68 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 19 Oct 2026 11:15:42 +0200 Subject: [PATCH] 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 --- deployment/idunn/recipe.toml | 2 ++ docs/app-profiles/streampixels.md | 8 ++++++-- docs/service-contract.md | 26 ++++++++++++++++++++++---- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/deployment/idunn/recipe.toml b/deployment/idunn/recipe.toml index 8524451..f520258 100644 --- a/deployment/idunn/recipe.toml +++ b/deployment/idunn/recipe.toml @@ -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", diff --git a/docs/app-profiles/streampixels.md b/docs/app-profiles/streampixels.md index f28b40a..703dde7 100644 --- a/docs/app-profiles/streampixels.md +++ b/docs/app-profiles/streampixels.md @@ -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 diff --git a/docs/service-contract.md b/docs/service-contract.md index c639178..47fa7fa 100644 --- a/docs/service-contract.md +++ b/docs/service-contract.md @@ -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