@revealui/auth 0.2.0 → 0.3.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 (87) hide show
  1. package/README.md +58 -34
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/react/index.d.ts +4 -0
  4. package/dist/react/index.d.ts.map +1 -1
  5. package/dist/react/index.js +2 -0
  6. package/dist/react/useMFA.d.ts +83 -0
  7. package/dist/react/useMFA.d.ts.map +1 -0
  8. package/dist/react/useMFA.js +182 -0
  9. package/dist/react/usePasskey.d.ts +88 -0
  10. package/dist/react/usePasskey.d.ts.map +1 -0
  11. package/dist/react/usePasskey.js +203 -0
  12. package/dist/react/useSession.d.ts.map +1 -1
  13. package/dist/react/useSession.js +16 -5
  14. package/dist/react/useSignIn.d.ts +9 -3
  15. package/dist/react/useSignIn.d.ts.map +1 -1
  16. package/dist/react/useSignIn.js +32 -10
  17. package/dist/react/useSignOut.d.ts.map +1 -1
  18. package/dist/react/useSignUp.d.ts +1 -0
  19. package/dist/react/useSignUp.d.ts.map +1 -1
  20. package/dist/react/useSignUp.js +25 -9
  21. package/dist/server/auth.d.ts +2 -0
  22. package/dist/server/auth.d.ts.map +1 -1
  23. package/dist/server/auth.js +93 -5
  24. package/dist/server/brute-force.d.ts +10 -1
  25. package/dist/server/brute-force.d.ts.map +1 -1
  26. package/dist/server/brute-force.js +46 -23
  27. package/dist/server/errors.d.ts +4 -0
  28. package/dist/server/errors.d.ts.map +1 -1
  29. package/dist/server/errors.js +8 -0
  30. package/dist/server/index.d.ts +17 -6
  31. package/dist/server/index.d.ts.map +1 -1
  32. package/dist/server/index.js +12 -5
  33. package/dist/server/magic-link.d.ts +52 -0
  34. package/dist/server/magic-link.d.ts.map +1 -0
  35. package/dist/server/magic-link.js +111 -0
  36. package/dist/server/mfa.d.ts +87 -0
  37. package/dist/server/mfa.d.ts.map +1 -0
  38. package/dist/server/mfa.js +263 -0
  39. package/dist/server/oauth.d.ts +86 -0
  40. package/dist/server/oauth.d.ts.map +1 -0
  41. package/dist/server/oauth.js +355 -0
  42. package/dist/server/passkey.d.ts +132 -0
  43. package/dist/server/passkey.d.ts.map +1 -0
  44. package/dist/server/passkey.js +257 -0
  45. package/dist/server/password-reset.d.ts +32 -6
  46. package/dist/server/password-reset.d.ts.map +1 -1
  47. package/dist/server/password-reset.js +116 -47
  48. package/dist/server/password-validation.d.ts.map +1 -1
  49. package/dist/server/providers/github.d.ts +14 -0
  50. package/dist/server/providers/github.d.ts.map +1 -0
  51. package/dist/server/providers/github.js +89 -0
  52. package/dist/server/providers/google.d.ts +11 -0
  53. package/dist/server/providers/google.d.ts.map +1 -0
  54. package/dist/server/providers/google.js +69 -0
  55. package/dist/server/providers/vercel.d.ts +11 -0
  56. package/dist/server/providers/vercel.d.ts.map +1 -0
  57. package/dist/server/providers/vercel.js +63 -0
  58. package/dist/server/rate-limit.d.ts +10 -1
  59. package/dist/server/rate-limit.d.ts.map +1 -1
  60. package/dist/server/rate-limit.js +61 -43
  61. package/dist/server/session.d.ts +48 -1
  62. package/dist/server/session.d.ts.map +1 -1
  63. package/dist/server/session.js +126 -7
  64. package/dist/server/signed-cookie.d.ts +32 -0
  65. package/dist/server/signed-cookie.d.ts.map +1 -0
  66. package/dist/server/signed-cookie.js +67 -0
  67. package/dist/server/storage/database.d.ts +10 -1
  68. package/dist/server/storage/database.d.ts.map +1 -1
  69. package/dist/server/storage/database.js +43 -5
  70. package/dist/server/storage/in-memory.d.ts +4 -0
  71. package/dist/server/storage/in-memory.d.ts.map +1 -1
  72. package/dist/server/storage/in-memory.js +16 -6
  73. package/dist/server/storage/index.d.ts +11 -3
  74. package/dist/server/storage/index.d.ts.map +1 -1
  75. package/dist/server/storage/index.js +18 -4
  76. package/dist/server/storage/interface.d.ts +11 -1
  77. package/dist/server/storage/interface.d.ts.map +1 -1
  78. package/dist/server/storage/interface.js +1 -1
  79. package/dist/types.d.ts +23 -8
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/types.js +2 -2
  82. package/dist/utils/database.d.ts.map +1 -1
  83. package/dist/utils/database.js +12 -2
  84. package/dist/utils/token.d.ts +9 -1
  85. package/dist/utils/token.d.ts.map +1 -1
  86. package/dist/utils/token.js +9 -1
  87. package/package.json +26 -8
