auth 1.7.2 → 1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.mts CHANGED
@@ -12,6 +12,11 @@ interface SchemaGeneratorResult {
12
12
  * without corrupting the rows it already holds.
13
13
  */
14
14
  unsafeChanges?: string[];
15
+ /**
16
+ * Columns the database requires that Better Auth never writes. No generated
17
+ * migration removes them; each entry names the change that does.
18
+ */
19
+ schemaProblems?: string[];
15
20
  }
16
21
  interface SchemaGenerator {
17
22
  <Options extends BetterAuthOptions>(opts: {
package/dist/api.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, resolveDatabaseSchemaIndexes } from "@better-auth/core/db/internal";
2
+ import { diffSchema, formatSchemaFinding, getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, resolveDatabaseSchemaIndexes } from "@better-auth/core/db/internal";
3
3
  import { capitalizeFirstLetter, toSnakeCase } from "@better-auth/core/utils/string";
4
4
  import { initGetFieldName, initGetModelName } from "better-auth/adapters";
5
5
  import { getAuthTables } from "better-auth/db";
@@ -8,7 +8,8 @@ import { getMigrations } from "better-auth/db/migration";
8
8
  import fs from "node:fs/promises";
9
9
  import path from "node:path";
10
10
  import { BetterAuthError } from "@better-auth/core/error";
11
- import { produceSchema } from "@mrleebo/prisma-ast";
11
+ import { getSchema, produceSchema } from "@mrleebo/prisma-ast";
12
+ import * as z from "zod";
12
13
  //#region src/generators/drizzle.ts
13
14
  function convertToSnakeCase(str, camelCase) {
14
15
  return camelCase ? str : toSnakeCase(str);
@@ -333,17 +334,20 @@ function commentBanner(unsafeChanges) {
333
334
  return lines.join("\n");
334
335
  }
335
336
  const generateKyselySchema = async ({ options, file }) => {
336
- const { compileMigrations, unsafeChanges } = await getMigrations(options, { throwOnUnsafe: false });
337
+ const { compileMigrations, unsafeChanges, schemaProblems } = await getMigrations(options, { throwOnUnsafe: false });
337
338
  const migrations = await compileMigrations();
338
339
  const code = migrations.trim() === ";" ? "" : migrations;
339
340
  return {
340
341
  code: unsafeChanges.length ? `${commentBanner(unsafeChanges)}${code}` : code,
341
342
  unsafeChanges,
343
+ schemaProblems,
342
344
  fileName: file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql`
343
345
  };
344
346
  };
345
- //#endregion
346
- //#region src/utils/get-package-info.ts
347
+ z.object({
348
+ name: z.string(),
349
+ version: z.string()
350
+ });
347
351
  function getPackageInfo(cwd) {
348
352
  const packageJsonPath = cwd ? path.join(cwd, "package.json") : path.join("package.json");
349
353
  return JSON.parse(readFileSync(packageJsonPath, "utf-8"));
@@ -407,6 +411,27 @@ function getPrismaIndexDefinition(property) {
407
411
  function prismaIndexMatches(existing, configured) {
408
412
  return existing.validFullColumns && existing.unique === (configured.unique ?? false) && existing.columns.length === configured.columns.length && existing.columns.every((column, position) => column === configured.columns[position]);
409
413
  }
414
+ /**
415
+ * The models an existing schema declares, read the way the comparison wants
416
+ * them: each model by name, each scalar or enum field by name. A relation
417
+ * field is not a column and is skipped.
418
+ */
419
+ function introspectPrismaSchema(schemaPrisma) {
420
+ const models = getSchema(schemaPrisma).list.filter((block) => block.type === "model");
421
+ const modelNames = new Set(models.map((model) => model.name));
422
+ return models.map((model) => ({
423
+ name: model.name,
424
+ columns: model.properties.flatMap((property) => {
425
+ if (property.type !== "field") return [];
426
+ if (typeof property.fieldType === "string" && modelNames.has(property.fieldType) || property.attributes?.some((attribute) => attribute.name === "relation")) return [];
427
+ return [{
428
+ name: property.name,
429
+ nullable: property.optional === true,
430
+ hasDefault: property.attributes?.some((attribute) => attribute.name === "default" || attribute.name === "updatedAt") ?? false
431
+ }];
432
+ })
433
+ }));
434
+ }
410
435
  const generatePrismaSchema = async ({ adapter, options, file }) => {
411
436
  const provider = adapter.options?.provider || "postgresql";
412
437
  const tables = getAuthTables(options);
@@ -450,6 +475,13 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
450
475
  if (urlIndex !== -1) datasource.properties.splice(urlIndex, 1);
451
476
  }
452
477
  });
478
+ const expectedModels = {};
479
+ for (const table in tables) {
480
+ if (isMigrationDisabled(table)) continue;
481
+ const modelName = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table));
482
+ const entry = expectedModels[modelName] ??= { fields: {} };
483
+ for (const [field, attr] of Object.entries(tables[table]?.fields ?? {})) entry.fields[attr.fieldName || field] = attr;
484
+ }
453
485
  const manyToManyRelations = /* @__PURE__ */ new Map();
454
486
  for (const table in tables) {
455
487
  if (isMigrationDisabled(table)) continue;
@@ -695,10 +727,12 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
695
727
  }
696
728
  });
697
729
  const schemaChanged = schema.trim() !== schemaPrisma.trim();
730
+ const schemaProblems = schemaPrismaExist ? diffSchema(expectedModels, introspectPrismaSchema(schemaPrisma)).filter((finding) => finding.kind === "unexpected-required-column").map((finding) => formatSchemaFinding(finding, "prisma")) : [];
698
731
  return {
699
732
  code: schemaChanged ? schema : "",
700
733
  fileName: filePath,
701
- overwrite: schemaPrismaExist && schemaChanged
734
+ overwrite: schemaPrismaExist && schemaChanged,
735
+ schemaProblems
702
736
  };
703
737
  };
704
738
  const getNewPrisma = (provider, cwd) => {
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  import { APIError, BetterAuthError } from "@better-auth/core/error";
3
4
  import { Command } from "commander";
4
5
  import { exec, execSync, spawn } from "node:child_process";
@@ -20,13 +21,13 @@ import { createPathsMatcher, getTsconfig, parseTsconfig } from "get-tsconfig";
20
21
  import fs$1 from "node:fs/promises";
21
22
  import { createTelemetry, getTelemetryAuthConfig } from "@better-auth/telemetry";
22
23
  import { getAdapter } from "better-auth/db/adapter";
23
- import { getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, resolveDatabaseSchemaIndexes } from "@better-auth/core/db/internal";
24
+ import { diffSchema, formatSchemaFinding, getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, resolveDatabaseSchemaIndexes } from "@better-auth/core/db/internal";
24
25
  import { capitalizeFirstLetter, toSnakeCase } from "@better-auth/core/utils/string";
25
26
  import { initGetFieldName, initGetModelName } from "better-auth/adapters";
26
27
  import { getAuthTables } from "better-auth/db";
27
28
  import prettier, { format } from "prettier";
28
29
  import { UnsafeMigrationError, getMigrations } from "better-auth/db/migration";
29
- import { produceSchema } from "@mrleebo/prisma-ast";
30
+ import { getSchema, produceSchema } from "@mrleebo/prisma-ast";
30
31
  import Crypto from "node:crypto";
31
32
  import open from "open";
32
33
  import { env } from "@better-auth/core/env";
@@ -1679,12 +1680,13 @@ function commentBanner(unsafeChanges) {
1679
1680
  return lines.join("\n");
1680
1681
  }
1681
1682
  const generateKyselySchema = async ({ options, file }) => {
1682
- const { compileMigrations, unsafeChanges } = await getMigrations(options, { throwOnUnsafe: false });
1683
+ const { compileMigrations, unsafeChanges, schemaProblems } = await getMigrations(options, { throwOnUnsafe: false });
1683
1684
  const migrations = await compileMigrations();
1684
1685
  const code = migrations.trim() === ";" ? "" : migrations;
1685
1686
  return {
1686
1687
  code: unsafeChanges.length ? `${commentBanner(unsafeChanges)}${code}` : code,
1687
1688
  unsafeChanges,
1689
+ schemaProblems,
1688
1690
  fileName: file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql`
1689
1691
  };
1690
1692
  };
@@ -1721,10 +1723,52 @@ const spawnCommand = (cmd, cwd = process.cwd()) => new Promise((resolve, reject)
1721
1723
  });
1722
1724
  //#endregion
1723
1725
  //#region src/utils/get-package-info.ts
1726
+ const packageManifestSchema = z.object({
1727
+ name: z.string(),
1728
+ version: z.string()
1729
+ });
1724
1730
  function getPackageInfo(cwd) {
1725
1731
  const packageJsonPath = cwd ? path.join(cwd, "package.json") : path.join("package.json");
1726
1732
  return JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1727
1733
  }
1734
+ function readPackageVersion(packageJsonPath, expectedPackageName) {
1735
+ if (!existsSync(packageJsonPath)) return null;
1736
+ try {
1737
+ const result = packageManifestSchema.safeParse(JSON.parse(readFileSync(packageJsonPath, "utf-8")));
1738
+ return result.success && result.data.name === expectedPackageName ? result.data.version : null;
1739
+ } catch {
1740
+ return null;
1741
+ }
1742
+ }
1743
+ function findPackageVersion(resolvedPath, packageName) {
1744
+ let currentDirectory = path.dirname(resolvedPath);
1745
+ const rootDirectory = path.parse(currentDirectory).root;
1746
+ while (true) {
1747
+ const version = readPackageVersion(path.join(currentDirectory, "package.json"), packageName);
1748
+ if (version) return version;
1749
+ if (currentDirectory === rootDirectory) return null;
1750
+ currentDirectory = path.dirname(currentDirectory);
1751
+ }
1752
+ }
1753
+ function findPackageVersionInNodeModules(packageName, projectDirectory) {
1754
+ let currentDirectory = path.resolve(projectDirectory);
1755
+ const rootDirectory = path.parse(currentDirectory).root;
1756
+ const packagePath = packageName.split("/");
1757
+ while (true) {
1758
+ const version = readPackageVersion(path.join(currentDirectory, "node_modules", ...packagePath, "package.json"), packageName);
1759
+ if (version) return version;
1760
+ if (currentDirectory === rootDirectory) return null;
1761
+ currentDirectory = path.dirname(currentDirectory);
1762
+ }
1763
+ }
1764
+ function getInstalledPackageVersion(packageName, projectDirectory) {
1765
+ const resolve = createRequire(path.join(path.resolve(projectDirectory), "package.json")).resolve;
1766
+ for (const specifier of [`${packageName}/package.json`, packageName]) try {
1767
+ const version = findPackageVersion(resolve(specifier), packageName);
1768
+ if (version) return version;
1769
+ } catch {}
1770
+ return findPackageVersionInNodeModules(packageName, projectDirectory);
1771
+ }
1728
1772
  function getPrismaVersion(cwd) {
1729
1773
  try {
1730
1774
  const packageInfo = getPackageInfo(cwd);
@@ -1838,6 +1882,27 @@ function getPrismaIndexDefinition(property) {
1838
1882
  function prismaIndexMatches(existing, configured) {
1839
1883
  return existing.validFullColumns && existing.unique === (configured.unique ?? false) && existing.columns.length === configured.columns.length && existing.columns.every((column, position) => column === configured.columns[position]);
1840
1884
  }
1885
+ /**
1886
+ * The models an existing schema declares, read the way the comparison wants
1887
+ * them: each model by name, each scalar or enum field by name. A relation
1888
+ * field is not a column and is skipped.
1889
+ */
1890
+ function introspectPrismaSchema(schemaPrisma) {
1891
+ const models = getSchema(schemaPrisma).list.filter((block) => block.type === "model");
1892
+ const modelNames = new Set(models.map((model) => model.name));
1893
+ return models.map((model) => ({
1894
+ name: model.name,
1895
+ columns: model.properties.flatMap((property) => {
1896
+ if (property.type !== "field") return [];
1897
+ if (typeof property.fieldType === "string" && modelNames.has(property.fieldType) || property.attributes?.some((attribute) => attribute.name === "relation")) return [];
1898
+ return [{
1899
+ name: property.name,
1900
+ nullable: property.optional === true,
1901
+ hasDefault: property.attributes?.some((attribute) => attribute.name === "default" || attribute.name === "updatedAt") ?? false
1902
+ }];
1903
+ })
1904
+ }));
1905
+ }
1841
1906
  const generatePrismaSchema = async ({ adapter, options, file }) => {
1842
1907
  const provider = adapter.options?.provider || "postgresql";
1843
1908
  const tables = getAuthTables(options);
@@ -1881,6 +1946,13 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
1881
1946
  if (urlIndex !== -1) datasource.properties.splice(urlIndex, 1);
1882
1947
  }
1883
1948
  });
1949
+ const expectedModels = {};
1950
+ for (const table in tables) {
1951
+ if (isMigrationDisabled(table)) continue;
1952
+ const modelName = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table));
1953
+ const entry = expectedModels[modelName] ??= { fields: {} };
1954
+ for (const [field, attr] of Object.entries(tables[table]?.fields ?? {})) entry.fields[attr.fieldName || field] = attr;
1955
+ }
1884
1956
  const manyToManyRelations = /* @__PURE__ */ new Map();
1885
1957
  for (const table in tables) {
1886
1958
  if (isMigrationDisabled(table)) continue;
@@ -2126,10 +2198,12 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
2126
2198
  }
2127
2199
  });
