From 1e912c7548ce70913ae96155950b90480684bf81 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Wed, 29 Apr 2026 18:42:59 -0700 Subject: [PATCH 1/2] fix(seed): serialize SAML seed; drop overlay cast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two cleanups in seed-dummy-data, both flagged on PR #1395: - The parallel `Promise.all` block ran `seedSamlConnections(projectId)` alongside another `overrideEnvironmentConfigOverride` write on the same project (`payments.testMode`). Both are read-modify-write, so concurrent reads of the env config can each see the stale state and the second write clobbers the first — the existing TODO at `config.ts:491` already documents the underlying race. Sequence the SAML seed after the parallel block to avoid the race until the override is wrapped in a serializable txn. - Type the SAML overlay with the target parameter type directly instead of using an `as Parameters<...>` cast, per project style. --- apps/backend/src/lib/seed-dummy-data.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/lib/seed-dummy-data.ts b/apps/backend/src/lib/seed-dummy-data.ts index 5b5784278..479cd1088 100644 --- a/apps/backend/src/lib/seed-dummy-data.ts +++ b/apps/backend/src/lib/seed-dummy-data.ts @@ -1983,7 +1983,7 @@ async function seedSamlConnections(projectId: string): Promise { // dot-keys — config normalization with onDotIntoNonObject="ignore" // drops dot-keys that try to navigate into a record entry that // doesn't yet exist (same convention as auth.oauth.providers). - const overlay: Record = {}; + const overlay: Parameters[0]["environmentConfigOverrideOverride"] = {}; for (const f of fetched) { overlay[`auth.saml.connections.${f.slug}`] = { displayName: f.displayName, @@ -1998,7 +1998,7 @@ async function seedSamlConnections(projectId: string): Promise { await overrideEnvironmentConfigOverride({ projectId, branchId: DEFAULT_BRANCH_ID, - environmentConfigOverrideOverride: overlay as Parameters[0]["environmentConfigOverrideOverride"], + environmentConfigOverrideOverride: overlay, }); } @@ -2131,7 +2131,6 @@ export async function seedDummyProject(options: SeedDummyProjectOptions): Promis "payments.testMode": true, }, }), - ...(getEnvVariable("STACK_SEED_ENABLE_SAML", "false") === "true" ? [seedSamlConnections(projectId)] : []), ...options.skipGithubConfigSource ? [] : [setBranchConfigOverrideSource({ projectId, branchId: DEFAULT_BRANCH_ID, @@ -2154,6 +2153,16 @@ export async function seedDummyProject(options: SeedDummyProjectOptions): Promis }), ]); + // Run sequentially after the parallel block. Both this and the + // `payments.testMode` write above target the same environment config, + // and `overrideEnvironmentConfigOverride` is read-modify-write — running + // them in parallel races and one write clobbers the other (TODO at + // config.ts:491 already documents this). Sequencing avoids the race + // until the underlying override is wrapped in a serializable txn. + if (getEnvVariable("STACK_SEED_ENABLE_SAML", "false") === "true") { + await seedSamlConnections(projectId); + } + await seedDummyTransactions({ prisma: dummyPrisma, tenancyId: dummyTenancy.id, From 8f6ad97e40311b5ef5b252cc920b91c69d145a9b Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Wed, 29 Apr 2026 18:45:02 -0700 Subject: [PATCH 2/2] fix(saml): skip allowSignIn=false connections in /auth/saml/discover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discover is the entry point for the SDK's signInWithSso({ email }) flow. Previously it returned every connection whose domain matched, including ones the project admin had disabled (`allowSignIn: false`). The SDK would then send the user through /auth/saml/login, which intentionally 403s for disabled connections — so disabling a connection was a sharp UX cliff: domain match → branded "Sign in with Acme SSO" CTA → 403. Treat disabled connections as if they didn't exist for discovery purposes. Direct sign-in via signInWithSaml({ connectionId }) is still gated separately in the login route, which is the right place for "intentional, explicit access by ID." --- apps/backend/src/app/api/latest/auth/saml/discover/route.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/backend/src/app/api/latest/auth/saml/discover/route.tsx b/apps/backend/src/app/api/latest/auth/saml/discover/route.tsx index bcf49cd64..13651fcaf 100644 --- a/apps/backend/src/app/api/latest/auth/saml/discover/route.tsx +++ b/apps/backend/src/app/api/latest/auth/saml/discover/route.tsx @@ -42,8 +42,13 @@ export const GET = createSmartRouteHandler({ } // Inject `id` into each connection so it satisfies SamlConnectionConfig — // the config schema stores id as the record key, not a value field. + // Skip connections with sign-in disabled — discover is the entry point + // for the signInWithSso flow, so returning a disabled connection would + // direct the user through `/auth/saml/login`, where it 403s. Treat + // disabled connections as if they didn't exist for discovery purposes. const connections: Record = {}; for (const [id, conn] of typedEntries(tenancy.config.auth.saml.connections)) { + if (conn.allowSignIn === false) continue; if (!conn.idpEntityId || !conn.idpSsoUrl || !conn.idpCertificate) continue; connections[id] = { id,