@draftlab/auth 0.4.0 → 0.5.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 (92) hide show
  1. package/dist/adapters/{node.js → node.mjs} +2 -4
  2. package/dist/{allow.js → allow.mjs} +1 -1
  3. package/dist/{client.d.ts → client.d.mts} +2 -2
  4. package/dist/{client.js → client.mjs} +55 -10
  5. package/dist/{core.d.ts → core.d.mts} +10 -10
  6. package/dist/{core.js → core.mjs} +72 -55
  7. package/dist/index.d.mts +2 -0
  8. package/dist/index.mjs +3 -0
  9. package/dist/{keys.d.ts → keys.d.mts} +1 -1
  10. package/dist/{keys.js → keys.mjs} +6 -8
  11. package/dist/{pkce.js → pkce.mjs} +5 -10
  12. package/dist/plugin/{builder.d.ts → builder.d.mts} +1 -1
  13. package/dist/plugin/{manager.d.ts → manager.d.mts} +2 -2
  14. package/dist/plugin/{manager.js → manager.mjs} +1 -1
  15. package/dist/plugin/{plugin.d.ts → plugin.d.mts} +1 -1
  16. package/dist/plugin/plugin.mjs +1 -0
  17. package/dist/plugin/{types.d.ts → types.d.mts} +1 -1
  18. package/dist/provider/{code.d.ts → code.d.mts} +1 -1
  19. package/dist/provider/{code.js → code.mjs} +2 -3
  20. package/dist/provider/{discord.d.ts → discord.d.mts} +2 -2
  21. package/dist/provider/{discord.js → discord.mjs} +59 -1
  22. package/dist/provider/{facebook.d.ts → facebook.d.mts} +2 -2
  23. package/dist/provider/{facebook.js → facebook.mjs} +57 -1
  24. package/dist/provider/{github.d.ts → github.d.mts} +2 -2
  25. package/dist/provider/{github.js → github.mjs} +79 -1
  26. package/dist/provider/{google.d.ts → google.d.mts} +2 -2
  27. package/dist/provider/{google.js → google.mjs} +45 -1
  28. package/dist/provider/{linkedin.d.ts → linkedin.d.mts} +2 -2
  29. package/dist/provider/{linkedin.js → linkedin.mjs} +57 -1
  30. package/dist/provider/{magiclink.d.ts → magiclink.d.mts} +1 -1
  31. package/dist/provider/{magiclink.js → magiclink.mjs} +4 -6
  32. package/dist/provider/{microsoft.d.ts → microsoft.d.mts} +2 -2
  33. package/dist/provider/{microsoft.js → microsoft.mjs} +68 -1
  34. package/dist/provider/{oauth2.d.ts → oauth2.d.mts} +1 -1
  35. package/dist/provider/{oauth2.js → oauth2.mjs} +4 -4
  36. package/dist/provider/{passkey.d.ts → passkey.d.mts} +1 -1
  37. package/dist/provider/{passkey.js → passkey.mjs} +8 -13
  38. package/dist/provider/{password.d.ts → password.d.mts} +1 -1
  39. package/dist/provider/{password.js → password.mjs} +31 -44
  40. package/dist/provider/{provider.d.ts → provider.d.mts} +1 -1
  41. package/dist/provider/{totp.d.ts → totp.d.mts} +1 -1
  42. package/dist/provider/{totp.js → totp.mjs} +51 -14
  43. package/dist/{random.js → random.mjs} +1 -2
  44. package/dist/storage/{memory.d.ts → memory.d.mts} +1 -1
  45. package/dist/storage/{memory.js → memory.mjs} +3 -5
  46. package/dist/storage/{storage.d.ts → storage.d.mts} +27 -10
  47. package/dist/storage/storage.mjs +104 -0
  48. package/dist/storage/{turso.d.ts → turso.d.mts} +1 -1
  49. package/dist/storage/{turso.js → turso.mjs} +1 -1
  50. package/dist/storage/{unstorage.d.ts → unstorage.d.mts} +1 -1
  51. package/dist/storage/{unstorage.js → unstorage.mjs} +11 -4
  52. package/dist/{subject.d.ts → subject.d.mts} +1 -1
  53. package/dist/types.mjs +1 -0
  54. package/dist/ui/{base.d.ts → base.d.mts} +1 -1
  55. package/dist/ui/{base.js → base.mjs} +1 -1
  56. package/dist/ui/{code.d.ts → code.d.mts} +1 -1
  57. package/dist/ui/{code.js → code.mjs} +3 -4
  58. package/dist/ui/{magiclink.d.ts → magiclink.d.mts} +1 -1
  59. package/dist/ui/{magiclink.js → magiclink.mjs} +3 -4
  60. package/dist/ui/{passkey.d.ts → passkey.d.mts} +1 -1
  61. package/dist/ui/{passkey.js → passkey.mjs} +2 -2
  62. package/dist/ui/{password.d.ts → password.d.mts} +1 -1
  63. package/dist/ui/{password.js → password.mjs} +3 -4
  64. package/dist/ui/{select.d.ts → select.d.mts} +1 -1
  65. package/dist/ui/{select.js → select.mjs} +2 -2
  66. package/dist/ui/{totp.d.ts → totp.d.mts} +1 -1
  67. package/dist/ui/{totp.js → totp.mjs} +2 -2
  68. package/dist/{util.js → util.mjs} +2 -5
  69. package/package.json +18 -17
  70. package/dist/index.d.ts +0 -2
  71. package/dist/index.js +0 -3
  72. package/dist/plugin/plugin.js +0 -0
  73. package/dist/storage/storage.js +0 -62
  74. package/dist/types.js +0 -0
  75. /package/dist/adapters/{node.d.ts → node.d.mts} +0 -0
  76. /package/dist/{allow.d.ts → allow.d.mts} +0 -0
  77. /package/dist/{error.d.ts → error.d.mts} +0 -0
  78. /package/dist/{error.js → error.mjs} +0 -0
  79. /package/dist/{pkce.d.ts → pkce.d.mts} +0 -0
  80. /package/dist/plugin/{builder.js → builder.mjs} +0 -0
  81. /package/dist/plugin/{types.js → types.mjs} +0 -0
  82. /package/dist/provider/{provider.js → provider.mjs} +0 -0
  83. /package/dist/{random.d.ts → random.d.mts} +0 -0
  84. /package/dist/{subject.js → subject.mjs} +0 -0
  85. /package/dist/themes/{theme.d.ts → theme.d.mts} +0 -0
  86. /package/dist/themes/{theme.js → theme.mjs} +0 -0
  87. /package/dist/{types.d.ts → types.d.mts} +0 -0
  88. /package/dist/ui/{form.d.ts → form.d.mts} +0 -0
  89. /package/dist/ui/{form.js → form.mjs} +0 -0
  90. /package/dist/ui/{icon.d.ts → icon.d.mts} +0 -0
  91. /package/dist/ui/{icon.js → icon.mjs} +0 -0
  92. /package/dist/{util.d.ts → util.d.mts} +0 -0
