@better-auth/core 1.7.0-beta.3 → 1.7.0-beta.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/context/global.mjs +1 -1
- package/dist/db/adapter/factory.mjs +63 -1
- package/dist/db/adapter/index.d.mts +35 -1
- package/dist/db/adapter/types.d.mts +1 -1
- package/dist/db/type.d.mts +12 -0
- package/dist/error/codes.d.mts +1 -0
- package/dist/error/codes.mjs +1 -0
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/authorization-params.d.mts +12 -0
- package/dist/oauth2/authorization-params.mjs +12 -0
- package/dist/oauth2/basic-credentials.d.mts +30 -0
- package/dist/oauth2/basic-credentials.mjs +64 -0
- package/dist/oauth2/client-assertion.d.mts +38 -22
- package/dist/oauth2/client-assertion.mjs +63 -28
- package/dist/oauth2/client-credentials-token.d.mts +19 -40
- package/dist/oauth2/client-credentials-token.mjs +18 -29
- package/dist/oauth2/create-authorization-url.d.mts +9 -1
- package/dist/oauth2/create-authorization-url.mjs +23 -5
- package/dist/oauth2/index.d.mts +10 -7
- package/dist/oauth2/index.mjs +9 -7
- package/dist/oauth2/oauth-provider.d.mts +21 -2
- package/dist/oauth2/refresh-access-token.d.mts +20 -40
- package/dist/oauth2/refresh-access-token.mjs +19 -32
- package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
- package/dist/oauth2/token-endpoint-auth.mjs +89 -0
- package/dist/oauth2/utils.d.mts +9 -1
- package/dist/oauth2/utils.mjs +12 -1
- package/dist/oauth2/validate-authorization-code.d.mts +17 -52
- package/dist/oauth2/validate-authorization-code.mjs +17 -30
- package/dist/oauth2/verify.mjs +15 -5
- package/dist/social-providers/apple.d.mts +4 -18
- package/dist/social-providers/apple.mjs +14 -3
- package/dist/social-providers/atlassian.d.mts +3 -1
- package/dist/social-providers/atlassian.mjs +5 -2
- package/dist/social-providers/cognito.d.mts +16 -1
- package/dist/social-providers/cognito.mjs +6 -2
- package/dist/social-providers/discord.d.mts +4 -2
- package/dist/social-providers/discord.mjs +16 -3
- package/dist/social-providers/dropbox.d.mts +3 -1
- package/dist/social-providers/dropbox.mjs +5 -4
- package/dist/social-providers/facebook.d.mts +3 -1
- package/dist/social-providers/facebook.mjs +5 -2
- package/dist/social-providers/figma.d.mts +3 -1
- package/dist/social-providers/figma.mjs +3 -2
- package/dist/social-providers/github.d.mts +3 -1
- package/dist/social-providers/github.mjs +5 -4
- package/dist/social-providers/gitlab.d.mts +3 -1
- package/dist/social-providers/gitlab.mjs +3 -2
- package/dist/social-providers/google.d.mts +3 -1
- package/dist/social-providers/google.mjs +5 -2
- package/dist/social-providers/huggingface.d.mts +3 -1
- package/dist/social-providers/huggingface.mjs +3 -2
- package/dist/social-providers/index.d.mts +104 -36
- package/dist/social-providers/kakao.d.mts +3 -1
- package/dist/social-providers/kakao.mjs +3 -2
- package/dist/social-providers/kick.d.mts +3 -1
- package/dist/social-providers/kick.mjs +3 -2
- package/dist/social-providers/line.d.mts +3 -1
- package/dist/social-providers/line.mjs +3 -2
- package/dist/social-providers/linear.d.mts +3 -1
- package/dist/social-providers/linear.mjs +3 -2
- package/dist/social-providers/linkedin.d.mts +3 -1
- package/dist/social-providers/linkedin.mjs +3 -2
- package/dist/social-providers/microsoft-entra-id.d.mts +2 -1
- package/dist/social-providers/microsoft-entra-id.mjs +3 -2
- package/dist/social-providers/naver.d.mts +3 -1
- package/dist/social-providers/naver.mjs +3 -2
- package/dist/social-providers/notion.d.mts +3 -1
- package/dist/social-providers/notion.mjs +5 -2
- package/dist/social-providers/paybin.d.mts +3 -1
- package/dist/social-providers/paybin.mjs +3 -2
- package/dist/social-providers/paypal.d.mts +3 -1
- package/dist/social-providers/paypal.mjs +4 -3
- package/dist/social-providers/polar.d.mts +3 -1
- package/dist/social-providers/polar.mjs +3 -2
- package/dist/social-providers/railway.d.mts +3 -1
- package/dist/social-providers/railway.mjs +3 -2
- package/dist/social-providers/reddit.d.mts +3 -1
- package/dist/social-providers/reddit.mjs +3 -2
- package/dist/social-providers/roblox.d.mts +4 -2
- package/dist/social-providers/roblox.mjs +12 -2
- package/dist/social-providers/salesforce.d.mts +3 -1
- package/dist/social-providers/salesforce.mjs +3 -2
- package/dist/social-providers/slack.d.mts +4 -2
- package/dist/social-providers/slack.mjs +11 -8
- package/dist/social-providers/spotify.d.mts +3 -1
- package/dist/social-providers/spotify.mjs +3 -2
- package/dist/social-providers/tiktok.d.mts +3 -1
- package/dist/social-providers/tiktok.mjs +14 -2
- package/dist/social-providers/twitch.d.mts +3 -1
- package/dist/social-providers/twitch.mjs +3 -2
- package/dist/social-providers/twitter.d.mts +5 -2
- package/dist/social-providers/twitter.mjs +2 -1
- package/dist/social-providers/vercel.d.mts +3 -1
- package/dist/social-providers/vercel.mjs +3 -2
- package/dist/social-providers/vk.d.mts +3 -1
- package/dist/social-providers/vk.mjs +3 -2
- package/dist/social-providers/wechat.d.mts +3 -1
- package/dist/social-providers/wechat.mjs +7 -1
- package/dist/social-providers/zoom.d.mts +4 -2
- package/dist/social-providers/zoom.mjs +10 -17
- package/dist/types/context.d.mts +23 -3
- package/dist/types/init-options.d.mts +29 -5
- package/dist/utils/ip.d.mts +5 -4
- package/dist/utils/ip.mjs +3 -3
- package/dist/utils/redirect-uri.d.mts +20 -0
- package/dist/utils/redirect-uri.mjs +48 -0
- package/dist/utils/string.d.mts +5 -1
- package/dist/utils/string.mjs +20 -1
- package/dist/utils/url.d.mts +18 -1
- package/dist/utils/url.mjs +30 -1
- package/package.json +9 -8
- package/src/db/adapter/factory.ts +118 -0
- package/src/db/adapter/index.ts +32 -0
- package/src/db/adapter/types.ts +1 -0
- package/src/db/type.ts +12 -0
- package/src/error/codes.ts +1 -0
- package/src/oauth2/authorization-params.ts +28 -0
- package/src/oauth2/basic-credentials.ts +87 -0
- package/src/oauth2/client-assertion.ts +131 -58
- package/src/oauth2/client-credentials-token.ts +48 -72
- package/src/oauth2/create-authorization-url.ts +28 -6
- package/src/oauth2/index.ts +25 -9
- package/src/oauth2/oauth-provider.ts +21 -2
- package/src/oauth2/refresh-access-token.ts +50 -76
- package/src/oauth2/token-endpoint-auth.ts +221 -0
- package/src/oauth2/utils.ts +19 -0
- package/src/oauth2/validate-authorization-code.ts +55 -85
- package/src/oauth2/verify.ts +20 -4
- package/src/social-providers/apple.ts +26 -2
- package/src/social-providers/atlassian.ts +8 -1
- package/src/social-providers/cognito.ts +26 -1
- package/src/social-providers/discord.ts +21 -17
- package/src/social-providers/dropbox.ts +7 -5
- package/src/social-providers/facebook.ts +11 -6
- package/src/social-providers/figma.ts +8 -1
- package/src/social-providers/github.ts +4 -2
- package/src/social-providers/gitlab.ts +2 -0
- package/src/social-providers/google.ts +2 -0
- package/src/social-providers/huggingface.ts +8 -1
- package/src/social-providers/kakao.ts +2 -1
- package/src/social-providers/kick.ts +8 -1
- package/src/social-providers/line.ts +2 -0
- package/src/social-providers/linear.ts +8 -1
- package/src/social-providers/linkedin.ts +2 -0
- package/src/social-providers/microsoft-entra-id.ts +1 -0
- package/src/social-providers/naver.ts +2 -1
- package/src/social-providers/notion.ts +8 -1
- package/src/social-providers/paybin.ts +2 -0
- package/src/social-providers/paypal.ts +7 -1
- package/src/social-providers/polar.ts +8 -1
- package/src/social-providers/railway.ts +8 -1
- package/src/social-providers/reddit.ts +2 -1
- package/src/social-providers/roblox.ts +16 -11
- package/src/social-providers/salesforce.ts +8 -1
- package/src/social-providers/slack.ts +15 -9
- package/src/social-providers/spotify.ts +8 -1
- package/src/social-providers/tiktok.ts +22 -9
- package/src/social-providers/twitch.ts +2 -1
- package/src/social-providers/twitter.ts +1 -0
- package/src/social-providers/vercel.ts +8 -1
- package/src/social-providers/vk.ts +8 -1
- package/src/social-providers/wechat.ts +9 -1
- package/src/social-providers/zoom.ts +15 -19
- package/src/types/context.ts +25 -4
- package/src/types/init-options.ts +29 -5
- package/src/utils/ip.ts +12 -13
- package/src/utils/redirect-uri.ts +54 -0
- package/src/utils/string.ts +37 -0
- package/src/utils/url.ts +28 -0
package/dist/utils/url.mjs
CHANGED
|
@@ -27,5 +27,34 @@ function normalizePathname(requestUrl, basePath) {
|
|
|
27
27
|
if (pathname.startsWith(basePath + "/")) return pathname.slice(basePath.length).replace(/\/+$/, "") || "/";
|
|
28
28
|
return pathname;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Schemes that execute or embed code when navigated to or accepted as a
|
|
32
|
+
* redirect target. These are never safe as an OAuth `redirect_uri` or as a
|
|
33
|
+
* client-side navigation target (`window.location.href`, `location.assign`, ...).
|
|
34
|
+
*/
|
|
35
|
+
const DANGEROUS_URL_SCHEMES = [
|
|
36
|
+
"javascript:",
|
|
37
|
+
"data:",
|
|
38
|
+
"vbscript:"
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Returns `false` only when `value` is an absolute URL using a dangerous scheme
|
|
42
|
+
* (`javascript:`, `data:`, `vbscript:`). Relative URLs (e.g. `/dashboard`) and
|
|
43
|
+
* safe absolute schemes (`http`, `https`, custom app schemes such as
|
|
44
|
+
* `myapp://`) return `true`.
|
|
45
|
+
*
|
|
46
|
+
* Use this to guard browser navigation sinks and any redirect target that may
|
|
47
|
+
* originate from untrusted input. It is intentionally narrow: it blocks code
|
|
48
|
+
* execution schemes without rejecting relative paths or mobile deep links.
|
|
49
|
+
*/
|
|
50
|
+
function isSafeUrlScheme(value) {
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = new URL(value);
|
|
54
|
+
} catch {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return !DANGEROUS_URL_SCHEMES.includes(parsed.protocol);
|
|
58
|
+
}
|
|
30
59
|
//#endregion
|
|
31
|
-
export { normalizePathname };
|
|
60
|
+
export { DANGEROUS_URL_SCHEMES, isSafeUrlScheme, normalizePathname };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/core",
|
|
3
|
-
"version": "1.7.0-beta.
|
|
3
|
+
"version": "1.7.0-beta.4",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -93,12 +93,13 @@
|
|
|
93
93
|
"./instrumentation": {
|
|
94
94
|
"dev-source": "./src/instrumentation/index.ts",
|
|
95
95
|
"types": "./dist/instrumentation/index.d.mts",
|
|
96
|
+
"workerd": "./dist/instrumentation/pure.index.mjs",
|
|
97
|
+
"edge": "./dist/instrumentation/pure.index.mjs",
|
|
98
|
+
"browser": "./dist/instrumentation/pure.index.mjs",
|
|
96
99
|
"node": "./dist/instrumentation/index.mjs",
|
|
97
100
|
"deno": "./dist/instrumentation/index.mjs",
|
|
98
101
|
"bun": "./dist/instrumentation/index.mjs",
|
|
99
|
-
"
|
|
100
|
-
"workerd": "./dist/instrumentation/pure.index.mjs",
|
|
101
|
-
"browser": "./dist/instrumentation/pure.index.mjs",
|
|
102
|
+
"import": "./dist/instrumentation/index.mjs",
|
|
102
103
|
"default": "./dist/instrumentation/index.mjs"
|
|
103
104
|
}
|
|
104
105
|
},
|
|
@@ -151,7 +152,7 @@
|
|
|
151
152
|
"zod": "^4.3.6"
|
|
152
153
|
},
|
|
153
154
|
"devDependencies": {
|
|
154
|
-
"@better-auth/utils": "0.4.
|
|
155
|
+
"@better-auth/utils": "0.4.1",
|
|
155
156
|
"@better-fetch/fetch": "1.1.21",
|
|
156
157
|
"@opentelemetry/api": "^1.9.0",
|
|
157
158
|
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
|
@@ -159,18 +160,18 @@
|
|
|
159
160
|
"better-call": "1.3.5",
|
|
160
161
|
"@cloudflare/workers-types": "^4.20250121.0",
|
|
161
162
|
"jose": "^6.1.3",
|
|
162
|
-
"kysely": "^0.28.
|
|
163
|
+
"kysely": "^0.28.17 || ^0.29.0",
|
|
163
164
|
"nanostores": "^1.1.1",
|
|
164
165
|
"tsdown": "0.21.1"
|
|
165
166
|
},
|
|
166
167
|
"peerDependencies": {
|
|
167
|
-
"@better-auth/utils": "0.4.
|
|
168
|
+
"@better-auth/utils": "0.4.1",
|
|
168
169
|
"@better-fetch/fetch": "1.1.21",
|
|
169
170
|
"@opentelemetry/api": "^1.9.0",
|
|
170
171
|
"better-call": "1.3.5",
|
|
171
172
|
"@cloudflare/workers-types": ">=4",
|
|
172
173
|
"jose": "^6.1.0",
|
|
173
|
-
"kysely": "^0.28.5",
|
|
174
|
+
"kysely": "^0.28.5 || ^0.29.0",
|
|
174
175
|
"nanostores": "^1.0.1"
|
|
175
176
|
},
|
|
176
177
|
"peerDependenciesMeta": {
|
|
@@ -133,6 +133,11 @@ export const createAdapterFactory =
|
|
|
133
133
|
!config.debugLogs.deleteMany
|
|
134
134
|
) {
|
|
135
135
|
return;
|
|
136
|
+
} else if (
|
|
137
|
+
method === "consumeOne" &&
|
|
138
|
+
!config.debugLogs.consumeOne
|
|
139
|
+
) {
|
|
140
|
+
return;
|
|
136
141
|
} else if (method === "count" && !config.debugLogs.count) {
|
|
137
142
|
return;
|
|
138
143
|
}
|
|
@@ -485,6 +490,7 @@ export const createAdapterFactory =
|
|
|
485
490
|
| "updateMany"
|
|
486
491
|
| "delete"
|
|
487
492
|
| "deleteMany"
|
|
493
|
+
| "consumeOne"
|
|
488
494
|
| "count";
|
|
489
495
|
}): W extends undefined ? undefined : CleanedWhere[] => {
|
|
490
496
|
if (!where) return undefined as any;
|
|
@@ -1312,6 +1318,118 @@ export const createAdapterFactory =
|
|
|
1312
1318
|
);
|
|
1313
1319
|
return res;
|
|
1314
1320
|
},
|
|
1321
|
+
consumeOne: async <T>({
|
|
1322
|
+
model: unsafeModel,
|
|
1323
|
+
where: unsafeWhere,
|
|
1324
|
+
}: {
|
|
1325
|
+
model: string;
|
|
1326
|
+
where: Where[];
|
|
1327
|
+
}): Promise<T | null> => {
|
|
1328
|
+
transactionId++;
|
|
1329
|
+
const thisTransactionId = transactionId;
|
|
1330
|
+
const model = getModelName(unsafeModel);
|
|
1331
|
+
const where = transformWhereClause({
|
|
1332
|
+
model: unsafeModel,
|
|
1333
|
+
where: unsafeWhere,
|
|
1334
|
+
action: "consumeOne",
|
|
1335
|
+
});
|
|
1336
|
+
unsafeModel = getDefaultModelName(unsafeModel);
|
|
1337
|
+
debugLog(
|
|
1338
|
+
{ method: "consumeOne" },
|
|
1339
|
+
`${formatTransactionId(thisTransactionId)} ${formatStep(1, 3)}`,
|
|
1340
|
+
`${formatMethod("consumeOne")} ${formatAction("ConsumeOne")}:`,
|
|
1341
|
+
{ model, where },
|
|
1342
|
+
);
|
|
1343
|
+
|
|
1344
|
+
let res: T | null;
|
|
1345
|
+
let resultNeedsOutputTransform = true;
|
|
1346
|
+
if (adapterInstance.consumeOne) {
|
|
1347
|
+
res = await withSpan(
|
|
1348
|
+
`db consumeOne ${model}`,
|
|
1349
|
+
{
|
|
1350
|
+
[ATTR_DB_OPERATION_NAME]: "consumeOne",
|
|
1351
|
+
[ATTR_DB_COLLECTION_NAME]: model,
|
|
1352
|
+
},
|
|
1353
|
+
() => adapterInstance.consumeOne!<T>({ model, where }),
|
|
1354
|
+
);
|
|
1355
|
+
} else {
|
|
1356
|
+
// TODO(consume-one-required): adapters without native `consumeOne`
|
|
1357
|
+
// fall back to `transaction(findMany + deleteMany)`. Race-safe on
|
|
1358
|
+
// engines with real transaction isolation; race window narrows
|
|
1359
|
+
// (does not close) on adapters that fall through to sequential
|
|
1360
|
+
// execution. Remove this branch when consumeOne becomes required.
|
|
1361
|
+
// FIXME(consume-one-nested-transaction): custom adapters without a
|
|
1362
|
+
// native consumeOne have no portable signal for "already inside a
|
|
1363
|
+
// transaction". First-party adapters mark transaction-scoped
|
|
1364
|
+
// adapters as as-is; make that capability explicit in the next
|
|
1365
|
+
// breaking adapter contract.
|
|
1366
|
+
res = await withSpan(
|
|
1367
|
+
`db consumeOne ${model}`,
|
|
1368
|
+
{
|
|
1369
|
+
[ATTR_DB_OPERATION_NAME]: "consumeOne",
|
|
1370
|
+
[ATTR_DB_COLLECTION_NAME]: model,
|
|
1371
|
+
},
|
|
1372
|
+
() =>
|
|
1373
|
+
adapter.transaction(async (trx) => {
|
|
1374
|
+
const rows = await trx.findMany<Record<string, any>>({
|
|
1375
|
+
model: unsafeModel,
|
|
1376
|
+
where: unsafeWhere,
|
|
1377
|
+
limit: 1,
|
|
1378
|
+
});
|
|
1379
|
+
const target = rows[0];
|
|
1380
|
+
if (!target) return null;
|
|
1381
|
+
const deleted = await trx.deleteMany({
|
|
1382
|
+
model: unsafeModel,
|
|
1383
|
+
where: [
|
|
1384
|
+
...unsafeWhere,
|
|
1385
|
+
{
|
|
1386
|
+
field: "id",
|
|
1387
|
+
value: target.id,
|
|
1388
|
+
operator: "eq",
|
|
1389
|
+
connector: "AND",
|
|
1390
|
+
mode: "sensitive",
|
|
1391
|
+
},
|
|
1392
|
+
],
|
|
1393
|
+
});
|
|
1394
|
+
// A non-numeric count coerces to a false miss, so fail loud.
|
|
1395
|
+
if (typeof deleted !== "number") {
|
|
1396
|
+
throw new BetterAuthError(
|
|
1397
|
+
`Adapter "${config.adapterId}" returned a non-numeric value from deleteMany during the consumeOne fallback. Return the number of deleted rows, or implement a native consumeOne for atomic single-use consumption.`,
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
return deleted > 0 ? (target as T) : null;
|
|
1401
|
+
}),
|
|
1402
|
+
);
|
|
1403
|
+
resultNeedsOutputTransform = false;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
debugLog(
|
|
1407
|
+
{ method: "consumeOne" },
|
|
1408
|
+
`${formatTransactionId(thisTransactionId)} ${formatStep(2, 3)}`,
|
|
1409
|
+
`${formatMethod("consumeOne")} ${formatAction("DB Result")}:`,
|
|
1410
|
+
{ model, data: res },
|
|
1411
|
+
);
|
|
1412
|
+
let transformed: any = res;
|
|
1413
|
+
if (
|
|
1414
|
+
!config.disableTransformOutput &&
|
|
1415
|
+
resultNeedsOutputTransform &&
|
|
1416
|
+
res
|
|
1417
|
+
) {
|
|
1418
|
+
transformed = await transformOutput(
|
|
1419
|
+
res as Record<string, any>,
|
|
1420
|
+
unsafeModel,
|
|
1421
|
+
undefined,
|
|
1422
|
+
undefined,
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
debugLog(
|
|
1426
|
+
{ method: "consumeOne" },
|
|
1427
|
+
`${formatTransactionId(thisTransactionId)} ${formatStep(3, 3)}`,
|
|
1428
|
+
`${formatMethod("consumeOne")} ${formatAction("Parsed Result")}:`,
|
|
1429
|
+
{ model, data: transformed },
|
|
1430
|
+
);
|
|
1431
|
+
return transformed as T | null;
|
|
1432
|
+
},
|
|
1315
1433
|
count: async ({
|
|
1316
1434
|
model: unsafeModel,
|
|
1317
1435
|
where: unsafeWhere,
|
package/src/db/adapter/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type DBAdapterDebugLogOption =
|
|
|
15
15
|
findMany?: boolean | undefined;
|
|
16
16
|
delete?: boolean | undefined;
|
|
17
17
|
deleteMany?: boolean | undefined;
|
|
18
|
+
consumeOne?: boolean | undefined;
|
|
18
19
|
count?: boolean | undefined;
|
|
19
20
|
}
|
|
20
21
|
| {
|
|
@@ -211,6 +212,7 @@ export interface DBAdapterFactoryConfig<
|
|
|
211
212
|
| "updateMany"
|
|
212
213
|
| "delete"
|
|
213
214
|
| "deleteMany"
|
|
215
|
+
| "consumeOne"
|
|
214
216
|
| "count";
|
|
215
217
|
/**
|
|
216
218
|
* The model name.
|
|
@@ -445,6 +447,23 @@ export type DBAdapter<Options extends BetterAuthOptions = BetterAuthOptions> = {
|
|
|
445
447
|
}) => Promise<number>;
|
|
446
448
|
delete: <_T>(data: { model: string; where: Where[] }) => Promise<void>;
|
|
447
449
|
deleteMany: (data: { model: string; where: Where[] }) => Promise<number>;
|
|
450
|
+
/**
|
|
451
|
+
* Atomically consume a single row matching the where clause: delete it and
|
|
452
|
+
* return the deleted row, or return `null` if no row matched.
|
|
453
|
+
* Implementations MUST NOT delete any additional rows that also match a
|
|
454
|
+
* non-unique predicate.
|
|
455
|
+
*
|
|
456
|
+
* Under concurrent invocation against the same row, exactly one caller
|
|
457
|
+
* receives the row; subsequent racers receive `null`. This is the
|
|
458
|
+
* race-safe primitive for consuming single-use credentials
|
|
459
|
+
* (verification tokens, authorization codes, one-time tokens).
|
|
460
|
+
*
|
|
461
|
+
* Always defined on the factory-wrapped adapter. When the underlying
|
|
462
|
+
* `CustomAdapter` does not implement `consumeOne`, the factory provides
|
|
463
|
+
* a fallback that wraps `findMany + deleteMany` in `transaction(...)`
|
|
464
|
+
* and returns the row only when the delete reports an affected row.
|
|
465
|
+
*/
|
|
466
|
+
consumeOne: <T>(data: { model: string; where: Where[] }) => Promise<T | null>;
|
|
448
467
|
/**
|
|
449
468
|
* Execute multiple operations in a transaction.
|
|
450
469
|
* If the adapter doesn't support transactions, operations will be executed sequentially.
|
|
@@ -531,6 +550,19 @@ export interface CustomAdapter {
|
|
|
531
550
|
model: string;
|
|
532
551
|
where: CleanedWhere[];
|
|
533
552
|
}) => Promise<number>;
|
|
553
|
+
/**
|
|
554
|
+
* Optional native atomic single-row consume. When omitted, the adapter
|
|
555
|
+
* factory falls back to `transaction(findMany + deleteMany)`.
|
|
556
|
+
* Implementing this method natively (e.g. `DELETE ... RETURNING *`,
|
|
557
|
+
* `findOneAndDelete`, `OUTPUT deleted.*`) gives one round trip and the
|
|
558
|
+
* strongest race-safety guarantee. Implementations must delete at most
|
|
559
|
+
* one matching row. TODO(consume-one-required): tighten to required in the
|
|
560
|
+
* next minor on `next`.
|
|
561
|
+
*/
|
|
562
|
+
consumeOne?: <T>(data: {
|
|
563
|
+
model: string;
|
|
564
|
+
where: CleanedWhere[];
|
|
565
|
+
}) => Promise<T | null>;
|
|
534
566
|
count: ({
|
|
535
567
|
model,
|
|
536
568
|
where,
|
package/src/db/adapter/types.ts
CHANGED
package/src/db/type.ts
CHANGED
|
@@ -311,6 +311,18 @@ export interface SecondaryStorage {
|
|
|
311
311
|
* @returns - Value of the key
|
|
312
312
|
*/
|
|
313
313
|
get: (key: string) => Awaitable<unknown>;
|
|
314
|
+
/**
|
|
315
|
+
* Atomically get a value and delete it from storage.
|
|
316
|
+
*
|
|
317
|
+
* This is optional for backwards compatibility with existing secondary
|
|
318
|
+
* storage implementations. Single-use credential consumers use it when
|
|
319
|
+
* present to avoid a read-then-delete race.
|
|
320
|
+
*
|
|
321
|
+
* TODO(secondary-storage-atomic-consume): make this required in the next
|
|
322
|
+
* breaking release, or require database-backed verification storage for
|
|
323
|
+
* security-sensitive consume paths.
|
|
324
|
+
*/
|
|
325
|
+
getAndDelete?: (key: string) => Awaitable<unknown>;
|
|
314
326
|
set: (
|
|
315
327
|
/**
|
|
316
328
|
* Key to store
|
package/src/error/codes.ts
CHANGED
|
@@ -37,6 +37,7 @@ export const BASE_ERROR_CODES = defineErrorCodes({
|
|
|
37
37
|
USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL:
|
|
38
38
|
"User already exists. Use another email.",
|
|
39
39
|
EMAIL_CAN_NOT_BE_UPDATED: "Email can not be updated",
|
|
40
|
+
CHANGE_EMAIL_DISABLED: "Change email is disabled",
|
|
40
41
|
CREDENTIAL_ACCOUNT_NOT_FOUND: "Credential account not found",
|
|
41
42
|
SESSION_EXPIRED: "Session expired. Re-authenticate to perform this action.",
|
|
42
43
|
FAILED_TO_UNLINK_LAST_ACCOUNT: "You can't unlink your last account",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import {
|
|
3
|
+
RESERVED_AUTHORIZATION_PARAMS,
|
|
4
|
+
RESERVED_AUTHORIZATION_PARAMS_SET,
|
|
5
|
+
} from "./create-authorization-url";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Zod schema for the `additionalParams` field on social sign-in and
|
|
9
|
+
* account-linking request bodies. Rejects any key reserved by the
|
|
10
|
+
* authorization-URL builder (see `RESERVED_AUTHORIZATION_PARAMS`), so
|
|
11
|
+
* a caller cannot overwrite `state`, PKCE, `redirect_uri`, etc.
|
|
12
|
+
*/
|
|
13
|
+
export const additionalAuthorizationParamsSchema = z
|
|
14
|
+
.record(z.string(), z.string())
|
|
15
|
+
.refine(
|
|
16
|
+
(value) =>
|
|
17
|
+
!Object.keys(value).some((key) =>
|
|
18
|
+
RESERVED_AUTHORIZATION_PARAMS_SET.has(key),
|
|
19
|
+
),
|
|
20
|
+
{
|
|
21
|
+
message: `additionalParams cannot include reserved OAuth parameters: ${RESERVED_AUTHORIZATION_PARAMS.join(", ")}`,
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
.meta({
|
|
25
|
+
description:
|
|
26
|
+
"Extra query parameters to append to the provider authorization URL (e.g. Cognito identity_provider, Google hd).",
|
|
27
|
+
})
|
|
28
|
+
.optional();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { base64 } from "@better-auth/utils/base64";
|
|
2
|
+
|
|
3
|
+
// RFC 7235 §2.1: auth scheme is case-insensitive and is followed by one or
|
|
4
|
+
// more SP before the credentials. The trailing capture is everything after
|
|
5
|
+
// the whitespace (may be empty, which downstream checks reject).
|
|
6
|
+
const BASIC_AUTHORIZATION_PATTERN = /^Basic +(.*)$/i;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Encodes a value using `application/x-www-form-urlencoded` per the URL
|
|
10
|
+
* Living Standard. Differs from `encodeURIComponent` in two ways: it escapes
|
|
11
|
+
* `!`, `'`, `(`, and `)`, and it represents space as `+` rather than `%20`.
|
|
12
|
+
* `*` is left unescaped, matching the URL Standard's percent-encode set.
|
|
13
|
+
*/
|
|
14
|
+
function formUrlEncode(value: string): string {
|
|
15
|
+
return new URLSearchParams({ v: value }).toString().slice("v=".length);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Inverse of `formUrlEncode`: decodes a single `application/x-www-form-urlencoded`
|
|
20
|
+
* value, handling both `+` and `%20` as space.
|
|
21
|
+
*/
|
|
22
|
+
function formUrlDecode(value: string): string {
|
|
23
|
+
const decoded = new URLSearchParams(`v=${value}`).get("v");
|
|
24
|
+
if (decoded === null) {
|
|
25
|
+
throw new Error("form-url-encoded value could not be decoded");
|
|
26
|
+
}
|
|
27
|
+
return decoded;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Encodes an OAuth client id and secret as an HTTP Basic credential string.
|
|
32
|
+
*
|
|
33
|
+
* Follows RFC 6749 §2.3.1: both values are `application/x-www-form-urlencoded`
|
|
34
|
+
* prior to base64 encoding. The returned string is the full value of the
|
|
35
|
+
* `Authorization` header, including the `Basic ` prefix.
|
|
36
|
+
*/
|
|
37
|
+
export function encodeBasicCredentials(
|
|
38
|
+
clientId: string,
|
|
39
|
+
clientSecret: string,
|
|
40
|
+
): string {
|
|
41
|
+
const payload = `${formUrlEncode(clientId)}:${formUrlEncode(clientSecret)}`;
|
|
42
|
+
return `Basic ${base64.encode(payload)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decodes an `Authorization: Basic …` header value into its OAuth client id
|
|
47
|
+
* and secret.
|
|
48
|
+
*
|
|
49
|
+
* Scheme matching is case-insensitive and tolerates one or more spaces
|
|
50
|
+
* between the scheme and credentials per RFC 7235 §2.1. The base64 payload
|
|
51
|
+
* is split on the first `:` only, so secrets containing colons round-trip
|
|
52
|
+
* correctly. Each half is form-url-decoded per RFC 6749 §2.3.1, accepting
|
|
53
|
+
* both `+` and `%20` as space. Per the URL Living Standard, invalid
|
|
54
|
+
* percent-escapes pass through as-is; downstream client lookup will fail
|
|
55
|
+
* with `invalid_client` for malformed credentials.
|
|
56
|
+
*
|
|
57
|
+
* Throws when the header is not a Basic credential, when the base64 payload
|
|
58
|
+
* contains no `:`, or when either half is empty.
|
|
59
|
+
*/
|
|
60
|
+
export function decodeBasicCredentials(authorization: string): {
|
|
61
|
+
clientId: string;
|
|
62
|
+
clientSecret: string;
|
|
63
|
+
} {
|
|
64
|
+
const match = authorization.match(BASIC_AUTHORIZATION_PATTERN);
|
|
65
|
+
if (!match) {
|
|
66
|
+
throw new Error("Authorization header is not a Basic credential");
|
|
67
|
+
}
|
|
68
|
+
const encoded = match[1] ?? "";
|
|
69
|
+
const decoded = new TextDecoder().decode(base64.decode(encoded));
|
|
70
|
+
const separatorIndex = decoded.indexOf(":");
|
|
71
|
+
if (separatorIndex === -1) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Basic credential is missing the client id/secret separator",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const rawClientId = decoded.slice(0, separatorIndex);
|
|
77
|
+
const rawClientSecret = decoded.slice(separatorIndex + 1);
|
|
78
|
+
if (!rawClientId || !rawClientSecret) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"Basic credential client id and secret must both be non-empty",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
clientId: formUrlDecode(rawClientId),
|
|
85
|
+
clientSecret: formUrlDecode(rawClientSecret),
|
|
86
|
+
};
|
|
87
|
+
}
|