@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
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Internal base64url + JWT segment helpers.
3
+ *
4
+ * @module authToken/authToken.encoding
5
+ *
6
+ * Not exported from the package barrel. Every JWT segment in this package —
7
+ * verified or not — is decoded through here so that the bounds and the
8
+ * decoding rules stay in exactly one place.
9
+ */
10
+ /**
11
+ * Maximum accepted length, in characters, of a complete JWT.
12
+ *
13
+ * An HS256 JWT with a generous set of claims is well under 2 KB; 8 KB is the
14
+ * conventional HTTP header limit. Anything larger is rejected before it is
15
+ * decoded so that an unauthenticated request cannot make the process
16
+ * allocate and parse an arbitrarily large buffer.
17
+ */
18
+ export const MAX_TOKEN_LENGTH = 8192;
19
+ /**
20
+ * Maximum accepted length, in characters, of the JOSE header segment.
21
+ * A real header (`{"alg":"HS256","typ":"JWT"}`) is 36 characters encoded.
22
+ */
23
+ export const MAX_HEADER_SEGMENT_LENGTH = 1024;
24
+ /** Encode a UTF-8 string as base64url. */
25
+ export function base64UrlEncode(data) {
26
+ return Buffer.from(data, "utf-8").toString("base64url");
27
+ }
28
+ /** Decode a base64url segment to a UTF-8 string. */
29
+ export function base64UrlDecode(data) {
30
+ return Buffer.from(data, "base64url").toString("utf-8");
31
+ }
32
+ /**
33
+ * Split a JWT into its three segments, rejecting anything that is not a
34
+ * string, is empty, or exceeds {@link MAX_TOKEN_LENGTH}.
35
+ *
36
+ * @returns The three segments, or `null` when the input is not a
37
+ * plausibly-shaped, in-bounds JWT.
38
+ */
39
+ export function splitToken(token) {
40
+ if (typeof token !== "string")
41
+ return null;
42
+ if (token.length === 0 || token.length > MAX_TOKEN_LENGTH)
43
+ return null;
44
+ const parts = token.split(".");
45
+ if (parts.length !== 3)
46
+ return null;
47
+ const [header, body, signature] = parts;
48
+ if (header.length > MAX_HEADER_SEGMENT_LENGTH)
49
+ return null;
50
+ if (header.length === 0 || body.length === 0 || signature.length === 0) {
51
+ return null;
52
+ }
53
+ return [header, body, signature];
54
+ }
55
+ /**
56
+ * Decode a JWT body segment into a plain object.
57
+ *
58
+ * @returns The parsed object, or `null` when the segment is not valid JSON
59
+ * or does not decode to a non-null, non-array object.
60
+ */
61
+ export function decodeJsonSegment(segment) {
62
+ let parsed;
63
+ try {
64
+ parsed = JSON.parse(base64UrlDecode(segment));
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
70
+ return null;
71
+ }
72
+ return parsed;
73
+ }
74
+ //# sourceMappingURL=authToken.encoding.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * In-memory token revocation store.
3
+ *
4
+ * @module authToken/authToken.revocation
5
+ *
6
+ * Good for development and single-process deployments. For production,
7
+ * implement TokenRevocationStore with Redis or a database so revocations
8
+ * are shared across instances.
9
+ */
10
+ import type { TokenRevocationStore } from "../authTypes/authToken.type.js";
11
+ /**
12
+ * Create an in-memory token revocation store.
13
+ *
14
+ * Expired entries are dropped lazily: the key being read is checked on every
15
+ * access (O(1)), and a full sweep runs at most once per `purgeIntervalMs`.
16
+ * The sweep is deliberately *not* driven by a timer — an interval would keep
17
+ * the process alive and leak without a `dispose()`.
18
+ *
19
+ * @param options.purgeIntervalMs - Minimum gap between full sweeps
20
+ * (default: 60000). Set to 0 to sweep on every access.
21
+ */
22
+ export declare function createMemoryTokenRevocationStore(options?: {
23
+ readonly purgeIntervalMs?: number;
24
+ }): TokenRevocationStore;
25
+ //# sourceMappingURL=authToken.revocation.d.ts.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * In-memory token revocation store.
3
+ *
4
+ * @module authToken/authToken.revocation
5
+ *
6
+ * Good for development and single-process deployments. For production,
7
+ * implement TokenRevocationStore with Redis or a database so revocations
8
+ * are shared across instances.
9
+ */
10
+ /** Minimum interval between full sweeps of the revocation map. */
11
+ const DEFAULT_PURGE_INTERVAL_MS = 60_000;
12
+ /**
13
+ * Create an in-memory token revocation store.
14
+ *
15
+ * Expired entries are dropped lazily: the key being read is checked on every
16
+ * access (O(1)), and a full sweep runs at most once per `purgeIntervalMs`.
17
+ * The sweep is deliberately *not* driven by a timer — an interval would keep
18
+ * the process alive and leak without a `dispose()`.
19
+ *
20
+ * @param options.purgeIntervalMs - Minimum gap between full sweeps
21
+ * (default: 60000). Set to 0 to sweep on every access.
22
+ */
23
+ export function createMemoryTokenRevocationStore(options) {
24
+ const revoked = new Map();
25
+ const purgeIntervalMs = options?.purgeIntervalMs ?? DEFAULT_PURGE_INTERVAL_MS;
26
+ let lastPurge = 0;
27
+ /** Full sweep, rate-limited so a large map cannot be walked per request. */
28
+ function maybePurgeExpired() {
29
+ const nowMs = Date.now();
30
+ if (nowMs - lastPurge < purgeIntervalMs)
31
+ return;
32
+ lastPurge = nowMs;
33
+ const now = Math.floor(nowMs / 1000);
34
+ for (const [id, expiresAt] of revoked) {
35
+ if (expiresAt < now) {
36
+ revoked.delete(id);
37
+ }
38
+ }
39
+ }
40
+ /** O(1) expiry check for a single key. */
41
+ function isLive(tokenId) {
42
+ const expiresAt = revoked.get(tokenId);
43
+ if (expiresAt === undefined)
44
+ return false;
45
+ if (expiresAt < Math.floor(Date.now() / 1000)) {
46
+ revoked.delete(tokenId);
47
+ return false;
48
+ }
49
+ return true;
50
+ }
51
+ return {
52
+ async revoke(tokenId, expiresAt) {
53
+ maybePurgeExpired();
54
+ revoked.set(tokenId, expiresAt);
55
+ },
56
+ async isRevoked(tokenId) {
57
+ maybePurgeExpired();
58
+ return isLive(tokenId);
59
+ },
60
+ async revokeIfNotRevoked(tokenId, expiresAt) {
61
+ // The check and the write happen in one synchronous block with no
62
+ // intervening await, so two concurrent callers cannot both win.
63
+ maybePurgeExpired();
64
+ if (isLive(tokenId))
65
+ return false;
66
+ revoked.set(tokenId, expiresAt);
67
+ return true;
68
+ },
69
+ };
70
+ }
71
+ //# sourceMappingURL=authToken.revocation.js.map
@@ -6,12 +6,36 @@
6
6
  * Not exported from the package barrel — used internally by authToken.core.ts.
