@awesome-lang-auth/node 1.10.1 → 1.10.3

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.
@@ -110,6 +110,7 @@ export class MyUserStore implements IUserStore {
110
110
 
111
111
  // ---- Required: token field updates -----------------------------------------------
112
112
 
113
+ /** Single-device fallback when no ISessionStore is configured. With a sessionStore, tokens are per-session and not written here. */
113
114
  async updateRefreshToken(userId: string, token: string | null, expiry: Date | null): Promise<void> { /* ... */ }
114
115
  async updateResetToken(userId: string, token: string | null, expiry: Date | null): Promise<void> { /* ... */ }
115
116
  async updatePassword(userId: string, hashedPassword: string): Promise<void> { /* ... */ }
@@ -430,7 +431,7 @@ If they are on **completely different domains**:
430
431
  import { AuthConfig } from '@awesome-lang-auth/node';
431
432
 
432
433
  const config: AuthConfig = {
433
- // Required
434
+ // Required (refreshTokenSecret must differ from accessTokenSecret when a sessionStore is configured)
434
435
  accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!,
435
436
  refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!,
436
437
 
@@ -728,8 +729,18 @@ app.use('/auth', auth.router({
728
729
  When `findOrCreateUser` throws an `AuthError` with code `'OAUTH_ACCOUNT_CONFLICT'`, the built-in OAuth callback automatically redirects to:
729
730
 
730
731
  ```
731
- {siteUrl}/auth/account-conflict?provider=google&code=OAUTH_ACCOUNT_CONFLICT&email=user%40example.com
732
+ {siteUrl}{apiPrefix}/ui/account-conflict?provider=google&code=OAUTH_ACCOUNT_CONFLICT&email=user%40example.com
732
733
  ```
734
+ (or `{siteUrl}{apiPrefix}/account-conflict?...` when `ui.enabled` is false; `apiPrefix` defaults to `/auth`).
735
+
736
+ Note that `return_path` is ignored on account conflict redirects to prevent path pollution. If `allowedOrigins` is empty, any origin from state passes (same as `resolveOAuthRedirect`). (Note: a separate follow-up issue tracks the 2FA redirect path `${redirectTo}/auth/2fa?tempToken=...`).
737
+
738
+ #### OAuth State Nonce CSRF Protection
739
+
740
+ To protect against Login CSRF attacks where an attacker tricks a victim's browser into completing an OAuth callback with the attacker's authorization code:
741
+ 1. `GET /auth/oauth/<provider>` generates a secure random nonce, encodes it in the `state` parameter, and sets an `HttpOnly`, `SameSite=Lax` cookie (`oauth_nonce_<provider>` / `oauth_state`, valid for 10 minutes) scoped to the strategy callback path.
742
+ 2. `GET /auth/oauth/<provider>/callback` strictly validates that the cookie is present and that its nonce matches the nonce in the `state` query parameter using constant-time comparison (`crypto.timingSafeEqual`).
743
+ 3. If the cookie is missing, `state` is missing, or the nonces do not match, the callback rejects the request immediately with `400 {"error":"Invalid OAuth state","code":"INVALID_OAUTH_STATE"}` and never invokes `handleCallback`.
733
744
 
734
745
  When you also attach `{ email, providerAccountId }` to the thrown `AuthError`’s `data` field **and** provide a `pendingLinkStore` in `RouterOptions`, the library stashes the conflicting provider details automatically so the front-end can drive the full conflict-resolution flow without any custom server routes:
735
746
 
@@ -777,7 +788,7 @@ app.use('/auth', createAuthRouter(userStore, config, {
777
788
  **End-to-end unauthenticated conflict-linking flow:**
778
789
 
779
790
  1. User tries to sign in with Google; `findOrCreateUser` detects the email already belongs to an existing account and throws `AuthError('...', 'OAUTH_ACCOUNT_CONFLICT', 409, { email, providerAccountId })`.
780
- 2. Library calls `pendingLinkStore.stash(email, 'google', providerAccountId)` and redirects the browser to `{siteUrl}/auth/account-conflict?provider=google&email=user%40example.com`.
791
+ 2. Library calls `pendingLinkStore.stash(email, 'google', providerAccountId)` and redirects the browser to `{siteUrl}{apiPrefix}/ui/account-conflict?provider=google&code=OAUTH_ACCOUNT_CONFLICT&email=user%40example.com` (or `{siteUrl}{apiPrefix}/account-conflict?...` if UI is disabled).
781
792
  3. Frontend prompts the user to verify ownership (e.g. sends a magic link / password check). Once verified, the front-end has a `linkToken` from `POST /auth/link-request`.
782
793
  4. Frontend calls `POST /auth/link-verify` with `{ token, loginAfterLinking: true }`. The library retrieves the stashed `providerAccountId`, links the account, clears the stash, and returns a full session.
783
794
 
@@ -1812,7 +1823,7 @@ The `X-Auth-Strategy: bearer` header is respected by all token-issuing endpoints
1812
1823
 
1813
1824
  ### Logout (bearer)
1814
1825
 
1815
- `POST /auth/logout` ends the session named by the `Authorization: Bearer` access token and/or by a `refreshToken` in the JSON body (as for `/auth/refresh`): it revokes the stateful session (with a `sessionStore`) and clears the stored refresh token, so that refresh token is refused afterwards. A refresh token in the body counts only while it is the user's current one. Send both when you have them; an expired access token alone cannot identify the session.
1826
+ `POST /auth/logout` ends the session named by the `Authorization: Bearer` access token and/or by a `refreshToken` in the JSON body (as for `/auth/refresh`): with a `sessionStore`, each session refreshes independently and logout revokes the specific session while its session exists in the store without invalidating other devices. Without a `sessionStore`, single-device fallback applies where a refresh token in the body counts only while it is the user's current one. Send both when you have them; an expired access token alone cannot identify the session.
1816
1827
 
1817
1828
  ```typescript
1818
1829
  await fetch('/auth/logout', {
@@ -2953,6 +2964,11 @@ Unauthenticated requests get `401 { "error": "Unauthorized" }`, whatever their `
2953
2964
 
2954
2965
  > **Security note:** By configuring an `accessPolicy` (e.g., `'first-user'`, `'is-admin-flag'`) and `jwtSecret`, the Admin UI requests a session. When an unauthenticated browser opens the panel (`GET <apiPrefix>/admin/`), it is redirected to `${loginPath}?redirect=<URL-encoded admin path>` if `loginPath` is set; otherwise the panel shows its own sign-in form. For further security in production, mount the admin router behind a VPN or IP allow-list.
2955
2966
 
2967
+ > **Input robustness:**
2968
+ > - Query parameters `limit`, `offset`, and `filter` in admin list endpoints accept a single string value. If a parameter is repeated (e.g. `?filter=a&filter=b`), the first value is used. Invalid or non-numeric values fall back to defaults (`offset: 0`, `limit: 20` or endpoint default).
2969
+ > - Cookies with malformed percent-encoding (`URIError`) are safely kept raw and do not cause 500 errors.
2970
+ > - Endpoints expecting a JSON body safely handle missing bodies or non-JSON payloads (`400` with descriptive error instead of `500`).
2971
+
2956
2972
  ## RouterOptions
2957
2973
 
2958
2974
  All options passed to `auth.router(options)` (or `createAuthRouter(store, config, options)`):
@@ -3143,7 +3159,7 @@ buildTokenPayload: async (user) => ({
3143
3159
 
3144
3160
  You can control the performance/security trade-off via the `session.checkOn` option (with an `ISessionStore` passed to the router):
3145
3161
 
3146
- - `none`: Purely stateless. Very fast, but tokens remain valid until they expire even if the session is deleted.
3162
+ - `none`: Purely stateless for access-token verification. Note that `POST /refresh` always validates session existence in the store even when `checkOn: 'none'`; a deleted or revoked session can no longer refresh (`401 SESSION_REVOKED`).
3147
3163
  - `refresh` (default): Validates the session only when a new Access Token is requested. Fast, and ensures that once a session is revoked, the user cannot get new tokens.
3148
3164
  - `allcalls`: Validates the session ID on **every single request** via middleware: the auth router's own protected routes (`/me`, `/sessions`, `/change-password`, ...) and `auth.middleware()` when it has a store (`auth.middleware({ sessionStore })`, or a call made after `auth.router({ sessionStore })`). A revoked session gets `401 SESSION_REVOKED` on the next call. Highest security, handles instant "kill-switch" revocation. Recommended with high-performance stores (Redis/In-Memory).
3149
3165
 
@@ -88,5 +88,10 @@ export interface ISessionStore {
88
88
  * @returns The number of sessions that were deleted.
89
89
  */
90
90
  deleteExpiredSessions?(): Promise<number>;
91
+ /**
92
+ * Optional: update the stored refresh token hash for an existing session.
93
+ * Called during token refresh and session rotation.
94
+ */
95
+ updateSessionRefreshTokenHash?(sessionHandle: string, refreshTokenHash: string): Promise<void>;
91
96
  }
92
97
  //# sourceMappingURL=session-store.interface.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-store.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/session-store.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,aAAa,CACX,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,GACvC,OAAO,CAAC,WAAW,CAAC,CAAC;IAExB;;;OAGG;IACH,UAAU,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE/D;;;;OAIG;IACH,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAE9E;;;OAGG;IACH,uBAAuB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9D;;;;;;OAMG;IACH,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpD;;;OAGG;IACH,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3E;;;;;;OAMG;IACH,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAEvE;;;;;;OAMG;IACH,qBAAqB,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3C"}
1
+ {"version":3,"file":"session-store.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/session-store.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,aAAa,CACX,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,GACvC,OAAO,CAAC,WAAW,CAAC,CAAC;IAExB;;;OAGG;IACH,UAAU,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE/D;;;;OAIG;IACH,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAE9E;;;OAGG;IACH,uBAAuB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9D;;;;;;OAMG;IACH,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpD;;;OAGG;IACH,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3E;;;;;;OAMG;IACH,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAEvE;;;;;;OAMG;IACH,qBAAqB,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAE1C;;;OAGG;IACH,6BAA6B,CAAC,CAAC,aAAa,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChG"}
@@ -363,11 +363,26 @@ export interface AuthConfig {
363
363
  *
364
364
  * - `allcalls`: Verify the session on every single request. Maximum security, requires a low-latency store (Redis).
365
365
  * - `refresh`: Verify the session only during token refresh. High performance with good security.
366
- * - `none`: Sessions are created and tracked in the store but never verified during requests.
366
+ * - `none`: Access tokens are verified statelessly without session lookup. Note: `POST /refresh` always validates session existence in the store even under `'none'`; a revoked or rotated session can never refresh (`401 SESSION_REVOKED`).
367
367
  *
368
368
  * @default 'refresh'
369
369
  */
370
370
  checkOn?: 'allcalls' | 'refresh' | 'none';
371
+ /**
372
+ * If `true`, a user can only have one active session at a time.
373
+ * Logging in from a new device/client automatically revokes all previous active sessions
374
+ * for this user in the `sessionStore` (`revokeAllSessionsForUser`), disconnecting previous devices.
375
+ *
376
+ * If `false` (default), multiple concurrent sessions across different devices
377
+ * are allowed simultaneously. Each device maintains its own session and refresh token.
378
+ *
379
+ * @default false
380
+ */
381
+ singleSessionPerUser?: boolean;
382
+ /**
383
+ * Alias for `singleSessionPerUser`.
384
+ */
385
+ singleSession?: boolean;
371
386
  };
372
387
  /**
373
388
  * Optional template store for custom email and UI translations.
@@ -1 +1 @@
1
- {"version":3,"file":"auth-config.model.d.ts","sourceRoot":"","sources":["../../src/models/auth-config.model.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth-config.model.d.ts","sourceRoot":"","sources":["../../src/models/auth-config.model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AAExE;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,YAAY;IAC3B,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE;QACd,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;QACrC,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd;;;;;;WAMG;QACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,EAAE;QACL,qBAAqB;QACrB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;IACF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,CAAC,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,oBAAoB,CAAC,EAAE,MAAM,CAAC;KAC/B,CAAC;IACF,KAAK,CAAC,EAAE;QACN;;;;;;;;;;;;;;;;;;WAkBG;QACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAE5B;;;;;WAKG;QACH,MAAM,CAAC,EAAE,YAAY,CAAC;QAEtB;;;WAGG;QACH,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAE1F;;;WAGG;QACH,iBAAiB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAE9F;;;WAGG;QACH,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAE1F;;;WAGG;QACH,qBAAqB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAElG;;;;WAIG;QACH,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACnF,CAAC;IACF,KAAK,CAAC,EAAE;QACN,MAAM,CAAC,EAAE;YACP,QAAQ,EAAE,MAAM,CAAC;YACjB,YAAY,EAAE,MAAM,CAAC;YACrB,WAAW,EAAE,MAAM,CAAC;YACpB,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,CAAC;QACF,MAAM,CAAC,EAAE;YACP,QAAQ,EAAE,MAAM,CAAC;YACjB,YAAY,EAAE,MAAM,CAAC;YACrB,WAAW,EAAE,MAAM,CAAC;SACrB,CAAC;KACH,CAAC;IACF,SAAS,CAAC,EAAE;QACV,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF;;;;;;;;;;;;;;OAcG;IACH,qBAAqB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;IAEnD;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAEnC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEhE;;OAEG;IACH,EAAE,CAAC,EAAE;QACH,mEAAmE;QACnE,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB;;;;;;;;;;;;;;;;;;WAkBG;QACH,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,6FAA6F;QAC7F,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,mFAAmF;QACnF,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB;;;;WAIG;QACH,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,qEAAqE;QACrE,UAAU,CAAC,EAAE,MAAM,CAAC;QAGpB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAElB;;;WAGG;QACH,OAAO,CAAC,EAAE,MAAM,CAAC;QAEjB;;;;WAIG;QACH,OAAO,CAAC,EAAE,MAAM,CAAC;QAEjB;;;WAGG;QACH,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF;;OAEG;IACH,OAAO,CAAC,EAAE;QACR;;;;;;;;WAQG;QACH,OAAO,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,CAAC;QAE1C;;;;;;;;;WASG;QACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;QAE/B;;WAEG;QACH,aAAa,CAAC,EAAE,OAAO,CAAC;KACzB,CAAC;IACF;;OAEG;IACH,aAAa,CAAC,EAAE,cAAc,CAAC;IAE/B;;;OAGG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAE9B;;;OAGG;IACH,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC"}
@@ -24,5 +24,7 @@ export interface SessionInfo {
24
24
  ipAddress?: string;
25
25
  /** Arbitrary session-level data (e.g. device name, geo-location, etc.). */
26
26
  data?: Record<string, unknown>;
27
+ /** SHA-256 hash of the active refresh token for this session (multi-device support). */
28
+ refreshTokenHash?: string;
27
29
  }
28
30
  //# sourceMappingURL=session.model.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session.model.d.ts","sourceRoot":"","sources":["../../src/models/session.model.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,aAAa,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,SAAS,EAAE,IAAI,CAAC;IAChB,gCAAgC;IAChC,SAAS,EAAE,IAAI,CAAC;IAChB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,IAAI,CAAC;IACpB,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC"}
1
+ {"version":3,"file":"session.model.d.ts","sourceRoot":"","sources":["../../src/models/session.model.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,aAAa,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,SAAS,EAAE,IAAI,CAAC;IAChB,gCAAgC;IAChC,SAAS,EAAE,IAAI,CAAC;IAChB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,IAAI,CAAC;IACpB,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,wFAAwF;IACxF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"admin.router.d.ts","sourceRoot":"","sources":["../../src/router/admin.router.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"admin.router.d.ts","sourceRoot":"","sources":["../../src/router/admin.router.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,23 @@ const webhook_action_1 = require("../tools/webhook-action");
16
16
  const openapi_1 = require("./openapi");
17
17
  const auth_event_names_1 = require("../events/auth-event-names");
18
18
  const router_events_1 = require("./router-events");
19
+ /*
20
+ * Admin login lives here (typically /auth/admin/login when mounted via
21
+ * AuthConfigurator.buildAllRouters). End-user login lives in ui.router.ts at
22
+ * /auth/ui/login. Keep these two audiences distinct in code and docs.
23
+ */
24
+ function toSingleString(val, fallback = '') {
25
+ if (typeof val === 'string')
26
+ return val;
27
+ if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'string')
28
+ return val[0];
29
+ return fallback;
30
+ }
31
+ function toSafeInt(val, fallback) {
32
+ const str = toSingleString(val);
33
+ const parsed = parseInt(str, 10);
34
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
35
+ }
19
36
  /**
20
37
  * Id of the admin the `accessPolicy` guard authorized for this request (the
21
38
  * actor of a privilege change). Only the policy guard sets it, in
@@ -134,8 +151,15 @@ function buildPolicyGuard(policy, userStore, jwtSecret, rbacStore, loginPath, co
134
151
  const cookieHeader = req.headers.cookie ?? '';
135
152
  cookies = cookieHeader.split(';').reduce((acc, part) => {
136
153
  const [key, ...val] = part.trim().split('=');
137
- if (key)
138
- acc[key.trim()] = decodeURIComponent(val.join('='));
154
+ if (key) {
155
+ const rawVal = val.join('=');
156
+ try {
157
+ acc[key.trim()] = decodeURIComponent(rawVal);
158
+ }
159
+ catch {
160
+ acc[key.trim()] = rawVal;
161
+ }
162
+ }
139
163
  return acc;
140
164
  }, {});
141
165
  }
@@ -371,7 +395,7 @@ function createAdminRouter(userStore, options) {
371
395
  // ── Local Login Handler (Self-contained Auth) ──────────────────────────
372
396
  if (sessionBased && secret) {
373
397
  router.post('/login', async (req, res) => {
374
- const { email, password } = req.body;
398
+ const { email, password } = (req.body ?? {});
375
399
  if (!password) {
376
400
  res.status(400).json({ error: 'Password required' });
377
401
  return;
@@ -587,9 +611,9 @@ function createAdminRouter(userStore, options) {
587
611
  // GET /admin/api/users?limit=&offset=
588
612
  router.get('/api/users', guard, async (req, res) => {
589
613
  try {
590
- const limit = Math.min(parseInt(req.query['limit'] || '20', 10), 100);
591
- const offset = parseInt(req.query['offset'] || '0', 10);
592
- const filter = (req.query['filter'] || '').toLowerCase().trim();
614
+ const limit = Math.min(Math.max(toSafeInt(req.query['limit'], 20), 1), 100);
615
+ const offset = Math.max(toSafeInt(req.query['offset'], 0), 0);
616
+ const filter = toSingleString(req.query['filter']).toLowerCase().trim();
593
617
  if (!userStore.listUsers) {
594
618
  res.status(501).json({ error: 'IUserStore.listUsers is not implemented', users: [], total: 0 });
595
619
  return;
@@ -662,7 +686,7 @@ function createAdminRouter(userStore, options) {
662
686
  // POST /admin/api/2fa-policy — bulk set / clear the require-2FA flag on all users
663
687
  router.post('/api/2fa-policy', guard, async (req, res) => {
664
688
  try {
665
- const { required } = req.body;
689
+ const { required } = (req.body ?? {});
666
690
  if (typeof required !== 'boolean') {
667
691
  res.status(400).json({ error: '"required" must be a boolean' });
668
692
  return;
@@ -762,7 +786,7 @@ function createAdminRouter(userStore, options) {
762
786
  return;
763
787
  }
764
788
  try {
765
- const { role, tenantId } = req.body;
789
+ const { role, tenantId } = (req.body ?? {});
766
790
  if (!role) {
767
791
  res.status(400).json({ error: 'role is required' });
768
792
  return;
@@ -1024,9 +1048,9 @@ function createAdminRouter(userStore, options) {
1024
1048
  return;
1025
1049
  }
1026
1050
  try {
1027
- const limit = Math.min(parseInt(req.query['limit'] || '20', 10), 100);
1028
- const offset = parseInt(req.query['offset'] || '0', 10);
1029
- const filter = (req.query['filter'] || '').toLowerCase().trim();
1051
+ const limit = Math.min(Math.max(toSafeInt(req.query['limit'], 20), 1), 100);
1052
+ const offset = Math.max(toSafeInt(req.query['offset'], 0), 0);
1053
+ const filter = toSingleString(req.query['filter']).toLowerCase().trim();
1030
1054
  if (!options.sessionStore.getAllSessions) {
1031
1055
  res.status(501).json({ error: 'ISessionStore.getAllSessions is not implemented', sessions: [], total: 0 });
1032
1056
  return;
@@ -1092,7 +1116,7 @@ function createAdminRouter(userStore, options) {
1092
1116
  return;
1093
1117
  }
1094
1118
  try {
1095
- const { name, permissions } = req.body;
1119
+ const { name, permissions } = (req.body ?? {});
1096
1120
  if (!name) {
1097
1121
  res.status(400).json({ error: 'name is required' });
1098
1122
  return;
@@ -1140,7 +1164,7 @@ function createAdminRouter(userStore, options) {
1140
1164
  return;
1141
1165
  }
1142
1166
  try {
1143
- const { name, isActive } = req.body;
1167
+ const { name, isActive } = (req.body ?? {});
1144
1168
  if (!name) {
1145
1169
  res.status(400).json({ error: 'name is required' });
1146
1170
  return;
@@ -1188,7 +1212,7 @@ function createAdminRouter(userStore, options) {
1188
1212
  return;
1189
1213
  }
1190
1214
  try {
1191
- const { userId } = req.body;
1215
+ const { userId } = (req.body ?? {});
1192
1216
  if (!userId) {
1193
1217
  res.status(400).json({ error: 'userId is required' });
1194
1218
  return;
@@ -1222,9 +1246,9 @@ function createAdminRouter(userStore, options) {
1222
1246
  return;
1223
1247
  }
1224
1248
  try {
1225
- const limit = Math.min(parseInt(req.query['limit'] || '20', 10), 100);
1226
- const offset = parseInt(req.query['offset'] || '0', 10);
1227
- const filter = (req.query['filter'] || '').toLowerCase().trim();
1249
+ const limit = Math.min(Math.max(toSafeInt(req.query['limit'], 20), 1), 100);
1250
+ const offset = Math.max(toSafeInt(req.query['offset'], 0), 0);
1251
+ const filter = toSingleString(req.query['filter']).toLowerCase().trim();
1228
1252
  if (!options.apiKeyStore.listAll) {
1229
1253
  res.status(501).json({ error: 'IApiKeyStore.listAll is not implemented', keys: [], total: 0 });
1230
1254
  return;
@@ -1265,7 +1289,7 @@ function createAdminRouter(userStore, options) {
1265
1289
  return;
1266
1290
  }
1267
1291
  try {
1268
- const { name, serviceId, scopes, allowedIps, expiresAt } = req.body;
1292
+ const { name, serviceId, scopes, allowedIps, expiresAt } = (req.body ?? {});
1269
1293
  if (!name) {
1270
1294
  res.status(400).json({ error: 'name is required' });
1271
1295
  return;
@@ -1339,8 +1363,8 @@ function createAdminRouter(userStore, options) {
1339
1363
  return;
1340
1364
  }
1341
1365
  try {
1342
- const limit = Math.min(parseInt(req.query['limit'] || '20', 10), 100);
1343
- const offset = parseInt(req.query['offset'] || '0', 10);
1366
+ const limit = Math.min(Math.max(toSafeInt(req.query['limit'], 20), 1), 100);
1367
+ const offset = Math.max(toSafeInt(req.query['offset'], 0), 0);
1344
1368
  if (!options.webhookStore.listAll) {
1345
1369
  res.status(501).json({ error: 'IWebhookStore.listAll is not implemented', webhooks: [], total: 0 });
1346
1370
  return;
@@ -1369,7 +1393,7 @@ function createAdminRouter(userStore, options) {
1369
1393
  return;
1370
1394
  }
1371
1395
  try {
1372
- const { url, events, secret, tenantId, isActive, maxRetries, retryDelayMs } = req.body;
1396
+ const { url, events, secret, tenantId, isActive, maxRetries, retryDelayMs } = (req.body ?? {});
1373
1397
  if (!url) {
1374
1398
  res.status(400).json({ error: 'url is required' });
1375
1399
  return;
@@ -1440,7 +1464,7 @@ function createAdminRouter(userStore, options) {
1440
1464
  // POST /admin/api/templates/mail — create or update a mail template
1441
1465
  router.post('/api/templates/mail', guard, async (req, res) => {
1442
1466
  try {
1443
- const { id, baseHtml, baseText, translations } = req.body;
1467
+ const { id, baseHtml, baseText, translations } = (req.body ?? {});
1444
1468
  if (!id) {
1445
1469
  res.status(400).json({ error: 'id is required' });
1446
1470
  return;
@@ -1465,7 +1489,7 @@ function createAdminRouter(userStore, options) {
1465
1489
  // POST /admin/api/templates/ui — update UI translations for a page
1466
1490
  router.post('/api/templates/ui', guard, async (req, res) => {
1467
1491
  try {
1468
- const { page, translations } = req.body;
1492
+ const { page, translations } = (req.body ?? {});
1469
1493
  if (!page || !translations) {
1470
1494
  res.status(400).json({ error: 'page and translations are required' });
1471
1495
  return;