dauth-context-react 6.1.0 → 6.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.
@@ -4,6 +4,10 @@ import {
4
4
  ISessionResponse,
5
5
  IUpdateUserResponse,
6
6
  IDeleteAccountResponse,
7
+ IPasskeyCredentialsResponse,
8
+ IPasskeyRegistrationStartResponse,
9
+ IPasskeyRegistrationFinishResponse,
10
+ IDeletePasskeyResponse,
7
11
  } from './interfaces/dauth.api.responses';
8
12
 
9
13
  function getCsrfToken(): string {
@@ -85,3 +89,72 @@ export async function deleteAccountAPI(
85
89
  const data = await response.json();
86
90
  return { response, data };
87
91
  }
92
+
93
+ export async function getPasskeyCredentialsAPI(
94
+ basePath: string
95
+ ): Promise<IPasskeyCredentialsResponse> {
96
+ const response = await fetch(
97
+ `${basePath}/passkey/credentials`,
98
+ {
99
+ method: 'GET',
100
+ headers: { 'X-CSRF-Token': getCsrfToken() },
101
+ credentials: 'include',
102
+ }
103
+ );
104
+ const data = await response.json();
105
+ return { response, data };
106
+ }
107
+
108
+ export async function startPasskeyRegistrationAPI(
109
+ basePath: string
110
+ ): Promise<IPasskeyRegistrationStartResponse> {
111
+ const response = await fetch(
112
+ `${basePath}/passkey/register/start`,
113
+ {
114
+ method: 'POST',
115
+ headers: {
116
+ 'Content-Type': 'application/json',
117
+ 'X-CSRF-Token': getCsrfToken(),
118
+ },
119
+ credentials: 'include',
120
+ }
121
+ );
122
+ const data = await response.json();
123
+ return { response, data };
124
+ }
125
+
126
+ export async function finishPasskeyRegistrationAPI(
127
+ basePath: string,
128
+ body: { credential: unknown; name?: string }
129
+ ): Promise<IPasskeyRegistrationFinishResponse> {
130
+ const response = await fetch(
131
+ `${basePath}/passkey/register/finish`,
132
+ {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ 'X-CSRF-Token': getCsrfToken(),
137
+ },
138
+ credentials: 'include',
139
+ body: JSON.stringify(body),
140
+ }
141
+ );
142
+ const data = await response.json();
143
+ return { response, data };
144
+ }
145
+
146
+ export async function deletePasskeyCredentialAPI(
147
+ basePath: string,
148
+ credentialId: string
149
+ ): Promise<IDeletePasskeyResponse> {
150
+ const response = await fetch(
151
+ `${basePath}/passkey/credentials/${credentialId}`,
152
+ {
153
+ method: 'DELETE',
154
+ headers: { 'X-CSRF-Token': getCsrfToken() },
155
+ credentials: 'include',
156
+ }
157
+ );
158
+ const data = await response.json();
159
+ return { response, data };
160
+ }
@@ -33,3 +33,39 @@ export interface IDeleteAccountResponse {
33
33
  message: string;
34
34
  };
35
35
  }
36
+
37
+ export interface IPasskeyCredential {
38
+ _id: string;
39
+ name?: string;
40
+ deviceType: 'singleDevice' | 'multiDevice';
41
+ backedUp: boolean;
42
+ createdAt: string;
43
+ lastUsedAt?: string;
44
+ }
45
+
46
+ export interface IPasskeyCredentialsResponse {
47
+ response: Response;
48
+ data: {
49
+ credentials: IPasskeyCredential[];
50
+ };
51
+ }
52
+
53
+ export interface IPasskeyRegistrationStartResponse {
54
+ response: Response;
55
+ data: Record<string, unknown>;
56
+ }
57
+
58
+ export interface IPasskeyRegistrationFinishResponse {
59
+ response: Response;
60
+ data: {
61
+ credential: IPasskeyCredential;
62
+ message: string;
63
+ };
64
+ }
65
+
66
+ export interface IDeletePasskeyResponse {
67
+ response: Response;
68
+ data: {
69
+ message: string;
70
+ };
71
+ }
package/src/index.tsx CHANGED
@@ -17,7 +17,9 @@ import type {
17
17
  IDauthAuthMethods,
18
18
  IDauthUser,
19
19
  IFormField,
20
+ ICustomField,
20
21
  IModalTheme,
22
+ IPasskeyCredential,
21
23
  DauthProfileModalProps,
22
24
  } from './interfaces';