2128
2200
  const schemaChanged = schema.trim() !== schemaPrisma.trim();
2201
+ const schemaProblems = schemaPrismaExist ? diffSchema(expectedModels, introspectPrismaSchema(schemaPrisma)).filter((finding) => finding.kind === "unexpected-required-column").map((finding) => formatSchemaFinding(finding, "prisma")) : [];
2129
2202
  return {
2130
2203
  code: schemaChanged ? schema : "",
2131
2204
  fileName: filePath,
2132
- overwrite: schemaPrismaExist && schemaChanged
2205
+ overwrite: schemaPrismaExist && schemaChanged,
2206
+ schemaProblems
2133
2207
  };
2134
2208
  };
2135
2209
  const getNewPrisma = (provider, cwd) => {
@@ -2291,6 +2365,10 @@ async function generateAction(opts) {
2291
2365
  options: config
2292
2366
  });
2293
2367
  spinner.stop();
2368
+ if (schema.schemaProblems?.length) {
2369
+ console.warn(chalk.red.bold(`⚠ ${schema.schemaProblems.length} ${schema.schemaProblems.length === 1 ? "column rejects" : "columns reject"} every insert Better Auth makes. No migration fixes this.`));
2370
+ for (const problem of schema.schemaProblems) console.warn(chalk.red(`-> ${problem}`));
2371
+ }
2294
2372
  if (schema.unsafeChanges?.length) {
2295
2373
  console.warn(chalk.red.bold(`⚠ ${schema.unsafeChanges.length} ${schema.unsafeChanges.length === 1 ? "change in this schema corrupts" : "changes in this schema corrupt"} a populated database.`));
2296
2374
  for (const change of schema.unsafeChanges) console.warn(chalk.red(`-> ${change}`));
@@ -2393,6 +2471,16 @@ async function generateAction(opts) {
2393
2471
  const generate = new Command("generate").option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("--config <config>", "the path to the configuration file. defaults to the first configuration file found.").option("--output <output>", "the file to output to the generated schema").option("--adapter <adapter>", "specify the adapter type (e.g., prisma, drizzle, kysely) without requiring a configured adapter").option("--dialect <dialect>", "specify the database dialect/provider (e.g., postgresql, mysql, sqlite). For drizzle, postgresql maps to 'pg'").option("-y, --yes", "automatically answer yes to all prompts", false).option("--y", "(deprecated) same as --yes", false).action(generateAction);
2394
2472
  //#endregion
2395
2473
  //#region src/commands/info.ts
2474
+ function resolveReportedDependencies(projectRoot, dependencies, reportedDependencies) {
2475
+ return reportedDependencies.flatMap(({ name, packageName }) => {
2476
+ const declaredVersion = dependencies[packageName];
2477
+ if (!declaredVersion) return [];
2478
+ return [{
2479
+ name,
2480
+ version: getInstalledPackageVersion(packageName, projectRoot) ?? declaredVersion
2481
+ }];
2482
+ });
2483
+ }
2396
2484
  function getSystemInfo() {
2397
2485
  const platform = os.platform();
2398
2486
  const arch = os.arch();
@@ -2449,29 +2537,63 @@ function getFrameworkInfo(projectRoot) {
2449
2537
  if (!existsSync(packageJsonPath)) return null;
2450
2538
  try {
2451
2539
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
2452
- const deps = {
2540
+ const installedFrameworks = resolveReportedDependencies(projectRoot, {
2453
2541
  ...packageJson.dependencies,
2454
2542
  ...packageJson.devDependencies
2455
- };
2456
- const frameworks = {
2457
- next: deps["next"],
2458
- react: deps["react"],
2459
- vue: deps["vue"],
2460
- nuxt: deps["nuxt"],
2461
- svelte: deps["svelte"],
2462
- "@sveltejs/kit": deps["@sveltejs/kit"],
2463
- express: deps["express"],
2464
- fastify: deps["fastify"],
2465
- hono: deps["hono"],
2466
- "react-router": deps["react-router"],
2467
- astro: deps["astro"],
2468
- solid: deps["solid-js"],
2469
- qwik: deps["@builder.io/qwik"]
2470
- };
2471
- const installedFrameworks = Object.entries(frameworks).filter(([_, version]) => version).map(([name, version]) => ({
2472
- name,
2473
- version
2474
- }));
2543
+ }, [
2544
+ {
2545
+ name: "next",
2546
+ packageName: "next"
2547
+ },
2548
+ {
2549
+ name: "react",
2550
+ packageName: "react"
2551
+ },
2552
+ {
2553
+ name: "vue",
2554
+ packageName: "vue"
2555
+ },
2556
+ {
2557
+ name: "nuxt",
2558
+ packageName: "nuxt"
2559
+ },
2560
+ {
2561
+ name: "svelte",
2562
+ packageName: "svelte"
2563
+ },
2564
+ {
2565
+ name: "@sveltejs/kit",
2566
+ packageName: "@sveltejs/kit"
2567
+ },
2568
+ {
2569
+ name: "express",
2570
+ packageName: "express"
2571
+ },
2572
+ {
2573
+ name: "fastify",
2574
+ packageName: "fastify"
2575
+ },
2576
+ {
2577
+ name: "hono",
2578
+ packageName: "hono"
2579
+ },
2580
+ {
2581
+ name: "react-router",
2582
+ packageName: "react-router"
2583
+ },
2584
+ {
2585
+ name: "astro",
2586
+ packageName: "astro"
2587
+ },
2588
+ {
2589
+ name: "solid",
2590
+ packageName: "solid-js"
2591
+ },
2592
+ {
2593
+ name: "qwik",
2594
+ packageName: "@builder.io/qwik"
2595
+ }
2596
+ ]);
2475
2597
  return installedFrameworks.length > 0 ? installedFrameworks : null;
2476
2598
  } catch {
2477
2599
  return null;
@@ -2482,29 +2604,63 @@ function getDatabaseInfo(projectRoot) {
2482
2604
  if (!existsSync(packageJsonPath)) return null;
2483
2605
  try {
2484
2606
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
2485
- const deps = {
2607
+ const installedDatabases = resolveReportedDependencies(projectRoot, {
2486
2608
  ...packageJson.dependencies,
2487
2609
  ...packageJson.devDependencies
2488
- };
2489
- const databases = {
2490
- "better-sqlite3": deps["better-sqlite3"],
2491
- "@libsql/client": deps["@libsql/client"],
2492
- "@libsql/kysely-libsql": deps["@libsql/kysely-libsql"],
2493
- mysql2: deps["mysql2"],
2494
- pg: deps["pg"],
2495
- postgres: deps["postgres"],
2496
- "@prisma/client": deps["@prisma/client"],
2497
- drizzle: deps["drizzle-orm"],
2498
- kysely: deps["kysely"],
2499
- mongodb: deps["mongodb"],
2500
- "@neondatabase/serverless": deps["@neondatabase/serverless"],
2501
- "@vercel/postgres": deps["@vercel/postgres"],
2502
- "@planetscale/database": deps["@planetscale/database"]
2503
- };
2504
- const installedDatabases = Object.entries(databases).filter(([_, version]) => version).map(([name, version]) => ({
2505
- name,
2506
- version
2507
- }));
2610
+ }, [
2611
+ {
2612
+ name: "better-sqlite3",
2613
+ packageName: "better-sqlite3"
2614
+ },
2615
+ {
2616
+ name: "@libsql/client",
2617
+ packageName: "@libsql/client"
2618
+ },
2619
+ {
2620
+ name: "@libsql/kysely-libsql",
2621
+ packageName: "@libsql/kysely-libsql"
2622
+ },
2623
+ {
2624
+ name: "mysql2",
2625
+ packageName: "mysql2"
2626
+ },
2627
+ {
2628
+ name: "pg",
2629
+ packageName: "pg"
2630
+ },
2631
+ {
2632
+ name: "postgres",
2633
+ packageName: "postgres"
2634
+ },
2635
+ {
2636
+ name: "@prisma/client",
2637
+ packageName: "@prisma/client"
2638
+ },
2639
+ {
2640
+ name: "drizzle",
2641
+ packageName: "drizzle-orm"
2642
+ },
2643
+ {
2644
+ name: "kysely",
2645
+ packageName: "kysely"
2646
+ },
2647
+ {
2648
+ name: "mongodb",
2649
+ packageName: "mongodb"
2650
+ },
2651
+ {
2652
+ name: "@neondatabase/serverless",
2653
+ packageName: "@neondatabase/serverless"
2654
+ },
2655
+ {
2656
+ name: "@vercel/postgres",
2657
+ packageName: "@vercel/postgres"
2658
+ },
2659
+ {
2660
+ name: "@planetscale/database",
2661
+ packageName: "@planetscale/database"
2662
+ }
2663
+ ]);
2508
2664
  return installedDatabases.length > 0 ? installedDatabases : null;
2509
2665
  } catch {
2510
2666
  return null;
@@ -2598,6 +2754,12 @@ function sanitizeBetterAuthConfig(config) {
2598
2754
  return redactSensitive(sanitized);
2599
2755
  }
2600
2756
  async function getBetterAuthInfo(projectRoot, configPath, suppressLogs = false) {
2757
+ let betterAuthVersion = "Unknown";
2758
+ try {
2759
+ const packageInfo = getPackageInfo(projectRoot);
2760
+ const declaredVersion = packageInfo.dependencies?.["better-auth"] || packageInfo.devDependencies?.["better-auth"] || packageInfo.peerDependencies?.["better-auth"] || packageInfo.optionalDependencies?.["better-auth"];
2761
+ if (declaredVersion) betterAuthVersion = getInstalledPackageVersion("better-auth", projectRoot) ?? declaredVersion;
2762
+ } catch {}
2601
2763
  try {
2602
2764
  const originalLog = console.log;
2603
2765
  const originalWarn = console.warn;
@@ -2613,9 +2775,8 @@ async function getBetterAuthInfo(projectRoot, configPath, suppressLogs = false)
2613
2775
  configPath,
2614
2776
  shouldThrowOnError: true
2615
2777
  });
2616
- const packageInfo = await getPackageInfo();
2617
2778
  return {
2618
- version: packageInfo.dependencies?.["better-auth"] || packageInfo.devDependencies?.["better-auth"] || packageInfo.peerDependencies?.["better-auth"] || packageInfo.optionalDependencies?.["better-auth"] || "Unknown",
2779
+ version: betterAuthVersion,
2619
2780
  config: sanitizeBetterAuthConfig(config)
2620
2781
  };
2621
2782
  } finally {
@@ -2627,7 +2788,7 @@ async function getBetterAuthInfo(projectRoot, configPath, suppressLogs = false)
2627
2788
  }
2628
2789
  } catch (error) {
2629
2790
  return {
2630
- version: "Unknown",
2791
+ version: betterAuthVersion,
2631
2792
  config: null,
2632
2793
  error: error instanceof Error ? error.message : "Failed to load Better Auth config"
2633
2794
  };
@@ -2704,13 +2865,11 @@ const info = new Command("info").description("Display system and Better Auth con
2704
2865
  console.log(formatOutput(databases, 2));
2705
2866
  }
2706
2867
  console.log(chalk.bold.white("\nšŸ” Better Auth:"));
2868
+ console.log(` ${chalk.cyan("Version")}: ${betterAuthInfo.version}`);
2707
2869
  if (betterAuthInfo.error) console.log(` ${chalk.red("Error:")} ${betterAuthInfo.error}`);
2708
- else {
2709
- console.log(` ${chalk.cyan("Version")}: ${betterAuthInfo.version}`);
2710
- if (betterAuthInfo.config) {
2711
- console.log(` ${chalk.cyan("Configuration")}:`);
2712
- console.log(formatOutput(betterAuthInfo.config, 4));
2713
- }
2870
+ else if (betterAuthInfo.config) {
2871
+ console.log(` ${chalk.cyan("Configuration")}:`);
2872
+ console.log(formatOutput(betterAuthInfo.config, 4));
2714
2873
  }
2715
2874
  console.log(chalk.gray("\n" + "=".repeat(50)));
2716
2875
  console.log(chalk.gray("\nšŸ’” Tip: Use --json flag for JSON output"));
@@ -2960,11 +3119,14 @@ function installDependencies({ dependencies, packageManager, cwd, type = "prod",
2960
3119
  const flag = flagMap?.[type];
2961
3120
  if (flag) flags.push(flag);
2962
3121
  }
2963
- const command = `${installCommand}${flags.length > 0 ? ` ${flags.join(" ")}` : ""} ${Array.isArray(dependencies) ? dependencies.join(" ") : dependencies}`;
3122
+ const dependencyList = Array.isArray(dependencies) ? dependencies : [dependencies];
3123
+ if (dependencyList.length === 0) return Promise.resolve(true);
3124
+ const command = `${installCommand}${flags.length > 0 ? ` ${flags.join(" ")}` : ""} ${dependencyList.join(" ")}`;
2964
3125
  return new Promise((resolve, reject) => {
2965
3126
  exec(command, { cwd }, (error, stdout, stderr) => {
2966
3127
  if (error) {
2967
- reject(new Error(stderr));
3128
+ const message = stderr.trim() || stdout.trim() || error.message;
3129
+ reject(new Error(message, { cause: error }));
2968
3130
  return;
2969
3131
  }
2970
3132
  resolve(true);
@@ -2973,7 +3135,7 @@ function installDependencies({ dependencies, packageManager, cwd, type = "prod",
2973
3135
  }
2974
3136
  //#endregion
2975
3137
  //#region src/version.ts
2976
- const cliVersion = "1.7.2";
3138
+ const cliVersion = "1.7.4";
2977
3139
  //#endregion
2978
3140
  //#region src/commands/init/configs/frameworks.config.ts
2979
3141
  const FRAMEWORKS = [
@@ -3183,6 +3345,7 @@ export const Route = createFileRoute('/api/auth/$')({
3183
3345
  const SOCIAL_PROVIDERS = [
3184
3346
  "apple",
3185
3347
  "atlassian",
3348
+ "cloudflare",
3186
3349
  "cognito",
3187
3350
  "discord",
3188
3351
  "dropbox",
@@ -3233,6 +3396,13 @@ const SOCIAL_PROVIDER_CONFIGS = {
3233
3396
  name: "clientSecret",
3234
3397
  envVar: "ATLASSIAN_CLIENT_SECRET"
3235
3398
  }] },
3399
+ cloudflare: { options: [{
3400
+ name: "clientId",
3401
+ envVar: "CLOUDFLARE_CLIENT_ID"
3402
+ }, {
3403
+ name: "clientSecret",
3404
+ envVar: "CLOUDFLARE_CLIENT_SECRET"
3405
+ }] },
3236
3406
  cognito: { options: [
3237
3407
  {
3238
3408
  name: "clientId",
@@ -7156,6 +7326,7 @@ const logout = new Command("logout").description("Logout from Better Auth Infras
7156
7326
  const REMOTE_MCP_URL = "https://mcp.better-auth.com/mcp";
7157
7327
  async function mcpAction(options) {
7158
7328
  if (options.cursor) await handleCursorAction();
7329
+ else if (options.codex) handleCodexAction();
7159
7330
  else if (options.claudeCode) handleClaudeCodeAction();
7160
7331
  else if (options.openCode) handleOpenCodeAction();
7161
7332
  else if (options.manual) handleManualAction();
@@ -7191,6 +7362,20 @@ async function handleCursorAction() {
7191
7362
  console.log(chalk.gray("• You can now use Better Auth features directly in Cursor"));
7192
7363
  console.log(chalk.gray("• Try: \"Set up Better Auth with Google login\" or \"Help me debug my auth\""));
7193
7364
  }
7365
+ function handleCodexAction() {
7366
+ console.log(chalk.bold.blue("šŸ¤– Adding Better Auth MCP to Codex..."));
7367
+ const command = `codex mcp add better-auth --url ${REMOTE_MCP_URL}`;
7368
+ try {
7369
+ execSync(command, { stdio: "inherit" });
7370
+ console.log(chalk.green("\nāœ“ Codex MCP configured!"));
7371
+ } catch {
7372
+ console.log(chalk.yellow("\n⚠ Could not automatically add to Codex. Please run this command manually:"));
7373
+ console.log(chalk.cyan(command));
7374
+ }
7375
+ console.log(chalk.bold.white("\n✨ Next Steps:"));
7376
+ console.log(chalk.gray("• Once configured, ChatGPT desktop, Codex CLI, and the IDE extension share this MCP server"));
7377
+ console.log(chalk.gray("• Run `codex mcp list` to verify the connection"));
7378
+ }
7194
7379
  function handleClaudeCodeAction() {
7195
7380
  console.log(chalk.bold.blue("šŸ¤– Adding Better Auth MCP to Claude Code..."));
7196
7381
  const command = `claude mcp add --transport http better-auth ${REMOTE_MCP_URL}`;
@@ -7272,6 +7457,7 @@ function showAllOptions() {
7272
7457
  console.log();
7273
7458
  console.log(chalk.bold.white("MCP Clients:"));
7274
7459
  console.log(chalk.cyan(" --cursor ") + chalk.gray("Add to Cursor"));
7460
+ console.log(chalk.cyan(" --codex ") + chalk.gray("Add to Codex"));
7275
7461
  console.log(chalk.cyan(" --claude-code ") + chalk.gray("Add to Claude Code"));
7276
7462
  console.log(chalk.cyan(" --open-code ") + chalk.gray("Add to Open Code"));
7277
7463
  console.log(chalk.cyan(" --manual ") + chalk.gray("Manual configuration"));
@@ -7280,7 +7466,7 @@ function showAllOptions() {
7280
7466
  console.log(chalk.gray(" • ") + chalk.white("better-auth") + chalk.gray(" - Search documentation, code examples, setup assistance"));
7281
7467
  console.log();
7282
7468
  }
7283
- const mcp = new Command("mcp").description("Add Better Auth MCP server to MCP Clients").option("--cursor", "Automatically open Cursor with the MCP configuration").option("--claude-code", "Show Claude Code MCP configuration command").option("--open-code", "Show Open Code MCP configuration").option("--manual", "Show manual MCP configuration for mcp.json").action(mcpAction);
7469
+ const mcp = new Command("mcp").description("Add Better Auth MCP server to MCP Clients").option("--cursor", "Automatically open Cursor with the MCP configuration").option("--codex", "Add the MCP server to Codex").option("--claude-code", "Show Claude Code MCP configuration command").option("--open-code", "Show Open Code MCP configuration").option("--manual", "Show manual MCP configuration for mcp.json").action(mcpAction);
7284
7470
  //#endregion
7285
7471
  //#region src/commands/migrate.ts
7286
7472
  /** @internal */
@@ -7372,6 +7558,21 @@ async function migrateAction(opts) {
7372
7558
  } catch {}
7373
7559
  process.exit(1);
7374
7560
  }
7561
+ if (plan.schemaProblems.length) {
7562
+ spinner.stop();
7563
+ console.error(chalk.red.bold(`The database has ${plan.schemaProblems.length} ${plan.schemaProblems.length === 1 ? "column" : "columns"} Better Auth cannot write to. Nothing ran.`));
7564
+ for (const problem of plan.schemaProblems) console.error(chalk.red(`-> ${problem}`));
7565
+ try {
7566
+ await (await createTelemetry(config)).publish({
7567
+ type: "cli_migrate",
7568
+ payload: {
7569
+ outcome: "schema_problem",
7570
+ config: await getTelemetryAuthConfig(config)
7571
+ }
7572
+ });
7573
+ } catch {}
7574
+ process.exit(1);
7575
+ }
7375
7576
  const { toBeAdded, toBeAddedIndexes, toBeCreated, runMigrations } = plan;
7376
7577
  if (!toBeAdded.length && !toBeAddedIndexes.length && !toBeCreated.length) {
7377
7578
  spinner.stop();
@@ -7439,8 +7640,8 @@ const generateSecret = new Command("secret").action(() => {
7439
7640
  ${chalk.gray("# Auth Secret") + chalk.green(`\nBETTER_AUTH_SECRET=${secret}`)}`);
7440
7641
  });
7441
7642
  //#endregion
7442
- //#region src/commands/upgrade.ts
7443
- const fixedReleaseGroup = [[
7643
+ //#region ../../.changeset/config.json
7644
+ var fixed = [[
7444
7645
  "better-auth",
7445
7646
  "@better-auth/core",
7446
7647
  "auth",
@@ -7463,7 +7664,11 @@ const fixedReleaseGroup = [[
7463
7664
  "@better-auth/stripe",
7464
7665
  "@better-auth/telemetry",
7465
7666
  "@better-auth/test-utils"
7466
- ]].find((group) => group.includes("better-auth"));
7667
+ ]];
7668
+ //#endregion
7669
+ //#region src/commands/upgrade.ts
7670
+ const dependencyMapSchema = z.record(z.string(), z.string().catch("")).catch({});
7671
+ const fixedReleaseGroup = fixed.find((group) => group.includes("better-auth"));
7467
7672
  if (!fixedReleaseGroup) throw new Error("The Better Auth fixed release group is not configured.");
7468
7673
  const SYNCHRONIZED_BETTER_AUTH_PACKAGES = new Set(fixedReleaseGroup.filter((name) => name !== "auth"));
7469
7674
  function isSynchronizedBetterAuthPackage(name) {
@@ -7487,8 +7692,8 @@ async function upgradeAction(opts) {
7487
7692
  console.error(`Could not read package.json in "${cwd}". Make sure you are in a project directory.`);
7488
7693
  process.exit(1);
7489
7694
  }
7490
- const deps = packageJson.dependencies ?? {};
7491
- const devDeps = packageJson.devDependencies ?? {};
7695
+ const deps = dependencyMapSchema.parse(packageJson.dependencies);
7696
+ const devDeps = dependencyMapSchema.parse(packageJson.devDependencies);
7492
7697
  const candidates = [];
7493
7698
  for (const [name, version] of Object.entries(deps)) if (isSynchronizedBetterAuthPackage(name) && !version.startsWith("workspace:")) candidates.push({
7494
7699
  name,
@@ -7506,8 +7711,14 @@ async function upgradeAction(opts) {
7506
7711
  }
7507
7712
  const spinner = yoctoSpinner({ text: "checking for updates..." }).start();
7508
7713
  const upgrades = [];
7714
+ const warnings = [];
7509
7715
  for (const { name, current, depType } of candidates) {
7510
- const currentVersion = semver.minVersion(current);
7716
+ const currentRange = semver.validRange(current);
7717
+ if (!currentRange) {
7718
+ warnings.push(`Skipped ${name} (${current}). Automatic upgrades require a semver range.`);
7719
+ continue;
7720
+ }
7721
+ const currentVersion = semver.minVersion(currentRange);
7511
7722
  if (currentVersion && semver.lt(currentVersion, cliVersion)) upgrades.push({
7512
7723
  name,
7513
7724
  current,
@@ -7516,8 +7727,9 @@ async function upgradeAction(opts) {
7516
7727
  });
7517
7728
  }
7518
7729
  spinner.stop();
7730
+ for (const warning of warnings) console.warn(chalk.yellow(warning));
7519
7731
  if (upgrades.length === 0) {
7520
- console.log("All better-auth packages are up to date.");
7732
+ console.log(warnings.length > 0 ? "No supported Better Auth package upgrades were found." : "All better-auth packages are up to date.");
7521
7733
  return;
7522
7734
  }
7523
7735
  console.log(`\nThe following packages can be upgraded:\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auth",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
4
4
  "description": "The CLI for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -63,10 +63,10 @@
63
63
  "prompts": "^2.4.2",
64
64
  "semver": "^7.8.4",
65
65
  "yocto-spinner": "^1.2.0",
66
- "zod": "^4.3.6",
67
- "@better-auth/core": "1.7.2",
68
- "@better-auth/telemetry": "1.7.2",
69
- "better-auth": "1.7.2"
66
+ "zod": "^4.5.4",
67
+ "@better-auth/core": "1.7.4",
68
+ "@better-auth/telemetry": "1.7.4",
69
+ "better-auth": "1.7.4"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/better-sqlite3": "^7.6.13",
@@ -78,7 +78,7 @@
78
78
  "tsx": "^4.21.0",
79
79
  "type-fest": "^5.7.0",
80
80
  "typescript": "^6.0.3",
81
- "@better-auth/passkey": "1.7.2"
81
+ "@better-auth/passkey": "1.7.4"
82
82
  },
83
83
  "scripts": {
84
84
  "build": "tsdown",