@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.
Files changed (87) hide show
  1. package/dist/api/index.d.mts +44 -1
  2. package/dist/api/index.mjs +40 -1
  3. package/dist/context/global.mjs +1 -1
  4. package/dist/context/transaction.d.mts +7 -4
  5. package/dist/context/transaction.mjs +6 -3
  6. package/dist/db/adapter/factory.mjs +57 -31
  7. package/dist/db/adapter/index.d.mts +54 -10
  8. package/dist/db/adapter/types.d.mts +1 -1
  9. package/dist/db/type.d.mts +12 -7
  10. package/dist/instrumentation/tracer.mjs +1 -1
  11. package/dist/oauth2/create-authorization-url.d.mts +3 -1
  12. package/dist/oauth2/create-authorization-url.mjs +3 -1
  13. package/dist/oauth2/dpop.d.mts +142 -0
  14. package/dist/oauth2/dpop.mjs +246 -0
  15. package/dist/oauth2/index.d.mts +4 -3
  16. package/dist/oauth2/index.mjs +3 -2
  17. package/dist/oauth2/oauth-provider.d.mts +37 -3
  18. package/dist/oauth2/refresh-access-token.mjs +15 -1
  19. package/dist/oauth2/verify.d.mts +74 -15
  20. package/dist/oauth2/verify.mjs +172 -20
  21. package/dist/social-providers/apple.d.mts +2 -0
  22. package/dist/social-providers/atlassian.d.mts +2 -0
  23. package/dist/social-providers/cognito.d.mts +2 -0
  24. package/dist/social-providers/discord.d.mts +2 -0
  25. package/dist/social-providers/dropbox.d.mts +2 -0
  26. package/dist/social-providers/facebook.d.mts +2 -0
  27. package/dist/social-providers/figma.d.mts +2 -0
  28. package/dist/social-providers/github.d.mts +2 -0
  29. package/dist/social-providers/gitlab.d.mts +2 -0
  30. package/dist/social-providers/google.d.mts +2 -0
  31. package/dist/social-providers/huggingface.d.mts +2 -0
  32. package/dist/social-providers/index.d.mts +71 -0
  33. package/dist/social-providers/kakao.d.mts +2 -0
  34. package/dist/social-providers/kick.d.mts +2 -0
  35. package/dist/social-providers/line.d.mts +2 -0
  36. package/dist/social-providers/linear.d.mts +2 -0
  37. package/dist/social-providers/linkedin.d.mts +2 -0
  38. package/dist/social-providers/microsoft-entra-id.d.mts +12 -0
  39. package/dist/social-providers/microsoft-entra-id.mjs +17 -2
  40. package/dist/social-providers/naver.d.mts +2 -0
  41. package/dist/social-providers/notion.d.mts +2 -0
  42. package/dist/social-providers/paybin.d.mts +2 -0
  43. package/dist/social-providers/paypal.d.mts +2 -0
  44. package/dist/social-providers/polar.d.mts +2 -0
  45. package/dist/social-providers/railway.d.mts +2 -0
  46. package/dist/social-providers/reddit.d.mts +2 -0
  47. package/dist/social-providers/reddit.mjs +1 -1
  48. package/dist/social-providers/roblox.d.mts +2 -0
  49. package/dist/social-providers/salesforce.d.mts +2 -0
  50. package/dist/social-providers/slack.d.mts +2 -0
  51. package/dist/social-providers/spotify.d.mts +2 -0
  52. package/dist/social-providers/tiktok.d.mts +2 -0
  53. package/dist/social-providers/twitch.d.mts +2 -0
  54. package/dist/social-providers/twitter.d.mts +2 -0
  55. package/dist/social-providers/vercel.d.mts +2 -0
  56. package/dist/social-providers/vk.d.mts +2 -0
  57. package/dist/social-providers/wechat.d.mts +2 -0
  58. package/dist/social-providers/wechat.mjs +1 -1
  59. package/dist/social-providers/zoom.d.mts +2 -0
  60. package/dist/types/context.d.mts +17 -0
  61. package/dist/types/init-options.d.mts +45 -5
  62. package/dist/types/plugin-client.d.mts +12 -2
  63. package/dist/utils/host.d.mts +1 -1
  64. package/dist/utils/host.mjs +7 -0
  65. package/dist/utils/url.mjs +4 -3
  66. package/package.json +5 -5
  67. package/src/api/index.ts +82 -0
  68. package/src/context/transaction.ts +45 -12
  69. package/src/db/adapter/factory.ts +127 -72
  70. package/src/db/adapter/index.ts +54 -9
  71. package/src/db/adapter/types.ts +1 -0
  72. package/src/db/type.ts +12 -7
  73. package/src/oauth2/create-authorization-url.ts +4 -0
  74. package/src/oauth2/dpop.ts +568 -0
  75. package/src/oauth2/index.ts +45 -1
  76. package/src/oauth2/oauth-provider.ts +40 -2
  77. package/src/oauth2/refresh-access-token.ts +27 -3
  78. package/src/oauth2/verify-id-token.ts +2 -0
  79. package/src/oauth2/verify.ts +329 -66
  80. package/src/social-providers/microsoft-entra-id.ts +44 -1
  81. package/src/social-providers/reddit.ts +5 -1
  82. package/src/social-providers/wechat.ts +8 -1
  83. package/src/types/context.ts +18 -0
  84. package/src/types/init-options.ts +40 -8
  85. package/src/types/plugin-client.ts +16 -2
  86. package/src/utils/host.ts +25 -1
  87. 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: DBTransactionAdapter;
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
- fallback: DBTransactionAdapter,
32
- ): Promise<DBTransactionAdapter> => {
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 store?.adapter || fallback;
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 <R>(
44
- adapter: DBAdapter,
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({ adapter, pendingHooks }, fn);
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 <R>(
79
- adapter: DBAdapter,
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 = true;
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({ adapter: trx, pendingHooks }, fn);
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
- 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
- // `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,
@@ -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. 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.
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
- * Optional native atomic single-row consume. When omitted, the adapter
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. TODO(consume-one-required): tighten to required in the
560
- * next minor on `next`.
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
- consumeOne?: <T>(data: {
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,
@@ -123,6 +123,7 @@ export type AdapterFactoryCustomizeAdapterCreator = (config: {
123
123
  | "delete"
124
124
  | "deleteMany"
125
125
  | "consumeOne"
126
+ | "incrementOne"
126
127
  | "count";
127
128
  }) => W extends undefined ? undefined : CleanedWhere[];
128
129
  }) => CustomAdapter;
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
- * 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.
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
- * 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.
327
+ * Required so secondary-storage-backed rate limiting can enforce the limit
328
+ * in one distributed-safe operation.
324
329
  */
325
- getAndDelete?: (key: string) => Awaitable<unknown>;
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);