@better-auth/core 1.7.1 → 1.7.3
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/index.d.mts +3 -0
- package/dist/context/endpoint-context.d.mts +19 -5
- package/dist/context/endpoint-context.mjs +35 -16
- package/dist/context/global.mjs +5 -2
- package/dist/context/index.d.mts +2 -2
- package/dist/context/index.mjs +2 -2
- package/dist/context/transaction.mjs +3 -0
- package/dist/db/adapter/atomic-fallback.mjs +134 -0
- package/dist/db/adapter/factory.mjs +22 -4
- package/dist/db/adapter/index.d.mts +15 -11
- package/dist/db/get-tables.mjs +1 -9
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +2 -2
- package/dist/db/internal.d.mts +3 -1
- package/dist/db/internal.mjs +3 -1
- package/dist/db/schema/account.d.mts +2 -13
- package/dist/db/schema/account.mjs +1 -19
- package/dist/db/schema-check.d.mts +48 -0
- package/dist/db/schema-check.mjs +80 -0
- package/dist/db/schema-diff.d.mts +104 -0
- package/dist/db/schema-diff.mjs +154 -0
- package/dist/env/logger.mjs +16 -1
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/oauth-provider.d.mts +0 -10
- package/dist/oauth2/token-endpoint-auth.d.mts +26 -2
- package/dist/oauth2/token-endpoint-auth.mjs +11 -0
- package/dist/social-providers/apple.d.mts +0 -1
- package/dist/social-providers/apple.mjs +0 -1
- package/dist/social-providers/cloudflare.d.mts +132 -0
- package/dist/social-providers/cloudflare.mjs +85 -0
- package/dist/social-providers/cognito.d.mts +0 -1
- package/dist/social-providers/cognito.mjs +0 -1
- package/dist/social-providers/facebook.d.mts +0 -1
- package/dist/social-providers/facebook.mjs +0 -1
- package/dist/social-providers/google.d.mts +0 -1
- package/dist/social-providers/google.mjs +0 -1
- package/dist/social-providers/index.d.mts +53 -21
- package/dist/social-providers/index.mjs +3 -1
- package/dist/social-providers/line.d.mts +0 -1
- package/dist/social-providers/line.mjs +0 -1
- package/dist/social-providers/microsoft-entra-id.d.mts +0 -3
- package/dist/social-providers/microsoft-entra-id.mjs +0 -1
- package/dist/social-providers/paybin.d.mts +0 -1
- package/dist/social-providers/paybin.mjs +0 -1
- package/dist/social-providers/paypal.d.mts +3 -11
- package/dist/social-providers/paypal.mjs +20 -47
- package/dist/social-providers/reddit.mjs +22 -23
- package/dist/social-providers/roblox.mjs +5 -1
- package/dist/social-providers/tiktok.d.mts +1 -0
- package/dist/social-providers/tiktok.mjs +19 -10
- package/dist/social-providers/twitter.mjs +5 -1
- package/dist/social-providers/wechat.mjs +6 -1
- package/dist/types/context.d.mts +11 -0
- package/dist/types/init-options.d.mts +11 -0
- package/dist/utils/ip.mjs +11 -9
- package/dist/utils/url.d.mts +10 -1
- package/dist/utils/url.mjs +21 -1
- package/package.json +3 -3
- package/src/context/endpoint-context.ts +46 -21
- package/src/context/global.ts +7 -0
- package/src/context/index.ts +2 -0
- package/src/context/transaction.ts +5 -0
- package/src/db/adapter/atomic-fallback.ts +237 -0
- package/src/db/adapter/factory.ts +33 -17
- package/src/db/adapter/index.ts +15 -11
- package/src/db/get-tables.ts +1 -14
- package/src/db/index.ts +0 -2
- package/src/db/internal.ts +19 -0
- package/src/db/schema/account.ts +3 -22
- package/src/db/schema/user.ts +1 -1
- package/src/db/schema-check.ts +107 -0
- package/src/db/schema-diff.ts +270 -0
- package/src/env/logger.ts +22 -1
- package/src/oauth2/index.ts +2 -0
- package/src/oauth2/oauth-provider.ts +0 -10
- package/src/oauth2/token-endpoint-auth.ts +39 -6
- package/src/social-providers/apple.ts +0 -1
- package/src/social-providers/cloudflare.ts +221 -0
- package/src/social-providers/cognito.ts +0 -1
- package/src/social-providers/facebook.ts +0 -1
- package/src/social-providers/google.ts +0 -1
- package/src/social-providers/index.ts +3 -0
- package/src/social-providers/line.ts +0 -1
- package/src/social-providers/microsoft-entra-id.ts +0 -1
- package/src/social-providers/paybin.ts +0 -1
- package/src/social-providers/paypal.ts +30 -71
- package/src/social-providers/reddit.ts +34 -37
- package/src/social-providers/roblox.ts +5 -3
- package/src/social-providers/tiktok.ts +25 -14
- package/src/social-providers/twitter.ts +8 -2
- package/src/social-providers/wechat.ts +6 -6
- package/src/types/context.ts +11 -0
- package/src/types/init-options.ts +11 -0
- package/src/utils/ip.ts +13 -9
- package/src/utils/url.ts +43 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { SchemaMismatchError } from "./schema-diff.mjs";
|
|
2
|
+
//#region src/db/schema-check.ts
|
|
3
|
+
/**
|
|
4
|
+
* Whether the adapter validates its schema. Enabled in every environment
|
|
5
|
+
* unless explicitly disabled.
|
|
6
|
+
*/
|
|
7
|
+
function checksSchema(options) {
|
|
8
|
+
return options.advanced?.database?.validateSchema !== false;
|
|
9
|
+
}
|
|
10
|
+
const schemaChecks = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
const schemaRevisions = /* @__PURE__ */ new WeakMap();
|
|
12
|
+
/** Invalidates cached checks after Better Auth changes this database's schema. */
|
|
13
|
+
function invalidateSchemaChecks(database) {
|
|
14
|
+
const revision = schemaRevisions.get(database);
|
|
15
|
+
if (revision) revision.value++;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Attaches a check to the adapter it verifies. The adapter object itself is
|
|
19
|
+
* left untouched, so this works for adapters Better Auth does not own.
|
|
20
|
+
*/
|
|
21
|
+
function registerSchemaCheck(adapter, check) {
|
|
22
|
+
schemaChecks.set(adapter, check);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The check registered for an adapter, if its store is checked at all.
|
|
26
|
+
*/
|
|
27
|
+
function schemaCheckFor(adapter) {
|
|
28
|
+
return schemaChecks.get(adapter);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Turns a schema comparison into a check shared by one adapter instance.
|
|
32
|
+
*
|
|
33
|
+
* The first call runs `find` and every concurrent call shares that promise. A
|
|
34
|
+
* clean result is cached until invalidation. A mismatch is kept as one
|
|
35
|
+
* {@link SchemaMismatchError} and rethrown on every later call without asking
|
|
36
|
+
* the store again, until a migration invalidates it. When a database identity is supplied,
|
|
37
|
+
* checks for that identity share its schema revision. Pending callers follow
|
|
38
|
+
* the new check if their revision is invalidated. A failure to reach
|
|
39
|
+
* the store is not kept, so the next call asks again.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const checkSchema = createSchemaCheck(
|
|
44
|
+
* () => findSchemaProblems(db, "postgres", expected),
|
|
45
|
+
* "database",
|
|
46
|
+
* );
|
|
47
|
+
* const pending = checkSchema();
|
|
48
|
+
* if (pending) await pending;
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
function createSchemaCheck(find, source, database) {
|
|
52
|
+
let revision = database ? schemaRevisions.get(database) : void 0;
|
|
53
|
+
if (database && !revision) {
|
|
54
|
+
revision = { value: 0 };
|
|
55
|
+
schemaRevisions.set(database, revision);
|
|
56
|
+
}
|
|
57
|
+
let checkedRevision = revision?.value;
|
|
58
|
+
let clean = false;
|
|
59
|
+
let verdict;
|
|
60
|
+
return function checkSchema() {
|
|
61
|
+
const currentRevision = revision?.value;
|
|
62
|
+
if (checkedRevision !== currentRevision) {
|
|
63
|
+
checkedRevision = currentRevision;
|
|
64
|
+
clean = false;
|
|
65
|
+
verdict = void 0;
|
|
66
|
+
}
|
|
67
|
+
if (clean) return;
|
|
68
|
+
return verdict ??= Promise.resolve().then(find).then((findings) => {
|
|
69
|
+
if (revision?.value !== currentRevision) return checkSchema();
|
|
70
|
+
if (findings.length) throw new SchemaMismatchError(findings, source);
|
|
71
|
+
if (checkedRevision === currentRevision) clean = true;
|
|
72
|
+
}, (error) => {
|
|
73
|
+
if (revision?.value !== currentRevision) return checkSchema();
|
|
74
|
+
if (checkedRevision === currentRevision) verdict = void 0;
|
|
75
|
+
throw error;
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { checksSchema, createSchemaCheck, invalidateSchemaChecks, registerSchemaCheck, schemaCheckFor };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { DBFieldAttribute } from "./type.mjs";
|
|
2
|
+
import { BetterAuthOptions } from "../types/init-options.mjs";
|
|
3
|
+
import { BetterAuthError } from "../error/index.mjs";
|
|
4
|
+
//#region src/db/schema-diff.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* A column as the database, or an ORM schema definition, reports it.
|
|
7
|
+
*/
|
|
8
|
+
interface IntrospectedColumn {
|
|
9
|
+
name: string;
|
|
10
|
+
nullable: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The store fills the column when an insert omits it.
|
|
13
|
+
*/
|
|
14
|
+
hasDefault: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A table as the database, or an ORM schema definition, reports it.
|
|
18
|
+
*/
|
|
19
|
+
interface IntrospectedTable {
|
|
20
|
+
name: string;
|
|
21
|
+
/**
|
|
22
|
+
* The schema the table lives in, when the store has schemas.
|
|
23
|
+
*/
|
|
24
|
+
schema?: string | undefined;
|
|
25
|
+
columns: IntrospectedColumn[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The tables Better Auth writes, keyed the way the store addresses them:
|
|
29
|
+
* physical table name, then physical column name. A table that manages its
|
|
30
|
+
* own storage is excluded from migrations and from this comparison.
|
|
31
|
+
*/
|
|
32
|
+
type ExpectedSchema = Record<string, {
|
|
33
|
+
fields: Record<string, DBFieldAttribute>;
|
|
34
|
+
idColumn?: string | undefined;
|
|
35
|
+
disableMigrations?: boolean | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* The schema the table is addressed in. Unset when the store has no
|
|
38
|
+
* schemas or the table is found by name alone.
|
|
39
|
+
*/
|
|
40
|
+
schema?: string | undefined;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* The tables this configuration writes, keyed the way the adapter addresses
|
|
44
|
+
* them. Tables that share a physical name are merged into one entry.
|
|
45
|
+
*/
|
|
46
|
+
declare function getExpectedSchema(options: BetterAuthOptions, {
|
|
47
|
+
usePlural
|
|
48
|
+
}?: {
|
|
49
|
+
usePlural?: boolean | undefined;
|
|
50
|
+
}): ExpectedSchema;
|
|
51
|
+
type SchemaFinding = {
|
|
52
|
+
kind: "missing-table";
|
|
53
|
+
table: string;
|
|
54
|
+
} | {
|
|
55
|
+
kind: "missing-column";
|
|
56
|
+
table: string;
|
|
57
|
+
column: string;
|
|
58
|
+
} | {
|
|
59
|
+
kind: "unexpected-required-column";
|
|
60
|
+
table: string;
|
|
61
|
+
column: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* How the schema reaches the store, which decides the fix each finding names.
|
|
65
|
+
*/
|
|
66
|
+
type SchemaSource = "database" | "drizzle" | "prisma";
|
|
67
|
+
/**
|
|
68
|
+
* Compares the tables Better Auth writes with what the store holds.
|
|
69
|
+
*
|
|
70
|
+
* A table or column Better Auth writes must exist. A column Better Auth does
|
|
71
|
+
* not write must accept an insert that omits it, so it is nullable or carries
|
|
72
|
+
* a default. Otherwise every insert into that table fails with a constraint
|
|
73
|
+
* error that says nothing about why the schema drifted.
|
|
74
|
+
*/
|
|
75
|
+
declare function diffSchema(expected: ExpectedSchema, actual: readonly IntrospectedTable[]): SchemaFinding[];
|
|
76
|
+
/**
|
|
77
|
+
* One finding as a sentence that names the change resolving it.
|
|
78
|
+
*/
|
|
79
|
+
declare function formatSchemaFinding(finding: SchemaFinding, source: SchemaSource): string;
|
|
80
|
+
/**
|
|
81
|
+
* The store cannot hold what this configuration writes.
|
|
82
|
+
*
|
|
83
|
+
* `findings` carries every problem as data; `message` lists each one with the
|
|
84
|
+
* change that resolves it. Reported during initialization and thrown when
|
|
85
|
+
* requests await validation, in every environment. Also thrown by
|
|
86
|
+
* `auth migrate` before it changes anything.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* try {
|
|
91
|
+
* await auth.api.getSession({ headers });
|
|
92
|
+
* } catch (error) {
|
|
93
|
+
* if (error instanceof SchemaMismatchError) console.error(error.findings);
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
declare class SchemaMismatchError extends BetterAuthError {
|
|
98
|
+
readonly findings: readonly SchemaFinding[];
|
|
99
|
+
readonly source: SchemaSource;
|
|
100
|
+
readonly code = "SCHEMA_MISMATCH";
|
|
101
|
+
constructor(findings: readonly SchemaFinding[], source: SchemaSource);
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { ExpectedSchema, IntrospectedColumn, IntrospectedTable, SchemaFinding, SchemaMismatchError, SchemaSource, diffSchema, formatSchemaFinding, getExpectedSchema };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { BetterAuthError } from "../error/index.mjs";
|
|
2
|
+
import { getAuthTables } from "./get-tables.mjs";
|
|
3
|
+
//#region src/db/schema-diff.ts
|
|
4
|
+
/**
|
|
5
|
+
* The tables this configuration writes, keyed the way the adapter addresses
|
|
6
|
+
* them. Tables that share a physical name are merged into one entry.
|
|
7
|
+
*/
|
|
8
|
+
function getExpectedSchema(options, { usePlural = false } = {}) {
|
|
9
|
+
const expected = {};
|
|
10
|
+
for (const table of Object.values(getAuthTables(options))) {
|
|
11
|
+
const name = usePlural ? `${table.modelName}s` : table.modelName;
|
|
12
|
+
const entry = expected[name] ??= {
|
|
13
|
+
fields: {},
|
|
14
|
+
disableMigrations: true
|
|
15
|
+
};
|
|
16
|
+
for (const [key, field] of Object.entries(table.fields)) entry.fields[field.fieldName || key] = field;
|
|
17
|
+
entry.disableMigrations = entry.disableMigrations && !!table.disableMigrations;
|
|
18
|
+
}
|
|
19
|
+
return expected;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compares the tables Better Auth writes with what the store holds.
|
|
23
|
+
*
|
|
24
|
+
* A table or column Better Auth writes must exist. A column Better Auth does
|
|
25
|
+
* not write must accept an insert that omits it, so it is nullable or carries
|
|
26
|
+
* a default. Otherwise every insert into that table fails with a constraint
|
|
27
|
+
* error that says nothing about why the schema drifted.
|
|
28
|
+
*/
|
|
29
|
+
function diffSchema(expected, actual) {
|
|
30
|
+
const findings = [];
|
|
31
|
+
for (const [tableName, table] of Object.entries(expected)) {
|
|
32
|
+
if (table.disableMigrations) continue;
|
|
33
|
+
const actualTable = actual.find((candidate) => candidate.name === tableName && (table.schema === void 0 || candidate.schema === table.schema));
|
|
34
|
+
if (!actualTable) {
|
|
35
|
+
findings.push({
|
|
36
|
+
kind: "missing-table",
|
|
37
|
+
table: tableName
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const written = new Set([table.idColumn ?? "id", ...Object.keys(table.fields)]);
|
|
42
|
+
for (const column of written) if (!actualTable.columns.some((candidate) => candidate.name === column)) findings.push({
|
|
43
|
+
kind: "missing-column",
|
|
44
|
+
table: tableName,
|
|
45
|
+
column
|
|
46
|
+
});
|
|
47
|
+
for (const column of actualTable.columns) {
|
|
48
|
+
if (written.has(column.name) || column.nullable || column.hasDefault) continue;
|
|
49
|
+
findings.push({
|
|
50
|
+
kind: "unexpected-required-column",
|
|
51
|
+
table: tableName,
|
|
52
|
+
column: column.name
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return findings;
|
|
57
|
+
}
|
|
58
|
+
const applyHint = {
|
|
59
|
+
database: "Run `npx auth migrate` to add it.",
|
|
60
|
+
drizzle: "Run `npx auth generate` to refresh the Drizzle schema, then apply it with your migration tool.",
|
|
61
|
+
prisma: "Run `npx auth generate` to refresh the Prisma schema, then run `prisma migrate`."
|
|
62
|
+
};
|
|
63
|
+
const relaxHint = {
|
|
64
|
+
database: "Drop the column, make it nullable, or give it a database default.",
|
|
65
|
+
drizzle: "Remove it from the Drizzle schema, make it nullable, or give it a default, then apply the change with your migration tool.",
|
|
66
|
+
prisma: "Remove it from the Prisma schema, make it optional, or give it a default, then run `prisma migrate`."
|
|
67
|
+
};
|
|
68
|
+
const sourceLabel = {
|
|
69
|
+
database: "Database",
|
|
70
|
+
drizzle: "Drizzle",
|
|
71
|
+
prisma: "Prisma"
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* One finding as a sentence that names the change resolving it.
|
|
75
|
+
*/
|
|
76
|
+
function formatSchemaFinding(finding, source) {
|
|
77
|
+
switch (finding.kind) {
|
|
78
|
+
case "missing-table": return `Table "${finding.table}" is missing. ${applyHint[source]}`;
|
|
79
|
+
case "missing-column": return `Column "${finding.column}" is missing from table "${finding.table}". ${applyHint[source]}`;
|
|
80
|
+
case "unexpected-required-column": {
|
|
81
|
+
const issuer = finding.column === "issuer" ? " If this column came from Better Auth 1.7.0 through 1.7.2, follow the upgrade guide before removing it: https://www.better-auth.com/docs/guides/1-7-upgrade-guide" : "";
|
|
82
|
+
return `Column "${finding.column}" on table "${finding.table}" is required but Better Auth never writes it, so every insert into "${finding.table}" fails. ${relaxHint[source]}${issuer}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const repairHint = {
|
|
87
|
+
database: "Make the listed columns nullable, give them defaults, or remove them.",
|
|
88
|
+
drizzle: "Make the listed columns nullable in your Drizzle schema, give them defaults, or remove them.",
|
|
89
|
+
prisma: "Make the listed fields optional in your Prisma schema, give them defaults, or remove them."
|
|
90
|
+
};
|
|
91
|
+
const migrationHint = {
|
|
92
|
+
...applyHint,
|
|
93
|
+
database: "Run `npx auth migrate` to add the missing tables and columns."
|
|
94
|
+
};
|
|
95
|
+
function formatSchemaMismatch(findings, source) {
|
|
96
|
+
const tables = [];
|
|
97
|
+
const columns = [];
|
|
98
|
+
const required = [];
|
|
99
|
+
const affectedTables = /* @__PURE__ */ new Set();
|
|
100
|
+
let hasIssuer = false;
|
|
101
|
+
for (const finding of findings) switch (finding.kind) {
|
|
102
|
+
case "missing-table":
|
|
103
|
+
tables.push(finding.table);
|
|
104
|
+
break;
|
|
105
|
+
case "missing-column":
|
|
106
|
+
columns.push(`${finding.table}.${finding.column}`);
|
|
107
|
+
break;
|
|
108
|
+
case "unexpected-required-column":
|
|
109
|
+
required.push(`${finding.table}.${finding.column}`);
|
|
110
|
+
affectedTables.add(finding.table);
|
|
111
|
+
hasIssuer ||= finding.column === "issuer";
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
const sections = [`${sourceLabel[source]} schema mismatch`];
|
|
115
|
+
if (tables.length) sections.push(` Missing tables\n ${tables.join(", ")}`);
|
|
116
|
+
if (columns.length) sections.push(` Missing columns\n ${columns.join("\n ")}`);
|
|
117
|
+
if (required.length) {
|
|
118
|
+
sections.push(` Required columns Better Auth never writes\n ${required.join("\n ")}`);
|
|
119
|
+
sections.push(` Inserts into ${[...affectedTables].join(", ")} will fail.`);
|
|
120
|
+
}
|
|
121
|
+
const help = [];
|
|
122
|
+
if (required.length) help.push(repairHint[source]);
|
|
123
|
+
if (tables.length || columns.length || required.length && source !== "database") help.push(migrationHint[source]);
|
|
124
|
+
if (help.length) sections.push(` help: ${help.join("\n ")}`);
|
|
125
|
+
if (hasIssuer) sections.push(" note: If this column came from Better Auth 1.7.0 through 1.7.2,\n follow the upgrade guide before removing it:\n https://www.better-auth.com/docs/guides/1-7-upgrade-guide");
|
|
126
|
+
return sections.join("\n\n");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The store cannot hold what this configuration writes.
|
|
130
|
+
*
|
|
131
|
+
* `findings` carries every problem as data; `message` lists each one with the
|
|
132
|
+
* change that resolves it. Reported during initialization and thrown when
|
|
133
|
+
* requests await validation, in every environment. Also thrown by
|
|
134
|
+
* `auth migrate` before it changes anything.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* try {
|
|
139
|
+
* await auth.api.getSession({ headers });
|
|
140
|
+
* } catch (error) {
|
|
141
|
+
* if (error instanceof SchemaMismatchError) console.error(error.findings);
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
var SchemaMismatchError = class extends BetterAuthError {
|
|
146
|
+
code = "SCHEMA_MISMATCH";
|
|
147
|
+
constructor(findings, source) {
|
|
148
|
+
super(formatSchemaMismatch(findings, source));
|
|
149
|
+
this.findings = findings;
|
|
150
|
+
this.source = source;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
//#endregion
|
|
154
|
+
export { SchemaMismatchError, diffSchema, formatSchemaFinding, getExpectedSchema };
|
package/dist/env/logger.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { __getCurrentEndpointContext } from "../context/global.mjs";
|
|
1
2
|
import { getColorDepth } from "./color-depth.mjs";
|
|
2
3
|
//#region src/env/logger.ts
|
|
3
4
|
const TTY_COLORS = {
|
|
@@ -74,6 +75,20 @@ const createLogger = (options) => {
|
|
|
74
75
|
}
|
|
75
76
|
};
|
|
76
77
|
};
|
|
77
|
-
const
|
|
78
|
+
const defaultLogger = createLogger();
|
|
79
|
+
const getCurrentLogger = () => {
|
|
80
|
+
const currentLogger = __getCurrentEndpointContext()?.context.logger;
|
|
81
|
+
return currentLogger && currentLogger !== logger ? currentLogger : defaultLogger;
|
|
82
|
+
};
|
|
83
|
+
const logger = {
|
|
84
|
+
debug: (...params) => getCurrentLogger().debug(...params),
|
|
85
|
+
info: (...params) => getCurrentLogger().info(...params),
|
|
86
|
+
success: (...params) => getCurrentLogger().success(...params),
|
|
87
|
+
warn: (...params) => getCurrentLogger().warn(...params),
|
|
88
|
+
error: (...params) => getCurrentLogger().error(...params),
|
|
89
|
+
get level() {
|
|
90
|
+
return getCurrentLogger().level;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
78
93
|
//#endregion
|
|
79
94
|
export { TTY_COLORS, createLogger, levels, logger, shouldPublishLog };
|
|
@@ -2,7 +2,7 @@ import { ATTR_HTTP_RESPONSE_STATUS_CODE } from "./attributes.mjs";
|
|
|
2
2
|
import { getOpenTelemetryAPI } from "./api.mjs";
|
|
3
3
|
//#region src/instrumentation/tracer.ts
|
|
4
4
|
const INSTRUMENTATION_SCOPE = "better-auth";
|
|
5
|
-
const INSTRUMENTATION_VERSION = "1.7.
|
|
5
|
+
const INSTRUMENTATION_VERSION = "1.7.3";
|
|
6
6
|
/**
|
|
7
7
|
* Better-auth uses `throw ctx.redirect(url)` for flow control (e.g. OAuth
|
|
8
8
|
* callbacks). These are APIErrors with 3xx status codes and should not be
|
package/dist/oauth2/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import { additionalAuthorizationParamsSchema } from "./authorization-params.mjs"
|
|
|
2
2
|
import { decodeBasicCredentials, encodeBasicCredentials } from "./basic-credentials.mjs";
|
|
3
3
|
import { CLIENT_ASSERTION_TYPE, ClientAssertionContext, ClientAssertionGetter, ClientAssertionGrantType, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, PrivateKeyJwtClientAssertionGetterOptions, PrivateKeyJwtSigningAlgorithm, createPrivateKeyJwtClientAssertionGetter, resolveClientAssertionParams, signPrivateKeyJwtClientAssertion } from "./client-assertion.mjs";
|
|
4
4
|
import { OAuth2Tokens, OAuth2UserInfo, OAuthAccountKeyContext, OAuthAccountSubject, OAuthIdTokenConfig, OAuthMappedUser, OAuthProvider, OAuthRefreshContext, ProviderOptions } from "./oauth-provider.mjs";
|
|
5
|
-
import { TokenEndpointAuth, TokenEndpointAuthMethod, TokenEndpointSecretAuthentication } from "./token-endpoint-auth.mjs";
|
|
5
|
+
import { TokenEndpointAuth, TokenEndpointAuthMethod, TokenEndpointRequestContext, TokenEndpointRequestHook, TokenEndpointSecretAuthentication } from "./token-endpoint-auth.mjs";
|
|
6
6
|
import { clientCredentialsToken, clientCredentialsTokenRequest } from "./client-credentials-token.mjs";
|
|
7
7
|
import { RESERVED_AUTHORIZATION_PARAMS, RESERVED_AUTHORIZATION_PARAMS_SET, createAuthorizationURL } from "./create-authorization-url.mjs";
|
|
8
8
|
import { AccessTokenAuthorization, AccessTokenAuthorizationScheme, BEARER_AUTHORIZATION_SCHEME, DPOP_AUTHORIZATION_SCHEME, DPOP_PROOF_TYPE, DPOP_SIGNING_ALGORITHMS, DpopBindingError, DpopBindingErrorCode, DpopProofError, DpopProofErrorCode, DpopReplayReservation, DpopReplayReservations, DpopReplayStore, DpopSigningAlgorithm, EnforceDpopBindingParams, VerifiedDpopProof, VerifyDpopProofOptions, createDpopBindingError, createDpopProofError, createDpopReplayStore, createInMemoryDpopReplayStore, deriveDpopAth, deriveDpopJkt, enforceDpopBinding, getConfirmationJkt, getDpopJktFromPayload, isDpopBindingError, isDpopProofError, normalizeDpopHtu, parseAccessTokenAuthorization, stripAccessTokenAuthorizationScheme, verifyDpopProof } from "./dpop.mjs";
|
|
@@ -11,4 +11,4 @@ import { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens,
|
|
|
11
11
|
import { authorizationCodeRequest, validateAuthorizationCode, validateToken } from "./validate-authorization-code.mjs";
|
|
12
12
|
import { ResourceRequestInput, VerifyAccessTokenOptions, VerifyAccessTokenRequestOptions, createInsufficientScopeError, getJwks, isInsufficientScopeError, requestToResourceInput, verifyAccessTokenRequest, verifyBearerToken, verifyJwsAccessToken } from "./verify.mjs";
|
|
13
13
|
import { supportsIdTokenSignIn, verifyProviderIdToken } from "./verify-id-token.mjs";
|
|
14
|
-
export { type AccessTokenAuthorization, type AccessTokenAuthorizationScheme, BEARER_AUTHORIZATION_SCHEME, CLIENT_ASSERTION_TYPE, type ClientAssertionContext, type ClientAssertionGetter, type ClientAssertionGrantType, DPOP_AUTHORIZATION_SCHEME, DPOP_PROOF_TYPE, DPOP_SIGNING_ALGORITHMS, type DpopBindingError, type DpopBindingErrorCode, type DpopProofError, type DpopProofErrorCode, type DpopReplayReservation, type DpopReplayReservations, type DpopReplayStore, type DpopSigningAlgorithm, type EnforceDpopBindingParams, type OAuth2Tokens, type OAuth2UserInfo, type OAuthAccountKeyContext, type OAuthAccountSubject, type OAuthIdTokenConfig, type OAuthMappedUser, type OAuthProvider, type OAuthRefreshContext, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, type PrivateKeyJwtClientAssertionGetterOptions, type PrivateKeyJwtSigningAlgorithm, type ProviderOptions, RESERVED_AUTHORIZATION_PARAMS, RESERVED_AUTHORIZATION_PARAMS_SET, type ResourceRequestInput, type TokenEndpointAuth, type TokenEndpointAuthMethod, type TokenEndpointSecretAuthentication, type VerifiedDpopProof, type VerifyAccessTokenOptions, type VerifyAccessTokenRequestOptions, type VerifyDpopProofOptions, additionalAuthorizationParamsSchema, applyDefaultAccessTokenExpiry, authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationURL, createDpopBindingError, createDpopProofError, createDpopReplayStore, createInMemoryDpopReplayStore, createInsufficientScopeError, createPrivateKeyJwtClientAssertionGetter, decodeBasicCredentials, deriveDpopAth, deriveDpopJkt, encodeBasicCredentials, enforceDpopBinding, generateCodeChallenge, getConfirmationJkt, getDpopJktFromPayload, getJwks, getOAuth2Tokens, getPrimaryClientId, isDpopBindingError, isDpopProofError, isInsufficientScopeError, mergeScopes, normalizeDpopHtu, parseAccessTokenAuthorization, refreshAccessToken, refreshAccessTokenRequest, requestToResourceInput, resolveClientAssertionParams, signPrivateKeyJwtClientAssertion, stripAccessTokenAuthorizationScheme, supportsIdTokenSignIn, validateAuthorizationCode, validateToken, verifyAccessTokenRequest, verifyBearerToken, verifyDpopProof, verifyJwsAccessToken, verifyProviderIdToken };
|
|
14
|
+
export { type AccessTokenAuthorization, type AccessTokenAuthorizationScheme, BEARER_AUTHORIZATION_SCHEME, CLIENT_ASSERTION_TYPE, type ClientAssertionContext, type ClientAssertionGetter, type ClientAssertionGrantType, DPOP_AUTHORIZATION_SCHEME, DPOP_PROOF_TYPE, DPOP_SIGNING_ALGORITHMS, type DpopBindingError, type DpopBindingErrorCode, type DpopProofError, type DpopProofErrorCode, type DpopReplayReservation, type DpopReplayReservations, type DpopReplayStore, type DpopSigningAlgorithm, type EnforceDpopBindingParams, type OAuth2Tokens, type OAuth2UserInfo, type OAuthAccountKeyContext, type OAuthAccountSubject, type OAuthIdTokenConfig, type OAuthMappedUser, type OAuthProvider, type OAuthRefreshContext, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, type PrivateKeyJwtClientAssertionGetterOptions, type PrivateKeyJwtSigningAlgorithm, type ProviderOptions, RESERVED_AUTHORIZATION_PARAMS, RESERVED_AUTHORIZATION_PARAMS_SET, type ResourceRequestInput, type TokenEndpointAuth, type TokenEndpointAuthMethod, type TokenEndpointRequestContext, type TokenEndpointRequestHook, type TokenEndpointSecretAuthentication, type VerifiedDpopProof, type VerifyAccessTokenOptions, type VerifyAccessTokenRequestOptions, type VerifyDpopProofOptions, additionalAuthorizationParamsSchema, applyDefaultAccessTokenExpiry, authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationURL, createDpopBindingError, createDpopProofError, createDpopReplayStore, createInMemoryDpopReplayStore, createInsufficientScopeError, createPrivateKeyJwtClientAssertionGetter, decodeBasicCredentials, deriveDpopAth, deriveDpopJkt, encodeBasicCredentials, enforceDpopBinding, generateCodeChallenge, getConfirmationJkt, getDpopJktFromPayload, getJwks, getOAuth2Tokens, getPrimaryClientId, isDpopBindingError, isDpopProofError, isInsufficientScopeError, mergeScopes, normalizeDpopHtu, parseAccessTokenAuthorization, refreshAccessToken, refreshAccessTokenRequest, requestToResourceInput, resolveClientAssertionParams, signPrivateKeyJwtClientAssertion, stripAccessTokenAuthorizationScheme, supportsIdTokenSignIn, validateAuthorizationCode, validateToken, verifyAccessTokenRequest, verifyBearerToken, verifyDpopProof, verifyJwsAccessToken, verifyProviderIdToken };
|
|
@@ -214,16 +214,6 @@ interface OAuthProvider<T extends object = object, O extends object = Partial<Pr
|
|
|
214
214
|
* against this value to prevent authorization server mix-up attacks.
|
|
215
215
|
*/
|
|
216
216
|
issuer?: string | undefined;
|
|
217
|
-
/**
|
|
218
|
-
* Stable issuer used with the provider subject to recognize an account.
|
|
219
|
-
*
|
|
220
|
-
* Use the validated OpenID Connect issuer for OIDC providers. A resolver is
|
|
221
|
-
* supported for tenant-specific issuers and receives only provider-verified
|
|
222
|
-
* data. OAuth providers without an issuer omit this property and are scoped
|
|
223
|
-
* to the synthetic `local:oauth:<encoded providerId>` issuer, where the
|
|
224
|
-
* provider ID segment is percent-encoded.
|
|
225
|
-
*/
|
|
226
|
-
accountIssuer?: string | OAuthAccountKeyResolver<T, string> | undefined;
|
|
227
217
|
/**
|
|
228
218
|
* Require shared OAuth redirect routes to bind ID-token verification to an
|
|
229
219
|
* authorization request nonce. When true, routes generate `idTokenNonce`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ClientAssertionGetter } from "./client-assertion.mjs";
|
|
1
|
+
import { ClientAssertionGetter, ClientAssertionGrantType } from "./client-assertion.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/oauth2/token-endpoint-auth.d.ts
|
|
4
4
|
type TokenEndpointAuth = {
|
|
@@ -10,8 +10,32 @@ type TokenEndpointAuth = {
|
|
|
10
10
|
} | {
|
|
11
11
|
method: "private_key_jwt";
|
|
12
12
|
getClientAssertion: ClientAssertionGetter;
|
|
13
|
+
} | {
|
|
14
|
+
method: "custom";
|
|
15
|
+
/**
|
|
16
|
+
* Customize the token request after standard grant parameters are set.
|
|
17
|
+
*/
|
|
18
|
+
customizeRequest: TokenEndpointRequestHook;
|
|
13
19
|
};
|
|
14
20
|
type TokenEndpointAuthMethod = TokenEndpointAuth["method"];
|
|
15
21
|
type TokenEndpointSecretAuthentication = "basic" | "post";
|
|
22
|
+
/**
|
|
23
|
+
* Mutable token request state passed to a custom authentication strategy.
|
|
24
|
+
*/
|
|
25
|
+
interface TokenEndpointRequestContext {
|
|
26
|
+
body: URLSearchParams;
|
|
27
|
+
headers: Record<string, string>;
|
|
28
|
+
options: TokenEndpointClientOptions;
|
|
29
|
+
tokenEndpoint: string;
|
|
30
|
+
grantType: ClientAssertionGrantType;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Applies provider-specific authentication to a token request.
|
|
34
|
+
*/
|
|
35
|
+
type TokenEndpointRequestHook = (context: TokenEndpointRequestContext) => void | Promise<void>;
|
|
36
|
+
interface TokenEndpointClientOptions {
|
|
37
|
+
clientId?: string | string[] | undefined;
|
|
38
|
+
clientSecret?: string | undefined;
|
|
39
|
+
}
|
|
16
40
|
//#endregion
|
|
17
|
-
export { TokenEndpointAuth, TokenEndpointAuthMethod, TokenEndpointSecretAuthentication };
|
|
41
|
+
export { TokenEndpointAuth, TokenEndpointAuthMethod, TokenEndpointRequestContext, TokenEndpointRequestHook, TokenEndpointSecretAuthentication };
|
|
@@ -46,6 +46,17 @@ async function applyTokenEndpointAuth({ body, headers, options, tokenEndpoint, g
|
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
const auth = tokenEndpointAuth ?? getDefaultTokenEndpointAuth(options, authentication);
|
|
49
|
+
if (auth.method === "custom") {
|
|
50
|
+
await auth.customizeRequest({
|
|
51
|
+
body,
|
|
52
|
+
headers,
|
|
53
|
+
options,
|
|
54
|
+
tokenEndpoint,
|
|
55
|
+
grantType
|
|
56
|
+
});
|
|
57
|
+
assertCompleteManualClientAssertion(body);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
49
60
|
if (auth.method === "private_key_jwt") {
|
|
50
61
|
assertNoClientSecret(auth.method, options, body);
|
|
51
62
|
assertClientIdConfigured(auth.method, clientId);
|
|
@@ -13,7 +13,6 @@ const apple = (options) => {
|
|
|
13
13
|
id: "apple",
|
|
14
14
|
name: "Apple",
|
|
15
15
|
accountSubject: ({ profile }) => profile.sub,
|
|
16
|
-
accountIssuer: "https://appleid.apple.com",
|
|
17
16
|
async createAuthorizationURL({ state, scopes, redirectURI, additionalParams, codeVerifier }) {
|
|
18
17
|
if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
|
|
19
18
|
logger.error("Client ID and client secret are required for Apple. Make sure to provide them in the options.");
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { OAuth2Tokens, OAuth2UserInfo, OAuthAccountKeyContext, ProviderOptions } from "../oauth2/oauth-provider.mjs";
|
|
2
|
+
//#region src/social-providers/cloudflare.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The user profile returned by the Cloudflare API `/user` endpoint.
|
|
5
|
+
*
|
|
6
|
+
* @see https://developers.cloudflare.com/api/resources/user/methods/get/
|
|
7
|
+
*/
|
|
8
|
+
interface CloudflareProfile {
|
|
9
|
+
/**
|
|
10
|
+
* Identifier of the user.
|
|
11
|
+
*/
|
|
12
|
+
id: string;
|
|
13
|
+
/**
|
|
14
|
+
* Current email address of the user.
|
|
15
|
+
*/
|
|
16
|
+
email: string;
|
|
17
|
+
/**
|
|
18
|
+
* The user's first name.
|
|
19
|
+
*/
|
|
20
|
+
first_name?: string | null | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The user's last name.
|
|
23
|
+
*/
|
|
24
|
+
last_name?: string | null | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The country in which the user lives.
|
|
27
|
+
*/
|
|
28
|
+
country?: string | null | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The user's telephone number.
|
|
31
|
+
*/
|
|
32
|
+
telephone?: string | null | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* The zipcode or postal code where the user lives.
|
|
35
|
+
*/
|
|
36
|
+
zipcode?: string | null | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Indicates whether two-factor authentication is enabled for the user account.
|
|
39
|
+
*/
|
|
40
|
+
two_factor_authentication_enabled?: boolean | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Indicates whether the user has been suspended.
|
|
43
|
+
*/
|
|
44
|
+
suspended?: boolean | undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Token endpoint authentication supported by Cloudflare OAuth clients.
|
|
48
|
+
*
|
|
49
|
+
* @see https://developers.cloudflare.com/fundamentals/oauth/create-an-oauth-client/#choose-a-flow
|
|
50
|
+
*/
|
|
51
|
+
type CloudflareClientAuthentication = {
|
|
52
|
+
/**
|
|
53
|
+
* The client secret of a confidential Cloudflare OAuth client.
|
|
54
|
+
*/
|
|
55
|
+
clientSecret: string;
|
|
56
|
+
/**
|
|
57
|
+
* The authentication method configured for the token endpoint.
|
|
58
|
+
*
|
|
59
|
+
* @default "client_secret_basic"
|
|
60
|
+
*/
|
|
61
|
+
tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | undefined;
|
|
62
|
+
} | {
|
|
63
|
+
/**
|
|
64
|
+
* Clients that use PKCE do not have a client secret.
|
|
65
|
+
*/
|
|
66
|
+
clientSecret?: undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Clients without a secret do not authenticate at the token endpoint.
|
|
69
|
+
*
|
|
70
|
+
* @default "none"
|
|
71
|
+
*/
|
|
72
|
+
tokenEndpointAuthMethod?: "none" | undefined;
|
|
73
|
+
};
|
|
74
|
+
interface CloudflareBaseOptions extends ProviderOptions<CloudflareProfile> {
|
|
75
|
+
/**
|
|
76
|
+
* The client ID of the Cloudflare OAuth client.
|
|
77
|
+
*/
|
|
78
|
+
clientId: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Options for configuring the Cloudflare social provider.
|
|
82
|
+
*/
|
|
83
|
+
type CloudflareOptions = CloudflareBaseOptions & CloudflareClientAuthentication;
|
|
84
|
+
declare const cloudflare: (options: CloudflareOptions) => {
|
|
85
|
+
id: "cloudflare";
|
|
86
|
+
name: string;
|
|
87
|
+
accountSubject: ({
|
|
88
|
+
profile
|
|
89
|
+
}: OAuthAccountKeyContext<CloudflareProfile>) => string;
|
|
90
|
+
createAuthorizationURL({
|
|
91
|
+
state,
|
|
92
|
+
scopes,
|
|
93
|
+
codeVerifier,
|
|
94
|
+
redirectURI
|
|
95
|
+
}: {
|
|
96
|
+
state: string;
|
|
97
|
+
codeVerifier: string;
|
|
98
|
+
scopes?: string[] | undefined;
|
|
99
|
+
redirectURI: string;
|
|
100
|
+
display?: string | undefined;
|
|
101
|
+
loginHint?: string | undefined;
|
|
102
|
+
idTokenNonce?: string | undefined;
|
|
103
|
+
additionalParams?: Record<string, string> | undefined;
|
|
104
|
+
}): Promise<URL>;
|
|
105
|
+
validateAuthorizationCode: ({
|
|
106
|
+
code,
|
|
107
|
+
codeVerifier,
|
|
108
|
+
redirectURI
|
|
109
|
+
}: {
|
|
110
|
+
code: string;
|
|
111
|
+
redirectURI: string;
|
|
112
|
+
codeVerifier?: string | undefined;
|
|
113
|
+
deviceId?: string | undefined;
|
|
114
|
+
}) => Promise<OAuth2Tokens>;
|
|
115
|
+
refreshAccessToken: (refreshToken: string) => Promise<OAuth2Tokens>;
|
|
116
|
+
getUserInfo(token: OAuth2Tokens & {
|
|
117
|
+
expectedIdTokenNonce?: string | undefined;
|
|
118
|
+
user?: {
|
|
119
|
+
name?: {
|
|
120
|
+
firstName?: string;
|
|
121
|
+
lastName?: string;
|
|
122
|
+
};
|
|
123
|
+
email?: string;
|
|
124
|
+
} | undefined;
|
|
125
|
+
}): Promise<{
|
|
126
|
+
user: OAuth2UserInfo & Record<string, unknown>;
|
|
127
|
+
data: CloudflareProfile;
|
|
128
|
+
} | null>;
|
|
129
|
+
options: CloudflareOptions;
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
132
|
+
export { CloudflareOptions, CloudflareProfile, cloudflare };
|