@@ -0,0 +1,89 @@
1
+ /**
2
+ * GitHub OAuth Provider
3
+ *
4
+ * Uses native fetch — no additional npm dependencies.
5
+ * Scopes: read:user user:email
6
+ *
7
+ * Note: GitHub may return null email if user has set it private.
8
+ * In that case we fetch from /user/emails and pick the primary verified one.
9
+ */
10
+ export function buildAuthUrl(clientId, redirectUri, state) {
11
+ const url = new URL('https://github.com/login/oauth/authorize');
12
+ url.searchParams.set('client_id', clientId);
13
+ url.searchParams.set('redirect_uri', redirectUri);
14
+ url.searchParams.set('scope', 'read:user user:email');
15
+ url.searchParams.set('state', state);
16
+ return url.toString();
17
+ }
18
+ export async function exchangeCode(code, redirectUri) {
19
+ const response = await fetch('https://github.com/login/oauth/access_token', {
20
+ method: 'POST',
21
+ headers: {
22
+ 'Content-Type': 'application/x-www-form-urlencoded',
23
+ // biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
24
+ Accept: 'application/json',
25
+ },
26
+ body: new URLSearchParams({
27
+ code,
28
+ client_id: process.env.GITHUB_CLIENT_ID ?? '',
29
+ client_secret: process.env.GITHUB_CLIENT_SECRET ?? '',
30
+ redirect_uri: redirectUri,
31
+ }),
32
+ });
33
+ if (!response.ok) {
34
+ let detail = '';
35
+ try {
36
+ const err = (await response.json());
37
+ detail = err.error_description ?? err.error ?? '';
38
+ }
39
+ catch {
40
+ // Response body not JSON — use status only
41
+ }
42
+ throw new Error(`GitHub token exchange failed: ${response.status}${detail ? ` — ${detail}` : ''}`);
43
+ }
44
+ const data = (await response.json());
45
+ if (data.error) {
46
+ throw new Error(`GitHub token exchange error: ${data.error}`);
47
+ }
48
+ if (!data.access_token || typeof data.access_token !== 'string') {
49
+ throw new Error('GitHub token exchange returned no access_token');
50
+ }
51
+ return data.access_token;
52
+ }
53
+ export async function fetchUser(accessToken) {
54
+ const headers = {
55
+ // biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
56
+ Authorization: `Bearer ${accessToken}`,
57
+ // biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
58
+ Accept: 'application/vnd.github+json',
59
+ };
60
+ const userResponse = await fetch('https://api.github.com/user', { headers });
61
+ if (!userResponse.ok) {
62
+ let detail = '';
63
+ try {
64
+ const err = (await userResponse.json());
65
+ detail = err.message ?? '';
66
+ }
67
+ catch {
68
+ // Response body not JSON
69
+ }
70
+ throw new Error(`GitHub user fetch failed: ${userResponse.status}${detail ? ` — ${detail}` : ''}`);
71
+ }
72
+ const user = (await userResponse.json());
73
+ let email = user.email ?? null;
74
+ // Fetch emails if not public
75
+ if (!email) {
76
+ const emailsResponse = await fetch('https://api.github.com/user/emails', { headers });
77
+ if (emailsResponse.ok) {
78
+ const emails = (await emailsResponse.json());
79
+ const primary = emails.find((e) => e.primary && e.verified);
80
+ email = primary?.email ?? null;
81
+ }
82
+ }
83
+ return {
84
+ id: String(user.id),
85
+ email,
86
+ name: user.name ?? user.login,
87
+ avatarUrl: user.avatar_url ?? null,
88
+ };
89
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Google OAuth 2.0 Provider
3
+ *
4
+ * Uses native fetch — no additional npm dependencies.
5
+ * Scopes: openid email profile
6
+ */
7
+ import type { ProviderUser } from '../oauth.js';
8
+ export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string): string;
9
+ export declare function exchangeCode(code: string, redirectUri: string): Promise<string>;
10
+ export declare function fetchUser(accessToken: string): Promise<ProviderUser>;
11
+ //# sourceMappingURL=google.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../../src/server/providers/google.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CASzF;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA+BrF;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAgC1E"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Google OAuth 2.0 Provider
3
+ *
4
+ * Uses native fetch — no additional npm dependencies.
5
+ * Scopes: openid email profile
6
+ */
7
+ export function buildAuthUrl(clientId, redirectUri, state) {
8
+ const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
9
+ url.searchParams.set('client_id', clientId);
10
+ url.searchParams.set('redirect_uri', redirectUri);
11
+ url.searchParams.set('response_type', 'code');
12
+ url.searchParams.set('scope', 'openid email profile');
13
+ url.searchParams.set('state', state);
14
+ url.searchParams.set('access_type', 'online');
15
+ return url.toString();
16
+ }
17
+ export async function exchangeCode(code, redirectUri) {
18
+ const response = await fetch('https://oauth2.googleapis.com/token', {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
21
+ body: new URLSearchParams({
22
+ code,
23
+ client_id: process.env.GOOGLE_CLIENT_ID ?? '',
24
+ client_secret: process.env.GOOGLE_CLIENT_SECRET ?? '',
25
+ redirect_uri: redirectUri,
26
+ grant_type: 'authorization_code',
27
+ }),
28
+ });
29
+ if (!response.ok) {
30
+ let detail = '';
31
+ try {
32
+ const err = (await response.json());
33
+ detail = err.error_description ?? err.error ?? '';
34
+ }
35
+ catch {
36
+ // Response body not JSON — use status only
37
+ }
38
+ throw new Error(`Google token exchange failed: ${response.status}${detail ? ` — ${detail}` : ''}`);
39
+ }
40
+ const data = (await response.json());
41
+ if (!data.access_token || typeof data.access_token !== 'string') {
42
+ throw new Error('Google token exchange returned no access_token');
43
+ }
44
+ return data.access_token;
45
+ }
46
+ export async function fetchUser(accessToken) {
47
+ const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
48
+ // biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
49
+ headers: { Authorization: `Bearer ${accessToken}` },
50
+ });
51
+ if (!response.ok) {
52
+ let detail = '';
53
+ try {
54
+ const err = (await response.json());
55
+ detail = err.error?.message ?? '';
56
+ }
57
+ catch {
58
+ // Response body not JSON
59
+ }
60
+ throw new Error(`Google userinfo fetch failed: ${response.status}${detail ? ` — ${detail}` : ''}`);
61
+ }
62
+ const data = (await response.json());
63
+ return {
64
+ id: data.sub,
65
+ email: data.email ?? null,
66
+ name: data.name ?? 'Google User',
67
+ avatarUrl: data.picture ?? null,
68
+ };
69
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Vercel OAuth Provider
3
+ *
4
+ * Uses native fetch — no additional npm dependencies.
5
+ * No scopes required — Vercel uses full access by default.
6
+ */
7
+ import type { ProviderUser } from '../oauth.js';
8
+ export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string): string;
9
+ export declare function exchangeCode(code: string, redirectUri: string): Promise<string>;
10
+ export declare function fetchUser(accessToken: string): Promise<ProviderUser>;
11
+ //# sourceMappingURL=vercel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../../src/server/providers/vercel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMzF;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrF;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAkC1E"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Vercel OAuth Provider
3
+ *
4
+ * Uses native fetch — no additional npm dependencies.
5
+ * No scopes required — Vercel uses full access by default.
6
+ */
7
+ export function buildAuthUrl(clientId, redirectUri, state) {
8
+ const url = new URL('https://vercel.com/oauth/authorize');
9
+ url.searchParams.set('client_id', clientId);
10
+ url.searchParams.set('redirect_uri', redirectUri);
11
+ url.searchParams.set('state', state);
12
+ return url.toString();
13
+ }
14
+ export async function exchangeCode(code, redirectUri) {
15
+ const response = await fetch('https://api.vercel.com/v2/oauth/access_token', {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
18
+ body: new URLSearchParams({
19
+ code,
20
+ client_id: process.env.VERCEL_CLIENT_ID ?? '',
21
+ client_secret: process.env.VERCEL_CLIENT_SECRET ?? '',
22
+ redirect_uri: redirectUri,
23
+ }),
24
+ });
25
+ if (!response.ok) {
26
+ let detail = '';
27
+ try {
28
+ const err = (await response.json());
29
+ detail = err.error_description ?? err.error ?? '';
30
+ }
31
+ catch {
32
+ // Response body not JSON — use status only
33
+ }
34
+ throw new Error(`Vercel token exchange failed: ${response.status}${detail ? ` — ${detail}` : ''}`);
35
+ }
36
+ const data = (await response.json());
37
+ return data.access_token;
38
+ }
39
+ export async function fetchUser(accessToken) {
40
+ const response = await fetch('https://api.vercel.com/v2/user', {
41
+ // biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
42
+ headers: { Authorization: `Bearer ${accessToken}` },
43
+ });
44
+ if (!response.ok) {
45
+ let detail = '';
46
+ try {
47
+ const err = (await response.json());
48
+ detail = err.error?.message ?? '';
49
+ }
50
+ catch {
51
+ // Response body not JSON
52
+ }
53
+ throw new Error(`Vercel user fetch failed: ${response.status}${detail ? ` — ${detail}` : ''}`);
54
+ }
55
+ const data = (await response.json());
56
+ const u = data.user;
57
+ return {
58
+ id: u.id,
59
+ email: u.email,
60
+ name: u.name ?? u.username ?? 'Vercel User',
61
+ avatarUrl: u.avatar ? `https://avatar.vercel.sh/${u.avatar}` : null,
62
+ };
63
+ }
@@ -2,7 +2,7 @@
2
2
  * Rate Limiting Utilities
