@warlock.js/auth 5.10.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/esm/contracts/index.d.mts +3 -2
  3. package/esm/contracts/request-locals.d.mts +43 -0
  4. package/esm/contracts/request-locals.d.mts.map +1 -0
  5. package/esm/contracts/types.d.mts +66 -1
  6. package/esm/contracts/types.d.mts.map +1 -1
  7. package/esm/contracts/types.mjs.map +1 -1
  8. package/esm/index.d.mts +7 -3
  9. package/esm/index.mjs +3 -1
  10. package/esm/middleware/auth.middleware.d.mts.map +1 -1
  11. package/esm/middleware/auth.middleware.mjs +13 -2
  12. package/esm/middleware/auth.middleware.mjs.map +1 -1
  13. package/esm/middleware/csrf-origin-check.d.mts +40 -0
  14. package/esm/middleware/csrf-origin-check.d.mts.map +1 -0
  15. package/esm/middleware/csrf-origin-check.mjs +105 -0
  16. package/esm/middleware/csrf-origin-check.mjs.map +1 -0
  17. package/esm/middleware/index.d.mts +1 -0
  18. package/esm/middleware/index.mjs +1 -0
  19. package/esm/services/auth-config.mjs +12 -0
  20. package/esm/services/auth-config.mjs.map +1 -1
  21. package/esm/services/auth.service.d.mts +41 -1
  22. package/esm/services/auth.service.d.mts.map +1 -1
  23. package/esm/services/auth.service.mjs +58 -0
  24. package/esm/services/auth.service.mjs.map +1 -1
  25. package/esm/services/current-user.d.mts +21 -0
  26. package/esm/services/current-user.d.mts.map +1 -0
  27. package/esm/services/current-user.mjs +24 -0
  28. package/esm/services/current-user.mjs.map +1 -0
  29. package/esm/services/index.d.mts +1 -0
  30. package/esm/services/index.mjs +1 -0
  31. package/esm/utils/auth-error-codes.d.mts +10 -1
  32. package/esm/utils/auth-error-codes.d.mts.map +1 -1
  33. package/esm/utils/auth-error-codes.mjs +8 -0
  34. package/esm/utils/auth-error-codes.mjs.map +1 -1
  35. package/llms-full.txt +89 -19
  36. package/package.json +7 -7
  37. package/skills/handle-login-and-logout/SKILL.md +1 -1
  38. package/skills/overview/SKILL.md +1 -1
  39. package/skills/protect-routes/SKILL.md +71 -13
