theokit 0.6.0 → 0.6.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 (89) hide show
  1. package/dist/{actions-virtual-module-DL74V6SR.js → actions-virtual-module-4WVKHQ2X.js} +5 -2
  2. package/dist/actions-virtual-module-4WVKHQ2X.js.map +1 -0
  3. package/dist/{actions-virtual-module-ZDD54ZZE.js → actions-virtual-module-LTOXBSCF.js} +16 -2
  4. package/dist/actions-virtual-module-LTOXBSCF.js.map +1 -0
  5. package/dist/{app-typed-client-4M4NVSK3.js → app-typed-client-4N5OGNOQ.js} +15 -4
  6. package/dist/app-typed-client-4N5OGNOQ.js.map +1 -0
  7. package/dist/{app-typed-client-4GIKUKRT.js → app-typed-client-SAETWZT5.js} +5 -4
  8. package/dist/app-typed-client-SAETWZT5.js.map +1 -0
  9. package/dist/{build-ZZAQV2ES.js → build-KQD2EDU4.js} +5 -6
  10. package/dist/build-KQD2EDU4.js.map +1 -0
  11. package/dist/chunk-223EFY5X.js +469 -0
  12. package/dist/chunk-223EFY5X.js.map +1 -0
  13. package/dist/{chunk-HDA6M7P4.js → chunk-3LVRAGAZ.js} +3 -5
  14. package/dist/{chunk-4NLIESWK.js → chunk-4I47JW5O.js} +24 -417
  15. package/dist/chunk-4I47JW5O.js.map +1 -0
  16. package/dist/{chunk-P2RS3WWY.js → chunk-6FYD34NX.js} +194 -22
  17. package/dist/chunk-6FYD34NX.js.map +1 -0
  18. package/dist/{chunk-SVSUUK4H.js → chunk-6VSKLYT6.js} +7 -9
  19. package/dist/chunk-6VSKLYT6.js.map +1 -0
  20. package/dist/chunk-CBGRFVF5.js +421 -0
  21. package/dist/chunk-CBGRFVF5.js.map +1 -0
  22. package/dist/{chunk-B3L3TJVF.js → chunk-JBCHWRKF.js} +50 -50
  23. package/dist/chunk-JBCHWRKF.js.map +1 -0
  24. package/dist/{chunk-XMS5VRIK.js → chunk-JQSFBJR5.js} +3 -3
  25. package/dist/chunk-JQSFBJR5.js.map +1 -0
  26. package/dist/{chunk-D4SV7JOG.js → chunk-MV4LKTW6.js} +97 -135
  27. package/dist/chunk-MV4LKTW6.js.map +1 -0
  28. package/dist/chunk-PBEH6NXR.js +44 -0
  29. package/dist/chunk-PBEH6NXR.js.map +1 -0
  30. package/dist/chunk-PPPR5DGR.js +1 -0
  31. package/dist/cli/index.js +5 -5
  32. package/dist/client/index.d.ts +4 -4
  33. package/dist/{dev-MHCQ5RD7.js → dev-NDEZA2MP.js} +8 -6
  34. package/dist/dev-NDEZA2MP.js.map +1 -0
  35. package/dist/{dev-emit-6DJUK6DT.js → dev-emit-O4EGOSNV.js} +2 -4
  36. package/dist/{dev-emit-6DJUK6DT.js.map → dev-emit-O4EGOSNV.js.map} +1 -1
  37. package/dist/{dev-emit-4WG3B5BM.js → dev-emit-OUPI7NAQ.js} +3 -4
  38. package/dist/{dev-emit-4WG3B5BM.js.map → dev-emit-OUPI7NAQ.js.map} +1 -1
  39. package/dist/{dist-KRW6OVD4.js → dist-6GPD6WCU.js} +10231 -8164
  40. package/dist/dist-6GPD6WCU.js.map +1 -0
  41. package/dist/{index-CuHICqb9.d.ts → index-DWZF8uQD.d.ts} +132 -132
  42. package/dist/index.js +6 -6
  43. package/dist/index.js.map +1 -1
  44. package/dist/{lib-NST3PNZX.js → lib-JBI3XXH3.js} +27 -27
  45. package/dist/{lib-NST3PNZX.js.map → lib-JBI3XXH3.js.map} +1 -1
  46. package/dist/{openapi-KSY272CW.js → openapi-NXGRXABL.js} +3 -4
  47. package/dist/{openapi-KSY272CW.js.map → openapi-NXGRXABL.js.map} +1 -1
  48. package/dist/{routes-NWJA5L5I.js → routes-6A5XFGF7.js} +4 -6
  49. package/dist/routes-6A5XFGF7.js.map +1 -0
  50. package/dist/server/auth/index.d.ts +20 -20
  51. package/dist/server/http/index.d.ts +2 -2
  52. package/dist/server/index.d.ts +2 -2
  53. package/dist/server/index.js +9 -13
  54. package/dist/server/index.js.map +1 -1
  55. package/dist/server/scan/index.d.ts +13 -13
  56. package/dist/server/scan/index.js +8 -12
  57. package/dist/server/security/index.d.ts +69 -69
  58. package/dist/server/security/index.js +1 -1
  59. package/dist/{start-OAAAQ7Z4.js → start-SPFWOGHL.js} +14 -14
  60. package/dist/vite-plugin/index.js +4 -4
  61. package/dist/{vite-plugin-IF2O6BJV.js → vite-plugin-43KQU5S7.js} +7 -5
  62. package/dist/vite-plugin-43KQU5S7.js.map +1 -0
  63. package/package.json +19 -19
  64. package/dist/actions-virtual-module-DL74V6SR.js.map +0 -1
  65. package/dist/actions-virtual-module-ZDD54ZZE.js.map +0 -1
  66. package/dist/app-typed-client-4GIKUKRT.js.map +0 -1
  67. package/dist/app-typed-client-4M4NVSK3.js.map +0 -1
  68. package/dist/build-ZZAQV2ES.js.map +0 -1
  69. package/dist/chunk-4NLIESWK.js.map +0 -1
  70. package/dist/chunk-B3L3TJVF.js.map +0 -1
  71. package/dist/chunk-D4SV7JOG.js.map +0 -1
  72. package/dist/chunk-GPF64ENM.js +0 -73
  73. package/dist/chunk-HDA6M7P4.js.map +0 -1
  74. package/dist/chunk-OLPB36O2.js +0 -259
  75. package/dist/chunk-OLPB36O2.js.map +0 -1
  76. package/dist/chunk-P2RS3WWY.js.map +0 -1
  77. package/dist/chunk-P3SGM4NR.js +0 -156
  78. package/dist/chunk-P3SGM4NR.js.map +0 -1
  79. package/dist/chunk-SVSUUK4H.js.map +0 -1
  80. package/dist/chunk-UVHRD33F.js +0 -181
  81. package/dist/chunk-UVHRD33F.js.map +0 -1
  82. package/dist/chunk-XMS5VRIK.js.map +0 -1
  83. package/dist/dev-MHCQ5RD7.js.map +0 -1
  84. package/dist/dist-KRW6OVD4.js.map +0 -1
  85. package/dist/routes-NWJA5L5I.js.map +0 -1
  86. package/dist/{chunk-GPF64ENM.js.map → chunk-3LVRAGAZ.js.map} +0 -0
  87. package/dist/{vite-plugin-IF2O6BJV.js.map → chunk-PPPR5DGR.js.map} +0 -0
  88. package/dist/{module-loader-Cfz7dgCP.d.ts → match-CfbEFRG4.d.ts} +4 -4
  89. /package/dist/{start-OAAAQ7Z4.js.map → start-SPFWOGHL.js.map} +0 -0
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env node
2
+ import "tsx/esm";
3
+ import {
4
+ safeAudit
5
+ } from "./chunk-MV4LKTW6.js";
6
+
7
+ // src/server/http/batch-handler.ts
8
+ import { z } from "zod";
9
+ var STRIPPED_HEADERS = [
10
+ "authorization",
11
+ "cookie",
12
+ "x-forwarded-for",
13
+ "x-forwarded-host",
14
+ "x-forwarded-proto",
15
+ "x-real-ip",
16
+ "host"
17
+ ];
18
+ var BATCH_PATH = "/api/__theo_batch__";
19
+ var DEFAULT_MAX_BATCH = 32;
20
+ var batchRequestSchema = z.object({
21
+ path: z.string().min(1),
22
+ method: z.string().min(1),
23
+ query: z.record(z.string(), z.unknown()).optional(),
24
+ body: z.unknown().optional(),
25
+ headers: z.record(z.string(), z.string()).optional()
26
+ });
27
+ var batchPayloadSchema = z.object({
28
+ requests: z.array(batchRequestSchema).min(1)
29
+ });
30
+ function sanitizeItemHeaders(itemHeaders, outerHeaders) {
31
+ const out = {};
32
+ if (itemHeaders) {
33
+ for (const [k, v] of Object.entries(itemHeaders)) {
34
+ const lower = k.toLowerCase();
35
+ if (!STRIPPED_HEADERS.includes(lower)) {
36
+ out[lower] = v;
37
+ }
38
+ }
39
+ }
40
+ if (outerHeaders) {
41
+ for (const stripped of STRIPPED_HEADERS) {
42
+ if (Object.hasOwn(outerHeaders, stripped)) {
43
+ out[stripped] = outerHeaders[stripped];
44
+ }
45
+ }
46
+ }
47
+ return out;
48
+ }
49
+ async function handleBatchRequest(payload, options) {
50
+ const parsed = batchPayloadSchema.parse(payload);
51
+ const max = options.max ?? DEFAULT_MAX_BATCH;
52
+ if (parsed.requests.length > max) {
53
+ throw new Error(`Batch size ${parsed.requests.length} exceeds max ${max}`);
54
+ }
55
+ const results = [];
56
+ for (const item of parsed.requests) {
57
+ const sanitized = {
58
+ ...item,
59
+ headers: sanitizeItemHeaders(item.headers, options.outerHeaders)
60
+ };
61
+ try {
62
+ const r = await options.execute(sanitized);
63
+ results.push(r);
64
+ } catch (err) {
65
+ const message = err instanceof Error ? err.message : String(err);
66
+ results.push({ error: { message } });
67
+ }
68
+ }
69
+ return { results };
70
+ }
71
+
72
+ // src/server/http/cors.ts
73
+ var DEFAULT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
74
+ var DEFAULT_ALLOWED_HEADERS = ["Content-Type", "X-Theo-Action", "Authorization"];
75
+ var DEFAULT_MAX_AGE = 600;
76
+ function readOrigin(req) {
77
+ const raw = req.headers.origin;
78
+ if (raw === void 0) return void 0;
79
+ if (Array.isArray(raw)) {
80
+ return raw.find((v) => typeof v === "string" && v.length > 0);
81
+ }
82
+ return raw;
83
+ }
84
+ function matchesOrigin(origin, allowed) {
85
+ if (allowed === "*") return true;
86
+ if (typeof allowed === "string") return origin === allowed;
87
+ if (allowed instanceof RegExp) {
88
+ allowed.lastIndex = 0;
89
+ return allowed.test(origin);
90
+ }
91
+ if (Array.isArray(allowed)) {
92
+ for (const entry of allowed) {
93
+ if (typeof entry === "string" && origin === entry) return true;
94
+ if (entry instanceof RegExp) {
95
+ entry.lastIndex = 0;
96
+ if (entry.test(origin)) return true;
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+ if (typeof allowed === "function") {
102
+ try {
103
+ return allowed(origin);
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+ return false;
109
+ }
110
+ function createCorsHandler(config) {
111
+ const methods = (config.methods ?? DEFAULT_METHODS).slice();
112
+ const allowedHeaders = (config.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS).slice();
113
+ const maxAge = String(config.maxAge ?? DEFAULT_MAX_AGE);
114
+ const credentials = config.credentials === true;
115
+ return {
116
+ handlePreflight(req, res) {
117
+ if (req.method !== "OPTIONS") return false;
118
+ const acMethod = req.headers["access-control-request-method"];
119
+ if (!acMethod) return false;
120
+ const origin = readOrigin(req);
121
+ if (!origin) return false;
122
+ if (!matchesOrigin(origin, config.origins)) {
123
+ res.statusCode = 403;
124
+ res.end();
125
+ return true;
126
+ }
127
+ res.setHeader("Access-Control-Allow-Origin", origin);
128
+ res.setHeader("Access-Control-Allow-Methods", methods.join(", "));
129
+ res.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
130
+ res.setHeader("Access-Control-Max-Age", maxAge);
131
+ res.setHeader("Vary", "Origin");
132
+ if (credentials) res.setHeader("Access-Control-Allow-Credentials", "true");
133
+ res.statusCode = 204;
134
+ res.end();
135
+ return true;
136
+ },
137
+ applyHeaders(req, res) {
138
+ const origin = readOrigin(req);
139
+ if (!origin) return;
140
+ if (!matchesOrigin(origin, config.origins)) return;
141
+ res.setHeader("Access-Control-Allow-Origin", origin);
142
+ res.setHeader("Vary", "Origin");
143
+ if (credentials) res.setHeader("Access-Control-Allow-Credentials", "true");
144
+ if (config.exposedHeaders && config.exposedHeaders.length > 0) {
145
+ res.setHeader("Access-Control-Expose-Headers", config.exposedHeaders.join(", "));
146
+ }
147
+ }
148
+ };
149
+ }
150
+
151
+ // src/server/http/trace-context.ts
152
+ var TRACE_HEADER = "x-trace-id";
153
+ var TRACE_PARENT_HEADER = "traceparent";
154
+ var REQUEST_ID_HEADER = "x-request-id";
155
+ var TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;
156
+ function parseTraceparent(value) {
157
+ if (!value) return null;
158
+ const m = TRACEPARENT_RE.exec(value);
159
+ if (!m) return null;
160
+ const traceId = m[1];
161
+ if (/^0+$/.test(traceId)) return null;
162
+ return traceId;
163
+ }
164
+ function pickHeader(value) {
165
+ if (Array.isArray(value)) {
166
+ for (const v of value) {
167
+ if (typeof v === "string" && v.length > 0) return v;
168
+ }
169
+ return null;
170
+ }
171
+ if (typeof value === "string" && value.length > 0) return value;
172
+ return null;
173
+ }
174
+ function resolveTraceIdFromHeaders(traceparent, requestId) {
175
+ if (traceparent !== null) {
176
+ const parsed = parseTraceparent(traceparent);
177
+ if (parsed !== null) return parsed;
178
+ }
179
+ if (requestId !== null) return requestId;
180
+ return globalThis.crypto.randomUUID();
181
+ }
182
+ function extractTraceId(req) {
183
+ return resolveTraceIdFromHeaders(
184
+ pickHeader(req.headers[TRACE_PARENT_HEADER]),
185
+ pickHeader(req.headers[REQUEST_ID_HEADER])
186
+ );
187
+ }
188
+
189
+ // src/server/security/csp-report.ts
190
+ var CSP_REPORT_PATH = "/__theo/csp-report";
191
+ var MAX_BODY = 16 * 1024;
192
+ function pickHeader2(value) {
193
+ if (typeof value === "string") return value;
194
+ if (Array.isArray(value) && value.length > 0) return value[0];
195
+ return "";
196
+ }
197
+ function toStringSafe(value, fallback = "(missing)") {
198
+ if (typeof value === "string") return value;
199
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
200
+ return fallback;
201
+ }
202
+ function normalizeLegacy(raw) {
203
+ return {
204
+ blockedUrl: toStringSafe(raw["blocked-uri"]),
205
+ documentUrl: toStringSafe(raw["document-uri"]),
206
+ violatedDirective: toStringSafe(raw["violated-directive"]),
207
+ effectiveDirective: typeof raw["effective-directive"] === "string" ? raw["effective-directive"] : void 0,
208
+ originalPolicy: typeof raw["original-policy"] === "string" ? raw["original-policy"] : void 0,
209
+ disposition: raw.disposition === "enforce" || raw.disposition === "report" ? raw.disposition : void 0,
210
+ statusCode: typeof raw["status-code"] === "number" ? raw["status-code"] : void 0,
211
+ sourceFile: typeof raw["source-file"] === "string" ? raw["source-file"] : void 0,
212
+ lineNumber: typeof raw["line-number"] === "number" ? raw["line-number"] : void 0,
213
+ columnNumber: typeof raw["column-number"] === "number" ? raw["column-number"] : void 0
214
+ };
215
+ }
216
+ function normalizeNew(entry) {
217
+ if (!entry || typeof entry !== "object") return null;
218
+ const body = entry.body;
219
+ if (!body || typeof body !== "object") return null;
220
+ const b = body;
221
+ return {
222
+ blockedUrl: toStringSafe(b.blockedURL),
223
+ documentUrl: toStringSafe(b.documentURL),
224
+ violatedDirective: toStringSafe(b.violatedDirective),
225
+ effectiveDirective: typeof b.effectiveDirective === "string" ? b.effectiveDirective : void 0,
226
+ originalPolicy: typeof b.originalPolicy === "string" ? b.originalPolicy : void 0,
227
+ disposition: b.disposition === "enforce" || b.disposition === "report" ? b.disposition : void 0,
228
+ statusCode: typeof b.statusCode === "number" ? b.statusCode : void 0,
229
+ sourceFile: typeof b.sourceFile === "string" ? b.sourceFile : void 0,
230
+ lineNumber: typeof b.lineNumber === "number" ? b.lineNumber : void 0,
231
+ columnNumber: typeof b.columnNumber === "number" ? b.columnNumber : void 0
232
+ };
233
+ }
234
+ async function readBody(req, maxBytes) {
235
+ return await new Promise((resolve, reject) => {
236
+ const chunks = [];
237
+ let total = 0;
238
+ req.on("data", (chunk) => {
239
+ total += chunk.length;
240
+ if (total > maxBytes) {
241
+ reject(new Error("body too large"));
242
+ req.destroy();
243
+ return;
244
+ }
245
+ chunks.push(chunk);
246
+ });
247
+ req.on("end", () => {
248
+ resolve(Buffer.concat(chunks).toString("utf8"));
249
+ });
250
+ req.on("error", reject);
251
+ });
252
+ }
253
+ async function handleCspReport(req, res, opts) {
254
+ const ctHeader = req.headers["content-type"];
255
+ const ct = pickHeader2(ctHeader);
256
+ let raw;
257
+ try {
258
+ raw = await readBody(req, MAX_BODY);
259
+ } catch {
260
+ res.statusCode = 413;
261
+ res.end();
262
+ return;
263
+ }
264
+ let violations;
265
+ try {
266
+ if (ct.startsWith("application/csp-report")) {
267
+ const parsed = JSON.parse(raw);
268
+ const inner = parsed["csp-report"];
269
+ if (!inner || typeof inner !== "object") {
270
+ res.statusCode = 204;
271
+ res.end();
272
+ return;
273
+ }
274
+ violations = [normalizeLegacy(inner)];
275
+ } else if (ct.startsWith("application/reports+json")) {
276
+ const parsed = JSON.parse(raw);
277
+ const entries = Array.isArray(parsed) ? parsed : [];
278
+ violations = entries.map((e) => normalizeNew(e)).filter((v) => v !== null);
279
+ } else {
280
+ res.statusCode = 415;
281
+ res.end();
282
+ return;
283
+ }
284
+ } catch {
285
+ res.statusCode = 400;
286
+ res.end();
287
+ return;
288
+ }
289
+ for (const v of violations) {
290
+ safeAudit(opts.auditLogger, {
291
+ action: "csp.violation",
292
+ metadata: v
293
+ });
294
+ try {
295
+ opts.devtoolsDispatcher?.onCspViolation?.(v);
296
+ } catch {
297
+ }
298
+ try {
299
+ opts.onViolation?.(v);
300
+ } catch {
301
+ }
302
+ }
303
+ res.statusCode = 204;
304
+ res.end();
305
+ }
306
+
307
+ // src/server/security/csrf-readiness-endpoint.ts
308
+ var CSRF_READINESS_PATH = "/__theo/csrf-readiness";
309
+ var CSRF_READINESS_RESET_PATH = "/__theo/csrf-readiness/reset";
310
+ function originMatchesHost(req) {
311
+ const origin = req.headers.origin;
312
+ if (typeof origin !== "string") return false;
313
+ const host = req.headers.host;
314
+ if (typeof host !== "string") return false;
315
+ try {
316
+ const parsed = new URL(origin);
317
+ return parsed.host === host;
318
+ } catch {
319
+ return false;
320
+ }
321
+ }
322
+ function sendJson(res, status, body) {
323
+ res.writeHead(status, { "content-type": "application/json" });
324
+ res.end(JSON.stringify(body));
325
+ }
326
+ function sendNoContent(res) {
327
+ res.writeHead(204);
328
+ res.end();
329
+ }
330
+ function sendError(res, status, code, message) {
331
+ res.writeHead(status, { "content-type": "application/json" });
332
+ res.end(JSON.stringify({ error: { code, message } }));
333
+ }
334
+ async function handleCsrfReadiness(req, res, store) {
335
+ const url = (req.url ?? "").split("?")[0];
336
+ const method = req.method ?? "GET";
337
+ if (url === CSRF_READINESS_PATH) {
338
+ if (method === "GET") {
339
+ sendJson(res, 200, store.summary());
340
+ return true;
341
+ }
342
+ sendError(res, 405, "METHOD_NOT_ALLOWED", `Use GET on ${CSRF_READINESS_PATH}`);
343
+ return true;
344
+ }
345
+ if (url === CSRF_READINESS_RESET_PATH) {
346
+ if (method !== "POST") {
347
+ sendError(res, 405, "METHOD_NOT_ALLOWED", `Use POST on ${CSRF_READINESS_RESET_PATH}`);
348
+ return true;
349
+ }
350
+ const hasHeader = req.headers["x-theo-action"] === "1";
351
+ if (!hasHeader || !originMatchesHost(req)) {
352
+ sendError(res, 403, "CSRF_INVALID", "Reset requires X-Theo-Action: 1 + same-origin");
353
+ return true;
354
+ }
355
+ store.reset();
356
+ sendNoContent(res);
357
+ return true;
358
+ }
359
+ return false;
360
+ }
361
+
362
+ // src/server/security/csrf-readiness-store.ts
363
+ var CsrfReadinessStore = class _CsrfReadinessStore {
364
+ static MAX_ENTRIES = 1e3;
365
+ entries = /* @__PURE__ */ new Map();
366
+ keyFor(event) {
367
+ return `${event.method}\0${event.path}\0${event.reason}`;
368
+ }
369
+ record(event) {
370
+ const key = this.keyFor(event);
371
+ const now = (/* @__PURE__ */ new Date()).toISOString();
372
+ const existing = this.entries.get(key);
373
+ if (existing) {
374
+ existing.count++;
375
+ existing.lastSeen = now;
376
+ return;
377
+ }
378
+ if (this.entries.size >= _CsrfReadinessStore.MAX_ENTRIES) {
379
+ const firstKey = this.entries.keys().next().value;
380
+ if (firstKey !== void 0) this.entries.delete(firstKey);
381
+ }
382
+ this.entries.set(key, { count: 1, firstSeen: now, lastSeen: now });
383
+ }
384
+ summary() {
385
+ let total = 0;
386
+ const routes = [];
387
+ for (const [key, entry] of this.entries) {
388
+ const [method, path, reason] = key.split("\0");
389
+ routes.push({
390
+ method,
391
+ path,
392
+ reason,
393
+ count: entry.count,
394
+ firstSeen: entry.firstSeen,
395
+ lastSeen: entry.lastSeen
396
+ });
397
+ total += entry.count;
398
+ }
399
+ return {
400
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
401
+ totalEvents: total,
402
+ routes
403
+ };
404
+ }
405
+ reset() {
406
+ this.entries.clear();
407
+ }
408
+ };
409
+
410
+ export {
411
+ BATCH_PATH,
412
+ handleBatchRequest,
413
+ createCorsHandler,
414
+ TRACE_HEADER,
415
+ extractTraceId,
416
+ CSP_REPORT_PATH,
417
+ handleCspReport,
418
+ handleCsrfReadiness,
419
+ CsrfReadinessStore
420
+ };
421
+ //# sourceMappingURL=chunk-CBGRFVF5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/http/batch-handler.ts","../src/server/http/cors.ts","../src/server/http/trace-context.ts","../src/server/security/csp-report.ts","../src/server/security/csrf-readiness-endpoint.ts","../src/server/security/csrf-readiness-store.ts"],"sourcesContent":["/**\n * T1.4 — Server-side batch handler.\n *\n * Receives `{ requests: [...] }` POSTed to `/api/__theo_batch__` and returns\n * `{ results: [...] }` with per-item error isolation. EC-2: items cannot\n * override auth/forwarded headers (would be a session-bypass vector).\n */\n\nimport { z } from 'zod'\n\nexport const STRIPPED_HEADERS = [\n 'authorization',\n 'cookie',\n 'x-forwarded-for',\n 'x-forwarded-host',\n 'x-forwarded-proto',\n 'x-real-ip',\n 'host',\n] as const\n\nexport const BATCH_PATH = '/api/__theo_batch__'\nconst DEFAULT_MAX_BATCH = 32\n\nexport class BatchPathConflictError extends Error {\n constructor(path: string) {\n super(\n `Server route conflicts with reserved batch path ${path}. ` +\n `Rename the route or disable batching in theo.config.ts.`,\n )\n this.name = 'BatchPathConflictError'\n }\n}\n\nconst batchRequestSchema = z.object({\n path: z.string().min(1),\n method: z.string().min(1),\n query: z.record(z.string(), z.unknown()).optional(),\n body: z.unknown().optional(),\n headers: z.record(z.string(), z.string()).optional(),\n})\n\nconst batchPayloadSchema = z.object({\n requests: z.array(batchRequestSchema).min(1),\n})\n\nexport type BatchRequestItem = z.infer<typeof batchRequestSchema>\nexport type BatchPayload = z.infer<typeof batchPayloadSchema>\n\nexport type BatchExecuteFn = (\n req: BatchRequestItem,\n) => Promise<{ data: unknown } | { error: { message: string; code?: string } }>\n\nexport interface HandleBatchOptions {\n execute: BatchExecuteFn\n /** Max number of items per batch. Default 32. */\n max?: number\n /** Outer-request headers that override per-item headers in STRIPPED_HEADERS. */\n outerHeaders?: Record<string, string>\n}\n\nexport type BatchResultItem = { data: unknown } | { error: { message: string; code?: string } }\n\nexport interface BatchResponse {\n results: BatchResultItem[]\n}\n\n/**\n * Sanitize a per-item header object: keys in STRIPPED_HEADERS are removed,\n * then the outer request's values for those keys are layered on top so that\n * downstream middlewares see the real session/auth headers.\n */\nfunction sanitizeItemHeaders(\n itemHeaders: Record<string, string> | undefined,\n outerHeaders: Record<string, string> | undefined,\n): Record<string, string> {\n const out: Record<string, string> = {}\n if (itemHeaders) {\n for (const [k, v] of Object.entries(itemHeaders)) {\n const lower = k.toLowerCase()\n if (!(STRIPPED_HEADERS as readonly string[]).includes(lower)) {\n out[lower] = v\n }\n }\n }\n if (outerHeaders) {\n for (const stripped of STRIPPED_HEADERS) {\n // `Object.hasOwn` defends against missing keys without triggering\n // `no-unnecessary-condition` (TypeScript types `outerHeaders` keys\n // as defined; `hasOwn` keeps the conditional honest at runtime).\n if (Object.hasOwn(outerHeaders, stripped)) {\n out[stripped] = outerHeaders[stripped]\n }\n }\n }\n return out\n}\n\n/**\n * Validate + execute a batch request.\n *\n * CR-028 fix: previously the signature was `payload: BatchPayload`,\n * suggesting the caller had already validated. In reality `api-middleware`\n * passes `JSON.parse(rawBody) as BatchPayload` — an unsafe cast over raw\n * HTTP input. We widen the input type to `unknown` so the type system\n * forces validation, then run Zod once at this single trust boundary.\n */\nexport async function handleBatchRequest(\n payload: unknown,\n options: HandleBatchOptions,\n): Promise<BatchResponse> {\n const parsed = batchPayloadSchema.parse(payload)\n const max = options.max ?? DEFAULT_MAX_BATCH\n if (parsed.requests.length > max) {\n throw new Error(`Batch size ${parsed.requests.length} exceeds max ${max}`)\n }\n\n const results: BatchResultItem[] = []\n for (const item of parsed.requests) {\n const sanitized: BatchRequestItem = {\n ...item,\n headers: sanitizeItemHeaders(item.headers, options.outerHeaders),\n }\n try {\n const r = await options.execute(sanitized)\n results.push(r)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n results.push({ error: { message } })\n }\n }\n return { results }\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http'\n\n/**\n * T1.2 — CORS middleware.\n *\n * Single global middleware that runs FIRST in the request pipeline:\n * CORS preflight → rate limit → CSRF → security headers → handler\n *\n * Preflight (`OPTIONS` with `Access-Control-Request-Method`) is handled\n * by `handlePreflight`, which short-circuits the response with 204 +\n * Access-Control-* headers.\n *\n * Non-preflight requests pass through; `applyHeaders` adds\n * `Access-Control-Allow-Origin` (echoing the request's Origin),\n * `Access-Control-Expose-Headers`, and `Access-Control-Allow-Credentials`.\n *\n * Per ADR D3: preflight responses are deterministic and only the matched\n * origin is echoed back (never `'*'` when credentials are enabled —\n * required by the CORS spec).\n */\n\n// `'*'` is conceptually distinct from a regular host string (it means\n// \"any origin\"), but TypeScript-wise it is just `string`, so we collapse\n// the union and document the wildcard via the type's name. Callers should\n// still pass the literal `'*'` to mean \"allow anywhere\" for readability.\nexport type CorsOrigin =\n | string\n | RegExp\n | readonly (string | RegExp)[]\n | ((origin: string) => boolean)\n\nexport interface CorsConfig {\n origins: CorsOrigin\n methods?: readonly ('GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD')[]\n allowedHeaders?: readonly string[]\n exposedHeaders?: readonly string[]\n credentials?: boolean\n maxAge?: number\n}\n\nexport interface CorsHandler {\n handlePreflight(req: IncomingMessage, res: ServerResponse): boolean\n applyHeaders(req: IncomingMessage, res: ServerResponse): void\n}\n\nconst DEFAULT_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'] as const\nconst DEFAULT_ALLOWED_HEADERS = ['Content-Type', 'X-Theo-Action', 'Authorization'] as const\nconst DEFAULT_MAX_AGE = 600\n\n/**\n * Read the request Origin header. Per Node typing, it can be a string,\n * string[] (proxy doubled), or undefined. We take the FIRST non-empty\n * value (consistent with `csrf.ts:121-122`).\n */\nfunction readOrigin(req: IncomingMessage): string | undefined {\n const raw = req.headers.origin\n if (raw === undefined) return undefined\n if (Array.isArray(raw)) {\n return raw.find((v): v is string => typeof v === 'string' && v.length > 0)\n }\n return raw\n}\n\n/**\n * Test whether `origin` is allowed by `allowed`. Pure function — no I/O.\n *\n * EC-8: callback variants that throw are fail-closed (deny). Without\n * this, a transient datastore outage would silently widen CORS during\n * the failure window.\n */\nexport function matchesOrigin(origin: string, allowed: CorsOrigin): boolean {\n if (allowed === '*') return true\n if (typeof allowed === 'string') return origin === allowed\n if (allowed instanceof RegExp) {\n allowed.lastIndex = 0\n return allowed.test(origin)\n }\n if (Array.isArray(allowed)) {\n for (const entry of allowed) {\n if (typeof entry === 'string' && origin === entry) return true\n if (entry instanceof RegExp) {\n entry.lastIndex = 0\n if (entry.test(origin)) return true\n }\n }\n return false\n }\n if (typeof allowed === 'function') {\n // EC-8: fail-closed on any throw\n try {\n return allowed(origin)\n } catch {\n return false\n }\n }\n return false\n}\n\nexport function createCorsHandler(config: CorsConfig): CorsHandler {\n const methods = (config.methods ?? DEFAULT_METHODS).slice()\n const allowedHeaders = (config.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS).slice()\n const maxAge = String(config.maxAge ?? DEFAULT_MAX_AGE)\n const credentials = config.credentials === true\n\n return {\n handlePreflight(req, res) {\n if (req.method !== 'OPTIONS') return false\n const acMethod = req.headers['access-control-request-method']\n if (!acMethod) return false\n const origin = readOrigin(req)\n if (!origin) return false\n\n if (!matchesOrigin(origin, config.origins)) {\n res.statusCode = 403\n res.end()\n return true\n }\n\n // Echo the matched origin (NEVER '*' when credentials are enabled —\n // browsers reject wildcard responses with credentials per CORS spec).\n res.setHeader('Access-Control-Allow-Origin', origin)\n res.setHeader('Access-Control-Allow-Methods', methods.join(', '))\n res.setHeader('Access-Control-Allow-Headers', allowedHeaders.join(', '))\n res.setHeader('Access-Control-Max-Age', maxAge)\n // Mark the response as origin-varying so caches don't poison\n res.setHeader('Vary', 'Origin')\n if (credentials) res.setHeader('Access-Control-Allow-Credentials', 'true')\n res.statusCode = 204\n res.end()\n return true\n },\n\n applyHeaders(req, res) {\n const origin = readOrigin(req)\n if (!origin) return\n if (!matchesOrigin(origin, config.origins)) return\n\n res.setHeader('Access-Control-Allow-Origin', origin)\n res.setHeader('Vary', 'Origin')\n if (credentials) res.setHeader('Access-Control-Allow-Credentials', 'true')\n if (config.exposedHeaders && config.exposedHeaders.length > 0) {\n res.setHeader('Access-Control-Expose-Headers', config.exposedHeaders.join(', '))\n }\n },\n }\n}\n\n/**\n * T5a.2 Phase B slice 5/6 — Web-Standards CORS handler.\n *\n * Mirror of `createCorsHandler(config): CorsHandler` for the Web `Request`\n * shape. Same `CorsConfig`, same `matchesOrigin` logic (pure helper\n * already), same security guarantees (echo matched origin only — never\n * `'*'` when credentials enabled; EC-8 fail-closed on callback throw).\n *\n * Signature differences vs the IncomingMessage pair:\n * - `handlePreflightRequest(request, config): Response | null` — returns\n * `Response` when this is a CORS preflight; `null` when not. Caller\n * short-circuits accordingly (same control-flow semantic as boolean).\n * - `applyCorsHeaders(request, target, config): void` — mutates a\n * `Headers` instance in place. Caller passes the headers they're\n * building for their Response.\n */\nexport interface CorsWebHandler {\n handlePreflightRequest(request: Request): Response | null\n applyCorsHeaders(request: Request, target: Headers): void\n}\n\nexport function createCorsWebHandler(config: CorsConfig): CorsWebHandler {\n const methods = (config.methods ?? DEFAULT_METHODS).slice()\n const allowedHeaders = (config.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS).slice()\n const maxAge = String(config.maxAge ?? DEFAULT_MAX_AGE)\n const credentials = config.credentials === true\n\n return {\n handlePreflightRequest(request) {\n if (request.method !== 'OPTIONS') return null\n const acMethod = request.headers.get('access-control-request-method')\n if (acMethod === null || acMethod.length === 0) return null\n const origin = request.headers.get('origin')\n if (origin === null || origin.length === 0) return null\n\n if (!matchesOrigin(origin, config.origins)) {\n return new Response(null, { status: 403 })\n }\n\n // Echo matched origin only (security: never `'*'` when credentials\n // enabled — browsers reject per CORS spec).\n const headers = new Headers({\n 'Access-Control-Allow-Origin': origin,\n 'Access-Control-Allow-Methods': methods.join(', '),\n 'Access-Control-Allow-Headers': allowedHeaders.join(', '),\n 'Access-Control-Max-Age': maxAge,\n Vary: 'Origin',\n })\n if (credentials) headers.set('Access-Control-Allow-Credentials', 'true')\n return new Response(null, { status: 204, headers })\n },\n\n applyCorsHeaders(request, target) {\n const origin = request.headers.get('origin')\n if (origin === null || origin.length === 0) return\n if (!matchesOrigin(origin, config.origins)) return\n\n target.set('Access-Control-Allow-Origin', origin)\n target.set('Vary', 'Origin')\n if (credentials) target.set('Access-Control-Allow-Credentials', 'true')\n if (config.exposedHeaders !== undefined && config.exposedHeaders.length > 0) {\n target.set('Access-Control-Expose-Headers', config.exposedHeaders.join(', '))\n }\n },\n }\n}\n","// T5a.1b — Web Crypto migration. randomUUID() moved to globalThis.crypto.\n// IncomingMessage stays as a type-only import (runtime-clean — TS erases at\n// build); full IncomingMessage→Request boundary migration deferred to a\n// later T5a.1c+ slice per ADR-0028 incremental leaf-first sequence.\nimport type { IncomingMessage } from 'node:http'\n\n/**\n * Phase 7 — Observability: traceId propagation (D7).\n *\n * Extract a stable identifier from incoming requests so a single value\n * correlates the client request, every server log line, the response\n * envelope, and any downstream span. Precedence:\n *\n * 1. `traceparent` (W3C Trace Context — `00-{32-hex}-{16-hex}-{flags}`)\n * 2. `x-request-id` (Heroku / GCP / generic proxy header)\n * 3. Generated UUID (fresh per request)\n *\n * UUIDs are accepted as trace identifiers by every major vendor that\n * does not enforce strict 32-hex (Datadog, Honeycomb, Sentry, Logflare,\n * Axiom, etc). We don't need ULIDs to ship this surface.\n */\n\nexport const TRACE_HEADER = 'x-trace-id'\nexport const TRACE_PARENT_HEADER = 'traceparent'\nconst REQUEST_ID_HEADER = 'x-request-id'\n\n// W3C Trace Context: 00-<trace-id 32 hex>-<span-id 16 hex>-<flags 2 hex>\nconst TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/\n\n/**\n * Parse a W3C Trace Context `traceparent` header value. Returns the\n * 32-hex trace-id when valid (and not the reserved all-zeros), else\n * `null`.\n */\nexport function parseTraceparent(value: string): string | null {\n if (!value) return null\n const m = TRACEPARENT_RE.exec(value)\n if (!m) return null\n const traceId = m[1]\n // W3C: trace-id of all zeroes is invalid by spec\n if (/^0+$/.test(traceId)) return null\n return traceId\n}\n\n/**\n * Pick the first string value out of an IncomingMessage header. Node\n * collapses repeated headers into arrays; proxies sometimes do this for\n * `x-request-id`. Empty strings count as absent.\n */\nfunction pickHeader(value: string | string[] | undefined): string | null {\n if (Array.isArray(value)) {\n for (const v of value) {\n if (typeof v === 'string' && v.length > 0) return v\n }\n return null\n }\n if (typeof value === 'string' && value.length > 0) return value\n return null\n}\n\n/**\n * T5a.2 Phase C slice 1/2 — pure traceId resolution from pre-extracted\n * header values. Shared between IncomingMessage and Web Request wrappers.\n */\nfunction resolveTraceIdFromHeaders(traceparent: string | null, requestId: string | null): string {\n if (traceparent !== null) {\n const parsed = parseTraceparent(traceparent)\n if (parsed !== null) return parsed\n }\n if (requestId !== null) return requestId\n return globalThis.crypto.randomUUID()\n}\n\n/**\n * Resolve the request's traceId following the precedence above.\n */\nexport function extractTraceId(req: IncomingMessage): string {\n return resolveTraceIdFromHeaders(\n pickHeader(req.headers[TRACE_PARENT_HEADER]),\n pickHeader(req.headers[REQUEST_ID_HEADER]),\n )\n}\n\n/**\n * T5a.2 Phase C slice 1/2 — Web-Standards-shaped traceId resolver.\n *\n * Mirror of `extractTraceId(req: IncomingMessage)` for the Web `Request`\n * shape. Same precedence (`traceparent` → `x-request-id` → generated\n * UUID). Uses `request.headers.get(name)` (native Web `Headers` API)\n * instead of the Node indexer.\n *\n * **Multi-value note:** Web `Headers` collapses repeated headers into a\n * single comma-separated string at parse. The IncomingMessage path's\n * `pickHeader` \"first non-empty value\" semantic is naturally satisfied\n * because there's no array to pick from on the Web side — `.get()`\n * returns the comma-joined value, which for `traceparent` / `x-request-id`\n * is treated as a single string anyway (both headers are conventionally\n * single-valued).\n */\nexport function extractTraceIdFromRequest(request: Request): string {\n return resolveTraceIdFromHeaders(\n request.headers.get(TRACE_PARENT_HEADER),\n request.headers.get(REQUEST_ID_HEADER),\n )\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http'\n\nimport { safeAudit, type AuditLogger } from '../observability/audit-log.js'\n\n/**\n * T5.1 — Built-in CSP report endpoint.\n *\n * Browsers POST violation reports to `report-uri` (legacy `application/csp-report`)\n * or `Reporting API` (`application/reports+json`). Framework auto-registers this\n * endpoint so `cspMode: 'report-only'` is actually useful out of the box.\n *\n * Forwards normalized violations to:\n * - audit logger (`csp.violation`)\n * - devtools dispatcher (dev only, for Errors tab)\n * - optional user hook (Sentry, etc.)\n *\n * EC-2: browser MAY send `{\"csp-report\": null}`, empty `{}`, or\n * reports+json entries lacking `body`. Handler MUST short-circuit to\n * 204 (valid format, no violation), NEVER crash via null deref.\n */\n\nexport const CSP_REPORT_PATH = '/__theo/csp-report'\n\n/** Max body bytes accepted. Real CSP reports are < 2 KB; 16 KB is generous. */\nconst MAX_BODY = 16 * 1024\n\nexport interface CspViolation {\n blockedUrl: string\n documentUrl: string\n violatedDirective: string\n effectiveDirective?: string\n originalPolicy?: string\n disposition?: 'enforce' | 'report'\n statusCode?: number\n sourceFile?: string\n lineNumber?: number\n columnNumber?: number\n}\n\nexport interface CspReportHandlerOptions {\n auditLogger?: AuditLogger\n devtoolsDispatcher?: {\n onCspViolation?: (v: CspViolation) => void\n }\n /** Optional user-provided sink (Sentry, custom log router). Errors here are swallowed. */\n onViolation?: (v: CspViolation) => void\n}\n\n/**\n * Map a legacy `application/csp-report` payload to the internal shape.\n * Caller must ensure `raw` is a non-null object.\n */\nfunction pickHeader(value: string | string[] | undefined): string {\n if (typeof value === 'string') return value\n if (Array.isArray(value) && value.length > 0) return value[0]\n return ''\n}\n\nfunction toStringSafe(value: unknown, fallback = '(missing)'): string {\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n return fallback\n}\n\nexport function normalizeLegacy(raw: Record<string, unknown>): CspViolation {\n return {\n blockedUrl: toStringSafe(raw['blocked-uri']),\n documentUrl: toStringSafe(raw['document-uri']),\n violatedDirective: toStringSafe(raw['violated-directive']),\n effectiveDirective:\n typeof raw['effective-directive'] === 'string' ? raw['effective-directive'] : undefined,\n originalPolicy: typeof raw['original-policy'] === 'string' ? raw['original-policy'] : undefined,\n disposition:\n raw.disposition === 'enforce' || raw.disposition === 'report' ? raw.disposition : undefined,\n statusCode: typeof raw['status-code'] === 'number' ? raw['status-code'] : undefined,\n sourceFile: typeof raw['source-file'] === 'string' ? raw['source-file'] : undefined,\n lineNumber: typeof raw['line-number'] === 'number' ? raw['line-number'] : undefined,\n columnNumber: typeof raw['column-number'] === 'number' ? raw['column-number'] : undefined,\n }\n}\n\n/**\n * Map a `reports+json` entry to the internal shape. Returns `null` if\n * the entry lacks a usable `body` object (EC-2).\n */\nexport function normalizeNew(entry: unknown): CspViolation | null {\n if (!entry || typeof entry !== 'object') return null\n const body = (entry as { body?: unknown }).body\n if (!body || typeof body !== 'object') return null\n const b = body as Record<string, unknown>\n return {\n blockedUrl: toStringSafe(b.blockedURL),\n documentUrl: toStringSafe(b.documentURL),\n violatedDirective: toStringSafe(b.violatedDirective),\n effectiveDirective: typeof b.effectiveDirective === 'string' ? b.effectiveDirective : undefined,\n originalPolicy: typeof b.originalPolicy === 'string' ? b.originalPolicy : undefined,\n disposition:\n b.disposition === 'enforce' || b.disposition === 'report' ? b.disposition : undefined,\n statusCode: typeof b.statusCode === 'number' ? b.statusCode : undefined,\n sourceFile: typeof b.sourceFile === 'string' ? b.sourceFile : undefined,\n lineNumber: typeof b.lineNumber === 'number' ? b.lineNumber : undefined,\n columnNumber: typeof b.columnNumber === 'number' ? b.columnNumber : undefined,\n }\n}\n\nasync function readBody(req: IncomingMessage, maxBytes: number): Promise<string> {\n return await new Promise<string>((resolve, reject) => {\n const chunks: Buffer[] = []\n let total = 0\n req.on('data', (chunk: Buffer) => {\n total += chunk.length\n if (total > maxBytes) {\n reject(new Error('body too large'))\n req.destroy()\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n resolve(Buffer.concat(chunks).toString('utf8'))\n })\n req.on('error', reject)\n })\n}\n\nexport async function handleCspReport(\n req: IncomingMessage,\n res: ServerResponse,\n opts: CspReportHandlerOptions,\n): Promise<void> {\n const ctHeader = req.headers['content-type']\n const ct = pickHeader(ctHeader)\n\n let raw: string\n try {\n raw = await readBody(req, MAX_BODY)\n } catch {\n res.statusCode = 413\n res.end()\n return\n }\n\n let violations: CspViolation[]\n try {\n if (ct.startsWith('application/csp-report')) {\n // EC-2: browsers MAY POST {\"csp-report\": null} or {} on\n // disposition='report' policies. Guard against null/undefined/non-object.\n const parsed = JSON.parse(raw) as Record<string, unknown>\n const inner = parsed['csp-report']\n if (!inner || typeof inner !== 'object') {\n res.statusCode = 204\n res.end()\n return\n }\n violations = [normalizeLegacy(inner as Record<string, unknown>)]\n } else if (ct.startsWith('application/reports+json')) {\n // EC-2: filter out entries lacking `body` BEFORE normalizing.\n const parsed: unknown = JSON.parse(raw)\n const entries = Array.isArray(parsed) ? parsed : []\n violations = entries.map((e) => normalizeNew(e)).filter((v): v is CspViolation => v !== null)\n } else {\n res.statusCode = 415\n res.end()\n return\n }\n } catch {\n res.statusCode = 400\n res.end()\n return\n }\n\n for (const v of violations) {\n safeAudit(opts.auditLogger, {\n action: 'csp.violation',\n metadata: v as unknown as Record<string, unknown>,\n })\n try {\n opts.devtoolsDispatcher?.onCspViolation?.(v)\n } catch {\n // dispatcher errors are isolated from the request\n }\n try {\n opts.onViolation?.(v)\n } catch {\n // user hook throws are swallowed — never crash the request\n }\n }\n\n res.statusCode = 204\n res.end()\n}\n\n/**\n * T5a.2 Phase B slice 4/6 — Web-Standards CSP report handler.\n *\n * Mirror of `handleCspReport(req: IncomingMessage, res: ServerResponse,\n * opts): Promise<void>` for the Web `Request` shape. Returns\n * `Response` directly instead of mutating `res`.\n *\n * Same content-type dispatch (legacy `application/csp-report` vs new\n * `application/reports+json`), same normalizers (`normalizeLegacy`,\n * `normalizeNew`), same side-effect loop (safeAudit + devtools + user\n * hook). The body cap is enforced post-read because Web Request body\n * streaming doesn't have a portable mid-stream rejection primitive\n * across CF Workers / Bun / Deno; CSP reports are < 2 KB typical, well\n * under MAX_BODY (16 KB).\n *\n * Returns:\n * - 204 on accepted reports OR no-op empty payload\n * - 413 on body too large (post-read check)\n * - 415 on unsupported content-type\n * - 400 on malformed JSON\n */\nasync function readBodyFromRequest(request: Request, maxBytes: number): Promise<string | null> {\n const contentLengthHeader = request.headers.get('content-length')\n if (contentLengthHeader !== null) {\n const declared = Number.parseInt(contentLengthHeader, 10)\n if (Number.isFinite(declared) && declared > maxBytes) return null\n }\n const text = await request.text()\n if (text.length > maxBytes) return null\n return text\n}\n\nfunction dispatchViolations(violations: CspViolation[], opts: CspReportHandlerOptions): void {\n for (const v of violations) {\n safeAudit(opts.auditLogger, {\n action: 'csp.violation',\n metadata: v as unknown as Record<string, unknown>,\n })\n try {\n opts.devtoolsDispatcher?.onCspViolation?.(v)\n } catch {\n // dispatcher errors are isolated from the request\n }\n try {\n opts.onViolation?.(v)\n } catch {\n // user hook throws are swallowed — never crash the request\n }\n }\n}\n\nexport async function handleCspReportRequest(\n request: Request,\n opts: CspReportHandlerOptions,\n): Promise<Response> {\n const ct = request.headers.get('content-type') ?? ''\n\n const raw = await readBodyFromRequest(request, MAX_BODY)\n if (raw === null) {\n return new Response(null, { status: 413 })\n }\n\n let violations: CspViolation[]\n try {\n if (ct.startsWith('application/csp-report')) {\n const parsed = JSON.parse(raw) as Record<string, unknown>\n const inner = parsed['csp-report']\n if (!inner || typeof inner !== 'object') {\n return new Response(null, { status: 204 })\n }\n violations = [normalizeLegacy(inner as Record<string, unknown>)]\n } else if (ct.startsWith('application/reports+json')) {\n const parsed: unknown = JSON.parse(raw)\n const entries = Array.isArray(parsed) ? parsed : []\n violations = entries.map((e) => normalizeNew(e)).filter((v): v is CspViolation => v !== null)\n } else {\n return new Response(null, { status: 415 })\n }\n } catch {\n return new Response(null, { status: 400 })\n }\n\n dispatchViolations(violations, opts)\n return new Response(null, { status: 204 })\n}\n","/**\n * T2.2 — `/__theo/csrf-readiness` endpoint.\n *\n * GET /__theo/csrf-readiness → 200 + JSON summary\n * POST /__theo/csrf-readiness/reset → 204; clears the store\n *\n * The reset endpoint enforces CSRF (own dog food) — requires\n * `X-Theo-Action: 1` AND a matching Origin header (EC-15). This avoids\n * the endpoint being weaponizable from a cross-origin page when the\n * endpoint is opt-in exposed in production.\n *\n * Mount opt-in: in dev mode, the host wires this in unconditionally. In\n * production, only mount when `config.security.csrfTelemetry.exposeReadinessEndpoint === true`.\n * EC-10 parallel: returns 404 (via boolean false return) when the URL\n * does not match — the caller continues normal request handling.\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http'\n\nimport type { CsrfReadinessStore } from './csrf-readiness-store.js'\n\nexport const CSRF_READINESS_PATH = '/__theo/csrf-readiness'\nexport const CSRF_READINESS_RESET_PATH = '/__theo/csrf-readiness/reset'\n\nfunction originMatchesHost(req: IncomingMessage): boolean {\n const origin = req.headers.origin\n if (typeof origin !== 'string') return false\n const host = req.headers.host\n if (typeof host !== 'string') return false\n try {\n const parsed = new URL(origin)\n return parsed.host === host\n } catch {\n return false\n }\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json' })\n res.end(JSON.stringify(body))\n}\n\nfunction sendNoContent(res: ServerResponse): void {\n res.writeHead(204)\n res.end()\n}\n\nfunction sendError(res: ServerResponse, status: number, code: string, message: string): void {\n res.writeHead(status, { 'content-type': 'application/json' })\n res.end(JSON.stringify({ error: { code, message } }))\n}\n\n// eslint-disable-next-line @typescript-eslint/require-await -- async return for future telemetry hooks\nexport async function handleCsrfReadiness(\n req: IncomingMessage,\n res: ServerResponse,\n store: CsrfReadinessStore,\n): Promise<boolean> {\n const url = (req.url ?? '').split('?')[0]\n const method = req.method ?? 'GET'\n\n if (url === CSRF_READINESS_PATH) {\n if (method === 'GET') {\n sendJson(res, 200, store.summary())\n return true\n }\n sendError(res, 405, 'METHOD_NOT_ALLOWED', `Use GET on ${CSRF_READINESS_PATH}`)\n return true\n }\n\n if (url === CSRF_READINESS_RESET_PATH) {\n if (method !== 'POST') {\n sendError(res, 405, 'METHOD_NOT_ALLOWED', `Use POST on ${CSRF_READINESS_RESET_PATH}`)\n return true\n }\n // Enforce CSRF on our own endpoint (dog-food).\n const hasHeader = req.headers['x-theo-action'] === '1'\n if (!hasHeader || !originMatchesHost(req)) {\n sendError(res, 403, 'CSRF_INVALID', 'Reset requires X-Theo-Action: 1 + same-origin')\n return true\n }\n store.reset()\n sendNoContent(res)\n return true\n }\n\n return false\n}\n\n/**\n * T5a.2 Phase B slice 3/6 — Web-Standards-shaped CSRF readiness handler.\n *\n * Mirror of `handleCsrfReadiness(req: IncomingMessage, res: ServerResponse,\n * store): Promise<boolean>` for the Web `Request` shape. Returns\n * `Response | null` instead of mutating `res` and returning a boolean:\n * - `Response` → this handler claimed the URL; caller short-circuits.\n * - `null` → URL doesn't match our endpoint; caller continues normal dispatch.\n *\n * Same routes (GET `CSRF_READINESS_PATH`, POST `CSRF_READINESS_RESET_PATH`).\n * Same CSRF dog-food on reset (X-Theo-Action + same-origin).\n *\n * Wire into `executeWebRequest` integration via a future Phase B sub-slice\n * (consumer can also call this directly today via the\n * `theokit/server/security` sub-path).\n */\nfunction buildJsonResponse(status: number, body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\nfunction buildErrorResponse(status: number, code: string, message: string): Response {\n return buildJsonResponse(status, { error: { code, message } })\n}\n\n/**\n * Returns true when `Origin` header and request URL host match (RFC 6454\n * same-origin check). Web-Standards counterpart of `originMatchesHost`.\n */\nfunction originMatchesHostFromRequest(request: Request): boolean {\n const origin = request.headers.get('origin')\n if (origin === null || origin.length === 0) return false\n const host = request.headers.get('host')\n if (host === null || host.length === 0) {\n // Fallback: derive host from request.url (Web Request guarantees absolute URL)\n try {\n const reqHost = new URL(request.url).host\n const originHost = new URL(origin).host\n return originHost === reqHost\n } catch {\n return false\n }\n }\n try {\n return new URL(origin).host === host\n } catch {\n return false\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/require-await -- async return for future telemetry hooks\nexport async function handleCsrfReadinessRequest(\n request: Request,\n store: CsrfReadinessStore,\n): Promise<Response | null> {\n const url = new URL(request.url).pathname\n const method = request.method.toUpperCase()\n\n if (url === CSRF_READINESS_PATH) {\n if (method === 'GET') {\n return buildJsonResponse(200, store.summary())\n }\n return buildErrorResponse(405, 'METHOD_NOT_ALLOWED', `Use GET on ${CSRF_READINESS_PATH}`)\n }\n\n if (url === CSRF_READINESS_RESET_PATH) {\n if (method !== 'POST') {\n return buildErrorResponse(\n 405,\n 'METHOD_NOT_ALLOWED',\n `Use POST on ${CSRF_READINESS_RESET_PATH}`,\n )\n }\n // Enforce CSRF on our own endpoint (dog-food).\n const hasHeader = request.headers.get('x-theo-action') === '1'\n if (!hasHeader || !originMatchesHostFromRequest(request)) {\n return buildErrorResponse(\n 403,\n 'CSRF_INVALID',\n 'Reset requires X-Theo-Action: 1 + same-origin',\n )\n }\n store.reset()\n return new Response(null, { status: 204 })\n }\n\n return null\n}\n","/**\n * T2.2 — In-memory bounded counter for CSRF warn events.\n *\n * Aggregates `csrf.warn` payloads by `(method, path, reason)` triple.\n * Used by the `/__theo/csrf-readiness` endpoint to expose a summary the\n * developer (or devtools tab) can inspect, so users do NOT need to grep\n * stdout to know which endpoints will break under 0.3.0 strict CSRF.\n *\n * Bounded at 1000 distinct keys (EC-22). Eviction is LRU-by-insertion —\n * when the cap is hit, the oldest key is dropped and a re-record of an\n * evicted key starts fresh (count: 1).\n *\n * Single-threaded by virtue of the JS event loop; Map operations are\n * atomic. NOT shared across processes — each worker has its own store.\n */\n\nexport interface CsrfWarnRecord {\n method: string\n path: string\n reason: string\n}\n\nexport interface CsrfReadinessRouteSummary {\n method: string\n path: string\n reason: string\n count: number\n firstSeen: string\n lastSeen: string\n}\n\nexport interface CsrfReadinessSummary {\n generatedAt: string\n totalEvents: number\n routes: CsrfReadinessRouteSummary[]\n}\n\ninterface StoredEntry {\n count: number\n firstSeen: string\n lastSeen: string\n}\n\nexport class CsrfReadinessStore {\n static readonly MAX_ENTRIES = 1000\n private readonly entries = new Map<string, StoredEntry>()\n\n private keyFor(event: CsrfWarnRecord): string {\n return `${event.method}\u0000${event.path}\u0000${event.reason}`\n }\n\n record(event: CsrfWarnRecord): void {\n const key = this.keyFor(event)\n const now = new Date().toISOString()\n const existing = this.entries.get(key)\n if (existing) {\n existing.count++\n existing.lastSeen = now\n return\n }\n if (this.entries.size >= CsrfReadinessStore.MAX_ENTRIES) {\n const firstKey = this.entries.keys().next().value\n if (firstKey !== undefined) this.entries.delete(firstKey)\n }\n this.entries.set(key, { count: 1, firstSeen: now, lastSeen: now })\n }\n\n summary(): CsrfReadinessSummary {\n let total = 0\n const routes: CsrfReadinessRouteSummary[] = []\n for (const [key, entry] of this.entries) {\n const [method, path, reason] = key.split('\u0000')\n routes.push({\n method,\n path,\n reason,\n count: entry.count,\n firstSeen: entry.firstSeen,\n lastSeen: entry.lastSeen,\n })\n total += entry.count\n }\n return {\n generatedAt: new Date().toISOString(),\n totalEvents: total,\n routes,\n }\n }\n\n reset(): void {\n this.entries.clear()\n }\n}\n"],"mappings":"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","names":["pickHeader"]}
@@ -2,54 +2,6 @@ import {
2
2
  safeAudit
3
3
  } from "./chunk-UD3LFGDL.js";
4
4
 
5
- // src/server/security/csrf-readiness-store.ts
6
- var CsrfReadinessStore = class _CsrfReadinessStore {
7
- static MAX_ENTRIES = 1e3;
8
- entries = /* @__PURE__ */ new Map();
9
- keyFor(event) {
10
- return `${event.method}\0${event.path}\0${event.reason}`;
11
- }
12
- record(event) {
13
- const key = this.keyFor(event);
14
- const now = (/* @__PURE__ */ new Date()).toISOString();
15
- const existing = this.entries.get(key);
16
- if (existing) {
17
- existing.count++;
18
- existing.lastSeen = now;
19
- return;
20
- }
21
- if (this.entries.size >= _CsrfReadinessStore.MAX_ENTRIES) {
22
- const firstKey = this.entries.keys().next().value;
23
- if (firstKey !== void 0) this.entries.delete(firstKey);
24
- }
25
- this.entries.set(key, { count: 1, firstSeen: now, lastSeen: now });
26
- }
27
- summary() {
28
- let total = 0;
29
- const routes = [];
30
- for (const [key, entry] of this.entries) {
31
- const [method, path, reason] = key.split("\0");
32
- routes.push({
33
- method,
34
- path,
35
- reason,
36
- count: entry.count,
37
- firstSeen: entry.firstSeen,
38
- lastSeen: entry.lastSeen
39
- });
40
- total += entry.count;
41
- }
42
- return {
43
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
44
- totalEvents: total,
45
- routes
46
- };
47
- }
48
- reset() {
49
- this.entries.clear();
50
- }
51
- };
52
-
53
5
  // src/server/security/csp-report.ts
54
6
  var CSP_REPORT_PATH = "/__theo/csp-report";
55
7
  var MAX_BODY = 16 * 1024;
@@ -335,6 +287,54 @@ async function handleCsrfReadinessRequest(request, store) {
335
287
  return null;
336
288
  }
337
289
 
290
+ // src/server/security/csrf-readiness-store.ts
291
+ var CsrfReadinessStore = class _CsrfReadinessStore {
292
+ static MAX_ENTRIES = 1e3;
293
+ entries = /* @__PURE__ */ new Map();
294
+ keyFor(event) {
295
+ return `${event.method}\0${event.path}\0${event.reason}`;
296
+ }
297
+ record(event) {
298
+ const key = this.keyFor(event);
299
+ const now = (/* @__PURE__ */ new Date()).toISOString();
300
+ const existing = this.entries.get(key);
301
+ if (existing) {
302
+ existing.count++;
303
+ existing.lastSeen = now;
304
+ return;
305
+ }
306
+ if (this.entries.size >= _CsrfReadinessStore.MAX_ENTRIES) {
307
+ const firstKey = this.entries.keys().next().value;
308
+ if (firstKey !== void 0) this.entries.delete(firstKey);
309
+ }
310
+ this.entries.set(key, { count: 1, firstSeen: now, lastSeen: now });
311
+ }
312
+ summary() {
313
+ let total = 0;
314
+ const routes = [];
315
+ for (const [key, entry] of this.entries) {
316
+ const [method, path, reason] = key.split("\0");
317
+ routes.push({
318
+ method,
319
+ path,
320
+ reason,
321
+ count: entry.count,
322
+ firstSeen: entry.firstSeen,
323
+ lastSeen: entry.lastSeen
324
+ });
325
+ total += entry.count;
326
+ }
327
+ return {
328
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
329
+ totalEvents: total,
330
+ routes
331
+ };
332
+ }
333
+ reset() {
334
+ this.entries.clear();
335
+ }
336
+ };
337
+
338
338
  // src/server/security/security-headers.ts
339
339
  var DEFAULT_CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ws: wss:; frame-ancestors 'none'; report-uri /__theo/csp-report";
340
340
  var DEFAULT_PERMISSIONS_POLICY = "geolocation=(), camera=(), microphone=(), payment=(), usb=(), accelerometer=(), gyroscope=()";
@@ -388,7 +388,6 @@ function applySecurityHeaders(res, config, env, options = {}) {
388
388
  }
389
389
 
390
390
  export {
391
- CsrfReadinessStore,
392
391
  CSP_REPORT_PATH,
393
392
  normalizeLegacy,
394
393
  normalizeNew,
@@ -398,9 +397,10 @@ export {
398
397
  CSRF_READINESS_RESET_PATH,
399
398
  handleCsrfReadiness,
400
399
  handleCsrfReadinessRequest,
400
+ CsrfReadinessStore,
401
401
  DEFAULT_CSP,
402
402
  DEFAULT_PERMISSIONS_POLICY,
403
403
  buildSecurityHeaders,
404
404
  applySecurityHeaders
405
405
  };
406
- //# sourceMappingURL=chunk-B3L3TJVF.js.map
406
+ //# sourceMappingURL=chunk-JBCHWRKF.js.map