@zudojs/auth 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +33 -1
  2. package/dist/authErrors/authError.base.d.ts +0 -1
  3. package/dist/authErrors/authError.base.js +0 -1
  4. package/dist/authErrors/authError.throttle.d.ts +0 -1
  5. package/dist/authErrors/authError.throttle.js +0 -1
  6. package/dist/authErrors/index.d.ts +0 -1
  7. package/dist/authErrors/index.js +0 -1
  8. package/dist/authPassword/authPassword.core.d.ts +0 -1
  9. package/dist/authPassword/authPassword.core.js +0 -1
  10. package/dist/authPassword/authPassword.legacy.d.ts +0 -1
  11. package/dist/authPassword/authPassword.legacy.js +0 -1
  12. package/dist/authPassword/authPassword.policy.d.ts +0 -1
  13. package/dist/authPassword/authPassword.policy.js +0 -1
  14. package/dist/authPassword/authPassword.rehash.d.ts +0 -1
  15. package/dist/authPassword/authPassword.rehash.js +0 -1
  16. package/dist/authPassword/index.d.ts +0 -1
  17. package/dist/authPassword/index.js +0 -1
  18. package/dist/authProvider/authAttempt.eviction.d.ts +0 -1
  19. package/dist/authProvider/authAttempt.eviction.js +0 -1
  20. package/dist/authProvider/authAttempt.memory.d.ts +4 -1
  21. package/dist/authProvider/authAttempt.memory.js +9 -6
  22. package/dist/authProvider/authProvider.core.d.ts +18 -1
  23. package/dist/authProvider/authProvider.core.js +9 -3
  24. package/dist/authProvider/authProvider.external.d.ts +0 -1
  25. package/dist/authProvider/authProvider.external.js +1 -1
  26. package/dist/authProvider/authProvider.throttle.d.ts +4 -2
  27. package/dist/authProvider/authProvider.throttle.js +6 -4
  28. package/dist/authProvider/index.d.ts +0 -1
  29. package/dist/authProvider/index.js +0 -1
  30. package/dist/authSession/authSession.core.d.ts +4 -1
  31. package/dist/authSession/authSession.core.js +8 -5
  32. package/dist/authSession/index.d.ts +0 -1
  33. package/dist/authSession/index.js +0 -1
  34. package/dist/authToken/authToken.core.d.ts +20 -8
  35. package/dist/authToken/authToken.core.js +13 -22
  36. package/dist/authToken/authToken.encoding.d.ts +0 -1
  37. package/dist/authToken/authToken.encoding.js +0 -1
  38. package/dist/authToken/authToken.revocation.d.ts +19 -1
  39. package/dist/authToken/authToken.revocation.js +39 -6
  40. package/dist/authToken/authToken.signing.d.ts +0 -1
  41. package/dist/authToken/authToken.signing.js +2 -2
  42. package/dist/authToken/index.d.ts +2 -3
  43. package/dist/authToken/index.js +1 -2
  44. package/dist/authToken/jwt.namespace.d.ts +0 -1
  45. package/dist/authToken/jwt.namespace.js +0 -1
  46. package/dist/authTypes/authAttempt.type.d.ts +0 -1
  47. package/dist/authTypes/authAttempt.type.js +0 -1
  48. package/dist/authTypes/authCredentials.type.d.ts +0 -1
  49. package/dist/authTypes/authCredentials.type.js +0 -1
  50. package/dist/authTypes/authRbac.type.d.ts +0 -1
  51. package/dist/authTypes/authRbac.type.js +0 -1
  52. package/dist/authTypes/authSession.type.d.ts +0 -1
  53. package/dist/authTypes/authSession.type.js +0 -1
  54. package/dist/authTypes/authToken.type.d.ts +8 -1
  55. package/dist/authTypes/authToken.type.js +0 -1
  56. package/dist/authTypes/authUser.type.d.ts +7 -2
  57. package/dist/authTypes/authUser.type.js +0 -1
  58. package/dist/authTypes/index.d.ts +0 -1
  59. package/dist/authTypes/index.js +0 -1
  60. package/dist/authUtils/authUtils.claims.d.ts +21 -0
  61. package/dist/authUtils/authUtils.claims.js +44 -0
  62. package/dist/authUtils/authUtils.helper.d.ts +4 -2
  63. package/dist/authUtils/authUtils.helper.js +3 -3
  64. package/dist/authUtils/authUtils.identifier.d.ts +0 -1
  65. package/dist/authUtils/authUtils.identifier.js +0 -1
  66. package/dist/authUtils/index.d.ts +1 -1
  67. package/dist/authUtils/index.js +1 -1
  68. package/dist/index.d.ts +0 -1
  69. package/dist/index.js +0 -1
  70. package/package.json +6 -6
package/README.md CHANGED
@@ -55,6 +55,7 @@ const tokenConfig: TokenConfig = {
55
55
  issuer: "my-api",
56
56
  audience: "my-app",
57
57
  clockToleranceSeconds: 5, // optional skew allowance for exp/iat/nbf
58
+ // clock: myClock, // optional { now(): number } for deterministic expiry tests
58
59
  };
59
60
 
60
61
  const auth = createAuthService({
@@ -194,7 +195,38 @@ With a `revocationStore` configured, `refresh()` claims the presented token's
194
195
  Replaying an already-used refresh token throws `TokenRevokedError` **and**
195
196
  destroys every session for that user, on the assumption that the chain is
196
197
  compromised (RFC 6819 §5.2.2.3). Implement `revokeIfNotRevoked` in any custom
197
- store — the `isRevoked` + `revoke` fallback is racy.
198
+ store — the `isRevoked` + `revoke` fallback is racy. A store without it is
199
+ reported at construction: `createAuthService()` emits a `SecurityWarning`
200
+ (`process.emitWarning`, code `ZUDO_AUTH_RACY_REVOCATION`), and with
201
+ `requireAtomicRevocation: true` it throws `AuthConfigurationError` instead.
202
+
203
+ ### Custom claims
204
+
205
+ `AuthUser.claims` (for example `{ plan: "pro", org: "acme" }`) is embedded in
206
+ every token the service mints — `login()`, `refresh()` and
207
+ `createSessionForUser()` — and comes back on `verifyToken()`'s payload. Reserved
208
+ names (`sub`, `iat`, `exp`, `nbf`, `typ`, `jti`, `sid`, `roles`, `iss`, `aud`)
209
+ are dropped, never overridden. The standalone `createTokenPair()` takes the same
210
+ `claims` option. Claims are readable by anyone holding the token, so keep them
211
+ small and non-sensitive.
212
+
213
+ ### Deterministic time in tests
214
+
215
+ Pass a `{ now(): number }` clock instead of faking global timers:
216
+
217
+ ```ts
218
+ const clock = { now: () => fixedMs };
219
+ const auth = createAuthService({
220
+ clock,
221
+ sessionStore: createMemorySessionStore({ clock }),
222
+ revocationStore: createMemoryTokenRevocationStore({ clock }),
223
+ loginThrottle: { store: createMemoryLoginAttemptStore({ clock }) },
224
+ // ...
225
+ });
226
+ ```
227
+
228
+ Token `iat`/`exp`, verification, session expiry, revocation expiry and
229
+ lockout deadlines all read that clock; advance it to test expiry.
198
230
 
199
231
  ### Access control
200
232
 
@@ -75,4 +75,3 @@ export declare class AccessDeniedError extends AuthError {
75
75
  export declare class SessionExpiredError extends AuthError {
76
76
  constructor(message?: string, options?: AuthErrorOptions);
77
77
  }
78
- //# sourceMappingURL=authError.base.d.ts.map
@@ -132,4 +132,3 @@ export class SessionExpiredError extends AuthError {
132
132
  });
133
133
  }
134
134
  }