package/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ All notable changes to `@warlock.js/auth` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 5.12.0 - 2026-09-16
8
+
9
+ ### Added
10
+
11
+ - `authService.setAuthCookie(response, token, options?)` and `authService.clearAuthCookie(response, options?)` — the write side of the `cookie:<name>` token source `authMiddleware([], "cookie:<name>")` has accepted since 5.0.0 (card 50bf4f1a). `setAuthCookie` accepts a raw token string or an `AccessTokenOutput` (deriving `Max-Age` from `expiresAt` automatically); both are explicit app-controller calls — `login`/`logout` never set cookies implicitly, so upgrading never starts emitting `Set-Cookie` for an existing bearer-only app. Cookie name/path are configurable via the new `auth.cookie` config block (defaults: `"access_token"` / `"/"`); attribute flags (`HttpOnly`, `SameSite=Lax`, `Secure` outside dev) are not — they come from core's `secureCookieDefaults()`, the framework-wide floor.
12
+
13
+ ### Security
14
+
15
+ - **CSRF Origin check for cookie-authenticated writes.** `authMiddleware` now automatically rejects, with `403` (`AuthErrorCodes.CsrfOriginMismatch`, `"EC006"`), any request whose credential came from a `cookie:` source and whose method is unsafe (`POST`/`PUT`/`PATCH`/`DELETE`) unless `Origin` (or, absent that, `Referer`) names the request's own origin or an entry in the new `auth.csrf.allowedOrigins` config (default `[]`). A request with neither header is rejected, fail-closed. Header-token authentication and safe methods (`GET`/`HEAD`/`OPTIONS`) are completely unaffected. This closes the residual CSRF gap `SameSite=Lax` alone leaves open for cookie auth (a same-site GET redirect chain, or a client that ignores `SameSite`); a double-submit token mechanism is deferred to a later release.
16
+ - The CSRF Origin check's own-origin comparison includes the request's port (read from the `Host` header, since core's `request.hostname` never carries one), with default ports (`:80` on `http`, `:443` on `https`) normalised as equivalent to no port. A same-origin cookie-authenticated write on a non-default port — e.g. every `warlock dev` session — is now correctly allowed instead of being rejected with `403 EC006`.
17
+
18
+ ### Changed
19
+
20
+ - **BREAKING:** the authenticated user now lives at `request.locals.user`, declared by `@warlock.js/auth`. `request.user` is removed (in development it throws with the new location). `@warlock.js/auth`'s middleware writes `request.locals.user` after a successful token resolution and clears it (`= undefined`) on a forged, malformed, expired, or wrong-type token; `RequestUser` — the augmentable, empty-by-default interface apps narrow to their own model — moved from `@warlock.js/core` into `@warlock.js/auth` alongside it.
21
+
22
+ ```typescript
23
+ // Before (removed)
24
+ declare module "@warlock.js/core" {
25
+ interface RequestUser {
26
+ id: string | number;
27
+ }
28
+ }
29
+ const user = request.user;
30
+
31
+ // After
32
+ declare module "@warlock.js/auth" {
33
+ interface RequestUser {
34
+ id: string | number;
35
+ }
36
+ }
37
+ const user = request.locals.user;
38
+ ```
39
+
40
+ A typed `currentUser<UserType extends Auth>()` helper is also exported, wrapping core's `useCurrentUser()` for callers that want `Auth`-derived typing without repeating the assertion at every call site.
41
+
42
+ ## 5.11.0 - 2026-09-14
43
+
44
+ _Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.11.0._
45
+
7
46
  ## 5.10.0 - 2026-09-14
8
47
 
9
48
  _Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.10.0._
@@ -1,2 +1,3 @@
1
- import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshTokenConfig, TokenFrom, TokenPair } from "./types.mjs";
2
- import { Authenticable } from "./auth-contract.mjs";
1
+ import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair } from "./types.mjs";
2
+ import { Authenticable } from "./auth-contract.mjs";
3
+ import { RequestUser } from "./request-locals.mjs";
@@ -0,0 +1,43 @@
1
+ //#region ../auth/src/contracts/request-locals.d.ts
2
+ /**
3
+ * The authenticated user attached to the current request —
4
+ * `request.locals.user`.
5
+ *
6
+ * Moved here from `@warlock.js/core` in 5.12.0 (core's `Request` no longer
7
+ * has a `user` property at all — see `RequestUserMovedError`). Empty by
8
+ * default, exactly as it was in core, so any shape is assignable at the
9
+ * declaration site; an app narrows it via module augmentation:
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * declare module "@warlock.js/auth" {
14
+ * interface RequestUser {
15
+ * id: string | number;
16
+ * }
17
+ * }
18
+ * ```
19
+ *
20
+ * Deliberately NOT `extends Auth<ModelSchema>` here: an `interface` can
21
+ * extend at most one class hierarchy, and a library-side
22
+ * `interface RequestUser extends Auth<ModelSchema>` would pre-claim that one
23
+ * extends slot, so an app's own `interface RequestUser extends User {}`
24
+ * would fail to compile (TS2320). See `access/src/middleware/gate.middleware.ts`
25
+ * for the same hazard documented at its call site.
26
+ */
27
+ interface RequestUser {}
28
+ /**
29
+ * Augments core's `RequestLocals` (`request.locals`) with the `user` key —
30
+ * done in the module that OWNS the key, per `RequestLocals`' own contract
31
+ * (`core/src/http/types.ts`). `authMiddleware` (`middleware/auth.middleware.ts`)
32
+ * is the sole writer: it sets `request.locals.user` after a successful token
33
+ * resolution and clears it (`= undefined`) on a forged, malformed, expired,
34
+ * or wrong-type token.
35
+ */
36
+ declare module "@warlock.js/core" {
37
+ interface RequestLocals {
38
+ user?: RequestUser;
39
+ }
40
+ } //# sourceMappingURL=request-locals.d.ts.map
41
+ //#endregion
42
+ export { RequestUser };
43
+ //# sourceMappingURL=request-locals.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-locals.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/request-locals.ts"],"mappings":";;AAyBA;;;;AAA4B;AAAG;;;;;;;;;AAYT;;;;;;;;;;UAZL,WAAA;;;;;;;;;;YAWL,aAAA;IACR,IAAA,GAAO,WAAW;EAAA;AAAA"}
@@ -183,6 +183,16 @@ type AuthConfigurations = {
183
183
  * Opt-in; API routes are unaffected. Read via `authConfig.pageAuth`.
184
184
  */
185
185
  pageAuth?: PageAuthConfig;
186
+ /**
187
+ * Cookie auth session config — read by `authService.setAuthCookie` /
188
+ * `clearAuthCookie`. Opt-in: unset ⇒ the helpers fall back to their own
189
+ * defaults (`name: "access_token"`, `path: "/"`).
190
+ */
191
+ cookie?: CookieAuthConfig;
192
+ /**
193
+ * CSRF Origin-check config for cookie-authenticated unsafe-method requests.
194
+ */
195
+ csrf?: CsrfConfig;
186
196
  /**
187
197
  * @deprecated Use `accessToken` / `refreshToken`. Read via a backward-compatible shim.
188
198
  */
@@ -241,6 +251,61 @@ type LoginResult<UserType extends Auth> = {
241
251
  user: UserType;
242
252
  tokens: TokenPair;
243
253
  };
254
+ /**
255
+ * Configuration for the cookie an app opts into via `authService.setAuthCookie`
256
+ * / `clearAuthCookie` and reads back with `authMiddleware([], "cookie:<name>")`.
257
+ * Attribute flags (`httpOnly` / `sameSite` / `secure`) are deliberately NOT
258
+ * configurable here — they come from core's `secureCookieDefaults()`, the
259
+ * same floor every `response.cookie()` call gets, so a cookie auth session can
260
+ * never be weakened to a laxer policy than the framework default.
261
+ */
262
+ type CookieAuthConfig = {
263
+ /**
264
+ * Cookie name `setAuthCookie`/`clearAuthCookie` write and clear, and that a
265
+ * `cookie:<name>` token source must match to read it back.
266
+ * @default "access_token"
267
+ */
268
+ name?: string;
269
+ /**
270
+ * Cookie `Path` attribute.
271
+ * @default "/"
272
+ */
273
+ path?: string;
274
+ };
275
+ /**
276
+ * CSRF Origin-check configuration. Read via
277
+ * `authConfig.csrf.allowedOrigins()`.
278
+ */
279
+ type CsrfConfig = {
280
+ /**
281
+ * Extra origins allowed on a cookie-authenticated unsafe-method request,
282
+ * in addition to the request's own origin.
283
+ * @default []
284
+ */
285
+ allowedOrigins?: string[];
286
+ };
287
+ /**
288
+ * Per-call overrides for {@link AuthService.setAuthCookie}. Anything omitted
289
+ * falls back to `auth.cookie.*` config, then the package default. Cookie
290
+ * *attribute* flags (`httpOnly`/`sameSite`/`secure`) are never settable here —
291
+ * see {@link CookieAuthConfig}.
292
+ */
293
+ type SetAuthCookieOptions = {
294
+ /** Cookie name; defaults to `auth.cookie.name` (package default `"access_token"`). */name?: string; /** Cookie `Path`; defaults to `auth.cookie.path` (package default `"/"`). */
295
+ path?: string;
296
+ /**
297
+ * `Max-Age`, in seconds. Overrides the expiry `setAuthCookie` would
298
+ * otherwise derive from an `AccessTokenOutput`'s `expiresAt`. Passing a
299
+ * bare token string with no `maxAge` produces a session cookie (cleared
300
+ * when the browser closes).
301
+ */
302
+ maxAge?: number;
303
+ };
304
+ /** Per-call overrides for {@link AuthService.clearAuthCookie}. */
305
+ type ClearAuthCookieOptions = {
306
+ /** Cookie name; defaults to `auth.cookie.name` (package default `"access_token"`). */name?: string; /** Cookie `Path`; defaults to `auth.cookie.path` (package default `"/"`). Must match the `Path` the cookie was set with. */
307
+ path?: string;
308
+ };
244
309
  //#endregion
245
- export { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshTokenConfig, TokenFrom, TokenPair };
310
+ export { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair };
246
311
  //# sourceMappingURL=types.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"mappings":";;;;;;;;;;;AAyBA;;;;AAA0B;AAO1B;;;;AAAsC;AAGtC;;;;AAAqB;cAVR,aAAA;;;;;;KAOD,0BAAA;;KAGA,SAAA;AAMZ;AAAA,KAHY,eAAA,IAAmB,IAAA,EAAM,IAAA,eAAmB,OAAO;;KAGnD,eAAA,GAAkB,MAAM;EAClC,QAAQ;AAAA;;;;;;;KASE,iBAAA;EAeE;;;AAUH;AAMX;EAzBE,KAAA,UAAe,WAAA;;;;EAIf,MAAA;EA+BA;;;;EA1BA,SAAA,GAAY,SAAS;EAsDrB;;;AAA+C;AASjD;;;;;EArDE,SAAA;AAAA;;;;KAMU,kBAAA;EAsDR;;;;EAjDF,KAAA,UAAe,YAAA;EAoDkC;AAAA;AAYnD;;EA3DE,OAAA;EAiEA;AAMc;AAGhB;;EArEE,MAAA;EA2EiC;;;;;;;EAnEjC,SAAA;EA2FqB;;;;EAtFrB,QAAA;EAoEA;;;;EA/DA,UAAA;EAuEe;;;;EAlEf,kBAAA,GAAqB,0BAA0B;AAAA;AA4E1B;AAGvB;;;;AAQW;AAXY,KAnEX,eAAA;EACV,MAAA;EACA,SAAA,GAAY,SAAA;EACZ,SAAA;EACA,OAAA;IACE,MAAA;IACA,OAAA;IACA,SAAA;IACA,QAAA;IACA,UAAA;IACA,kBAAA,GAAqB,0BAA0B;EAAA;AAAA;;;;;;;;;KAYvC,cAAA;EAoGA;;;;;EA9FV,SAAA;EAgGiB;;;;;EA1FjB,cAAc;AAAA;AAAA,KAGJ,kBAAA;EAuFO;AAAA;;;EAlFjB,QAAA;IAAA,CACG,QAAA,WAAmB,UAAA,CAAW,IAAA;EAAA;;;;;EAMjC,eAAA,GAAkB,eAAA;;;;EAIlB,WAAA,GAAc,iBAAA;;;;EAId,YAAA,GAAe,kBAAA;;;;;;EAMf,QAAA,GAAW,cAAA;;;;EAIX,GAAA,GAAM,eAAA;AAAA;AAAA,KAGI,iBAAA;;;;EAIV,KAAA;;;;EAIA,SAAS;AAAA;;;;KAMC,SAAA;;;;EAIV,WAAA,EAAa,iBAAA;;;;EAIb,YAAA,GAAe,iBAAiB;AAAA;;;;KAMtB,UAAA;;;;EAIV,SAAA;;;;EAIA,EAAA;;;;EAIA,QAAA;;;;;EAKA,QAAA;;;;EAIA,OAAA,GAAU,MAAM;AAAA;AAAA,KAGN,WAAA,kBAA6B,IAAA;EACvC,IAAA,EAAM,QAAA;EACN,MAAA,EAAQ,SAAA;AAAA"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"mappings":";;;;;;;;;;;AAyBA;;;;AAA0B;AAO1B;;;;AAAsC;AAGtC;;;;AAAqB;cAVR,aAAA;;;;;;KAOD,0BAAA;;KAGA,SAAA;AAMZ;AAAA,KAHY,eAAA,IAAmB,IAAA,EAAM,IAAA,eAAmB,OAAO;;KAGnD,eAAA,GAAkB,MAAM;EAClC,QAAQ;AAAA;;;;;;;KASE,iBAAA;EAeE;;;AAUH;AAMX;EAzBE,KAAA,UAAe,WAAA;;;;EAIf,MAAA;EA+BA;;;;EA1BA,SAAA,GAAY,SAAS;EAsDrB;;;AAA+C;AASjD;;;;;EArDE,SAAA;AAAA;;;;KAMU,kBAAA;EAsDR;;;;EAjDF,KAAA,UAAe,YAAA;EAoDkC;AAAA;AAYnD;;EA3DE,OAAA;EAiEA;AAMc;AAGhB;;EArEE,MAAA;EA2EiC;;;;;;;EAnEjC,SAAA;EAqGM;;;;EAhGN,QAAA;EA8DsB;;;;EAzDtB,UAAA;EAmEc;;;;EA9Dd,kBAAA,GAAqB,0BAA0B;AAAA;;;;;;;KASrC,eAAA;EACV,MAAA;EACA,SAAA,GAAY,SAAA;EACZ,SAAA;EACA,OAAA;IACE,MAAA;IACA,OAAA;IACA,SAAA;IACA,QAAA;IACA,UAAA;IACA,kBAAA,GAAqB,0BAA0B;EAAA;AAAA;;;AA4FjB;AAMlC;;;;;KAtFY,cAAA;EAkGV;;;;;EA5FA,SAAA;EAwGU;;;;;EAlGV,cAAc;AAAA;AAAA,KAGJ,kBAAA;EA+FY;;;;EA1FtB,QAAA;IAAA,CACG,QAAA,WAAmB,UAAA,CAAW,IAAA;EAAA;EA2FhB;AAWnB;;;EAhGE,eAAA,GAAkB,eAAA;EA2Gd;AAON;;EA9GE,WAAA,GAAc,iBAAA;EAoHA;AAAA;AAShB;EAzHE,YAAA,GAAe,kBAAA;;;;;;EAMf,QAAA,GAAW,cAAA;EA8HL;AAIR;;;;EA5HE,MAAA,GAAS,gBAAA;;;;EAIT,IAAA,GAAO,UAAA;;;;EAIP,GAAA,GAAM,eAAA;AAAA;AAAA,KAGI,iBAAA;;;;EAIV,KAAA;;;;EAIA,SAAS;AAAA;;;;KAMC,SAAA;;;;EAIV,WAAA,EAAa,iBAAA;;;;EAIb,YAAA,GAAe,iBAAiB;AAAA;;;;KAMtB,UAAA;;;;EAIV,SAAA;;;;EAIA,EAAA;;;;EAIA,QAAA;;;;;EAKA,QAAA;;;;EAIA,OAAA,GAAU,MAAM;AAAA;AAAA,KAGN,WAAA,kBAA6B,IAAA;EACvC,IAAA,EAAM,QAAA;EACN,MAAA,EAAQ,SAAA;AAAA;;;;;;;;;KAWE,gBAAA;;;;;;EAMV,IAAA;;;;;EAKA,IAAI;AAAA;;;;;KAOM,UAAA;;;;;;EAMV,cAAc;AAAA;;;;;;;KASJ,oBAAA;wFAEV,IAAA;EAEA,IAAA;;;;;;;EAOA,MAAA;AAAA;;KAIU,sBAAA;wFAEV,IAAA;EAEA,IAAI;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"sourcesContent":["import { type ChildModel } from \"@warlock.js/cascade\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport type { AccessToken } from \"../models/access-token\";\r\nimport type { Auth } from \"../models/auth.model\";\r\nimport type { RefreshToken } from \"../models/refresh-token\";\r\n\r\n/**\r\n * Symbol to indicate no expiration for tokens\r\n * Use this when you explicitly want tokens to never expire\r\n *\r\n * @example\r\n * ```typescript\r\n * // src/config/auth.ts\r\n * import { NO_EXPIRATION, type AuthConfigurations } from \"@warlock.js/auth\";\r\n *\r\n * const authConfigurations: AuthConfigurations = {\r\n * accessToken: {\r\n * secret: env(\"JWT_SECRET\"),\r\n * expiresIn: NO_EXPIRATION, // Token expires within 100 years\r\n * },\r\n * };\r\n *\r\n * export default authConfigurations;\r\n * ```\r\n */\r\nexport const NO_EXPIRATION = \"100y\";\r\n\r\n/**\r\n * Behavior when logout is called without a refresh token\r\n * - \"revoke-all\": Revoke all refresh tokens for the user (secure default)\r\n * - \"error\": Return an error requiring the refresh token\r\n */\r\nexport type LogoutWithoutTokenBehavior = \"revoke-all\" | \"error\";\r\n\r\n/** The single request credential source an auth middleware instance accepts. */\r\nexport type TokenFrom = \"header\" | `cookie:${string}`;\r\n\r\n/** Application policy that decides whether a resolved user may authenticate. */\r\nexport type CanAuthenticate = (user: Auth) => boolean | Promise<boolean>;\r\n\r\n/** Password credentials plus the application-defined identity fields. */\r\nexport type AuthCredentials = Record<string, unknown> & {\r\n password: string;\r\n};\r\n\r\n/**\r\n * Access-token configuration.\r\n *\r\n * `secret` may also be supplied via the deprecated `jwt.secret` (resolved by a\r\n * backward-compatible shim); one of the two is required at runtime.\r\n */\r\nexport type AccessTokenConfig = {\r\n /**\r\n * Override the persisted access-token model — extend {@link AccessToken} to\r\n * add columns (e.g. a tenant key) or rename them, then register the subclass\r\n * here. Defaults to the package's `AccessToken`.\r\n */\r\n model?: typeof AccessToken;\r\n /**\r\n * Secret used to sign access tokens.\r\n */\r\n secret?: string;\r\n /**\r\n * JWT algorithm.\r\n * @default \"HS256\"\r\n */\r\n algorithm?: Algorithm;\r\n /**\r\n * Access-token lifetime — a duration string `ms` parses to a **positive**\r\n * number of milliseconds. A value `ms` cannot parse (`\"30dayz\"`), a\r\n * non-positive one (`\"0d\"`), or a bare number throws naming this key on the\r\n * first token issue: each of them would otherwise sign a token with no `exp`\r\n * claim at all.\r\n * @example \"1h\" or NO_EXPIRATION\r\n * @default \"1h\"\r\n */\r\n expiresIn?: string;\r\n};\r\n\r\n/**\r\n * Refresh-token configuration.\r\n */\r\nexport type RefreshTokenConfig = {\r\n /**\r\n * Override the persisted refresh-token model — extend {@link RefreshToken}.\r\n * Defaults to the package's `RefreshToken`.\r\n */\r\n model?: typeof RefreshToken;\r\n /**\r\n * Enable refresh tokens.\r\n * @default true\r\n */\r\n enabled?: boolean;\r\n /**\r\n * Separate secret for refresh tokens (recommended for security). Falls back\r\n * to the access-token secret when omitted.\r\n */\r\n secret?: string;\r\n /**\r\n * Refresh-token lifetime — a duration string `ms` parses to a **positive**\r\n * number of milliseconds; an unusable value throws naming this key on issue\r\n * (see {@link AccessTokenConfig.expiresIn}).\r\n * @example \"7d\" or \"1w\"\r\n * @default \"7d\"\r\n */\r\n expiresIn?: string;\r\n /**\r\n * Rotate the refresh token on each use (old token revoked, replay detected).\r\n * @default true\r\n */\r\n rotation?: boolean;\r\n /**\r\n * Maximum active refresh tokens per user; the oldest are revoked past it.\r\n * @default 5\r\n */\r\n maxPerUser?: number;\r\n /**\r\n * Behavior when logout is called without a refresh token.\r\n * @default \"revoke-all\"\r\n */\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n};\r\n\r\n/**\r\n * Legacy JWT configuration shape. Still honored by the resolver shim with a\r\n * deprecation warning.\r\n *\r\n * @deprecated Use the top-level `accessToken` / `refreshToken` blocks.\r\n */\r\nexport type LegacyJwtConfig = {\r\n secret?: string;\r\n algorithm?: Algorithm;\r\n expiresIn?: string;\r\n refresh?: {\r\n secret?: string;\r\n enabled?: boolean;\r\n expiresIn?: string;\r\n rotation?: boolean;\r\n maxPerUser?: number;\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n };\r\n};\r\n\r\n/**\r\n * Page-route auth-failure behavior. When a guarded PAGE route (a React-SSR page,\r\n * `route.isPage`) rejects a logged-out browser, this decides whether it redirects\r\n * to a login page instead of returning the API-style JSON 401.\r\n *\r\n * Read at runtime through `authConfig.pageAuth` (`auth.pageAuth.*`). API routes\r\n * always keep the JSON 401 contract regardless of this setting.\r\n */\r\nexport type PageAuthConfig = {\r\n /**\r\n * Login path a guarded page route redirects a logged-out browser to (e.g.\r\n * `\"/login\"`). Unset ⇒ page-route auth failures keep the JSON 401, so the\r\n * behavior is opt-in and backward-compatible.\r\n */\r\n loginPath?: string;\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.\r\n * @default \"returnUrl\"\r\n */\r\n returnUrlParam?: string;\r\n};\r\n\r\nexport type AuthConfigurations = {\r\n /**\r\n * Define all user types — maps a user-type slug to its `Auth` model class so\r\n * tokens and the middleware can resolve the right model.\r\n */\r\n userType: {\r\n [userType: string]: ChildModel<Auth>;\r\n };\r\n /**\r\n * Decide whether a resolved user may authenticate or receive new tokens.\r\n * @default () => true\r\n */\r\n canAuthenticate?: CanAuthenticate;\r\n /**\r\n * Access-token configuration (secret, expiry, algorithm, model override).\r\n */\r\n accessToken?: AccessTokenConfig;\r\n /**\r\n * Refresh-token configuration (rotation, lifetime, cap, model override).\r\n */\r\n refreshToken?: RefreshTokenConfig;\r\n /**\r\n * Page-route auth-failure behavior — set `loginPath` to redirect a logged-out\r\n * browser on a guarded PAGE route to a login page instead of the JSON 401.\r\n * Opt-in; API routes are unaffected. Read via `authConfig.pageAuth`.\r\n */\r\n pageAuth?: PageAuthConfig;\r\n /**\r\n * @deprecated Use `accessToken` / `refreshToken`. Read via a backward-compatible shim.\r\n */\r\n jwt?: LegacyJwtConfig;\r\n};\r\n\r\nexport type AccessTokenOutput = {\r\n /**\r\n * JWT Token\r\n */\r\n token: string;\r\n /**\r\n * Exprie time in ISO format UTC time\r\n */\r\n expiresAt: string;\r\n};\r\n\r\n/**\r\n * Token pair returned after login or token refresh\r\n */\r\nexport type TokenPair = {\r\n /**\r\n * JWT access token (short-lived)\r\n */\r\n accessToken: AccessTokenOutput;\r\n /**\r\n * JWT refresh token (long-lived)\r\n */\r\n refreshToken?: AccessTokenOutput;\r\n};\r\n\r\n/**\r\n * Device information for session tracking\r\n */\r\nexport type DeviceInfo = {\r\n /**\r\n * User agent string from request\r\n */\r\n userAgent?: string;\r\n /**\r\n * Client IP address\r\n */\r\n ip?: string;\r\n /**\r\n * Optional device identifier\r\n */\r\n deviceId?: string;\r\n /**\r\n * Token family ID (for rotation tracking)\r\n * @internal\r\n */\r\n familyId?: string;\r\n /**\r\n * Access token payload\r\n */\r\n payload?: Record<string, unknown>;\r\n};\r\n\r\nexport type LoginResult<UserType extends Auth> = {\r\n user: UserType;\r\n tokens: TokenPair;\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,gBAAgB"}
1
+ {"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"sourcesContent":["import { type ChildModel } from \"@warlock.js/cascade\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport type { AccessToken } from \"../models/access-token\";\r\nimport type { Auth } from \"../models/auth.model\";\r\nimport type { RefreshToken } from \"../models/refresh-token\";\r\n\r\n/**\r\n * Symbol to indicate no expiration for tokens\r\n * Use this when you explicitly want tokens to never expire\r\n *\r\n * @example\r\n * ```typescript\r\n * // src/config/auth.ts\r\n * import { NO_EXPIRATION, type AuthConfigurations } from \"@warlock.js/auth\";\r\n *\r\n * const authConfigurations: AuthConfigurations = {\r\n * accessToken: {\r\n * secret: env(\"JWT_SECRET\"),\r\n * expiresIn: NO_EXPIRATION, // Token expires within 100 years\r\n * },\r\n * };\r\n *\r\n * export default authConfigurations;\r\n * ```\r\n */\r\nexport const NO_EXPIRATION = \"100y\";\r\n\r\n/**\r\n * Behavior when logout is called without a refresh token\r\n * - \"revoke-all\": Revoke all refresh tokens for the user (secure default)\r\n * - \"error\": Return an error requiring the refresh token\r\n */\r\nexport type LogoutWithoutTokenBehavior = \"revoke-all\" | \"error\";\r\n\r\n/** The single request credential source an auth middleware instance accepts. */\r\nexport type TokenFrom = \"header\" | `cookie:${string}`;\r\n\r\n/** Application policy that decides whether a resolved user may authenticate. */\r\nexport type CanAuthenticate = (user: Auth) => boolean | Promise<boolean>;\r\n\r\n/** Password credentials plus the application-defined identity fields. */\r\nexport type AuthCredentials = Record<string, unknown> & {\r\n password: string;\r\n};\r\n\r\n/**\r\n * Access-token configuration.\r\n *\r\n * `secret` may also be supplied via the deprecated `jwt.secret` (resolved by a\r\n * backward-compatible shim); one of the two is required at runtime.\r\n */\r\nexport type AccessTokenConfig = {\r\n /**\r\n * Override the persisted access-token model — extend {@link AccessToken} to\r\n * add columns (e.g. a tenant key) or rename them, then register the subclass\r\n * here. Defaults to the package's `AccessToken`.\r\n */\r\n model?: typeof AccessToken;\r\n /**\r\n * Secret used to sign access tokens.\r\n */\r\n secret?: string;\r\n /**\r\n * JWT algorithm.\r\n * @default \"HS256\"\r\n */\r\n algorithm?: Algorithm;\r\n /**\r\n * Access-token lifetime — a duration string `ms` parses to a **positive**\r\n * number of milliseconds. A value `ms` cannot parse (`\"30dayz\"`), a\r\n * non-positive one (`\"0d\"`), or a bare number throws naming this key on the\r\n * first token issue: each of them would otherwise sign a token with no `exp`\r\n * claim at all.\r\n * @example \"1h\" or NO_EXPIRATION\r\n * @default \"1h\"\r\n */\r\n expiresIn?: string;\r\n};\r\n\r\n/**\r\n * Refresh-token configuration.\r\n */\r\nexport type RefreshTokenConfig = {\r\n /**\r\n * Override the persisted refresh-token model — extend {@link RefreshToken}.\r\n * Defaults to the package's `RefreshToken`.\r\n */\r\n model?: typeof RefreshToken;\r\n /**\r\n * Enable refresh tokens.\r\n * @default true\r\n */\r\n enabled?: boolean;\r\n /**\r\n * Separate secret for refresh tokens (recommended for security). Falls back\r\n * to the access-token secret when omitted.\r\n */\r\n secret?: string;\r\n /**\r\n * Refresh-token lifetime — a duration string `ms` parses to a **positive**\r\n * number of milliseconds; an unusable value throws naming this key on issue\r\n * (see {@link AccessTokenConfig.expiresIn}).\r\n * @example \"7d\" or \"1w\"\r\n * @default \"7d\"\r\n */\r\n expiresIn?: string;\r\n /**\r\n * Rotate the refresh token on each use (old token revoked, replay detected).\r\n * @default true\r\n */\r\n rotation?: boolean;\r\n /**\r\n * Maximum active refresh tokens per user; the oldest are revoked past it.\r\n * @default 5\r\n */\r\n maxPerUser?: number;\r\n /**\r\n * Behavior when logout is called without a refresh token.\r\n * @default \"revoke-all\"\r\n */\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n};\r\n\r\n/**\r\n * Legacy JWT configuration shape. Still honored by the resolver shim with a\r\n * deprecation warning.\r\n *\r\n * @deprecated Use the top-level `accessToken` / `refreshToken` blocks.\r\n */\r\nexport type LegacyJwtConfig = {\r\n secret?: string;\r\n algorithm?: Algorithm;\r\n expiresIn?: string;\r\n refresh?: {\r\n secret?: string;\r\n enabled?: boolean;\r\n expiresIn?: string;\r\n rotation?: boolean;\r\n maxPerUser?: number;\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n };\r\n};\r\n\r\n/**\r\n * Page-route auth-failure behavior. When a guarded PAGE route (a React-SSR page,\r\n * `route.isPage`) rejects a logged-out browser, this decides whether it redirects\r\n * to a login page instead of returning the API-style JSON 401.\r\n *\r\n * Read at runtime through `authConfig.pageAuth` (`auth.pageAuth.*`). API routes\r\n * always keep the JSON 401 contract regardless of this setting.\r\n */\r\nexport type PageAuthConfig = {\r\n /**\r\n * Login path a guarded page route redirects a logged-out browser to (e.g.\r\n * `\"/login\"`). Unset ⇒ page-route auth failures keep the JSON 401, so the\r\n * behavior is opt-in and backward-compatible.\r\n */\r\n loginPath?: string;\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.\r\n * @default \"returnUrl\"\r\n */\r\n returnUrlParam?: string;\r\n};\r\n\r\nexport type AuthConfigurations = {\r\n /**\r\n * Define all user types — maps a user-type slug to its `Auth` model class so\r\n * tokens and the middleware can resolve the right model.\r\n */\r\n userType: {\r\n [userType: string]: ChildModel<Auth>;\r\n };\r\n /**\r\n * Decide whether a resolved user may authenticate or receive new tokens.\r\n * @default () => true\r\n */\r\n canAuthenticate?: CanAuthenticate;\r\n /**\r\n * Access-token configuration (secret, expiry, algorithm, model override).\r\n */\r\n accessToken?: AccessTokenConfig;\r\n /**\r\n * Refresh-token configuration (rotation, lifetime, cap, model override).\r\n */\r\n refreshToken?: RefreshTokenConfig;\r\n /**\r\n * Page-route auth-failure behavior — set `loginPath` to redirect a logged-out\r\n * browser on a guarded PAGE route to a login page instead of the JSON 401.\r\n * Opt-in; API routes are unaffected. Read via `authConfig.pageAuth`.\r\n */\r\n pageAuth?: PageAuthConfig;\r\n /**\r\n * Cookie auth session config — read by `authService.setAuthCookie` /\r\n * `clearAuthCookie`. Opt-in: unset ⇒ the helpers fall back to their own\r\n * defaults (`name: \"access_token\"`, `path: \"/\"`).\r\n */\r\n cookie?: CookieAuthConfig;\r\n /**\r\n * CSRF Origin-check config for cookie-authenticated unsafe-method requests.\r\n */\r\n csrf?: CsrfConfig;\r\n /**\r\n * @deprecated Use `accessToken` / `refreshToken`. Read via a backward-compatible shim.\r\n */\r\n jwt?: LegacyJwtConfig;\r\n};\r\n\r\nexport type AccessTokenOutput = {\r\n /**\r\n * JWT Token\r\n */\r\n token: string;\r\n /**\r\n * Exprie time in ISO format UTC time\r\n */\r\n expiresAt: string;\r\n};\r\n\r\n/**\r\n * Token pair returned after login or token refresh\r\n */\r\nexport type TokenPair = {\r\n /**\r\n * JWT access token (short-lived)\r\n */\r\n accessToken: AccessTokenOutput;\r\n /**\r\n * JWT refresh token (long-lived)\r\n */\r\n refreshToken?: AccessTokenOutput;\r\n};\r\n\r\n/**\r\n * Device information for session tracking\r\n */\r\nexport type DeviceInfo = {\r\n /**\r\n * User agent string from request\r\n */\r\n userAgent?: string;\r\n /**\r\n * Client IP address\r\n */\r\n ip?: string;\r\n /**\r\n * Optional device identifier\r\n */\r\n deviceId?: string;\r\n /**\r\n * Token family ID (for rotation tracking)\r\n * @internal\r\n */\r\n familyId?: string;\r\n /**\r\n * Access token payload\r\n */\r\n payload?: Record<string, unknown>;\r\n};\r\n\r\nexport type LoginResult<UserType extends Auth> = {\r\n user: UserType;\r\n tokens: TokenPair;\r\n};\r\n\r\n/**\r\n * Configuration for the cookie an app opts into via `authService.setAuthCookie`\r\n * / `clearAuthCookie` and reads back with `authMiddleware([], \"cookie:<name>\")`.\r\n * Attribute flags (`httpOnly` / `sameSite` / `secure`) are deliberately NOT\r\n * configurable here — they come from core's `secureCookieDefaults()`, the\r\n * same floor every `response.cookie()` call gets, so a cookie auth session can\r\n * never be weakened to a laxer policy than the framework default.\r\n */\r\nexport type CookieAuthConfig = {\r\n /**\r\n * Cookie name `setAuthCookie`/`clearAuthCookie` write and clear, and that a\r\n * `cookie:<name>` token source must match to read it back.\r\n * @default \"access_token\"\r\n */\r\n name?: string;\r\n /**\r\n * Cookie `Path` attribute.\r\n * @default \"/\"\r\n */\r\n path?: string;\r\n};\r\n\r\n/**\r\n * CSRF Origin-check configuration. Read via\r\n * `authConfig.csrf.allowedOrigins()`.\r\n */\r\nexport type CsrfConfig = {\r\n /**\r\n * Extra origins allowed on a cookie-authenticated unsafe-method request,\r\n * in addition to the request's own origin.\r\n * @default []\r\n */\r\n allowedOrigins?: string[];\r\n};\r\n\r\n/**\r\n * Per-call overrides for {@link AuthService.setAuthCookie}. Anything omitted\r\n * falls back to `auth.cookie.*` config, then the package default. Cookie\r\n * *attribute* flags (`httpOnly`/`sameSite`/`secure`) are never settable here —\r\n * see {@link CookieAuthConfig}.\r\n */\r\nexport type SetAuthCookieOptions = {\r\n /** Cookie name; defaults to `auth.cookie.name` (package default `\"access_token\"`). */\r\n name?: string;\r\n /** Cookie `Path`; defaults to `auth.cookie.path` (package default `\"/\"`). */\r\n path?: string;\r\n /**\r\n * `Max-Age`, in seconds. Overrides the expiry `setAuthCookie` would\r\n * otherwise derive from an `AccessTokenOutput`'s `expiresAt`. Passing a\r\n * bare token string with no `maxAge` produces a session cookie (cleared\r\n * when the browser closes).\r\n */\r\n maxAge?: number;\r\n};\r\n\r\n/** Per-call overrides for {@link AuthService.clearAuthCookie}. */\r\nexport type ClearAuthCookieOptions = {\r\n /** Cookie name; defaults to `auth.cookie.name` (package default `\"access_token\"`). */\r\n name?: string;\r\n /** Cookie `Path`; defaults to `auth.cookie.path` (package default `\"/\"`). Must match the `Path` the cookie was set with. */\r\n path?: string;\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,gBAAgB"}
package/esm/index.d.mts CHANGED
@@ -3,16 +3,20 @@ import { registerAuthPurgeNeverExpiringCommand } from "./commands/auth-purge-nev
3
3
  import { registerJWTSecretGeneratorCommand } from "./commands/jwt-secret-generator-command.mjs";
4
4
  import { Auth } from "./models/auth.model.mjs";
5
5
  import { AccessToken, accessTokenSchema } from "./models/access-token/access-token.model.mjs";
6
- import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshTokenConfig, TokenFrom, TokenPair } from "./contracts/types.mjs";
6
+ import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair } from "./contracts/types.mjs";
7
7
  import { RefreshToken, RefreshTokenIssueOptions, refreshTokenSchema } from "./models/refresh-token/refresh-token.model.mjs";
