@warlock.js/auth 4.2.11 → 4.3.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 CHANGED
@@ -1,56 +1,56 @@
1
- # Changelog — @warlock.js/auth
2
-
3
- All notable changes to `@warlock.js/auth` are documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
-
7
- ## [Unreleased]
8
-
9
- ## 4.2.11
10
-
11
- ### Changed
12
-
13
- - Bumped `@mongez/reinforcements` to 3.3.0
14
-
15
- ## 4.2.10
16
-
17
- ### Fixed
18
-
19
- - `@mongez/copper`, `@mongez/events`, and `@mongez/reinforcements` are now regular `dependencies` instead of `peerDependencies` — matching every other framework package. They're framework-internal utilities your app never imports directly, so declaring them as peers forced consumers to install them and produced `unmet peer dependency` warnings on install. They now resolve transitively and silently.
20
-
21
- ## 4.2.0
22
-
23
- ### Added
24
-
25
- - `loginThrottleMiddleware` — failure-aware brute-force / credential-stuffing protection that counts only failed logins, resets on a successful one, locks per-account and per-IP, and rejects pre-controller with `429` (cache-backed, fails open on a cache outage). Adds `AuthErrorCodes.TooManyAttempts` (`EC004`).
26
- - `accessToken` / `refreshToken` configuration blocks, making a separate refresh-token secret first-class.
27
- - Overridable token storage — register a custom model under `config.auth.accessToken.model` / `config.auth.refreshToken.model` and `.extend()` the exported `accessTokenSchema` / `refreshTokenSchema` to add columns (e.g. a multi-tenant `organization_id`). Models own issuance through `issue()` and expose named statics, so the service hard-codes no column names.
28
- - `tokenType` (`access` | `refresh`) claim, stamped on issue and verified on read, so an access token can no longer be presented as a refresh token.
29
- - `expires_at` on access tokens; `warlock auth.cleanup` now purges expired access tokens as well as refresh tokens.
30
-
31
- ### Fixed
32
-
33
- - Default access-token lifetime was ~3.6 seconds (a numeric `expiresIn` interpreted as milliseconds) and is now **1 hour**.
34
- - Targeted revocation queried `userId` instead of the `user_id` column, so logout and refresh-token removal threw on Postgres and silently no-oped on MongoDB. The service now routes every token query through named model statics, so no column name is hard-coded.
35
- - Token deletions were fire-and-forget inside `Promise<void>` methods (false success for callers, uncatchable rejections) and are now awaited.
36
- - The route middleware matched on `userType` instead of the `user_type` column.
37
- - `revokeAllTokens` / `revokeTokenFamily` reported an empty set — a re-query on `revoked_at: null` after the update matched nothing — so the `token.revoked` / `token.familyRevoked` events never fired. The revoked rows are now captured before revocation.
38
- - A throwing synchronous auth-event listener no longer turns a completed login into a `500`.
39
-
40
- ### Security
41
-
42
- - `warlock jwt.generate` now derives `JWT_SECRET` / `JWT_REFRESH_SECRET` from a CSPRNG (`Random.token`) instead of `Math.random()`-backed `Random.string`.
43
- - Refresh-token rotation is atomic: a guarded conditional `UPDATE` means two concurrent rotations of the same token can never both succeed, and a replayed token revokes its entire family.
44
-
45
- ### Deprecated
46
-
47
- - The `auth.jwt.*` configuration block (`jwt: { secret, expiresIn, refresh }`). Use `accessToken` / `refreshToken` instead — the legacy shape is still read and mapped forward with a one-time deprecation warning.
48
-
49
- ### Removed
50
-
51
- - Unread `access_tokens` columns `is_active` and `last_access`.
52
- - The unused `auth.password.salt` configuration key.
53
-
54
- ## 4.1.15
55
-
56
- - Baseline — per-package changelog tracking starts at this version.
1
+ # Changelog — @warlock.js/auth
2
+
3
+ All notable changes to `@warlock.js/auth` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## 4.2.11
10
+
11
+ ### Changed
12
+
13
+ - Bumped `@mongez/reinforcements` to 3.3.0
14
+
15
+ ## 4.2.10
16
+
17
+ ### Fixed
18
+
19
+ - `@mongez/copper`, `@mongez/events`, and `@mongez/reinforcements` are now regular `dependencies` instead of `peerDependencies` — matching every other framework package. They're framework-internal utilities your app never imports directly, so declaring them as peers forced consumers to install them and produced `unmet peer dependency` warnings on install. They now resolve transitively and silently.
20
+
21
+ ## 4.2.0
22
+
23
+ ### Added
24
+
25
+ - `loginThrottleMiddleware` — failure-aware brute-force / credential-stuffing protection that counts only failed logins, resets on a successful one, locks per-account and per-IP, and rejects pre-controller with `429` (cache-backed, fails open on a cache outage). Adds `AuthErrorCodes.TooManyAttempts` (`EC004`).
26
+ - `accessToken` / `refreshToken` configuration blocks, making a separate refresh-token secret first-class.
27
+ - Overridable token storage — register a custom model under `config.auth.accessToken.model` / `config.auth.refreshToken.model` and `.extend()` the exported `accessTokenSchema` / `refreshTokenSchema` to add columns (e.g. a multi-tenant `organization_id`). Models own issuance through `issue()` and expose named statics, so the service hard-codes no column names.
28
+ - `tokenType` (`access` | `refresh`) claim, stamped on issue and verified on read, so an access token can no longer be presented as a refresh token.
29
+ - `expires_at` on access tokens; `warlock auth.cleanup` now purges expired access tokens as well as refresh tokens.
30
+
31
+ ### Fixed
32
+
33
+ - Default access-token lifetime was ~3.6 seconds (a numeric `expiresIn` interpreted as milliseconds) and is now **1 hour**.
34
+ - Targeted revocation queried `userId` instead of the `user_id` column, so logout and refresh-token removal threw on Postgres and silently no-oped on MongoDB. The service now routes every token query through named model statics, so no column name is hard-coded.
35
+ - Token deletions were fire-and-forget inside `Promise<void>` methods (false success for callers, uncatchable rejections) and are now awaited.
36
+ - The route middleware matched on `userType` instead of the `user_type` column.
37
+ - `revokeAllTokens` / `revokeTokenFamily` reported an empty set — a re-query on `revoked_at: null` after the update matched nothing — so the `token.revoked` / `token.familyRevoked` events never fired. The revoked rows are now captured before revocation.
38
+ - A throwing synchronous auth-event listener no longer turns a completed login into a `500`.
39
+
40
+ ### Security
41
+
42
+ - `warlock jwt.generate` now derives `JWT_SECRET` / `JWT_REFRESH_SECRET` from a CSPRNG (`Random.token`) instead of `Math.random()`-backed `Random.string`.
43
+ - Refresh-token rotation is atomic: a guarded conditional `UPDATE` means two concurrent rotations of the same token can never both succeed, and a replayed token revokes its entire family.
44
+
45
+ ### Deprecated
46
+
47
+ - The `auth.jwt.*` configuration block (`jwt: { secret, expiresIn, refresh }`). Use `accessToken` / `refreshToken` instead — the legacy shape is still read and mapped forward with a one-time deprecation warning.
48
+
49
+ ### Removed
50
+
51
+ - Unread `access_tokens` columns `is_active` and `last_access`.
52
+ - The unused `auth.password.salt` configuration key.
53
+
54
+ ## 4.1.15
55
+
56
+ - Baseline — per-package changelog tracking starts at this version.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) Hassan Zohdy
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) Hassan Zohdy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,78 +1,78 @@
1
- # Warlock Auth
2
-
3
- JWT authentication for [Warlock.js](https://github.com/warlockjs/core) applications — a base `Auth` model your user types extend, an `authMiddleware` route gate, an `authService` for login / logout / refresh (with refresh-token rotation + replay detection), persisted access + refresh tokens, multi-user-type support, lifecycle events, brute-force throttling, and two CLI commands.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- yarn add @warlock.js/auth
9
- ```
10
-
11
- `@warlock.js/auth` is coupled to `@warlock.js/core` — install it inside a Warlock project.
12
-
13
- ## Configure
14
-
15
- ```ts title="src/config/auth.ts"
16
- import { type AuthConfigurations } from "@warlock.js/auth";
17
- import { env } from "@warlock.js/core";
18
- import { User } from "app/users/models/user";
19
-
20
- const authConfigurations: AuthConfigurations = {
21
- userType: { user: User },
22
- accessToken: {
23
- secret: env("JWT_SECRET"),
24
- expiresIn: "1h",
25
- },
26
- refreshToken: {
27
- secret: env("JWT_REFRESH_SECRET"),
28
- expiresIn: "30d",
29
- rotation: true,
30
- },
31
- };
32
-
33
- export default authConfigurations;
34
- ```
35
-
36
- The legacy `jwt: { secret, expiresIn, refresh: {…} }` block is still honored (with a deprecation warning), but prefer the `accessToken` / `refreshToken` blocks.
37
-
38
- ## Documentation
39
-
40
- Task-focused guides live under [`skills/`](./skills):
41
-
42
- - **overview** — what the package does and when to reach for it
43
- - **auth-basics** — the `Auth` model, middleware, and service
44
- - **handle-login-and-logout** — issue and revoke tokens
45
- - **register-user** — sign up + first token pair
46
- - **protect-routes** — gate routes with `authMiddleware`
47
- - **manage-tokens** — the full token lifecycle (rotation, revocation, cleanup)
48
- - **customize-user-type** — multiple user types in one system
49
- - **customize-token-storage** — override the token models (multi-tenant columns, custom storage)
50
- - **throttle-login-attempts** — brute-force protection via `loginThrottleMiddleware`
51
- - **run-auth-commands** — the bundled CLI commands
52
-
53
- ## Generate the JWT secret
54
-
55
- Register the command in `warlock.config.ts`:
56
-
57
- ```ts
58
- import { registerJWTSecretGeneratorCommand } from "@warlock.js/auth";
59
- import { defineConfig } from "@warlock.js/core";
60
-
61
- export default defineConfig({
62
- cli: {
63
- commands: [registerJWTSecretGeneratorCommand()],
64
- },
65
- });
66
- ```
67
-
68
- Then run:
69
-
70
- ```bash
71
- warlock jwt.generate
72
- ```
73
-
74
- It generates cryptographically-strong `JWT_SECRET` and `JWT_REFRESH_SECRET` values and writes them to your `.env` file. Existing values are left untouched, so it's safe to re-run.
75
-
76
- ## License
77
-
78
- MIT © Hasan Zohdy
1
+ # Warlock Auth
2
+
3
+ JWT authentication for [Warlock.js](https://github.com/warlockjs/core) applications — a base `Auth` model your user types extend, an `authMiddleware` route gate, an `authService` for login / logout / refresh (with refresh-token rotation + replay detection), persisted access + refresh tokens, multi-user-type support, lifecycle events, brute-force throttling, and two CLI commands.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ yarn add @warlock.js/auth
9
+ ```
10
+
11
+ `@warlock.js/auth` is coupled to `@warlock.js/core` — install it inside a Warlock project.
12
+
13
+ ## Configure
14
+
15
+ ```ts title="src/config/auth.ts"
16
+ import { type AuthConfigurations } from "@warlock.js/auth";
17
+ import { env } from "@warlock.js/core";
18
+ import { User } from "app/users/models/user";
19
+
20
+ const authConfigurations: AuthConfigurations = {
21
+ userType: { user: User },
22
+ accessToken: {
23
+ secret: env("JWT_SECRET"),
24
+ expiresIn: "1h",
25
+ },
26
+ refreshToken: {
27
+ secret: env("JWT_REFRESH_SECRET"),
28
+ expiresIn: "30d",
29
+ rotation: true,
30
+ },
31
+ };
32
+
33
+ export default authConfigurations;
34
+ ```
35
+
36
+ The legacy `jwt: { secret, expiresIn, refresh: {…} }` block is still honored (with a deprecation warning), but prefer the `accessToken` / `refreshToken` blocks.
37
+
38
+ ## Documentation
39
+
40
+ Task-focused guides live under [`skills/`](./skills):
41
+
42
+ - **overview** — what the package does and when to reach for it
43
+ - **auth-basics** — the `Auth` model, middleware, and service
44
+ - **handle-login-and-logout** — issue and revoke tokens
45
+ - **register-user** — sign up + first token pair
46
+ - **protect-routes** — gate routes with `authMiddleware`
47
+ - **manage-tokens** — the full token lifecycle (rotation, revocation, cleanup)
48
+ - **customize-user-type** — multiple user types in one system
49
+ - **customize-token-storage** — override the token models (multi-tenant columns, custom storage)
50
+ - **throttle-login-attempts** — brute-force protection via `loginThrottleMiddleware`
51
+ - **run-auth-commands** — the bundled CLI commands
52
+
53
+ ## Generate the JWT secret
54
+
55
+ Register the command in `warlock.config.ts`:
56
+
57
+ ```ts
58
+ import { registerJWTSecretGeneratorCommand } from "@warlock.js/auth";
59
+ import { defineConfig } from "@warlock.js/core";
60
+
61
+ export default defineConfig({
62
+ cli: {
63
+ commands: [registerJWTSecretGeneratorCommand()],
64
+ },
65
+ });
66
+ ```
67
+
68
+ Then run:
69
+
70
+ ```bash
71
+ warlock jwt.generate
72
+ ```
73
+
74
+ It generates cryptographically-strong `JWT_SECRET` and `JWT_REFRESH_SECRET` values and writes them to your `.env` file. Existing values are left untouched, so it's safe to re-run.
75
+
76
+ ## License
77
+
78
+ MIT © Hasan Zohdy
@@ -1 +1 @@
1
- {"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/contracts/types.ts"],"sourcesContent":["import { type ChildModel } from \"@warlock.js/cascade\";\nimport { type Algorithm } from \"fast-jwt\";\nimport type { AccessToken } from \"../models/access-token\";\nimport type { Auth } from \"../models/auth.model\";\nimport type { RefreshToken } from \"../models/refresh-token\";\n\n/**\n * Symbol to indicate no expiration for tokens\n * Use this when you explicitly want tokens to never expire\n *\n * @example\n * ```typescript\n * // src/config/auth.ts\n * import { NO_EXPIRATION, type AuthConfigurations } from \"@warlock.js/auth\";\n *\n * const authConfigurations: AuthConfigurations = {\n * accessToken: {\n * secret: env(\"JWT_SECRET\"),\n * expiresIn: NO_EXPIRATION, // Token expires within 100 years\n * },\n * };\n *\n * export default authConfigurations;\n * ```\n */\nexport const NO_EXPIRATION = \"100y\";\n\n/**\n * Behavior when logout is called without a refresh token\n * - \"revoke-all\": Revoke all refresh tokens for the user (secure default)\n * - \"error\": Return an error requiring the refresh token\n */\nexport type LogoutWithoutTokenBehavior = \"revoke-all\" | \"error\";\n\n/**\n * Access-token configuration.\n *\n * `secret` may also be supplied via the deprecated `jwt.secret` (resolved by a\n * backward-compatible shim); one of the two is required at runtime.\n */\nexport type AccessTokenConfig = {\n /**\n * Override the persisted access-token model — extend {@link AccessToken} to\n * add columns (e.g. a tenant key) or rename them, then register the subclass\n * here. Defaults to the package's `AccessToken`.\n */\n model?: typeof AccessToken;\n /**\n * Secret used to sign access tokens.\n */\n secret?: string;\n /**\n * JWT algorithm.\n * @default \"HS256\"\n */\n algorithm?: Algorithm;\n /**\n * Access-token lifetime — any value the `ms` package accepts.\n * @example \"1h\" or NO_EXPIRATION\n * @default \"1h\"\n */\n expiresIn?: string;\n};\n\n/**\n * Refresh-token configuration.\n */\nexport type RefreshTokenConfig = {\n /**\n * Override the persisted refresh-token model — extend {@link RefreshToken}.\n * Defaults to the package's `RefreshToken`.\n */\n model?: typeof RefreshToken;\n /**\n * Enable refresh tokens.\n * @default true\n */\n enabled?: boolean;\n /**\n * Separate secret for refresh tokens (recommended for security). Falls back\n * to the access-token secret when omitted.\n */\n secret?: string;\n /**\n * Refresh-token lifetime — any value the `ms` package accepts.\n * @example \"7d\" or \"1w\"\n * @default \"7d\"\n */\n expiresIn?: string;\n /**\n * Rotate the refresh token on each use (old token revoked, replay detected).\n * @default true\n */\n rotation?: boolean;\n /**\n * Maximum active refresh tokens per user; the oldest are revoked past it.\n * @default 5\n */\n maxPerUser?: number;\n /**\n * Behavior when logout is called without a refresh token.\n * @default \"revoke-all\"\n */\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\n};\n\n/**\n * Legacy JWT configuration shape. Still honored by the resolver shim with a\n * deprecation warning.\n *\n * @deprecated Use the top-level `accessToken` / `refreshToken` blocks.\n */\nexport type LegacyJwtConfig = {\n secret?: string;\n algorithm?: Algorithm;\n expiresIn?: string;\n refresh?: {\n secret?: string;\n enabled?: boolean;\n expiresIn?: string;\n rotation?: boolean;\n maxPerUser?: number;\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\n };\n};\n\nexport type AuthConfigurations = {\n /**\n * Define all user types — maps a user-type slug to its `Auth` model class so\n * tokens and the middleware can resolve the right model.\n */\n userType: {\n [userType: string]: ChildModel<Auth>;\n };\n /**\n * Access-token configuration (secret, expiry, algorithm, model override).\n */\n accessToken?: AccessTokenConfig;\n /**\n * Refresh-token configuration (rotation, lifetime, cap, model override).\n */\n refreshToken?: RefreshTokenConfig;\n /**\n * @deprecated Use `accessToken` / `refreshToken`. Read via a backward-compatible shim.\n */\n jwt?: LegacyJwtConfig;\n};\n\nexport type AccessTokenOutput = {\n /**\n * JWT Token\n */\n token: string;\n /**\n * Exprie time in ISO format UTC time\n */\n expiresAt: string;\n};\n\n/**\n * Token pair returned after login or token refresh\n */\nexport type TokenPair = {\n /**\n * JWT access token (short-lived)\n */\n accessToken: AccessTokenOutput;\n /**\n * JWT refresh token (long-lived)\n */\n refreshToken?: AccessTokenOutput;\n};\n\n/**\n * Device information for session tracking\n */\nexport type DeviceInfo = {\n /**\n * User agent string from request\n */\n userAgent?: string;\n /**\n * Client IP address\n */\n ip?: string;\n /**\n * Optional device identifier\n */\n deviceId?: string;\n /**\n * Token family ID (for rotation tracking)\n * @internal\n */\n familyId?: string;\n /**\n * Access token payload\n */\n payload?: Record<string, any>;\n};\n\nexport type LoginResult<UserType extends Auth> = {\n user: UserType;\n tokens: TokenPair;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,gBAAgB"}
1
+ {"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/contracts/types.ts"],"sourcesContent":["import { type ChildModel } from \"@warlock.js/cascade\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport type { AccessToken } from \"../models/access-token\";\r\nimport type { Auth } from \"../models/auth.model\";\r\nimport type { RefreshToken } from \"../models/refresh-token\";\r\n\r\n/**\r\n * Symbol to indicate no expiration for tokens\r\n * Use this when you explicitly want tokens to never expire\r\n *\r\n * @example\r\n * ```typescript\r\n * // src/config/auth.ts\r\n * import { NO_EXPIRATION, type AuthConfigurations } from \"@warlock.js/auth\";\r\n *\r\n * const authConfigurations: AuthConfigurations = {\r\n * accessToken: {\r\n * secret: env(\"JWT_SECRET\"),\r\n * expiresIn: NO_EXPIRATION, // Token expires within 100 years\r\n * },\r\n * };\r\n *\r\n * export default authConfigurations;\r\n * ```\r\n */\r\nexport const NO_EXPIRATION = \"100y\";\r\n\r\n/**\r\n * Behavior when logout is called without a refresh token\r\n * - \"revoke-all\": Revoke all refresh tokens for the user (secure default)\r\n * - \"error\": Return an error requiring the refresh token\r\n */\r\nexport type LogoutWithoutTokenBehavior = \"revoke-all\" | \"error\";\r\n\r\n/**\r\n * Access-token configuration.\r\n *\r\n * `secret` may also be supplied via the deprecated `jwt.secret` (resolved by a\r\n * backward-compatible shim); one of the two is required at runtime.\r\n */\r\nexport type AccessTokenConfig = {\r\n /**\r\n * Override the persisted access-token model — extend {@link AccessToken} to\r\n * add columns (e.g. a tenant key) or rename them, then register the subclass\r\n * here. Defaults to the package's `AccessToken`.\r\n */\r\n model?: typeof AccessToken;\r\n /**\r\n * Secret used to sign access tokens.\r\n */\r\n secret?: string;\r\n /**\r\n * JWT algorithm.\r\n * @default \"HS256\"\r\n */\r\n algorithm?: Algorithm;\r\n /**\r\n * Access-token lifetime — any value the `ms` package accepts.\r\n * @example \"1h\" or NO_EXPIRATION\r\n * @default \"1h\"\r\n */\r\n expiresIn?: string;\r\n};\r\n\r\n/**\r\n * Refresh-token configuration.\r\n */\r\nexport type RefreshTokenConfig = {\r\n /**\r\n * Override the persisted refresh-token model — extend {@link RefreshToken}.\r\n * Defaults to the package's `RefreshToken`.\r\n */\r\n model?: typeof RefreshToken;\r\n /**\r\n * Enable refresh tokens.\r\n * @default true\r\n */\r\n enabled?: boolean;\r\n /**\r\n * Separate secret for refresh tokens (recommended for security). Falls back\r\n * to the access-token secret when omitted.\r\n */\r\n secret?: string;\r\n /**\r\n * Refresh-token lifetime — any value the `ms` package accepts.\r\n * @example \"7d\" or \"1w\"\r\n * @default \"7d\"\r\n */\r\n expiresIn?: string;\r\n /**\r\n * Rotate the refresh token on each use (old token revoked, replay detected).\r\n * @default true\r\n */\r\n rotation?: boolean;\r\n /**\r\n * Maximum active refresh tokens per user; the oldest are revoked past it.\r\n * @default 5\r\n */\r\n maxPerUser?: number;\r\n /**\r\n * Behavior when logout is called without a refresh token.\r\n * @default \"revoke-all\"\r\n */\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n};\r\n\r\n/**\r\n * Legacy JWT configuration shape. Still honored by the resolver shim with a\r\n * deprecation warning.\r\n *\r\n * @deprecated Use the top-level `accessToken` / `refreshToken` blocks.\r\n */\r\nexport type LegacyJwtConfig = {\r\n secret?: string;\r\n algorithm?: Algorithm;\r\n expiresIn?: string;\r\n refresh?: {\r\n secret?: string;\r\n enabled?: boolean;\r\n expiresIn?: string;\r\n rotation?: boolean;\r\n maxPerUser?: number;\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n };\r\n};\r\n\r\nexport type AuthConfigurations = {\r\n /**\r\n * Define all user types — maps a user-type slug to its `Auth` model class so\r\n * tokens and the middleware can resolve the right model.\r\n */\r\n userType: {\r\n [userType: string]: ChildModel<Auth>;\r\n };\r\n /**\r\n * Access-token configuration (secret, expiry, algorithm, model override).\r\n */\r\n accessToken?: AccessTokenConfig;\r\n /**\r\n * Refresh-token configuration (rotation, lifetime, cap, model override).\r\n */\r\n refreshToken?: RefreshTokenConfig;\r\n /**\r\n * @deprecated Use `accessToken` / `refreshToken`. Read via a backward-compatible shim.\r\n */\r\n jwt?: LegacyJwtConfig;\r\n};\r\n\r\nexport type AccessTokenOutput = {\r\n /**\r\n * JWT Token\r\n */\r\n token: string;\r\n /**\r\n * Exprie time in ISO format UTC time\r\n */\r\n expiresAt: string;\r\n};\r\n\r\n/**\r\n * Token pair returned after login or token refresh\r\n */\r\nexport type TokenPair = {\r\n /**\r\n * JWT access token (short-lived)\r\n */\r\n accessToken: AccessTokenOutput;\r\n /**\r\n * JWT refresh token (long-lived)\r\n */\r\n refreshToken?: AccessTokenOutput;\r\n};\r\n\r\n/**\r\n * Device information for session tracking\r\n */\r\nexport type DeviceInfo = {\r\n /**\r\n * User agent string from request\r\n */\r\n userAgent?: string;\r\n /**\r\n * Client IP address\r\n */\r\n ip?: string;\r\n /**\r\n * Optional device identifier\r\n */\r\n deviceId?: string;\r\n /**\r\n * Token family ID (for rotation tracking)\r\n * @internal\r\n */\r\n familyId?: string;\r\n /**\r\n * Access token payload\r\n */\r\n payload?: Record<string, any>;\r\n};\r\n\r\nexport type LoginResult<UserType extends Auth> = {\r\n user: UserType;\r\n tokens: TokenPair;\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,gBAAgB"}
@@ -1 +1 @@
1
- {"version":3,"file":"auth.middleware.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/middleware/auth.middleware.ts"],"sourcesContent":["import { config, t, type Middleware, type Request, type Response } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport { AccessToken } from \"../models/access-token\";\nimport { jwt } from \"../services/jwt\";\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\n\n/**\n * Decoded access-token claims the middleware reads. The full payload carries\n * more (`created_at`, `tokenType`, `iat`, `exp`) but only these drive routing.\n */\ntype DecodedAccessToken = {\n id: string | number;\n userType?: string;\n};\n\n/**\n * Build a route gate that always requires an authenticated request.\n *\n * The argument is mandatory and selects which user types may pass:\n * - `[]` — any authenticated user (token required, type not checked).\n * - `\"admin\"` / `[\"admin\", \"staff\"]` — token required AND the user's\n * `userType` must be one of the listed types.\n *\n * There is no anonymous/optional mode: a request without a valid access\n * token is always rejected with `401`. Routes that should be public\n * simply omit the middleware.\n *\n * @example\n * router.get(\"/account\", authMiddleware([]), accountController);\n * router.get(\"/admin\", authMiddleware(\"admin\"), adminController);\n * router.get(\"/back-office\", authMiddleware([\"admin\", \"staff\"]), backOfficeController);\n */\nexport function authMiddleware(allowedUserType: string | string[]) {\n const allowedTypes = Array.isArray(allowedUserType) ? allowedUserType : [allowedUserType];\n\n const auth: Middleware = async (request: Request, response: Response) => {\n try {\n const authorizationValue = request.authorizationValue;\n\n if (!authorizationValue) {\n return response.unauthorized({\n error: t(\"auth.errors.missingAccessToken\"),\n errorCode: AuthErrorCodes.MissingAccessToken,\n });\n }\n\n const decoded = await jwt.verify<DecodedAccessToken>(authorizationValue);\n\n request.decodedAccessToken = decoded;\n\n // A valid signature is not enough — the token must still exist in storage,\n // so deleting the row (logout) invalidates it before its JWT expiry.\n const AccessTokenModel = config.key(\"auth.accessToken.model\", AccessToken);\n const accessToken = await AccessTokenModel.findByToken(authorizationValue);\n\n if (!accessToken) {\n return response.unauthorized({\n error: t(\"auth.errors.invalidAccessToken\"),\n errorCode: AuthErrorCodes.InvalidAccessToken,\n });\n }\n\n const userType = decoded.userType ?? accessToken.userType;\n\n if (allowedTypes.length && !allowedTypes.includes(userType)) {\n return response.unauthorized({\n error: t(\"auth.errors.unauthorized\"),\n errorCode: AuthErrorCodes.Unauthorized,\n });\n }\n\n const UserModel = config.key(`auth.userType.${userType}`);\n\n if (!UserModel) {\n throw new Error(`User type ${userType} is unknown type.`);\n }\n\n const currentUser = await UserModel.find(decoded.id);\n\n if (!currentUser) {\n await accessToken.destroy();\n\n return response.unauthorized({\n error: t(\"auth.errors.invalidAccessToken\"),\n errorCode: AuthErrorCodes.InvalidAccessToken,\n });\n }\n\n request.user = currentUser;\n } catch (error: any) {\n log.error(\"http\", \"auth\", error);\n\n request.clearCurrentUser();\n\n return response.unauthorized({\n error: t(\"auth.errors.invalidAccessToken\"),\n errorCode: AuthErrorCodes.InvalidAccessToken,\n });\n }\n };\n\n return auth;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,eAAe,iBAAoC;CACjE,MAAM,eAAe,MAAM,QAAQ,eAAe,IAAI,kBAAkB,CAAC,eAAe;CAExF,MAAM,OAAmB,OAAO,SAAkB,aAAuB;EACvE,IAAI;GACF,MAAM,qBAAqB,QAAQ;GAEnC,IAAI,CAAC,oBACH,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;GAGH,MAAM,UAAU,MAAM,IAAI,OAA2B,kBAAkB;GAEvE,QAAQ,qBAAqB;GAK7B,MAAM,cAAc,MADK,OAAO,IAAI,0BAA0B,WACrB,CAAC,CAAC,YAAY,kBAAkB;GAEzE,IAAI,CAAC,aACH,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;GAGH,MAAM,WAAW,QAAQ,YAAY,YAAY;GAEjD,IAAI,aAAa,UAAU,CAAC,aAAa,SAAS,QAAQ,GACxD,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,0BAA0B;IACnC;GACF,CAAC;GAGH,MAAM,YAAY,OAAO,IAAI,iBAAiB,UAAU;GAExD,IAAI,CAAC,WACH,MAAM,IAAI,MAAM,aAAa,SAAS,kBAAkB;GAG1D,MAAM,cAAc,MAAM,UAAU,KAAK,QAAQ,EAAE;GAEnD,IAAI,CAAC,aAAa;IAChB,MAAM,YAAY,QAAQ;IAE1B,OAAO,SAAS,aAAa;KAC3B,OAAO,EAAE,gCAAgC;KACzC;IACF,CAAC;GACH;GAEA,QAAQ,OAAO;EACjB,SAAS,OAAY;GACnB,IAAI,MAAM,QAAQ,QAAQ,KAAK;GAE/B,QAAQ,iBAAiB;GAEzB,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;CACF;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"auth.middleware.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/middleware/auth.middleware.ts"],"sourcesContent":["import { config, t, type Middleware, type Request, type Response } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { AccessToken } from \"../models/access-token\";\r\nimport { jwt } from \"../services/jwt\";\r\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\r\n\r\n/**\r\n * Decoded access-token claims the middleware reads. The full payload carries\r\n * more (`created_at`, `tokenType`, `iat`, `exp`) but only these drive routing.\r\n */\r\ntype DecodedAccessToken = {\r\n id: string | number;\r\n userType?: string;\r\n};\r\n\r\n/**\r\n * Build a route gate that always requires an authenticated request.\r\n *\r\n * The argument is mandatory and selects which user types may pass:\r\n * - `[]` — any authenticated user (token required, type not checked).\r\n * - `\"admin\"` / `[\"admin\", \"staff\"]` — token required AND the user's\r\n * `userType` must be one of the listed types.\r\n *\r\n * There is no anonymous/optional mode: a request without a valid access\r\n * token is always rejected with `401`. Routes that should be public\r\n * simply omit the middleware.\r\n *\r\n * @example\r\n * router.get(\"/account\", authMiddleware([]), accountController);\r\n * router.get(\"/admin\", authMiddleware(\"admin\"), adminController);\r\n * router.get(\"/back-office\", authMiddleware([\"admin\", \"staff\"]), backOfficeController);\r\n */\r\nexport function authMiddleware(allowedUserType: string | string[]) {\r\n const allowedTypes = Array.isArray(allowedUserType) ? allowedUserType : [allowedUserType];\r\n\r\n const auth: Middleware = async (request: Request, response: Response) => {\r\n try {\r\n const authorizationValue = request.authorizationValue;\r\n\r\n if (!authorizationValue) {\r\n return response.unauthorized({\r\n error: t(\"auth.errors.missingAccessToken\"),\r\n errorCode: AuthErrorCodes.MissingAccessToken,\r\n });\r\n }\r\n\r\n const decoded = await jwt.verify<DecodedAccessToken>(authorizationValue);\r\n\r\n request.decodedAccessToken = decoded;\r\n\r\n // A valid signature is not enough — the token must still exist in storage,\r\n // so deleting the row (logout) invalidates it before its JWT expiry.\r\n const AccessTokenModel = config.key(\"auth.accessToken.model\", AccessToken);\r\n const accessToken = await AccessTokenModel.findByToken(authorizationValue);\r\n\r\n if (!accessToken) {\r\n return response.unauthorized({\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n const userType = decoded.userType ?? accessToken.userType;\r\n\r\n if (allowedTypes.length && !allowedTypes.includes(userType)) {\r\n return response.unauthorized({\r\n error: t(\"auth.errors.unauthorized\"),\r\n errorCode: AuthErrorCodes.Unauthorized,\r\n });\r\n }\r\n\r\n const UserModel = config.key(`auth.userType.${userType}`);\r\n\r\n if (!UserModel) {\r\n throw new Error(`User type ${userType} is unknown type.`);\r\n }\r\n\r\n const currentUser = await UserModel.find(decoded.id);\r\n\r\n if (!currentUser) {\r\n await accessToken.destroy();\r\n\r\n return response.unauthorized({\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n request.user = currentUser;\r\n } catch (error: any) {\r\n log.error(\"http\", \"auth\", error);\r\n\r\n request.clearCurrentUser();\r\n\r\n return response.unauthorized({\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n };\r\n\r\n return auth;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,eAAe,iBAAoC;CACjE,MAAM,eAAe,MAAM,QAAQ,eAAe,IAAI,kBAAkB,CAAC,eAAe;CAExF,MAAM,OAAmB,OAAO,SAAkB,aAAuB;EACvE,IAAI;GACF,MAAM,qBAAqB,QAAQ;GAEnC,IAAI,CAAC,oBACH,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;GAGH,MAAM,UAAU,MAAM,IAAI,OAA2B,kBAAkB;GAEvE,QAAQ,qBAAqB;GAK7B,MAAM,cAAc,MADK,OAAO,IAAI,0BAA0B,WACrB,CAAC,CAAC,YAAY,kBAAkB;GAEzE,IAAI,CAAC,aACH,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;GAGH,MAAM,WAAW,QAAQ,YAAY,YAAY;GAEjD,IAAI,aAAa,UAAU,CAAC,aAAa,SAAS,QAAQ,GACxD,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,0BAA0B;IACnC;GACF,CAAC;GAGH,MAAM,YAAY,OAAO,IAAI,iBAAiB,UAAU;GAExD,IAAI,CAAC,WACH,MAAM,IAAI,MAAM,aAAa,SAAS,kBAAkB;GAG1D,MAAM,cAAc,MAAM,UAAU,KAAK,QAAQ,EAAE;GAEnD,IAAI,CAAC,aAAa;IAChB,MAAM,YAAY,QAAQ;IAE1B,OAAO,SAAS,aAAa;KAC3B,OAAO,EAAE,gCAAgC;KACzC;IACF,CAAC;GACH;GAEA,QAAQ,OAAO;EACjB,SAAS,OAAY;GACnB,IAAI,MAAM,QAAQ,QAAQ,KAAK;GAE/B,QAAQ,iBAAiB;GAEzB,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;CACF;CAEA,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"login-throttle.middleware.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/middleware/login-throttle.middleware.ts"],"sourcesContent":["import { cache } from \"@warlock.js/cache\";\nimport { t, type Middleware, type Request, type Response } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\n\n/**\n * Built-in identifier kinds the throttle knows how to extract from a request.\n *\n * - `\"email\"` tracks per-account, so an attacker can't spray one user from\n * many IPs.\n * - `\"ip\"` tracks per-source, so an attacker can't stuff many accounts from\n * one IP.\n *\n * Lockout trips when ANY tracked identifier crosses `max` — defense-in-depth.\n */\nexport type LoginThrottleIdentifier = \"email\" | \"ip\";\n\n/**\n * Options for {@link loginThrottleMiddleware}.\n *\n * Failure-aware by design: only counts requests whose response is non-2xx\n * (or whatever `isFailure` says) and resets on success, so a user who types\n * a wrong password three times then the right one is not penalised.\n *\n * @example\n * loginThrottleMiddleware({ max: 5, window: \"15m\", lockoutDuration: \"15m\" });\n */\nexport type LoginThrottleOptions = {\n /**\n * Failures allowed within the window before the identifier is locked.\n *\n * @default 5\n */\n max?: number;\n\n /**\n * Counting window. `ms`-format string (`\"15m\"`, `\"1h\"`) or seconds as a number.\n *\n * @default \"15m\"\n */\n window?: string | number;\n\n /**\n * Lockout duration once the threshold is tripped. `ms`-format string or seconds.\n *\n * @default \"15m\"\n */\n lockoutDuration?: string | number;\n\n /**\n * Identifiers tracked. Each is counted and locked independently.\n *\n * @default [\"email\", \"ip\"]\n */\n by?: LoginThrottleIdentifier[];\n\n /**\n * Credential field consulted when `\"email\"` is tracked. Switch to\n * `\"username\"` (or any other input field) for systems that don't\n * authenticate by email.\n *\n * @default \"email\"\n */\n identifierKey?: string;\n\n /**\n * Override the default error message returned with the 429 response.\n */\n errorMessage?: string;\n\n /**\n * Decide whether the response represents a failed login. Defaults to\n * \"non-2xx is a failure\"; override if your controller signals failure\n * through a 200-style body.\n *\n * @default (response) => !response.isOk\n */\n isFailure?: (response: Response) => boolean;\n\n /**\n * Compute the identifiers tracked for this request. Override to derive the\n * account key from a header, a JWT subject, or any other shape that does\n * not match the built-in `email` / `ip` extraction.\n */\n identify?: (request: Request) => string[];\n};\n\nconst COUNTER_PREFIX = \"auth.throttle.count.\";\nconst LOCK_PREFIX = \"auth.throttle.lock.\";\n\nconst counterKey = (identifier: string): string => `${COUNTER_PREFIX}${identifier}`;\nconst lockKey = (identifier: string): string => `${LOCK_PREFIX}${identifier}`;\n\n/**\n * Built-in identifier extractor — reads the configured credential field from\n * the request body for the per-account key, and `request.detectIp()` for the\n * per-source key. Untruthy values are dropped; a request that yields zero\n * identifiers is left untracked so the middleware no-ops for it.\n */\nfunction buildDefaultIdentifiers(\n request: Request,\n tracked: LoginThrottleIdentifier[],\n credentialField: string,\n): string[] {\n const identifiers: string[] = [];\n\n if (tracked.includes(\"email\")) {\n const value = request.input(credentialField);\n\n if (typeof value === \"string\" && value.length > 0) {\n identifiers.push(`email.${value.toLowerCase()}`);\n }\n }\n\n if (tracked.includes(\"ip\")) {\n const ip = request.detectIp();\n\n if (ip) {\n identifiers.push(`ip.${ip}`);\n }\n }\n\n return identifiers;\n}\n\n/**\n * Failure-aware login throttle. Locks an identifier (account, IP, or both)\n * after too many failed login attempts within a window — without penalising\n * a user who eventually types the right password.\n *\n * Mechanism (per identifier):\n *\n * 1. **Before the controller** — reject with `429` if a lock key is set,\n * skipping the DB lookup AND the bcrypt verify (this also neutralises the\n * CPU-DoS angle of brute-forcing).\n * 2. **After the controller** — observe the outcome via `response.onSent`.\n * On failure (non-2xx by default), bump a fixed-window counter; once it\n * reaches `max`, set a lock key with `lockoutDuration` TTL. On success,\n * clear both the counter and the lock for that identifier.\n *\n * Storage is `@warlock.js/cache` (a peer dependency of `@warlock.js/auth`,\n * transitively guaranteed via `@warlock.js/core`). Lockout state is shared\n * across replicas — correct security model, because an attacker can't bypass\n * by round-robin to a different pod. Every cache call is guarded so a\n * misconfigured driver fails open (a throttle outage must never escalate\n * into a full auth outage).\n *\n * @example\n * import { loginThrottleMiddleware } from \"@warlock.js/auth\";\n * import { router } from \"@warlock.js/core\";\n *\n * router.post(\"/auth/login\", loginController, {\n * middleware: [loginThrottleMiddleware({ max: 5, window: \"15m\" })],\n * });\n *\n * @example\n * // Per-IP only — avoid account-lockout-as-DoS on anonymous-attack vectors\n * router.post(\"/auth/refresh-token\", refreshController, {\n * middleware: [loginThrottleMiddleware({ by: [\"ip\"], max: 10, window: \"1m\" })],\n * });\n */\nexport function loginThrottleMiddleware(options: LoginThrottleOptions = {}): Middleware {\n const max = options.max ?? 5;\n const window = options.window ?? \"15m\";\n const lockoutDuration = options.lockoutDuration ?? \"15m\";\n const tracked = options.by ?? [\"email\", \"ip\"];\n const credentialField = options.identifierKey ?? \"email\";\n\n return async (request: Request, response: Response) => {\n const identifiers = options.identify\n ? options.identify(request)\n : buildDefaultIdentifiers(request, tracked, credentialField);\n\n if (identifiers.length === 0) return;\n\n try {\n for (const identifier of identifiers) {\n const locked = await cache.get(lockKey(identifier));\n\n if (locked) {\n return response.tooManyRequests({\n error: options.errorMessage ?? t(\"auth.errors.tooManyAttempts\"),\n errorCode: AuthErrorCodes.TooManyAttempts,\n });\n }\n }\n } catch (error) {\n // Fail open — a cache outage (driver not initialised, network blip)\n // must never escalate into a full auth outage. Log and let the request\n // through; the onSent recorder below is independently guarded so an\n // outage there is also harmless.\n log.error(\"auth\", \"login-throttle\", error);\n\n return;\n }\n\n response.onSent(async (resolvedResponse: Response) => {\n try {\n const failed = options.isFailure\n ? options.isFailure(resolvedResponse)\n : !resolvedResponse.isOk;\n\n if (!failed) {\n for (const identifier of identifiers) {\n await cache.remove(counterKey(identifier));\n await cache.remove(lockKey(identifier));\n }\n\n return;\n }\n\n for (const identifier of identifiers) {\n const existing = await cache.get(counterKey(identifier));\n const ttlOption = existing === null ? { ttl: window } : undefined;\n const count = await cache.update<number>(\n counterKey(identifier),\n (current) => (current ?? 0) + 1,\n ttlOption,\n );\n\n if ((count ?? 0) >= max) {\n await cache.set(lockKey(identifier), true, { ttl: lockoutDuration });\n }\n }\n } catch (error) {\n log.error(\"auth\", \"login-throttle\", error);\n }\n });\n };\n}\n"],"mappings":";;;;;;AAuFA,MAAM,iBAAiB;AACvB,MAAM,cAAc;AAEpB,MAAM,cAAc,eAA+B,GAAG,iBAAiB;AACvE,MAAM,WAAW,eAA+B,GAAG,cAAc;;;;;;;AAQjE,SAAS,wBACP,SACA,SACA,iBACU;CACV,MAAM,cAAwB,CAAC;CAE/B,IAAI,QAAQ,SAAS,OAAO,GAAG;EAC7B,MAAM,QAAQ,QAAQ,MAAM,eAAe;EAE3C,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAC9C,YAAY,KAAK,SAAS,MAAM,YAAY,GAAG;CAEnD;CAEA,IAAI,QAAQ,SAAS,IAAI,GAAG;EAC1B,MAAM,KAAK,QAAQ,SAAS;EAE5B,IAAI,IACF,YAAY,KAAK,MAAM,IAAI;CAE/B;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,wBAAwB,UAAgC,CAAC,GAAe;CACtF,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,kBAAkB,QAAQ,mBAAmB;CACnD,MAAM,UAAU,QAAQ,MAAM,CAAC,SAAS,IAAI;CAC5C,MAAM,kBAAkB,QAAQ,iBAAiB;CAEjD,OAAO,OAAO,SAAkB,aAAuB;EACrD,MAAM,cAAc,QAAQ,WACxB,QAAQ,SAAS,OAAO,IACxB,wBAAwB,SAAS,SAAS,eAAe;EAE7D,IAAI,YAAY,WAAW,GAAG;EAE9B,IAAI;GACF,KAAK,MAAM,cAAc,aAGvB,IAAI,MAFiB,MAAM,IAAI,QAAQ,UAAU,CAAC,GAGhD,OAAO,SAAS,gBAAgB;IAC9B,OAAO,QAAQ,gBAAgB,EAAE,6BAA6B;IAC9D;GACF,CAAC;EAGP,SAAS,OAAO;GAKd,IAAI,MAAM,QAAQ,kBAAkB,KAAK;GAEzC;EACF;EAEA,SAAS,OAAO,OAAO,qBAA+B;GACpD,IAAI;IAKF,IAAI,EAJW,QAAQ,YACnB,QAAQ,UAAU,gBAAgB,IAClC,CAAC,iBAAiB,OAET;KACX,KAAK,MAAM,cAAc,aAAa;MACpC,MAAM,MAAM,OAAO,WAAW,UAAU,CAAC;MACzC,MAAM,MAAM,OAAO,QAAQ,UAAU,CAAC;KACxC;KAEA;IACF;IAEA,KAAK,MAAM,cAAc,aAAa;KAEpC,MAAM,YAAY,MADK,MAAM,IAAI,WAAW,UAAU,CAAC,MACxB,OAAO,EAAE,KAAK,OAAO,IAAI;KAOxD,KAAK,MANe,MAAM,OACxB,WAAW,UAAU,IACpB,aAAa,WAAW,KAAK,GAC9B,SACF,KAEc,MAAM,KAClB,MAAM,MAAM,IAAI,QAAQ,UAAU,GAAG,MAAM,EAAE,KAAK,gBAAgB,CAAC;IAEvE;GACF,SAAS,OAAO;IACd,IAAI,MAAM,QAAQ,kBAAkB,KAAK;GAC3C;EACF,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"login-throttle.middleware.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/middleware/login-throttle.middleware.ts"],"sourcesContent":["import { cache } from \"@warlock.js/cache\";\r\nimport { t, type Middleware, type Request, type Response } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\r\n\r\n/**\r\n * Built-in identifier kinds the throttle knows how to extract from a request.\r\n *\r\n * - `\"email\"` tracks per-account, so an attacker can't spray one user from\r\n * many IPs.\r\n * - `\"ip\"` tracks per-source, so an attacker can't stuff many accounts from\r\n * one IP.\r\n *\r\n * Lockout trips when ANY tracked identifier crosses `max` — defense-in-depth.\r\n */\r\nexport type LoginThrottleIdentifier = \"email\" | \"ip\";\r\n\r\n/**\r\n * Options for {@link loginThrottleMiddleware}.\r\n *\r\n * Failure-aware by design: only counts requests whose response is non-2xx\r\n * (or whatever `isFailure` says) and resets on success, so a user who types\r\n * a wrong password three times then the right one is not penalised.\r\n *\r\n * @example\r\n * loginThrottleMiddleware({ max: 5, window: \"15m\", lockoutDuration: \"15m\" });\r\n */\r\nexport type LoginThrottleOptions = {\r\n /**\r\n * Failures allowed within the window before the identifier is locked.\r\n *\r\n * @default 5\r\n */\r\n max?: number;\r\n\r\n /**\r\n * Counting window. `ms`-format string (`\"15m\"`, `\"1h\"`) or seconds as a number.\r\n *\r\n * @default \"15m\"\r\n */\r\n window?: string | number;\r\n\r\n /**\r\n * Lockout duration once the threshold is tripped. `ms`-format string or seconds.\r\n *\r\n * @default \"15m\"\r\n */\r\n lockoutDuration?: string | number;\r\n\r\n /**\r\n * Identifiers tracked. Each is counted and locked independently.\r\n *\r\n * @default [\"email\", \"ip\"]\r\n */\r\n by?: LoginThrottleIdentifier[];\r\n\r\n /**\r\n * Credential field consulted when `\"email\"` is tracked. Switch to\r\n * `\"username\"` (or any other input field) for systems that don't\r\n * authenticate by email.\r\n *\r\n * @default \"email\"\r\n */\r\n identifierKey?: string;\r\n\r\n /**\r\n * Override the default error message returned with the 429 response.\r\n */\r\n errorMessage?: string;\r\n\r\n /**\r\n * Decide whether the response represents a failed login. Defaults to\r\n * \"non-2xx is a failure\"; override if your controller signals failure\r\n * through a 200-style body.\r\n *\r\n * @default (response) => !response.isOk\r\n */\r\n isFailure?: (response: Response) => boolean;\r\n\r\n /**\r\n * Compute the identifiers tracked for this request. Override to derive the\r\n * account key from a header, a JWT subject, or any other shape that does\r\n * not match the built-in `email` / `ip` extraction.\r\n */\r\n identify?: (request: Request) => string[];\r\n};\r\n\r\nconst COUNTER_PREFIX = \"auth.throttle.count.\";\r\nconst LOCK_PREFIX = \"auth.throttle.lock.\";\r\n\r\nconst counterKey = (identifier: string): string => `${COUNTER_PREFIX}${identifier}`;\r\nconst lockKey = (identifier: string): string => `${LOCK_PREFIX}${identifier}`;\r\n\r\n/**\r\n * Built-in identifier extractor — reads the configured credential field from\r\n * the request body for the per-account key, and `request.detectIp()` for the\r\n * per-source key. Untruthy values are dropped; a request that yields zero\r\n * identifiers is left untracked so the middleware no-ops for it.\r\n */\r\nfunction buildDefaultIdentifiers(\r\n request: Request,\r\n tracked: LoginThrottleIdentifier[],\r\n credentialField: string,\r\n): string[] {\r\n const identifiers: string[] = [];\r\n\r\n if (tracked.includes(\"email\")) {\r\n const value = request.input(credentialField);\r\n\r\n if (typeof value === \"string\" && value.length > 0) {\r\n identifiers.push(`email.${value.toLowerCase()}`);\r\n }\r\n }\r\n\r\n if (tracked.includes(\"ip\")) {\r\n const ip = request.detectIp();\r\n\r\n if (ip) {\r\n identifiers.push(`ip.${ip}`);\r\n }\r\n }\r\n\r\n return identifiers;\r\n}\r\n\r\n/**\r\n * Failure-aware login throttle. Locks an identifier (account, IP, or both)\r\n * after too many failed login attempts within a window — without penalising\r\n * a user who eventually types the right password.\r\n *\r\n * Mechanism (per identifier):\r\n *\r\n * 1. **Before the controller** — reject with `429` if a lock key is set,\r\n * skipping the DB lookup AND the bcrypt verify (this also neutralises the\r\n * CPU-DoS angle of brute-forcing).\r\n * 2. **After the controller** — observe the outcome via `response.onSent`.\r\n * On failure (non-2xx by default), bump a fixed-window counter; once it\r\n * reaches `max`, set a lock key with `lockoutDuration` TTL. On success,\r\n * clear both the counter and the lock for that identifier.\r\n *\r\n * Storage is `@warlock.js/cache` (a peer dependency of `@warlock.js/auth`,\r\n * transitively guaranteed via `@warlock.js/core`). Lockout state is shared\r\n * across replicas — correct security model, because an attacker can't bypass\r\n * by round-robin to a different pod. Every cache call is guarded so a\r\n * misconfigured driver fails open (a throttle outage must never escalate\r\n * into a full auth outage).\r\n *\r\n * @example\r\n * import { loginThrottleMiddleware } from \"@warlock.js/auth\";\r\n * import { router } from \"@warlock.js/core\";\r\n *\r\n * router.post(\"/auth/login\", loginController, {\r\n * middleware: [loginThrottleMiddleware({ max: 5, window: \"15m\" })],\r\n * });\r\n *\r\n * @example\r\n * // Per-IP only — avoid account-lockout-as-DoS on anonymous-attack vectors\r\n * router.post(\"/auth/refresh-token\", refreshController, {\r\n * middleware: [loginThrottleMiddleware({ by: [\"ip\"], max: 10, window: \"1m\" })],\r\n * });\r\n */\r\nexport function loginThrottleMiddleware(options: LoginThrottleOptions = {}): Middleware {\r\n const max = options.max ?? 5;\r\n const window = options.window ?? \"15m\";\r\n const lockoutDuration = options.lockoutDuration ?? \"15m\";\r\n const tracked = options.by ?? [\"email\", \"ip\"];\r\n const credentialField = options.identifierKey ?? \"email\";\r\n\r\n return async (request: Request, response: Response) => {\r\n const identifiers = options.identify\r\n ? options.identify(request)\r\n : buildDefaultIdentifiers(request, tracked, credentialField);\r\n\r\n if (identifiers.length === 0) return;\r\n\r\n try {\r\n for (const identifier of identifiers) {\r\n const locked = await cache.get(lockKey(identifier));\r\n\r\n if (locked) {\r\n return response.tooManyRequests({\r\n error: options.errorMessage ?? t(\"auth.errors.tooManyAttempts\"),\r\n errorCode: AuthErrorCodes.TooManyAttempts,\r\n });\r\n }\r\n }\r\n } catch (error) {\r\n // Fail open — a cache outage (driver not initialised, network blip)\r\n // must never escalate into a full auth outage. Log and let the request\r\n // through; the onSent recorder below is independently guarded so an\r\n // outage there is also harmless.\r\n log.error(\"auth\", \"login-throttle\", error);\r\n\r\n return;\r\n }\r\n\r\n response.onSent(async (resolvedResponse: Response) => {\r\n try {\r\n const failed = options.isFailure\r\n ? options.isFailure(resolvedResponse)\r\n : !resolvedResponse.isOk;\r\n\r\n if (!failed) {\r\n for (const identifier of identifiers) {\r\n await cache.remove(counterKey(identifier));\r\n await cache.remove(lockKey(identifier));\r\n }\r\n\r\n return;\r\n }\r\n\r\n for (const identifier of identifiers) {\r\n const existing = await cache.get(counterKey(identifier));\r\n const ttlOption = existing === null ? { ttl: window } : undefined;\r\n const count = await cache.update<number>(\r\n counterKey(identifier),\r\n (current) => (current ?? 0) + 1,\r\n ttlOption,\r\n );\r\n\r\n if ((count ?? 0) >= max) {\r\n await cache.set(lockKey(identifier), true, { ttl: lockoutDuration });\r\n }\r\n }\r\n } catch (error) {\r\n log.error(\"auth\", \"login-throttle\", error);\r\n }\r\n });\r\n };\r\n}\r\n"],"mappings":";;;;;;AAuFA,MAAM,iBAAiB;AACvB,MAAM,cAAc;AAEpB,MAAM,cAAc,eAA+B,GAAG,iBAAiB;AACvE,MAAM,WAAW,eAA+B,GAAG,cAAc;;;;;;;AAQjE,SAAS,wBACP,SACA,SACA,iBACU;CACV,MAAM,cAAwB,CAAC;CAE/B,IAAI,QAAQ,SAAS,OAAO,GAAG;EAC7B,MAAM,QAAQ,QAAQ,MAAM,eAAe;EAE3C,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAC9C,YAAY,KAAK,SAAS,MAAM,YAAY,GAAG;CAEnD;CAEA,IAAI,QAAQ,SAAS,IAAI,GAAG;EAC1B,MAAM,KAAK,QAAQ,SAAS;EAE5B,IAAI,IACF,YAAY,KAAK,MAAM,IAAI;CAE/B;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,wBAAwB,UAAgC,CAAC,GAAe;CACtF,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,kBAAkB,QAAQ,mBAAmB;CACnD,MAAM,UAAU,QAAQ,MAAM,CAAC,SAAS,IAAI;CAC5C,MAAM,kBAAkB,QAAQ,iBAAiB;CAEjD,OAAO,OAAO,SAAkB,aAAuB;EACrD,MAAM,cAAc,QAAQ,WACxB,QAAQ,SAAS,OAAO,IACxB,wBAAwB,SAAS,SAAS,eAAe;EAE7D,IAAI,YAAY,WAAW,GAAG;EAE9B,IAAI;GACF,KAAK,MAAM,cAAc,aAGvB,IAAI,MAFiB,MAAM,IAAI,QAAQ,UAAU,CAAC,GAGhD,OAAO,SAAS,gBAAgB;IAC9B,OAAO,QAAQ,gBAAgB,EAAE,6BAA6B;IAC9D;GACF,CAAC;EAGP,SAAS,OAAO;GAKd,IAAI,MAAM,QAAQ,kBAAkB,KAAK;GAEzC;EACF;EAEA,SAAS,OAAO,OAAO,qBAA+B;GACpD,IAAI;IAKF,IAAI,EAJW,QAAQ,YACnB,QAAQ,UAAU,gBAAgB,IAClC,CAAC,iBAAiB,OAET;KACX,KAAK,MAAM,cAAc,aAAa;MACpC,MAAM,MAAM,OAAO,WAAW,UAAU,CAAC;MACzC,MAAM,MAAM,OAAO,QAAQ,UAAU,CAAC;KACxC;KAEA;IACF;IAEA,KAAK,MAAM,cAAc,aAAa;KAEpC,MAAM,YAAY,MADK,MAAM,IAAI,WAAW,UAAU,CAAC,MACxB,OAAO,EAAE,KAAK,OAAO,IAAI;KAOxD,KAAK,MANe,MAAM,OACxB,WAAW,UAAU,IACpB,aAAa,WAAW,KAAK,GAC9B,SACF,KAEc,MAAM,KAClB,MAAM,MAAM,IAAI,QAAQ,UAAU,GAAG,MAAM,EAAE,KAAK,gBAAgB,CAAC;IAEvE;GACF,SAAS,OAAO;IACd,IAAI,MAAM,QAAQ,kBAAkB,KAAK;GAC3C;EACF,CAAC;CACH;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"access-token.model.mjs","names":[],"sources":["../../../../../../../../@warlock.js/auth/src/models/access-token/access-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\nimport { v } from \"@warlock.js/seal\";\nimport type { Auth } from \"../auth.model\";\n\n/**\n * Seal schema for the persisted access-token record. Exported so an override\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\n * base shape.\n *\n * `last_access` and the `is_active` soft-revoke flag were removed — neither was\n * ever read; access tokens are revoked by deleting the row.\n */\nexport const accessTokenSchema = v.object({\n token: v.string().required(),\n user_id: v.scalar().required(),\n user_type: v.string().required(),\n expires_at: v.date().required(),\n});\n\n/**\n * Persisted access-token record + the data layer for access tokens.\n *\n * **Role.** Owns access-token persistence and lookup. The middleware checks a\n * presented JWT against this table so deleting a row (logout) invalidates the\n * token immediately, before its JWT expiry. The auth service goes through the\n * named statics exclusively, so it never hard-codes a column name and an\n * override can rename/add columns by registering under\n * `config.auth.accessToken.model`.\n */\nexport class AccessToken extends Model {\n public static table = \"access_tokens\";\n\n public static schema = accessTokenSchema;\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 /**\n * Persist a freshly-signed access token for the user.\n */\n public static issue(user: Auth, token: string, expiresAt: Date) {\n return this.create({\n token,\n user_id: user.id,\n user_type: user.userType,\n expires_at: expiresAt,\n });\n }\n\n /**\n * Find an access-token row by its raw token string.\n */\n public static findByToken(token: string): Promise<AccessToken | null> {\n return this.first({ token });\n }\n\n /**\n * Delete a specific token that belongs to the given user.\n */\n public static deleteForUser(user: Auth, token: string) {\n return this.delete({ token, user_id: user.id });\n }\n\n /**\n * Delete every access token belonging to the user.\n */\n public static deleteAllForUser(user: Auth) {\n return this.delete({ user_id: user.id });\n }\n\n /**\n * Hard-delete every expired access-token row. Returns the number removed.\n * Runs from the `auth.cleanup` CLI command (a cold batch path).\n */\n public static async purgeExpired(): Promise<number> {\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\n\n for (const token of expiredTokens) {\n await token.destroy();\n }\n\n return expiredTokens.length;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAYA,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;;;;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;AACF"}
1
+ {"version":3,"file":"access-token.model.mjs","names":[],"sources":["../../../../../../../../@warlock.js/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 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 * 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"],"mappings":";;;;;;;;;;;;AAYA,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;;;;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;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../@warlock.js/auth/src/models/access-token/migration.ts"],"sourcesContent":["import { migrate } from \"@warlock.js/cascade\";\nimport { AccessToken } from \"./access-token.model\";\n\nexport const AccessTokenMigration = migrate(AccessToken, {\n name: \"accessToken\",\n up() {\n // Create table\n this.createTableIfNotExists();\n\n // Primary key\n this.primaryUuid();\n\n // Token field\n this.text(\"token\").unique();\n\n // User reference (flat columns for cross-driver compatibility)\n this.uuid(\"user_id\").index();\n this.string(\"user_type\", 50).nullable();\n\n // Expiry — enables server-side cleanup of stale access-token rows\n this.timestamp(\"expires_at\").index().nullable();\n\n // Timestamps\n this.timestamps();\n },\n down() {\n this.dropTableIfExists();\n },\n});\n"],"mappings":";;;;AAGA,MAAa,uBAAuB,QAAQ,aAAa;CACvD,MAAM;CACN,KAAK;EAEH,KAAK,uBAAuB;EAG5B,KAAK,YAAY;EAGjB,KAAK,KAAK,OAAO,CAAC,CAAC,OAAO;EAG1B,KAAK,KAAK,SAAS,CAAC,CAAC,MAAM;EAC3B,KAAK,OAAO,aAAa,EAAE,CAAC,CAAC,SAAS;EAGtC,KAAK,UAAU,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EAG9C,KAAK,WAAW;CAClB;CACA,OAAO;EACL,KAAK,kBAAkB;CACzB;AACF,CAAC"}
1
+ {"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../@warlock.js/auth/src/models/access-token/migration.ts"],"sourcesContent":["import { migrate } from \"@warlock.js/cascade\";\r\nimport { AccessToken } from \"./access-token.model\";\r\n\r\nexport const AccessTokenMigration = migrate(AccessToken, {\r\n name: \"accessToken\",\r\n up() {\r\n // Create table\r\n this.createTableIfNotExists();\r\n\r\n // Primary key\r\n this.primaryUuid();\r\n\r\n // Token field\r\n this.text(\"token\").unique();\r\n\r\n // User reference (flat columns for cross-driver compatibility)\r\n this.uuid(\"user_id\").index();\r\n this.string(\"user_type\", 50).nullable();\r\n\r\n // Expiry — enables server-side cleanup of stale access-token rows\r\n this.timestamp(\"expires_at\").index().nullable();\r\n\r\n // Timestamps\r\n this.timestamps();\r\n },\r\n down() {\r\n this.dropTableIfExists();\r\n },\r\n});\r\n"],"mappings":";;;;AAGA,MAAa,uBAAuB,QAAQ,aAAa;CACvD,MAAM;CACN,KAAK;EAEH,KAAK,uBAAuB;EAG5B,KAAK,YAAY;EAGjB,KAAK,KAAK,OAAO,CAAC,CAAC,OAAO;EAG1B,KAAK,KAAK,SAAS,CAAC,CAAC,MAAM;EAC3B,KAAK,OAAO,aAAa,EAAE,CAAC,CAAC,SAAS;EAGtC,KAAK,UAAU,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EAG9C,KAAK,WAAW;CAClB;CACA,OAAO;EACL,KAAK,kBAAkB;CACzB;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../@warlock.js/auth/src/models/refresh-token/migration.ts"],"sourcesContent":["import { migrate } from \"@warlock.js/cascade\";\nimport { RefreshToken } from \"./refresh-token.model\";\n\nexport const RefreshTokenMigration = migrate(RefreshToken, {\n name: \"refreshToken\",\n up() {\n // Create table\n this.createTableIfNotExists();\n\n // Primary key\n this.primaryUuid();\n\n // Token fields\n this.text(\"token\").unique();\n this.uuid(\"user_id\").index();\n this.string(\"user_type\", 50).nullable();\n this.text(\"family_id\").index().nullable();\n this.timestamp(\"expires_at\").index().nullable();\n this.timestamp(\"last_used_at\").nullable();\n this.timestamp(\"revoked_at\").nullable();\n this.json(\"device_info\").nullable();\n\n // Composite index for the per-user active-session lookups\n // (revokeAllFor / enforceMax / activeFor all filter on user_id + user_type).\n this.index([\"user_id\", \"user_type\"]);\n\n // Timestamps\n this.timestamps();\n },\n down() {\n this.dropTableIfExists();\n },\n});\n"],"mappings":";;;;AAGA,MAAa,wBAAwB,QAAQ,cAAc;CACzD,MAAM;CACN,KAAK;EAEH,KAAK,uBAAuB;EAG5B,KAAK,YAAY;EAGjB,KAAK,KAAK,OAAO,CAAC,CAAC,OAAO;EAC1B,KAAK,KAAK,SAAS,CAAC,CAAC,MAAM;EAC3B,KAAK,OAAO,aAAa,EAAE,CAAC,CAAC,SAAS;EACtC,KAAK,KAAK,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EACxC,KAAK,UAAU,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EAC9C,KAAK,UAAU,cAAc,CAAC,CAAC,SAAS;EACxC,KAAK,UAAU,YAAY,CAAC,CAAC,SAAS;EACtC,KAAK,KAAK,aAAa,CAAC,CAAC,SAAS;EAIlC,KAAK,MAAM,CAAC,WAAW,WAAW,CAAC;EAGnC,KAAK,WAAW;CAClB;CACA,OAAO;EACL,KAAK,kBAAkB;CACzB;AACF,CAAC"}
1
+ {"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../@warlock.js/auth/src/models/refresh-token/migration.ts"],"sourcesContent":["import { migrate } from \"@warlock.js/cascade\";\r\nimport { RefreshToken } from \"./refresh-token.model\";\r\n\r\nexport const RefreshTokenMigration = migrate(RefreshToken, {\r\n name: \"refreshToken\",\r\n up() {\r\n // Create table\r\n this.createTableIfNotExists();\r\n\r\n // Primary key\r\n this.primaryUuid();\r\n\r\n // Token fields\r\n this.text(\"token\").unique();\r\n this.uuid(\"user_id\").index();\r\n this.string(\"user_type\", 50).nullable();\r\n this.text(\"family_id\").index().nullable();\r\n this.timestamp(\"expires_at\").index().nullable();\r\n this.timestamp(\"last_used_at\").nullable();\r\n this.timestamp(\"revoked_at\").nullable();\r\n this.json(\"device_info\").nullable();\r\n\r\n // Composite index for the per-user active-session lookups\r\n // (revokeAllFor / enforceMax / activeFor all filter on user_id + user_type).\r\n this.index([\"user_id\", \"user_type\"]);\r\n\r\n // Timestamps\r\n this.timestamps();\r\n },\r\n down() {\r\n this.dropTableIfExists();\r\n },\r\n});\r\n"],"mappings":";;;;AAGA,MAAa,wBAAwB,QAAQ,cAAc;CACzD,MAAM;CACN,KAAK;EAEH,KAAK,uBAAuB;EAG5B,KAAK,YAAY;EAGjB,KAAK,KAAK,OAAO,CAAC,CAAC,OAAO;EAC1B,KAAK,KAAK,SAAS,CAAC,CAAC,MAAM;EAC3B,KAAK,OAAO,aAAa,EAAE,CAAC,CAAC,SAAS;EACtC,KAAK,KAAK,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EACxC,KAAK,UAAU,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EAC9C,KAAK,UAAU,cAAc,CAAC,CAAC,SAAS;EACxC,KAAK,UAAU,YAAY,CAAC,CAAC,SAAS;EACtC,KAAK,KAAK,aAAa,CAAC,CAAC,SAAS;EAIlC,KAAK,MAAM,CAAC,WAAW,WAAW,CAAC;EAGnC,KAAK,WAAW;CAClB;CACA,OAAO;EACL,KAAK,kBAAkB;CACzB;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"refresh-token.model.mjs","names":[],"sources":["../../../../../../../../@warlock.js/auth/src/models/refresh-token/refresh-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\nimport { v } from \"@warlock.js/seal\";\nimport type { DeviceInfo } from \"../../contracts/types\";\nimport type { Auth } from \"../auth.model\";\n\n/**\n * Seal schema for the persisted refresh-token record. Exported so an override\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\n * base shape:\n *\n * @example\n * export class AppRefreshToken extends RefreshToken {\n * public static schema = refreshTokenSchema.extend({\n * organization_id: v.string().required(),\n * });\n * }\n */\nexport const refreshTokenSchema = v.object({\n token: v.string().required(),\n user_id: v.scalar().required(),\n user_type: v.string().required(),\n family_id: v.string().required(),\n expires_at: v.date().required(),\n last_used_at: v.date().default(() => new Date()),\n revoked_at: v.date().optional(),\n device_info: v.record(v.any()).optional(),\n});\n\n/**\n * Extra attributes captured when a refresh token is issued.\n */\nexport type RefreshTokenIssueOptions = {\n familyId: string;\n expiresAt: string;\n deviceInfo?: DeviceInfo;\n};\n\n/**\n * Persisted refresh-token record + the data layer for refresh tokens.\n *\n * **Role.** Owns every refresh-token read, write, and lifecycle transition.\n * The auth service drives refresh-token state exclusively through this model's\n * named statics and instance methods, so it never hard-codes a column name —\n * which is what makes a snake/camel mismatch (the historical `userId` bug)\n * structurally impossible and lets an override rename or add columns by\n * extending this class and registering it under `config.auth.refreshToken.model`.\n */\nexport class RefreshToken extends Model {\n public static table = \"refresh_tokens\";\n\n public static schema = refreshTokenSchema;\n\n /** Token family this row belongs to (rotation / replay grouping). */\n public get familyId(): string {\n return this.get(\"family_id\");\n }\n\n /** Whether the token's `expires_at` is in the past. */\n public get isExpired(): boolean {\n const expiresAt = this.get(\"expires_at\");\n\n if (!expiresAt) return false;\n\n return new Date() > new Date(expiresAt);\n }\n\n /** Whether the token has been revoked. */\n public get isRevoked(): boolean {\n return !!this.get(\"revoked_at\");\n }\n\n /** Whether the token is still usable (not expired and not revoked). */\n public get isValid(): boolean {\n return !this.isExpired && !this.isRevoked;\n }\n\n /**\n * Unconditionally stamp `revoked_at` on this token.\n */\n public async revoke(): Promise<this> {\n return this.merge({ revoked_at: new Date() }).save();\n }\n\n /**\n * Atomically revoke this token ONLY if it is still active. Resolves to `true`\n * when this call performed the revoke, `false` when a concurrent request had\n * already revoked it — the win/lose signal that powers rotation replay\n * detection. Uses a conditional UPDATE so two concurrent rotations of the\n * same token can never both succeed.\n */\n public async revokeIfActive(): Promise<boolean> {\n const modelClass = this.constructor as typeof RefreshToken;\n\n const revokedCount = await modelClass.atomic(\n { id: this.id, revoked_at: null },\n { $set: { revoked_at: new Date() } },\n );\n\n return revokedCount > 0;\n }\n\n /**\n * Touch `last_used_at` without revoking — the non-rotating refresh path.\n */\n public async markAsUsed(): Promise<void> {\n await this.merge({ last_used_at: new Date() }).save();\n }\n\n /**\n * Persist a freshly-signed refresh token for the user.\n */\n public static issue(user: Auth, token: string, options: RefreshTokenIssueOptions) {\n return this.create({\n token,\n user_id: user.id,\n user_type: user.userType,\n family_id: options.familyId,\n expires_at: options.expiresAt,\n device_info: options.deviceInfo\n ? {\n userAgent: options.deviceInfo.userAgent,\n ip: options.deviceInfo.ip,\n deviceId: options.deviceInfo.deviceId,\n }\n : undefined,\n });\n }\n\n /**\n * Find a refresh-token row by its raw token string.\n */\n public static findByToken(token: string): Promise<RefreshToken | null> {\n return this.first({ token });\n }\n\n /**\n * Find a refresh token scoped to a user — used by logout so a caller can only\n * revoke a token that actually belongs to them.\n */\n public static findForUser(user: Auth, token: string): Promise<RefreshToken | null> {\n return this.first({ token, user_id: user.id });\n }\n\n /**\n * Delete a specific refresh token belonging to the user.\n */\n public static deleteForUser(user: Auth, token: string) {\n return this.delete({ token, user_id: user.id });\n }\n\n /**\n * Active, unexpired sessions for the user, newest first.\n */\n public static activeFor(user: Auth): Promise<RefreshToken[]> {\n return this.query()\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\n .where(\"expires_at\", \">\", new Date())\n .orderBy(\"created_at\", \"desc\")\n .get();\n }\n\n /**\n * Revoke every still-active refresh token for the user, returning the rows\n * that were revoked so the caller can emit a per-token event for each.\n *\n * Rows are fetched BEFORE they are revoked: a bulk `findAndUpdate` keyed on\n * `revoked_at: null` would re-query the same predicate after the update and\n * match nothing, returning an empty set.\n */\n public static async revokeAllFor(user: Auth): Promise<RefreshToken[]> {\n const tokens = await this.query()\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\n .get();\n\n for (const token of tokens) {\n await token.revoke();\n }\n\n return tokens;\n }\n\n /**\n * Revoke every still-active token in a family (rotation breach containment),\n * returning the revoked rows (fetched before revocation — see `revokeAllFor`).\n */\n public static async revokeFamily(familyId: string): Promise<RefreshToken[]> {\n const tokens = await this.query().where({ family_id: familyId, revoked_at: null }).get();\n\n for (const token of tokens) {\n await token.revoke();\n }\n\n return tokens;\n }\n\n /**\n * Revoke the oldest active tokens so at most `max - 1` remain — making room\n * for the about-to-be-issued one. Bounded by `max`, so the per-row loop is\n * small.\n */\n public static async enforceMax(user: Auth, max: number): Promise<void> {\n const activeTokens = await this.query()\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\n .orderBy(\"created_at\", \"asc\")\n .get();\n\n if (activeTokens.length < max) return;\n\n const tokensToRevoke = activeTokens.slice(0, activeTokens.length - max + 1);\n\n for (const token of tokensToRevoke) {\n await token.revoke();\n }\n }\n\n /**\n * Hard-delete every expired refresh token, returning the deleted rows so the\n * caller can emit a per-token event. Runs from the `auth.cleanup` CLI command\n * (a cold batch path).\n */\n public static async purgeExpired(): Promise<RefreshToken[]> {\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\n\n for (const token of expiredTokens) {\n await token.destroy();\n }\n\n return expiredTokens;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"refresh-token.model.mjs","names":[],"sources":["../../../../../../../../@warlock.js/auth/src/models/refresh-token/refresh-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport type { DeviceInfo } from \"../../contracts/types\";\r\nimport type { Auth } from \"../auth.model\";\r\n\r\n/**\r\n * Seal schema for the persisted refresh-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 * @example\r\n * export class AppRefreshToken extends RefreshToken {\r\n * public static schema = refreshTokenSchema.extend({\r\n * organization_id: v.string().required(),\r\n * });\r\n * }\r\n */\r\nexport const refreshTokenSchema = v.object({\r\n token: v.string().required(),\r\n user_id: v.scalar().required(),\r\n user_type: v.string().required(),\r\n family_id: v.string().required(),\r\n expires_at: v.date().required(),\r\n last_used_at: v.date().default(() => new Date()),\r\n revoked_at: v.date().optional(),\r\n device_info: v.record(v.any()).optional(),\r\n});\r\n\r\n/**\r\n * Extra attributes captured when a refresh token is issued.\r\n */\r\nexport type RefreshTokenIssueOptions = {\r\n familyId: string;\r\n expiresAt: string;\r\n deviceInfo?: DeviceInfo;\r\n};\r\n\r\n/**\r\n * Persisted refresh-token record + the data layer for refresh tokens.\r\n *\r\n * **Role.** Owns every refresh-token read, write, and lifecycle transition.\r\n * The auth service drives refresh-token state exclusively through this model's\r\n * named statics and instance methods, so it never hard-codes a column name —\r\n * which is what makes a snake/camel mismatch (the historical `userId` bug)\r\n * structurally impossible and lets an override rename or add columns by\r\n * extending this class and registering it under `config.auth.refreshToken.model`.\r\n */\r\nexport class RefreshToken extends Model {\r\n public static table = \"refresh_tokens\";\r\n\r\n public static schema = refreshTokenSchema;\r\n\r\n /** Token family this row belongs to (rotation / replay grouping). */\r\n public get familyId(): string {\r\n return this.get(\"family_id\");\r\n }\r\n\r\n /** Whether the token's `expires_at` is in the past. */\r\n public get isExpired(): boolean {\r\n const expiresAt = this.get(\"expires_at\");\r\n\r\n if (!expiresAt) return false;\r\n\r\n return new Date() > new Date(expiresAt);\r\n }\r\n\r\n /** Whether the token has been revoked. */\r\n public get isRevoked(): boolean {\r\n return !!this.get(\"revoked_at\");\r\n }\r\n\r\n /** Whether the token is still usable (not expired and not revoked). */\r\n public get isValid(): boolean {\r\n return !this.isExpired && !this.isRevoked;\r\n }\r\n\r\n /**\r\n * Unconditionally stamp `revoked_at` on this token.\r\n */\r\n public async revoke(): Promise<this> {\r\n return this.merge({ revoked_at: new Date() }).save();\r\n }\r\n\r\n /**\r\n * Atomically revoke this token ONLY if it is still active. Resolves to `true`\r\n * when this call performed the revoke, `false` when a concurrent request had\r\n * already revoked it — the win/lose signal that powers rotation replay\r\n * detection. Uses a conditional UPDATE so two concurrent rotations of the\r\n * same token can never both succeed.\r\n */\r\n public async revokeIfActive(): Promise<boolean> {\r\n const modelClass = this.constructor as typeof RefreshToken;\r\n\r\n const revokedCount = await modelClass.atomic(\r\n { id: this.id, revoked_at: null },\r\n { $set: { revoked_at: new Date() } },\r\n );\r\n\r\n return revokedCount > 0;\r\n }\r\n\r\n /**\r\n * Touch `last_used_at` without revoking — the non-rotating refresh path.\r\n */\r\n public async markAsUsed(): Promise<void> {\r\n await this.merge({ last_used_at: new Date() }).save();\r\n }\r\n\r\n /**\r\n * Persist a freshly-signed refresh token for the user.\r\n */\r\n public static issue(user: Auth, token: string, options: RefreshTokenIssueOptions) {\r\n return this.create({\r\n token,\r\n user_id: user.id,\r\n user_type: user.userType,\r\n family_id: options.familyId,\r\n expires_at: options.expiresAt,\r\n device_info: options.deviceInfo\r\n ? {\r\n userAgent: options.deviceInfo.userAgent,\r\n ip: options.deviceInfo.ip,\r\n deviceId: options.deviceInfo.deviceId,\r\n }\r\n : undefined,\r\n });\r\n }\r\n\r\n /**\r\n * Find a refresh-token row by its raw token string.\r\n */\r\n public static findByToken(token: string): Promise<RefreshToken | null> {\r\n return this.first({ token });\r\n }\r\n\r\n /**\r\n * Find a refresh token scoped to a user — used by logout so a caller can only\r\n * revoke a token that actually belongs to them.\r\n */\r\n public static findForUser(user: Auth, token: string): Promise<RefreshToken | null> {\r\n return this.first({ token, user_id: user.id });\r\n }\r\n\r\n /**\r\n * Delete a specific refresh token belonging to the 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 * Active, unexpired sessions for the user, newest first.\r\n */\r\n public static activeFor(user: Auth): Promise<RefreshToken[]> {\r\n return this.query()\r\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\r\n .where(\"expires_at\", \">\", new Date())\r\n .orderBy(\"created_at\", \"desc\")\r\n .get();\r\n }\r\n\r\n /**\r\n * Revoke every still-active refresh token for the user, returning the rows\r\n * that were revoked so the caller can emit a per-token event for each.\r\n *\r\n * Rows are fetched BEFORE they are revoked: a bulk `findAndUpdate` keyed on\r\n * `revoked_at: null` would re-query the same predicate after the update and\r\n * match nothing, returning an empty set.\r\n */\r\n public static async revokeAllFor(user: Auth): Promise<RefreshToken[]> {\r\n const tokens = await this.query()\r\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\r\n .get();\r\n\r\n for (const token of tokens) {\r\n await token.revoke();\r\n }\r\n\r\n return tokens;\r\n }\r\n\r\n /**\r\n * Revoke every still-active token in a family (rotation breach containment),\r\n * returning the revoked rows (fetched before revocation — see `revokeAllFor`).\r\n */\r\n public static async revokeFamily(familyId: string): Promise<RefreshToken[]> {\r\n const tokens = await this.query().where({ family_id: familyId, revoked_at: null }).get();\r\n\r\n for (const token of tokens) {\r\n await token.revoke();\r\n }\r\n\r\n return tokens;\r\n }\r\n\r\n /**\r\n * Revoke the oldest active tokens so at most `max - 1` remain — making room\r\n * for the about-to-be-issued one. Bounded by `max`, so the per-row loop is\r\n * small.\r\n */\r\n public static async enforceMax(user: Auth, max: number): Promise<void> {\r\n const activeTokens = await this.query()\r\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\r\n .orderBy(\"created_at\", \"asc\")\r\n .get();\r\n\r\n if (activeTokens.length < max) return;\r\n\r\n const tokensToRevoke = activeTokens.slice(0, activeTokens.length - max + 1);\r\n\r\n for (const token of tokensToRevoke) {\r\n await token.revoke();\r\n }\r\n }\r\n\r\n /**\r\n * Hard-delete every expired refresh token, returning the deleted rows so the\r\n * caller can emit a per-token event. Runs from the `auth.cleanup` CLI command\r\n * (a cold batch path).\r\n */\r\n public static async purgeExpired(): Promise<RefreshToken[]> {\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;\r\n }\r\n}\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"auth-config.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/services/auth-config.ts"],"sourcesContent":["import { config } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport { type Algorithm } from \"fast-jwt\";\nimport type { LogoutWithoutTokenBehavior } from \"../contracts/types\";\n\nconst warnedLegacyKeys = new Set<string>();\n\n/**\n * Resolve an auth setting, preferring the new `auth.accessToken.*` /\n * `auth.refreshToken.*` key and falling back to the deprecated `auth.jwt.*`\n * shape — warning once per legacy key. Returns `fallback` when neither is set.\n *\n * This is the backward-compatible shim that lets existing `auth.jwt.*` configs\n * keep working after the config split.\n */\nfunction resolve<T>(newKey: string, legacyKey: string, fallback?: T): T {\n const fromNew = config.key(`auth.${newKey}`);\n\n if (fromNew !== undefined && fromNew !== null) {\n return fromNew as T;\n }\n\n const fromLegacy = config.key(`auth.${legacyKey}`);\n\n if (fromLegacy !== undefined && fromLegacy !== null) {\n if (!warnedLegacyKeys.has(legacyKey)) {\n warnedLegacyKeys.add(legacyKey);\n log.warn(\"auth\", \"config-deprecation\", `auth.${legacyKey} is deprecated — use auth.${newKey}`);\n }\n\n return fromLegacy as T;\n }\n\n return fallback as T;\n}\n\n/**\n * Typed, backward-compatible access to auth configuration. The service and the\n * jwt signer read configuration exclusively through here, so the new split\n * config and the legacy `auth.jwt.*` shape both resolve the same way.\n */\nexport const authConfig = {\n accessToken: {\n /** Signing secret (legacy: `auth.jwt.secret`). Throws if neither is set. */\n secret: (): string => {\n const secret = resolve<string | undefined>(\"accessToken.secret\", \"jwt.secret\");\n\n if (!secret) {\n throw new Error(\"auth: no JWT secret configured — set `auth.accessToken.secret`.\");\n }\n\n return secret;\n },\n /** Signing algorithm (legacy: `auth.jwt.algorithm`). */\n algorithm: (): Algorithm => resolve(\"accessToken.algorithm\", \"jwt.algorithm\", \"HS256\"),\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.expiresIn`). */\n expiresIn: (): string | undefined => resolve(\"accessToken.expiresIn\", \"jwt.expiresIn\"),\n },\n refreshToken: {\n /** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty ⇒ fall back to the access secret. */\n secret: (): string | undefined => resolve(\"refreshToken.secret\", \"jwt.refresh.secret\"),\n /** Whether refresh tokens are enabled (legacy: `auth.jwt.refresh.enabled`). */\n enabled: (): boolean => resolve(\"refreshToken.enabled\", \"jwt.refresh.enabled\", true),\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.refresh.expiresIn`). */\n expiresIn: (): string => resolve(\"refreshToken.expiresIn\", \"jwt.refresh.expiresIn\", \"7d\"),\n /** Rotate-on-use (legacy: `auth.jwt.refresh.rotation`). */\n rotation: (): boolean => resolve(\"refreshToken.rotation\", \"jwt.refresh.rotation\", true),\n /** Max active tokens per user (legacy: `auth.jwt.refresh.maxPerUser`). */\n maxPerUser: (): number => resolve(\"refreshToken.maxPerUser\", \"jwt.refresh.maxPerUser\", 5),\n /** Logout-without-token behavior (legacy: `auth.jwt.refresh.logoutWithoutToken`). */\n logoutWithoutToken: (): LogoutWithoutTokenBehavior =>\n resolve(\"refreshToken.logoutWithoutToken\", \"jwt.refresh.logoutWithoutToken\", \"revoke-all\"),\n },\n};\n"],"mappings":";;;;AAKA,MAAM,mCAAmB,IAAI,IAAY;;;;;;;;;AAUzC,SAAS,QAAW,QAAgB,WAAmB,UAAiB;CACtE,MAAM,UAAU,OAAO,IAAI,QAAQ,QAAQ;CAE3C,IAAI,YAAY,UAAa,YAAY,MACvC,OAAO;CAGT,MAAM,aAAa,OAAO,IAAI,QAAQ,WAAW;CAEjD,IAAI,eAAe,UAAa,eAAe,MAAM;EACnD,IAAI,CAAC,iBAAiB,IAAI,SAAS,GAAG;GACpC,iBAAiB,IAAI,SAAS;GAC9B,IAAI,KAAK,QAAQ,sBAAsB,QAAQ,UAAU,4BAA4B,QAAQ;EAC/F;EAEA,OAAO;CACT;CAEA,OAAO;AACT;;;;;;AAOA,MAAa,aAAa;CACxB,aAAa;;EAEX,cAAsB;GACpB,MAAM,SAAS,QAA4B,sBAAsB,YAAY;GAE7E,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,iEAAiE;GAGnF,OAAO;EACT;;EAEA,iBAA4B,QAAQ,yBAAyB,iBAAiB,OAAO;;EAErF,iBAAqC,QAAQ,yBAAyB,eAAe;CACvF;CACA,cAAc;;EAEZ,cAAkC,QAAQ,uBAAuB,oBAAoB;;EAErF,eAAwB,QAAQ,wBAAwB,uBAAuB,IAAI;;EAEnF,iBAAyB,QAAQ,0BAA0B,yBAAyB,IAAI;;EAExF,gBAAyB,QAAQ,yBAAyB,wBAAwB,IAAI;;EAEtF,kBAA0B,QAAQ,2BAA2B,0BAA0B,CAAC;;EAExF,0BACE,QAAQ,mCAAmC,kCAAkC,YAAY;CAC7F;AACF"}
1
+ {"version":3,"file":"auth-config.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/services/auth-config.ts"],"sourcesContent":["import { config } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport type { LogoutWithoutTokenBehavior } from \"../contracts/types\";\r\n\r\nconst warnedLegacyKeys = new Set<string>();\r\n\r\n/**\r\n * Resolve an auth setting, preferring the new `auth.accessToken.*` /\r\n * `auth.refreshToken.*` key and falling back to the deprecated `auth.jwt.*`\r\n * shape — warning once per legacy key. Returns `fallback` when neither is set.\r\n *\r\n * This is the backward-compatible shim that lets existing `auth.jwt.*` configs\r\n * keep working after the config split.\r\n */\r\nfunction resolve<T>(newKey: string, legacyKey: string, fallback?: T): T {\r\n const fromNew = config.key(`auth.${newKey}`);\r\n\r\n if (fromNew !== undefined && fromNew !== null) {\r\n return fromNew as T;\r\n }\r\n\r\n const fromLegacy = config.key(`auth.${legacyKey}`);\r\n\r\n if (fromLegacy !== undefined && fromLegacy !== null) {\r\n if (!warnedLegacyKeys.has(legacyKey)) {\r\n warnedLegacyKeys.add(legacyKey);\r\n log.warn(\"auth\", \"config-deprecation\", `auth.${legacyKey} is deprecated — use auth.${newKey}`);\r\n }\r\n\r\n return fromLegacy as T;\r\n }\r\n\r\n return fallback as T;\r\n}\r\n\r\n/**\r\n * Typed, backward-compatible access to auth configuration. The service and the\r\n * jwt signer read configuration exclusively through here, so the new split\r\n * config and the legacy `auth.jwt.*` shape both resolve the same way.\r\n */\r\nexport const authConfig = {\r\n accessToken: {\r\n /** Signing secret (legacy: `auth.jwt.secret`). Throws if neither is set. */\r\n secret: (): string => {\r\n const secret = resolve<string | undefined>(\"accessToken.secret\", \"jwt.secret\");\r\n\r\n if (!secret) {\r\n throw new Error(\"auth: no JWT secret configured — set `auth.accessToken.secret`.\");\r\n }\r\n\r\n return secret;\r\n },\r\n /** Signing algorithm (legacy: `auth.jwt.algorithm`). */\r\n algorithm: (): Algorithm => resolve(\"accessToken.algorithm\", \"jwt.algorithm\", \"HS256\"),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.expiresIn`). */\r\n expiresIn: (): string | undefined => resolve(\"accessToken.expiresIn\", \"jwt.expiresIn\"),\r\n },\r\n refreshToken: {\r\n /** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty ⇒ fall back to the access secret. */\r\n secret: (): string | undefined => resolve(\"refreshToken.secret\", \"jwt.refresh.secret\"),\r\n /** Whether refresh tokens are enabled (legacy: `auth.jwt.refresh.enabled`). */\r\n enabled: (): boolean => resolve(\"refreshToken.enabled\", \"jwt.refresh.enabled\", true),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.refresh.expiresIn`). */\r\n expiresIn: (): string => resolve(\"refreshToken.expiresIn\", \"jwt.refresh.expiresIn\", \"7d\"),\r\n /** Rotate-on-use (legacy: `auth.jwt.refresh.rotation`). */\r\n rotation: (): boolean => resolve(\"refreshToken.rotation\", \"jwt.refresh.rotation\", true),\r\n /** Max active tokens per user (legacy: `auth.jwt.refresh.maxPerUser`). */\r\n maxPerUser: (): number => resolve(\"refreshToken.maxPerUser\", \"jwt.refresh.maxPerUser\", 5),\r\n /** Logout-without-token behavior (legacy: `auth.jwt.refresh.logoutWithoutToken`). */\r\n logoutWithoutToken: (): LogoutWithoutTokenBehavior =>\r\n resolve(\"refreshToken.logoutWithoutToken\", \"jwt.refresh.logoutWithoutToken\", \"revoke-all\"),\r\n },\r\n};\r\n"],"mappings":";;;;AAKA,MAAM,mCAAmB,IAAI,IAAY;;;;;;;;;AAUzC,SAAS,QAAW,QAAgB,WAAmB,UAAiB;CACtE,MAAM,UAAU,OAAO,IAAI,QAAQ,QAAQ;CAE3C,IAAI,YAAY,UAAa,YAAY,MACvC,OAAO;CAGT,MAAM,aAAa,OAAO,IAAI,QAAQ,WAAW;CAEjD,IAAI,eAAe,UAAa,eAAe,MAAM;EACnD,IAAI,CAAC,iBAAiB,IAAI,SAAS,GAAG;GACpC,iBAAiB,IAAI,SAAS;GAC9B,IAAI,KAAK,QAAQ,sBAAsB,QAAQ,UAAU,4BAA4B,QAAQ;EAC/F;EAEA,OAAO;CACT;CAEA,OAAO;AACT;;;;;;AAOA,MAAa,aAAa;CACxB,aAAa;;EAEX,cAAsB;GACpB,MAAM,SAAS,QAA4B,sBAAsB,YAAY;GAE7E,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,iEAAiE;GAGnF,OAAO;EACT;;EAEA,iBAA4B,QAAQ,yBAAyB,iBAAiB,OAAO;;EAErF,iBAAqC,QAAQ,yBAAyB,eAAe;CACvF;CACA,cAAc;;EAEZ,cAAkC,QAAQ,uBAAuB,oBAAoB;;EAErF,eAAwB,QAAQ,wBAAwB,uBAAuB,IAAI;;EAEnF,iBAAyB,QAAQ,0BAA0B,yBAAyB,IAAI;;EAExF,gBAAyB,QAAQ,yBAAyB,wBAAwB,IAAI;;EAEtF,kBAA0B,QAAQ,2BAA2B,0BAA0B,CAAC;;EAExF,0BACE,QAAQ,mCAAmC,kCAAkC,YAAY;CAC7F;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"auth-events.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/services/auth-events.ts"],"sourcesContent":["import events, { type EventSubscription } from \"@mongez/events\";\nimport { log } from \"@warlock.js/logger\";\nimport type { DeviceInfo, TokenPair } from \"../contracts/types\";\nimport type { Auth } from \"../models/auth.model\";\nimport type { RefreshToken } from \"../models/refresh-token\";\n\n/**\n * Auth event payload types\n */\nexport type AuthEventPayloads = {\n // Login events\n \"login.success\": [user: Auth, tokenPair: TokenPair, deviceInfo?: DeviceInfo];\n \"login.failed\": [credentials: { email?: string; username?: string }, reason: string];\n \"login.attempt\": [credentials: { email?: string; username?: string }];\n\n // Logout events\n logout: [user: Auth];\n \"logout.all\": [user: Auth];\n \"logout.failsafe\": [user: Auth];\n\n // Token events\n \"token.created\": [user: Auth, tokenPair: TokenPair];\n \"token.refreshed\": [user: Auth, newTokenPair: TokenPair, oldRefreshToken: RefreshToken];\n \"token.revoked\": [user: Auth, token: RefreshToken];\n \"token.expired\": [token: RefreshToken];\n \"token.familyRevoked\": [familyId: string, tokens: RefreshToken[]];\n\n // Password events\n \"password.changed\": [user: Auth];\n \"password.resetRequested\": [user: Auth, resetToken: string];\n \"password.reset\": [user: Auth];\n\n // Session events\n \"session.created\": [user: Auth, refreshToken: RefreshToken, deviceInfo?: DeviceInfo];\n \"session.destroyed\": [user: Auth, refreshToken: RefreshToken];\n\n // Cleanup events\n \"cleanup.completed\": [expiredCount: number];\n};\n\n/**\n * Auth event names\n */\nexport type AuthEventName = keyof AuthEventPayloads;\n\n/**\n * Callback type for a specific event\n */\nexport type AuthEventCallback<T extends AuthEventName> = (\n ...args: AuthEventPayloads[T]\n) => void | Promise<void>;\n\n/**\n * Event namespace prefix for auth events\n */\nconst AUTH_EVENT_PREFIX = \"auth.\";\n\n/**\n * Type-safe auth events manager\n *\n * @example\n * ```typescript\n * // Subscribe to events with full autocomplete\n * authEvents.on(\"login.success\", (user, tokenPair, deviceInfo) => {\n * console.log(`User ${user.id} logged in`);\n * });\n *\n * authEvents.on(\"token.refreshed\", (user, newPair, oldToken) => {\n * console.log(`Token refreshed for user ${user.id}`);\n * });\n *\n * // Trigger events\n * authEvents.emit(\"login.success\", user, tokenPair, deviceInfo);\n * ```\n */\nexport const authEvents = {\n /**\n * Subscribe to an auth event\n */\n on<T extends AuthEventName>(event: T, callback: AuthEventCallback<T>): EventSubscription {\n return events.subscribe(AUTH_EVENT_PREFIX + event, callback as Function);\n },\n\n /**\n * Subscribe to an auth event (alias for `on`)\n */\n subscribe<T extends AuthEventName>(event: T, callback: AuthEventCallback<T>): EventSubscription {\n return this.on(event, callback);\n },\n\n /**\n * Emit an auth event\n */\n emit<T extends AuthEventName>(event: T, ...args: AuthEventPayloads[T]): void {\n try {\n events.trigger(AUTH_EVENT_PREFIX + event, ...args);\n } catch (error) {\n // Auth events are fire-and-forget notifications: a throwing listener must\n // never break the flow that emitted it (e.g. turn a completed login into a\n // 500). Log and swallow. NOTE: this guards synchronous listener throws;\n // async listener rejections and \"later listeners still run\" need a fix in\n // @mongez/events (tracked separately).\n log.error(\"auth\", \"event\", error);\n }\n },\n\n /**\n * Emit an auth event (alias for `emit`)\n */\n trigger<T extends AuthEventName>(event: T, ...args: AuthEventPayloads[T]): void {\n this.emit(event, ...args);\n },\n\n /**\n * Unsubscribe from all auth events\n */\n unsubscribeAll(): void {\n events.unsubscribeNamespace(AUTH_EVENT_PREFIX.slice(0, -1));\n },\n\n /**\n * Unsubscribe from a specific auth event\n */\n off(event?: AuthEventName): void {\n if (event) {\n events.unsubscribe(AUTH_EVENT_PREFIX + event);\n } else {\n this.unsubscribeAll();\n }\n },\n};\n"],"mappings":";;;;;;;AAuDA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;AAoB1B,MAAa,aAAa;;;;CAIxB,GAA4B,OAAU,UAAmD;EACvF,OAAO,OAAO,UAAU,oBAAoB,OAAO,QAAoB;CACzE;;;;CAKA,UAAmC,OAAU,UAAmD;EAC9F,OAAO,KAAK,GAAG,OAAO,QAAQ;CAChC;;;;CAKA,KAA8B,OAAU,GAAG,MAAkC;EAC3E,IAAI;GACF,OAAO,QAAQ,oBAAoB,OAAO,GAAG,IAAI;EACnD,SAAS,OAAO;GAMd,IAAI,MAAM,QAAQ,SAAS,KAAK;EAClC;CACF;;;;CAKA,QAAiC,OAAU,GAAG,MAAkC;EAC9E,KAAK,KAAK,OAAO,GAAG,IAAI;CAC1B;;;;CAKA,iBAAuB;EACrB,OAAO,qBAAqB,kBAAkB,MAAM,GAAG,EAAE,CAAC;CAC5D;;;;CAKA,IAAI,OAA6B;EAC/B,IAAI,OACF,OAAO,YAAY,oBAAoB,KAAK;OAE5C,KAAK,eAAe;CAExB;AACF"}
1
+ {"version":3,"file":"auth-events.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/services/auth-events.ts"],"sourcesContent":["import events, { type EventSubscription } from \"@mongez/events\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport type { DeviceInfo, TokenPair } from \"../contracts/types\";\r\nimport type { Auth } from \"../models/auth.model\";\r\nimport type { RefreshToken } from \"../models/refresh-token\";\r\n\r\n/**\r\n * Auth event payload types\r\n */\r\nexport type AuthEventPayloads = {\r\n // Login events\r\n \"login.success\": [user: Auth, tokenPair: TokenPair, deviceInfo?: DeviceInfo];\r\n \"login.failed\": [credentials: { email?: string; username?: string }, reason: string];\r\n \"login.attempt\": [credentials: { email?: string; username?: string }];\r\n\r\n // Logout events\r\n logout: [user: Auth];\r\n \"logout.all\": [user: Auth];\r\n \"logout.failsafe\": [user: Auth];\r\n\r\n // Token events\r\n \"token.created\": [user: Auth, tokenPair: TokenPair];\r\n \"token.refreshed\": [user: Auth, newTokenPair: TokenPair, oldRefreshToken: RefreshToken];\r\n \"token.revoked\": [user: Auth, token: RefreshToken];\r\n \"token.expired\": [token: RefreshToken];\r\n \"token.familyRevoked\": [familyId: string, tokens: RefreshToken[]];\r\n\r\n // Password events\r\n \"password.changed\": [user: Auth];\r\n \"password.resetRequested\": [user: Auth, resetToken: string];\r\n \"password.reset\": [user: Auth];\r\n\r\n // Session events\r\n \"session.created\": [user: Auth, refreshToken: RefreshToken, deviceInfo?: DeviceInfo];\r\n \"session.destroyed\": [user: Auth, refreshToken: RefreshToken];\r\n\r\n // Cleanup events\r\n \"cleanup.completed\": [expiredCount: number];\r\n};\r\n\r\n/**\r\n * Auth event names\r\n */\r\nexport type AuthEventName = keyof AuthEventPayloads;\r\n\r\n/**\r\n * Callback type for a specific event\r\n */\r\nexport type AuthEventCallback<T extends AuthEventName> = (\r\n ...args: AuthEventPayloads[T]\r\n) => void | Promise<void>;\r\n\r\n/**\r\n * Event namespace prefix for auth events\r\n */\r\nconst AUTH_EVENT_PREFIX = \"auth.\";\r\n\r\n/**\r\n * Type-safe auth events manager\r\n *\r\n * @example\r\n * ```typescript\r\n * // Subscribe to events with full autocomplete\r\n * authEvents.on(\"login.success\", (user, tokenPair, deviceInfo) => {\r\n * console.log(`User ${user.id} logged in`);\r\n * });\r\n *\r\n * authEvents.on(\"token.refreshed\", (user, newPair, oldToken) => {\r\n * console.log(`Token refreshed for user ${user.id}`);\r\n * });\r\n *\r\n * // Trigger events\r\n * authEvents.emit(\"login.success\", user, tokenPair, deviceInfo);\r\n * ```\r\n */\r\nexport const authEvents = {\r\n /**\r\n * Subscribe to an auth event\r\n */\r\n on<T extends AuthEventName>(event: T, callback: AuthEventCallback<T>): EventSubscription {\r\n return events.subscribe(AUTH_EVENT_PREFIX + event, callback as Function);\r\n },\r\n\r\n /**\r\n * Subscribe to an auth event (alias for `on`)\r\n */\r\n subscribe<T extends AuthEventName>(event: T, callback: AuthEventCallback<T>): EventSubscription {\r\n return this.on(event, callback);\r\n },\r\n\r\n /**\r\n * Emit an auth event\r\n */\r\n emit<T extends AuthEventName>(event: T, ...args: AuthEventPayloads[T]): void {\r\n try {\r\n events.trigger(AUTH_EVENT_PREFIX + event, ...args);\r\n } catch (error) {\r\n // Auth events are fire-and-forget notifications: a throwing listener must\r\n // never break the flow that emitted it (e.g. turn a completed login into a\r\n // 500). Log and swallow. NOTE: this guards synchronous listener throws;\r\n // async listener rejections and \"later listeners still run\" need a fix in\r\n // @mongez/events (tracked separately).\r\n log.error(\"auth\", \"event\", error);\r\n }\r\n },\r\n\r\n /**\r\n * Emit an auth event (alias for `emit`)\r\n */\r\n trigger<T extends AuthEventName>(event: T, ...args: AuthEventPayloads[T]): void {\r\n this.emit(event, ...args);\r\n },\r\n\r\n /**\r\n * Unsubscribe from all auth events\r\n */\r\n unsubscribeAll(): void {\r\n events.unsubscribeNamespace(AUTH_EVENT_PREFIX.slice(0, -1));\r\n },\r\n\r\n /**\r\n * Unsubscribe from a specific auth event\r\n */\r\n off(event?: AuthEventName): void {\r\n if (event) {\r\n events.unsubscribe(AUTH_EVENT_PREFIX + event);\r\n } else {\r\n this.unsubscribeAll();\r\n }\r\n },\r\n};\r\n"],"mappings":";;;;;;;AAuDA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;AAoB1B,MAAa,aAAa;;;;CAIxB,GAA4B,OAAU,UAAmD;EACvF,OAAO,OAAO,UAAU,oBAAoB,OAAO,QAAoB;CACzE;;;;CAKA,UAAmC,OAAU,UAAmD;EAC9F,OAAO,KAAK,GAAG,OAAO,QAAQ;CAChC;;;;CAKA,KAA8B,OAAU,GAAG,MAAkC;EAC3E,IAAI;GACF,OAAO,QAAQ,oBAAoB,OAAO,GAAG,IAAI;EACnD,SAAS,OAAO;GAMd,IAAI,MAAM,QAAQ,SAAS,KAAK;EAClC;CACF;;;;CAKA,QAAiC,OAAU,GAAG,MAAkC;EAC9E,KAAK,KAAK,OAAO,GAAG,IAAI;CAC1B;;;;CAKA,iBAAuB;EACrB,OAAO,qBAAqB,kBAAkB,MAAM,GAAG,EAAE,CAAC;CAC5D;;;;CAKA,IAAI,OAA6B;EAC/B,IAAI,OACF,OAAO,YAAY,oBAAoB,KAAK;OAE5C,KAAK,eAAe;CAExB;AACF"}