135
- //# sourceMappingURL=authError.base.js.map
@@ -39,4 +39,3 @@ export declare class AuthRateLimitError extends AuthError {
39
39
  readonly headers: Readonly<Record<string, string>>;
40
40
  constructor(message?: string, options?: ThrottleErrorOptions);
41
41
  }
42
- //# sourceMappingURL=authError.throttle.d.ts.map
@@ -66,4 +66,3 @@ export class AuthRateLimitError extends AuthError {
66
66
  this.headers = retryAfterHeaders(seconds);
67
67
  }
68
68
  }
69
- //# sourceMappingURL=authError.throttle.js.map
@@ -5,4 +5,3 @@
5
5
  */
6
6
  export { AuthError, type AuthErrorOptions, AuthConfigurationError, InvalidCredentialsError, TokenExpiredError, TokenInvalidError, TokenRevokedError, AccountDeactivatedError, AccessDeniedError, SessionExpiredError, } from "./authError.base.js";
7
7
  export { AccountLockedError, AuthRateLimitError, type ThrottleErrorOptions, } from "./authError.throttle.js";
8
- //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,3 @@
5
5
  */
6
6
  export { AuthError, AuthConfigurationError, InvalidCredentialsError, TokenExpiredError, TokenInvalidError, TokenRevokedError, AccountDeactivatedError, AccessDeniedError, SessionExpiredError, } from "./authError.base.js";
7
7
  export { AccountLockedError, AuthRateLimitError, } from "./authError.throttle.js";
8
- //# sourceMappingURL=index.js.map
@@ -51,4 +51,3 @@ export declare function verifyPassword(password: string, hashedPassword: string)
51
51
  * @returns Hex-encoded random string
52
52
  */
53
53
  export declare function generateRandomToken(length?: number): string;
54
- //# sourceMappingURL=authPassword.core.d.ts.map
@@ -96,4 +96,3 @@ export function generateRandomToken(length = 32) {
96
96
  }
97
97
  return randomBytes(length).toString("hex");
98
98
  }
99
- //# sourceMappingURL=authPassword.core.js.map
@@ -18,4 +18,3 @@ export declare function isLegacyPasswordHash(hashedPassword: string): boolean;
18
18
  * unparseable hash or parameters scrypt cannot satisfy; never throws.
19
19
  */
20
20
  export declare function verifyLegacyPassword(password: string, hashedPassword: string): Promise<boolean>;
21
- //# sourceMappingURL=authPassword.legacy.d.ts.map
@@ -82,4 +82,3 @@ function deriveLegacyKey(password, salt, params) {
82
82
  });
83
83
  });
84
84
  }
85
- //# sourceMappingURL=authPassword.legacy.js.map
@@ -27,4 +27,3 @@ export declare const KEY_LENGTH = 64;
27
27
  export declare const SCRYPT_N = 16384;
28
28
  export declare const SCRYPT_R = 8;
29
29
  export declare const SCRYPT_P = 5;
30
- //# sourceMappingURL=authPassword.policy.d.ts.map
@@ -27,4 +27,3 @@ export const KEY_LENGTH = 64;
27
27
  export const SCRYPT_N = 16384;
28
28
  export const SCRYPT_R = 8;
29
29
  export const SCRYPT_P = 5;
30
- //# sourceMappingURL=authPassword.policy.js.map
@@ -14,4 +14,3 @@
14
14
  * @returns Whether the hash should be regenerated
15
15
  */
16
16
  export declare function needsRehash(hashedPassword: string): boolean;
17
- //# sourceMappingURL=authPassword.rehash.d.ts.map
@@ -47,4 +47,3 @@ export function needsRehash(hashedPassword) {
47
47
  return true;
48
48
  }
49
49
  }
50
- //# sourceMappingURL=authPassword.rehash.js.map
@@ -6,4 +6,3 @@
6
6
  export { hashPassword, verifyPassword, generateRandomToken, } from "./authPassword.core.js";
7
7
  export { needsRehash } from "./authPassword.rehash.js";
8
8
  export { MIN_SALT_LENGTH, MAX_SALT_LENGTH, MAX_PASSWORD_BYTES, } from "./authPassword.policy.js";
9
- //# sourceMappingURL=index.d.ts.map
@@ -6,4 +6,3 @@
6
6
  export { hashPassword, verifyPassword, generateRandomToken, } from "./authPassword.core.js";
7
7
  export { needsRehash } from "./authPassword.rehash.js";
8
8
  export { MIN_SALT_LENGTH, MAX_SALT_LENGTH, MAX_PASSWORD_BYTES, } from "./authPassword.policy.js";
9
- //# sourceMappingURL=index.js.map
@@ -23,4 +23,3 @@ export declare function isStale(entry: EvictableAttemptEntry, now: number, windo
23
23
  * insertion order), or the oldest entry of all when every entry is locked.
24
24
  */
25
25
  export declare function evictOne<E extends EvictableAttemptEntry>(entries: Map<string, E>, now: number): void;
26
- //# sourceMappingURL=authAttempt.eviction.d.ts.map
@@ -34,4 +34,3 @@ export function evictOne(entries, now) {
34
34
  if (!oldest.done)
35
35
  entries.delete(oldest.value);
36
36
  }
37
- //# sourceMappingURL=authAttempt.eviction.js.map
@@ -6,6 +6,7 @@
6
6
  * Single-process only: counters are not shared between instances. Back
7
7
  * `LoginAttemptStore` with Redis for a real deployment.
8
8
  */
9
+ import type { Clock } from "@zudojs/types";
9
10
  import type { LoginAttemptStore } from "../authTypes/authAttempt.type.js";
10
11
  /**
11
12
  * Create an in-memory {@link LoginAttemptStore}.
@@ -20,11 +21,13 @@ import type { LoginAttemptStore } from "../authTypes/authAttempt.type.js";
20
21
  * @param options.maxEntries - Hard cap on tracked identifiers (default:
21
22
  * 100000). At the cap the oldest unlocked entry is evicted; locked
22
23
  * entries are only evicted when every entry is locked.
24
+ * @param options.clock - Time source for windows, lockouts and eviction
25
+ * (default: `Date.now`). Pass the same clock to `createAuthService`.
23
26
  */
24
27
  export declare function createMemoryLoginAttemptStore(options?: {
25
28
  readonly windowSeconds?: number;
26
29
  readonly purgeIntervalMs?: number;
27
30
  readonly failureTtlSeconds?: number;
28
31
  readonly maxEntries?: number;
32
+ readonly clock?: Clock;
29
33
  }): LoginAttemptStore;
30
- //# sourceMappingURL=authAttempt.memory.d.ts.map
@@ -25,8 +25,12 @@ const EMPTY = { failures: 0, attempts: 0 };
25
25
  * @param options.maxEntries - Hard cap on tracked identifiers (default:
26
26
  * 100000). At the cap the oldest unlocked entry is evicted; locked
27
27
  * entries are only evicted when every entry is locked.
28
+ * @param options.clock - Time source for windows, lockouts and eviction
29
+ * (default: `Date.now`). Pass the same clock to `createAuthService`.
28
30
  */