8
8
  import { Authenticable } from "./contracts/auth-contract.mjs";
9
+ import { RequestUser } from "./contracts/request-locals.mjs";
9
10
  import { authMiddleware } from "./middleware/auth.middleware.mjs";
11
+ import { AuthErrorCodes } from "./utils/auth-error-codes.mjs";
12
+ import { CsrfOriginMismatchError, CsrfOriginMismatchReason, assertCsrfOriginAllowed, requiresCsrfOriginCheck } from "./middleware/csrf-origin-check.mjs";
10
13
  import { LoginThrottleIdentifier, LoginThrottleOptions, loginThrottleMiddleware } from "./middleware/login-throttle.middleware.mjs";
11
14
  import { authMigrations } from "./models/index.mjs";
12
15
  import { AuthEventCallback, AuthEventName, AuthEventPayloads, authEvents } from "./services/auth-events.mjs";
13
16
  import { authService } from "./services/auth.service.mjs";
17
+ import { currentUser } from "./services/current-user.mjs";
14
18
  import { generateJWTSecret } from "./services/generate-jwt-secret.mjs";
15
- import { AuthErrorCodes } from "./utils/auth-error-codes.mjs";
16
19
  import { TokenType, TokenTypeError, isInvalidCredentialError, jwt } from "./services/jwt.mjs";
