@zudojs/auth 0.1.0 → 1.0.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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -17
  3. package/dist/authErrors/authError.base.d.ts +50 -16
  4. package/dist/authErrors/authError.base.js +94 -18
  5. package/dist/authErrors/index.d.ts +1 -1
  6. package/dist/authErrors/index.js +1 -1
  7. package/dist/authPassword/authPassword.core.d.ts +31 -4
  8. package/dist/authPassword/authPassword.core.js +119 -20
  9. package/dist/authPassword/index.d.ts +1 -1
  10. package/dist/authPassword/index.js +1 -1
  11. package/dist/authProvider/authAttempt.memory.d.ts +21 -0
  12. package/dist/authProvider/authAttempt.memory.js +91 -0
  13. package/dist/authProvider/authProvider.core.d.ts +56 -8
  14. package/dist/authProvider/authProvider.core.js +211 -37
  15. package/dist/authProvider/index.d.ts +2 -1
  16. package/dist/authProvider/index.js +1 -0
  17. package/dist/authSession/authSession.core.d.ts +13 -3
  18. package/dist/authSession/authSession.core.js +62 -13
  19. package/dist/authToken/authToken.core.d.ts +27 -1
  20. package/dist/authToken/authToken.core.js +35 -4
  21. package/dist/authToken/authToken.encoding.d.ts +43 -0
  22. package/dist/authToken/authToken.encoding.js +74 -0
  23. package/dist/authToken/authToken.revocation.d.ts +25 -0
  24. package/dist/authToken/authToken.revocation.js +71 -0
  25. package/dist/authToken/authToken.signing.d.ts +25 -1
  26. package/dist/authToken/authToken.signing.js +111 -33
  27. package/dist/authToken/index.d.ts +1 -0
  28. package/dist/authToken/index.js +1 -0
  29. package/dist/authToken/jwt.namespace.d.ts +18 -8
  30. package/dist/authToken/jwt.namespace.js +11 -1
  31. package/dist/authTypes/authAttempt.type.d.ts +63 -0
  32. package/dist/authTypes/authAttempt.type.js +7 -0
  33. package/dist/authTypes/authCredentials.type.d.ts +26 -0
  34. package/dist/authTypes/authCredentials.type.js +11 -0
  35. package/dist/authTypes/authSession.type.d.ts +31 -3
  36. package/dist/authTypes/authSession.type.js +17 -1
  37. package/dist/authTypes/authToken.type.d.ts +48 -1
  38. package/dist/authTypes/authUser.type.d.ts +12 -0
  39. package/dist/authTypes/authUser.type.js +17 -1
  40. package/dist/authTypes/index.d.ts +6 -5
  41. package/dist/authTypes/index.js +5 -4
  42. package/dist/authUtils/authUtils.helper.d.ts +31 -10
  43. package/dist/authUtils/authUtils.helper.js +70 -31
  44. package/package.json +23 -16
  45. package/dist/.tsbuildinfo +0 -1
  46. package/dist/authErrors/authError.base.d.ts.map +0 -1
  47. package/dist/authErrors/authError.base.js.map +0 -1
  48. package/dist/authErrors/index.d.ts.map +0 -1
  49. package/dist/authErrors/index.js.map +0 -1
  50. package/dist/authPassword/authPassword.core.d.ts.map +0 -1
  51. package/dist/authPassword/authPassword.core.js.map +0 -1
  52. package/dist/authPassword/index.d.ts.map +0 -1
  53. package/dist/authPassword/index.js.map +0 -1
  54. package/dist/authProvider/authProvider.core.d.ts.map +0 -1
  55. package/dist/authProvider/authProvider.core.js.map +0 -1
  56. package/dist/authProvider/index.d.ts.map +0 -1
  57. package/dist/authProvider/index.js.map +0 -1
  58. package/dist/authSession/authSession.core.d.ts.map +0 -1
  59. package/dist/authSession/authSession.core.js.map +0 -1
  60. package/dist/authSession/index.d.ts.map +0 -1
  61. package/dist/authSession/index.js.map +0 -1
  62. package/dist/authToken/authToken.core.d.ts.map +0 -1
  63. package/dist/authToken/authToken.core.js.map +0 -1
  64. package/dist/authToken/authToken.signing.d.ts.map +0 -1
  65. package/dist/authToken/authToken.signing.js.map +0 -1
  66. package/dist/authToken/index.d.ts.map +0 -1
  67. package/dist/authToken/index.js.map +0 -1
  68. package/dist/authToken/jwt.namespace.d.ts.map +0 -1
  69. package/dist/authToken/jwt.namespace.js.map +0 -1
  70. package/dist/authTypes/authRbac.type.d.ts.map +0 -1
  71. package/dist/authTypes/authRbac.type.js.map +0 -1
  72. package/dist/authTypes/authSession.type.d.ts.map +0 -1
  73. package/dist/authTypes/authSession.type.js.map +0 -1
  74. package/dist/authTypes/authStrategy.type.d.ts +0 -83
  75. package/dist/authTypes/authStrategy.type.d.ts.map +0 -1
  76. package/dist/authTypes/authStrategy.type.js +0 -7
  77. package/dist/authTypes/authStrategy.type.js.map +0 -1
  78. package/dist/authTypes/authToken.type.d.ts.map +0 -1
  79. package/dist/authTypes/authToken.type.js.map +0 -1
  80. package/dist/authTypes/authUser.type.d.ts.map +0 -1
  81. package/dist/authTypes/authUser.type.js.map +0 -1
  82. package/dist/authTypes/index.d.ts.map +0 -1
  83. package/dist/authTypes/index.js.map +0 -1
  84. package/dist/authUtils/authUtils.helper.d.ts.map +0 -1
  85. package/dist/authUtils/authUtils.helper.js.map +0 -1
  86. package/dist/authUtils/index.d.ts.map +0 -1
  87. package/dist/authUtils/index.js.map +0 -1
  88. package/dist/index.d.ts.map +0 -1
  89. package/dist/index.js.map +0 -1
