@better-auth/core 1.7.0-beta.5 → 1.7.0-beta.7
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 +44 -1
- package/dist/api/index.mjs +40 -1
- package/dist/context/global.mjs +1 -1
- package/dist/context/transaction.d.mts +7 -4
- package/dist/context/transaction.mjs +6 -3
- package/dist/db/adapter/factory.mjs +57 -31
- package/dist/db/adapter/index.d.mts +54 -10
- package/dist/db/adapter/types.d.mts +1 -1
- package/dist/db/type.d.mts +12 -7
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/create-authorization-url.d.mts +3 -1
- package/dist/oauth2/create-authorization-url.mjs +3 -1
- package/dist/oauth2/dpop.d.mts +142 -0
- package/dist/oauth2/dpop.mjs +246 -0
- package/dist/oauth2/index.d.mts +4 -3
- package/dist/oauth2/index.mjs +3 -2
- package/dist/oauth2/oauth-provider.d.mts +37 -3
- package/dist/oauth2/refresh-access-token.mjs +15 -1
- package/dist/oauth2/verify.d.mts +74 -15
- package/dist/oauth2/verify.mjs +172 -20
- package/dist/social-providers/apple.d.mts +2 -0
- package/dist/social-providers/atlassian.d.mts +2 -0
- package/dist/social-providers/cognito.d.mts +2 -0
- package/dist/social-providers/discord.d.mts +2 -0
- package/dist/social-providers/dropbox.d.mts +2 -0
- package/dist/social-providers/facebook.d.mts +2 -0
- package/dist/social-providers/figma.d.mts +2 -0
- package/dist/social-providers/github.d.mts +2 -0
- package/dist/social-providers/gitlab.d.mts +2 -0
- package/dist/social-providers/google.d.mts +2 -0
- package/dist/social-providers/huggingface.d.mts +2 -0
- package/dist/social-providers/index.d.mts +71 -0
- package/dist/social-providers/kakao.d.mts +2 -0
- package/dist/social-providers/kick.d.mts +2 -0
- package/dist/social-providers/line.d.mts +2 -0
- package/dist/social-providers/linear.d.mts +2 -0
- package/dist/social-providers/linkedin.d.mts +2 -0
- package/dist/social-providers/microsoft-entra-id.d.mts +12 -0
- package/dist/social-providers/microsoft-entra-id.mjs +17 -2
- package/dist/social-providers/naver.d.mts +2 -0
- package/dist/social-providers/notion.d.mts +2 -0
- package/dist/social-providers/paybin.d.mts +2 -0
- package/dist/social-providers/paypal.d.mts +2 -0
- package/dist/social-providers/polar.d.mts +2 -0
- package/dist/social-providers/railway.d.mts +2 -0
- package/dist/social-providers/reddit.d.mts +2 -0
- package/dist/social-providers/reddit.mjs +1 -1
- package/dist/social-providers/roblox.d.mts +2 -0
- package/dist/social-providers/salesforce.d.mts +2 -0
- package/dist/social-providers/slack.d.mts +2 -0
- package/dist/social-providers/spotify.d.mts +2 -0
- package/dist/social-providers/tiktok.d.mts +2 -0
- package/dist/social-providers/twitch.d.mts +2 -0
- package/dist/social-providers/twitter.d.mts +2 -0
- package/dist/social-providers/vercel.d.mts +2 -0
- package/dist/social-providers/vk.d.mts +2 -0
- package/dist/social-providers/wechat.d.mts +2 -0
- package/dist/social-providers/wechat.mjs +1 -1
- package/dist/social-providers/zoom.d.mts +2 -0
- package/dist/types/context.d.mts +17 -0
- package/dist/types/init-options.d.mts +45 -5
- package/dist/types/plugin-client.d.mts +12 -2
- package/dist/utils/host.d.mts +1 -1
- package/dist/utils/host.mjs +7 -0
- package/dist/utils/url.mjs +4 -3
- package/package.json +5 -5
- package/src/api/index.ts +82 -0
- package/src/context/transaction.ts +45 -12
- package/src/db/adapter/factory.ts +127 -72
- package/src/db/adapter/index.ts +54 -9
- package/src/db/adapter/types.ts +1 -0
- package/src/db/type.ts +12 -7
- package/src/oauth2/create-authorization-url.ts +4 -0
- package/src/oauth2/dpop.ts +568 -0
- package/src/oauth2/index.ts +45 -1
- package/src/oauth2/oauth-provider.ts +40 -2
- package/src/oauth2/refresh-access-token.ts +27 -3
- package/src/oauth2/verify-id-token.ts +2 -0
- package/src/oauth2/verify.ts +329 -66
- package/src/social-providers/microsoft-entra-id.ts +44 -1
- package/src/social-providers/reddit.ts +5 -1
- package/src/social-providers/wechat.ts +8 -1
- package/src/types/context.ts +18 -0
- package/src/types/init-options.ts +40 -8
- package/src/types/plugin-client.ts +16 -2
- package/src/utils/host.ts +25 -1
- package/src/utils/url.ts +10 -4
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { getAsyncLocalStorage } from "@better-auth/core/async_hooks";
|
|
3
3
|
import type { DBAdapter, DBTransactionAdapter } from "../db/adapter";
|
|
4
|
+
import type { BetterAuthOptions } from "../types";
|
|
4
5
|
import { __getBetterAuthGlobal } from "./global";
|
|
5
6
|
|
|
7
|
+
type StoredAdapter = DBTransactionAdapter<BetterAuthOptions>;
|
|
8
|
+
|
|
6
9
|
type HookContext = {
|
|
7
|
-
adapter:
|
|
10
|
+
adapter: StoredAdapter;
|
|
8
11
|
pendingHooks: Array<() => Promise<void>>;
|
|
12
|
+
isTransactionActive: boolean;
|
|
9
13
|
};
|
|
10
14
|
|
|
11
15
|
const ensureAsyncStorage = async () => {
|
|
@@ -27,21 +31,29 @@ export const getCurrentDBAdapterAsyncLocalStorage = async () => {
|
|
|
27
31
|
return ensureAsyncStorage();
|
|
28
32
|
};
|
|
29
33
|
|
|
30
|
-
export const getCurrentAdapter = async
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
export const getCurrentAdapter = async <
|
|
35
|
+
Options extends BetterAuthOptions = BetterAuthOptions,
|
|
36
|
+
>(
|
|
37
|
+
fallback: DBTransactionAdapter<Options>,
|
|
38
|
+
): Promise<DBTransactionAdapter<Options>> => {
|
|
33
39
|
return ensureAsyncStorage()
|
|
34
40
|
.then((als) => {
|
|
35
41
|
const store = als.getStore();
|
|
36
|
-
return
|
|
42
|
+
return (
|
|
43
|
+
(store?.adapter as DBTransactionAdapter<Options> | undefined) ||
|
|
44
|
+
fallback
|
|
45
|
+
);
|
|
37
46
|
})
|
|
38
47
|
.catch(() => {
|
|
39
48
|
return fallback;
|
|
40
49
|
});
|
|
41
50
|
};
|
|
42
51
|
|
|
43
|
-
export const runWithAdapter = async <
|
|
44
|
-
|
|
52
|
+
export const runWithAdapter = async <
|
|
53
|
+
R,
|
|
54
|
+
Options extends BetterAuthOptions = BetterAuthOptions,
|
|
55
|
+
>(
|
|
56
|
+
adapter: DBAdapter<Options>,
|
|
45
57
|
fn: () => R,
|
|
46
58
|
): Promise<R> => {
|
|
47
59
|
let called = false;
|
|
@@ -53,7 +65,14 @@ export const runWithAdapter = async <R>(
|
|
|
53
65
|
let error: unknown;
|
|
54
66
|
let hasError = false;
|
|
55
67
|
try {
|
|
56
|
-
result = await als.run(
|
|
68
|
+
result = await als.run(
|
|
69
|
+
{
|
|
70
|
+
adapter: adapter as unknown as StoredAdapter,
|
|
71
|
+
pendingHooks,
|
|
72
|
+
isTransactionActive: false,
|
|
73
|
+
},
|
|
74
|
+
fn,
|
|
75
|
+
);
|
|
57
76
|
} catch (err) {
|
|
58
77
|
error = err;
|
|
59
78
|
hasError = true;
|
|
@@ -75,21 +94,35 @@ export const runWithAdapter = async <R>(
|
|
|
75
94
|
});
|
|
76
95
|
};
|
|
77
96
|
|
|
78
|
-
export const runWithTransaction = async <
|
|
79
|
-
|
|
97
|
+
export const runWithTransaction = async <
|
|
98
|
+
R,
|
|
99
|
+
Options extends BetterAuthOptions = BetterAuthOptions,
|
|
100
|
+
>(
|
|
101
|
+
adapter: DBAdapter<Options>,
|
|
80
102
|
fn: () => R,
|
|
81
103
|
): Promise<R> => {
|
|
82
|
-
let called =
|
|
104
|
+
let called = false;
|
|
83
105
|
return ensureAsyncStorage()
|
|
84
106
|
.then(async (als) => {
|
|
85
107
|
called = true;
|
|
108
|
+
const store = als.getStore();
|
|
109
|
+
if (store?.isTransactionActive) {
|
|
110
|
+
return fn();
|
|
111
|
+
}
|
|
86
112
|
const pendingHooks: Array<() => Promise<void>> = [];
|
|
87
113
|
let result: Awaited<R>;
|
|
88
114
|
let error: unknown;
|
|
89
115
|
let hasError = false;
|
|
90
116
|
try {
|
|
91
117
|
result = await adapter.transaction(async (trx) => {
|
|
92
|
-
return als.run(
|
|
118
|
+
return als.run(
|
|
119
|
+
{
|
|
120
|
+
adapter: trx as unknown as StoredAdapter,
|
|
121
|
+
pendingHooks,
|
|
122
|
+
isTransactionActive: true,
|
|
123
|
+
},
|
|
124
|
+
fn,
|
|
125
|
+
);
|
|
93
126
|
});
|
|
94
127
|
} catch (e) {
|
|
95
128
|
hasError = true;
|
|
@@ -138,6 +138,11 @@ export const createAdapterFactory =
|
|
|
138
138
|
!config.debugLogs.consumeOne
|
|
139
139
|
) {
|
|
140
140
|
return;
|
|
141
|
+
} else if (
|
|
142
|
+
method === "incrementOne" &&
|
|
143
|
+
!config.debugLogs.incrementOne
|
|
144
|
+
) {
|
|
145
|
+
return;
|
|
141
146
|
} else if (method === "count" && !config.debugLogs.count) {
|
|
142
147
|
return;
|
|
143
148
|
}
|
|
@@ -491,6 +496,7 @@ export const createAdapterFactory =
|
|
|
491
496
|
| "delete"
|
|
492
497
|
| "deleteMany"
|
|
493
498
|
| "consumeOne"
|
|
499
|
+
| "incrementOne"
|
|
494
500
|
| "count";
|
|
495
501
|
}): W extends undefined ? undefined : CleanedWhere[] => {
|
|
496
502
|
if (!where) return undefined as any;
|
|
@@ -1057,6 +1063,14 @@ export const createAdapterFactory =
|
|
|
1057
1063
|
update: data,
|
|
1058
1064
|
}),
|
|
1059
1065
|
);
|
|
1066
|
+
if (
|
|
1067
|
+
typeof updatedCount !== "number" ||
|
|
1068
|
+
!Number.isFinite(updatedCount)
|
|
1069
|
+
) {
|
|
1070
|
+
throw new BetterAuthError(
|
|
1071
|
+
`Adapter "${config.adapterId}" updateMany must return a finite number affected row count.`,
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1060
1074
|
debugLog(
|
|
1061
1075
|
{ method: "updateMany" },
|
|
1062
1076
|
`${formatTransactionId(thisTransactionId)} ${formatStep(3, 4)}`,
|
|
@@ -1341,75 +1355,19 @@ export const createAdapterFactory =
|
|
|
1341
1355
|
{ model, where },
|
|
1342
1356
|
);
|
|
1343
1357
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
-
// `deleteMany` is typed `Promise<number>`. A non-number breaks
|
|
1395
|
-
// the contract, so fail loud. A finite-number check then closes
|
|
1396
|
-
// the NaN/Infinity hole: `NaN > 0` is false and `Infinity > 0`
|
|
1397
|
-
// is true, so a bare `deleted > 0` would misclassify both. Only
|
|
1398
|
-
// a finite positive count proves we won the delete race; any
|
|
1399
|
-
// other value fails closed (returns null) so a single-use row
|
|
1400
|
-
// is never reported consumed without proof.
|
|
1401
|
-
if (typeof deleted !== "number") {
|
|
1402
|
-
throw new BetterAuthError(
|
|
1403
|
-
`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.`,
|
|
1404
|
-
);
|
|
1405
|
-
}
|
|
1406
|
-
return Number.isFinite(deleted) && deleted > 0
|
|
1407
|
-
? (target as T)
|
|
1408
|
-
: null;
|
|
1409
|
-
}),
|
|
1358
|
+
if (typeof adapterInstance.consumeOne !== "function") {
|
|
1359
|
+
throw new BetterAuthError(
|
|
1360
|
+
`Adapter "${config.adapterId}" must implement consumeOne for atomic single-use credential consumption.`,
|
|
1410
1361
|
);
|
|
1411
|
-
resultNeedsOutputTransform = false;
|
|
1412
1362
|
}
|
|
1363
|
+
const res = await withSpan(
|
|
1364
|
+
`db consumeOne ${model}`,
|
|
1365
|
+
{
|
|
1366
|
+
[ATTR_DB_OPERATION_NAME]: "consumeOne",
|
|
1367
|
+
[ATTR_DB_COLLECTION_NAME]: model,
|
|
1368
|
+
},
|
|
1369
|
+
() => adapterInstance.consumeOne<T>({ model, where }),
|
|
1370
|
+
);
|
|
1413
1371
|
|
|
1414
1372
|
debugLog(
|
|
1415
1373
|
{ method: "consumeOne" },
|
|
@@ -1418,11 +1376,7 @@ export const createAdapterFactory =
|
|
|
1418
1376
|
{ model, data: res },
|
|
1419
1377
|
);
|
|
1420
1378
|
let transformed: any = res;
|
|
1421
|
-
if (
|
|
1422
|
-
!config.disableTransformOutput &&
|
|
1423
|
-
resultNeedsOutputTransform &&
|
|
1424
|
-
res
|
|
1425
|
-
) {
|
|
1379
|
+
if (!config.disableTransformOutput && res) {
|
|
1426
1380
|
transformed = await transformOutput(
|
|
1427
1381
|
res as Record<string, any>,
|
|
1428
1382
|
unsafeModel,
|
|
@@ -1438,6 +1392,107 @@ export const createAdapterFactory =
|
|
|
1438
1392
|
);
|
|
1439
1393
|
return transformed as T | null;
|
|
1440
1394
|
},
|
|
1395
|
+
incrementOne: async <T>({
|
|
1396
|
+
model: unsafeModel,
|
|
1397
|
+
where: unsafeWhere,
|
|
1398
|
+
increment: unsafeIncrement,
|
|
1399
|
+
set: unsafeSet,
|
|
1400
|
+
}: {
|
|
1401
|
+
model: string;
|
|
1402
|
+
where: Where[];
|
|
1403
|
+
increment: Record<string, number>;
|
|
1404
|
+
set?: Record<string, unknown> | undefined;
|
|
1405
|
+
}): Promise<T | null> => {
|
|
1406
|
+
const hasIncrement = Object.keys(unsafeIncrement).length > 0;
|
|
1407
|
+
const hasSet = !!unsafeSet && Object.keys(unsafeSet).length > 0;
|
|
1408
|
+
if (!hasIncrement && !hasSet) {
|
|
1409
|
+
// An empty `increment` and empty `set` compiles to `UPDATE ... SET `
|
|
1410
|
+
// with no assignments, which is a syntax error on kysely, drizzle, and
|
|
1411
|
+
// Prisma. Fail fast with an actionable message instead.
|
|
1412
|
+
throw new BetterAuthError(
|
|
1413
|
+
"incrementOne requires a non-empty `increment` or `set`; both were empty.",
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
transactionId++;
|
|
1417
|
+
const thisTransactionId = transactionId;
|
|
1418
|
+
const model = getModelName(unsafeModel);
|
|
1419
|
+
const where = transformWhereClause({
|
|
1420
|
+
model: unsafeModel,
|
|
1421
|
+
where: unsafeWhere,
|
|
1422
|
+
action: "incrementOne",
|
|
1423
|
+
});
|
|
1424
|
+
unsafeModel = getDefaultModelName(unsafeModel);
|
|
1425
|
+
debugLog(
|
|
1426
|
+
{ method: "incrementOne" },
|
|
1427
|
+
`${formatTransactionId(thisTransactionId)} ${formatStep(1, 3)}`,
|
|
1428
|
+
`${formatMethod("incrementOne")} ${formatAction("IncrementOne")}:`,
|
|
1429
|
+
{ model, where, increment: unsafeIncrement, set: unsafeSet },
|
|
1430
|
+
);
|
|
1431
|
+
|
|
1432
|
+
if (typeof adapterInstance.incrementOne !== "function") {
|
|
1433
|
+
throw new BetterAuthError(
|
|
1434
|
+
`Adapter "${config.adapterId}" must implement incrementOne for atomic guarded counter updates.`,
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
const mappedKeys = config.mapKeysTransformInput ?? {};
|
|
1438
|
+
const increment: Record<string, number> = {};
|
|
1439
|
+
for (const [field, delta] of Object.entries(unsafeIncrement)) {
|
|
1440
|
+
increment[
|
|
1441
|
+
mappedKeys[field] || getFieldName({ model: unsafeModel, field })
|
|
1442
|
+
] = delta;
|
|
1443
|
+
}
|
|
1444
|
+
let set: Record<string, unknown> | undefined;
|
|
1445
|
+
if (unsafeSet && !config.disableTransformInput) {
|
|
1446
|
+
set = await transformInput(unsafeSet, unsafeModel, "update");
|
|
1447
|
+
} else {
|
|
1448
|
+
set = unsafeSet;
|
|
1449
|
+
}
|
|
1450
|
+
if (
|
|
1451
|
+
Object.keys(increment).length === 0 &&
|
|
1452
|
+
(!set || Object.keys(set).length === 0)
|
|
1453
|
+
) {
|
|
1454
|
+
throw new BetterAuthError(
|
|
1455
|
+
"incrementOne resolved to an empty update: every increment/set field was unknown to the schema or transformed away.",
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
const res = await withSpan(
|
|
1459
|
+
`db incrementOne ${model}`,
|
|
1460
|
+
{
|
|
1461
|
+
[ATTR_DB_OPERATION_NAME]: "incrementOne",
|
|
1462
|
+
[ATTR_DB_COLLECTION_NAME]: model,
|
|
1463
|
+
},
|
|
1464
|
+
() =>
|
|
1465
|
+
adapterInstance.incrementOne<T>({
|
|
1466
|
+
model,
|
|
1467
|
+
where,
|
|
1468
|
+
increment,
|
|
1469
|
+
set,
|
|
1470
|
+
}),
|
|
1471
|
+
);
|
|
1472
|
+
|
|
1473
|
+
debugLog(
|
|
1474
|
+
{ method: "incrementOne" },
|
|
1475
|
+
`${formatTransactionId(thisTransactionId)} ${formatStep(2, 3)}`,
|
|
1476
|
+
`${formatMethod("incrementOne")} ${formatAction("DB Result")}:`,
|
|
1477
|
+
{ model, data: res },
|
|
1478
|
+
);
|
|
1479
|
+
let transformed: any = res;
|
|
1480
|
+
if (!config.disableTransformOutput && res) {
|
|
1481
|
+
transformed = await transformOutput(
|
|
1482
|
+
res as Record<string, any>,
|
|
1483
|
+
unsafeModel,
|
|
1484
|
+
undefined,
|
|
1485
|
+
undefined,
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
debugLog(
|
|
1489
|
+
{ method: "incrementOne" },
|
|
1490
|
+
`${formatTransactionId(thisTransactionId)} ${formatStep(3, 3)}`,
|
|
1491
|
+
`${formatMethod("incrementOne")} ${formatAction("Parsed Result")}:`,
|
|
1492
|
+
{ model, data: transformed },
|
|
1493
|
+
);
|
|
1494
|
+
return transformed as T | null;
|
|
1495
|
+
},
|
|
1441
1496
|
count: async ({
|
|
1442
1497
|
model: unsafeModel,
|
|
1443
1498
|
where: unsafeWhere,
|
package/src/db/adapter/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type DBAdapterDebugLogOption =
|
|
|
16
16
|
delete?: boolean | undefined;
|
|
17
17
|
deleteMany?: boolean | undefined;
|
|
18
18
|
consumeOne?: boolean | undefined;
|
|
19
|
+
incrementOne?: boolean | undefined;
|
|
19
20
|
count?: boolean | undefined;
|
|
20
21
|
}
|
|
21
22
|
| {
|
|
@@ -213,6 +214,7 @@ export interface DBAdapterFactoryConfig<
|
|
|
213
214
|
| "delete"
|
|
214
215
|
| "deleteMany"
|
|
215
216
|
| "consumeOne"
|
|
217
|
+
| "incrementOne"
|
|
216
218
|
| "count";
|
|
217
219
|
/**
|
|
218
220
|
* The model name.
|
|
@@ -458,12 +460,40 @@ export type DBAdapter<Options extends BetterAuthOptions = BetterAuthOptions> = {
|
|
|
458
460
|
* race-safe primitive for consuming single-use credentials
|
|
459
461
|
* (verification tokens, authorization codes, one-time tokens).
|
|
460
462
|
*
|
|
461
|
-
* Always defined on the factory-wrapped adapter.
|
|
462
|
-
* `CustomAdapter`
|
|
463
|
-
*
|
|
464
|
-
* and returns the row only when the delete reports an affected row.
|
|
463
|
+
* Always defined on the factory-wrapped adapter. The underlying
|
|
464
|
+
* `CustomAdapter` must implement this natively; there is no portable
|
|
465
|
+
* fallback that can guarantee cross-process single-use semantics.
|
|
465
466
|
*/
|
|
466
467
|
consumeOne: <T>(data: { model: string; where: Where[] }) => Promise<T | null>;
|
|
468
|
+
/**
|
|
469
|
+
* Atomically apply signed numeric deltas to a single row matching the where
|
|
470
|
+
* clause. For each entry in `increment`, the operation applies
|
|
471
|
+
* `field = field + delta` in one atomic step; a negative delta decrements.
|
|
472
|
+
*
|
|
473
|
+
* The `where` clause is both the selector AND the guard: comparison
|
|
474
|
+
* operators are honored, so passing `{ field: "remaining", operator: "gt",
|
|
475
|
+
* value: 0 }` only mutates the row while `remaining` is still above zero.
|
|
476
|
+
* When the guard matches no row, the operation makes no change and returns
|
|
477
|
+
* `null`.
|
|
478
|
+
*
|
|
479
|
+
* The optional `set` map assigns absolute values to fields in the same
|
|
480
|
+
* atomic operation, alongside the increments.
|
|
481
|
+
*
|
|
482
|
+
* Returns the updated row, or `null` when the guard matched no row. Under
|
|
483
|
+
* concurrent invocation against the same row, this is the race-safe
|
|
484
|
+
* primitive for guarded counter updates (e.g. decrementing a remaining-uses
|
|
485
|
+
* counter only while it is still positive).
|
|
486
|
+
*
|
|
487
|
+
* Always defined on the factory-wrapped adapter. The underlying
|
|
488
|
+
* `CustomAdapter` must implement this natively; there is no portable
|
|
489
|
+
* fallback that can guarantee guarded counter semantics across runtimes.
|
|
490
|
+
*/
|
|
491
|
+
incrementOne: <T>(data: {
|
|
492
|
+
model: string;
|
|
493
|
+
where: Where[];
|
|
494
|
+
increment: Record<string, number>;
|
|
495
|
+
set?: Record<string, unknown> | undefined;
|
|
496
|
+
}) => Promise<T | null>;
|
|
467
497
|
/**
|
|
468
498
|
* Execute multiple operations in a transaction.
|
|
469
499
|
* If the adapter doesn't support transactions, operations will be executed sequentially.
|
|
@@ -551,17 +581,32 @@ export interface CustomAdapter {
|
|
|
551
581
|
where: CleanedWhere[];
|
|
552
582
|
}) => Promise<number>;
|
|
553
583
|
/**
|
|
554
|
-
*
|
|
555
|
-
* factory falls back to `transaction(findMany + deleteMany)`.
|
|
584
|
+
* Native atomic single-row consume.
|
|
556
585
|
* Implementing this method natively (e.g. `DELETE ... RETURNING *`,
|
|
557
586
|
* `findOneAndDelete`, `OUTPUT deleted.*`) gives one round trip and the
|
|
558
587
|
* strongest race-safety guarantee. Implementations must delete at most
|
|
559
|
-
* one matching row.
|
|
560
|
-
|
|
588
|
+
* one matching row.
|
|
589
|
+
*/
|
|
590
|
+
consumeOne: <T>(data: {
|
|
591
|
+
model: string;
|
|
592
|
+
where: CleanedWhere[];
|
|
593
|
+
}) => Promise<T | null>;
|
|
594
|
+
/**
|
|
595
|
+
* Native atomic guarded counter mutation. Applies
|
|
596
|
+
* `field = field + delta` for each entry in `increment` (negative deltas
|
|
597
|
+
* decrement), with `where` acting as both selector and guard and `set`
|
|
598
|
+
* assigning absolute values in the same operation. Returns the updated row,
|
|
599
|
+
* or `null` when the guard matched no row.
|
|
600
|
+
*
|
|
601
|
+
* Implementing this natively (e.g. `UPDATE ... SET n = n + $delta WHERE ...
|
|
602
|
+
* RETURNING *`) gives one round trip and the strongest race-safety
|
|
603
|
+
* guarantee.
|
|
561
604
|
*/
|
|
562
|
-
|
|
605
|
+
incrementOne: <T>(data: {
|
|
563
606
|
model: string;
|
|
564
607
|
where: CleanedWhere[];
|
|
608
|
+
increment: Record<string, number>;
|
|
609
|
+
set?: Record<string, unknown> | undefined;
|
|
565
610
|
}) => Promise<T | null>;
|
|
566
611
|
count: ({
|
|
567
612
|
model,
|
package/src/db/adapter/types.ts
CHANGED
package/src/db/type.ts
CHANGED
|
@@ -313,16 +313,21 @@ export interface SecondaryStorage {
|
|
|
313
313
|
get: (key: string) => Awaitable<unknown>;
|
|
314
314
|
/**
|
|
315
315
|
* Atomically get a value and delete it from storage.
|
|
316
|
+
*/
|
|
317
|
+
getAndDelete: (key: string) => Awaitable<unknown>;
|
|
318
|
+
/**
|
|
319
|
+
* Atomically increment the counter at `key` by one, returning the
|
|
320
|
+
* post-increment value.
|
|
316
321
|
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
322
|
+
* When the key is absent, it is created with a value of `1` and the given
|
|
323
|
+
* `ttl` (in SECONDS). The TTL is applied only on creation; later increments
|
|
324
|
+
* never extend it, so the counter expires a fixed window after it was first
|
|
325
|
+
* created.
|
|
320
326
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
* security-sensitive consume paths.
|
|
327
|
+
* Required so secondary-storage-backed rate limiting can enforce the limit
|
|
328
|
+
* in one distributed-safe operation.
|
|
324
329
|
*/
|
|
325
|
-
|
|
330
|
+
increment: (key: string, ttl: number) => Awaitable<number>;
|
|
326
331
|
set: (
|
|
327
332
|
/**
|
|
328
333
|
* Key to store
|
|
@@ -15,6 +15,7 @@ export const RESERVED_AUTHORIZATION_PARAMS = [
|
|
|
15
15
|
"response_type",
|
|
16
16
|
"code_challenge",
|
|
17
17
|
"code_challenge_method",
|
|
18
|
+
"nonce",
|
|
18
19
|
"scope",
|
|
19
20
|
] as const;
|
|
20
21
|
|
|
@@ -37,6 +38,7 @@ export async function createAuthorizationURL({
|
|
|
37
38
|
responseType,
|
|
38
39
|
display,
|
|
39
40
|
loginHint,
|
|
41
|
+
nonce,
|
|
40
42
|
hd,
|
|
41
43
|
responseMode,
|
|
42
44
|
additionalParams,
|
|
@@ -56,6 +58,7 @@ export async function createAuthorizationURL({
|
|
|
56
58
|
responseType?: string | undefined;
|
|
57
59
|
display?: string | undefined;
|
|
58
60
|
loginHint?: string | undefined;
|
|
61
|
+
nonce?: string | undefined;
|
|
59
62
|
hd?: string | undefined;
|
|
60
63
|
responseMode?: string | undefined;
|
|
61
64
|
additionalParams?: Record<string, string> | undefined;
|
|
@@ -77,6 +80,7 @@ export async function createAuthorizationURL({
|
|
|
77
80
|
duration && url.searchParams.set("duration", duration);
|
|
78
81
|
display && url.searchParams.set("display", display);
|
|
79
82
|
loginHint && url.searchParams.set("login_hint", loginHint);
|
|
83
|
+
nonce && url.searchParams.set("nonce", nonce);
|
|
80
84
|
prompt && url.searchParams.set("prompt", prompt);
|
|
81
85
|
hd && url.searchParams.set("hd", hd);
|
|
82
86
|
accessType && url.searchParams.set("access_type", accessType);
|