OAuth start: connect keeps admitting identity providers, as Bifrost uses it

The first gate refused connect for a provider outside managedConnectionProviders,
which broke Bifrost's existing Discord connect. sign_in and link still take
identityProviders only, so a managed-only provider (Joystick) can never sign
anyone in or link; connect takes either list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-19 12:38:13 +02:00
commit c1fbc1261a
3 changed files with 12 additions and 7 deletions

View file

@ -42,7 +42,7 @@ In the service-first Heimdall version, the most likely reusable slice is:
- provider OAuth authority
- identity-linking authority
- managed Twitch/YouTube/Joystick credential custody for creator connections (Joystick is a managed connection only, not a viewer sign-in provider: `startOAuthFlow` admits `sign_in` and `link` only for `identityProviders` and `connect` only for `managedConnectionProviders`, and refuses anything else before any redirect)
- managed Twitch/YouTube/Joystick credential custody for creator connections (Joystick is a managed connection only, not a viewer sign-in provider: `startOAuthFlow` admits `sign_in` and `link` only for `identityProviders` and `connect` only for `managedConnectionProviders` or `identityProviders`, and refuses anything else before any redirect)
- entitlement refresh
- shared claim semantics

View file

@ -225,13 +225,15 @@ export function serializeAppProfile(profile: AppProfile): Record<string, unknown
/**
* Whether the profile admits this provider for this mode. The role lists
* decide: `sign_in` and `link` mint viewer identity, so they take only
* `identityProviders`; `connect` takes custody of a creator credential, so it
* takes only `managedConnectionProviders`. A provider in neither list for the
* mode is refused before any redirect.
* `identityProviders`; a provider that is only a managed connection (Joystick)
* can never sign anyone in or link. `connect` takes either list:
* `managedConnectionProviders` for creator credentials, and
* `identityProviders` because Bifrost and its siblings already connect their
* identity providers. A provider in neither list is refused before any redirect.
*/
export function admitsProviderForMode(profile: AppProfile, provider: Provider, mode: OAuthMode): boolean {
return mode === "connect"
? profile.managedConnectionProviders.includes(provider)
? profile.managedConnectionProviders.includes(provider) || profile.identityProviders.includes(provider)
: profile.identityProviders.includes(provider);
}

View file

@ -2023,7 +2023,7 @@ describe("OAuth start admits a provider by the profile's role for the mode", ()
const allProviders = ["discord", "patreon", "github", "twitch", "youtube", "spotify", "joystick"] as const;
const modes = ["sign_in", "link", "connect"] as const;
it("sign_in and link take identityProviders only; connect takes managedConnectionProviders only, for every profile", async () => {
it("sign_in and link take identityProviders only; connect takes managedConnectionProviders or identityProviders, for every profile", async () => {
const app = await buildApp({ config: createTestConfig() });
try {
for (const profile of Object.values(builtInAppProfiles)) {
@ -2034,7 +2034,10 @@ describe("OAuth start admits a provider by the profile's role for the mode", ()
url: `/v1/oauth/${provider}/start`,
payload: { appSlug: profile.slug, mode, returnTo: `${profile.allowedReturnOrigins[0]}/x` },
});
const roleList = mode === "connect" ? profile.managedConnectionProviders : profile.identityProviders;
const roleList =
mode === "connect"
? [...profile.managedConnectionProviders, ...profile.identityProviders]
: profile.identityProviders;
const refused = start.json().error === "provider_not_supported_for_app";
expect([profile.slug, provider, mode, refused]).toEqual([profile.slug, provider, mode, !roleList.includes(provider)]);
}