@warlock.js/auth 5.12.0 → 5.14.0

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 (169) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +88 -86
  3. package/esm/commands/auth-cleanup-command.mjs +1 -1
  4. package/esm/commands/auth-cleanup-command.mjs.map +1 -1
  5. package/esm/contracts/index.d.mts +2 -1
  6. package/esm/contracts/providers.d.mts +101 -0
  7. package/esm/contracts/providers.d.mts.map +1 -0
  8. package/esm/contracts/types.d.mts +94 -1
  9. package/esm/contracts/types.d.mts.map +1 -1
  10. package/esm/contracts/types.mjs.map +1 -1
  11. package/esm/errors/auth-provider-sdk-missing.error.d.mts +17 -0
  12. package/esm/errors/auth-provider-sdk-missing.error.d.mts.map +1 -0
  13. package/esm/errors/auth-provider-sdk-missing.error.mjs +22 -0
  14. package/esm/errors/auth-provider-sdk-missing.error.mjs.map +1 -0
  15. package/esm/errors/email-not-verified.error.d.mts +16 -0
  16. package/esm/errors/email-not-verified.error.d.mts.map +1 -0
  17. package/esm/errors/email-not-verified.error.mjs +20 -0
  18. package/esm/errors/email-not-verified.error.mjs.map +1 -0
  19. package/esm/errors/index.d.mts +7 -0
  20. package/esm/errors/index.mjs +9 -0
  21. package/esm/errors/invalid-one-time-token.error.d.mts +17 -0
  22. package/esm/errors/invalid-one-time-token.error.d.mts.map +1 -0
  23. package/esm/errors/invalid-one-time-token.error.mjs +21 -0
  24. package/esm/errors/invalid-one-time-token.error.mjs.map +1 -0
  25. package/esm/errors/invalid-passkey.error.d.mts +18 -0
  26. package/esm/errors/invalid-passkey.error.d.mts.map +1 -0
  27. package/esm/errors/invalid-passkey.error.mjs +23 -0
  28. package/esm/errors/invalid-passkey.error.mjs.map +1 -0
  29. package/esm/errors/invalid-provider-callback.error.d.mts +18 -0
  30. package/esm/errors/invalid-provider-callback.error.d.mts.map +1 -0
  31. package/esm/errors/invalid-provider-callback.error.mjs +23 -0
  32. package/esm/errors/invalid-provider-callback.error.mjs.map +1 -0
  33. package/esm/errors/notifications-unavailable.error.d.mts +15 -0
  34. package/esm/errors/notifications-unavailable.error.d.mts.map +1 -0
  35. package/esm/errors/notifications-unavailable.error.mjs +20 -0
  36. package/esm/errors/notifications-unavailable.error.mjs.map +1 -0
  37. package/esm/errors/provider-email-not-verified.error.d.mts +16 -0
  38. package/esm/errors/provider-email-not-verified.error.d.mts.map +1 -0
  39. package/esm/errors/provider-email-not-verified.error.mjs +20 -0
  40. package/esm/errors/provider-email-not-verified.error.mjs.map +1 -0
  41. package/esm/index.d.mts +27 -3
  42. package/esm/index.mjs +27 -1
  43. package/esm/middleware/index.d.mts +4 -1
  44. package/esm/middleware/index.mjs +3 -0
  45. package/esm/middleware/one-time-token-throttle.middleware.d.mts +36 -0
  46. package/esm/middleware/one-time-token-throttle.middleware.d.mts.map +1 -0
  47. package/esm/middleware/one-time-token-throttle.middleware.mjs +53 -0
  48. package/esm/middleware/one-time-token-throttle.middleware.mjs.map +1 -0
  49. package/esm/middleware/otp-throttle.middleware.d.mts +30 -0
  50. package/esm/middleware/otp-throttle.middleware.d.mts.map +1 -0
  51. package/esm/middleware/otp-throttle.middleware.mjs +45 -0
  52. package/esm/middleware/otp-throttle.middleware.mjs.map +1 -0
  53. package/esm/middleware/require-verified-email.middleware.d.mts +18 -0
  54. package/esm/middleware/require-verified-email.middleware.d.mts.map +1 -0
  55. package/esm/middleware/require-verified-email.middleware.mjs +24 -0
  56. package/esm/middleware/require-verified-email.middleware.mjs.map +1 -0
  57. package/esm/models/access-token/access-token.model.d.mts +8 -0
  58. package/esm/models/access-token/access-token.model.d.mts.map +1 -1
  59. package/esm/models/access-token/access-token.model.mjs +3 -0
  60. package/esm/models/access-token/access-token.model.mjs.map +1 -1
  61. package/esm/models/index.d.mts +3 -1
  62. package/esm/models/index.d.mts.map +1 -1
  63. package/esm/models/index.mjs +16 -1
  64. package/esm/models/index.mjs.map +1 -1
  65. package/esm/models/one-time-token/index.d.mts +1 -0
  66. package/esm/models/one-time-token/index.mjs +3 -0
  67. package/esm/models/one-time-token/migration.mjs +31 -0
  68. package/esm/models/one-time-token/migration.mjs.map +1 -0
  69. package/esm/models/one-time-token/one-time-token.model.d.mts +173 -0
  70. package/esm/models/one-time-token/one-time-token.model.d.mts.map +1 -0
  71. package/esm/models/one-time-token/one-time-token.model.mjs +151 -0
  72. package/esm/models/one-time-token/one-time-token.model.mjs.map +1 -0
  73. package/esm/models/passkey-credential/index.d.mts +1 -0
  74. package/esm/models/passkey-credential/index.mjs +3 -0
  75. package/esm/models/passkey-credential/migration.mjs +25 -0
  76. package/esm/models/passkey-credential/migration.mjs.map +1 -0
  77. package/esm/models/passkey-credential/passkey-credential.model.d.mts +108 -0
  78. package/esm/models/passkey-credential/passkey-credential.model.d.mts.map +1 -0
  79. package/esm/models/passkey-credential/passkey-credential.model.mjs +70 -0
  80. package/esm/models/passkey-credential/passkey-credential.model.mjs.map +1 -0
  81. package/esm/models/provider-account/index.d.mts +1 -0
  82. package/esm/models/provider-account/index.mjs +3 -0
  83. package/esm/models/provider-account/migration.mjs +25 -0
  84. package/esm/models/provider-account/migration.mjs.map +1 -0
  85. package/esm/models/provider-account/provider-account.model.d.mts +75 -0
  86. package/esm/models/provider-account/provider-account.model.d.mts.map +1 -0
  87. package/esm/models/provider-account/provider-account.model.mjs +55 -0
  88. package/esm/models/provider-account/provider-account.model.mjs.map +1 -0
  89. package/esm/models/refresh-token/refresh-token.model.d.mts +7 -0
  90. package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
  91. package/esm/models/refresh-token/refresh-token.model.mjs +3 -0
  92. package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
  93. package/esm/otp/index.d.mts +1 -0
  94. package/esm/otp/index.mjs +3 -0
  95. package/esm/otp/otp.d.mts +38 -0
  96. package/esm/otp/otp.d.mts.map +1 -0
  97. package/esm/otp/otp.mjs +105 -0
  98. package/esm/otp/otp.mjs.map +1 -0
  99. package/esm/passkeys/index.d.mts +3 -0
  100. package/esm/passkeys/index.mjs +4 -0
  101. package/esm/passkeys/passkey-authentication.d.mts +33 -0
  102. package/esm/passkeys/passkey-authentication.d.mts.map +1 -0
  103. package/esm/passkeys/passkey-authentication.mjs +93 -0
  104. package/esm/passkeys/passkey-authentication.mjs.map +1 -0
  105. package/esm/passkeys/passkey-challenges.mjs +53 -0
  106. package/esm/passkeys/passkey-challenges.mjs.map +1 -0
  107. package/esm/passkeys/passkey-registration.d.mts +29 -0
  108. package/esm/passkeys/passkey-registration.d.mts.map +1 -0
  109. package/esm/passkeys/passkey-registration.mjs +88 -0
  110. package/esm/passkeys/passkey-registration.mjs.map +1 -0
  111. package/esm/passkeys/simplewebauthn.d.mts +21 -0
  112. package/esm/passkeys/simplewebauthn.d.mts.map +1 -0
  113. package/esm/passkeys/simplewebauthn.mjs +13 -0
  114. package/esm/passkeys/simplewebauthn.mjs.map +1 -0
  115. package/esm/providers/google-provider.d.mts +40 -0
  116. package/esm/providers/google-provider.d.mts.map +1 -0
  117. package/esm/providers/google-provider.mjs +105 -0
  118. package/esm/providers/google-provider.mjs.map +1 -0
  119. package/esm/providers/index.d.mts +2 -0
  120. package/esm/providers/index.mjs +4 -0
  121. package/esm/providers/provider-login.d.mts +43 -0
  122. package/esm/providers/provider-login.d.mts.map +1 -0
  123. package/esm/providers/provider-login.mjs +86 -0
  124. package/esm/providers/provider-login.mjs.map +1 -0
  125. package/esm/providers/provider-state-cookie.mjs +63 -0
  126. package/esm/providers/provider-state-cookie.mjs.map +1 -0
  127. package/esm/providers/resolve-provider-user.mjs +45 -0
  128. package/esm/providers/resolve-provider-user.mjs.map +1 -0
  129. package/esm/services/auth-config.mjs +72 -0
  130. package/esm/services/auth-config.mjs.map +1 -1
  131. package/esm/services/auth.service.d.mts +18 -1
  132. package/esm/services/auth.service.d.mts.map +1 -1
  133. package/esm/services/auth.service.mjs +28 -1
  134. package/esm/services/auth.service.mjs.map +1 -1
  135. package/esm/services/email-verification.d.mts +35 -0
  136. package/esm/services/email-verification.d.mts.map +1 -0
  137. package/esm/services/email-verification.mjs +55 -0
  138. package/esm/services/email-verification.mjs.map +1 -0
  139. package/esm/services/index.d.mts +4 -1
  140. package/esm/services/index.mjs +3 -0
  141. package/esm/services/one-time-tokens.d.mts +32 -0
  142. package/esm/services/one-time-tokens.d.mts.map +1 -0
  143. package/esm/services/one-time-tokens.mjs +56 -0
  144. package/esm/services/one-time-tokens.mjs.map +1 -0
  145. package/esm/services/optional-peer.mjs +37 -0
  146. package/esm/services/optional-peer.mjs.map +1 -0
  147. package/esm/services/password-reset.d.mts +34 -0
  148. package/esm/services/password-reset.d.mts.map +1 -0
  149. package/esm/services/password-reset.mjs +81 -0
  150. package/esm/services/password-reset.mjs.map +1 -0
  151. package/esm/services/token-notifications.mjs +87 -0
  152. package/esm/services/token-notifications.mjs.map +1 -0
  153. package/esm/utils/auth-error-codes.d.mts +39 -1
  154. package/esm/utils/auth-error-codes.d.mts.map +1 -1
  155. package/esm/utils/auth-error-codes.mjs +33 -0
  156. package/esm/utils/auth-error-codes.mjs.map +1 -1
  157. package/esm/utils/index.mjs +1 -0
  158. package/esm/utils/safe-equal.d.mts +6 -0
  159. package/esm/utils/safe-equal.d.mts.map +1 -0
  160. package/esm/utils/safe-equal.mjs +13 -0
  161. package/esm/utils/safe-equal.mjs.map +1 -0
  162. package/llms-full.txt +313 -10
  163. package/llms.txt +2 -0
  164. package/package.json +21 -7
  165. package/skills/login-with-providers/SKILL.md +154 -0
  166. package/skills/manage-tokens/SKILL.md +7 -1
  167. package/skills/register-user/SKILL.md +6 -8
  168. package/skills/run-auth-commands/SKILL.md +1 -1
  169. package/skills/verify-email-and-reset-password/SKILL.md +137 -0
