chore(deps): upgrade better-auth family to 1.7.2 (#378)

better-auth, @better-auth/oauth-provider and @better-auth/sso move from
1.6.23 to 1.7.2. This is the first stable line with the oauth-provider fix
for unbound resource indicators (Dependabot alert #55); no 1.6.x release
has it.

1.7 keys accounts by (issuer, accountId) and looks the credential account
up with issuer "local:credential" on sign-in, so migration 0016 adds the
column and backfills existing rows: local accounts get local:credential,
SSO accounts take their provider's issuer, other OAuth accounts get the
local:oauth namespace. It also adds jwks.alg and jwks.crv, the 1.7
oauth-provider columns, and the oauth_resources, oauth_client_resources
and oauth_client_assertions tables.

A database that booted the reverted 1.7.0-rc.4 build (on main for a few
minutes on 2026-08-06, and any dev database from then) already has part of
that schema without a 0016 record, which makes 0016 fail on every boot.
repairStrandedBetterAuth17Schema drops the leftovers before migrate runs,
in the same way the #312 repair handles stranded SSO columns; the
migration validator exercises it.

createOAuthClient no longer takes "type"; the SSO applications route sends
application_type (web or native) and reads it back with the legacy column
as fallback. bun.nix is regenerated. reset-users writes the issuer.

Supersedes #374.

Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX
This commit is contained in:
ARUNAVO RAY authored and GitHub committed 2026-09-02 14:07:08 +05:30
1 parent d211e54877
commit 3ff05ec7f3
12 files changed
+3320 -84

No files matched your search

+1
View File
@@ -258,6 +258,7 @@ async function resetPassword() {
accountId: user.id,
userId: user.id,
providerId: "credential",
issuer: "local:credential", // better-auth 1.7 keys accounts by issuer
password: hashedPassword,
createdAt: now,
updatedAt: now,
+167 -1
View File
@@ -5,6 +5,7 @@ import { readFileSync } from "fs";
import path from "path";
import {
repairDuplicateSsoColumns,
repairStrandedBetterAuth17Schema,
restoreSsoDataAfter0013,
} from "../src/lib/db/migration-repairs";
@@ -408,6 +409,100 @@ function verify0015Migration(db: any) {
);
}
function seedPre0016Database(db: any) {
// Migrations 0000-0015 have run: accounts have no issuer column and the
// OAuth provider tables are in their better-auth 1.6 shape. Seed one
// account of each kind so the issuer backfill can be checked, plus a row
// in every OAuth table the 1.7 column adds touch.
db.run("INSERT INTO users (id, email, username, name) VALUES ('u-16', 'u16@example.com', 'u16', 'User Sixteen')");
db.run("INSERT INTO accounts (id, account_id, user_id, provider_id, password) VALUES ('acct-cred', 'u-16', 'u-16', 'credential', 'hashed')");
db.run("INSERT INTO sso_providers (id, issuer, domain, oidc_config, user_id, provider_id) VALUES ('sso-16', 'https://idp.example.com', 'example.com', '{}', 'u-16', 'example-oidc')");
db.run("INSERT INTO accounts (id, account_id, user_id, provider_id) VALUES ('acct-sso', 'sub-123', 'u-16', 'example-oidc')");
db.run("INSERT INTO accounts (id, account_id, user_id, provider_id) VALUES ('acct-oauth', 'gh-123', 'u-16', 'github')");
db.run("INSERT INTO oauth_clients (id, client_id, redirect_uris) VALUES ('app-16', 'client-16', '[\"https://example.com/cb\"]')");
db.run("INSERT INTO oauth_access_tokens (id, token, client_id, user_id, scopes) VALUES ('at-16', 'access-16', 'client-16', 'u-16', '[\"openid\"]')");
db.run("INSERT INTO oauth_refresh_tokens (id, token, client_id, user_id, scopes) VALUES ('rt-16', 'refresh-16', 'client-16', 'u-16', '[\"openid\"]')");
db.run("INSERT INTO oauth_consents (id, client_id, user_id, scopes) VALUES ('co-16', 'client-16', 'u-16', '[\"openid\"]')");
db.run("INSERT INTO jwks (id, public_key, private_key) VALUES ('key-16', 'pub', 'priv')");
}
function verify0016Migration(db: any) {
// better-auth 1.7 keys accounts by (issuer, accountId); sign-in looks the
// credential account up with issuer "local:credential", so the backfill
// is what keeps existing users able to log in.
const issuers = Object.fromEntries(
(db.query("SELECT id, issuer FROM accounts ORDER BY id").all() as Array<{ id: string; issuer: string }>).map(
(row) => [row.id, row.issuer],
),
);
assert(issuers["acct-cred"] === "local:credential", `Expected credential account issuer local:credential, got ${issuers["acct-cred"]}`);
assert(issuers["acct-sso"] === "https://idp.example.com", `Expected SSO account to take its provider issuer, got ${issuers["acct-sso"]}`);
assert(issuers["acct-oauth"] === "local:oauth:github", `Expected OAuth account issuer local:oauth:github, got ${issuers["acct-oauth"]}`);
const accountCols = db.query("PRAGMA table_info(accounts)").all() as Array<{ name: string; notnull: number; dflt_value: string | null }>;
const issuerCol = accountCols.find((c) => c.name === "issuer");
assert(issuerCol && issuerCol.notnull === 1, "Expected accounts.issuer to be NOT NULL");
// jwks gained nullable alg and crv.
const jwksCols = db.query("PRAGMA table_info(jwks)").all() as Array<{ name: string; notnull: number }>;
for (const name of ["alg", "crv"]) {
const col = jwksCols.find((c) => c.name === name);
assert(col && col.notnull === 0, `Expected jwks.${name} to exist and be nullable`);
}
const key = db.query("SELECT id, alg FROM jwks WHERE id = 'key-16'").get() as { id: string; alg: string | null } | null;
assert(key && key.alg === null, "Expected existing jwks row to survive with NULL alg");
// Columns added for @better-auth/oauth-provider 1.7, all nullable so
// pre-existing rows are untouched.
const expectedNewColumns: Record<string, string[]> = {
oauth_clients: [
"client_discovery_id",
"client_credentials_scopes",
"backchannel_logout_uri",
"backchannel_logout_session_required",
"application_type",
"jwks",
"jwks_uri",
"dpop_bound_access_tokens",
],
oauth_access_tokens: ["authorization_code_id", "resources", "requested_user_info_claims", "revoked", "confirmation"],
oauth_refresh_tokens: [
"authorization_code_id",
"resources",
"requested_user_info_claims",
"rotated_at",
"rotation_replay_response",
"rotation_replay_expires_at",
"confirmation",
],
oauth_consents: ["resources", "requested_user_info_claims"],
};
for (const [table, columns] of Object.entries(expectedNewColumns)) {
const cols = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string; notnull: number }>;
for (const column of columns) {
const col = cols.find((c) => c.name === column);
assert(col, `Expected ${table}.${column} column to exist`);
assert(col.notnull === 0, `Expected ${table}.${column} to be nullable`);
}
}
// Seeded rows survive the column adds.
for (const [table, id] of [
["oauth_clients", "app-16"],
["oauth_access_tokens", "at-16"],
["oauth_refresh_tokens", "rt-16"],
["oauth_consents", "co-16"],
] as const) {
const row = db.query(`SELECT id FROM ${table} WHERE id = '${id}'`).get();
assert(row, `Expected ${table} row '${id}' to survive migration`);
}
// The three new 1.7 tables exist and accept rows.
db.run("INSERT INTO oauth_resources (id, identifier, name) VALUES ('res-16', 'https://api.example.com', 'Example API')");
db.run("INSERT INTO oauth_client_resources (id, client_id, resource_id) VALUES ('cr-16', 'client-16', 'https://api.example.com')");
db.run("INSERT INTO oauth_client_assertions (id, expires_at) VALUES ('jti-16', 1234567890)");
}
const MIGRATION_0012_TIMESTAMP = 1774062000000;
const MIGRATION_0013_TIMESTAMP = 1780377747526;
@@ -502,6 +597,70 @@ function validateBroken0013Repair() {
}
}
const MIGRATION_0015_TIMESTAMP = 1788333701612;
/**
* Reproduce a database that booted the reverted better-auth 1.7.0-rc.4 build:
* migrations recorded through 0015, but the three rc tables and some of the
* rc columns already present. Migration 0016 must fail on it as-is, and
* repairStrandedBetterAuth17Schema() must reconcile it so 0016 then runs and
* backfills account issuers.
*/
function validateStranded0016Repair() {
const migration0016 = migrations.find((m) => m.entry.tag === "0016_better_auth_1_7");
if (!migration0016) return;
const db = new Database(":memory:");
try {
runMigrations(db, migrations.slice(0, 16)); // 0000-0015
db.run(
"CREATE TABLE IF NOT EXISTS `__drizzle_migrations` (id INTEGER PRIMARY KEY AUTOINCREMENT, hash text NOT NULL, created_at numeric)",
);
db.run("INSERT INTO `__drizzle_migrations` (hash, created_at) VALUES ('through-0015', ?)", [
MIGRATION_0015_TIMESTAMP,
]);
// Leftovers from the rc build.
db.run("CREATE TABLE oauth_client_assertions (id text PRIMARY KEY NOT NULL, expires_at integer NOT NULL)");
db.run("CREATE TABLE oauth_resources (id text PRIMARY KEY NOT NULL, identifier text NOT NULL, name text NOT NULL)");
db.run("ALTER TABLE oauth_clients ADD client_discovery_id text");
db.run("ALTER TABLE oauth_access_tokens ADD resources text");
db.run("INSERT INTO users (id, email, username, name) VALUES ('u-rc', 'rc@example.com', 'rc', 'RC User')");
db.run("INSERT INTO accounts (id, account_id, user_id, provider_id, password) VALUES ('acct-rc', 'u-rc', 'u-rc', 'credential', 'hashed')");
// As-is, 0016 cannot run: the CREATE TABLE collides.
let failedAsIs = false;
try {
runMigration(db, migration0016);
} catch {
failedAsIs = true;
}
assert(failedAsIs, "Expected migration 0016 to fail on the rc leftovers without the repair");
const dropped = repairStrandedBetterAuth17Schema(db);
assert(dropped === 4, `Expected the repair to drop 2 tables and 2 columns, dropped ${dropped}`);
runMigration(db, migration0016);
const account = db
.query("SELECT issuer FROM accounts WHERE id = 'acct-rc'")
.get() as { issuer: string } | null;
assert(account?.issuer === "local:credential", `Expected the credential account to be backfilled after the repair, got ${account?.issuer}`);
const clientCols = (db.query("PRAGMA table_info(oauth_clients)").all() as TableInfoRow[]).map((c) => c.name);
assert(clientCols.includes("client_discovery_id") && clientCols.includes("jwks_uri"), "Expected 0016 to re-add the oauth_clients columns in full");
db.run("INSERT INTO oauth_client_assertions (id, expires_at) VALUES ('jti-rc', 1)");
// Idempotency: with 0016 recorded the repair is a no-op.
db.run("INSERT INTO `__drizzle_migrations` (hash, created_at) VALUES ('through-0016', ?)", [
1788336988526,
]);
assert(repairStrandedBetterAuth17Schema(db) === 0, "Expected the repair to no-op once migration 0016 is recorded");
} finally {
db.close();
}
}
const latestUpgradeFixtures: Record<string, UpgradeFixture> = {
"0009_nervous_tyger_tiger": {
seed: seedPre0009Database,
@@ -531,6 +690,10 @@ const latestUpgradeFixtures: Record<string, UpgradeFixture> = {
seed: seedPre0015Database,
verify: verify0015Migration,
},
"0016_better_auth_1_7": {
seed: seedPre0016Database,
verify: verify0016Migration,
},
};
function lintMigrations(selectedMigrations: Migration[]) {
@@ -584,8 +747,11 @@ function validateMigrations() {
// Exercise the runtime repair for the issue #312 duplicate-column crash.
validateBroken0013Repair();
// Exercise the repair for databases that booted the reverted 1.7.0-rc.4 build.
validateStranded0016Repair();
console.log(
`Validated ${migrations.length} migrations from scratch and upgrade path for ${latestMigration.entry.tag}, plus the #312 SSO-column repair.`,
`Validated ${migrations.length} migrations from scratch and upgrade path for ${latestMigration.entry.tag}, plus the #312 SSO-column repair and the better-auth 1.7 rc repair.`,
);
}