17
20
  import { isNeverExpiring, isUsableExpiry, tokenHasExpClaim } from "./utils/token-expiry.mjs";
18
- export { AccessToken, AccessTokenConfig, AccessTokenOutput, Auth, AuthConfigurations, AuthCredentials, AuthErrorCodes, AuthEventCallback, AuthEventName, AuthEventPayloads, Authenticable, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LoginThrottleIdentifier, LoginThrottleOptions, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshToken, RefreshTokenConfig, RefreshTokenIssueOptions, TokenFrom, TokenPair, TokenType, TokenTypeError, accessTokenSchema, authEvents, authMiddleware, authMigrations, authService, generateJWTSecret, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, jwt, loginThrottleMiddleware, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerJWTSecretGeneratorCommand, tokenHasExpClaim };
21
+ export { AccessToken, AccessTokenConfig, AccessTokenOutput, Auth, AuthConfigurations, AuthCredentials, AuthErrorCodes, AuthEventCallback, AuthEventName, AuthEventPayloads, Authenticable, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, CsrfOriginMismatchError, CsrfOriginMismatchReason, DeviceInfo, LegacyJwtConfig, LoginResult, LoginThrottleIdentifier, LoginThrottleOptions, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshToken, RefreshTokenConfig, RefreshTokenIssueOptions, RequestUser, SetAuthCookieOptions, TokenFrom, TokenPair, TokenType, TokenTypeError, accessTokenSchema, assertCsrfOriginAllowed, authEvents, authMiddleware, authMigrations, authService, currentUser, generateJWTSecret, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, jwt, loginThrottleMiddleware, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerJWTSecretGeneratorCommand, requiresCsrfOriginCheck, tokenHasExpClaim };
22
+ import "./contracts/request-locals.mjs";
package/esm/index.mjs CHANGED
@@ -10,12 +10,14 @@ import { registerAuthPurgeNeverExpiringCommand } from "./commands/auth-purge-nev
10
10
  import { generateJWTSecret } from "./services/generate-jwt-secret.mjs";
11
11
  import { registerJWTSecretGeneratorCommand } from "./commands/jwt-secret-generator-command.mjs";
12
12
  import { NO_EXPIRATION } from "./contracts/types.mjs";
13
+ import { CsrfOriginMismatchError, assertCsrfOriginAllowed, requiresCsrfOriginCheck } from "./middleware/csrf-origin-check.mjs";
13
14
  import { authMiddleware } from "./middleware/auth.middleware.mjs";
14
15
  import { loginThrottleMiddleware } from "./middleware/login-throttle.middleware.mjs";
15
16
  import "./middleware/index.mjs";
17
+ import { currentUser } from "./services/current-user.mjs";
16
18
  import "./services/index.mjs";
17
19
  import { Auth } from "./models/auth.model.mjs";
18
20
  import { authMigrations } from "./models/index.mjs";
19
21
  import "./utils/index.mjs";
20
22
 
