dauth-context-react 6.4.0 → 6.6.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.
@@ -9,6 +9,8 @@ import {
9
9
  IPasskeyRegistrationFinishResponse,
10
10
  IDeletePasskeyResponse,
11
11
  IUploadAvatarResponse,
12
+ IPasskeyAuthStartResponse,
13
+ IPasskeyAuthFinishResponse,
12
14
  } from './interfaces/dauth.api.responses';
13
15
 
14
16
  function getCsrfToken(): string {
@@ -94,14 +96,11 @@ export async function deleteAccountAPI(
94
96
  export async function getPasskeyCredentialsAPI(
95
97
  basePath: string
96
98
  ): Promise<IPasskeyCredentialsResponse> {
97
- const response = await fetch(
98
- `${basePath}/passkey/credentials`,
99
- {
100
- method: 'GET',
101
- headers: { 'X-CSRF-Token': getCsrfToken() },
102
- credentials: 'include',
103
- }
104
- );
99
+ const response = await fetch(`${basePath}/passkey/credentials`, {
100
+ method: 'GET',
101
+ headers: { 'X-CSRF-Token': getCsrfToken() },
102
+ credentials: 'include',
103
+ });
105
104
  const data = await response.json();
106
105
  return { response, data };
107
106
  }
@@ -109,17 +108,14 @@ export async function getPasskeyCredentialsAPI(
109
108
  export async function startPasskeyRegistrationAPI(
110
109
  basePath: string
111
110
  ): Promise<IPasskeyRegistrationStartResponse> {
112
- const response = await fetch(
113
- `${basePath}/passkey/register/start`,
114
- {
115
- method: 'POST',
116
- headers: {
117
- 'Content-Type': 'application/json',
118
- 'X-CSRF-Token': getCsrfToken(),
119
- },
120
- credentials: 'include',
121
- }
122
- );
111
+ const response = await fetch(`${basePath}/passkey/register/start`, {
112
+ method: 'POST',
113
+ headers: {
114
+ 'Content-Type': 'application/json',
115
+ 'X-CSRF-Token': getCsrfToken(),
116
+ },
117
+ credentials: 'include',
118
+ });
123
119
  const data = await response.json();
124
120
  return { response, data };
125
121
  }
@@ -128,18 +124,45 @@ export async function finishPasskeyRegistrationAPI(
128
124
  basePath: string,
129
125
  body: { credential: unknown; name?: string }
130
126
  ): Promise<IPasskeyRegistrationFinishResponse> {
131
- const response = await fetch(
132
- `${basePath}/passkey/register/finish`,
133
- {
134
- method: 'POST',
135
- headers: {
136
- 'Content-Type': 'application/json',
137
- 'X-CSRF-Token': getCsrfToken(),
138
- },
139
- credentials: 'include',
140
- body: JSON.stringify(body),
141
- }
142
- );
127
+ const response = await fetch(`${basePath}/passkey/register/finish`, {
128
+ method: 'POST',
129
+ headers: {
130
+ 'Content-Type': 'application/json',
131
+ 'X-CSRF-Token': getCsrfToken(),
132
+ },
133
+ credentials: 'include',
134
+ body: JSON.stringify(body),
135
+ });
136
+ const data = await response.json();
137
+ return { response, data };
138
+ }
139
+
140
+ export async function startPasskeyAuthAPI(
141
+ basePath: string
142
+ ): Promise<IPasskeyAuthStartResponse> {
143
+ const response = await fetch(`${basePath}/passkey/auth-start`, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ },
148
+ credentials: 'include',
149
+ });
150
+ const data = await response.json();
151
+ return { response, data };
152
+ }
153
+
154
+ export async function finishPasskeyAuthAPI(
155
+ basePath: string,
156
+ body: { credential: unknown; sessionId: string }
157
+ ): Promise<IPasskeyAuthFinishResponse> {
158
+ const response = await fetch(`${basePath}/passkey/auth-finish`, {
159
+ method: 'POST',
160
+ headers: {
161
+ 'Content-Type': 'application/json',
162
+ },
163
+ credentials: 'include',
164
+ body: JSON.stringify(body),
165
+ });
143
166
  const data = await response.json();
144
167
  return { response, data };
145
168
  }
@@ -70,6 +70,24 @@ export interface IDeletePasskeyResponse {
70
70
  };
71
71
  }
72
72
 