@@ -7,8 +7,22 @@
7
7
  * Compatible with Node.js ≥ 24 (no external dependencies).
8
8
  */
9
9
  import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
10
+ import { ErrorCode } from "@zudojs/errors";
11
+ import { AuthError } from "../authErrors/authError.base.js";
10
12
  /** Default salt length in bytes. */
11
13
  const SALT_LENGTH = 32;
14
+ /** Accepted range for a caller-supplied salt length, in bytes. */
15
+ export const MIN_SALT_LENGTH = 16;
16
+ export const MAX_SALT_LENGTH = 64;
17
+ /**
18
+ * Maximum accepted password length in bytes.
19
+ *
20
+ * scrypt's cost is set by N/r, not by the input length, so a long password
21
+ * is not a work-factor amplifier — but it is still an unbounded allocation
22
+ * driven by an unauthenticated request body. 1024 bytes is far past any
23
+ * real passphrase.
24
+ */
25
+ export const MAX_PASSWORD_BYTES = 1024;
12
26
  /** Default key length for scrypt. */
13
27
  const KEY_LENGTH = 64;
14
28
  /** Scrypt parameters (N, r, p). */
@@ -18,31 +32,73 @@ const SCRYPT_P = 1;
18
32
  /**
19
33
  * Hash a plain-text password.
20
34
  *
21
- * @param password - Plain-text password
22
- * @param saltLength - Salt length in bytes (default: 32)
23
- * @returns Hashed password string in format "scrypt$salt$hash"
35
+ * @param password - Plain-text password. Must be at most
36
+ * {@link MAX_PASSWORD_BYTES} bytes of UTF-8.
37
+ * @param saltLength - Salt length in bytes (default: 32). Must be an integer
38
+ * between {@link MIN_SALT_LENGTH} and {@link MAX_SALT_LENGTH}; `0` would
39
+ * otherwise silently produce unsalted, rainbow-table-able hashes.
40
+ * @returns Hashed password string in format "scrypt$N$r$p$salt$hash"
41
+ * @throws {AuthError} with `ErrorCode.INVALID_INPUT` when the password is
42
+ * not a string, is too long, or the salt length is out of range.
24
43
  */