7
7
  */
8
8
  import type { JwtToken, TokenId, TokenPayload, TokenConfig, TokenVerificationResult } from "../authTypes/authToken.type.js";
9
+ /**
10
+ * Minimum accepted signing-secret length in bytes.
11
+ *
12
+ * HS256 keys should be at least as long as the digest (32 bytes); shorter
13
+ * keys are brute-forceable offline from a single captured token.
14
+ */
15
+ export declare const MIN_SECRET_BYTES = 32;
16
+ /**
17
+ * Validate a {@link TokenConfig}'s signing secrets.
18
+ *
19
+ * Called on every mint and every verification. An empty or short secret
20
+ * yields tokens anyone can forge, and Node accepts a zero-length HMAC key
21
+ * without complaint, so this has to be checked explicitly rather than
22
+ * assumed.
23
+ *
24
+ * @throws {AuthConfigurationError} when either secret is missing, shorter
25
+ * than {@link MIN_SECRET_BYTES}, or when both secrets are identical.
26
+ */
27
+ export declare function assertTokenSecrets(config: TokenConfig): void;
9
28
  /**
10
29
  * Sign a JWT payload with HMAC SHA-256.
11
30
  */
12
31
  export declare function signToken(payload: TokenPayload, secret: string): JwtToken;
13
32
  /**
14
- * Verify a JWT token's signature, expiration, and type.
33
+ * Verify a JWT token's signature, algorithm, expiration, not-before, type,
34
+ * and (when configured) issuer and audience.
35
+ *
36
+ * Never throws for untrusted input: every failure is reported as
37
+ * `{ valid: false, error }`. Oversized tokens are rejected before anything
38
+ * is decoded.
15
39
  */
