@draftlab/auth 0.0.1

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 (86) hide show
  1. package/dist/adapters/node.d.ts +18 -0
  2. package/dist/adapters/node.js +71 -0
  3. package/dist/allow-CixonwTW.d.ts +59 -0
  4. package/dist/allow-DX5cehSc.js +63 -0
  5. package/dist/allow.d.ts +2 -0
  6. package/dist/allow.js +4 -0
  7. package/dist/base-DRutbxgL.js +422 -0
  8. package/dist/client.d.ts +413 -0
  9. package/dist/client.js +209 -0
  10. package/dist/code-l_uvMR1j.d.ts +212 -0
  11. package/dist/core-8WTqfnb4.d.ts +129 -0
  12. package/dist/core-CncE5rPg.js +498 -0
  13. package/dist/core.d.ts +9 -0
  14. package/dist/core.js +14 -0
  15. package/dist/error-CWAdNAzm.d.ts +243 -0
  16. package/dist/error-DgAKK7b2.js +237 -0
  17. package/dist/error.d.ts +2 -0
  18. package/dist/error.js +3 -0
  19. package/dist/form-6XKM_cOk.js +61 -0
  20. package/dist/icon-Ci5uqGB_.js +192 -0
  21. package/dist/index.d.ts +9 -0
  22. package/dist/index.js +14 -0
  23. package/dist/keys-EEfxEGfO.js +140 -0
  24. package/dist/keys.d.ts +67 -0
  25. package/dist/keys.js +5 -0
  26. package/dist/oauth2-B7-6Z7Lc.js +155 -0
  27. package/dist/oauth2-DtKwtl8p.d.ts +176 -0
  28. package/dist/password-Cm0dRMwa.d.ts +385 -0
  29. package/dist/pkce-276Za_rZ.js +162 -0
  30. package/dist/pkce.d.ts +72 -0
  31. package/dist/pkce.js +3 -0
  32. package/dist/provider/code.d.ts +4 -0
  33. package/dist/provider/code.js +145 -0
  34. package/dist/provider/facebook.d.ts +137 -0
  35. package/dist/provider/facebook.js +85 -0
  36. package/dist/provider/github.d.ts +141 -0
  37. package/dist/provider/github.js +88 -0
  38. package/dist/provider/google.d.ts +113 -0
  39. package/dist/provider/google.js +62 -0
  40. package/dist/provider/oauth2.d.ts +4 -0
  41. package/dist/provider/oauth2.js +7 -0
  42. package/dist/provider/password.d.ts +4 -0
  43. package/dist/provider/password.js +366 -0
  44. package/dist/provider/provider.d.ts +3 -0
  45. package/dist/provider/provider.js +44 -0
  46. package/dist/provider-CwWMG-1l.d.ts +227 -0
  47. package/dist/random-SXMYlaVr.js +87 -0
  48. package/dist/random.d.ts +66 -0
  49. package/dist/random.js +3 -0
  50. package/dist/select-BjySLL8I.js +280 -0
  51. package/dist/storage/memory.d.ts +82 -0
  52. package/dist/storage/memory.js +127 -0
  53. package/dist/storage/storage.d.ts +2 -0
  54. package/dist/storage/storage.js +3 -0
  55. package/dist/storage/turso.d.ts +31 -0
  56. package/dist/storage/turso.js +117 -0
  57. package/dist/storage/unstorage.d.ts +38 -0
  58. package/dist/storage/unstorage.js +97 -0
  59. package/dist/storage-BEaqEPNQ.js +62 -0
  60. package/dist/storage-CxKerLlc.d.ts +162 -0
  61. package/dist/subject-DiQdRWGt.d.ts +62 -0
  62. package/dist/subject.d.ts +3 -0
  63. package/dist/subject.js +36 -0
  64. package/dist/theme-C9by7VXf.d.ts +209 -0
  65. package/dist/theme-CswaLtbW.js +120 -0
  66. package/dist/themes/theme.d.ts +2 -0
  67. package/dist/themes/theme.js +3 -0
  68. package/dist/types.d.ts +94 -0
  69. package/dist/types.js +0 -0
  70. package/dist/ui/base.d.ts +43 -0
  71. package/dist/ui/base.js +4 -0
  72. package/dist/ui/code.d.ts +158 -0
  73. package/dist/ui/code.js +197 -0
  74. package/dist/ui/form.d.ts +31 -0
  75. package/dist/ui/form.js +3 -0
  76. package/dist/ui/icon.d.ts +98 -0
  77. package/dist/ui/icon.js +3 -0
  78. package/dist/ui/password.d.ts +54 -0
  79. package/dist/ui/password.js +300 -0
  80. package/dist/ui/select.d.ts +233 -0
  81. package/dist/ui/select.js +6 -0
  82. package/dist/util-CSdHUFOo.js +108 -0
  83. package/dist/util-ChlgVqPN.d.ts +72 -0
  84. package/dist/util.d.ts +2 -0
  85. package/dist/util.js +3 -0
  86. package/package.json +63 -0