25
44
  export async function hashPassword(password, saltLength = SALT_LENGTH) {
45
+ if (typeof password !== "string") {
46
+ throw new AuthError("Password must be a string.", {
47
+ code: ErrorCode.INVALID_INPUT,
48
+ statusCode: 400,
49
+ });
50
+ }
51
+ if (Buffer.byteLength(password, "utf-8") > MAX_PASSWORD_BYTES) {
52
+ throw new AuthError(`Password exceeds the maximum of ${MAX_PASSWORD_BYTES} bytes.`, { code: ErrorCode.INVALID_INPUT, statusCode: 400 });
53
+ }
54
+ if (!Number.isInteger(saltLength) ||
55
+ saltLength < MIN_SALT_LENGTH ||
56
+ saltLength > MAX_SALT_LENGTH) {
57
+ throw new AuthError(`saltLength must be an integer between ${MIN_SALT_LENGTH} and ${MAX_SALT_LENGTH} bytes.`, { code: ErrorCode.INVALID_INPUT, statusCode: 400 });
58
+ }
26
59
  const salt = randomBytes(saltLength).toString("hex");
27
- const derivedKey = await deriveKey(password, salt);
28
- return `scrypt${salt}$${derivedKey}`;
60
+ const derivedKey = await deriveKey(password, salt, {
61
+ N: SCRYPT_N,
62
+ r: SCRYPT_R,
63
+ p: SCRYPT_P,
64
+ });
65
+ return `scrypt$${SCRYPT_N}$${SCRYPT_R}$${SCRYPT_P}$${salt}$${derivedKey}`;
29
66
  }
30
67
  /**
31
68
  * Verify a plain-text password against a hash.
32
69
  *
70
+ * Accepts the current "scrypt$N$r$p$salt$hash" format as well as the
71
+ * legacy "scrypt<salt>$<hash>" format produced by versions ≤ 0.1.1.
72
+ *
73
+ * Never throws: any input this function cannot make sense of — a
74
+ * non-string, an over-length password (see {@link MAX_PASSWORD_BYTES}), an
75
+ * unparseable hash — is a non-match. Callers are on the request path and
76
+ * treat a `false` as "wrong password", which is the correct outcome for all
77
+ * of those.
78
+ *
33
79
  * @param password - Plain-text password to verify
34
80
  * @param hashedPassword - Previously hashed password
35
81
  * @returns Whether the password matches
36
82
  */