3
3
  *
4
4
  * Rate limiting for authentication endpoints using storage abstraction.
5
- * Supports in-memory (dev), Redis (production), or database (fallback).
5
+ * Supports in-memory (dev) or database (production) backends.
6
6
  */
7
7
  /**
8
8
  * Rate limit configuration
@@ -12,6 +12,15 @@ export interface RateLimitConfig {
12
12
  windowMs: number;
13
13
  blockDurationMs?: number;
14
14
  }
15
+ /**
16
+ * Override default rate limit configuration globally.
17
+ * Per-call config parameters still take precedence.
18
+ */
19
+ export declare function configureRateLimit(overrides: Partial<RateLimitConfig>): void;
20
+ /**
21
+ * Reset rate limit configuration to defaults (for testing).
22
+ */
23
+ export declare function resetRateLimitConfig(): void;
15
24
  /**
16
25
  * Checks if an action should be rate limited
17
26
  *
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/server/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAqCD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,eAAgC,GACvC,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAuDnE;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI/D;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,eAAgC,GACvC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAqBhE"}
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/server/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAUD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAE5E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AA+BD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,eAA8B,GACrC,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA4DnE;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI/D;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,eAA8B,GACrC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAqBhE"}
@@ -2,7 +2,7 @@
2
2
  * Rate Limiting Utilities
3
3
  *
4
4
  * Rate limiting for authentication endpoints using storage abstraction.
5
- * Supports in-memory (dev), Redis (production), or database (fallback).
5
+ * Supports in-memory (dev) or database (production) backends.
6
6
  */
