@warlock.js/auth 5.11.0 → 5.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/esm/contracts/index.d.mts +3 -2
- package/esm/contracts/request-locals.d.mts +43 -0
- package/esm/contracts/request-locals.d.mts.map +1 -0
- package/esm/contracts/types.d.mts +66 -1
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/index.d.mts +7 -3
- package/esm/index.mjs +3 -1
- package/esm/middleware/auth.middleware.d.mts.map +1 -1
- package/esm/middleware/auth.middleware.mjs +13 -2
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/csrf-origin-check.d.mts +40 -0
- package/esm/middleware/csrf-origin-check.d.mts.map +1 -0
- package/esm/middleware/csrf-origin-check.mjs +105 -0
- package/esm/middleware/csrf-origin-check.mjs.map +1 -0
- package/esm/middleware/index.d.mts +1 -0
- package/esm/middleware/index.mjs +1 -0
- package/esm/services/auth-config.mjs +12 -0
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +41 -1
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +58 -0
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/current-user.d.mts +21 -0
- package/esm/services/current-user.d.mts.map +1 -0
- package/esm/services/current-user.mjs +24 -0
- package/esm/services/current-user.mjs.map +1 -0
- package/esm/services/index.d.mts +1 -0
- package/esm/services/index.mjs +1 -0
- package/esm/utils/auth-error-codes.d.mts +10 -1
- package/esm/utils/auth-error-codes.d.mts.map +1 -1
- package/esm/utils/auth-error-codes.mjs +8 -0
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/llms-full.txt +89 -19
- package/package.json +7 -7
- package/skills/handle-login-and-logout/SKILL.md +1 -1
- package/skills/overview/SKILL.md +1 -1
- package/skills/protect-routes/SKILL.md +71 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-config.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth-config.ts"],"sourcesContent":["import { config } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport ms from \"ms\";\r\nimport type { CanAuthenticate, LogoutWithoutTokenBehavior } from \"../contracts/types\";\r\nimport type { Auth } from \"../models/auth.model\";\r\n\r\nconst warnedLegacyKeys = new Set<string>();\r\n\r\nconst permitAuthentication: CanAuthenticate = (): boolean => true;\r\n\r\n/**\r\n * `ms` accepts anything at runtime and answers `undefined` for a string it\r\n * cannot parse (`\"30dayz\"`), a *formatted string* for a number (`2592000` ⇒\r\n * `\"43m\"`), and throws for `\"\"`. Its published types claim the far narrower\r\n * `(value: ms.StringValue) => number`, which is exactly what let an\r\n * `as ms.StringValue` cast smuggle arbitrary config text past the compiler and\r\n * hand `undefined` to the JWT signer. Declare the honest runtime signature once,\r\n * here, so every caller is forced to deal with the `undefined`.\r\n */\r\nconst parseMs = ms as unknown as (value: unknown) => number | string | undefined;\r\n\r\n/**\r\n * Turn a configured lifetime into a positive number of milliseconds, or throw\r\n * naming the key.\r\n *\r\n * A token lifetime is a security boundary, so an unusable value is a hard error\r\n * rather than a silent fallback: substituting a default would trade one\r\n * unintended lifetime for another, just as quietly — and quiet is the whole\r\n * problem. Every non-positive, non-finite or non-numeric parse is rejected:\r\n *\r\n * - `\"30dayz\"` / `\"thirty days\"` ⇒ `undefined` ⇒ a JWT signed with **no `exp`\r\n * claim** and an `Invalid Date` written to the token row.\r\n * - `\"0d\"` ⇒ parses cleanly to `0`, which `fast-jwt` treats as falsy and also\r\n * emits with **no `exp` claim** — it survives any guard that only rejects\r\n * `undefined`. `\"-1h\"` signs an already-expired token.\r\n * - `2592000` (a bare number) ⇒ `ms` *formats* it as `\"43m\"`, a string, which\r\n * then turns `Date.now() + expiresIn` into an `Invalid Date`.\r\n */\r\nfunction parseDuration(key: string, raw: unknown): number {\r\n let parsed: number | string | undefined;\r\n\r\n try {\r\n parsed = parseMs(raw);\r\n } catch {\r\n // ms throws (rather than returning undefined) for \"\" and non-strings\r\n parsed = undefined;\r\n }\r\n\r\n if (typeof parsed !== \"number\" || !Number.isFinite(parsed) || parsed <= 0) {\r\n throw new Error(\r\n `auth.${key}: ${JSON.stringify(raw)} is not a valid ms duration — ` +\r\n `use a positive duration string such as \"1h\", \"7d\", or NO_EXPIRATION.`,\r\n );\r\n }\r\n\r\n return parsed;\r\n}\r\n\r\n/** Access-token lifetime used when nothing is configured. */\r\nconst DEFAULT_ACCESS_TOKEN_EXPIRES_IN = \"1h\";\r\n\r\n/**\r\n * Resolve an auth setting, preferring the new `auth.accessToken.*` /\r\n * `auth.refreshToken.*` key and falling back to the deprecated `auth.jwt.*`\r\n * shape — warning once per legacy key. Returns `fallback` when neither is set.\r\n *\r\n * This is the backward-compatible shim that lets existing `auth.jwt.*` configs\r\n * keep working after the config split.\r\n */\r\nfunction resolve<T>(newKey: string, legacyKey: string, fallback?: T): T {\r\n const fromNew = config.key(`auth.${newKey}`);\r\n\r\n if (fromNew !== undefined && fromNew !== null) {\r\n return fromNew as T;\r\n }\r\n\r\n const fromLegacy = config.key(`auth.${legacyKey}`);\r\n\r\n if (fromLegacy !== undefined && fromLegacy !== null) {\r\n if (!warnedLegacyKeys.has(legacyKey)) {\r\n warnedLegacyKeys.add(legacyKey);\r\n log.warn(\"auth\", \"config-deprecation\", `auth.${legacyKey} is deprecated — use auth.${newKey}`);\r\n }\r\n\r\n return fromLegacy as T;\r\n }\r\n\r\n return fallback as T;\r\n}\r\n\r\n/**\r\n * Typed, backward-compatible access to auth configuration. The service and the\r\n * jwt signer read configuration exclusively through here, so the new split\r\n * config and the legacy `auth.jwt.*` shape both resolve the same way.\r\n */\r\nexport const authConfig = {\r\n canAuthenticate: async (user: Auth): Promise<boolean> => {\r\n const canAuthenticate = config.key<CanAuthenticate>(\r\n \"auth.canAuthenticate\",\r\n permitAuthentication,\r\n );\r\n\r\n return canAuthenticate(user);\r\n },\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 * Lifetime in milliseconds — the only form a token issuer may use. Defaults\r\n * to 1 hour when unset; throws naming the key when the configured value is\r\n * not a positive `ms` duration. See {@link parseDuration}.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\r\n \"accessToken.expiresIn\",\r\n authConfig.accessToken.expiresIn() ?? DEFAULT_ACCESS_TOKEN_EXPIRES_IN,\r\n ),\r\n },\r\n pageAuth: {\r\n /**\r\n * Login path a guarded PAGE route redirects a logged-out browser to.\r\n *\r\n * Unset (the default) ⇒ page-route auth failures keep the JSON 401 API\r\n * contract, so existing apps see NO behavior change. Set it (e.g. \"/login\")\r\n * to opt a browser-facing app into login-redirect UX: a logged-out human\r\n * navigating to a guarded page lands on the login screen instead of reading\r\n * a raw JSON error blob. Finding b9ab9804.\r\n */\r\n loginPath: (): string | undefined => config.key(\"auth.pageAuth.loginPath\"),\r\n /**\r\n * Query parameter the redirect carries the originally-requested path in, so\r\n * the login flow can send the user back where they were headed. Defaults to\r\n * `\"returnUrl\"`.\r\n */\r\n returnUrlParam: (): string => config.key(\"auth.pageAuth.returnUrlParam\", \"returnUrl\"),\r\n },\r\n refreshToken: {\r\n /** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty ⇒ fall back to the access secret. */\r\n secret: (): string | undefined => resolve(\"refreshToken.secret\", \"jwt.refresh.secret\"),\r\n /** Whether refresh tokens are enabled (legacy: `auth.jwt.refresh.enabled`). */\r\n enabled: (): boolean => resolve(\"refreshToken.enabled\", \"jwt.refresh.enabled\", true),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.refresh.expiresIn`). */\r\n expiresIn: (): string => resolve(\"refreshToken.expiresIn\", \"jwt.refresh.expiresIn\", \"7d\"),\r\n /**\r\n * Lifetime in milliseconds — the only form a token issuer may use. Throws\r\n * naming the key when the configured value is not a positive `ms` duration.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\"refreshToken.expiresIn\", authConfig.refreshToken.expiresIn()),\r\n /** Rotate-on-use (legacy: `auth.jwt.refresh.rotation`). */\r\n rotation: (): boolean => resolve(\"refreshToken.rotation\", \"jwt.refresh.rotation\", true),\r\n /** Max active tokens per user (legacy: `auth.jwt.refresh.maxPerUser`). */\r\n maxPerUser: (): number => resolve(\"refreshToken.maxPerUser\", \"jwt.refresh.maxPerUser\", 5),\r\n /** Logout-without-token behavior (legacy: `auth.jwt.refresh.logoutWithoutToken`). */\r\n logoutWithoutToken: (): LogoutWithoutTokenBehavior =>\r\n resolve(\"refreshToken.logoutWithoutToken\", \"jwt.refresh.logoutWithoutToken\", \"revoke-all\"),\r\n },\r\n};\r\n"],"mappings":";;;;;AAOA,MAAM,mCAAmB,IAAI,IAAY;AAEzC,MAAM,6BAAuD;;;;;;;;;;AAW7D,MAAM,UAAU;;;;;;;;;;;;;;;;;;AAmBhB,SAAS,cAAc,KAAa,KAAsB;CACxD,IAAI;CAEJ,IAAI;EACF,SAAS,QAAQ,GAAG;CACtB,QAAQ;EAEN,SAAS;CACX;CAEA,IAAI,OAAO,WAAW,YAAY,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GACtE,MAAM,IAAI,MACR,QAAQ,IAAI,IAAI,KAAK,UAAU,GAAG,EAAE,mGAEtC;CAGF,OAAO;AACT;;AAGA,MAAM,kCAAkC;;;;;;;;;AAUxC,SAAS,QAAW,QAAgB,WAAmB,UAAiB;CACtE,MAAM,UAAU,OAAO,IAAI,QAAQ,QAAQ;CAE3C,IAAI,YAAY,UAAa,YAAY,MACvC,OAAO;CAGT,MAAM,aAAa,OAAO,IAAI,QAAQ,WAAW;CAEjD,IAAI,eAAe,UAAa,eAAe,MAAM;EACnD,IAAI,CAAC,iBAAiB,IAAI,SAAS,GAAG;GACpC,iBAAiB,IAAI,SAAS;GAC9B,IAAI,KAAK,QAAQ,sBAAsB,QAAQ,UAAU,4BAA4B,QAAQ;EAC/F;EAEA,OAAO;CACT;CAEA,OAAO;AACT;;;;;;AAOA,MAAa,aAAa;CACxB,iBAAiB,OAAO,SAAiC;EAMvD,OALwB,OAAO,IAC7B,wBACA,oBAGmB,CAAC,CAAC,IAAI;CAC7B;CACA,aAAa;;EAEX,cAAsB;GACpB,MAAM,SAAS,QAA4B,sBAAsB,YAAY;GAE7E,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,iEAAiE;GAGnF,OAAO;EACT;;EAEA,iBAA4B,QAAQ,yBAAyB,iBAAiB,OAAO;;EAErF,iBAAqC,QAAQ,yBAAyB,eAAe;;;;;;EAMrF,mBACE,cACE,yBACA,WAAW,YAAY,UAAU,KAAK,+BACxC;CACJ;CACA,UAAU;;;;;;;;;;EAUR,iBAAqC,OAAO,IAAI,yBAAyB;;;;;;EAMzE,sBAA8B,OAAO,IAAI,gCAAgC,WAAW;CACtF;CACA,cAAc;;EAEZ,cAAkC,QAAQ,uBAAuB,oBAAoB;;EAErF,eAAwB,QAAQ,wBAAwB,uBAAuB,IAAI;;EAEnF,iBAAyB,QAAQ,0BAA0B,yBAAyB,IAAI;;;;;EAKxF,mBACE,cAAc,0BAA0B,WAAW,aAAa,UAAU,CAAC;;EAE7E,gBAAyB,QAAQ,yBAAyB,wBAAwB,IAAI;;EAEtF,kBAA0B,QAAQ,2BAA2B,0BAA0B,CAAC;;EAExF,0BACE,QAAQ,mCAAmC,kCAAkC,YAAY;CAC7F;AACF"}
|
|
1
|
+
{"version":3,"file":"auth-config.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth-config.ts"],"sourcesContent":["import { config } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport ms from \"ms\";\r\nimport type { CanAuthenticate, LogoutWithoutTokenBehavior } from \"../contracts/types\";\r\nimport type { Auth } from \"../models/auth.model\";\r\n\r\nconst warnedLegacyKeys = new Set<string>();\r\n\r\nconst permitAuthentication: CanAuthenticate = (): boolean => true;\r\n\r\n/**\r\n * `ms` accepts anything at runtime and answers `undefined` for a string it\r\n * cannot parse (`\"30dayz\"`), a *formatted string* for a number (`2592000` ⇒\r\n * `\"43m\"`), and throws for `\"\"`. Its published types claim the far narrower\r\n * `(value: ms.StringValue) => number`, which is exactly what let an\r\n * `as ms.StringValue` cast smuggle arbitrary config text past the compiler and\r\n * hand `undefined` to the JWT signer. Declare the honest runtime signature once,\r\n * here, so every caller is forced to deal with the `undefined`.\r\n */\r\nconst parseMs = ms as unknown as (value: unknown) => number | string | undefined;\r\n\r\n/**\r\n * Turn a configured lifetime into a positive number of milliseconds, or throw\r\n * naming the key.\r\n *\r\n * A token lifetime is a security boundary, so an unusable value is a hard error\r\n * rather than a silent fallback: substituting a default would trade one\r\n * unintended lifetime for another, just as quietly — and quiet is the whole\r\n * problem. Every non-positive, non-finite or non-numeric parse is rejected:\r\n *\r\n * - `\"30dayz\"` / `\"thirty days\"` ⇒ `undefined` ⇒ a JWT signed with **no `exp`\r\n * claim** and an `Invalid Date` written to the token row.\r\n * - `\"0d\"` ⇒ parses cleanly to `0`, which `fast-jwt` treats as falsy and also\r\n * emits with **no `exp` claim** — it survives any guard that only rejects\r\n * `undefined`. `\"-1h\"` signs an already-expired token.\r\n * - `2592000` (a bare number) ⇒ `ms` *formats* it as `\"43m\"`, a string, which\r\n * then turns `Date.now() + expiresIn` into an `Invalid Date`.\r\n */\r\nfunction parseDuration(key: string, raw: unknown): number {\r\n let parsed: number | string | undefined;\r\n\r\n try {\r\n parsed = parseMs(raw);\r\n } catch {\r\n // ms throws (rather than returning undefined) for \"\" and non-strings\r\n parsed = undefined;\r\n }\r\n\r\n if (typeof parsed !== \"number\" || !Number.isFinite(parsed) || parsed <= 0) {\r\n throw new Error(\r\n `auth.${key}: ${JSON.stringify(raw)} is not a valid ms duration — ` +\r\n `use a positive duration string such as \"1h\", \"7d\", or NO_EXPIRATION.`,\r\n );\r\n }\r\n\r\n return parsed;\r\n}\r\n\r\n/** Access-token lifetime used when nothing is configured. */\r\nconst DEFAULT_ACCESS_TOKEN_EXPIRES_IN = \"1h\";\r\n\r\n/**\r\n * Resolve an auth setting, preferring the new `auth.accessToken.*` /\r\n * `auth.refreshToken.*` key and falling back to the deprecated `auth.jwt.*`\r\n * shape — warning once per legacy key. Returns `fallback` when neither is set.\r\n *\r\n * This is the backward-compatible shim that lets existing `auth.jwt.*` configs\r\n * keep working after the config split.\r\n */\r\nfunction resolve<T>(newKey: string, legacyKey: string, fallback?: T): T {\r\n const fromNew = config.key(`auth.${newKey}`);\r\n\r\n if (fromNew !== undefined && fromNew !== null) {\r\n return fromNew as T;\r\n }\r\n\r\n const fromLegacy = config.key(`auth.${legacyKey}`);\r\n\r\n if (fromLegacy !== undefined && fromLegacy !== null) {\r\n if (!warnedLegacyKeys.has(legacyKey)) {\r\n warnedLegacyKeys.add(legacyKey);\r\n log.warn(\"auth\", \"config-deprecation\", `auth.${legacyKey} is deprecated — use auth.${newKey}`);\r\n }\r\n\r\n return fromLegacy as T;\r\n }\r\n\r\n return fallback as T;\r\n}\r\n\r\n/**\r\n * Typed, backward-compatible access to auth configuration. The service and the\r\n * jwt signer read configuration exclusively through here, so the new split\r\n * config and the legacy `auth.jwt.*` shape both resolve the same way.\r\n */\r\nexport const authConfig = {\r\n canAuthenticate: async (user: Auth): Promise<boolean> => {\r\n const canAuthenticate = config.key<CanAuthenticate>(\r\n \"auth.canAuthenticate\",\r\n permitAuthentication,\r\n );\r\n\r\n return canAuthenticate(user);\r\n },\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 * Lifetime in milliseconds — the only form a token issuer may use. Defaults\r\n * to 1 hour when unset; throws naming the key when the configured value is\r\n * not a positive `ms` duration. See {@link parseDuration}.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\r\n \"accessToken.expiresIn\",\r\n authConfig.accessToken.expiresIn() ?? DEFAULT_ACCESS_TOKEN_EXPIRES_IN,\r\n ),\r\n },\r\n pageAuth: {\r\n /**\r\n * Login path a guarded PAGE route redirects a logged-out browser to.\r\n *\r\n * Unset (the default) ⇒ page-route auth failures keep the JSON 401 API\r\n * contract, so existing apps see NO behavior change. Set it (e.g. \"/login\")\r\n * to opt a browser-facing app into login-redirect UX: a logged-out human\r\n * navigating to a guarded page lands on the login screen instead of reading\r\n * a raw JSON error blob. Finding b9ab9804.\r\n */\r\n loginPath: (): string | undefined => config.key(\"auth.pageAuth.loginPath\"),\r\n /**\r\n * Query parameter the redirect carries the originally-requested path in, so\r\n * the login flow can send the user back where they were headed. Defaults to\r\n * `\"returnUrl\"`.\r\n */\r\n returnUrlParam: (): string => config.key(\"auth.pageAuth.returnUrlParam\", \"returnUrl\"),\r\n },\r\n cookie: {\r\n /** Cookie name `setAuthCookie`/`clearAuthCookie` use. @default \"access_token\" */\r\n name: (): string => config.key(\"auth.cookie.name\", \"access_token\"),\r\n /** Cookie `Path` attribute `setAuthCookie`/`clearAuthCookie` use. @default \"/\" */\r\n path: (): string => config.key(\"auth.cookie.path\", \"/\"),\r\n },\r\n csrf: {\r\n /**\r\n * Extra origins allowed on a cookie-authenticated unsafe-method request,\r\n * beyond the request's own origin. @default []\r\n */\r\n allowedOrigins: (): string[] => config.key(\"auth.csrf.allowedOrigins\", []),\r\n },\r\n refreshToken: {\r\n /** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty ⇒ fall back to the access secret. */\r\n secret: (): string | undefined => resolve(\"refreshToken.secret\", \"jwt.refresh.secret\"),\r\n /** Whether refresh tokens are enabled (legacy: `auth.jwt.refresh.enabled`). */\r\n enabled: (): boolean => resolve(\"refreshToken.enabled\", \"jwt.refresh.enabled\", true),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.refresh.expiresIn`). */\r\n expiresIn: (): string => resolve(\"refreshToken.expiresIn\", \"jwt.refresh.expiresIn\", \"7d\"),\r\n /**\r\n * Lifetime in milliseconds — the only form a token issuer may use. Throws\r\n * naming the key when the configured value is not a positive `ms` duration.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\"refreshToken.expiresIn\", authConfig.refreshToken.expiresIn()),\r\n /** Rotate-on-use (legacy: `auth.jwt.refresh.rotation`). */\r\n rotation: (): boolean => resolve(\"refreshToken.rotation\", \"jwt.refresh.rotation\", true),\r\n /** Max active tokens per user (legacy: `auth.jwt.refresh.maxPerUser`). */\r\n maxPerUser: (): number => resolve(\"refreshToken.maxPerUser\", \"jwt.refresh.maxPerUser\", 5),\r\n /** Logout-without-token behavior (legacy: `auth.jwt.refresh.logoutWithoutToken`). */\r\n logoutWithoutToken: (): LogoutWithoutTokenBehavior =>\r\n resolve(\"refreshToken.logoutWithoutToken\", \"jwt.refresh.logoutWithoutToken\", \"revoke-all\"),\r\n },\r\n};\r\n"],"mappings":";;;;;AAOA,MAAM,mCAAmB,IAAI,IAAY;AAEzC,MAAM,6BAAuD;;;;;;;;;;AAW7D,MAAM,UAAU;;;;;;;;;;;;;;;;;;AAmBhB,SAAS,cAAc,KAAa,KAAsB;CACxD,IAAI;CAEJ,IAAI;EACF,SAAS,QAAQ,GAAG;CACtB,QAAQ;EAEN,SAAS;CACX;CAEA,IAAI,OAAO,WAAW,YAAY,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GACtE,MAAM,IAAI,MACR,QAAQ,IAAI,IAAI,KAAK,UAAU,GAAG,EAAE,mGAEtC;CAGF,OAAO;AACT;;AAGA,MAAM,kCAAkC;;;;;;;;;AAUxC,SAAS,QAAW,QAAgB,WAAmB,UAAiB;CACtE,MAAM,UAAU,OAAO,IAAI,QAAQ,QAAQ;CAE3C,IAAI,YAAY,UAAa,YAAY,MACvC,OAAO;CAGT,MAAM,aAAa,OAAO,IAAI,QAAQ,WAAW;CAEjD,IAAI,eAAe,UAAa,eAAe,MAAM;EACnD,IAAI,CAAC,iBAAiB,IAAI,SAAS,GAAG;GACpC,iBAAiB,IAAI,SAAS;GAC9B,IAAI,KAAK,QAAQ,sBAAsB,QAAQ,UAAU,4BAA4B,QAAQ;EAC/F;EAEA,OAAO;CACT;CAEA,OAAO;AACT;;;;;;AAOA,MAAa,aAAa;CACxB,iBAAiB,OAAO,SAAiC;EAMvD,OALwB,OAAO,IAC7B,wBACA,oBAGmB,CAAC,CAAC,IAAI;CAC7B;CACA,aAAa;;EAEX,cAAsB;GACpB,MAAM,SAAS,QAA4B,sBAAsB,YAAY;GAE7E,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,iEAAiE;GAGnF,OAAO;EACT;;EAEA,iBAA4B,QAAQ,yBAAyB,iBAAiB,OAAO;;EAErF,iBAAqC,QAAQ,yBAAyB,eAAe;;;;;;EAMrF,mBACE,cACE,yBACA,WAAW,YAAY,UAAU,KAAK,+BACxC;CACJ;CACA,UAAU;;;;;;;;;;EAUR,iBAAqC,OAAO,IAAI,yBAAyB;;;;;;EAMzE,sBAA8B,OAAO,IAAI,gCAAgC,WAAW;CACtF;CACA,QAAQ;;EAEN,YAAoB,OAAO,IAAI,oBAAoB,cAAc;;EAEjE,YAAoB,OAAO,IAAI,oBAAoB,GAAG;CACxD;CACA,MAAM;;;;;AAKJ,sBAAgC,OAAO,IAAI,4BAA4B,CAAC,CAAC,EAC3E;CACA,cAAc;;EAEZ,cAAkC,QAAQ,uBAAuB,oBAAoB;;EAErF,eAAwB,QAAQ,wBAAwB,uBAAuB,IAAI;;EAEnF,iBAAyB,QAAQ,0BAA0B,yBAAyB,IAAI;;;;;EAKxF,mBACE,cAAc,0BAA0B,WAAW,aAAa,UAAU,CAAC;;EAE7E,gBAAyB,QAAQ,yBAAyB,wBAAwB,IAAI;;EAEtF,kBAA0B,QAAQ,2BAA2B,0BAA0B,CAAC;;EAExF,0BACE,QAAQ,mCAAmC,kCAAkC,YAAY;CAC7F;AACF"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Auth } from "../models/auth.model.mjs";
|
|
2
2
|
import { AccessToken } from "../models/access-token/access-token.model.mjs";
|
|
3
|
-
import { AccessTokenOutput, AuthCredentials, DeviceInfo, LoginResult, TokenPair } from "../contracts/types.mjs";
|
|
3
|
+
import { AccessTokenOutput, AuthCredentials, ClearAuthCookieOptions, DeviceInfo, LoginResult, SetAuthCookieOptions, TokenPair } from "../contracts/types.mjs";
|
|
4
4
|
import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
|
|
5
|
+
import { Response } from "@warlock.js/core";
|
|
5
6
|
import { ChildModel } from "@warlock.js/cascade";
|
|
6
7
|
|
|
7
8
|
//#region ../auth/src/services/auth.service.d.ts
|
|
@@ -135,6 +136,45 @@ declare class AuthService {
|
|
|
135
136
|
* Active, unexpired sessions for the user, newest first.
|
|
136
137
|
*/
|
|
137
138
|
getActiveSessions(user: Auth): Promise<RefreshToken[]>;
|
|
139
|
+
/**
|
|
140
|
+
* `Max-Age` (seconds) `setAuthCookie` should apply, derived from an
|
|
141
|
+
* `AccessTokenOutput`'s `expiresAt` — or `undefined` for a bare token
|
|
142
|
+
* string (a session cookie) or an unparseable `expiresAt`.
|
|
143
|
+
*/
|
|
144
|
+
private cookieMaxAgeFromToken;
|
|
145
|
+
/**
|
|
146
|
+
* Write the auth cookie on `response` — the write side of the `cookie:<name>`
|
|
147
|
+
* token source `authMiddleware([], "cookie:<name>")` already reads. An explicit
|
|
148
|
+
* app-controller call, never a side effect of {@link login}, so an existing
|
|
149
|
+
* bearer-only app never starts emitting `Set-Cookie` just by upgrading.
|
|
150
|
+
*
|
|
151
|
+
* Attribute flags (`HttpOnly`, `SameSite=Lax`, `Secure` outside dev) come
|
|
152
|
+
* from `response.cookie()`'s own `secureCookieDefaults()` floor — this
|
|
153
|
+
* method never overrides them, only `name`/`path`/`maxAge`. `raw: true`
|
|
154
|
+
* writes the token unquoted, so the same string a `cookie:<name>` source
|
|
155
|
+
* reads back is exactly what was issued, not a JSON-wrapped copy.
|
|
156
|
+
*
|
|
157
|
+
* @param token - the token string, or an `AccessTokenOutput` (e.g. from
|
|
158
|
+
* {@link login}) whose `expiresAt` sets `Max-Age` automatically when
|
|
159
|
+
* `options.maxAge` is not given. A bare string with no `options.maxAge`
|
|
160
|
+
* produces a session cookie.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* const { user, tokens } = await authService.login(User, credentials);
|
|
164
|
+
* authService.setAuthCookie(response, tokens.accessToken);
|
|
165
|
+
*/
|
|
166
|
+
setAuthCookie(response: Response, token: string | AccessTokenOutput, options?: SetAuthCookieOptions): void;
|
|
167
|
+
/**
|
|
168
|
+
* Clear the auth cookie {@link setAuthCookie} wrote. An explicit
|
|
169
|
+
* app-controller call — pair it with {@link logout} after the token row is
|
|
170
|
+
* revoked. `path` must match what the cookie was set with, or the browser
|
|
171
|
+
* silently ignores the clear (see `response.clearCookie`'s own doc comment).
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* await authService.logout(user, accessToken, refreshToken);
|
|
175
|
+
* authService.clearAuthCookie(response);
|
|
176
|
+
*/
|
|
177
|
+
clearAuthCookie(response: Response, options?: ClearAuthCookieOptions): void;
|
|
138
178
|
}
|
|
139
179
|
declare const authService: AuthService;
|
|
140
180
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.service.d.mts","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.service.d.mts","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"mappings":";;;;;;;;cAyBM,WAAA;;;;AALiD;;;cAYzC,gBAAA;EAcgC;;;EAAA,YAPhC,iBAAA;EAiDA;;;EA1CL,uBAAA,CAAwB,IAAA,EAAM,IAAA,GAAO,MAAA;EAmF7B;EA1EF,eAAA,CAAgB,IAAA,EAAM,IAAA,GAAO,OAAA;EAAA,QAI5B,qBAAA;EAAA,QAMA,gBAAA;EAmGwC;;;;;EA9EzC,mBAAA,CACX,IAAA,EAAM,IAAA,EACN,OAAA,GAAU,MAAA,oBACT,OAAA,CAAQ,iBAAA;EAAA,QAMG,iBAAA;EAoK+B;;;;EApIhC,kBAAA,CACX,IAAA,EAAM,IAAA,EACN,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,YAAA;EAAA,QAQG,cAAA;EAoIX;;;EA1GU,eAAA,CAAgB,IAAA,EAAM,IAAA,EAAM,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,SAAA;EA4I5D;;;;;EAjIF,aAAA,CACX,kBAAA,UACA,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,SAAA;EA8JmE;;;EApFjE,cAAA,CAAe,aAAA,UAAuB,cAAA,WAAyB,OAAA;EAwH5B;;;EAjHnC,YAAA,CAAa,QAAA,WAAmB,OAAA;EAiIH;;;;EAzH7B,YAAA,WAAuB,IAAA,EAClC,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,IAAA,EAAM,eAAA,GACL,OAAA,CAAQ,CAAA;EAwK6B;;;;EAxI3B,KAAA,WAAgB,IAAA,EAC3B,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,WAAA,EAAa,eAAA,EACb,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,WAAA,CAAY,CAAA;EAmNX;;;;;;;;;;EArLC,MAAA,CAAO,IAAA,EAAM,IAAA,EAAM,WAAA,WAAsB,YAAA,YAAwB,OAAA;EAlS/C;;;EA+TlB,iBAAA,CAAkB,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA;EAtT9B;;;EA6ThB,qBAAA,CAAsB,IAAA,EAAM,IAAA,GAAO,OAAA;EA9RnC;;;EAqSA,kBAAA,CAAmB,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA;EAnS1D;;;;;EA4SW,eAAA,CAAgB,IAAA,EAAM,IAAA,GAAO,OAAA;EApQxC;;;EAmRW,iBAAA,CAAkB,QAAA,WAAmB,OAAA;EAjRvC;;;;;EA4RE,oBAAA,IAAwB,OAAA;EA1PI;;;;;;;;;;EAkR5B,uBAAA,IAA2B,OAAA;IACtC,YAAA,EAAc,WAAA;IACd,aAAA,EAAe,YAAA;EAAA;EArLS;;;;;;;;EAqMb,wBAAA,IAA4B,OAAA;IACvC,YAAA;IACA,aAAA;EAAA;EA5JW;;;EA8KA,iBAAA,CAAkB,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;EA7KhC;;;;;EAAA,QAsLZ,qBAAA;EAnLL;;;;;;;;;;;;;;;;;;;;;EAkNI,aAAA,CACL,QAAA,EAAU,QAAA,EACV,KAAA,WAAgB,iBAAA,EAChB,OAAA,GAAS,oBAAA;EA5IiD;;;;;;;;;;EAoKrD,eAAA,CAAgB,QAAA,EAAU,QAAA,EAAU,OAAA,GAAS,sBAAA;AAAA;AAAA,cAQzC,WAAA,EAAW,WAAoB"}
|
|
@@ -311,6 +311,64 @@ var AuthService = class {
|
|
|
311
311
|
async getActiveSessions(user) {
|
|
312
312
|
return this.refreshTokenModel.activeFor(user);
|
|
313
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* `Max-Age` (seconds) `setAuthCookie` should apply, derived from an
|
|
316
|
+
* `AccessTokenOutput`'s `expiresAt` — or `undefined` for a bare token
|
|
317
|
+
* string (a session cookie) or an unparseable `expiresAt`.
|
|
318
|
+
*/
|
|
319
|
+
cookieMaxAgeFromToken(token) {
|
|
320
|
+
if (typeof token === "string") return void 0;
|
|
321
|
+
const expiresAt = new Date(token.expiresAt).getTime();
|
|
322
|
+
if (Number.isNaN(expiresAt)) return void 0;
|
|
323
|
+
return Math.max(0, Math.round((expiresAt - Date.now()) / 1e3));
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Write the auth cookie on `response` — the write side of the `cookie:<name>`
|
|
327
|
+
* token source `authMiddleware([], "cookie:<name>")` already reads. An explicit
|
|
328
|
+
* app-controller call, never a side effect of {@link login}, so an existing
|
|
329
|
+
* bearer-only app never starts emitting `Set-Cookie` just by upgrading.
|
|
330
|
+
*
|
|
331
|
+
* Attribute flags (`HttpOnly`, `SameSite=Lax`, `Secure` outside dev) come
|
|
332
|
+
* from `response.cookie()`'s own `secureCookieDefaults()` floor — this
|
|
333
|
+
* method never overrides them, only `name`/`path`/`maxAge`. `raw: true`
|
|
334
|
+
* writes the token unquoted, so the same string a `cookie:<name>` source
|
|
335
|
+
* reads back is exactly what was issued, not a JSON-wrapped copy.
|
|
336
|
+
*
|
|
337
|
+
* @param token - the token string, or an `AccessTokenOutput` (e.g. from
|
|
338
|
+
* {@link login}) whose `expiresAt` sets `Max-Age` automatically when
|
|
339
|
+
* `options.maxAge` is not given. A bare string with no `options.maxAge`
|
|
340
|
+
* produces a session cookie.
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* const { user, tokens } = await authService.login(User, credentials);
|
|
344
|
+
* authService.setAuthCookie(response, tokens.accessToken);
|
|
345
|
+
*/
|
|
346
|
+
setAuthCookie(response, token, options = {}) {
|
|
347
|
+
const name = options.name ?? authConfig.cookie.name();
|
|
348
|
+
const path = options.path ?? authConfig.cookie.path();
|
|
349
|
+
const tokenValue = typeof token === "string" ? token : token.token;
|
|
350
|
+
const maxAge = options.maxAge ?? this.cookieMaxAgeFromToken(token);
|
|
351
|
+
response.cookie(name, tokenValue, {
|
|
352
|
+
raw: true,
|
|
353
|
+
path,
|
|
354
|
+
...maxAge !== void 0 ? { maxAge } : {}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Clear the auth cookie {@link setAuthCookie} wrote. An explicit
|
|
359
|
+
* app-controller call — pair it with {@link logout} after the token row is
|
|
360
|
+
* revoked. `path` must match what the cookie was set with, or the browser
|
|
361
|
+
* silently ignores the clear (see `response.clearCookie`'s own doc comment).
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* await authService.logout(user, accessToken, refreshToken);
|
|
365
|
+
* authService.clearAuthCookie(response);
|
|
366
|
+
*/
|
|
367
|
+
clearAuthCookie(response, options = {}) {
|
|
368
|
+
const name = options.name ?? authConfig.cookie.name();
|
|
369
|
+
const path = options.path ?? authConfig.cookie.path();
|
|
370
|
+
response.clearCookie(name, { path });
|
|
371
|
+
}
|
|
314
372
|
};
|
|
315
373
|
const authService = new AuthService();
|
|
316
374
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.service.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"sourcesContent":["import { Random } from \"@mongez/reinforcements\";\r\nimport type { ChildModel } from \"@warlock.js/cascade\";\r\nimport { config, ForbiddenError, hashPassword, verifyPassword } from \"@warlock.js/core\";\nimport type {\n AccessTokenOutput,\n AuthCredentials,\n DeviceInfo,\n LoginResult,\n TokenPair,\n} from \"../contracts/types\";\nimport { AccessToken } from \"../models/access-token\";\nimport type { Auth } from \"../models/auth.model\";\nimport { RefreshToken } from \"../models/refresh-token\";\nimport { authConfig } from \"./auth-config\";\nimport { authEvents } from \"./auth-events\";\nimport { isInvalidCredentialError, jwt } from \"./jwt\";\n\nclass AuthService {\n /**\r\n * Resolve the active access-token model — the package default, or a subclass\r\n * an app registered under `config.auth.accessToken.model` (e.g. to add a\r\n * tenant column). The service never references the concrete class directly so\r\n * an override is a pure config change.\r\n */\r\n private get accessTokenModel(): typeof AccessToken {\r\n return config.key(\"auth.accessToken.model\", AccessToken);\r\n }\r\n\r\n /**\r\n * Resolve the active refresh-token model (default or registered override).\r\n */\r\n private get refreshTokenModel(): typeof RefreshToken {\r\n return config.key(\"auth.refreshToken.model\", RefreshToken);\r\n }\r\n\r\n /**\r\n * Build the default access-token JWT payload from a user.\r\n */\r\n public buildAccessTokenPayload(user: Auth): Record<string, unknown> {\n return {\r\n id: user.id,\r\n userType: user.userType,\r\n created_at: Date.now(),\r\n };\r\n }\n\n /** One policy seam keeps account-state checks identical across all auth paths. */\n public async canAuthenticate(user: Auth): Promise<boolean> {\n return authConfig.canAuthenticate(user);\n }\n\n private async assertCanAuthenticate(user: Auth): Promise<void> {\n if (!(await this.canAuthenticate(user))) {\n throw new ForbiddenError(\"This user cannot authenticate.\");\n }\n }\n\n private async issueAccessToken(\n user: Auth,\n payload?: Record<string, unknown>,\n ): Promise<AccessTokenOutput> {\n const data = payload || this.buildAccessTokenPayload(user);\n // Validate before signing so an unusable lifetime cannot mint an immortal token.\n const expiresIn = authConfig.accessToken.expiresInMs();\n\n const token = await jwt.generate(data, { expiresIn });\n const expiresAt = new Date(Date.now() + expiresIn);\n\n await this.accessTokenModel.issue(user, token, expiresAt);\n\n return { token, expiresAt: expiresAt.toISOString() };\n }\n\r\n /**\r\n * Sign + persist an access token for the user and return the token with its\r\n * expiry. The expiry is computed locally from `expiresIn` rather than by\r\n * re-verifying the token we just signed.\r\n */\r\n public async generateAccessToken(\n user: Auth,\n payload?: Record<string, unknown>,\n ): Promise<AccessTokenOutput> {\n await this.assertCanAuthenticate(user);\n\n return this.issueAccessToken(user, payload);\n }\n\r\n private async issueRefreshToken(\n user: Auth,\n deviceInfo?: DeviceInfo,\n ): Promise<RefreshToken | undefined> {\n if (!authConfig.refreshToken.enabled()) return;\r\n\r\n // Validate the lifetime first — before the per-user cap is enforced, before\r\n // anything is signed — so a bad value can never revoke a user's oldest\r\n // session on its way to failing.\r\n const expiresIn = authConfig.refreshToken.expiresInMs();\r\n\r\n const familyId = deviceInfo?.familyId || Random.string(32);\r\n\r\n const payload = {\r\n userId: user.id,\r\n userType: user.userType,\r\n familyId,\r\n };\r\n\r\n const expiresAt = new Date(Date.now() + expiresIn).toISOString();\r\n\r\n await this.refreshTokenModel.enforceMax(user, authConfig.refreshToken.maxPerUser());\r\n\r\n const token = await jwt.generateRefreshToken(payload, { expiresIn });\r\n\r\n return this.refreshTokenModel.issue(user, token, { familyId, expiresAt, deviceInfo });\n }\n\n /**\n * Sign + persist a refresh token for the user (enforcing the per-user cap\n * first). Resolves to `undefined` when refresh tokens are disabled in config.\n */\n public async createRefreshToken(\n user: Auth,\n deviceInfo?: DeviceInfo,\n ): Promise<RefreshToken | undefined> {\n if (!authConfig.refreshToken.enabled()) return;\n\n await this.assertCanAuthenticate(user);\n\n return this.issueRefreshToken(user, deviceInfo);\n }\n\n private async issueTokenPair(user: Auth, deviceInfo?: DeviceInfo): Promise<TokenPair> {\n const accessToken = await this.issueAccessToken(user, deviceInfo?.payload);\n const refreshToken = await this.issueRefreshToken(user, deviceInfo);\n\n const tokenPair: TokenPair = {\n accessToken,\n refreshToken: refreshToken\n ? {\n token: refreshToken.get(\"token\"),\n expiresAt: refreshToken.get(\"expires_at\"),\n }\n : undefined,\n };\n\n authEvents.emit(\"token.created\", user, tokenPair);\n\n if (refreshToken) {\n authEvents.emit(\"session.created\", user, refreshToken, deviceInfo);\n }\n\n return tokenPair;\n }\n\r\n /**\r\n * Issue both an access and a refresh token, emitting the creation events.\r\n */\r\n public async createTokenPair(user: Auth, deviceInfo?: DeviceInfo): Promise<TokenPair> {\n await this.assertCanAuthenticate(user);\n\n return this.issueTokenPair(user, deviceInfo);\n }\n\r\n /**\r\n * Exchange a refresh token for a new token pair, with rotation + replay\r\n * detection. A concurrent reuse of the same token loses the atomic revoke and\r\n * is treated as a breach — the whole family is revoked and the request fails.\r\n */\r\n public async refreshTokens(\n refreshTokenString: string,\n deviceInfo?: DeviceInfo,\n ): Promise<TokenPair | null> {\n let decoded:\n | {\n userId: number;\n userType: string;\n familyId: string;\n }\n | null;\n\n try {\n decoded = await jwt.verifyRefreshToken<{\n userId: number;\n userType: string;\n familyId: string;\n }>(refreshTokenString);\n } catch (error) {\n if (isInvalidCredentialError(error)) return null;\n\n throw error;\n }\n\n if (!decoded) return null;\n\r\n const refreshToken = await this.refreshTokenModel.findByToken(refreshTokenString);\n\n if (!refreshToken?.isValid) {\n // An already-invalid token may be a replay of a rotated credential.\n if (refreshToken) {\n await this.revokeTokenFamily(refreshToken.familyId);\n }\n\n return null;\n }\n\n const UserModel = config.key(`auth.userType.${decoded.userType}`);\n\n if (!UserModel) {\n throw new Error(`User type ${decoded.userType} is unknown type.`);\n }\n\n const user = (await UserModel.find(decoded.userId)) as Auth | null;\n\n if (!user) return null;\n\n if (!(await this.canAuthenticate(user))) return null;\n\n const rotationEnabled = authConfig.refreshToken.rotation();\n\n if (rotationEnabled) {\n const won = await refreshToken.revokeIfActive();\n\r\n if (!won) {\n // A concurrent request already rotated this token.\n await this.revokeTokenFamily(refreshToken.familyId);\n\n return null;\n }\n } else {\n await refreshToken.markAsUsed();\n }\n\n const newTokenPair = await this.issueTokenPair(user, {\n ...deviceInfo,\n familyId: refreshToken.familyId,\n });\n\n authEvents.emit(\"token.refreshed\", user, newTokenPair, refreshToken);\n\n return newTokenPair;\n }\n\r\n /**\r\n * Verify a plaintext password against a stored hash.\r\n */\r\n public async verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean> {\r\n return verifyPassword(plainPassword, hashedPassword);\r\n }\r\n\r\n /**\r\n * Hash a plaintext password.\r\n */\r\n public async hashPassword(password: string): Promise<string> {\r\n return hashPassword(password);\r\n }\r\n\r\n /**\r\n * Resolve a user by credentials, verifying the password. Returns `null` on a\r\n * missing user or a wrong password, emitting `login.failed` either way.\r\n */\r\n public async attemptLogin<T extends Auth>(\n Model: ChildModel<T>,\n data: AuthCredentials,\n ): Promise<T | null> {\n const { password, ...otherData } = data;\r\n\r\n authEvents.emit(\"login.attempt\", otherData);\r\n\r\n const user = (await Model.first(otherData)) as T | null;\r\n\r\n if (!user) {\r\n authEvents.emit(\"login.failed\", otherData, \"User not found\");\r\n\r\n return null;\r\n }\r\n\r\n if (!(await this.verifyPassword(password, user.string(\"password\")!))) {\n authEvents.emit(\"login.failed\", otherData, \"Invalid password\");\n\n return null;\n }\n\n if (!(await this.canAuthenticate(user))) {\n authEvents.emit(\"login.failed\", otherData, \"Authentication not allowed\");\n\n return null;\n }\n\n return user;\n }\r\n\r\n /**\r\n * Full login flow: validate credentials, issue tokens, emit events. Returns\r\n * the user + token pair on success, `null` on failure.\r\n */\r\n public async login<T extends Auth>(\r\n Model: ChildModel<T>,\r\n credentials: AuthCredentials,\n deviceInfo?: DeviceInfo,\r\n ): Promise<LoginResult<T> | null> {\r\n const user = await this.attemptLogin(Model, credentials);\r\n\r\n if (!user) {\r\n return null;\r\n }\r\n\r\n if (!authConfig.refreshToken.enabled()) {\n const accessToken = await this.issueAccessToken(user, deviceInfo?.payload);\n\n return { user, tokens: { accessToken } };\n }\n\n const tokens = await this.issueTokenPair(user, deviceInfo);\n\r\n authEvents.emit(\"login.success\", user, tokens, deviceInfo);\r\n\r\n return { user, tokens };\n }\r\n\r\n /**\r\n * Log a user out.\r\n *\r\n * @param accessToken - access token string to revoke (optional)\r\n * @param refreshToken - refresh token string to revoke (optional)\r\n *\r\n * When no refresh token is supplied, `config.auth.refreshToken.logoutWithoutToken`\r\n * decides the behavior: `\"revoke-all\"` (default, fail-safe) revokes every\r\n * refresh token; `\"error\"` requires the caller to pass one.\r\n */\r\n public async logout(user: Auth, accessToken?: string, refreshToken?: string): Promise<void> {\r\n if (accessToken) {\r\n await this.removeAccessToken(user, accessToken);\r\n }\r\n\r\n if (refreshToken) {\r\n const token = await this.refreshTokenModel.findForUser(user, refreshToken);\r\n\r\n if (token) {\r\n await token.revoke();\r\n authEvents.emit(\"session.destroyed\", user, token);\r\n }\r\n } else {\r\n const behavior = authConfig.refreshToken.logoutWithoutToken();\r\n\r\n if (behavior === \"error\") {\r\n throw new Error(\"Refresh token required for logout\");\r\n }\r\n\r\n await this.revokeAllTokens(user);\r\n authEvents.emit(\"logout.failsafe\", user);\r\n }\r\n\r\n authEvents.emit(\"logout\", user);\r\n }\r\n\r\n /**\r\n * Remove a specific access token belonging to the user.\r\n */\r\n public async removeAccessToken(user: Auth, token: string): Promise<void> {\r\n await this.accessTokenModel.deleteForUser(user, token);\r\n }\r\n\r\n /**\r\n * Remove every access token belonging to the user.\r\n */\r\n public async removeAllAccessTokens(user: Auth): Promise<void> {\r\n await this.accessTokenModel.deleteAllForUser(user);\r\n }\r\n\r\n /**\r\n * Remove a specific refresh token belonging to the user.\r\n */\r\n public async removeRefreshToken(user: Auth, token: string): Promise<void> {\r\n await this.refreshTokenModel.deleteForUser(user, token);\r\n }\r\n\r\n /**\r\n * Revoke every active refresh token for the user and delete their access\r\n * tokens — \"log out of all devices\". Revocation is a single bulk update; an\r\n * event fires per revoked token.\r\n */\r\n public async revokeAllTokens(user: Auth): Promise<void> {\r\n const revokedTokens = await this.refreshTokenModel.revokeAllFor(user);\r\n\r\n for (const token of revokedTokens) {\r\n authEvents.emit(\"token.revoked\", user, token);\r\n }\r\n\r\n await this.removeAllAccessTokens(user);\r\n\r\n authEvents.emit(\"logout.all\", user);\r\n }\r\n\r\n /**\r\n * Revoke an entire token family — rotation breach containment.\r\n */\r\n public async revokeTokenFamily(familyId: string): Promise<void> {\r\n const revokedTokens = await this.refreshTokenModel.revokeFamily(familyId);\r\n\r\n authEvents.emit(\"token.familyRevoked\", familyId, revokedTokens);\r\n }\r\n\r\n /**\r\n * Delete expired tokens (refresh + access). Emits `token.expired` per refresh\r\n * token and `cleanup.completed` with the refresh count. Drives the\r\n * `auth.cleanup` CLI command.\r\n */\r\n public async cleanupExpiredTokens(): Promise<number> {\r\n const expiredTokens = await this.refreshTokenModel.purgeExpired();\r\n\r\n for (const token of expiredTokens) {\r\n authEvents.emit(\"token.expired\", token);\r\n }\r\n\r\n await this.accessTokenModel.purgeExpired();\r\n\r\n authEvents.emit(\"cleanup.completed\", expiredTokens.length);\r\n\r\n return expiredTokens.length;\r\n }\r\n\r\n /**\r\n * Find every persisted token — access and refresh — that can never retire\r\n * itself: an unusable `expires_at`, or a token carrying no `exp` claim.\r\n *\r\n * This is the remediation read for the pre-4.12.0 `expiresIn` defect (#25).\r\n * Nothing else surfaces these rows: `auth.cleanup` selects `expires_at < now`,\r\n * which an `Invalid Date` never satisfies, and a row whose date column is\r\n * perfectly fine can still hold a token with no deadline in it. Read-only —\r\n * pair with {@link purgeNeverExpiringTokens} to act on the answer.\r\n */\r\n public async findNeverExpiringTokens(): Promise<{\r\n accessTokens: AccessToken[];\r\n refreshTokens: RefreshToken[];\r\n }> {\r\n return {\r\n accessTokens: await this.accessTokenModel.findNeverExpiring(),\r\n refreshTokens: await this.refreshTokenModel.findNeverExpiring(),\r\n };\r\n }\r\n\r\n /**\r\n * Revoke every never-expiring token by deleting its row, emitting\r\n * `token.revoked` per refresh token. Drives `warlock auth.purge-never-expiring`.\r\n *\r\n * Access tokens first: deleting one is immediate revocation, while a refresh\r\n * token left in place for a moment can only mint an access token through a\r\n * verifier that now rejects it for the missing `exp`.\r\n */\r\n public async purgeNeverExpiringTokens(): Promise<{\r\n accessTokens: number;\r\n refreshTokens: number;\r\n }> {\r\n const accessTokens = await this.accessTokenModel.purgeNeverExpiring();\r\n const refreshTokens = await this.refreshTokenModel.purgeNeverExpiring();\r\n\r\n // `token.expired` (the event `auth.cleanup` already emits for a removed\r\n // refresh token) rather than `token.revoked`, which carries a loaded `Auth`\r\n // this batch path has no reason to fetch a user row for.\r\n for (const token of refreshTokens) {\r\n authEvents.emit(\"token.expired\", token);\r\n }\r\n\r\n return { accessTokens: accessTokens.length, refreshTokens: refreshTokens.length };\r\n }\r\n\r\n /**\r\n * Active, unexpired sessions for the user, newest first.\r\n */\r\n public async getActiveSessions(user: Auth): Promise<RefreshToken[]> {\r\n return this.refreshTokenModel.activeFor(user);\r\n }\r\n}\r\n\r\nexport const authService = new AuthService();\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auth.service.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"sourcesContent":["import { Random } from \"@mongez/reinforcements\";\nimport type { ChildModel } from \"@warlock.js/cascade\";\nimport {\n config,\n ForbiddenError,\n hashPassword,\n type Response,\n verifyPassword,\n} from \"@warlock.js/core\";\nimport type {\n AccessTokenOutput,\n AuthCredentials,\n ClearAuthCookieOptions,\n DeviceInfo,\n LoginResult,\n SetAuthCookieOptions,\n TokenPair,\n} from \"../contracts/types\";\nimport { AccessToken } from \"../models/access-token\";\nimport type { Auth } from \"../models/auth.model\";\nimport { RefreshToken } from \"../models/refresh-token\";\nimport { authConfig } from \"./auth-config\";\nimport { authEvents } from \"./auth-events\";\nimport { isInvalidCredentialError, jwt } from \"./jwt\";\n\nclass AuthService {\n /**\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): Record<string, unknown> {\n return {\n id: user.id,\n userType: user.userType,\n created_at: Date.now(),\n };\n }\n\n /** One policy seam keeps account-state checks identical across all auth paths. */\n public async canAuthenticate(user: Auth): Promise<boolean> {\n return authConfig.canAuthenticate(user);\n }\n\n private async assertCanAuthenticate(user: Auth): Promise<void> {\n if (!(await this.canAuthenticate(user))) {\n throw new ForbiddenError(\"This user cannot authenticate.\");\n }\n }\n\n private async issueAccessToken(\n user: Auth,\n payload?: Record<string, unknown>,\n ): Promise<AccessTokenOutput> {\n const data = payload || this.buildAccessTokenPayload(user);\n // Validate before signing so an unusable lifetime cannot mint an immortal token.\n const expiresIn = authConfig.accessToken.expiresInMs();\n\n const token = await jwt.generate(data, { expiresIn });\n const expiresAt = new Date(Date.now() + expiresIn);\n\n await this.accessTokenModel.issue(user, token, expiresAt);\n\n return { token, expiresAt: expiresAt.toISOString() };\n }\n\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(\n user: Auth,\n payload?: Record<string, unknown>,\n ): Promise<AccessTokenOutput> {\n await this.assertCanAuthenticate(user);\n\n return this.issueAccessToken(user, payload);\n }\n\n private async issueRefreshToken(\n user: Auth,\n deviceInfo?: DeviceInfo,\n ): Promise<RefreshToken | undefined> {\n if (!authConfig.refreshToken.enabled()) return;\n\n // Validate the lifetime first — before the per-user cap is enforced, before\n // anything is signed — so a bad value can never revoke a user's oldest\n // session on its way to failing.\n const expiresIn = authConfig.refreshToken.expiresInMs();\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 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 * Sign + persist a refresh token for the user (enforcing the per-user cap\n * first). Resolves to `undefined` when refresh tokens are disabled in config.\n */\n public async createRefreshToken(\n user: Auth,\n deviceInfo?: DeviceInfo,\n ): Promise<RefreshToken | undefined> {\n if (!authConfig.refreshToken.enabled()) return;\n\n await this.assertCanAuthenticate(user);\n\n return this.issueRefreshToken(user, deviceInfo);\n }\n\n private async issueTokenPair(user: Auth, deviceInfo?: DeviceInfo): Promise<TokenPair> {\n const accessToken = await this.issueAccessToken(user, deviceInfo?.payload);\n const refreshToken = await this.issueRefreshToken(user, deviceInfo);\n\n const tokenPair: TokenPair = {\n accessToken,\n refreshToken: refreshToken\n ? {\n token: refreshToken.get(\"token\"),\n expiresAt: refreshToken.get(\"expires_at\"),\n }\n : undefined,\n };\n\n authEvents.emit(\"token.created\", user, tokenPair);\n\n if (refreshToken) {\n authEvents.emit(\"session.created\", user, refreshToken, deviceInfo);\n }\n\n return tokenPair;\n }\n\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 await this.assertCanAuthenticate(user);\n\n return this.issueTokenPair(user, deviceInfo);\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 let decoded:\n | {\n userId: number;\n userType: string;\n familyId: string;\n }\n | null;\n\n try {\n decoded = await jwt.verifyRefreshToken<{\n userId: number;\n userType: string;\n familyId: string;\n }>(refreshTokenString);\n } catch (error) {\n if (isInvalidCredentialError(error)) return null;\n\n throw error;\n }\n\n if (!decoded) return null;\n\n const refreshToken = await this.refreshTokenModel.findByToken(refreshTokenString);\n\n if (!refreshToken?.isValid) {\n // An already-invalid token may be a replay of a rotated credential.\n if (refreshToken) {\n await this.revokeTokenFamily(refreshToken.familyId);\n }\n\n return null;\n }\n\n const UserModel = config.key(`auth.userType.${decoded.userType}`);\n\n if (!UserModel) {\n throw new Error(`User type ${decoded.userType} is unknown type.`);\n }\n\n const user = (await UserModel.find(decoded.userId)) as Auth | null;\n\n if (!user) return null;\n\n if (!(await this.canAuthenticate(user))) return null;\n\n const rotationEnabled = authConfig.refreshToken.rotation();\n\n if (rotationEnabled) {\n const won = await refreshToken.revokeIfActive();\n\n if (!won) {\n // A concurrent request already rotated this token.\n await this.revokeTokenFamily(refreshToken.familyId);\n\n return null;\n }\n } else {\n await refreshToken.markAsUsed();\n }\n\n const newTokenPair = await this.issueTokenPair(user, {\n ...deviceInfo,\n familyId: refreshToken.familyId,\n });\n\n authEvents.emit(\"token.refreshed\", user, newTokenPair, refreshToken);\n\n return newTokenPair;\n }\n\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>(\n Model: ChildModel<T>,\n data: AuthCredentials,\n ): 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 if (!(await this.canAuthenticate(user))) {\n authEvents.emit(\"login.failed\", otherData, \"Authentication not allowed\");\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: AuthCredentials,\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.issueAccessToken(user, deviceInfo?.payload);\n\n return { user, tokens: { accessToken } };\n }\n\n const tokens = await this.issueTokenPair(user, deviceInfo);\n\n authEvents.emit(\"login.success\", user, tokens, deviceInfo);\n\n return { user, tokens };\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 * Find every persisted token — access and refresh — that can never retire\n * itself: an unusable `expires_at`, or a token carrying no `exp` claim.\n *\n * This is the remediation read for the pre-4.12.0 `expiresIn` defect (#25).\n * Nothing else surfaces these rows: `auth.cleanup` selects `expires_at < now`,\n * which an `Invalid Date` never satisfies, and a row whose date column is\n * perfectly fine can still hold a token with no deadline in it. Read-only —\n * pair with {@link purgeNeverExpiringTokens} to act on the answer.\n */\n public async findNeverExpiringTokens(): Promise<{\n accessTokens: AccessToken[];\n refreshTokens: RefreshToken[];\n }> {\n return {\n accessTokens: await this.accessTokenModel.findNeverExpiring(),\n refreshTokens: await this.refreshTokenModel.findNeverExpiring(),\n };\n }\n\n /**\n * Revoke every never-expiring token by deleting its row, emitting\n * `token.revoked` per refresh token. Drives `warlock auth.purge-never-expiring`.\n *\n * Access tokens first: deleting one is immediate revocation, while a refresh\n * token left in place for a moment can only mint an access token through a\n * verifier that now rejects it for the missing `exp`.\n */\n public async purgeNeverExpiringTokens(): Promise<{\n accessTokens: number;\n refreshTokens: number;\n }> {\n const accessTokens = await this.accessTokenModel.purgeNeverExpiring();\n const refreshTokens = await this.refreshTokenModel.purgeNeverExpiring();\n\n // `token.expired` (the event `auth.cleanup` already emits for a removed\n // refresh token) rather than `token.revoked`, which carries a loaded `Auth`\n // this batch path has no reason to fetch a user row for.\n for (const token of refreshTokens) {\n authEvents.emit(\"token.expired\", token);\n }\n\n return { accessTokens: accessTokens.length, refreshTokens: refreshTokens.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 /**\n * `Max-Age` (seconds) `setAuthCookie` should apply, derived from an\n * `AccessTokenOutput`'s `expiresAt` — or `undefined` for a bare token\n * string (a session cookie) or an unparseable `expiresAt`.\n */\n private cookieMaxAgeFromToken(token: string | AccessTokenOutput): number | undefined {\n if (typeof token === \"string\") return undefined;\n\n const expiresAt = new Date(token.expiresAt).getTime();\n\n if (Number.isNaN(expiresAt)) return undefined;\n\n return Math.max(0, Math.round((expiresAt - Date.now()) / 1000));\n }\n\n /**\n * Write the auth cookie on `response` — the write side of the `cookie:<name>`\n * token source `authMiddleware([], \"cookie:<name>\")` already reads. An explicit\n * app-controller call, never a side effect of {@link login}, so an existing\n * bearer-only app never starts emitting `Set-Cookie` just by upgrading.\n *\n * Attribute flags (`HttpOnly`, `SameSite=Lax`, `Secure` outside dev) come\n * from `response.cookie()`'s own `secureCookieDefaults()` floor — this\n * method never overrides them, only `name`/`path`/`maxAge`. `raw: true`\n * writes the token unquoted, so the same string a `cookie:<name>` source\n * reads back is exactly what was issued, not a JSON-wrapped copy.\n *\n * @param token - the token string, or an `AccessTokenOutput` (e.g. from\n * {@link login}) whose `expiresAt` sets `Max-Age` automatically when\n * `options.maxAge` is not given. A bare string with no `options.maxAge`\n * produces a session cookie.\n *\n * @example\n * const { user, tokens } = await authService.login(User, credentials);\n * authService.setAuthCookie(response, tokens.accessToken);\n */\n public setAuthCookie(\n response: Response,\n token: string | AccessTokenOutput,\n options: SetAuthCookieOptions = {},\n ): void {\n const name = options.name ?? authConfig.cookie.name();\n const path = options.path ?? authConfig.cookie.path();\n const tokenValue = typeof token === \"string\" ? token : token.token;\n const maxAge = options.maxAge ?? this.cookieMaxAgeFromToken(token);\n\n response.cookie(name, tokenValue, {\n raw: true,\n path,\n ...(maxAge !== undefined ? { maxAge } : {}),\n });\n }\n\n /**\n * Clear the auth cookie {@link setAuthCookie} wrote. An explicit\n * app-controller call — pair it with {@link logout} after the token row is\n * revoked. `path` must match what the cookie was set with, or the browser\n * silently ignores the clear (see `response.clearCookie`'s own doc comment).\n *\n * @example\n * await authService.logout(user, accessToken, refreshToken);\n * authService.clearAuthCookie(response);\n */\n public clearAuthCookie(response: Response, options: ClearAuthCookieOptions = {}): void {\n const name = options.name ?? authConfig.cookie.name();\n const path = options.path ?? authConfig.cookie.path();\n\n response.clearCookie(name, { path });\n }\n}\n\nexport const authService = new AuthService();\n"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Auth } from "../models/auth.model.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../auth/src/services/current-user.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Typed accessor over the request-scoped current user, for callers that want
|
|
6
|
+
* `Auth`-derived typing without repeating core's `unknown` → `UserType`
|
|
7
|
+
* assertion at every call site.
|
|
8
|
+
*
|
|
9
|
+
* Core's own `useCurrentUser()` returns `unknown` cast to the caller's
|
|
10
|
+
* generic (it cannot know about `RequestUser`/`Auth` — see
|
|
11
|
+
* `core/src/http/context/request-context.ts`). This wrapper narrows that
|
|
12
|
+
* generic to `Auth`, matching what `@warlock.js/auth`'s middleware actually
|
|
13
|
+
* writes to `request.locals.user`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const user = currentUser<AdminUser>();
|
|
17
|
+
*/
|
|
18
|
+
declare function currentUser<UserType extends Auth = Auth>(): UserType | undefined;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { currentUser };
|
|
21
|
+
//# sourceMappingURL=current-user.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"current-user.d.mts","names":[],"sources":["../../../../../../../auth/src/services/current-user.ts"],"mappings":";;;;;AAiBA;;;;;;;;;;;;iBAAgB,WAAA,kBAA6B,IAAA,GAAO,IAAA,KAAS,QAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useCurrentUser } from "@warlock.js/core";
|
|
2
|
+
|
|
3
|
+
//#region ../auth/src/services/current-user.ts
|
|
4
|
+
/**
|
|
5
|
+
* Typed accessor over the request-scoped current user, for callers that want
|
|
6
|
+
* `Auth`-derived typing without repeating core's `unknown` → `UserType`
|
|
7
|
+
* assertion at every call site.
|
|
8
|
+
*
|
|
9
|
+
* Core's own `useCurrentUser()` returns `unknown` cast to the caller's
|
|
10
|
+
* generic (it cannot know about `RequestUser`/`Auth` — see
|
|
11
|
+
* `core/src/http/context/request-context.ts`). This wrapper narrows that
|
|
12
|
+
* generic to `Auth`, matching what `@warlock.js/auth`'s middleware actually
|
|
13
|
+
* writes to `request.locals.user`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const user = currentUser<AdminUser>();
|
|
17
|
+
*/
|
|
18
|
+
function currentUser() {
|
|
19
|
+
return useCurrentUser();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { currentUser };
|
|
24
|
+
//# sourceMappingURL=current-user.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"current-user.mjs","names":[],"sources":["../../../../../../../auth/src/services/current-user.ts"],"sourcesContent":["import { useCurrentUser } from \"@warlock.js/core\";\nimport type { Auth } from \"../models/auth.model\";\n\n/**\n * Typed accessor over the request-scoped current user, for callers that want\n * `Auth`-derived typing without repeating core's `unknown` → `UserType`\n * assertion at every call site.\n *\n * Core's own `useCurrentUser()` returns `unknown` cast to the caller's\n * generic (it cannot know about `RequestUser`/`Auth` — see\n * `core/src/http/context/request-context.ts`). This wrapper narrows that\n * generic to `Auth`, matching what `@warlock.js/auth`'s middleware actually\n * writes to `request.locals.user`.\n *\n * @example\n * const user = currentUser<AdminUser>();\n */\nexport function currentUser<UserType extends Auth = Auth>(): UserType | undefined {\n return useCurrentUser<UserType>();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,SAAgB,cAAkE;CAChF,OAAO,eAAyB;AAClC"}
|
package/esm/services/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AuthEventCallback, AuthEventName, AuthEventPayloads, authEvents } from "./auth-events.mjs";
|
|
2
2
|
import { authService } from "./auth.service.mjs";
|
|
3
|
+
import { currentUser } from "./current-user.mjs";
|
|
3
4
|
import { generateJWTSecret } from "./generate-jwt-secret.mjs";
|
|
4
5
|
import { TokenType, TokenTypeError, isInvalidCredentialError, jwt } from "./jwt.mjs";
|
package/esm/services/index.mjs
CHANGED
|
@@ -2,5 +2,6 @@ import { authEvents } from "./auth-events.mjs";
|
|
|
2
2
|
import { TokenTypeError, isInvalidCredentialError, jwt } from "./jwt.mjs";
|
|
3
3
|
import { authService } from "./auth.service.mjs";
|
|
4
4
|
import { generateJWTSecret } from "./generate-jwt-secret.mjs";
|
|
5
|
+
import { currentUser } from "./current-user.mjs";
|
|
5
6
|
|
|
6
7
|
export { };
|
|
@@ -33,7 +33,16 @@ declare enum AuthErrorCodes {
|
|
|
33
33
|
* `Error` thrown by `assertTokenType` (`services/jwt.ts`) so callers can
|
|
34
34
|
* classify it as a credential failure without matching on the message.
|
|
35
35
|
*/
|
|
36
|
-
InvalidTokenType = "EC005"
|
|
36
|
+
InvalidTokenType = "EC005",
|
|
37
|
+
// Error Code 005
|
|
38
|
+
/**
|
|
39
|
+
* CSRF Origin Mismatch Error Code EC006
|
|
40
|
+
* EC006 = a cookie-authenticated, unsafe-method (POST/PUT/PATCH/DELETE)
|
|
41
|
+
* request whose `Origin` (or, absent that, `Referer`) did not match the
|
|
42
|
+
* request's own origin or an entry in `auth.csrf.allowedOrigins`, or whose
|
|
43
|
+
* request carried neither header at all.
|
|
44
|
+
*/
|
|
45
|
+
CsrfOriginMismatch = "EC006"
|
|
37
46
|
}
|
|
38
47
|
//#endregion
|
|
39
48
|
export { AuthErrorCodes };
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;EA4BkB;AAAA;;;EAvBlB,YAAA;EAAA;;;;;;EAMA,eAAA;EAAA;;;;;;;;;EASA,gBAAA;EAAA;;;;;;;;EAQA,kBAAA;AAAA"}
|
|
@@ -30,6 +30,14 @@ let AuthErrorCodes = /* @__PURE__ */ function(AuthErrorCodes) {
|
|
|
30
30
|
* classify it as a credential failure without matching on the message.
|
|
31
31
|
*/
|
|
32
32
|
AuthErrorCodes["InvalidTokenType"] = "EC005";
|
|
33
|
+
/**
|
|
34
|
+
* CSRF Origin Mismatch Error Code EC006
|
|
35
|
+
* EC006 = a cookie-authenticated, unsafe-method (POST/PUT/PATCH/DELETE)
|
|
36
|
+
* request whose `Origin` (or, absent that, `Referer`) did not match the
|
|
37
|
+
* request's own origin or an entry in `auth.csrf.allowedOrigins`, or whose
|
|
38
|
+
* request carried neither header at all.
|
|
39
|
+
*/
|
|
40
|
+
AuthErrorCodes["CsrfOriginMismatch"] = "EC006";
|
|
33
41
|
return AuthErrorCodes;
|
|
34
42
|
}({});
|
|
35
43
|
|
|
@@ -1 +1 @@
|
|
|
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 * Invalid Token Type Error Code EC005\r\n * EC005 = Invalid Token Type — the token's `tokenType` claim does not match\r\n * what the caller expects (e.g. a refresh token presented where an access\r\n * token is required, or vice versa). Carried on the `code` property of the\r\n * `Error` thrown by `assertTokenType` (`services/jwt.ts`) so callers can\r\n * classify it as a credential failure without matching on the message.\r\n */\r\n InvalidTokenType = \"EC005\", // Error Code 005\r\n}\r\n"],"mappings":";AAAA,IAAY,iBAAL;;;;;CAKL;;;;;CAKA;;;;;CAKA;;;;;;CAMA;;;;;;;;;CASA;;AACF"}
|
|
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 * Invalid Token Type Error Code EC005\r\n * EC005 = Invalid Token Type — the token's `tokenType` claim does not match\r\n * what the caller expects (e.g. a refresh token presented where an access\r\n * token is required, or vice versa). Carried on the `code` property of the\r\n * `Error` thrown by `assertTokenType` (`services/jwt.ts`) so callers can\r\n * classify it as a credential failure without matching on the message.\r\n */\r\n InvalidTokenType = \"EC005\", // Error Code 005\r\n /**\r\n * CSRF Origin Mismatch Error Code EC006\r\n * EC006 = a cookie-authenticated, unsafe-method (POST/PUT/PATCH/DELETE)\r\n * request whose `Origin` (or, absent that, `Referer`) did not match the\r\n * request's own origin or an entry in `auth.csrf.allowedOrigins`, or whose\r\n * request carried neither header at all.\r\n */\r\n CsrfOriginMismatch = \"EC006\", // Error Code 006\r\n}\r\n"],"mappings":";AAAA,IAAY,iBAAL;;;;;CAKL;;;;;CAKA;;;;;CAKA;;;;;;CAMA;;;;;;;;;CASA;;;;;;;;CAQA;;AACF"}
|