@sleepy-hollow/framework 0.3.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 (52) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +373 -0
  3. package/README.md +95 -0
  4. package/dist/chunk-53TZY5YP.js +470 -0
  5. package/dist/chunk-53TZY5YP.js.map +1 -0
  6. package/dist/chunk-5WRI5ZAA.js +31 -0
  7. package/dist/chunk-5WRI5ZAA.js.map +1 -0
  8. package/dist/chunk-BAKXP7IR.js +85 -0
  9. package/dist/chunk-BAKXP7IR.js.map +1 -0
  10. package/dist/chunk-BJONRVDG.js +429 -0
  11. package/dist/chunk-BJONRVDG.js.map +1 -0
  12. package/dist/chunk-CAPFDC25.js +598 -0
  13. package/dist/chunk-CAPFDC25.js.map +1 -0
  14. package/dist/chunk-D4U3ZY4O.js +4585 -0
  15. package/dist/chunk-D4U3ZY4O.js.map +1 -0
  16. package/dist/chunk-DGTHFZPZ.js +830 -0
  17. package/dist/chunk-DGTHFZPZ.js.map +1 -0
  18. package/dist/chunk-LNJDFJGT.js +47 -0
  19. package/dist/chunk-LNJDFJGT.js.map +1 -0
  20. package/dist/cli.d.ts +427 -0
  21. package/dist/cli.js +5910 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/database.d.ts +25 -0
  24. package/dist/database.js +16 -0
  25. package/dist/database.js.map +1 -0
  26. package/dist/dist-DUSC2237.js +546 -0
  27. package/dist/dist-DUSC2237.js.map +1 -0
  28. package/dist/index.d.ts +241 -0
  29. package/dist/index.js +71 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/magic-string.es-GTFBNHZR.js +1309 -0
  32. package/dist/magic-string.es-GTFBNHZR.js.map +1 -0
  33. package/dist/routing.d.ts +89 -0
  34. package/dist/routing.js +17 -0
  35. package/dist/routing.js.map +1 -0
  36. package/dist/security.d.ts +319 -0
  37. package/dist/security.js +21 -0
  38. package/dist/security.js.map +1 -0
  39. package/dist/server.d.ts +10 -0
  40. package/dist/server.js +8 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/testing.d.ts +157 -0
  43. package/dist/testing.js +29 -0
  44. package/dist/testing.js.map +1 -0
  45. package/dist/types-BC7LJJ6G.d.ts +131 -0
  46. package/dist/types-BUXw3UwN.d.ts +54 -0
  47. package/dist/types-Bet36nZS.d.ts +390 -0
  48. package/dist/types-DmzdxsaA.d.ts +113 -0
  49. package/dist/validation.d.ts +57 -0
  50. package/dist/validation.js +20 -0
  51. package/dist/validation.js.map +1 -0
  52. package/package.json +84 -0