7
7
  import { getStorage } from './storage/index.js';
8
8
  const DEFAULT_CONFIG = {
@@ -10,6 +10,20 @@ const DEFAULT_CONFIG = {
10
10
  windowMs: 15 * 60 * 1000, // 15 minutes
11
11
  blockDurationMs: 30 * 60 * 1000, // 30 minutes block after max attempts
12
12
  };
13
+ let globalConfig = { ...DEFAULT_CONFIG };
14
+ /**
15
+ * Override default rate limit configuration globally.
16
+ * Per-call config parameters still take precedence.
17
+ */
18
+ export function configureRateLimit(overrides) {
19
+ globalConfig = { ...DEFAULT_CONFIG, ...overrides };
20
+ }
21
+ /**
22
+ * Reset rate limit configuration to defaults (for testing).
23
+ */
24
+ export function resetRateLimitConfig() {
25
+ globalConfig = { ...DEFAULT_CONFIG };
26
+ }
13
27
  /**
14
28
  * Serialize rate limit entry to string
15
29
  */
@@ -43,54 +57,58 @@ function getStorageKey(key) {
43
57
  * @param config - Rate limit configuration
44
58
  * @returns Rate limit result
45
59
  */
46
- export async function checkRateLimit(key, config = DEFAULT_CONFIG) {
60
+ export async function checkRateLimit(key, config = globalConfig) {
47
61
  const storage = getStorage();
48
62
  const storageKey = getStorageKey(key);
49
63
  const now = Date.now();
50
- // Get existing entry
51
- const entryData = await storage.get(storageKey);
52
- let entry = deserializeEntry(entryData);
53
- // Clean up expired entries
54
- if (entry && entry.resetAt < now) {
55
- await storage.del(storageKey);
56
- entry = null;
57
- }
58
- // Get or create entry
59
- const currentEntry = entry || {
60
- count: 0,
61
- resetAt: now + config.windowMs,
62
- };
63
- // Check if blocked
64
- if (config.blockDurationMs && currentEntry.count >= config.maxAttempts) {
65
- const blockUntil = currentEntry.resetAt + (config.blockDurationMs - config.windowMs);
66
- if (now < blockUntil) {
67
- return {
68
- allowed: false,
69
- remaining: 0,
70
- resetAt: blockUntil,
71
- };
64
+ // Use atomicUpdate to avoid the read-modify-write race condition.
65
+ // The result is captured via closure since atomicUpdate returns void.
66
+ let result;
67
+ const updater = (entryData) => {
68
+ let entry = deserializeEntry(entryData);
69
+ // Clean up expired entries
70
+ if (entry && entry.resetAt < now) {
71
+ entry = null;
72
72
  }
73
- // Block expired, reset
74
- currentEntry.count = 0;
75
- currentEntry.resetAt = now + config.windowMs;
76
- }
77
- // Check if within window
78
- if (currentEntry.count >= config.maxAttempts) {
79
- return {
80
- allowed: false,
81
- remaining: 0,
73
+ // Get or create entry
74
+ const currentEntry = entry || {
75
+ count: 0,
76
+ resetAt: now + config.windowMs,
77
+ };
78
+ // Check if blocked
79
+ if (config.blockDurationMs && currentEntry.count >= config.maxAttempts) {
80
+ const blockUntil = now + config.blockDurationMs;
81
+ const blockTtlSeconds = Math.ceil(config.blockDurationMs / 1000);
82
+ result = { allowed: false, remaining: 0, resetAt: blockUntil };
83
+ return { value: serializeEntry(currentEntry), ttlSeconds: blockTtlSeconds };
84
+ }
85
+ // Check if within window
86
+ if (currentEntry.count >= config.maxAttempts) {
87
+ const ttlSeconds = Math.ceil((currentEntry.resetAt - now) / 1000);
88
+ result = { allowed: false, remaining: 0, resetAt: currentEntry.resetAt };
89
+ return { value: serializeEntry(currentEntry), ttlSeconds };
90
+ }
91
+ // Increment and update
92
+ currentEntry.count++;
93
+ const ttlSeconds = Math.ceil((currentEntry.resetAt - now) / 1000);
94
+ result = {
95
+ allowed: true,
96
+ remaining: config.maxAttempts - currentEntry.count,
82
97
  resetAt: currentEntry.resetAt,
83
98
  };
84
- }
85
- // Increment and update
86
- currentEntry.count++;
87
- const ttlSeconds = Math.ceil((currentEntry.resetAt - now) / 1000);
88
- await storage.set(storageKey, serializeEntry(currentEntry), ttlSeconds);
89
- return {
90
- allowed: true,
91
- remaining: config.maxAttempts - currentEntry.count,
92
- resetAt: currentEntry.resetAt,
99
+ return { value: serializeEntry(currentEntry), ttlSeconds };
93
100
  };
101
+ if (storage.atomicUpdate) {
102
+ await storage.atomicUpdate(storageKey, updater);
103
+ }
104
+ else {
105
+ // Fallback for storage backends without atomicUpdate
106
+ const existing = await storage.get(storageKey);
107
+ const { value, ttlSeconds } = updater(existing);
108
+ await storage.set(storageKey, value, ttlSeconds);
109
+ }
110
+ // biome-ignore lint/style/noNonNullAssertion: result is always assigned by updater before this point
111
+ return result;
94
112
  }
95
113
  /**
96
114
  * Resets rate limit for a key
@@ -109,7 +127,7 @@ export async function resetRateLimit(key) {
109
127
  * @param config - Rate limit configuration
110
128
  * @returns Rate limit status
111
129
  */
112
- export async function getRateLimitStatus(key, config = DEFAULT_CONFIG) {
130
+ export async function getRateLimitStatus(key, config = globalConfig) {
113
131
  const storage = getStorage();
114
132
  const storageKey = getStorageKey(key);
115
133
  const now = Date.now();
@@ -5,6 +5,30 @@
5
5
  * Sessions are stored in PostgreSQL and validated on each request.
6
6
  */
7
7
  import type { Session, User } from '../types.js';
8
+ export interface SessionBindingConfig {
9
+ /** Invalidate session when user-agent changes (default: true) */
10
+ enforceUserAgent: boolean;
11
+ /** Invalidate session when IP address changes (default: false — users roam) */
12
+ enforceIp: boolean;
13
+ /** Log a warning when IP changes but don't invalidate (default: true) */
14
+ warnOnIpChange: boolean;
15
+ }
16
+ /** Override session binding behaviour (useful for tests or strict deployments). */
17
+ export declare function configureSessionBinding(overrides: Partial<SessionBindingConfig>): void;
18
+ /** Reset to defaults (for tests). */
19
+ export declare function resetSessionBindingConfig(): void;
20
+ export interface RequestContext {
21
+ userAgent?: string;
22
+ ipAddress?: string;
23
+ }
24
+ /**
25
+ * Validate that the current request context matches the session's stored
26
+ * binding values (IP address, user-agent).
27
+ *
28
+ * @returns `null` when the session is valid, or a reason string when it should
29
+ * be invalidated.
30
+ */
31
+ export declare function validateSessionBinding(session: Session, ctx: RequestContext): string | null;
8
32
  export interface SessionData {
9
33
  session: Session;
10
34
  user: User;
@@ -13,9 +37,10 @@ export interface SessionData {
13
37
  * Get session from request headers (cookie)
14
38
  *
15
39
  * @param headers - Request headers containing cookies
40
+ * @param requestContext - Optional IP / user-agent for session binding validation
16
41
  * @returns Session data with user, or null if invalid/expired
17
42
  */
18
- export declare function getSession(headers: Headers): Promise<SessionData | null>;
43
+ export declare function getSession(headers: Headers, requestContext?: RequestContext): Promise<SessionData | null>;
19
44
  /**
20
45
  * Create a new session for a user
21
46
  *
@@ -27,6 +52,28 @@ export declare function createSession(userId: string, options?: {
27
52
  persistent?: boolean;
28
53
  userAgent?: string;
29
54
  ipAddress?: string;
55
+ expiresAt?: Date;
56
+ metadata?: Record<string, unknown>;
57
+ }): Promise<{
58
+ token: string;
59
+ session: Session;
60
+ }>;
61
+ /**
62
+ * Rotate a user's session to prevent session fixation attacks.
63
+ *
64
+ * Deletes the old session (by token hash) or all sessions for the user,
65
+ * then creates a fresh session with a new token.
66
+ *
67
+ * @param userId - User ID to rotate sessions for
68
+ * @param options - Rotation options
69
+ * @returns New session token and session data
70
+ */
71
+ export declare function rotateSession(userId: string, options?: {
72
+ /** Raw token of the old session to invalidate. When omitted, all user sessions are deleted. */
73
+ oldSessionToken?: string;
74
+ persistent?: boolean;
75
+ userAgent?: string;
76
+ ipAddress?: string;
30
77
  }): Promise<{
31
78
  token: string;
32
79
  session: Session;
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAIhD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,IAAI,CAAA;CACX;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAsF9E;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAgE9C;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAmBtE;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAQjD,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,+EAA+E;IAC/E,SAAS,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,cAAc,EAAE,OAAO,CAAC;CACzB;AAUD,mFAAmF;AACnF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAEtF;AAED,qCAAqC;AACrC,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAMD,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CA0B3F;AAMD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA4G7B;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAmE9C;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,+FAA+F;IAC/F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAoC9C;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAoBtE;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE"}