21
- export { AccessToken, Auth, AuthErrorCodes, NO_EXPIRATION, RefreshToken, TokenTypeError, accessTokenSchema, authEvents, authMiddleware, authMigrations, authService, generateJWTSecret, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, jwt, loginThrottleMiddleware, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerJWTSecretGeneratorCommand, tokenHasExpClaim };
23
+ export { AccessToken, Auth, AuthErrorCodes, CsrfOriginMismatchError, NO_EXPIRATION, RefreshToken, TokenTypeError, accessTokenSchema, assertCsrfOriginAllowed, authEvents, authMiddleware, authMigrations, authService, currentUser, generateJWTSecret, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, jwt, loginThrottleMiddleware, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerJWTSecretGeneratorCommand, requiresCsrfOriginCheck, tokenHasExpClaim };
@@ -1 +1 @@
1
- {"version":3,"file":"auth.middleware.d.mts","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"mappings":";;;;;;AAwGA;;;;;;;;;AAGa;;;;;;;;;;iBAHG,cAAA,CACd,eAAA,qBACA,SAAA,GAAW,SAAA,GACV,UAAU"}
1
+ {"version":3,"file":"auth.middleware.d.mts","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"mappings":";;;;;;AA6GA;;;;;;;;;AAGa;;;;;;;;;;iBAHG,cAAA,CACd,eAAA,qBACA,SAAA,GAAW,SAAA,GACV,UAAU"}
@@ -4,6 +4,7 @@ import { authConfig } from "../services/auth-config.mjs";
4
4
  import { AuthErrorCodes } from "../utils/auth-error-codes.mjs";
5
5
  import { isInvalidCredentialError, jwt } from "../services/jwt.mjs";
6
6
  import { authService } from "../services/auth.service.mjs";
7
+ import { CsrfOriginMismatchError, assertCsrfOriginAllowed, requiresCsrfOriginCheck } from "./csrf-origin-check.mjs";
7
8
  import { config, t } from "@warlock.js/core";
8
9
  import { log } from "@warlock.js/logger";
9
10
 
@@ -90,13 +91,23 @@ function authMiddleware(allowedUserType, tokenFrom = "header") {
90
91
  error: t("auth.errors.missingAccessToken"),
91
92
  errorCode: "EC001"
92
93
  });
94
+ if (requiresCsrfOriginCheck(tokenFrom, request.method)) try {
95
+ assertCsrfOriginAllowed(request);
96
+ } catch (error) {
97
+ if (!(error instanceof CsrfOriginMismatchError)) throw error;
98
+ log.error("http", "auth", error);
99
+ return response.forbidden({
100
+ error: t("auth.errors.csrfOriginMismatch"),
101
+ errorCode: "EC006"
102
+ });
103
+ }
93
104
  let decoded;
94
105
  try {
95
106
  decoded = await jwt.verify(authorizationValue);
96
107
  } catch (error) {
97
108
  if (!isInvalidCredentialError(error)) throw error;
98
109
  log.error("http", "auth", error);
99
- request.clearCurrentUser();
110
+ request.locals.user = void 0;
100
111
  return rejectUnauthorized({
101
112
  request,
102
113
  response
@@ -152,7 +163,7 @@ function authMiddleware(allowedUserType, tokenFrom = "header") {
152
163
  error: t("auth.errors.unauthorized"),
153
164
  errorCode: "EC003"
154
165
  });
155
- request.user = currentUser;
166
+ request.locals.user = currentUser;
156
167
  };
157
168
  return auth;
158
169
  }
@@ -1 +1 @@
1
- {"version":3,"file":"auth.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"sourcesContent":["import { config, type HttpContext, t, type Middleware, type Request } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport type { TokenFrom } from \"../contracts/types\";\r\nimport { AccessToken } from \"../models/access-token\";\r\nimport { authConfig } from \"../services/auth-config\";\r\nimport { authService } from \"../services/auth.service\";\r\nimport { isInvalidCredentialError, jwt } from \"../services/jwt\";\r\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\r\n\r\n/** The 401 body shape every rejection in this middleware carries. */\r\ntype UnauthorizedPayload = { error: string; errorCode: string };\r\n\r\n/**\r\n * Reject an unauthenticated request, choosing the representation by ROUTE KIND.\r\n *\r\n * An API route always gets the JSON 401 it has always gotten. A PAGE route\r\n * (`request.route.isPage` — the React-SSR pages) instead redirects a logged-out\r\n * browser to the configured login path with a `returnUrl`, so a human\r\n * navigating to a guarded page lands on the login screen rather than reading a\r\n * raw JSON error blob — the defect in finding b9ab9804.\r\n *\r\n * The redirect is OPT-IN and backward-compatible: with no `auth.pageAuth.loginPath`\r\n * configured, even a page route falls back to the JSON 401, so an app that has\r\n * not asked for page-login UX keeps exactly its current behavior. The API 401\r\n * contract is never changed for API routes.\r\n *\r\n * `returnUrl` carries `request.url` — the relative path + query the browser was\r\n * on — never the absolute URL: a relative target cannot be turned into an\r\n * open-redirect off this app's origin.\r\n */\r\nfunction rejectUnauthorized({ request, response }: HttpContext, payload: UnauthorizedPayload) {\r\n const loginPath = authConfig.pageAuth.loginPath();\r\n\r\n if (loginPath && request.route?.isPage) {\r\n const separator = loginPath.includes(\"?\") ? \"&\" : \"?\";\r\n const returnUrl = encodeURIComponent(request.url);\r\n\r\n return response.redirect(\r\n `${loginPath}${separator}${authConfig.pageAuth.returnUrlParam()}=${returnUrl}`,\r\n );\r\n }\r\n\r\n return response.unauthorized(payload);\r\n}\r\n\r\n/**\r\n * Decoded access-token claims the middleware reads. The full payload carries\r\n * more (`created_at`, `tokenType`, `iat`, `exp`) but only these drive routing.\r\n */\r\ntype DecodedAccessToken = {\r\n id: string | number;\r\n userType?: string;\r\n};\r\n\r\n/**\r\n * Whether the persisted row says the token is dead.\r\n *\r\n * The answer belongs to the model (`AccessToken.isExpired`), so a registered\r\n * override that renames or reshapes its expiry column stays authoritative and\r\n * the middleware never touches a column name. A row that cannot answer at all —\r\n * an override that dropped the getter — is treated as **expired**: the failure\r\n * mode of this whole defect class was a check that quietly answered \"fine\" when\r\n * it had nothing to check, and that is not repeated here.\r\n *\r\n * This is independent of the `exp` claim required by `jwt.verify`. That guard\r\n * catches a token whose *claims* carry no deadline; this one catches a token\r\n * whose *row* says the deadline has passed — a logged-out or expired session\r\n * whose JWT is still within its own lifetime. Neither subsumes the other.\r\n */\r\nfunction accessTokenRowIsExpired(accessToken: AccessToken): boolean {\r\n return typeof accessToken.isExpired === \"boolean\" ? accessToken.isExpired : true;\r\n}\r\n\r\nfunction readCredential(request: Request, tokenFrom: TokenFrom): string {\r\n if (tokenFrom === \"header\") {\r\n return request.authorizationValue;\r\n }\r\n\r\n const value = request.cookie(tokenFrom.slice(\"cookie:\".length));\r\n\r\n return value ? String(value) : \"\";\r\n}\r\n\r\n/**\r\n * Build a route gate that always requires an authenticated request.\r\n *\r\n * The argument is mandatory and selects which user types may pass:\r\n * - `[]` — any authenticated user (token required, type not checked).\r\n * - `\"admin\"` / `[\"admin\", \"staff\"]` — token required AND the user's\r\n * `userType` must be one of the listed types.\r\n *\r\n * There is no anonymous/optional mode: a request without a valid access\r\n * token is always rejected with `401`. Routes that should be public\r\n * simply omit the middleware.\r\n *\r\n * `tokenFrom` selects one credential source and defaults to `\"header\"`.\r\n * A single source avoids making credential precedence depend on array order.\r\n *\r\n * @example\r\n * router.get(\"/account\", authMiddleware([]), accountController);\r\n * router.get(\"/admin\", authMiddleware(\"admin\"), adminController);\r\n * router.get(\"/back-office\", authMiddleware([\"admin\", \"staff\"]), backOfficeController);\r\n * router.get(\"/browser-account\", authMiddleware([], \"cookie:token\"), accountController);\r\n */\r\nexport function authMiddleware(\r\n allowedUserType: string | string[],\r\n tokenFrom: TokenFrom = \"header\",\r\n): Middleware {\r\n const allowedTypes = Array.isArray(allowedUserType) ? allowedUserType : [allowedUserType];\r\n\r\n const auth: Middleware = async ({ request, response }) => {\r\n const authorizationValue = readCredential(request, tokenFrom);\r\n\r\n if (!authorizationValue) {\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.missingAccessToken\"),\r\n errorCode: AuthErrorCodes.MissingAccessToken,\r\n });\r\n }\r\n\r\n let decoded: DecodedAccessToken;\r\n\r\n // The ONLY try in this middleware, and it wraps a single call. Everything\r\n // after it is storage and configuration, where an exception means the\r\n // server is broken rather than the caller (D5).\r\n try {\r\n decoded = await jwt.verify<DecodedAccessToken>(authorizationValue);\r\n } catch (error) {\r\n if (!isInvalidCredentialError(error)) {\r\n // A DB/cache outage, a missing `JWT_SECRET`, or any other server-side\r\n // fault — not a verdict on this credential. Propagate to the request's\r\n // normal error path (a 500 monitoring can see) instead of answering\r\n // 401 and clearing the caller's session.\r\n throw error;\r\n }\r\n\r\n // A forged, malformed, expired token — or (D6) a `tokenType` mismatch —\r\n // is not an incident: it is a request carrying a credential the server\r\n // will never accept.\r\n log.error(\"http\", \"auth\", error);\r\n\r\n request.clearCurrentUser();\r\n\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n request.decodedAccessToken = decoded;\r\n\r\n // A valid signature is not enough — the token must still exist in storage,\r\n // so deleting the row (logout) invalidates it before its JWT expiry.\r\n const AccessTokenModel = config.key(\"auth.accessToken.model\", AccessToken);\r\n const accessToken = await AccessTokenModel.findByToken(authorizationValue);\r\n\r\n if (!accessToken) {\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n // ... and the row must still be live. Existence alone was the whole check\r\n // before 4.12.0, so a row whose own `expires_at` had passed still opened\r\n // the gate. The stored expiry is now enforced, and the dead row is\r\n // removed on the way out rather than left for the cleanup command.\r\n if (accessTokenRowIsExpired(accessToken)) {\r\n await accessToken.destroy();\r\n\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n const userType = decoded.userType ?? accessToken.userType;\r\n\r\n if (allowedTypes.length && !allowedTypes.includes(userType)) {\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.unauthorized\"),\r\n errorCode: AuthErrorCodes.Unauthorized,\r\n });\r\n }\r\n\r\n const UserModel = config.key(`auth.userType.${userType}`);\r\n\r\n if (!UserModel) {\r\n // Configuration, not credentials. Throwing keeps a mis-registered app\r\n // loudly broken instead of quietly rejecting every request of this type.\r\n throw new Error(`User type ${userType} is unknown type.`);\r\n }\r\n\r\n const currentUser = await UserModel.find(decoded.id);\r\n\r\n if (!currentUser) {\r\n await accessToken.destroy();\r\n\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n if (!(await authService.canAuthenticate(currentUser))) {\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.unauthorized\"),\r\n errorCode: AuthErrorCodes.Unauthorized,\r\n });\r\n }\r\n\r\n request.user = currentUser;\r\n };\r\n\r\n return auth;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAS,mBAAmB,EAAE,SAAS,YAAyB,SAA8B;CAC5F,MAAM,YAAY,WAAW,SAAS,UAAU;CAEhD,IAAI,aAAa,QAAQ,OAAO,QAAQ;EACtC,MAAM,YAAY,UAAU,SAAS,GAAG,IAAI,MAAM;EAClD,MAAM,YAAY,mBAAmB,QAAQ,GAAG;EAEhD,OAAO,SAAS,SACd,GAAG,YAAY,YAAY,WAAW,SAAS,eAAe,EAAE,GAAG,WACrE;CACF;CAEA,OAAO,SAAS,aAAa,OAAO;AACtC;;;;;;;;;;;;;;;;AA0BA,SAAS,wBAAwB,aAAmC;CAClE,OAAO,OAAO,YAAY,cAAc,YAAY,YAAY,YAAY;AAC9E;AAEA,SAAS,eAAe,SAAkB,WAA8B;CACtE,IAAI,cAAc,UAChB,OAAO,QAAQ;CAGjB,MAAM,QAAQ,QAAQ,OAAO,UAAU,MAAM,CAAgB,CAAC;CAE9D,OAAO,QAAQ,OAAO,KAAK,IAAI;AACjC;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,eACd,iBACA,YAAuB,UACX;CACZ,MAAM,eAAe,MAAM,QAAQ,eAAe,IAAI,kBAAkB,CAAC,eAAe;CAExF,MAAM,OAAmB,OAAO,EAAE,SAAS,eAAe;EACxD,MAAM,qBAAqB,eAAe,SAAS,SAAS;EAE5D,IAAI,CAAC,oBACH,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,gCAAgC;GACzC;EACF,CAAC;EAGH,IAAI;EAKJ,IAAI;GACF,UAAU,MAAM,IAAI,OAA2B,kBAAkB;EACnE,SAAS,OAAO;GACd,IAAI,CAAC,yBAAyB,KAAK,GAKjC,MAAM;GAMR,IAAI,MAAM,QAAQ,QAAQ,KAAK;GAE/B,QAAQ,iBAAiB;GAEzB,OAAO,mBAAmB;IAAE;IAAS;GAAS,GAAG;IAC/C,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,QAAQ,qBAAqB;EAK7B,MAAM,cAAc,MADK,OAAO,IAAI,0BAA0B,WACrB,CAAC,CAAC,YAAY,kBAAkB;EAEzE,IAAI,CAAC,aACH,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,gCAAgC;GACzC;EACF,CAAC;EAOH,IAAI,wBAAwB,WAAW,GAAG;GACxC,MAAM,YAAY,QAAQ;GAE1B,OAAO,mBAAmB;IAAE;IAAS;GAAS,GAAG;IAC/C,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,MAAM,WAAW,QAAQ,YAAY,YAAY;EAEjD,IAAI,aAAa,UAAU,CAAC,aAAa,SAAS,QAAQ,GACxD,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,0BAA0B;GACnC;EACF,CAAC;EAGH,MAAM,YAAY,OAAO,IAAI,iBAAiB,UAAU;EAExD,IAAI,CAAC,WAGH,MAAM,IAAI,MAAM,aAAa,SAAS,kBAAkB;EAG1D,MAAM,cAAc,MAAM,UAAU,KAAK,QAAQ,EAAE;EAEnD,IAAI,CAAC,aAAa;GAChB,MAAM,YAAY,QAAQ;GAE1B,OAAO,mBAAmB;IAAE;IAAS;GAAS,GAAG;IAC/C,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,IAAI,CAAE,MAAM,YAAY,gBAAgB,WAAW,GACjD,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,0BAA0B;GACnC;EACF,CAAC;EAGH,QAAQ,OAAO;CACjB;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"auth.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"sourcesContent":["import { config, type HttpContext, t, type Middleware, type Request } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport type { TokenFrom } from \"../contracts/types\";\r\nimport { AccessToken } from \"../models/access-token\";\r\nimport { authConfig } from \"../services/auth-config\";\r\nimport { authService } from \"../services/auth.service\";\r\nimport { isInvalidCredentialError, jwt } from \"../services/jwt\";\r\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\r\nimport {\r\n assertCsrfOriginAllowed,\r\n CsrfOriginMismatchError,\r\n requiresCsrfOriginCheck,\r\n} from \"./csrf-origin-check\";\r\n\r\n/** The 401 body shape every rejection in this middleware carries. */\r\ntype UnauthorizedPayload = { error: string; errorCode: string };\r\n\r\n/**\r\n * Reject an unauthenticated request, choosing the representation by ROUTE KIND.\r\n *\r\n * An API route always gets the JSON 401 it has always gotten. A PAGE route\r\n * (`request.route.isPage` — the React-SSR pages) instead redirects a logged-out\r\n * browser to the configured login path with a `returnUrl`, so a human\r\n * navigating to a guarded page lands on the login screen rather than reading a\r\n * raw JSON error blob — the defect in finding b9ab9804.\r\n *\r\n * The redirect is OPT-IN and backward-compatible: with no `auth.pageAuth.loginPath`\r\n * configured, even a page route falls back to the JSON 401, so an app that has\r\n * not asked for page-login UX keeps exactly its current behavior. The API 401\r\n * contract is never changed for API routes.\r\n *\r\n * `returnUrl` carries `request.url` — the relative path + query the browser was\r\n * on — never the absolute URL: a relative target cannot be turned into an\r\n * open-redirect off this app's origin.\r\n */\r\nfunction rejectUnauthorized({ request, response }: HttpContext, payload: UnauthorizedPayload) {\r\n const loginPath = authConfig.pageAuth.loginPath();\r\n\r\n if (loginPath && request.route?.isPage) {\r\n const separator = loginPath.includes(\"?\") ? \"&\" : \"?\";\r\n const returnUrl = encodeURIComponent(request.url);\r\n\r\n return response.redirect(\r\n `${loginPath}${separator}${authConfig.pageAuth.returnUrlParam()}=${returnUrl}`,\r\n );\r\n }\r\n\r\n return response.unauthorized(payload);\r\n}\r\n\r\n/**\r\n * Decoded access-token claims the middleware reads. The full payload carries\r\n * more (`created_at`, `tokenType`, `iat`, `exp`) but only these drive routing.\r\n */\r\ntype DecodedAccessToken = {\r\n id: string | number;\r\n userType?: string;\r\n};\r\n\r\n/**\r\n * Whether the persisted row says the token is dead.\r\n *\r\n * The answer belongs to the model (`AccessToken.isExpired`), so a registered\r\n * override that renames or reshapes its expiry column stays authoritative and\r\n * the middleware never touches a column name. A row that cannot answer at all —\r\n * an override that dropped the getter — is treated as **expired**: the failure\r\n * mode of this whole defect class was a check that quietly answered \"fine\" when\r\n * it had nothing to check, and that is not repeated here.\r\n *\r\n * This is independent of the `exp` claim required by `jwt.verify`. That guard\r\n * catches a token whose *claims* carry no deadline; this one catches a token\r\n * whose *row* says the deadline has passed — a logged-out or expired session\r\n * whose JWT is still within its own lifetime. Neither subsumes the other.\r\n */\r\nfunction accessTokenRowIsExpired(accessToken: AccessToken): boolean {\r\n return typeof accessToken.isExpired === \"boolean\" ? accessToken.isExpired : true;\r\n}\r\n\r\nfunction readCredential(request: Request, tokenFrom: TokenFrom): string {\r\n if (tokenFrom === \"header\") {\r\n return request.authorizationValue;\r\n }\r\n\r\n const value = request.cookie(tokenFrom.slice(\"cookie:\".length));\r\n\r\n return value ? String(value) : \"\";\r\n}\r\n\r\n/**\r\n * Build a route gate that always requires an authenticated request.\r\n *\r\n * The argument is mandatory and selects which user types may pass:\r\n * - `[]` — any authenticated user (token required, type not checked).\r\n * - `\"admin\"` / `[\"admin\", \"staff\"]` — token required AND the user's\r\n * `userType` must be one of the listed types.\r\n *\r\n * There is no anonymous/optional mode: a request without a valid access\r\n * token is always rejected with `401`. Routes that should be public\r\n * simply omit the middleware.\r\n *\r\n * `tokenFrom` selects one credential source and defaults to `\"header\"`.\r\n * A single source avoids making credential precedence depend on array order.\r\n *\r\n * @example\r\n * router.get(\"/account\", authMiddleware([]), accountController);\r\n * router.get(\"/admin\", authMiddleware(\"admin\"), adminController);\r\n * router.get(\"/back-office\", authMiddleware([\"admin\", \"staff\"]), backOfficeController);\r\n * router.get(\"/browser-account\", authMiddleware([], \"cookie:token\"), accountController);\r\n */\r\nexport function authMiddleware(\r\n allowedUserType: string | string[],\r\n tokenFrom: TokenFrom = \"header\",\r\n): Middleware {\r\n const allowedTypes = Array.isArray(allowedUserType) ? allowedUserType : [allowedUserType];\r\n\r\n const auth: Middleware = async ({ request, response }) => {\r\n const authorizationValue = readCredential(request, tokenFrom);\r\n\r\n if (!authorizationValue) {\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.missingAccessToken\"),\r\n errorCode: AuthErrorCodes.MissingAccessToken,\r\n });\r\n }\r\n\r\n // CSRF Origin check: only in scope for a cookie-sourced\r\n // credential on an unsafe method. Header-token auth and safe methods\r\n // (GET/HEAD/OPTIONS) never reach `assertCsrfOriginAllowed`.\r\n if (requiresCsrfOriginCheck(tokenFrom, request.method)) {\r\n try {\r\n assertCsrfOriginAllowed(request);\r\n } catch (error) {\r\n if (!(error instanceof CsrfOriginMismatchError)) {\r\n throw error;\r\n }\r\n\r\n log.error(\"http\", \"auth\", error);\r\n\r\n return response.forbidden({\r\n error: t(\"auth.errors.csrfOriginMismatch\"),\r\n errorCode: AuthErrorCodes.CsrfOriginMismatch,\r\n });\r\n }\r\n }\r\n\r\n let decoded: DecodedAccessToken;\r\n\r\n // The ONLY try in this middleware, and it wraps a single call. Everything\r\n // after it is storage and configuration, where an exception means the\r\n // server is broken rather than the caller (D5).\r\n try {\r\n decoded = await jwt.verify<DecodedAccessToken>(authorizationValue);\r\n } catch (error) {\r\n if (!isInvalidCredentialError(error)) {\r\n // A DB/cache outage, a missing `JWT_SECRET`, or any other server-side\r\n // fault — not a verdict on this credential. Propagate to the request's\r\n // normal error path (a 500 monitoring can see) instead of answering\r\n // 401 and clearing the caller's session.\r\n throw error;\r\n }\r\n\r\n // A forged, malformed, expired token — or (D6) a `tokenType` mismatch —\r\n // is not an incident: it is a request carrying a credential the server\r\n // will never accept.\r\n log.error(\"http\", \"auth\", error);\r\n\r\n // Clearing rather than leaving a stale value matters: a request that\r\n // failed authentication must not carry the identity of whoever\r\n // `request.locals.user` last held (see the two-sided spec below that\r\n // pins this — removing the clear must make the previous identity\r\n // survive).\r\n request.locals.user = undefined;\r\n\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n request.decodedAccessToken = decoded;\r\n\r\n // A valid signature is not enough — the token must still exist in storage,\r\n // so deleting the row (logout) invalidates it before its JWT expiry.\r\n const AccessTokenModel = config.key(\"auth.accessToken.model\", AccessToken);\r\n const accessToken = await AccessTokenModel.findByToken(authorizationValue);\r\n\r\n if (!accessToken) {\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n // ... and the row must still be live. Existence alone was the whole check\r\n // before 4.12.0, so a row whose own `expires_at` had passed still opened\r\n // the gate. The stored expiry is now enforced, and the dead row is\r\n // removed on the way out rather than left for the cleanup command.\r\n if (accessTokenRowIsExpired(accessToken)) {\r\n await accessToken.destroy();\r\n\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n const userType = decoded.userType ?? accessToken.userType;\r\n\r\n if (allowedTypes.length && !allowedTypes.includes(userType)) {\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.unauthorized\"),\r\n errorCode: AuthErrorCodes.Unauthorized,\r\n });\r\n }\r\n\r\n const UserModel = config.key(`auth.userType.${userType}`);\r\n\r\n if (!UserModel) {\r\n // Configuration, not credentials. Throwing keeps a mis-registered app\r\n // loudly broken instead of quietly rejecting every request of this type.\r\n throw new Error(`User type ${userType} is unknown type.`);\r\n }\r\n\r\n const currentUser = await UserModel.find(decoded.id);\r\n\r\n if (!currentUser) {\r\n await accessToken.destroy();\r\n\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n if (!(await authService.canAuthenticate(currentUser))) {\r\n return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.unauthorized\"),\r\n errorCode: AuthErrorCodes.Unauthorized,\r\n });\r\n }\r\n\r\n request.locals.user = currentUser;\r\n };\r\n\r\n return auth;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAS,mBAAmB,EAAE,SAAS,YAAyB,SAA8B;CAC5F,MAAM,YAAY,WAAW,SAAS,UAAU;CAEhD,IAAI,aAAa,QAAQ,OAAO,QAAQ;EACtC,MAAM,YAAY,UAAU,SAAS,GAAG,IAAI,MAAM;EAClD,MAAM,YAAY,mBAAmB,QAAQ,GAAG;EAEhD,OAAO,SAAS,SACd,GAAG,YAAY,YAAY,WAAW,SAAS,eAAe,EAAE,GAAG,WACrE;CACF;CAEA,OAAO,SAAS,aAAa,OAAO;AACtC;;;;;;;;;;;;;;;;AA0BA,SAAS,wBAAwB,aAAmC;CAClE,OAAO,OAAO,YAAY,cAAc,YAAY,YAAY,YAAY;AAC9E;AAEA,SAAS,eAAe,SAAkB,WAA8B;CACtE,IAAI,cAAc,UAChB,OAAO,QAAQ;CAGjB,MAAM,QAAQ,QAAQ,OAAO,UAAU,MAAM,CAAgB,CAAC;CAE9D,OAAO,QAAQ,OAAO,KAAK,IAAI;AACjC;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,eACd,iBACA,YAAuB,UACX;CACZ,MAAM,eAAe,MAAM,QAAQ,eAAe,IAAI,kBAAkB,CAAC,eAAe;CAExF,MAAM,OAAmB,OAAO,EAAE,SAAS,eAAe;EACxD,MAAM,qBAAqB,eAAe,SAAS,SAAS;EAE5D,IAAI,CAAC,oBACH,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,gCAAgC;GACzC;EACF,CAAC;EAMH,IAAI,wBAAwB,WAAW,QAAQ,MAAM,GACnD,IAAI;GACF,wBAAwB,OAAO;EACjC,SAAS,OAAO;GACd,IAAI,EAAE,iBAAiB,0BACrB,MAAM;GAGR,IAAI,MAAM,QAAQ,QAAQ,KAAK;GAE/B,OAAO,SAAS,UAAU;IACxB,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAGF,IAAI;EAKJ,IAAI;GACF,UAAU,MAAM,IAAI,OAA2B,kBAAkB;EACnE,SAAS,OAAO;GACd,IAAI,CAAC,yBAAyB,KAAK,GAKjC,MAAM;GAMR,IAAI,MAAM,QAAQ,QAAQ,KAAK;GAO/B,QAAQ,OAAO,OAAO;GAEtB,OAAO,mBAAmB;IAAE;IAAS;GAAS,GAAG;IAC/C,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,QAAQ,qBAAqB;EAK7B,MAAM,cAAc,MADK,OAAO,IAAI,0BAA0B,WACrB,CAAC,CAAC,YAAY,kBAAkB;EAEzE,IAAI,CAAC,aACH,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,gCAAgC;GACzC;EACF,CAAC;EAOH,IAAI,wBAAwB,WAAW,GAAG;GACxC,MAAM,YAAY,QAAQ;GAE1B,OAAO,mBAAmB;IAAE;IAAS;GAAS,GAAG;IAC/C,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,MAAM,WAAW,QAAQ,YAAY,YAAY;EAEjD,IAAI,aAAa,UAAU,CAAC,aAAa,SAAS,QAAQ,GACxD,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,0BAA0B;GACnC;EACF,CAAC;EAGH,MAAM,YAAY,OAAO,IAAI,iBAAiB,UAAU;EAExD,IAAI,CAAC,WAGH,MAAM,IAAI,MAAM,aAAa,SAAS,kBAAkB;EAG1D,MAAM,cAAc,MAAM,UAAU,KAAK,QAAQ,EAAE;EAEnD,IAAI,CAAC,aAAa;GAChB,MAAM,YAAY,QAAQ;GAE1B,OAAO,mBAAmB;IAAE;IAAS;GAAS,GAAG;IAC/C,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,IAAI,CAAE,MAAM,YAAY,gBAAgB,WAAW,GACjD,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,0BAA0B;GACnC;EACF,CAAC;EAGH,QAAQ,OAAO,OAAO;CACxB;CAEA,OAAO;AACT"}
@@ -0,0 +1,40 @@
1
+ import { TokenFrom } from "../contracts/types.mjs";
2
+ import { AuthErrorCodes } from "../utils/auth-error-codes.mjs";
3
+ import { Request } from "@warlock.js/core";
4
+
5
+ //#region ../auth/src/middleware/csrf-origin-check.d.ts
6
+ /** Why {@link assertCsrfOriginAllowed} refused a request. */
7
+ type CsrfOriginMismatchReason = "origin-mismatch" | "referer-mismatch" | "missing-origin-and-referer";
8
+ /**
9
+ * A cookie-authenticated, unsafe-method request whose `Origin`/`Referer`
10
+ * failed the check. `reason` names exactly why,
11
+ * so an error transformer or an incident log never has to string-match the
12
+ * message.
13
+ */
14
+ declare class CsrfOriginMismatchError extends Error {
15
+ readonly reason: CsrfOriginMismatchReason;
16
+ readonly code = AuthErrorCodes.CsrfOriginMismatch;
17
+ constructor(reason: CsrfOriginMismatchReason);
18
+ }
19
+ /**
20
+ * Whether a request is in scope for the CSRF Origin check at all — only a
21
+ * `cookie:`-sourced credential on an unsafe method (POST/PUT/PATCH/DELETE) is.
22
+ * Header-token auth and safe methods (GET/HEAD/OPTIONS) are always exempt:
23
+ * only a cookie-sourced credential can be silently replayed cross-site by a
24
+ * browser, so only it needs the check.
25
+ */
26
+ declare function requiresCsrfOriginCheck(tokenFrom: TokenFrom, method: string): boolean;
27
+ /**
28
+ * CSRF Origin check for a cookie-authenticated unsafe-method request (lead
29
+ * decision 3). Allowed when `Origin` — or, when `Origin` is absent, `Referer`
30
+ * — names the request's own origin or an entry in `auth.csrf.allowedOrigins`.
31
+ * Throws {@link CsrfOriginMismatchError} otherwise, including when BOTH
32
+ * headers are absent.
33
+ *
34
+ * Callers gate this behind {@link requiresCsrfOriginCheck} — this function
35
+ * itself does not re-check the token source or the method.
36
+ */
37
+ declare function assertCsrfOriginAllowed(request: Request): void;
38
+ //#endregion
39
+ export { CsrfOriginMismatchError, CsrfOriginMismatchReason, assertCsrfOriginAllowed, requiresCsrfOriginCheck };
40
+ //# sourceMappingURL=csrf-origin-check.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf-origin-check.d.mts","names":[],"sources":["../../../../../../../auth/src/middleware/csrf-origin-check.ts"],"mappings":";;;;;;KASY,wBAAA;AAAZ;;;;AAAoC;AAWpC;AAXA,cAWa,uBAAA,SAAgC,KAAA;EAAA,SAGf,MAAA,EAAQ,wBAAA;EAAA,SAF3B,IAAA,GAAI,cAAA,CAAA,kBAAA;cAEe,MAAA,EAAQ,wBAAA;AAAA;;;;;;;;iBAatB,uBAAA,CAAwB,SAAA,EAAW,SAAS,EAAE,MAAA;;;;;;AAbA;AAa9D;;;;iBAmEgB,uBAAA,CAAwB,OAAgB,EAAP,OAAO"}
@@ -0,0 +1,105 @@
1
+ import { authConfig } from "../services/auth-config.mjs";
2
+ import { AuthErrorCodes } from "../utils/auth-error-codes.mjs";
3
+
4
+ //#region ../auth/src/middleware/csrf-origin-check.ts
5
+ /** HTTP methods the CSRF Origin check exempts — reads, never state changes. */
6
+ const SAFE_METHODS = new Set([
7
+ "GET",
8
+ "HEAD",
9
+ "OPTIONS"
10
+ ]);
11
+ /**
12
+ * A cookie-authenticated, unsafe-method request whose `Origin`/`Referer`
13
+ * failed the check. `reason` names exactly why,
14
+ * so an error transformer or an incident log never has to string-match the
15
+ * message.
16
+ */
17
+ var CsrfOriginMismatchError = class extends Error {
18
+ constructor(reason) {
19
+ super(`CSRF Origin check failed: ${reason}`);
20
+ this.reason = reason;
21
+ this.code = "EC006";
22
+ this.name = "CsrfOriginMismatchError";
23
+ }
24
+ };
25
+ /**
26
+ * Whether a request is in scope for the CSRF Origin check at all — only a
27
+ * `cookie:`-sourced credential on an unsafe method (POST/PUT/PATCH/DELETE) is.
28
+ * Header-token auth and safe methods (GET/HEAD/OPTIONS) are always exempt:
29
+ * only a cookie-sourced credential can be silently replayed cross-site by a
30
+ * browser, so only it needs the check.
31
+ */
32
+ function requiresCsrfOriginCheck(tokenFrom, method) {
33
+ return tokenFrom.startsWith("cookie:") && !SAFE_METHODS.has(method.toUpperCase());
34
+ }
35
+ /**
36
+ * The request's own origin — what an `Origin`/`Referer` header must match.
37
+ *
38
+ * `request.hostname` (core's `Request`, backed by Express/Fastify's
39
+ * `hostname`) never carries a port. A browser's `Origin` header on a
40
+ * non-default port (e.g. any `warlock dev` session) does, so the port must
41
+ * come from the raw `Host` header instead — core exposes no getter for that,
42
+ * so it is read directly here.
43
+ */
44
+ function ownOrigin(request) {
45
+ const hostHeader = request.header("host");
46
+ const host = typeof hostHeader === "string" && hostHeader ? hostHeader : request.hostname;
47
+ return `${request.protocol}://${host}`;
48
+ }
49
+ /** Extract `scheme://host` from a full URL (e.g. a `Referer` header value). */
50
+ function originOf(rawUrl) {
51
+ try {
52
+ const url = new URL(rawUrl);
53
+ return `${url.protocol}//${url.host}`;
54
+ } catch {
55
+ return;
56
+ }
57
+ }
58
+ /**
59
+ * Normalize an origin string (`scheme://host[:port]`) so that a default port
60
+ * (`:80` on `http:`, `:443` on `https:`) compares equal to the same origin
61
+ * written without a port. Falls back to the raw value if it does not parse
62
+ * as a URL (in which case it will simply fail the exact-match comparison).
63
+ */
64
+ function normalizeOrigin(origin) {
65
+ try {
66
+ const url = new URL(origin);
67
+ const isDefaultPort = url.protocol === "http:" && (url.port === "" || url.port === "80") || url.protocol === "https:" && (url.port === "" || url.port === "443");
68
+ return `${url.protocol}//${isDefaultPort ? url.hostname : url.host}`;
69
+ } catch {
70
+ return origin;
71
+ }
72
+ }
73
+ /** Same-origin, or an explicit entry in `auth.csrf.allowedOrigins` (default `[]`). */
74
+ function isAllowedOrigin(origin, request) {
75
+ if (normalizeOrigin(origin) === normalizeOrigin(ownOrigin(request))) return true;
76
+ return authConfig.csrf.allowedOrigins().includes(origin);
77
+ }
78
+ /**
79
+ * CSRF Origin check for a cookie-authenticated unsafe-method request (lead
80
+ * decision 3). Allowed when `Origin` — or, when `Origin` is absent, `Referer`
81
+ * — names the request's own origin or an entry in `auth.csrf.allowedOrigins`.
82
+ * Throws {@link CsrfOriginMismatchError} otherwise, including when BOTH
83
+ * headers are absent.
84
+ *
85
+ * Callers gate this behind {@link requiresCsrfOriginCheck} — this function
86
+ * itself does not re-check the token source or the method.
87
+ */
88
+ function assertCsrfOriginAllowed(request) {
89
+ const origin = request.origin;
90
+ if (origin) {
91
+ if (isAllowedOrigin(origin, request)) return;
92
+ throw new CsrfOriginMismatchError("origin-mismatch");
93
+ }
94
+ const referer = request.header("referer");
95
+ const refererOrigin = typeof referer === "string" ? originOf(referer) : void 0;
96
+ if (refererOrigin) {
97
+ if (isAllowedOrigin(refererOrigin, request)) return;
98
+ throw new CsrfOriginMismatchError("referer-mismatch");
99
+ }
100
+ throw new CsrfOriginMismatchError("missing-origin-and-referer");
101
+ }
102
+
103
+ //#endregion
104
+ export { CsrfOriginMismatchError, assertCsrfOriginAllowed, requiresCsrfOriginCheck };
105
+ //# sourceMappingURL=csrf-origin-check.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf-origin-check.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/csrf-origin-check.ts"],"sourcesContent":["import type { Request } from \"@warlock.js/core\";\nimport type { TokenFrom } from \"../contracts/types\";\nimport { authConfig } from \"../services/auth-config\";\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\n\n/** HTTP methods the CSRF Origin check exempts — reads, never state changes. */\nconst SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/** Why {@link assertCsrfOriginAllowed} refused a request. */\nexport type CsrfOriginMismatchReason =\n | \"origin-mismatch\"\n | \"referer-mismatch\"\n | \"missing-origin-and-referer\";\n\n/**\n * A cookie-authenticated, unsafe-method request whose `Origin`/`Referer`\n * failed the check. `reason` names exactly why,\n * so an error transformer or an incident log never has to string-match the\n * message.\n */\nexport class CsrfOriginMismatchError extends Error {\n readonly code = AuthErrorCodes.CsrfOriginMismatch;\n\n constructor(public readonly reason: CsrfOriginMismatchReason) {\n super(`CSRF Origin check failed: ${reason}`);\n this.name = \"CsrfOriginMismatchError\";\n }\n}\n\n/**\n * Whether a request is in scope for the CSRF Origin check at all — only a\n * `cookie:`-sourced credential on an unsafe method (POST/PUT/PATCH/DELETE) is.\n * Header-token auth and safe methods (GET/HEAD/OPTIONS) are always exempt:\n * only a cookie-sourced credential can be silently replayed cross-site by a\n * browser, so only it needs the check.\n */\nexport function requiresCsrfOriginCheck(tokenFrom: TokenFrom, method: string): boolean {\n return tokenFrom.startsWith(\"cookie:\") && !SAFE_METHODS.has(method.toUpperCase());\n}\n\n/**\n * The request's own origin — what an `Origin`/`Referer` header must match.\n *\n * `request.hostname` (core's `Request`, backed by Express/Fastify's\n * `hostname`) never carries a port. A browser's `Origin` header on a\n * non-default port (e.g. any `warlock dev` session) does, so the port must\n * come from the raw `Host` header instead — core exposes no getter for that,\n * so it is read directly here.\n */\nfunction ownOrigin(request: Request): string {\n const hostHeader = request.header(\"host\");\n const host = typeof hostHeader === \"string\" && hostHeader ? hostHeader : request.hostname;\n\n return `${request.protocol}://${host}`;\n}\n\n/** Extract `scheme://host` from a full URL (e.g. a `Referer` header value). */\nfunction originOf(rawUrl: string): string | undefined {\n try {\n const url = new URL(rawUrl);\n\n return `${url.protocol}//${url.host}`;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Normalize an origin string (`scheme://host[:port]`) so that a default port\n * (`:80` on `http:`, `:443` on `https:`) compares equal to the same origin\n * written without a port. Falls back to the raw value if it does not parse\n * as a URL (in which case it will simply fail the exact-match comparison).\n */\nfunction normalizeOrigin(origin: string): string {\n try {\n const url = new URL(origin);\n const isDefaultPort =\n (url.protocol === \"http:\" && (url.port === \"\" || url.port === \"80\")) ||\n (url.protocol === \"https:\" && (url.port === \"\" || url.port === \"443\"));\n\n return `${url.protocol}//${isDefaultPort ? url.hostname : url.host}`;\n } catch {\n return origin;\n }\n}\n\n/** Same-origin, or an explicit entry in `auth.csrf.allowedOrigins` (default `[]`). */\nfunction isAllowedOrigin(origin: string, request: Request): boolean {\n if (normalizeOrigin(origin) === normalizeOrigin(ownOrigin(request))) return true;\n\n return authConfig.csrf.allowedOrigins().includes(origin);\n}\n\n/**\n * CSRF Origin check for a cookie-authenticated unsafe-method request (lead\n * decision 3). Allowed when `Origin` — or, when `Origin` is absent, `Referer`\n * — names the request's own origin or an entry in `auth.csrf.allowedOrigins`.\n * Throws {@link CsrfOriginMismatchError} otherwise, including when BOTH\n * headers are absent.\n *\n * Callers gate this behind {@link requiresCsrfOriginCheck} — this function\n * itself does not re-check the token source or the method.\n */\nexport function assertCsrfOriginAllowed(request: Request): void {\n const origin = request.origin;\n\n if (origin) {\n if (isAllowedOrigin(origin, request)) return;\n\n throw new CsrfOriginMismatchError(\"origin-mismatch\");\n }\n\n const referer = request.header(\"referer\");\n const refererOrigin = typeof referer === \"string\" ? originOf(referer) : undefined;\n\n if (refererOrigin) {\n if (isAllowedOrigin(refererOrigin, request)) return;\n\n throw new CsrfOriginMismatchError(\"referer-mismatch\");\n }\n\n throw new CsrfOriginMismatchError(\"missing-origin-and-referer\");\n}\n"],"mappings":";;;;;AAMA,MAAM,eAAe,IAAI,IAAI;CAAC;CAAO;CAAQ;AAAS,CAAC;;;;;;;AAcvD,IAAa,0BAAb,cAA6C,MAAM;CAGjD,YAAY,AAAgB,QAAkC;EAC5D,MAAM,6BAA6B,QAAQ;EADjB;;EAE1B,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,SAAgB,wBAAwB,WAAsB,QAAyB;CACrF,OAAO,UAAU,WAAW,SAAS,KAAK,CAAC,aAAa,IAAI,OAAO,YAAY,CAAC;AAClF;;;;;;;;;;AAWA,SAAS,UAAU,SAA0B;CAC3C,MAAM,aAAa,QAAQ,OAAO,MAAM;CACxC,MAAM,OAAO,OAAO,eAAe,YAAY,aAAa,aAAa,QAAQ;CAEjF,OAAO,GAAG,QAAQ,SAAS,KAAK;AAClC;;AAGA,SAAS,SAAS,QAAoC;CACpD,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,MAAM;EAE1B,OAAO,GAAG,IAAI,SAAS,IAAI,IAAI;CACjC,QAAQ;EACN;CACF;AACF;;;;;;;AAQA,SAAS,gBAAgB,QAAwB;CAC/C,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,MAAM;EAC1B,MAAM,gBACH,IAAI,aAAa,YAAY,IAAI,SAAS,MAAM,IAAI,SAAS,SAC7D,IAAI,aAAa,aAAa,IAAI,SAAS,MAAM,IAAI,SAAS;EAEjE,OAAO,GAAG,IAAI,SAAS,IAAI,gBAAgB,IAAI,WAAW,IAAI;CAChE,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAS,gBAAgB,QAAgB,SAA2B;CAClE,IAAI,gBAAgB,MAAM,MAAM,gBAAgB,UAAU,OAAO,CAAC,GAAG,OAAO;CAE5E,OAAO,WAAW,KAAK,eAAe,CAAC,CAAC,SAAS,MAAM;AACzD;;;;;;;;;;;AAYA,SAAgB,wBAAwB,SAAwB;CAC9D,MAAM,SAAS,QAAQ;CAEvB,IAAI,QAAQ;EACV,IAAI,gBAAgB,QAAQ,OAAO,GAAG;EAEtC,MAAM,IAAI,wBAAwB,iBAAiB;CACrD;CAEA,MAAM,UAAU,QAAQ,OAAO,SAAS;CACxC,MAAM,gBAAgB,OAAO,YAAY,WAAW,SAAS,OAAO,IAAI;CAExE,IAAI,eAAe;EACjB,IAAI,gBAAgB,eAAe,OAAO,GAAG;EAE7C,MAAM,IAAI,wBAAwB,kBAAkB;CACtD;CAEA,MAAM,IAAI,wBAAwB,4BAA4B;AAChE"}
@@ -1,2 +1,3 @@
1
1
  import { authMiddleware } from "./auth.middleware.mjs";
2
+ import { CsrfOriginMismatchError, CsrfOriginMismatchReason, assertCsrfOriginAllowed, requiresCsrfOriginCheck } from "./csrf-origin-check.mjs";
2
3
  import { LoginThrottleIdentifier, LoginThrottleOptions, loginThrottleMiddleware } from "./login-throttle.middleware.mjs";
@@ -1,3 +1,4 @@
1
+ import { CsrfOriginMismatchError, assertCsrfOriginAllowed, requiresCsrfOriginCheck } from "./csrf-origin-check.mjs";
1
2
  import { authMiddleware } from "./auth.middleware.mjs";
2
3
  import { loginThrottleMiddleware } from "./login-throttle.middleware.mjs";
3
4
 
@@ -110,6 +110,18 @@ const authConfig = {
110
110
  */
111
111
  returnUrlParam: () => config.key("auth.pageAuth.returnUrlParam", "returnUrl")
112
112
  },
113
+ cookie: {
114
+ /** Cookie name `setAuthCookie`/`clearAuthCookie` use. @default "access_token" */
115
+ name: () => config.key("auth.cookie.name", "access_token"),
116
+ /** Cookie `Path` attribute `setAuthCookie`/`clearAuthCookie` use. @default "/" */
117
+ path: () => config.key("auth.cookie.path", "/")
118
+ },
119
+ csrf: {
120
+ /**
121
+ * Extra origins allowed on a cookie-authenticated unsafe-method request,
122
+ * beyond the request's own origin. @default []
123
+ */
124
+ allowedOrigins: () => config.key("auth.csrf.allowedOrigins", []) },
113
125
  refreshToken: {
114
126
  /** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty ⇒ fall back to the access secret. */
115
127
  secret: () => resolve("refreshToken.secret", "jwt.refresh.secret"),