@revealui/auth 0.4.3 → 0.4.5

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.
package/README.md CHANGED
@@ -1,3 +1,11 @@
1
+ ---
2
+ title: "@revealui/auth"
3
+ description: "Session-based authentication for RevealUI - database-backed sessions, rate limiting, brute force protection, and password reset."
4
+ visibility: public
5
+ status: verified
6
+ audience: user
7
+ ---
8
+
1
9
  # @revealui/auth
2
10
 
3
11
  Session-based authentication for RevealUI - database-backed sessions, rate limiting, brute force protection, and password reset.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * CSRF Double-Submit Token Helper
3
+ *
4
+ * Shared by the react auth hooks (`usePasskey`, `useMFA`, `useSignOut`) whose
5
+ * POSTs target the RevealUI admin proxy's CSRF-gated endpoints.
6
+ */
7
+ /**
8
+ * Read the JS-readable `revealui-csrf` cookie. The RevealUI admin proxy
9
+ * (`apps/admin/src/proxy.ts`) requires it as an `X-CSRF-Token` header on any
10
+ * session-cookie-bearing unsafe request, and may re-issue the cookie on any
11
+ * response — so call this immediately before each fetch rather than once per
12
+ * flow. Returns undefined outside the browser or when the cookie is
13
+ * absent/empty. Mirrors the cookie-read in `@revealui/core`'s admin APIClient
14
+ * (`request()`) and `@revealui/ai`'s `useAgentStream`.
15
+ */
16
+ export declare function readCsrfToken(): string | undefined;
17
+ //# sourceMappingURL=csrf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../../src/react/csrf.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,aAAa,IAAI,MAAM,GAAG,SAAS,CAgBlD"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * CSRF Double-Submit Token Helper
3
+ *
4
+ * Shared by the react auth hooks (`usePasskey`, `useMFA`, `useSignOut`) whose
5
+ * POSTs target the RevealUI admin proxy's CSRF-gated endpoints.
6
+ */
7
+ /**
8
+ * Read the JS-readable `revealui-csrf` cookie. The RevealUI admin proxy
9
+ * (`apps/admin/src/proxy.ts`) requires it as an `X-CSRF-Token` header on any
10
+ * session-cookie-bearing unsafe request, and may re-issue the cookie on any
11
+ * response — so call this immediately before each fetch rather than once per
12
+ * flow. Returns undefined outside the browser or when the cookie is
13
+ * absent/empty. Mirrors the cookie-read in `@revealui/core`'s admin APIClient
14
+ * (`request()`) and `@revealui/ai`'s `useAgentStream`.
15
+ */
16
+ export function readCsrfToken() {
17
+ if (typeof document === 'undefined')
18
+ return undefined;
19
+ for (const part of document.cookie.split(';')) {
20
+ const eqIdx = part.indexOf('=');
21
+ if (eqIdx === -1)
22
+ continue;
23
+ if (part.slice(0, eqIdx).trim() === 'revealui-csrf') {
24
+ const raw = part.slice(eqIdx + 1).trim();
25
+ if (!raw)
26
+ return undefined;
27
+ try {
28
+ return decodeURIComponent(raw);
29
+ }
30
+ catch {
31
+ return raw;
32
+ }
33
+ }
34
+ }
35
+ return undefined;
36
+ }
@@ -2,6 +2,14 @@
2
2
  * MFA Hooks
3
3
  *
4
4
  * React hooks for Multi-Factor Authentication setup and verification.
5
+ *
6
+ * CSRF: every POST echoes the JS-readable `revealui-csrf` cookie (the signed
7
+ * double-submit token the RevealUI admin proxy issues on page load) as an
8
+ * `X-CSRF-Token` header when the cookie is present. The proxy requires it on
9
+ * session-cookie-bearing unsafe requests and `/api/auth/mfa/*` is not exempt —
10
+ * MFA enrollment always runs with a session, so without the header it 403s
11
+ * ("CSRF token missing"). Cookie-less callers send no header and are
12
+ * unchanged.
5
13
  */