73
+ export interface IPasskeyAuthStartResponse {
74
+ response: Response;
75
+ data: {
76
+ status: string;
77
+ options?: Record<string, unknown>;
78
+ sessionId?: string;
79
+ };
80
+ }
81
+
82
+ export interface IPasskeyAuthFinishResponse {
83
+ response: Response;
84
+ data: {
85
+ status: string;
86
+ redirect?: string;
87
+ message?: string;
88
+ };
89
+ }
90
+
73
91
  export interface IUploadAvatarResponse {
74
92
  response: Response;
75
93
  data: {
@@ -12,15 +12,9 @@ function checkIsLocalhost(): boolean {
12
12
  return (
13
13
  hostname === 'localhost' ||
14
14
  hostname === '[::1]' ||
15
- /^127(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/.test(
16
- hostname
17
- ) ||
18
- /^192\.168(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){2}$/.test(
19
- hostname
20
- ) ||
21
- /^10(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/.test(
22
- hostname
23
- )
15
+ /^127(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/.test(hostname) ||
16
+ /^192\.168(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){2}$/.test(hostname) ||
17
+ /^10(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/.test(hostname)
24
18
  );
25
19
  }
26
20
 
package/src/index.tsx CHANGED
@@ -21,6 +21,7 @@ import type {
21
21
  IModalTheme,
22
22
  IPasskeyCredential,
23
23
  DauthProfileModalProps,
24
+ ThemeMode,
24
25
  } from './interfaces';
25
26
 
26
27
  export { DauthProfileModal } from './DauthProfileModal';
@@ -32,6 +33,7 @@ export type {
32
33
  IModalTheme,
33
34
  IPasskeyCredential,
34
35
  DauthProfileModalProps,
36
+ ThemeMode,
35
37
  };
36
38
 
37
39
  const defaultOnError = (error: Error) => console.error(error);
@@ -92,10 +94,12 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
92
94
  telPrefix,
93
95
  telSuffix,
94
96
  language,
97
+ theme,
95
98
  avatar,
96
99
  birthDate,
97
100
  country,
98
101
  metadata,
102
+ customFields,
99
103
  } = fields;
100
104
  const user = {
101
105
  name,
@@ -104,10 +108,12 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
104
108
  telPrefix,
105
109
  telSuffix,
106
110
  language,
111
+ theme,
107
112
  avatar,
108
113
  birthDate,
109
114
  country,
110
115
  metadata,
116
+ customFields,
111
117
  } as Partial<IDauthUser>;
112
118
  return action.updateUserAction(ctx, user);
113
119
  },
@@ -140,6 +146,11 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
140
146
  [ctx]
141
147
  );
142
148
 
149
+ const loginWithPasskey = useCallback(
150
+ () => action.loginWithPasskeyAction(ctx),
151
+ [ctx]
152
+ );
153
+
143
154
  const memoProvider = useMemo(
144
155
  () => ({
145
156
  ...dauthState,
@@ -151,6 +162,7 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
151
162
  registerPasskey,
152
163
  deletePasskeyCredential,
153
164
  uploadAvatar,
165
+ loginWithPasskey,
154
166
  }),
155
167
  [
156
168
  dauthState,
@@ -162,6 +174,7 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
162
174
  registerPasskey,
163
175
  deletePasskeyCredential,
164
176
  uploadAvatar,
177
+ loginWithPasskey,
165
178
  ]
166
179
  );
167
180
 
@@ -6,6 +6,7 @@ const initialDauthState: IDauthState = {
6
6
  (typeof window !== 'undefined'
7
7
  ? window.document.documentElement.getAttribute('lang')
8
8
  : null) || 'es',
9
+ theme: 'system',
9
10
  } as IDauthUser,
10
11
  domain: {} as IDauthDomainState,
11
12
  isLoading: true,
@@ -18,6 +19,7 @@ const initialDauthState: IDauthState = {
18
19
  registerPasskey: () => Promise.resolve(null),
19
20
  deletePasskeyCredential: () => Promise.resolve(false),
20
21
  uploadAvatar: () => Promise.resolve(false),
22
+ loginWithPasskey: () => Promise.resolve(false),
21
23
  };
22
24
 
23
25
  export default initialDauthState;
package/src/interfaces.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type ThemeMode = 'dark' | 'light' | 'system';
2
+
1
3
  export interface IDauthUser {
2
4
  _id: string;
3
5
  name: string;
@@ -6,6 +8,7 @@ export interface IDauthUser {
6
8
  email: string;
7
9
  isVerified: boolean;
8
10
  language: string;
11
+ theme: ThemeMode;
9
12
  avatar: {
10
13
  id: string;
11
14
  url: string;
@@ -83,13 +86,10 @@ export interface IDauthState {
83
86
  updateUser: (fields: Partial<IDauthUser>) => Promise<boolean>;
84
87
  deleteAccount: () => Promise<boolean>;
85
88
  getPasskeyCredentials: () => Promise<IPasskeyCredential[]>;
86
- registerPasskey: (
87
- name?: string
88
- ) => Promise<IPasskeyCredential | null>;
89
- deletePasskeyCredential: (
90
- credentialId: string
91
- ) => Promise<boolean>;
89
+ registerPasskey: (name?: string) => Promise<IPasskeyCredential | null>;
90
+ deletePasskeyCredential: (credentialId: string) => Promise<boolean>;
92
91
  uploadAvatar: (file: File) => Promise<boolean>;
92
+ loginWithPasskey: () => Promise<boolean>;
93
93
  }
94
94
 
95
95
  export interface DauthProfileModalProps {
@@ -9,9 +9,11 @@ import {
9
9
  finishPasskeyRegistrationAPI,
10
10
  deletePasskeyCredentialAPI,
11
11
  uploadAvatarAPI,
12
+ startPasskeyAuthAPI,
13
+ finishPasskeyAuthAPI,
12
14
  } from '../api/dauth.api';
13
15
  import type { IPasskeyCredential } from '../api/interfaces/dauth.api.responses';
14
- import { createPasskeyCredential } from '../webauthn';
16
+ import { createPasskeyCredential, getPasskeyCredential } from '../webauthn';
15
17
  import { IDauthDomainState, IDauthUser } from '../interfaces';
16
18
  import * as DauthTypes from './dauth.types';
17
19
 
@@ -182,38 +184,31 @@ export async function registerPasskeyAction(
182
184
  const { authProxyPath, onError } = ctx;
183
185
  try {
184
186
  // Step 1: Get registration options from server
185
- const startResult =
186
- await startPasskeyRegistrationAPI(authProxyPath);
187
+ const startResult = await startPasskeyRegistrationAPI(authProxyPath);
187
188
  if (startResult.response.status !== 200) {
188
189
  onError(new Error('Failed to start passkey registration'));
189
190
  return null;
190
191
  }
191
192
 
192
193
  // Step 2: Execute WebAuthn ceremony in the browser
193
- const credential = await createPasskeyCredential(
194
- startResult.data
195
- );
194
+ const credential = await createPasskeyCredential(startResult.data);
196
195
 
197
196
  // Step 3: Send the credential back to the server
198
- const finishResult = await finishPasskeyRegistrationAPI(
199
- authProxyPath,
200
- { credential, name }
201
- );
197
+ const finishResult = await finishPasskeyRegistrationAPI(authProxyPath, {
198
+ credential,
199
+ name,
200
+ });
202
201
  if (
203
202
  finishResult.response.status === 200 ||
204
203
  finishResult.response.status === 201
205
204
  ) {
206
205
  return finishResult.data.credential;
207
206
  }
208
- onError(
209
- new Error('Failed to finish passkey registration')
210
- );
207
+ onError(new Error('Failed to finish passkey registration'));
211
208
  return null;
212
209
  } catch (error) {
213
210
  onError(
214
- error instanceof Error
215
- ? error
216
- : new Error('Passkey registration error')
211
+ error instanceof Error ? error : new Error('Passkey registration error')
217
212
  );
218
213
  return null;
219
214
  }
@@ -231,11 +226,7 @@ export async function deletePasskeyCredentialAction(
231
226
  );
232
227
  return result.response.status === 200;
233
228
  } catch (error) {
234
- onError(
235
- error instanceof Error
236
- ? error
237
- : new Error('Delete passkey error')
238
- );
229
+ onError(error instanceof Error ? error : new Error('Delete passkey error'));
239
230
  return false;
240
231
  }
241
232
  }
@@ -254,18 +245,84 @@ export async function uploadAvatarAction(
254
245
  });
255
246
  return true;
256
247
  }
257
- onError(
258
- new Error(
259
- 'Avatar upload error: ' + result.data.message
260
- )
248
+ onError(new Error('Avatar upload error: ' + result.data.message));
249
+ return false;
250
+ } catch (error) {
251
+ onError(error instanceof Error ? error : new Error('Avatar upload error'));
252
+ return false;
253
+ }
254
+ }
255
+
256
+ export async function loginWithPasskeyAction(
257
+ ctx: ActionContext
258
+ ): Promise<boolean> {
259
+ const { dispatch, authProxyPath, onError } = ctx;
260
+ dispatch({
261
+ type: DauthTypes.SET_IS_LOADING,
262
+ payload: { isLoading: true },
263
+ });
264
+
265
+ try {
266
+ // Step 1: Get authentication options
267
+ const startResult = await startPasskeyAuthAPI(authProxyPath);
268
+ if (startResult.data.status !== 'success' || !startResult.data.options) {
269
+ dispatch({
270
+ type: DauthTypes.SET_IS_LOADING,
271
+ payload: { isLoading: false },
272
+ });
273
+ return false;
274
+ }
275
+
276
+ // Step 2: Run WebAuthn ceremony
277
+ const credential = await getPasskeyCredential(
278
+ startResult.data.options as Record<string, any>
261
279
  );
280
+
281
+ // Step 3: Verify credential with server
282
+ const finishResult = await finishPasskeyAuthAPI(authProxyPath, {
283
+ credential,
284
+ sessionId: startResult.data.sessionId!,
285
+ });
286
+
287
+ if (
288
+ finishResult.data.status === 'login-success' &&
289
+ finishResult.data.redirect
290
+ ) {
291
+ // The redirect URL contains ?code=... for the auth code exchange
292
+ const code = new URL(finishResult.data.redirect).searchParams.get('code');
293
+ if (code) {
294
+ const exchangeResult = await exchangeCodeAPI(authProxyPath, code);
295
+ if (exchangeResult.response.status === 200) {
296
+ dispatch({
297
+ type: DauthTypes.LOGIN,
298
+ payload: {
299
+ user: exchangeResult.data.user,
300
+ domain: exchangeResult.data.domain,
301
+ isAuthenticated: true,
302
+ },
303
+ });
304
+ dispatch({
305
+ type: DauthTypes.SET_IS_LOADING,
306
+ payload: { isLoading: false },
307
+ });
308
+ return true;
309
+ }
310
+ }
311
+ }
312
+
313
+ dispatch({
314
+ type: DauthTypes.SET_IS_LOADING,
315
+ payload: { isLoading: false },
316
+ });
262
317
  return false;
263
318
  } catch (error) {
264
319
  onError(
265
- error instanceof Error
266
- ? error
267
- : new Error('Avatar upload error')
320
+ error instanceof Error ? error : new Error('Passkey authentication error')
268
321
  );
322
+ dispatch({
323
+ type: DauthTypes.SET_IS_LOADING,
324
+ payload: { isLoading: false },
325
+ });
269
326
  return false;
270
327
  }
271
328
  }
package/src/webauthn.ts CHANGED
@@ -3,12 +3,8 @@
3
3
  * Uses the raw Web Credentials API — no external dependencies.
4
4
  */
5
5
 
6
- export function base64urlToBuffer(
7
- base64url: string
8
- ): ArrayBuffer {
9
- const base64 = base64url
10
- .replace(/-/g, '+')
11
- .replace(/_/g, '/');
6
+ export function base64urlToBuffer(base64url: string): ArrayBuffer {
7
+ const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
12
8
  const pad = base64.length % 4;
13
9
  const padded = pad ? base64 + '='.repeat(4 - pad) : base64;
14
10
  const binary = atob(padded);
@@ -19,9 +15,7 @@ export function base64urlToBuffer(
19
15
  return bytes.buffer;
20
16
  }
21
17
 
22
- export function bufferToBase64url(
23
- buffer: ArrayBuffer
24
- ): string {
18
+ export function bufferToBase64url(buffer: ArrayBuffer): string {
25
19
  const bytes = new Uint8Array(buffer);
26
20
  let binary = '';
27
21
  for (let i = 0; i < bytes.length; i++) {
@@ -44,17 +38,16 @@ export async function createPasskeyCredential(
44
38
  ): Promise<Record<string, any>> {
45
39
  const publicKey = {
46
40
  ...options,
41
+ rp: { ...options.rp, id: window.location.hostname },
47
42
  challenge: base64urlToBuffer(options.challenge),
48
43
  user: {
49
44
  ...options.user,
50
45
  id: base64urlToBuffer(options.user.id),
51
46
  },
52
- excludeCredentials: (options.excludeCredentials ?? []).map(
53
- (c: any) => ({
54
- ...c,
55
- id: base64urlToBuffer(c.id),
56
- })
57
- ),
47
+ excludeCredentials: (options.excludeCredentials ?? []).map((c: any) => ({
48
+ ...c,
49
+ id: base64urlToBuffer(c.id),
50
+ })),
58
51
  } as PublicKeyCredentialCreationOptions;
59
52
 
60
53
  const credential = (await navigator.credentials.create({
@@ -65,34 +58,26 @@ export async function createPasskeyCredential(
65
58
  throw new Error('Passkey registration was cancelled');
66
59
  }
67
60
 
68
- const attestation =
69
- credential.response as AuthenticatorAttestationResponse;
61
+ const attestation = credential.response as AuthenticatorAttestationResponse;
70
62
 
71
63
  return {
72
64
  id: credential.id,
73
65
  rawId: bufferToBase64url(credential.rawId),
74
66
  type: credential.type,
75
67
  response: {
76
- clientDataJSON: bufferToBase64url(
77
- attestation.clientDataJSON
78
- ),
79
- attestationObject: bufferToBase64url(
80
- attestation.attestationObject
81
- ),
68
+ clientDataJSON: bufferToBase64url(attestation.clientDataJSON),
69
+ attestationObject: bufferToBase64url(attestation.attestationObject),
82
70
  ...(attestation.getTransports
83
71
  ? { transports: attestation.getTransports() }
84
72
  : {}),
85
73
  ...(attestation.getPublicKeyAlgorithm
86
74
  ? {
87
- publicKeyAlgorithm:
88
- attestation.getPublicKeyAlgorithm(),
75
+ publicKeyAlgorithm: attestation.getPublicKeyAlgorithm(),
89
76
  }
90
77
  : {}),
91
78
  ...(attestation.getPublicKey
92
79
  ? {
93
- publicKey: bufferToBase64url(
94
- attestation.getPublicKey()!
95
- ),
80
+ publicKey: bufferToBase64url(attestation.getPublicKey()!),
96
81
  }
97
82
  : {}),
98
83
  ...(attestation.getAuthenticatorData
@@ -103,9 +88,54 @@ export async function createPasskeyCredential(
103
88
  }
104
89
  : {}),
105
90
  },
106
- clientExtensionResults:
107
- credential.getClientExtensionResults(),
108
- authenticatorAttachment:
109
- credential.authenticatorAttachment ?? undefined,
91
+ clientExtensionResults: credential.getClientExtensionResults(),
92
+ authenticatorAttachment: credential.authenticatorAttachment ?? undefined,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Converts server-provided authentication options (base64url strings)
98
+ * into the format expected by navigator.credentials.get(),
99
+ * executes the WebAuthn ceremony, and serializes the response
100
+ * back to JSON with base64url-encoded ArrayBuffers.
101
+ */
102
+ export async function getPasskeyCredential(
103
+ options: Record<string, any>
104
+ ): Promise<Record<string, any>> {
105
+ const publicKey: PublicKeyCredentialRequestOptions = {
106
+ challenge: base64urlToBuffer(options.challenge),
107
+ timeout: options.timeout,
108
+ rpId: window.location.hostname,
109
+ userVerification: options.userVerification || 'preferred',
110
+ allowCredentials: (options.allowCredentials || []).map(
111
+ (c: { id: string; type: string; transports?: string[] }) => ({
112
+ ...c,
113
+ id: base64urlToBuffer(c.id),
114
+ })
115
+ ),
116
+ };
117
+
118
+ const credential = (await navigator.credentials.get({
119
+ publicKey,
120
+ })) as PublicKeyCredential;
121
+
122
+ if (!credential) throw new Error('No credential returned');
123
+
124
+ const assertion = credential.response as AuthenticatorAssertionResponse;
125
+
126
+ return {
127
+ id: credential.id,
128
+ rawId: bufferToBase64url(credential.rawId),
129
+ type: credential.type,
130
+ response: {
131
+ clientDataJSON: bufferToBase64url(assertion.clientDataJSON),
132
+ authenticatorData: bufferToBase64url(assertion.authenticatorData),
133
+ signature: bufferToBase64url(assertion.signature),
134
+ userHandle: assertion.userHandle
135
+ ? bufferToBase64url(assertion.userHandle)
136
+ : null,
137
+ },
138
+ clientExtensionResults: credential.getClientExtensionResults(),
139
+ authenticatorAttachment: credential.authenticatorAttachment,
110
140
  };
111
141
  }