@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.
- package/CHANGELOG.md +30 -0
- package/README.md +88 -86
- package/esm/commands/auth-cleanup-command.mjs +1 -1
- package/esm/commands/auth-cleanup-command.mjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -1
- package/esm/contracts/providers.d.mts +101 -0
- package/esm/contracts/providers.d.mts.map +1 -0
- package/esm/contracts/types.d.mts +94 -1
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/errors/auth-provider-sdk-missing.error.d.mts +17 -0
- package/esm/errors/auth-provider-sdk-missing.error.d.mts.map +1 -0
- package/esm/errors/auth-provider-sdk-missing.error.mjs +22 -0
- package/esm/errors/auth-provider-sdk-missing.error.mjs.map +1 -0
- package/esm/errors/email-not-verified.error.d.mts +16 -0
- package/esm/errors/email-not-verified.error.d.mts.map +1 -0
- package/esm/errors/email-not-verified.error.mjs +20 -0
- package/esm/errors/email-not-verified.error.mjs.map +1 -0
- package/esm/errors/index.d.mts +7 -0
- package/esm/errors/index.mjs +9 -0
- package/esm/errors/invalid-one-time-token.error.d.mts +17 -0
- package/esm/errors/invalid-one-time-token.error.d.mts.map +1 -0
- package/esm/errors/invalid-one-time-token.error.mjs +21 -0
- package/esm/errors/invalid-one-time-token.error.mjs.map +1 -0
- package/esm/errors/invalid-passkey.error.d.mts +18 -0
- package/esm/errors/invalid-passkey.error.d.mts.map +1 -0
- package/esm/errors/invalid-passkey.error.mjs +23 -0
- package/esm/errors/invalid-passkey.error.mjs.map +1 -0
- package/esm/errors/invalid-provider-callback.error.d.mts +18 -0
- package/esm/errors/invalid-provider-callback.error.d.mts.map +1 -0
- package/esm/errors/invalid-provider-callback.error.mjs +23 -0
- package/esm/errors/invalid-provider-callback.error.mjs.map +1 -0
- package/esm/errors/notifications-unavailable.error.d.mts +15 -0
- package/esm/errors/notifications-unavailable.error.d.mts.map +1 -0
- package/esm/errors/notifications-unavailable.error.mjs +20 -0
- package/esm/errors/notifications-unavailable.error.mjs.map +1 -0
- package/esm/errors/provider-email-not-verified.error.d.mts +16 -0
- package/esm/errors/provider-email-not-verified.error.d.mts.map +1 -0
- package/esm/errors/provider-email-not-verified.error.mjs +20 -0
- package/esm/errors/provider-email-not-verified.error.mjs.map +1 -0
- package/esm/index.d.mts +27 -3
- package/esm/index.mjs +27 -1
- package/esm/middleware/index.d.mts +4 -1
- package/esm/middleware/index.mjs +3 -0
- package/esm/middleware/one-time-token-throttle.middleware.d.mts +36 -0
- package/esm/middleware/one-time-token-throttle.middleware.d.mts.map +1 -0
- package/esm/middleware/one-time-token-throttle.middleware.mjs +53 -0
- package/esm/middleware/one-time-token-throttle.middleware.mjs.map +1 -0
- package/esm/middleware/otp-throttle.middleware.d.mts +30 -0
- package/esm/middleware/otp-throttle.middleware.d.mts.map +1 -0
- package/esm/middleware/otp-throttle.middleware.mjs +45 -0
- package/esm/middleware/otp-throttle.middleware.mjs.map +1 -0
- package/esm/middleware/require-verified-email.middleware.d.mts +18 -0
- package/esm/middleware/require-verified-email.middleware.d.mts.map +1 -0
- package/esm/middleware/require-verified-email.middleware.mjs +24 -0
- package/esm/middleware/require-verified-email.middleware.mjs.map +1 -0
- package/esm/models/access-token/access-token.model.d.mts +8 -0
- package/esm/models/access-token/access-token.model.d.mts.map +1 -1
- package/esm/models/access-token/access-token.model.mjs +3 -0
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/index.d.mts +3 -1
- package/esm/models/index.d.mts.map +1 -1
- package/esm/models/index.mjs +16 -1
- package/esm/models/index.mjs.map +1 -1
- package/esm/models/one-time-token/index.d.mts +1 -0
- package/esm/models/one-time-token/index.mjs +3 -0
- package/esm/models/one-time-token/migration.mjs +31 -0
- package/esm/models/one-time-token/migration.mjs.map +1 -0
- package/esm/models/one-time-token/one-time-token.model.d.mts +173 -0
- package/esm/models/one-time-token/one-time-token.model.d.mts.map +1 -0
- package/esm/models/one-time-token/one-time-token.model.mjs +151 -0
- package/esm/models/one-time-token/one-time-token.model.mjs.map +1 -0
- package/esm/models/passkey-credential/index.d.mts +1 -0
- package/esm/models/passkey-credential/index.mjs +3 -0
- package/esm/models/passkey-credential/migration.mjs +25 -0
- package/esm/models/passkey-credential/migration.mjs.map +1 -0
- package/esm/models/passkey-credential/passkey-credential.model.d.mts +108 -0
- package/esm/models/passkey-credential/passkey-credential.model.d.mts.map +1 -0
- package/esm/models/passkey-credential/passkey-credential.model.mjs +70 -0
- package/esm/models/passkey-credential/passkey-credential.model.mjs.map +1 -0
- package/esm/models/provider-account/index.d.mts +1 -0
- package/esm/models/provider-account/index.mjs +3 -0
- package/esm/models/provider-account/migration.mjs +25 -0
- package/esm/models/provider-account/migration.mjs.map +1 -0
- package/esm/models/provider-account/provider-account.model.d.mts +75 -0
- package/esm/models/provider-account/provider-account.model.d.mts.map +1 -0
- package/esm/models/provider-account/provider-account.model.mjs +55 -0
- package/esm/models/provider-account/provider-account.model.mjs.map +1 -0
- package/esm/models/refresh-token/refresh-token.model.d.mts +7 -0
- package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs +3 -0
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/otp/index.d.mts +1 -0
- package/esm/otp/index.mjs +3 -0
- package/esm/otp/otp.d.mts +38 -0
- package/esm/otp/otp.d.mts.map +1 -0
- package/esm/otp/otp.mjs +105 -0
- package/esm/otp/otp.mjs.map +1 -0
- package/esm/passkeys/index.d.mts +3 -0
- package/esm/passkeys/index.mjs +4 -0
- package/esm/passkeys/passkey-authentication.d.mts +33 -0
- package/esm/passkeys/passkey-authentication.d.mts.map +1 -0
- package/esm/passkeys/passkey-authentication.mjs +93 -0
- package/esm/passkeys/passkey-authentication.mjs.map +1 -0
- package/esm/passkeys/passkey-challenges.mjs +53 -0
- package/esm/passkeys/passkey-challenges.mjs.map +1 -0
- package/esm/passkeys/passkey-registration.d.mts +29 -0
- package/esm/passkeys/passkey-registration.d.mts.map +1 -0
- package/esm/passkeys/passkey-registration.mjs +88 -0
- package/esm/passkeys/passkey-registration.mjs.map +1 -0
- package/esm/passkeys/simplewebauthn.d.mts +21 -0
- package/esm/passkeys/simplewebauthn.d.mts.map +1 -0
- package/esm/passkeys/simplewebauthn.mjs +13 -0
- package/esm/passkeys/simplewebauthn.mjs.map +1 -0
- package/esm/providers/google-provider.d.mts +40 -0
- package/esm/providers/google-provider.d.mts.map +1 -0
- package/esm/providers/google-provider.mjs +105 -0
- package/esm/providers/google-provider.mjs.map +1 -0
- package/esm/providers/index.d.mts +2 -0
- package/esm/providers/index.mjs +4 -0
- package/esm/providers/provider-login.d.mts +43 -0
- package/esm/providers/provider-login.d.mts.map +1 -0
- package/esm/providers/provider-login.mjs +86 -0
- package/esm/providers/provider-login.mjs.map +1 -0
- package/esm/providers/provider-state-cookie.mjs +63 -0
- package/esm/providers/provider-state-cookie.mjs.map +1 -0
- package/esm/providers/resolve-provider-user.mjs +45 -0
- package/esm/providers/resolve-provider-user.mjs.map +1 -0
- package/esm/services/auth-config.mjs +72 -0
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +18 -1
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +28 -1
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/email-verification.d.mts +35 -0
- package/esm/services/email-verification.d.mts.map +1 -0
- package/esm/services/email-verification.mjs +55 -0
- package/esm/services/email-verification.mjs.map +1 -0
- package/esm/services/index.d.mts +4 -1
- package/esm/services/index.mjs +3 -0
- package/esm/services/one-time-tokens.d.mts +32 -0
- package/esm/services/one-time-tokens.d.mts.map +1 -0
- package/esm/services/one-time-tokens.mjs +56 -0
- package/esm/services/one-time-tokens.mjs.map +1 -0
- package/esm/services/optional-peer.mjs +37 -0
- package/esm/services/optional-peer.mjs.map +1 -0
- package/esm/services/password-reset.d.mts +34 -0
- package/esm/services/password-reset.d.mts.map +1 -0
- package/esm/services/password-reset.mjs +81 -0
- package/esm/services/password-reset.mjs.map +1 -0
- package/esm/services/token-notifications.mjs +87 -0
- package/esm/services/token-notifications.mjs.map +1 -0
- package/esm/utils/auth-error-codes.d.mts +39 -1
- package/esm/utils/auth-error-codes.d.mts.map +1 -1
- package/esm/utils/auth-error-codes.mjs +33 -0
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/esm/utils/index.mjs +1 -0
- package/esm/utils/safe-equal.d.mts +6 -0
- package/esm/utils/safe-equal.d.mts.map +1 -0
- package/esm/utils/safe-equal.mjs +13 -0
- package/esm/utils/safe-equal.mjs.map +1 -0
- package/llms-full.txt +313 -10
- package/llms.txt +2 -0
- package/package.json +21 -7
- package/skills/login-with-providers/SKILL.md +154 -0
- package/skills/manage-tokens/SKILL.md +7 -1
- package/skills/register-user/SKILL.md +6 -8
- package/skills/run-auth-commands/SKILL.md +1 -1
- 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
|
|
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"}
|
|
@@ -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;;;
|
|
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"}
|
package/esm/models/index.d.mts
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../../../../../auth/src/models/index.ts"],"mappings":";;;;;;;cAaa,cAAA,gCAAc,oBAAA"}
|
package/esm/models/index.mjs
CHANGED
|
@@ -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 = [
|
|
19
|
+
const authMigrations = [
|
|
20
|
+
AccessTokenMigration,
|
|
21
|
+
RefreshTokenMigration,
|
|
22
|
+
OneTimeTokenMigration,
|
|
23
|
+
ProviderAccountMigration,
|
|
24
|
+
PasskeyCredentialMigration
|
|
25
|
+
];
|
|
11
26
|
|
|
12
27
|
//#endregion
|
|
13
28
|
export { authMigrations };
|
package/esm/models/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../auth/src/models/index.ts"],"sourcesContent":["import { AccessTokenMigration } from \"./access-token/migration\";\
|
|
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,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,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"}
|