6
14
  /**
7
15
  * MFA setup data returned when initiating TOTP setup.
@@ -73,7 +81,8 @@ export interface UseMFAVerifyResult {
73
81
  * const handleSubmit = async (code: string) => {
74
82
  * const success = await verify(code);
75
83
  * if (success) {
76
- * router.push('/admin');
84
+ * // Full navigation: a soft router.push() would replay the pre-auth Router Cache
85
+ * window.location.href = '/admin';
77
86
  * }
78
87
  * };
79
88
  * }
@@ -1 +1 @@
1
- {"version":3,"file":"useMFA.d.ts","sourceRoot":"","sources":["../../src/react/useMFA.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,KAAK,EAAE,MAAM,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC1C,0CAA0C;IAC1C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CAqE/C;AAED,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,wCAAwC;IACxC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,IAAI,kBAAkB,CAsEjD"}
1
+ {"version":3,"file":"useMFA.d.ts","sourceRoot":"","sources":["../../src/react/useMFA.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,KAAK,EAAE,MAAM,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC1C,0CAA0C;IAC1C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CA6E/C;AAED,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,wCAAwC;IACxC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,IAAI,kBAAkB,CA8EjD"}
@@ -2,9 +2,18 @@
2
2
  * MFA Hooks
3
3
  *
4
4
  * React hooks for Multi-Factor Authentication setup and verification.
5
+ *
6
+ * CSRF: every POST echoes the JS-readable `revealui-csrf` cookie (the signed
7
+ * double-submit token the RevealUI admin proxy issues on page load) as an
8
+ * `X-CSRF-Token` header when the cookie is present. The proxy requires it on
9
+ * session-cookie-bearing unsafe requests and `/api/auth/mfa/*` is not exempt —
10
+ * MFA enrollment always runs with a session, so without the header it 403s
11
+ * ("CSRF token missing"). Cookie-less callers send no header and are
12
+ * unchanged.
5
13
  */
6
14
  'use client';
7
15
  import { useState } from 'react';
