@warlock.js/auth 5.0.2 → 5.2.2
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 +21 -0
- package/README.md +4 -0
- package/esm/commands/auth-cleanup-command.d.mts.map +1 -1
- package/esm/commands/auth-purge-never-expiring-command.d.mts.map +1 -1
- package/esm/commands/jwt-secret-generator-command.d.mts.map +1 -1
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/models/access-token/access-token.model.d.mts.map +1 -1
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/access-token/migration.mjs.map +1 -1
- package/esm/models/auth.model.d.mts.map +1 -1
- package/esm/models/refresh-token/migration.mjs.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/generate-jwt-secret.d.mts.map +1 -1
- package/esm/services/jwt.mjs.map +1 -1
- package/esm/utils/token-expiry.mjs.map +1 -1
- package/llms-full.txt +2 -0
- package/package.json +10 -7
- package/skills/auth-basics/SKILL.md +1 -1
- package/skills/overview/SKILL.md +2 -0
- package/skills/run-auth-commands/SKILL.md +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ All notable changes to `@warlock.js/auth` are documented in this file.
|
|
|
4
4
|
|
|
5
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
6
|
|
|
7
|
+
## 5.2.2
|
|
8
|
+
|
|
9
|
+
- `package.json` now declares `"warlock": { "environment": "server" }` — build-boundary metadata `@warlock.js/web` uses to refuse value-imports of this package from app client code (type-only imports are still allowed; server loaders/controllers/modules are unaffected).
|
|
10
|
+
|
|
11
|
+
## 5.1.0
|
|
12
|
+
|
|
13
|
+
No changes to `@warlock.js/auth`. Released in lockstep with the `@warlock.js/web`
|
|
14
|
+
React-execution fix and the `@warlock.js/core` CLI additions — see those packages'
|
|
15
|
+
changelogs.
|
|
16
|
+
|
|
17
|
+
## 5.0.2 - 2026-08-25
|
|
18
|
+
|
|
19
|
+
No changes to `@warlock.js/auth`. Released in lockstep with the `@warlock.js/web` SSR
|
|
20
|
+
fix (`ssr.noExternal`) — see that package's changelog.
|
|
21
|
+
|
|
22
|
+
## 5.0.1 - 2026-08-25
|
|
23
|
+
|
|
24
|
+
No changes to `@warlock.js/auth`. Released in lockstep with the `create-warlock` vite
|
|
25
|
+
resolution pin and the `@warlock.js/web` peer narrowing — see those packages'
|
|
26
|
+
changelogs.
|
|
27
|
+
|
|
7
28
|
## 5.0.0 - 2026-08-25
|
|
8
29
|
|
|
9
30
|
### Changed
|
package/README.md
CHANGED
|
@@ -10,6 +10,10 @@ yarn add @warlock.js/auth
|
|
|
10
10
|
|
|
11
11
|
`@warlock.js/auth` is coupled to `@warlock.js/core` — install it inside a Warlock project.
|
|
12
12
|
|
|
13
|
+
## Server-only
|
|
14
|
+
|
|
15
|
+
`@warlock.js/auth`'s entire runtime surface is server-only — its `package.json` declares `"warlock": { "environment": "server" }`. `@warlock.js/web`'s build treats this as a boundary: app client code cannot value-import this package (type-only imports are still allowed), while server loaders/controllers/modules can import it freely.
|
|
16
|
+
|
|
13
17
|
## Configure
|
|
14
18
|
|
|
15
19
|
```ts title="src/config/auth.ts"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-cleanup-command.d.mts","names":[],"sources":["../../../../../../../auth/src/commands/auth-cleanup-command.ts"],"mappings":";;AAYA;;;;AAA0C;;;iBAA1B,0BAAA
|
|
1
|
+
{"version":3,"file":"auth-cleanup-command.d.mts","names":[],"sources":["../../../../../../../auth/src/commands/auth-cleanup-command.ts"],"mappings":";;AAYA;;;;AAA0C;;;iBAA1B,0BAAA,CAAA,8BAA0B,UAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-purge-never-expiring-command.d.mts","names":[],"sources":["../../../../../../../auth/src/commands/auth-purge-never-expiring-command.ts"],"mappings":";;AA4BA;;;;AAAqD;;;;;;;;;;;;;;;;;;;iBAArC,qCAAA
|
|
1
|
+
{"version":3,"file":"auth-purge-never-expiring-command.d.mts","names":[],"sources":["../../../../../../../auth/src/commands/auth-purge-never-expiring-command.ts"],"mappings":";;AA4BA;;;;AAAqD;;;;;;;;;;;;;;;;;;;iBAArC,qCAAA,CAAA,8BAAqC,UAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt-secret-generator-command.d.mts","names":[],"sources":["../../../../../../../auth/src/commands/jwt-secret-generator-command.ts"],"mappings":";iBAGgB,iCAAA
|
|
1
|
+
{"version":3,"file":"jwt-secret-generator-command.d.mts","names":[],"sources":["../../../../../../../auth/src/commands/jwt-secret-generator-command.ts"],"mappings":";iBAGgB,iCAAA,CAAA,8BAAiC,UAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"sourcesContent":["import { config, t, type Middleware, type Request } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport type { TokenFrom } from \"../contracts/types\";\nimport { AccessToken } from \"../models/access-token\";\nimport { authService } from \"../services/auth.service\";\nimport { isInvalidCredentialError, jwt } from \"../services/jwt\";\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\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/**\n * Whether the persisted row says the token is dead.\n *\r\n * The answer belongs to the model (`AccessToken.isExpired`), so a registered\r\n * override that renames or reshapes its expiry column stays authoritative and\r\n * the middleware never touches a column name. A row that cannot answer at all —\r\n * an override that dropped the getter — is treated as **expired**: the failure\r\n * mode of this whole defect class was a check that quietly answered \"fine\" when\r\n * it had nothing to check, and that is not repeated here.\r\n *\r\n * This is independent of the `exp` claim required by `jwt.verify`. That guard\r\n * catches a token whose *claims* carry no deadline; this one catches a token\r\n * whose *row* says the deadline has passed — a logged-out or expired session\r\n * whose JWT is still within its own lifetime. Neither subsumes the other.\r\n */\r\nfunction accessTokenRowIsExpired(accessToken: AccessToken): boolean {\n return typeof accessToken.isExpired === \"boolean\" ? accessToken.isExpired : true;\n}\n\nfunction readCredential(request: Request, tokenFrom: TokenFrom): string {\n if (tokenFrom === \"header\") {\n return request.authorizationValue;\n }\n\n const value = request.cookie(tokenFrom.slice(\"cookie:\".length));\n\n return value ? String(value) : \"\";\n}\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\n * token is always rejected with `401`. Routes that should be public\n * simply omit the middleware.\n *\n * `tokenFrom` selects one credential source and defaults to `\"header\"`.\n * A single source avoids making credential precedence depend on array order.\n *\r\n * @example\r\n * router.get(\"/account\", authMiddleware([]), accountController);\r\n * router.get(\"/admin\", authMiddleware(\"admin\"), adminController);\n * router.get(\"/back-office\", authMiddleware([\"admin\", \"staff\"]), backOfficeController);\n * router.get(\"/browser-account\", authMiddleware([], \"cookie:token\"), accountController);\n */\nexport function authMiddleware(\n allowedUserType: string | string[],\n tokenFrom: TokenFrom = \"header\",\n): Middleware {\n const allowedTypes = Array.isArray(allowedUserType) ? allowedUserType : [allowedUserType];\r\n\r\n const auth: Middleware = async ({ request, response }) => {\r\n const authorizationValue = readCredential(request, tokenFrom);\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 let decoded: DecodedAccessToken;\r\n\r\n // The ONLY try in this middleware, and it wraps a single call. Everything\r\n // after it is storage and configuration, where an exception means the\r\n // server is broken rather than the caller (D5).\r\n try {\r\n decoded = await jwt.verify<DecodedAccessToken>(authorizationValue);\r\n } catch (error) {\r\n if (!isInvalidCredentialError(error)) {\r\n // A DB/cache outage, a missing `JWT_SECRET`, or any other server-side\r\n // fault — not a verdict on this credential. Propagate to the request's\r\n // normal error path (a 500 monitoring can see) instead of answering\r\n // 401 and clearing the caller's session.\r\n throw error;\r\n }\r\n\r\n // A forged, malformed, expired token — or (D6) a `tokenType` mismatch —\r\n // is not an incident: it is a request carrying a credential the server\r\n // will never accept.\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 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 // ... and the row must still be live. Existence alone was the whole check\r\n // before 4.12.0, so a row whose own `expires_at` had passed still opened\r\n // the gate. The stored expiry is now enforced, and the dead row is\r\n // removed on the way out rather than left for the cleanup command.\r\n if (accessTokenRowIsExpired(accessToken)) {\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 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 // Configuration, not credentials. Throwing keeps a mis-registered app\r\n // loudly broken instead of quietly rejecting every request of this type.\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) {\n await accessToken.destroy();\n\n return response.unauthorized({\n error: t(\"auth.errors.invalidAccessToken\"),\n errorCode: AuthErrorCodes.InvalidAccessToken,\n });\n }\n\n if (!(await authService.canAuthenticate(currentUser))) {\n return response.unauthorized({\n error: t(\"auth.errors.unauthorized\"),\n errorCode: AuthErrorCodes.Unauthorized,\n });\n }\n\n request.user = currentUser;\n };\r\n\r\n return auth;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAS,wBAAwB,aAAmC;CAClE,OAAO,OAAO,YAAY,cAAc,YAAY,YAAY,YAAY;AAC9E;AAEA,SAAS,eAAe,SAAkB,WAA8B;CACtE,IAAI,cAAc,UAChB,OAAO,QAAQ;CAGjB,MAAM,QAAQ,QAAQ,OAAO,UAAU,MAAM,CAAgB,CAAC;CAE9D,OAAO,QAAQ,OAAO,KAAK,IAAI;AACjC;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,eACd,iBACA,YAAuB,UACX;CACZ,MAAM,eAAe,MAAM,QAAQ,eAAe,IAAI,kBAAkB,CAAC,eAAe;CAExF,MAAM,OAAmB,OAAO,EAAE,SAAS,eAAe;EACxD,MAAM,qBAAqB,eAAe,SAAS,SAAS;EAE5D,IAAI,CAAC,oBACH,OAAO,SAAS,aAAa;GAC3B,OAAO,EAAE,gCAAgC;GACzC;EACF,CAAC;EAGH,IAAI;EAKJ,IAAI;GACF,UAAU,MAAM,IAAI,OAA2B,kBAAkB;EACnE,SAAS,OAAO;GACd,IAAI,CAAC,yBAAyB,KAAK,GAKjC,MAAM;GAMR,IAAI,MAAM,QAAQ,QAAQ,KAAK;GAE/B,QAAQ,iBAAiB;GAEzB,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,QAAQ,qBAAqB;EAK7B,MAAM,cAAc,MADK,OAAO,IAAI,0BAA0B,WACrB,
|
|
1
|
+
{"version":3,"file":"auth.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"sourcesContent":["import { config, t, type Middleware, type Request } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport type { TokenFrom } from \"../contracts/types\";\nimport { AccessToken } from \"../models/access-token\";\nimport { authService } from \"../services/auth.service\";\nimport { isInvalidCredentialError, jwt } from \"../services/jwt\";\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\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/**\n * Whether the persisted row says the token is dead.\n *\r\n * The answer belongs to the model (`AccessToken.isExpired`), so a registered\r\n * override that renames or reshapes its expiry column stays authoritative and\r\n * the middleware never touches a column name. A row that cannot answer at all —\r\n * an override that dropped the getter — is treated as **expired**: the failure\r\n * mode of this whole defect class was a check that quietly answered \"fine\" when\r\n * it had nothing to check, and that is not repeated here.\r\n *\r\n * This is independent of the `exp` claim required by `jwt.verify`. That guard\r\n * catches a token whose *claims* carry no deadline; this one catches a token\r\n * whose *row* says the deadline has passed — a logged-out or expired session\r\n * whose JWT is still within its own lifetime. Neither subsumes the other.\r\n */\r\nfunction accessTokenRowIsExpired(accessToken: AccessToken): boolean {\n return typeof accessToken.isExpired === \"boolean\" ? accessToken.isExpired : true;\n}\n\nfunction readCredential(request: Request, tokenFrom: TokenFrom): string {\n if (tokenFrom === \"header\") {\n return request.authorizationValue;\n }\n\n const value = request.cookie(tokenFrom.slice(\"cookie:\".length));\n\n return value ? String(value) : \"\";\n}\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\n * token is always rejected with `401`. Routes that should be public\n * simply omit the middleware.\n *\n * `tokenFrom` selects one credential source and defaults to `\"header\"`.\n * A single source avoids making credential precedence depend on array order.\n *\r\n * @example\r\n * router.get(\"/account\", authMiddleware([]), accountController);\r\n * router.get(\"/admin\", authMiddleware(\"admin\"), adminController);\n * router.get(\"/back-office\", authMiddleware([\"admin\", \"staff\"]), backOfficeController);\n * router.get(\"/browser-account\", authMiddleware([], \"cookie:token\"), accountController);\n */\nexport function authMiddleware(\n allowedUserType: string | string[],\n tokenFrom: TokenFrom = \"header\",\n): Middleware {\n const allowedTypes = Array.isArray(allowedUserType) ? allowedUserType : [allowedUserType];\r\n\r\n const auth: Middleware = async ({ request, response }) => {\r\n const authorizationValue = readCredential(request, tokenFrom);\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 let decoded: DecodedAccessToken;\r\n\r\n // The ONLY try in this middleware, and it wraps a single call. Everything\r\n // after it is storage and configuration, where an exception means the\r\n // server is broken rather than the caller (D5).\r\n try {\r\n decoded = await jwt.verify<DecodedAccessToken>(authorizationValue);\r\n } catch (error) {\r\n if (!isInvalidCredentialError(error)) {\r\n // A DB/cache outage, a missing `JWT_SECRET`, or any other server-side\r\n // fault — not a verdict on this credential. Propagate to the request's\r\n // normal error path (a 500 monitoring can see) instead of answering\r\n // 401 and clearing the caller's session.\r\n throw error;\r\n }\r\n\r\n // A forged, malformed, expired token — or (D6) a `tokenType` mismatch —\r\n // is not an incident: it is a request carrying a credential the server\r\n // will never accept.\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 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 // ... and the row must still be live. Existence alone was the whole check\r\n // before 4.12.0, so a row whose own `expires_at` had passed still opened\r\n // the gate. The stored expiry is now enforced, and the dead row is\r\n // removed on the way out rather than left for the cleanup command.\r\n if (accessTokenRowIsExpired(accessToken)) {\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 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 // Configuration, not credentials. Throwing keeps a mis-registered app\r\n // loudly broken instead of quietly rejecting every request of this type.\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) {\n await accessToken.destroy();\n\n return response.unauthorized({\n error: t(\"auth.errors.invalidAccessToken\"),\n errorCode: AuthErrorCodes.InvalidAccessToken,\n });\n }\n\n if (!(await authService.canAuthenticate(currentUser))) {\n return response.unauthorized({\n error: t(\"auth.errors.unauthorized\"),\n errorCode: AuthErrorCodes.Unauthorized,\n });\n }\n\n request.user = currentUser;\n };\r\n\r\n return auth;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAS,wBAAwB,aAAmC;CAClE,OAAO,OAAO,YAAY,cAAc,YAAY,YAAY,YAAY;AAC9E;AAEA,SAAS,eAAe,SAAkB,WAA8B;CACtE,IAAI,cAAc,UAChB,OAAO,QAAQ;CAGjB,MAAM,QAAQ,QAAQ,OAAO,UAAU,MAAM,CAAgB,CAAC;CAE9D,OAAO,QAAQ,OAAO,KAAK,IAAI;AACjC;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,eACd,iBACA,YAAuB,UACX;CACZ,MAAM,eAAe,MAAM,QAAQ,eAAe,IAAI,kBAAkB,CAAC,eAAe;CAExF,MAAM,OAAmB,OAAO,EAAE,SAAS,eAAe;EACxD,MAAM,qBAAqB,eAAe,SAAS,SAAS;EAE5D,IAAI,CAAC,oBACH,OAAO,SAAS,aAAa;GAC3B,OAAO,EAAE,gCAAgC;GACzC;EACF,CAAC;EAGH,IAAI;EAKJ,IAAI;GACF,UAAU,MAAM,IAAI,OAA2B,kBAAkB;EACnE,SAAS,OAAO;GACd,IAAI,CAAC,yBAAyB,KAAK,GAKjC,MAAM;GAMR,IAAI,MAAM,QAAQ,QAAQ,KAAK;GAE/B,QAAQ,iBAAiB;GAEzB,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,QAAQ,qBAAqB;EAK7B,MAAM,cAAc,MADK,OAAO,IAAI,0BAA0B,WACrB,EAAE,YAAY,kBAAkB;EAEzE,IAAI,CAAC,aACH,OAAO,SAAS,aAAa;GAC3B,OAAO,EAAE,gCAAgC;GACzC;EACF,CAAC;EAOH,IAAI,wBAAwB,WAAW,GAAG;GACxC,MAAM,YAAY,QAAQ;GAE1B,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,MAAM,WAAW,QAAQ,YAAY,YAAY;EAEjD,IAAI,aAAa,UAAU,CAAC,aAAa,SAAS,QAAQ,GACxD,OAAO,SAAS,aAAa;GAC3B,OAAO,EAAE,0BAA0B;GACnC;EACF,CAAC;EAGH,MAAM,YAAY,OAAO,IAAI,iBAAiB,UAAU;EAExD,IAAI,CAAC,WAGH,MAAM,IAAI,MAAM,aAAa,SAAS,kBAAkB;EAG1D,MAAM,cAAc,MAAM,UAAU,KAAK,QAAQ,EAAE;EAEnD,IAAI,CAAC,aAAa;GAChB,MAAM,YAAY,QAAQ;GAE1B,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,IAAI,CAAE,MAAM,YAAY,gBAAgB,WAAW,GACjD,OAAO,SAAS,aAAa;GAC3B,OAAO,EAAE,0BAA0B;GACnC;EACF,CAAC;EAGH,QAAQ,OAAO;CACjB;CAEA,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-token.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"mappings":";;;;;;AAaA;;;;;;cAAa,iBAAA,6BAAiB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;cAiBjB,WAAA,SAAoB,KAAA;EAAA,OACjB,KAAA;EAAA,OAEA,MAAA,6BAAM,eAAA;;;;;;;;;;;;;;;;;MAGT,MAAA;;MAKA,QAAA;;;;;;AAXb;;;;;;;;;;MA8Ba,SAAA;EAqBmD;;;;;;EAAA,IAPnD,YAAA;EAiC8B;;;EAAA,OA1B3B,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,UAAe,SAAA,EAAW,IAAA,GAAI,OAAA,CAAA,WAAA;EAkEZ;;;EAAA,OAtDpC,WAAA,CAAY,KAAA,WAAgB,OAAA,CAAQ,WAAA;EA/Dd;;;EAAA,OAsEtB,aAAA,CAAc,IAAA,EAAM,IAAA,EAAM,KAAA,WAAa,OAAA;EAnEjC;;;EAAA,OA0EN,gBAAA,CAAiB,IAAA,EAAM,IAAA,GAAI,OAAA;;;;;SAQrB,YAAA,
|
|
1
|
+
{"version":3,"file":"access-token.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"mappings":";;;;;;AAaA;;;;;;cAAa,iBAAA,6BAAiB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;cAiBjB,WAAA,SAAoB,KAAA;EAAA,OACjB,KAAA;EAAA,OAEA,MAAA,6BAAM,eAAA;;;;;;;;;;;;;;;;;MAGT,MAAA,CAAA;;MAKA,QAAA,CAAA;;;;;;AAXb;;;;;;;;;;MA8Ba,SAAA,CAAA;EAqBmD;;;;;;EAAA,IAPnD,YAAA,CAAA;EAiC8B;;;EAAA,OA1B3B,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,UAAe,SAAA,EAAW,IAAA,GAAI,OAAA,CAAA,WAAA;EAkEZ;;;EAAA,OAtDpC,WAAA,CAAY,KAAA,WAAgB,OAAA,CAAQ,WAAA;EA/Dd;;;EAAA,OAsEtB,aAAA,CAAc,IAAA,EAAM,IAAA,EAAM,KAAA,WAAa,OAAA;EAnEjC;;;EAAA,OA0EN,gBAAA,CAAiB,IAAA,EAAM,IAAA,GAAI,OAAA;;;;;SAQrB,YAAA,CAAA,GAAgB,OAAA;;;;;;;;;;;;SAqBhB,iBAAA,CAAA,GAAqB,OAAA,CAAQ,WAAA;;;;;;SAW7B,kBAAA,CAAA,GAAsB,OAAA,CAAQ,WAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-token.model.mjs","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { isNeverExpiring, isUsableExpiry } from \"../../utils/token-expiry\";\r\nimport type { Auth } from \"../auth.model\";\r\n\r\n/**\r\n * Seal schema for the persisted access-token record. Exported so an override\r\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\r\n * base shape.\r\n *\r\n * `last_access` and the `is_active` soft-revoke flag were removed — neither was\r\n * ever read; access tokens are revoked by deleting the row.\r\n */\r\nexport const accessTokenSchema = v.object({\r\n token: v.string().required(),\r\n user_id: v.scalar().required(),\r\n user_type: v.string().required(),\r\n expires_at: v.date().required(),\r\n});\r\n\r\n/**\r\n * Persisted access-token record + the data layer for access tokens.\r\n *\r\n * **Role.** Owns access-token persistence and lookup. The middleware checks a\r\n * presented JWT against this table so deleting a row (logout) invalidates the\r\n * token immediately, before its JWT expiry. The auth service goes through the\r\n * named statics exclusively, so it never hard-codes a column name and an\r\n * override can rename/add columns by registering under\r\n * `config.auth.accessToken.model`.\r\n */\r\nexport class AccessToken extends Model {\r\n public static table = \"access_tokens\";\r\n\r\n public static schema = accessTokenSchema;\r\n\r\n /** The user this token was issued for. */\r\n public get userId() {\r\n return this.get(\"user_id\");\r\n }\r\n\r\n /** The user-type slug this token was issued for. */\r\n public get userType(): string {\r\n return this.get(\"user_type\");\r\n }\r\n\r\n /**\r\n * Whether the persisted expiry has passed.\r\n *\r\n * The middleware asks this on every request, which is what makes the row the\r\n * authority it always looked like it was: before 4.12.0 the gate only checked\r\n * that the row *existed*, so a row the database knew was dead still let its\r\n * token through — the database was never asked.\r\n *\r\n * **Fails closed.** A missing or unparseable `expires_at` counts as expired.\r\n * `expires_at` is `required` in the schema, so a row that cannot answer \"when\r\n * does this die\" is malformed, and the safe reading of a malformed credential\r\n * is that it is not one. This is deliberately stricter than a naive\r\n * `now > expires_at`, which answers `false` for an `Invalid Date` and thereby\r\n * grants exactly the poisoned rows an unlimited life.\r\n */\r\n public get isExpired(): boolean {\r\n const expiresAt = this.get(\"expires_at\");\r\n\r\n if (!isUsableExpiry(expiresAt)) return true;\r\n\r\n return new Date(expiresAt).getTime() <= Date.now();\r\n }\r\n\r\n /**\r\n * Whether nothing about this row can ever retire it — an unusable\r\n * `expires_at`, or a token carrying no `exp` claim. See\r\n * {@link isNeverExpiring}; this is the predicate `purgeNeverExpiring` selects\r\n * on.\r\n */\r\n public get neverExpires(): boolean {\r\n return isNeverExpiring(this.get(\"token\"), this.get(\"expires_at\"));\r\n }\r\n\r\n /**\r\n * Persist a freshly-signed access token for the user.\r\n */\r\n public static issue(user: Auth, token: string, expiresAt: Date) {\r\n return this.create({\r\n token,\r\n user_id: user.id,\r\n user_type: user.userType,\r\n expires_at: expiresAt,\r\n });\r\n }\r\n\r\n /**\r\n * Find an access-token row by its raw token string.\r\n */\r\n public static findByToken(token: string): Promise<AccessToken | null> {\r\n return this.first({ token });\r\n }\r\n\r\n /**\r\n * Delete a specific token that belongs to the given user.\r\n */\r\n public static deleteForUser(user: Auth, token: string) {\r\n return this.delete({ token, user_id: user.id });\r\n }\r\n\r\n /**\r\n * Delete every access token belonging to the user.\r\n */\r\n public static deleteAllForUser(user: Auth) {\r\n return this.delete({ user_id: user.id });\r\n }\r\n\r\n /**\r\n * Hard-delete every expired access-token row. Returns the number removed.\r\n * Runs from the `auth.cleanup` CLI command (a cold batch path).\r\n */\r\n public static async purgeExpired(): Promise<number> {\r\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\r\n\r\n for (const token of expiredTokens) {\r\n await token.destroy();\r\n }\r\n\r\n return expiredTokens.length;\r\n }\r\n\r\n /**\r\n * Every row that can never retire itself — see {@link neverExpires}.\r\n *\r\n * **A full scan, filtered in memory, on purpose.** The defining case is a row\r\n * whose `expires_at` is an `Invalid Date`, which no date predicate can select\r\n * (`< now` and `> now` are both `false` for it), and the definitive case is a\r\n * token with no `exp` claim, which lives inside the token string rather than\r\n * in a column. Neither is expressible as a `where`, so the rows have to be\r\n * read to be judged. This runs from a one-off remediation command, not a\r\n * request path.\r\n */\r\n public static async findNeverExpiring(): Promise<AccessToken[]> {\r\n const tokens = await this.query().get();\r\n\r\n return tokens.filter((token: AccessToken) => token.neverExpires);\r\n }\r\n\r\n /**\r\n * Hard-delete every never-expiring row, returning the rows removed so the\r\n * caller can report what it revoked. Deletion *is* revocation for access\r\n * tokens — the middleware rejects a token with no row.\r\n */\r\n public static async purgeNeverExpiring(): Promise<AccessToken[]> {\r\n const tokens = await this.findNeverExpiring();\r\n\r\n for (const token of tokens) {\r\n await token.destroy();\r\n }\r\n\r\n return tokens;\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,oBAAoB,EAAE,OAAO;CACxC,OAAO,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"access-token.model.mjs","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { isNeverExpiring, isUsableExpiry } from \"../../utils/token-expiry\";\r\nimport type { Auth } from \"../auth.model\";\r\n\r\n/**\r\n * Seal schema for the persisted access-token record. Exported so an override\r\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\r\n * base shape.\r\n *\r\n * `last_access` and the `is_active` soft-revoke flag were removed — neither was\r\n * ever read; access tokens are revoked by deleting the row.\r\n */\r\nexport const accessTokenSchema = v.object({\r\n token: v.string().required(),\r\n user_id: v.scalar().required(),\r\n user_type: v.string().required(),\r\n expires_at: v.date().required(),\r\n});\r\n\r\n/**\r\n * Persisted access-token record + the data layer for access tokens.\r\n *\r\n * **Role.** Owns access-token persistence and lookup. The middleware checks a\r\n * presented JWT against this table so deleting a row (logout) invalidates the\r\n * token immediately, before its JWT expiry. The auth service goes through the\r\n * named statics exclusively, so it never hard-codes a column name and an\r\n * override can rename/add columns by registering under\r\n * `config.auth.accessToken.model`.\r\n */\r\nexport class AccessToken extends Model {\r\n public static table = \"access_tokens\";\r\n\r\n public static schema = accessTokenSchema;\r\n\r\n /** The user this token was issued for. */\r\n public get userId() {\r\n return this.get(\"user_id\");\r\n }\r\n\r\n /** The user-type slug this token was issued for. */\r\n public get userType(): string {\r\n return this.get(\"user_type\");\r\n }\r\n\r\n /**\r\n * Whether the persisted expiry has passed.\r\n *\r\n * The middleware asks this on every request, which is what makes the row the\r\n * authority it always looked like it was: before 4.12.0 the gate only checked\r\n * that the row *existed*, so a row the database knew was dead still let its\r\n * token through — the database was never asked.\r\n *\r\n * **Fails closed.** A missing or unparseable `expires_at` counts as expired.\r\n * `expires_at` is `required` in the schema, so a row that cannot answer \"when\r\n * does this die\" is malformed, and the safe reading of a malformed credential\r\n * is that it is not one. This is deliberately stricter than a naive\r\n * `now > expires_at`, which answers `false` for an `Invalid Date` and thereby\r\n * grants exactly the poisoned rows an unlimited life.\r\n */\r\n public get isExpired(): boolean {\r\n const expiresAt = this.get(\"expires_at\");\r\n\r\n if (!isUsableExpiry(expiresAt)) return true;\r\n\r\n return new Date(expiresAt).getTime() <= Date.now();\r\n }\r\n\r\n /**\r\n * Whether nothing about this row can ever retire it — an unusable\r\n * `expires_at`, or a token carrying no `exp` claim. See\r\n * {@link isNeverExpiring}; this is the predicate `purgeNeverExpiring` selects\r\n * on.\r\n */\r\n public get neverExpires(): boolean {\r\n return isNeverExpiring(this.get(\"token\"), this.get(\"expires_at\"));\r\n }\r\n\r\n /**\r\n * Persist a freshly-signed access token for the user.\r\n */\r\n public static issue(user: Auth, token: string, expiresAt: Date) {\r\n return this.create({\r\n token,\r\n user_id: user.id,\r\n user_type: user.userType,\r\n expires_at: expiresAt,\r\n });\r\n }\r\n\r\n /**\r\n * Find an access-token row by its raw token string.\r\n */\r\n public static findByToken(token: string): Promise<AccessToken | null> {\r\n return this.first({ token });\r\n }\r\n\r\n /**\r\n * Delete a specific token that belongs to the given user.\r\n */\r\n public static deleteForUser(user: Auth, token: string) {\r\n return this.delete({ token, user_id: user.id });\r\n }\r\n\r\n /**\r\n * Delete every access token belonging to the user.\r\n */\r\n public static deleteAllForUser(user: Auth) {\r\n return this.delete({ user_id: user.id });\r\n }\r\n\r\n /**\r\n * Hard-delete every expired access-token row. Returns the number removed.\r\n * Runs from the `auth.cleanup` CLI command (a cold batch path).\r\n */\r\n public static async purgeExpired(): Promise<number> {\r\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\r\n\r\n for (const token of expiredTokens) {\r\n await token.destroy();\r\n }\r\n\r\n return expiredTokens.length;\r\n }\r\n\r\n /**\r\n * Every row that can never retire itself — see {@link neverExpires}.\r\n *\r\n * **A full scan, filtered in memory, on purpose.** The defining case is a row\r\n * whose `expires_at` is an `Invalid Date`, which no date predicate can select\r\n * (`< now` and `> now` are both `false` for it), and the definitive case is a\r\n * token with no `exp` claim, which lives inside the token string rather than\r\n * in a column. Neither is expressible as a `where`, so the rows have to be\r\n * read to be judged. This runs from a one-off remediation command, not a\r\n * request path.\r\n */\r\n public static async findNeverExpiring(): Promise<AccessToken[]> {\r\n const tokens = await this.query().get();\r\n\r\n return tokens.filter((token: AccessToken) => token.neverExpires);\r\n }\r\n\r\n /**\r\n * Hard-delete every never-expiring row, returning the rows removed so the\r\n * caller can report what it revoked. Deletion *is* revocation for access\r\n * tokens — the middleware rejects a token with no row.\r\n */\r\n public static async purgeNeverExpiring(): Promise<AccessToken[]> {\r\n const tokens = await this.findNeverExpiring();\r\n\r\n for (const token of tokens) {\r\n await token.destroy();\r\n }\r\n\r\n return tokens;\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,oBAAoB,EAAE,OAAO;CACxC,OAAO,EAAE,OAAO,EAAE,SAAS;CAC3B,SAAS,EAAE,OAAO,EAAE,SAAS;CAC7B,WAAW,EAAE,OAAO,EAAE,SAAS;CAC/B,YAAY,EAAE,KAAK,EAAE,SAAS;AAChC,CAAC;;;;;;;;;;;AAYD,IAAa,cAAb,cAAiC,MAAM;;eACf;;;gBAEC;;;CAGvB,IAAW,SAAS;EAClB,OAAO,KAAK,IAAI,SAAS;CAC3B;;CAGA,IAAW,WAAmB;EAC5B,OAAO,KAAK,IAAI,WAAW;CAC7B;;;;;;;;;;;;;;;;CAiBA,IAAW,YAAqB;EAC9B,MAAM,YAAY,KAAK,IAAI,YAAY;EAEvC,IAAI,CAAC,eAAe,SAAS,GAAG,OAAO;EAEvC,OAAO,IAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,KAAK,IAAI;CACnD;;;;;;;CAQA,IAAW,eAAwB;EACjC,OAAO,gBAAgB,KAAK,IAAI,OAAO,GAAG,KAAK,IAAI,YAAY,CAAC;CAClE;;;;CAKA,OAAc,MAAM,MAAY,OAAe,WAAiB;EAC9D,OAAO,KAAK,OAAO;GACjB;GACA,SAAS,KAAK;GACd,WAAW,KAAK;GAChB,YAAY;EACd,CAAC;CACH;;;;CAKA,OAAc,YAAY,OAA4C;EACpE,OAAO,KAAK,MAAM,EAAE,MAAM,CAAC;CAC7B;;;;CAKA,OAAc,cAAc,MAAY,OAAe;EACrD,OAAO,KAAK,OAAO;GAAE;GAAO,SAAS,KAAK;EAAG,CAAC;CAChD;;;;CAKA,OAAc,iBAAiB,MAAY;EACzC,OAAO,KAAK,OAAO,EAAE,SAAS,KAAK,GAAG,CAAC;CACzC;;;;;CAMA,aAAoB,eAAgC;EAClD,MAAM,gBAAgB,MAAM,KAAK,MAAM,EAAE,MAAM,cAAc,qBAAK,IAAI,KAAK,CAAC,EAAE,IAAI;EAElF,KAAK,MAAM,SAAS,eAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO,cAAc;CACvB;;;;;;;;;;;;CAaA,aAAoB,oBAA4C;EAG9D,QAAO,MAFc,KAAK,MAAM,EAAE,IAAI,GAExB,QAAQ,UAAuB,MAAM,YAAY;CACjE;;;;;;CAOA,aAAoB,qBAA6C;EAC/D,MAAM,SAAS,MAAM,KAAK,kBAAkB;EAE5C,KAAK,MAAM,SAAS,QAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO;CACT;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../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,
|
|
1
|
+
{"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../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,EAAE,OAAO;EAG1B,KAAK,KAAK,SAAS,EAAE,MAAM;EAC3B,KAAK,OAAO,aAAa,EAAE,EAAE,SAAS;EAGtC,KAAK,UAAU,YAAY,EAAE,MAAM,EAAE,SAAS;EAG9C,KAAK,WAAW;CAClB;CACA,OAAO;EACL,KAAK,kBAAkB;CACzB;AACF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.model.d.mts","names":[],"sources":["../../../../../../../auth/src/models/auth.model.ts"],"mappings":";;;;;;uBAMsB,IAAA,gBAAoB,WAAA,GAAc,WAAA,UAC9C,KAAA,CAAM,MAAA,aACH,aAAA;;AAFb;;eAOsB,QAAA;EAPoB;;;EAYjC,kBAAA,
|
|
1
|
+
{"version":3,"file":"auth.model.d.mts","names":[],"sources":["../../../../../../../auth/src/models/auth.model.ts"],"mappings":";;;;;;uBAMsB,IAAA,gBAAoB,WAAA,GAAc,WAAA,UAC9C,KAAA,CAAM,MAAA,aACH,aAAA;;AAFb;;eAOsB,QAAA,CAAA;EAPoB;;;EAYjC,kBAAA,CAAA,GAAkB,MAAA;EAOiB;;;EAA7B,eAAA,CAAgB,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,SAAA;EAOjB;;;EAAjC,mBAAA,CAAoB,IAAA,SAAa,OAAA,CAAQ,iBAAA;EAcP;;;EAPlC,oBAAA,CAAqB,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,YAAA;EAmC7B;;;EA5B1B,iBAAA,CAAkB,KAAA,WAAgB,OAAA;EAmCyB;;;EA5B3D,kBAAA,CAAmB,KAAA,WAAgB,OAAA;EA7CrC;;;EAoDE,qBAAA,CAAA,GAAyB,OAAA;EAtDE;;;EA6D3B,eAAA,CAAA,GAAmB,OAAA;EA3DrB;;;EAkEE,cAAA,CAAA,GAAkB,OAAA,CAAQ,YAAA;EAjD1B;;;EAAA,OAwDO,OAAA,CAAQ,IAAA,EAAM,UAAA,CAAW,IAAA,GAAO,IAAA,QAAY,OAAA,CAAQ,IAAA;EAxDT;;;EA+DlD,eAAA,CAAgB,QAAA,WAAmB,OAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../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,
|
|
1
|
+
{"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../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,EAAE,OAAO;EAC1B,KAAK,KAAK,SAAS,EAAE,MAAM;EAC3B,KAAK,OAAO,aAAa,EAAE,EAAE,SAAS;EACtC,KAAK,KAAK,WAAW,EAAE,MAAM,EAAE,SAAS;EACxC,KAAK,UAAU,YAAY,EAAE,MAAM,EAAE,SAAS;EAC9C,KAAK,UAAU,cAAc,EAAE,SAAS;EACxC,KAAK,UAAU,YAAY,EAAE,SAAS;EACtC,KAAK,KAAK,aAAa,EAAE,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.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/refresh-token/refresh-token.model.ts"],"mappings":";;;;;;;AAkBA;;;;;;;;;;cAAa,kBAAA,6BAAkB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAcnB,wBAAA;EACV,QAAA;EACA,SAAA;EACA,UAAA,GAAa,UAAU;AAAA;;;;;;;;;;;cAaZ,YAAA,SAAqB,KAAA;EAAA,OAClB,KAAA;EAAA,OAEA,MAAA,6BAAM,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAGT,QAAA;;;;;;;;;;;MAcA,SAAA;;;AApCb;;MAgDa,YAAA;EA7CY;EAAA,IAkDZ,SAAA;EAnDX;EAAA,IAwDW,OAAA;EAvDE;;AAAU;EA8DV,MAAA,
|
|
1
|
+
{"version":3,"file":"refresh-token.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/refresh-token/refresh-token.model.ts"],"mappings":";;;;;;;AAkBA;;;;;;;;;;cAAa,kBAAA,6BAAkB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAcnB,wBAAA;EACV,QAAA;EACA,SAAA;EACA,UAAA,GAAa,UAAU;AAAA;;;;;;;;;;;cAaZ,YAAA,SAAqB,KAAA;EAAA,OAClB,KAAA;EAAA,OAEA,MAAA,6BAAM,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAGT,QAAA,CAAA;;;;;;;;;;;MAcA,SAAA,CAAA;;;AApCb;;MAgDa,YAAA,CAAA;EA7CY;EAAA,IAkDZ,SAAA,CAAA;EAnDX;EAAA,IAwDW,OAAA,CAAA;EAvDE;;AAAU;EA8DV,MAAA,CAAA,GAAU,OAAA;EAjDC;;;;;;;EA4DX,cAAA,CAAA,GAAkB,OAAA;;;;EAclB,UAAA,CAAA,GAAc,OAAA;;;;SAOb,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,UAAe,OAAA,EAAS,wBAAA,GAAwB,OAAA,CAAA,YAAA;;;;SAoBlE,WAAA,CAAY,KAAA,WAAgB,OAAA,CAAQ,YAAA;;;;;SAQpC,WAAA,CAAY,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA,CAAQ,YAAA;;;;SAOhD,aAAA,CAAc,IAAA,EAAM,IAAA,EAAM,KAAA,WAAa,OAAA;EAnC3B;;;EAAA,OA0CZ,SAAA,CAAU,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;EAtBK;;;;;;;;EAAA,OAsC9B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;EAhBjB;;;;EAAA,OAgCjB,YAAA,CAAa,QAAA,WAAmB,OAAA,CAAQ,YAAA;EAAR;;;;;EAAA,OAehC,UAAA,CAAW,IAAA,EAAM,IAAA,EAAM,GAAA,WAAc,OAAA;EAoChB;;;;;EAAA,OAhBrB,YAAA,CAAA,GAAgB,OAAA,CAAQ,YAAA;EA9LZ;;;;;;EAAA,OA8MZ,iBAAA,CAAA,GAAqB,OAAA,CAAQ,YAAA;;;;;;;;;;SAe7B,kBAAA,CAAA,GAAsB,OAAA,CAAQ,YAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh-token.model.mjs","names":[],"sources":["../../../../../../../../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 { isNeverExpiring, isUsableExpiry } from \"../../utils/token-expiry\";\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 /**\r\n * Whether the token's `expires_at` is in the past.\r\n *\r\n * **Fails closed as of 4.12.0**: a missing or unparseable `expires_at` now\r\n * counts as expired. It previously answered `false` for both — \"no expiry\r\n * recorded ⇒ never expires\" — which handed an unlimited life to precisely the\r\n * malformed rows, including the `Invalid Date` a pre-4.12.0 unparseable\r\n * `expiresIn` could write. `expires_at` is `required` in the schema, so a row\r\n * that cannot say when it dies is malformed, not immortal.\r\n */\r\n public get isExpired(): boolean {\r\n const expiresAt = this.get(\"expires_at\");\r\n\r\n if (!isUsableExpiry(expiresAt)) return true;\r\n\r\n return new Date().getTime() > new Date(expiresAt).getTime();\r\n }\r\n\r\n /**\r\n * Whether nothing can ever retire this row — an unusable `expires_at`, or a\r\n * token carrying no `exp` claim. See {@link isNeverExpiring}.\r\n */\r\n public get neverExpires(): boolean {\r\n return isNeverExpiring(this.get(\"token\"), this.get(\"expires_at\"));\r\n }\r\n\r\n /** 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 /**\r\n * Every row that can never retire itself — see {@link neverExpires}. A full\r\n * scan filtered in memory, for the reason given on\r\n * {@link AccessToken.findNeverExpiring}: neither an `Invalid Date` nor a\r\n * missing `exp` claim is expressible as a `where`.\r\n */\r\n public static async findNeverExpiring(): Promise<RefreshToken[]> {\r\n const tokens = await this.query().get();\r\n\r\n return tokens.filter((token: RefreshToken) => token.neverExpires);\r\n }\r\n\r\n /**\r\n * Hard-delete every never-expiring row, returning the rows removed.\r\n *\r\n * Deleted rather than `revoked_at`-stamped: a revoked row is still a row this\r\n * table has to carry, and one of the two shapes being removed is a row whose\r\n * date column cannot be compared at all — leaving it in place keeps a\r\n * permanently unpurgeable record. Rotation replay-detection is unaffected;\r\n * these rows can no longer be presented successfully either way.\r\n */\r\n public static async purgeNeverExpiring(): Promise<RefreshToken[]> {\r\n const tokens = await this.findNeverExpiring();\r\n\r\n for (const token of tokens) {\r\n await token.destroy();\r\n }\r\n\r\n return tokens;\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"refresh-token.model.mjs","names":[],"sources":["../../../../../../../../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 { isNeverExpiring, isUsableExpiry } from \"../../utils/token-expiry\";\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 /**\r\n * Whether the token's `expires_at` is in the past.\r\n *\r\n * **Fails closed as of 4.12.0**: a missing or unparseable `expires_at` now\r\n * counts as expired. It previously answered `false` for both — \"no expiry\r\n * recorded ⇒ never expires\" — which handed an unlimited life to precisely the\r\n * malformed rows, including the `Invalid Date` a pre-4.12.0 unparseable\r\n * `expiresIn` could write. `expires_at` is `required` in the schema, so a row\r\n * that cannot say when it dies is malformed, not immortal.\r\n */\r\n public get isExpired(): boolean {\r\n const expiresAt = this.get(\"expires_at\");\r\n\r\n if (!isUsableExpiry(expiresAt)) return true;\r\n\r\n return new Date().getTime() > new Date(expiresAt).getTime();\r\n }\r\n\r\n /**\r\n * Whether nothing can ever retire this row — an unusable `expires_at`, or a\r\n * token carrying no `exp` claim. See {@link isNeverExpiring}.\r\n */\r\n public get neverExpires(): boolean {\r\n return isNeverExpiring(this.get(\"token\"), this.get(\"expires_at\"));\r\n }\r\n\r\n /** 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 /**\r\n * Every row that can never retire itself — see {@link neverExpires}. A full\r\n * scan filtered in memory, for the reason given on\r\n * {@link AccessToken.findNeverExpiring}: neither an `Invalid Date` nor a\r\n * missing `exp` claim is expressible as a `where`.\r\n */\r\n public static async findNeverExpiring(): Promise<RefreshToken[]> {\r\n const tokens = await this.query().get();\r\n\r\n return tokens.filter((token: RefreshToken) => token.neverExpires);\r\n }\r\n\r\n /**\r\n * Hard-delete every never-expiring row, returning the rows removed.\r\n *\r\n * Deleted rather than `revoked_at`-stamped: a revoked row is still a row this\r\n * table has to carry, and one of the two shapes being removed is a row whose\r\n * date column cannot be compared at all — leaving it in place keeps a\r\n * permanently unpurgeable record. Rotation replay-detection is unaffected;\r\n * these rows can no longer be presented successfully either way.\r\n */\r\n public static async purgeNeverExpiring(): Promise<RefreshToken[]> {\r\n const tokens = await this.findNeverExpiring();\r\n\r\n for (const token of tokens) {\r\n await token.destroy();\r\n }\r\n\r\n return tokens;\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-config.mjs","names":[],"sources":["../../../../../../../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 ms from \"ms\";\r\nimport type { CanAuthenticate, LogoutWithoutTokenBehavior } from \"../contracts/types\";\nimport type { Auth } from \"../models/auth.model\";\n\nconst warnedLegacyKeys = new Set<string>();\n\nconst permitAuthentication: CanAuthenticate = (): boolean => true;\n\r\n/**\r\n * `ms` accepts anything at runtime and answers `undefined` for a string it\r\n * cannot parse (`\"30dayz\"`), a *formatted string* for a number (`2592000` ⇒\r\n * `\"43m\"`), and throws for `\"\"`. Its published types claim the far narrower\r\n * `(value: ms.StringValue) => number`, which is exactly what let an\r\n * `as ms.StringValue` cast smuggle arbitrary config text past the compiler and\r\n * hand `undefined` to the JWT signer. Declare the honest runtime signature once,\r\n * here, so every caller is forced to deal with the `undefined`.\r\n */\r\nconst parseMs = ms as unknown as (value: unknown) => number | string | undefined;\r\n\r\n/**\r\n * Turn a configured lifetime into a positive number of milliseconds, or throw\r\n * naming the key.\r\n *\r\n * A token lifetime is a security boundary, so an unusable value is a hard error\r\n * rather than a silent fallback: substituting a default would trade one\r\n * unintended lifetime for another, just as quietly — and quiet is the whole\r\n * problem. Every non-positive, non-finite or non-numeric parse is rejected:\r\n *\r\n * - `\"30dayz\"` / `\"thirty days\"` ⇒ `undefined` ⇒ a JWT signed with **no `exp`\r\n * claim** and an `Invalid Date` written to the token row.\r\n * - `\"0d\"` ⇒ parses cleanly to `0`, which `fast-jwt` treats as falsy and also\r\n * emits with **no `exp` claim** — it survives any guard that only rejects\r\n * `undefined`. `\"-1h\"` signs an already-expired token.\r\n * - `2592000` (a bare number) ⇒ `ms` *formats* it as `\"43m\"`, a string, which\r\n * then turns `Date.now() + expiresIn` into an `Invalid Date`.\r\n */\r\nfunction parseDuration(key: string, raw: unknown): number {\r\n let parsed: number | string | undefined;\r\n\r\n try {\r\n parsed = parseMs(raw);\r\n } catch {\r\n // ms throws (rather than returning undefined) for \"\" and non-strings\r\n parsed = undefined;\r\n }\r\n\r\n if (typeof parsed !== \"number\" || !Number.isFinite(parsed) || parsed <= 0) {\r\n throw new Error(\r\n `auth.${key}: ${JSON.stringify(raw)} is not a valid ms duration — ` +\r\n `use a positive duration string such as \"1h\", \"7d\", or NO_EXPIRATION.`,\r\n );\r\n }\r\n\r\n return parsed;\r\n}\r\n\r\n/** Access-token lifetime used when nothing is configured. */\r\nconst DEFAULT_ACCESS_TOKEN_EXPIRES_IN = \"1h\";\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 = {\n canAuthenticate: async (user: Auth): Promise<boolean> => {\n const canAuthenticate = config.key<CanAuthenticate>(\n \"auth.canAuthenticate\",\n permitAuthentication,\n );\n\n return canAuthenticate(user);\n },\n accessToken: {\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 * Lifetime in milliseconds — the only form a token issuer may use. Defaults\r\n * to 1 hour when unset; throws naming the key when the configured value is\r\n * not a positive `ms` duration. See {@link parseDuration}.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\r\n \"accessToken.expiresIn\",\r\n authConfig.accessToken.expiresIn() ?? DEFAULT_ACCESS_TOKEN_EXPIRES_IN,\r\n ),\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 /**\r\n * Lifetime in milliseconds — the only form a token issuer may use. Throws\r\n * naming the key when the configured value is not a positive `ms` duration.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\"refreshToken.expiresIn\", authConfig.refreshToken.expiresIn()),\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":";;;;;AAOA,MAAM,mCAAmB,IAAI,IAAY;AAEzC,MAAM,6BAAuD;;;;;;;;;;AAW7D,MAAM,UAAU;;;;;;;;;;;;;;;;;;AAmBhB,SAAS,cAAc,KAAa,KAAsB;CACxD,IAAI;CAEJ,IAAI;EACF,SAAS,QAAQ,GAAG;CACtB,QAAQ;EAEN,SAAS;CACX;CAEA,IAAI,OAAO,WAAW,YAAY,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GACtE,MAAM,IAAI,MACR,QAAQ,IAAI,IAAI,KAAK,UAAU,GAAG,EAAE,mGAEtC;CAGF,OAAO;AACT;;AAGA,MAAM,kCAAkC;;;;;;;;;AAUxC,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,iBAAiB,OAAO,SAAiC;EAMvD,OALwB,OAAO,IAC7B,wBACA,oBAGmB,
|
|
1
|
+
{"version":3,"file":"auth-config.mjs","names":[],"sources":["../../../../../../../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 ms from \"ms\";\r\nimport type { CanAuthenticate, LogoutWithoutTokenBehavior } from \"../contracts/types\";\nimport type { Auth } from \"../models/auth.model\";\n\nconst warnedLegacyKeys = new Set<string>();\n\nconst permitAuthentication: CanAuthenticate = (): boolean => true;\n\r\n/**\r\n * `ms` accepts anything at runtime and answers `undefined` for a string it\r\n * cannot parse (`\"30dayz\"`), a *formatted string* for a number (`2592000` ⇒\r\n * `\"43m\"`), and throws for `\"\"`. Its published types claim the far narrower\r\n * `(value: ms.StringValue) => number`, which is exactly what let an\r\n * `as ms.StringValue` cast smuggle arbitrary config text past the compiler and\r\n * hand `undefined` to the JWT signer. Declare the honest runtime signature once,\r\n * here, so every caller is forced to deal with the `undefined`.\r\n */\r\nconst parseMs = ms as unknown as (value: unknown) => number | string | undefined;\r\n\r\n/**\r\n * Turn a configured lifetime into a positive number of milliseconds, or throw\r\n * naming the key.\r\n *\r\n * A token lifetime is a security boundary, so an unusable value is a hard error\r\n * rather than a silent fallback: substituting a default would trade one\r\n * unintended lifetime for another, just as quietly — and quiet is the whole\r\n * problem. Every non-positive, non-finite or non-numeric parse is rejected:\r\n *\r\n * - `\"30dayz\"` / `\"thirty days\"` ⇒ `undefined` ⇒ a JWT signed with **no `exp`\r\n * claim** and an `Invalid Date` written to the token row.\r\n * - `\"0d\"` ⇒ parses cleanly to `0`, which `fast-jwt` treats as falsy and also\r\n * emits with **no `exp` claim** — it survives any guard that only rejects\r\n * `undefined`. `\"-1h\"` signs an already-expired token.\r\n * - `2592000` (a bare number) ⇒ `ms` *formats* it as `\"43m\"`, a string, which\r\n * then turns `Date.now() + expiresIn` into an `Invalid Date`.\r\n */\r\nfunction parseDuration(key: string, raw: unknown): number {\r\n let parsed: number | string | undefined;\r\n\r\n try {\r\n parsed = parseMs(raw);\r\n } catch {\r\n // ms throws (rather than returning undefined) for \"\" and non-strings\r\n parsed = undefined;\r\n }\r\n\r\n if (typeof parsed !== \"number\" || !Number.isFinite(parsed) || parsed <= 0) {\r\n throw new Error(\r\n `auth.${key}: ${JSON.stringify(raw)} is not a valid ms duration — ` +\r\n `use a positive duration string such as \"1h\", \"7d\", or NO_EXPIRATION.`,\r\n );\r\n }\r\n\r\n return parsed;\r\n}\r\n\r\n/** Access-token lifetime used when nothing is configured. */\r\nconst DEFAULT_ACCESS_TOKEN_EXPIRES_IN = \"1h\";\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 = {\n canAuthenticate: async (user: Auth): Promise<boolean> => {\n const canAuthenticate = config.key<CanAuthenticate>(\n \"auth.canAuthenticate\",\n permitAuthentication,\n );\n\n return canAuthenticate(user);\n },\n accessToken: {\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 * Lifetime in milliseconds — the only form a token issuer may use. Defaults\r\n * to 1 hour when unset; throws naming the key when the configured value is\r\n * not a positive `ms` duration. See {@link parseDuration}.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\r\n \"accessToken.expiresIn\",\r\n authConfig.accessToken.expiresIn() ?? DEFAULT_ACCESS_TOKEN_EXPIRES_IN,\r\n ),\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 /**\r\n * Lifetime in milliseconds — the only form a token issuer may use. Throws\r\n * naming the key when the configured value is not a positive `ms` duration.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\"refreshToken.expiresIn\", authConfig.refreshToken.expiresIn()),\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":";;;;;AAOA,MAAM,mCAAmB,IAAI,IAAY;AAEzC,MAAM,6BAAuD;;;;;;;;;;AAW7D,MAAM,UAAU;;;;;;;;;;;;;;;;;;AAmBhB,SAAS,cAAc,KAAa,KAAsB;CACxD,IAAI;CAEJ,IAAI;EACF,SAAS,QAAQ,GAAG;CACtB,QAAQ;EAEN,SAAS;CACX;CAEA,IAAI,OAAO,WAAW,YAAY,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GACtE,MAAM,IAAI,MACR,QAAQ,IAAI,IAAI,KAAK,UAAU,GAAG,EAAE,mGAEtC;CAGF,OAAO;AACT;;AAGA,MAAM,kCAAkC;;;;;;;;;AAUxC,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,iBAAiB,OAAO,SAAiC;EAMvD,OALwB,OAAO,IAC7B,wBACA,oBAGmB,EAAE,IAAI;CAC7B;CACA,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;;;;;;EAMrF,mBACE,cACE,yBACA,WAAW,YAAY,UAAU,KAAK,+BACxC;CACJ;CACA,cAAc;;EAEZ,cAAkC,QAAQ,uBAAuB,oBAAoB;;EAErF,eAAwB,QAAQ,wBAAwB,uBAAuB,IAAI;;EAEnF,iBAAyB,QAAQ,0BAA0B,yBAAyB,IAAI;;;;;EAKxF,mBACE,cAAc,0BAA0B,WAAW,aAAa,UAAU,CAAC;;EAE7E,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.service.d.mts","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"mappings":";;;;;;;cAiBM,WAAA;;;;AALiD;;;cAYzC,gBAAA;EAcgC;;;EAAA,YAPhC,iBAAA;EAiDA;;;EA1CL,uBAAA,CAAwB,IAAA,EAAM,IAAA,GAAO,MAAA;EAmF7B;EA1EF,eAAA,CAAgB,IAAA,EAAM,IAAA,GAAO,OAAA;EAAA,QAI5B,qBAAA;EAAA,QAMA,gBAAA;EAmGwC;;;;;EA9EzC,mBAAA,CACX,IAAA,EAAM,IAAA,EACN,OAAA,GAAU,MAAA,oBACT,OAAA,CAAQ,iBAAA;EAAA,QAMG,iBAAA;EAoK+B;;;;EApIhC,kBAAA,CACX,IAAA,EAAM,IAAA,EACN,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,YAAA;EAAA,QAQG,cAAA;EAoIX;;;EA1GU,eAAA,CAAgB,IAAA,EAAM,IAAA,EAAM,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,SAAA;EA4I5D;;;;;EAjIF,aAAA,CACX,kBAAA,UACA,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,SAAA;EA8JmE;;;EApFjE,cAAA,CAAe,aAAA,UAAuB,cAAA,WAAyB,OAAA;EAwH5B;;;EAjHnC,YAAA,CAAa,QAAA,WAAmB,OAAA;EAiIH;;;;EAzH7B,YAAA,WAAuB,IAAA,
|
|
1
|
+
{"version":3,"file":"auth.service.d.mts","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"mappings":";;;;;;;cAiBM,WAAA;;;;AALiD;;;cAYzC,gBAAA,CAAA;EAcgC;;;EAAA,YAPhC,iBAAA,CAAA;EAiDA;;;EA1CL,uBAAA,CAAwB,IAAA,EAAM,IAAA,GAAO,MAAA;EAmF7B;EA1EF,eAAA,CAAgB,IAAA,EAAM,IAAA,GAAO,OAAA;EAAA,QAI5B,qBAAA;EAAA,QAMA,gBAAA;EAmGwC;;;;;EA9EzC,mBAAA,CACX,IAAA,EAAM,IAAA,EACN,OAAA,GAAU,MAAA,oBACT,OAAA,CAAQ,iBAAA;EAAA,QAMG,iBAAA;EAoK+B;;;;EApIhC,kBAAA,CACX,IAAA,EAAM,IAAA,EACN,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,YAAA;EAAA,QAQG,cAAA;EAoIX;;;EA1GU,eAAA,CAAgB,IAAA,EAAM,IAAA,EAAM,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,SAAA;EA4I5D;;;;;EAjIF,aAAA,CACX,kBAAA,UACA,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,SAAA;EA8JmE;;;EApFjE,cAAA,CAAe,aAAA,UAAuB,cAAA,WAAyB,OAAA;EAwH5B;;;EAjHnC,YAAA,CAAa,QAAA,WAAmB,OAAA;EAiIH;;;;EAzH7B,YAAA,WAAuB,IAAA,CAAA,CAClC,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,IAAA,EAAM,eAAA,GACL,OAAA,CAAQ,CAAA;EAwK6B;;;;EAxI3B,KAAA,WAAgB,IAAA,CAAA,CAC3B,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,WAAA,EAAa,eAAA,EACb,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,WAAA,CAAY,CAAA;EA0K4B;;;;;;;;;;EA5ItC,MAAA,CAAO,IAAA,EAAM,IAAA,EAAM,WAAA,WAAsB,YAAA,YAAwB,OAAA;EAzRpC;;;EAsT7B,iBAAA,CAAkB,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA;EAtRnD;;;EA6RK,qBAAA,CAAsB,IAAA,EAAM,IAAA,GAAO,OAAA;EA3R7C;;;EAkSU,kBAAA,CAAmB,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA;EA3PpD;;;;;EAoQK,eAAA,CAAgB,IAAA,EAAM,IAAA,GAAO,OAAA;EA1P5B;;;EAyQD,iBAAA,CAAkB,QAAA,WAAmB,OAAA;EA/OI;;;;;EA0PzC,oBAAA,CAAA,GAAwB,OAAA;EA7OtB;;;;;;;;;;EAqQF,uBAAA,CAAA,GAA2B,OAAA;IACtC,YAAA,EAAc,WAAA;IACd,aAAA,EAAe,YAAA;EAAA;EA5KR;;;;;;;;EA4LI,wBAAA,CAAA,GAA4B,OAAA;IACvC,YAAA;IACA,aAAA;EAAA;EA3JA;;;EA6KW,iBAAA,CAAkB,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;AAAA;AAAA,cAKzC,WAAA,EAAW,WAAoB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.service.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"sourcesContent":["import { Random } from \"@mongez/reinforcements\";\r\nimport type { ChildModel } from \"@warlock.js/cascade\";\r\nimport { config, ForbiddenError, hashPassword, verifyPassword } from \"@warlock.js/core\";\nimport type {\n AccessTokenOutput,\n AuthCredentials,\n DeviceInfo,\n LoginResult,\n TokenPair,\n} from \"../contracts/types\";\nimport { AccessToken } from \"../models/access-token\";\nimport type { Auth } from \"../models/auth.model\";\nimport { RefreshToken } from \"../models/refresh-token\";\nimport { authConfig } from \"./auth-config\";\nimport { authEvents } from \"./auth-events\";\nimport { isInvalidCredentialError, jwt } from \"./jwt\";\n\nclass AuthService {\n /**\r\n * Resolve the active access-token model — the package default, or a subclass\r\n * an app registered under `config.auth.accessToken.model` (e.g. to add a\r\n * tenant column). The service never references the concrete class directly so\r\n * an override is a pure config change.\r\n */\r\n private get accessTokenModel(): typeof AccessToken {\r\n return config.key(\"auth.accessToken.model\", AccessToken);\r\n }\r\n\r\n /**\r\n * Resolve the active refresh-token model (default or registered override).\r\n */\r\n private get refreshTokenModel(): typeof RefreshToken {\r\n return config.key(\"auth.refreshToken.model\", RefreshToken);\r\n }\r\n\r\n /**\r\n * Build the default access-token JWT payload from a user.\r\n */\r\n public buildAccessTokenPayload(user: Auth): Record<string, unknown> {\n return {\r\n id: user.id,\r\n userType: user.userType,\r\n created_at: Date.now(),\r\n };\r\n }\n\n /** One policy seam keeps account-state checks identical across all auth paths. */\n public async canAuthenticate(user: Auth): Promise<boolean> {\n return authConfig.canAuthenticate(user);\n }\n\n private async assertCanAuthenticate(user: Auth): Promise<void> {\n if (!(await this.canAuthenticate(user))) {\n throw new ForbiddenError(\"This user cannot authenticate.\");\n }\n }\n\n private async issueAccessToken(\n user: Auth,\n payload?: Record<string, unknown>,\n ): Promise<AccessTokenOutput> {\n const data = payload || this.buildAccessTokenPayload(user);\n // Validate before signing so an unusable lifetime cannot mint an immortal token.\n const expiresIn = authConfig.accessToken.expiresInMs();\n\n const token = await jwt.generate(data, { expiresIn });\n const expiresAt = new Date(Date.now() + expiresIn);\n\n await this.accessTokenModel.issue(user, token, expiresAt);\n\n return { token, expiresAt: expiresAt.toISOString() };\n }\n\r\n /**\r\n * Sign + persist an access token for the user and return the token with its\r\n * expiry. The expiry is computed locally from `expiresIn` rather than by\r\n * re-verifying the token we just signed.\r\n */\r\n public async generateAccessToken(\n user: Auth,\n payload?: Record<string, unknown>,\n ): Promise<AccessTokenOutput> {\n await this.assertCanAuthenticate(user);\n\n return this.issueAccessToken(user, payload);\n }\n\r\n private async issueRefreshToken(\n user: Auth,\n deviceInfo?: DeviceInfo,\n ): Promise<RefreshToken | undefined> {\n if (!authConfig.refreshToken.enabled()) return;\r\n\r\n // Validate the lifetime first — before the per-user cap is enforced, before\r\n // anything is signed — so a bad value can never revoke a user's oldest\r\n // session on its way to failing.\r\n const expiresIn = authConfig.refreshToken.expiresInMs();\r\n\r\n const familyId = deviceInfo?.familyId || Random.string(32);\r\n\r\n const payload = {\r\n userId: user.id,\r\n userType: user.userType,\r\n familyId,\r\n };\r\n\r\n const expiresAt = new Date(Date.now() + expiresIn).toISOString();\r\n\r\n await this.refreshTokenModel.enforceMax(user, authConfig.refreshToken.maxPerUser());\r\n\r\n const token = await jwt.generateRefreshToken(payload, { expiresIn });\r\n\r\n return this.refreshTokenModel.issue(user, token, { familyId, expiresAt, deviceInfo });\n }\n\n /**\n * Sign + persist a refresh token for the user (enforcing the per-user cap\n * first). Resolves to `undefined` when refresh tokens are disabled in config.\n */\n public async createRefreshToken(\n user: Auth,\n deviceInfo?: DeviceInfo,\n ): Promise<RefreshToken | undefined> {\n if (!authConfig.refreshToken.enabled()) return;\n\n await this.assertCanAuthenticate(user);\n\n return this.issueRefreshToken(user, deviceInfo);\n }\n\n private async issueTokenPair(user: Auth, deviceInfo?: DeviceInfo): Promise<TokenPair> {\n const accessToken = await this.issueAccessToken(user, deviceInfo?.payload);\n const refreshToken = await this.issueRefreshToken(user, deviceInfo);\n\n const tokenPair: TokenPair = {\n accessToken,\n refreshToken: refreshToken\n ? {\n token: refreshToken.get(\"token\"),\n expiresAt: refreshToken.get(\"expires_at\"),\n }\n : undefined,\n };\n\n authEvents.emit(\"token.created\", user, tokenPair);\n\n if (refreshToken) {\n authEvents.emit(\"session.created\", user, refreshToken, deviceInfo);\n }\n\n return tokenPair;\n }\n\r\n /**\r\n * Issue both an access and a refresh token, emitting the creation events.\r\n */\r\n public async createTokenPair(user: Auth, deviceInfo?: DeviceInfo): Promise<TokenPair> {\n await this.assertCanAuthenticate(user);\n\n return this.issueTokenPair(user, deviceInfo);\n }\n\r\n /**\r\n * Exchange a refresh token for a new token pair, with rotation + replay\r\n * detection. A concurrent reuse of the same token loses the atomic revoke and\r\n * is treated as a breach — the whole family is revoked and the request fails.\r\n */\r\n public async refreshTokens(\n refreshTokenString: string,\n deviceInfo?: DeviceInfo,\n ): Promise<TokenPair | null> {\n let decoded:\n | {\n userId: number;\n userType: string;\n familyId: string;\n }\n | null;\n\n try {\n decoded = await jwt.verifyRefreshToken<{\n userId: number;\n userType: string;\n familyId: string;\n }>(refreshTokenString);\n } catch (error) {\n if (isInvalidCredentialError(error)) return null;\n\n throw error;\n }\n\n if (!decoded) return null;\n\r\n const refreshToken = await this.refreshTokenModel.findByToken(refreshTokenString);\n\n if (!refreshToken?.isValid) {\n // An already-invalid token may be a replay of a rotated credential.\n if (refreshToken) {\n await this.revokeTokenFamily(refreshToken.familyId);\n }\n\n return null;\n }\n\n const UserModel = config.key(`auth.userType.${decoded.userType}`);\n\n if (!UserModel) {\n throw new Error(`User type ${decoded.userType} is unknown type.`);\n }\n\n const user = (await UserModel.find(decoded.userId)) as Auth | null;\n\n if (!user) return null;\n\n if (!(await this.canAuthenticate(user))) return null;\n\n const rotationEnabled = authConfig.refreshToken.rotation();\n\n if (rotationEnabled) {\n const won = await refreshToken.revokeIfActive();\n\r\n if (!won) {\n // A concurrent request already rotated this token.\n await this.revokeTokenFamily(refreshToken.familyId);\n\n return null;\n }\n } else {\n await refreshToken.markAsUsed();\n }\n\n const newTokenPair = await this.issueTokenPair(user, {\n ...deviceInfo,\n familyId: refreshToken.familyId,\n });\n\n authEvents.emit(\"token.refreshed\", user, newTokenPair, refreshToken);\n\n return newTokenPair;\n }\n\r\n /**\r\n * Verify a plaintext password against a stored hash.\r\n */\r\n public async verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean> {\r\n return verifyPassword(plainPassword, hashedPassword);\r\n }\r\n\r\n /**\r\n * Hash a plaintext password.\r\n */\r\n public async hashPassword(password: string): Promise<string> {\r\n return hashPassword(password);\r\n }\r\n\r\n /**\r\n * Resolve a user by credentials, verifying the password. Returns `null` on a\r\n * missing user or a wrong password, emitting `login.failed` either way.\r\n */\r\n public async attemptLogin<T extends Auth>(\n Model: ChildModel<T>,\n data: AuthCredentials,\n ): Promise<T | null> {\n const { password, ...otherData } = data;\r\n\r\n authEvents.emit(\"login.attempt\", otherData);\r\n\r\n const user = (await Model.first(otherData)) as T | null;\r\n\r\n if (!user) {\r\n authEvents.emit(\"login.failed\", otherData, \"User not found\");\r\n\r\n return null;\r\n }\r\n\r\n if (!(await this.verifyPassword(password, user.string(\"password\")!))) {\n authEvents.emit(\"login.failed\", otherData, \"Invalid password\");\n\n return null;\n }\n\n if (!(await this.canAuthenticate(user))) {\n authEvents.emit(\"login.failed\", otherData, \"Authentication not allowed\");\n\n return null;\n }\n\n return user;\n }\r\n\r\n /**\r\n * Full login flow: validate credentials, issue tokens, emit events. Returns\r\n * the user + token pair on success, `null` on failure.\r\n */\r\n public async login<T extends Auth>(\r\n Model: ChildModel<T>,\r\n credentials: AuthCredentials,\n deviceInfo?: DeviceInfo,\r\n ): Promise<LoginResult<T> | null> {\r\n const user = await this.attemptLogin(Model, credentials);\r\n\r\n if (!user) {\r\n return null;\r\n }\r\n\r\n if (!authConfig.refreshToken.enabled()) {\n const accessToken = await this.issueAccessToken(user, deviceInfo?.payload);\n\n return { user, tokens: { accessToken } };\n }\n\n const tokens = await this.issueTokenPair(user, deviceInfo);\n\r\n authEvents.emit(\"login.success\", user, tokens, deviceInfo);\r\n\r\n return { user, tokens };\n }\r\n\r\n /**\r\n * Log a user out.\r\n *\r\n * @param accessToken - access token string to revoke (optional)\r\n * @param refreshToken - refresh token string to revoke (optional)\r\n *\r\n * When no refresh token is supplied, `config.auth.refreshToken.logoutWithoutToken`\r\n * decides the behavior: `\"revoke-all\"` (default, fail-safe) revokes every\r\n * refresh token; `\"error\"` requires the caller to pass one.\r\n */\r\n public async logout(user: Auth, accessToken?: string, refreshToken?: string): Promise<void> {\r\n if (accessToken) {\r\n await this.removeAccessToken(user, accessToken);\r\n }\r\n\r\n if (refreshToken) {\r\n const token = await this.refreshTokenModel.findForUser(user, refreshToken);\r\n\r\n if (token) {\r\n await token.revoke();\r\n authEvents.emit(\"session.destroyed\", user, token);\r\n }\r\n } else {\r\n const behavior = authConfig.refreshToken.logoutWithoutToken();\r\n\r\n if (behavior === \"error\") {\r\n throw new Error(\"Refresh token required for logout\");\r\n }\r\n\r\n await this.revokeAllTokens(user);\r\n authEvents.emit(\"logout.failsafe\", user);\r\n }\r\n\r\n authEvents.emit(\"logout\", user);\r\n }\r\n\r\n /**\r\n * Remove a specific access token belonging to the user.\r\n */\r\n public async removeAccessToken(user: Auth, token: string): Promise<void> {\r\n await this.accessTokenModel.deleteForUser(user, token);\r\n }\r\n\r\n /**\r\n * Remove every access token belonging to the user.\r\n */\r\n public async removeAllAccessTokens(user: Auth): Promise<void> {\r\n await this.accessTokenModel.deleteAllForUser(user);\r\n }\r\n\r\n /**\r\n * Remove a specific refresh token belonging to the user.\r\n */\r\n public async removeRefreshToken(user: Auth, token: string): Promise<void> {\r\n await this.refreshTokenModel.deleteForUser(user, token);\r\n }\r\n\r\n /**\r\n * Revoke every active refresh token for the user and delete their access\r\n * tokens — \"log out of all devices\". Revocation is a single bulk update; an\r\n * event fires per revoked token.\r\n */\r\n public async revokeAllTokens(user: Auth): Promise<void> {\r\n const revokedTokens = await this.refreshTokenModel.revokeAllFor(user);\r\n\r\n for (const token of revokedTokens) {\r\n authEvents.emit(\"token.revoked\", user, token);\r\n }\r\n\r\n await this.removeAllAccessTokens(user);\r\n\r\n authEvents.emit(\"logout.all\", user);\r\n }\r\n\r\n /**\r\n * Revoke an entire token family — rotation breach containment.\r\n */\r\n public async revokeTokenFamily(familyId: string): Promise<void> {\r\n const revokedTokens = await this.refreshTokenModel.revokeFamily(familyId);\r\n\r\n authEvents.emit(\"token.familyRevoked\", familyId, revokedTokens);\r\n }\r\n\r\n /**\r\n * Delete expired tokens (refresh + access). Emits `token.expired` per refresh\r\n * token and `cleanup.completed` with the refresh count. Drives the\r\n * `auth.cleanup` CLI command.\r\n */\r\n public async cleanupExpiredTokens(): Promise<number> {\r\n const expiredTokens = await this.refreshTokenModel.purgeExpired();\r\n\r\n for (const token of expiredTokens) {\r\n authEvents.emit(\"token.expired\", token);\r\n }\r\n\r\n await this.accessTokenModel.purgeExpired();\r\n\r\n authEvents.emit(\"cleanup.completed\", expiredTokens.length);\r\n\r\n return expiredTokens.length;\r\n }\r\n\r\n /**\r\n * Find every persisted token — access and refresh — that can never retire\r\n * itself: an unusable `expires_at`, or a token carrying no `exp` claim.\r\n *\r\n * This is the remediation read for the pre-4.12.0 `expiresIn` defect (#25).\r\n * Nothing else surfaces these rows: `auth.cleanup` selects `expires_at < now`,\r\n * which an `Invalid Date` never satisfies, and a row whose date column is\r\n * perfectly fine can still hold a token with no deadline in it. Read-only —\r\n * pair with {@link purgeNeverExpiringTokens} to act on the answer.\r\n */\r\n public async findNeverExpiringTokens(): Promise<{\r\n accessTokens: AccessToken[];\r\n refreshTokens: RefreshToken[];\r\n }> {\r\n return {\r\n accessTokens: await this.accessTokenModel.findNeverExpiring(),\r\n refreshTokens: await this.refreshTokenModel.findNeverExpiring(),\r\n };\r\n }\r\n\r\n /**\r\n * Revoke every never-expiring token by deleting its row, emitting\r\n * `token.revoked` per refresh token. Drives `warlock auth.purge-never-expiring`.\r\n *\r\n * Access tokens first: deleting one is immediate revocation, while a refresh\r\n * token left in place for a moment can only mint an access token through a\r\n * verifier that now rejects it for the missing `exp`.\r\n */\r\n public async purgeNeverExpiringTokens(): Promise<{\r\n accessTokens: number;\r\n refreshTokens: number;\r\n }> {\r\n const accessTokens = await this.accessTokenModel.purgeNeverExpiring();\r\n const refreshTokens = await this.refreshTokenModel.purgeNeverExpiring();\r\n\r\n // `token.expired` (the event `auth.cleanup` already emits for a removed\r\n // refresh token) rather than `token.revoked`, which carries a loaded `Auth`\r\n // this batch path has no reason to fetch a user row for.\r\n for (const token of refreshTokens) {\r\n authEvents.emit(\"token.expired\", token);\r\n }\r\n\r\n return { accessTokens: accessTokens.length, refreshTokens: refreshTokens.length };\r\n }\r\n\r\n /**\r\n * Active, unexpired sessions for the user, newest first.\r\n */\r\n public async getActiveSessions(user: Auth): Promise<RefreshToken[]> {\r\n return this.refreshTokenModel.activeFor(user);\r\n }\r\n}\r\n\r\nexport const authService = new AuthService();\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auth.service.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"sourcesContent":["import { Random } from \"@mongez/reinforcements\";\r\nimport type { ChildModel } from \"@warlock.js/cascade\";\r\nimport { config, ForbiddenError, hashPassword, verifyPassword } from \"@warlock.js/core\";\nimport type {\n AccessTokenOutput,\n AuthCredentials,\n DeviceInfo,\n LoginResult,\n TokenPair,\n} from \"../contracts/types\";\nimport { AccessToken } from \"../models/access-token\";\nimport type { Auth } from \"../models/auth.model\";\nimport { RefreshToken } from \"../models/refresh-token\";\nimport { authConfig } from \"./auth-config\";\nimport { authEvents } from \"./auth-events\";\nimport { isInvalidCredentialError, jwt } from \"./jwt\";\n\nclass AuthService {\n /**\r\n * Resolve the active access-token model — the package default, or a subclass\r\n * an app registered under `config.auth.accessToken.model` (e.g. to add a\r\n * tenant column). The service never references the concrete class directly so\r\n * an override is a pure config change.\r\n */\r\n private get accessTokenModel(): typeof AccessToken {\r\n return config.key(\"auth.accessToken.model\", AccessToken);\r\n }\r\n\r\n /**\r\n * Resolve the active refresh-token model (default or registered override).\r\n */\r\n private get refreshTokenModel(): typeof RefreshToken {\r\n return config.key(\"auth.refreshToken.model\", RefreshToken);\r\n }\r\n\r\n /**\r\n * Build the default access-token JWT payload from a user.\r\n */\r\n public buildAccessTokenPayload(user: Auth): Record<string, unknown> {\n return {\r\n id: user.id,\r\n userType: user.userType,\r\n created_at: Date.now(),\r\n };\r\n }\n\n /** One policy seam keeps account-state checks identical across all auth paths. */\n public async canAuthenticate(user: Auth): Promise<boolean> {\n return authConfig.canAuthenticate(user);\n }\n\n private async assertCanAuthenticate(user: Auth): Promise<void> {\n if (!(await this.canAuthenticate(user))) {\n throw new ForbiddenError(\"This user cannot authenticate.\");\n }\n }\n\n private async issueAccessToken(\n user: Auth,\n payload?: Record<string, unknown>,\n ): Promise<AccessTokenOutput> {\n const data = payload || this.buildAccessTokenPayload(user);\n // Validate before signing so an unusable lifetime cannot mint an immortal token.\n const expiresIn = authConfig.accessToken.expiresInMs();\n\n const token = await jwt.generate(data, { expiresIn });\n const expiresAt = new Date(Date.now() + expiresIn);\n\n await this.accessTokenModel.issue(user, token, expiresAt);\n\n return { token, expiresAt: expiresAt.toISOString() };\n }\n\r\n /**\r\n * Sign + persist an access token for the user and return the token with its\r\n * expiry. The expiry is computed locally from `expiresIn` rather than by\r\n * re-verifying the token we just signed.\r\n */\r\n public async generateAccessToken(\n user: Auth,\n payload?: Record<string, unknown>,\n ): Promise<AccessTokenOutput> {\n await this.assertCanAuthenticate(user);\n\n return this.issueAccessToken(user, payload);\n }\n\r\n private async issueRefreshToken(\n user: Auth,\n deviceInfo?: DeviceInfo,\n ): Promise<RefreshToken | undefined> {\n if (!authConfig.refreshToken.enabled()) return;\r\n\r\n // Validate the lifetime first — before the per-user cap is enforced, before\r\n // anything is signed — so a bad value can never revoke a user's oldest\r\n // session on its way to failing.\r\n const expiresIn = authConfig.refreshToken.expiresInMs();\r\n\r\n const familyId = deviceInfo?.familyId || Random.string(32);\r\n\r\n const payload = {\r\n userId: user.id,\r\n userType: user.userType,\r\n familyId,\r\n };\r\n\r\n const expiresAt = new Date(Date.now() + expiresIn).toISOString();\r\n\r\n await this.refreshTokenModel.enforceMax(user, authConfig.refreshToken.maxPerUser());\r\n\r\n const token = await jwt.generateRefreshToken(payload, { expiresIn });\r\n\r\n return this.refreshTokenModel.issue(user, token, { familyId, expiresAt, deviceInfo });\n }\n\n /**\n * Sign + persist a refresh token for the user (enforcing the per-user cap\n * first). Resolves to `undefined` when refresh tokens are disabled in config.\n */\n public async createRefreshToken(\n user: Auth,\n deviceInfo?: DeviceInfo,\n ): Promise<RefreshToken | undefined> {\n if (!authConfig.refreshToken.enabled()) return;\n\n await this.assertCanAuthenticate(user);\n\n return this.issueRefreshToken(user, deviceInfo);\n }\n\n private async issueTokenPair(user: Auth, deviceInfo?: DeviceInfo): Promise<TokenPair> {\n const accessToken = await this.issueAccessToken(user, deviceInfo?.payload);\n const refreshToken = await this.issueRefreshToken(user, deviceInfo);\n\n const tokenPair: TokenPair = {\n accessToken,\n refreshToken: refreshToken\n ? {\n token: refreshToken.get(\"token\"),\n expiresAt: refreshToken.get(\"expires_at\"),\n }\n : undefined,\n };\n\n authEvents.emit(\"token.created\", user, tokenPair);\n\n if (refreshToken) {\n authEvents.emit(\"session.created\", user, refreshToken, deviceInfo);\n }\n\n return tokenPair;\n }\n\r\n /**\r\n * Issue both an access and a refresh token, emitting the creation events.\r\n */\r\n public async createTokenPair(user: Auth, deviceInfo?: DeviceInfo): Promise<TokenPair> {\n await this.assertCanAuthenticate(user);\n\n return this.issueTokenPair(user, deviceInfo);\n }\n\r\n /**\r\n * Exchange a refresh token for a new token pair, with rotation + replay\r\n * detection. A concurrent reuse of the same token loses the atomic revoke and\r\n * is treated as a breach — the whole family is revoked and the request fails.\r\n */\r\n public async refreshTokens(\n refreshTokenString: string,\n deviceInfo?: DeviceInfo,\n ): Promise<TokenPair | null> {\n let decoded:\n | {\n userId: number;\n userType: string;\n familyId: string;\n }\n | null;\n\n try {\n decoded = await jwt.verifyRefreshToken<{\n userId: number;\n userType: string;\n familyId: string;\n }>(refreshTokenString);\n } catch (error) {\n if (isInvalidCredentialError(error)) return null;\n\n throw error;\n }\n\n if (!decoded) return null;\n\r\n const refreshToken = await this.refreshTokenModel.findByToken(refreshTokenString);\n\n if (!refreshToken?.isValid) {\n // An already-invalid token may be a replay of a rotated credential.\n if (refreshToken) {\n await this.revokeTokenFamily(refreshToken.familyId);\n }\n\n return null;\n }\n\n const UserModel = config.key(`auth.userType.${decoded.userType}`);\n\n if (!UserModel) {\n throw new Error(`User type ${decoded.userType} is unknown type.`);\n }\n\n const user = (await UserModel.find(decoded.userId)) as Auth | null;\n\n if (!user) return null;\n\n if (!(await this.canAuthenticate(user))) return null;\n\n const rotationEnabled = authConfig.refreshToken.rotation();\n\n if (rotationEnabled) {\n const won = await refreshToken.revokeIfActive();\n\r\n if (!won) {\n // A concurrent request already rotated this token.\n await this.revokeTokenFamily(refreshToken.familyId);\n\n return null;\n }\n } else {\n await refreshToken.markAsUsed();\n }\n\n const newTokenPair = await this.issueTokenPair(user, {\n ...deviceInfo,\n familyId: refreshToken.familyId,\n });\n\n authEvents.emit(\"token.refreshed\", user, newTokenPair, refreshToken);\n\n return newTokenPair;\n }\n\r\n /**\r\n * Verify a plaintext password against a stored hash.\r\n */\r\n public async verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean> {\r\n return verifyPassword(plainPassword, hashedPassword);\r\n }\r\n\r\n /**\r\n * Hash a plaintext password.\r\n */\r\n public async hashPassword(password: string): Promise<string> {\r\n return hashPassword(password);\r\n }\r\n\r\n /**\r\n * Resolve a user by credentials, verifying the password. Returns `null` on a\r\n * missing user or a wrong password, emitting `login.failed` either way.\r\n */\r\n public async attemptLogin<T extends Auth>(\n Model: ChildModel<T>,\n data: AuthCredentials,\n ): Promise<T | null> {\n const { password, ...otherData } = data;\r\n\r\n authEvents.emit(\"login.attempt\", otherData);\r\n\r\n const user = (await Model.first(otherData)) as T | null;\r\n\r\n if (!user) {\r\n authEvents.emit(\"login.failed\", otherData, \"User not found\");\r\n\r\n return null;\r\n }\r\n\r\n if (!(await this.verifyPassword(password, user.string(\"password\")!))) {\n authEvents.emit(\"login.failed\", otherData, \"Invalid password\");\n\n return null;\n }\n\n if (!(await this.canAuthenticate(user))) {\n authEvents.emit(\"login.failed\", otherData, \"Authentication not allowed\");\n\n return null;\n }\n\n return user;\n }\r\n\r\n /**\r\n * Full login flow: validate credentials, issue tokens, emit events. Returns\r\n * the user + token pair on success, `null` on failure.\r\n */\r\n public async login<T extends Auth>(\r\n Model: ChildModel<T>,\r\n credentials: AuthCredentials,\n deviceInfo?: DeviceInfo,\r\n ): Promise<LoginResult<T> | null> {\r\n const user = await this.attemptLogin(Model, credentials);\r\n\r\n if (!user) {\r\n return null;\r\n }\r\n\r\n if (!authConfig.refreshToken.enabled()) {\n const accessToken = await this.issueAccessToken(user, deviceInfo?.payload);\n\n return { user, tokens: { accessToken } };\n }\n\n const tokens = await this.issueTokenPair(user, deviceInfo);\n\r\n authEvents.emit(\"login.success\", user, tokens, deviceInfo);\r\n\r\n return { user, tokens };\n }\r\n\r\n /**\r\n * Log a user out.\r\n *\r\n * @param accessToken - access token string to revoke (optional)\r\n * @param refreshToken - refresh token string to revoke (optional)\r\n *\r\n * When no refresh token is supplied, `config.auth.refreshToken.logoutWithoutToken`\r\n * decides the behavior: `\"revoke-all\"` (default, fail-safe) revokes every\r\n * refresh token; `\"error\"` requires the caller to pass one.\r\n */\r\n public async logout(user: Auth, accessToken?: string, refreshToken?: string): Promise<void> {\r\n if (accessToken) {\r\n await this.removeAccessToken(user, accessToken);\r\n }\r\n\r\n if (refreshToken) {\r\n const token = await this.refreshTokenModel.findForUser(user, refreshToken);\r\n\r\n if (token) {\r\n await token.revoke();\r\n authEvents.emit(\"session.destroyed\", user, token);\r\n }\r\n } else {\r\n const behavior = authConfig.refreshToken.logoutWithoutToken();\r\n\r\n if (behavior === \"error\") {\r\n throw new Error(\"Refresh token required for logout\");\r\n }\r\n\r\n await this.revokeAllTokens(user);\r\n authEvents.emit(\"logout.failsafe\", user);\r\n }\r\n\r\n authEvents.emit(\"logout\", user);\r\n }\r\n\r\n /**\r\n * Remove a specific access token belonging to the user.\r\n */\r\n public async removeAccessToken(user: Auth, token: string): Promise<void> {\r\n await this.accessTokenModel.deleteForUser(user, token);\r\n }\r\n\r\n /**\r\n * Remove every access token belonging to the user.\r\n */\r\n public async removeAllAccessTokens(user: Auth): Promise<void> {\r\n await this.accessTokenModel.deleteAllForUser(user);\r\n }\r\n\r\n /**\r\n * Remove a specific refresh token belonging to the user.\r\n */\r\n public async removeRefreshToken(user: Auth, token: string): Promise<void> {\r\n await this.refreshTokenModel.deleteForUser(user, token);\r\n }\r\n\r\n /**\r\n * Revoke every active refresh token for the user and delete their access\r\n * tokens — \"log out of all devices\". Revocation is a single bulk update; an\r\n * event fires per revoked token.\r\n */\r\n public async revokeAllTokens(user: Auth): Promise<void> {\r\n const revokedTokens = await this.refreshTokenModel.revokeAllFor(user);\r\n\r\n for (const token of revokedTokens) {\r\n authEvents.emit(\"token.revoked\", user, token);\r\n }\r\n\r\n await this.removeAllAccessTokens(user);\r\n\r\n authEvents.emit(\"logout.all\", user);\r\n }\r\n\r\n /**\r\n * Revoke an entire token family — rotation breach containment.\r\n */\r\n public async revokeTokenFamily(familyId: string): Promise<void> {\r\n const revokedTokens = await this.refreshTokenModel.revokeFamily(familyId);\r\n\r\n authEvents.emit(\"token.familyRevoked\", familyId, revokedTokens);\r\n }\r\n\r\n /**\r\n * Delete expired tokens (refresh + access). Emits `token.expired` per refresh\r\n * token and `cleanup.completed` with the refresh count. Drives the\r\n * `auth.cleanup` CLI command.\r\n */\r\n public async cleanupExpiredTokens(): Promise<number> {\r\n const expiredTokens = await this.refreshTokenModel.purgeExpired();\r\n\r\n for (const token of expiredTokens) {\r\n authEvents.emit(\"token.expired\", token);\r\n }\r\n\r\n await this.accessTokenModel.purgeExpired();\r\n\r\n authEvents.emit(\"cleanup.completed\", expiredTokens.length);\r\n\r\n return expiredTokens.length;\r\n }\r\n\r\n /**\r\n * Find every persisted token — access and refresh — that can never retire\r\n * itself: an unusable `expires_at`, or a token carrying no `exp` claim.\r\n *\r\n * This is the remediation read for the pre-4.12.0 `expiresIn` defect (#25).\r\n * Nothing else surfaces these rows: `auth.cleanup` selects `expires_at < now`,\r\n * which an `Invalid Date` never satisfies, and a row whose date column is\r\n * perfectly fine can still hold a token with no deadline in it. Read-only —\r\n * pair with {@link purgeNeverExpiringTokens} to act on the answer.\r\n */\r\n public async findNeverExpiringTokens(): Promise<{\r\n accessTokens: AccessToken[];\r\n refreshTokens: RefreshToken[];\r\n }> {\r\n return {\r\n accessTokens: await this.accessTokenModel.findNeverExpiring(),\r\n refreshTokens: await this.refreshTokenModel.findNeverExpiring(),\r\n };\r\n }\r\n\r\n /**\r\n * Revoke every never-expiring token by deleting its row, emitting\r\n * `token.revoked` per refresh token. Drives `warlock auth.purge-never-expiring`.\r\n *\r\n * Access tokens first: deleting one is immediate revocation, while a refresh\r\n * token left in place for a moment can only mint an access token through a\r\n * verifier that now rejects it for the missing `exp`.\r\n */\r\n public async purgeNeverExpiringTokens(): Promise<{\r\n accessTokens: number;\r\n refreshTokens: number;\r\n }> {\r\n const accessTokens = await this.accessTokenModel.purgeNeverExpiring();\r\n const refreshTokens = await this.refreshTokenModel.purgeNeverExpiring();\r\n\r\n // `token.expired` (the event `auth.cleanup` already emits for a removed\r\n // refresh token) rather than `token.revoked`, which carries a loaded `Auth`\r\n // this batch path has no reason to fetch a user row for.\r\n for (const token of refreshTokens) {\r\n authEvents.emit(\"token.expired\", token);\r\n }\r\n\r\n return { accessTokens: accessTokens.length, refreshTokens: refreshTokens.length };\r\n }\r\n\r\n /**\r\n * Active, unexpired sessions for the user, newest first.\r\n */\r\n public async getActiveSessions(user: Auth): Promise<RefreshToken[]> {\r\n return this.refreshTokenModel.activeFor(user);\r\n }\r\n}\r\n\r\nexport const authService = new AuthService();\r\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-jwt-secret.d.mts","names":[],"sources":["../../../../../../../auth/src/services/generate-jwt-secret.ts"],"mappings":";iBAKsB,iBAAA,
|
|
1
|
+
{"version":3,"file":"generate-jwt-secret.d.mts","names":[],"sources":["../../../../../../../auth/src/services/generate-jwt-secret.ts"],"mappings":";iBAKsB,iBAAA,CAAA,GAAiB,OAAA"}
|
package/esm/services/jwt.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.mjs","names":[],"sources":["../../../../../../../auth/src/services/jwt.ts"],"sourcesContent":["import { createSigner, createVerifier, type SignerOptions, type VerifierOptions } from \"fast-jwt\";\r\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\r\nimport { authConfig } from \"./auth-config\";\r\n\r\nconst getSecretKey = () => authConfig.accessToken.secret();\r\nconst getAlgorithm = () => authConfig.accessToken.algorithm();\r\n\r\n// Refresh tokens may declare their own secret; when unset/empty we fall back to\r\n// the access-token secret (the documented optional behavior).\r\nconst getRefreshSecretKey = () => authConfig.refreshToken.secret() || getSecretKey();\r\n\r\n/**\r\n * Token class. Stamped as the `tokenType` claim on every signed token and\r\n * checked on verify so an access token can never be accepted where a refresh\r\n * token is expected (and vice versa) — even when both share the same secret\r\n * under the documented refresh-secret fallback. Legacy tokens minted before\r\n * this claim existed carry no `tokenType` and remain accepted; a *mismatched*\r\n * type is always rejected.\r\n */\r\nexport type TokenType = \"access\" | \"refresh\";\r\n\r\nconst ACCESS_TOKEN_TYPE: TokenType = \"access\";\nconst REFRESH_TOKEN_TYPE: TokenType = \"refresh\";\n\n/**\n * Error codes that mean \"the credential itself is bad\", as opposed to \"this\n * server could not check it\". This is an ALLOWLIST and must stay one. The codes\n * deliberately left out — `FAST_JWT_INVALID_KEY`, `FAST_JWT_MISSING_KEY`,\n * `FAST_JWT_KEY_FETCHING_ERROR`, `FAST_JWT_INVALID_OPTION`,\n * `FAST_JWT_VERIFY_ERROR`, and `FAST_JWT_SIGN_ERROR` — describe a broken\n * server, not a broken token, and so does every unknown future code.\n */\nconst INVALID_CREDENTIAL_ERROR_CODES = new Set<string>([\n \"FAST_JWT_MALFORMED\",\n \"FAST_JWT_INVALID_SIGNATURE\",\n \"FAST_JWT_MISSING_SIGNATURE\",\n \"FAST_JWT_INVALID_ALGORITHM\",\n \"FAST_JWT_EXPIRED\",\n \"FAST_JWT_INACTIVE\",\n // How a token carrying no `exp` is rejected — `jwt.verify` forces `exp` into\n // `requiredClaims`, so this code IS the missing-deadline guard firing.\n \"FAST_JWT_MISSING_REQUIRED_CLAIM\",\n \"FAST_JWT_INVALID_CLAIM_VALUE\",\n \"FAST_JWT_INVALID_CLAIM_TYPE\",\n \"FAST_JWT_INVALID_CRIT_HEADER\",\n \"FAST_JWT_INVALID_TYPE\",\n \"FAST_JWT_INVALID_PAYLOAD\",\n AuthErrorCodes.InvalidTokenType,\n]);\n\n/**\n * Only coded credential failures become authentication misses. Plain errors,\n * including missing-secret configuration failures, propagate to the central\n * server-error path instead of reading as \"everyone's token is bad\".\n */\nexport function isInvalidCredentialError(error: unknown): boolean {\n const code = (error as { code?: unknown } | null | undefined)?.code;\n\n return typeof code === \"string\" && INVALID_CREDENTIAL_ERROR_CODES.has(code);\n}\n\n/**\n * A `tokenType` claim that does not match what the caller expects. This IS a\n * bad credential (an access-token cookie can only hold a refresh token\r\n * through an app bug) — but unlike `fast-jwt`'s own rejections it was\r\n * previously a plain `Error`, unclassifiable by a caller that wants to answer\r\n * 401 without string-matching a message that could reword on any release.\r\n * `code` mirrors how `fast-jwt`'s `TokenError` carries its own code, so a\r\n * caller can classify both through `isInvalidCredentialError` above.\n */\nexport class TokenTypeError extends Error {\n readonly code = AuthErrorCodes.InvalidTokenType;\r\n\r\n constructor(expected: TokenType, actual: string) {\r\n super(`Invalid token type: expected \"${expected}\", received \"${actual}\".`);\r\n this.name = \"TokenTypeError\";\r\n }\r\n}\r\n\r\n/**\r\n * Reject the token when its `tokenType` claim is present and does not match the\r\n * expected class. Absent claim ⇒ legacy token, accepted (backward compatible).\r\n */\r\nfunction assertTokenType(decoded: unknown, expected: TokenType): void {\r\n const actual = (decoded as { tokenType?: unknown } | null | undefined)?.tokenType;\r\n\r\n if (typeof actual === \"string\" && actual !== expected) {\r\n throw new TokenTypeError(expected, actual);\r\n }\r\n}\r\n\r\n/**\r\n * Claims no token may be missing, whatever the caller asks for.\r\n *\r\n * A JWT with no `exp` is not \"a token with a long life\" — it is a credential\r\n * with *no* life, because a verifier with no deadline to check simply succeeds,\r\n * forever (measured against `fast-jwt@6.2.4`: a token with no `exp` verifies\r\n * unchanged at `clockTimestamp` + 100 years). Nothing in this package can mint\r\n * one as of 4.12.0, but tokens minted by an earlier version are already in the\r\n * wild, so the rejection lives on the *verify* side where it catches a token\r\n * from any version, including one signed by a service this package never ran.\r\n *\r\n * There is no legitimate source to preserve: an app that wants a token that\r\n * effectively never expires sets `expiresIn: NO_EXPIRATION` (`\"100y\"`), which\r\n * mints a real `exp` roughly a century out (measured: `ms(\"100y\")` ⇒\r\n * `3155760000000`, `exp - iat` ⇒ `3155760000` seconds). \"No deadline\" and \"a\r\n * distant deadline\" are different things, and only the second one is asked for.\r\n */\r\nconst REQUIRED_CLAIMS = [\"exp\"];\r\n\r\n/**\r\n * Union the caller's `requiredClaims` with the mandatory ones — a caller may\r\n * add requirements, never drop them.\r\n */\r\nfunction withRequiredClaims(callerClaims?: string[]): string[] {\r\n if (!callerClaims?.length) return REQUIRED_CLAIMS;\r\n\r\n return [...new Set([...callerClaims, ...REQUIRED_CLAIMS])];\r\n}\r\n\r\nexport const jwt = {\r\n /**\r\n * Generate a new JWT token for the user.\r\n * @param payload The payload to encode in the JWT token.\r\n */\r\n async generate(\r\n payload: any,\r\n {\r\n key = getSecretKey(),\r\n algorithm = getAlgorithm(),\r\n ...options\r\n }: SignerOptions & { key?: string } = {},\r\n ): Promise<string> {\r\n // Create a signer function with predefined options\r\n const sign = createSigner({ key, ...options, algorithm });\r\n\r\n const token = await sign({ ...payload, tokenType: ACCESS_TOKEN_TYPE });\r\n return token;\r\n },\r\n\r\n /**\r\n * Verify the given token.\r\n * @param token The JWT token to verify.\r\n * @returns The decoded token payload if verification is successful.\r\n */\r\n async verify<T = unknown>(\r\n token: string,\r\n {\r\n key = getSecretKey(),\r\n algorithms = [getAlgorithm()],\r\n requiredClaims,\r\n ...options\r\n }: VerifierOptions & { key?: string } = {},\r\n ): Promise<T> {\r\n const verify = createVerifier({\r\n key,\r\n ...options,\r\n algorithms,\r\n requiredClaims: withRequiredClaims(requiredClaims),\r\n });\r\n\r\n const decoded = await verify(token as string);\r\n\r\n assertTokenType(decoded, ACCESS_TOKEN_TYPE);\r\n\r\n return decoded;\r\n },\r\n\r\n /**\r\n * Generate a new refresh token for the user.\r\n */\r\n async generateRefreshToken(\r\n payload: any,\r\n {\r\n key = getRefreshSecretKey(),\r\n expiresIn,\r\n algorithm = getAlgorithm(),\r\n ...options\r\n }: SignerOptions & { key?: string } = {},\r\n ): Promise<string> {\r\n const sign = createSigner({ key, expiresIn, algorithm, ...options });\r\n return sign({ ...payload, tokenType: REFRESH_TOKEN_TYPE });\r\n },\r\n\r\n /**\r\n * Verify the given refresh token.\r\n */\r\n async verifyRefreshToken<T = unknown>(\r\n token: string,\r\n {\r\n key = getRefreshSecretKey(),\r\n algorithms = [getAlgorithm()],\r\n requiredClaims,\r\n ...options\r\n }: VerifierOptions & { key?: string } = {},\r\n ): Promise<T> {\r\n const verify = createVerifier({\r\n key,\r\n algorithms,\r\n ...options,\r\n requiredClaims: withRequiredClaims(requiredClaims),\r\n });\r\n\r\n const decoded = await verify(token);\r\n\r\n assertTokenType(decoded, REFRESH_TOKEN_TYPE);\r\n\r\n return decoded;\r\n },\r\n};\r\n"],"mappings":";;;;;AAIA,MAAM,qBAAqB,WAAW,YAAY,OAAO;AACzD,MAAM,qBAAqB,WAAW,YAAY,UAAU;AAI5D,MAAM,4BAA4B,WAAW,aAAa,OAAO,KAAK,aAAa;AAYnF,MAAM,oBAA+B;AACrC,MAAM,qBAAgC;;;;;;;;;AAUtC,MAAM,iCAAiC,IAAI,IAAY;CACrD;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;;AAEF,CAAC;;;;;;AAOD,SAAgB,yBAAyB,OAAyB;CAChE,MAAM,OAAQ,OAAiD;CAE/D,OAAO,OAAO,SAAS,YAAY,+BAA+B,IAAI,IAAI;AAC5E;;;;;;;;;;AAWA,IAAa,iBAAb,cAAoC,MAAM;CAGxC,YAAY,UAAqB,QAAgB;EAC/C,MAAM,iCAAiC,SAAS,eAAe,OAAO,GAAG;;EACzE,KAAK,OAAO;CACd;AACF;;;;;AAMA,SAAS,gBAAgB,SAAkB,UAA2B;CACpE,MAAM,SAAU,SAAwD;CAExE,IAAI,OAAO,WAAW,YAAY,WAAW,UAC3C,MAAM,IAAI,eAAe,UAAU,MAAM;AAE7C;;;;;;;;;;;;;;;;;;AAmBA,MAAM,kBAAkB,CAAC,KAAK;;;;;AAM9B,SAAS,mBAAmB,cAAmC;CAC7D,IAAI,CAAC,cAAc,QAAQ,OAAO;CAElC,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,cAAc,GAAG,eAAe,CAAC,CAAC;AAC3D;AAEA,MAAa,MAAM;;;;;CAKjB,MAAM,SACJ,SACA,EACE,MAAM,aAAa,GACnB,YAAY,aAAa,GACzB,GAAG,YACiC,CAAC,GACtB;EAKjB,OAAO,MAHM,aAAa;GAAE;GAAK,GAAG;GAAS;EAAU,CAEhC,
|
|
1
|
+
{"version":3,"file":"jwt.mjs","names":[],"sources":["../../../../../../../auth/src/services/jwt.ts"],"sourcesContent":["import { createSigner, createVerifier, type SignerOptions, type VerifierOptions } from \"fast-jwt\";\r\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\r\nimport { authConfig } from \"./auth-config\";\r\n\r\nconst getSecretKey = () => authConfig.accessToken.secret();\r\nconst getAlgorithm = () => authConfig.accessToken.algorithm();\r\n\r\n// Refresh tokens may declare their own secret; when unset/empty we fall back to\r\n// the access-token secret (the documented optional behavior).\r\nconst getRefreshSecretKey = () => authConfig.refreshToken.secret() || getSecretKey();\r\n\r\n/**\r\n * Token class. Stamped as the `tokenType` claim on every signed token and\r\n * checked on verify so an access token can never be accepted where a refresh\r\n * token is expected (and vice versa) — even when both share the same secret\r\n * under the documented refresh-secret fallback. Legacy tokens minted before\r\n * this claim existed carry no `tokenType` and remain accepted; a *mismatched*\r\n * type is always rejected.\r\n */\r\nexport type TokenType = \"access\" | \"refresh\";\r\n\r\nconst ACCESS_TOKEN_TYPE: TokenType = \"access\";\nconst REFRESH_TOKEN_TYPE: TokenType = \"refresh\";\n\n/**\n * Error codes that mean \"the credential itself is bad\", as opposed to \"this\n * server could not check it\". This is an ALLOWLIST and must stay one. The codes\n * deliberately left out — `FAST_JWT_INVALID_KEY`, `FAST_JWT_MISSING_KEY`,\n * `FAST_JWT_KEY_FETCHING_ERROR`, `FAST_JWT_INVALID_OPTION`,\n * `FAST_JWT_VERIFY_ERROR`, and `FAST_JWT_SIGN_ERROR` — describe a broken\n * server, not a broken token, and so does every unknown future code.\n */\nconst INVALID_CREDENTIAL_ERROR_CODES = new Set<string>([\n \"FAST_JWT_MALFORMED\",\n \"FAST_JWT_INVALID_SIGNATURE\",\n \"FAST_JWT_MISSING_SIGNATURE\",\n \"FAST_JWT_INVALID_ALGORITHM\",\n \"FAST_JWT_EXPIRED\",\n \"FAST_JWT_INACTIVE\",\n // How a token carrying no `exp` is rejected — `jwt.verify` forces `exp` into\n // `requiredClaims`, so this code IS the missing-deadline guard firing.\n \"FAST_JWT_MISSING_REQUIRED_CLAIM\",\n \"FAST_JWT_INVALID_CLAIM_VALUE\",\n \"FAST_JWT_INVALID_CLAIM_TYPE\",\n \"FAST_JWT_INVALID_CRIT_HEADER\",\n \"FAST_JWT_INVALID_TYPE\",\n \"FAST_JWT_INVALID_PAYLOAD\",\n AuthErrorCodes.InvalidTokenType,\n]);\n\n/**\n * Only coded credential failures become authentication misses. Plain errors,\n * including missing-secret configuration failures, propagate to the central\n * server-error path instead of reading as \"everyone's token is bad\".\n */\nexport function isInvalidCredentialError(error: unknown): boolean {\n const code = (error as { code?: unknown } | null | undefined)?.code;\n\n return typeof code === \"string\" && INVALID_CREDENTIAL_ERROR_CODES.has(code);\n}\n\n/**\n * A `tokenType` claim that does not match what the caller expects. This IS a\n * bad credential (an access-token cookie can only hold a refresh token\r\n * through an app bug) — but unlike `fast-jwt`'s own rejections it was\r\n * previously a plain `Error`, unclassifiable by a caller that wants to answer\r\n * 401 without string-matching a message that could reword on any release.\r\n * `code` mirrors how `fast-jwt`'s `TokenError` carries its own code, so a\r\n * caller can classify both through `isInvalidCredentialError` above.\n */\nexport class TokenTypeError extends Error {\n readonly code = AuthErrorCodes.InvalidTokenType;\r\n\r\n constructor(expected: TokenType, actual: string) {\r\n super(`Invalid token type: expected \"${expected}\", received \"${actual}\".`);\r\n this.name = \"TokenTypeError\";\r\n }\r\n}\r\n\r\n/**\r\n * Reject the token when its `tokenType` claim is present and does not match the\r\n * expected class. Absent claim ⇒ legacy token, accepted (backward compatible).\r\n */\r\nfunction assertTokenType(decoded: unknown, expected: TokenType): void {\r\n const actual = (decoded as { tokenType?: unknown } | null | undefined)?.tokenType;\r\n\r\n if (typeof actual === \"string\" && actual !== expected) {\r\n throw new TokenTypeError(expected, actual);\r\n }\r\n}\r\n\r\n/**\r\n * Claims no token may be missing, whatever the caller asks for.\r\n *\r\n * A JWT with no `exp` is not \"a token with a long life\" — it is a credential\r\n * with *no* life, because a verifier with no deadline to check simply succeeds,\r\n * forever (measured against `fast-jwt@6.2.4`: a token with no `exp` verifies\r\n * unchanged at `clockTimestamp` + 100 years). Nothing in this package can mint\r\n * one as of 4.12.0, but tokens minted by an earlier version are already in the\r\n * wild, so the rejection lives on the *verify* side where it catches a token\r\n * from any version, including one signed by a service this package never ran.\r\n *\r\n * There is no legitimate source to preserve: an app that wants a token that\r\n * effectively never expires sets `expiresIn: NO_EXPIRATION` (`\"100y\"`), which\r\n * mints a real `exp` roughly a century out (measured: `ms(\"100y\")` ⇒\r\n * `3155760000000`, `exp - iat` ⇒ `3155760000` seconds). \"No deadline\" and \"a\r\n * distant deadline\" are different things, and only the second one is asked for.\r\n */\r\nconst REQUIRED_CLAIMS = [\"exp\"];\r\n\r\n/**\r\n * Union the caller's `requiredClaims` with the mandatory ones — a caller may\r\n * add requirements, never drop them.\r\n */\r\nfunction withRequiredClaims(callerClaims?: string[]): string[] {\r\n if (!callerClaims?.length) return REQUIRED_CLAIMS;\r\n\r\n return [...new Set([...callerClaims, ...REQUIRED_CLAIMS])];\r\n}\r\n\r\nexport const jwt = {\r\n /**\r\n * Generate a new JWT token for the user.\r\n * @param payload The payload to encode in the JWT token.\r\n */\r\n async generate(\r\n payload: any,\r\n {\r\n key = getSecretKey(),\r\n algorithm = getAlgorithm(),\r\n ...options\r\n }: SignerOptions & { key?: string } = {},\r\n ): Promise<string> {\r\n // Create a signer function with predefined options\r\n const sign = createSigner({ key, ...options, algorithm });\r\n\r\n const token = await sign({ ...payload, tokenType: ACCESS_TOKEN_TYPE });\r\n return token;\r\n },\r\n\r\n /**\r\n * Verify the given token.\r\n * @param token The JWT token to verify.\r\n * @returns The decoded token payload if verification is successful.\r\n */\r\n async verify<T = unknown>(\r\n token: string,\r\n {\r\n key = getSecretKey(),\r\n algorithms = [getAlgorithm()],\r\n requiredClaims,\r\n ...options\r\n }: VerifierOptions & { key?: string } = {},\r\n ): Promise<T> {\r\n const verify = createVerifier({\r\n key,\r\n ...options,\r\n algorithms,\r\n requiredClaims: withRequiredClaims(requiredClaims),\r\n });\r\n\r\n const decoded = await verify(token as string);\r\n\r\n assertTokenType(decoded, ACCESS_TOKEN_TYPE);\r\n\r\n return decoded;\r\n },\r\n\r\n /**\r\n * Generate a new refresh token for the user.\r\n */\r\n async generateRefreshToken(\r\n payload: any,\r\n {\r\n key = getRefreshSecretKey(),\r\n expiresIn,\r\n algorithm = getAlgorithm(),\r\n ...options\r\n }: SignerOptions & { key?: string } = {},\r\n ): Promise<string> {\r\n const sign = createSigner({ key, expiresIn, algorithm, ...options });\r\n return sign({ ...payload, tokenType: REFRESH_TOKEN_TYPE });\r\n },\r\n\r\n /**\r\n * Verify the given refresh token.\r\n */\r\n async verifyRefreshToken<T = unknown>(\r\n token: string,\r\n {\r\n key = getRefreshSecretKey(),\r\n algorithms = [getAlgorithm()],\r\n requiredClaims,\r\n ...options\r\n }: VerifierOptions & { key?: string } = {},\r\n ): Promise<T> {\r\n const verify = createVerifier({\r\n key,\r\n algorithms,\r\n ...options,\r\n requiredClaims: withRequiredClaims(requiredClaims),\r\n });\r\n\r\n const decoded = await verify(token);\r\n\r\n assertTokenType(decoded, REFRESH_TOKEN_TYPE);\r\n\r\n return decoded;\r\n },\r\n};\r\n"],"mappings":";;;;;AAIA,MAAM,qBAAqB,WAAW,YAAY,OAAO;AACzD,MAAM,qBAAqB,WAAW,YAAY,UAAU;AAI5D,MAAM,4BAA4B,WAAW,aAAa,OAAO,KAAK,aAAa;AAYnF,MAAM,oBAA+B;AACrC,MAAM,qBAAgC;;;;;;;;;AAUtC,MAAM,iCAAiC,IAAI,IAAY;CACrD;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;;AAEF,CAAC;;;;;;AAOD,SAAgB,yBAAyB,OAAyB;CAChE,MAAM,OAAQ,OAAiD;CAE/D,OAAO,OAAO,SAAS,YAAY,+BAA+B,IAAI,IAAI;AAC5E;;;;;;;;;;AAWA,IAAa,iBAAb,cAAoC,MAAM;CAGxC,YAAY,UAAqB,QAAgB;EAC/C,MAAM,iCAAiC,SAAS,eAAe,OAAO,GAAG;;EACzE,KAAK,OAAO;CACd;AACF;;;;;AAMA,SAAS,gBAAgB,SAAkB,UAA2B;CACpE,MAAM,SAAU,SAAwD;CAExE,IAAI,OAAO,WAAW,YAAY,WAAW,UAC3C,MAAM,IAAI,eAAe,UAAU,MAAM;AAE7C;;;;;;;;;;;;;;;;;;AAmBA,MAAM,kBAAkB,CAAC,KAAK;;;;;AAM9B,SAAS,mBAAmB,cAAmC;CAC7D,IAAI,CAAC,cAAc,QAAQ,OAAO;CAElC,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,cAAc,GAAG,eAAe,CAAC,CAAC;AAC3D;AAEA,MAAa,MAAM;;;;;CAKjB,MAAM,SACJ,SACA,EACE,MAAM,aAAa,GACnB,YAAY,aAAa,GACzB,GAAG,YACiC,CAAC,GACtB;EAKjB,OAAO,MAHM,aAAa;GAAE;GAAK,GAAG;GAAS;EAAU,CAEhC,EAAE;GAAE,GAAG;GAAS,WAAW;EAAkB,CAAC;CAEvE;;;;;;CAOA,MAAM,OACJ,OACA,EACE,MAAM,aAAa,GACnB,aAAa,CAAC,aAAa,CAAC,GAC5B,gBACA,GAAG,YACmC,CAAC,GAC7B;EAQZ,MAAM,UAAU,MAPD,eAAe;GAC5B;GACA,GAAG;GACH;GACA,gBAAgB,mBAAmB,cAAc;EACnD,CAE2B,EAAE,KAAe;EAE5C,gBAAgB,SAAS,iBAAiB;EAE1C,OAAO;CACT;;;;CAKA,MAAM,qBACJ,SACA,EACE,MAAM,oBAAoB,GAC1B,WACA,YAAY,aAAa,GACzB,GAAG,YACiC,CAAC,GACtB;EAEjB,OADa,aAAa;GAAE;GAAK;GAAW;GAAW,GAAG;EAAQ,CACxD,EAAE;GAAE,GAAG;GAAS,WAAW;EAAmB,CAAC;CAC3D;;;;CAKA,MAAM,mBACJ,OACA,EACE,MAAM,oBAAoB,GAC1B,aAAa,CAAC,aAAa,CAAC,GAC5B,gBACA,GAAG,YACmC,CAAC,GAC7B;EAQZ,MAAM,UAAU,MAPD,eAAe;GAC5B;GACA;GACA,GAAG;GACH,gBAAgB,mBAAmB,cAAc;EACnD,CAE2B,EAAE,KAAK;EAElC,gBAAgB,SAAS,kBAAkB;EAE3C,OAAO;CACT;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-expiry.mjs","names":[],"sources":["../../../../../../../auth/src/utils/token-expiry.ts"],"sourcesContent":["import { createDecoder } from \"fast-jwt\";\n\n/**\n * Payload decoder. Deliberately signature-*unaware*: these helpers answer\n * \"does this token carry a deadline at all\", which is a property of the claims,\n * not of the signature. Every caller that acts on the answer (the middleware,\n * the purge command) has already verified the signature or is about to delete\n * the row anyway.\n */\nconst decodePayload = createDecoder();\n\n/**\n * Whether a persisted `expires_at` can serve as a deadline at all.\n *\n * Answers `false` for absent, empty, and — the case that matters — `Invalid\n * Date`, which is what a pre-4.12.0 unparseable `expiresIn` wrote to the token\n * row. An `Invalid Date` compares `false` against *every* other date, so a row\n * holding one satisfies neither `expires_at < now` (cleanup never purges it)\n * nor `expires_at > now` (it never shows as an active session): it is outside\n * the reach of every date predicate rather than merely wrong.\n */\nexport function isUsableExpiry(value: unknown): boolean {\n if (value === undefined || value === null || value === \"\") return false;\n\n return !Number.isNaN(new Date(value as string | number | Date).getTime());\n}\n\n/**\n * Whether the JWT carries a finite numeric `exp` claim.\n *\n * This is the store-independent signal for a never-expiring credential: a\n * token with no `exp` has nothing for a verifier to check, so it verifies\n * indefinitely no matter what the row beside it says. An undecodable string\n * answers `false` — it cannot be shown to expire, and a row whose token cannot\n * even be parsed is unusable regardless.\n */\nexport function tokenHasExpClaim(token: unknown): boolean {\n if (typeof token !== \"string\" || token === \"\") return false;\n\n try {\n const payload = decodePayload(token) as { exp?: unknown } | null;\n\n return typeof payload?.exp === \"number\" && Number.isFinite(payload.exp);\n } catch {\n return false;\n }\n}\n\n/**\n * Whether a token row can ever stop being accepted on its own.\n *\n * Two independent ways to answer \"no\", either of which is enough:\n * - the persisted expiry is unusable, so no date check can ever retire the row;\n * - the token itself carries no `exp`, so signature verification never retires it.\n *\n * This is the predicate the `auth.purge-never-expiring` remediation selects on.\n */\nexport function isNeverExpiring(token: unknown, expiresAt: unknown): boolean {\n return !isUsableExpiry(expiresAt) || !tokenHasExpClaim(token);\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,gBAAgB,cAAc;;;;;;;;;;;AAYpC,SAAgB,eAAe,OAAyB;CACtD,IAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,IAAI,OAAO;CAElE,OAAO,CAAC,OAAO,MAAM,IAAI,KAAK,KAA+B,
|
|
1
|
+
{"version":3,"file":"token-expiry.mjs","names":[],"sources":["../../../../../../../auth/src/utils/token-expiry.ts"],"sourcesContent":["import { createDecoder } from \"fast-jwt\";\n\n/**\n * Payload decoder. Deliberately signature-*unaware*: these helpers answer\n * \"does this token carry a deadline at all\", which is a property of the claims,\n * not of the signature. Every caller that acts on the answer (the middleware,\n * the purge command) has already verified the signature or is about to delete\n * the row anyway.\n */\nconst decodePayload = createDecoder();\n\n/**\n * Whether a persisted `expires_at` can serve as a deadline at all.\n *\n * Answers `false` for absent, empty, and — the case that matters — `Invalid\n * Date`, which is what a pre-4.12.0 unparseable `expiresIn` wrote to the token\n * row. An `Invalid Date` compares `false` against *every* other date, so a row\n * holding one satisfies neither `expires_at < now` (cleanup never purges it)\n * nor `expires_at > now` (it never shows as an active session): it is outside\n * the reach of every date predicate rather than merely wrong.\n */\nexport function isUsableExpiry(value: unknown): boolean {\n if (value === undefined || value === null || value === \"\") return false;\n\n return !Number.isNaN(new Date(value as string | number | Date).getTime());\n}\n\n/**\n * Whether the JWT carries a finite numeric `exp` claim.\n *\n * This is the store-independent signal for a never-expiring credential: a\n * token with no `exp` has nothing for a verifier to check, so it verifies\n * indefinitely no matter what the row beside it says. An undecodable string\n * answers `false` — it cannot be shown to expire, and a row whose token cannot\n * even be parsed is unusable regardless.\n */\nexport function tokenHasExpClaim(token: unknown): boolean {\n if (typeof token !== \"string\" || token === \"\") return false;\n\n try {\n const payload = decodePayload(token) as { exp?: unknown } | null;\n\n return typeof payload?.exp === \"number\" && Number.isFinite(payload.exp);\n } catch {\n return false;\n }\n}\n\n/**\n * Whether a token row can ever stop being accepted on its own.\n *\n * Two independent ways to answer \"no\", either of which is enough:\n * - the persisted expiry is unusable, so no date check can ever retire the row;\n * - the token itself carries no `exp`, so signature verification never retires it.\n *\n * This is the predicate the `auth.purge-never-expiring` remediation selects on.\n */\nexport function isNeverExpiring(token: unknown, expiresAt: unknown): boolean {\n return !isUsableExpiry(expiresAt) || !tokenHasExpClaim(token);\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,gBAAgB,cAAc;;;;;;;;;;;AAYpC,SAAgB,eAAe,OAAyB;CACtD,IAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,IAAI,OAAO;CAElE,OAAO,CAAC,OAAO,MAAM,IAAI,KAAK,KAA+B,EAAE,QAAQ,CAAC;AAC1E;;;;;;;;;;AAWA,SAAgB,iBAAiB,OAAyB;CACxD,IAAI,OAAO,UAAU,YAAY,UAAU,IAAI,OAAO;CAEtD,IAAI;EACF,MAAM,UAAU,cAAc,KAAK;EAEnC,OAAO,OAAO,SAAS,QAAQ,YAAY,OAAO,SAAS,QAAQ,GAAG;CACxE,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;AAWA,SAAgB,gBAAgB,OAAgB,WAA6B;CAC3E,OAAO,CAAC,eAAe,SAAS,KAAK,CAAC,iBAAiB,KAAK;AAC9D"}
|
package/llms-full.txt
CHANGED
|
@@ -742,6 +742,8 @@ JWT authentication for Warlock apps. You get a base `Auth` model your user types
|
|
|
742
742
|
|
|
743
743
|
Coupled to `@warlock.js/core` — you're inside a Warlock project before this package makes sense.
|
|
744
744
|
|
|
745
|
+
Server-only: `package.json` declares `"warlock": { "environment": "server" }`. `@warlock.js/web`'s build refuses app client code that value-imports this package; type-only imports are allowed, and server loaders/controllers/modules are unaffected.
|
|
746
|
+
|
|
745
747
|
## When to reach for it
|
|
746
748
|
|
|
747
749
|
- You're building a Warlock app that needs login, protected routes, and token-based sessions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warlock.js/auth",
|
|
3
3
|
"description": "Authentication system for Warlock.js applications",
|
|
4
|
+
"warlock": {
|
|
5
|
+
"environment": "server"
|
|
6
|
+
},
|
|
4
7
|
"dependencies": {
|
|
5
8
|
"@mongez/copper": "^2.1.2",
|
|
6
9
|
"@mongez/events": "^2.2.7",
|
|
@@ -9,12 +12,12 @@
|
|
|
9
12
|
"ms": "^2.1.3"
|
|
10
13
|
},
|
|
11
14
|
"peerDependencies": {
|
|
12
|
-
"@warlock.js/fs": "5.
|
|
13
|
-
"@warlock.js/cache": "5.
|
|
14
|
-
"@warlock.js/cascade": "5.
|
|
15
|
-
"@warlock.js/core": "5.
|
|
16
|
-
"@warlock.js/logger": "5.
|
|
17
|
-
"@warlock.js/seal": "5.
|
|
15
|
+
"@warlock.js/fs": "5.2.2",
|
|
16
|
+
"@warlock.js/cache": "5.2.2",
|
|
17
|
+
"@warlock.js/cascade": "5.2.2",
|
|
18
|
+
"@warlock.js/core": "5.2.2",
|
|
19
|
+
"@warlock.js/logger": "5.2.2",
|
|
20
|
+
"@warlock.js/seal": "5.2.2"
|
|
18
21
|
},
|
|
19
22
|
"repository": {
|
|
20
23
|
"type": "git",
|
|
@@ -30,7 +33,7 @@
|
|
|
30
33
|
],
|
|
31
34
|
"author": "hassanzohdy",
|
|
32
35
|
"license": "MIT",
|
|
33
|
-
"version": "5.
|
|
36
|
+
"version": "5.2.2",
|
|
34
37
|
"type": "module",
|
|
35
38
|
"main": "./esm/index.mjs",
|
|
36
39
|
"module": "./esm/index.mjs",
|
package/skills/overview/SKILL.md
CHANGED
|
@@ -9,6 +9,8 @@ JWT authentication for Warlock apps. You get a base `Auth` model your user types
|
|
|
9
9
|
|
|
10
10
|
Coupled to `@warlock.js/core` — you're inside a Warlock project before this package makes sense.
|
|
11
11
|
|
|
12
|
+
Server-only: `package.json` declares `"warlock": { "environment": "server" }`. `@warlock.js/web`'s build refuses app client code that value-imports this package; type-only imports are allowed, and server loaders/controllers/modules are unaffected.
|
|
13
|
+
|
|
12
14
|
## When to reach for it
|
|
13
15
|
|
|
14
16
|
- You're building a Warlock app that needs login, protected routes, and token-based sessions.
|
|
@@ -31,7 +31,7 @@ export default defineConfig({
|
|
|
31
31
|
## `warlock jwt.generate` — JWT secret bootstrap
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
|
|
34
|
+
pnpm warlock jwt.generate
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Generates a cryptographically strong secret string and writes it to your `.env` as `JWT_SECRET=...` (and `JWT_REFRESH_SECRET=...` if refresh tokens are enabled).
|
|
@@ -43,7 +43,7 @@ Run it once when setting up a new project. Each developer typically runs it loca
|
|
|
43
43
|
## `warlock auth.cleanup` — expired token sweep
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
46
|
+
pnpm warlock auth.cleanup
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Runs `authService.cleanupExpiredTokens()` — deletes every refresh token whose `expires_at` has passed, then sweeps expired access-token rows too. Fires `token.expired` per refresh token and `cleanup.completed` once.
|
|
@@ -71,7 +71,7 @@ In-process — no shell call. See [`@warlock.js/scheduler/scheduler-basics/SKILL
|
|
|
71
71
|
### Via system cron
|
|
72
72
|
|
|
73
73
|
```cron
|
|
74
|
-
0 3 * * * cd /path/to/app && /usr/local/bin/
|
|
74
|
+
0 3 * * * cd /path/to/app && /usr/local/bin/pnpm warlock auth.cleanup
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
Out-of-process — works when you don't want the scheduler subsystem running in this service.
|
|
@@ -85,8 +85,8 @@ If you have very-short-lived refresh tokens (1h expiry) and a million-user scale
|
|
|
85
85
|
## `warlock auth.purge-never-expiring` — one-off remediation
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
pnpm warlock auth.purge-never-expiring --dry-run # report only
|
|
89
|
+
pnpm warlock auth.purge-never-expiring # report, then revoke
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
Register with `registerAuthPurgeNeverExpiringCommand()`.
|