37
83
  export async function verifyPassword(password, hashedPassword) {
38
- const parts = hashedPassword.split("$");
39
- if (parts.length !== 2 || !parts[0].startsWith("scrypt")) {
84
+ if (typeof password !== "string" || typeof hashedPassword !== "string") {
85
+ return false;
86
+ }
87
+ if (Buffer.byteLength(password, "utf-8") > MAX_PASSWORD_BYTES) {
88
+ return false;
89
+ }
90
+ const parsed = parseHash(hashedPassword);
91
+ if (!parsed)
40
92
  return false;
93
+ let derivedKey;
94
+ try {
95
+ derivedKey = await deriveKey(password, parsed.salt, parsed.params);
41
96
  }
42
- const salt = parts[0].slice(6);
43
- const storedHash = parts[1];
44
- const derivedKey = await deriveKey(password, salt);
45
- const storedBuffer = Buffer.from(storedHash, "hex");
97
+ catch {
98
+ // scrypt rejects params it cannot satisfy (e.g. over maxmem)
99
+ return false;
100
+ }
101
+ const storedBuffer = Buffer.from(parsed.hash, "hex");
46
102
  const derivedBuffer = Buffer.from(derivedKey, "hex");
47
103
  if (storedBuffer.length !== derivedBuffer.length) {
48
104
  return false;
@@ -50,19 +106,21 @@ export async function verifyPassword(password, hashedPassword) {
50
106
  return timingSafeEqual(storedBuffer, derivedBuffer);
51
107
  }
52
108
  /**
53
- * Check if a password hash needs rehashing (e.g. after salt length change).
109
+ * Check if a password hash needs rehashing (legacy format, changed
110
+ * scrypt parameters, or changed salt length).
54
111
  *
55
112
  * @param hashedPassword - The stored hash
56
113
  * @returns Whether the hash should be regenerated
57
114
  */
58
115
  export function needsRehash(hashedPassword) {
59
- const parts = hashedPassword.split("$");
60
- if (parts.length !== 2 || !parts[0].startsWith("scrypt")) {
116
+ const parsed = parseHash(hashedPassword);
117
+ if (!parsed || parsed.legacy)
61
118
  return true;
62
- }
63
- const salt = parts[0].slice(6);
64
- const saltBytes = salt.length / 2;
65
- return saltBytes !== SALT_LENGTH;
119
+ const saltBytes = parsed.salt.length / 2;
120
+ return (saltBytes !== SALT_LENGTH ||
121
+ parsed.params.N !== SCRYPT_N ||
122
+ parsed.params.r !== SCRYPT_R ||
123
+ parsed.params.p !== SCRYPT_P);
66
124
  }
67
125
  /**
68
126
  * Generate a random token string (for password reset, etc.).
@@ -71,11 +129,52 @@ export function needsRehash(hashedPassword) {
71
129
  * @returns Hex-encoded random string
72
130
  */
73
131
  export function generateRandomToken(length = 32) {
132
+ if (!Number.isInteger(length) || length < 16 || length > 1024) {
133
+ throw new AuthError("generateRandomToken length must be an integer between 16 and 1024 bytes.", { code: ErrorCode.INVALID_INPUT, statusCode: 400 });
134
+ }
74
135
  return randomBytes(length).toString("hex");
75
136
  }
76
- function deriveKey(password, salt) {
137
+ function parseHash(hashedPassword) {
138
+ const parts = hashedPassword.split("$");
139
+ if (parts.length === 6 && parts[0] === "scrypt") {
140
+ const N = Number(parts[1]);
141
+ const r = Number(parts[2]);
142
+ const p = Number(parts[3]);
143
+ // Bounds also cap the memory/CPU a corrupted hash string can request.
144
+ if (!Number.isInteger(N) ||
145
+ !Number.isInteger(r) ||
146
+ !Number.isInteger(p) ||
147
+ N < 2 ||
148
+ (N & (N - 1)) !== 0 ||
149
+ N > 1 << 20 ||
150
+ r < 1 ||
151
+ r > 64 ||
152
+ p < 1 ||
153
+ p > 16) {
154
+ return null;
155
+ }
156
+ return {
157
+ params: { N, r, p },
158
+ salt: parts[4],
159
+ hash: parts[5],
160
+ legacy: false,
161
+ };
162
+ }
163
+ // Legacy format from ≤ 0.1.1: "scrypt<salt>$<hash>" (no separator
164
+ // between the prefix and the salt, params not stored).
165
+ if (parts.length === 2 && parts[0].startsWith("scrypt")) {
166
+ return {
167
+ params: { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P },
168
+ salt: parts[0].slice(6),
169
+ hash: parts[1],
170
+ legacy: true,
171
+ };
172
+ }
173
+ return null;
174
+ }
175
+ function deriveKey(password, salt, params) {
77
176
  return new Promise((resolve, reject) => {
78
- scrypt(password, salt, KEY_LENGTH, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }, (err, derivedKey) => {
177
+ scrypt(password, salt, KEY_LENGTH, { N: params.N, r: params.r, p: params.p }, (err, derivedKey) => {
79
178
  if (err)
80
179
  reject(err);
81
180
  else
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module authPassword
5
5
  */
6
- export { hashPassword, verifyPassword, needsRehash, generateRandomToken, } from "./authPassword.core.js";
6
+ export { hashPassword, verifyPassword, needsRehash, generateRandomToken, MIN_SALT_LENGTH, MAX_SALT_LENGTH, MAX_PASSWORD_BYTES, } from "./authPassword.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module authPassword
5
5
  */
6
- export { hashPassword, verifyPassword, needsRehash, generateRandomToken, } from "./authPassword.core.js";
6
+ export { hashPassword, verifyPassword, needsRehash, generateRandomToken, MIN_SALT_LENGTH, MAX_SALT_LENGTH, MAX_PASSWORD_BYTES, } from "./authPassword.core.js";
7
7
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * In-memory login attempt store.
3
+ *
4
+ * @module authProvider/authAttempt.memory
5
+ *
6
+ * Single-process only: counters are not shared between instances. Back
7
+ * `LoginAttemptStore` with Redis for a real deployment.
8
+ */
9
+ import type { LoginAttemptStore } from "../authTypes/authAttempt.type.js";
10
+ /**
11
+ * Create an in-memory {@link LoginAttemptStore}.
12
+ *
13
+ * @param options.windowSeconds - Rate-limit window length (default: 60).
14
+ * @param options.purgeIntervalMs - Minimum gap between sweeps of stale
15
+ * entries (default: 60000).
16
+ */
17
+ export declare function createMemoryLoginAttemptStore(options?: {
18
+ readonly windowSeconds?: number;
19
+ readonly purgeIntervalMs?: number;
20
+ }): LoginAttemptStore;
21
+ //# sourceMappingURL=authAttempt.memory.d.ts.map
@@ -0,0 +1,91 @@
1
+ /**
2
+ * In-memory login attempt store.
3
+ *
4
+ * @module authProvider/authAttempt.memory
5
+ *
6
+ * Single-process only: counters are not shared between instances. Back
7
+ * `LoginAttemptStore` with Redis for a real deployment.
8
+ */
9
+ const DEFAULT_WINDOW_SECONDS = 60;
10
+ const DEFAULT_PURGE_INTERVAL_MS = 60_000;
11
+ const EMPTY = { failures: 0, attempts: 0 };
12
+ /**
13
+ * Create an in-memory {@link LoginAttemptStore}.
14
+ *
15
+ * @param options.windowSeconds - Rate-limit window length (default: 60).
16
+ * @param options.purgeIntervalMs - Minimum gap between sweeps of stale
17
+ * entries (default: 60000).
18
+ */
19
+ export function createMemoryLoginAttemptStore(options) {
20
+ const windowMs = (options?.windowSeconds ?? DEFAULT_WINDOW_SECONDS) * 1000;
21
+ const purgeIntervalMs = options?.purgeIntervalMs ?? DEFAULT_PURGE_INTERVAL_MS;
22
+ const entries = new Map();
23
+ let lastPurge = 0;
24
+ function maybePurge(now) {
25
+ if (now - lastPurge < purgeIntervalMs)
26
+ return;
27
+ lastPurge = now;
28
+ for (const [key, entry] of entries) {
29
+ const locked = entry.lockedUntil !== undefined && entry.lockedUntil > now;
30
+ const fresh = now - entry.windowStart < windowMs;
31
+ if (!locked && !fresh && entry.failures === 0) {
32
+ entries.delete(key);
33
+ }
34
+ }
35
+ }
36
+ function load(identifier, now) {
37
+ maybePurge(now);
38
+ let entry = entries.get(identifier);
39
+ if (!entry) {
40
+ entry = { failures: 0, attempts: 0, windowStart: now };
41
+ entries.set(identifier, entry);
42
+ }
43
+ if (now - entry.windowStart >= windowMs) {
44
+ entry.windowStart = now;
45
+ entry.attempts = 0;
46
+ }
47
+ if (entry.lockedUntil !== undefined && entry.lockedUntil <= now) {
48
+ // Lockout lapsed: clear it and the failure streak that caused it.
49
+ delete entry.lockedUntil;
50
+ entry.failures = 0;
51
+ }
52
+ return entry;
53
+ }
54
+ function snapshot(entry) {
55
+ return entry.lockedUntil !== undefined
56
+ ? {
57
+ failures: entry.failures,
58
+ attempts: entry.attempts,
59
+ lockedUntil: entry.lockedUntil,
60
+ }
61
+ : { failures: entry.failures, attempts: entry.attempts };
62
+ }
63
+ return {
64
+ async get(identifier) {
65
+ const now = Date.now();
66
+ if (!entries.has(identifier)) {
67
+ maybePurge(now);
68
+ return EMPTY;
69
+ }
70
+ return snapshot(load(identifier, now));
71
+ },
72
+ async recordAttempt(identifier) {
73
+ const entry = load(identifier, Date.now());
74
+ entry.attempts++;
75
+ return snapshot(entry);
76
+ },
77
+ async recordFailure(identifier) {
78
+ const entry = load(identifier, Date.now());
79
+ entry.failures++;
80
+ return snapshot(entry);
81
+ },
82
+ async lock(identifier, until) {
83
+ const entry = load(identifier, Date.now());
84
+ entry.lockedUntil = until;
85
+ },
86
+ async reset(identifier) {
87
+ entries.delete(identifier);
88
+ },
89
+ };
90
+ }
91
+ //# sourceMappingURL=authAttempt.memory.js.map
@@ -4,12 +4,15 @@
4
4
  * @module authProvider/authProvider
5
5
  */
6
6
  import type { AuthUser, UserCredentials, UserId } from "../authTypes/authUser.type.js";
7
- import type { TokenPair, TokenConfig } from "../authTypes/authToken.type.js";
7
+ import type { TokenPair, TokenPayload, TokenConfig, TokenRevocationStore } from "../authTypes/authToken.type.js";
8
8
  import type { SessionStore, SessionId } from "../authTypes/authSession.type.js";
9
- import type { GuardResult } from "../authTypes/authRbac.type.js";
9
+ import type { LoginThrottleConfig } from "../authTypes/authAttempt.type.js";
10
+ import type { GuardContext, GuardResult } from "../authTypes/authRbac.type.js";
10
11
  import type { PermissionEngine } from "@zudojs/permissions";
11
- /** User lookup function provided by the consumer. */
12
+ /** User lookup function provided by the consumer, keyed by login identifier. */
12
13
  export type UserLookup = (identifier: string) => Promise<AuthUser | null>;
14
+ /** User lookup function provided by the consumer, keyed by user id. */
15
+ export type UserByIdLookup = (userId: UserId) => Promise<AuthUser | null>;
13
16
  /** Password verifier function provided by the consumer. */
14
17
  export type PasswordVerifier = (userId: UserId, password: string) => Promise<boolean>;
15
18
  /**
@@ -20,14 +23,52 @@ export interface AuthServiceConfig {
20
23
  readonly token: TokenConfig;
21
24
  /** Session store */
22
25
  readonly sessionStore: SessionStore;
23
- /** User lookup function */
26
+ /** User lookup function, keyed by the identifier submitted at login */
24
27
  readonly findUser: UserLookup;
28
+ /**
29
+ * User lookup keyed by user id (the `sub` claim).
30
+ *
31
+ * Used by `refresh()` to re-load the user on every rotation so that
32
+ * deactivation and role changes take effect instead of being frozen into
33
+ * the refresh token for its whole lifetime.
34
+ *
35
+ * Required. `findUser` is keyed by the login identifier, which for most
36
+ * deployments is an email or username rather than an id — silently reusing
37
+ * it here would make every refresh fail closed at runtime. Requiring this
38
+ * surfaces the mismatch at compile time instead. If your `findUser` really
39
+ * is id-keyed, pass it for both.
40
+ */
41
+ readonly findUserById: UserByIdLookup;
25
42
  /** Password verifier function */
26
43
  readonly verifyPassword: PasswordVerifier;
27
- /** Session TTL in seconds */
44
+ /** Session idle TTL in seconds */
28
45
  readonly sessionTtlSeconds: number;
46
+ /**
47
+ * Absolute maximum session lifetime in seconds. Without it, a session that
48
+ * is used at least once per idle window never expires.
49
+ */
50
+ readonly absoluteSessionTtlSeconds?: number;
29
51
  /** Optional permission engine */
30
52
  readonly permissions?: PermissionEngine;
53
+ /**
54
+ * Optional revocation store. When provided, `refresh()` rotates refresh
55
+ * tokens atomically: the used token's `jti` is revoked so it cannot be
56
+ * replayed, and a replay attempt destroys the user's sessions.
57
+ */
58
+ readonly revocationStore?: TokenRevocationStore;
59
+ /** Optional brute-force lockout and login rate limiting. */
60
+ readonly loginThrottle?: LoginThrottleConfig;
61
+ /**
62
+ * Allow `checkAccess()` to fall back to the built-in `simpleGuard` when no
63
+ * `permissions` engine is configured.
64
+ *
65
+ * The fallback grants a resource owner *every* permission and grants the
66
+ * `admin` role everything, so it is opt-in: without this flag,
67
+ * `checkAccess()` throws instead of silently returning `allowed: true`.
68
+ */
69
+ readonly allowInsecureFallbackGuard?: boolean;
70
+ /** Role name the fallback guard treats as superuser (default: "admin"). */
71
+ readonly fallbackAdminRole?: string;
31
72
  }
32
73
  /**
33
74
  * Auth service interface.
@@ -37,10 +78,17 @@ export interface AuthService {
37
78
  readonly userAgent?: string;
38
79
  readonly ip?: string;
39
80
  }): Promise<LoginResult>;
40
- verifyToken(token: string): Record<string, unknown>;
81
+ /**
82
+ * Verify an access token and return its payload.
83
+ *
84
+ * Async because a token carrying a `sid` claim is only accepted while its
85
+ * session is still alive.
86
+ */
87
+ verifyToken(token: string): Promise<TokenPayload>;
41
88
  refresh(refreshToken: string): Promise<TokenPair>;
42
- logout(sessionId: SessionId): Promise<void>;
43
- checkAccess(userId: UserId, userRoles: readonly string[], permission: string, resourceOwnerId?: UserId): Promise<GuardResult>;
89
+ logout(sessionId: SessionId, refreshToken?: string): Promise<void>;
90
+ logoutAll(userId: UserId): Promise<void>;
91
+ checkAccess(context: GuardContext): Promise<GuardResult>;
44
92
  hashPassword(password: string): Promise<string>;
45
93
  verifyPasswordHash(password: string, hash: string): Promise<boolean>;
46
94
  }