@@ -0,0 +1,18 @@
1
+ import { Middleware } from "@warlock.js/core";
2
+
3
+ //#region ../auth/src/middleware/require-verified-email.middleware.d.ts
4
+ /**
5
+ * Opt-in route gate that rejects a user who has not verified their email with
6
+ * {@link EmailNotVerifiedError} (`403`, `EC007`). Place it AFTER
7
+ * `authMiddleware` — it reads `request.locals.user`. A request with no user
8
+ * fails closed with the same error.
9
+ *
10
+ * @example
11
+ * router.post("/orders", createOrder, {
12
+ * middleware: [authMiddleware("user"), requireVerifiedEmail()],
13
+ * });
14
+ */
15
+ declare function requireVerifiedEmail(): Middleware;
16
+ //#endregion
17
+ export { requireVerifiedEmail };
18
+ //# sourceMappingURL=require-verified-email.middleware.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-verified-email.middleware.d.mts","names":[],"sources":["../../../../../../../auth/src/middleware/require-verified-email.middleware.ts"],"mappings":";;;;;AAeA;;;;AAAkD;;;;;iBAAlC,oBAAA,IAAwB,UAAU"}
@@ -0,0 +1,24 @@
1
+ import { authConfig } from "../services/auth-config.mjs";
2
+ import { EmailNotVerifiedError } from "../errors/email-not-verified.error.mjs";
3
+
4
+ //#region ../auth/src/middleware/require-verified-email.middleware.ts
5
+ /**
6
+ * Opt-in route gate that rejects a user who has not verified their email with
7
+ * {@link EmailNotVerifiedError} (`403`, `EC007`). Place it AFTER
8
+ * `authMiddleware` — it reads `request.locals.user`. A request with no user
9
+ * fails closed with the same error.
10
+ *
11
+ * @example
12
+ * router.post("/orders", createOrder, {
13
+ * middleware: [authMiddleware("user"), requireVerifiedEmail()],
14
+ * });
15
+ */
16
+ function requireVerifiedEmail() {
17
+ return async ({ request }) => {
18
+ if (!request.locals.user?.get?.(authConfig.verification.field())) throw new EmailNotVerifiedError();
19
+ };
20
+ }
21
+
22
+ //#endregion
23
+ export { requireVerifiedEmail };
24
+ //# sourceMappingURL=require-verified-email.middleware.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-verified-email.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/require-verified-email.middleware.ts"],"sourcesContent":["import type { Middleware } from \"@warlock.js/core\";\nimport { EmailNotVerifiedError } from \"../errors/email-not-verified.error\";\nimport { authConfig } from \"../services/auth-config\";\n\n/**\n * Opt-in route gate that rejects a user who has not verified their email with\n * {@link EmailNotVerifiedError} (`403`, `EC007`). Place it AFTER\n * `authMiddleware` — it reads `request.locals.user`. A request with no user\n * fails closed with the same error.\n *\n * @example\n * router.post(\"/orders\", createOrder, {\n * middleware: [authMiddleware(\"user\"), requireVerifiedEmail()],\n * });\n */\nexport function requireVerifiedEmail(): Middleware {\n return async ({ request }) => {\n const user = request.locals.user as { get?: (key: string) => unknown } | undefined;\n\n if (!user?.get?.(authConfig.verification.field())) {\n throw new EmailNotVerifiedError();\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,uBAAmC;CACjD,OAAO,OAAO,EAAE,cAAc;EAG5B,IAAI,CAFS,QAAQ,OAAO,MAEjB,MAAM,WAAW,aAAa,MAAM,CAAC,GAC9C,MAAM,IAAI,sBAAsB;CAEpC;AACF"}
@@ -54,6 +54,14 @@ declare class AccessToken extends Model {
54
54
  user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
55
55
  expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
56
56
  }>>;
57
+ /**
58
+ * Permanent regardless of the data source default — Mongo's driver default
59
+ * is `"trash"`, which would copy live credential material (the JWT itself)
60
+ * into `access_tokensTrash` on every `destroy()` (logout, expiry purge,
61
+ * never-expiring purge). A revoked/expired access token must not survive
62
+ * anywhere.
63
+ */
64
+ static deleteStrategy: "permanent";
57
65
  /** The user this token was issued for. */
58
66
  get userId(): any;
59
67
  /** The user-type slug this token was issued for. */
