@better-auth/core 1.7.0-rc.0 → 1.7.0-rc.2
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 +28 -28
- package/dist/async_hooks/index.d.mts +0 -1
- package/dist/async_hooks/pure.index.d.mts +0 -1
- package/dist/context/endpoint-context.d.mts +1 -1
- package/dist/context/global.d.mts +0 -1
- package/dist/context/global.mjs +1 -1
- package/dist/context/request-state.d.mts +0 -1
- package/dist/context/request-state.mjs +8 -5
- package/dist/context/transaction.d.mts +8 -3
- package/dist/context/transaction.mjs +27 -10
- package/dist/db/adapter/factory.d.mts +3 -5
- package/dist/db/adapter/factory.mjs +7 -5
- package/dist/db/adapter/get-default-field-name.d.mts +2 -9
- package/dist/db/adapter/get-default-model-name.d.mts +1 -5
- package/dist/db/adapter/get-default-model-name.mjs +6 -5
- package/dist/db/adapter/get-field-attributes.d.mts +4 -11
- package/dist/db/adapter/get-field-name.d.mts +2 -9
- package/dist/db/adapter/get-id-field.d.mts +4 -12
- package/dist/db/adapter/get-model-name.d.mts +1 -5
- package/dist/db/adapter/index.d.mts +7 -32
- package/dist/db/adapter/types.d.mts +5 -17
- package/dist/db/adapter/utils.d.mts +0 -1
- package/dist/db/database-index.d.mts +44 -0
- package/dist/db/database-index.mjs +168 -0
- package/dist/db/get-tables.d.mts +7 -1
- package/dist/db/get-tables.mjs +47 -6
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/internal.d.mts +3 -0
- package/dist/db/internal.mjs +3 -0
- package/dist/db/plugin.d.mts +4 -3
- package/dist/db/schema/account.d.mts +16 -3
- package/dist/db/schema/account.mjs +35 -2
- package/dist/db/schema/rate-limit.d.mts +1 -1
- package/dist/db/schema/rate-limit.mjs +9 -0
- package/dist/db/schema/session.d.mts +1 -1
- package/dist/db/schema/shared.d.mts +0 -1
- package/dist/db/schema/user.d.mts +1 -1
- package/dist/db/schema/verification.d.mts +1 -1
- package/dist/db/type.d.mts +17 -9
- package/dist/env/logger.d.mts +25 -25
- package/dist/error/codes.d.mts +0 -1
- package/dist/error/index.d.mts +0 -1
- package/dist/index.d.mts +1 -1
- package/dist/instrumentation/attributes.d.mts +0 -1
- package/dist/instrumentation/pure.index.d.mts +0 -1
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/authorization-params.d.mts +0 -1
- package/dist/oauth2/client-assertion.d.mts +3 -13
- package/dist/oauth2/client-credentials-token.d.mts +3 -17
- package/dist/oauth2/client-credentials-token.mjs +1 -4
- package/dist/oauth2/create-authorization-url.d.mts +3 -21
- package/dist/oauth2/dpop.d.mts +2 -23
- package/dist/oauth2/dpop.mjs +1 -1
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/oauth-provider.d.mts +85 -36
- package/dist/oauth2/refresh-access-token.d.mts +3 -19
- package/dist/oauth2/refresh-access-token.mjs +3 -9
- package/dist/oauth2/reject-redirects.mjs +1 -1
- package/dist/oauth2/token-endpoint-auth.d.mts +0 -1
- package/dist/oauth2/utils.d.mts +0 -1
- package/dist/oauth2/utils.mjs +2 -3
- package/dist/oauth2/validate-authorization-code.d.mts +5 -29
- package/dist/oauth2/verify-id-token.d.mts +3 -2
- package/dist/oauth2/verify-id-token.mjs +3 -3
- package/dist/oauth2/verify.d.mts +5 -7
- package/dist/oauth2/verify.mjs +2 -2
- package/dist/social-providers/apple.d.mts +9 -24
- package/dist/social-providers/apple.mjs +2 -1
- package/dist/social-providers/atlassian.d.mts +18 -19
- package/dist/social-providers/atlassian.mjs +1 -1
- package/dist/social-providers/cognito.d.mts +33 -22
- package/dist/social-providers/cognito.mjs +2 -2
- package/dist/social-providers/discord.d.mts +7 -20
- package/dist/social-providers/discord.mjs +1 -1
- package/dist/social-providers/dropbox.d.mts +7 -22
- package/dist/social-providers/dropbox.mjs +1 -1
- package/dist/social-providers/facebook.d.mts +19 -27
- package/dist/social-providers/facebook.mjs +6 -17
- package/dist/social-providers/figma.d.mts +7 -22
- package/dist/social-providers/figma.mjs +1 -1
- package/dist/social-providers/github.d.mts +7 -23
- package/dist/social-providers/github.mjs +1 -1
- package/dist/social-providers/gitlab.d.mts +6 -43
- package/dist/social-providers/gitlab.mjs +1 -1
- package/dist/social-providers/google.d.mts +10 -32
- package/dist/social-providers/google.mjs +2 -1
- package/dist/social-providers/huggingface.d.mts +7 -22
- package/dist/social-providers/huggingface.mjs +1 -1
- package/dist/social-providers/index.d.mts +229 -787
- package/dist/social-providers/kakao.d.mts +8 -33
- package/dist/social-providers/kakao.mjs +1 -1
- package/dist/social-providers/kick.d.mts +7 -22
- package/dist/social-providers/kick.mjs +1 -1
- package/dist/social-providers/line.d.mts +8 -44
- package/dist/social-providers/line.mjs +4 -6
- package/dist/social-providers/linear.d.mts +7 -21
- package/dist/social-providers/linear.mjs +1 -1
- package/dist/social-providers/linkedin.d.mts +7 -21
- package/dist/social-providers/linkedin.mjs +1 -1
- package/dist/social-providers/microsoft-entra-id.d.mts +8 -18
- package/dist/social-providers/microsoft-entra-id.mjs +18 -2
- package/dist/social-providers/naver.d.mts +27 -42
- package/dist/social-providers/naver.mjs +1 -1
- package/dist/social-providers/notion.d.mts +7 -21
- package/dist/social-providers/notion.mjs +1 -1
- package/dist/social-providers/paybin.d.mts +8 -23
- package/dist/social-providers/paybin.mjs +2 -1
- package/dist/social-providers/paypal.d.mts +8 -33
- package/dist/social-providers/paypal.mjs +1 -1
- package/dist/social-providers/polar.d.mts +7 -22
- package/dist/social-providers/polar.mjs +1 -1
- package/dist/social-providers/railway.d.mts +7 -22
- package/dist/social-providers/railway.mjs +1 -1
- package/dist/social-providers/reddit.d.mts +7 -20
- package/dist/social-providers/reddit.mjs +1 -1
- package/dist/social-providers/roblox.d.mts +7 -20
- package/dist/social-providers/roblox.mjs +1 -1
- package/dist/social-providers/salesforce.d.mts +7 -22
- package/dist/social-providers/salesforce.mjs +1 -1
- package/dist/social-providers/slack.d.mts +7 -20
- package/dist/social-providers/slack.mjs +1 -1
- package/dist/social-providers/spotify.d.mts +7 -22
- package/dist/social-providers/spotify.mjs +1 -1
- package/dist/social-providers/tiktok.d.mts +10 -22
- package/dist/social-providers/tiktok.mjs +1 -1
- package/dist/social-providers/twitch.d.mts +7 -20
- package/dist/social-providers/twitch.mjs +1 -1
- package/dist/social-providers/twitter.d.mts +29 -26
- package/dist/social-providers/twitter.mjs +1 -1
- package/dist/social-providers/vercel.d.mts +7 -22
- package/dist/social-providers/vercel.mjs +1 -1
- package/dist/social-providers/vk.d.mts +8 -24
- package/dist/social-providers/vk.mjs +5 -6
- package/dist/social-providers/wechat.d.mts +7 -19
- package/dist/social-providers/wechat.mjs +1 -1
- package/dist/social-providers/zoom.d.mts +9 -25
- package/dist/social-providers/zoom.mjs +1 -1
- package/dist/types/context.d.mts +16 -9
- package/dist/types/cookie.d.mts +0 -1
- package/dist/types/helper.d.mts +1 -1
- package/dist/types/index.d.mts +1 -1
- package/dist/types/init-options.d.mts +37 -26
- package/dist/types/plugin-client.d.mts +1 -3
- package/dist/types/plugin.d.mts +2 -2
- package/dist/utils/async.d.mts +0 -1
- package/dist/utils/db.d.mts +1 -0
- package/dist/utils/deprecate.d.mts +0 -1
- package/dist/utils/error-codes.d.mts +2 -2
- package/dist/utils/host.d.mts +27 -1
- package/dist/utils/host.mjs +1 -1
- package/dist/utils/ip.d.mts +1 -0
- package/dist/utils/ip.mjs +1 -1
- package/dist/utils/is-api-error.d.mts +0 -1
- package/dist/utils/redirect-uri.d.mts +0 -1
- package/package.json +10 -2
- package/src/context/request-state.ts +26 -6
- package/src/context/transaction.ts +37 -18
- package/src/db/adapter/factory.ts +24 -18
- package/src/db/adapter/get-default-model-name.ts +23 -14
- package/src/db/database-index.ts +351 -0
- package/src/db/get-tables.ts +89 -9
- package/src/db/index.ts +4 -0
- package/src/db/internal.ts +12 -0
- package/src/db/plugin.ts +3 -1
- package/src/db/schema/account.ts +27 -1
- package/src/db/schema/user.ts +1 -1
- package/src/db/type.ts +12 -0
- package/src/oauth2/index.ts +3 -0
- package/src/oauth2/oauth-provider.ts +94 -36
- package/src/oauth2/verify-id-token.ts +4 -2
- package/src/social-providers/apple.ts +2 -1
- package/src/social-providers/atlassian.ts +1 -1
- package/src/social-providers/cognito.ts +2 -2
- package/src/social-providers/discord.ts +1 -1
- package/src/social-providers/dropbox.ts +1 -1
- package/src/social-providers/facebook.ts +21 -28
- package/src/social-providers/figma.ts +1 -1
- package/src/social-providers/github.ts +1 -1
- package/src/social-providers/gitlab.ts +1 -1
- package/src/social-providers/google.ts +2 -1
- package/src/social-providers/huggingface.ts +1 -1
- package/src/social-providers/kakao.ts +1 -1
- package/src/social-providers/kick.ts +1 -1
- package/src/social-providers/line.ts +7 -12
- package/src/social-providers/linear.ts +1 -1
- package/src/social-providers/linkedin.ts +1 -1
- package/src/social-providers/microsoft-entra-id.ts +3 -2
- package/src/social-providers/naver.ts +1 -1
- package/src/social-providers/notion.ts +1 -1
- package/src/social-providers/paybin.ts +2 -1
- package/src/social-providers/paypal.ts +1 -1
- package/src/social-providers/polar.ts +1 -1
- package/src/social-providers/railway.ts +1 -1
- package/src/social-providers/reddit.ts +1 -1
- package/src/social-providers/roblox.ts +1 -1
- package/src/social-providers/salesforce.ts +1 -1
- package/src/social-providers/slack.ts +1 -1
- package/src/social-providers/spotify.ts +1 -1
- package/src/social-providers/tiktok.ts +2 -2
- package/src/social-providers/twitch.ts +1 -1
- package/src/social-providers/twitter.ts +1 -1
- package/src/social-providers/vercel.ts +1 -1
- package/src/social-providers/vk.ts +2 -2
- package/src/social-providers/wechat.ts +1 -1
- package/src/social-providers/zoom.ts +1 -1
- package/src/types/context.ts +21 -14
- package/src/types/init-options.ts +15 -14
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { BetterAuthError } from "../error";
|
|
2
|
+
import type { DBFieldAttribute, DBTableIndex } from "./type";
|
|
3
|
+
|
|
4
|
+
const MAX_DATABASE_INDEX_NAME_BYTES = 63;
|
|
5
|
+
const MAX_DATABASE_INDEX_FIELDS = 16;
|
|
6
|
+
|
|
7
|
+
export function getPortableDatabaseIdentifierKey(identifier: string) {
|
|
8
|
+
return identifier.toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getUtf8ByteLength(value: string) {
|
|
12
|
+
return new TextEncoder().encode(value).length;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function truncateUtf8(value: string, maxBytes: number) {
|
|
16
|
+
let result = "";
|
|
17
|
+
let byteLength = 0;
|
|
18
|
+
for (const character of value) {
|
|
19
|
+
const characterByteLength = getUtf8ByteLength(character);
|
|
20
|
+
if (byteLength + characterByteLength > maxBytes) break;
|
|
21
|
+
result += character;
|
|
22
|
+
byteLength += characterByteLength;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getStableIndexNameHash(value: string) {
|
|
28
|
+
let hash = 0x811c9dc5;
|
|
29
|
+
for (let index = 0; index < value.length; index++) {
|
|
30
|
+
hash ^= value.charCodeAt(index);
|
|
31
|
+
hash = Math.imul(hash, 0x01000193);
|
|
32
|
+
}
|
|
33
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A table-level index resolved to physical database columns. */
|
|
37
|
+
export interface ResolvedDBTableIndex extends Omit<DBTableIndex, "fields"> {
|
|
38
|
+
/** Physical database column names, in index order. */
|
|
39
|
+
columns: readonly [string, ...string[]];
|
|
40
|
+
name: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DBTableIndexSource {
|
|
44
|
+
fields: Readonly<Record<string, DBFieldAttribute>>;
|
|
45
|
+
indexes: readonly DBTableIndex[] | undefined;
|
|
46
|
+
tableName: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type BoundedDatabaseIndexDialect = "mssql" | "mysql";
|
|
50
|
+
|
|
51
|
+
/** Returns the stable database name for a table-level index. */
|
|
52
|
+
export function getDatabaseIndexName(
|
|
53
|
+
tableName: string,
|
|
54
|
+
index: DBTableIndex,
|
|
55
|
+
): string {
|
|
56
|
+
if (index.name !== undefined) {
|
|
57
|
+
if (index.name.trim().length === 0) {
|
|
58
|
+
throw new BetterAuthError(
|
|
59
|
+
"Database index names must contain at least one visible character.",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(index.name)) {
|
|
63
|
+
throw new BetterAuthError(
|
|
64
|
+
"Database index names must start with a letter or underscore and contain only letters, numbers, and underscores.",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (getUtf8ByteLength(index.name) > MAX_DATABASE_INDEX_NAME_BYTES) {
|
|
68
|
+
throw new BetterAuthError(
|
|
69
|
+
`Database index names must be at most ${MAX_DATABASE_INDEX_NAME_BYTES} UTF-8 bytes.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return index.name;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const indexKind = index.unique ? "uidx" : "idx";
|
|
76
|
+
const generatedName = `${tableName}_${index.fields.join("_")}_${indexKind}`;
|
|
77
|
+
if (getUtf8ByteLength(generatedName) <= MAX_DATABASE_INDEX_NAME_BYTES) {
|
|
78
|
+
return generatedName;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const suffix = `_${getStableIndexNameHash(generatedName)}_${indexKind}`;
|
|
82
|
+
return `${truncateUtf8(
|
|
83
|
+
generatedName.slice(0, -indexKind.length - 1),
|
|
84
|
+
MAX_DATABASE_INDEX_NAME_BYTES - getUtf8ByteLength(suffix),
|
|
85
|
+
)}${suffix}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Returns the database name used by legacy field-level index metadata. */
|
|
89
|
+
export function getDatabaseFieldIndexName(
|
|
90
|
+
tableName: string,
|
|
91
|
+
columnName: string,
|
|
92
|
+
unique: boolean,
|
|
93
|
+
) {
|
|
94
|
+
return getDatabaseIndexName(tableName, {
|
|
95
|
+
fields: [columnName],
|
|
96
|
+
unique,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Resolves logical index fields to their configured database column names. */
|
|
101
|
+
export function resolveDatabaseTableIndexes({
|
|
102
|
+
fields,
|
|
103
|
+
indexes,
|
|
104
|
+
tableName,
|
|
105
|
+
}: {
|
|
106
|
+
fields: Readonly<Record<string, DBFieldAttribute>>;
|
|
107
|
+
indexes: readonly DBTableIndex[] | undefined;
|
|
108
|
+
tableName: string;
|
|
109
|
+
}): readonly ResolvedDBTableIndex[] {
|
|
110
|
+
const resolvedIndexes = (indexes ?? []).map((index) => {
|
|
111
|
+
if (index.fields.length === 0) {
|
|
112
|
+
throw new BetterAuthError(
|
|
113
|
+
`Index on table "${tableName}" must include at least one field.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (index.fields.length > MAX_DATABASE_INDEX_FIELDS) {
|
|
117
|
+
throw new BetterAuthError(
|
|
118
|
+
`Index on table "${tableName}" can include at most ${MAX_DATABASE_INDEX_FIELDS} fields so it works across supported databases.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (new Set(index.fields).size !== index.fields.length) {
|
|
122
|
+
throw new BetterAuthError(
|
|
123
|
+
`Index on table "${tableName}" contains the same field more than once.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (
|
|
127
|
+
index.unique &&
|
|
128
|
+
index.fields.some((fieldName) => fields[fieldName]?.required === false)
|
|
129
|
+
) {
|
|
130
|
+
throw new BetterAuthError(
|
|
131
|
+
`Unique index on table "${tableName}" can only include required fields so its behavior is consistent across databases.`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const unsupportedField = index.fields.find((fieldName) => {
|
|
135
|
+
const fieldType = fields[fieldName]?.type;
|
|
136
|
+
return (
|
|
137
|
+
fieldType === "json" ||
|
|
138
|
+
fieldType === "string[]" ||
|
|
139
|
+
fieldType === "number[]"
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
if (unsupportedField) {
|
|
143
|
+
throw new BetterAuthError(
|
|
144
|
+
`Index on table "${tableName}" references field "${unsupportedField}", whose type is not portably indexable.`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const resolveFieldName = (fieldName: string) => {
|
|
148
|
+
const field = fields[fieldName];
|
|
149
|
+
if (!field) {
|
|
150
|
+
throw new BetterAuthError(
|
|
151
|
+
`Index on table "${tableName}" references unknown field "${fieldName}".`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return field.fieldName || fieldName;
|
|
155
|
+
};
|
|
156
|
+
const [firstField, ...remainingFields] = index.fields;
|
|
157
|
+
const columns = [
|
|
158
|
+
resolveFieldName(firstField),
|
|
159
|
+
...remainingFields.map(resolveFieldName),
|
|
160
|
+
] as ResolvedDBTableIndex["columns"];
|
|
161
|
+
if (
|
|
162
|
+
new Set(columns.map(getPortableDatabaseIdentifierKey)).size !==
|
|
163
|
+
columns.length
|
|
164
|
+
) {
|
|
165
|
+
throw new BetterAuthError(
|
|
166
|
+
`Index on table "${tableName}" resolves more than one field to the same database column.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
columns,
|
|
172
|
+
name: getDatabaseIndexName(tableName, {
|
|
173
|
+
...index,
|
|
174
|
+
fields: columns,
|
|
175
|
+
}),
|
|
176
|
+
unique: index.unique,
|
|
177
|
+
} satisfies ResolvedDBTableIndex;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const definitionsByName = new Map<string, string>();
|
|
181
|
+
const deduplicatedIndexes: ResolvedDBTableIndex[] = [];
|
|
182
|
+
for (const index of resolvedIndexes) {
|
|
183
|
+
const definition = JSON.stringify([index.columns, index.unique ?? false]);
|
|
184
|
+
const identifierKey = getPortableDatabaseIdentifierKey(index.name);
|
|
185
|
+
const existingDefinition = definitionsByName.get(identifierKey);
|
|
186
|
+
if (existingDefinition && existingDefinition !== definition) {
|
|
187
|
+
throw new BetterAuthError(
|
|
188
|
+
`Database index name "${index.name}" identifies more than one index on table "${tableName}".`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (existingDefinition) continue;
|
|
192
|
+
definitionsByName.set(identifierKey, definition);
|
|
193
|
+
deduplicatedIndexes.push(index);
|
|
194
|
+
}
|
|
195
|
+
return deduplicatedIndexes;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Returns a safe generated string length for a column across all of its table
|
|
200
|
+
* indexes in byte-limited SQL dialects.
|
|
201
|
+
*/
|
|
202
|
+
export function getDatabaseIndexStringLength({
|
|
203
|
+
columnName,
|
|
204
|
+
dialect,
|
|
205
|
+
fields,
|
|
206
|
+
indexes,
|
|
207
|
+
}: {
|
|
208
|
+
columnName: string;
|
|
209
|
+
dialect: BoundedDatabaseIndexDialect;
|
|
210
|
+
fields: Readonly<Record<string, DBFieldAttribute>>;
|
|
211
|
+
indexes: readonly ResolvedDBTableIndex[];
|
|
212
|
+
}): number | undefined {
|
|
213
|
+
const fieldsByColumn = new Map(
|
|
214
|
+
Object.entries(fields).map(([fieldName, field]) => [
|
|
215
|
+
field.fieldName || fieldName,
|
|
216
|
+
field,
|
|
217
|
+
]),
|
|
218
|
+
);
|
|
219
|
+
const containingIndexes = indexes.filter((index) =>
|
|
220
|
+
index.columns.includes(columnName),
|
|
221
|
+
);
|
|
222
|
+
if (containingIndexes.length === 0) return undefined;
|
|
223
|
+
|
|
224
|
+
const byteBudget = dialect === "mysql" ? 3072 : 1700;
|
|
225
|
+
const bytesPerCharacter = dialect === "mysql" ? 4 : 1;
|
|
226
|
+
const defaultLength = dialect === "mysql" ? 191 : 255;
|
|
227
|
+
return containingIndexes.reduce((length, index) => {
|
|
228
|
+
const stringColumnCount = index.columns.filter((column) => {
|
|
229
|
+
const type = fieldsByColumn.get(column)?.type;
|
|
230
|
+
return type === "string" || Array.isArray(type);
|
|
231
|
+
}).length;
|
|
232
|
+
if (stringColumnCount === 0) return length;
|
|
233
|
+
const nonStringColumnBytes =
|
|
234
|
+
(index.columns.length - stringColumnCount) * 16;
|
|
235
|
+
const safeLength = Math.floor(
|
|
236
|
+
Math.max(1, byteBudget - nonStringColumnBytes) /
|
|
237
|
+
bytesPerCharacter /
|
|
238
|
+
stringColumnCount,
|
|
239
|
+
);
|
|
240
|
+
return Math.min(length, safeLength);
|
|
241
|
+
}, defaultLength);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolves and validates every table index as one portable database schema.
|
|
246
|
+
*
|
|
247
|
+
* Index names are schema-wide because SQLite and PostgreSQL do not scope them
|
|
248
|
+
* to an individual table.
|
|
249
|
+
*/
|
|
250
|
+
export function resolveDatabaseSchemaIndexes(
|
|
251
|
+
sources: readonly DBTableIndexSource[],
|
|
252
|
+
): ReadonlyMap<string, readonly ResolvedDBTableIndex[]> {
|
|
253
|
+
const mergedSourcesByTable = new Map<
|
|
254
|
+
string,
|
|
255
|
+
{
|
|
256
|
+
fields: Record<string, DBFieldAttribute>;
|
|
257
|
+
indexes: DBTableIndex[];
|
|
258
|
+
tableName: string;
|
|
259
|
+
}
|
|
260
|
+
>();
|
|
261
|
+
for (const source of sources) {
|
|
262
|
+
const existingSource = mergedSourcesByTable.get(source.tableName);
|
|
263
|
+
if (existingSource) {
|
|
264
|
+
if (
|
|
265
|
+
existingSource.indexes.length > 0 ||
|
|
266
|
+
(source.indexes?.length ?? 0) > 0
|
|
267
|
+
) {
|
|
268
|
+
throw new BetterAuthError(
|
|
269
|
+
`Database schema resolves more than one indexed logical table to "${source.tableName}". Define table-level indexes through one logical schema key instead of aliasing multiple keys to the same database table.`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
Object.assign(existingSource.fields, source.fields);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const mergedSource: {
|
|
276
|
+
fields: Record<string, DBFieldAttribute>;
|
|
277
|
+
indexes: DBTableIndex[];
|
|
278
|
+
tableName: string;
|
|
279
|
+
} = {
|
|
280
|
+
fields: {},
|
|
281
|
+
indexes: [],
|
|
282
|
+
tableName: source.tableName,
|
|
283
|
+
};
|
|
284
|
+
Object.assign(mergedSource.fields, source.fields);
|
|
285
|
+
mergedSource.indexes.push(...(source.indexes ?? []));
|
|
286
|
+
mergedSourcesByTable.set(source.tableName, mergedSource);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const indexesByTable = new Map<string, ResolvedDBTableIndex[]>();
|
|
290
|
+
const indexOwnerByName = new Map<string, string>();
|
|
291
|
+
const tableNamesByIdentifier = new Map(
|
|
292
|
+
[...mergedSourcesByTable.keys()].map((tableName) => [
|
|
293
|
+
getPortableDatabaseIdentifierKey(tableName),
|
|
294
|
+
tableName,
|
|
295
|
+
]),
|
|
296
|
+
);
|
|
297
|
+
const fieldIndexOwnerByName = new Map<string, string>();
|
|
298
|
+
|
|
299
|
+
for (const source of mergedSourcesByTable.values()) {
|
|
300
|
+
for (const [fieldName, field] of Object.entries(source.fields)) {
|
|
301
|
+
if (!field.index && !field.unique) continue;
|
|
302
|
+
const indexName = getDatabaseFieldIndexName(
|
|
303
|
+
source.tableName,
|
|
304
|
+
field.fieldName || fieldName,
|
|
305
|
+
field.unique ?? false,
|
|
306
|
+
);
|
|
307
|
+
const identifierKey = getPortableDatabaseIdentifierKey(indexName);
|
|
308
|
+
if (tableNamesByIdentifier.has(identifierKey)) {
|
|
309
|
+
throw new BetterAuthError(
|
|
310
|
+
`Database index name "${indexName}" conflicts with a table name. Index and table names must be unique across the schema.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
const existingOwner = fieldIndexOwnerByName.get(identifierKey);
|
|
314
|
+
if (existingOwner) {
|
|
315
|
+
throw new BetterAuthError(
|
|
316
|
+
`Database field-level index name "${indexName}" is used by both table "${existingOwner}" and table "${source.tableName}".`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
fieldIndexOwnerByName.set(identifierKey, source.tableName);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
for (const source of mergedSourcesByTable.values()) {
|
|
324
|
+
const resolvedIndexes = resolveDatabaseTableIndexes(source);
|
|
325
|
+
indexesByTable.set(source.tableName, [...resolvedIndexes]);
|
|
326
|
+
|
|
327
|
+
for (const index of resolvedIndexes) {
|
|
328
|
+
const identifierKey = getPortableDatabaseIdentifierKey(index.name);
|
|
329
|
+
if (tableNamesByIdentifier.has(identifierKey)) {
|
|
330
|
+
throw new BetterAuthError(
|
|
331
|
+
`Database index name "${index.name}" conflicts with a table name. Index and table names must be unique across the schema.`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const fieldIndexOwner = fieldIndexOwnerByName.get(identifierKey);
|
|
335
|
+
if (fieldIndexOwner) {
|
|
336
|
+
throw new BetterAuthError(
|
|
337
|
+
`Database index name "${index.name}" is already reserved by field-level index metadata on table "${fieldIndexOwner}". Remove the duplicate table-level index or give it a distinct name.`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
const indexOwner = indexOwnerByName.get(identifierKey);
|
|
341
|
+
if (indexOwner && indexOwner !== source.tableName) {
|
|
342
|
+
throw new BetterAuthError(
|
|
343
|
+
`Database index name "${index.name}" is used by both table "${indexOwner}" and table "${source.tableName}". Index names must be unique across the schema.`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
indexOwnerByName.set(identifierKey, source.tableName);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return indexesByTable;
|
|
351
|
+
}
|
package/src/db/get-tables.ts
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import type { BetterAuthOptions } from "../types";
|
|
2
|
-
import
|
|
2
|
+
import { resolveDatabaseSchemaIndexes } from "./database-index";
|
|
3
|
+
import type {
|
|
4
|
+
BetterAuthDBSchema,
|
|
5
|
+
DBFieldAttribute,
|
|
6
|
+
DBTableIndex,
|
|
7
|
+
} from "./type";
|
|
3
8
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
)
|
|
9
|
+
function mergeTableIndexes(
|
|
10
|
+
...indexCollections: ReadonlyArray<readonly DBTableIndex[] | undefined>
|
|
11
|
+
) {
|
|
12
|
+
const indexes: DBTableIndex[] = [];
|
|
13
|
+
const seenIndexDefinitions = new Set<string>();
|
|
14
|
+
for (const index of indexCollections.flatMap(
|
|
15
|
+
(collection) => collection ?? [],
|
|
16
|
+
)) {
|
|
17
|
+
const definition = JSON.stringify([
|
|
18
|
+
index.name ?? null,
|
|
19
|
+
index.fields,
|
|
20
|
+
index.unique ?? false,
|
|
21
|
+
]);
|
|
22
|
+
if (seenIndexDefinitions.has(definition)) continue;
|
|
23
|
+
seenIndexDefinitions.add(definition);
|
|
24
|
+
indexes.push(index);
|
|
25
|
+
}
|
|
26
|
+
return indexes;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const buildAuthTables = (options: BetterAuthOptions): BetterAuthDBSchema => {
|
|
7
30
|
const pluginSchema = (options.plugins ?? []).reduce(
|
|
8
31
|
(acc, plugin) => {
|
|
9
32
|
const schema = plugin.schema;
|
|
@@ -14,6 +37,7 @@ export const getAuthTables = (
|
|
|
14
37
|
...acc[key]?.fields,
|
|
15
38
|
...value.fields,
|
|
16
39
|
},
|
|
40
|
+
indexes: mergeTableIndexes(acc[key]?.indexes, value.indexes),
|
|
17
41
|
modelName: value.modelName || key,
|
|
18
42
|
disableMigrations:
|
|
19
43
|
value.disableMigration ?? acc[key]?.disableMigrations,
|
|
@@ -25,6 +49,7 @@ export const getAuthTables = (
|
|
|
25
49
|
string,
|
|
26
50
|
{
|
|
27
51
|
fields: Record<string, DBFieldAttribute>;
|
|
52
|
+
indexes?: readonly DBTableIndex[] | undefined;
|
|
28
53
|
modelName: string;
|
|
29
54
|
disableMigrations?: boolean | undefined;
|
|
30
55
|
}
|
|
@@ -64,6 +89,7 @@ export const getAuthTables = (
|
|
|
64
89
|
const verificationTable = {
|
|
65
90
|
verification: {
|
|
66
91
|
modelName: options.verification?.modelName || "verification",
|
|
92
|
+
indexes: verification?.indexes,
|
|
67
93
|
fields: {
|
|
68
94
|
identifier: {
|
|
69
95
|
type: "string",
|
|
@@ -104,6 +130,7 @@ export const getAuthTables = (
|
|
|
104
130
|
const sessionTable = {
|
|
105
131
|
session: {
|
|
106
132
|
modelName: options.session?.modelName || "session",
|
|
133
|
+
indexes: session?.indexes,
|
|
107
134
|
fields: {
|
|
108
135
|
expiresAt: {
|
|
109
136
|
type: "date",
|
|
@@ -142,7 +169,18 @@ export const getAuthTables = (
|
|
|
142
169
|
type: "string",
|
|
143
170
|
fieldName: options.session?.fields?.userId || "userId",
|
|
144
171
|
references: {
|
|
145
|
-
|
|
172
|
+
// Use the canonical user schema key here rather than
|
|
173
|
+
// `options.user.modelName`. Downstream consumers (e.g.
|
|
174
|
+
// `getSchema`, `getMigrations`, and the runtime adapter
|
|
175
|
+
// resolvers) treat `references.model` as a schema key
|
|
176
|
+
// and look it up via `tables[references.model]` /
|
|
177
|
+
// `getDefaultModelName`. Writing the modelName alias
|
|
178
|
+
// here would collide when a user picks a modelName that
|
|
179
|
+
// matches another schema key (for example
|
|
180
|
+
// `user.modelName = "account"`), causing the FK to
|
|
181
|
+
// resolve to the wrong table.
|
|
182
|
+
// @see https://github.com/better-auth/better-auth/issues/8111
|
|
183
|
+
model: "user",
|
|
146
184
|
field: "id",
|
|
147
185
|
onDelete: "cascade",
|
|
148
186
|
},
|
|
@@ -156,9 +194,10 @@ export const getAuthTables = (
|
|
|
156
194
|
},
|
|
157
195
|
} satisfies BetterAuthDBSchema;
|
|
158
196
|
|
|
159
|
-
|
|
197
|
+
const authTables = {
|
|
160
198
|
user: {
|
|
161
199
|
modelName: options.user?.modelName || "user",
|
|
200
|
+
indexes: user?.indexes,
|
|
162
201
|
fields: {
|
|
163
202
|
name: {
|
|
164
203
|
type: "string",
|
|
@@ -211,11 +250,26 @@ export const getAuthTables = (
|
|
|
211
250
|
: {}),
|
|
212
251
|
account: {
|
|
213
252
|
modelName: options.account?.modelName || "account",
|
|
253
|
+
indexes: mergeTableIndexes(
|
|
254
|
+
[
|
|
255
|
+
{
|
|
256
|
+
fields: ["issuer", "providerAccountId"],
|
|
257
|
+
unique: true,
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
account?.indexes,
|
|
261
|
+
),
|
|
214
262
|
fields: {
|
|
215
|
-
|
|
263
|
+
issuer: {
|
|
264
|
+
type: "string",
|
|
265
|
+
required: true,
|
|
266
|
+
fieldName: options.account?.fields?.issuer || "issuer",
|
|
267
|
+
},
|
|
268
|
+
providerAccountId: {
|
|
216
269
|
type: "string",
|
|
217
270
|
required: true,
|
|
218
|
-
fieldName:
|
|
271
|
+
fieldName:
|
|
272
|
+
options.account?.fields?.providerAccountId || "providerAccountId",
|
|
219
273
|
},
|
|
220
274
|
providerId: {
|
|
221
275
|
type: "string",
|
|
@@ -225,7 +279,11 @@ export const getAuthTables = (
|
|
|
225
279
|
userId: {
|
|
226
280
|
type: "string",
|
|
227
281
|
references: {
|
|
228
|
-
|
|
282
|
+
// See note on `session.userId.references.model` above:
|
|
283
|
+
// always use the canonical user schema key so the FK
|
|
284
|
+
// target survives `user.modelName` aliasing.
|
|
285
|
+
// @see https://github.com/better-auth/better-auth/issues/8111
|
|
286
|
+
model: "user",
|
|
229
287
|
field: "id",
|
|
230
288
|
onDelete: "cascade",
|
|
231
289
|
},
|
|
@@ -301,4 +359,26 @@ export const getAuthTables = (
|
|
|
301
359
|
...pluginTables,
|
|
302
360
|
...(shouldAddRateLimitTable ? rateLimitTable : {}),
|
|
303
361
|
} satisfies BetterAuthDBSchema;
|
|
362
|
+
|
|
363
|
+
return authTables;
|
|
304
364
|
};
|
|
365
|
+
|
|
366
|
+
export function getAuthTablesWithResolvedIndexes(options: BetterAuthOptions) {
|
|
367
|
+
const tables = buildAuthTables(options);
|
|
368
|
+
const indexesByTable = resolveDatabaseSchemaIndexes(
|
|
369
|
+
Object.values(tables)
|
|
370
|
+
.filter(
|
|
371
|
+
(table) => !("disableMigrations" in table) || !table.disableMigrations,
|
|
372
|
+
)
|
|
373
|
+
.map((table) => ({
|
|
374
|
+
fields: table.fields,
|
|
375
|
+
indexes: "indexes" in table ? table.indexes : undefined,
|
|
376
|
+
tableName: table.modelName,
|
|
377
|
+
})),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
return { indexesByTable, tables };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export const getAuthTables = (options: BetterAuthOptions): BetterAuthDBSchema =>
|
|
384
|
+
getAuthTablesWithResolvedIndexes(options).tables;
|
package/src/db/index.ts
CHANGED
|
@@ -2,8 +2,11 @@ export { getAuthTables } from "./get-tables";
|
|
|
2
2
|
export type { BetterAuthPluginDBSchema } from "./plugin";
|
|
3
3
|
export {
|
|
4
4
|
type Account,
|
|
5
|
+
type AccountKey,
|
|
5
6
|
accountSchema,
|
|
6
7
|
type BaseAccount,
|
|
8
|
+
createLocalAccountIssuer,
|
|
9
|
+
createOAuthAccountIssuer,
|
|
7
10
|
} from "./schema/account";
|
|
8
11
|
export {
|
|
9
12
|
type BaseRateLimit,
|
|
@@ -29,6 +32,7 @@ export type {
|
|
|
29
32
|
DBFieldAttributeConfig,
|
|
30
33
|
DBFieldType,
|
|
31
34
|
DBPrimitive,
|
|
35
|
+
DBTableIndex,
|
|
32
36
|
InferDBFieldInput,
|
|
33
37
|
InferDBFieldOutput,
|
|
34
38
|
InferDBFieldsFromOptions,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type BoundedDatabaseIndexDialect,
|
|
3
|
+
type DBTableIndexSource,
|
|
4
|
+
getDatabaseFieldIndexName,
|
|
5
|
+
getDatabaseIndexName,
|
|
6
|
+
getDatabaseIndexStringLength,
|
|
7
|
+
getPortableDatabaseIdentifierKey,
|
|
8
|
+
type ResolvedDBTableIndex,
|
|
9
|
+
resolveDatabaseSchemaIndexes,
|
|
10
|
+
resolveDatabaseTableIndexes,
|
|
11
|
+
} from "./database-index";
|
|
12
|
+
export { getAuthTablesWithResolvedIndexes } from "./get-tables";
|
package/src/db/plugin.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { DBFieldAttribute } from "./type";
|
|
1
|
+
import type { DBFieldAttribute, DBTableIndex } from "./type";
|
|
2
2
|
|
|
3
3
|
export type BetterAuthPluginDBSchema = {
|
|
4
4
|
[table in string]: {
|
|
5
5
|
fields: {
|
|
6
6
|
[field: string]: DBFieldAttribute;
|
|
7
7
|
};
|
|
8
|
+
/** Table-level indexes, including compound indexes. */
|
|
9
|
+
indexes?: readonly DBTableIndex[] | undefined;
|
|
8
10
|
disableMigration?: boolean | undefined;
|
|
9
11
|
modelName?: string | undefined;
|
|
10
12
|
};
|
package/src/db/schema/account.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { coreSchema } from "./shared";
|
|
|
9
9
|
|
|
10
10
|
export const accountSchema = coreSchema.extend({
|
|
11
11
|
providerId: z.string(),
|
|
12
|
-
|
|
12
|
+
issuer: z.string(),
|
|
13
|
+
providerAccountId: z.string(),
|
|
13
14
|
userId: z.coerce.string(),
|
|
14
15
|
accessToken: z.string().nullish(),
|
|
15
16
|
refreshToken: z.string().nullish(),
|
|
@@ -37,6 +38,31 @@ export const accountSchema = coreSchema.extend({
|
|
|
37
38
|
|
|
38
39
|
export type BaseAccount = z.infer<typeof accountSchema>;
|
|
39
40
|
|
|
41
|
+
/** The stable provider-side key used to recognize an account. */
|
|
42
|
+
export type AccountKey = Readonly<
|
|
43
|
+
Pick<BaseAccount, "issuer" | "providerAccountId">
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
function encodeAccountIssuerProviderId(providerId: string): string {
|
|
47
|
+
return encodeURIComponent(providerId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates the synthetic issuer used by providers without an issuer of their own.
|
|
52
|
+
*/
|
|
53
|
+
export function createLocalAccountIssuer(providerId: string): string {
|
|
54
|
+
return `local:${encodeAccountIssuerProviderId(providerId)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates the synthetic issuer used by OAuth providers without an issuer of
|
|
59
|
+
* their own. OAuth identities use a distinct namespace so a provider ID
|
|
60
|
+
* cannot collide with an internal local authentication method.
|
|
61
|
+
*/
|
|
62
|
+
export function createOAuthAccountIssuer(providerId: string): string {
|
|
63
|
+
return `local:oauth:${encodeAccountIssuerProviderId(providerId)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
40
66
|
/**
|
|
41
67
|
* Account schema type used by better-auth, note that it's possible that account could have additional fields
|
|
42
68
|
*/
|
package/src/db/schema/user.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { coreSchema } from "./shared";
|
|
|
9
9
|
export const userSchema = coreSchema.extend({
|
|
10
10
|
// TODO(#9124): widen to nullish in v2. OAuth providers (Discord phone-only,
|
|
11
11
|
// Apple subsequent sign-ins, etc.) can legitimately omit email; identity
|
|
12
|
-
// must key on (
|
|
12
|
+
// must key on (issuer, providerAccountId) per OpenID Connect Core §5.7.
|
|
13
13
|
email: z.string().transform((val) => val.toLowerCase()),
|
|
14
14
|
emailVerified: z.boolean().default(false),
|
|
15
15
|
name: z.string(),
|
package/src/db/type.ts
CHANGED
|
@@ -281,6 +281,16 @@ export type DBFieldAttribute<T extends DBFieldType = DBFieldType> = {
|
|
|
281
281
|
type: T;
|
|
282
282
|
} & DBFieldAttributeConfig;
|
|
283
283
|
|
|
284
|
+
/** A database index spanning one or more logical schema fields. */
|
|
285
|
+
export interface DBTableIndex {
|
|
286
|
+
/** One to sixteen logical field names included in the index, in index order. */
|
|
287
|
+
fields: readonly [string, ...string[]];
|
|
288
|
+
/** Portable database index name of at most 63 UTF-8 bytes. */
|
|
289
|
+
name?: string | undefined;
|
|
290
|
+
/** Whether the indexed field tuple must be unique. */
|
|
291
|
+
unique?: boolean | undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
284
294
|
export type BetterAuthDBSchema = Record<
|
|
285
295
|
string,
|
|
286
296
|
{
|
|
@@ -292,6 +302,8 @@ export type BetterAuthDBSchema = Record<
|
|
|
292
302
|
* The fields of the table
|
|
293
303
|
*/
|
|
294
304
|
fields: Record<string, DBFieldAttribute>;
|
|
305
|
+
/** Table-level indexes, including compound indexes. */
|
|
306
|
+
indexes?: readonly DBTableIndex[] | undefined;
|
|
295
307
|
/**
|
|
296
308
|
* Whether to disable migrations for this table
|
|
297
309
|
* @default false
|