16
40
  export declare function verifyToken(token: JwtToken, secret: string, expectedType: "access" | "refresh", config: TokenConfig): TokenVerificationResult;
17
41
  /**
@@ -5,10 +5,53 @@
5
5
  *
6
6
  * Not exported from the package barrel — used internally by authToken.core.ts.
7
7
  */
8
- import { createHmac, timingSafeEqual } from "node:crypto";
9
- import { randomBytes } from "node:crypto";
8
+ import { createHmac, timingSafeEqual, randomBytes } from "node:crypto";
9
+ import { AuthConfigurationError } from "../authErrors/authError.base.js";
10
+ import { base64UrlEncode, decodeJsonSegment, splitToken, } from "./authToken.encoding.js";
10
11
  /** HMAC signing algorithm. */
11
12
  const ALGORITHM = "HS256";
13
+ /**
14
+ * Minimum accepted signing-secret length in bytes.
15
+ *
16
+ * HS256 keys should be at least as long as the digest (32 bytes); shorter
17
+ * keys are brute-forceable offline from a single captured token.
18
+ */
19
+ export const MIN_SECRET_BYTES = 32;
20
+ /**
21
+ * Validate a {@link TokenConfig}'s signing secrets.
22
+ *
23
+ * Called on every mint and every verification. An empty or short secret
24
+ * yields tokens anyone can forge, and Node accepts a zero-length HMAC key
25
+ * without complaint, so this has to be checked explicitly rather than
26
+ * assumed.
27
+ *
28
+ * @throws {AuthConfigurationError} when either secret is missing, shorter
29
+ * than {@link MIN_SECRET_BYTES}, or when both secrets are identical.
30
+ */
31
+ export function assertTokenSecrets(config) {
32
+ assertSecret(config?.accessSecret, "accessSecret");
33
+ assertSecret(config?.refreshSecret, "refreshSecret");
34
+ if (config.accessSecret === config.refreshSecret) {
35
+ throw new AuthConfigurationError("TokenConfig.accessSecret and TokenConfig.refreshSecret must differ; " +
36
+ "sharing one secret collapses the separation between access and " +
37
+ "refresh tokens.");
38
+ }
39
+ if (config.clockToleranceSeconds !== undefined &&
40
+ (!Number.isFinite(config.clockToleranceSeconds) ||
41
+ config.clockToleranceSeconds < 0 ||
42
+ config.clockToleranceSeconds > 300)) {
43
+ throw new AuthConfigurationError("TokenConfig.clockToleranceSeconds must be between 0 and 300 seconds.");
44
+ }
45
+ }
46
+ function assertSecret(secret, field) {
47
+ if (typeof secret !== "string" || secret.length === 0) {
48
+ throw new AuthConfigurationError(`TokenConfig.${field} is required and must be a non-empty string.`);
49
+ }
50
+ if (Buffer.byteLength(secret, "utf-8") < MIN_SECRET_BYTES) {
51
+ throw new AuthConfigurationError(`TokenConfig.${field} must be at least ${MIN_SECRET_BYTES} bytes; ` +
52
+ "a shorter HMAC key can be recovered offline from a single token.");
53
+ }
54
+ }
12
55
  /**
13
56
  * Sign a JWT payload with HMAC SHA-256.
14
57
  */
@@ -20,41 +63,95 @@ export function signToken(payload, secret) {
20
63
  return `${signatureInput}.${signature}`;
21
64
  }
22
65
  /**
23
- * Verify a JWT token's signature, expiration, and type.
66
+ * Verify a JWT token's signature, algorithm, expiration, not-before, type,
67
+ * and (when configured) issuer and audience.
68
+ *
69
+ * Never throws for untrusted input: every failure is reported as
70
+ * `{ valid: false, error }`. Oversized tokens are rejected before anything
71
+ * is decoded.
24
72
  */
