@draftlab/auth 0.13.1 → 0.15.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.
@@ -10,10 +10,12 @@ const DEFAULT_COPY = {
10
10
  login_prompt: "Already have an account?",
11
11
  login: "Login",
12
12
  button_continue: "Continue",
13
- change_prompt: "Forgot password?",
14
- code_resend: "Resend code",
15
- code_return: "Back to",
16
- input_email: "Email"
13
+ input_email: "Email",
14
+ error_register_already_registered: "This device is already registered. Please use the login page or try a different device.",
15
+ error_register_cancelled: "Registration was cancelled or timed out. Please try again.",
16
+ error_register_failed: "Registration failed. Please try again.",
17
+ error_auth_cancelled: "Authentication was cancelled or timed out. Please try again.",
18
+ error_auth_failed: "Authentication failed. Please try again."
17
19
  };
18
20
  const PasskeyUI = (options) => {
19
21
  const { rpName, rpID, origin, userCanRegisterPasskey, authenticatorSelection, attestationType, timeout } = options;
@@ -57,8 +59,17 @@ const PasskeyUI = (options) => {
57
59
  // Pass the options to the authenticator and wait for a response
58
60
  attResp = await startAuthentication({ optionsJSON });
59
61
  } catch (error) {
60
- message.textContent = error;
61
- throw error;
62
+ // Handle WebAuthn errors with friendly messages
63
+ const errorName = error.name;
64
+ if (errorName === "NotAllowedError") {
65
+ message.textContent = "${copy.error_auth_cancelled}";
66
+ } else if (errorName === "InvalidStateError") {
67
+ message.textContent = "${copy.error_auth_failed}";
68
+ } else {
69
+ message.textContent = error.message || "${copy.error_auth_failed}";
70
+ }
71
+ console.error(error);
72
+ return;
62
73
  }
63
74
 
64
75
  const verificationResp = await fetch(
@@ -147,6 +158,7 @@ const PasskeyUI = (options) => {
147
158
  window.addEventListener("load", async () => {
148
159
  const { startRegistration } = SimpleWebAuthnBrowser;
149
160
  const registerForm = document.getElementById("registerForm");
161
+ const btnOtherDevice = document.getElementById("btnOtherDevice");
150
162
  const message = document.querySelector("[data-slot='message']");
151
163
  const origin = window.location.origin;
152
164
  const rpID = window.location.hostname;
@@ -181,8 +193,17 @@ const PasskeyUI = (options) => {
181
193
  // Pass the options to the authenticator and wait for a response
182
194
  attResp = await startRegistration({ optionsJSON });
183
195
  } catch (error) {
184
- message.textContent = error;
185
- throw error;
196
+ // Handle WebAuthn errors with friendly messages
197
+ const errorName = error.name;
198
+ if (errorName === "InvalidStateError") {
199
+ message.textContent = "${copy.error_register_already_registered}";
200
+ } else if (errorName === "NotAllowedError") {
201
+ message.textContent = "${copy.error_register_cancelled}";
202
+ } else {
203
+ message.textContent = error.message || "${copy.error_register_failed}";
204
+ }
205
+ console.error(error);
206
+ return;
186
207
  }
187
208
 
188
209
  // POST the response to the endpoint that calls
@@ -232,6 +253,11 @@ const PasskeyUI = (options) => {
232
253
  e.preventDefault();
233
254
  register();
234
255
  });
256
+
257
+ btnOtherDevice.addEventListener("click", (e) => {
258
+ e.preventDefault();
259
+ register(true);
260
+ });
235
261
  });
236
262
  ` } }),
237
263
  /* @__PURE__ */ jsxs("form", {
@@ -11,10 +11,6 @@ interface PasswordUICopy {
11
11
  readonly error_invalid_email: string;
12
12
  readonly error_invalid_password: string;
13
13
  readonly error_password_mismatch: string;
14
- readonly register_title: string;
15
- readonly register_description: string;
16
- readonly login_title: string;
17
- readonly login_description: string;
18
14
  readonly register: string;
19
15
  readonly register_prompt: string;
20
16
  readonly login_prompt: string;
@@ -1,3 +1,4 @@
1
+ import { run } from "../util.mjs";
1
2
  import { Layout, renderToHTML } from "./base.mjs";
2
3
  import { FormAlert } from "./form.mjs";
3
4
  import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
@@ -12,10 +13,6 @@ const DEFAULT_COPY = {
12
13
  error_invalid_email: "Email is not valid.",
13
14
  error_invalid_password: "Password is incorrect.",
14
15
  error_password_mismatch: "Passwords do not match.",
15
- register_title: "Welcome to the app",
16
- register_description: "Sign in with your email",
17
- login_title: "Welcome to the app",
18
- login_description: "Sign in with your email",
19
16
  register: "Register",
20
17
  register_prompt: "Don't have an account?",
21
18
  login_prompt: "Already have an account?",
@@ -104,130 +101,133 @@ const PasswordUI = (options) => {
104
101
  "password_mismatch",
105
102
  "validation_error"
106
103
  ].includes(error?.type || "");
107
- return /* @__PURE__ */ jsx(Layout, { children: state.type === "start" ? /* @__PURE__ */ jsxs("form", {
108
- "data-component": "form",
109
- method: "post",
110
- children: [
111
- /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
112
- /* @__PURE__ */ jsx("input", {
113
- name: "action",
114
- type: "hidden",
115
- value: "register"
116
- }),
117
- /* @__PURE__ */ jsx("input", {
118
- type: "email",
119
- name: "email",
120
- placeholder: copy.input_email,
121
- value: emailError ? "" : form?.get("email")?.toString() || "",
122
- autoComplete: "email",
123
- "data-component": "input",
124
- required: true
125
- }),
126
- /* @__PURE__ */ jsx("input", {
127
- type: "password",
128
- name: "password",
129
- placeholder: copy.input_password,
130
- value: passwordError ? "" : form?.get("password")?.toString() || "",
131
- autoComplete: "new-password",
132
- "data-component": "input",
133
- required: true
134
- }),
135
- /* @__PURE__ */ jsx("input", {
136
- type: "password",
137
- name: "repeat",
138
- placeholder: copy.input_repeat,
139
- autoComplete: "new-password",
140
- "data-component": "input",
141
- required: true
142
- }),
143
- /* @__PURE__ */ jsx("button", {
144
- "data-component": "button",
145
- type: "submit",
146
- children: copy.button_continue
147
- }),
148
- /* @__PURE__ */ jsx("div", {
149
- "data-component": "form-footer",
150
- children: /* @__PURE__ */ jsxs("span", { children: [
151
- copy.login_prompt,
152
- " ",
153
- /* @__PURE__ */ jsx("a", {
154
- "data-component": "link",
155
- href: "./authorize",
156
- children: copy.login
157
- })
158
- ] })
159
- })
160
- ]
161
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("form", {
162
- "data-component": "form",
163
- method: "post",
164
- children: [
165
- /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
166
- /* @__PURE__ */ jsx("input", {
167
- name: "action",
168
- type: "hidden",
169
- value: "verify"
170
- }),
171
- /* @__PURE__ */ jsx("input", {
172
- type: "text",
173
- name: "code",
174
- placeholder: copy.input_code,
175
- "aria-label": "6-digit verification code",
176
- autoComplete: "one-time-code",
177
- "data-component": "input",
178
- inputMode: "numeric",
179
- maxLength: 6,
180
- minLength: 6,
181
- pattern: "[0-9]{6}",
182
- required: true
183
- }),
184
- /* @__PURE__ */ jsx("button", {
185
- "data-component": "button",
186
- type: "submit",
187
- children: copy.button_continue
188
- })
189
- ]
190
- }), /* @__PURE__ */ jsxs("form", {
191
- method: "post",
192
- children: [
193
- /* @__PURE__ */ jsx("input", {
194
- name: "action",
195
- type: "hidden",
196
- value: "register"
197
- }),
198
- /* @__PURE__ */ jsx("input", {
199
- name: "email",
200
- type: "hidden",
201
- value: state.email
202
- }),
203
- /* @__PURE__ */ jsx("input", {
204
- name: "password",
205
- type: "hidden",
206
- value: ""
207
- }),
208
- /* @__PURE__ */ jsx("input", {
209
- name: "repeat",
210
- type: "hidden",
211
- value: ""
212
- }),
213
- /* @__PURE__ */ jsxs("div", {
214
- "data-component": "form-footer",
215
- children: [/* @__PURE__ */ jsxs("span", { children: [
216
- copy.code_return,
217
- " ",
218
- /* @__PURE__ */ jsx("a", {
219
- "data-component": "link",
220
- href: "./authorize",
221
- children: copy.login
222
- })
223
- ] }), /* @__PURE__ */ jsx("button", {
104
+ return /* @__PURE__ */ jsx(Layout, { children: run(() => {
105
+ if (state.type === "start") return /* @__PURE__ */ jsxs("form", {
106
+ "data-component": "form",
107
+ method: "post",
108
+ children: [
109
+ /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
110
+ /* @__PURE__ */ jsx("input", {
111
+ name: "action",
112
+ type: "hidden",
113
+ value: "register"
114
+ }),
115
+ /* @__PURE__ */ jsx("input", {
116
+ type: "email",
117
+ name: "email",
118
+ placeholder: copy.input_email,
119
+ value: emailError ? "" : form?.get("email")?.toString() || "",
120
+ autoComplete: "email",
121
+ "data-component": "input",
122
+ required: true
123
+ }),
124
+ /* @__PURE__ */ jsx("input", {
125
+ type: "password",
126
+ name: "password",
127
+ placeholder: copy.input_password,
128
+ value: passwordError ? "" : form?.get("password")?.toString() || "",
129
+ autoComplete: "new-password",
130
+ "data-component": "input",
131
+ required: true
132
+ }),
133
+ /* @__PURE__ */ jsx("input", {
134
+ type: "password",
135
+ name: "repeat",
136
+ placeholder: copy.input_repeat,
137
+ autoComplete: "new-password",
138
+ "data-component": "input",
139
+ required: true
140
+ }),
141
+ /* @__PURE__ */ jsx("button", {
142
+ "data-component": "button",
224
143
  type: "submit",
225
- "data-component": "link",
226
- children: copy.code_resend
227
- })]
228
- })
229
- ]
230
- })] }) });
144
+ children: copy.button_continue
145
+ }),
146
+ /* @__PURE__ */ jsx("div", {
147
+ "data-component": "form-footer",
148
+ children: /* @__PURE__ */ jsxs("span", { children: [
149
+ copy.login_prompt,
150
+ " ",
151
+ /* @__PURE__ */ jsx("a", {
152
+ "data-component": "link",
153
+ href: "./authorize",
154
+ children: copy.login
155
+ })
156
+ ] })
157
+ })
158
+ ]
159
+ });
160
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("form", {
161
+ "data-component": "form",
162
+ method: "post",
163
+ children: [
164
+ /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
165
+ /* @__PURE__ */ jsx("input", {
166
+ name: "action",
167
+ type: "hidden",
168
+ value: "verify"
169
+ }),
170
+ /* @__PURE__ */ jsx("input", {
171
+ type: "text",
172
+ name: "code",
173
+ placeholder: copy.input_code,
174
+ "aria-label": "6-digit verification code",
175
+ autoComplete: "one-time-code",
176
+ "data-component": "input",
177
+ inputMode: "numeric",
178
+ maxLength: 6,
179
+ minLength: 6,
180
+ pattern: "[0-9]{6}",
181
+ required: true
182
+ }),
183
+ /* @__PURE__ */ jsx("button", {
184
+ "data-component": "button",
185
+ type: "submit",
186
+ children: copy.button_continue
187
+ })
188
+ ]
189
+ }), /* @__PURE__ */ jsxs("form", {
190
+ method: "post",
191
+ children: [
192
+ /* @__PURE__ */ jsx("input", {
193
+ name: "action",
194
+ type: "hidden",
195
+ value: "register"
196
+ }),
197
+ /* @__PURE__ */ jsx("input", {
198
+ name: "email",
199
+ type: "hidden",
200
+ value: state.email
201
+ }),
202
+ /* @__PURE__ */ jsx("input", {
203
+ name: "password",
204
+ type: "hidden",
205
+ value: ""
206
+ }),
207
+ /* @__PURE__ */ jsx("input", {
208
+ name: "repeat",
209
+ type: "hidden",
210
+ value: ""
211
+ }),
212
+ /* @__PURE__ */ jsxs("div", {
213
+ "data-component": "form-footer",
214
+ children: [/* @__PURE__ */ jsxs("span", { children: [
215
+ copy.code_return,
216
+ " ",
217
+ /* @__PURE__ */ jsx("a", {
218
+ "data-component": "link",
219
+ href: "./authorize",
220
+ children: copy.login
221
+ })
222
+ ] }), /* @__PURE__ */ jsx("button", {
223
+ type: "submit",
224
+ "data-component": "link",
225
+ children: copy.code_resend
226
+ })]
227
+ })
228
+ ]
229
+ })] });
230
+ }) });
231
231
  };
232
232
  /**
233
233
  * Renders the password change form based on current state
@@ -238,124 +238,140 @@ const PasswordUI = (options) => {
238
238
  "password_mismatch",
239
239
  "validation_error"
240
240
  ].includes(error?.type || "");
241
- return /* @__PURE__ */ jsx(Layout, { children: state.type === "start" ? /* @__PURE__ */ jsxs("form", {
242
- "data-component": "form",
243
- method: "post",
244
- children: [
245
- /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
246
- /* @__PURE__ */ jsx("input", {
247
- name: "action",
248
- type: "hidden",
249
- value: "code"
250
- }),
251
- /* @__PURE__ */ jsx("input", {
252
- type: "email",
253
- name: "email",
254
- placeholder: copy.input_email,
255
- value: form?.get("email")?.toString() || "",
256
- autoComplete: "email",
257
- "data-component": "input",
258
- required: true
259
- }),
260
- /* @__PURE__ */ jsx("button", {
261
- "data-component": "button",
262
- type: "submit",
263
- children: copy.button_continue
264
- })
265
- ]
266
- }) : state.type === "code" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("form", {
267
- "data-component": "form",
268
- method: "post",
269
- children: [
270
- /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
271
- /* @__PURE__ */ jsx("input", {
272
- name: "action",
273
- type: "hidden",
274
- value: "verify"
275
- }),
276
- /* @__PURE__ */ jsx("input", {
277
- type: "text",
278
- name: "code",
279
- placeholder: copy.input_code,
280
- "aria-label": "6-digit verification code",
281
- autoComplete: "one-time-code",
282
- inputMode: "numeric",
283
- maxLength: 6,
284
- minLength: 6,
285
- "data-component": "input",
286
- pattern: "[0-9]{6}",
287
- required: true
288
- }),
289
- /* @__PURE__ */ jsx("button", {
290
- "data-component": "button",
291
- type: "submit",
292
- children: copy.button_continue
293
- })
294
- ]
295
- }), /* @__PURE__ */ jsxs("form", {
296
- method: "post",
297
- children: [
298
- /* @__PURE__ */ jsx("input", {
299
- name: "action",
300
- type: "hidden",
301
- value: "code"
302
- }),
303
- /* @__PURE__ */ jsx("input", {
304
- name: "email",
305
- type: "hidden",
306
- value: state.email
307
- }),
308
- /* @__PURE__ */ jsxs("div", {
309
- "data-component": "form-footer",
310
- children: [/* @__PURE__ */ jsxs("span", { children: [
311
- copy.code_return,
312
- " ",
313
- /* @__PURE__ */ jsx("a", {
241
+ return /* @__PURE__ */ jsx(Layout, { children: run(() => {
242
+ if (state.type === "start") return /* @__PURE__ */ jsxs("form", {
243
+ "data-component": "form",
244
+ method: "post",
245
+ children: [
246
+ /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
247
+ /* @__PURE__ */ jsx("input", {
248
+ name: "action",
249
+ type: "hidden",
250
+ value: "code"
251
+ }),
252
+ /* @__PURE__ */ jsx("input", {
253
+ type: "email",
254
+ name: "email",
255
+ placeholder: copy.input_email,
256
+ value: form?.get("email")?.toString() || "",
257
+ autoComplete: "email",
258
+ "data-component": "input",
259
+ required: true
260
+ }),
261
+ /* @__PURE__ */ jsx("button", {
262
+ "data-component": "button",
263
+ type: "submit",
264
+ children: copy.button_continue
265
+ }),
266
+ /* @__PURE__ */ jsx("div", {
267
+ "data-component": "form-footer",
268
+ children: /* @__PURE__ */ jsxs("span", { children: [
269
+ copy.code_return,
270
+ " ",
271
+ /* @__PURE__ */ jsx("a", {
272
+ "data-component": "link",
273
+ href: "./authorize",
274
+ children: copy.login
275
+ })
276
+ ] })
277
+ })
278
+ ]
279
+ });
280
+ if (state.type === "code") return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("form", {
281
+ "data-component": "form",
282
+ method: "post",
283
+ children: [
284
+ /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
285
+ /* @__PURE__ */ jsx("input", {
286
+ name: "action",
287
+ type: "hidden",
288
+ value: "verify"
289
+ }),
290
+ /* @__PURE__ */ jsx("input", {
291
+ type: "text",
292
+ name: "code",
293
+ placeholder: copy.input_code,
294
+ "aria-label": "6-digit verification code",
295
+ autoComplete: "one-time-code",
296
+ inputMode: "numeric",
297
+ maxLength: 6,
298
+ minLength: 6,
299
+ "data-component": "input",
300
+ pattern: "[0-9]{6}",
301
+ required: true
302
+ }),
303
+ /* @__PURE__ */ jsx("button", {
304
+ "data-component": "button",
305
+ type: "submit",
306
+ children: copy.button_continue
307
+ })
308
+ ]
309
+ }), /* @__PURE__ */ jsxs("form", {
310
+ method: "post",
311
+ children: [
312
+ /* @__PURE__ */ jsx("input", {
313
+ name: "action",
314
+ type: "hidden",
315
+ value: "code"
316
+ }),
317
+ /* @__PURE__ */ jsx("input", {
318
+ name: "email",
319
+ type: "hidden",
320
+ value: state.email
321
+ }),
322
+ /* @__PURE__ */ jsxs("div", {
323
+ "data-component": "form-footer",
324
+ children: [/* @__PURE__ */ jsxs("span", { children: [
325
+ copy.code_return,
326
+ " ",
327
+ /* @__PURE__ */ jsx("a", {
328
+ "data-component": "link",
329
+ href: "./authorize",
330
+ children: copy.login
331
+ })
332
+ ] }), /* @__PURE__ */ jsx("button", {
333
+ type: "submit",
314
334
  "data-component": "link",
315
- href: "./authorize",
316
- children: copy.login
317
- })
318
- ] }), /* @__PURE__ */ jsx("button", {
335
+ children: copy.code_resend
336
+ })]
337
+ })
338
+ ]
339
+ })] });
340
+ return /* @__PURE__ */ jsxs("form", {
341
+ "data-component": "form",
342
+ method: "post",
343
+ children: [
344
+ /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
345
+ /* @__PURE__ */ jsx("input", {
346
+ name: "action",
347
+ type: "hidden",
348
+ value: "update"
349
+ }),
350
+ /* @__PURE__ */ jsx("input", {
351
+ type: "password",
352
+ name: "password",
353
+ placeholder: copy.input_password,
354
+ value: passwordError ? "" : form?.get("password")?.toString() || "",
355
+ autoComplete: "new-password",
356
+ "data-component": "input",
357
+ required: true
358
+ }),
359
+ /* @__PURE__ */ jsx("input", {
360
+ type: "password",
361
+ name: "repeat",
362
+ placeholder: copy.input_repeat,
363
+ value: passwordError ? "" : form?.get("repeat")?.toString() || "",
364
+ autoComplete: "new-password",
365
+ "data-component": "input",
366
+ required: true
367
+ }),
368
+ /* @__PURE__ */ jsx("button", {
369
+ "data-component": "button",
319
370
  type: "submit",
320
- "data-component": "link",
321
- children: copy.code_resend
322
- })]
323
- })
324
- ]
325
- })] }) : /* @__PURE__ */ jsxs("form", {
326
- "data-component": "form",
327
- method: "post",
328
- children: [
329
- /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
330
- /* @__PURE__ */ jsx("input", {
331
- name: "action",
332
- type: "hidden",
333
- value: "update"
334
- }),
335
- /* @__PURE__ */ jsx("input", {
336
- type: "password",
337
- name: "password",
338
- placeholder: copy.input_password,
339
- value: passwordError ? "" : form?.get("password")?.toString() || "",
340
- autoComplete: "new-password",
341
- "data-component": "input",
342
- required: true
343
- }),
344
- /* @__PURE__ */ jsx("input", {
345
- type: "password",
346
- name: "repeat",
347
- placeholder: copy.input_repeat,
348
- value: passwordError ? "" : form?.get("repeat")?.toString() || "",
349
- autoComplete: "new-password",
350
- "data-component": "input",
351
- required: true
352
- }),
353
- /* @__PURE__ */ jsx("button", {
354
- "data-component": "button",
355
- type: "submit",
356
- children: copy.button_continue
357
- })
358
- ]
371
+ children: copy.button_continue
372
+ })
373
+ ]
374
+ });
359
375
  }) });
360
376
  };
361
377
  return {
package/dist/util.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { RouterContext } from "@draftlab/auth-router/types";
1
+ import { RouterContext } from "./router/types.mjs";
2
2
 
3
3
  //#region src/util.d.ts
4
4
 
@@ -68,5 +68,28 @@ declare const isDomainMatch: (a: string, b: string) => boolean;
68
68
  * ```
69
69
  */
70
70
  declare const lazy: <T>(fn: () => T) => (() => T);
71
+ /**
72
+ * Utility function to immediately invoke a function and return its result.
73
+ * Useful for complex conditional rendering logic in JSX/TSX where you want
74
+ * to use if/else statements instead of ternary operators.
75
+ *
76
+ * @template T - The return type of the function
77
+ * @param fn - Function to execute immediately
78
+ * @returns The result of executing the function
79
+ *
80
+ * @example
81
+ * ```tsx
82
+ * return (
83
+ * <div>
84
+ * {run(() => {
85
+ * if (state === "loading") return <Spinner />
86
+ * if (state === "error") return <Error />
87
+ * return <Content />
88
+ * })}
89
+ * </div>
90
+ * )
91
+ * ```
92
+ */
93
+ declare const run: <T>(fn: () => T) => T;
71
94
  //#endregion
72
- export { Prettify, getRelativeUrl, isDomainMatch, lazy };
95
+ export { Prettify, getRelativeUrl, isDomainMatch, lazy, run };