29
31
  export function createMemoryLoginAttemptStore(options) {
32
+ const clock = options?.clock;
33
+ const currentMs = () => (clock ? clock.now() : Date.now());
30
34
  const windowMs = (options?.windowSeconds ?? DEFAULT_WINDOW_SECONDS) * 1000;
31
35
  const purgeIntervalMs = options?.purgeIntervalMs ?? DEFAULT_PURGE_INTERVAL_MS;
32
36
  const failureTtlMs = (options?.failureTtlSeconds ?? DEFAULT_FAILURE_TTL_SECONDS) * 1000;
@@ -78,7 +82,7 @@ export function createMemoryLoginAttemptStore(options) {
78
82
  }
79
83
  return {
80
84
  async get(identifier) {
81
- const now = Date.now();
85
+ const now = currentMs();
82
86
  if (!entries.has(identifier)) {
83
87
  maybePurge(now);
84
88
  return EMPTY;
@@ -86,18 +90,18 @@ export function createMemoryLoginAttemptStore(options) {
86
90
  return snapshot(load(identifier, now));
87
91
  },
88
92
  async recordAttempt(identifier) {
89
- const entry = load(identifier, Date.now());
93
+ const entry = load(identifier, currentMs());
90
94
  entry.attempts++;
91
95
  return snapshot(entry);
92
96
  },
93
97
  async recordFailure(identifier) {
94
- const entry = load(identifier, Date.now());
98
+ const entry = load(identifier, currentMs());
95
99
  entry.failures++;
96
- entry.lastFailureAt = Date.now();
100
+ entry.lastFailureAt = currentMs();
97
101
  return snapshot(entry);
98
102
  },
99
103
  async lock(identifier, until) {
100
- const entry = load(identifier, Date.now());
104
+ const entry = load(identifier, currentMs());
101
105
  entry.lockedUntil = until;
102
106
  },
103
107
  async reset(identifier) {
@@ -105,4 +109,3 @@ export function createMemoryLoginAttemptStore(options) {
105
109
  },
106
110
  };
107
111
  }
108
- //# sourceMappingURL=authAttempt.memory.js.map
@@ -7,6 +7,7 @@ import type { AuthUser, UserCredentials, UserId } from "../authTypes/authUser.ty
7
7
  import type { TokenPair, TokenPayload, TokenConfig, TokenRevocationStore } from "../authTypes/authToken.type.js";
8
8
  import type { SessionStore, SessionId } from "../authTypes/authSession.type.js";
9
9
  import type { LoginThrottleConfig } from "../authTypes/authAttempt.type.js";
10
+ import type { Clock } from "@zudojs/types";
10
11
  import { type ExternalSessionOptions } from "./authProvider.external.js";
11
12
  import type { GuardContext, GuardResult } from "../authTypes/authRbac.type.js";
12
13
  import type { PermissionEngine } from "@zudojs/permissions";
@@ -100,6 +101,23 @@ export interface AuthServiceConfig {
100
101
  * string, or your own function to match how you store identifiers.
101
102
  */
102
103
  readonly normalizeIdentifier?: false | ((identifier: string) => string);
104
+ /**
105
+ * Time source for token `iat`/`exp`, token verification and lockout
106
+ * deadlines (default: `Date.now`). It is copied into `token.clock` unless
107
+ * that is already set. Pass the same clock to the memory stores
108
+ * (`createMemorySessionStore({ clock })`, and the revocation and attempt
109
+ * stores) so expiry tests need no fake timers.
110
+ */
111
+ readonly clock?: Clock;
112
+ /**
113
+ * Refuse a `revocationStore` that lacks `revokeIfNotRevoked` (default:
114
+ * `false`). Without that method `refresh()` falls back to `isRevoked()` +
115
+ * `revoke()`, which is racy; by default construction emits a
116
+ * `SecurityWarning` (code `ZUDO_AUTH_RACY_REVOCATION`) through
117
+ * `process.emitWarning`, and with this flag it throws
118
+ * `AuthConfigurationError` instead.
119
+ */
120
+ readonly requireAtomicRevocation?: boolean;
103
121
  }
104
122
  /**
105
123
  * Auth service interface.
@@ -150,4 +168,3 @@ export interface LoginResult {
150
168
  * Create an auth service.
151
169
  */
152
170
  export declare function createAuthService(config: AuthServiceConfig): AuthService;
153
- //# sourceMappingURL=authProvider.core.d.ts.map
@@ -4,6 +4,7 @@
4
4
  * @module authProvider/authProvider
5
5
  */
6
6
  import { createLoginThrottleGate } from "./authProvider.throttle.js";
7
+ import { assertAtomicRevocationStore } from "../authToken/authToken.revocation.js";
7
8
  import { createExternalSessionStarter, } from "./authProvider.external.js";
8
9
  import { normalizeLoginIdentifier } from "../authUtils/authUtils.identifier.js";
9
10
  import { hashPassword, verifyPassword, } from "../authPassword/authPassword.core.js";
@@ -24,7 +25,8 @@ export { throttleKey } from "./authProvider.throttle.js";
24
25
  * Create an auth service.
25
26
  */
26
27
  export function createAuthService(config) {
27
- const { token: tokenConfig, sessionStore, findUser, findUserById, verifyPassword: verifyPwd, sessionTtlSeconds, absoluteSessionTtlSeconds, permissions, revocationStore, loginThrottle, allowInsecureFallbackGuard, fallbackAdminRole, allowSessionlessTokens, externalSessionMethods, normalizeIdentifier, } = config;
28
+ const { sessionStore, findUser, findUserById, verifyPassword: verifyPwd, sessionTtlSeconds, absoluteSessionTtlSeconds, permissions, revocationStore, loginThrottle, allowInsecureFallbackGuard, fallbackAdminRole, allowSessionlessTokens, externalSessionMethods, normalizeIdentifier, clock, requireAtomicRevocation, } = config;
29
+ const tokenConfig = clock && !config.token.clock ? { ...config.token, clock } : config.token;
28
30
  // Fail at construction, not at the first login: a bad secret or a NaN
29
31
  // lifetime (`Number(process.env.X)` with X unset) otherwise surfaced as
30
32
  // a runtime error on the request path — or, for the session TTL, not at
@@ -32,7 +34,10 @@ export function createAuthService(config) {
32
34
  assertTokenSecrets(tokenConfig);
33
35
  assertPositiveSeconds(sessionTtlSeconds, "sessionTtlSeconds");
34
36
  assertPositiveSeconds(absoluteSessionTtlSeconds, "absoluteSessionTtlSeconds");
35
- const throttle = createLoginThrottleGate(loginThrottle);
37
+ if (revocationStore) {
38
+ assertAtomicRevocationStore(revocationStore, requireAtomicRevocation === true);
39
+ }
40
+ const throttle = createLoginThrottleGate(loginThrottle, tokenConfig.clock);
36
41
  const normalize = normalizeIdentifier === false
37
42
  ? (identifier) => identifier
38
43
  : (normalizeIdentifier ?? normalizeLoginIdentifier);
@@ -124,6 +129,7 @@ export function createAuthService(config) {
124
129
  const tokens = createTokenPair(user.id, tokenConfig, {
125
130
  roles: user.roles,
126
131
  sessionId: session.id,
132
+ claims: user.claims,
127
133
  });
128
134
  return { user, tokens, sessionId: session.id };
129
135
  },
@@ -186,6 +192,7 @@ export function createAuthService(config) {
186
192
  }
187
193
  return createTokenPair(user.id, tokenConfig, {
188
194
  roles: user.roles,
195
+ claims: user.claims,
189
196
  ...(sid ? { sessionId: sid } : {}),
190
197
  });
191
198
  },
@@ -291,4 +298,3 @@ function simpleGuard(userRoles, permission, userId, resourceOwnerId, adminRole)
291
298
  userRoles: [...userRoles],
292
299
  };
293
300
  }
294
- //# sourceMappingURL=authProvider.core.js.map
@@ -48,4 +48,3 @@ export interface ExternalSessionDependencies {
48
48
  * users.
49
49
  */
50
50
  export declare function createExternalSessionStarter(deps: ExternalSessionDependencies): (userId: UserId, options: ExternalSessionOptions) => Promise<ExternalSessionResult>;
51
- //# sourceMappingURL=authProvider.external.d.ts.map
@@ -43,8 +43,8 @@ export function createExternalSessionStarter(deps) {
43
43
  const tokens = createTokenPair(user.id, deps.tokenConfig, {
44
44
  roles: user.roles,
45
45
  sessionId: session.id,
46
+ claims: user.claims,
46
47
  });
47
48
  return { user, tokens, sessionId: session.id };
48
49
  };
49
50
  }
50
- //# sourceMappingURL=authProvider.external.js.map
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * @module authProvider/authProvider.throttle
6
6
  */
7
+ import type { Clock } from "@zudojs/types";
7
8
  import type { LoginThrottleConfig } from "../authTypes/authAttempt.type.js";
8
9
  /**
9
10
  * The key under which an identifier's login attempts are counted.
@@ -47,6 +48,7 @@ export interface LoginThrottleGate {
47
48
  *
48
49
  * An attempt that throws for another reason (e.g. the user lookup fails)
49
50
  * keeps its reservation, so it counts as a failure.
51
+ *
52
+ * @param clock - Time source for lockout deadlines (default: `Date.now`).
50
53
  */
51
- export declare function createLoginThrottleGate(config: LoginThrottleConfig | undefined): LoginThrottleGate;
52
- //# sourceMappingURL=authProvider.throttle.d.ts.map
54
+ export declare function createLoginThrottleGate(config: LoginThrottleConfig | undefined, clock?: Clock): LoginThrottleGate;
@@ -36,8 +36,11 @@ const NO_SLOT = Object.freeze({ failures: 0 });
36
36
  *
37
37
  * An attempt that throws for another reason (e.g. the user lookup fails)
38
38
  * keeps its reservation, so it counts as a failure.
39
+ *
40
+ * @param clock - Time source for lockout deadlines (default: `Date.now`).
39
41
  */
40
- export function createLoginThrottleGate(config) {
42
+ export function createLoginThrottleGate(config, clock) {
43
+ const currentMs = () => (clock ? clock.now() : Date.now());
41
44
  const maxFailedAttempts = config?.maxFailedAttempts ?? DEFAULT_MAX_FAILED_ATTEMPTS;
42
45
  const lockoutMs = (config?.lockoutSeconds ?? DEFAULT_LOCKOUT_SECONDS) * 1000;
43
46
  const maxAttemptsPerWindow = config?.maxAttemptsPerWindow ?? DEFAULT_MAX_ATTEMPTS_PER_WINDOW;
@@ -53,7 +56,7 @@ export function createLoginThrottleGate(config) {
53
56
  if (!config)
54
57
  return NO_SLOT;
55
58
  const key = throttleKey(identifier);
56
- const now = Date.now();
59
+ const now = currentMs();
57
60
  const current = await config.store.get(key);
58
61
  if (current.lockedUntil !== undefined && current.lockedUntil > now) {
59
62
  throw locked(current, now);
@@ -77,7 +80,7 @@ export function createLoginThrottleGate(config) {
77
80
  if (!config)
78
81
  return;
79
82
  if (slot.failures >= maxFailedAttempts) {
80
- await config.store.lock(throttleKey(identifier), Date.now() + lockoutMs);
83
+ await config.store.lock(throttleKey(identifier), currentMs() + lockoutMs);
81
84
  }
82
85
  },
83
86
  async succeed(identifier) {
@@ -85,4 +88,3 @@ export function createLoginThrottleGate(config) {
85
88
  },
86
89
  };
87
90
  }
88
- //# sourceMappingURL=authProvider.throttle.js.map
@@ -6,4 +6,3 @@
6
6
  export { createAuthService, type AuthService, type AuthServiceConfig, type LoginResult, type UserLookup, type UserByIdLookup, type PasswordVerifier, } from "./authProvider.core.js";
7
7
  export { createMemoryLoginAttemptStore } from "./authAttempt.memory.js";
8
8
  export { type ExternalSessionOptions, type ExternalSessionResult, } from "./authProvider.external.js";
9
- //# sourceMappingURL=index.d.ts.map
@@ -6,4 +6,3 @@
6
6
  export { createAuthService, } from "./authProvider.core.js";
7
7
  export { createMemoryLoginAttemptStore } from "./authAttempt.memory.js";
8
8
  export {} from "./authProvider.external.js";
9
- //# sourceMappingURL=index.js.map
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * For production, implement SessionStore backed by Redis, database, etc.
7
7
  */
8
+ import type { Clock } from "@zudojs/types";
8
9
  import type { SessionStore } from "../authTypes/authSession.type.js";
9
10
  /**
10
11
  * Create an in-memory session store.
@@ -19,9 +20,12 @@ import type { SessionStore } from "../authTypes/authSession.type.js";
19
20
  *
20
21
  * @param options.purgeIntervalMs - Minimum gap between full sweeps
21
22
  * (default: 60000). Set to 0 to sweep on every access.
23
+ * @param options.clock - Time source for creation, activity and expiry
24
+ * (default: `Date.now`). Pass the same clock to `createAuthService`.
22
25
  */
23
26
  export declare function createMemorySessionStore(storeOptions?: {
24
27
  readonly purgeIntervalMs?: number;
28
+ readonly clock?: Clock;
25
29
  }): SessionStore;
26
30
  /**
27
31
  * Reject a TTL that cannot produce a real expiry.
@@ -30,4 +34,3 @@ export declare function createMemorySessionStore(storeOptions?: {
30
34
  * finite number greater than zero.
31
35
  */
32
36
  export declare function assertPositiveSeconds(value: number | undefined, field: string): void;
33
- //# sourceMappingURL=authSession.core.d.ts.map
@@ -23,14 +23,18 @@ const DEFAULT_PURGE_INTERVAL_MS = 60_000;
23
23
  *
24
24
  * @param options.purgeIntervalMs - Minimum gap between full sweeps
25
25
  * (default: 60000). Set to 0 to sweep on every access.
26
+ * @param options.clock - Time source for creation, activity and expiry
27
+ * (default: `Date.now`). Pass the same clock to `createAuthService`.
26
28
  */
27
29
  export function createMemorySessionStore(storeOptions) {
28
30
  const sessions = new Map();
29
31
  const ttls = new Map();
30
32
  const purgeIntervalMs = storeOptions?.purgeIntervalMs ?? DEFAULT_PURGE_INTERVAL_MS;
33
+ const clock = storeOptions?.clock;
34
+ const currentMs = () => (clock ? clock.now() : Date.now());
31
35
  let lastPurge = 0;
32
36
  function maybePurgeExpired() {
33
- const nowMs = Date.now();
37
+ const nowMs = currentMs();
34
38
  if (nowMs - lastPurge < purgeIntervalMs)
35
39
  return;
36
40
  lastPurge = nowMs;
@@ -50,7 +54,7 @@ export function createMemorySessionStore(storeOptions) {
50
54
  assertPositiveSeconds(options.absoluteTtlSeconds, "absoluteTtlSeconds");
51
55
  maybePurgeExpired();
52
56
  const id = await generateSessionId();
53
- const now = new Date();
57
+ const now = new Date(currentMs());
54
58
  const ttlMs = (options.ttlSeconds ?? DEFAULT_TTL_SECONDS) * 1000;
55
59
  const absoluteExpiresAt = options.absoluteTtlSeconds !== undefined
56
60
  ? new Date(now.getTime() + options.absoluteTtlSeconds * 1000)
@@ -76,7 +80,7 @@ export function createMemorySessionStore(storeOptions) {
76
80
  const session = sessions.get(sessionId);
77
81
  if (!session)
78
82
  return null;
79
- if (Date.now() > session.expiresAt.getTime()) {
83
+ if (currentMs() > session.expiresAt.getTime()) {
80
84
  sessions.delete(sessionId);
81
85
  ttls.delete(sessionId);
82
86
  return null;
@@ -88,7 +92,7 @@ export function createMemorySessionStore(storeOptions) {
88
92
  const session = sessions.get(sessionId);
89
93
  if (!session)
90
94
  return;
91
- const now = new Date();
95
+ const now = new Date(currentMs());
92
96
  if (now.getTime() > session.expiresAt.getTime()) {
93
97
  sessions.delete(sessionId);
94
98
  ttls.delete(sessionId);
@@ -140,4 +144,3 @@ function clampToAbsolute(expiresAt, absolute) {
140
144
  async function generateSessionId() {
141
145
  return (await randomHex(64));
142
146
  }
143
- //# sourceMappingURL=authSession.core.js.map
@@ -4,4 +4,3 @@
4
4
  * @module authSession
5
5
  */
6
6
  export { createMemorySessionStore } from "./authSession.core.js";
7
- //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,3 @@
4
4
  * @module authSession
5
5
  */
6
6
  export { createMemorySessionStore } from "./authSession.core.js";
7
- //# sourceMappingURL=index.js.map
@@ -8,19 +8,32 @@
8
8
  */
9
9
  import type { SessionId, UserId } from "@zudojs/constants";
10
10
  import type { JwtToken, TokenPair, TokenConfig, TokenVerificationResult } from "../authTypes/authToken.type.js";
11
+ /** Options for {@link createTokenPair}. */
12
+ export interface CreateTokenPairOptions {
13
+ /** Roles to embed in both tokens. */
14
+ readonly roles?: readonly string[];
15
+ /**
16
+ * Session to bind the pair to (`sid` claim). `createAuthService()` sets
17
+ * this so that `logout()` invalidates the pair.
18
+ */
19
+ readonly sessionId?: SessionId;
20
+ /**
21
+ * Custom claims embedded in both tokens, e.g. `{ plan: "pro" }`.
22
+ * Reserved names (`sub`, `iat`, `exp`, `nbf`, `typ`, `jti`, `sid`,
23
+ * `roles`, `iss`, `aud`) are dropped, never overridden. Keep them small:
24
+ * a token over 8 KB is rejected on verification.
25
+ */
26
+ readonly claims?: Readonly<Record<string, unknown>>;
27
+ }
11
28
  /**
12
29
  * Create a new token pair (access + refresh).
13
30
  *
14
- * @param options.roles - Roles to embed in both tokens.
15
- * @param options.sessionId - Session to bind the pair to (`sid` claim).
16
- * `createAuthService()` sets this so that `logout()` invalidates the pair.
31
+ * `iat`/`exp` come from `config.clock` when set (default: `Date.now`).
32
+ *
17
33
  * @throws {AuthConfigurationError} when the signing secrets are missing,
18
34
  * shorter than 32 bytes, or identical to each other.
19
35
  */
20
- export declare function createTokenPair(userId: UserId, config: TokenConfig, options?: {
21
- readonly roles?: readonly string[];
22
- readonly sessionId?: SessionId;
23
- }): TokenPair;
36
+ export declare function createTokenPair(userId: UserId, config: TokenConfig, options?: CreateTokenPairOptions): TokenPair;
24
37
  /**
25
38
  * Verify and decode an access token.
26
39
  *
@@ -60,4 +73,3 @@ export declare function verifyRefreshToken(token: JwtToken, config: TokenConfig)
60
73
  export declare function refreshAccessToken(refreshToken: JwtToken, config: TokenConfig, options?: {
61
74
  readonly roles?: readonly string[];
62
75
  }): TokenPair | null;
63
- //# sourceMappingURL=authToken.core.d.ts.map
@@ -6,6 +6,7 @@
6
6
  * Pure Node.js implementation (no jsonwebtoken dependency).
7
7
  * Uses HMAC SHA-256 for signing.
8
8
  */
9
+ import { sanitizeCustomClaims } from "../authUtils/authUtils.claims.js";
9
10
  import { signToken, verifyToken, generateTokenId, assertTokenSecrets, } from "./authToken.signing.js";
10
11
  // TTLs are in seconds — they are added to Unix-second `iat`/`exp` claims.
11
12
  const DEFAULT_ACCESS_TTL = 900; // 15 minutes
@@ -13,9 +14,8 @@ const DEFAULT_REFRESH_TTL = 604_800; // 7 days
13
14
  /**
14
15
  * Create a new token pair (access + refresh).
15
16
  *
16
- * @param options.roles - Roles to embed in both tokens.
17
- * @param options.sessionId - Session to bind the pair to (`sid` claim).
18
- * `createAuthService()` sets this so that `logout()` invalidates the pair.
17
+ * `iat`/`exp` come from `config.clock` when set (default: `Date.now`).
18
+ *
19
19
  * @throws {AuthConfigurationError} when the signing secrets are missing,
20
20
  * shorter than 32 bytes, or identical to each other.
21
21
  */
@@ -23,32 +23,24 @@ export function createTokenPair(userId, config, options) {
23
23
  assertTokenSecrets(config);
24
24
  const accessTtl = config.accessTtl ?? DEFAULT_ACCESS_TTL;
25
25
  const refreshTtl = config.refreshTtl ?? DEFAULT_REFRESH_TTL;
26
- const now = Math.floor(Date.now() / 1000);
27
- const accessToken = signToken({
26
+ const nowMs = config.clock ? config.clock.now() : Date.now();
27
+ const now = Math.floor(nowMs / 1000);
28
+ const custom = sanitizeCustomClaims(options?.claims);
29
+ const payload = (typ, ttl) => ({
30
+ ...custom,
28
31
  sub: userId,
29
32
  iat: now,
30
- exp: now + accessTtl,
31
- typ: "access",
33
+ exp: now + ttl,
34
+ typ,
32
35
  jti: generateTokenId(),
33
36
  roles: options?.roles,
34
37
  ...(options?.sessionId ? { sid: options.sessionId } : {}),
35
38
  ...(config.issuer ? { iss: config.issuer } : {}),
36
39
  ...(config.audience ? { aud: config.audience } : {}),
37
- }, config.accessSecret);
38
- const refreshToken = signToken({
39
- sub: userId,
40
- iat: now,
41
- exp: now + refreshTtl,
42
- typ: "refresh",
43
- jti: generateTokenId(),
44
- roles: options?.roles,
45
- ...(options?.sessionId ? { sid: options.sessionId } : {}),
46
- ...(config.issuer ? { iss: config.issuer } : {}),
47
- ...(config.audience ? { aud: config.audience } : {}),
48
- }, config.refreshSecret);
40
+ });
49
41
  return {
50
- accessToken,
51
- refreshToken,
42
+ accessToken: signToken(payload("access", accessTtl), config.accessSecret),
43
+ refreshToken: signToken(payload("refresh", refreshTtl), config.refreshSecret),
52
44
  expiresIn: accessTtl,
53
45
  tokenType: "Bearer",
54
46
  };
@@ -105,4 +97,3 @@ export function refreshAccessToken(refreshToken, config, options) {
105
97
  ...(sid ? { sessionId: sid } : {}),
106
98
  });
107
99
  }
108
- //# sourceMappingURL=authToken.core.js.map
@@ -40,4 +40,3 @@ export declare function splitToken(token: unknown): readonly [string, string, st
40
40
  * or does not decode to a non-null, non-array object.
41
41
  */
42
42
  export declare function decodeJsonSegment(segment: string): Record<string, unknown> | null;
43
- //# sourceMappingURL=authToken.encoding.d.ts.map
@@ -71,4 +71,3 @@ export function decodeJsonSegment(segment) {
71
71
  }
72
72
  return parsed;
73
73
  }
74
- //# sourceMappingURL=authToken.encoding.js.map
@@ -7,7 +7,10 @@
7
7
  * implement TokenRevocationStore with Redis or a database so revocations
8
8
  * are shared across instances.
9
9
  */
10
+ import type { Clock } from "@zudojs/types";
10
11
  import type { TokenRevocationStore } from "../authTypes/authToken.type.js";
12
+ /** `code` of the process warning emitted for a non-atomic revocation store. */
13
+ export declare const RACY_REVOCATION_WARNING_CODE = "ZUDO_AUTH_RACY_REVOCATION";
11
14
  /**
12
15
  * Create an in-memory token revocation store.
13
16
  *
@@ -18,8 +21,23 @@ import type { TokenRevocationStore } from "../authTypes/authToken.type.js";
18
21
  *
19
22
  * @param options.purgeIntervalMs - Minimum gap between full sweeps
20
23
  * (default: 60000). Set to 0 to sweep on every access.
24
+ * @param options.clock - Time source for expiry (default: `Date.now`).
21
25
  */
22
26
  export declare function createMemoryTokenRevocationStore(options?: {
23
27
  readonly purgeIntervalMs?: number;
28
+ readonly clock?: Clock;
24
29
  }): TokenRevocationStore;
25
- //# sourceMappingURL=authToken.revocation.d.ts.map
30
+ /**
31
+ * Check that a revocation store can claim a refresh token atomically.
32
+ *
33
+ * Without `revokeIfNotRevoked`, `createAuthService().refresh()` falls back
34
+ * to `isRevoked()` then `revoke()`, which leaves a window in which two
35
+ * concurrent replays of one refresh token both mint a valid pair. That
36
+ * used to happen silently. Now a `SecurityWarning` with code
37
+ * {@link RACY_REVOCATION_WARNING_CODE} is emitted through
38
+ * `process.emitWarning` at construction, or, with `required`, the service
39
+ * refuses to start.
40
+ *
41
+ * @throws {AuthConfigurationError} when `required` and the method is absent.
42
+ */
43
+ export declare function assertAtomicRevocationStore(store: TokenRevocationStore, required: boolean): void;
@@ -7,8 +7,11 @@
7
7
  * implement TokenRevocationStore with Redis or a database so revocations
8
8
  * are shared across instances.
9
9
  */
10
+ import { AuthConfigurationError } from "../authErrors/authError.base.js";
10
11
  /** Minimum interval between full sweeps of the revocation map. */
11
12
  const DEFAULT_PURGE_INTERVAL_MS = 60_000;
13
+ /** `code` of the process warning emitted for a non-atomic revocation store. */
14
+ export const RACY_REVOCATION_WARNING_CODE = "ZUDO_AUTH_RACY_REVOCATION";
12
15
  /**
13
16
  * Create an in-memory token revocation store.
14
17
  *
@@ -19,18 +22,21 @@ const DEFAULT_PURGE_INTERVAL_MS = 60_000;
19
22
  *
20
23
  * @param options.purgeIntervalMs - Minimum gap between full sweeps
21
24
  * (default: 60000). Set to 0 to sweep on every access.
25
+ * @param options.clock - Time source for expiry (default: `Date.now`).
22
26
  */
23
27
  export function createMemoryTokenRevocationStore(options) {
24
28
  const revoked = new Map();
25
29
  const purgeIntervalMs = options?.purgeIntervalMs ?? DEFAULT_PURGE_INTERVAL_MS;
30
+ const clock = options?.clock;
31
+ const nowMs = () => (clock ? clock.now() : Date.now());
26
32
  let lastPurge = 0;
27
33
  /** Full sweep, rate-limited so a large map cannot be walked per request. */
28
34
  function maybePurgeExpired() {
29
- const nowMs = Date.now();
30
- if (nowMs - lastPurge < purgeIntervalMs)
35
+ const current = nowMs();
36
+ if (current - lastPurge < purgeIntervalMs)
31
37
  return;
32
- lastPurge = nowMs;
33
- const now = Math.floor(nowMs / 1000);
38
+ lastPurge = current;
39
+ const now = Math.floor(current / 1000);
34
40
  for (const [id, expiresAt] of revoked) {
35
41
  if (expiresAt < now) {
36
42
  revoked.delete(id);
@@ -42,7 +48,7 @@ export function createMemoryTokenRevocationStore(options) {
42
48
  const expiresAt = revoked.get(tokenId);
43
49
  if (expiresAt === undefined)
44
50
  return false;
45
- if (expiresAt < Math.floor(Date.now() / 1000)) {
51
+ if (expiresAt < Math.floor(nowMs() / 1000)) {
46
52
  revoked.delete(tokenId);
47
53
  return false;
48
54
  }
@@ -68,4 +74,31 @@ export function createMemoryTokenRevocationStore(options) {
68
74
  },
69
75
  };
70
76
  }
71
- //# sourceMappingURL=authToken.revocation.js.map
77
+ /**
78
+ * Check that a revocation store can claim a refresh token atomically.
79
+ *
80
+ * Without `revokeIfNotRevoked`, `createAuthService().refresh()` falls back
81
+ * to `isRevoked()` then `revoke()`, which leaves a window in which two
82
+ * concurrent replays of one refresh token both mint a valid pair. That
83
+ * used to happen silently. Now a `SecurityWarning` with code
84
+ * {@link RACY_REVOCATION_WARNING_CODE} is emitted through
85
+ * `process.emitWarning` at construction, or, with `required`, the service
86
+ * refuses to start.
87
+ *
88
+ * @throws {AuthConfigurationError} when `required` and the method is absent.
89
+ */
90
+ export function assertAtomicRevocationStore(store, required) {
91
+ if (typeof store.revokeIfNotRevoked === "function")
92
+ return;
93
+ const message = "TokenRevocationStore does not implement revokeIfNotRevoked(); " +
94
+ "refresh() falls back to isRevoked() + revoke(), which is racy: two " +
95
+ "concurrent replays of one refresh token can both succeed. Implement " +
96
+ "revokeIfNotRevoked (SET NX in Redis) in any store used in production.";
97
+ if (required) {
98
+ throw new AuthConfigurationError(`${message} Set requireAtomicRevocation: false to accept the fallback.`);
99
+ }
100
+ process.emitWarning(message, {
101
+ type: "SecurityWarning",
102
+ code: RACY_REVOCATION_WARNING_CODE,
103
+ });
104
+ }
@@ -47,4 +47,3 @@ export declare function verifyToken(token: JwtToken, secret: string, expectedTyp
47
47
  * Generate a random token ID.
48
48
  */
49
49
  export declare function generateTokenId(): TokenId;
50
- //# sourceMappingURL=authToken.signing.d.ts.map
@@ -119,7 +119,8 @@ export function verifyToken(token, secret, expectedType, config) {
119
119
  return { valid: false, error: "Invalid payload" };
120
120
  }
121
121
  const payload = decoded;
122
- const now = Math.floor(Date.now() / 1000);
122
+ const nowMs = config.clock ? config.clock.now() : Date.now();
123
+ const now = Math.floor(nowMs / 1000);
123
124
  const skew = config.clockToleranceSeconds ?? 0;
124
125
  if (payload.exp + skew < now) {
125
126
  return { valid: false, error: "Token expired" };
@@ -182,4 +183,3 @@ export function generateTokenId() {
182
183
  function hmacSha256(data, secret) {
183
184
  return createHmac("sha256", secret).update(data).digest("base64url");
184
185
  }
185
- //# sourceMappingURL=authToken.signing.js.map
@@ -3,6 +3,5 @@
3
3
  *
4
4
  * @module authToken
5
5
  */
6
- export { createTokenPair, verifyAccessToken, verifyRefreshToken, refreshAccessToken, } from "./authToken.core.js";
7
- export { createMemoryTokenRevocationStore } from "./authToken.revocation.js";
8
- //# sourceMappingURL=index.d.ts.map
6
+ export { createTokenPair, verifyAccessToken, verifyRefreshToken, refreshAccessToken, type CreateTokenPairOptions, } from "./authToken.core.js";
7
+ export { createMemoryTokenRevocationStore, assertAtomicRevocationStore, RACY_REVOCATION_WARNING_CODE, } from "./authToken.revocation.js";
@@ -4,5 +4,4 @@
4
4
  * @module authToken
5
5
  */
6
6
  export { createTokenPair, verifyAccessToken, verifyRefreshToken, refreshAccessToken, } from "./authToken.core.js";
7
- export { createMemoryTokenRevocationStore } from "./authToken.revocation.js";
8
- //# sourceMappingURL=index.js.map
7
+ export { createMemoryTokenRevocationStore, assertAtomicRevocationStore, RACY_REVOCATION_WARNING_CODE, } from "./authToken.revocation.js";
@@ -25,4 +25,3 @@ export declare const jwt: {
25
25
  readonly isTokenExpired: typeof isTokenExpired;
26
26
  readonly extractUserId: typeof extractUserId;
27
27
  };
28
- //# sourceMappingURL=jwt.namespace.d.ts.map
@@ -24,4 +24,3 @@ export const jwt = {
24
24
  isTokenExpired,
25
25
  extractUserId,
26
26
  };
27
- //# sourceMappingURL=jwt.namespace.js.map
@@ -72,4 +72,3 @@ export interface LoginThrottleConfig {
72
72
  */
73
73
  readonly windowSeconds?: number;
74
74
  }
75
- //# sourceMappingURL=authAttempt.type.d.ts.map
@@ -4,4 +4,3 @@
4
4
  * @module authTypes/authAttempt
5
5
  */
6
6
  export {};
7
- //# sourceMappingURL=authAttempt.type.js.map
@@ -23,4 +23,3 @@ export interface PasswordCredentials {
23
23
  export interface ApiKeyCredentials {
24
24
  readonly apiKey: string;
25
25
  }
26
- //# sourceMappingURL=authCredentials.type.d.ts.map
@@ -8,4 +8,3 @@
8
8
  * identifier and password directly.
9
9
  */
10
10
  export {};
11
- //# sourceMappingURL=authCredentials.type.js.map
@@ -42,4 +42,3 @@ export interface GuardContext {
42
42
  /** Optional resource owner ID (for ownership checks) */
43
43
  readonly resourceOwnerId?: string;
44
44
  }
45
- //# sourceMappingURL=authRbac.type.d.ts.map
@@ -7,4 +7,3 @@
7
7
  * For new code, import directly from @zudojs/permissions.
8
8
  */
9
9
  export {};
10
- //# sourceMappingURL=authRbac.type.js.map
@@ -82,4 +82,3 @@ export interface SessionStore {
82
82
  /** Destroy all sessions for a user */
83
83
  destroyAllForUser(userId: UserId): Promise<void>;
84
84
  }
85
- //# sourceMappingURL=authSession.type.d.ts.map
@@ -20,4 +20,3 @@ export function toSessionId(value) {
20
20
  }
21
21
  return value;
22
22
  }
23
- //# sourceMappingURL=authSession.type.js.map
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { UserId } from "../authTypes/authUser.type.js";
7
7
  import type { SessionId, TokenId } from "@zudojs/constants";
8
+ import type { Clock } from "@zudojs/types";
8
9
  /** JWT token string. */
9
10
  export type JwtToken = string;
10
11
  /** Token identifier. Re-exported from @zudojs/constants for type safety. */
@@ -70,6 +71,13 @@ export interface TokenConfig {
70
71
  * (default: 0, maximum: 300).
71
72
  */
72
73
  readonly clockToleranceSeconds?: number;
74
+ /**
75
+ * Time source for `iat`/`exp` when minting and for expiry when verifying
76
+ * (default: `Date.now`). Inject a fixed or advanceable clock in tests
77
+ * instead of faking global timers. `createAuthService({ clock })` sets it
78
+ * for you.
79
+ */
80
+ readonly clock?: Clock;
73
81
  }
74
82
  /**
75
83
  * Store for revoked token IDs (`jti` claims).
@@ -116,4 +124,3 @@ export interface TokenVerificationResult {
116
124
  /** Error message (if invalid) */
117
125
  readonly error?: string;
118
126
  }
119
- //# sourceMappingURL=authToken.type.d.ts.map
@@ -4,4 +4,3 @@
4
4
  * @module authToken/authToken
5
5
  */
6
6
  export {};
7
- //# sourceMappingURL=authToken.type.js.map
@@ -29,7 +29,13 @@ export interface AuthUser {
29
29
  readonly name?: string;
30
30
  /** Assigned roles */
31
31
  readonly roles: readonly string[];
32
- /** Custom claims */
32
+ /**
33
+ * Custom claims embedded in every token `createAuthService()` mints for
34
+ * this user (`login()`, `refresh()`, `createSessionForUser()`), e.g.
35
+ * `{ plan: "pro" }`. Reserved JWT names (`sub`, `exp`, `roles`, `sid`, ...)
36
+ * are dropped, never overridden; see `RESERVED_JWT_CLAIMS`. Keep them
37
+ * small and non-sensitive: they are readable by anyone holding the token.
38
+ */
33
39
  readonly claims?: Record<string, unknown>;
34
40
  /** Whether the user is active */
35
41
  readonly active: boolean;
@@ -56,4 +62,3 @@ export interface UserRegistration {
56
62
  readonly name?: string;
57
63
  readonly roles?: readonly string[];
58
64
  }
59
- //# sourceMappingURL=authUser.type.d.ts.map
@@ -20,4 +20,3 @@ export function toUserId(value) {
20
20
  }
21
21
  return value;
22
22
  }
23
- //# sourceMappingURL=authUser.type.js.map
@@ -9,4 +9,3 @@ export { type SessionId, toSessionId, type AuthSession, type CreateSessionOption
9
9
  export { type PasswordCredentials, type ApiKeyCredentials, } from "./authCredentials.type.js";
10
10
  export { type LoginAttemptRecord, type LoginAttemptStore, type LoginThrottleConfig, } from "./authAttempt.type.js";
11
11
  export { type Permission, type Role, type GuardResult, type GuardContext, } from "./authRbac.type.js";
12
- //# sourceMappingURL=index.d.ts.map
@@ -9,4 +9,3 @@ export { toSessionId, } from "./authSession.type.js";
9
9
  export {} from "./authCredentials.type.js";
10
10
  export {} from "./authAttempt.type.js";
11
11
  export {} from "./authRbac.type.js";
12
- //# sourceMappingURL=index.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Custom JWT claim handling.
3
+ *
4
+ * @module authUtils/authUtils.claims
5
+ */
6
+ /**
7
+ * Claim names the token minter owns. A custom claim with one of these names
8
+ * is dropped rather than embedded, so `user.claims` can never forge the
9
+ * subject, lifetime, type, id, session binding, roles, issuer or audience of
10
+ * a token.
11
+ */
12
+ export declare const RESERVED_JWT_CLAIMS: ReadonlySet<string>;
13
+ /**
14
+ * Return the custom claims that may be embedded in a token: every own,
15
+ * enumerable entry of `claims` whose name is not reserved and whose value
16
+ * is not `undefined`.
17
+ *
18
+ * @param claims - Custom claims, typically `AuthUser.claims`.
19
+ * @returns A fresh plain object, or `undefined` when nothing survives.
20
+ */
21
+ export declare function sanitizeCustomClaims(claims: Readonly<Record<string, unknown>> | undefined): Record<string, unknown> | undefined;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Custom JWT claim handling.
3
+ *
4
+ * @module authUtils/authUtils.claims
5
+ */
6
+ /**
7
+ * Claim names the token minter owns. A custom claim with one of these names
8
+ * is dropped rather than embedded, so `user.claims` can never forge the
9
+ * subject, lifetime, type, id, session binding, roles, issuer or audience of
10
+ * a token.
11
+ */
12
+ export const RESERVED_JWT_CLAIMS = new Set([
13
+ "sub",
14
+ "iat",
15
+ "exp",
16
+ "nbf",
17
+ "typ",
18
+ "jti",
19
+ "sid",
20
+ "roles",
21
+ "iss",
22
+ "aud",
23
+ ]);
24
+ /**
25
+ * Return the custom claims that may be embedded in a token: every own,
26
+ * enumerable entry of `claims` whose name is not reserved and whose value
27
+ * is not `undefined`.
28
+ *
29
+ * @param claims - Custom claims, typically `AuthUser.claims`.
30
+ * @returns A fresh plain object, or `undefined` when nothing survives.
31
+ */
32
+ export function sanitizeCustomClaims(claims) {
33
+ if (!claims)
34
+ return undefined;
35
+ const result = {};
36
+ let count = 0;
37
+ for (const [name, value] of Object.entries(claims)) {
38
+ if (RESERVED_JWT_CLAIMS.has(name) || value === undefined)
39
+ continue;
40
+ result[name] = value;
41
+ count++;
42
+ }
43
+ return count > 0 ? result : undefined;
44
+ }
@@ -35,10 +35,13 @@ export declare function parseCookies(cookie: unknown): Record<string, string>;
35
35
  * I refresh before calling?"), never as an authorization decision.
36
36
  *
37
37
  * @param token - JWT token string
38
+ * @param clock - Time source (default: `Date.now`).
38
39
  * @returns Whether the token appears expired. Unparseable, oversized, or
39
40
  * malformed input is reported as expired.
40
41
  */
41
- export declare function isTokenExpired(token: unknown): boolean;
42
+ export declare function isTokenExpired(token: unknown, clock?: {
43
+ now(): number;
44
+ }): boolean;
42
45
  /**
43
46
  * Extract the user ID from a JWT payload **without verifying the signature**.
44
47
  *
@@ -62,4 +65,3 @@ export declare function extractUserId(token: unknown): string | null;
62
65
  * @returns Random hex string for CSRF protection
63
66
  */
64
67
  export declare function generateCsrfToken(): string;
65
- //# sourceMappingURL=authUtils.helper.d.ts.map
@@ -76,10 +76,11 @@ export function parseCookies(cookie) {
76
76
  * I refresh before calling?"), never as an authorization decision.
77
77
  *
78
78
  * @param token - JWT token string
79
+ * @param clock - Time source (default: `Date.now`).
79
80
  * @returns Whether the token appears expired. Unparseable, oversized, or
80
81
  * malformed input is reported as expired.
81
82
  */
82
- export function isTokenExpired(token) {
83
+ export function isTokenExpired(token, clock) {
83
84
  const parts = splitToken(token);
84
85
  if (!parts)
85
86
  return true;
@@ -87,7 +88,7 @@ export function isTokenExpired(token) {
87
88
  if (!payload)
88
89
  return true;
89
90
  const exp = payload["exp"];
90
- const now = Math.floor(Date.now() / 1000);
91
+ const now = Math.floor((clock ? clock.now() : Date.now()) / 1000);
91
92
  return typeof exp !== "number" || !Number.isFinite(exp) || exp < now;
92
93
  }
93
94
  /**
@@ -124,4 +125,3 @@ export function extractUserId(token) {
124
125
  export function generateCsrfToken() {
125
126
  return randomBytes(32).toString("hex");
126
127
  }
127
- //# sourceMappingURL=authUtils.helper.js.map
@@ -18,4 +18,3 @@
18
18
  * @returns The normalized identifier.
19
19
  */
20
20
  export declare function normalizeLoginIdentifier(identifier: string): string;
21
- //# sourceMappingURL=authUtils.identifier.d.ts.map
@@ -22,4 +22,3 @@ export function normalizeLoginIdentifier(identifier) {
22
22
  const trimmed = String(identifier).normalize("NFKC").trim();
23
23
  return isEmail(trimmed) ? trimmed.toLowerCase() : trimmed;
24
24
  }
25
- //# sourceMappingURL=authUtils.identifier.js.map
@@ -6,4 +6,4 @@
6
6
  */
7
7
  export { parseBearerToken, parseCookies, isTokenExpired, extractUserId, generateCsrfToken, } from "./authUtils.helper.js";
8
8
  export { normalizeLoginIdentifier } from "./authUtils.identifier.js";
9
- //# sourceMappingURL=index.d.ts.map
9
+ export { RESERVED_JWT_CLAIMS, sanitizeCustomClaims, } from "./authUtils.claims.js";
@@ -6,4 +6,4 @@
6
6
  */
7
7
  export { parseBearerToken, parseCookies, isTokenExpired, extractUserId, generateCsrfToken, } from "./authUtils.helper.js";
8
8
  export { normalizeLoginIdentifier } from "./authUtils.identifier.js";
9
- //# sourceMappingURL=index.js.map
9
+ export { RESERVED_JWT_CLAIMS, sanitizeCustomClaims, } from "./authUtils.claims.js";
package/dist/index.d.ts CHANGED
@@ -16,4 +16,3 @@ export { jwt } from "./authToken/jwt.namespace.js";
16
16
  export * from "./authSession/index.js";
17
17
  export * from "./authProvider/index.js";
18
18
  export * from "./authUtils/index.js";
19
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -16,4 +16,3 @@ export { jwt } from "./authToken/jwt.namespace.js";
16
16
  export * from "./authSession/index.js";
17
17
  export * from "./authProvider/index.js";
18
18
  export * from "./authUtils/index.js";
19
- //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/auth",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Authentication and authorization services for the Zudojs framework — JWT, sessions, RBAC, and password management.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -25,11 +25,11 @@
25
25
  "!dist/.tsbuildinfo"
26
26
  ],
27
27
  "dependencies": {
28
- "@zudojs/constants": "1.1.4",
29
- "@zudojs/crypto": "1.3.3",
30
- "@zudojs/errors": "1.3.2",
31
- "@zudojs/permissions": "1.4.3",
32
- "@zudojs/types": "1.2.0"
28
+ "@zudojs/constants": "1.2.0",
29
+ "@zudojs/crypto": "1.4.0",
30
+ "@zudojs/errors": "1.4.0",
31
+ "@zudojs/permissions": "1.5.0",
32
+ "@zudojs/types": "1.3.0"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=24.0.0"