diff --git a/docs/architecture.md b/docs/architecture.md index 936ab76..96b56a5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -7,8 +7,9 @@ authority service for GameCult-hosted experiments. Its basic promise is: -- let people sign in with Discord and/or Patreon -- decide access from GameCult/community entitlements and local grants +- let people sign in through app-relevant providers such as Discord, Patreon, + GitHub, Twitch, or YouTube +- decide access from provider entitlements, local grants, and per-app policy - issue signed local claims that hosted apps can verify without trusting raw provider tokens @@ -23,7 +24,7 @@ If every experiment rebuilds: - entitlement refresh - session cookies - grant handling -- "Discord role OR Patreon tier" logic +- provider-specific gate logic then we get five near-identical auth pits and deserve what happens next. @@ -56,14 +57,14 @@ bind its own policy and runtime seams onto it. - `LinkedIdentity` one external provider identity attached to exactly one local account - `EntitlementSnapshot` - cached result from a provider or grant source saying whether some claim is + cached result from an entitlement source saying whether some claim is currently true - `CapabilityGrant` a reusable local grant such as `global_member`, `app_access`, or `admin_access`, scoped either globally or to one app - `AppAccessProfile` - the per-app binding that names capabilities, allowed provider sources, and - policy rules + the per-app binding that names capabilities, relevant identity providers, + entitlement sources, and policy rules - `Session` signed local app/browser session; the browser gets this, not provider tokens - `AccessClaim` @@ -71,14 +72,39 @@ bind its own policy and runtime seams onto it. - `AuditEvent` durable record for login, link, unlink, refresh, denial, and admin overrides +## Provider taxonomy + +Heimdall should classify outside systems by what role they play, not by whether +their brand name appears somewhere in an app. + +- `Identity provider` + proves who the user is and can supply one `LinkedIdentity` +- `Entitlement source` + answers whether some access-relevant claim is currently true +- `Local grant source` + records app or operator-issued overrides inside Heimdall +- `App-owned connector OAuth` + authenticates an app's operational integration with a third-party platform + and does **not** become Heimdall territory just because it uses OAuth + +One provider can play different roles in different apps: + +- Repixelizer can treat Discord and Patreon as both identity and entitlement + sources +- Bifrost can treat GitHub as identity proof while keeping membership approval, + roles, and governance state app-local +- StreamPixels can reuse shared viewer identity patterns while keeping + creator-side Twitch/YouTube connector credentials and runtime plumbing + StreamPixels-owned + ## System shape ```mermaid flowchart TD A["Browser / app web UI"] --> B["Hosted experiment web surface"] B -->|start auth / link| C["Heimdall"] - C --> D["Discord OAuth + guild role checks"] - C --> E["Patreon OAuth + campaign/tier checks"] + C --> D["Shared identity providers"] + C --> E["Shared entitlement sources"] C --> F["Local auth/control-plane store"] C --> G["Signed sessions / claims"] B -->|verify claims locally| H["Hosted experiment runtime"] @@ -90,6 +116,8 @@ The important split: sessions, claims, grants, and audit surfaces - the host experiment owns its workload, queue, editor, render path, viewer profile model, creator model, or other product-specific machinery +- app-owned operational connector OAuth stays with the app unless there is a + very deliberate reason to centralize it Do not smear provider logic directly through every host runtime. @@ -190,25 +218,42 @@ Important invariants: - one provider identity belongs to one local account only - provider tokens stay server-side only - session cookies represent local sessions, not provider trust directly -- app-specific capability checks read signed local claims, not Discord or - Patreon on every route +- app-specific capability checks read signed local claims, not upstream + providers on every route ## Provider adapters -Shared provider/gate adapters should be boring and reusable: +Shared adapters should be boring and reusable: -- `DiscordRoleEntitlementProvider` +- `DiscordIdentityProvider` - login via Discord OAuth +- `PatreonIdentityProvider` + - login via Patreon OAuth +- `GitHubIdentityProvider` + - login via GitHub OAuth for apps that use GitHub identity as the local + person-key +- `TwitchIdentityProvider` + - optional shared login identity when an app wants Twitch-backed sign-in +- `YouTubeIdentityProvider` + - optional shared login identity when an app wants YouTube-backed sign-in +- `DiscordRoleEntitlementProvider` - entitlement check via guild membership and role ids, ideally through the GameCult bot token server-side - `PatreonMembershipEntitlementProvider` - - login via Patreon OAuth - entitlement check via campaign membership and tier ids - `ManualGrantProvider` - local override for staff, migrations, experiments, or emergency access - `InviteGrantProvider` - reusable invite-issued app or role grants where that pattern makes sense +Important rule: + +- a provider being used anywhere in an app does not automatically make all uses + of that provider Heimdall-owned +- creator/operational connector OAuth such as StreamPixels broadcaster + connectors stays app-owned unless it is deliberately promoted into the shared + auth authority + ## Policy model The shared authority should not hard-code one app's access rules. It should @@ -235,6 +280,10 @@ queue_submit = app_access admin_access = grant.operator || grant.admin_access ``` +The important part is not the provider names in the example. It is that app +profiles choose which identity facts, entitlement snapshots, and local grants +feed each capability. + ## Request path discipline This is where shared authority stays useful instead of becoming a nuisance. @@ -350,18 +399,27 @@ Suggested generic env surface: - `GC_ACCESS_BASE_URL=https://heimdall.gamecult.org` - `GC_ACCESS_INTERNAL_URL=http://127.0.0.1:4100` - `GC_ACCESS_SESSION_SECRET=...` -- `GC_ACCESS_DISCORD_CLIENT_ID=...` -- `GC_ACCESS_DISCORD_CLIENT_SECRET=...` -- `GC_ACCESS_DISCORD_BOT_TOKEN=...` -- `GC_ACCESS_DISCORD_GUILD_ID=...` -- `GC_ACCESS_PATREON_CLIENT_ID=...` -- `GC_ACCESS_PATREON_CLIENT_SECRET=...` -- `GC_ACCESS_PATREON_CAMPAIGN_ID=...` +- `GC_ACCESS_PROVIDER_DISCORD_CLIENT_ID=...` +- `GC_ACCESS_PROVIDER_DISCORD_CLIENT_SECRET=...` +- `GC_ACCESS_PROVIDER_DISCORD_BOT_TOKEN=...` +- `GC_ACCESS_PROVIDER_DISCORD_GUILD_ID=...` +- `GC_ACCESS_PROVIDER_PATREON_CLIENT_ID=...` +- `GC_ACCESS_PROVIDER_PATREON_CLIENT_SECRET=...` +- `GC_ACCESS_PROVIDER_PATREON_CAMPAIGN_ID=...` +- `GC_ACCESS_PROVIDER_GITHUB_CLIENT_ID=...` +- `GC_ACCESS_PROVIDER_GITHUB_CLIENT_SECRET=...` +- `GC_ACCESS_PROVIDER_TWITCH_CLIENT_ID=...` +- `GC_ACCESS_PROVIDER_TWITCH_CLIENT_SECRET=...` +- `GC_ACCESS_PROVIDER_YOUTUBE_CLIENT_ID=...` +- `GC_ACCESS_PROVIDER_YOUTUBE_CLIENT_SECRET=...` - `GC_ACCESS_ENTITLEMENT_CACHE_TTL_SECONDS=900` - `GC_ACCESS_PROVIDER_FAILURE_GRACE_SECONDS=3600` +Only configure the providers Heimdall actually owns for a given deployment. + Per-app binding should live in code or profile config, not in cloned provider -env namespaces. +env namespaces. App-owned connector OAuth should stay under the app's own env +surface even when the upstream provider name overlaps. ## Audit Result diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 51d14c4..886cfe7 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -2,3 +2,4 @@ {"ts":"2026-04-26T00:00:00Z","type":"design","status":"superseded","note":"The initial first implementation hypothesis was an embedded shared gamecult_access package. This was later superseded by the Heimdall service-first shape after the cross-runtime split and Yggdrasil topology review."} {"ts":"2026-04-26T00:00:00Z","type":"boundary","status":"accepted","note":"StreamPixels can reuse the same auth/control-plane backbone, but streamer-audience data, creator memberships, connector credentials, and runtime state should remain StreamPixels-owned."} {"ts":"2026-04-26T06:00:00Z","type":"design","status":"accepted","note":"The cross-runtime split and current Yggdrasil topology changed the first implementation shape from an embedded shared package to Heimdall: a shared auth authority service whose signed claims are verified locally by each app."} +{"ts": "2026-04-26T11:00:02+00:00", "type": "design", "status": "accepted", "note": "Heimdall's provider model splits shared identity providers and entitlement sources from app-owned connector OAuth. Different provider mixes change adapter inventory and claim vocabulary, not the core service boundary."}