@draftlab/auth 0.0.3 → 0.1.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.
Files changed (78) hide show
  1. package/dist/allow.d.ts +58 -1
  2. package/dist/allow.js +61 -2
  3. package/dist/client.d.ts +2 -3
  4. package/dist/client.js +2 -2
  5. package/dist/core.d.ts +128 -8
  6. package/dist/core.js +496 -12
  7. package/dist/error.d.ts +242 -1
  8. package/dist/error.js +235 -1
  9. package/dist/index.d.ts +1 -8
  10. package/dist/index.js +1 -12
  11. package/dist/keys.d.ts +1 -1
  12. package/dist/keys.js +138 -3
  13. package/dist/pkce.js +160 -1
  14. package/dist/provider/code.d.ts +227 -3
  15. package/dist/provider/code.js +27 -14
  16. package/dist/provider/facebook.d.ts +2 -3
  17. package/dist/provider/facebook.js +1 -5
  18. package/dist/provider/github.d.ts +2 -3
  19. package/dist/provider/github.js +1 -5
  20. package/dist/provider/google.d.ts +2 -3
  21. package/dist/provider/google.js +1 -5
  22. package/dist/provider/oauth2.d.ts +175 -3
  23. package/dist/provider/oauth2.js +153 -5
  24. package/dist/provider/password.d.ts +384 -3
  25. package/dist/provider/password.js +4 -4
  26. package/dist/provider/provider.d.ts +226 -2
  27. package/dist/random.js +85 -1
  28. package/dist/storage/memory.d.ts +2 -2
  29. package/dist/storage/memory.js +1 -1
  30. package/dist/storage/storage.d.ts +161 -1
  31. package/dist/storage/storage.js +60 -1
  32. package/dist/storage/turso.d.ts +1 -1
  33. package/dist/storage/turso.js +1 -1
  34. package/dist/storage/unstorage.d.ts +2 -2
  35. package/dist/storage/unstorage.js +2 -2
  36. package/dist/subject.d.ts +61 -2
  37. package/dist/themes/theme.d.ts +208 -1
  38. package/dist/themes/theme.js +118 -1
  39. package/dist/ui/base.d.ts +22 -35
  40. package/dist/ui/base.js +388 -3
  41. package/dist/ui/code.d.ts +22 -137
  42. package/dist/ui/code.js +199 -161
  43. package/dist/ui/form.d.ts +8 -6
  44. package/dist/ui/form.js +57 -1
  45. package/dist/ui/icon.d.ts +7 -84
  46. package/dist/ui/icon.js +69 -2
  47. package/dist/ui/password.d.ts +30 -37
  48. package/dist/ui/password.js +340 -237
  49. package/dist/ui/select.d.ts +19 -218
  50. package/dist/ui/select.js +91 -4
  51. package/dist/util.d.ts +71 -1
  52. package/dist/util.js +106 -1
  53. package/package.json +5 -3
  54. package/dist/allow-CixonwTW.d.ts +0 -59
  55. package/dist/allow-DX5cehSc.js +0 -63
  56. package/dist/base-DRutbxgL.js +0 -422
  57. package/dist/code-DJxdFR7p.d.ts +0 -212
  58. package/dist/core-BZHEAefX.d.ts +0 -129
  59. package/dist/core-CDM5o4rs.js +0 -498
  60. package/dist/error-CWAdNAzm.d.ts +0 -243
  61. package/dist/error-DgAKK7b2.js +0 -237
  62. package/dist/form-6XKM_cOk.js +0 -61
  63. package/dist/icon-Ci5uqGB_.js +0 -192
  64. package/dist/keys-EEfxEGfO.js +0 -140
  65. package/dist/oauth2-B7-6Z7Lc.js +0 -155
  66. package/dist/oauth2-CXHukHf2.d.ts +0 -176
  67. package/dist/password-C4KLmO0O.d.ts +0 -385
  68. package/dist/pkce-276Za_rZ.js +0 -162
  69. package/dist/provider-tndlqCzp.d.ts +0 -227
  70. package/dist/random-SXMYlaVr.js +0 -87
  71. package/dist/select-BjySLL8I.js +0 -280
  72. package/dist/storage-BEaqEPNQ.js +0 -62
  73. package/dist/storage-CxKerLlc.d.ts +0 -162
  74. package/dist/subject-DMIMVtaT.d.ts +0 -62
  75. package/dist/theme-C9by7VXf.d.ts +0 -209
  76. package/dist/theme-CswaLtbW.js +0 -120
  77. package/dist/util-CSdHUFOo.js +0 -108
  78. package/dist/util-DbSKG1Xm.d.ts +0 -72