25
73
  export function verifyToken(token, secret, expectedType, config) {
26
- const parts = token.split(".");
27
- if (parts.length !== 3) {
74
+ const parts = splitToken(token);
75
+ if (!parts) {
28
76
  return { valid: false, error: "Invalid token format" };
29
77
  }
30
78
  const [headerB64, bodyB64, signature] = parts;
79
+ const header = decodeJsonSegment(headerB64);
80
+ if (!header) {
81
+ return { valid: false, error: "Invalid header" };
82
+ }
83
+ if (header["alg"] !== ALGORITHM) {
84
+ return { valid: false, error: "Unsupported algorithm" };
85
+ }
31
86
  const signatureInput = `${headerB64}.${bodyB64}`;
32
87
  const expectedSignature = hmacSha256(signatureInput, secret);
33
- const sigBuffer = Buffer.from(signature ?? "", "hex");
34
- const expectedBuffer = Buffer.from(expectedSignature, "hex");
88
+ const sigBuffer = Buffer.from(signature, "base64url");
89
+ const expectedBuffer = Buffer.from(expectedSignature, "base64url");
35
90
  if (sigBuffer.length !== expectedBuffer.length ||
36
91
  !timingSafeEqual(sigBuffer, expectedBuffer)) {
37
92
  return { valid: false, error: "Invalid signature" };
38
93
  }
39
- let payload;
40
- try {
41
- payload = JSON.parse(base64UrlDecode(bodyB64));
94
+ const decoded = decodeJsonSegment(bodyB64);
95
+ if (!decoded) {
96
+ return { valid: false, error: "Invalid payload" };
42
97
  }
43
- catch {
98
+ if (!isTokenPayload(decoded)) {
44
99
  return { valid: false, error: "Invalid payload" };
45
100
  }
101
+ const payload = decoded;
46
102
  const now = Math.floor(Date.now() / 1000);
47
- if (payload.exp < now) {
103
+ const skew = config.clockToleranceSeconds ?? 0;
104
+ if (payload.exp + skew < now) {
48
105
  return { valid: false, error: "Token expired" };
49
106
  }
107
+ if (payload.iat - skew > now) {
108
+ return { valid: false, error: "Token issued in the future" };
109
+ }
110
+ const nbf = payload["nbf"];
111
+ if (typeof nbf === "number" && nbf - skew > now) {
112
+ return { valid: false, error: "Token not yet valid" };
113
+ }
50
114
  if (payload.typ !== expectedType) {
51
115
  return { valid: false, error: `Expected ${expectedType} token` };
52
116
  }
53
- if (config.issuer && payload.iss !== config.issuer) {
117
+ if (config.issuer && payload["iss"] !== config.issuer) {
54
118
  return { valid: false, error: "Invalid issuer" };
55
119
  }
120
+ if (config.audience && payload["aud"] !== config.audience) {
121
+ return { valid: false, error: "Invalid audience" };
122
+ }
56
123
  return { valid: true, payload };
57
124
  }
125
+ /**
126
+ * Narrow a decoded JWT body to a {@link TokenPayload}.
127
+ *
128
+ * Replaces the `as TokenPayload` cast that previously let a `null`, an
129
+ * array, or a payload with a missing/mistyped `sub`, `jti`, `exp`, `iat` or
130
+ * `typ` flow into the caller.
131
+ */
132
+ function isTokenPayload(value) {
133
+ if (typeof value["sub"] !== "string" || value["sub"].length === 0) {
134
+ return false;
135
+ }
136
+ if (typeof value["jti"] !== "string" || value["jti"].length === 0) {
137
+ return false;
138
+ }
139
+ if (value["typ"] !== "access" && value["typ"] !== "refresh")
140
+ return false;
141
+ if (!isFiniteNumber(value["exp"]))
142
+ return false;
143
+ if (!isFiniteNumber(value["iat"]))
144
+ return false;
145
+ const roles = value["roles"];
146
+ if (roles !== undefined &&
147
+ (!Array.isArray(roles) || roles.some((r) => typeof r !== "string"))) {
148
+ return false;
149
+ }
150
+ return true;
151
+ }
152
+ function isFiniteNumber(value) {
153
+ return typeof value === "number" && Number.isFinite(value);
154
+ }
58
155
  /**
59
156
  * Generate a random token ID.
60
157
  */
@@ -63,25 +160,6 @@ export function generateTokenId() {
63
160
  }
64
161
  // ─── Internal helpers ─────────────────────────────────────────────────────
65
162
  function hmacSha256(data, secret) {
66
- return createHmac("sha256", secret).update(data).digest("hex");
67
- }
68
- function base64UrlEncode(data) {
69
- const encoded = Buffer.from(data).toString("base64");
70
- let result = "";
71
- for (let i = 0; i < encoded.length; i++) {
72
- const char = encoded[i];
73
- if (char === "+")
74
- result += "-";
75
- else if (char === "/")
76
- result += "_";
77
- else if (char !== "=")
78
- result += char;
79
- }
80
- return result;
81
- }
82
- function base64UrlDecode(data) {
83
- const padded = data.replace(/-/g, "+").replace(/_/g, "/");
84
- const padLength = (4 - (padded.length % 4)) % 4;
85
- return Buffer.from(padded + "=".repeat(padLength), "base64").toString("utf-8");
163
+ return createHmac("sha256", secret).update(data).digest("base64url");
86
164
  }
87
165
  //# sourceMappingURL=authToken.signing.js.map
@@ -4,4 +4,5 @@
4
4
  * @module authToken
5
5
  */
6
6
  export { createTokenPair, verifyAccessToken, verifyRefreshToken, refreshAccessToken, } from "./authToken.core.js";
7
+ export { createMemoryTokenRevocationStore } from "./authToken.revocation.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,5 @@
4
4
  * @module authToken
5
5
  */
6
6
  export { createTokenPair, verifyAccessToken, verifyRefreshToken, refreshAccessToken, } from "./authToken.core.js";
7
+ export { createMemoryTokenRevocationStore } from "./authToken.revocation.js";
7
8
  //# sourceMappingURL=index.js.map
@@ -1,18 +1,28 @@
1
1
  /**
2
2
  * @zudojs/auth — JWT Namespace
3
3
  *
4
- * Convenience namespace for all JWT-related utilities.
4
+ * Convenience namespace for the JWT primitives: minting, verification, the
5
+ * non-rotating refresh helper, the in-memory revocation store, and the
6
+ * unverified header/claim parsers.
7
+ *
8
+ * Note what is *not* here: `createAuthService()`. `jwt.refreshAccessToken`
9
+ * performs no rotation and consults no revocation store — pair it with
10
+ * `jwt.createMemoryTokenRevocationStore` yourself, or use
11
+ * `createAuthService().refresh()`, which rotates, revokes, re-loads the user
12
+ * and validates the session.
5
13
  */
6
14
  import { createTokenPair, verifyAccessToken, verifyRefreshToken, refreshAccessToken } from "./authToken.core.js";
15
+ import { createMemoryTokenRevocationStore } from "./authToken.revocation.js";
7
16
  import { parseBearerToken, isTokenExpired, extractUserId } from "../authUtils/authUtils.helper.js";
8
17
  export type { JwtToken, TokenPair, TokenConfig, TokenVerificationResult, } from "../authTypes/authToken.type.js";
9
18
  export declare const jwt: {
10
- createTokenPair: typeof createTokenPair;
11
- verifyAccessToken: typeof verifyAccessToken;
12
- verifyRefreshToken: typeof verifyRefreshToken;
13
- refreshAccessToken: typeof refreshAccessToken;
14
- parseBearerToken: typeof parseBearerToken;
15
- isTokenExpired: typeof isTokenExpired;
16
- extractUserId: typeof extractUserId;
19
+ readonly createTokenPair: typeof createTokenPair;
20
+ readonly verifyAccessToken: typeof verifyAccessToken;
21
+ readonly verifyRefreshToken: typeof verifyRefreshToken;
22
+ readonly refreshAccessToken: typeof refreshAccessToken;
23
+ readonly createMemoryTokenRevocationStore: typeof createMemoryTokenRevocationStore;
24
+ readonly parseBearerToken: typeof parseBearerToken;
25
+ readonly isTokenExpired: typeof isTokenExpired;
26
+ readonly extractUserId: typeof extractUserId;
17
27
  };
18
28
  //# sourceMappingURL=jwt.namespace.d.ts.map
@@ -1,15 +1,25 @@
1
1
  /**
2
2
  * @zudojs/auth — JWT Namespace
3
3
  *
4
- * Convenience namespace for all JWT-related utilities.
4
+ * Convenience namespace for the JWT primitives: minting, verification, the
5
+ * non-rotating refresh helper, the in-memory revocation store, and the
6
+ * unverified header/claim parsers.
7
+ *
8
+ * Note what is *not* here: `createAuthService()`. `jwt.refreshAccessToken`
9
+ * performs no rotation and consults no revocation store — pair it with
10
+ * `jwt.createMemoryTokenRevocationStore` yourself, or use
11
+ * `createAuthService().refresh()`, which rotates, revokes, re-loads the user
12
+ * and validates the session.
5
13
  */
6
14
  import { createTokenPair, verifyAccessToken, verifyRefreshToken, refreshAccessToken, } from "./authToken.core.js";
15
+ import { createMemoryTokenRevocationStore } from "./authToken.revocation.js";
7
16
  import { parseBearerToken, isTokenExpired, extractUserId, } from "../authUtils/authUtils.helper.js";
8
17
  export const jwt = {
9
18
  createTokenPair,
10
19
  verifyAccessToken,
11
20
  verifyRefreshToken,
12
21
  refreshAccessToken,
22
+ createMemoryTokenRevocationStore,
13
23
  parseBearerToken,
14
24
  isTokenExpired,
15
25
  extractUserId,
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Login attempt tracking types — brute-force lockout and rate limiting.
3
+ *
4
+ * @module authTypes/authAttempt
5
+ */
6
+ /**
7
+ * Current attempt state for one login identifier.
8
+ */
9
+ export interface LoginAttemptRecord {
10
+ /** Consecutive failed authentications since the last successful login. */
11
+ readonly failures: number;
12
+ /** Attempts counted inside the current rate-limit window. */
13
+ readonly attempts: number;
14
+ /** Unix milliseconds until which the identifier is locked, if locked. */
15
+ readonly lockedUntil?: number;
16
+ }
17
+ /**
18
+ * Store backing failed-attempt lockout and login rate limiting.
19
+ *
20
+ * Keys are the *submitted* identifier, not a resolved user id, so unknown
21
+ * and known accounts are throttled identically and the endpoint stays free
22
+ * of an existence oracle. The in-memory implementation
23
+ * (`createMemoryLoginAttemptStore`) is per-process; back this with Redis to
24
+ * make limits hold across instances.
25
+ */
26
+ export interface LoginAttemptStore {
27
+ /** Read the current state without recording anything. */
28
+ get(identifier: string): Promise<LoginAttemptRecord>;
29
+ /** Count an attempt (before credentials are checked). */
30
+ recordAttempt(identifier: string): Promise<LoginAttemptRecord>;
31
+ /** Count a failed authentication. */
32
+ recordFailure(identifier: string): Promise<LoginAttemptRecord>;
33
+ /** Lock an identifier until `until` (Unix milliseconds). */
34
+ lock(identifier: string, until: number): Promise<void>;
35
+ /** Clear all state for an identifier (successful login). */
36
+ reset(identifier: string): Promise<void>;
37
+ }
38
+ /**
39
+ * Brute-force protection settings for `createAuthService()`.
40
+ */
41
+ export interface LoginThrottleConfig {
42
+ /** Where attempt counters live. */
43
+ readonly store: LoginAttemptStore;
44
+ /**
45
+ * Consecutive failures that trigger a lockout (default: 5).
46
+ * `login()` then throws `AccountLockedError` until the lockout lapses.
47
+ */
48
+ readonly maxFailedAttempts?: number;
49
+ /** Lockout duration in seconds (default: 900). */
50
+ readonly lockoutSeconds?: number;
51
+ /**
52
+ * Attempts allowed per identifier inside the store's rate-limit window
53
+ * (default: 20). Exceeding it throws `AuthRateLimitError`. This bounds the
54
+ * scrypt work an attacker can force the server to perform.
55
+ */
56
+ readonly maxAttemptsPerWindow?: number;
57
+ /**
58
+ * Window length in seconds reported on `AuthRateLimitError.retryAfterSeconds`
59
+ * (default: 60). The store owns the actual window.
60
+ */
61
+ readonly windowSeconds?: number;
62
+ }
63
+ //# sourceMappingURL=authAttempt.type.d.ts.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Login attempt tracking types — brute-force lockout and rate limiting.
3
+ *
4
+ * @module authTypes/authAttempt
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=authAttempt.type.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Credential shapes accepted by the authentication entry points.
3
+ *
4
+ * @module authTypes/authCredentials
5
+ *
6
+ * These are plain contracts for the credentials a caller collects from a
7
+ * request. They carry no behaviour; `createAuthService()` takes the
8
+ * identifier and password directly.
9
+ */
10
+ /**
11
+ * Password credentials: an identifier (username or email) and a password.
12
+ */
13
+ export interface PasswordCredentials {
14
+ readonly identifier: string;
15
+ readonly password: string;
16
+ }
17
+ /**
18
+ * API key credentials.
19
+ *
20
+ * The type exists for callers that model machine-to-machine auth; this
21
+ * package implements no API-key verification.
22
+ */
23
+ export interface ApiKeyCredentials {
24
+ readonly apiKey: string;
25
+ }
26
+ //# sourceMappingURL=authCredentials.type.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Credential shapes accepted by the authentication entry points.
3
+ *
4
+ * @module authTypes/authCredentials
5
+ *
6
+ * These are plain contracts for the credentials a caller collects from a
7
+ * request. They carry no behaviour; `createAuthService()` takes the
8
+ * identifier and password directly.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=authCredentials.type.js.map
@@ -6,6 +6,18 @@
6
6
  import type { UserId } from "../authTypes/authUser.type.js";
7
7
  import type { SessionId } from "@zudojs/constants";
8
8
  export type { SessionId } from "@zudojs/constants";
9
+ /**
10
+ * Brands a plain string as a {@link SessionId}.
11
+ *
12
+ * `SessionId` is a branded type with no public constructor, so callers holding
13
+ * an id from a session store, a cookie or a request parameter have no way to
14
+ * produce one without a cast. This is that constructor.
15
+ *
16
+ * @param value - Non-empty session identifier.
17
+ * @returns The same string, typed as a `SessionId`.
18
+ * @throws {TypeError} If `value` is not a non-empty string.
19
+ */
20
+ export declare function toSessionId(value: string): SessionId;
9
21
  /**
10
22
  * Server-side session representation.
11
23
  */
@@ -22,10 +34,19 @@ export interface AuthSession {
22
34
  readonly createdAt: Date;
23
35
  /** Last activity time */
24
36
  readonly lastActivityAt: Date;
25
- /** Session expiration time */
37
+ /**
38
+ * Session expiration time.
39
+ *
40
+ * With sliding expiration this moves forward on every `touch()`, but never
41
+ * past `createdAt + absoluteTtlSeconds` when an absolute lifetime was
42
+ * requested.
43
+ */
26
44
  readonly expiresAt: Date;
27
- /** Whether the session is active */
28
- readonly active: boolean;
45
+ /**
46
+ * Hard deadline for the session, when one was requested at creation.
47
+ * `touch()` never extends `expiresAt` beyond this.
48
+ */
49
+ readonly absoluteExpiresAt?: Date;
29
50
  /** Session metadata */
30
51
  readonly metadata?: Record<string, unknown>;
31
52
  }
@@ -36,7 +57,14 @@ export interface CreateSessionOptions {
36
57
  readonly userId: UserId;
37
58
  readonly userAgent?: string;
38
59
  readonly ip?: string;
60
+ /** Idle timeout in seconds; refreshed by `touch()` (default: 86400). */
39
61
  readonly ttlSeconds?: number;
62
+ /**
63
+ * Absolute maximum session lifetime in seconds, measured from creation.
64
+ * Without it a session that is touched once per idle window never expires,
65
+ * so a stolen session id is valid indefinitely.
66
+ */
67
+ readonly absoluteTtlSeconds?: number;
40
68
  readonly metadata?: Record<string, unknown>;
41
69
  }
42
70
  /**
@@ -3,5 +3,21 @@
3
3
  *
4
4
  * @module authSession/authSession
5
5
  */
6
- export {};
6
+ /**
7
+ * Brands a plain string as a {@link SessionId}.
8
+ *
9
+ * `SessionId` is a branded type with no public constructor, so callers holding
10
+ * an id from a session store, a cookie or a request parameter have no way to
11
+ * produce one without a cast. This is that constructor.
12
+ *
13
+ * @param value - Non-empty session identifier.
14
+ * @returns The same string, typed as a `SessionId`.
15
+ * @throws {TypeError} If `value` is not a non-empty string.
16
+ */
17
+ export function toSessionId(value) {
18
+ if (typeof value !== "string" || value.length === 0) {
19
+ throw new TypeError("toSessionId: value must be a non-empty string.");
20
+ }
21
+ return value;
22
+ }
7
23
  //# sourceMappingURL=authSession.type.js.map