@revealui/auth 0.2.1 → 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.
- package/dist/index.d.ts.map +1 -1
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/useMFA.d.ts +83 -0
- package/dist/react/useMFA.d.ts.map +1 -0
- package/dist/react/useMFA.js +182 -0
- package/dist/react/usePasskey.d.ts +88 -0
- package/dist/react/usePasskey.d.ts.map +1 -0
- package/dist/react/usePasskey.js +203 -0
- package/dist/react/useSession.d.ts.map +1 -1
- package/dist/react/useSession.js +16 -5
- package/dist/react/useSignIn.d.ts +9 -3
- package/dist/react/useSignIn.d.ts.map +1 -1
- package/dist/react/useSignIn.js +32 -10
- package/dist/react/useSignOut.d.ts.map +1 -1
- package/dist/react/useSignUp.d.ts.map +1 -1
- package/dist/react/useSignUp.js +25 -9
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +75 -4
- package/dist/server/brute-force.d.ts +10 -1
- package/dist/server/brute-force.d.ts.map +1 -1
- package/dist/server/brute-force.js +17 -3
- package/dist/server/errors.d.ts.map +1 -1
- package/dist/server/index.d.ts +16 -6
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +11 -5
- package/dist/server/magic-link.d.ts +52 -0
- package/dist/server/magic-link.d.ts.map +1 -0
- package/dist/server/magic-link.js +111 -0
- package/dist/server/mfa.d.ts +87 -0
- package/dist/server/mfa.d.ts.map +1 -0
- package/dist/server/mfa.js +263 -0
- package/dist/server/oauth.d.ts +37 -0
- package/dist/server/oauth.d.ts.map +1 -1
- package/dist/server/oauth.js +135 -3
- package/dist/server/passkey.d.ts +132 -0
- package/dist/server/passkey.d.ts.map +1 -0
- package/dist/server/passkey.js +257 -0
- package/dist/server/password-reset.d.ts +15 -0
- package/dist/server/password-reset.d.ts.map +1 -1
- package/dist/server/password-reset.js +44 -1
- package/dist/server/password-validation.d.ts.map +1 -1
- package/dist/server/providers/github.d.ts.map +1 -1
- package/dist/server/providers/github.js +18 -2
- package/dist/server/providers/google.d.ts.map +1 -1
- package/dist/server/providers/google.js +18 -2
- package/dist/server/providers/vercel.d.ts.map +1 -1
- package/dist/server/providers/vercel.js +18 -2
- package/dist/server/rate-limit.d.ts +10 -1
- package/dist/server/rate-limit.d.ts.map +1 -1
- package/dist/server/rate-limit.js +61 -43
- package/dist/server/session.d.ts +48 -1
- package/dist/server/session.d.ts.map +1 -1
- package/dist/server/session.js +125 -6
- package/dist/server/signed-cookie.d.ts +32 -0
- package/dist/server/signed-cookie.d.ts.map +1 -0
- package/dist/server/signed-cookie.js +67 -0
- package/dist/server/storage/database.d.ts +1 -1
- package/dist/server/storage/database.d.ts.map +1 -1
- package/dist/server/storage/database.js +15 -7
- package/dist/server/storage/in-memory.d.ts.map +1 -1
- package/dist/server/storage/in-memory.js +7 -7
- package/dist/server/storage/index.d.ts +11 -3
- package/dist/server/storage/index.d.ts.map +1 -1
- package/dist/server/storage/index.js +18 -4
- package/dist/server/storage/interface.d.ts +1 -1
- package/dist/server/storage/interface.d.ts.map +1 -1
- package/dist/server/storage/interface.js +1 -1
- package/dist/types.d.ts +20 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/utils/database.d.ts.map +1 -1
- package/dist/utils/database.js +9 -2
- package/package.json +26 -8
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,CAAC;AAEjC,cAAc,mBAAmB,CAAC;AAGlC,YAAY,EACV,WAAW,EACX,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,IAAI,GACL,MAAM,YAAY,CAAC"}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Client-side React hooks for authentication.
|
|
5
5
|
* Inspired by Better Auth and TanStack Start patterns.
|
|
6
6
|
*/
|
|
7
|
+
export type { MFASetupData, UseMFASetupResult, UseMFAVerifyResult, } from './useMFA.js';
|
|
8
|
+
export { useMFASetup, useMFAVerify } from './useMFA.js';
|
|
9
|
+
export type { PasskeyRegisterOptions, PasskeyRegisterResult, UsePasskeyRegisterResult, UsePasskeySignInResult, } from './usePasskey.js';
|
|
10
|
+
export { usePasskeyRegister, usePasskeySignIn } from './usePasskey.js';
|
|
7
11
|
export type { UseSessionResult } from './useSession.js';
|
|
8
12
|
export { useSession } from './useSession.js';
|
|
9
13
|
export type { SignInInput, UseSignInResult } from './useSignIn.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EACV,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACvE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/react/index.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Client-side React hooks for authentication.
|
|
5
5
|
* Inspired by Better Auth and TanStack Start patterns.
|
|
6
6
|
*/
|
|
7
|
+
export { useMFASetup, useMFAVerify } from './useMFA.js';
|
|
8
|
+
export { usePasskeyRegister, usePasskeySignIn } from './usePasskey.js';
|
|
7
9
|
export { useSession } from './useSession.js';
|
|
8
10
|
export { useSignIn } from './useSignIn.js';
|
|
9
11
|
export { useSignOut } from './useSignOut.js';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MFA Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for Multi-Factor Authentication setup and verification.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* MFA setup data returned when initiating TOTP setup.
|
|
8
|
+
*/
|
|
9
|
+
export interface MFASetupData {
|
|
10
|
+
/** Base32-encoded TOTP secret */
|
|
11
|
+
secret: string;
|
|
12
|
+
/** otpauth:// URI for QR code generation */
|
|
13
|
+
uri: string;
|
|
14
|
+
/** One-time backup codes for account recovery */
|
|
15
|
+
backupCodes: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface UseMFASetupResult {
|
|
18
|
+
/** Initiate MFA setup — returns secret, QR URI, and backup codes */
|
|
19
|
+
setup: () => Promise<MFASetupData | null>;
|
|
20
|
+
/** Verify a TOTP code to confirm setup */
|
|
21
|
+
verifySetup: (code: string) => Promise<boolean>;
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
error: string | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Hook for MFA setup on the security settings page.
|
|
27
|
+
*
|
|
28
|
+
* @returns Setup and verify functions, loading state, and error
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* function SecuritySettings() {
|
|
33
|
+
* const { setup, verifySetup, isLoading, error } = useMFASetup();
|
|
34
|
+
*
|
|
35
|
+
* const handleEnable = async () => {
|
|
36
|
+
* const data = await setup();
|
|
37
|
+
* if (data) {
|
|
38
|
+
* // Show QR code using data.uri, display backup codes
|
|
39
|
+
* }
|
|
40
|
+
* };
|
|
41
|
+
*
|
|
42
|
+
* const handleVerify = async (code: string) => {
|
|
43
|
+
* const success = await verifySetup(code);
|
|
44
|
+
* if (success) {
|
|
45
|
+
* // MFA is now enabled
|
|
46
|
+
* }
|
|
47
|
+
* };
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function useMFASetup(): UseMFASetupResult;
|
|
52
|
+
export interface UseMFAVerifyResult {
|
|
53
|
+
/** Verify a TOTP code during login */
|
|
54
|
+
verify: (code: string) => Promise<boolean>;
|
|
55
|
+
/** Verify a backup code during login */
|
|
56
|
+
verifyBackupCode: (code: string) => Promise<boolean>;
|
|
57
|
+
isLoading: boolean;
|
|
58
|
+
error: string | null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Hook for MFA verification during the login flow.
|
|
62
|
+
*
|
|
63
|
+
* After sign-in returns `requiresMfa: true`, redirect to the MFA page
|
|
64
|
+
* and use this hook to complete authentication.
|
|
65
|
+
*
|
|
66
|
+
* @returns Verify functions, loading state, and error
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* function MFAPage() {
|
|
71
|
+
* const { verify, verifyBackupCode, isLoading, error } = useMFAVerify();
|
|
72
|
+
*
|
|
73
|
+
* const handleSubmit = async (code: string) => {
|
|
74
|
+
* const success = await verify(code);
|
|
75
|
+
* if (success) {
|
|
76
|
+
* router.push('/admin');
|
|
77
|
+
* }
|
|
78
|
+
* };
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function useMFAVerify(): UseMFAVerifyResult;
|
|
83
|
+
//# sourceMappingURL=useMFA.d.ts.map
|
|
@@ -0,0 +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,oEAAoE;IACpE,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"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MFA Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for Multi-Factor Authentication setup and verification.
|
|
5
|
+
*/
|
|
6
|
+
'use client';
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
/**
|
|
9
|
+
* Hook for MFA setup on the security settings page.
|
|
10
|
+
*
|
|
11
|
+
* @returns Setup and verify functions, loading state, and error
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* function SecuritySettings() {
|
|
16
|
+
* const { setup, verifySetup, isLoading, error } = useMFASetup();
|
|
17
|
+
*
|
|
18
|
+
* const handleEnable = async () => {
|
|
19
|
+
* const data = await setup();
|
|
20
|
+
* if (data) {
|
|
21
|
+
* // Show QR code using data.uri, display backup codes
|
|
22
|
+
* }
|
|
23
|
+
* };
|
|
24
|
+
*
|
|
25
|
+
* const handleVerify = async (code: string) => {
|
|
26
|
+
* const success = await verifySetup(code);
|
|
27
|
+
* if (success) {
|
|
28
|
+
* // MFA is now enabled
|
|
29
|
+
* }
|
|
30
|
+
* };
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function useMFASetup() {
|
|
35
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
36
|
+
const [error, setError] = useState(null);
|
|
37
|
+
const setup = async () => {
|
|
38
|
+
try {
|
|
39
|
+
setIsLoading(true);
|
|
40
|
+
setError(null);
|
|
41
|
+
const response = await fetch('/api/auth/mfa/setup', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
credentials: 'include',
|
|
44
|
+
});
|
|
45
|
+
const json = await response.json();
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const errorData = json;
|
|
48
|
+
setError(errorData.error ?? 'Failed to set up MFA');
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const data = json;
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
+
setError(message);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const verifySetup = async (code) => {
|
|
64
|
+
try {
|
|
65
|
+
setIsLoading(true);
|
|
66
|
+
setError(null);
|
|
67
|
+
const response = await fetch('/api/auth/mfa/verify-setup', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
credentials: 'include',
|
|
71
|
+
body: JSON.stringify({ code }),
|
|
72
|
+
});
|
|
73
|
+
const json = await response.json();
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const errorData = json;
|
|
76
|
+
setError(errorData.error ?? 'Failed to verify MFA code');
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
83
|
+
setError(message);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
setup,
|
|
92
|
+
verifySetup,
|
|
93
|
+
isLoading,
|
|
94
|
+
error,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Hook for MFA verification during the login flow.
|
|
99
|
+
*
|
|
100
|
+
* After sign-in returns `requiresMfa: true`, redirect to the MFA page
|
|
101
|
+
* and use this hook to complete authentication.
|
|
102
|
+
*
|
|
103
|
+
* @returns Verify functions, loading state, and error
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* function MFAPage() {
|
|
108
|
+
* const { verify, verifyBackupCode, isLoading, error } = useMFAVerify();
|
|
109
|
+
*
|
|
110
|
+
* const handleSubmit = async (code: string) => {
|
|
111
|
+
* const success = await verify(code);
|
|
112
|
+
* if (success) {
|
|
113
|
+
* router.push('/admin');
|
|
114
|
+
* }
|
|
115
|
+
* };
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export function useMFAVerify() {
|
|
120
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
121
|
+
const [error, setError] = useState(null);
|
|
122
|
+
const verify = async (code) => {
|
|
123
|
+
try {
|
|
124
|
+
setIsLoading(true);
|
|
125
|
+
setError(null);
|
|
126
|
+
const response = await fetch('/api/auth/mfa/verify', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
credentials: 'include',
|
|
130
|
+
body: JSON.stringify({ code }),
|
|
131
|
+
});
|
|
132
|
+
const json = await response.json();
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
const errorData = json;
|
|
135
|
+
setError(errorData.error ?? 'Invalid verification code');
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
142
|
+
setError(message);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
setIsLoading(false);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const verifyBackupCode = async (code) => {
|
|
150
|
+
try {
|
|
151
|
+
setIsLoading(true);
|
|
152
|
+
setError(null);
|
|
153
|
+
const response = await fetch('/api/auth/mfa/backup', {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: { 'Content-Type': 'application/json' },
|
|
156
|
+
credentials: 'include',
|
|
157
|
+
body: JSON.stringify({ code }),
|
|
158
|
+
});
|
|
159
|
+
const json = await response.json();
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
const errorData = json;
|
|
162
|
+
setError(errorData.error ?? 'Invalid backup code');
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
169
|
+
setError(message);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
setIsLoading(false);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
verify,
|
|
178
|
+
verifyBackupCode,
|
|
179
|
+
isLoading,
|
|
180
|
+
error,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkey Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for WebAuthn passkey registration and authentication.
|
|
5
|
+
* Uses dynamic imports for @simplewebauthn/browser to avoid SSR issues.
|
|
6
|
+
*/
|
|
7
|
+
export interface PasskeyRegisterOptions {
|
|
8
|
+
/** Email for passkey registration (sign-up flow) */
|
|
9
|
+
email?: string;
|
|
10
|
+
/** Display name for the credential */
|
|
11
|
+
name?: string;
|
|
12
|
+
/** Human-readable device name (e.g., "MacBook Pro") */
|
|
13
|
+
deviceName?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface PasskeyRegisterResult {
|
|
16
|
+
/** Backup codes returned during sign-up flow */
|
|
17
|
+
backupCodes?: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface UsePasskeyRegisterResult {
|
|
20
|
+
/** Register a new passkey credential */
|
|
21
|
+
register: (options?: PasskeyRegisterOptions) => Promise<PasskeyRegisterResult | null>;
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
error: string | null;
|
|
24
|
+
/** Whether the browser supports WebAuthn */
|
|
25
|
+
supported: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Hook for passkey registration (sign-up and security settings).
|
|
29
|
+
*
|
|
30
|
+
* @returns Register function, loading state, error, and browser support flag
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* function AddPasskey() {
|
|
35
|
+
* const { register, isLoading, error, supported } = usePasskeyRegister();
|
|
36
|
+
*
|
|
37
|
+
* if (!supported) return <p>Passkeys are not supported in this browser.</p>;
|
|
38
|
+
*
|
|
39
|
+
* const handleAdd = async () => {
|
|
40
|
+
* const result = await register({ deviceName: 'My Laptop' });
|
|
41
|
+
* if (result) {
|
|
42
|
+
* // Passkey registered successfully
|
|
43
|
+
* }
|
|
44
|
+
* };
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function usePasskeyRegister(): UsePasskeyRegisterResult;
|
|
49
|
+
export interface UsePasskeySignInResult {
|
|
50
|
+
/** Authenticate with a passkey */
|
|
51
|
+
signIn: () => Promise<boolean>;
|
|
52
|
+
isLoading: boolean;
|
|
53
|
+
error: string | null;
|
|
54
|
+
/** Whether the browser supports WebAuthn */
|
|
55
|
+
supported: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Hook for passkey authentication (login page).
|
|
59
|
+
*
|
|
60
|
+
* @returns Sign-in function, loading state, error, and browser support flag
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```tsx
|
|
64
|
+
* function LoginPage() {
|
|
65
|
+
* const { signIn, isLoading, error, supported } = usePasskeySignIn();
|
|
66
|
+
*
|
|
67
|
+
* const handlePasskeyLogin = async () => {
|
|
68
|
+
* const success = await signIn();
|
|
69
|
+
* if (success) {
|
|
70
|
+
* router.push('/admin');
|
|
71
|
+
* }
|
|
72
|
+
* };
|
|
73
|
+
*
|
|
74
|
+
* return (
|
|
75
|
+
* <>
|
|
76
|
+
* {supported && (
|
|
77
|
+
* <button onClick={handlePasskeyLogin} disabled={isLoading}>
|
|
78
|
+
* Sign in with Passkey
|
|
79
|
+
* </button>
|
|
80
|
+
* )}
|
|
81
|
+
* {error && <p>{error}</p>}
|
|
82
|
+
* </>
|
|
83
|
+
* );
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare function usePasskeySignIn(): UsePasskeySignInResult;
|
|
88
|
+
//# sourceMappingURL=usePasskey.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkey Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for WebAuthn passkey registration and authentication.
|
|
5
|
+
* Uses dynamic imports for @simplewebauthn/browser to avoid SSR issues.
|
|
6
|
+
*/
|
|
7
|
+
'use client';
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
/**
|
|
10
|
+
* Hook for passkey registration (sign-up and security settings).
|
|
11
|
+
*
|
|
12
|
+
* @returns Register function, loading state, error, and browser support flag
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* function AddPasskey() {
|
|
17
|
+
* const { register, isLoading, error, supported } = usePasskeyRegister();
|
|
18
|
+
*
|
|
19
|
+
* if (!supported) return <p>Passkeys are not supported in this browser.</p>;
|
|
20
|
+
*
|
|
21
|
+
* const handleAdd = async () => {
|
|
22
|
+
* const result = await register({ deviceName: 'My Laptop' });
|
|
23
|
+
* if (result) {
|
|
24
|
+
* // Passkey registered successfully
|
|
25
|
+
* }
|
|
26
|
+
* };
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function usePasskeyRegister() {
|
|
31
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
32
|
+
const [error, setError] = useState(null);
|
|
33
|
+
const [supported, setSupported] = useState(false);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setSupported(!!window.PublicKeyCredential);
|
|
36
|
+
}, []);
|
|
37
|
+
const register = async (options) => {
|
|
38
|
+
if (!supported) {
|
|
39
|
+
setError('Passkeys are not supported in this browser');
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
setIsLoading(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
// Step 1: Get registration options from the server
|
|
46
|
+
const optionsResponse = await fetch('/api/auth/passkey/register-options', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
credentials: 'include',
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
email: options?.email,
|
|
52
|
+
name: options?.name,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
const optionsJson = await optionsResponse.json();
|
|
56
|
+
if (!optionsResponse.ok) {
|
|
57
|
+
const errorData = optionsJson;
|
|
58
|
+
setError(errorData.error ?? 'Failed to get registration options');
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
// Step 2: Start browser WebAuthn registration ceremony
|
|
62
|
+
// Server wraps options: { options: { challenge, ... } }
|
|
63
|
+
// @simplewebauthn/browser v13+ expects { optionsJSON: ... }
|
|
64
|
+
const { startRegistration } = await import('@simplewebauthn/browser');
|
|
65
|
+
const optionsData = optionsJson;
|
|
66
|
+
const attestationResponse = await startRegistration({ optionsJSON: optionsData.options });
|
|
67
|
+
// Step 3: Verify with the server
|
|
68
|
+
const verifyResponse = await fetch('/api/auth/passkey/register-verify', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
credentials: 'include',
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
attestationResponse,
|
|
74
|
+
deviceName: options?.deviceName,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const verifyJson = await verifyResponse.json();
|
|
78
|
+
if (!verifyResponse.ok) {
|
|
79
|
+
const errorData = verifyJson;
|
|
80
|
+
setError(errorData.error ?? 'Failed to verify passkey registration');
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const result = verifyJson;
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
// Handle user cancellation of the WebAuthn ceremony
|
|
88
|
+
if (err instanceof DOMException && err.name === 'NotAllowedError') {
|
|
89
|
+
setError('Passkey registration was cancelled');
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
93
|
+
setError(message);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
setIsLoading(false);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
register,
|
|
102
|
+
isLoading,
|
|
103
|
+
error,
|
|
104
|
+
supported,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Hook for passkey authentication (login page).
|
|
109
|
+
*
|
|
110
|
+
* @returns Sign-in function, loading state, error, and browser support flag
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```tsx
|
|
114
|
+
* function LoginPage() {
|
|
115
|
+
* const { signIn, isLoading, error, supported } = usePasskeySignIn();
|
|
116
|
+
*
|
|
117
|
+
* const handlePasskeyLogin = async () => {
|
|
118
|
+
* const success = await signIn();
|
|
119
|
+
* if (success) {
|
|
120
|
+
* router.push('/admin');
|
|
121
|
+
* }
|
|
122
|
+
* };
|
|
123
|
+
*
|
|
124
|
+
* return (
|
|
125
|
+
* <>
|
|
126
|
+
* {supported && (
|
|
127
|
+
* <button onClick={handlePasskeyLogin} disabled={isLoading}>
|
|
128
|
+
* Sign in with Passkey
|
|
129
|
+
* </button>
|
|
130
|
+
* )}
|
|
131
|
+
* {error && <p>{error}</p>}
|
|
132
|
+
* </>
|
|
133
|
+
* );
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export function usePasskeySignIn() {
|
|
138
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
139
|
+
const [error, setError] = useState(null);
|
|
140
|
+
const [supported, setSupported] = useState(false);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
setSupported(!!window.PublicKeyCredential);
|
|
143
|
+
}, []);
|
|
144
|
+
const signIn = async () => {
|
|
145
|
+
if (!supported) {
|
|
146
|
+
setError('Passkeys are not supported in this browser');
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
setIsLoading(true);
|
|
151
|
+
setError(null);
|
|
152
|
+
// Step 1: Get authentication options from the server
|
|
153
|
+
const optionsResponse = await fetch('/api/auth/passkey/authenticate-options', {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
credentials: 'include',
|
|
156
|
+
});
|
|
157
|
+
const optionsJson = await optionsResponse.json();
|
|
158
|
+
if (!optionsResponse.ok) {
|
|
159
|
+
const errorData = optionsJson;
|
|
160
|
+
setError(errorData.error ?? 'Failed to get authentication options');
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
// Step 2: Start browser WebAuthn authentication ceremony
|
|
164
|
+
// @simplewebauthn/browser v13+ expects { optionsJSON: ... }
|
|
165
|
+
const { startAuthentication } = await import('@simplewebauthn/browser');
|
|
166
|
+
const authOptionsData = optionsJson;
|
|
167
|
+
const assertionResponse = await startAuthentication({ optionsJSON: authOptionsData.options });
|
|
168
|
+
// Step 3: Verify with the server
|
|
169
|
+
const verifyResponse = await fetch('/api/auth/passkey/authenticate-verify', {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'Content-Type': 'application/json' },
|
|
172
|
+
credentials: 'include',
|
|
173
|
+
body: JSON.stringify({ authenticationResponse: assertionResponse }),
|
|
174
|
+
});
|
|
175
|
+
const verifyJson = await verifyResponse.json();
|
|
176
|
+
if (!verifyResponse.ok) {
|
|
177
|
+
const errorData = verifyJson;
|
|
178
|
+
setError(errorData.error ?? 'Passkey authentication failed');
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
// Handle user cancellation of the WebAuthn ceremony
|
|
185
|
+
if (err instanceof DOMException && err.name === 'NotAllowedError') {
|
|
186
|
+
setError('Passkey authentication was cancelled');
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
190
|
+
setError(message);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
setIsLoading(false);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
signIn,
|
|
199
|
+
isLoading,
|
|
200
|
+
error,
|
|
201
|
+
supported,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSession.d.ts","sourceRoot":"","sources":["../../src/react/useSession.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,
|
|
1
|
+
{"version":3,"file":"useSession.d.ts","sourceRoot":"","sources":["../../src/react/useSession.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAiC/C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,IAAI,gBAAgB,CA2D7C"}
|
package/dist/react/useSession.js
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
'use client';
|
|
8
8
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
9
9
|
import { z } from 'zod/v4';
|
|
10
|
-
//
|
|
10
|
+
// Zod validates the required fields; .passthrough() preserves the rest.
|
|
11
|
+
// The API returns JSON-serialized session/user objects (Dates as ISO strings).
|
|
12
|
+
// z.infer output has an index signature incompatible with the concrete
|
|
13
|
+
// AuthSession interface, so we convert via the helper below.
|
|
11
14
|
const AuthSessionSchema = z.object({
|
|
12
15
|
session: z
|
|
13
16
|
.object({
|
|
@@ -21,8 +24,18 @@ const AuthSessionSchema = z.object({
|
|
|
21
24
|
email: z.string(),
|
|
22
25
|
name: z.string().nullable().optional(),
|
|
23
26
|
})
|
|
24
|
-
.passthrough(),
|
|
27
|
+
.passthrough(),
|
|
25
28
|
});
|
|
29
|
+
/**
|
|
30
|
+
* Narrow Zod-validated API response data to AuthSession.
|
|
31
|
+
*
|
|
32
|
+
* The cast is safe because: (1) Zod verified the required fields on both
|
|
33
|
+
* session and user, (2) .passthrough() preserves all other properties, and
|
|
34
|
+
* (3) the API serializes full Session + User rows (Dates become ISO strings).
|
|
35
|
+
*/
|
|
36
|
+
function toAuthSession(validated) {
|
|
37
|
+
return validated;
|
|
38
|
+
}
|
|
26
39
|
/**
|
|
27
40
|
* Hook to get the current session
|
|
28
41
|
*
|
|
@@ -66,9 +79,7 @@ export function useSession() {
|
|
|
66
79
|
}
|
|
67
80
|
const json = await response.json();
|
|
68
81
|
const validated = AuthSessionSchema.parse(json);
|
|
69
|
-
|
|
70
|
-
// The API returns serialized data (Dates as strings), so we cast to expected type
|
|
71
|
-
setData(validated);
|
|
82
|
+
setData(toAuthSession(validated));
|
|
72
83
|
}
|
|
73
84
|
catch (err) {
|
|
74
85
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
@@ -10,9 +10,15 @@ export interface SignInInput {
|
|
|
10
10
|
}
|
|
11
11
|
export interface UseSignInResult {
|
|
12
12
|
signIn: (input: SignInInput) => Promise<{
|
|
13
|
-
success:
|
|
14
|
-
user
|
|
15
|
-
|
|
13
|
+
success: true;
|
|
14
|
+
user: User;
|
|
15
|
+
} | {
|
|
16
|
+
success: false;
|
|
17
|
+
error: string;
|
|
18
|
+
} | {
|
|
19
|
+
success: false;
|
|
20
|
+
requiresMfa: true;
|
|
21
|
+
mfaUserId: string;
|
|
16
22
|
}>;
|
|
17
23
|
isLoading: boolean;
|
|
18
24
|
error: Error | null;
|
|
@@ -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,
|
|
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"}
|