@@ -1,422 +0,0 @@
1
- import { getTheme } from "./theme-CswaLtbW.js";
2
-
3
- //#region src/ui/base.ts
4
- const css = `@import url("https://unpkg.com/tailwindcss@3.4.15/src/css/preflight.css");
5
-
6
- :root {
7
- --color-background-dark: #0e0e11;
8
- --color-background-light: #ffffff;
9
- --color-primary-dark: #6772e5;
10
- --color-primary-light: #6772e5;
11
-
12
- --color-background-success-dark: oklch(0.3 0.04 172);
13
- --color-background-success-light: oklch(
14
- from var(--color-background-success-dark) 0.83 c h
15
- );
16
- --color-success-dark: oklch(
17
- from var(--color-background-success-dark) 0.92 c h
18
- );
19
- --color-success-light: oklch(
20
- from var(--color-background-success-dark) 0.25 c h
21
- );
22
-
23
- --color-background-error-dark: oklch(0.32 0.07 15);
24
- --color-background-error-light: oklch(
25
- from var(--color-background-error-dark) 0.92 c h
26
- );
27
- --color-error-dark: oklch(from var(--color-background-error-dark) 0.92 c h);
28
- --color-error-light: oklch(from var(--color-background-error-dark) 0.25 c h);
29
-
30
- --border-radius: 0;
31
-
32
- --color-background: var(--color-background-dark);
33
- --color-primary: var(--color-primary-dark);
34
-
35
- --color-background-success: var(--color-background-success-dark);
36
- --color-success: var(--color-success-dark);
37
- --color-background-error: var(--color-background-error-dark);
38
- --color-error: var(--color-error-dark);
39
-
40
- @media (prefers-color-scheme: light) {
41
- --color-background: var(--color-background-light);
42
- --color-primary: var(--color-primary-light);
43
-
44
- --color-background-success: var(--color-background-success-light);
45
- --color-success: var(--color-success-light);
46
- --color-background-error: var(--color-background-error-light);
47
- --color-error: var(--color-error-light);
48
- }
49
-
50
- --color-high: oklch(
51
- from var(--color-background) clamp(0, calc((l - 0.714) * -1000), 1) 0 0
52
- );
53
- --color-low: oklch(
54
- from var(--color-background) clamp(0, calc((l - 0.714) * 1000), 1) 0 0
55
- );
56
- --lightness-high: color-mix(
57
- in oklch,
58
- var(--color-high) 0%,
59
- oklch(var(--color-high) 0 0)
60
- );
61
- --lightness-low: color-mix(
62
- in oklch,
63
- var(--color-low) 0%,
64
- oklch(var(--color-low) 0 0)
65
- );
66
- --font-family:
67
- ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
68
- "Segoe UI Symbol", "Noto Color Emoji";
69
- --font-scale: 1;
70
-
71
- --font-size-xs: calc(0.75rem * var(--font-scale));
72
- --font-size-sm: calc(0.875rem * var(--font-scale));
73
- --font-size-md: calc(1rem * var(--font-scale));
74
- --font-size-lg: calc(1.125rem * var(--font-scale));
75
- --font-size-xl: calc(1.25rem * var(--font-scale));
76
- --font-size-2xl: calc(1.5rem * var(--font-scale));
77
- }
78
-
79
- [data-component="root"] {
80
- font-family: var(--font-family);
81
- background-color: var(--color-background);
82
- padding: 1rem;
83
- color: white;
84
- position: absolute;
85
- inset: 0;
86
- display: flex;
87
- align-items: center;
88
- justify-content: center;
89
- flex-direction: column;
90
- user-select: none;
91
- color: var(--color-high);
92
- }
93
-
94
- [data-component="center"] {
95
- width: 380px;
96
- display: flex;
97
- flex-direction: column;
98
- gap: 1.5rem;
99
- }
100
-
101
- [data-component="center"][data-size="small"] {
102
- width: 300px;
103
- }
104
-
105
- [data-component="link"] {
106
- text-decoration: underline;
107
- text-underline-offset: 0.125rem;
108
- font-weight: 600;
109
- }
110
-
111
- [data-component="label"] {
112
- display: flex;
113
- gap: 0.75rem;
114
- flex-direction: column;
115
- font-size: var(--font-size-xs);
116
- }
117
-
118
- [data-component="logo"] {
119
- margin: 0 auto;
120
- height: 2.5rem;
121
- width: auto;
122
- display: none;
123
- }
124
-
125
- @media (prefers-color-scheme: light) {
126
- [data-component="logo"][data-mode="light"] {
127
- display: block;
128
- }
129
- }
130
-
131
- @media (prefers-color-scheme: dark) {
132
- [data-component="logo"][data-mode="dark"] {
133
- display: block;
134
- }
135
- }
136
-
137
- [data-component="logo-default"] {
138
- margin: 0 auto;
139
- height: 2.5rem;
140
- width: auto;
141
- }
142
-
143
- @media (prefers-color-scheme: light) {
144
- [data-component="logo-default"] {
145
- color: var(--color-high);
146
- }
147
- }
148
-
149
- @media (prefers-color-scheme: dark) {
150
- [data-component="logo-default"] {
151
- color: var(--color-high);
152
- }
153
- }
154
-
155
- [data-component="input"] {
156
- width: 100%;
157
- height: 2.5rem;
158
- padding: 0 1rem;
159
- border: 1px solid transparent;
160
- --background: oklch(
161
- from var(--color-background)
162
- calc(l + (-0.06 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.03)) c h
163
- );
164
- background: var(--background);
165
- border-color: oklch(
166
- from var(--color-background)
167
- calc(
168
- clamp(
169
- 0.22,
170
- l +
171
- (-0.12 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.06),
172
- 0.88
173
- )
174
- )
175
- c h
176
- );
177
- border-radius: calc(var(--border-radius) * 0.25rem);
178
- font-size: var(--font-size-sm);
179
- outline: none;
180
- }
181
-
182
- [data-component="input"]:focus {
183
- border-color: oklch(
184
- from var(--color-background)
185
- calc(
186
- clamp(
187
- 0.3,
188
- l +
189
- (-0.2 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.1),
190
- 0.7
191
- )
192
- )
193
- c h
194
- );
195
- }
196
-
197
- [data-component="input"]:user-invalid:not(:focus) {
198
- border-color: oklch(0.4 0.09 7.91);
199
- }
200
-
201
- [data-component="button"] {
202
- height: 2.5rem;
203
- cursor: pointer;
204
- border: 0;
205
- font-weight: 500;
206
- font-size: var(--font-size-sm);
207
- border-radius: calc(var(--border-radius) * 0.25rem);
208
- display: flex;
209
- gap: 0.75rem;
210
- align-items: center;
211
- justify-content: center;
212
- background: var(--color-primary);
213
- color: oklch(
214
- from var(--color-primary) clamp(0, calc((l - 0.714) * -1000), 1) 0 0
215
- );
216
- }
217
-
218
- [data-component="button"][data-color="ghost"] {
219
- background: transparent;
220
- color: var(--color-high);
221
- border: 1px solid
222
- oklch(
223
- from var(--color-background)
224
- calc(
225
- clamp(
226
- 0.22,
227
- l +
228
- (-0.12 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.06),
229
- 0.88
230
- )
231
- )
232
- c h
233
- );
234
- }
235
-
236
- [data-component="button"] [data-slot="icon"] {
237
- width: 16px;
238
- height: 16px;
239
- }
240
-
241
- [data-component="button"] [data-slot="icon"] svg {
242
- width: 100%;
243
- height: 100%;
244
- }
245
-
246
- [data-component="form"] {
247
- max-width: 100%;
248
- display: flex;
249
- flex-direction: column;
250
- gap: 1rem;
251
- margin: 0;
252
- }
253
-
254
- [data-component="form-alert"] {
255
- height: 2.5rem;
256
- display: flex;
257
- align-items: center;
258
- padding: 0 1rem;
259
- border-radius: calc(var(--border-radius) * 0.25rem);
260
- background: var(--color-background-error);
261
- color: var(--color-error);
262
- text-align: left;
263
- font-size: 0.75rem;
264
- gap: 0.5rem;
265
- }
266
-
267
- [data-component="form-alert"][data-color="success"] {
268
- background: var(--color-background-success);
269
- color: var(--color-success);
270
- }
271
-
272
- [data-component="form-alert"][data-color="success"] [data-slot="icon-success"] {
273
- display: block;
274
- }
275
- [data-component="form-alert"][data-color="success"] [data-slot="icon-danger"] {
276
- display: none;
277
- }
278
-
279
- [data-component="form-alert"]:has([data-slot="message"]:empty) {
280
- display: none;
281
- }
282
-
283
- [data-component="form-alert"] [data-slot="icon-success"],
284
- [data-component="form-alert"] [data-slot="icon-danger"] {
285
- width: 1rem;
286
- height: 1rem;
287
- }
288
- [data-component="form-alert"] [data-slot="icon-success"] {
289
- display: none;
290
- }
291
-
292
- [data-component="form-footer"] {
293
- display: flex;
294
- gap: 1rem;
295
- font-size: 0.75rem;
296
- align-items: center;
297
- justify-content: center;
298
- }
299
-
300
- [data-component="form-footer"]:has(> :nth-child(2)) {
301
- justify-content: space-between;
302
- }
303
- `;
304
- /**
305
- * Main layout component that wraps all authentication UI screens.
306
- * Handles theming, logo display, and provides consistent styling.
307
- *
308
- * @param props - Layout props including children and optional size
309
- * @returns Complete HTML document as a string with theming and branding applied
310
- */
311
- const Layout = (props) => {
312
- const theme = getTheme();
313
- /**
314
- * Gets a theme value for a specific key and color mode.
315
- * Handles both string values and light/dark object configurations.
316
- */
317
- const getThemeValue = (key, mode) => {
318
- if (!theme?.[key]) return;
319
- if (typeof theme[key] === "string") return theme[key];
320
- return theme[key][mode];
321
- };
322
- /**
323
- * Calculates border radius value based on theme configuration.
324
- */
325
- const getBorderRadius = () => {
326
- switch (theme?.radius) {
327
- case "none": return "0";
328
- case "sm": return "1";
329
- case "md": return "1.25";
330
- case "lg": return "1.5";
331
- case "full": return "1000000000001";
332
- default: return "1";
333
- }
334
- };
335
- /**
336
- * Checks if both light and dark logo variants are available.
337
- */
338
- const hasCustomLogo = Boolean(getThemeValue("logo", "light") && getThemeValue("logo", "dark"));
339
- /**
340
- * CSS custom properties for theming.
341
- */
342
- const themeStyles = [
343
- `--color-background-light: ${getThemeValue("background", "light") || ""}`,
344
- `--color-background-dark: ${getThemeValue("background", "dark") || ""}`,
345
- `--color-primary-light: ${getThemeValue("primary", "light") || ""}`,
346
- `--color-primary-dark: ${getThemeValue("primary", "dark") || ""}`,
347
- `--font-family: ${theme?.font?.family || ""}`,
348
- `--font-scale: ${theme?.font?.scale || ""}`,
349
- `--border-radius: ${getBorderRadius()}`
350
- ].join("; ");
351
- const faviconHtml = theme?.favicon ? `<link href="${theme.favicon}" rel="icon" />` : `
352
- <link href="https://openauth.js.org/favicon.ico" rel="icon" sizes="48x48" />
353
- <link href="https://openauth.js.org/favicon.svg" media="(prefers-color-scheme: light)" rel="icon" />
354
- <link href="https://openauth.js.org/favicon-dark.svg" media="(prefers-color-scheme: dark)" rel="icon" />
355
- <link href="https://openauth.js.org/favicon.svg" rel="shortcut icon" type="image/svg+xml" />
356
- `;
357
- const logoHtml = hasCustomLogo ? `
358
- <img
359
- alt="Logo Light"
360
- data-component="logo"
361
- data-mode="light"
362
- src="${getThemeValue("logo", "light") || ""}"
363
- />
364
- <img
365
- alt="Logo Dark"
366
- data-component="logo"
367
- data-mode="dark"
368
- src="${getThemeValue("logo", "dark") || ""}"
369
- />
370
- ` : DefaultDraftAuthLogo();
371
- const childrenHtml = props.children || "";
372
- return `
373
- <!DOCTYPE html>
374
- <html lang="en" style="${themeStyles}">
375
- <head>
376
- <title>${theme?.title || "Draft Auth"}</title>
377
- <meta charset="utf-8" />
378
- <meta content="width=device-width, initial-scale=1" name="viewport" />
379
-
380
- ${faviconHtml}
381
-
382
- <!-- Base CSS styles -->
383
- <style>${css}</style>
384
-
385
- <!-- Custom theme CSS if provided -->
386
- ${theme?.css ? `<style>${theme.css}</style>` : ""}
387
- </head>
388
- <body>
389
- <div data-component="root">
390
- <div data-component="center" data-size="${props.size || ""}">
391
- ${logoHtml}
392
- ${childrenHtml}
393
- </div>
394
- </div>
395
- </body>
396
- </html>
397
- `;
398
- };
399
- /**
400
- * Default Draft Auth logo component.
401
- * Used when no custom logo is provided in the theme configuration.
402
- */
403
- const DefaultDraftAuthLogo = () => `
404
- <svg
405
- aria-label="Draft Auth Logo"
406
- data-component="logo-default"
407
- fill="none"
408
- height="51"
409
- viewBox="0 0 51 51"
410
- width="51"
411
- xmlns="http://www.w3.org/2000/svg"
412
- >
413
- <title>Draft Auth Logo</title>
414
- <path
415
- d="M0 50.2303V0.12854H50.1017V50.2303H0ZM3.08002 11.8326H11.7041V3.20856H3.08002V11.8326ZM14.8526 11.8326H23.4766V3.20856H14.8526V11.8326ZM26.5566 11.8326H35.1807V3.20856H26.5566V11.8326ZM38.3292 11.8326H47.0217V3.20856H38.3292V11.8326ZM3.08002 23.6052H11.7041V14.9811H3.08002V23.6052ZM14.8526 23.6052H23.4766V14.9811H14.8526V23.6052ZM26.5566 23.6052H35.1807V14.9811H26.5566V23.6052ZM38.3292 23.6052H47.0217V14.9811H38.3292V23.6052ZM3.08002 35.3092H11.7041V26.6852H3.08002V35.3092ZM14.8526 35.3092H23.4766V26.6852H14.8526V35.3092ZM26.5566 35.3092H35.1807V26.6852H26.5566V35.3092ZM38.3292 35.3092H47.0217V26.6852H38.3292V35.3092ZM3.08002 47.1502H11.7041V38.3893H3.08002V47.1502ZM14.8526 47.1502H23.4766V38.3893H14.8526V47.1502ZM26.5566 47.1502H35.1807V38.3893H26.5566V47.1502ZM38.3292 47.1502H47.0217V38.3893H38.3292V47.1502Z"
416
- fill="currentColor"
417
- />
418
- </svg>
419
- `;
420
-
421
- //#endregion
422
- export { Layout };
@@ -1,212 +0,0 @@
1
- import { Provider } from "./provider-tndlqCzp.js";
2
-
3
- //#region src/provider/code.d.ts
4
-
5
- /**
6
- * Configuration options for the PIN code authentication provider.
7
- *
8
- * @template Claims - Type of claims collected during authentication (email, phone, etc.)
9
- */
10
- interface CodeProviderConfig<Claims extends Record<string, string> = Record<string, string>> {
11
- /**
12
- * The length of the generated PIN code.
13
- * Common values are 4, 6, or 8 digits.
14
- *
15
- * @default 6
16
- *
17
- * @example
18
- * ```ts
19
- * {
20
- * length: 4 // 4-digit PIN for easier entry
21
- * }
22
- * ```
23
- */
24
- readonly length?: number;
25
- /**
26
- * Request handler for rendering the authentication UI.
27
- * Handles both the initial claim collection and PIN code entry screens.
28
- *
29
- * @param req - The HTTP request object
30
- * @param state - Current authentication state (start or code verification)
31
- * @param form - Form data from POST requests (if any)
32
- * @param error - Authentication error to display (if any)
33
- * @returns Promise resolving to the authentication page response
34
- *
35
- * @example
36
- * ```ts
37
- * request: async (req, state, form, error) => {
38
- * if (state.type === 'start') {
39
- * return new Response(renderClaimForm(form, error))
40
- * } else {
41
- * return new Response(renderCodeForm(state.claims.email, error))
42
- * }
43
- * }
44
- * ```
45
- */
46
- request: (req: Request, state: CodeProviderState, form?: FormData, error?: CodeProviderError) => Promise<Response>;
47
- /**
48
- * Callback for sending PIN codes to users via their preferred method.
49
- * Should handle delivery via email, SMS, or other communication channels.
50
- *
51
- * @param claims - User claims containing contact information
52
- * @param code - The generated PIN code to send
53
- * @returns Promise resolving to undefined on success, or error object on failure
54
- *
55
- * @example
56
- * ```ts
57
- * sendCode: async (claims, code) => {
58
- * try {
59
- * if (claims.email) {
60
- * await emailService.send({
61
- * to: claims.email,
62
- * subject: 'Your verification code',
63
- * text: `Your PIN code is: ${code}`
64
- * })
65
- * } else if (claims.phone) {
66
- * await smsService.send(claims.phone, `PIN: ${code}`)
67
- * } else {
68
- * return {
69
- * type: "invalid_claim",
70
- * key: "contact",
71
- * value: "No email or phone provided"
72
- * }
73
- * }
74
- * } catch (error) {
75
- * return {
76
- * type: "invalid_claim",
77
- * key: "delivery",
78
- * value: "Failed to send code"
79
- * }
80
- * }
81
- * }
82
- * ```
83
- */
84
- sendCode: (claims: Claims, code: string) => Promise<CodeProviderError | undefined>;
85
- }
86
- /**
87
- * Authentication flow states for the PIN code provider.
88
- * The provider transitions between these states during authentication.
89
- */
90
- type CodeProviderState = {
91
- /** Initial state: user enters their claims (email, phone, etc.) */
92
- readonly type: "start";
93
- } | {
94
- /** Code verification state: user enters the PIN code */
95
- readonly type: "code";
96
- /** Whether this is a code resend request */
97
- readonly resend?: boolean;
98
- /** The generated PIN code for verification */
99
- readonly code: string;
100
- /** User claims collected during the start phase */
101
- readonly claims: Record<string, string>;
102
- };
103
- /**
104
- * Possible errors during PIN code authentication.
105
- */
106
- type CodeProviderError = {
107
- /** The entered PIN code is incorrect */
108
- readonly type: "invalid_code";
109
- } | {
110
- /** A user claim is invalid or missing */
111
- readonly type: "invalid_claim";
112
- /** The claim field that failed validation */
113
- readonly key: string;
114
- /** The invalid value or error description */
115
- readonly value: string;
116
- };
117
- /**
118
- * User data returned by successful PIN code authentication.
119
- *
120
- * @template Claims - Type of claims collected during authentication
121
- */
122
- interface CodeUserData<Claims extends Record<string, string> = Record<string, string>> {
123
- /** The verified claims collected during authentication */
124
- readonly claims: Claims;
125
- }
126
- /**
127
- * Creates a PIN code authentication provider.
128
- * Implements a flexible claim-based authentication flow with PIN verification.
129
- *
130
- * @template Claims - Type of claims to collect (email, phone, username, etc.)
131
- * @param config - PIN code provider configuration
132
- * @returns Provider instance implementing PIN code authentication
133
- *
134
- * @example
135
- * ```ts
136
- * // Email-based PIN authentication
137
- * const emailCodeProvider = CodeProvider<{ email: string }>({
138
- * length: 6,
139
- * request: async (req, state, form, error) => {
140
- * if (state.type === 'start') {
141
- * return new Response(renderEmailForm(form?.get('email'), error))
142
- * } else {
143
- * return new Response(renderPinForm(state.claims.email, error, state.resend))
144
- * }
145
- * },
146
- * sendCode: async (claims, code) => {
147
- * if (!claims.email || !isValidEmail(claims.email)) {
148
- * return {
149
- * type: "invalid_claim",
150
- * key: "email",
151
- * value: "Invalid email address"
152
- * }
153
- * }
154
- *
155
- * await emailService.send(claims.email, `Your verification code: ${code}`)
156
- * }
157
- * })
158
- *
159
- * // Multi-channel PIN authentication (email or phone)
160
- * const flexibleCodeProvider = CodeProvider<{ email?: string; phone?: string }>({
161
- * length: 4,
162
- * request: async (req, state, form, error) => {
163
- * if (state.type === 'start') {
164
- * return new Response(renderContactForm(form, error))
165
- * } else {
166
- * const contact = state.claims.email || state.claims.phone
167
- * return new Response(renderPinForm(contact, error))
168
- * }
169
- * },
170
- * sendCode: async (claims, code) => {
171
- * if (claims.email) {
172
- * await emailService.send(claims.email, `PIN: ${code}`)
173
- * } else if (claims.phone) {
174
- * await smsService.send(claims.phone, `PIN: ${code}`)
175
- * } else {
176
- * return {
177
- * type: "invalid_claim",
178
- * key: "contact",
179
- * value: "Provide either email or phone number"
180
- * }
181
- * }
182
- * }
183
- * })
184
- *
185
- * // Usage in issuer
186
- * export default issuer({
187
- * providers: {
188
- * email: emailCodeProvider,
189
- * flexible: flexibleCodeProvider
190
- * },
191
- * success: async (ctx, value) => {
192
- * if (value.provider === "code") {
193
- * const email = value.claims.email
194
- * const phone = value.claims.phone
195
- *
196
- * // Look up or create user based on verified claims
197
- * const userId = await findOrCreateUser({ email, phone })
198
- *
199
- * return ctx.subject("user", { userId, email, phone })
200
- * }
201
- * }
202
- * })
203
- * ```
204
- */
205
- declare const CodeProvider: <Claims extends Record<string, string> = Record<string, string>>(config: CodeProviderConfig<Claims>) => Provider<CodeUserData<Claims>>;
206
- /**
207
- * Type helper for CodeProvider configuration options.
208
- * @internal
209
- */
210
- type CodeProviderOptions = Parameters<typeof CodeProvider>[0];
211
- //#endregion
212
- export { CodeProvider, CodeProviderConfig, CodeProviderError, CodeProviderOptions, CodeProviderState, CodeUserData };