@@ -1,7 +1,7 @@
1
- import { getRelativeUrl } from "../util.js";
2
- import { UnknownStateError } from "../error.js";
3
- import { generateUnbiasedDigits, timingSafeCompare } from "../random.js";
4
- import { Storage } from "../storage/storage.js";
1
+ import { getRelativeUrl } from "../util.mjs";
2
+ import { UnknownStateError } from "../error.mjs";
3
+ import { generateUnbiasedDigits, timingSafeCompare } from "../random.mjs";
4
+ import { Storage } from "../storage/storage.mjs";
5
5
  import * as jose from "jose";
6
6
  import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
7
7
  import { TextEncoder } from "node:util";
@@ -123,12 +123,11 @@ const PasswordProvider = (config) => {
123
123
  message: validationError
124
124
  });
125
125
  }
126
- const existingUser = await Storage.get(ctx.storage, [
126
+ if (await Storage.get(ctx.storage, [
127
127
  "email",
128
128
  email,
129
129
  "password"
130
- ]);
131
- if (existingUser) return transition(provider, { type: "email_taken" });
130
+ ])) return transition(provider, { type: "email_taken" });
132
131
  const code = generateCode();
133
132
  await config.sendCode(email, code);
134
133
  return transition({
@@ -151,12 +150,11 @@ const PasswordProvider = (config) => {
151
150
  if (action === "verify" && provider.type === "code") {
152
151
  const code = formData.get("code")?.toString();
153
152
  if (!(code && timingSafeCompare(code, provider.code))) return transition(provider, { type: "invalid_code" });
154
- const existingUser = await Storage.get(ctx.storage, [
153
+ if (await Storage.get(ctx.storage, [
155
154
  "email",
156
155
  provider.email,
157
156
  "password"
158
- ]);
159
- if (existingUser) return transition({ type: "start" }, { type: "email_taken" });
157
+ ])) return transition({ type: "start" }, { type: "email_taken" });
160
158
  await Storage.set(ctx.storage, [
161
159
  "email",
162
160
  provider.email,
@@ -170,10 +168,9 @@ const PasswordProvider = (config) => {
170
168
  * GET /change - Display password change form
171
169
  */
172
170
  routes.get("/change", async (c) => {
173
- const redirect = c.query("redirect_uri") || getRelativeUrl(c, "/authorize");
174
171
  const state = {
175
172
  type: "start",
176
- redirect
173
+ redirect: c.query("redirect_uri") || getRelativeUrl(c, "/authorize")
177
174
  };
178
175
  await ctx.set(c, "provider", 3600 * 24, state);
179
176
  return ctx.forward(c, await config.change(c.request, state));
@@ -215,12 +212,11 @@ const PasswordProvider = (config) => {
215
212
  });
216
213
  }
217
214
  if (action === "update" && provider.type === "update") {
218
- const existingPassword = await Storage.get(ctx.storage, [
215
+ if (!await Storage.get(ctx.storage, [
219
216
  "email",
220
217
  provider.email,
221
218
  "password"
222
- ]);
223
- if (!existingPassword) return c.redirect(provider.redirect, 302);
219
+ ])) return c.redirect(provider.redirect, 302);
224
220
  const password = formData.get("password")?.toString();
225
221
  const repeat = formData.get("repeat")?.toString();
226
222
  if (!password) return transition(provider, { type: "invalid_password" });
@@ -274,8 +270,7 @@ const PBKDF2Hasher = (opts) => {
274
270
  const iterations = opts?.iterations ?? 6e5;
275
271
  return {
276
272
  async hash(password) {
277
- const encoder = new TextEncoder();
278
- const passwordBytes = encoder.encode(password);
273
+ const passwordBytes = new TextEncoder().encode(password);
279
274
  const salt = crypto.getRandomValues(new Uint8Array(16));
280
275
  const keyMaterial = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
281
276
  const hashBuffer = await crypto.subtle.deriveBits({
@@ -284,17 +279,14 @@ const PBKDF2Hasher = (opts) => {
284
279
  salt,
285
280
  iterations
286
281
  }, keyMaterial, 256);
287
- const hashBase64 = jose.base64url.encode(new Uint8Array(hashBuffer));
288
- const saltBase64 = jose.base64url.encode(salt);
289
282
  return {
290
- hash: hashBase64,
291
- salt: saltBase64,
283
+ hash: jose.base64url.encode(new Uint8Array(hashBuffer)),
284
+ salt: jose.base64url.encode(salt),
292
285
  iterations
293
286
  };
294
287
  },
295
288
  async verify(password, compare) {
296
- const encoder = new TextEncoder();
297
- const passwordBytes = encoder.encode(password);
289
+ const passwordBytes = new TextEncoder().encode(password);
298
290
  const salt = jose.base64url.decode(compare.salt);
299
291
  const keyMaterial = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
300
292
  const hashBuffer = await crypto.subtle.deriveBits({
@@ -303,8 +295,7 @@ const PBKDF2Hasher = (opts) => {
303
295
  salt,
304
296
  iterations: compare.iterations
305
297
  }, keyMaterial, 256);
306
- const hashBase64 = jose.base64url.encode(new Uint8Array(hashBuffer));
307
- return timingSafeCompare(hashBase64, compare.hash);
298
+ return timingSafeCompare(jose.base64url.encode(new Uint8Array(hashBuffer)), compare.hash);
308
299
  }
309
300
  };
310
301
  };
@@ -324,21 +315,18 @@ const ScryptHasher = (opts) => {
324
315
  async hash(password) {
325
316
  const salt = randomBytes(16);
326
317
  const keyLength = 32;
327
- const derivedKey = await new Promise((resolve, reject) => {
328
- scrypt(password, salt, keyLength, {
329
- N,
330
- r,
331
- p
332
- }, (err, derivedKey$1) => {
333
- if (err) reject(err);
334
- else resolve(derivedKey$1);
335
- });
336
- });
337
- const hashBase64 = derivedKey.toString("base64");
338
- const saltBase64 = salt.toString("base64");
339
318
  return {
340
- hash: hashBase64,
341
- salt: saltBase64,
319
+ hash: (await new Promise((resolve, reject) => {
320
+ scrypt(password, salt, keyLength, {
321
+ N,
322
+ r,
323
+ p
324
+ }, (err, derivedKey) => {
325
+ if (err) reject(err);
326
+ else resolve(derivedKey);
327
+ });
328
+ })).toString("base64"),
329
+ salt: salt.toString("base64"),
342
330
  N,
343
331
  r,
344
332
  p
@@ -347,17 +335,16 @@ const ScryptHasher = (opts) => {
347
335
  async verify(password, compare) {
348
336
  const salt = Buffer.from(compare.salt, "base64");
349
337
  const keyLength = 32;
350
- const derivedKey = await new Promise((resolve, reject) => {
338
+ return timingSafeEqual(await new Promise((resolve, reject) => {
351
339
  scrypt(password, salt, keyLength, {
352
340
  N: compare.N,
353
341
  r: compare.r,
354
342
  p: compare.p
355
- }, (err, derivedKey$1) => {
343
+ }, (err, derivedKey) => {
356
344
  if (err) reject(err);
357
- else resolve(derivedKey$1);
345
+ else resolve(derivedKey);
358
346
  });
359
- });
360
- return timingSafeEqual(derivedKey, Buffer.from(compare.hash, "base64"));
347
+ }), Buffer.from(compare.hash, "base64"));
361
348
  }
362
349
  };
363
350
  };
@@ -1,4 +1,4 @@
1
- import { StorageAdapter } from "../storage/storage.js";
1
+ import { StorageAdapter } from "../storage/storage.mjs";
2
2
  import { Router } from "@draftlab/auth-router";
3
3
  import { RouterContext } from "@draftlab/auth-router/types";
4
4
 
@@ -1,4 +1,4 @@
1
- import { Provider } from "./provider.js";
1
+ import { Provider } from "./provider.mjs";
2
2
 
3
3
  //#region src/provider/totp.d.ts
4
4
 
@@ -1,8 +1,50 @@
1
- import { generateSecureToken } from "../random.js";
2
- import { Storage } from "../storage/storage.js";
1
+ import { generateSecureToken } from "../random.mjs";
2
+ import { Storage } from "../storage/storage.mjs";
3
3
  import { Secret, TOTP } from "otpauth";
4
4
 
5
5
  //#region src/provider/totp.ts
6
+ /**
7
+ * Configures a provider that supports TOTP (Time-based One-Time Password) authentication.
8
+ *
9
+ * ```ts
10
+ * import { TOTPProvider } from "@draftlab/auth/provider/totp"
11
+ *
12
+ * export default issuer({
13
+ * providers: {
14
+ * totp: TOTPProvider({
15
+ * issuer: "My Application",
16
+ * setup: async (req, qrCode, secret, backupCodes) => {
17
+ * return new Response(renderSetupPage(qrCode, secret, backupCodes))
18
+ * },
19
+ * verify: async (req, error) => {
20
+ * return new Response(renderVerifyPage(error))
21
+ * },
22
+ * recovery: async (req, error) => {
23
+ * return new Response(renderRecoveryPage(error))
24
+ * }
25
+ * })
26
+ * },
27
+ * // ...
28
+ * })
29
+ * ```
30
+ *
31
+ * TOTPProvider implements Time-based One-Time Password authentication.
32
+ * It provides secure TOTP token generation and verification with backup recovery codes.
33
+ *
34
+ * The provider requires configuration of:
35
+ * - Issuer name for authenticator apps
36
+ * - UI handlers for setup, verification, and recovery flows
37
+ * - Optional TOTP parameters (algorithm, digits, period)
38
+ *
39
+ * It automatically manages:
40
+ * - Secure secret generation
41
+ * - QR code URL generation for authenticator apps
42
+ * - Token validation with timing attack protection
43
+ * - Recovery codes generation and one-time usage
44
+ * - Storage of TOTP configuration and backup codes
45
+ *
46
+ * @packageDocumentation
47
+ */
6
48
  const totpKey = (userId) => [
7
49
  "totp",
8
50
  "user",
@@ -72,16 +114,14 @@ const TOTPProvider = (config) => {
72
114
  const secret = new Secret({ size: 20 });
73
115
  const label = config.generateLabel ? await config.generateLabel(email) : email;
74
116
  const backupCodes = generateBackupCodes(backupCodesCount);
75
- const totp$1 = createTOTPInstance(secret.base32, label);
76
- const qrCodeUrl$1 = totp$1.toString();
77
- const totpData$1 = {
117
+ const qrCodeUrl$1 = createTOTPInstance(secret.base32, label).toString();
118
+ await saveTOTPData(email, {
78
119
  secret: secret.base32,
79
120
  enabled: false,
80
121
  backupCodes,
81
122
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
82
123
  label
83
- };
84
- await saveTOTPData(email, totpData$1);
124
+ });
85
125
  return ctx.forward(c, await config.register(c.request, qrCodeUrl$1, secret.base32, backupCodes, void 0, email));
86
126
  }
87
127
  const token = formData.get("token")?.toString();
@@ -89,11 +129,10 @@ const TOTPProvider = (config) => {
89
129
  const totpData = await getTOTPData(email);
90
130
  if (!totpData) return ctx.forward(c, await config.register(c.request, "", "", [], "TOTP setup session not found"));
91
131
  const totp = createTOTPInstance(totpData.secret, totpData.label || email);
92
- const delta = totp.validate({
132
+ if (totp.validate({
93
133
  token,
94
134
  window
95
- });
96
- if (delta !== null) {
135
+ }) !== null) {
97
136
  totpData.enabled = true;
98
137
  await saveTOTPData(email, totpData);
99
138
  return ctx.success(c, {
@@ -114,12 +153,10 @@ const TOTPProvider = (config) => {
114
153
  if (!email || !token) return ctx.forward(c, await config.authorize(c.request, "Email and verification code are required"));
115
154
  const totpData = await getTOTPData(email);
116
155
  if (!totpData || !totpData.enabled) return ctx.forward(c, await config.authorize(c.request, "TOTP is not set up for this email"));
117
- const totp = createTOTPInstance(totpData.secret, totpData.label || email);
118
- const delta = totp.validate({
156
+ if (createTOTPInstance(totpData.secret, totpData.label || email).validate({
119
157
  token,
120
158
  window
121
- });
122
- if (delta !== null) return ctx.success(c, {
159
+ }) !== null) return ctx.success(c, {
123
160
  email,
124
161
  method: "totp"
125
162
  });
@@ -25,8 +25,7 @@ const generateSecureToken = (length = 32) => {
25
25
  if (length <= 0 || !Number.isInteger(length)) throw new RangeError("Token length must be a positive integer");
26
26
  const randomBytes$1 = new Uint8Array(length);
27
27
  crypto.getRandomValues(randomBytes$1);
28
- const base64 = btoa(String.fromCharCode.apply(null, Array.from(randomBytes$1)));
29
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
28
+ return btoa(String.fromCharCode.apply(null, Array.from(randomBytes$1))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
30
29
  };
31
30
  /**
32
31
  * Generates a cryptographically secure string of random digits without modulo bias.
@@ -1,4 +1,4 @@
1
- import { StorageAdapter } from "./storage.js";
1
+ import { StorageAdapter } from "./storage.mjs";
2
2
 
3
3
  //#region src/storage/memory.d.ts
4
4
 
@@ -1,4 +1,4 @@
1
- import { joinKey, splitKey } from "./storage.js";
1
+ import { joinKey, splitKey } from "./storage.mjs";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { writeFile } from "node:fs/promises";
4
4
 
@@ -79,8 +79,7 @@ const MemoryStorage = (options) => {
79
79
  };
80
80
  return {
81
81
  async get(key) {
82
- const searchKey = joinKey(key);
83
- const match = search(searchKey);
82
+ const match = search(joinKey(key));
84
83
  if (!match.found) return;
85
84
  const storeEntry = store[match.index];
86
85
  if (!storeEntry) return;
@@ -104,8 +103,7 @@ const MemoryStorage = (options) => {
104
103
  await save();
105
104
  },
106
105
  async remove(key) {
107
- const searchKey = joinKey(key);
108
- const match = search(searchKey);
106
+ const match = search(joinKey(key));
109
107
  if (match.found) {
110
108
  store.splice(match.index, 1);
111
109
  await save();
@@ -42,50 +42,64 @@ interface StorageAdapter {
42
42
  }
43
43
  /**
44
44
  * Joins an array of key segments into a single string using the separator.
45
+ * Segments are properly escaped to handle any input, including separators and escape characters.
45
46
  *
46
47
  * @param key - Array of key segments to join
47
48
  * @returns Single string representing the full key path
48
49
  *
49
50
  * @example
50
51
  * ```ts
51
- * joinKey(['user', 'session', '123'])
52
- * // Returns: "user\x1fsession\x1f123"
52
+ * joinKey(['user', 'data\x1fwith\x1fseparators'])
53
+ * // Returns: "user\x1fdata\\x1fwith\\x1fseparators"
53
54
  * ```
54
55
  */
55
56
  declare const joinKey: (key: string[]) => string;
56
57
  /**
57
58
  * Splits a joined key string back into its component segments.
59
+ * Handles escaped characters properly.
58
60
  *
59
61
  * @param key - Joined key string to split
60
62
  * @returns Array of individual key segments
61
63
  *
62
64
  * @example
63
65
  * ```ts
64
- * splitKey("user\x1fsession\x1f123")
65
- * // Returns: ['user', 'session', '123']
66
+ * splitKey("user\x1fdata\\x1fwith\\x1fseparators")
67
+ * // Returns: ['user', 'data\x1fwith\x1fseparators']
66
68
  * ```
67
69
  */
68
70
  declare const splitKey: (key: string) => string[];
69
71
  /**
70
72
  * High-level storage operations with key encoding and type safety.
71
73
  * Provides a convenient interface over storage adapters with additional features
72
- * like TTL conversion and key sanitization.
74
+ * like TTL validation and secure key encoding to prevent collisions.
73
75
  */
74
76
  declare const Storage: {
75
77
  /**
76
- * Encodes key segments by removing any separator characters to prevent conflicts.
77
- * Ensures storage keys don't contain characters that could break key parsing.
78
+ * Encodes key segments by escaping special characters.
79
+ * Ensures storage keys don't contain unescaped separator characters that could cause collisions.
78
80
  *
79
81
  * @param key - Array of key segments to encode
80
- * @returns Array of sanitized key segments
82
+ * @returns Array of properly escaped key segments
83
+ *
84
+ * @throws {Error} If any segment is empty or whitespace-only
81
85
  *
82
86
  * @example
83
87
  * ```ts
84
88
  * Storage.encode(['user', 'data\x1fwith\x1fseparators'])
85
- * // Returns: ['user', 'datawithseparators']
89
+ * // Returns: ['user', 'data\\x1fwith\\x1fseparators']
86
90
  * ```
87
91
  */
88
92
  readonly encode: (key: string[]) => string[];
93
+ /**
94
+ * Decodes key segments by unescaping special characters.
95
+ * Reverse operation of encode().
96
+ *
97
+ * @param key - Array of encoded key segments
98
+ * @returns Array of decoded key segments
99
+ *
100
+ * @internal
101
+ */
102
+ readonly decode: (key: string[]) => string[];
89
103
  /**
90
104
  * Retrieves a typed value from storage.
91
105
  *
@@ -110,6 +124,7 @@ declare const Storage: {
110
124
  readonly get: <T = Record<string, unknown>>(adapter: StorageAdapter, key: string[]) => Promise<T | null>;
111
125
  /**
112
126
  * Stores a value with optional time-to-live in seconds.
127
+ * Validates that TTL is a positive integer to prevent edge cases like negative or overflow values.
113
128
  *
114
129
  * @param adapter - Storage adapter to use
115
130
  * @param key - Array of key segments identifying where to store
@@ -117,12 +132,14 @@ declare const Storage: {
117
132
  * @param ttlSeconds - Optional TTL in seconds for automatic expiration
118
133
  * @returns Promise that resolves when storage is complete
119
134
  *
135
+ * @throws {RangeError} If TTL is invalid (negative, non-integer, or exceeds maximum)
136
+ *
120
137
  * @example
121
138
  * ```ts
122
139
  * // Store with 1 hour TTL
123
140
  * await Storage.set(adapter, ['sessions', sessionId], sessionData, 3600)
124
141
  *
125
- * // Store permanently
142
+ * // Store permanently (no expiration)
126
143
  * await Storage.set(adapter, ['users', userId], userData)
127
144
  * ```
128
145
  */
@@ -0,0 +1,104 @@
1
+ //#region src/storage/storage.ts
2
+ /**
3
+ * ASCII unit separator character used to join key segments.
4
+ * Using a control character ensures it won't conflict with user data.
5
+ */
6
+ const SEPARATOR = String.fromCharCode(31);
7
+ /**
8
+ * Escape character used to escape SEPARATOR characters in key segments.
9
+ * Uses backslash as the escape character, which is then itself escaped when appearing.
10
+ */
11
+ const ESCAPE = "\\";
12
+ /**
13
+ * Joins an array of key segments into a single string using the separator.
14
+ * Segments are properly escaped to handle any input, including separators and escape characters.
15
+ *
16
+ * @param key - Array of key segments to join
17
+ * @returns Single string representing the full key path
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * joinKey(['user', 'data\x1fwith\x1fseparators'])
22
+ * // Returns: "user\x1fdata\\x1fwith\\x1fseparators"
23
+ * ```
24
+ */
25
+ const joinKey = (key) => {
26
+ return key.join(SEPARATOR);
27
+ };
28
+ /**
29
+ * Splits a joined key string back into its component segments.
30
+ * Handles escaped characters properly.
31
+ *
32
+ * @param key - Joined key string to split
33
+ * @returns Array of individual key segments
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * splitKey("user\x1fdata\\x1fwith\\x1fseparators")
38
+ * // Returns: ['user', 'data\x1fwith\x1fseparators']
39
+ * ```
40
+ */
41
+ const splitKey = (key) => {
42
+ return key.split(SEPARATOR);
43
+ };
44
+ /**
45
+ * Encodes a single key segment by escaping special characters.
46
+ * Prevents collisions by properly escaping separator and escape characters.
47
+ *
48
+ * @param segment - The key segment to encode
49
+ * @returns Encoded segment with special characters escaped
50
+ * @throws {Error} If segment is empty or whitespace-only
51
+ *
52
+ * @internal
53
+ */
54
+ const encodeSegment = (segment) => {
55
+ if (!segment || !segment.trim()) throw new Error(`Storage key segment cannot be empty or whitespace-only: "${segment}"`);
56
+ return segment.replaceAll(ESCAPE, ESCAPE + ESCAPE).replaceAll(SEPARATOR, ESCAPE + SEPARATOR);
57
+ };
58
+ /**
59
+ * Decodes a key segment by unescaping special characters.
60
+ * Reverse of encodeSegment operation.
61
+ *
62
+ * @param segment - The encoded segment to decode
63
+ * @returns Decoded segment with special characters restored
64
+ *
65
+ * @internal
66
+ */
67
+ const decodeSegment = (segment) => {
68
+ return segment.replaceAll(ESCAPE + SEPARATOR, SEPARATOR).replaceAll(ESCAPE + ESCAPE, ESCAPE);
69
+ };
70
+ /**
71
+ * High-level storage operations with key encoding and type safety.
72
+ * Provides a convenient interface over storage adapters with additional features
73
+ * like TTL validation and secure key encoding to prevent collisions.
74
+ */
75
+ const Storage = {
76
+ encode: (key) => {
77
+ return key.map(encodeSegment);
78
+ },
79
+ decode: (key) => {
80
+ return key.map(decodeSegment);
81
+ },
82
+ get: (adapter, key) => {
83
+ return adapter.get(Storage.encode(key));
84
+ },
85
+ set: (adapter, key, value, ttlSeconds) => {
86
+ if (ttlSeconds !== void 0 && ttlSeconds !== null) {
87
+ if (!Number.isInteger(ttlSeconds)) throw new RangeError(`Storage TTL must be an integer in seconds, received ${typeof ttlSeconds}`);
88
+ if (ttlSeconds <= 0) throw new RangeError(`Storage TTL must be positive, received ${ttlSeconds}`);
89
+ const maxTtlSeconds = 3600 * 24 * 365 * 10;
90
+ if (ttlSeconds > maxTtlSeconds) throw new RangeError(`Storage TTL exceeds maximum (${maxTtlSeconds}s = 10 years), received ${ttlSeconds}s`);
91
+ }
92
+ const expiry = ttlSeconds ? new Date(Date.now() + ttlSeconds * 1e3) : void 0;
93
+ return adapter.set(Storage.encode(key), value, expiry);
94
+ },
95
+ remove: (adapter, key) => {
96
+ return adapter.remove(Storage.encode(key));
97
+ },
98
+ scan: (adapter, prefix) => {
99
+ return adapter.scan(Storage.encode(prefix));
100
+ }
101
+ };
102
+
103
+ //#endregion
104
+ export { Storage, joinKey, splitKey };
@@ -1,4 +1,4 @@
1
- import { StorageAdapter } from "./storage.js";
1
+ import { StorageAdapter } from "./storage.mjs";
2
2
  import { Client } from "@libsql/client";
3
3
 
4
4
  //#region src/storage/turso.d.ts
@@ -1,4 +1,4 @@
1
- import { joinKey, splitKey } from "./storage.js";
1
+ import { joinKey, splitKey } from "./storage.mjs";
2
2
 
3
3
  //#region src/storage/turso.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { StorageAdapter } from "./storage.js";
1
+ import { StorageAdapter } from "./storage.mjs";
2
2
  import { Driver } from "unstorage";
3
3
 
4
4
  //#region src/storage/unstorage.d.ts
@@ -1,8 +1,15 @@
1
- import { joinKey, splitKey } from "./storage.js";
1
+ import { joinKey, splitKey } from "./storage.mjs";
2
2
  import { createStorage } from "unstorage";
3
3
 
4
4
  //#region src/storage/unstorage.ts
5
5
  /**
6
+ * Universal storage adapter for Draft Auth using Unstorage drivers.
7
+ * Provides seamless integration with any Unstorage-compatible backend including
8
+ * Redis, Cloudflare KV, Vercel KV, and more.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ /**
6
13
  * Creates a Draft Auth storage adapter using Unstorage drivers.
7
14
  * Supports automatic expiration, error handling, and any Unstorage driver.
8
15
  *
@@ -35,15 +42,15 @@ const UnStorage = ({ driver } = {}) => {
35
42
  try {
36
43
  const keyPath = joinKey(key);
37
44
  const entry = await store.getItem(keyPath);
38
- if (!entry) return void 0;
45
+ if (!entry) return;
39
46
  if (entry.expiry && Date.now() >= entry.expiry) {
40
47
  store.removeItem(keyPath).catch(() => {});
41
- return void 0;
48
+ return;
42
49
  }
43
50
  return entry.value;
44
51
  } catch (error) {
45
52
  console.error("UnStorage get error:", error);
46
- return void 0;
53
+ return;
47
54
  }
48
55
  },
49
56
  async set(key, value, expiry) {
@@ -1,4 +1,4 @@
1
- import { Prettify } from "./util.js";
1
+ import { Prettify } from "./util.mjs";
2
2
  import { StandardSchemaV1 } from "@standard-schema/spec";
3
3
 
4
4
  //#region src/subject.d.ts
package/dist/types.mjs ADDED
@@ -0,0 +1 @@
1
+ export { };
@@ -1,4 +1,4 @@
1
- import { Theme } from "../themes/theme.js";
1
+ import { Theme } from "../themes/theme.mjs";
2
2
  import * as preact0 from "preact";
3
3
  import { ComponentChildren } from "preact";
4
4
 
@@ -1,4 +1,4 @@
1
- import { getTheme } from "../themes/theme.js";
1
+ import { getTheme } from "../themes/theme.mjs";
2
2
  import { render } from "preact-render-to-string";
3
3
  import { jsx, jsxs } from "preact/jsx-runtime";
4
4
 
@@ -1,4 +1,4 @@
1
- import { CodeProviderOptions } from "../provider/code.js";
1
+ import { CodeProviderOptions } from "../provider/code.mjs";
2
2
 
3
3
  //#region src/ui/code.d.ts
4
4
 
@@ -1,5 +1,5 @@
1
- import { Layout, renderToHTML } from "./base.js";
2
- import { FormAlert } from "./form.js";
1
+ import { Layout, renderToHTML } from "./base.mjs";
2
+ import { FormAlert } from "./form.mjs";
3
3
  import { jsx, jsxs } from "preact/jsx-runtime";
4
4
 
5
5
  //#region src/ui/code.tsx
@@ -34,9 +34,8 @@ const getErrorMessage = (error, copy) => {
34
34
  const getSuccessMessage = (state, copy) => {
35
35
  if (state.type === "start" || !state.claims) return void 0;
36
36
  const contact = state.claims.email || state.claims.phone || "";
37
- const prefix = state.resend ? copy.code_resent : copy.code_sent;
38
37
  return {
39
- message: `${prefix}${contact}`,
38
+ message: `${state.resend ? copy.code_resent : copy.code_sent}${contact}`,
40
39
  contact
41
40
  };
42
41
  };
@@ -1,4 +1,4 @@
1
- import { MagicLinkConfig } from "../provider/magiclink.js";
1
+ import { MagicLinkConfig } from "../provider/magiclink.mjs";
2
2
 
3
3
  //#region src/ui/magiclink.d.ts
4
4
 
@@ -1,5 +1,5 @@
1
- import { Layout, renderToHTML } from "./base.js";
2
- import { FormAlert } from "./form.js";
1
+ import { Layout, renderToHTML } from "./base.mjs";
2
+ import { FormAlert } from "./form.mjs";
3
3
  import { jsx, jsxs } from "preact/jsx-runtime";
4
4
 
5
5
  //#region src/ui/magiclink.tsx
@@ -32,9 +32,8 @@ const getErrorMessage = (error, copy) => {
32
32
  const getSuccessMessage = (state, copy, mode) => {
33
33
  if (state.type === "start" || !state.claims) return void 0;
34
34
  const contact = state.claims[mode] || "";
35
- const prefix = state.resend ? copy.link_resent : copy.link_sent;
36
35
  return {
37
- message: `${prefix}${contact}`,
36
+ message: `${state.resend ? copy.link_resent : copy.link_sent}${contact}`,
38
37
  contact
39
38
  };
40
39
  };
@@ -1,4 +1,4 @@
1
- import { PasskeyProviderConfig } from "../provider/passkey.js";
1
+ import { PasskeyProviderConfig } from "../provider/passkey.mjs";
2
2
 
3
3
  //#region src/ui/passkey.d.ts
4
4