23
25
 
@@ -26,7 +28,9 @@ export type {
26
28
  IDauthProviderProps,
27
29
  IDauthAuthMethods,
28
30
  IFormField,
31
+ ICustomField,
29
32
  IModalTheme,
33
+ IPasskeyCredential,
30
34
  DauthProfileModalProps,
31
35
  };
32
36
 
@@ -115,6 +119,22 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
115
119
  [ctx]
116
120
  );
117
121
 
122
+ const getPasskeyCredentials = useCallback(
123
+ () => action.getPasskeyCredentialsAction(ctx),
124
+ [ctx]
125
+ );
126
+
127
+ const registerPasskey = useCallback(
128
+ (name?: string) => action.registerPasskeyAction(ctx, name),
129
+ [ctx]
130
+ );
131
+
132
+ const deletePasskeyCredential = useCallback(
133
+ (credentialId: string) =>
134
+ action.deletePasskeyCredentialAction(ctx, credentialId),
135
+ [ctx]
136
+ );
137
+
118
138
  const memoProvider = useMemo(
119
139
  () => ({
120
140
  ...dauthState,
@@ -122,8 +142,20 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
122
142
  logout,
123
143
  updateUser,
124
144
  deleteAccount,
145
+ getPasskeyCredentials,
146
+ registerPasskey,
147
+ deletePasskeyCredential,
125
148
  }),
126
- [dauthState, loginWithRedirect, logout, updateUser, deleteAccount]
149
+ [
150
+ dauthState,
151
+ loginWithRedirect,
152
+ logout,
153
+ updateUser,
154
+ deleteAccount,
155
+ getPasskeyCredentials,
156
+ registerPasskey,
157
+ deletePasskeyCredential,
158
+ ]
127
159
  );
128
160
 
