From cd4eb5627f1265ef13fc8df4f9482a670f7ca68e Mon Sep 17 00:00:00 2001 From: Gustavo Valverde Date: Wed, 22 Jul 2026 14:22:40 -0400 Subject: [PATCH] fix(sync): address behavioral review findings --- docs/content/docs/plugins/sso.mdx | 2 + package.json | 2 +- .../last-login-method.test.ts | 2 +- .../src/plugins/open-api/generator.ts | 2 +- .../src/plugins/open-api/open-api.test.ts | 31 +++++++++ packages/cli/src/commands/generate.ts | 1 + packages/cli/test/generate.test.ts | 63 +++++++++++++++++++ packages/sso/src/providers.test.ts | 41 ++++++++++++ packages/sso/src/routes/providers.ts | 7 ++- packages/sso/src/routes/schemas.ts | 1 + 10 files changed, 147 insertions(+), 5 deletions(-) diff --git a/docs/content/docs/plugins/sso.mdx b/docs/content/docs/plugins/sso.mdx index 222f57beb3..454dd4b18a 100644 --- a/docs/content/docs/plugins/sso.mdx +++ b/docs/content/docs/plugins/sso.mdx @@ -1530,6 +1530,8 @@ await authClient.signIn.sso({ For unsolicited **IdP-initiated** flows where `signIn.sso()` is not used (and therefore no client-side `callbackURL` is set), configure `idpInitiatedCallbackUrl` in the provider's `samlConfig` or in the global `saml` plugin options. The provider setting takes precedence over the global setting, and the same fallback applies to validation error redirects. Better Auth-generated `RelayState` success and error callbacks retain higher priority. If neither fallback is configured, the existing redirect behavior is preserved. +When updating a provider, pass `null` for `samlConfig.idpInitiatedCallbackUrl` to remove its override and fall back to the global setting. + The callback route supports both GET and POST methods automatically, so you don't need to create any additional route handlers in your framework. ### Additional Provider Fields diff --git a/package.json b/package.json index 90235b91b5..96dcd3e8da 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "url": "git+https://github.com/better-auth/better-auth.git" }, "scripts": { - "prepare": "git rev-parse --git-dir >/dev/null 2>&1 && lefthook install --reset-hooks-path || echo \"skipping lefthook (not a git repo)\"", + "prepare": "if git rev-parse --git-dir >/dev/null 2>&1; then lefthook install --reset-hooks-path; else echo \"skipping lefthook (not a git repo)\"; fi", "build": "turbo build --filter=./packages/*", "dev": "turbo dev --filter=./packages/* --filter=!./packages/cli", "clean": "turbo clean --filter=./packages/* && rm -rf node_modules", diff --git a/packages/better-auth/src/plugins/last-login-method/last-login-method.test.ts b/packages/better-auth/src/plugins/last-login-method/last-login-method.test.ts index a19c4043a2..db5ce5bc67 100644 --- a/packages/better-auth/src/plugins/last-login-method/last-login-method.test.ts +++ b/packages/better-auth/src/plugins/last-login-method/last-login-method.test.ts @@ -1096,7 +1096,7 @@ describe("lastLoginMethod", async () => { consentGiven = true; // Second login with consent - cookie should be set - await client.signOut(); + await client.signOut({ fetchOptions: { headers: headers1 } }); const headers2 = new Headers(); await client.signIn.email( { diff --git a/packages/better-auth/src/plugins/open-api/generator.ts b/packages/better-auth/src/plugins/open-api/generator.ts index cdd63917ff..f8373e5113 100644 --- a/packages/better-auth/src/plugins/open-api/generator.ts +++ b/packages/better-auth/src/plugins/open-api/generator.ts @@ -440,7 +440,7 @@ function dbFieldToRequestBodyProperty(field: DBFieldAttribute): OpenAPISchema { return { type: "string", format: "date-time" }; } if (field.type === "json") { - return { type: "object", additionalProperties: true }; + return {}; } if (field.type === "string[]") { return { type: "array", items: { type: "string" } }; diff --git a/packages/better-auth/src/plugins/open-api/open-api.test.ts b/packages/better-auth/src/plugins/open-api/open-api.test.ts index 82d73465e5..2a4cb55368 100644 --- a/packages/better-auth/src/plugins/open-api/open-api.test.ts +++ b/packages/better-auth/src/plugins/open-api/open-api.test.ts @@ -402,6 +402,37 @@ describe("open-api", async () => { expect(schemas["User"]!.required).toContain("scores"); }); + /** + * @see https://github.com/better-auth/better-auth/issues/10430 + */ + it("should allow every JSON value in additional field request bodies", async () => { + const { auth } = await getTestInstance( + { + plugins: [openAPI()], + user: { + additionalFields: { + metadata: { + type: "json", + }, + }, + }, + }, + { disableTestUser: true }, + ); + const schema = await auth.api.generateOpenAPISchema(); + const paths = schema.paths as Record; + + const signUpSchema = getPostRequestBody(paths, "/sign-up/email").content[ + "application/json" + ].schema; + expect(getSchemaProperty(signUpSchema, "metadata")).toEqual({}); + + const updateUserSchema = getPostRequestBody(paths, "/update-user").content[ + "application/json" + ].schema; + expect(getSchemaProperty(updateUserSchema, "metadata")).toEqual({}); + }); + it("should include additionalFields on sign-up and update-user request bodies", async () => { const schema = await auth.api.generateOpenAPISchema(); const paths = schema.paths as Record; diff --git a/packages/cli/src/commands/generate.ts b/packages/cli/src/commands/generate.ts index 5952ce1a97..147f245ca6 100644 --- a/packages/cli/src/commands/generate.ts +++ b/packages/cli/src/commands/generate.ts @@ -167,6 +167,7 @@ async function generateAction(opts: any) { ); return; } + await removeGeneratedStub(); let adapter: DBAdapter; if (options.adapter) { diff --git a/packages/cli/test/generate.test.ts b/packages/cli/test/generate.test.ts index 72d1d04f60..d2cf444620 100644 --- a/packages/cli/test/generate.test.ts +++ b/packages/cli/test/generate.test.ts @@ -128,6 +128,69 @@ export const auth = betterAuth({ } } }); + + /** + * @see https://github.com/better-auth/better-auth/issues/10136 + */ + it("should remove a config import stub before generating a new Prisma schema", async () => { + const cacheDir = path.join( + process.cwd(), + "node_modules", + ".cache", + "generate-output-", + ); + fs.mkdirSync(path.dirname(cacheDir), { recursive: true }); + const tmpDir = fs.mkdtempSync(cacheDir); + const outputPath = path.join(tmpDir, "schema.ts"); + fs.writeFileSync( + path.join(tmpDir, "auth.ts"), + `import schema from "./schema"; +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + secret: "test-secret", + baseURL: "http://localhost:3000", +}); + +export const __schema = schema; +`, + ); + + try { + await execFileAsync( + process.execPath, + [ + cliPath, + "generate", + "--cwd", + tmpDir, + "--config", + "auth.ts", + "--adapter", + "prisma", + "--dialect", + "sqlite", + "--output", + "schema.ts", + "--yes", + ], + { + cwd: tmpDir, + env: { + ...process.env, + BETTER_AUTH_TELEMETRY_DISABLED: "true", + }, + }, + ); + + const schema = fs.readFileSync(outputPath, "utf-8"); + expect(schema).toContain("generator client"); + expect(schema).toContain("datasource db"); + expect(schema).toContain("model User"); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); }); it("should generate prisma schema", async () => { diff --git a/packages/sso/src/providers.test.ts b/packages/sso/src/providers.test.ts index 6cda06e4d9..92ca26dcea 100644 --- a/packages/sso/src/providers.test.ts +++ b/packages/sso/src/providers.test.ts @@ -1068,6 +1068,47 @@ kBGIJYs= ); }); + /** + * @see https://github.com/better-auth/better-auth/issues/10329 + */ + it("should clear a provider-level IdP-initiated callback URL", async () => { + const { auth, getAuthHeaders, data } = createTestAuth(false); + const headers = await getAuthHeaders({ + email: "owner@example.com", + password: "password123", + name: "Owner", + }); + + await auth.api.registerSSOProvider({ + body: { + providerId: "my-saml-provider", + issuer: "https://idp.example.com", + domain: "example.com", + samlConfig: { + entryPoint: "https://idp.example.com/sso", + cert: TEST_CERT, + idpInitiatedCallbackUrl: "/dashboard", + idpMetadata: { entityID: "https://idp.example.com" }, + }, + }, + headers, + }); + + const updated = await auth.api.updateSSOProvider({ + body: { + providerId: "my-saml-provider", + samlConfig: { idpInitiatedCallbackUrl: null }, + }, + headers, + }); + + expect(updated.samlConfig?.idpInitiatedCallbackUrl).toBeUndefined(); + const storedConfig = safeJsonParse( + data.ssoProvider[0]!.samlConfig!, + ); + expect(storedConfig?.idpInitiatedCallbackUrl).toBeUndefined(); + }); + it("should perform partial update on OIDC provider", async () => { const { auth, getAuthHeaders, createOIDCProviderData, data } = createTestAuth(false); diff --git a/packages/sso/src/routes/providers.ts b/packages/sso/src/routes/providers.ts index b92cf78a5c..f7945ce2f5 100644 --- a/packages/sso/src/routes/providers.ts +++ b/packages/sso/src/routes/providers.ts @@ -590,8 +590,9 @@ function parseAndValidateConfig( type SAMLConfigUpdate = Omit< Partial, - "idpMetadata" | "spMetadata" + "idpInitiatedCallbackUrl" | "idpMetadata" | "spMetadata" > & { + idpInitiatedCallbackUrl?: string | null | undefined; idpMetadata?: Partial | undefined; spMetadata?: Partial> | undefined; }; @@ -631,7 +632,9 @@ function mergeSAMLConfig( audience: updates.audience ?? current.audience, callbackUrl: updates.callbackUrl ?? current.callbackUrl, idpInitiatedCallbackUrl: - updates.idpInitiatedCallbackUrl ?? current.idpInitiatedCallbackUrl, + updates.idpInitiatedCallbackUrl === null + ? undefined + : (updates.idpInitiatedCallbackUrl ?? current.idpInitiatedCallbackUrl), wantAssertionsSigned: updates.wantAssertionsSigned ?? current.wantAssertionsSigned, authnRequestsSigned: diff --git a/packages/sso/src/routes/schemas.ts b/packages/sso/src/routes/schemas.ts index e6d6344a70..af8354f279 100644 --- a/packages/sso/src/routes/schemas.ts +++ b/packages/sso/src/routes/schemas.ts @@ -326,6 +326,7 @@ const updateSSOProviderBodySchema = z.object({ .omit({ idpMetadata: true }) .partial() .extend({ + idpInitiatedCallbackUrl: samlRedirectUrlSchema.nullable().optional(), idpMetadata: samlIdentityProviderMetadataUpdateSchema.optional(), }) .optional(),