@warlock.js/auth 4.8.2 โ 4.9.1
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/esm/commands/auth-cleanup-command.d.mts +1 -1
- package/esm/commands/auth-cleanup-command.d.mts.map +1 -1
- package/esm/commands/auth-cleanup-command.mjs +1 -1
- package/esm/commands/auth-cleanup-command.mjs.map +1 -1
- package/esm/commands/jwt-secret-generator-command.d.mts +1 -1
- package/esm/commands/jwt-secret-generator-command.d.mts.map +1 -1
- package/esm/commands/jwt-secret-generator-command.mjs +1 -1
- package/esm/commands/jwt-secret-generator-command.mjs.map +1 -1
- package/esm/contracts/auth-contract.d.mts +1 -1
- package/esm/contracts/auth-contract.d.mts.map +1 -1
- package/esm/contracts/types.d.mts +1 -1
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/middleware/auth.middleware.d.mts +1 -1
- package/esm/middleware/auth.middleware.d.mts.map +1 -1
- package/esm/middleware/auth.middleware.mjs +1 -1
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/login-throttle.middleware.d.mts +1 -1
- package/esm/middleware/login-throttle.middleware.d.mts.map +1 -1
- package/esm/middleware/login-throttle.middleware.mjs +1 -1
- package/esm/middleware/login-throttle.middleware.mjs.map +1 -1
- package/esm/models/access-token/access-token.model.d.mts +1 -1
- package/esm/models/access-token/access-token.model.d.mts.map +1 -1
- package/esm/models/access-token/access-token.model.mjs +1 -1
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/access-token/migration.mjs +1 -1
- package/esm/models/access-token/migration.mjs.map +1 -1
- package/esm/models/auth.model.d.mts +1 -1
- package/esm/models/auth.model.d.mts.map +1 -1
- package/esm/models/auth.model.mjs +1 -1
- package/esm/models/auth.model.mjs.map +1 -1
- package/esm/models/index.d.mts +1 -1
- package/esm/models/index.d.mts.map +1 -1
- package/esm/models/index.mjs +1 -1
- package/esm/models/index.mjs.map +1 -1
- package/esm/models/refresh-token/migration.mjs +1 -1
- package/esm/models/refresh-token/migration.mjs.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.d.mts +1 -1
- package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/services/auth-config.mjs +1 -1
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth-events.d.mts +1 -1
- package/esm/services/auth-events.d.mts.map +1 -1
- package/esm/services/auth-events.mjs +1 -1
- package/esm/services/auth-events.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +1 -1
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +1 -1
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/generate-jwt-secret.d.mts +1 -1
- package/esm/services/generate-jwt-secret.d.mts.map +1 -1
- package/esm/services/generate-jwt-secret.mjs +1 -1
- package/esm/services/generate-jwt-secret.mjs.map +1 -1
- package/esm/services/jwt.d.mts +1 -1
- package/esm/services/jwt.d.mts.map +1 -1
- package/esm/services/jwt.mjs +1 -1
- package/esm/services/jwt.mjs.map +1 -1
- package/esm/utils/auth-error-codes.d.mts +1 -1
- package/esm/utils/auth-error-codes.d.mts.map +1 -1
- package/esm/utils/auth-error-codes.mjs +1 -1
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/package.json +7 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-cleanup-command.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth-cleanup-command.d.mts","names":[],"sources":["../../../../../../../auth/src/commands/auth-cleanup-command.ts"],"mappings":";;AAYA;;;;AAA0C;;;iBAA1B,0BAAA"}
|
|
@@ -2,7 +2,7 @@ import { authService } from "../services/auth.service.mjs";
|
|
|
2
2
|
import { colors } from "@mongez/copper";
|
|
3
3
|
import { command } from "@warlock.js/core";
|
|
4
4
|
|
|
5
|
-
//#region
|
|
5
|
+
//#region ../auth/src/commands/auth-cleanup-command.ts
|
|
6
6
|
/**
|
|
7
7
|
* Register the auth:cleanup CLI command
|
|
8
8
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-cleanup-command.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth-cleanup-command.mjs","names":[],"sources":["../../../../../../../auth/src/commands/auth-cleanup-command.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { command } from \"@warlock.js/core\";\nimport { authService } from \"../services/auth.service\";\n\n/**\n * Register the auth:cleanup CLI command\n *\n * @example\n * ```bash\n * warlock auth:cleanup\n * ```\n */\nexport function registerAuthCleanupCommand() {\n return command({\n name: \"auth.cleanup\",\n description: \"Remove expired refresh tokens from the database\",\n preload: {\n env: true,\n config: [\"auth\", \"database\"],\n connectors: [\"database\"],\n },\n action: async () => {\n console.log(colors.cyan(\"๐งน Cleaning up expired tokens...\"));\n\n const count = await authService.cleanupExpiredTokens();\n\n if (count === 0) {\n console.log(colors.green(\"โ
No expired tokens found.\"));\n } else {\n console.log(colors.green(`โ
Removed ${count} expired token(s).`));\n }\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,6BAA6B;CAC3C,OAAO,QAAQ;EACb,MAAM;EACN,aAAa;EACb,SAAS;GACP,KAAK;GACL,QAAQ,CAAC,QAAQ,UAAU;GAC3B,YAAY,CAAC,UAAU;EACzB;EACA,QAAQ,YAAY;GAClB,QAAQ,IAAI,OAAO,KAAK,kCAAkC,CAAC;GAE3D,MAAM,QAAQ,MAAM,YAAY,qBAAqB;GAErD,IAAI,UAAU,GACZ,QAAQ,IAAI,OAAO,MAAM,4BAA4B,CAAC;QAEtD,QAAQ,IAAI,OAAO,MAAM,aAAa,MAAM,mBAAmB,CAAC;EAEpE;CACF,CAAC;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt-secret-generator-command.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"jwt-secret-generator-command.d.mts","names":[],"sources":["../../../../../../../auth/src/commands/jwt-secret-generator-command.ts"],"mappings":";iBAGgB,iCAAA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { generateJWTSecret } from "../services/generate-jwt-secret.mjs";
|
|
2
2
|
import { command } from "@warlock.js/core";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../auth/src/commands/jwt-secret-generator-command.ts
|
|
5
5
|
function registerJWTSecretGeneratorCommand() {
|
|
6
6
|
return command({
|
|
7
7
|
name: "jwt.generate",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt-secret-generator-command.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"jwt-secret-generator-command.mjs","names":[],"sources":["../../../../../../../auth/src/commands/jwt-secret-generator-command.ts"],"sourcesContent":["import { command } from \"@warlock.js/core\";\r\nimport { generateJWTSecret } from \"../services/generate-jwt-secret\";\r\n\r\nexport function registerJWTSecretGeneratorCommand() {\r\n return command({\r\n name: \"jwt.generate\",\r\n description: \"Generate JWT Secret key in .env file\",\r\n action: generateJWTSecret,\r\n });\r\n}\r\n"],"mappings":";;;;AAGA,SAAgB,oCAAoC;CAClD,OAAO,QAAQ;EACb,MAAM;EACN,aAAa;EACb,QAAQ;CACV,CAAC;AACH"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AccessTokenOutput, DeviceInfo, TokenPair } from "./types.mjs";
|
|
2
2
|
import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../auth/src/contracts/auth-contract.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Surface every authenticable user model exposes once it extends the
|
|
7
7
|
* `Auth` base. The `Auth` class implements this contract; subclasses only
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-contract.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth-contract.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/auth-contract.ts"],"mappings":";;;;;;AAWA;;;;;;UAAiB,aAAA;EAiBwC;;;;EAAA,IAZnD,QAAA;EAsB+B;;;;EAhBnC,mBAAA,CAAoB,OAAA,GAAU,MAAA,oBAA0B,OAAA,CAAQ,iBAAA;EAAlC;;;;EAM9B,oBAAA,CAAqB,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,YAAA;EAArB;;;EAKlC,eAAA,CAAgB,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,SAAA;EAAlD;;;EAKA,eAAA,CAAgB,QAAA,WAAmB,OAAA;AAAA"}
|
|
@@ -4,7 +4,7 @@ import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
|
|
|
4
4
|
import { ChildModel } from "@warlock.js/cascade";
|
|
5
5
|
import { Algorithm } from "fast-jwt";
|
|
6
6
|
|
|
7
|
-
//#region
|
|
7
|
+
//#region ../auth/src/contracts/types.d.ts
|
|
8
8
|
/**
|
|
9
9
|
* Symbol to indicate no expiration for tokens
|
|
10
10
|
* Use this when you explicitly want tokens to never expire
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"mappings":";;;;;;;;;;;AAyBA;;;;AAA0B;AAO1B;;;;AAAsC;AAQtC;;;;;cAfa,aAAA;;;;;;KAOD,0BAAA;AAmCZ;;;;;;AAAA,KA3BY,iBAAA;EA0CV;;;;;EApCA,KAAA,UAAe,WAAA;EAyDgC;AAAA;AASjD;EA9DE,MAAA;;;;;EAKA,SAAA,GAAY,SAAS;EA4DrB;;;;;EAtDA,SAAA;AAAA;;;;KAMU,kBAAA;EA2DA;;;;EAtDV,KAAA,UAAe,YAAA;EAiED;;;;EA5Dd,OAAA;EAsDA;;;;EAjDA,MAAA;EAuDc;;;;;EAjDd,SAAA;EAyDqB;AAGvB;;;EAvDE,QAAA;EA+DS;AAMX;;;EAhEE,UAAA;EAoEA;;;;EA/DA,kBAAA,GAAqB,0BAA0B;AAAA;AAyEjD;;;;;;AAAA,KAhEY,eAAA;EACV,MAAA;EACA,SAAA,GAAY,SAAA;EACZ,SAAA;EACA,OAAA;IACE,MAAA;IACA,OAAA;IACA,SAAA;IACA,QAAA;IACA,UAAA;IACA,kBAAA,GAAqB,0BAA0B;EAAA;AAAA;AAAA,KAIvC,kBAAA;EA0EY;;;;EArEtB,QAAA;IAAA,CACG,QAAA,WAAmB,UAAA,CAAW,IAAA;EAAA;EAsEhB;;;EAjEjB,WAAA,GAAc,iBAAA;;;;EAId,YAAA,GAAe,kBAAA;;;;EAIf,GAAA,GAAM,eAAA;AAAA;AAAA,KAGI,iBAAA;;;;EAIV,KAAA;;;;EAIA,SAAS;AAAA;;;;KAMC,SAAA;;;;EAIV,WAAA,EAAa,iBAAA;;;;EAIb,YAAA,GAAe,iBAAiB;AAAA;;;;KAMtB,UAAA;;;;EAIV,SAAA;;;;EAIA,EAAA;;;;EAIA,QAAA;;;;;EAKA,QAAA;;;;EAIA,OAAA,GAAU,MAAM;AAAA;AAAA,KAGN,WAAA,kBAA6B,IAAA;EACvC,IAAA,EAAM,QAAA;EACN,MAAA,EAAQ,SAAA;AAAA"}
|
package/esm/contracts/types.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"sourcesContent":["import { type ChildModel } from \"@warlock.js/cascade\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport type { AccessToken } from \"../models/access-token\";\r\nimport type { Auth } from \"../models/auth.model\";\r\nimport type { RefreshToken } from \"../models/refresh-token\";\r\n\r\n/**\r\n * Symbol to indicate no expiration for tokens\r\n * Use this when you explicitly want tokens to never expire\r\n *\r\n * @example\r\n * ```typescript\r\n * // src/config/auth.ts\r\n * import { NO_EXPIRATION, type AuthConfigurations } from \"@warlock.js/auth\";\r\n *\r\n * const authConfigurations: AuthConfigurations = {\r\n * accessToken: {\r\n * secret: env(\"JWT_SECRET\"),\r\n * expiresIn: NO_EXPIRATION, // Token expires within 100 years\r\n * },\r\n * };\r\n *\r\n * export default authConfigurations;\r\n * ```\r\n */\r\nexport const NO_EXPIRATION = \"100y\";\r\n\r\n/**\r\n * Behavior when logout is called without a refresh token\r\n * - \"revoke-all\": Revoke all refresh tokens for the user (secure default)\r\n * - \"error\": Return an error requiring the refresh token\r\n */\r\nexport type LogoutWithoutTokenBehavior = \"revoke-all\" | \"error\";\r\n\r\n/**\r\n * Access-token configuration.\r\n *\r\n * `secret` may also be supplied via the deprecated `jwt.secret` (resolved by a\r\n * backward-compatible shim); one of the two is required at runtime.\r\n */\r\nexport type AccessTokenConfig = {\r\n /**\r\n * Override the persisted access-token model โ extend {@link AccessToken} to\r\n * add columns (e.g. a tenant key) or rename them, then register the subclass\r\n * here. Defaults to the package's `AccessToken`.\r\n */\r\n model?: typeof AccessToken;\r\n /**\r\n * Secret used to sign access tokens.\r\n */\r\n secret?: string;\r\n /**\r\n * JWT algorithm.\r\n * @default \"HS256\"\r\n */\r\n algorithm?: Algorithm;\r\n /**\r\n * Access-token lifetime โ any value the `ms` package accepts.\r\n * @example \"1h\" or NO_EXPIRATION\r\n * @default \"1h\"\r\n */\r\n expiresIn?: string;\r\n};\r\n\r\n/**\r\n * Refresh-token configuration.\r\n */\r\nexport type RefreshTokenConfig = {\r\n /**\r\n * Override the persisted refresh-token model โ extend {@link RefreshToken}.\r\n * Defaults to the package's `RefreshToken`.\r\n */\r\n model?: typeof RefreshToken;\r\n /**\r\n * Enable refresh tokens.\r\n * @default true\r\n */\r\n enabled?: boolean;\r\n /**\r\n * Separate secret for refresh tokens (recommended for security). Falls back\r\n * to the access-token secret when omitted.\r\n */\r\n secret?: string;\r\n /**\r\n * Refresh-token lifetime โ any value the `ms` package accepts.\r\n * @example \"7d\" or \"1w\"\r\n * @default \"7d\"\r\n */\r\n expiresIn?: string;\r\n /**\r\n * Rotate the refresh token on each use (old token revoked, replay detected).\r\n * @default true\r\n */\r\n rotation?: boolean;\r\n /**\r\n * Maximum active refresh tokens per user; the oldest are revoked past it.\r\n * @default 5\r\n */\r\n maxPerUser?: number;\r\n /**\r\n * Behavior when logout is called without a refresh token.\r\n * @default \"revoke-all\"\r\n */\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n};\r\n\r\n/**\r\n * Legacy JWT configuration shape. Still honored by the resolver shim with a\r\n * deprecation warning.\r\n *\r\n * @deprecated Use the top-level `accessToken` / `refreshToken` blocks.\r\n */\r\nexport type LegacyJwtConfig = {\r\n secret?: string;\r\n algorithm?: Algorithm;\r\n expiresIn?: string;\r\n refresh?: {\r\n secret?: string;\r\n enabled?: boolean;\r\n expiresIn?: string;\r\n rotation?: boolean;\r\n maxPerUser?: number;\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n };\r\n};\r\n\r\nexport type AuthConfigurations = {\r\n /**\r\n * Define all user types โ maps a user-type slug to its `Auth` model class so\r\n * tokens and the middleware can resolve the right model.\r\n */\r\n userType: {\r\n [userType: string]: ChildModel<Auth>;\r\n };\r\n /**\r\n * Access-token configuration (secret, expiry, algorithm, model override).\r\n */\r\n accessToken?: AccessTokenConfig;\r\n /**\r\n * Refresh-token configuration (rotation, lifetime, cap, model override).\r\n */\r\n refreshToken?: RefreshTokenConfig;\r\n /**\r\n * @deprecated Use `accessToken` / `refreshToken`. Read via a backward-compatible shim.\r\n */\r\n jwt?: LegacyJwtConfig;\r\n};\r\n\r\nexport type AccessTokenOutput = {\r\n /**\r\n * JWT Token\r\n */\r\n token: string;\r\n /**\r\n * Exprie time in ISO format UTC time\r\n */\r\n expiresAt: string;\r\n};\r\n\r\n/**\r\n * Token pair returned after login or token refresh\r\n */\r\nexport type TokenPair = {\r\n /**\r\n * JWT access token (short-lived)\r\n */\r\n accessToken: AccessTokenOutput;\r\n /**\r\n * JWT refresh token (long-lived)\r\n */\r\n refreshToken?: AccessTokenOutput;\r\n};\r\n\r\n/**\r\n * Device information for session tracking\r\n */\r\nexport type DeviceInfo = {\r\n /**\r\n * User agent string from request\r\n */\r\n userAgent?: string;\r\n /**\r\n * Client IP address\r\n */\r\n ip?: string;\r\n /**\r\n * Optional device identifier\r\n */\r\n deviceId?: string;\r\n /**\r\n * Token family ID (for rotation tracking)\r\n * @internal\r\n */\r\n familyId?: string;\r\n /**\r\n * Access token payload\r\n */\r\n payload?: Record<string, any>;\r\n};\r\n\r\nexport type LoginResult<UserType extends Auth> = {\r\n user: UserType;\r\n tokens: TokenPair;\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,gBAAgB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.middleware.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth.middleware.d.mts","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"mappings":";;AAgCA;;;;AAAiE;;;;;;;;;;;;iBAAjD,cAAA,CAAe,eAAA,sBAAkC,UAAA"}
|
|
@@ -5,7 +5,7 @@ import { AuthErrorCodes } from "../utils/auth-error-codes.mjs";
|
|
|
5
5
|
import { config, t } from "@warlock.js/core";
|
|
6
6
|
import { log } from "@warlock.js/logger";
|
|
7
7
|
|
|
8
|
-
//#region
|
|
8
|
+
//#region ../auth/src/middleware/auth.middleware.ts
|
|
9
9
|
/**
|
|
10
10
|
* Build a route gate that always requires an authenticated request.
|
|
11
11
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.middleware.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"sourcesContent":["import { config, t, type Middleware, type Request, type Response } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { AccessToken } from \"../models/access-token\";\r\nimport { jwt } from \"../services/jwt\";\r\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\r\n\r\n/**\r\n * Decoded access-token claims the middleware reads. The full payload carries\r\n * more (`created_at`, `tokenType`, `iat`, `exp`) but only these drive routing.\r\n */\r\ntype DecodedAccessToken = {\r\n id: string | number;\r\n userType?: string;\r\n};\r\n\r\n/**\r\n * Build a route gate that always requires an authenticated request.\r\n *\r\n * The argument is mandatory and selects which user types may pass:\r\n * - `[]` โ any authenticated user (token required, type not checked).\r\n * - `\"admin\"` / `[\"admin\", \"staff\"]` โ token required AND the user's\r\n * `userType` must be one of the listed types.\r\n *\r\n * There is no anonymous/optional mode: a request without a valid access\r\n * token is always rejected with `401`. Routes that should be public\r\n * simply omit the middleware.\r\n *\r\n * @example\r\n * router.get(\"/account\", authMiddleware([]), accountController);\r\n * router.get(\"/admin\", authMiddleware(\"admin\"), adminController);\r\n * router.get(\"/back-office\", authMiddleware([\"admin\", \"staff\"]), backOfficeController);\r\n */\r\nexport function authMiddleware(allowedUserType: string | string[]) {\r\n const allowedTypes = Array.isArray(allowedUserType) ? allowedUserType : [allowedUserType];\r\n\r\n const auth: Middleware = async (request: Request, response: Response) => {\r\n try {\r\n const authorizationValue = request.authorizationValue;\r\n\r\n if (!authorizationValue) {\r\n return response.unauthorized({\r\n error: t(\"auth.errors.missingAccessToken\"),\r\n errorCode: AuthErrorCodes.MissingAccessToken,\r\n });\r\n }\r\n\r\n const decoded = await jwt.verify<DecodedAccessToken>(authorizationValue);\r\n\r\n request.decodedAccessToken = decoded;\r\n\r\n // A valid signature is not enough โ the token must still exist in storage,\r\n // so deleting the row (logout) invalidates it before its JWT expiry.\r\n const AccessTokenModel = config.key(\"auth.accessToken.model\", AccessToken);\r\n const accessToken = await AccessTokenModel.findByToken(authorizationValue);\r\n\r\n if (!accessToken) {\r\n return response.unauthorized({\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n const userType = decoded.userType ?? accessToken.userType;\r\n\r\n if (allowedTypes.length && !allowedTypes.includes(userType)) {\r\n return response.unauthorized({\r\n error: t(\"auth.errors.unauthorized\"),\r\n errorCode: AuthErrorCodes.Unauthorized,\r\n });\r\n }\r\n\r\n const UserModel = config.key(`auth.userType.${userType}`);\r\n\r\n if (!UserModel) {\r\n throw new Error(`User type ${userType} is unknown type.`);\r\n }\r\n\r\n const currentUser = await UserModel.find(decoded.id);\r\n\r\n if (!currentUser) {\r\n await accessToken.destroy();\r\n\r\n return response.unauthorized({\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n request.user = currentUser;\r\n } catch (error: any) {\r\n log.error(\"http\", \"auth\", error);\r\n\r\n request.clearCurrentUser();\r\n\r\n return response.unauthorized({\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n };\r\n\r\n return auth;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,eAAe,iBAAoC;CACjE,MAAM,eAAe,MAAM,QAAQ,eAAe,IAAI,kBAAkB,CAAC,eAAe;CAExF,MAAM,OAAmB,OAAO,SAAkB,aAAuB;EACvE,IAAI;GACF,MAAM,qBAAqB,QAAQ;GAEnC,IAAI,CAAC,oBACH,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;GAGH,MAAM,UAAU,MAAM,IAAI,OAA2B,kBAAkB;GAEvE,QAAQ,qBAAqB;GAK7B,MAAM,cAAc,MADK,OAAO,IAAI,0BAA0B,WACrB,CAAC,CAAC,YAAY,kBAAkB;GAEzE,IAAI,CAAC,aACH,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;GAGH,MAAM,WAAW,QAAQ,YAAY,YAAY;GAEjD,IAAI,aAAa,UAAU,CAAC,aAAa,SAAS,QAAQ,GACxD,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,0BAA0B;IACnC;GACF,CAAC;GAGH,MAAM,YAAY,OAAO,IAAI,iBAAiB,UAAU;GAExD,IAAI,CAAC,WACH,MAAM,IAAI,MAAM,aAAa,SAAS,kBAAkB;GAG1D,MAAM,cAAc,MAAM,UAAU,KAAK,QAAQ,EAAE;GAEnD,IAAI,CAAC,aAAa;IAChB,MAAM,YAAY,QAAQ;IAE1B,OAAO,SAAS,aAAa;KAC3B,OAAO,EAAE,gCAAgC;KACzC;IACF,CAAC;GACH;GAEA,QAAQ,OAAO;EACjB,SAAS,OAAY;GACnB,IAAI,MAAM,QAAQ,QAAQ,KAAK;GAE/B,QAAQ,iBAAiB;GAEzB,OAAO,SAAS,aAAa;IAC3B,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;CACF;CAEA,OAAO;AACT"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Middleware, Request, Response } from "@warlock.js/core";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../auth/src/middleware/login-throttle.middleware.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Built-in identifier kinds the throttle knows how to extract from a request.
|
|
6
6
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-throttle.middleware.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"login-throttle.middleware.d.mts","names":[],"sources":["../../../../../../../auth/src/middleware/login-throttle.middleware.ts"],"mappings":";;;;;AAeA;;;;AAAmC;AAYnC;;;KAZY,uBAAA;;;;;;;;;;;KAYA,oBAAA;EAkDV;;;;;EA5CA,GAAA;EAmD4B;AAAA;AA6E9B;;;EAzHE,MAAA;EAyH+C;;;;AAAsC;EAlHrF,eAAA;;;;;;EAOA,EAAA,GAAK,uBAAA;;;;;;;;EASL,aAAA;;;;EAKA,YAAA;;;;;;;;EASA,SAAA,IAAa,QAAA,EAAU,QAAA;;;;;;EAOvB,QAAA,IAAY,OAAA,EAAS,OAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6EP,uBAAA,CAAwB,OAAA,GAAS,oBAAA,GAA4B,UAAU"}
|
|
@@ -3,7 +3,7 @@ import { t } from "@warlock.js/core";
|
|
|
3
3
|
import { log } from "@warlock.js/logger";
|
|
4
4
|
import { cache } from "@warlock.js/cache";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../auth/src/middleware/login-throttle.middleware.ts
|
|
7
7
|
const COUNTER_PREFIX = "auth.throttle.count.";
|
|
8
8
|
const LOCK_PREFIX = "auth.throttle.lock.";
|
|
9
9
|
const counterKey = (identifier) => `${COUNTER_PREFIX}${identifier}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-throttle.middleware.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/middleware/login-throttle.middleware.ts"],"sourcesContent":["import { cache } from \"@warlock.js/cache\";\r\nimport { t, type Middleware, type Request, type Response } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\r\n\r\n/**\r\n * Built-in identifier kinds the throttle knows how to extract from a request.\r\n *\r\n * - `\"email\"` tracks per-account, so an attacker can't spray one user from\r\n * many IPs.\r\n * - `\"ip\"` tracks per-source, so an attacker can't stuff many accounts from\r\n * one IP.\r\n *\r\n * Lockout trips when ANY tracked identifier crosses `max` โ defense-in-depth.\r\n */\r\nexport type LoginThrottleIdentifier = \"email\" | \"ip\";\r\n\r\n/**\r\n * Options for {@link loginThrottleMiddleware}.\r\n *\r\n * Failure-aware by design: only counts requests whose response is non-2xx\r\n * (or whatever `isFailure` says) and resets on success, so a user who types\r\n * a wrong password three times then the right one is not penalised.\r\n *\r\n * @example\r\n * loginThrottleMiddleware({ max: 5, window: \"15m\", lockoutDuration: \"15m\" });\r\n */\r\nexport type LoginThrottleOptions = {\r\n /**\r\n * Failures allowed within the window before the identifier is locked.\r\n *\r\n * @default 5\r\n */\r\n max?: number;\r\n\r\n /**\r\n * Counting window. `ms`-format string (`\"15m\"`, `\"1h\"`) or seconds as a number.\r\n *\r\n * @default \"15m\"\r\n */\r\n window?: string | number;\r\n\r\n /**\r\n * Lockout duration once the threshold is tripped. `ms`-format string or seconds.\r\n *\r\n * @default \"15m\"\r\n */\r\n lockoutDuration?: string | number;\r\n\r\n /**\r\n * Identifiers tracked. Each is counted and locked independently.\r\n *\r\n * @default [\"email\", \"ip\"]\r\n */\r\n by?: LoginThrottleIdentifier[];\r\n\r\n /**\r\n * Credential field consulted when `\"email\"` is tracked. Switch to\r\n * `\"username\"` (or any other input field) for systems that don't\r\n * authenticate by email.\r\n *\r\n * @default \"email\"\r\n */\r\n identifierKey?: string;\r\n\r\n /**\r\n * Override the default error message returned with the 429 response.\r\n */\r\n errorMessage?: string;\r\n\r\n /**\r\n * Decide whether the response represents a failed login. Defaults to\r\n * \"non-2xx is a failure\"; override if your controller signals failure\r\n * through a 200-style body.\r\n *\r\n * @default (response) => !response.isOk\r\n */\r\n isFailure?: (response: Response) => boolean;\r\n\r\n /**\r\n * Compute the identifiers tracked for this request. Override to derive the\r\n * account key from a header, a JWT subject, or any other shape that does\r\n * not match the built-in `email` / `ip` extraction.\r\n */\r\n identify?: (request: Request) => string[];\r\n};\r\n\r\nconst COUNTER_PREFIX = \"auth.throttle.count.\";\r\nconst LOCK_PREFIX = \"auth.throttle.lock.\";\r\n\r\nconst counterKey = (identifier: string): string => `${COUNTER_PREFIX}${identifier}`;\r\nconst lockKey = (identifier: string): string => `${LOCK_PREFIX}${identifier}`;\r\n\r\n/**\r\n * Built-in identifier extractor โ reads the configured credential field from\r\n * the request body for the per-account key, and `request.detectIp()` for the\r\n * per-source key. Untruthy values are dropped; a request that yields zero\r\n * identifiers is left untracked so the middleware no-ops for it.\r\n */\r\nfunction buildDefaultIdentifiers(\r\n request: Request,\r\n tracked: LoginThrottleIdentifier[],\r\n credentialField: string,\r\n): string[] {\r\n const identifiers: string[] = [];\r\n\r\n if (tracked.includes(\"email\")) {\r\n const value = request.input(credentialField);\r\n\r\n if (typeof value === \"string\" && value.length > 0) {\r\n identifiers.push(`email.${value.toLowerCase()}`);\r\n }\r\n }\r\n\r\n if (tracked.includes(\"ip\")) {\r\n const ip = request.detectIp();\r\n\r\n if (ip) {\r\n identifiers.push(`ip.${ip}`);\r\n }\r\n }\r\n\r\n return identifiers;\r\n}\r\n\r\n/**\r\n * Failure-aware login throttle. Locks an identifier (account, IP, or both)\r\n * after too many failed login attempts within a window โ without penalising\r\n * a user who eventually types the right password.\r\n *\r\n * Mechanism (per identifier):\r\n *\r\n * 1. **Before the controller** โ reject with `429` if a lock key is set,\r\n * skipping the DB lookup AND the bcrypt verify (this also neutralises the\r\n * CPU-DoS angle of brute-forcing).\r\n * 2. **After the controller** โ observe the outcome via `response.onSent`.\r\n * On failure (non-2xx by default), bump a fixed-window counter; once it\r\n * reaches `max`, set a lock key with `lockoutDuration` TTL. On success,\r\n * clear both the counter and the lock for that identifier.\r\n *\r\n * Storage is `@warlock.js/cache` (a peer dependency of `@warlock.js/auth`,\r\n * transitively guaranteed via `@warlock.js/core`). Lockout state is shared\r\n * across replicas โ correct security model, because an attacker can't bypass\r\n * by round-robin to a different pod. Every cache call is guarded so a\r\n * misconfigured driver fails open (a throttle outage must never escalate\r\n * into a full auth outage).\r\n *\r\n * @example\r\n * import { loginThrottleMiddleware } from \"@warlock.js/auth\";\r\n * import { router } from \"@warlock.js/core\";\r\n *\r\n * router.post(\"/auth/login\", loginController, {\r\n * middleware: [loginThrottleMiddleware({ max: 5, window: \"15m\" })],\r\n * });\r\n *\r\n * @example\r\n * // Per-IP only โ avoid account-lockout-as-DoS on anonymous-attack vectors\r\n * router.post(\"/auth/refresh-token\", refreshController, {\r\n * middleware: [loginThrottleMiddleware({ by: [\"ip\"], max: 10, window: \"1m\" })],\r\n * });\r\n */\r\nexport function loginThrottleMiddleware(options: LoginThrottleOptions = {}): Middleware {\r\n const max = options.max ?? 5;\r\n const window = options.window ?? \"15m\";\r\n const lockoutDuration = options.lockoutDuration ?? \"15m\";\r\n const tracked = options.by ?? [\"email\", \"ip\"];\r\n const credentialField = options.identifierKey ?? \"email\";\r\n\r\n return async (request: Request, response: Response) => {\r\n const identifiers = options.identify\r\n ? options.identify(request)\r\n : buildDefaultIdentifiers(request, tracked, credentialField);\r\n\r\n if (identifiers.length === 0) return;\r\n\r\n try {\r\n for (const identifier of identifiers) {\r\n const locked = await cache.get(lockKey(identifier));\r\n\r\n if (locked) {\r\n return response.tooManyRequests({\r\n error: options.errorMessage ?? t(\"auth.errors.tooManyAttempts\"),\r\n errorCode: AuthErrorCodes.TooManyAttempts,\r\n });\r\n }\r\n }\r\n } catch (error) {\r\n // Fail open โ a cache outage (driver not initialised, network blip)\r\n // must never escalate into a full auth outage. Log and let the request\r\n // through; the onSent recorder below is independently guarded so an\r\n // outage there is also harmless.\r\n log.error(\"auth\", \"login-throttle\", error);\r\n\r\n return;\r\n }\r\n\r\n response.onSent(async (resolvedResponse: Response) => {\r\n try {\r\n const failed = options.isFailure\r\n ? options.isFailure(resolvedResponse)\r\n : !resolvedResponse.isOk;\r\n\r\n if (!failed) {\r\n for (const identifier of identifiers) {\r\n await cache.remove(counterKey(identifier));\r\n await cache.remove(lockKey(identifier));\r\n }\r\n\r\n return;\r\n }\r\n\r\n for (const identifier of identifiers) {\r\n const existing = await cache.get(counterKey(identifier));\r\n const ttlOption = existing === null ? { ttl: window } : undefined;\r\n const count = await cache.update<number>(\r\n counterKey(identifier),\r\n (current) => (current ?? 0) + 1,\r\n ttlOption,\r\n );\r\n\r\n if ((count ?? 0) >= max) {\r\n await cache.set(lockKey(identifier), true, { ttl: lockoutDuration });\r\n }\r\n }\r\n } catch (error) {\r\n log.error(\"auth\", \"login-throttle\", error);\r\n }\r\n });\r\n };\r\n}\r\n"],"mappings":";;;;;;AAuFA,MAAM,iBAAiB;AACvB,MAAM,cAAc;AAEpB,MAAM,cAAc,eAA+B,GAAG,iBAAiB;AACvE,MAAM,WAAW,eAA+B,GAAG,cAAc;;;;;;;AAQjE,SAAS,wBACP,SACA,SACA,iBACU;CACV,MAAM,cAAwB,CAAC;CAE/B,IAAI,QAAQ,SAAS,OAAO,GAAG;EAC7B,MAAM,QAAQ,QAAQ,MAAM,eAAe;EAE3C,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAC9C,YAAY,KAAK,SAAS,MAAM,YAAY,GAAG;CAEnD;CAEA,IAAI,QAAQ,SAAS,IAAI,GAAG;EAC1B,MAAM,KAAK,QAAQ,SAAS;EAE5B,IAAI,IACF,YAAY,KAAK,MAAM,IAAI;CAE/B;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,wBAAwB,UAAgC,CAAC,GAAe;CACtF,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,kBAAkB,QAAQ,mBAAmB;CACnD,MAAM,UAAU,QAAQ,MAAM,CAAC,SAAS,IAAI;CAC5C,MAAM,kBAAkB,QAAQ,iBAAiB;CAEjD,OAAO,OAAO,SAAkB,aAAuB;EACrD,MAAM,cAAc,QAAQ,WACxB,QAAQ,SAAS,OAAO,IACxB,wBAAwB,SAAS,SAAS,eAAe;EAE7D,IAAI,YAAY,WAAW,GAAG;EAE9B,IAAI;GACF,KAAK,MAAM,cAAc,aAGvB,IAAI,MAFiB,MAAM,IAAI,QAAQ,UAAU,CAAC,GAGhD,OAAO,SAAS,gBAAgB;IAC9B,OAAO,QAAQ,gBAAgB,EAAE,6BAA6B;IAC9D;GACF,CAAC;EAGP,SAAS,OAAO;GAKd,IAAI,MAAM,QAAQ,kBAAkB,KAAK;GAEzC;EACF;EAEA,SAAS,OAAO,OAAO,qBAA+B;GACpD,IAAI;IAKF,IAAI,EAJW,QAAQ,YACnB,QAAQ,UAAU,gBAAgB,IAClC,CAAC,iBAAiB,OAET;KACX,KAAK,MAAM,cAAc,aAAa;MACpC,MAAM,MAAM,OAAO,WAAW,UAAU,CAAC;MACzC,MAAM,MAAM,OAAO,QAAQ,UAAU,CAAC;KACxC;KAEA;IACF;IAEA,KAAK,MAAM,cAAc,aAAa;KAEpC,MAAM,YAAY,MADK,MAAM,IAAI,WAAW,UAAU,CAAC,MACxB,OAAO,EAAE,KAAK,OAAO,IAAI;KAOxD,KAAK,MANe,MAAM,OACxB,WAAW,UAAU,IACpB,aAAa,WAAW,KAAK,GAC9B,SACF,KAEc,MAAM,KAClB,MAAM,MAAM,IAAI,QAAQ,UAAU,GAAG,MAAM,EAAE,KAAK,gBAAgB,CAAC;IAEvE;GACF,SAAS,OAAO;IACd,IAAI,MAAM,QAAQ,kBAAkB,KAAK;GAC3C;EACF,CAAC;CACH;AACF"}
|
|
1
|
+
{"version":3,"file":"login-throttle.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/login-throttle.middleware.ts"],"sourcesContent":["import { cache } from \"@warlock.js/cache\";\r\nimport { t, type Middleware, type Request, type Response } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\r\n\r\n/**\r\n * Built-in identifier kinds the throttle knows how to extract from a request.\r\n *\r\n * - `\"email\"` tracks per-account, so an attacker can't spray one user from\r\n * many IPs.\r\n * - `\"ip\"` tracks per-source, so an attacker can't stuff many accounts from\r\n * one IP.\r\n *\r\n * Lockout trips when ANY tracked identifier crosses `max` โ defense-in-depth.\r\n */\r\nexport type LoginThrottleIdentifier = \"email\" | \"ip\";\r\n\r\n/**\r\n * Options for {@link loginThrottleMiddleware}.\r\n *\r\n * Failure-aware by design: only counts requests whose response is non-2xx\r\n * (or whatever `isFailure` says) and resets on success, so a user who types\r\n * a wrong password three times then the right one is not penalised.\r\n *\r\n * @example\r\n * loginThrottleMiddleware({ max: 5, window: \"15m\", lockoutDuration: \"15m\" });\r\n */\r\nexport type LoginThrottleOptions = {\r\n /**\r\n * Failures allowed within the window before the identifier is locked.\r\n *\r\n * @default 5\r\n */\r\n max?: number;\r\n\r\n /**\r\n * Counting window. `ms`-format string (`\"15m\"`, `\"1h\"`) or seconds as a number.\r\n *\r\n * @default \"15m\"\r\n */\r\n window?: string | number;\r\n\r\n /**\r\n * Lockout duration once the threshold is tripped. `ms`-format string or seconds.\r\n *\r\n * @default \"15m\"\r\n */\r\n lockoutDuration?: string | number;\r\n\r\n /**\r\n * Identifiers tracked. Each is counted and locked independently.\r\n *\r\n * @default [\"email\", \"ip\"]\r\n */\r\n by?: LoginThrottleIdentifier[];\r\n\r\n /**\r\n * Credential field consulted when `\"email\"` is tracked. Switch to\r\n * `\"username\"` (or any other input field) for systems that don't\r\n * authenticate by email.\r\n *\r\n * @default \"email\"\r\n */\r\n identifierKey?: string;\r\n\r\n /**\r\n * Override the default error message returned with the 429 response.\r\n */\r\n errorMessage?: string;\r\n\r\n /**\r\n * Decide whether the response represents a failed login. Defaults to\r\n * \"non-2xx is a failure\"; override if your controller signals failure\r\n * through a 200-style body.\r\n *\r\n * @default (response) => !response.isOk\r\n */\r\n isFailure?: (response: Response) => boolean;\r\n\r\n /**\r\n * Compute the identifiers tracked for this request. Override to derive the\r\n * account key from a header, a JWT subject, or any other shape that does\r\n * not match the built-in `email` / `ip` extraction.\r\n */\r\n identify?: (request: Request) => string[];\r\n};\r\n\r\nconst COUNTER_PREFIX = \"auth.throttle.count.\";\r\nconst LOCK_PREFIX = \"auth.throttle.lock.\";\r\n\r\nconst counterKey = (identifier: string): string => `${COUNTER_PREFIX}${identifier}`;\r\nconst lockKey = (identifier: string): string => `${LOCK_PREFIX}${identifier}`;\r\n\r\n/**\r\n * Built-in identifier extractor โ reads the configured credential field from\r\n * the request body for the per-account key, and `request.detectIp()` for the\r\n * per-source key. Untruthy values are dropped; a request that yields zero\r\n * identifiers is left untracked so the middleware no-ops for it.\r\n */\r\nfunction buildDefaultIdentifiers(\r\n request: Request,\r\n tracked: LoginThrottleIdentifier[],\r\n credentialField: string,\r\n): string[] {\r\n const identifiers: string[] = [];\r\n\r\n if (tracked.includes(\"email\")) {\r\n const value = request.input(credentialField);\r\n\r\n if (typeof value === \"string\" && value.length > 0) {\r\n identifiers.push(`email.${value.toLowerCase()}`);\r\n }\r\n }\r\n\r\n if (tracked.includes(\"ip\")) {\r\n const ip = request.detectIp();\r\n\r\n if (ip) {\r\n identifiers.push(`ip.${ip}`);\r\n }\r\n }\r\n\r\n return identifiers;\r\n}\r\n\r\n/**\r\n * Failure-aware login throttle. Locks an identifier (account, IP, or both)\r\n * after too many failed login attempts within a window โ without penalising\r\n * a user who eventually types the right password.\r\n *\r\n * Mechanism (per identifier):\r\n *\r\n * 1. **Before the controller** โ reject with `429` if a lock key is set,\r\n * skipping the DB lookup AND the bcrypt verify (this also neutralises the\r\n * CPU-DoS angle of brute-forcing).\r\n * 2. **After the controller** โ observe the outcome via `response.onSent`.\r\n * On failure (non-2xx by default), bump a fixed-window counter; once it\r\n * reaches `max`, set a lock key with `lockoutDuration` TTL. On success,\r\n * clear both the counter and the lock for that identifier.\r\n *\r\n * Storage is `@warlock.js/cache` (a peer dependency of `@warlock.js/auth`,\r\n * transitively guaranteed via `@warlock.js/core`). Lockout state is shared\r\n * across replicas โ correct security model, because an attacker can't bypass\r\n * by round-robin to a different pod. Every cache call is guarded so a\r\n * misconfigured driver fails open (a throttle outage must never escalate\r\n * into a full auth outage).\r\n *\r\n * @example\r\n * import { loginThrottleMiddleware } from \"@warlock.js/auth\";\r\n * import { router } from \"@warlock.js/core\";\r\n *\r\n * router.post(\"/auth/login\", loginController, {\r\n * middleware: [loginThrottleMiddleware({ max: 5, window: \"15m\" })],\r\n * });\r\n *\r\n * @example\r\n * // Per-IP only โ avoid account-lockout-as-DoS on anonymous-attack vectors\r\n * router.post(\"/auth/refresh-token\", refreshController, {\r\n * middleware: [loginThrottleMiddleware({ by: [\"ip\"], max: 10, window: \"1m\" })],\r\n * });\r\n */\r\nexport function loginThrottleMiddleware(options: LoginThrottleOptions = {}): Middleware {\r\n const max = options.max ?? 5;\r\n const window = options.window ?? \"15m\";\r\n const lockoutDuration = options.lockoutDuration ?? \"15m\";\r\n const tracked = options.by ?? [\"email\", \"ip\"];\r\n const credentialField = options.identifierKey ?? \"email\";\r\n\r\n return async (request: Request, response: Response) => {\r\n const identifiers = options.identify\r\n ? options.identify(request)\r\n : buildDefaultIdentifiers(request, tracked, credentialField);\r\n\r\n if (identifiers.length === 0) return;\r\n\r\n try {\r\n for (const identifier of identifiers) {\r\n const locked = await cache.get(lockKey(identifier));\r\n\r\n if (locked) {\r\n return response.tooManyRequests({\r\n error: options.errorMessage ?? t(\"auth.errors.tooManyAttempts\"),\r\n errorCode: AuthErrorCodes.TooManyAttempts,\r\n });\r\n }\r\n }\r\n } catch (error) {\r\n // Fail open โ a cache outage (driver not initialised, network blip)\r\n // must never escalate into a full auth outage. Log and let the request\r\n // through; the onSent recorder below is independently guarded so an\r\n // outage there is also harmless.\r\n log.error(\"auth\", \"login-throttle\", error);\r\n\r\n return;\r\n }\r\n\r\n response.onSent(async (resolvedResponse: Response) => {\r\n try {\r\n const failed = options.isFailure\r\n ? options.isFailure(resolvedResponse)\r\n : !resolvedResponse.isOk;\r\n\r\n if (!failed) {\r\n for (const identifier of identifiers) {\r\n await cache.remove(counterKey(identifier));\r\n await cache.remove(lockKey(identifier));\r\n }\r\n\r\n return;\r\n }\r\n\r\n for (const identifier of identifiers) {\r\n const existing = await cache.get(counterKey(identifier));\r\n const ttlOption = existing === null ? { ttl: window } : undefined;\r\n const count = await cache.update<number>(\r\n counterKey(identifier),\r\n (current) => (current ?? 0) + 1,\r\n ttlOption,\r\n );\r\n\r\n if ((count ?? 0) >= max) {\r\n await cache.set(lockKey(identifier), true, { ttl: lockoutDuration });\r\n }\r\n }\r\n } catch (error) {\r\n log.error(\"auth\", \"login-throttle\", error);\r\n }\r\n });\r\n };\r\n}\r\n"],"mappings":";;;;;;AAuFA,MAAM,iBAAiB;AACvB,MAAM,cAAc;AAEpB,MAAM,cAAc,eAA+B,GAAG,iBAAiB;AACvE,MAAM,WAAW,eAA+B,GAAG,cAAc;;;;;;;AAQjE,SAAS,wBACP,SACA,SACA,iBACU;CACV,MAAM,cAAwB,CAAC;CAE/B,IAAI,QAAQ,SAAS,OAAO,GAAG;EAC7B,MAAM,QAAQ,QAAQ,MAAM,eAAe;EAE3C,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAC9C,YAAY,KAAK,SAAS,MAAM,YAAY,GAAG;CAEnD;CAEA,IAAI,QAAQ,SAAS,IAAI,GAAG;EAC1B,MAAM,KAAK,QAAQ,SAAS;EAE5B,IAAI,IACF,YAAY,KAAK,MAAM,IAAI;CAE/B;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,wBAAwB,UAAgC,CAAC,GAAe;CACtF,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,kBAAkB,QAAQ,mBAAmB;CACnD,MAAM,UAAU,QAAQ,MAAM,CAAC,SAAS,IAAI;CAC5C,MAAM,kBAAkB,QAAQ,iBAAiB;CAEjD,OAAO,OAAO,SAAkB,aAAuB;EACrD,MAAM,cAAc,QAAQ,WACxB,QAAQ,SAAS,OAAO,IACxB,wBAAwB,SAAS,SAAS,eAAe;EAE7D,IAAI,YAAY,WAAW,GAAG;EAE9B,IAAI;GACF,KAAK,MAAM,cAAc,aAGvB,IAAI,MAFiB,MAAM,IAAI,QAAQ,UAAU,CAAC,GAGhD,OAAO,SAAS,gBAAgB;IAC9B,OAAO,QAAQ,gBAAgB,EAAE,6BAA6B;IAC9D;GACF,CAAC;EAGP,SAAS,OAAO;GAKd,IAAI,MAAM,QAAQ,kBAAkB,KAAK;GAEzC;EACF;EAEA,SAAS,OAAO,OAAO,qBAA+B;GACpD,IAAI;IAKF,IAAI,EAJW,QAAQ,YACnB,QAAQ,UAAU,gBAAgB,IAClC,CAAC,iBAAiB,OAET;KACX,KAAK,MAAM,cAAc,aAAa;MACpC,MAAM,MAAM,OAAO,WAAW,UAAU,CAAC;MACzC,MAAM,MAAM,OAAO,QAAQ,UAAU,CAAC;KACxC;KAEA;IACF;IAEA,KAAK,MAAM,cAAc,aAAa;KAEpC,MAAM,YAAY,MADK,MAAM,IAAI,WAAW,UAAU,CAAC,MACxB,OAAO,EAAE,KAAK,OAAO,IAAI;KAOxD,KAAK,MANe,MAAM,OACxB,WAAW,UAAU,IACpB,aAAa,WAAW,KAAK,GAC9B,SACF,KAEc,MAAM,KAClB,MAAM,MAAM,IAAI,QAAQ,UAAU,GAAG,MAAM,EAAE,KAAK,gBAAgB,CAAC;IAEvE;GACF,SAAS,OAAO;IACd,IAAI,MAAM,QAAQ,kBAAkB,KAAK;GAC3C;EACF,CAAC;CACH;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Auth } from "../auth.model.mjs";
|
|
2
2
|
import { Model } from "@warlock.js/cascade";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../auth/src/models/access-token/access-token.model.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Seal schema for the persisted access-token record. Exported so an override
|
|
7
7
|
* can spread it and add columns (e.g. a tenant key) without re-declaring the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-token.model.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"access-token.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"mappings":";;;;;;AAYA;;;;AAKE;AAYF;cAjBa,iBAAA;;;;;;;;;;;cAiBA,WAAA,SAAoB,KAAA;EAAA,OACjB,KAAA;EAAA,OAEA,MAAA;EAAA;EAAA,IAGH,MAAA;EAKA;EAAA,IAAA,QAAA;EAOe;;;EAAA,OAAZ,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,UAAe,SAAA,EAAW,IAAA;EAAX;;;EAAA,OAYjC,WAAA,CAAY,KAAA,WAAgB,OAAA,CAAQ,WAAA;EAAA;;;EAAA,OAOpC,aAAA,CAAc,IAAA,EAAM,IAAA,EAAM,KAAA;EAAA;;;EAAA,OAO1B,gBAAA,CAAiB,IAAA,EAAM,IAAA;EAQjB;;;AAAuB;EAAvB,OAAA,YAAA,IAAgB,OAAA;AAAA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Model } from "@warlock.js/cascade";
|
|
2
2
|
import { v } from "@warlock.js/seal";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../auth/src/models/access-token/access-token.model.ts
|
|
5
5
|
/**
|
|
6
6
|
* Seal schema for the persisted access-token record. Exported so an override
|
|
7
7
|
* can spread it and add columns (e.g. a tenant key) without re-declaring the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-token.model.mjs","names":[],"sources":["
|
|
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 type { Auth } from \"../auth.model\";\r\n\r\n/**\r\n * Seal schema for the persisted access-token record. Exported so an override\r\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\r\n * base shape.\r\n *\r\n * `last_access` and the `is_active` soft-revoke flag were removed โ neither was\r\n * ever read; access tokens are revoked by deleting the row.\r\n */\r\nexport const accessTokenSchema = v.object({\r\n token: v.string().required(),\r\n user_id: v.scalar().required(),\r\n user_type: v.string().required(),\r\n expires_at: v.date().required(),\r\n});\r\n\r\n/**\r\n * Persisted access-token record + the data layer for access tokens.\r\n *\r\n * **Role.** Owns access-token persistence and lookup. The middleware checks a\r\n * presented JWT against this table so deleting a row (logout) invalidates the\r\n * token immediately, before its JWT expiry. The auth service goes through the\r\n * named statics exclusively, so it never hard-codes a column name and an\r\n * override can rename/add columns by registering under\r\n * `config.auth.accessToken.model`.\r\n */\r\nexport class AccessToken extends Model {\r\n public static table = \"access_tokens\";\r\n\r\n public static schema = accessTokenSchema;\r\n\r\n /** The user this token was issued for. */\r\n public get userId() {\r\n return this.get(\"user_id\");\r\n }\r\n\r\n /** The user-type slug this token was issued for. */\r\n public get userType(): string {\r\n return this.get(\"user_type\");\r\n }\r\n\r\n /**\r\n * Persist a freshly-signed access token for the user.\r\n */\r\n public static issue(user: Auth, token: string, expiresAt: Date) {\r\n return this.create({\r\n token,\r\n user_id: user.id,\r\n user_type: user.userType,\r\n expires_at: expiresAt,\r\n });\r\n }\r\n\r\n /**\r\n * Find an access-token row by its raw token string.\r\n */\r\n public static findByToken(token: string): Promise<AccessToken | null> {\r\n return this.first({ token });\r\n }\r\n\r\n /**\r\n * Delete a specific token that belongs to the given user.\r\n */\r\n public static deleteForUser(user: Auth, token: string) {\r\n return this.delete({ token, user_id: user.id });\r\n }\r\n\r\n /**\r\n * Delete every access token belonging to the user.\r\n */\r\n public static deleteAllForUser(user: Auth) {\r\n return this.delete({ user_id: user.id });\r\n }\r\n\r\n /**\r\n * Hard-delete every expired access-token row. Returns the number removed.\r\n * Runs from the `auth.cleanup` CLI command (a cold batch path).\r\n */\r\n public static async purgeExpired(): Promise<number> {\r\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\r\n\r\n for (const token of expiredTokens) {\r\n await token.destroy();\r\n }\r\n\r\n return expiredTokens.length;\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;AAYA,MAAa,oBAAoB,EAAE,OAAO;CACxC,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,WAAW,EAAE,OAAO,CAAC,CAAC,SAAS;CAC/B,YAAY,EAAE,KAAK,CAAC,CAAC,SAAS;AAChC,CAAC;;;;;;;;;;;AAYD,IAAa,cAAb,cAAiC,MAAM;;eACf;;;gBAEC;;;CAGvB,IAAW,SAAS;EAClB,OAAO,KAAK,IAAI,SAAS;CAC3B;;CAGA,IAAW,WAAmB;EAC5B,OAAO,KAAK,IAAI,WAAW;CAC7B;;;;CAKA,OAAc,MAAM,MAAY,OAAe,WAAiB;EAC9D,OAAO,KAAK,OAAO;GACjB;GACA,SAAS,KAAK;GACd,WAAW,KAAK;GAChB,YAAY;EACd,CAAC;CACH;;;;CAKA,OAAc,YAAY,OAA4C;EACpE,OAAO,KAAK,MAAM,EAAE,MAAM,CAAC;CAC7B;;;;CAKA,OAAc,cAAc,MAAY,OAAe;EACrD,OAAO,KAAK,OAAO;GAAE;GAAO,SAAS,KAAK;EAAG,CAAC;CAChD;;;;CAKA,OAAc,iBAAiB,MAAY;EACzC,OAAO,KAAK,OAAO,EAAE,SAAS,KAAK,GAAG,CAAC;CACzC;;;;;CAMA,aAAoB,eAAgC;EAClD,MAAM,gBAAgB,MAAM,KAAK,MAAM,CAAC,CAAC,MAAM,cAAc,qBAAK,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI;EAElF,KAAK,MAAM,SAAS,eAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO,cAAc;CACvB;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AccessToken } from "./access-token.model.mjs";
|
|
2
2
|
import { migrate } from "@warlock.js/cascade";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../auth/src/models/access-token/migration.ts
|
|
5
5
|
const AccessTokenMigration = migrate(AccessToken, {
|
|
6
6
|
name: "accessToken",
|
|
7
7
|
up() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration.mjs","names":[],"sources":["
|
|
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,CAAC,CAAC,OAAO;EAG1B,KAAK,KAAK,SAAS,CAAC,CAAC,MAAM;EAC3B,KAAK,OAAO,aAAa,EAAE,CAAC,CAAC,SAAS;EAGtC,KAAK,UAAU,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EAG9C,KAAK,WAAW;CAClB;CACA,OAAO;EACL,KAAK,kBAAkB;CACzB;AACF,CAAC"}
|
|
@@ -3,7 +3,7 @@ import { RefreshToken } from "./refresh-token/refresh-token.model.mjs";
|
|
|
3
3
|
import { Authenticable } from "../contracts/auth-contract.mjs";
|
|
4
4
|
import { ChildModel, Model, ModelSchema } from "@warlock.js/cascade";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../auth/src/models/auth.model.d.ts
|
|
7
7
|
declare abstract class Auth<Schema extends ModelSchema = ModelSchema> extends Model<Schema> implements Authenticable {
|
|
8
8
|
/**
|
|
9
9
|
* Get user type
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.model.d.mts","names":[],"sources":["
|
|
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;;;;;EAqBwC;;;EAdlC,eAAA,CAAgB,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,SAAA;EA4Bf;;;EArBnC,mBAAA,CAAoB,IAAA,SAAa,OAAA,CAAQ,iBAAA;EA0CvB;;;EAnClB,oBAAA,CAAqB,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,YAAA;EA0CJ;;;EAnCnD,iBAAA,CAAkB,KAAA,WAAgB,OAAA;EAtCvB;;;EA6CX,kBAAA,CAAmB,KAAA,WAAgB,OAAA;EA/CM;;;EAsDzC,qBAAA,IAAyB,OAAA;EA/ClB;;;EAsDP,eAAA,IAAmB,OAAA;;;;EAOnB,cAAA,IAAkB,OAAA,CAAQ,YAAA;EAjDgB;;;EAAA,OAwDnC,OAAA,CAAQ,IAAA,EAAM,UAAA,CAAW,IAAA,GAAO,IAAA,QAAY,OAAA,CAAQ,IAAA;EAjD1B;;;EAwDjC,eAAA,CAAgB,QAAA,WAAmB,OAAA;AAAA"}
|
|
@@ -2,7 +2,7 @@ import { authService } from "../services/auth.service.mjs";
|
|
|
2
2
|
import "../services/index.mjs";
|
|
3
3
|
import { Model } from "@warlock.js/cascade";
|
|
4
4
|
|
|
5
|
-
//#region
|
|
5
|
+
//#region ../auth/src/models/auth.model.ts
|
|
6
6
|
var Auth = class extends Model {
|
|
7
7
|
/**
|
|
8
8
|
* Get access token payload
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.model.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth.model.mjs","names":[],"sources":["../../../../../../../auth/src/models/auth.model.ts"],"sourcesContent":["import { type ChildModel, Model, type ModelSchema } from \"@warlock.js/cascade\";\r\nimport type { Authenticable } from \"../contracts/auth-contract\";\r\nimport type { AccessTokenOutput, DeviceInfo, TokenPair } from \"../contracts/types\";\r\nimport { authService } from \"../services\";\r\nimport type { RefreshToken } from \"./refresh-token/refresh-token.model\";\r\n\r\nexport abstract class Auth<Schema extends ModelSchema = ModelSchema>\r\n extends Model<Schema>\r\n implements Authenticable\r\n{\r\n /**\r\n * Get user type\r\n */\r\n public abstract get userType(): string;\r\n\r\n /**\r\n * Get access token payload\r\n */\r\n public accessTokenPayload() {\r\n return authService.buildAccessTokenPayload(this);\r\n }\r\n\r\n /**\r\n * Create both access and refresh tokens\r\n */\r\n public async createTokenPair(deviceInfo?: DeviceInfo): Promise<TokenPair> {\r\n return authService.createTokenPair(this, deviceInfo);\r\n }\r\n\r\n /**\r\n * Generate access token\r\n */\r\n public async generateAccessToken(data?: any): Promise<AccessTokenOutput> {\r\n return authService.generateAccessToken(this, data);\r\n }\r\n\r\n /**\r\n * Generate refresh token\r\n */\r\n public async generateRefreshToken(deviceInfo?: DeviceInfo): Promise<RefreshToken | undefined> {\r\n return authService.createRefreshToken(this, deviceInfo);\r\n }\r\n\r\n /**\r\n * Remove current access token\r\n */\r\n public async removeAccessToken(token: string): Promise<void> {\r\n return authService.removeAccessToken(this, token);\r\n }\r\n\r\n /**\r\n * Remove refresh token\r\n */\r\n public async removeRefreshToken(token: string): Promise<void> {\r\n return authService.removeRefreshToken(this, token);\r\n }\r\n\r\n /**\r\n * Remove all access tokens\r\n */\r\n public async removeAllAccessTokens(): Promise<void> {\r\n return authService.removeAllAccessTokens(this);\r\n }\r\n\r\n /**\r\n * Revoke all tokens (logout from all devices)\r\n */\r\n public async revokeAllTokens(): Promise<void> {\r\n return authService.revokeAllTokens(this);\r\n }\r\n\r\n /**\r\n * Get active sessions\r\n */\r\n public async activeSessions(): Promise<RefreshToken[]> {\r\n return authService.getActiveSessions(this);\r\n }\r\n\r\n /**\r\n * Attempt to login the user\r\n */\r\n public static async attempt(this: ChildModel<Auth>, data: any): Promise<Auth | null> {\r\n return authService.attemptLogin(this, data);\r\n }\r\n\r\n /**\r\n * Confirm password\r\n */\r\n public async confirmPassword(password: string): Promise<boolean> {\r\n return authService.verifyPassword(password, this.string(\"password\")!);\r\n }\r\n}\r\n"],"mappings":";;;;;AAMA,IAAsB,OAAtB,cACU,MAEV;;;;CASE,AAAO,qBAAqB;EAC1B,OAAO,YAAY,wBAAwB,IAAI;CACjD;;;;CAKA,MAAa,gBAAgB,YAA6C;EACxE,OAAO,YAAY,gBAAgB,MAAM,UAAU;CACrD;;;;CAKA,MAAa,oBAAoB,MAAwC;EACvE,OAAO,YAAY,oBAAoB,MAAM,IAAI;CACnD;;;;CAKA,MAAa,qBAAqB,YAA4D;EAC5F,OAAO,YAAY,mBAAmB,MAAM,UAAU;CACxD;;;;CAKA,MAAa,kBAAkB,OAA8B;EAC3D,OAAO,YAAY,kBAAkB,MAAM,KAAK;CAClD;;;;CAKA,MAAa,mBAAmB,OAA8B;EAC5D,OAAO,YAAY,mBAAmB,MAAM,KAAK;CACnD;;;;CAKA,MAAa,wBAAuC;EAClD,OAAO,YAAY,sBAAsB,IAAI;CAC/C;;;;CAKA,MAAa,kBAAiC;EAC5C,OAAO,YAAY,gBAAgB,IAAI;CACzC;;;;CAKA,MAAa,iBAA0C;EACrD,OAAO,YAAY,kBAAkB,IAAI;CAC3C;;;;CAKA,aAAoB,QAAgC,MAAiC;EACnF,OAAO,YAAY,aAAa,MAAM,IAAI;CAC5C;;;;CAKA,MAAa,gBAAgB,UAAoC;EAC/D,OAAO,YAAY,eAAe,UAAU,KAAK,OAAO,UAAU,CAAE;CACtE;AACF"}
|
package/esm/models/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import { Auth } from "./auth.model.mjs";
|
|
|
2
2
|
import { AccessToken, accessTokenSchema } from "./access-token/access-token.model.mjs";
|
|
3
3
|
import { RefreshToken, RefreshTokenIssueOptions, refreshTokenSchema } from "./refresh-token/refresh-token.model.mjs";
|
|
4
4
|
|
|
5
|
-
//#region
|
|
5
|
+
//#region ../auth/src/models/index.d.ts
|
|
6
6
|
declare const authMigrations: any[];
|
|
7
7
|
//#endregion
|
|
8
8
|
export { authMigrations };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../../../../../auth/src/models/index.ts"],"mappings":";;;;;cAOa,cAAA"}
|
package/esm/models/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { AccessTokenMigration } from "./access-token/migration.mjs";
|
|
|
6
6
|
import { RefreshTokenMigration } from "./refresh-token/migration.mjs";
|
|
7
7
|
import { Auth } from "./auth.model.mjs";
|
|
8
8
|
|
|
9
|
-
//#region
|
|
9
|
+
//#region ../auth/src/models/index.ts
|
|
10
10
|
const authMigrations = [AccessTokenMigration, RefreshTokenMigration];
|
|
11
11
|
|
|
12
12
|
//#endregion
|
package/esm/models/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../auth/src/models/index.ts"],"sourcesContent":["import { AccessTokenMigration } from \"./access-token/migration\";\r\nimport { RefreshTokenMigration } from \"./refresh-token/migration\";\r\n\r\nexport * from \"./access-token\";\r\nexport * from \"./auth.model\";\r\nexport * from \"./refresh-token\";\r\n\r\nexport const authMigrations = [AccessTokenMigration, RefreshTokenMigration];\r\n"],"mappings":";;;;;;;;;AAOA,MAAa,iBAAiB,CAAC,sBAAsB,qBAAqB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RefreshToken } from "./refresh-token.model.mjs";
|
|
2
2
|
import { migrate } from "@warlock.js/cascade";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../auth/src/models/refresh-token/migration.ts
|
|
5
5
|
const RefreshTokenMigration = migrate(RefreshToken, {
|
|
6
6
|
name: "refreshToken",
|
|
7
7
|
up() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration.mjs","names":[],"sources":["
|
|
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,CAAC,CAAC,OAAO;EAC1B,KAAK,KAAK,SAAS,CAAC,CAAC,MAAM;EAC3B,KAAK,OAAO,aAAa,EAAE,CAAC,CAAC,SAAS;EACtC,KAAK,KAAK,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EACxC,KAAK,UAAU,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EAC9C,KAAK,UAAU,cAAc,CAAC,CAAC,SAAS;EACxC,KAAK,UAAU,YAAY,CAAC,CAAC,SAAS;EACtC,KAAK,KAAK,aAAa,CAAC,CAAC,SAAS;EAIlC,KAAK,MAAM,CAAC,WAAW,WAAW,CAAC;EAGnC,KAAK,WAAW;CAClB;CACA,OAAO;EACL,KAAK,kBAAkB;CACzB;AACF,CAAC"}
|
|
@@ -2,7 +2,7 @@ import { Auth } from "../auth.model.mjs";
|
|
|
2
2
|
import { DeviceInfo } from "../../contracts/types.mjs";
|
|
3
3
|
import { Model } from "@warlock.js/cascade";
|
|
4
4
|
|
|
5
|
-
//#region
|
|
5
|
+
//#region ../auth/src/models/refresh-token/refresh-token.model.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Seal schema for the persisted refresh-token record. Exported so an override
|
|
8
8
|
* can spread it and add columns (e.g. a tenant key) without re-declaring the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh-token.model.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"refresh-token.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/refresh-token/refresh-token.model.ts"],"mappings":";;;;;;;AAiBA;;;;AASE;AAKF;;;;;cAda,kBAAA;;;;KAcD,wBAAA;EACV,QAAA;EACA,SAAA;EACA,UAAA,GAAa,UAAU;AAAA;;;;;;;;;;;cAaZ,YAAA,SAAqB,KAAA;EAAA,OAClB,KAAA;EAAA,OAEA,MAAA;EAuHyB;EAAA,IApH5B,QAAA;EAoHmC;EAAA,IA/GnC,SAAA;EA+HyC;EAAA,IAtHzC,SAAA;EAqI8C;EAAA,IAhI9C,OAAA;EAoJyB;;;EA7IvB,MAAA,IAAU,OAAA;EAhCS;;;;;;;EA2CnB,cAAA,IAAkB,OAAA;EAXR;;;EAyBV,UAAA,IAAc,OAAA;EAAA;;;EAAA,OAOb,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,UAAe,OAAA,EAAS,wBAAA;EAAxB;;;EAAA,OAoBlB,WAAA,CAAY,KAAA,WAAgB,OAAA,CAAQ,YAAA;EAAxB;;;;EAAA,OAQZ,WAAA,CAAY,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA,CAAQ,YAAA;EAApC;;;EAAA,OAOZ,aAAA,CAAc,IAAA,EAAM,IAAA,EAAM,KAAA;EAA1B;;;EAAA,OAOA,SAAA,CAAU,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;EAA/B;;;;;;;;EAAA,OAgBM,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;EAAA;;;;EAAA,OAgBlC,YAAA,CAAa,QAAA,WAAmB,OAAA,CAAQ,YAAA;EAexC;;;;;EAAA,OAAA,UAAA,CAAW,IAAA,EAAM,IAAA,EAAM,GAAA,WAAc,OAAA;EAoBrB;;;AAAoB;;EAApB,OAAhB,YAAA,IAAgB,OAAA,CAAQ,YAAA;AAAA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Model } from "@warlock.js/cascade";
|
|
2
2
|
import { v } from "@warlock.js/seal";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../auth/src/models/refresh-token/refresh-token.model.ts
|
|
5
5
|
/**
|
|
6
6
|
* Seal schema for the persisted refresh-token record. Exported so an override
|
|
7
7
|
* can spread it and add columns (e.g. a tenant key) without re-declaring the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh-token.model.mjs","names":[],"sources":["../../../../../../../../@warlock.js/auth/src/models/refresh-token/refresh-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport type { DeviceInfo } from \"../../contracts/types\";\r\nimport type { Auth } from \"../auth.model\";\r\n\r\n/**\r\n * Seal schema for the persisted refresh-token record. Exported so an override\r\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\r\n * base shape:\r\n *\r\n * @example\r\n * export class AppRefreshToken extends RefreshToken {\r\n * public static schema = refreshTokenSchema.extend({\r\n * organization_id: v.string().required(),\r\n * });\r\n * }\r\n */\r\nexport const refreshTokenSchema = v.object({\r\n token: v.string().required(),\r\n user_id: v.scalar().required(),\r\n user_type: v.string().required(),\r\n family_id: v.string().required(),\r\n expires_at: v.date().required(),\r\n last_used_at: v.date().default(() => new Date()),\r\n revoked_at: v.date().optional(),\r\n device_info: v.record(v.any()).optional(),\r\n});\r\n\r\n/**\r\n * Extra attributes captured when a refresh token is issued.\r\n */\r\nexport type RefreshTokenIssueOptions = {\r\n familyId: string;\r\n expiresAt: string;\r\n deviceInfo?: DeviceInfo;\r\n};\r\n\r\n/**\r\n * Persisted refresh-token record + the data layer for refresh tokens.\r\n *\r\n * **Role.** Owns every refresh-token read, write, and lifecycle transition.\r\n * The auth service drives refresh-token state exclusively through this model's\r\n * named statics and instance methods, so it never hard-codes a column name โ\r\n * which is what makes a snake/camel mismatch (the historical `userId` bug)\r\n * structurally impossible and lets an override rename or add columns by\r\n * extending this class and registering it under `config.auth.refreshToken.model`.\r\n */\r\nexport class RefreshToken extends Model {\r\n public static table = \"refresh_tokens\";\r\n\r\n public static schema = refreshTokenSchema;\r\n\r\n /** Token family this row belongs to (rotation / replay grouping). */\r\n public get familyId(): string {\r\n return this.get(\"family_id\");\r\n }\r\n\r\n /** Whether the token's `expires_at` is in the past. */\r\n public get isExpired(): boolean {\r\n const expiresAt = this.get(\"expires_at\");\r\n\r\n if (!expiresAt) return false;\r\n\r\n return new Date() > new Date(expiresAt);\r\n }\r\n\r\n /** Whether the token has been revoked. */\r\n public get isRevoked(): boolean {\r\n return !!this.get(\"revoked_at\");\r\n }\r\n\r\n /** Whether the token is still usable (not expired and not revoked). */\r\n public get isValid(): boolean {\r\n return !this.isExpired && !this.isRevoked;\r\n }\r\n\r\n /**\r\n * Unconditionally stamp `revoked_at` on this token.\r\n */\r\n public async revoke(): Promise<this> {\r\n return this.merge({ revoked_at: new Date() }).save();\r\n }\r\n\r\n /**\r\n * Atomically revoke this token ONLY if it is still active. Resolves to `true`\r\n * when this call performed the revoke, `false` when a concurrent request had\r\n * already revoked it โ the win/lose signal that powers rotation replay\r\n * detection. Uses a conditional UPDATE so two concurrent rotations of the\r\n * same token can never both succeed.\r\n */\r\n public async revokeIfActive(): Promise<boolean> {\r\n const modelClass = this.constructor as typeof RefreshToken;\r\n\r\n const revokedCount = await modelClass.atomic(\r\n { id: this.id, revoked_at: null },\r\n { $set: { revoked_at: new Date() } },\r\n );\r\n\r\n return revokedCount > 0;\r\n }\r\n\r\n /**\r\n * Touch `last_used_at` without revoking โ the non-rotating refresh path.\r\n */\r\n public async markAsUsed(): Promise<void> {\r\n await this.merge({ last_used_at: new Date() }).save();\r\n }\r\n\r\n /**\r\n * Persist a freshly-signed refresh token for the user.\r\n */\r\n public static issue(user: Auth, token: string, options: RefreshTokenIssueOptions) {\r\n return this.create({\r\n token,\r\n user_id: user.id,\r\n user_type: user.userType,\r\n family_id: options.familyId,\r\n expires_at: options.expiresAt,\r\n device_info: options.deviceInfo\r\n ? {\r\n userAgent: options.deviceInfo.userAgent,\r\n ip: options.deviceInfo.ip,\r\n deviceId: options.deviceInfo.deviceId,\r\n }\r\n : undefined,\r\n });\r\n }\r\n\r\n /**\r\n * Find a refresh-token row by its raw token string.\r\n */\r\n public static findByToken(token: string): Promise<RefreshToken | null> {\r\n return this.first({ token });\r\n }\r\n\r\n /**\r\n * Find a refresh token scoped to a user โ used by logout so a caller can only\r\n * revoke a token that actually belongs to them.\r\n */\r\n public static findForUser(user: Auth, token: string): Promise<RefreshToken | null> {\r\n return this.first({ token, user_id: user.id });\r\n }\r\n\r\n /**\r\n * Delete a specific refresh token belonging to the user.\r\n */\r\n public static deleteForUser(user: Auth, token: string) {\r\n return this.delete({ token, user_id: user.id });\r\n }\r\n\r\n /**\r\n * Active, unexpired sessions for the user, newest first.\r\n */\r\n public static activeFor(user: Auth): Promise<RefreshToken[]> {\r\n return this.query()\r\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\r\n .where(\"expires_at\", \">\", new Date())\r\n .orderBy(\"created_at\", \"desc\")\r\n .get();\r\n }\r\n\r\n /**\r\n * Revoke every still-active refresh token for the user, returning the rows\r\n * that were revoked so the caller can emit a per-token event for each.\r\n *\r\n * Rows are fetched BEFORE they are revoked: a bulk `findAndUpdate` keyed on\r\n * `revoked_at: null` would re-query the same predicate after the update and\r\n * match nothing, returning an empty set.\r\n */\r\n public static async revokeAllFor(user: Auth): Promise<RefreshToken[]> {\r\n const tokens = await this.query()\r\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\r\n .get();\r\n\r\n for (const token of tokens) {\r\n await token.revoke();\r\n }\r\n\r\n return tokens;\r\n }\r\n\r\n /**\r\n * Revoke every still-active token in a family (rotation breach containment),\r\n * returning the revoked rows (fetched before revocation โ see `revokeAllFor`).\r\n */\r\n public static async revokeFamily(familyId: string): Promise<RefreshToken[]> {\r\n const tokens = await this.query().where({ family_id: familyId, revoked_at: null }).get();\r\n\r\n for (const token of tokens) {\r\n await token.revoke();\r\n }\r\n\r\n return tokens;\r\n }\r\n\r\n /**\r\n * Revoke the oldest active tokens so at most `max - 1` remain โ making room\r\n * for the about-to-be-issued one. Bounded by `max`, so the per-row loop is\r\n * small.\r\n */\r\n public static async enforceMax(user: Auth, max: number): Promise<void> {\r\n const activeTokens = await this.query()\r\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\r\n .orderBy(\"created_at\", \"asc\")\r\n .get();\r\n\r\n if (activeTokens.length < max) return;\r\n\r\n const tokensToRevoke = activeTokens.slice(0, activeTokens.length - max + 1);\r\n\r\n for (const token of tokensToRevoke) {\r\n await token.revoke();\r\n }\r\n }\r\n\r\n /**\r\n * Hard-delete every expired refresh token, returning the deleted rows so the\r\n * caller can emit a per-token event. Runs from the `auth.cleanup` CLI command\r\n * (a cold batch path).\r\n */\r\n public static async purgeExpired(): Promise<RefreshToken[]> {\r\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\r\n\r\n for (const token of expiredTokens) {\r\n await token.destroy();\r\n }\r\n\r\n return expiredTokens;\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"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 type { Auth } from \"../auth.model\";\r\n\r\n/**\r\n * Seal schema for the persisted refresh-token record. Exported so an override\r\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\r\n * base shape:\r\n *\r\n * @example\r\n * export class AppRefreshToken extends RefreshToken {\r\n * public static schema = refreshTokenSchema.extend({\r\n * organization_id: v.string().required(),\r\n * });\r\n * }\r\n */\r\nexport const refreshTokenSchema = v.object({\r\n token: v.string().required(),\r\n user_id: v.scalar().required(),\r\n user_type: v.string().required(),\r\n family_id: v.string().required(),\r\n expires_at: v.date().required(),\r\n last_used_at: v.date().default(() => new Date()),\r\n revoked_at: v.date().optional(),\r\n device_info: v.record(v.any()).optional(),\r\n});\r\n\r\n/**\r\n * Extra attributes captured when a refresh token is issued.\r\n */\r\nexport type RefreshTokenIssueOptions = {\r\n familyId: string;\r\n expiresAt: string;\r\n deviceInfo?: DeviceInfo;\r\n};\r\n\r\n/**\r\n * Persisted refresh-token record + the data layer for refresh tokens.\r\n *\r\n * **Role.** Owns every refresh-token read, write, and lifecycle transition.\r\n * The auth service drives refresh-token state exclusively through this model's\r\n * named statics and instance methods, so it never hard-codes a column name โ\r\n * which is what makes a snake/camel mismatch (the historical `userId` bug)\r\n * structurally impossible and lets an override rename or add columns by\r\n * extending this class and registering it under `config.auth.refreshToken.model`.\r\n */\r\nexport class RefreshToken extends Model {\r\n public static table = \"refresh_tokens\";\r\n\r\n public static schema = refreshTokenSchema;\r\n\r\n /** Token family this row belongs to (rotation / replay grouping). */\r\n public get familyId(): string {\r\n return this.get(\"family_id\");\r\n }\r\n\r\n /** Whether the token's `expires_at` is in the past. */\r\n public get isExpired(): boolean {\r\n const expiresAt = this.get(\"expires_at\");\r\n\r\n if (!expiresAt) return false;\r\n\r\n return new Date() > new Date(expiresAt);\r\n }\r\n\r\n /** Whether the token has been revoked. */\r\n public get isRevoked(): boolean {\r\n return !!this.get(\"revoked_at\");\r\n }\r\n\r\n /** Whether the token is still usable (not expired and not revoked). */\r\n public get isValid(): boolean {\r\n return !this.isExpired && !this.isRevoked;\r\n }\r\n\r\n /**\r\n * Unconditionally stamp `revoked_at` on this token.\r\n */\r\n public async revoke(): Promise<this> {\r\n return this.merge({ revoked_at: new Date() }).save();\r\n }\r\n\r\n /**\r\n * Atomically revoke this token ONLY if it is still active. Resolves to `true`\r\n * when this call performed the revoke, `false` when a concurrent request had\r\n * already revoked it โ the win/lose signal that powers rotation replay\r\n * detection. Uses a conditional UPDATE so two concurrent rotations of the\r\n * same token can never both succeed.\r\n */\r\n public async revokeIfActive(): Promise<boolean> {\r\n const modelClass = this.constructor as typeof RefreshToken;\r\n\r\n const revokedCount = await modelClass.atomic(\r\n { id: this.id, revoked_at: null },\r\n { $set: { revoked_at: new Date() } },\r\n );\r\n\r\n return revokedCount > 0;\r\n }\r\n\r\n /**\r\n * Touch `last_used_at` without revoking โ the non-rotating refresh path.\r\n */\r\n public async markAsUsed(): Promise<void> {\r\n await this.merge({ last_used_at: new Date() }).save();\r\n }\r\n\r\n /**\r\n * Persist a freshly-signed refresh token for the user.\r\n */\r\n public static issue(user: Auth, token: string, options: RefreshTokenIssueOptions) {\r\n return this.create({\r\n token,\r\n user_id: user.id,\r\n user_type: user.userType,\r\n family_id: options.familyId,\r\n expires_at: options.expiresAt,\r\n device_info: options.deviceInfo\r\n ? {\r\n userAgent: options.deviceInfo.userAgent,\r\n ip: options.deviceInfo.ip,\r\n deviceId: options.deviceInfo.deviceId,\r\n }\r\n : undefined,\r\n });\r\n }\r\n\r\n /**\r\n * Find a refresh-token row by its raw token string.\r\n */\r\n public static findByToken(token: string): Promise<RefreshToken | null> {\r\n return this.first({ token });\r\n }\r\n\r\n /**\r\n * Find a refresh token scoped to a user โ used by logout so a caller can only\r\n * revoke a token that actually belongs to them.\r\n */\r\n public static findForUser(user: Auth, token: string): Promise<RefreshToken | null> {\r\n return this.first({ token, user_id: user.id });\r\n }\r\n\r\n /**\r\n * Delete a specific refresh token belonging to the user.\r\n */\r\n public static deleteForUser(user: Auth, token: string) {\r\n return this.delete({ token, user_id: user.id });\r\n }\r\n\r\n /**\r\n * Active, unexpired sessions for the user, newest first.\r\n */\r\n public static activeFor(user: Auth): Promise<RefreshToken[]> {\r\n return this.query()\r\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\r\n .where(\"expires_at\", \">\", new Date())\r\n .orderBy(\"created_at\", \"desc\")\r\n .get();\r\n }\r\n\r\n /**\r\n * Revoke every still-active refresh token for the user, returning the rows\r\n * that were revoked so the caller can emit a per-token event for each.\r\n *\r\n * Rows are fetched BEFORE they are revoked: a bulk `findAndUpdate` keyed on\r\n * `revoked_at: null` would re-query the same predicate after the update and\r\n * match nothing, returning an empty set.\r\n */\r\n public static async revokeAllFor(user: Auth): Promise<RefreshToken[]> {\r\n const tokens = await this.query()\r\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\r\n .get();\r\n\r\n for (const token of tokens) {\r\n await token.revoke();\r\n }\r\n\r\n return tokens;\r\n }\r\n\r\n /**\r\n * Revoke every still-active token in a family (rotation breach containment),\r\n * returning the revoked rows (fetched before revocation โ see `revokeAllFor`).\r\n */\r\n public static async revokeFamily(familyId: string): Promise<RefreshToken[]> {\r\n const tokens = await this.query().where({ family_id: familyId, revoked_at: null }).get();\r\n\r\n for (const token of tokens) {\r\n await token.revoke();\r\n }\r\n\r\n return tokens;\r\n }\r\n\r\n /**\r\n * Revoke the oldest active tokens so at most `max - 1` remain โ making room\r\n * for the about-to-be-issued one. Bounded by `max`, so the per-row loop is\r\n * small.\r\n */\r\n public static async enforceMax(user: Auth, max: number): Promise<void> {\r\n const activeTokens = await this.query()\r\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\r\n .orderBy(\"created_at\", \"asc\")\r\n .get();\r\n\r\n if (activeTokens.length < max) return;\r\n\r\n const tokensToRevoke = activeTokens.slice(0, activeTokens.length - max + 1);\r\n\r\n for (const token of tokensToRevoke) {\r\n await token.revoke();\r\n }\r\n }\r\n\r\n /**\r\n * Hard-delete every expired refresh token, returning the deleted rows so the\r\n * caller can emit a per-token event. Runs from the `auth.cleanup` CLI command\r\n * (a cold batch path).\r\n */\r\n public static async purgeExpired(): Promise<RefreshToken[]> {\r\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\r\n\r\n for (const token of expiredTokens) {\r\n await token.destroy();\r\n }\r\n\r\n return expiredTokens;\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { config } from "@warlock.js/core";
|
|
2
2
|
import { log } from "@warlock.js/logger";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../auth/src/services/auth-config.ts
|
|
5
5
|
const warnedLegacyKeys = /* @__PURE__ */ new Set();
|
|
6
6
|
/**
|
|
7
7
|
* Resolve an auth setting, preferring the new `auth.accessToken.*` /
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-config.mjs","names":[],"sources":["
|
|
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 type { LogoutWithoutTokenBehavior } from \"../contracts/types\";\r\n\r\nconst warnedLegacyKeys = new Set<string>();\r\n\r\n/**\r\n * Resolve an auth setting, preferring the new `auth.accessToken.*` /\r\n * `auth.refreshToken.*` key and falling back to the deprecated `auth.jwt.*`\r\n * shape โ warning once per legacy key. Returns `fallback` when neither is set.\r\n *\r\n * This is the backward-compatible shim that lets existing `auth.jwt.*` configs\r\n * keep working after the config split.\r\n */\r\nfunction resolve<T>(newKey: string, legacyKey: string, fallback?: T): T {\r\n const fromNew = config.key(`auth.${newKey}`);\r\n\r\n if (fromNew !== undefined && fromNew !== null) {\r\n return fromNew as T;\r\n }\r\n\r\n const fromLegacy = config.key(`auth.${legacyKey}`);\r\n\r\n if (fromLegacy !== undefined && fromLegacy !== null) {\r\n if (!warnedLegacyKeys.has(legacyKey)) {\r\n warnedLegacyKeys.add(legacyKey);\r\n log.warn(\"auth\", \"config-deprecation\", `auth.${legacyKey} is deprecated โ use auth.${newKey}`);\r\n }\r\n\r\n return fromLegacy as T;\r\n }\r\n\r\n return fallback as T;\r\n}\r\n\r\n/**\r\n * Typed, backward-compatible access to auth configuration. The service and the\r\n * jwt signer read configuration exclusively through here, so the new split\r\n * config and the legacy `auth.jwt.*` shape both resolve the same way.\r\n */\r\nexport const authConfig = {\r\n accessToken: {\r\n /** Signing secret (legacy: `auth.jwt.secret`). Throws if neither is set. */\r\n secret: (): string => {\r\n const secret = resolve<string | undefined>(\"accessToken.secret\", \"jwt.secret\");\r\n\r\n if (!secret) {\r\n throw new Error(\"auth: no JWT secret configured โ set `auth.accessToken.secret`.\");\r\n }\r\n\r\n return secret;\r\n },\r\n /** Signing algorithm (legacy: `auth.jwt.algorithm`). */\r\n algorithm: (): Algorithm => resolve(\"accessToken.algorithm\", \"jwt.algorithm\", \"HS256\"),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.expiresIn`). */\r\n expiresIn: (): string | undefined => resolve(\"accessToken.expiresIn\", \"jwt.expiresIn\"),\r\n },\r\n refreshToken: {\r\n /** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty โ fall back to the access secret. */\r\n secret: (): string | undefined => resolve(\"refreshToken.secret\", \"jwt.refresh.secret\"),\r\n /** Whether refresh tokens are enabled (legacy: `auth.jwt.refresh.enabled`). */\r\n enabled: (): boolean => resolve(\"refreshToken.enabled\", \"jwt.refresh.enabled\", true),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.refresh.expiresIn`). */\r\n expiresIn: (): string => resolve(\"refreshToken.expiresIn\", \"jwt.refresh.expiresIn\", \"7d\"),\r\n /** Rotate-on-use (legacy: `auth.jwt.refresh.rotation`). */\r\n rotation: (): boolean => resolve(\"refreshToken.rotation\", \"jwt.refresh.rotation\", true),\r\n /** Max active tokens per user (legacy: `auth.jwt.refresh.maxPerUser`). */\r\n maxPerUser: (): number => resolve(\"refreshToken.maxPerUser\", \"jwt.refresh.maxPerUser\", 5),\r\n /** Logout-without-token behavior (legacy: `auth.jwt.refresh.logoutWithoutToken`). */\r\n logoutWithoutToken: (): LogoutWithoutTokenBehavior =>\r\n resolve(\"refreshToken.logoutWithoutToken\", \"jwt.refresh.logoutWithoutToken\", \"revoke-all\"),\r\n },\r\n};\r\n"],"mappings":";;;;AAKA,MAAM,mCAAmB,IAAI,IAAY;;;;;;;;;AAUzC,SAAS,QAAW,QAAgB,WAAmB,UAAiB;CACtE,MAAM,UAAU,OAAO,IAAI,QAAQ,QAAQ;CAE3C,IAAI,YAAY,UAAa,YAAY,MACvC,OAAO;CAGT,MAAM,aAAa,OAAO,IAAI,QAAQ,WAAW;CAEjD,IAAI,eAAe,UAAa,eAAe,MAAM;EACnD,IAAI,CAAC,iBAAiB,IAAI,SAAS,GAAG;GACpC,iBAAiB,IAAI,SAAS;GAC9B,IAAI,KAAK,QAAQ,sBAAsB,QAAQ,UAAU,4BAA4B,QAAQ;EAC/F;EAEA,OAAO;CACT;CAEA,OAAO;AACT;;;;;;AAOA,MAAa,aAAa;CACxB,aAAa;;EAEX,cAAsB;GACpB,MAAM,SAAS,QAA4B,sBAAsB,YAAY;GAE7E,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,iEAAiE;GAGnF,OAAO;EACT;;EAEA,iBAA4B,QAAQ,yBAAyB,iBAAiB,OAAO;;EAErF,iBAAqC,QAAQ,yBAAyB,eAAe;CACvF;CACA,cAAc;;EAEZ,cAAkC,QAAQ,uBAAuB,oBAAoB;;EAErF,eAAwB,QAAQ,wBAAwB,uBAAuB,IAAI;;EAEnF,iBAAyB,QAAQ,0BAA0B,yBAAyB,IAAI;;EAExF,gBAAyB,QAAQ,yBAAyB,wBAAwB,IAAI;;EAEtF,kBAA0B,QAAQ,2BAA2B,0BAA0B,CAAC;;EAExF,0BACE,QAAQ,mCAAmC,kCAAkC,YAAY;CAC7F;AACF"}
|
|
@@ -3,7 +3,7 @@ import { DeviceInfo, TokenPair } from "../contracts/types.mjs";
|
|
|
3
3
|
import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
|
|
4
4
|
import { EventSubscription } from "@mongez/events";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../auth/src/services/auth-events.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* Auth event payload types
|
|
9
9
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-events.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth-events.d.mts","names":[],"sources":["../../../../../../../auth/src/services/auth-events.ts"],"mappings":";;;;;;;;;KASY,iBAAA;EAEV,eAAA,GAAkB,IAAA,EAAM,IAAA,EAAM,SAAA,EAAW,SAAA,EAAW,UAAA,GAAa,UAAA;EACjE,cAAA,GAAiB,WAAA;IAAe,KAAA;IAAgB,QAAA;EAAA,GAAqB,MAAA;EACrE,eAAA,GAAkB,WAAA;IAAe,KAAA;IAAgB,QAAA;EAAA;EAGjD,MAAA,GAAS,IAAA,EAAM,IAAA;EACf,YAAA,GAAe,IAAA,EAAM,IAAA;EACrB,iBAAA,GAAoB,IAAA,EAAM,IAAA;EAG1B,eAAA,GAAkB,IAAA,EAAM,IAAA,EAAM,SAAA,EAAW,SAAA;EACzC,iBAAA,GAAoB,IAAA,EAAM,IAAA,EAAM,YAAA,EAAc,SAAA,EAAW,eAAA,EAAiB,YAAA;EAC1E,eAAA,GAAkB,IAAA,EAAM,IAAA,EAAM,KAAA,EAAO,YAAA;EACrC,eAAA,GAAkB,KAAA,EAAO,YAAA;EACzB,qBAAA,GAAwB,QAAA,UAAkB,MAAA,EAAQ,YAAA;EAGlD,kBAAA,GAAqB,IAAA,EAAM,IAAA;EAC3B,yBAAA,GAA4B,IAAA,EAAM,IAAA,EAAM,UAAA;EACxC,gBAAA,GAAmB,IAAA,EAAM,IAAA;EAGzB,iBAAA,GAAoB,IAAA,EAAM,IAAA,EAAM,YAAA,EAAc,YAAA,EAAc,UAAA,GAAa,UAAA;EACzE,mBAAA,GAAsB,IAAA,EAAM,IAAA,EAAM,YAAA,EAAc,YAAA;EAGhD,mBAAA,GAAsB,YAAA;AAAA;;;;KAMZ,aAAA,SAAsB,iBAAiB;;;;KAKvC,iBAAA,WAA4B,aAAA,QACnC,IAAA,EAAM,iBAAA,CAAkB,CAAA,aACjB,OAAA;;;;;;;;;;;;;;;;;;;cAyBC,UAAA;EAzDe;;;eA6Db,aAAA,EAAa,KAAA,EAAS,CAAA,EAAC,QAAA,EAAY,iBAAA,CAAkB,CAAA,IAAK,iBAAA;EA1DzC;;;sBAiEV,aAAA,EAAa,KAAA,EAAS,CAAA,EAAC,QAAA,EAAY,iBAAA,CAAkB,CAAA,IAAK,iBAAA;EAhEpD;;;iBAuEX,aAAA,EAAa,KAAA,EAAS,CAAA,KAAC,IAAA,EAAW,iBAAA,CAAkB,CAAA;EAvEO;;;oBAuFxD,aAAA,EAAa,KAAA,EAAS,CAAA,KAAC,IAAA,EAAW,iBAAA,CAAkB,CAAA;EAtFxC;;;;EACL;;;cAmGb,aAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-events.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth-events.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth-events.ts"],"sourcesContent":["import events, { type EventSubscription } from \"@mongez/events\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport type { DeviceInfo, TokenPair } from \"../contracts/types\";\r\nimport type { Auth } from \"../models/auth.model\";\r\nimport type { RefreshToken } from \"../models/refresh-token\";\r\n\r\n/**\r\n * Auth event payload types\r\n */\r\nexport type AuthEventPayloads = {\r\n // Login events\r\n \"login.success\": [user: Auth, tokenPair: TokenPair, deviceInfo?: DeviceInfo];\r\n \"login.failed\": [credentials: { email?: string; username?: string }, reason: string];\r\n \"login.attempt\": [credentials: { email?: string; username?: string }];\r\n\r\n // Logout events\r\n logout: [user: Auth];\r\n \"logout.all\": [user: Auth];\r\n \"logout.failsafe\": [user: Auth];\r\n\r\n // Token events\r\n \"token.created\": [user: Auth, tokenPair: TokenPair];\r\n \"token.refreshed\": [user: Auth, newTokenPair: TokenPair, oldRefreshToken: RefreshToken];\r\n \"token.revoked\": [user: Auth, token: RefreshToken];\r\n \"token.expired\": [token: RefreshToken];\r\n \"token.familyRevoked\": [familyId: string, tokens: RefreshToken[]];\r\n\r\n // Password events\r\n \"password.changed\": [user: Auth];\r\n \"password.resetRequested\": [user: Auth, resetToken: string];\r\n \"password.reset\": [user: Auth];\r\n\r\n // Session events\r\n \"session.created\": [user: Auth, refreshToken: RefreshToken, deviceInfo?: DeviceInfo];\r\n \"session.destroyed\": [user: Auth, refreshToken: RefreshToken];\r\n\r\n // Cleanup events\r\n \"cleanup.completed\": [expiredCount: number];\r\n};\r\n\r\n/**\r\n * Auth event names\r\n */\r\nexport type AuthEventName = keyof AuthEventPayloads;\r\n\r\n/**\r\n * Callback type for a specific event\r\n */\r\nexport type AuthEventCallback<T extends AuthEventName> = (\r\n ...args: AuthEventPayloads[T]\r\n) => void | Promise<void>;\r\n\r\n/**\r\n * Event namespace prefix for auth events\r\n */\r\nconst AUTH_EVENT_PREFIX = \"auth.\";\r\n\r\n/**\r\n * Type-safe auth events manager\r\n *\r\n * @example\r\n * ```typescript\r\n * // Subscribe to events with full autocomplete\r\n * authEvents.on(\"login.success\", (user, tokenPair, deviceInfo) => {\r\n * console.log(`User ${user.id} logged in`);\r\n * });\r\n *\r\n * authEvents.on(\"token.refreshed\", (user, newPair, oldToken) => {\r\n * console.log(`Token refreshed for user ${user.id}`);\r\n * });\r\n *\r\n * // Trigger events\r\n * authEvents.emit(\"login.success\", user, tokenPair, deviceInfo);\r\n * ```\r\n */\r\nexport const authEvents = {\r\n /**\r\n * Subscribe to an auth event\r\n */\r\n on<T extends AuthEventName>(event: T, callback: AuthEventCallback<T>): EventSubscription {\r\n return events.subscribe(AUTH_EVENT_PREFIX + event, callback as Function);\r\n },\r\n\r\n /**\r\n * Subscribe to an auth event (alias for `on`)\r\n */\r\n subscribe<T extends AuthEventName>(event: T, callback: AuthEventCallback<T>): EventSubscription {\r\n return this.on(event, callback);\r\n },\r\n\r\n /**\r\n * Emit an auth event\r\n */\r\n emit<T extends AuthEventName>(event: T, ...args: AuthEventPayloads[T]): void {\r\n try {\r\n events.trigger(AUTH_EVENT_PREFIX + event, ...args);\r\n } catch (error) {\r\n // Auth events are fire-and-forget notifications: a throwing listener must\r\n // never break the flow that emitted it (e.g. turn a completed login into a\r\n // 500). Log and swallow. NOTE: this guards synchronous listener throws;\r\n // async listener rejections and \"later listeners still run\" need a fix in\r\n // @mongez/events (tracked separately).\r\n log.error(\"auth\", \"event\", error);\r\n }\r\n },\r\n\r\n /**\r\n * Emit an auth event (alias for `emit`)\r\n */\r\n trigger<T extends AuthEventName>(event: T, ...args: AuthEventPayloads[T]): void {\r\n this.emit(event, ...args);\r\n },\r\n\r\n /**\r\n * Unsubscribe from all auth events\r\n */\r\n unsubscribeAll(): void {\r\n events.unsubscribeNamespace(AUTH_EVENT_PREFIX.slice(0, -1));\r\n },\r\n\r\n /**\r\n * Unsubscribe from a specific auth event\r\n */\r\n off(event?: AuthEventName): void {\r\n if (event) {\r\n events.unsubscribe(AUTH_EVENT_PREFIX + event);\r\n } else {\r\n this.unsubscribeAll();\r\n }\r\n },\r\n};\r\n"],"mappings":";;;;;;;AAuDA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;AAoB1B,MAAa,aAAa;;;;CAIxB,GAA4B,OAAU,UAAmD;EACvF,OAAO,OAAO,UAAU,oBAAoB,OAAO,QAAoB;CACzE;;;;CAKA,UAAmC,OAAU,UAAmD;EAC9F,OAAO,KAAK,GAAG,OAAO,QAAQ;CAChC;;;;CAKA,KAA8B,OAAU,GAAG,MAAkC;EAC3E,IAAI;GACF,OAAO,QAAQ,oBAAoB,OAAO,GAAG,IAAI;EACnD,SAAS,OAAO;GAMd,IAAI,MAAM,QAAQ,SAAS,KAAK;EAClC;CACF;;;;CAKA,QAAiC,OAAU,GAAG,MAAkC;EAC9E,KAAK,KAAK,OAAO,GAAG,IAAI;CAC1B;;;;CAKA,iBAAuB;EACrB,OAAO,qBAAqB,kBAAkB,MAAM,GAAG,EAAE,CAAC;CAC5D;;;;CAKA,IAAI,OAA6B;EAC/B,IAAI,OACF,OAAO,YAAY,oBAAoB,KAAK;OAE5C,KAAK,eAAe;CAExB;AACF"}
|
|
@@ -3,7 +3,7 @@ import { AccessTokenOutput, DeviceInfo, LoginResult, TokenPair } from "../contra
|
|
|
3
3
|
import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
|
|
4
4
|
import { ChildModel } from "@warlock.js/cascade";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../auth/src/services/auth.service.d.ts
|
|
7
7
|
declare class AuthService {
|
|
8
8
|
/**
|
|
9
9
|
* Resolve the active access-token model โ the package default, or a subclass
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.service.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth.service.d.mts","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"mappings":";;;;;;cAYM,WAAA;;;AALiD;;;;cAYzC,gBAAA;EA2ByD;;;EAAA,YApBzD,iBAAA;EA0CD;;;EAnCJ,uBAAA,CAAwB,IAAA,EAAM,IAAA;;;;;EA0FlC;;;;;EA7EU,mBAAA,CAAoB,IAAA,EAAM,IAAA,EAAM,OAAA,SAAgB,OAAA,CAAQ,iBAAA;EAwJe;;;;EArIvE,kBAAA,CACX,IAAA,EAAM,IAAA,EACN,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,YAAA;EA+JI;;;EAvIF,eAAA,CAAgB,IAAA,EAAM,IAAA,EAAM,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,SAAA;EAsKjD;;;;;EA1Ib,aAAA,CACX,kBAAA,UACA,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,SAAA;EAkL2B;;;EAtHzB,cAAA,CAAe,aAAA,UAAuB,cAAA,WAAyB,OAAA;EA8I1B;;;EAvIrC,YAAA,CAAa,QAAA,WAAmB,OAAA;EAmKD;;;;EA3J/B,YAAA,WAAuB,IAAA,EAAM,KAAA,EAAO,UAAA,CAAW,CAAA,GAAI,IAAA,QAAY,OAAA,CAAQ,CAAA;EArK7E;;;;EA+LM,KAAA,WAAgB,IAAA,EAC3B,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,WAAA,OACA,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,WAAA,CAAY,CAAA;;;;;;;;;;;EA8BV,MAAA,CAAO,IAAA,EAAM,IAAA,EAAM,WAAA,WAAsB,YAAA,YAAwB,OAAA;EA/L5E;;;EA4NW,iBAAA,CAAkB,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA;EAnMxB;;;EA0MtB,qBAAA,CAAsB,IAAA,EAAM,IAAA,GAAO,OAAA;EA1MmB;;;EAiNtD,kBAAA,CAAmB,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA;EAnL7C;;;;;EA4LF,eAAA,CAAgB,IAAA,EAAM,IAAA,GAAO,OAAA;EA/HS;;;EA8ItC,iBAAA,CAAkB,QAAA,WAAmB,OAAA;EAvIL;;;;;EAkJhC,oBAAA,IAAwB,OAAA;EA1IK;;;EA2J7B,iBAAA,CAAkB,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;AAAA;AAAA,cAKzC,WAAA,EAAW,WAAoB"}
|
|
@@ -9,7 +9,7 @@ import { config, hashPassword, verifyPassword } from "@warlock.js/core";
|
|
|
9
9
|
import { Random } from "@mongez/reinforcements";
|
|
10
10
|
import ms from "ms";
|
|
11
11
|
|
|
12
|
-
//#region
|
|
12
|
+
//#region ../auth/src/services/auth.service.ts
|
|
13
13
|
var AuthService = class {
|
|
14
14
|
/**
|
|
15
15
|
* Resolve the active access-token model โ the package default, or a subclass
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.service.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/services/auth.service.ts"],"sourcesContent":["import { Random } from \"@mongez/reinforcements\";\r\nimport type { ChildModel } from \"@warlock.js/cascade\";\r\nimport { config, hashPassword, verifyPassword } from \"@warlock.js/core\";\r\nimport ms from \"ms\";\r\nimport type { AccessTokenOutput, DeviceInfo, LoginResult, TokenPair } from \"../contracts/types\";\r\nimport { AccessToken } from \"../models/access-token\";\r\nimport type { Auth } from \"../models/auth.model\";\r\nimport { RefreshToken } from \"../models/refresh-token\";\r\nimport { authConfig } from \"./auth-config\";\r\nimport { authEvents } from \"./auth-events\";\r\nimport { jwt } from \"./jwt\";\r\n\r\nclass AuthService {\r\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) {\r\n return {\r\n id: user.id,\r\n userType: user.userType,\r\n created_at: Date.now(),\r\n };\r\n }\r\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(user: Auth, payload?: any): Promise<AccessTokenOutput> {\r\n const data = payload || this.buildAccessTokenPayload(user);\r\n const expiresInConfig = authConfig.accessToken.expiresIn();\r\n const expiresIn = expiresInConfig\r\n ? ms(expiresInConfig as ms.StringValue)\r\n : ms(\"1h\"); // default 1 hour\r\n\r\n const token = await jwt.generate(data, { expiresIn });\r\n const expiresAt = new Date(Date.now() + expiresIn);\r\n\r\n await this.accessTokenModel.issue(user, token, expiresAt);\r\n\r\n return { token, expiresAt: expiresAt.toISOString() };\r\n }\r\n\r\n /**\r\n * Sign + persist a refresh token for the user (enforcing the per-user cap\r\n * first). Resolves to `undefined` when refresh tokens are disabled in config.\r\n */\r\n public async createRefreshToken(\r\n user: Auth,\r\n deviceInfo?: DeviceInfo,\r\n ): Promise<RefreshToken | undefined> {\r\n if (!authConfig.refreshToken.enabled()) return;\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 expiresIn = ms(authConfig.refreshToken.expiresIn() as ms.StringValue);\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 });\r\n }\r\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> {\r\n const accessToken = await this.generateAccessToken(user, deviceInfo?.payload);\r\n const refreshToken = await this.createRefreshToken(user, deviceInfo);\r\n\r\n const tokenPair: TokenPair = {\r\n accessToken,\r\n refreshToken: refreshToken\r\n ? {\r\n token: refreshToken.get(\"token\"),\r\n expiresAt: refreshToken.get(\"expires_at\"),\r\n }\r\n : undefined,\r\n };\r\n\r\n authEvents.emit(\"token.created\", user, tokenPair);\r\n\r\n if (refreshToken) {\r\n authEvents.emit(\"session.created\", user, refreshToken, deviceInfo);\r\n }\r\n\r\n return tokenPair;\r\n }\r\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(\r\n refreshTokenString: string,\r\n deviceInfo?: DeviceInfo,\r\n ): Promise<TokenPair | null> {\r\n try {\r\n const decoded = await jwt.verifyRefreshToken<{\r\n userId: number;\r\n userType: string;\r\n familyId: string;\r\n }>(refreshTokenString);\r\n\r\n if (!decoded) return null;\r\n\r\n const refreshToken = await this.refreshTokenModel.findByToken(refreshTokenString);\r\n\r\n if (!refreshToken?.isValid) {\r\n // Already-invalid token presented โ likely a replayed (rotated) token.\r\n if (refreshToken) {\r\n await this.revokeTokenFamily(refreshToken.familyId);\r\n }\r\n\r\n return null;\r\n }\r\n\r\n const UserModel = config.key(`auth.userType.${decoded.userType}`);\r\n\r\n if (!UserModel) return null;\r\n\r\n const user = (await UserModel.find(decoded.userId)) as Auth | null;\r\n\r\n if (!user) return null;\r\n\r\n const rotationEnabled = authConfig.refreshToken.rotation();\r\n\r\n if (rotationEnabled) {\r\n const won = await refreshToken.revokeIfActive();\r\n\r\n if (!won) {\r\n // A concurrent request already rotated this token (reuse / replay).\r\n await this.revokeTokenFamily(refreshToken.familyId);\r\n\r\n return null;\r\n }\r\n } else {\r\n await refreshToken.markAsUsed();\r\n }\r\n\r\n const newTokenPair = await this.createTokenPair(user, {\r\n ...deviceInfo,\r\n familyId: refreshToken.familyId,\r\n });\r\n\r\n authEvents.emit(\"token.refreshed\", user, newTokenPair, refreshToken);\r\n\r\n return newTokenPair;\r\n } catch {\r\n return null;\r\n }\r\n }\r\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>(Model: ChildModel<T>, data: any): Promise<T | null> {\r\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\")!))) {\r\n authEvents.emit(\"login.failed\", otherData, \"Invalid password\");\r\n\r\n return null;\r\n }\r\n\r\n return user;\r\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: any,\r\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()) {\r\n const accessToken = await this.generateAccessToken(user, deviceInfo?.payload);\r\n\r\n return { user, tokens: { accessToken } };\r\n }\r\n\r\n const tokens = await this.createTokenPair(user, deviceInfo);\r\n\r\n authEvents.emit(\"login.success\", user, tokens, deviceInfo);\r\n\r\n return { user, tokens } as LoginResult<T>;\r\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 * 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, hashPassword, verifyPassword } from \"@warlock.js/core\";\r\nimport ms from \"ms\";\r\nimport type { AccessTokenOutput, DeviceInfo, LoginResult, TokenPair } from \"../contracts/types\";\r\nimport { AccessToken } from \"../models/access-token\";\r\nimport type { Auth } from \"../models/auth.model\";\r\nimport { RefreshToken } from \"../models/refresh-token\";\r\nimport { authConfig } from \"./auth-config\";\r\nimport { authEvents } from \"./auth-events\";\r\nimport { jwt } from \"./jwt\";\r\n\r\nclass AuthService {\r\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) {\r\n return {\r\n id: user.id,\r\n userType: user.userType,\r\n created_at: Date.now(),\r\n };\r\n }\r\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(user: Auth, payload?: any): Promise<AccessTokenOutput> {\r\n const data = payload || this.buildAccessTokenPayload(user);\r\n const expiresInConfig = authConfig.accessToken.expiresIn();\r\n const expiresIn = expiresInConfig\r\n ? ms(expiresInConfig as ms.StringValue)\r\n : ms(\"1h\"); // default 1 hour\r\n\r\n const token = await jwt.generate(data, { expiresIn });\r\n const expiresAt = new Date(Date.now() + expiresIn);\r\n\r\n await this.accessTokenModel.issue(user, token, expiresAt);\r\n\r\n return { token, expiresAt: expiresAt.toISOString() };\r\n }\r\n\r\n /**\r\n * Sign + persist a refresh token for the user (enforcing the per-user cap\r\n * first). Resolves to `undefined` when refresh tokens are disabled in config.\r\n */\r\n public async createRefreshToken(\r\n user: Auth,\r\n deviceInfo?: DeviceInfo,\r\n ): Promise<RefreshToken | undefined> {\r\n if (!authConfig.refreshToken.enabled()) return;\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 expiresIn = ms(authConfig.refreshToken.expiresIn() as ms.StringValue);\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 });\r\n }\r\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> {\r\n const accessToken = await this.generateAccessToken(user, deviceInfo?.payload);\r\n const refreshToken = await this.createRefreshToken(user, deviceInfo);\r\n\r\n const tokenPair: TokenPair = {\r\n accessToken,\r\n refreshToken: refreshToken\r\n ? {\r\n token: refreshToken.get(\"token\"),\r\n expiresAt: refreshToken.get(\"expires_at\"),\r\n }\r\n : undefined,\r\n };\r\n\r\n authEvents.emit(\"token.created\", user, tokenPair);\r\n\r\n if (refreshToken) {\r\n authEvents.emit(\"session.created\", user, refreshToken, deviceInfo);\r\n }\r\n\r\n return tokenPair;\r\n }\r\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(\r\n refreshTokenString: string,\r\n deviceInfo?: DeviceInfo,\r\n ): Promise<TokenPair | null> {\r\n try {\r\n const decoded = await jwt.verifyRefreshToken<{\r\n userId: number;\r\n userType: string;\r\n familyId: string;\r\n }>(refreshTokenString);\r\n\r\n if (!decoded) return null;\r\n\r\n const refreshToken = await this.refreshTokenModel.findByToken(refreshTokenString);\r\n\r\n if (!refreshToken?.isValid) {\r\n // Already-invalid token presented โ likely a replayed (rotated) token.\r\n if (refreshToken) {\r\n await this.revokeTokenFamily(refreshToken.familyId);\r\n }\r\n\r\n return null;\r\n }\r\n\r\n const UserModel = config.key(`auth.userType.${decoded.userType}`);\r\n\r\n if (!UserModel) return null;\r\n\r\n const user = (await UserModel.find(decoded.userId)) as Auth | null;\r\n\r\n if (!user) return null;\r\n\r\n const rotationEnabled = authConfig.refreshToken.rotation();\r\n\r\n if (rotationEnabled) {\r\n const won = await refreshToken.revokeIfActive();\r\n\r\n if (!won) {\r\n // A concurrent request already rotated this token (reuse / replay).\r\n await this.revokeTokenFamily(refreshToken.familyId);\r\n\r\n return null;\r\n }\r\n } else {\r\n await refreshToken.markAsUsed();\r\n }\r\n\r\n const newTokenPair = await this.createTokenPair(user, {\r\n ...deviceInfo,\r\n familyId: refreshToken.familyId,\r\n });\r\n\r\n authEvents.emit(\"token.refreshed\", user, newTokenPair, refreshToken);\r\n\r\n return newTokenPair;\r\n } catch {\r\n return null;\r\n }\r\n }\r\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>(Model: ChildModel<T>, data: any): Promise<T | null> {\r\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\")!))) {\r\n authEvents.emit(\"login.failed\", otherData, \"Invalid password\");\r\n\r\n return null;\r\n }\r\n\r\n return user;\r\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: any,\r\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()) {\r\n const accessToken = await this.generateAccessToken(user, deviceInfo?.payload);\r\n\r\n return { user, tokens: { accessToken } };\r\n }\r\n\r\n const tokens = await this.createTokenPair(user, deviceInfo);\r\n\r\n authEvents.emit(\"login.success\", user, tokens, deviceInfo);\r\n\r\n return { user, tokens } as LoginResult<T>;\r\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 * 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":["
|
|
1
|
+
{"version":3,"file":"generate-jwt-secret.d.mts","names":[],"sources":["../../../../../../../auth/src/services/generate-jwt-secret.ts"],"mappings":";iBAKsB,iBAAA,IAAiB,OAAA"}
|
|
@@ -3,7 +3,7 @@ import { Random } from "@mongez/reinforcements";
|
|
|
3
3
|
import { log } from "@warlock.js/logger";
|
|
4
4
|
import { fs } from "@warlock.js/fs";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../auth/src/services/generate-jwt-secret.ts
|
|
7
7
|
async function generateJWTSecret() {
|
|
8
8
|
let envFile = rootPath(".env");
|
|
9
9
|
log.info("jwt", "generating", "Generating JWT secrets");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-jwt-secret.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"generate-jwt-secret.mjs","names":[],"sources":["../../../../../../../auth/src/services/generate-jwt-secret.ts"],"sourcesContent":["import { fs } from \"@warlock.js/fs\";\nimport { Random } from \"@mongez/reinforcements\";\nimport { environment, rootPath } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\n\nexport async function generateJWTSecret() {\n let envFile = rootPath(\".env\");\n\n log.info(\"jwt\", \"generating\", \"Generating JWT secrets\");\n\n const environmentMode = environment();\n\n if (!(await fs.files.exists(envFile))) {\n const envFileType = environmentMode === \"production\" ? \".env.production\" : \".env.development\";\n envFile = rootPath(envFileType);\n }\n\n if (!(await fs.files.exists(envFile))) {\n log.error(\"jwt\", \"error\", \".env file not found\");\n return;\n }\n\n let contents = await fs.files.get(envFile);\n\n const hasJwtSecret = contents.includes(\"JWT_SECRET\");\n const hasJwtRefreshSecret = contents.includes(\"JWT_REFRESH_SECRET\");\n\n if (hasJwtSecret && hasJwtRefreshSecret) {\n log.warn(\"jwt\", \"exists\", \"JWT secrets already exist in the .env file.\");\n return;\n }\n\n let secretsToAdd = \"\";\n\n if (!hasJwtSecret) {\n const jwtSecret = Random.token(32);\n secretsToAdd += `\n# JWT Secret\nJWT_SECRET=${jwtSecret}\n`;\n log.success(\"jwt\", \"generated\", \"JWT_SECRET generated and added to the .env file.\");\n } else {\n log.info(\"jwt\", \"exists\", \"JWT_SECRET already exists in the .env file.\");\n }\n\n if (!hasJwtRefreshSecret) {\n const jwtRefreshSecret = Random.token(32);\n secretsToAdd += `\n# JWT Refresh Secret\nJWT_REFRESH_SECRET=${jwtRefreshSecret}\n`;\n log.success(\"jwt\", \"generated\", \"JWT_REFRESH_SECRET generated and added to the .env file.\");\n } else {\n log.info(\"jwt\", \"exists\", \"JWT_REFRESH_SECRET already exists in the .env file.\");\n }\n\n if (secretsToAdd) {\n contents += secretsToAdd;\n await fs.files.put(envFile, contents);\n }\n}\n"],"mappings":";;;;;;AAKA,eAAsB,oBAAoB;CACxC,IAAI,UAAU,SAAS,MAAM;CAE7B,IAAI,KAAK,OAAO,cAAc,wBAAwB;CAEtD,MAAM,kBAAkB,YAAY;CAEpC,IAAI,CAAE,MAAM,GAAG,MAAM,OAAO,OAAO,GAEjC,UAAU,SADU,oBAAoB,eAAe,oBAAoB,kBAC7C;CAGhC,IAAI,CAAE,MAAM,GAAG,MAAM,OAAO,OAAO,GAAI;EACrC,IAAI,MAAM,OAAO,SAAS,qBAAqB;EAC/C;CACF;CAEA,IAAI,WAAW,MAAM,GAAG,MAAM,IAAI,OAAO;CAEzC,MAAM,eAAe,SAAS,SAAS,YAAY;CACnD,MAAM,sBAAsB,SAAS,SAAS,oBAAoB;CAElE,IAAI,gBAAgB,qBAAqB;EACvC,IAAI,KAAK,OAAO,UAAU,6CAA6C;EACvE;CACF;CAEA,IAAI,eAAe;CAEnB,IAAI,CAAC,cAAc;EACjB,MAAM,YAAY,OAAO,MAAM,EAAE;EACjC,gBAAgB;;aAEP,UAAU;;EAEnB,IAAI,QAAQ,OAAO,aAAa,kDAAkD;CACpF,OACE,IAAI,KAAK,OAAO,UAAU,6CAA6C;CAGzE,IAAI,CAAC,qBAAqB;EACxB,MAAM,mBAAmB,OAAO,MAAM,EAAE;EACxC,gBAAgB;;qBAEC,iBAAiB;;EAElC,IAAI,QAAQ,OAAO,aAAa,0DAA0D;CAC5F,OACE,IAAI,KAAK,OAAO,UAAU,qDAAqD;CAGjF,IAAI,cAAc;EAChB,YAAY;EACZ,MAAM,GAAG,MAAM,IAAI,SAAS,QAAQ;CACtC;AACF"}
|
package/esm/services/jwt.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SignerOptions, VerifierOptions } from "fast-jwt";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../auth/src/services/jwt.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Token class. Stamped as the `tokenType` claim on every signed token and
|
|
6
6
|
* checked on verify so an access token can never be accepted where a refresh
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"jwt.d.mts","names":[],"sources":["../../../../../../../auth/src/services/jwt.ts"],"mappings":";;;;;AAkBA;;;;AAAqB;AAiBrB;KAjBY,SAAA;AAAA,cAiBC,GAAA;EAMG;;;;;IAAA,GAAA;IAAA,SAAA;IAAA,GAAA;EAAA,IAKT,aAAA;IAAkB,GAAA;EAAA,IACpB,OAAA;EAoBQ;;;;;sBAPG,KAAA;IACC,GAAA;IAAA,UAAA;IAAA,GAAA;EAAA,IAKV,eAAA;IAAoB,GAAA;EAAA,IACtB,OAAA,CAAQ,CAAA;EA8BI;;;;IAhBD,GAAA;IAAA,SAAA;IAAA,SAAA;IAAA,GAAA;EAAA,IAMT,aAAA;IAAkB,GAAA;EAAA,IACpB,OAAA;;;;kCAQuB,KAAA;IACX,GAAA;IAAA,UAAA;IAAA,GAAA;EAAA,IAKV,eAAA;IAAoB,GAAA;EAAA,IACtB,OAAA,CAAQ,CAAA;AAAA"}
|
package/esm/services/jwt.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { authConfig } from "./auth-config.mjs";
|
|
2
2
|
import { createSigner, createVerifier } from "fast-jwt";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../auth/src/services/jwt.ts
|
|
5
5
|
const getSecretKey = () => authConfig.accessToken.secret();
|
|
6
6
|
const getAlgorithm = () => authConfig.accessToken.algorithm();
|
|
7
7
|
const getRefreshSecretKey = () => authConfig.refreshToken.secret() || getSecretKey();
|
package/esm/services/jwt.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.mjs","names":[],"sources":["
|
|
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 { 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\";\r\nconst REFRESH_TOKEN_TYPE: TokenType = \"refresh\";\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 Error(`Invalid token type: expected \"${expected}\", received \"${actual}\".`);\r\n }\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 ...options\r\n }: VerifierOptions & { key?: string } = {},\r\n ): Promise<T> {\r\n const verify = createVerifier({ key, ...options, algorithms });\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 ...options\r\n }: VerifierOptions & { key?: string } = {},\r\n ): Promise<T> {\r\n const verify = createVerifier({ key, algorithms, ...options });\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":";;;;AAGA,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;;;;;AAMtC,SAAS,gBAAgB,SAAkB,UAA2B;CACpE,MAAM,SAAU,SAAwD;CAExE,IAAI,OAAO,WAAW,YAAY,WAAW,UAC3C,MAAM,IAAI,MAAM,iCAAiC,SAAS,eAAe,OAAO,GAAG;AAEvF;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,CAAC,CAAC;GAAE,GAAG;GAAS,WAAW;EAAkB,CAAC;CAEvE;;;;;;CAOA,MAAM,OACJ,OACA,EACE,MAAM,aAAa,GACnB,aAAa,CAAC,aAAa,CAAC,GAC5B,GAAG,YACmC,CAAC,GAC7B;EAGZ,MAAM,UAAU,MAFD,eAAe;GAAE;GAAK,GAAG;GAAS;EAAW,CAEjC,CAAC,CAAC,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,CAAC,CAAC;GAAE,GAAG;GAAS,WAAW;EAAmB,CAAC;CAC3D;;;;CAKA,MAAM,mBACJ,OACA,EACE,MAAM,oBAAoB,GAC1B,aAAa,CAAC,aAAa,CAAC,GAC5B,GAAG,YACmC,CAAC,GAC7B;EAGZ,MAAM,UAAU,MAFD,eAAe;GAAE;GAAK;GAAY,GAAG;EAAQ,CAEjC,CAAC,CAAC,KAAK;EAElC,gBAAgB,SAAS,kBAAkB;EAE3C,OAAO;CACT;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-error-codes.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth-error-codes.d.mts","names":[],"sources":["../../../../../../../auth/src/utils/auth-error-codes.ts"],"mappings":";aAAY,cAAA;EAAA;;;;EAKV,kBAAA;EAAA;EAKA;;;;EAAA,kBAAA;EAAA;;;;;EAKA,YAAA;EAAA;;;;;;EAMA,eAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-error-codes.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth-error-codes.mjs","names":[],"sources":["../../../../../../../auth/src/utils/auth-error-codes.ts"],"sourcesContent":["export enum AuthErrorCodes {\r\n /**\r\n * Missing Access Token Error Code EC001\r\n * EC001 = Missing Access Token\r\n */\r\n MissingAccessToken = \"EC001\", // Error Code 001\r\n /**\r\n * Invalid Access Token Error Code EC002\r\n * EC002 = Invalid Access Token\r\n */\r\n InvalidAccessToken = \"EC002\", // Error Code 002\r\n /**\r\n * Unauthorized Error Code EC003\r\n * EC003 = Unauthorized\r\n */\r\n Unauthorized = \"EC003\", // Error Code 003\r\n /**\r\n * Too Many Attempts Error Code EC004\r\n * EC004 = Too Many Attempts โ issued by the login-throttle middleware once\r\n * a per-account or per-IP failure counter trips its threshold.\r\n */\r\n TooManyAttempts = \"EC004\", // Error Code 004\r\n}\r\n"],"mappings":";AAAA,IAAY,iBAAL;;;;;CAKL;;;;;CAKA;;;;;CAKA;;;;;;CAMA;;AACF"}
|
package/package.json
CHANGED
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
"ms": "^2.1.3"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@warlock.js/fs": "4.
|
|
27
|
-
"@warlock.js/cache": "4.
|
|
28
|
-
"@warlock.js/cascade": "4.
|
|
29
|
-
"@warlock.js/core": "4.
|
|
30
|
-
"@warlock.js/logger": "4.
|
|
31
|
-
"@warlock.js/seal": "4.
|
|
26
|
+
"@warlock.js/fs": "4.9.1",
|
|
27
|
+
"@warlock.js/cache": "4.9.1",
|
|
28
|
+
"@warlock.js/cascade": "4.9.1",
|
|
29
|
+
"@warlock.js/core": "4.9.1",
|
|
30
|
+
"@warlock.js/logger": "4.9.1",
|
|
31
|
+
"@warlock.js/seal": "4.9.1"
|
|
32
32
|
},
|
|
33
|
-
"version": "4.
|
|
33
|
+
"version": "4.9.1",
|
|
34
34
|
"type": "module",
|
|
35
35
|
"main": "./esm/index.mjs",
|
|
36
36
|
"module": "./esm/index.mjs",
|