@@ -0,0 +1,830 @@
1
+ import {
2
+ createValidatedRouter
3
+ } from "./chunk-CAPFDC25.js";
4
+ import {
5
+ platform
6
+ } from "./chunk-53TZY5YP.js";
7
+
8
+ // core/security/declaration.ts
9
+ import { isAbsolute, resolve, sep } from "path";
10
+ import { pathToFileURL } from "url";
11
+
12
+ // core/security/redact.ts
13
+ var SECRET_FIELD = /^(authorization|proxyauthorization|cookie|setcookie|.*token.*|.*secret.*|.*password.*|.*session.*|.*apikey.*|.*credential.*)$/i;
14
+ function normalizedField(value) {
15
+ return String(value).replace(/[^a-z0-9]/gi, "");
16
+ }
17
+ function redactSecurityData(value) {
18
+ const active = /* @__PURE__ */ new WeakSet();
19
+ function visit(current, depth) {
20
+ if (current === null || typeof current !== "object") return current;
21
+ if (depth > 12) return "[Truncated]";
22
+ if (current instanceof Request) {
23
+ const url = new URL(current.url);
24
+ return { kind: "Request", method: current.method, path: url.pathname };
25
+ }
26
+ if (current instanceof Headers) return "[REDACTED_HEADERS]";
27
+ if (current instanceof Response) {
28
+ return { kind: "Response", status: current.status };
29
+ }
30
+ if (current instanceof Error) return { name: current.name };
31
+ if (current instanceof Date) return current.toISOString();
32
+ if (active.has(current)) return "[Circular]";
33
+ active.add(current);
34
+ let result;
35
+ if (Array.isArray(current)) {
36
+ result = current.map((item) => visit(item, depth + 1));
37
+ } else {
38
+ const object = {};
39
+ for (const [key, item] of Object.entries(current)) {
40
+ object[key] = SECRET_FIELD.test(normalizedField(key)) ? "[REDACTED]" : visit(item, depth + 1);
41
+ }
42
+ result = object;
43
+ }
44
+ active.delete(current);
45
+ return result;
46
+ }
47
+ return visit(value, 0);
48
+ }
49
+
50
+ // core/security/types.ts
51
+ var SecurityConfigurationError = class extends Error {
52
+ /**
53
+ * Builds an error whose message lists every diagnostic, one per line.
54
+ *
55
+ * @param diagnostics Every fault found, in the order detected.
56
+ */
57
+ constructor(diagnostics) {
58
+ super(
59
+ diagnostics.map(
60
+ (diagnostic2) => `${diagnostic2.code}: ${diagnostic2.summary}`
61
+ ).join("\n")
62
+ );
63
+ this.diagnostics = diagnostics;
64
+ this.name = "SecurityConfigurationError";
65
+ }
66
+ diagnostics;
67
+ };
68
+
69
+ // core/security/normalize.ts
70
+ function routeName(route) {
71
+ return `${route.method} ${route.path}`;
72
+ }
73
+ function diagnostic(code, summary, correction, route, policy) {
74
+ return {
75
+ code,
76
+ severity: "error",
77
+ summary,
78
+ ...route ? { route: routeName(route), source: route.source } : {},
79
+ ...policy ? { policy } : {},
80
+ correction
81
+ };
82
+ }
83
+ function nonempty(value) {
84
+ return typeof value === "string" && value.trim().length > 0;
85
+ }
86
+ function hasResponse(route, status) {
87
+ const schemas = route.operation.schemas;
88
+ return Boolean(
89
+ schemas?.responses && Object.hasOwn(schemas.responses, status)
90
+ );
91
+ }
92
+ function validOrigin(origin) {
93
+ try {
94
+ const url = new URL(origin);
95
+ return (url.protocol === "https:" || url.protocol === "http:") && url.origin === origin && url.pathname === "/" && !url.search && !url.hash;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+ function validHeaderValue(value) {
101
+ try {
102
+ new Headers({ "www-authenticate": value });
103
+ return true;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+ function normalizeCors(options, diagnostics) {
109
+ const raw = options.cors;
110
+ if (!options.cors) {
111
+ if (options.mode === "production") {
112
+ diagnostics.push(diagnostic(
113
+ "SH_CORS_REQUIRED",
114
+ "Production requires an explicit CORS decision",
115
+ "Set cors to { mode: 'deny' } or an explicit allow configuration."
116
+ ));
117
+ }
118
+ return { mode: "deny" };
119
+ }
120
+ if (!raw || typeof raw !== "object") {
121
+ diagnostics.push(diagnostic(
122
+ "SH_CORS_CONFIGURATION_INVALID",
123
+ "CORS configuration is malformed",
124
+ "Declare mode 'deny' or a complete allow configuration."
125
+ ));
126
+ return { mode: "deny" };
127
+ }
128
+ const rawCors = raw;
129
+ if (rawCors.mode === "deny") return { mode: "deny" };
130
+ if (rawCors.mode !== "allow" || !(rawCors.origins === "*" || Array.isArray(rawCors.origins)) || !Array.isArray(rawCors.methods) || !Array.isArray(rawCors.headers) || typeof rawCors.credentials !== "boolean") {
131
+ diagnostics.push(diagnostic(
132
+ "SH_CORS_CONFIGURATION_INVALID",
133
+ "CORS allow configuration is incomplete or malformed",
134
+ "Declare origins, methods, headers, and a credentials decision."
135
+ ));
136
+ return { mode: "deny" };
137
+ }
138
+ const cors = options.cors;
139
+ if (cors.origins === "*" && cors.credentials) {
140
+ diagnostics.push(diagnostic(
141
+ "SH_CORS_WILDCARD_CREDENTIALS",
142
+ "Credentialed CORS cannot use the wildcard origin",
143
+ "List exact origins or disable credentials."
144
+ ));
145
+ }
146
+ if (Array.isArray(cors.origins) && (cors.origins.length === 0 || cors.origins.some((origin) => !validOrigin(origin)))) {
147
+ diagnostics.push(diagnostic(
148
+ "SH_CORS_ORIGIN_INVALID",
149
+ "CORS origins must be non-empty exact HTTP origins",
150
+ "Use origins such as https://app.example without paths, queries, or fragments."
151
+ ));
152
+ }
153
+ if (cors.methods.length === 0 || cors.methods.some(
154
+ (method) => !/^(DELETE|GET|HEAD|OPTIONS|PATCH|POST|PUT)$/.test(method)
155
+ )) {
156
+ diagnostics.push(diagnostic(
157
+ "SH_CORS_METHOD_INVALID",
158
+ "CORS methods must be explicit supported uppercase HTTP methods",
159
+ "Declare at least one supported uppercase method."
160
+ ));
161
+ }
162
+ if (cors.headers.some((header) => !/^[!#$%&'*+.^_`|~0-9a-z-]+$/i.test(header))) {
163
+ diagnostics.push(diagnostic(
164
+ "SH_CORS_HEADER_INVALID",
165
+ "CORS headers must contain valid HTTP field names",
166
+ "Declare only valid header field names."
167
+ ));
168
+ }
169
+ return cors;
170
+ }
171
+ function validatePolicy(route, name, policy, options, diagnostics) {
172
+ if (!policy) {
173
+ diagnostics.push(diagnostic(
174
+ "SH_SECURITY_RATE_LIMIT_REQUIRED",
175
+ `Route references missing rate-limit policy '${name}'`,
176
+ "Register the named policy before starting the router.",
177
+ route,
178
+ name
179
+ ));
180
+ return;
181
+ }
182
+ if (!Number.isSafeInteger(policy.limit) || policy.limit <= 0 || !Number.isSafeInteger(policy.windowMs) || policy.windowMs <= 0 || typeof policy.key !== "function" || typeof policy.limiter?.consume !== "function" || !["process", "shared"].includes(policy.limiter?.scope)) {
183
+ diagnostics.push(diagnostic(
184
+ "SH_SECURITY_RATE_LIMIT_INVALID",
185
+ `Rate-limit policy '${name}' is malformed`,
186
+ "Provide positive integer limits, a key function, and a declared limiter scope.",
187
+ route,
188
+ name
189
+ ));
190
+ } else if (options.mode === "production" && policy.limiter.scope === "process") {
191
+ diagnostics.push(diagnostic(
192
+ "SH_SECURITY_RATE_LIMIT_PROCESS_SCOPE",
193
+ `Production route uses process-scoped rate-limit policy '${name}'`,
194
+ "Inject a shared-scope production limiter.",
195
+ route,
196
+ name
197
+ ));
198
+ }
199
+ }
200
+ function prepareSecurity(routes, options) {
201
+ const diagnostics = [];
202
+ if (!["development", "production", "test"].includes(options.mode)) {
203
+ diagnostics.push(diagnostic(
204
+ "SH_SECURITY_MODE_INVALID",
205
+ "Security mode must be explicit",
206
+ "Set mode to development, test, or production."
207
+ ));
208
+ }
209
+ const cors = normalizeCors(options, diagnostics);
210
+ const prepared = [];
211
+ for (const route of routes) {
212
+ const security = route.operation.security;
213
+ const authentication = security?.authentication;
214
+ if (!authentication || !["none", "required"].includes(authentication.mode)) {
215
+ diagnostics.push(diagnostic(
216
+ "SH_SECURITY_AUTHENTICATION_REQUIRED",
217
+ "Route must declare exactly one authentication mode",
218
+ "Declare authentication mode 'none' or 'required'.",
219
+ route
220
+ ));
221
+ continue;
222
+ }
223
+ if (authentication.mode === "required") {
224
+ const provider = options.providers?.[authentication.provider];
225
+ if (!nonempty(authentication.provider) || !nonempty(authentication.requirementId) || !provider || !nonempty(provider.challenge) || !validHeaderValue(provider.challenge) || typeof provider.authenticate !== "function") {
226
+ diagnostics.push(diagnostic(
227
+ "SH_SECURITY_PROVIDER_REQUIRED",
228
+ `Route requires unresolved provider '${authentication.provider}'`,
229
+ "Register a well-formed named provider and requirement ID.",
230
+ route
231
+ ));
232
+ }
233
+ if (!hasResponse(route, 401)) {
234
+ diagnostics.push(diagnostic(
235
+ "SH_SECURITY_RESPONSE_REQUIRED",
236
+ "Required-authentication route lacks a 401 response schema",
237
+ "Declare schemas.responses[401].",
238
+ route
239
+ ));
240
+ }
241
+ }
242
+ if (security.authorization) {
243
+ if (authentication.mode !== "required" || !nonempty(security.authorization.name) || !nonempty(security.authorization.requirementId) || typeof security.authorization.guard !== "function") {
244
+ diagnostics.push(diagnostic(
245
+ "SH_SECURITY_GUARD_INVALID",
246
+ "Authorization requires a named guard, requirement ID, and required authentication",
247
+ "Attach a well-formed guard only to a required-authentication route.",
248
+ route
249
+ ));
250
+ }
251
+ if (!hasResponse(route, 403)) {
252
+ diagnostics.push(diagnostic(
253
+ "SH_SECURITY_RESPONSE_REQUIRED",
254
+ "Authorization route lacks a 403 response schema",
255
+ "Declare schemas.responses[403].",
256
+ route
257
+ ));
258
+ }
259
+ }
260
+ if (security.rateLimit) {
261
+ if (!hasResponse(route, 429) || !hasResponse(route, 503)) {
262
+ diagnostics.push(diagnostic(
263
+ "SH_SECURITY_RESPONSE_REQUIRED",
264
+ "Rate-limited route lacks 429 or 503 response schemas",
265
+ "Declare schemas.responses[429] and schemas.responses[503].",
266
+ route,
267
+ security.rateLimit
268
+ ));
269
+ }
270
+ validatePolicy(
271
+ route,
272
+ security.rateLimit,
273
+ options.rateLimits?.[security.rateLimit],
274
+ options,
275
+ diagnostics
276
+ );
277
+ }
278
+ prepared.push({
279
+ source: route,
280
+ security,
281
+ metadata: {
282
+ method: route.method,
283
+ path: route.path,
284
+ source: route.source,
285
+ authentication: authentication.mode,
286
+ ...authentication.mode === "required" ? {
287
+ provider: authentication.provider,
288
+ authenticationRequirementId: authentication.requirementId
289
+ } : {},
290
+ ...security.authorization ? {
291
+ authorizationGuard: security.authorization.name,
292
+ authorizationRequirementId: security.authorization.requirementId
293
+ } : {},
294
+ ...security.rateLimit ? { rateLimitPolicy: security.rateLimit } : {},
295
+ corsMode: cors.mode,
296
+ secureHeaders: true,
297
+ requestId: true,
298
+ bodyLimits: "SH-F003",
299
+ boundedData: "SH-F004"
300
+ }
301
+ });
302
+ }
303
+ if (diagnostics.length > 0) {
304
+ for (const item of diagnostics) {
305
+ options.onDiagnostic?.({
306
+ ...item,
307
+ context: redactSecurityData(item.context)
308
+ });
309
+ }
310
+ throw new SecurityConfigurationError(diagnostics);
311
+ }
312
+ return { routes: prepared, cors };
313
+ }
314
+
315
+ // core/security/security_router.ts
316
+ var REQUEST_ID = /^[A-Za-z0-9._:-]{1,128}$/;
317
+ var RATE_KEY = /^[A-Za-z0-9._:-]{1,256}$/;
318
+ function problem(request, status, title, slug, headers = {}) {
319
+ return Response.json({
320
+ type: `https://sleepyhollow.dev/problems/${slug}`,
321
+ title,
322
+ status,
323
+ instance: new URL(request.url).pathname
324
+ }, {
325
+ status,
326
+ headers: {
327
+ "content-type": "application/problem+json",
328
+ ...headers
329
+ }
330
+ });
331
+ }
332
+ function emit(options, route, code, summary, correction, context, policy) {
333
+ const diagnostic2 = {
334
+ code,
335
+ severity: "error",
336
+ summary,
337
+ route: `${route.method} ${route.path}`,
338
+ source: route.source,
339
+ ...policy ? { policy } : {},
340
+ correction,
341
+ ...context === void 0 ? {} : { context: redactSecurityData(context) }
342
+ };
343
+ options.onDiagnostic?.(diagnostic2);
344
+ }
345
+ function validPrincipal(value) {
346
+ if (!value || typeof value !== "object") return false;
347
+ const principal = value;
348
+ if (typeof principal.id !== "string" || !principal.id.trim() || typeof principal.type !== "string" || !principal.type.trim()) return false;
349
+ if (principal.claims === void 0) return true;
350
+ if (principal.claims === null || typeof principal.claims !== "object" || Array.isArray(principal.claims)) return false;
351
+ const prototype = Object.getPrototypeOf(principal.claims);
352
+ return prototype === Object.prototype || prototype === null;
353
+ }
354
+ function validDecision(value) {
355
+ if (!value || typeof value !== "object") return false;
356
+ const decision = value;
357
+ return typeof decision.allowed === "boolean" && Number.isSafeInteger(decision.remaining) && Number(decision.remaining) >= 0 && typeof decision.resetAt === "number" && Number.isFinite(decision.resetAt);
358
+ }
359
+ function rawCredential(request, key) {
360
+ for (const [name, value] of request.headers) {
361
+ const normalized = name.replace(/[^a-z0-9]/gi, "");
362
+ if (/(authorization|cookie|token|secret|password|session|apikey|credential)/i.test(normalized) && value === key) return true;
363
+ }
364
+ return false;
365
+ }
366
+ async function enforceRateLimit(request, route, name, policy, options) {
367
+ try {
368
+ const key = await policy.key(request);
369
+ if (!RATE_KEY.test(key) || rawCredential(request, key)) {
370
+ throw new Error("invalid rate-limit key");
371
+ }
372
+ const decision = await policy.limiter.consume({
373
+ policy: name,
374
+ key,
375
+ limit: policy.limit,
376
+ windowMs: policy.windowMs
377
+ });
378
+ if (!validDecision(decision)) throw new Error("invalid limiter decision");
379
+ if (decision.allowed) return void 0;
380
+ const retryAfter = Math.max(
381
+ 1,
382
+ Math.ceil((decision.resetAt - Date.now()) / 1e3)
383
+ );
384
+ return problem(request, 429, "Too Many Requests", "rate-limit", {
385
+ "cache-control": "no-store",
386
+ "retry-after": String(retryAfter)
387
+ });
388
+ } catch (error) {
389
+ emit(
390
+ options,
391
+ route,
392
+ "SH_RATE_LIMIT_FAILED",
393
+ "Rate-limit enforcement failed closed",
394
+ "Inspect the protected limiter diagnostic and restore the policy adapter.",
395
+ { error, request },
396
+ name
397
+ );
398
+ return problem(
399
+ request,
400
+ 503,
401
+ "Service Unavailable",
402
+ "rate-limit-unavailable",
403
+ { "cache-control": "no-store" }
404
+ );
405
+ }
406
+ }
407
+ async function securedHandler(context, route, security, options) {
408
+ if (security.rateLimit) {
409
+ const limited = await enforceRateLimit(
410
+ context.request,
411
+ route,
412
+ security.rateLimit,
413
+ options.rateLimits[security.rateLimit],
414
+ options
415
+ );
416
+ if (limited) return limited;
417
+ }
418
+ let principal = null;
419
+ if (security.authentication.mode === "required") {
420
+ const provider = options.providers[security.authentication.provider];
421
+ try {
422
+ principal = await provider.authenticate(context.request);
423
+ } catch (error) {
424
+ emit(
425
+ options,
426
+ route,
427
+ "SH_AUTH_PROVIDER_FAILED",
428
+ "Authentication provider execution failed",
429
+ "Inspect the protected provider diagnostic and repair the adapter.",
430
+ { error, request: context.request }
431
+ );
432
+ throw error;
433
+ }
434
+ if (principal === null) {
435
+ return problem(context.request, 401, "Unauthorized", "unauthorized", {
436
+ "cache-control": "no-store",
437
+ "www-authenticate": provider.challenge
438
+ });
439
+ }
440
+ if (!validPrincipal(principal)) {
441
+ emit(
442
+ options,
443
+ route,
444
+ "SH_AUTH_PROVIDER_INVALID",
445
+ "Authentication provider returned a malformed principal",
446
+ "Return a principal with non-empty id and type fields.",
447
+ { principal }
448
+ );
449
+ throw new Error("SH_AUTH_PROVIDER_INVALID");
450
+ }
451
+ }
452
+ if (security.authorization) {
453
+ try {
454
+ const allowed = await security.authorization.guard({
455
+ principal,
456
+ request: context.request,
457
+ params: context.params
458
+ });
459
+ if (!allowed) {
460
+ return problem(context.request, 403, "Forbidden", "forbidden", {
461
+ "cache-control": "no-store"
462
+ });
463
+ }
464
+ } catch (error) {
465
+ emit(
466
+ options,
467
+ route,
468
+ "SH_AUTHORIZATION_FAILED",
469
+ "Authorization guard execution failed",
470
+ "Inspect the protected guard diagnostic and repair the guard.",
471
+ { error, request: context.request }
472
+ );
473
+ throw error;
474
+ }
475
+ }
476
+ const handler = route.operation.handler;
477
+ return await handler({
478
+ ...context,
479
+ principal,
480
+ requestId: context.request.headers.get("x-request-id")
481
+ });
482
+ }
483
+ function pathMatches(routePath, requestPath) {
484
+ let requestSegments;
485
+ try {
486
+ requestSegments = requestPath.split("/").filter(Boolean).map(
487
+ decodeURIComponent
488
+ );
489
+ } catch {
490
+ return false;
491
+ }
492
+ const routeSegments = routePath.split("/").filter(Boolean);
493
+ return routeSegments.length === requestSegments.length && routeSegments.every(
494
+ (segment, index) => segment.startsWith(":") || segment === requestSegments[index]
495
+ );
496
+ }
497
+ function allowedOrigin(cors, origin) {
498
+ return cors.mode === "allow" && (cors.origins === "*" || cors.origins.includes(origin));
499
+ }
500
+ function appendVary(headers, value) {
501
+ const current = headers.get("vary");
502
+ const values = current?.split(",").map((item) => item.trim()) ?? [];
503
+ if (!values.some((item) => item.toLowerCase() === value.toLowerCase())) {
504
+ headers.set("vary", [...values, value].filter(Boolean).join(", "));
505
+ }
506
+ }
507
+ function corsHeaders(headers, request, cors, preflight) {
508
+ if (cors.mode !== "allow") return;
509
+ const origin = request.headers.get("origin");
510
+ if (!origin || !allowedOrigin(cors, origin)) return;
511
+ headers.set(
512
+ "access-control-allow-origin",
513
+ cors.origins === "*" ? "*" : origin
514
+ );
515
+ if (cors.origins !== "*") appendVary(headers, "Origin");
516
+ if (cors.credentials) headers.set("access-control-allow-credentials", "true");
517
+ if (preflight) {
518
+ headers.set("access-control-allow-methods", cors.methods.join(", "));
519
+ if (cors.headers.length > 0) {
520
+ headers.set("access-control-allow-headers", cors.headers.join(", "));
521
+ }
522
+ }
523
+ }
524
+ function knownPreflight(request, routes, cors) {
525
+ if (cors.mode !== "allow" || request.method !== "OPTIONS") return false;
526
+ const origin = request.headers.get("origin");
527
+ const method = request.headers.get("access-control-request-method")?.toUpperCase();
528
+ if (!origin || !method || !allowedOrigin(cors, origin) || !cors.methods.includes(method)) return false;
529
+ const requestedHeaders = request.headers.get("access-control-request-headers")?.split(",").map((header) => header.trim().toLowerCase()).filter(Boolean) ?? [];
530
+ const allowedHeaders = new Set(
531
+ cors.headers.map((header) => header.toLowerCase())
532
+ );
533
+ if (requestedHeaders.some((header) => !allowedHeaders.has(header))) {
534
+ return false;
535
+ }
536
+ const path = new URL(request.url).pathname;
537
+ return routes.some(
538
+ (route) => route.method === method && pathMatches(route.path, path)
539
+ );
540
+ }
541
+ function isPreflight(request) {
542
+ return request.method === "OPTIONS" && request.headers.has("origin") && request.headers.has("access-control-request-method");
543
+ }
544
+ function requestWithId(request, options) {
545
+ const inbound = request.headers.get("x-request-id");
546
+ let requestId = inbound && REQUEST_ID.test(inbound) ? inbound : void 0;
547
+ if (!requestId && options.requestId) {
548
+ try {
549
+ requestId = options.requestId();
550
+ } catch {
551
+ requestId = void 0;
552
+ }
553
+ }
554
+ if (!requestId || !REQUEST_ID.test(requestId)) {
555
+ requestId = crypto.randomUUID();
556
+ }
557
+ const headers = new Headers(request.headers);
558
+ headers.set("x-request-id", requestId);
559
+ return { request: new Request(request, { headers }), requestId };
560
+ }
561
+ function hardenedResponse(response, request, requestId, cors, preflight = false) {
562
+ const headers = new Headers(response.headers);
563
+ headers.set("x-content-type-options", "nosniff");
564
+ headers.set("referrer-policy", "no-referrer");
565
+ headers.set(
566
+ "content-security-policy",
567
+ "default-src 'none'; frame-ancestors 'none'"
568
+ );
569
+ headers.set("x-request-id", requestId);
570
+ corsHeaders(headers, request, cors, preflight);
571
+ return new Response(response.body, {
572
+ status: response.status,
573
+ statusText: response.statusText,
574
+ headers
575
+ });
576
+ }
577
+ function createSecurityRouter(routes, options) {
578
+ const prepared = prepareSecurity(routes, options);
579
+ const wrapped = prepared.routes.map((item) => ({
580
+ ...item.source,
581
+ operation: {
582
+ ...item.source.operation,
583
+ handler: (context) => securedHandler(context, item.source, item.security, options)
584
+ }
585
+ }));
586
+ const validated = createValidatedRouter(wrapped, {
587
+ mode: options.mode,
588
+ onDiagnostic: options.onDiagnostic
589
+ });
590
+ return {
591
+ routes: prepared.routes.map((route) => route.metadata),
592
+ async fetch(originalRequest) {
593
+ const selected = requestWithId(originalRequest, options);
594
+ const preflight = isPreflight(selected.request);
595
+ if (preflight && knownPreflight(selected.request, routes, prepared.cors)) {
596
+ return hardenedResponse(
597
+ new Response(null, { status: 204 }),
598
+ selected.request,
599
+ selected.requestId,
600
+ prepared.cors,
601
+ true
602
+ );
603
+ }
604
+ const response = await validated.fetch(selected.request);
605
+ return hardenedResponse(
606
+ response,
607
+ selected.request,
608
+ selected.requestId,
609
+ preflight ? { mode: "deny" } : prepared.cors
610
+ );
611
+ }
612
+ };
613
+ }
614
+
615
+ // core/security/declaration.ts
616
+ function failure(code, summary, correction, source) {
617
+ const diagnostic2 = {
618
+ code,
619
+ severity: "error",
620
+ summary,
621
+ ...source ? { source } : {},
622
+ correction
623
+ };
624
+ return new SecurityConfigurationError([diagnostic2]);
625
+ }
626
+ function record(value) {
627
+ return typeof value === "object" && value !== null && !Array.isArray(value);
628
+ }
629
+ function defineSecurity(declaration) {
630
+ if (!record(declaration)) {
631
+ throw failure(
632
+ "SH_SECURITY_DECLARATION_INVALID",
633
+ "A security declaration must be one object",
634
+ "Pass one object of providers, rate limits, and CORS to defineSecurity."
635
+ );
636
+ }
637
+ for (const key of ["providers", "rateLimits"]) {
638
+ const value = declaration[key];
639
+ if (value !== void 0) Object.freeze(value);
640
+ }
641
+ return Object.freeze(declaration);
642
+ }
643
+ function validate(declaration, source) {
644
+ if (!record(declaration)) {
645
+ throw failure(
646
+ "SH_SECURITY_DECLARATION_INVALID",
647
+ `The security module ${source} has no default declaration object`,
648
+ "Default-export the result of defineSecurity.",
649
+ source
650
+ );
651
+ }
652
+ const providers = declaration.providers;
653
+ if (providers !== void 0) {
654
+ if (!record(providers)) {
655
+ throw failure(
656
+ "SH_SECURITY_DECLARATION_INVALID",
657
+ `The security module ${source} declares malformed providers`,
658
+ "Declare providers as a record of named authentication providers.",
659
+ source
660
+ );
661
+ }
662
+ for (const [name, provider] of Object.entries(providers)) {
663
+ if (!record(provider) || typeof provider.challenge !== "string" || typeof provider.authenticate !== "function") {
664
+ throw failure(
665
+ "SH_SECURITY_DECLARATION_INVALID",
666
+ `Provider '${name}' in ${source} is not a well-formed provider`,
667
+ "Declare a string challenge and an authenticate function.",
668
+ source
669
+ );
670
+ }
671
+ }
672
+ }
673
+ const rateLimits = declaration.rateLimits;
674
+ if (rateLimits !== void 0 && !record(rateLimits)) {
675
+ throw failure(
676
+ "SH_SECURITY_DECLARATION_INVALID",
677
+ `The security module ${source} declares malformed rate limits`,
678
+ "Declare rateLimits as a record of named policies.",
679
+ source
680
+ );
681
+ }
682
+ const cors = declaration.cors;
683
+ if (cors !== void 0 && !record(cors)) {
684
+ throw failure(
685
+ "SH_SECURITY_DECLARATION_INVALID",
686
+ `The security module ${source} declares malformed CORS`,
687
+ "Declare cors as one explicit deny or allow decision.",
688
+ source
689
+ );
690
+ }
691
+ return declaration;
692
+ }
693
+ async function declared(options) {
694
+ const named = options.securityModule;
695
+ if (named === void 0) return {};
696
+ if (typeof named !== "string" || named.trim() === "" || isAbsolute(named)) {
697
+ throw failure(
698
+ "SH_SECURITY_MODULE_INVALID",
699
+ "The declared security module is not a safe project-relative path",
700
+ "Name one project-contained module in securityModule.",
701
+ typeof named === "string" ? named : void 0
702
+ );
703
+ }
704
+ const root = resolve(options.root);
705
+ const target = resolve(root, named);
706
+ if (target !== root && !target.startsWith(root + sep)) {
707
+ throw failure(
708
+ "SH_SECURITY_MODULE_ESCAPE",
709
+ `The declared security module ${named} resolves outside the project`,
710
+ "Keep the security module inside the project.",
711
+ named
712
+ );
713
+ }
714
+ if (options.load === void 0) {
715
+ let real;
716
+ try {
717
+ real = await platform.realPath(target);
718
+ } catch {
719
+ throw failure(
720
+ "SH_SECURITY_MODULE_UNRESOLVED",
721
+ `The declared security module ${named} could not be resolved`,
722
+ "Create the named module or correct securityModule.",
723
+ named
724
+ );
725
+ }
726
+ const realRoot = await platform.realPath(root).catch(() => root);
727
+ if (real !== realRoot && !real.startsWith(realRoot + sep)) {
728
+ throw failure(
729
+ "SH_SECURITY_MODULE_ESCAPE",
730
+ `The declared security module ${named} resolves outside the project through a symlink`,
731
+ "Keep the security module and anything it links to inside the project.",
732
+ named
733
+ );
734
+ }
735
+ }
736
+ const specifier = pathToFileURL(target).href;
737
+ let loaded;
738
+ try {
739
+ loaded = await (options.load ? options.load(specifier) : import(specifier));
740
+ } catch (error) {
741
+ const failedToLoad = options.load === void 0;
742
+ throw failure(
743
+ failedToLoad ? "SH_SECURITY_MODULE_FAILED" : "SH_SECURITY_MODULE_UNRESOLVED",
744
+ failedToLoad ? `The declared security module ${named} threw ${error instanceof Error ? error.name : "an error"} while loading` : `The declared security module ${named} could not be resolved`,
745
+ failedToLoad ? "Repair the security module so it loads without throwing." : "Create the named module or correct securityModule.",
746
+ named
747
+ );
748
+ }
749
+ const value = record(loaded) && "default" in loaded ? loaded.default : void 0;
750
+ if (value === void 0) {
751
+ throw failure(
752
+ "SH_SECURITY_DECLARATION_INVALID",
753
+ `The security module ${named} has no default export`,
754
+ "Default-export the result of defineSecurity.",
755
+ named
756
+ );
757
+ }
758
+ return validate(value, named);
759
+ }
760
+ async function composeProjectSecurity(routes, options) {
761
+ const declaration = await declared(options);
762
+ return createSecurityRouter(routes, {
763
+ mode: options.mode,
764
+ ...declaration.providers ? { providers: declaration.providers } : {},
765
+ ...declaration.rateLimits ? { rateLimits: declaration.rateLimits } : {},
766
+ ...declaration.cors ? { cors: declaration.cors } : {},
767
+ ...options.onDiagnostic ? { onDiagnostic: options.onDiagnostic } : {},
768
+ ...options.requestId ? { requestId: options.requestId } : {}
769
+ });
770
+ }
771
+
772
+ // core/security/rate_limit.ts
773
+ var RATE_KEY2 = /^[A-Za-z0-9._:-]{1,256}$/;
774
+ function createMemoryRateLimiter(options) {
775
+ if (!Number.isSafeInteger(options.maxKeys) || options.maxKeys <= 0) {
776
+ throw new TypeError("maxKeys must be a positive integer");
777
+ }
778
+ const clock = options.clock ?? Date.now;
779
+ const windows = /* @__PURE__ */ new Map();
780
+ function validate2(input) {
781
+ if (!input.policy || !RATE_KEY2.test(input.key)) {
782
+ throw new TypeError("policy and a valid bounded key are required");
783
+ }
784
+ if (!Number.isSafeInteger(input.limit) || input.limit <= 0) {
785
+ throw new TypeError("limit must be a positive integer");
786
+ }
787
+ if (!Number.isSafeInteger(input.windowMs) || input.windowMs <= 0) {
788
+ throw new TypeError("windowMs must be a positive integer");
789
+ }
790
+ }
791
+ return {
792
+ scope: "process",
793
+ async consume(input) {
794
+ await Promise.resolve();
795
+ validate2(input);
796
+ const now = clock();
797
+ if (!Number.isFinite(now)) {
798
+ throw new TypeError("clock must return a finite value");
799
+ }
800
+ for (const [key, window2] of windows) {
801
+ if (window2.resetAt <= now) windows.delete(key);
802
+ }
803
+ const storageKey = `${input.policy}\0${input.key}`;
804
+ let window = windows.get(storageKey);
805
+ if (!window) {
806
+ if (windows.size >= options.maxKeys) {
807
+ throw new Error("SH_RATE_LIMIT_CAPACITY_EXHAUSTED");
808
+ }
809
+ window = { count: 0, resetAt: now + input.windowMs };
810
+ windows.set(storageKey, window);
811
+ }
812
+ window.count += 1;
813
+ return {
814
+ allowed: window.count <= input.limit,
815
+ remaining: Math.max(0, input.limit - window.count),
816
+ resetAt: window.resetAt
817
+ };
818
+ }
819
+ };
820
+ }
821
+
822
+ export {
823
+ redactSecurityData,
824
+ SecurityConfigurationError,
825
+ createSecurityRouter,
826
+ defineSecurity,
827
+ composeProjectSecurity,
828
+ createMemoryRateLimiter
829
+ };
830
+ //# sourceMappingURL=chunk-DGTHFZPZ.js.map