129
161
  return (
@@ -14,6 +14,9 @@ const initialDauthState: IDauthState = {
14
14
  logout: () => {},
15
15
  updateUser: () => Promise.resolve(false),
16
16
  deleteAccount: () => Promise.resolve(false),
17
+ getPasskeyCredentials: () => Promise.resolve([]),
18
+ registerPasskey: () => Promise.resolve(null),
19
+ deletePasskeyCredential: () => Promise.resolve(false),
17
20
  };
18
21
 
19
22
  export default initialDauthState;
package/src/interfaces.ts CHANGED
@@ -16,6 +16,7 @@ export interface IDauthUser {
16
16
  birthDate?: Date;
17
17
  country?: string;
18
18
  metadata?: Record<string, unknown>;
19
+ customFields?: Record<string, string>;
19
20
  createdAt: Date;
20
21
  updatedAt: Date;
21
22
  lastLogin: Date;
@@ -36,6 +37,12 @@ export interface IFormField {
36
37
  required: boolean;
37
38
  }
38
39
 
40
+ export interface ICustomField {
41
+ key: string;
42
+ label: string;
43
+ required: boolean;
44
+ }
45
+
39
46
  export interface IModalTheme {
40
47
  accent?: string;
41
48
  accentHover?: string;
@@ -53,9 +60,19 @@ export interface IDauthDomainState {
53
60
  allowedOrigins: string[];
54
61
  authMethods?: IDauthAuthMethods;
55
62
  formFields?: IFormField[];
63
+ customFields?: ICustomField[];
56
64
  modalTheme?: IModalTheme;
57
65
  }
58
66
 
67
+ export interface IPasskeyCredential {
68
+ _id: string;
69
+ name?: string;
70
+ deviceType: 'singleDevice' | 'multiDevice';
71
+ backedUp: boolean;
72
+ createdAt: string;
73
+ lastUsedAt?: string;
74
+ }
75
+
59
76
  export interface IDauthState {
60
77
  user: IDauthUser;
61
78
  domain: IDauthDomainState;
@@ -65,11 +82,22 @@ export interface IDauthState {
65
82
  logout: () => void;
66
83
  updateUser: (fields: Partial<IDauthUser>) => Promise<boolean>;
67
84
  deleteAccount: () => Promise<boolean>;
85
+ getPasskeyCredentials: () => Promise<IPasskeyCredential[]>;
86
+ registerPasskey: (
87
+ name?: string
88
+ ) => Promise<IPasskeyCredential | null>;
89
+ deletePasskeyCredential: (
90
+ credentialId: string
91
+ ) => Promise<boolean>;
68
92
  }
69
93
 
70
94
  export interface DauthProfileModalProps {
71
95
  open: boolean;
72
96
  onClose: () => void;
97
+ /** Optional: provide a function to handle avatar upload.
98
+ * Receives a File, should return the URL string.
99
+ * If not provided, the avatar edit button is hidden. */
100
+ onAvatarUpload?: (file: File) => Promise<string>;
73
101
  }
74
102
 
75
103
  export interface IActionStatus {
@@ -4,7 +4,13 @@ import {
4
4
  logoutAPI,
5
5
  updateUserAPI,
6
6
  deleteAccountAPI,
7
+ getPasskeyCredentialsAPI,
8
+ startPasskeyRegistrationAPI,
9
+ finishPasskeyRegistrationAPI,
10
+ deletePasskeyCredentialAPI,
7
11
  } from '../api/dauth.api';
12
+ import type { IPasskeyCredential } from '../api/interfaces/dauth.api.responses';
13
+ import { createPasskeyCredential } from '../webauthn';
8
14
  import { IDauthDomainState, IDauthUser } from '../interfaces';
9
15
  import * as DauthTypes from './dauth.types';
10
16
 
@@ -148,6 +154,91 @@ export async function deleteAccountAction(
148
154
  }
149
155
  }
150
156
 
157
+ export async function getPasskeyCredentialsAction(
158
+ ctx: ActionContext
159
+ ): Promise<IPasskeyCredential[]> {
160
+ const { authProxyPath, onError } = ctx;
161
+ try {
162
+ const result = await getPasskeyCredentialsAPI(authProxyPath);
163
+ if (result.response.status === 200) {
164
+ return result.data.credentials ?? [];
165
+ }
166
+ return [];
167
+ } catch (error) {
168
+ onError(
169
+ error instanceof Error
170
+ ? error
171
+ : new Error('Get passkey credentials error')
172
+ );
173
+ return [];
174
+ }
175
+ }
176
+
177
+ export async function registerPasskeyAction(
178
+ ctx: ActionContext,
179
+ name?: string
180
+ ): Promise<IPasskeyCredential | null> {
181
+ const { authProxyPath, onError } = ctx;
182
+ try {
183
+ // Step 1: Get registration options from server
184
+ const startResult =
185
+ await startPasskeyRegistrationAPI(authProxyPath);
186
+ if (startResult.response.status !== 200) {
187
+ onError(new Error('Failed to start passkey registration'));
188
+ return null;
189
+ }
190
+
191
+ // Step 2: Execute WebAuthn ceremony in the browser
192
+ const credential = await createPasskeyCredential(
193
+ startResult.data
194
+ );
195
+
196
+ // Step 3: Send the credential back to the server
197
+ const finishResult = await finishPasskeyRegistrationAPI(
198
+ authProxyPath,
199
+ { credential, name }
200
+ );
201
+ if (
202
+ finishResult.response.status === 200 ||
203
+ finishResult.response.status === 201
204
+ ) {
205
+ return finishResult.data.credential;
206
+ }
207
+ onError(
208
+ new Error('Failed to finish passkey registration')
209
+ );
210
+ return null;
211
+ } catch (error) {
212
+ onError(
213
+ error instanceof Error
214
+ ? error
215
+ : new Error('Passkey registration error')
216
+ );
217
+ return null;
218
+ }
219
+ }
220
+
221
+ export async function deletePasskeyCredentialAction(
222
+ ctx: ActionContext,
223
+ credentialId: string
224
+ ): Promise<boolean> {
225
+ const { authProxyPath, onError } = ctx;
226
+ try {
227
+ const result = await deletePasskeyCredentialAPI(
228
+ authProxyPath,
229
+ credentialId
230
+ );
231
+ return result.response.status === 200;
232
+ } catch (error) {
233
+ onError(
234
+ error instanceof Error
235
+ ? error
236
+ : new Error('Delete passkey error')
237
+ );
238
+ return false;
239
+ }
240
+ }
241
+
151
242
  export const resetUser = (dispatch: React.Dispatch<any>) => {
152
243
  return dispatch({
153
244
  type: DauthTypes.LOGIN,
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Minimal WebAuthn helpers for passkey registration.
3
+ * Uses the raw Web Credentials API — no external dependencies.
4
+ */
5
+
6
+ export function base64urlToBuffer(
7
+ base64url: string
8
+ ): ArrayBuffer {
9
+ const base64 = base64url
10
+ .replace(/-/g, '+')
11
+ .replace(/_/g, '/');
12
+ const pad = base64.length % 4;
13
+ const padded = pad ? base64 + '='.repeat(4 - pad) : base64;
14
+ const binary = atob(padded);
15
+ const bytes = new Uint8Array(binary.length);
16
+ for (let i = 0; i < binary.length; i++) {
17
+ bytes[i] = binary.charCodeAt(i);
18
+ }
19
+ return bytes.buffer;
20
+ }
21
+
22
+ export function bufferToBase64url(
23
+ buffer: ArrayBuffer
24
+ ): string {
25
+ const bytes = new Uint8Array(buffer);
26
+ let binary = '';
27
+ for (let i = 0; i < bytes.length; i++) {
28
+ binary += String.fromCharCode(bytes[i]);
29
+ }
30
+ return btoa(binary)
31
+ .replace(/\+/g, '-')
32
+ .replace(/\//g, '_')
33
+ .replace(/=+$/, '');
34
+ }
35
+
36
+ /**
37
+ * Converts server-provided registration options (base64url strings)
38
+ * into the format expected by navigator.credentials.create(),
39
+ * executes the WebAuthn ceremony, and serializes the response
40
+ * back to JSON with base64url-encoded ArrayBuffers.
41
+ */
42
+ export async function createPasskeyCredential(
43
+ options: Record<string, any>
44
+ ): Promise<Record<string, any>> {
45
+ const publicKey = {
46
+ ...options,
47
+ challenge: base64urlToBuffer(options.challenge),
48
+ user: {
49
+ ...options.user,
50
+ id: base64urlToBuffer(options.user.id),
51
+ },
52
+ excludeCredentials: (options.excludeCredentials ?? []).map(
53
+ (c: any) => ({
54
+ ...c,
55
+ id: base64urlToBuffer(c.id),
56
+ })
57
+ ),
58
+ } as PublicKeyCredentialCreationOptions;
59
+
60
+ const credential = (await navigator.credentials.create({
61
+ publicKey,
62
+ })) as PublicKeyCredential;
63
+
64
+ if (!credential) {
65
+ throw new Error('Passkey registration was cancelled');
66
+ }
67
+
68
+ const attestation =
69
+ credential.response as AuthenticatorAttestationResponse;
70
+
71
+ return {
72
+ id: credential.id,
73
+ rawId: bufferToBase64url(credential.rawId),
74
+ type: credential.type,
75
+ response: {
76
+ clientDataJSON: bufferToBase64url(
77
+ attestation.clientDataJSON
78
+ ),
79
+ attestationObject: bufferToBase64url(
80
+ attestation.attestationObject
81
+ ),
82
+ ...(attestation.getTransports
83
+ ? { transports: attestation.getTransports() }
84
+ : {}),
85
+ ...(attestation.getPublicKeyAlgorithm
86
+ ? {
87
+ publicKeyAlgorithm:
88
+ attestation.getPublicKeyAlgorithm(),
89
+ }
90
+ : {}),
91
+ ...(attestation.getPublicKey
92
+ ? {
93
+ publicKey: bufferToBase64url(
94
+ attestation.getPublicKey()!
95
+ ),
96
+ }
97
+ : {}),
98
+ ...(attestation.getAuthenticatorData
99
+ ? {
100
+ authenticatorData: bufferToBase64url(
101
+ attestation.getAuthenticatorData()
102
+ ),
103
+ }
104
+ : {}),
105
+ },
106
+ clientExtensionResults:
107
+ credential.getClientExtensionResults(),
108
+ authenticatorAttachment:
109
+ credential.authenticatorAttachment ?? undefined,
110
+ };
111
+ }