16
+ import { readCsrfToken } from './csrf.js';
8
17
  /**
9
18
  * Hook for MFA setup on the security settings page.
10
19
  *
@@ -38,9 +47,13 @@ export function useMFASetup() {
38
47
  try {
39
48
  setIsLoading(true);
40
49
  setError(null);
50
+ const csrfToken = readCsrfToken();
41
51
  const response = await fetch('/api/auth/mfa/setup', {
42
52
  method: 'POST',
43
53
  credentials: 'include',
54
+ // Conditional headers key keeps cookie-less setup requests
55
+ // byte-identical to the pre-CSRF request shape.
56
+ ...(csrfToken ? { headers: { 'X-CSRF-Token': csrfToken } } : {}),
44
57
  });
45
58
  const json = await response.json();
46
59
  if (!response.ok) {
@@ -64,9 +77,13 @@ export function useMFASetup() {
64
77
  try {
65
78
  setIsLoading(true);
66
79
  setError(null);
80
+ const csrfToken = readCsrfToken();
67
81
  const response = await fetch('/api/auth/mfa/verify-setup', {
68
82
  method: 'POST',
69
- headers: { 'Content-Type': 'application/json' },
83
+ headers: {
84
+ 'Content-Type': 'application/json',
85
+ ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}),
86
+ },
70
87
  credentials: 'include',
71
88
  body: JSON.stringify({ code }),
72
89
  });
@@ -110,7 +127,8 @@ export function useMFASetup() {
110
127
  * const handleSubmit = async (code: string) => {
111
128
  * const success = await verify(code);
112
129
  * if (success) {
113
- * router.push('/admin');
130
+ * // Full navigation: a soft router.push() would replay the pre-auth Router Cache
131
+ * window.location.href = '/admin';
114
132
  * }
115
133
  * };
116
134
  * }
@@ -123,9 +141,13 @@ export function useMFAVerify() {
123
141
  try {
124
142
  setIsLoading(true);
125
143
  setError(null);
144
+ const csrfToken = readCsrfToken();
126
145
  const response = await fetch('/api/auth/mfa/verify', {
127
146
  method: 'POST',
128
- headers: { 'Content-Type': 'application/json' },
147
+ headers: {
148
+ 'Content-Type': 'application/json',
149
+ ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}),
150
+ },
129
151
  credentials: 'include',
130
152
  body: JSON.stringify({ code }),
131
153
  });
@@ -150,9 +172,13 @@ export function useMFAVerify() {
150
172
  try {
151
173
  setIsLoading(true);
152
174
  setError(null);
175
+ const csrfToken = readCsrfToken();
153
176
  const response = await fetch('/api/auth/mfa/backup', {
154
177
  method: 'POST',
155
- headers: { 'Content-Type': 'application/json' },
178
+ headers: {
179
+ 'Content-Type': 'application/json',
180
+ ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}),
181
+ },
156
182
  credentials: 'include',
157
183
  body: JSON.stringify({ code }),
158
184
  });
@@ -3,6 +3,13 @@
3
3
  *
4
4
  * React hooks for WebAuthn passkey registration and authentication.
5
5
  * Uses dynamic imports for @simplewebauthn/browser to avoid SSR issues.
6
+ *
7
+ * CSRF: every POST echoes the JS-readable `revealui-csrf` cookie (the signed
8
+ * double-submit token the RevealUI admin proxy issues on page load) as an
9
+ * `X-CSRF-Token` header when the cookie is present. The proxy requires it on
10
+ * session-cookie-bearing unsafe requests — registration always runs with a
11
+ * session, so without the header it 403s ("CSRF token missing"). Cookie-less
12
+ * callers (no admin session) send no header and are unchanged.
6
13
  */
