@shtora/auth 0.0.0-stage → 0.1.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.
package/dist/auth.js ADDED
@@ -0,0 +1,510 @@
1
+ import { clearedSessionCookie, readSessionToken, sessionCookie } from "./cookies.js";
2
+ import { createSessionToken, hashSessionToken } from "./crypto.js";
3
+ import { parseActionTokenRequest, parseEmailRequest, parseLoginCredentials, parsePasswordReset, parseRegistrationCredentials, readJson, RequestError } from "./request.js";
4
+ const defaultBasePath = "/api/auth";
5
+ const defaultCookieName = "__Host-shtora.session";
6
+ const defaultSessionTtl = 60 * 60 * 24 * 7;
7
+ const maxSessionTtl = 60 * 60 * 24 * 365;
8
+ const defaultMaxRequestBytes = 8192;
9
+ const defaultVerificationTokenTtl = 60 * 60;
10
+ const defaultResetTokenTtl = 15 * 60;
11
+ const maxActionTokenTtl = 60 * 60 * 24;
12
+ const defaultPasswordPolicy = { minLength: 1, maxLength: 4096 };
13
+ const hardMaxPasswordLength = 4096;
14
+ export function createAuth(options) {
15
+ const basePath = options.basePath ?? defaultBasePath;
16
+ const cookieName = options.cookieName ?? defaultCookieName;
17
+ const sessionTtlSeconds = options.sessionTtlSeconds ?? defaultSessionTtl;
18
+ const maxRequestBytes = options.maxRequestBytes ?? defaultMaxRequestBytes;
19
+ const requireOrigin = options.requireOrigin ?? true;
20
+ const registrationMode = options.identity?.registration ?? "email";
21
+ const loginMode = options.identity?.login ?? "email";
22
+ const passwordPolicy = { ...defaultPasswordPolicy, ...options.passwordPolicy };
23
+ if (!["email", "username", "both", "either"].includes(registrationMode) ||
24
+ !["email", "username", "either"].includes(loginMode)) {
25
+ throw new TypeError("identity modes are invalid");
26
+ }
27
+ if ((registrationMode === "email" && loginMode !== "email") ||
28
+ (registrationMode === "username" && loginMode !== "username") ||
29
+ (registrationMode === "either" && loginMode !== "either")) {
30
+ throw new TypeError("identity login mode is incompatible with registration mode");
31
+ }
32
+ if (!Number.isInteger(passwordPolicy.minLength) || passwordPolicy.minLength < 1 ||
33
+ !Number.isInteger(passwordPolicy.maxLength) || passwordPolicy.maxLength > hardMaxPasswordLength ||
34
+ passwordPolicy.maxLength < passwordPolicy.minLength) {
35
+ throw new RangeError("passwordPolicy lengths must be integers between 1 and 4096 with minLength <= maxLength");
36
+ }
37
+ for (const flag of [
38
+ options.passwordPolicy?.requireUppercase,
39
+ options.passwordPolicy?.requireLowercase,
40
+ options.passwordPolicy?.requireSpecial,
41
+ ]) {
42
+ if (flag !== undefined && typeof flag !== "boolean")
43
+ throw new TypeError("passwordPolicy requirements must be booleans");
44
+ }
45
+ if (options.emailVerification && registrationMode !== "email" && registrationMode !== "both") {
46
+ throw new TypeError("emailVerification requires registration mode 'email' or 'both'");
47
+ }
48
+ if (!basePath.startsWith("/") || basePath.endsWith("/") || basePath.includes("?")) {
49
+ throw new TypeError("basePath must start with '/' and must not end with '/'");
50
+ }
51
+ if (!/^__Host-[A-Za-z0-9._-]+$/.test(cookieName)) {
52
+ throw new TypeError("cookieName must use the __Host- prefix");
53
+ }
54
+ if (!Number.isInteger(sessionTtlSeconds) || sessionTtlSeconds < 1 || sessionTtlSeconds > maxSessionTtl) {
55
+ throw new RangeError("sessionTtlSeconds must be between 1 and 31536000");
56
+ }
57
+ if (!Number.isInteger(maxRequestBytes) || maxRequestBytes < 1 || maxRequestBytes > 1024 * 1024) {
58
+ throw new RangeError("maxRequestBytes must be between 1 and 1048576");
59
+ }
60
+ if ((options.ipPolicy || options.rateLimiter) && !options.resolveClientIp) {
61
+ throw new TypeError("resolveClientIp is required when ipPolicy or rateLimiter is configured");
62
+ }
63
+ for (const ttl of [options.emailVerification?.tokenTtlSeconds, options.passwordReset?.tokenTtlSeconds]) {
64
+ if (ttl !== undefined && (!Number.isInteger(ttl) || ttl < 1 || ttl > maxActionTokenTtl)) {
65
+ throw new RangeError("tokenTtlSeconds must be between 1 and 86400");
66
+ }
67
+ }
68
+ function passwordAllowed(password) {
69
+ let length = 0;
70
+ for (const _character of password)
71
+ length += 1;
72
+ return length >= passwordPolicy.minLength &&
73
+ length <= passwordPolicy.maxLength &&
74
+ (!options.passwordPolicy?.requireUppercase || /\p{Lu}/u.test(password)) &&
75
+ (!options.passwordPolicy?.requireLowercase || /\p{Ll}/u.test(password)) &&
76
+ (!options.passwordPolicy?.requireSpecial || /[^\p{L}\p{N}\p{White_Space}]/u.test(password));
77
+ }
78
+ function requirePasswordPolicy(password) {
79
+ if (!passwordAllowed(password))
80
+ throw new RequestError(400, "invalid_request");
81
+ }
82
+ function requireEmail(user) {
83
+ if (typeof user.email !== "string" || user.email.length === 0) {
84
+ throw new TypeError("UserStore returned a user without an email");
85
+ }
86
+ return user.email;
87
+ }
88
+ async function isAllowed(policy, value, request) {
89
+ return policy ? policy.allow({ value, request }) : true;
90
+ }
91
+ async function clientIp(request) {
92
+ const ip = await options.resolveClientIp(request);
93
+ return typeof ip === "string" && ip.length > 0 ? ip : null;
94
+ }
95
+ async function report(operation, error) {
96
+ try {
97
+ await options.onError?.({ operation, error });
98
+ }
99
+ catch {
100
+ // Observers must not affect the public result.
101
+ }
102
+ }
103
+ function response(body, status, headers) {
104
+ const responseHeaders = new Headers(headers);
105
+ responseHeaders.set("Cache-Control", "no-store");
106
+ responseHeaders.set("Content-Type", "application/json; charset=utf-8");
107
+ responseHeaders.set("X-Content-Type-Options", "nosniff");
108
+ responseHeaders.set("Vary", "Cookie");
109
+ return new Response(JSON.stringify(body), { status, headers: responseHeaders });
110
+ }
111
+ function errorResponse(error) {
112
+ if (error instanceof RequestError)
113
+ return response({ error: error.code }, error.status);
114
+ return response({ error: "internal_error" }, 500);
115
+ }
116
+ function hostIsAllowed(request) {
117
+ if (!options.allowedHosts)
118
+ return true;
119
+ const host = request.headers.get("host");
120
+ if (!host || host.includes(","))
121
+ return false;
122
+ try {
123
+ const parsed = new URL(`https://${host}`);
124
+ return parsed.pathname === "/" && !parsed.username && !parsed.password &&
125
+ options.allowedHosts.some((allowed) => allowed.toLowerCase() === parsed.host.toLowerCase());
126
+ }
127
+ catch {
128
+ return false;
129
+ }
130
+ }
131
+ function originIsAllowed(request) {
132
+ const origin = request.headers.get("origin");
133
+ if (!origin)
134
+ return !requireOrigin;
135
+ try {
136
+ const parsedOrigin = new URL(origin);
137
+ const normalizedOrigin = parsedOrigin.origin.toLowerCase();
138
+ if (parsedOrigin.username ||
139
+ parsedOrigin.password ||
140
+ parsedOrigin.pathname !== "/" ||
141
+ parsedOrigin.search ||
142
+ parsedOrigin.hash ||
143
+ origin.toLowerCase() !== normalizedOrigin)
144
+ return false;
145
+ const requestUrl = new URL(request.url);
146
+ const host = request.headers.get("host");
147
+ const expectedOrigin = host ? new URL(`${requestUrl.protocol}//${host}`).origin : requestUrl.origin;
148
+ return normalizedOrigin === expectedOrigin.toLowerCase();
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ }
154
+ function assertUserIdentity(user) {
155
+ const validEmail = user.email === undefined ||
156
+ (typeof user.email === "string" && user.email.length <= 254 && user.email.includes("@") &&
157
+ !/\s/.test(user.email) && user.email === user.email.toLowerCase());
158
+ const validUsername = user.username === undefined ||
159
+ (typeof user.username === "string" && /^[a-z0-9._-]{3,32}$/.test(user.username));
160
+ const hasEmail = user.email !== undefined;
161
+ const hasUsername = user.username !== undefined;
162
+ const modeSatisfied = (registrationMode === "email" && hasEmail && !hasUsername) ||
163
+ (registrationMode === "username" && hasUsername && !hasEmail) ||
164
+ (registrationMode === "both" && hasEmail && hasUsername) ||
165
+ (registrationMode === "either" && hasEmail !== hasUsername);
166
+ if (typeof user.id !== "string" || user.id.length === 0 || !validEmail || !validUsername || !modeSatisfied) {
167
+ throw new TypeError("UserStore returned an invalid user identity");
168
+ }
169
+ }
170
+ function publicUser(user) {
171
+ return {
172
+ id: user.id,
173
+ ...(["email", "both"].includes(registrationMode) && user.email !== undefined ? { email: user.email } : {}),
174
+ ...(["username", "both"].includes(registrationMode) && user.username !== undefined ? { username: user.username } : {}),
175
+ ...(registrationMode === "either" && user.email !== undefined ? { email: user.email } : {}),
176
+ ...(registrationMode === "either" && user.username !== undefined ? { username: user.username } : {}),
177
+ };
178
+ }
179
+ async function getSession(request) {
180
+ try {
181
+ const token = readSessionToken(request.headers.get("cookie"), cookieName);
182
+ if (!token)
183
+ return null;
184
+ const tokenHash = await hashSessionToken(token);
185
+ const stored = await options.sessions.findByTokenHash(tokenHash);
186
+ const now = Date.now();
187
+ if (!stored || stored.tokenHash !== tokenHash || !Number.isFinite(stored.expiresAtMs) || stored.expiresAtMs <= now) {
188
+ if (stored?.tokenHash === tokenHash)
189
+ await options.sessions.deleteByTokenHash(tokenHash);
190
+ return null;
191
+ }
192
+ const user = await options.users.findById(stored.userId);
193
+ if (!user) {
194
+ await options.sessions.deleteByTokenHash(tokenHash);
195
+ return null;
196
+ }
197
+ assertUserIdentity(user);
198
+ if (options.emailVerification && !options.emailVerification.isVerified(user))
199
+ return null;
200
+ if (options.emailPolicy && (!user.email || !(await isAllowed(options.emailPolicy, user.email, request))))
201
+ return null;
202
+ if (options.ipPolicy) {
203
+ const ip = await clientIp(request);
204
+ if (!ip || !(await isAllowed(options.ipPolicy, ip, request)))
205
+ return null;
206
+ }
207
+ return publicUser(user);
208
+ }
209
+ catch (error) {
210
+ if (!(error instanceof RequestError))
211
+ await report("getSession", error);
212
+ throw error;
213
+ }
214
+ }
215
+ async function rateLimit(action, identifier, request) {
216
+ if (!options.rateLimiter)
217
+ return null;
218
+ const ip = await clientIp(request);
219
+ if (!ip) {
220
+ if (action === "login")
221
+ return response({ error: "invalid_credentials" }, 401);
222
+ return response({ status: "accepted" }, 202);
223
+ }
224
+ const result = await options.rateLimiter.check({ action, identifier, ip });
225
+ if (result.allowed)
226
+ return null;
227
+ const seconds = Number.isInteger(result.retryAfterSeconds) && result.retryAfterSeconds > 0
228
+ ? result.retryAfterSeconds
229
+ : 60;
230
+ return response({ error: "rate_limited" }, 429, { "Retry-After": String(seconds) });
231
+ }
232
+ async function issueActionToken(purpose, userId, email, tokens, delivery, defaultTtlSeconds, operation) {
233
+ try {
234
+ const token = createSessionToken();
235
+ const expiresAtMs = Date.now() + (delivery.tokenTtlSeconds ?? defaultTtlSeconds) * 1000;
236
+ await tokens.create({ purpose, userId, tokenHash: await hashSessionToken(token), expiresAtMs });
237
+ await delivery.send({ email, token, expiresAtMs });
238
+ }
239
+ catch (error) {
240
+ await report(operation, error);
241
+ }
242
+ }
243
+ async function passPolicies(email, request) {
244
+ if (options.emailPolicy && (!email || !(await isAllowed(options.emailPolicy, email, request))))
245
+ return false;
246
+ if (!options.ipPolicy)
247
+ return true;
248
+ const ip = await clientIp(request);
249
+ return ip !== null && isAllowed(options.ipPolicy, ip, request);
250
+ }
251
+ async function register(request) {
252
+ const credentials = parseRegistrationCredentials(await readJson(request, maxRequestBytes), registrationMode);
253
+ requirePasswordPolicy(credentials.password);
254
+ const identifier = credentials.email !== undefined
255
+ ? { type: "email", value: credentials.email }
256
+ : { type: "username", value: credentials.username };
257
+ const limited = await rateLimit("register", identifier, request);
258
+ if (limited)
259
+ return limited;
260
+ if (!(await passPolicies(credentials.email, request)))
261
+ return response({ status: "accepted" }, 202);
262
+ const created = await options.users.create(credentials);
263
+ if (created.created) {
264
+ if (!created.user)
265
+ throw new TypeError("UserStore must return the created user");
266
+ assertUserIdentity(created.user);
267
+ if (options.emailVerification) {
268
+ await issueActionToken("email-verification", created.user.id, requireEmail(created.user), options.emailVerification.tokens, options.emailVerification, defaultVerificationTokenTtl, "email-verification.send");
269
+ }
270
+ }
271
+ return response({ status: "accepted" }, 202);
272
+ }
273
+ async function requestEmailVerification(request) {
274
+ const flow = options.emailVerification;
275
+ if (!flow)
276
+ return response({ error: "not_found" }, 404);
277
+ const email = parseEmailRequest(await readJson(request, maxRequestBytes));
278
+ const limited = await rateLimit("email-verification", { type: "email", value: email }, request);
279
+ if (limited)
280
+ return limited;
281
+ if (!(await passPolicies(email, request)))
282
+ return response({ status: "accepted" }, 202);
283
+ const user = await options.users.findByEmail(email);
284
+ if (user && !flow.isVerified(user)) {
285
+ await issueActionToken("email-verification", user.id, requireEmail(user), flow.tokens, flow, defaultVerificationTokenTtl, "email-verification.send");
286
+ }
287
+ return response({ status: "accepted" }, 202);
288
+ }
289
+ async function confirmEmailVerification(request) {
290
+ const flow = options.emailVerification;
291
+ if (!flow)
292
+ return response({ error: "not_found" }, 404);
293
+ const token = parseActionTokenRequest(await readJson(request, maxRequestBytes));
294
+ const consumed = await flow.tokens.consume({
295
+ purpose: "email-verification",
296
+ tokenHash: await hashSessionToken(token),
297
+ nowMs: Date.now(),
298
+ });
299
+ if (!consumed)
300
+ return response({ error: "invalid_token" }, 400);
301
+ const user = await options.users.findById(consumed.userId);
302
+ if (!user)
303
+ return response({ error: "invalid_token" }, 400);
304
+ await flow.markVerified(user.id);
305
+ return new Response(null, { status: 204, headers: { "Cache-Control": "no-store" } });
306
+ }
307
+ async function requestPasswordReset(request) {
308
+ const flow = options.passwordReset;
309
+ if (!flow)
310
+ return response({ error: "not_found" }, 404);
311
+ const email = parseEmailRequest(await readJson(request, maxRequestBytes));
312
+ const limited = await rateLimit("password-reset", { type: "email", value: email }, request);
313
+ if (limited)
314
+ return limited;
315
+ if (!(await passPolicies(email, request)))
316
+ return response({ status: "accepted" }, 202);
317
+ const user = await options.users.findByEmail(email);
318
+ if (user) {
319
+ await issueActionToken("password-reset", user.id, requireEmail(user), flow.tokens, flow, defaultResetTokenTtl, "password-reset.send");
320
+ }
321
+ return response({ status: "accepted" }, 202);
322
+ }
323
+ async function confirmPasswordReset(request) {
324
+ const flow = options.passwordReset;
325
+ if (!flow)
326
+ return response({ error: "not_found" }, 404);
327
+ const { token, password } = parsePasswordReset(await readJson(request, maxRequestBytes));
328
+ requirePasswordPolicy(password);
329
+ const consumed = await flow.tokens.consume({
330
+ purpose: "password-reset",
331
+ tokenHash: await hashSessionToken(token),
332
+ nowMs: Date.now(),
333
+ });
334
+ if (!consumed)
335
+ return response({ error: "invalid_token" }, 400);
336
+ const user = await options.users.findById(consumed.userId);
337
+ if (!user)
338
+ return response({ error: "invalid_token" }, 400);
339
+ await options.sessions.deleteByUserId(user.id);
340
+ await flow.setPassword({ userId: user.id, password });
341
+ return new Response(null, { status: 204, headers: { "Cache-Control": "no-store" } });
342
+ }
343
+ async function createSessionForUser(user) {
344
+ assertUserIdentity(user);
345
+ const token = createSessionToken();
346
+ const tokenHash = await hashSessionToken(token);
347
+ const now = Date.now();
348
+ const session = {
349
+ id: createSessionToken(),
350
+ tokenHash,
351
+ userId: user.id,
352
+ createdAtMs: now,
353
+ expiresAtMs: now + sessionTtlSeconds * 1000,
354
+ };
355
+ await options.sessions.create(session);
356
+ return response({ user: publicUser(user) }, 200, {
357
+ "Set-Cookie": sessionCookie(token, cookieName, sessionTtlSeconds, now),
358
+ });
359
+ }
360
+ async function createSessionResponse(request, userId) {
361
+ try {
362
+ if (!hostIsAllowed(request))
363
+ return response({ error: "invalid_host" }, 403);
364
+ const user = await options.users.findById(userId);
365
+ if (!user)
366
+ return response({ error: "unauthenticated" }, 401);
367
+ assertUserIdentity(user);
368
+ if ((options.emailVerification && !options.emailVerification.isVerified(user)) ||
369
+ !(await passPolicies(user.email, request))) {
370
+ return response({ error: "unauthenticated" }, 401);
371
+ }
372
+ return await createSessionForUser(user);
373
+ }
374
+ catch (error) {
375
+ if (!(error instanceof RequestError))
376
+ await report("createSessionResponse", error);
377
+ throw error;
378
+ }
379
+ }
380
+ async function login(request) {
381
+ const { identifier, password } = parseLoginCredentials(await readJson(request, maxRequestBytes), loginMode);
382
+ const limited = await rateLimit("login", identifier, request);
383
+ if (limited)
384
+ return limited;
385
+ const user = await options.users.findByIdentifier(identifier);
386
+ const verified = await options.credentials.verify({ identifier, password, user });
387
+ if (user)
388
+ assertUserIdentity(user);
389
+ const policyAllowed = await passPolicies(identifier.type === "email" ? identifier.value : user?.email, request);
390
+ if (!user || !verified || !policyAllowed ||
391
+ (options.emailVerification && !options.emailVerification.isVerified(user))) {
392
+ return response({ error: "invalid_credentials" }, 401);
393
+ }
394
+ return createSessionForUser(user);
395
+ }
396
+ async function logout(request) {
397
+ const token = readSessionToken(request.headers.get("cookie"), cookieName);
398
+ if (token)
399
+ await options.sessions.deleteByTokenHash(await hashSessionToken(token));
400
+ return new Response(null, {
401
+ status: 204,
402
+ headers: { "Cache-Control": "no-store", "Set-Cookie": clearedSessionCookie(cookieName) },
403
+ });
404
+ }
405
+ async function cleanupExpired() {
406
+ const stores = new Set([options.sessions]);
407
+ if (options.emailVerification)
408
+ stores.add(options.emailVerification.tokens);
409
+ if (options.passwordReset)
410
+ stores.add(options.passwordReset.tokens);
411
+ const nowMs = Date.now();
412
+ try {
413
+ await Promise.all([...stores].map((store) => store.deleteExpired({ nowMs, limit: 500 })));
414
+ }
415
+ catch (error) {
416
+ await report("cleanupExpired", error);
417
+ throw error;
418
+ }
419
+ }
420
+ async function handle(request) {
421
+ if (!hostIsAllowed(request))
422
+ return response({ error: "invalid_host" }, 403);
423
+ const { pathname } = new URL(request.url);
424
+ if (!pathname.startsWith(`${basePath}/`))
425
+ return response({ error: "not_found" }, 404);
426
+ const route = pathname.slice(basePath.length);
427
+ const method = request.method.toUpperCase();
428
+ if (route === "/session" && method === "GET") {
429
+ if (!originIsAllowed(request) && request.headers.has("origin"))
430
+ return response({ error: "invalid_origin" }, 403);
431
+ try {
432
+ const user = await getSession(request);
433
+ return user ? response({ user }, 200) : response({ error: "unauthenticated" }, 401);
434
+ }
435
+ catch (error) {
436
+ return errorResponse(error);
437
+ }
438
+ }
439
+ if (route === "/sessions" || /^\/sessions\/[^/]+$/.test(route)) {
440
+ if ((route === "/sessions" && !["GET", "DELETE"].includes(method)) ||
441
+ (route !== "/sessions" && method !== "DELETE")) {
442
+ return response({ error: "method_not_allowed" }, 405, { Allow: route === "/sessions" ? "GET, DELETE" : "DELETE" });
443
+ }
444
+ if ((method !== "GET" || request.headers.has("origin")) && !originIsAllowed(request)) {
445
+ return response({ error: "invalid_origin" }, 403);
446
+ }
447
+ let user;
448
+ try {
449
+ user = await getSession(request);
450
+ }
451
+ catch (error) {
452
+ return errorResponse(error);
453
+ }
454
+ if (!user)
455
+ return response({ error: "unauthenticated" }, 401);
456
+ try {
457
+ if (route === "/sessions" && method === "GET") {
458
+ const sessions = (await options.sessions.listByUserId(user.id)).filter((session) => session.expiresAtMs > Date.now());
459
+ return response({ sessions: sessions.map(({ id, createdAtMs, expiresAtMs }) => ({ id, createdAtMs, expiresAtMs })) }, 200);
460
+ }
461
+ if (route === "/sessions") {
462
+ await options.sessions.deleteByUserId(user.id);
463
+ return new Response(null, {
464
+ status: 204,
465
+ headers: { "Set-Cookie": clearedSessionCookie(cookieName) },
466
+ });
467
+ }
468
+ await options.sessions.deleteByIdForUser(route.slice("/sessions/".length), user.id);
469
+ return new Response(null, { status: 204 });
470
+ }
471
+ catch (error) {
472
+ if (!(error instanceof RequestError))
473
+ await report(`sessions.${method.toLowerCase()}`, error);
474
+ return errorResponse(error);
475
+ }
476
+ }
477
+ const emailVerificationRoute = route === "/email-verification/request" || route === "/email-verification/confirm";
478
+ const passwordResetRoute = route === "/password-reset/request" || route === "/password-reset/confirm";
479
+ const enabled = ["/register", "/login", "/logout"].includes(route) ||
480
+ (emailVerificationRoute && !!options.emailVerification) ||
481
+ (passwordResetRoute && !!options.passwordReset);
482
+ if (!enabled)
483
+ return response({ error: "not_found" }, 404);
484
+ if (method !== "POST")
485
+ return response({ error: "method_not_allowed" }, 405, { Allow: "POST" });
486
+ if (!originIsAllowed(request))
487
+ return response({ error: "invalid_origin" }, 403);
488
+ try {
489
+ if (route === "/register")
490
+ return await register(request);
491
+ if (route === "/login")
492
+ return await login(request);
493
+ if (route === "/logout")
494
+ return await logout(request);
495
+ if (route === "/email-verification/request")
496
+ return await requestEmailVerification(request);
497
+ if (route === "/email-verification/confirm")
498
+ return await confirmEmailVerification(request);
499
+ if (route === "/password-reset/request")
500
+ return await requestPasswordReset(request);
501
+ return await confirmPasswordReset(request);
502
+ }
503
+ catch (error) {
504
+ if (!(error instanceof RequestError))
505
+ await report(route.slice(1), error);
506
+ return errorResponse(error);
507
+ }
508
+ }
509
+ return { handle, cleanupExpired, getSession, createSessionResponse };
510
+ }
@@ -0,0 +1,51 @@
1
+ import type { AuthActionTokenStore, AuthRateLimiter, AuthRequestRateLimiter, SessionStore } from "./types.js";
2
+ interface D1Result<Row> {
3
+ results?: Row[];
4
+ success?: boolean;
5
+ meta?: {
6
+ changes?: number;
7
+ };
8
+ }
9
+ type D1Value = string | number | boolean | null | ArrayBuffer | ArrayBufferView;
10
+ interface D1PreparedStatement {
11
+ bind(...values: D1Value[]): D1PreparedStatement;
12
+ first<Row = Record<string, unknown>>(): Promise<Row | null>;
13
+ all<Row = Record<string, unknown>>(): Promise<D1Result<Row>>;
14
+ run(): Promise<D1Result<never>>;
15
+ }
16
+ /** The subset of Cloudflare's D1 binding used by these adapters. */
17
+ export interface D1Database {
18
+ prepare(query: string): D1PreparedStatement;
19
+ batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>;
20
+ }
21
+ export type D1ActionTokenStore = AuthActionTokenStore;
22
+ export declare function createD1ActionTokenStore(db: D1Database): D1ActionTokenStore;
23
+ export declare function createD1SessionStore(db: D1Database): SessionStore;
24
+ export interface D1RateLimitWindow {
25
+ limit: number;
26
+ windowSeconds: number;
27
+ }
28
+ export interface D1RateLimiterOptions {
29
+ identifier: D1RateLimitWindow;
30
+ ip: D1RateLimitWindow;
31
+ /** Epoch milliseconds; injectable to make window boundaries deterministic in tests. */
32
+ now?: () => number;
33
+ }
34
+ export declare function createD1RateLimiter(db: D1Database, options: D1RateLimiterOptions): AuthRateLimiter;
35
+ export interface D1RequestRateLimiterOptions {
36
+ limit: number;
37
+ windowSeconds: number;
38
+ /** Epoch milliseconds; injectable to make window boundaries deterministic in tests. */
39
+ now?: () => number;
40
+ }
41
+ export declare function createD1RequestRateLimiter(db: D1Database, options: D1RequestRateLimiterOptions): AuthRequestRateLimiter;
42
+ export interface D1AuthCleanupOptions {
43
+ /** Epoch milliseconds; injectable to make cleanup deterministic in tests. */
44
+ nowMs?: number;
45
+ /** Longest configured rate-limit window, used to retain active counters. */
46
+ rateLimitMaxWindowSeconds: number;
47
+ /** Rows removed per table per call. Defaults to 500; capped at 1000. */
48
+ batchSize?: number;
49
+ }
50
+ export declare function cleanupD1AuthState(db: D1Database, options: D1AuthCleanupOptions): Promise<void>;
51
+ export {};