dauth-context-react 6.0.0 → 6.2.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,6 +17,8 @@ import type {
17
17
  IDauthAuthMethods,
18
18
  IDauthUser,
19
19
  IFormField,
20
+ IModalTheme,
21
+ IPasskeyCredential,
20
22
  DauthProfileModalProps,
21
23
  } from './interfaces';
22
24
 
@@ -25,6 +27,8 @@ export type {
25
27
  IDauthProviderProps,
26
28
  IDauthAuthMethods,
27
29
  IFormField,
30
+ IModalTheme,
31
+ IPasskeyCredential,
28
32
  DauthProfileModalProps,
29
33
  };
30
34
 
@@ -113,6 +117,22 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
113
117
  [ctx]
114
118
  );
115
119
 
120
+ const getPasskeyCredentials = useCallback(
121
+ () => action.getPasskeyCredentialsAction(ctx),
122
+ [ctx]
123
+ );
124
+
125
+ const registerPasskey = useCallback(
126
+ (name?: string) => action.registerPasskeyAction(ctx, name),
127
+ [ctx]
128
+ );
129
+
130
+ const deletePasskeyCredential = useCallback(
131
+ (credentialId: string) =>
132
+ action.deletePasskeyCredentialAction(ctx, credentialId),
133
+ [ctx]
134
+ );
135
+
116
136
  const memoProvider = useMemo(
117
137
  () => ({
118
138
  ...dauthState,
@@ -120,8 +140,20 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
120
140
  logout,
121
141
  updateUser,
122
142
  deleteAccount,
143
+ getPasskeyCredentials,
144
+ registerPasskey,
145
+ deletePasskeyCredential,
123
146
  }),
124
- [dauthState, loginWithRedirect, logout, updateUser, deleteAccount]
147
+ [
148
+ dauthState,
149
+ loginWithRedirect,
150
+ logout,
151
+ updateUser,
152
+ deleteAccount,
153
+ getPasskeyCredentials,
154
+ registerPasskey,
155
+ deletePasskeyCredential,
156
+ ]
125
157
  );
126
158
 
127
159
  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
@@ -36,6 +36,16 @@ export interface IFormField {
36
36
  required: boolean;
37
37
  }
38
38
 
39
+ export interface IModalTheme {
40
+ accent?: string;
41
+ accentHover?: string;
42
+ surface?: string;
43
+ surfaceHover?: string;
44
+ textPrimary?: string;
45
+ textSecondary?: string;
46
+ border?: string;
47
+ }
48
+
39
49
  export interface IDauthDomainState {
40
50
  name: string;
41
51
  environments?: IDauthDomainEnvironment[];
@@ -43,6 +53,16 @@ export interface IDauthDomainState {
43
53
  allowedOrigins: string[];
44
54
  authMethods?: IDauthAuthMethods;
45
55
  formFields?: IFormField[];
56
+ modalTheme?: IModalTheme;
57
+ }
58
+
59
+ export interface IPasskeyCredential {
60
+ _id: string;
61
+ name?: string;
62
+ deviceType: 'singleDevice' | 'multiDevice';
63
+ backedUp: boolean;
64
+ createdAt: string;
65
+ lastUsedAt?: string;
46
66
  }
47
67
 
48
68
  export interface IDauthState {
@@ -54,11 +74,22 @@ export interface IDauthState {
54
74
  logout: () => void;
55
75
  updateUser: (fields: Partial<IDauthUser>) => Promise<boolean>;
56
76
  deleteAccount: () => Promise<boolean>;
77
+ getPasskeyCredentials: () => Promise<IPasskeyCredential[]>;
78
+ registerPasskey: (
79
+ name?: string
80
+ ) => Promise<IPasskeyCredential | null>;
81
+ deletePasskeyCredential: (
82
+ credentialId: string
83
+ ) => Promise<boolean>;
57
84
  }
58
85
 
59
86
  export interface DauthProfileModalProps {
60
87
  open: boolean;
61
88
  onClose: () => void;
89
+ /** Optional: provide a function to handle avatar upload.
90
+ * Receives a File, should return the URL string.
91
+ * If not provided, the avatar edit button is hidden. */
92
+ onAvatarUpload?: (file: File) => Promise<string>;
62
93
  }
63
94
 
64
95
  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
+ }