@warlock.js/auth 4.2.11 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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\";\nimport type { ChildModel } from \"@warlock.js/cascade\";\nimport { config, hashPassword, verifyPassword } from \"@warlock.js/core\";\nimport ms from \"ms\";\nimport type { AccessTokenOutput, DeviceInfo, LoginResult, TokenPair } from \"../contracts/types\";\nimport { AccessToken } from \"../models/access-token\";\nimport type { Auth } from \"../models/auth.model\";\nimport { RefreshToken } from \"../models/refresh-token\";\nimport { authConfig } from \"./auth-config\";\nimport { authEvents } from \"./auth-events\";\nimport { jwt } from \"./jwt\";\n\nclass AuthService {\n /**\n * Resolve the active access-token model — the package default, or a subclass\n * an app registered under `config.auth.accessToken.model` (e.g. to add a\n * tenant column). The service never references the concrete class directly so\n * an override is a pure config change.\n */\n private get accessTokenModel(): typeof AccessToken {\n return config.key(\"auth.accessToken.model\", AccessToken);\n }\n\n /**\n * Resolve the active refresh-token model (default or registered override).\n */\n private get refreshTokenModel(): typeof RefreshToken {\n return config.key(\"auth.refreshToken.model\", RefreshToken);\n }\n\n /**\n * Build the default access-token JWT payload from a user.\n */\n public buildAccessTokenPayload(user: Auth) {\n return {\n id: user.id,\n userType: user.userType,\n created_at: Date.now(),\n };\n }\n\n /**\n * Sign + persist an access token for the user and return the token with its\n * expiry. The expiry is computed locally from `expiresIn` rather than by\n * re-verifying the token we just signed.\n */\n public async generateAccessToken(user: Auth, payload?: any): Promise<AccessTokenOutput> {\n const data = payload || this.buildAccessTokenPayload(user);\n const expiresInConfig = authConfig.accessToken.expiresIn();\n const expiresIn = expiresInConfig\n ? ms(expiresInConfig as ms.StringValue)\n : ms(\"1h\"); // default 1 hour\n\n const token = await jwt.generate(data, { expiresIn });\n const expiresAt = new Date(Date.now() + expiresIn);\n\n await this.accessTokenModel.issue(user, token, expiresAt);\n\n return { token, expiresAt: expiresAt.toISOString() };\n }\n\n /**\n * Sign + persist a refresh token for the user (enforcing the per-user cap\n * first). Resolves to `undefined` when refresh tokens are disabled in config.\n */\n public async createRefreshToken(\n user: Auth,\n deviceInfo?: DeviceInfo,\n ): Promise<RefreshToken | undefined> {\n if (!authConfig.refreshToken.enabled()) return;\n\n const familyId = deviceInfo?.familyId || Random.string(32);\n\n const payload = {\n userId: user.id,\n userType: user.userType,\n familyId,\n };\n\n const expiresIn = ms(authConfig.refreshToken.expiresIn() as ms.StringValue);\n const expiresAt = new Date(Date.now() + expiresIn).toISOString();\n\n await this.refreshTokenModel.enforceMax(user, authConfig.refreshToken.maxPerUser());\n\n const token = await jwt.generateRefreshToken(payload, { expiresIn });\n\n return this.refreshTokenModel.issue(user, token, { familyId, expiresAt, deviceInfo });\n }\n\n /**\n * Issue both an access and a refresh token, emitting the creation events.\n */\n public async createTokenPair(user: Auth, deviceInfo?: DeviceInfo): Promise<TokenPair> {\n const accessToken = await this.generateAccessToken(user, deviceInfo?.payload);\n const refreshToken = await this.createRefreshToken(user, deviceInfo);\n\n const tokenPair: TokenPair = {\n accessToken,\n refreshToken: refreshToken\n ? {\n token: refreshToken.get(\"token\"),\n expiresAt: refreshToken.get(\"expires_at\"),\n }\n : undefined,\n };\n\n authEvents.emit(\"token.created\", user, tokenPair);\n\n if (refreshToken) {\n authEvents.emit(\"session.created\", user, refreshToken, deviceInfo);\n }\n\n return tokenPair;\n }\n\n /**\n * Exchange a refresh token for a new token pair, with rotation + replay\n * detection. A concurrent reuse of the same token loses the atomic revoke and\n * is treated as a breach — the whole family is revoked and the request fails.\n */\n public async refreshTokens(\n refreshTokenString: string,\n deviceInfo?: DeviceInfo,\n ): Promise<TokenPair | null> {\n try {\n const decoded = await jwt.verifyRefreshToken<{\n userId: number;\n userType: string;\n familyId: string;\n }>(refreshTokenString);\n\n if (!decoded) return null;\n\n const refreshToken = await this.refreshTokenModel.findByToken(refreshTokenString);\n\n if (!refreshToken?.isValid) {\n // Already-invalid token presented → likely a replayed (rotated) token.\n if (refreshToken) {\n await this.revokeTokenFamily(refreshToken.familyId);\n }\n\n return null;\n }\n\n const UserModel = config.key(`auth.userType.${decoded.userType}`);\n\n if (!UserModel) return null;\n\n const user = (await UserModel.find(decoded.userId)) as Auth | null;\n\n if (!user) return null;\n\n const rotationEnabled = authConfig.refreshToken.rotation();\n\n if (rotationEnabled) {\n const won = await refreshToken.revokeIfActive();\n\n if (!won) {\n // A concurrent request already rotated this token (reuse / replay).\n await this.revokeTokenFamily(refreshToken.familyId);\n\n return null;\n }\n } else {\n await refreshToken.markAsUsed();\n }\n\n const newTokenPair = await this.createTokenPair(user, {\n ...deviceInfo,\n familyId: refreshToken.familyId,\n });\n\n authEvents.emit(\"token.refreshed\", user, newTokenPair, refreshToken);\n\n return newTokenPair;\n } catch {\n return null;\n }\n }\n\n /**\n * Verify a plaintext password against a stored hash.\n */\n public async verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean> {\n return verifyPassword(plainPassword, hashedPassword);\n }\n\n /**\n * Hash a plaintext password.\n */\n public async hashPassword(password: string): Promise<string> {\n return hashPassword(password);\n }\n\n /**\n * Resolve a user by credentials, verifying the password. Returns `null` on a\n * missing user or a wrong password, emitting `login.failed` either way.\n */\n public async attemptLogin<T extends Auth>(Model: ChildModel<T>, data: any): Promise<T | null> {\n const { password, ...otherData } = data;\n\n authEvents.emit(\"login.attempt\", otherData);\n\n const user = (await Model.first(otherData)) as T | null;\n\n if (!user) {\n authEvents.emit(\"login.failed\", otherData, \"User not found\");\n\n return null;\n }\n\n if (!(await this.verifyPassword(password, user.string(\"password\")!))) {\n authEvents.emit(\"login.failed\", otherData, \"Invalid password\");\n\n return null;\n }\n\n return user;\n }\n\n /**\n * Full login flow: validate credentials, issue tokens, emit events. Returns\n * the user + token pair on success, `null` on failure.\n */\n public async login<T extends Auth>(\n Model: ChildModel<T>,\n credentials: any,\n deviceInfo?: DeviceInfo,\n ): Promise<LoginResult<T> | null> {\n const user = await this.attemptLogin(Model, credentials);\n\n if (!user) {\n return null;\n }\n\n if (!authConfig.refreshToken.enabled()) {\n const accessToken = await this.generateAccessToken(user, deviceInfo?.payload);\n\n return { user, tokens: { accessToken } };\n }\n\n const tokens = await this.createTokenPair(user, deviceInfo);\n\n authEvents.emit(\"login.success\", user, tokens, deviceInfo);\n\n return { user, tokens } as LoginResult<T>;\n }\n\n /**\n * Log a user out.\n *\n * @param accessToken - access token string to revoke (optional)\n * @param refreshToken - refresh token string to revoke (optional)\n *\n * When no refresh token is supplied, `config.auth.refreshToken.logoutWithoutToken`\n * decides the behavior: `\"revoke-all\"` (default, fail-safe) revokes every\n * refresh token; `\"error\"` requires the caller to pass one.\n */\n public async logout(user: Auth, accessToken?: string, refreshToken?: string): Promise<void> {\n if (accessToken) {\n await this.removeAccessToken(user, accessToken);\n }\n\n if (refreshToken) {\n const token = await this.refreshTokenModel.findForUser(user, refreshToken);\n\n if (token) {\n await token.revoke();\n authEvents.emit(\"session.destroyed\", user, token);\n }\n } else {\n const behavior = authConfig.refreshToken.logoutWithoutToken();\n\n if (behavior === \"error\") {\n throw new Error(\"Refresh token required for logout\");\n }\n\n await this.revokeAllTokens(user);\n authEvents.emit(\"logout.failsafe\", user);\n }\n\n authEvents.emit(\"logout\", user);\n }\n\n /**\n * Remove a specific access token belonging to the user.\n */\n public async removeAccessToken(user: Auth, token: string): Promise<void> {\n await this.accessTokenModel.deleteForUser(user, token);\n }\n\n /**\n * Remove every access token belonging to the user.\n */\n public async removeAllAccessTokens(user: Auth): Promise<void> {\n await this.accessTokenModel.deleteAllForUser(user);\n }\n\n /**\n * Remove a specific refresh token belonging to the user.\n */\n public async removeRefreshToken(user: Auth, token: string): Promise<void> {\n await this.refreshTokenModel.deleteForUser(user, token);\n }\n\n /**\n * Revoke every active refresh token for the user and delete their access\n * tokens — \"log out of all devices\". Revocation is a single bulk update; an\n * event fires per revoked token.\n */\n public async revokeAllTokens(user: Auth): Promise<void> {\n const revokedTokens = await this.refreshTokenModel.revokeAllFor(user);\n\n for (const token of revokedTokens) {\n authEvents.emit(\"token.revoked\", user, token);\n }\n\n await this.removeAllAccessTokens(user);\n\n authEvents.emit(\"logout.all\", user);\n }\n\n /**\n * Revoke an entire token family — rotation breach containment.\n */\n public async revokeTokenFamily(familyId: string): Promise<void> {\n const revokedTokens = await this.refreshTokenModel.revokeFamily(familyId);\n\n authEvents.emit(\"token.familyRevoked\", familyId, revokedTokens);\n }\n\n /**\n * Delete expired tokens (refresh + access). Emits `token.expired` per refresh\n * token and `cleanup.completed` with the refresh count. Drives the\n * `auth.cleanup` CLI command.\n */\n public async cleanupExpiredTokens(): Promise<number> {\n const expiredTokens = await this.refreshTokenModel.purgeExpired();\n\n for (const token of expiredTokens) {\n authEvents.emit(\"token.expired\", token);\n }\n\n await this.accessTokenModel.purgeExpired();\n\n authEvents.emit(\"cleanup.completed\", expiredTokens.length);\n\n return expiredTokens.length;\n }\n\n /**\n * Active, unexpired sessions for the user, newest first.\n */\n public async getActiveSessions(user: Auth): Promise<RefreshToken[]> {\n return this.refreshTokenModel.activeFor(user);\n }\n}\n\nexport const authService = new AuthService();\n"],"mappings":"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"}
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 +1 @@
1
- {"version":3,"file":"auth-error-codes.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/utils/auth-error-codes.ts"],"sourcesContent":["export enum AuthErrorCodes {\n /**\n * Missing Access Token Error Code EC001\n * EC001 = Missing Access Token\n */\n MissingAccessToken = \"EC001\", // Error Code 001\n /**\n * Invalid Access Token Error Code EC002\n * EC002 = Invalid Access Token\n */\n InvalidAccessToken = \"EC002\", // Error Code 002\n /**\n * Unauthorized Error Code EC003\n * EC003 = Unauthorized\n */\n Unauthorized = \"EC003\", // Error Code 003\n /**\n * Too Many Attempts Error Code EC004\n * EC004 = Too Many Attempts — issued by the login-throttle middleware once\n * a per-account or per-IP failure counter trips its threshold.\n */\n TooManyAttempts = \"EC004\", // Error Code 004\n}\n"],"mappings":";AAAA,IAAY,iBAAL;;;;;CAKL;;;;;CAKA;;;;;CAKA;;;;;;CAMA;;AACF"}
1
+ {"version":3,"file":"auth-error-codes.mjs","names":[],"sources":["../../../../../../../@warlock.js/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"}