@@ -1 +1 @@
1
- {"version":3,"file":"access-token.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"mappings":";;;;;;AAaA;;;;;;cAAa,iBAAA,6BAAiB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;cAiBjB,WAAA,SAAoB,KAAA;EAAA,OACjB,KAAA;EAAA,OAEA,MAAA,6BAAM,eAAA;;;;;;;;;;;;;;;;;MAGT,MAAA;;MAKA,QAAA;;;;;;AAXb;;;;;;;;;;MA8Ba,SAAA;EAqBmD;;;;;;EAAA,IAPnD,YAAA;EAiC8B;;;EAAA,OA1B3B,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,UAAe,SAAA,EAAW,IAAA,GAAI,OAAA,CAAA,WAAA;EAkEZ;;;EAAA,OAtDpC,WAAA,CAAY,KAAA,WAAgB,OAAA,CAAQ,WAAA;EA/Dd;;;EAAA,OAsEtB,aAAA,CAAc,IAAA,EAAM,IAAA,EAAM,KAAA,WAAa,OAAA;EAnEjC;;;EAAA,OA0EN,gBAAA,CAAiB,IAAA,EAAM,IAAA,GAAI,OAAA;;;;;SAQrB,YAAA,IAAgB,OAAA;;;;;;;;;;;;SAqBhB,iBAAA,IAAqB,OAAA,CAAQ,WAAA;;;;;;SAW7B,kBAAA,IAAsB,OAAA,CAAQ,WAAA;AAAA"}
1
+ {"version":3,"file":"access-token.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"mappings":";;;;;;AAaA;;;;;;cAAa,iBAAA,6BAAiB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;cAiBjB,WAAA,SAAoB,KAAA;EAAA,OACjB,KAAA;EAAA,OAEA,MAAA,6BAAM,eAAA;;;;;;;;;;;;;;;;;;;;;;;SASN,cAAA;;MAGH,MAAA;EAfY;EAAA,IAoBZ,QAAA;;;;;;;;;;;;;;;;MAmBA,SAAA;EAuDyB;;;;;;EAAA,IAzCzB,YAAA;EArDyB;;;EAAA,OA4DtB,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,UAAe,SAAA,EAAW,IAAA,GAAI,OAAA,CAAA,WAAA;EAzD1C;;;EAAA,OAqEN,WAAA,CAAY,KAAA,WAAgB,OAAA,CAAQ,WAAA;;;;SAOpC,aAAA,CAAc,IAAA,EAAM,IAAA,EAAM,KAAA,WAAa,OAAA;;;;SAOvC,gBAAA,CAAiB,IAAA,EAAM,IAAA,GAAI,OAAA;;;;;SAQrB,YAAA,IAAgB,OAAA;;;;;;;;;;;;SAqBhB,iBAAA,IAAqB,OAAA,CAAQ,WAAA;;;;;;SAW7B,kBAAA,IAAsB,OAAA,CAAQ,WAAA;AAAA"}
@@ -34,6 +34,9 @@ var AccessToken = class extends Model {
34
34
  static {
35
35
  this.schema = accessTokenSchema;
36
36
  }
37
+ static {
38
+ this.deleteStrategy = "permanent";
39
+ }
37
40
  /** The user this token was issued for. */
38
41
  get userId() {
39
42
  return this.get("user_id");
@@ -1 +1 @@
1
- {"version":3,"file":"access-token.model.mjs","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { isNeverExpiring, isUsableExpiry } from \"../../utils/token-expiry\";\r\nimport type { Auth } from \"../auth.model\";\r\n\r\n/**\r\n * Seal schema for the persisted access-token record. Exported so an override\r\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\r\n * base shape.\r\n *\r\n * `last_access` and the `is_active` soft-revoke flag were removed — neither was\r\n * ever read; access tokens are revoked by deleting the row.\r\n */\r\nexport const accessTokenSchema = v.object({\r\n token: v.string().required(),\r\n user_id: v.scalar().required(),\r\n user_type: v.string().required(),\r\n expires_at: v.date().required(),\r\n});\r\n\r\n/**\r\n * Persisted access-token record + the data layer for access tokens.\r\n *\r\n * **Role.** Owns access-token persistence and lookup. The middleware checks a\r\n * presented JWT against this table so deleting a row (logout) invalidates the\r\n * token immediately, before its JWT expiry. The auth service goes through the\r\n * named statics exclusively, so it never hard-codes a column name and an\r\n * override can rename/add columns by registering under\r\n * `config.auth.accessToken.model`.\r\n */\r\nexport class AccessToken extends Model {\r\n public static table = \"access_tokens\";\r\n\r\n public static schema = accessTokenSchema;\r\n\r\n /** The user this token was issued for. */\r\n public get userId() {\r\n return this.get(\"user_id\");\r\n }\r\n\r\n /** The user-type slug this token was issued for. */\r\n public get userType(): string {\r\n return this.get(\"user_type\");\r\n }\r\n\r\n /**\r\n * Whether the persisted expiry has passed.\r\n *\r\n * The middleware asks this on every request, which is what makes the row the\r\n * authority it always looked like it was: before 4.12.0 the gate only checked\r\n * that the row *existed*, so a row the database knew was dead still let its\r\n * token through — the database was never asked.\r\n *\r\n * **Fails closed.** A missing or unparseable `expires_at` counts as expired.\r\n * `expires_at` is `required` in the schema, so a row that cannot answer \"when\r\n * does this die\" is malformed, and the safe reading of a malformed credential\r\n * is that it is not one. This is deliberately stricter than a naive\r\n * `now > expires_at`, which answers `false` for an `Invalid Date` and thereby\r\n * grants exactly the poisoned rows an unlimited life.\r\n */\r\n public get isExpired(): boolean {\r\n const expiresAt = this.get(\"expires_at\");\r\n\r\n if (!isUsableExpiry(expiresAt)) return true;\r\n\r\n return new Date(expiresAt).getTime() <= Date.now();\r\n }\r\n\r\n /**\r\n * Whether nothing about this row can ever retire it — an unusable\r\n * `expires_at`, or a token carrying no `exp` claim. See\r\n * {@link isNeverExpiring}; this is the predicate `purgeNeverExpiring` selects\r\n * on.\r\n */\r\n public get neverExpires(): boolean {\r\n return isNeverExpiring(this.get(\"token\"), this.get(\"expires_at\"));\r\n }\r\n\r\n /**\r\n * Persist a freshly-signed access token for the user.\r\n */\r\n public static issue(user: Auth, token: string, expiresAt: Date) {\r\n return this.create({\r\n token,\r\n user_id: user.id,\r\n user_type: user.userType,\r\n expires_at: expiresAt,\r\n });\r\n }\r\n\r\n /**\r\n * Find an access-token row by its raw token string.\r\n */\r\n public static findByToken(token: string): Promise<AccessToken | null> {\r\n return this.first({ token });\r\n }\r\n\r\n /**\r\n * Delete a specific token that belongs to the given user.\r\n */\r\n public static deleteForUser(user: Auth, token: string) {\r\n return this.delete({ token, user_id: user.id });\r\n }\r\n\r\n /**\r\n * Delete every access token belonging to the user.\r\n */\r\n public static deleteAllForUser(user: Auth) {\r\n return this.delete({ user_id: user.id });\r\n }\r\n\r\n /**\r\n * Hard-delete every expired access-token row. Returns the number removed.\r\n * Runs from the `auth.cleanup` CLI command (a cold batch path).\r\n */\r\n public static async purgeExpired(): Promise<number> {\r\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\r\n\r\n for (const token of expiredTokens) {\r\n await token.destroy();\r\n }\r\n\r\n return expiredTokens.length;\r\n }\r\n\r\n /**\r\n * Every row that can never retire itself — see {@link neverExpires}.\r\n *\r\n * **A full scan, filtered in memory, on purpose.** The defining case is a row\r\n * whose `expires_at` is an `Invalid Date`, which no date predicate can select\r\n * (`< now` and `> now` are both `false` for it), and the definitive case is a\r\n * token with no `exp` claim, which lives inside the token string rather than\r\n * in a column. Neither is expressible as a `where`, so the rows have to be\r\n * read to be judged. This runs from a one-off remediation command, not a\r\n * request path.\r\n */\r\n public static async findNeverExpiring(): Promise<AccessToken[]> {\r\n const tokens = await this.query().get();\r\n\r\n return tokens.filter((token: AccessToken) => token.neverExpires);\r\n }\r\n\r\n /**\r\n * Hard-delete every never-expiring row, returning the rows removed so the\r\n * caller can report what it revoked. Deletion *is* revocation for access\r\n * tokens — the middleware rejects a token with no row.\r\n */\r\n public static async purgeNeverExpiring(): Promise<AccessToken[]> {\r\n const tokens = await this.findNeverExpiring();\r\n\r\n for (const token of tokens) {\r\n await token.destroy();\r\n }\r\n\r\n return tokens;\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,oBAAoB,EAAE,OAAO;CACxC,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,WAAW,EAAE,OAAO,CAAC,CAAC,SAAS;CAC/B,YAAY,EAAE,KAAK,CAAC,CAAC,SAAS;AAChC,CAAC;;;;;;;;;;;AAYD,IAAa,cAAb,cAAiC,MAAM;;eACf;;;gBAEC;;;CAGvB,IAAW,SAAS;EAClB,OAAO,KAAK,IAAI,SAAS;CAC3B;;CAGA,IAAW,WAAmB;EAC5B,OAAO,KAAK,IAAI,WAAW;CAC7B;;;;;;;;;;;;;;;;CAiBA,IAAW,YAAqB;EAC9B,MAAM,YAAY,KAAK,IAAI,YAAY;EAEvC,IAAI,CAAC,eAAe,SAAS,GAAG,OAAO;EAEvC,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,QAAQ,KAAK,KAAK,IAAI;CACnD;;;;;;;CAQA,IAAW,eAAwB;EACjC,OAAO,gBAAgB,KAAK,IAAI,OAAO,GAAG,KAAK,IAAI,YAAY,CAAC;CAClE;;;;CAKA,OAAc,MAAM,MAAY,OAAe,WAAiB;EAC9D,OAAO,KAAK,OAAO;GACjB;GACA,SAAS,KAAK;GACd,WAAW,KAAK;GAChB,YAAY;EACd,CAAC;CACH;;;;CAKA,OAAc,YAAY,OAA4C;EACpE,OAAO,KAAK,MAAM,EAAE,MAAM,CAAC;CAC7B;;;;CAKA,OAAc,cAAc,MAAY,OAAe;EACrD,OAAO,KAAK,OAAO;GAAE;GAAO,SAAS,KAAK;EAAG,CAAC;CAChD;;;;CAKA,OAAc,iBAAiB,MAAY;EACzC,OAAO,KAAK,OAAO,EAAE,SAAS,KAAK,GAAG,CAAC;CACzC;;;;;CAMA,aAAoB,eAAgC;EAClD,MAAM,gBAAgB,MAAM,KAAK,MAAM,CAAC,CAAC,MAAM,cAAc,qBAAK,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI;EAElF,KAAK,MAAM,SAAS,eAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO,cAAc;CACvB;;;;;;;;;;;;CAaA,aAAoB,oBAA4C;EAG9D,QAAO,MAFc,KAAK,MAAM,CAAC,CAAC,IAAI,EAEzB,CAAC,QAAQ,UAAuB,MAAM,YAAY;CACjE;;;;;;CAOA,aAAoB,qBAA6C;EAC/D,MAAM,SAAS,MAAM,KAAK,kBAAkB;EAE5C,KAAK,MAAM,SAAS,QAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"access-token.model.mjs","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { isNeverExpiring, isUsableExpiry } from \"../../utils/token-expiry\";\r\nimport type { Auth } from \"../auth.model\";\r\n\r\n/**\r\n * Seal schema for the persisted access-token record. Exported so an override\r\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\r\n * base shape.\r\n *\r\n * `last_access` and the `is_active` soft-revoke flag were removed — neither was\r\n * ever read; access tokens are revoked by deleting the row.\r\n */\r\nexport const accessTokenSchema = v.object({\r\n token: v.string().required(),\r\n user_id: v.scalar().required(),\r\n user_type: v.string().required(),\r\n expires_at: v.date().required(),\r\n});\r\n\r\n/**\r\n * Persisted access-token record + the data layer for access tokens.\r\n *\r\n * **Role.** Owns access-token persistence and lookup. The middleware checks a\r\n * presented JWT against this table so deleting a row (logout) invalidates the\r\n * token immediately, before its JWT expiry. The auth service goes through the\r\n * named statics exclusively, so it never hard-codes a column name and an\r\n * override can rename/add columns by registering under\r\n * `config.auth.accessToken.model`.\r\n */\r\nexport class AccessToken extends Model {\r\n public static table = \"access_tokens\";\r\n\r\n public static schema = accessTokenSchema;\r\n\r\n /**\r\n * Permanent regardless of the data source default — Mongo's driver default\r\n * is `\"trash\"`, which would copy live credential material (the JWT itself)\r\n * into `access_tokensTrash` on every `destroy()` (logout, expiry purge,\r\n * never-expiring purge). A revoked/expired access token must not survive\r\n * anywhere.\r\n */\r\n public static deleteStrategy = \"permanent\" as const;\r\n\r\n /** The user this token was issued for. */\r\n public get userId() {\r\n return this.get(\"user_id\");\r\n }\r\n\r\n /** The user-type slug this token was issued for. */\r\n public get userType(): string {\r\n return this.get(\"user_type\");\r\n }\r\n\r\n /**\r\n * Whether the persisted expiry has passed.\r\n *\r\n * The middleware asks this on every request, which is what makes the row the\r\n * authority it always looked like it was: before 4.12.0 the gate only checked\r\n * that the row *existed*, so a row the database knew was dead still let its\r\n * token through — the database was never asked.\r\n *\r\n * **Fails closed.** A missing or unparseable `expires_at` counts as expired.\r\n * `expires_at` is `required` in the schema, so a row that cannot answer \"when\r\n * does this die\" is malformed, and the safe reading of a malformed credential\r\n * is that it is not one. This is deliberately stricter than a naive\r\n * `now > expires_at`, which answers `false` for an `Invalid Date` and thereby\r\n * grants exactly the poisoned rows an unlimited life.\r\n */\r\n public get isExpired(): boolean {\r\n const expiresAt = this.get(\"expires_at\");\r\n\r\n if (!isUsableExpiry(expiresAt)) return true;\r\n\r\n return new Date(expiresAt).getTime() <= Date.now();\r\n }\r\n\r\n /**\r\n * Whether nothing about this row can ever retire it — an unusable\r\n * `expires_at`, or a token carrying no `exp` claim. See\r\n * {@link isNeverExpiring}; this is the predicate `purgeNeverExpiring` selects\r\n * on.\r\n */\r\n public get neverExpires(): boolean {\r\n return isNeverExpiring(this.get(\"token\"), this.get(\"expires_at\"));\r\n }\r\n\r\n /**\r\n * Persist a freshly-signed access token for the user.\r\n */\r\n public static issue(user: Auth, token: string, expiresAt: Date) {\r\n return this.create({\r\n token,\r\n user_id: user.id,\r\n user_type: user.userType,\r\n expires_at: expiresAt,\r\n });\r\n }\r\n\r\n /**\r\n * Find an access-token row by its raw token string.\r\n */\r\n public static findByToken(token: string): Promise<AccessToken | null> {\r\n return this.first({ token });\r\n }\r\n\r\n /**\r\n * Delete a specific token that belongs to the given user.\r\n */\r\n public static deleteForUser(user: Auth, token: string) {\r\n return this.delete({ token, user_id: user.id });\r\n }\r\n\r\n /**\r\n * Delete every access token belonging to the user.\r\n */\r\n public static deleteAllForUser(user: Auth) {\r\n return this.delete({ user_id: user.id });\r\n }\r\n\r\n /**\r\n * Hard-delete every expired access-token row. Returns the number removed.\r\n * Runs from the `auth.cleanup` CLI command (a cold batch path).\r\n */\r\n public static async purgeExpired(): Promise<number> {\r\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\r\n\r\n for (const token of expiredTokens) {\r\n await token.destroy();\r\n }\r\n\r\n return expiredTokens.length;\r\n }\r\n\r\n /**\r\n * Every row that can never retire itself — see {@link neverExpires}.\r\n *\r\n * **A full scan, filtered in memory, on purpose.** The defining case is a row\r\n * whose `expires_at` is an `Invalid Date`, which no date predicate can select\r\n * (`< now` and `> now` are both `false` for it), and the definitive case is a\r\n * token with no `exp` claim, which lives inside the token string rather than\r\n * in a column. Neither is expressible as a `where`, so the rows have to be\r\n * read to be judged. This runs from a one-off remediation command, not a\r\n * request path.\r\n */\r\n public static async findNeverExpiring(): Promise<AccessToken[]> {\r\n const tokens = await this.query().get();\r\n\r\n return tokens.filter((token: AccessToken) => token.neverExpires);\r\n }\r\n\r\n /**\r\n * Hard-delete every never-expiring row, returning the rows removed so the\r\n * caller can report what it revoked. Deletion *is* revocation for access\r\n * tokens — the middleware rejects a token with no row.\r\n */\r\n public static async purgeNeverExpiring(): Promise<AccessToken[]> {\r\n const tokens = await this.findNeverExpiring();\r\n\r\n for (const token of tokens) {\r\n await token.destroy();\r\n }\r\n\r\n return tokens;\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,oBAAoB,EAAE,OAAO;CACxC,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,WAAW,EAAE,OAAO,CAAC,CAAC,SAAS;CAC/B,YAAY,EAAE,KAAK,CAAC,CAAC,SAAS;AAChC,CAAC;;;;;;;;;;;AAYD,IAAa,cAAb,cAAiC,MAAM;;eACf;;;gBAEC;;;wBASQ;;;CAG/B,IAAW,SAAS;EAClB,OAAO,KAAK,IAAI,SAAS;CAC3B;;CAGA,IAAW,WAAmB;EAC5B,OAAO,KAAK,IAAI,WAAW;CAC7B;;;;;;;;;;;;;;;;CAiBA,IAAW,YAAqB;EAC9B,MAAM,YAAY,KAAK,IAAI,YAAY;EAEvC,IAAI,CAAC,eAAe,SAAS,GAAG,OAAO;EAEvC,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,QAAQ,KAAK,KAAK,IAAI;CACnD;;;;;;;CAQA,IAAW,eAAwB;EACjC,OAAO,gBAAgB,KAAK,IAAI,OAAO,GAAG,KAAK,IAAI,YAAY,CAAC;CAClE;;;;CAKA,OAAc,MAAM,MAAY,OAAe,WAAiB;EAC9D,OAAO,KAAK,OAAO;GACjB;GACA,SAAS,KAAK;GACd,WAAW,KAAK;GAChB,YAAY;EACd,CAAC;CACH;;;;CAKA,OAAc,YAAY,OAA4C;EACpE,OAAO,KAAK,MAAM,EAAE,MAAM,CAAC;CAC7B;;;;CAKA,OAAc,cAAc,MAAY,OAAe;EACrD,OAAO,KAAK,OAAO;GAAE;GAAO,SAAS,KAAK;EAAG,CAAC;CAChD;;;;CAKA,OAAc,iBAAiB,MAAY;EACzC,OAAO,KAAK,OAAO,EAAE,SAAS,KAAK,GAAG,CAAC;CACzC;;;;;CAMA,aAAoB,eAAgC;EAClD,MAAM,gBAAgB,MAAM,KAAK,MAAM,CAAC,CAAC,MAAM,cAAc,qBAAK,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI;EAElF,KAAK,MAAM,SAAS,eAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO,cAAc;CACvB;;;;;;;;;;;;CAaA,aAAoB,oBAA4C;EAG9D,QAAO,MAFc,KAAK,MAAM,CAAC,CAAC,IAAI,EAEzB,CAAC,QAAQ,UAAuB,MAAM,YAAY;CACjE;;;;;;CAOA,aAAoB,qBAA6C;EAC/D,MAAM,SAAS,MAAM,KAAK,kBAAkB;EAE5C,KAAK,MAAM,SAAS,QAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO;CACT;AACF"}
@@ -1,7 +1,9 @@
1
1
  import { Auth } from "./auth.model.mjs";
2
2
  import { AccessToken, accessTokenSchema } from "./access-token/access-token.model.mjs";
3
+ import { OneTimeToken, OneTimeTokenPurpose, oneTimeTokenSchema } from "./one-time-token/one-time-token.model.mjs";
3
4
  import { RefreshToken, RefreshTokenIssueOptions, refreshTokenSchema } from "./refresh-token/refresh-token.model.mjs";
4
-
5
+ import { PasskeyCredential, passkeyCredentialSchema } from "./passkey-credential/passkey-credential.model.mjs";
6
+ import { ProviderAccount, providerAccountSchema } from "./provider-account/provider-account.model.mjs";
5
7
  //#region ../auth/src/models/index.d.ts
6
8
  declare const authMigrations: import("@warlock.js/cascade").MigrationConstructor[];
7
9
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../../../../auth/src/models/index.ts"],"mappings":";;;;;cAOa,cAAA,gCAAc,oBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../../../../auth/src/models/index.ts"],"mappings":";;;;;;;cAaa,cAAA,gCAAc,oBAAA"}
@@ -1,13 +1,28 @@
1
1
  import { AccessToken, accessTokenSchema } from "./access-token/access-token.model.mjs";
2
2
  import "./access-token/index.mjs";
3
+ import { OneTimeToken, oneTimeTokenSchema } from "./one-time-token/one-time-token.model.mjs";
4
+ import "./one-time-token/index.mjs";
3
5
  import { RefreshToken, refreshTokenSchema } from "./refresh-token/refresh-token.model.mjs";
4
6
  import "./refresh-token/index.mjs";
5
7
  import { AccessTokenMigration } from "./access-token/migration.mjs";
8
+ import { OneTimeTokenMigration } from "./one-time-token/migration.mjs";
9
+ import { PasskeyCredential, passkeyCredentialSchema } from "./passkey-credential/passkey-credential.model.mjs";
10
+ import { PasskeyCredentialMigration } from "./passkey-credential/migration.mjs";
11
+ import { ProviderAccount, providerAccountSchema } from "./provider-account/provider-account.model.mjs";
12
+ import { ProviderAccountMigration } from "./provider-account/migration.mjs";
6
13
  import { RefreshTokenMigration } from "./refresh-token/migration.mjs";
7
14
  import { Auth } from "./auth.model.mjs";
15
+ import "./passkey-credential/index.mjs";
16
+ import "./provider-account/index.mjs";
8
17
 
9
18
  //#region ../auth/src/models/index.ts
10
- const authMigrations = [AccessTokenMigration, RefreshTokenMigration];
19
+ const authMigrations = [
20
+ AccessTokenMigration,
21
+ RefreshTokenMigration,
22
+ OneTimeTokenMigration,
23
+ ProviderAccountMigration,
24
+ PasskeyCredentialMigration
25
+ ];
11
26
 
12
27
  //#endregion
13
28
  export { authMigrations };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../auth/src/models/index.ts"],"sourcesContent":["import { AccessTokenMigration } from \"./access-token/migration\";\r\nimport { RefreshTokenMigration } from \"./refresh-token/migration\";\r\n\r\nexport * from \"./access-token\";\r\nexport * from \"./auth.model\";\r\nexport * from \"./refresh-token\";\r\n\r\nexport const authMigrations = [AccessTokenMigration, RefreshTokenMigration];\r\n"],"mappings":";;;;;;;;;AAOA,MAAa,iBAAiB,CAAC,sBAAsB,qBAAqB"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../auth/src/models/index.ts"],"sourcesContent":["import { AccessTokenMigration } from \"./access-token/migration\";\nimport { OneTimeTokenMigration } from \"./one-time-token/migration\";\nimport { PasskeyCredentialMigration } from \"./passkey-credential/migration\";\nimport { ProviderAccountMigration } from \"./provider-account/migration\";\nimport { RefreshTokenMigration } from \"./refresh-token/migration\";\n\nexport * from \"./access-token\";\nexport * from \"./auth.model\";\nexport * from \"./one-time-token\";\nexport * from \"./passkey-credential\";\nexport * from \"./provider-account\";\nexport * from \"./refresh-token\";\n\nexport const authMigrations = [\n AccessTokenMigration,\n RefreshTokenMigration,\n OneTimeTokenMigration,\n ProviderAccountMigration,\n PasskeyCredentialMigration,\n];\n"],"mappings":";;;;;;;;;;;;;;;;;;AAaA,MAAa,iBAAiB;CAC5B;CACA;CACA;CACA;CACA;AACF"}
@@ -0,0 +1 @@
1
+ import { OneTimeToken, OneTimeTokenPurpose, oneTimeTokenSchema } from "./one-time-token.model.mjs";
@@ -0,0 +1,3 @@
1
+ import { OneTimeToken, oneTimeTokenSchema } from "./one-time-token.model.mjs";
2
+
3
+ export { };
@@ -0,0 +1,31 @@
1
+ import { OneTimeToken } from "./one-time-token.model.mjs";
2
+ import { migrate } from "@warlock.js/cascade";
3
+
4
+ //#region ../auth/src/models/one-time-token/migration.ts
5
+ const OneTimeTokenMigration = migrate(OneTimeToken, {
6
+ name: "oneTimeToken",
7
+ up() {
8
+ this.createTableIfNotExists();
9
+ this.primaryUuid();
10
+ this.string("token_hash", 64).unique();
11
+ this.string("purpose", 32);
12
+ this.foreignId("user_id").nullable().index();
13
+ this.string("user_type", 50).nullable();
14
+ this.timestamp("expires_at").index().nullable();
15
+ this.timestamp("consumed_at").nullable();
16
+ this.integer("attempts").default(0);
17
+ this.index([
18
+ "user_id",
19
+ "user_type",
20
+ "purpose"
21
+ ]);
22
+ this.timestamps();
23
+ },
24
+ down() {
25
+ this.dropTableIfExists();
26
+ }
27
+ });
28
+
29
+ //#endregion
30
+ export { OneTimeTokenMigration };
31
+ //# sourceMappingURL=migration.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../auth/src/models/one-time-token/migration.ts"],"sourcesContent":["import { migrate } from \"@warlock.js/cascade\";\nimport { OneTimeToken } from \"./one-time-token.model\";\n\nexport const OneTimeTokenMigration = migrate(OneTimeToken, {\n name: \"oneTimeToken\",\n up() {\n this.createTableIfNotExists();\n\n this.primaryUuid();\n\n // SHA-256 hex digest of the raw token — the raw token is never stored.\n this.string(\"token_hash\", 64).unique();\n this.string(\"purpose\", 32);\n\n // `foreignId` follows the DataSource's default primary-key type, matching\n // the access/refresh token tables. Nullable only for a passkey\n // authentication challenge, which is issued before anyone is identified.\n this.foreignId(\"user_id\").nullable().index();\n this.string(\"user_type\", 50).nullable();\n\n this.timestamp(\"expires_at\").index().nullable();\n this.timestamp(\"consumed_at\").nullable();\n\n // Failed verify attempts — only OTP codes are capped on it.\n this.integer(\"attempts\").default(0);\n\n // Invalidating a user's previous reset tokens filters on these.\n this.index([\"user_id\", \"user_type\", \"purpose\"]);\n\n this.timestamps();\n },\n down() {\n this.dropTableIfExists();\n },\n});\n"],"mappings":";;;;AAGA,MAAa,wBAAwB,QAAQ,cAAc;CACzD,MAAM;CACN,KAAK;EACH,KAAK,uBAAuB;EAE5B,KAAK,YAAY;EAGjB,KAAK,OAAO,cAAc,EAAE,CAAC,CAAC,OAAO;EACrC,KAAK,OAAO,WAAW,EAAE;EAKzB,KAAK,UAAU,SAAS,CAAC,CAAC,SAAS,CAAC,CAAC,MAAM;EAC3C,KAAK,OAAO,aAAa,EAAE,CAAC,CAAC,SAAS;EAEtC,KAAK,UAAU,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EAC9C,KAAK,UAAU,aAAa,CAAC,CAAC,SAAS;EAGvC,KAAK,QAAQ,UAAU,CAAC,CAAC,QAAQ,CAAC;EAGlC,KAAK,MAAM;GAAC;GAAW;GAAa;EAAS,CAAC;EAE9C,KAAK,WAAW;CAClB;CACA,OAAO;EACL,KAAK,kBAAkB;CACzB;AACF,CAAC"}
@@ -0,0 +1,173 @@
1
+ import { Auth } from "../auth.model.mjs";
2
+ import { Model } from "@warlock.js/cascade";
3
+
4
+ //#region ../auth/src/models/one-time-token/one-time-token.model.d.ts
5
+ /** What a one-time token may be used for. A token only ever satisfies its own purpose. */
6
+ type OneTimeTokenPurpose = "email-verification" | "password-reset" | "otp" | "passkey-registration" | "passkey-authentication";
7
+ /**
8
+ * Seal schema for the persisted one-time token. Exported so an override can
9
+ * extend it (e.g. a tenant key).
10
+ */
11
+ declare const oneTimeTokenSchema: import("@warlock.js/seal").ObjectValidator<{
12
+ token_hash: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
13
+ purpose: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
14
+ user_id: import("@warlock.js/seal").ScalarValidator & import("@warlock.js/seal").StandardSchemaV1<string | number | boolean, string | number | boolean> & {
15
+ isOptional: true;
16
+ };
17
+ user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string> & {
18
+ isOptional: true;
19
+ };
20
+ expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
21
+ consumed_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
22
+ isOptional: true;
23
+ };
24
+ attempts: import("@warlock.js/seal").IntValidator & import("@warlock.js/seal").StandardSchemaV1<number, number> & {
25
+ isOptional: true;
26
+ };
27
+ }> & import("@warlock.js/seal").StandardSchemaV1<import("@warlock.js/seal").InferInputObjectShape<{
28
+ token_hash: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
29
+ purpose: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
30
+ user_id: import("@warlock.js/seal").ScalarValidator & import("@warlock.js/seal").StandardSchemaV1<string | number | boolean, string | number | boolean> & {
31
+ isOptional: true;
32
+ };
33
+ user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string> & {
34
+ isOptional: true;
35
+ };
36
+ expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
37
+ consumed_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
38
+ isOptional: true;
39
+ };
40
+ attempts: import("@warlock.js/seal").IntValidator & import("@warlock.js/seal").StandardSchemaV1<number, number> & {
41
+ isOptional: true;
42
+ };
43
+ }>, import("@warlock.js/seal").InferInputObjectShape<{
44
+ token_hash: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
45
+ purpose: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
46
+ user_id: import("@warlock.js/seal").ScalarValidator & import("@warlock.js/seal").StandardSchemaV1<string | number | boolean, string | number | boolean> & {
47
+ isOptional: true;
48
+ };
49
+ user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string> & {
50
+ isOptional: true;
51
+ };
52
+ expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
53
+ consumed_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
54
+ isOptional: true;
55
+ };
56
+ attempts: import("@warlock.js/seal").IntValidator & import("@warlock.js/seal").StandardSchemaV1<number, number> & {
57
+ isOptional: true;
58
+ };
59
+ }>>;
60
+ /**
61
+ * Persisted one-time secret: email-verification / password-reset token, OTP
62
+ * code, or passkey ceremony challenge.
63
+ *
64
+ * **Only a SHA-256 hash is stored** — the raw token exists in the notification
65
+ * and nowhere else, so a leaked table cannot be replayed. Single use is
66
+ * enforced by {@link consumeIfActive}, a conditional update in the same shape
67
+ * as `RefreshToken.revokeIfActive`. Override via `config.auth.oneTimeToken.model`.
68
+ */
69
+ declare class OneTimeToken extends Model {
70
+ static table: string;
71
+ static schema: import("@warlock.js/seal").ObjectValidator<{
72
+ token_hash: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
73
+ purpose: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
74
+ user_id: import("@warlock.js/seal").ScalarValidator & import("@warlock.js/seal").StandardSchemaV1<string | number | boolean, string | number | boolean> & {
75
+ isOptional: true;
76
+ };
77
+ user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string> & {
78
+ isOptional: true;
79
+ };
80
+ expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
81
+ consumed_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
82
+ isOptional: true;
83
+ };
84
+ attempts: import("@warlock.js/seal").IntValidator & import("@warlock.js/seal").StandardSchemaV1<number, number> & {
85
+ isOptional: true;
86
+ };
87
+ }> & import("@warlock.js/seal").StandardSchemaV1<import("@warlock.js/seal").InferInputObjectShape<{
88
+ token_hash: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
89
+ purpose: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
90
+ user_id: import("@warlock.js/seal").ScalarValidator & import("@warlock.js/seal").StandardSchemaV1<string | number | boolean, string | number | boolean> & {
91
+ isOptional: true;
92
+ };
93
+ user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string> & {
94
+ isOptional: true;
95
+ };
96
+ expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
97
+ consumed_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
98
+ isOptional: true;
99
+ };
100
+ attempts: import("@warlock.js/seal").IntValidator & import("@warlock.js/seal").StandardSchemaV1<number, number> & {
101
+ isOptional: true;
102
+ };
103
+ }>, import("@warlock.js/seal").InferInputObjectShape<{
104
+ token_hash: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
105
+ purpose: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
106
+ user_id: import("@warlock.js/seal").ScalarValidator & import("@warlock.js/seal").StandardSchemaV1<string | number | boolean, string | number | boolean> & {
107
+ isOptional: true;
108
+ };
109
+ user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string> & {
110
+ isOptional: true;
111
+ };
112
+ expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
113
+ consumed_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
114
+ isOptional: true;
115
+ };
116
+ attempts: import("@warlock.js/seal").IntValidator & import("@warlock.js/seal").StandardSchemaV1<number, number> & {
117
+ isOptional: true;
118
+ };
119
+ }>>;
120
+ /**
121
+ * Permanent regardless of the data source default — Mongo's driver default
122
+ * is `"trash"`, which would copy spent token hashes into
123
+ * `one_time_tokensTrash` on every `destroy()`. A purged one-time token must
124
+ * not survive anywhere.
125
+ */
126
+ static deleteStrategy: "permanent";
127
+ /** The user this token was issued for. */
128
+ get userId(): any;
129
+ /** The user-type slug this token was issued for. */
130
+ get userType(): string;
131
+ /** Whether the token has been used (or invalidated by a newer one). */
132
+ get isConsumed(): boolean;
133
+ /** Whether the expiry has passed. Fails closed: an unusable `expires_at` counts as expired. */
134
+ get isExpired(): boolean;
135
+ /**
136
+ * Atomically mark this token consumed ONLY if nobody has yet. Resolves `true`
137
+ * for the single caller that won; every concurrent or later caller gets
138
+ * `false`.
139
+ */
140
+ consumeIfActive(): Promise<boolean>;
141
+ /** Persist a new token row. `tokenHash` is the SHA-256 hex digest, never the raw token. */
142
+ static issue(user: Auth, purpose: OneTimeTokenPurpose, tokenHash: string, expiresAt: Date): Promise<OneTimeToken>;
143
+ /**
144
+ * Persist a passkey ceremony challenge. `user` is omitted for an
145
+ * authentication challenge, issued before anyone is identified.
146
+ */
147
+ static issueChallenge(purpose: OneTimeTokenPurpose, tokenHash: string, expiresAt: Date, user?: Auth): Promise<OneTimeToken>;
148
+ /** The user's newest unconsumed token of `purpose` (at most one for OTP). */
149
+ static findActiveFor(user: Auth, purpose: OneTimeTokenPurpose): Promise<OneTimeToken | null>;
150
+ /**
151
+ * Atomically count one verify attempt, ONLY while fewer than `max` have been
152
+ * counted and the token is unconsumed. Resolves `false` once the cap is
153
+ * reached — of N concurrent attempts, at most `max` ever resolve `true`.
154
+ */
155
+ recordAttempt(max: number): Promise<boolean>;
156
+ /**
157
+ * Hard-delete every spent row — expired, or consumed (used, invalidated by a
158
+ * newer token, or exhausted). Neither can ever be redeemed again, so nothing
159
+ * reads them. Hard-deleted, never trashed. Returns the number removed. Runs from the `auth.cleanup` CLI
160
+ * command (a cold batch path).
161
+ */
162
+ static purgeSpent(): Promise<number>;
163
+ /** Find a token by hash, scoped to a purpose — a token of the other purpose is not found. */
164
+ static findByHash(tokenHash: string, purpose: OneTimeTokenPurpose): Promise<OneTimeToken | null>;
165
+ /**
166
+ * Consume every still-unused token of `purpose` for the user, returning how
167
+ * many were invalidated. Used before issuing a new password-reset token.
168
+ */
169
+ static invalidateActiveFor(user: Auth, purpose: OneTimeTokenPurpose): Promise<number>;
170
+ }
171
+ //#endregion
172
+ export { OneTimeToken, OneTimeTokenPurpose, oneTimeTokenSchema };
173
+ //# sourceMappingURL=one-time-token.model.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"one-time-token.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/one-time-token/one-time-token.model.ts"],"mappings":";;;;;KAMY,mBAAA;AAAZ;;;;AAAA,cAWa,kBAAA,6BAAkB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAoBlB,YAAA,SAAqB,KAAA;EAAA,OAClB,KAAA;EAAA,OAEA,MAAA,6BAAM,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAHtB;;;;SAWgB,cAAA;;MAGH,MAAA;;MAKA,QAAA;;MAKA,UAAA;;MAKA,SAAA;;;;;;EAaE,eAAA,IAAmB,OAAA;EAgBf;EAAA,OAJH,KAAA,CACZ,IAAA,EAAM,IAAA,EACN,OAAA,EAAS,mBAAA,EACT,SAAA,UACA,SAAA,EAAW,IAAA,GAAI,OAAA,CAAA,YAAA;EAkBN;;;;EAAA,OADG,cAAA,CACZ,OAAA,EAAS,mBAAA,EACT,SAAA,UACA,SAAA,EAAW,IAAA,EACX,IAAA,GAAO,IAAA,GAAI,OAAA,CAAA,YAAA;EAeL;EAAA,OADM,aAAA,CACZ,IAAA,EAAM,IAAA,EACN,OAAA,EAAS,mBAAA,GACR,OAAA,CAAQ,YAAA;EAAA;;;;;EAYE,aAAA,CAAc,GAAA,WAAc,OAAA;EAqCtC;;;;;;EAAA,OAlBiB,UAAA,IAAc,OAAA;EA/HF;EAAA,OA8IlB,UAAA,CACZ,SAAA,UACA,OAAA,EAAS,mBAAA,GACR,OAAA,CAAQ,YAAA;EA9IG;;;;EAAA,OAsJA,mBAAA,CAAoB,IAAA,EAAM,IAAA,EAAM,OAAA,EAAS,mBAAA,GAAsB,OAAA;AAAA"}
@@ -0,0 +1,151 @@
1
+ import { isUsableExpiry } from "../../utils/token-expiry.mjs";
2
+ import { Model } from "@warlock.js/cascade";
3
+ import { v } from "@warlock.js/seal";
4
+
5
+ //#region ../auth/src/models/one-time-token/one-time-token.model.ts
6
+ /**
7
+ * Seal schema for the persisted one-time token. Exported so an override can
8
+ * extend it (e.g. a tenant key).
9
+ */
10
+ const oneTimeTokenSchema = v.object({
11
+ token_hash: v.string().required(),
12
+ purpose: v.string().required(),
13
+ user_id: v.scalar().optional(),
14
+ user_type: v.string().optional(),
15
+ expires_at: v.date().required(),
16
+ consumed_at: v.date().optional(),
17
+ attempts: v.int().optional()
18
+ });
19
+ /**
20
+ * Persisted one-time secret: email-verification / password-reset token, OTP
21
+ * code, or passkey ceremony challenge.
22
+ *
23
+ * **Only a SHA-256 hash is stored** — the raw token exists in the notification
24
+ * and nowhere else, so a leaked table cannot be replayed. Single use is
25
+ * enforced by {@link consumeIfActive}, a conditional update in the same shape
26
+ * as `RefreshToken.revokeIfActive`. Override via `config.auth.oneTimeToken.model`.
27
+ */
28
+ var OneTimeToken = class extends Model {
29
+ static {
30
+ this.table = "one_time_tokens";
31
+ }
32
+ static {
33
+ this.schema = oneTimeTokenSchema;
34
+ }
35
+ static {
36
+ this.deleteStrategy = "permanent";
37
+ }
38
+ /** The user this token was issued for. */
39
+ get userId() {
40
+ return this.get("user_id");
41
+ }
42
+ /** The user-type slug this token was issued for. */
43
+ get userType() {
44
+ return this.get("user_type");
45
+ }
46
+ /** Whether the token has been used (or invalidated by a newer one). */
47
+ get isConsumed() {
48
+ return !!this.get("consumed_at");
49
+ }
50
+ /** Whether the expiry has passed. Fails closed: an unusable `expires_at` counts as expired. */
51
+ get isExpired() {
52
+ const expiresAt = this.get("expires_at");
53
+ if (!isUsableExpiry(expiresAt)) return true;
54
+ return new Date(expiresAt).getTime() <= Date.now();
55
+ }
56
+ /**
57
+ * Atomically mark this token consumed ONLY if nobody has yet. Resolves `true`
58
+ * for the single caller that won; every concurrent or later caller gets
59
+ * `false`.
60
+ */
61
+ async consumeIfActive() {
62
+ return await this.constructor.atomic({
63
+ id: this.id,
64
+ consumed_at: null
65
+ }, { $set: { consumed_at: /* @__PURE__ */ new Date() } }) > 0;
66
+ }
67
+ /** Persist a new token row. `tokenHash` is the SHA-256 hex digest, never the raw token. */
68
+ static issue(user, purpose, tokenHash, expiresAt) {
69
+ return this.create({
70
+ token_hash: tokenHash,
71
+ purpose,
72
+ user_id: user.id,
73
+ user_type: user.userType,
74
+ expires_at: expiresAt,
75
+ consumed_at: null,
76
+ attempts: 0
77
+ });
78
+ }
79
+ /**
80
+ * Persist a passkey ceremony challenge. `user` is omitted for an
81
+ * authentication challenge, issued before anyone is identified.
82
+ */
83
+ static issueChallenge(purpose, tokenHash, expiresAt, user) {
84
+ return this.create({
85
+ token_hash: tokenHash,
86
+ purpose,
87
+ user_id: user?.id ?? null,
88
+ user_type: user?.userType ?? null,
89
+ expires_at: expiresAt,
90
+ consumed_at: null,
91
+ attempts: 0
92
+ });
93
+ }
94
+ /** The user's newest unconsumed token of `purpose` (at most one for OTP). */
95
+ static findActiveFor(user, purpose) {
96
+ return this.query().where({
97
+ user_id: user.id,
98
+ user_type: user.userType,
99
+ purpose,
100
+ consumed_at: null
101
+ }).orderBy("created_at", "desc").first();
102
+ }
103
+ /**
104
+ * Atomically count one verify attempt, ONLY while fewer than `max` have been
105
+ * counted and the token is unconsumed. Resolves `false` once the cap is
106
+ * reached — of N concurrent attempts, at most `max` ever resolve `true`.
107
+ */
108
+ async recordAttempt(max) {
109
+ return await this.constructor.atomic({
110
+ id: this.id,
111
+ consumed_at: null,
112
+ attempts: { $lt: max }
113
+ }, { $inc: { attempts: 1 } }, { trustedFilter: true }) > 0;
114
+ }
115
+ /**
116
+ * Hard-delete every spent row — expired, or consumed (used, invalidated by a
117
+ * newer token, or exhausted). Neither can ever be redeemed again, so nothing
118
+ * reads them. Hard-deleted, never trashed. Returns the number removed. Runs from the `auth.cleanup` CLI
119
+ * command (a cold batch path).
120
+ */
121
+ static async purgeSpent() {
122
+ const expired = await this.query().where("expires_at", "<", /* @__PURE__ */ new Date()).get();
123
+ const consumed = await this.query().whereNotNull("consumed_at").get();
124
+ const spent = new Map([...expired, ...consumed].map((token) => [String(token.id), token]));
125
+ for (const token of spent.values()) await token.destroy();
126
+ return spent.size;
127
+ }
128
+ /** Find a token by hash, scoped to a purpose — a token of the other purpose is not found. */
129
+ static findByHash(tokenHash, purpose) {
130
+ return this.first({
131
+ token_hash: tokenHash,
132
+ purpose
133
+ });
134
+ }
135
+ /**
136
+ * Consume every still-unused token of `purpose` for the user, returning how
137
+ * many were invalidated. Used before issuing a new password-reset token.
138
+ */
139
+ static invalidateActiveFor(user, purpose) {
140
+ return this.atomic({
141
+ user_id: user.id,
142
+ user_type: user.userType,
143
+ purpose,
144
+ consumed_at: null
145
+ }, { $set: { consumed_at: /* @__PURE__ */ new Date() } });
146
+ }
147
+ };
148
+
149
+ //#endregion
150
+ export { OneTimeToken, oneTimeTokenSchema };
151
+ //# sourceMappingURL=one-time-token.model.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"one-time-token.model.mjs","names":[],"sources":["../../../../../../../../auth/src/models/one-time-token/one-time-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\nimport { v } from \"@warlock.js/seal\";\nimport { isUsableExpiry } from \"../../utils/token-expiry\";\nimport type { Auth } from \"../auth.model\";\n\n/** What a one-time token may be used for. A token only ever satisfies its own purpose. */\nexport type OneTimeTokenPurpose =\n | \"email-verification\"\n | \"password-reset\"\n | \"otp\"\n | \"passkey-registration\"\n | \"passkey-authentication\";\n\n/**\n * Seal schema for the persisted one-time token. Exported so an override can\n * extend it (e.g. a tenant key).\n */\nexport const oneTimeTokenSchema = v.object({\n token_hash: v.string().required(),\n purpose: v.string().required(),\n // Absent only on a passkey authentication challenge (no user identified yet).\n user_id: v.scalar().optional(),\n user_type: v.string().optional(),\n expires_at: v.date().required(),\n consumed_at: v.date().optional(),\n attempts: v.int().optional(),\n});\n\n/**\n * Persisted one-time secret: email-verification / password-reset token, OTP\n * code, or passkey ceremony challenge.\n *\n * **Only a SHA-256 hash is stored** — the raw token exists in the notification\n * and nowhere else, so a leaked table cannot be replayed. Single use is\n * enforced by {@link consumeIfActive}, a conditional update in the same shape\n * as `RefreshToken.revokeIfActive`. Override via `config.auth.oneTimeToken.model`.\n */\nexport class OneTimeToken extends Model {\n public static table = \"one_time_tokens\";\n\n public static schema = oneTimeTokenSchema;\n\n /**\n * Permanent regardless of the data source default — Mongo's driver default\n * is `\"trash\"`, which would copy spent token hashes into\n * `one_time_tokensTrash` on every `destroy()`. A purged one-time token must\n * not survive anywhere.\n */\n public static deleteStrategy = \"permanent\" as const;\n\n /** The user this token was issued for. */\n public get userId() {\n return this.get(\"user_id\");\n }\n\n /** The user-type slug this token was issued for. */\n public get userType(): string {\n return this.get(\"user_type\");\n }\n\n /** Whether the token has been used (or invalidated by a newer one). */\n public get isConsumed(): boolean {\n return !!this.get(\"consumed_at\");\n }\n\n /** Whether the expiry has passed. Fails closed: an unusable `expires_at` counts as expired. */\n public get isExpired(): boolean {\n const expiresAt = this.get(\"expires_at\");\n\n if (!isUsableExpiry(expiresAt)) return true;\n\n return new Date(expiresAt).getTime() <= Date.now();\n }\n\n /**\n * Atomically mark this token consumed ONLY if nobody has yet. Resolves `true`\n * for the single caller that won; every concurrent or later caller gets\n * `false`.\n */\n public async consumeIfActive(): Promise<boolean> {\n const modelClass = this.constructor as typeof OneTimeToken;\n\n const consumed = await modelClass.atomic(\n { id: this.id, consumed_at: null },\n { $set: { consumed_at: new Date() } },\n );\n\n return consumed > 0;\n }\n\n /** Persist a new token row. `tokenHash` is the SHA-256 hex digest, never the raw token. */\n public static issue(\n user: Auth,\n purpose: OneTimeTokenPurpose,\n tokenHash: string,\n expiresAt: Date,\n ) {\n return this.create({\n token_hash: tokenHash,\n purpose,\n user_id: user.id,\n user_type: user.userType,\n expires_at: expiresAt,\n consumed_at: null,\n attempts: 0,\n });\n }\n\n /**\n * Persist a passkey ceremony challenge. `user` is omitted for an\n * authentication challenge, issued before anyone is identified.\n */\n public static issueChallenge(\n purpose: OneTimeTokenPurpose,\n tokenHash: string,\n expiresAt: Date,\n user?: Auth,\n ) {\n return this.create({\n token_hash: tokenHash,\n purpose,\n user_id: user?.id ?? null,\n user_type: user?.userType ?? null,\n expires_at: expiresAt,\n consumed_at: null,\n attempts: 0,\n });\n }\n\n /** The user's newest unconsumed token of `purpose` (at most one for OTP). */\n public static findActiveFor(\n user: Auth,\n purpose: OneTimeTokenPurpose,\n ): Promise<OneTimeToken | null> {\n return this.query()\n .where({ user_id: user.id, user_type: user.userType, purpose, consumed_at: null })\n .orderBy(\"created_at\", \"desc\")\n .first();\n }\n\n /**\n * Atomically count one verify attempt, ONLY while fewer than `max` have been\n * counted and the token is unconsumed. Resolves `false` once the cap is\n * reached — of N concurrent attempts, at most `max` ever resolve `true`.\n */\n public async recordAttempt(max: number): Promise<boolean> {\n const modelClass = this.constructor as typeof OneTimeToken;\n\n const counted = await modelClass.atomic(\n { id: this.id, consumed_at: null, attempts: { $lt: max } },\n { $inc: { attempts: 1 } },\n // Code-authored filter; nothing here comes from the request.\n { trustedFilter: true },\n );\n\n return counted > 0;\n }\n\n /**\n * Hard-delete every spent row — expired, or consumed (used, invalidated by a\n * newer token, or exhausted). Neither can ever be redeemed again, so nothing\n * reads them. Hard-deleted, never trashed. Returns the number removed. Runs from the `auth.cleanup` CLI\n * command (a cold batch path).\n */\n public static async purgeSpent(): Promise<number> {\n const expired = await this.query().where(\"expires_at\", \"<\", new Date()).get();\n const consumed = await this.query().whereNotNull(\"consumed_at\").get();\n const spent = new Map([...expired, ...consumed].map((token) => [String(token.id), token]));\n\n for (const token of spent.values()) {\n // `static deleteStrategy` above already forces \"permanent\" regardless\n // of the data source default (Mongo's is \"trash\").\n await token.destroy();\n }\n\n return spent.size;\n }\n\n /** Find a token by hash, scoped to a purpose — a token of the other purpose is not found. */\n public static findByHash(\n tokenHash: string,\n purpose: OneTimeTokenPurpose,\n ): Promise<OneTimeToken | null> {\n return this.first({ token_hash: tokenHash, purpose });\n }\n\n /**\n * Consume every still-unused token of `purpose` for the user, returning how\n * many were invalidated. Used before issuing a new password-reset token.\n */\n public static invalidateActiveFor(user: Auth, purpose: OneTimeTokenPurpose): Promise<number> {\n return this.atomic(\n { user_id: user.id, user_type: user.userType, purpose, consumed_at: null },\n { $set: { consumed_at: new Date() } },\n );\n }\n}\n"],"mappings":";;;;;;;;;AAiBA,MAAa,qBAAqB,EAAE,OAAO;CACzC,YAAY,EAAE,OAAO,CAAC,CAAC,SAAS;CAChC,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAE7B,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,WAAW,EAAE,OAAO,CAAC,CAAC,SAAS;CAC/B,YAAY,EAAE,KAAK,CAAC,CAAC,SAAS;CAC9B,aAAa,EAAE,KAAK,CAAC,CAAC,SAAS;CAC/B,UAAU,EAAE,IAAI,CAAC,CAAC,SAAS;AAC7B,CAAC;;;;;;;;;;AAWD,IAAa,eAAb,cAAkC,MAAM;;eAChB;;;gBAEC;;;wBAQQ;;;CAG/B,IAAW,SAAS;EAClB,OAAO,KAAK,IAAI,SAAS;CAC3B;;CAGA,IAAW,WAAmB;EAC5B,OAAO,KAAK,IAAI,WAAW;CAC7B;;CAGA,IAAW,aAAsB;EAC/B,OAAO,CAAC,CAAC,KAAK,IAAI,aAAa;CACjC;;CAGA,IAAW,YAAqB;EAC9B,MAAM,YAAY,KAAK,IAAI,YAAY;EAEvC,IAAI,CAAC,eAAe,SAAS,GAAG,OAAO;EAEvC,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,QAAQ,KAAK,KAAK,IAAI;CACnD;;;;;;CAOA,MAAa,kBAAoC;EAQ/C,OAAO,MAPY,KAAK,YAEU,OAChC;GAAE,IAAI,KAAK;GAAI,aAAa;EAAK,GACjC,EAAE,MAAM,EAAE,6BAAa,IAAI,KAAK,EAAE,EAAE,CACtC,IAEkB;CACpB;;CAGA,OAAc,MACZ,MACA,SACA,WACA,WACA;EACA,OAAO,KAAK,OAAO;GACjB,YAAY;GACZ;GACA,SAAS,KAAK;GACd,WAAW,KAAK;GAChB,YAAY;GACZ,aAAa;GACb,UAAU;EACZ,CAAC;CACH;;;;;CAMA,OAAc,eACZ,SACA,WACA,WACA,MACA;EACA,OAAO,KAAK,OAAO;GACjB,YAAY;GACZ;GACA,SAAS,MAAM,MAAM;GACrB,WAAW,MAAM,YAAY;GAC7B,YAAY;GACZ,aAAa;GACb,UAAU;EACZ,CAAC;CACH;;CAGA,OAAc,cACZ,MACA,SAC8B;EAC9B,OAAO,KAAK,MAAM,CAAC,CAChB,MAAM;GAAE,SAAS,KAAK;GAAI,WAAW,KAAK;GAAU;GAAS,aAAa;EAAK,CAAC,CAAC,CACjF,QAAQ,cAAc,MAAM,CAAC,CAC7B,MAAM;CACX;;;;;;CAOA,MAAa,cAAc,KAA+B;EAUxD,OAAO,MATY,KAAK,YAES,OAC/B;GAAE,IAAI,KAAK;GAAI,aAAa;GAAM,UAAU,EAAE,KAAK,IAAI;EAAE,GACzD,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,GAExB,EAAE,eAAe,KAAK,CACxB,IAEiB;CACnB;;;;;;;CAQA,aAAoB,aAA8B;EAChD,MAAM,UAAU,MAAM,KAAK,MAAM,CAAC,CAAC,MAAM,cAAc,qBAAK,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI;EAC5E,MAAM,WAAW,MAAM,KAAK,MAAM,CAAC,CAAC,aAAa,aAAa,CAAC,CAAC,IAAI;EACpE,MAAM,QAAQ,IAAI,IAAI,CAAC,GAAG,SAAS,GAAG,QAAQ,CAAC,CAAC,KAAK,UAAU,CAAC,OAAO,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC;EAEzF,KAAK,MAAM,SAAS,MAAM,OAAO,GAG/B,MAAM,MAAM,QAAQ;EAGtB,OAAO,MAAM;CACf;;CAGA,OAAc,WACZ,WACA,SAC8B;EAC9B,OAAO,KAAK,MAAM;GAAE,YAAY;GAAW;EAAQ,CAAC;CACtD;;;;;CAMA,OAAc,oBAAoB,MAAY,SAA+C;EAC3F,OAAO,KAAK,OACV;GAAE,SAAS,KAAK;GAAI,WAAW,KAAK;GAAU;GAAS,aAAa;EAAK,GACzE,EAAE,MAAM,EAAE,6BAAa,IAAI,KAAK,EAAE,EAAE,CACtC;CACF;AACF"}
@@ -0,0 +1 @@
1
+ import { PasskeyCredential, passkeyCredentialSchema } from "./passkey-credential.model.mjs";
@@ -0,0 +1,3 @@
1
+ import { PasskeyCredential, passkeyCredentialSchema } from "./passkey-credential.model.mjs";
2
+
3
+ export { };
@@ -0,0 +1,25 @@
1
+ import { PasskeyCredential } from "./passkey-credential.model.mjs";
2
+ import { migrate } from "@warlock.js/cascade";
3
+
4
+ //#region ../auth/src/models/passkey-credential/migration.ts
5
+ const PasskeyCredentialMigration = migrate(PasskeyCredential, {
6
+ name: "passkeyCredential",
7
+ up() {
8
+ this.createTableIfNotExists();
9
+ this.primaryUuid();
10
+ this.text("credential_id").unique();
11
+ this.text("public_key");
12
+ this.bigInteger("counter").default(0);
13
+ this.json("transports").nullable();
14
+ this.foreignId("user_id").index();
15
+ this.string("user_type", 50).nullable();
16
+ this.timestamps();
17
+ },
18
+ down() {
19
+ this.dropTableIfExists();
20
+ }
21
+ });
22
+
23
+ //#endregion
24
+ export { PasskeyCredentialMigration };
25
+ //# sourceMappingURL=migration.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../auth/src/models/passkey-credential/migration.ts"],"sourcesContent":["import { migrate } from \"@warlock.js/cascade\";\nimport { PasskeyCredential } from \"./passkey-credential.model\";\n\nexport const PasskeyCredentialMigration = migrate(PasskeyCredential, {\n name: \"passkeyCredential\",\n up() {\n this.createTableIfNotExists();\n\n this.primaryUuid();\n\n this.text(\"credential_id\").unique();\n this.text(\"public_key\");\n this.bigInteger(\"counter\").default(0);\n this.json(\"transports\").nullable();\n this.foreignId(\"user_id\").index();\n this.string(\"user_type\", 50).nullable();\n\n this.timestamps();\n },\n down() {\n this.dropTableIfExists();\n },\n});\n"],"mappings":";;;;AAGA,MAAa,6BAA6B,QAAQ,mBAAmB;CACnE,MAAM;CACN,KAAK;EACH,KAAK,uBAAuB;EAE5B,KAAK,YAAY;EAEjB,KAAK,KAAK,eAAe,CAAC,CAAC,OAAO;EAClC,KAAK,KAAK,YAAY;EACtB,KAAK,WAAW,SAAS,CAAC,CAAC,QAAQ,CAAC;EACpC,KAAK,KAAK,YAAY,CAAC,CAAC,SAAS;EACjC,KAAK,UAAU,SAAS,CAAC,CAAC,MAAM;EAChC,KAAK,OAAO,aAAa,EAAE,CAAC,CAAC,SAAS;EAEtC,KAAK,WAAW;CAClB;CACA,OAAO;EACL,KAAK,kBAAkB;CACzB;AACF,CAAC"}