From 81c9143fe46cf6daa3b5a756d62e3cbdfe7e5797 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Wed, 11 Nov 2026 08:26:58 -0600 Subject: [PATCH] fix: upgrade Better Auth to 1.7.16 with account migration (#18906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip manages AI agents and their work. > - The board uses Better Auth for credential accounts and sessions. > - The server pins Better Auth 1.7.2. > - The November security release fixes 12 issues in Better Auth. > - The new version stops writing the old required account issuer field. > - This change updates the packages and the database contract together. > - New users can sign up, and existing users can still sign in. ## Linked Issues or Issue Description **What happened?** The server uses Better Auth 1.7.2. The [security release](https://better-auth.com/blog/security-release-2026-11-03) fixes 12 issues in 1.7.16. Earlier update PRs #17909 and #17911 target 1.7.7, which is below this security release. **Expected behavior** Use the patched stable release. Keep new and existing credential accounts usable. **Steps to reproduce** Read the server dependency pin. Upgrade it without a schema change. Better Auth then rejects the required account issuer column. See the [upgrade guide](https://better-auth.com/docs/guides/1-7-upgrade-guide). ## What Changed - Pin Better Auth to 1.7.16. The repository refresh bot owns the committed Core lockfile. - Make the old issuer column nullable. Keep existing issuer values. - Replace the issuer index with a unique provider/account index. - Generate the migration and retain the five newest schema snapshots. - Test new signup, new-account sign-in, legacy issuer sign-in, duplicate rejection, and separate provider identities with real PostgreSQL. - Update the database schema test. Test the historical issuer backfill separately. Verify upgrades over existing rows and rollback on duplicate provider/account keys. - Document the duplicate check and migration order. - Add the approved two-line dependency resolution step before the backup job’s frozen install. Leave the frozen check and backup tests intact. ## Verification - Core head: `af70282c7e24335b8688c8f3cc88fdf65045632c`; parent `0f1481e4462100357a7caa496b875cf0bc500d95` includes master `84fc67317`. - Nine changed files. The only workflow edit is the approved two-line backup dependency resolution step; no managed lockfile edits. - Migration `0465_better_auth_account_identity.sql` is three SQL statements; snapshot changes only `public.account`, five newest snapshots retained. - DB typecheck and 23 focused tests passed on the parent. Both real PostgreSQL migration/signup suites were rerun November 11: all 4 tests passed. Application source and tests are unchanged in this workflow-only commit. - The parent’s sandbox-provider lane passed all 205 file-sync tests but reported an unhandled EPIPE during the symlink rejection case; its aggregate verify therefore failed. No unrelated source change was made. The new head reruns the full applicable CI. - Reproduced the old backup install failure locally. Resolving manifests followed by a frozen install passed locally; restored the bot-owned committed lock afterward. Hosted backup qualification remains required. - Current-head CI and review are running; no final-head green claim or merge yet. ## Risks Apply the migration before the new server starts. Check for duplicate provider/account pairs first. Duplicate rows stop index creation. Keep separate users separate when resolving duplicates. The migration keeps issuer data. Do not restore its NOT NULL constraint while new rows contain null. No deployment is included. ## Model Used OpenAI GPT-6 Codex, with reasoning and tool use. The runtime did not expose a more specific model ID or context window. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- .../backup-checkpoint-qualification.yml | 2 + doc/DATABASE.md | 23 +++ ...tter-auth-account-issuer-migration.test.ts | 90 +++++++---- .../0465_better_auth_account_identity.sql | 3 + ...{0460_snapshot.json => 0465_snapshot.json} | 147 +++++++++++++++++- packages/db/src/migrations/meta/_journal.json | 7 + packages/db/src/schema/auth.ts | 18 +-- server/package.json | 2 +- ...auth-credential-signup.integration.test.ts | 19 ++- 9 files changed, 252 insertions(+), 59 deletions(-) create mode 100644 packages/db/src/migrations/0465_better_auth_account_identity.sql rename packages/db/src/migrations/meta/{0460_snapshot.json => 0465_snapshot.json} (99%) diff --git a/.github/workflows/backup-checkpoint-qualification.yml b/.github/workflows/backup-checkpoint-qualification.yml index 3b90802cc13..d325d927e89 100644 --- a/.github/workflows/backup-checkpoint-qualification.yml +++ b/.github/workflows/backup-checkpoint-qualification.yml @@ -46,6 +46,8 @@ jobs: with: node-version: '24' cache: pnpm + - name: Resolve dependency manifests for this run + run: pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile - run: pnpm install --frozen-lockfile --ignore-scripts - name: Build workspace import prerequisites run: | diff --git a/doc/DATABASE.md b/doc/DATABASE.md index c5822df0f02..b231152f6d4 100644 --- a/doc/DATABASE.md +++ b/doc/DATABASE.md @@ -1,5 +1,28 @@ # Database +### Better Auth 1.7.16 upgrade + +Better Auth 1.7.3 and later identify accounts by `provider_id` and `account_id`. +They no longer write the `issuer` column that 1.7.0–1.7.2 required. Apply the +schema migration before starting the updated server. It makes `issuer` nullable, +keeps its existing values, removes the old issuer index, and adds a unique index +on the provider/account pair. This permits new credential signups and preserves +existing accounts. + +Before the upgrade, check for duplicate provider/account pairs: + +```sql +SELECT provider_id, account_id, count(*) +FROM account +GROUP BY provider_id, account_id +HAVING count(*) > 1; +``` + +Resolve any duplicates before migration. Keep different users separate. A duplicate +causes index creation to fail rather than leaving an ambiguous login identity. +Do not restore the issuer `NOT NULL` constraint on rollback while new rows have +null issuers. See the [Better Auth upgrade guide](https://better-auth.com/docs/guides/1-7-upgrade-guide). + ### Durable native provider backups `native_harness_backups` journals encrypted provider-session archives in object diff --git a/packages/db/src/better-auth-account-issuer-migration.test.ts b/packages/db/src/better-auth-account-issuer-migration.test.ts index a8264d99ec7..ed260aa9a15 100644 --- a/packages/db/src/better-auth-account-issuer-migration.test.ts +++ b/packages/db/src/better-auth-account-issuer-migration.test.ts @@ -1,17 +1,8 @@ -/** - * The `account.issuer` backfill has to be total. The column lands NOT NULL, so - * a row the UPDATE misses aborts the whole migration and leaves an upgraded - * install with no working auth at all. This suite rewinds the migration, - * seeds the pre-upgrade rows an existing deployment would have, and re-applies - * it — a backfill narrowed to one provider fails here. - */ - -import { createHash, randomUUID } from "node:crypto"; +import { randomUUID } from "node:crypto"; import fs from "node:fs"; import { getTableConfig } from "drizzle-orm/pg-core"; import postgres from "postgres"; import { afterEach, describe, expect, it } from "vitest"; -import { applyPendingMigrations } from "./client.js"; import { authAccounts } from "./schema/auth.js"; import { getEmbeddedPostgresTestSupport, @@ -21,10 +12,6 @@ import { const MIGRATION_FILE = "0230_better_auth_account_issuer.sql"; const UNIQUE_INDEX = "account_issuer_account_id_uq"; -// Better Auth's own issuer helpers: `createLocalAccountIssuer("credential")` -// for email/password and `createOAuthAccountIssuer(providerId)` for a social -// provider that declares no issuer of its own. Sign-in looks the credential -// account up by this exact value. const CREDENTIAL_ISSUER = "local:credential"; const OAUTH_ISSUER_PREFIX = "local:oauth:"; @@ -32,33 +19,32 @@ const cleanups: Array<() => Promise> = []; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; -async function migrationHash() { - const content = await fs.promises.readFile( - new URL(`./migrations/${MIGRATION_FILE}`, import.meta.url), - "utf8", - ); - return createHash("sha256").update(content).digest("hex"); +async function applySqlMigration(sql: postgres.Sql, file: string) { + const content = await fs.promises.readFile(new URL(`./migrations/${file}`, import.meta.url), "utf8"); + await sql.begin(async (transaction) => { + for (const statement of content.split("--> statement-breakpoint")) { + if (statement.trim()) await transaction.unsafe(statement); + } + }); } describe("account issuer schema", () => { - it("declares the column Better Auth requires and the key it indexes on", () => { + it("keeps issuer nullable and identifies accounts by provider and account ID", () => { const config = getTableConfig(authAccounts); const issuer = config.columns.find((column) => column.name === "issuer"); expect(issuer).toBeDefined(); - expect(issuer?.notNull).toBe(true); + expect(issuer?.notNull).toBe(false); - // Better Auth declares `(issuer, accountId)` unique on the account model - // and resolves accounts by that pair. - const unique = config.indexes.find((index) => index.config.name === UNIQUE_INDEX); + const unique = config.indexes.find((index) => index.config.name === "account_provider_account_id_uq"); expect(unique?.config.unique).toBe(true); expect(unique?.config.columns.map((column) => (column as { name: string }).name)).toEqual([ - "issuer", + "provider_id", "account_id", ]); }); }); -describeEmbeddedPostgres("account issuer migration", () => { +describeEmbeddedPostgres("historical issuer migration and patched provider identity upgrade", () => { // Reverse registration order, one at a time. The raw `postgres` client is not // registered with the module's client registry, so the cluster teardown does // not close it. Stopping the cluster while that client is still draining kills @@ -75,8 +61,7 @@ describeEmbeddedPostgres("account issuer migration", () => { const sql = postgres(database.connectionString, { max: 1 }); cleanups.push(async () => sql.end()); - // Rewind to the pre-upgrade shape: no issuer column, no unique index. - await sql`DELETE FROM "drizzle"."__drizzle_migrations" WHERE "hash" = ${await migrationHash()}`; + await sql`DROP INDEX IF EXISTS "account_provider_account_id_uq"`; await sql`DROP INDEX IF EXISTS ${sql(UNIQUE_INDEX)}`; await sql`ALTER TABLE "account" DROP COLUMN IF EXISTS "issuer"`; @@ -101,7 +86,7 @@ describeEmbeddedPostgres("account issuer migration", () => { (${googleAccountId}, 'google-subject-1', 'google', ${socialUserId}, NULL, now(), now()) `; - await applyPendingMigrations(database.connectionString); + await applySqlMigration(sql, MIGRATION_FILE); const rows = await sql<{ id: string; issuer: string }[]>` SELECT "id", "issuer" FROM "account" ORDER BY "id" @@ -133,5 +118,50 @@ describeEmbeddedPostgres("account issuer migration", () => { VALUES (${`account-${randomUUID()}`}, ${CREDENTIAL_ISSUER}, ${credentialUserId}, 'credential', ${credentialUserId}, now(), now()) `, ).rejects.toMatchObject({ code: "23505", constraint_name: UNIQUE_INDEX }); + + await applySqlMigration(sql, "0465_better_auth_account_identity.sql"); + const preserved = await sql`SELECT "id", "issuer" FROM "account" ORDER BY "id"`; + expect(preserved).toEqual(rows); + const [nullable] = await sql` + SELECT "is_nullable" FROM "information_schema"."columns" + WHERE "table_schema" = 'public' AND "table_name" = 'account' AND "column_name" = 'issuer' + `; + expect(nullable?.is_nullable).toBe("YES"); + await expect(sql` + INSERT INTO "account" ("id", "account_id", "provider_id", "user_id", "created_at", "updated_at") + VALUES (${randomUUID()}, ${credentialUserId}, 'credential', ${credentialUserId}, now(), now()) + `).rejects.toMatchObject({ code: "23505", constraint_name: "account_provider_account_id_uq" }); + await sql` + INSERT INTO "account" ("id", "account_id", "provider_id", "user_id", "created_at", "updated_at") + VALUES (${randomUUID()}, 'new-subject', 'credential', ${credentialUserId}, now(), now()) + `; + }, 30_000); +}); + +describeEmbeddedPostgres("provider identity migration with duplicate legacy keys", () => { + it("rejects ambiguous provider/account keys without changing legacy rows", async () => { + const database = await startEmbeddedPostgresTestDatabase("paperclip-account-duplicates-"); + const sql = postgres(database.connectionString, { max: 1 }); + try { + await sql`DROP INDEX "account_provider_account_id_uq"`; + await sql`ALTER TABLE "account" ALTER COLUMN "issuer" SET NOT NULL`; + await sql`CREATE UNIQUE INDEX "account_issuer_account_id_uq" ON "account" ("issuer", "account_id")`; + const userId = randomUUID(); + await sql`INSERT INTO "user" ("id", "name", "email", "created_at", "updated_at") + VALUES (${userId}, 'Legacy User', 'legacy@example.com', now(), now())`; + await sql`INSERT INTO "account" ("id", "issuer", "account_id", "provider_id", "user_id", "created_at", "updated_at") + VALUES (${randomUUID()}, 'issuer-a', 'same-subject', 'credential', ${userId}, now(), now()), + (${randomUUID()}, 'issuer-b', 'same-subject', 'credential', ${userId}, now(), now())`; + const before = await sql`SELECT "id", "issuer" FROM "account" ORDER BY "id"`; + await expect(applySqlMigration(sql, "0465_better_auth_account_identity.sql")) + .rejects.toMatchObject({ code: "23505" }); + expect(await sql`SELECT "id", "issuer" FROM "account" ORDER BY "id"`).toEqual(before); + const [column] = await sql`SELECT "is_nullable" FROM "information_schema"."columns" + WHERE "table_schema" = 'public' AND "table_name" = 'account' AND "column_name" = 'issuer'`; + expect(column?.is_nullable).toBe("NO"); + } finally { + await sql.end(); + await database.cleanup(); + } }, 30_000); }); diff --git a/packages/db/src/migrations/0465_better_auth_account_identity.sql b/packages/db/src/migrations/0465_better_auth_account_identity.sql new file mode 100644 index 00000000000..5145e3b237f --- /dev/null +++ b/packages/db/src/migrations/0465_better_auth_account_identity.sql @@ -0,0 +1,3 @@ +DROP INDEX "account_issuer_account_id_uq";--> statement-breakpoint +ALTER TABLE "account" ALTER COLUMN "issuer" DROP NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "account_provider_account_id_uq" ON "account" USING btree ("provider_id","account_id"); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0460_snapshot.json b/packages/db/src/migrations/meta/0465_snapshot.json similarity index 99% rename from packages/db/src/migrations/meta/0460_snapshot.json rename to packages/db/src/migrations/meta/0465_snapshot.json index 38297d445c0..951e3c6deea 100644 --- a/packages/db/src/migrations/meta/0460_snapshot.json +++ b/packages/db/src/migrations/meta/0465_snapshot.json @@ -1,6 +1,6 @@ { - "id": "f93ccf18-7e86-4788-be77-b17430c86b2f", - "prevId": "e070677d-cfb8-4438-847c-29c4da7ca8c5", + "id": "38dbf2b9-da26-4c3a-b813-f37d0c19cba2", + "prevId": "e2a678da-05cc-4316-a40e-d85a69be01e3", "version": "7", "dialect": "postgresql", "tables": { @@ -6233,7 +6233,7 @@ "name": "issuer", "type": "text", "primaryKey": false, - "notNull": true + "notNull": false }, "account_id": { "name": "account_id", @@ -6309,11 +6309,11 @@ } }, "indexes": { - "account_issuer_account_id_uq": { - "name": "account_issuer_account_id_uq", + "account_provider_account_id_uq": { + "name": "account_provider_account_id_uq", "columns": [ { - "expression": "issuer", + "expression": "provider_id", "isExpression": false, "asc": true, "nulls": "last" @@ -43580,6 +43580,28 @@ "method": "btree", "with": {} }, + "issues_company_legacy_origin_run_idx": { + "name": "issues_company_legacy_origin_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"issues\".\"origin_issue_id\" is null and \"issues\".\"origin_run_id\" is not null", + "concurrently": true, + "method": "btree", + "with": {} + }, "issues_company_project_workspace_idx": { "name": "issues_company_project_workspace_idx", "columns": [ @@ -49085,6 +49107,93 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.plugin_lifecycle_resource_cursors": { + "name": "plugin_lifecycle_resource_cursors", + "schema": "", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "acknowledged_through_id": { + "name": "acknowledged_through_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "plugin_lifecycle_resource_cursors_plugin_id_plugins_id_fk": { + "name": "plugin_lifecycle_resource_cursors_plugin_id_plugins_id_fk", + "tableFrom": "plugin_lifecycle_resource_cursors", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_lifecycle_resource_cursors_company_id_companies_id_fk": { + "name": "plugin_lifecycle_resource_cursors_company_id_companies_id_fk", + "tableFrom": "plugin_lifecycle_resource_cursors", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_lifecycle_resource_cursors_pk": { + "name": "plugin_lifecycle_resource_cursors_pk", + "columns": [ + "plugin_id", + "company_id", + "resource_type", + "resource_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.plugin_logs": { "name": "plugin_logs", "schema": "", @@ -50949,6 +51058,12 @@ "primaryKey": false, "notNull": false }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "git_authorization_grant_id": { "name": "git_authorization_grant_id", "type": "uuid", @@ -51100,6 +51215,19 @@ "onDelete": "no action", "onUpdate": "no action" }, + "projects_environment_id_environments_id_fk": { + "name": "projects_environment_id_environments_id_fk", + "tableFrom": "projects", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, "projects_git_authorization_grant_id_connection_grants_id_fk": { "name": "projects_git_authorization_grant_id_connection_grants_id_fk", "tableFrom": "projects", @@ -59494,6 +59622,13 @@ "notNull": true, "default": "''" }, + "pending_reveal": { + "name": "pending_reveal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, "allowed_actions": { "name": "allowed_actions", "type": "jsonb", diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 1deaa2d7742..efa85154e34 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -3242,6 +3242,13 @@ "when": 1794346237220, "tag": "0464_project_environment", "breakpoints": true + }, + { + "idx": 465, + "version": "7", + "when": 1794400732201, + "tag": "0465_better_auth_account_identity", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/auth.ts b/packages/db/src/schema/auth.ts index 1e82b8a1256..4679accedbb 100644 --- a/packages/db/src/schema/auth.ts +++ b/packages/db/src/schema/auth.ts @@ -21,25 +21,11 @@ export const authSessions = pgTable("session", { userId: text("user_id").notNull().references(() => authUsers.id, { onDelete: "cascade" }), }); -/** - * Better Auth's `account` model. Every column here has to exist for the - * Drizzle adapter to accept the table at all: the adapter validates the - * model's fields against the schema on each write and throws - * `The field "" does not exist in the "account" Drizzle schema` - * when one is missing, which surfaces as a 500 on sign-up. - * - * `issuer` is the account namespace Better Auth 1.7 added. It is - * `local:credential` for email/password accounts and - * `local:oauth:` for social accounts that do not declare an - * issuer of their own, and it pairs with `account_id` as the stable - * provider-side key — hence the unique index, which mirrors the - * `(issuer, accountId)` unique index Better Auth declares on the model. - */ export const authAccounts = pgTable( "account", { id: text("id").primaryKey(), - issuer: text("issuer").notNull(), + issuer: text("issuer"), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), userId: text("user_id").notNull().references(() => authUsers.id, { onDelete: "cascade" }), @@ -54,7 +40,7 @@ export const authAccounts = pgTable( updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(), }, (table) => ({ - issuerAccountIdUq: uniqueIndex("account_issuer_account_id_uq").on(table.issuer, table.accountId), + providerAccountIdUq: uniqueIndex("account_provider_account_id_uq").on(table.providerId, table.accountId), }), ); diff --git a/server/package.json b/server/package.json index f6d4ab6491c..24877a9bd28 100644 --- a/server/package.json +++ b/server/package.json @@ -90,7 +90,7 @@ "ai": "7.0.130", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", - "better-auth": "1.7.2", + "better-auth": "1.7.16", "chat": "4.39.0", "chokidar": "^5.0.0", "compression": "^1.8.2", diff --git a/server/src/__tests__/better-auth-credential-signup.integration.test.ts b/server/src/__tests__/better-auth-credential-signup.integration.test.ts index aaf46716561..f4fd9643e71 100644 --- a/server/src/__tests__/better-auth-credential-signup.integration.test.ts +++ b/server/src/__tests__/better-auth-credential-signup.integration.test.ts @@ -28,11 +28,6 @@ const ORIGIN = "http://127.0.0.1:41999"; const EMAIL = "founder@example.com"; const PASSWORD = "correct-horse-battery-staple"; -// The issuer Better Auth stamps on an email/password account: -// `createLocalAccountIssuer("credential")`. Sign-in matches on it, so a row -// written with anything else is a row nobody can sign in as. -const CREDENTIAL_ISSUER = "local:credential"; - const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; @@ -107,7 +102,7 @@ describeEmbeddedPostgres("Better Auth credential sign-up against the real schema expect(accounts).toHaveLength(1); expect(accounts[0]).toMatchObject({ providerId: "credential", - issuer: CREDENTIAL_ISSUER, + issuer: null, }); expect(accounts[0]?.password).toBeTruthy(); @@ -119,5 +114,17 @@ describeEmbeddedPostgres("Better Auth credential sign-up against the real schema expect(signIn.status).toBe(200); expect(signIn.body?.user?.email).toBe(EMAIL); expect(sessionCookies(signIn).length).toBeGreaterThan(0); + + await db.update(authAccounts).set({ issuer: "local:credential" }); + const legacySignIn = await request(app) + .post("/api/auth/sign-in/email") + .set("origin", ORIGIN) + .send({ email: EMAIL, password: PASSWORD }); + expect(legacySignIn.status).toBe(200); + expect(legacySignIn.body?.user?.email).toBe(EMAIL); + + await expect(db.insert(authAccounts).values({ ...accounts[0]!, id: "duplicate-credential" })) + .rejects.toMatchObject({ cause: { code: "23505", constraint_name: "account_provider_account_id_uq" } }); + await db.insert(authAccounts).values({ ...accounts[0]!, id: "other-provider", providerId: "other" }); }); });