7
14
  export interface PasskeyRegisterOptions {
8
15
  /** Email for passkey registration (sign-up flow) */
@@ -67,7 +74,8 @@ export interface UsePasskeySignInResult {
67
74
  * const handlePasskeyLogin = async () => {
68
75
  * const success = await signIn();
69
76
  * if (success) {
70
- * router.push('/admin');
77
+ * // Full navigation: a soft router.push() would replay the pre-auth Router Cache
78
+ * window.location.href = '/admin';
71
79
  * }
72
80
  * };
73
81
  *
@@ -1 +1 @@
1
- {"version":3,"file":"usePasskey.d.ts","sourceRoot":"","sources":["../../src/react/usePasskey.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,WAAW,sBAAsB;IACrC,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,wCAAwC;IACxC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,sBAAsB,KAAK,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IACtF,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,4CAA4C;IAC5C,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,IAAI,wBAAwB,CA0F7D;AAED,MAAM,WAAW,sBAAsB;IACrC,kCAAkC;IAClC,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,4CAA4C;IAC5C,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,gBAAgB,IAAI,sBAAsB,CA8EzD"}
1
+ {"version":3,"file":"usePasskey.d.ts","sourceRoot":"","sources":["../../src/react/usePasskey.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,MAAM,WAAW,sBAAsB;IACrC,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,wCAAwC;IACxC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,sBAAsB,KAAK,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IACtF,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,4CAA4C;IAC5C,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,IAAI,wBAAwB,CAkG7D;AAED,MAAM,WAAW,sBAAsB;IACrC,kCAAkC;IAClC,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,4CAA4C;IAC5C,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,gBAAgB,IAAI,sBAAsB,CAsFzD"}
@@ -3,9 +3,17 @@
3
3
  *
4
4
  * React hooks for WebAuthn passkey registration and authentication.
5
5
  * Uses dynamic imports for @simplewebauthn/browser to avoid SSR issues.
6
+ *
7
+ * CSRF: every POST echoes the JS-readable `revealui-csrf` cookie (the signed
8
+ * double-submit token the RevealUI admin proxy issues on page load) as an
9
+ * `X-CSRF-Token` header when the cookie is present. The proxy requires it on
10
+ * session-cookie-bearing unsafe requests — registration always runs with a
11
+ * session, so without the header it 403s ("CSRF token missing"). Cookie-less
12
+ * callers (no admin session) send no header and are unchanged.
6
13
  */
7
14
  'use client';
8
15
  import { useEffect, useState } from 'react';
16
+ import { readCsrfToken } from './csrf.js';
9
17
  /**
10
18
  * Hook for passkey registration (sign-up and security settings).
11
19
  *
@@ -43,9 +51,13 @@ export function usePasskeyRegister() {
43
51
  setIsLoading(true);
44
52
  setError(null);
45
53
  // Step 1: Get registration options from the server
54
+ const optionsCsrfToken = readCsrfToken();
46
55
  const optionsResponse = await fetch('/api/auth/passkey/register-options', {
47
56
  method: 'POST',
48
- headers: { 'Content-Type': 'application/json' },
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ ...(optionsCsrfToken ? { 'X-CSRF-Token': optionsCsrfToken } : {}),
60
+ },
49
61
  credentials: 'include',
50
62
  body: JSON.stringify({
51
63
  email: options?.email,
@@ -65,9 +77,13 @@ export function usePasskeyRegister() {
65
77
  const optionsData = optionsJson;
66
78
  const attestationResponse = await startRegistration({ optionsJSON: optionsData.options });
67
79
  // Step 3: Verify with the server
80
+ const verifyCsrfToken = readCsrfToken();
68
81
  const verifyResponse = await fetch('/api/auth/passkey/register-verify', {
69
82
  method: 'POST',
70
- headers: { 'Content-Type': 'application/json' },
83
+ headers: {
84
+ 'Content-Type': 'application/json',
85
+ ...(verifyCsrfToken ? { 'X-CSRF-Token': verifyCsrfToken } : {}),
86
+ },
71
87
  credentials: 'include',
72
88
  body: JSON.stringify({
73
89
  attestationResponse,
@@ -117,7 +133,8 @@ export function usePasskeyRegister() {
117
133
  * const handlePasskeyLogin = async () => {
118
134
  * const success = await signIn();
119
135
  * if (success) {
120
- * router.push('/admin');
136
+ * // Full navigation: a soft router.push() would replay the pre-auth Router Cache
137
+ * window.location.href = '/admin';
121
138
  * }
122
139
  * };
123
140
  *
@@ -150,9 +167,13 @@ export function usePasskeySignIn() {
150
167
  setIsLoading(true);
151
168
  setError(null);
152
169
  // Step 1: Get authentication options from the server
170
+ const optionsCsrfToken = readCsrfToken();
153
171
  const optionsResponse = await fetch('/api/auth/passkey/authenticate-options', {
154
172
  method: 'POST',
155
173
  credentials: 'include',
174
+ // Conditional headers key keeps cookie-less sign-in requests
175
+ // byte-identical to the pre-CSRF request shape.
176
+ ...(optionsCsrfToken ? { headers: { 'X-CSRF-Token': optionsCsrfToken } } : {}),
156
177
  });
157
178
  const optionsJson = await optionsResponse.json();
158
179
  if (!optionsResponse.ok) {
@@ -166,9 +187,13 @@ export function usePasskeySignIn() {
166
187
  const authOptionsData = optionsJson;
167
188
  const assertionResponse = await startAuthentication({ optionsJSON: authOptionsData.options });
168
189
  // Step 3: Verify with the server
190
+ const verifyCsrfToken = readCsrfToken();
169
191
  const verifyResponse = await fetch('/api/auth/passkey/authenticate-verify', {
170
192
  method: 'POST',
171
- headers: { 'Content-Type': 'application/json' },
193
+ headers: {
194
+ 'Content-Type': 'application/json',
195
+ ...(verifyCsrfToken ? { 'X-CSRF-Token': verifyCsrfToken } : {}),
196
+ },
172
197
  credentials: 'include',
173
198
  body: JSON.stringify({ authenticationResponse: assertionResponse }),
174
199
  });
@@ -37,7 +37,8 @@ export interface UseSignInResult {
37
37
  * e.preventDefault()
38
38
  * const result = await signIn({ email, password })
39
39
  * if (result.success) {
40
- * router.push('/dashboard')
40
+ * // Full navigation: a soft router.push() would replay the pre-auth Router Cache
41
+ * window.location.href = '/dashboard'
41
42
  * }
42
43
  * }
43
44
  * }
@@ -1 +1 @@
1
- {"version":3,"file":"useSignIn.d.ts","sourceRoot":"","sources":["../../src/react/useSignIn.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAkCxC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CACN,KAAK,EAAE,WAAW,KACf,OAAO,CACR;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,GAC7B;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GACjC;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,WAAW,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAC3D,CAAC;IACF,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,SAAS,IAAI,eAAe,CAsE3C"}
1
+ {"version":3,"file":"useSignIn.d.ts","sourceRoot":"","sources":["../../src/react/useSignIn.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAkCxC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CACN,KAAK,EAAE,WAAW,KACf,OAAO,CACR;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,GAC7B;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GACjC;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,WAAW,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAC3D,CAAC;IACF,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,IAAI,eAAe,CAsE3C"}
@@ -48,7 +48,8 @@ const SignInSuccessResponseSchema = z.object({
48
48
  * e.preventDefault()
49
49
  * const result = await signIn({ email, password })
50
50
  * if (result.success) {
51
- * router.push('/dashboard')
51
+ * // Full navigation: a soft router.push() would replay the pre-auth Router Cache
52
+ * window.location.href = '/dashboard'
52
53
  * }
53
54
  * }
54
55
  * }
@@ -2,6 +2,14 @@
2
2
  * useSignOut Hook
3
3
  *
4
4
  * React hook for signing out a user.
5
+ *
6
+ * CSRF: the sign-out POST echoes the JS-readable `revealui-csrf` cookie (the
7
+ * signed double-submit token the RevealUI admin proxy issues on page load) as
8
+ * an `X-CSRF-Token` header when the cookie is present. The proxy requires it
9
+ * on session-cookie-bearing unsafe requests and `/api/auth/sign-out` is not
10
+ * exempt — sign-out always carries a session, so without the header it 403s
11
+ * ("CSRF token missing") and the server-side session survives. Cookie-less
12
+ * callers send no header and are unchanged.
5
13
  */
6
14
  export interface UseSignOutResult {
7
15
  signOut: () => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"useSignOut.d.ts","sourceRoot":"","sources":["../../src/react/useSignOut.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,IAAI,gBAAgB,CAkC7C"}
1
+ {"version":3,"file":"useSignOut.d.ts","sourceRoot":"","sources":["../../src/react/useSignOut.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,IAAI,gBAAgB,CAsC7C"}
@@ -2,9 +2,18 @@
2
2
  * useSignOut Hook
3
3
  *
4
4
  * React hook for signing out a user.
5
+ *
6
+ * CSRF: the sign-out POST echoes the JS-readable `revealui-csrf` cookie (the
7
+ * signed double-submit token the RevealUI admin proxy issues on page load) as
8
+ * an `X-CSRF-Token` header when the cookie is present. The proxy requires it
9
+ * on session-cookie-bearing unsafe requests and `/api/auth/sign-out` is not
10
+ * exempt — sign-out always carries a session, so without the header it 403s
11
+ * ("CSRF token missing") and the server-side session survives. Cookie-less
12
+ * callers send no header and are unchanged.
5
13
  */
6
14
  'use client';
7
15
  import { useState } from 'react';
16
+ import { readCsrfToken } from './csrf.js';
8
17
  /**
9
18
  * Hook to sign out a user
10
19
  *
@@ -30,9 +39,13 @@ export function useSignOut() {
30
39
  try {
31
40
  setIsLoading(true);
32
41
  setError(null);
42
+ const csrfToken = readCsrfToken();
33
43
  const response = await fetch('/api/auth/sign-out', {
34
44
  method: 'POST',
35
45
  credentials: 'include',
46
+ // Conditional headers key keeps cookie-less sign-out requests
47
+ // byte-identical to the pre-CSRF request shape.
48
+ ...(csrfToken ? { headers: { 'X-CSRF-Token': csrfToken } } : {}),
36
49
  });
37
50
  if (!response.ok) {
38
51
  throw new Error('Failed to sign out');
@@ -33,7 +33,8 @@ export interface UseSignUpResult {
33
33
  * e.preventDefault()
34
34
  * const result = await signUp({ email, password, name })
35
35
  * if (result.success) {
36
- * router.push('/dashboard')
36
+ * // Full navigation: a soft router.push() would replay the pre-auth Router Cache
37
+ * window.location.href = '/dashboard'
37
38
  * }
38
39
  * }
39
40
  * }
@@ -1 +1 @@
1
- {"version":3,"file":"useSignUp.d.ts","sourceRoot":"","sources":["../../src/react/useSignUp.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAmCxC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3F,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,SAAS,IAAI,eAAe,CAoD3C"}
1
+ {"version":3,"file":"useSignUp.d.ts","sourceRoot":"","sources":["../../src/react/useSignUp.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAmCxC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3F,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,IAAI,eAAe,CAoD3C"}
@@ -49,7 +49,8 @@ const SignUpSuccessResponseSchema = z.object({
49
49
  * e.preventDefault()
50
50
  * const result = await signUp({ email, password, name })
51
51
  * if (result.success) {
52
- * router.push('/dashboard')
52
+ * // Full navigation: a soft router.push() would replay the pre-auth Router Cache
53
+ * window.location.href = '/dashboard'
53
54
  * }
54
55
  * }
55
56
  * }
@@ -93,8 +93,14 @@ export declare function rotateSession(userId: string, options?: {
93
93
  /**
94
94
  * Delete a session (sign out)
95
95
  *
96
+ * Revokes EVERY session token presented in the cookie header, not just the
97
+ * first: a browser can hold duplicate `revealui-session` cookies (e.g. a
98
+ * stale host-only cookie alongside the domain-scoped one), and RFC 6265
99
+ * ordering lets the stale duplicate shadow the live token. Sign-out must
100
+ * revoke them all.
101
+ *
96
102
  * @param headers - Request headers containing session cookie
97
- * @returns True if session was deleted, false if not found
103
+ * @returns True if at least one session row was deleted, false otherwise
98
104
  */
99
105
  export declare function deleteSession(headers: Headers): Promise<boolean>;
100
106
  /**
@@ -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,CAAC;AAQjD,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iFAAiF;IACjF,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;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAI1E;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAkH7B;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;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,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,iFAAiF;IACjF,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;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAI1E;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAkH7B;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;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BtE;AAED;;;;GAIG;AACH;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE"}
@@ -7,7 +7,7 @@
7
7
  import { logger } from '@revealui/core/observability/logger';
8
8
  import { getClient } from '@revealui/db/client';
9
9
  import { sessions, users } from '@revealui/db/schema';
10
- import { and, eq, gt, isNull, ne } from 'drizzle-orm';
10
+ import { and, eq, gt, inArray, isNull, ne } from 'drizzle-orm';
11
11
  import { hashToken } from '../utils/token.js';
12
12
  import { DatabaseError, TokenError } from './errors.js';
13
13
  const DEFAULT_SESSION_BINDING = {
@@ -310,21 +310,38 @@ export async function rotateSession(userId, options) {
310
310
  /**
311
311
  * Delete a session (sign out)
312
312
  *
313
+ * Revokes EVERY session token presented in the cookie header, not just the
314
+ * first: a browser can hold duplicate `revealui-session` cookies (e.g. a
315
+ * stale host-only cookie alongside the domain-scoped one), and RFC 6265
316
+ * ordering lets the stale duplicate shadow the live token. Sign-out must
317
+ * revoke them all.
318
+ *
313
319
  * @param headers - Request headers containing session cookie
314
- * @returns True if session was deleted, false if not found
320
+ * @returns True if at least one session row was deleted, false otherwise
315
321
  */
316
322
  export async function deleteSession(headers) {
317
323
  const cookieHeader = headers.get('cookie');
318
324
  if (!cookieHeader) {
319
325
  return false;
320
326
  }
321
- const sessionToken = extractSessionToken(cookieHeader);
322
- if (!sessionToken) {
327
+ const sessionTokens = extractAllSessionTokens(cookieHeader);
328
+ if (sessionTokens.length === 0) {
329
+ return false;
330
+ }
331
+ const tokenHashes = [];
332
+ for (const sessionToken of sessionTokens) {
333
+ try {
334
+ tokenHashes.push(hashToken(sessionToken));
335
+ }
336
+ catch {
337
+ logger.error('Error hashing session token during sign-out');
338
+ }
339
+ }
340
+ if (tokenHashes.length === 0) {
323
341
  return false;
324
342
  }
325
- const tokenHash = hashToken(sessionToken);
326
343
  const db = getClient();
327
- const result = await db.delete(sessions).where(eq(sessions.tokenHash, tokenHash));
344
+ const result = await db.delete(sessions).where(inArray(sessions.tokenHash, tokenHashes));
328
345
  // Check if any rows were deleted - Drizzle delete returns result with rowCount or similar
329
346
  const rowCount = 'rowCount' in result && typeof result.rowCount === 'number' ? result.rowCount : 0;
330
347
  return rowCount > 0;
@@ -405,6 +422,31 @@ function extractSessionToken(cookieHeader) {
405
422
  }
406
423
  return decodeURIComponent(sessionCookie.substring('revealui-session='.length));
407
424
  }
425
+ /**
426
+ * Extract every distinct session token from a cookie header.
427
+ *
428
+ * Duplicate `revealui-session` cookies are real: a host-only and a
429
+ * domain-scoped cookie with the same name travel together in one header.
430
+ * Deletion must see all of them; validation (getSession) deliberately keeps
431
+ * first-match semantics via extractSessionToken.
432
+ */
433
+ function extractAllSessionTokens(cookieHeader) {
434
+ const prefix = 'revealui-session=';
435
+ const tokens = new Set();
436
+ for (const part of cookieHeader.split(';')) {
437
+ const cookie = part.trim();
438
+ if (!cookie.startsWith(prefix)) {
439
+ continue;
440
+ }
441
+ try {
442
+ tokens.add(decodeURIComponent(cookie.substring(prefix.length)));
443
+ }
444
+ catch {
445
+ // Malformed percent-encoding in one duplicate must not abort sign-out.
446
+ }
447
+ }
448
+ return [...tokens];
449
+ }
408
450
  /**
409
451
  * Generate a secure random session token
410
452
  */
@@ -28,6 +28,19 @@ export declare function createStorage(): Storage;
28
28
  * Reset global storage (for testing)
29
29
  */
30
30
  export declare function resetStorage(): void;
31
+ /**
32
+ * Override the global storage instance (for testing).
33
+ *
34
+ * Lets a test pin a specific backend — e.g. {@link InMemoryStorage} — so the
35
+ * rate-limit/brute-force logic runs against in-process state instead of the
36
+ * shared `DatabaseStorage` singleton. The DB-backed integration suite runs
37
+ * single-threaded with `isolate: false`, so a shared Postgres-backed store
38
+ * under contention can intermittently drop a write (manifesting as e.g. "not
39
+ * locked after 5 attempts"); pinning InMemoryStorage removes that I/O entirely.
40
+ * Pair with {@link resetStorage} in teardown so later code re-derives the real
41
+ * backend.
42
+ */
43
+ export declare function setStorage(storage: Storage): void;
31
44
  export { DatabaseStorage } from './database.js';
32
45
  export { InMemoryStorage } from './in-memory.js';
33
46
  export type { Storage } from './interface.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/storage/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAI9C;;GAEG;AACH,wBAAgB,UAAU,IAAI,OAAO,CA6CpC;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAevC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAEnC;AAED,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,YAAY,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/storage/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAI9C;;GAEG;AACH,wBAAgB,UAAU,IAAI,OAAO,CA6CpC;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAevC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAEnC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAEjD;AAED,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,YAAY,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC"}
@@ -89,6 +89,21 @@ export function createStorage() {
89
89
  export function resetStorage() {
90
90
  globalStorage = null;
91
91
  }
92
+ /**
93
+ * Override the global storage instance (for testing).
94
+ *
95
+ * Lets a test pin a specific backend — e.g. {@link InMemoryStorage} — so the
96
+ * rate-limit/brute-force logic runs against in-process state instead of the
97
+ * shared `DatabaseStorage` singleton. The DB-backed integration suite runs
98
+ * single-threaded with `isolate: false`, so a shared Postgres-backed store
99
+ * under contention can intermittently drop a write (manifesting as e.g. "not
100
+ * locked after 5 attempts"); pinning InMemoryStorage removes that I/O entirely.
101
+ * Pair with {@link resetStorage} in teardown so later code re-derives the real
102
+ * backend.
103
+ */
104
+ export function setStorage(storage) {
105
+ globalStorage = storage;
106
+ }
92
107
  export { DatabaseStorage } from './database.js';
93
108
  // Export storage implementations
94
109
  export { InMemoryStorage } from './in-memory.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revealui/auth",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Database-backed session auth for Hono and Next.js — bcrypt, OAuth, brute-force protection, rate limiting, password reset. Ships with RevealUI.",
5
5
  "keywords": [
6
6
  "auth",
@@ -10,26 +10,26 @@
10
10
  ],
11
11
  "license": "MIT",
12
12
  "dependencies": {
13
- "@simplewebauthn/server": "^13.3.0",
13
+ "@simplewebauthn/server": "^13.3.1",
14
14
  "bcryptjs": "^3.0.3",
15
15
  "drizzle-orm": "^0.45.2",
16
16
  "zod": "^4.4.3",
17
- "@revealui/config": "0.4.1",
18
- "@revealui/contracts": "0.6.0",
19
- "@revealui/core": "0.9.0",
20
- "@revealui/db": "0.7.0",
21
- "@revealui/security": "0.4.0"
17
+ "@revealui/config": "0.4.2",
18
+ "@revealui/contracts": "0.6.1",
19
+ "@revealui/core": "0.10.1",
20
+ "@revealui/db": "0.7.1",
21
+ "@revealui/security": "0.4.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@simplewebauthn/browser": "^13.3.0",
25
25
  "@testing-library/react": "^16.3.2",
26
- "@types/node": "^25.6.0",
27
- "@types/react": "^19.2.15",
28
- "@vitest/coverage-v8": "^4.1.5",
29
- "happy-dom": "^20.9.0",
30
- "react": "^19.2.6",
26
+ "@types/node": "^25.9.1",
27
+ "@types/react": "^19.2.17",
28
+ "@vitest/coverage-v8": "^4.1.8",
29
+ "happy-dom": "^20.10.2",
30
+ "react": "^19.2.7",
31
31
  "typescript": "^6.0.3",
32
- "vitest": "^4.1.5",
32
+ "vitest": "^4.1.8",
33
33
  "@revealui/dev": "0.1.0"
34
34
  },
35
35
  "engines": {