@@ -0,0 +1,18 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ //#region src/adapters/node.d.ts
4
+
5
+ /**
6
+ * Converts Node.js IncomingMessage to Web Standards Request
7
+ */
8
+ declare const nodeRequestAdapter: (req: IncomingMessage) => Request;
9
+ /**
10
+ * Writes Web Standards Response to Node.js ServerResponse
11
+ */
12
+ declare const nodeResponseAdapter: (response: Response, res: ServerResponse) => Promise<void>;
13
+ /**
14
+ * Creates a Node.js HTTP handler from a Web Standards fetch function
15
+ */
16
+ declare const createNodeHandler: (fetchHandler: (request: Request) => Promise<Response>) => ((req: IncomingMessage, res: ServerResponse) => void);
17
+ //#endregion
18
+ export { createNodeHandler, nodeRequestAdapter, nodeResponseAdapter };
@@ -0,0 +1,71 @@
1
+ import { Readable } from "node:stream";
2
+
3
+ //#region src/adapters/node.ts
4
+ /**
5
+ * Converts Node.js IncomingMessage to Web Standards Request
6
+ */
7
+ const nodeRequestAdapter = (req) => {
8
+ const host = req.headers.host || "localhost";
9
+ const sanitizedHost = host.split(",")[0]?.trim();
10
+ const url = new URL(req.url || "/", `http://${sanitizedHost}`);
11
+ const headers = new Headers();
12
+ for (const [key, value] of Object.entries(req.headers)) if (value !== void 0) if (Array.isArray(value)) for (const v of value) headers.append(key, v);
13
+ else headers.set(key, value);
14
+ let body;
15
+ if (req.method !== "GET" && req.method !== "HEAD") body = Readable.toWeb(req);
16
+ return new Request(url.toString(), {
17
+ method: req.method || "GET",
18
+ headers,
19
+ body,
20
+ duplex: "half"
21
+ });
22
+ };
23
+ /**
24
+ * Writes Web Standards Response to Node.js ServerResponse
25
+ */
26
+ const nodeResponseAdapter = async (response, res) => {
27
+ res.statusCode = response.status;
28
+ res.statusMessage = response.statusText;
29
+ response.headers.forEach((value, key) => {
30
+ res.setHeader(key, value);
31
+ });
32
+ if (response.body) {
33
+ const reader = response.body.getReader();
34
+ try {
35
+ while (true) {
36
+ const { done, value } = await reader.read();
37
+ if (done) break;
38
+ if (!res.write(value)) await new Promise((resolve) => res.once("drain", resolve));
39
+ }
40
+ } finally {
41
+ reader.releaseLock();
42
+ }
43
+ }
44
+ res.end();
45
+ };
46
+ /**
47
+ * Creates a Node.js HTTP handler from a Web Standards fetch function
48
+ */
49
+ const createNodeHandler = (fetchHandler) => {
50
+ return (req, res) => {
51
+ try {
52
+ const request = nodeRequestAdapter(req);
53
+ fetchHandler(request).then((response) => nodeResponseAdapter(response, res)).catch((error) => {
54
+ console.error("Handler error:", error instanceof Error ? error.message : "Unknown error");
55
+ if (!res.headersSent) {
56
+ res.statusCode = 500;
57
+ res.end("Internal Server Error");
58
+ }
59
+ });
60
+ } catch (error) {
61
+ console.error("Request adapter error:", error instanceof Error ? error.message : "Unknown error");
62
+ if (!res.headersSent) {
63
+ res.statusCode = 400;
64
+ res.end("Bad Request");
65
+ }
66
+ }
67
+ };
68
+ };
69
+
70
+ //#endregion
71
+ export { createNodeHandler, nodeRequestAdapter, nodeResponseAdapter };
@@ -0,0 +1,59 @@
1
+ //#region src/allow.d.ts
2
+ /**
3
+ * Client authorization validation utilities.
4
+ * Provides security checks to determine if OAuth authorization requests should be permitted
5
+ * based on redirect URI validation and domain matching policies.
6
+ */
7
+ /**
8
+ * Input parameters for authorization allow checks.
9
+ * Contains all necessary information to validate if a client request should be permitted.
10
+ */
11
+ interface AllowCheckInput {
12
+ /** The client ID of the application requesting authorization */
13
+ readonly clientID: string;
14
+ /** The redirect URI where the user will be sent after authorization */
15
+ readonly redirectURI: string;
16
+ /** Optional audience parameter for the authorization request */
17
+ readonly audience?: string;
18
+ }
19
+ /**
20
+ * Default authorization check that validates client requests based on redirect URI security.
21
+ *
22
+ * ## Security Policy
23
+ * - **Localhost**: Always allowed (for development)
24
+ * - **Same domain**: Redirect URI must match request origin at TLD+1 level
25
+ * - **Cross-domain**: Rejected for security
26
+ *
27
+ * This prevents unauthorized applications from hijacking authorization codes by using
28
+ * malicious redirect URIs that don't belong to the legitimate client application.
29
+ *
30
+ * @param input - Client request details including ID and redirect URI
31
+ * @param req - The original HTTP request for domain comparison
32
+ * @returns Promise resolving to true if the request should be allowed
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * // Allowed: localhost development
37
+ * await defaultAllowCheck({
38
+ * clientID: "dev-app",
39
+ * redirectURI: "http://localhost:3000/callback"
40
+ * }, request) // → true
41
+ *
42
+ * // Allowed: same domain
43
+ * // Request from: https://myapp.com
44
+ * await defaultAllowCheck({
45
+ * clientID: "web-app",
46
+ * redirectURI: "https://auth.myapp.com/callback"
47
+ * }, request) // → true
48
+ *
49
+ * // Rejected: different domain
50
+ * // Request from: https://myapp.com
51
+ * await defaultAllowCheck({
52
+ * clientID: "malicious-app",
53
+ * redirectURI: "https://evil.com/steal-codes"
54
+ * }, request) // → false
55
+ * ```
56
+ */
57
+ declare const defaultAllowCheck: (input: AllowCheckInput, req: Request) => Promise<boolean>;
58
+ //#endregion
59
+ export { AllowCheckInput, defaultAllowCheck };
@@ -0,0 +1,63 @@
1
+ import { isDomainMatch } from "./util-CSdHUFOo.js";
2
+
3
+ //#region src/allow.ts
4
+ /**
5
+ * Default authorization check that validates client requests based on redirect URI security.
6
+ *
7
+ * ## Security Policy
8
+ * - **Localhost**: Always allowed (for development)
9
+ * - **Same domain**: Redirect URI must match request origin at TLD+1 level
10
+ * - **Cross-domain**: Rejected for security
11
+ *
12
+ * This prevents unauthorized applications from hijacking authorization codes by using
13
+ * malicious redirect URIs that don't belong to the legitimate client application.
14
+ *
15
+ * @param input - Client request details including ID and redirect URI
16
+ * @param req - The original HTTP request for domain comparison
17
+ * @returns Promise resolving to true if the request should be allowed
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * // Allowed: localhost development
22
+ * await defaultAllowCheck({
23
+ * clientID: "dev-app",
24
+ * redirectURI: "http://localhost:3000/callback"
25
+ * }, request) // → true
26
+ *
27
+ * // Allowed: same domain
28
+ * // Request from: https://myapp.com
29
+ * await defaultAllowCheck({
30
+ * clientID: "web-app",
31
+ * redirectURI: "https://auth.myapp.com/callback"
32
+ * }, request) // → true
33
+ *
34
+ * // Rejected: different domain
35
+ * // Request from: https://myapp.com
36
+ * await defaultAllowCheck({
37
+ * clientID: "malicious-app",
38
+ * redirectURI: "https://evil.com/steal-codes"
39
+ * }, request) // → false
40
+ * ```
41
+ */
42
+ const defaultAllowCheck = (input, req) => {
43
+ return Promise.resolve((() => {
44
+ let redirectHostname;
45
+ try {
46
+ redirectHostname = new URL(input.redirectURI).hostname;
47
+ } catch {
48
+ return false;
49
+ }
50
+ if (redirectHostname === "localhost" || redirectHostname === "127.0.0.1") return true;
51
+ let currentHostname;
52
+ try {
53
+ const forwardedHost = req.headers.get("x-forwarded-host");
54
+ currentHostname = forwardedHost ? new URL(`https://${forwardedHost}`).hostname : new URL(req.url).hostname;
55
+ } catch {
56
+ return false;
57
+ }
58
+ return isDomainMatch(redirectHostname, currentHostname);
59
+ })());
60
+ };
61
+
62
+ //#endregion
63
+ export { defaultAllowCheck };
@@ -0,0 +1,2 @@
1
+ import { AllowCheckInput, defaultAllowCheck } from "./allow-CixonwTW.js";
2
+ export { AllowCheckInput, defaultAllowCheck };
package/dist/allow.js ADDED
@@ -0,0 +1,4 @@
1
+ import "./util-CSdHUFOo.js";
2
+ import { defaultAllowCheck } from "./allow-DX5cehSc.js";
3
+
4
+ export { defaultAllowCheck };
@@ -0,0 +1,422 @@
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 };