theokit 0.2.0 → 0.2.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 (112) hide show
  1. package/dist/actions-virtual-module-E6PU47GV.js +175 -0
  2. package/dist/actions-virtual-module-E6PU47GV.js.map +1 -0
  3. package/dist/actions-virtual-module-E7IELVHL.js +174 -0
  4. package/dist/actions-virtual-module-E7IELVHL.js.map +1 -0
  5. package/dist/app-typed-client-4L4L4BWD.js +289 -0
  6. package/dist/app-typed-client-4L4L4BWD.js.map +1 -0
  7. package/dist/app-typed-client-6PAT6VOL.js +289 -0
  8. package/dist/app-typed-client-6PAT6VOL.js.map +1 -0
  9. package/dist/{build-IQQYCA3B.js → build-EWZDEDT3.js} +37 -11
  10. package/dist/build-EWZDEDT3.js.map +1 -0
  11. package/dist/build-request-body-preview-EOP2Y4F3.js +52 -0
  12. package/dist/build-request-body-preview-EOP2Y4F3.js.map +1 -0
  13. package/dist/build-request-body-preview-II2235E6.js +54 -0
  14. package/dist/build-request-body-preview-II2235E6.js.map +1 -0
  15. package/dist/{chunk-GQSTRXXB.js → chunk-2BQYEBCK.js} +77 -68
  16. package/dist/chunk-2BQYEBCK.js.map +1 -0
  17. package/dist/{chunk-FRBYJANT.js → chunk-4W447H7M.js} +195 -41
  18. package/dist/chunk-4W447H7M.js.map +1 -0
  19. package/dist/chunk-7DHGV77J.js +140 -0
  20. package/dist/chunk-7DHGV77J.js.map +1 -0
  21. package/dist/{chunk-WQCPKA2L.js → chunk-C27AZN46.js} +570 -165
  22. package/dist/chunk-C27AZN46.js.map +1 -0
  23. package/dist/chunk-ERAZNHHR.js +73 -0
  24. package/dist/chunk-ERAZNHHR.js.map +1 -0
  25. package/dist/chunk-F3TG5FJV.js +97 -0
  26. package/dist/chunk-F3TG5FJV.js.map +1 -0
  27. package/dist/{chunk-OK2PJOLK.js → chunk-KJVEJILQ.js} +7 -3
  28. package/dist/chunk-KJVEJILQ.js.map +1 -0
  29. package/dist/{chunk-JBC7PFV4.js → chunk-M23FP2AS.js} +2 -2
  30. package/dist/chunk-M23FP2AS.js.map +1 -0
  31. package/dist/{chunk-S3JBIFKS.js → chunk-MG7H3LAW.js} +2 -2
  32. package/dist/chunk-MOT42NAH.js +165 -0
  33. package/dist/chunk-MOT42NAH.js.map +1 -0
  34. package/dist/{chunk-OEB3OIYP.js → chunk-OJHO2X7C.js} +497 -168
  35. package/dist/chunk-OJHO2X7C.js.map +1 -0
  36. package/dist/chunk-QLGES2HC.js +189 -0
  37. package/dist/chunk-QLGES2HC.js.map +1 -0
  38. package/dist/{chunk-JZ5WEG3S.js → chunk-SEVYARIU.js} +9 -5
  39. package/dist/chunk-SEVYARIU.js.map +1 -0
  40. package/dist/{chunk-ZK5JCE5J.js → chunk-TFECHXR5.js} +201 -43
  41. package/dist/chunk-TFECHXR5.js.map +1 -0
  42. package/dist/chunk-WSJKACWB.js +17 -0
  43. package/dist/chunk-WSJKACWB.js.map +1 -0
  44. package/dist/{chunk-3Q36SEDK.js → chunk-ZBWROIIO.js} +43 -2
  45. package/dist/{chunk-3Q36SEDK.js.map → chunk-ZBWROIIO.js.map} +1 -1
  46. package/dist/chunk-ZOXNJV2O.js +358 -0
  47. package/dist/chunk-ZOXNJV2O.js.map +1 -0
  48. package/dist/cli/index.js +22 -7
  49. package/dist/cli/index.js.map +1 -1
  50. package/dist/client/index.d.ts +31 -1
  51. package/dist/client/index.js +100 -0
  52. package/dist/client/index.js.map +1 -1
  53. package/dist/{dev-VN6WS3CM.js → dev-74KPI5C5.js} +15 -7
  54. package/dist/dev-74KPI5C5.js.map +1 -0
  55. package/dist/dev-emit-3CHM73TV.js +390 -0
  56. package/dist/dev-emit-3CHM73TV.js.map +1 -0
  57. package/dist/dev-emit-I3CKOHML.js +44 -0
  58. package/dist/dev-emit-I3CKOHML.js.map +1 -0
  59. package/dist/devtools/entry.js +545 -181
  60. package/dist/devtools/entry.js.map +1 -1
  61. package/dist/{dispatcher-IK3FVMX5.js → dispatcher-AQJGI3HU.js} +2 -2
  62. package/dist/dispatcher-E6QV32BO.js +83 -0
  63. package/dist/dispatcher-E6QV32BO.js.map +1 -0
  64. package/dist/{dist-Q5VBFWVL.js → dist-R2Q7GVDW.js} +10357 -12068
  65. package/dist/dist-R2Q7GVDW.js.map +1 -0
  66. package/dist/{generate-SIO4JSKJ.js → generate-DT4IIAHF.js} +88 -22
  67. package/dist/generate-DT4IIAHF.js.map +1 -0
  68. package/dist/index.d.ts +108 -1
  69. package/dist/index.js +5 -2
  70. package/dist/index.js.map +1 -1
  71. package/dist/{info-B3UOXFUF.js → info-VM7EK4JC.js} +3 -3
  72. package/dist/openapi-CBASKC7W.js +84 -0
  73. package/dist/openapi-CBASKC7W.js.map +1 -0
  74. package/dist/{registry-BB3UYAOG.js → registry-QGSNLJKJ.js} +2 -2
  75. package/dist/{routes-CAGFZKUK.js → routes-7O4NUDYS.js} +9 -7
  76. package/dist/{routes-CAGFZKUK.js.map → routes-7O4NUDYS.js.map} +1 -1
  77. package/dist/{schema-Cxq2qqwh.d.ts → schema-CjVly9c_.d.ts} +43 -1
  78. package/dist/{schema-MVB2FVK6.js → schema-JAKF23GR.js} +2 -2
  79. package/dist/server/cost/index.js +1 -1
  80. package/dist/server/index.d.ts +219 -47
  81. package/dist/server/index.js +35 -116
  82. package/dist/server/index.js.map +1 -1
  83. package/dist/{start-ZGWW5MOL.js → start-5JTFWF4T.js} +20 -14
  84. package/dist/start-5JTFWF4T.js.map +1 -0
  85. package/dist/{static-BH6JQNM2.js → static-6PZZBY7G.js} +4 -4
  86. package/dist/{upgrade-readiness-OHL36FQL.js → upgrade-readiness-Q46KTG2J.js} +6 -1
  87. package/dist/upgrade-readiness-Q46KTG2J.js.map +1 -0
  88. package/dist/vite-plugin/index.d.ts +25 -1
  89. package/dist/vite-plugin/index.js +5 -2
  90. package/dist/{vite-plugin-WA3G45YY.js → vite-plugin-3EQP2JYM.js} +7 -6
  91. package/package.json +4 -2
  92. package/dist/build-IQQYCA3B.js.map +0 -1
  93. package/dist/chunk-FRBYJANT.js.map +0 -1
  94. package/dist/chunk-GQSTRXXB.js.map +0 -1
  95. package/dist/chunk-JBC7PFV4.js.map +0 -1
  96. package/dist/chunk-JZ5WEG3S.js.map +0 -1
  97. package/dist/chunk-OEB3OIYP.js.map +0 -1
  98. package/dist/chunk-OK2PJOLK.js.map +0 -1
  99. package/dist/chunk-WQCPKA2L.js.map +0 -1
  100. package/dist/chunk-ZK5JCE5J.js.map +0 -1
  101. package/dist/dev-VN6WS3CM.js.map +0 -1
  102. package/dist/dist-Q5VBFWVL.js.map +0 -1
  103. package/dist/generate-SIO4JSKJ.js.map +0 -1
  104. package/dist/start-ZGWW5MOL.js.map +0 -1
  105. package/dist/upgrade-readiness-OHL36FQL.js.map +0 -1
  106. /package/dist/{chunk-S3JBIFKS.js.map → chunk-MG7H3LAW.js.map} +0 -0
  107. /package/dist/{dispatcher-IK3FVMX5.js.map → dispatcher-AQJGI3HU.js.map} +0 -0
  108. /package/dist/{info-B3UOXFUF.js.map → info-VM7EK4JC.js.map} +0 -0
  109. /package/dist/{registry-BB3UYAOG.js.map → registry-QGSNLJKJ.js.map} +0 -0
  110. /package/dist/{schema-MVB2FVK6.js.map → schema-JAKF23GR.js.map} +0 -0
  111. /package/dist/{static-BH6JQNM2.js.map → static-6PZZBY7G.js.map} +0 -0
  112. /package/dist/{vite-plugin-WA3G45YY.js.map → vite-plugin-3EQP2JYM.js.map} +0 -0
@@ -7,9 +7,6 @@ import {
7
7
  createOutboxDispatcher,
8
8
  createQueueClient
9
9
  } from "./chunk-GY5Q27BJ.js";
10
- import {
11
- walkSourceFiles
12
- } from "./chunk-X2VVCJ4V.js";
13
10
 
14
11
  // src/config/load-env.ts
15
12
  import { lstatSync, readFileSync, realpathSync, statSync } from "fs";
@@ -108,6 +105,185 @@ function loadEnv(options = {}) {
108
105
  return result;
109
106
  }
110
107
 
108
+ // src/server/security/csrf-readiness-store.ts
109
+ var CsrfReadinessStore = class _CsrfReadinessStore {
110
+ static MAX_ENTRIES = 1e3;
111
+ entries = /* @__PURE__ */ new Map();
112
+ keyFor(event) {
113
+ return `${event.method}\0${event.path}\0${event.reason}`;
114
+ }
115
+ record(event) {
116
+ const key = this.keyFor(event);
117
+ const now = (/* @__PURE__ */ new Date()).toISOString();
118
+ const existing = this.entries.get(key);
119
+ if (existing) {
120
+ existing.count++;
121
+ existing.lastSeen = now;
122
+ return;
123
+ }
124
+ if (this.entries.size >= _CsrfReadinessStore.MAX_ENTRIES) {
125
+ const firstKey = this.entries.keys().next().value;
126
+ if (firstKey !== void 0) this.entries.delete(firstKey);
127
+ }
128
+ this.entries.set(key, { count: 1, firstSeen: now, lastSeen: now });
129
+ }
130
+ summary() {
131
+ let total = 0;
132
+ const routes = [];
133
+ for (const [key, entry] of this.entries) {
134
+ const [method, path, reason] = key.split("\0");
135
+ routes.push({
136
+ method,
137
+ path,
138
+ reason,
139
+ count: entry.count,
140
+ firstSeen: entry.firstSeen,
141
+ lastSeen: entry.lastSeen
142
+ });
143
+ total += entry.count;
144
+ }
145
+ return {
146
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
147
+ totalEvents: total,
148
+ routes
149
+ };
150
+ }
151
+ reset() {
152
+ this.entries.clear();
153
+ }
154
+ };
155
+
156
+ // src/core/contracts/action-protocol.ts
157
+ var CODE_TO_STATUS = {
158
+ VALIDATION_ERROR: 422,
159
+ BAD_REQUEST: 400,
160
+ UNAUTHORIZED: 401,
161
+ FORBIDDEN: 403,
162
+ NOT_FOUND: 404,
163
+ METHOD_NOT_ALLOWED: 405,
164
+ CONFLICT: 409,
165
+ CONTENT_TOO_LARGE: 413,
166
+ PAYLOAD_TOO_LARGE: 413,
167
+ UNSUPPORTED_MEDIA_TYPE: 415,
168
+ TOO_MANY_REQUESTS: 429,
169
+ INTERNAL_SERVER_ERROR: 500
170
+ };
171
+ var STATUS_TO_CODE = {
172
+ 400: "BAD_REQUEST",
173
+ 401: "UNAUTHORIZED",
174
+ 403: "FORBIDDEN",
175
+ 404: "NOT_FOUND",
176
+ 405: "METHOD_NOT_ALLOWED",
177
+ 409: "CONFLICT",
178
+ 413: "PAYLOAD_TOO_LARGE",
179
+ 415: "UNSUPPORTED_MEDIA_TYPE",
180
+ 422: "VALIDATION_ERROR",
181
+ 429: "TOO_MANY_REQUESTS",
182
+ 500: "INTERNAL_SERVER_ERROR"
183
+ };
184
+ var ActionError = class _ActionError extends Error {
185
+ // Discriminator widened to the full union so subclasses can narrow to their
186
+ // specific literal (TypeScript would otherwise reject the override). Concrete
187
+ // values are still always exact literals at runtime.
188
+ type = "TheoActionError";
189
+ code;
190
+ status;
191
+ constructor(params) {
192
+ super(params.message ?? params.code);
193
+ this.code = params.code;
194
+ this.status = _ActionError.codeToStatus(params.code);
195
+ if (params.stack) {
196
+ this.stack = params.stack;
197
+ }
198
+ }
199
+ static codeToStatus(code) {
200
+ return CODE_TO_STATUS[code];
201
+ }
202
+ static statusToCode(status) {
203
+ return STATUS_TO_CODE[status] ?? "INTERNAL_SERVER_ERROR";
204
+ }
205
+ /**
206
+ * Parse a serialized error JSON back into the typed class hierarchy.
207
+ * Distinguishes `TheoActionInputError` (with `issues` array) from
208
+ * `TheoActionError` via the `type` discriminator. Falls back to
209
+ * `INTERNAL_SERVER_ERROR` for malformed bodies (non-object, missing
210
+ * `type`, unknown `code`).
211
+ */
212
+ static fromJson(body) {
213
+ if (typeof body !== "object" || body === null) {
214
+ return new _ActionError({ code: "INTERNAL_SERVER_ERROR" });
215
+ }
216
+ const obj = body;
217
+ if (obj.type === "TheoActionInputError" && Array.isArray(obj.issues)) {
218
+ return new ActionInputError(obj.issues);
219
+ }
220
+ if (obj.type === "TheoActionError" && typeof obj.code === "string" && obj.code in CODE_TO_STATUS) {
221
+ return new _ActionError({
222
+ code: obj.code,
223
+ message: typeof obj.message === "string" ? obj.message : void 0
224
+ });
225
+ }
226
+ return new _ActionError({ code: "INTERNAL_SERVER_ERROR" });
227
+ }
228
+ };
229
+ var ActionInputError = class extends ActionError {
230
+ type = "TheoActionInputError";
231
+ issues;
232
+ fields;
233
+ constructor(rawIssues) {
234
+ super({ code: "VALIDATION_ERROR", message: "Validation failed" });
235
+ this.issues = extractUniversalIssues(rawIssues);
236
+ this.fields = buildFieldsMap(this.issues);
237
+ }
238
+ };
239
+ function buildFieldsMap(issues) {
240
+ const fields = {};
241
+ const seen = /* @__PURE__ */ new Set();
242
+ for (const issue of issues) {
243
+ const key = issue.path.length === 0 ? "" : issue.path.join(".");
244
+ const dedupeKey = `${key}\0${issue.message}`;
245
+ if (seen.has(dedupeKey)) continue;
246
+ seen.add(dedupeKey);
247
+ const bucket = fields[key] ?? [];
248
+ bucket.push(issue.message);
249
+ fields[key] = bucket;
250
+ }
251
+ return fields;
252
+ }
253
+ function extractUniversalIssues(raw) {
254
+ if (!Array.isArray(raw)) return [];
255
+ const out = [];
256
+ for (const entry of raw) {
257
+ if (typeof entry !== "object" || entry === null) continue;
258
+ const obj = entry;
259
+ if (!Array.isArray(obj.path)) continue;
260
+ if (typeof obj.message !== "string") continue;
261
+ const path = [];
262
+ let pathValid = true;
263
+ for (const seg of obj.path) {
264
+ if (typeof seg === "string" || typeof seg === "number") {
265
+ path.push(seg);
266
+ } else {
267
+ pathValid = false;
268
+ break;
269
+ }
270
+ }
271
+ if (!pathValid) continue;
272
+ out.push({
273
+ path,
274
+ message: obj.message,
275
+ code: typeof obj.code === "string" ? obj.code : void 0
276
+ });
277
+ }
278
+ return out;
279
+ }
280
+ function isActionError(value) {
281
+ return value instanceof ActionError;
282
+ }
283
+ function isInputError(value) {
284
+ return value instanceof ActionInputError;
285
+ }
286
+
111
287
  // src/server/body-parser.ts
112
288
  import { basename } from "path";
113
289
  import Busboy from "busboy";
@@ -127,6 +303,13 @@ var FileTooLargeError = class extends Error {
127
303
  code = "FILE_TOO_LARGE";
128
304
  status = 413;
129
305
  };
306
+ function stashBodyPreview(req, body) {
307
+ try {
308
+ ;
309
+ req[DEVTOOLS_BODY_PREVIEW] = body;
310
+ } catch {
311
+ }
312
+ }
130
313
  function parseJsonBody(req) {
131
314
  return new Promise((resolve2, reject) => {
132
315
  const chunks = [];
@@ -226,6 +409,7 @@ function parseMultipartBody(req, contentType, options) {
226
409
  req.pipe(bb);
227
410
  });
228
411
  }
412
+ var DEVTOOLS_BODY_PREVIEW = /* @__PURE__ */ Symbol("theo:devtools:bodyPreview");
229
413
  async function parseRequestBody(req, options) {
230
414
  const method = req.method?.toUpperCase() ?? "GET";
231
415
  if (!METHODS_WITH_BODY.has(method)) {
@@ -234,6 +418,7 @@ async function parseRequestBody(req, options) {
234
418
  const contentType = req.headers["content-type"] ?? "";
235
419
  if (contentType.includes("application/json")) {
236
420
  const json = await parseJsonBody(req);
421
+ stashBodyPreview(req, json);
237
422
  return { fields: {}, files: [], json };
238
423
  }
239
424
  if (contentType.includes("multipart/form-data")) {
@@ -243,6 +428,15 @@ async function parseRequestBody(req, options) {
243
428
  maxFieldSize: options?.maxFieldSize ?? DEFAULT_MAX_FIELD_SIZE
244
429
  };
245
430
  const { fields, files } = await parseMultipartBody(req, contentType, limits);
431
+ stashBodyPreview(req, {
432
+ fields,
433
+ files: files.map((f) => ({
434
+ fieldname: f.fieldname,
435
+ filename: f.filename,
436
+ mimeType: f.mimeType,
437
+ size: f.size
438
+ }))
439
+ });
246
440
  return { fields, files };
247
441
  }
248
442
  if (!contentType) {
@@ -255,7 +449,7 @@ async function parseRequestBody(req, options) {
255
449
  var defaultLoggerFn = (log) => {
256
450
  console.log(JSON.stringify(log));
257
451
  };
258
- function logRequest(info, customLogger) {
452
+ function logRequest(info, customLogger, req) {
259
453
  const log = {
260
454
  level: "info",
261
455
  ...info,
@@ -263,13 +457,24 @@ function logRequest(info, customLogger) {
263
457
  };
264
458
  const logger = customLogger ?? defaultLoggerFn;
265
459
  logger(log);
266
- broadcastRequestToDevtools(info);
460
+ broadcastRequestToDevtools(info, req);
267
461
  }
268
462
  function randomRequestId() {
269
463
  return `req-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
270
464
  }
271
- function broadcastRequestToDevtools(info) {
272
- void import("./broadcast-4SX7R5XE.js").then(({ broadcastRequest }) => {
465
+ function broadcastRequestToDevtools(info, req) {
466
+ const headers = req?.headers ? Object.fromEntries(
467
+ Object.entries(req.headers).map(([k, v]) => [
468
+ k,
469
+ Array.isArray(v) ? v.join(", ") : v ?? ""
470
+ ])
471
+ ) : void 0;
472
+ const stashedBody = req ? req[DEVTOOLS_BODY_PREVIEW] : void 0;
473
+ void Promise.all([
474
+ import("./broadcast-4SX7R5XE.js"),
475
+ import("./build-request-body-preview-EOP2Y4F3.js")
476
+ ]).then(([{ broadcastRequest }, { buildRequestBodyPreview }]) => {
477
+ const bodyInfo = buildRequestBodyPreview(stashedBody);
273
478
  broadcastRequest({
274
479
  id: randomRequestId(),
275
480
  traceId: info.requestId,
@@ -277,7 +482,13 @@ function broadcastRequestToDevtools(info) {
277
482
  path: info.url,
278
483
  status: info.status,
279
484
  durationMs: info.duration,
280
- startedAt: Date.now() - info.duration
485
+ startedAt: Date.now() - info.duration,
486
+ ...headers ? { headers } : {},
487
+ ...bodyInfo ? {
488
+ bodyPreview: bodyInfo.preview,
489
+ bodyLength: bodyInfo.length,
490
+ bodyTruncated: bodyInfo.truncated
491
+ } : {}
281
492
  });
282
493
  }).catch(() => {
283
494
  });
@@ -954,6 +1165,90 @@ async function executeRoute(ctx) {
954
1165
  }
955
1166
  }
956
1167
 
1168
+ // src/server/http/form-data-to-object.ts
1169
+ import { z } from "zod";
1170
+ function formDataToObject(formData, schema, prefix = "") {
1171
+ const shape = schema._def.shape();
1172
+ const out = {};
1173
+ for (const [key, rawValidator] of Object.entries(shape)) {
1174
+ const fullKey = prefix + key;
1175
+ const validator = unwrapWrappers(rawValidator);
1176
+ if (validator instanceof z.ZodObject) {
1177
+ const nestedPrefix = `${fullKey}.`;
1178
+ const hasNestedKeys = [...formData.keys()].some((k) => k.startsWith(nestedPrefix));
1179
+ if (hasNestedKeys) {
1180
+ out[key] = formDataToObject(formData, validator, nestedPrefix);
1181
+ continue;
1182
+ }
1183
+ out[key] = unwrapMissingDefault(rawValidator);
1184
+ continue;
1185
+ }
1186
+ if (validator instanceof z.ZodArray) {
1187
+ const values = formData.getAll(fullKey);
1188
+ out[key] = coerceArrayElements(values, validator);
1189
+ continue;
1190
+ }
1191
+ if (validator instanceof z.ZodBoolean) {
1192
+ out[key] = coerceBoolean(formData, fullKey);
1193
+ continue;
1194
+ }
1195
+ if (formData.has(fullKey)) {
1196
+ const raw = formData.get(fullKey);
1197
+ out[key] = coerceScalar(raw, validator);
1198
+ } else {
1199
+ out[key] = unwrapMissingDefault(rawValidator);
1200
+ }
1201
+ }
1202
+ return out;
1203
+ }
1204
+ function unwrapWrappers(validator) {
1205
+ let inner = validator;
1206
+ while (inner instanceof z.ZodOptional || inner instanceof z.ZodNullable || inner instanceof z.ZodDefault) {
1207
+ inner = inner._def.innerType;
1208
+ }
1209
+ return inner;
1210
+ }
1211
+ function unwrapMissingDefault(validator) {
1212
+ let cursor = validator;
1213
+ while (cursor instanceof z.ZodOptional || cursor instanceof z.ZodNullable || cursor instanceof z.ZodDefault) {
1214
+ if (cursor instanceof z.ZodDefault) {
1215
+ const def = cursor._def.defaultValue;
1216
+ return typeof def === "function" ? def() : def;
1217
+ }
1218
+ if (cursor instanceof z.ZodNullable) return null;
1219
+ cursor = cursor._def.innerType;
1220
+ }
1221
+ return void 0;
1222
+ }
1223
+ function coerceScalar(raw, validator) {
1224
+ if (raw === null) return void 0;
1225
+ if (validator instanceof z.ZodNumber) {
1226
+ return typeof raw === "string" ? Number(raw) : raw;
1227
+ }
1228
+ return raw;
1229
+ }
1230
+ function coerceArrayElements(values, arrayValidator) {
1231
+ const elementType = unwrapWrappers(arrayValidator._def.type);
1232
+ if (elementType instanceof z.ZodNumber) {
1233
+ return values.map((v) => typeof v === "string" ? Number(v) : v);
1234
+ }
1235
+ if (elementType instanceof z.ZodBoolean) {
1236
+ return values.map((v) => {
1237
+ if (v === "true") return true;
1238
+ if (v === "false") return false;
1239
+ return Boolean(v);
1240
+ });
1241
+ }
1242
+ return values;
1243
+ }
1244
+ function coerceBoolean(formData, key) {
1245
+ if (!formData.has(key)) return void 0;
1246
+ const val = formData.get(key);
1247
+ if (val === "true") return true;
1248
+ if (val === "false") return false;
1249
+ return Boolean(val);
1250
+ }
1251
+
957
1252
  // src/server/http/handle-request-error.ts
958
1253
  async function handleRequestError(err, c) {
959
1254
  if (c.pluginRunner) {
@@ -999,7 +1294,78 @@ async function handleRequestError(err, c) {
999
1294
  }
1000
1295
  }
1001
1296
 
1297
+ // src/server/http/serialize-action-result.ts
1298
+ import { stringify as devalueStringify } from "devalue";
1299
+ var DEFAULT_RESPONSE_BODY_SIZE_LIMIT = 5 * 1024 * 1024;
1300
+ function serializeActionResult(result, options = {}) {
1301
+ const limit = options.responseBodySizeLimit ?? DEFAULT_RESPONSE_BODY_SIZE_LIMIT;
1302
+ if (result.error !== void 0) {
1303
+ const body2 = result.error instanceof ActionInputError ? JSON.stringify({
1304
+ type: result.error.type,
1305
+ code: result.error.code,
1306
+ message: result.error.message,
1307
+ issues: result.error.issues,
1308
+ fields: result.error.fields
1309
+ }) : JSON.stringify({
1310
+ type: result.error.type,
1311
+ code: result.error.code,
1312
+ message: result.error.message
1313
+ });
1314
+ if (Buffer.byteLength(body2, "utf8") > limit) {
1315
+ throw new ActionError({
1316
+ code: "PAYLOAD_TOO_LARGE",
1317
+ message: `Serialized error body exceeds limit (${limit} bytes)`
1318
+ });
1319
+ }
1320
+ return {
1321
+ type: "error",
1322
+ status: result.error.status,
1323
+ contentType: "application/json",
1324
+ body: body2
1325
+ };
1326
+ }
1327
+ if (result.data === void 0) {
1328
+ return { type: "empty", status: 204 };
1329
+ }
1330
+ if (result.data instanceof Response) {
1331
+ throw new ActionError({
1332
+ code: "INTERNAL_SERVER_ERROR",
1333
+ message: "Action handler cannot serialize Response objects \u2014 return plain data instead"
1334
+ });
1335
+ }
1336
+ let body;
1337
+ try {
1338
+ body = devalueStringify(result.data, {
1339
+ URL: (value) => value instanceof URL && value.href
1340
+ });
1341
+ } catch (e) {
1342
+ throw new ActionError({
1343
+ code: "INTERNAL_SERVER_ERROR",
1344
+ message: `Action data not serializable: ${e instanceof Error ? e.message : String(e)}`
1345
+ });
1346
+ }
1347
+ if (Buffer.byteLength(body, "utf8") > limit) {
1348
+ throw new ActionError({
1349
+ code: "PAYLOAD_TOO_LARGE",
1350
+ message: `Serialized response body exceeds limit (${limit} bytes)`
1351
+ });
1352
+ }
1353
+ return {
1354
+ type: "data",
1355
+ status: 200,
1356
+ contentType: "application/json+devalue",
1357
+ body
1358
+ };
1359
+ }
1360
+
1002
1361
  // src/server/http/action-execute.ts
1362
+ var __IS_DEV = (() => {
1363
+ try {
1364
+ return import.meta.env?.DEV === true;
1365
+ } catch {
1366
+ return process.env.NODE_ENV !== "production";
1367
+ }
1368
+ })();
1003
1369
  function isActionConfig(value) {
1004
1370
  if (typeof value !== "object" || value === null) return false;
1005
1371
  const candidate = value;
@@ -1066,16 +1432,72 @@ async function runPreHandlerPipeline(p) {
1066
1432
  }
1067
1433
  return true;
1068
1434
  }
1069
- async function readActionBody(req, res, requestId) {
1435
+ async function readActionBody(req, res, requestId, actionConfig) {
1070
1436
  try {
1071
1437
  const parsed = await parseRequestBody(req);
1072
- const body = parsed.json !== void 0 ? parsed.json : parsed.fields;
1438
+ const accept = actionConfig.accept ?? "json";
1439
+ let body;
1440
+ if (accept === "form") {
1441
+ body = formDataToObject(
1442
+ synthesizeFormData(parsed),
1443
+ actionConfig.input
1444
+ );
1445
+ } else if (parsed.json !== void 0) {
1446
+ body = parsed.json;
1447
+ } else {
1448
+ body = parsed.fields;
1449
+ }
1073
1450
  return { ok: true, body };
1074
1451
  } catch (err) {
1075
1452
  sendError(res, "VALIDATION_ERROR", err.message, 400, void 0, requestId);
1076
1453
  return { ok: false };
1077
1454
  }
1078
1455
  }
1456
+ function synthesizeFormData(parsed) {
1457
+ const fd = new FormData();
1458
+ for (const [name, value] of Object.entries(parsed.fields)) {
1459
+ fd.append(name, value);
1460
+ }
1461
+ for (const file of parsed.files) {
1462
+ const blob = new Blob([file.buffer], {
1463
+ type: file.mimeType
1464
+ });
1465
+ fd.append(file.fieldname, blob, file.filename);
1466
+ }
1467
+ return fd;
1468
+ }
1469
+ function writeSerialized(res, serialized) {
1470
+ if (serialized.type === "empty") {
1471
+ res.statusCode = serialized.status;
1472
+ res.end();
1473
+ return;
1474
+ }
1475
+ res.statusCode = serialized.status;
1476
+ res.setHeader("Content-Type", serialized.contentType);
1477
+ res.end(serialized.body);
1478
+ }
1479
+ async function emitActionCallTelemetry(name, startedAt, input, outcome) {
1480
+ if (!__IS_DEV) return;
1481
+ try {
1482
+ const mod = await import("./dispatcher-AQJGI3HU.js");
1483
+ mod.dispatcher.onActionCall({
1484
+ // eslint-disable-next-line sonarjs/pseudo-random -- non-secret correlation id
1485
+ id: `act-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`,
1486
+ timestamp: startedAt,
1487
+ name,
1488
+ input,
1489
+ output: outcome.status === "success" ? outcome.output : void 0,
1490
+ error: outcome.status === "error" ? {
1491
+ code: outcome.error.code,
1492
+ message: outcome.error.message,
1493
+ fields: outcome.error instanceof ActionInputError ? outcome.error.fields : void 0
1494
+ } : void 0,
1495
+ durationMs: Date.now() - startedAt,
1496
+ status: outcome.status
1497
+ });
1498
+ } catch {
1499
+ }
1500
+ }
1079
1501
  async function executeActionWithOptions(opts) {
1080
1502
  const {
1081
1503
  filePath,
@@ -1122,22 +1544,24 @@ async function executeActionWithOptions(opts) {
1122
1544
  };
1123
1545
  if (!await runPreHandlerPipeline(pipeline)) return;
1124
1546
  ctx = pipeline.ctx;
1125
- const bodyOutcome = await readActionBody(req, res, requestId);
1547
+ const bodyOutcome = await readActionBody(req, res, requestId, actionConfig);
1126
1548
  if (!bodyOutcome.ok) return;
1549
+ const actionName = exportName === "default" ? deriveActionNameFromPath(filePath) : exportName;
1550
+ const startedAt = Date.now();
1127
1551
  const result = actionConfig.input.safeParse(bodyOutcome.body);
1128
1552
  if (!result.success) {
1129
- sendError(
1130
- res,
1131
- "VALIDATION_ERROR",
1132
- "Invalid action input",
1133
- 400,
1134
- result.error?.issues,
1135
- requestId
1136
- );
1553
+ const inputErr = new ActionInputError(result.error?.issues ?? []);
1554
+ writeSerialized(res, serializeActionResult({ data: void 0, error: inputErr }));
1555
+ await emitActionCallTelemetry(actionName, startedAt, bodyOutcome.body, {
1556
+ status: "error",
1557
+ error: inputErr
1558
+ });
1137
1559
  return;
1138
1560
  }
1139
1561
  await runActionHandler({
1140
1562
  actionConfig,
1563
+ actionName,
1564
+ startedAt,
1141
1565
  input: result.data,
1142
1566
  ctx,
1143
1567
  res,
@@ -1148,12 +1572,44 @@ async function executeActionWithOptions(opts) {
1148
1572
  await handleActionError(err, { req, res, ctx, requestId, pluginRunner, buildPluginCtx });
1149
1573
  }
1150
1574
  }
1575
+ function deriveActionNameFromPath(filePath) {
1576
+ const base = filePath.split(/[\\/]/).pop() ?? "unknown";
1577
+ return base.replace(/\.[jt]sx?$/, "");
1578
+ }
1579
+ function isActionErrorLike(err) {
1580
+ if (err === null || typeof err !== "object") return false;
1581
+ const obj = err;
1582
+ return (obj.type === "TheoActionError" || obj.type === "TheoActionInputError") && typeof obj.code === "string" && typeof obj.status === "number";
1583
+ }
1151
1584
  async function runActionHandler(args) {
1152
- const handlerResult = await args.actionConfig.handler({ input: args.input, ctx: args.ctx });
1153
- const status = handlerResult === void 0 || handlerResult === null ? 204 : 200;
1154
- sendJson(args.res, handlerResult ?? null, status);
1155
- if (args.pluginRunner) {
1156
- await args.pluginRunner.runOnResponse(args.buildPluginCtx(args.ctx));
1585
+ try {
1586
+ const handlerResult = await args.actionConfig.handler({ input: args.input, ctx: args.ctx });
1587
+ writeSerialized(args.res, serializeActionResult({ data: handlerResult, error: void 0 }));
1588
+ if (args.pluginRunner) {
1589
+ await args.pluginRunner.runOnResponse(args.buildPluginCtx(args.ctx));
1590
+ }
1591
+ await emitActionCallTelemetry(args.actionName, args.startedAt, args.input, {
1592
+ status: "success",
1593
+ output: handlerResult
1594
+ });
1595
+ } catch (err) {
1596
+ if (isActionErrorLike(err)) {
1597
+ writeSerialized(args.res, serializeActionResult({ data: void 0, error: err }));
1598
+ await emitActionCallTelemetry(args.actionName, args.startedAt, args.input, {
1599
+ status: "error",
1600
+ error: err
1601
+ });
1602
+ return;
1603
+ }
1604
+ const wrapped = new ActionError({
1605
+ code: "INTERNAL_SERVER_ERROR",
1606
+ message: err instanceof Error ? err.message : "Action handler threw"
1607
+ });
1608
+ await emitActionCallTelemetry(args.actionName, args.startedAt, args.input, {
1609
+ status: "error",
1610
+ error: wrapped
1611
+ });
1612
+ throw err;
1157
1613
  }
1158
1614
  }
1159
1615
  async function handleActionError(err, c) {
@@ -1197,28 +1653,6 @@ function findSuggestion(input, candidates, maxDistance = 3) {
1197
1653
  return best;
1198
1654
  }
1199
1655
 
1200
- // src/server/scan/action-scan.ts
1201
- import { existsSync as existsSync3, statSync as statSync3 } from "fs";
1202
- import { extname as extname2, join as join3, relative } from "path";
1203
- var ACTION_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
1204
- function scanServerActions(serverDir) {
1205
- const actionsDir = join3(serverDir, "actions");
1206
- if (!existsSync3(actionsDir) || !statSync3(actionsDir).isDirectory()) {
1207
- return [];
1208
- }
1209
- const results = [];
1210
- walkSourceFiles(actionsDir, { extensions: ACTION_EXTENSIONS }, (absPath) => {
1211
- let rel = relative(actionsDir, absPath);
1212
- rel = rel.replace(/\\/g, "/");
1213
- rel = rel.slice(0, -extname2(rel).length);
1214
- results.push({
1215
- filePath: absPath,
1216
- actionPath: rel
1217
- });
1218
- });
1219
- return results;
1220
- }
1221
-
1222
1656
  // src/server/scan/module-loader.ts
1223
1657
  import { pathToFileURL } from "url";
1224
1658
  function createViteLoader(vite) {
@@ -1232,7 +1666,7 @@ function createProductionLoader() {
1232
1666
  }
1233
1667
 
1234
1668
  // src/server/http/batch-handler.ts
1235
- import { z } from "zod";
1669
+ import { z as z2 } from "zod";
1236
1670
  var STRIPPED_HEADERS = [
1237
1671
  "authorization",
1238
1672
  "cookie",
@@ -1252,15 +1686,15 @@ var BatchPathConflictError = class extends Error {
1252
1686
  this.name = "BatchPathConflictError";
1253
1687
  }
1254
1688
  };
1255
- var batchRequestSchema = z.object({
1256
- path: z.string().min(1),
1257
- method: z.string().min(1),
1258
- query: z.record(z.unknown()).optional(),
1259
- body: z.unknown().optional(),
1260
- headers: z.record(z.string()).optional()
1689
+ var batchRequestSchema = z2.object({
1690
+ path: z2.string().min(1),
1691
+ method: z2.string().min(1),
1692
+ query: z2.record(z2.unknown()).optional(),
1693
+ body: z2.unknown().optional(),
1694
+ headers: z2.record(z2.string()).optional()
1261
1695
  });
1262
- var batchPayloadSchema = z.object({
1263
- requests: z.array(batchRequestSchema).min(1)
1696
+ var batchPayloadSchema = z2.object({
1697
+ requests: z2.array(batchRequestSchema).min(1)
1264
1698
  });
1265
1699
  function sanitizeItemHeaders(itemHeaders, outerHeaders) {
1266
1700
  const out = {};
@@ -1535,88 +1969,6 @@ function resultFromState(state, config) {
1535
1969
  };
1536
1970
  }
1537
1971
 
1538
- // src/server/scan/match.ts
1539
- function compilePattern(routePath) {
1540
- const paramNames = [];
1541
- const regexStr = routePath.replace(/:(?:\.\.\.)?([^/]+)/g, (match, name) => {
1542
- paramNames.push(name);
1543
- return match.startsWith(":...") ? "(.+)" : "([^/]+)";
1544
- });
1545
- return { pattern: new RegExp(`^${regexStr}$`), paramNames };
1546
- }
1547
- function matchRoute(url, routes) {
1548
- let path = url.split("?")[0];
1549
- if (path.length > 1 && path.endsWith("/")) {
1550
- path = path.slice(0, -1);
1551
- }
1552
- for (const route of routes) {
1553
- const match = route.pattern.exec(path);
1554
- if (match) {
1555
- const params = {};
1556
- route.paramNames.forEach((name, i) => {
1557
- params[name] = match[i + 1];
1558
- });
1559
- return { route, params };
1560
- }
1561
- }
1562
- return null;
1563
- }
1564
-
1565
- // src/server/scan/scan.ts
1566
- import { existsSync as existsSync4, statSync as statSync4 } from "fs";
1567
- import { extname as extname3, join as join4, relative as relative2 } from "path";
1568
- var ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
1569
- function fileToRoutePath(filePath, routesDir) {
1570
- let rel = relative2(routesDir, filePath);
1571
- const ext = extname3(rel);
1572
- rel = rel.slice(0, -ext.length);
1573
- rel = rel.replace(/\\/g, "/");
1574
- if (rel.endsWith("/index")) {
1575
- rel = rel.slice(0, -6);
1576
- } else if (rel === "index") {
1577
- rel = "";
1578
- }
1579
- rel = rel.replace(/\[\.\.\.([^\]]+)\]/g, ":...$1");
1580
- rel = rel.replace(/\[([^\]]+)\]/g, ":$1");
1581
- return `/api/${rel}`;
1582
- }
1583
- function scanServerRoutes(serverDir) {
1584
- const routesDir = join4(serverDir, "routes");
1585
- if (!existsSync4(routesDir) || !statSync4(routesDir).isDirectory()) {
1586
- return [];
1587
- }
1588
- const results = [];
1589
- walkSourceFiles(routesDir, { extensions: ROUTE_EXTENSIONS }, (absPath) => {
1590
- const routePath = fileToRoutePath(absPath, routesDir);
1591
- const { pattern, paramNames } = compilePattern(routePath);
1592
- results.push({
1593
- filePath: absPath,
1594
- routePath,
1595
- paramNames,
1596
- pattern
1597
- });
1598
- });
1599
- const conflicting = results.find((r) => r.routePath === "/api/__theo_batch__");
1600
- if (conflicting) {
1601
- throw new Error(
1602
- `Server route ${conflicting.filePath} resolves to '/api/__theo_batch__' which is reserved for the batch endpoint. Rename the route or disable batching in theo.config.ts.`
1603
- );
1604
- }
1605
- const isCatchAll = (route) => route.routePath.includes(":...");
1606
- results.sort((a, b) => {
1607
- const aStatic = a.paramNames.length === 0;
1608
- const bStatic = b.paramNames.length === 0;
1609
- const aCatchAll = isCatchAll(a);
1610
- const bCatchAll = isCatchAll(b);
1611
- if (aStatic && !bStatic) return -1;
1612
- if (!aStatic && bStatic) return 1;
1613
- if (aCatchAll && !bCatchAll) return 1;
1614
- if (!aCatchAll && bCatchAll) return -1;
1615
- return a.routePath.localeCompare(b.routePath);
1616
- });
1617
- return results;
1618
- }
1619
-
1620
1972
  // src/server/security/csp-report.ts
1621
1973
  var CSP_REPORT_PATH = "/__theo/csp-report";
1622
1974
  var MAX_BODY = 16 * 1024;
@@ -2029,33 +2381,15 @@ function resolveTransformer(selector) {
2029
2381
  return selector;
2030
2382
  }
2031
2383
 
2032
- // src/server/scan/ws-scan.ts
2033
- import { existsSync as existsSync5, statSync as statSync5 } from "fs";
2034
- import { extname as extname4, join as join5, relative as relative3 } from "path";
2035
- var WS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
2036
- function scanWebSocketRoutes(serverDir) {
2037
- const wsDir = join5(serverDir, "ws");
2038
- if (!existsSync5(wsDir) || !statSync5(wsDir).isDirectory()) {
2039
- return [];
2040
- }
2041
- const results = [];
2042
- walkSourceFiles(wsDir, { extensions: WS_EXTENSIONS }, (absPath) => {
2043
- let rel = relative3(wsDir, absPath);
2044
- rel = rel.replace(/\\/g, "/");
2045
- rel = rel.slice(0, -extname4(rel).length);
2046
- if (rel.endsWith("/index")) rel = rel.slice(0, -6);
2047
- else if (rel === "index") rel = "";
2048
- results.push({
2049
- filePath: absPath,
2050
- wsPath: `/ws/${rel}`
2051
- });
2052
- });
2053
- return results;
2054
- }
2055
-
2056
2384
  export {
2057
2385
  _resetEnvCache,
2058
2386
  loadEnv,
2387
+ CsrfReadinessStore,
2388
+ ActionError,
2389
+ ActionInputError,
2390
+ extractUniversalIssues,
2391
+ isActionError,
2392
+ isInputError,
2059
2393
  FileTooLargeError,
2060
2394
  parseRequestBody,
2061
2395
  logRequest,
@@ -2079,7 +2413,6 @@ export {
2079
2413
  executeAction,
2080
2414
  levenshtein,
2081
2415
  findSuggestion,
2082
- scanServerActions,
2083
2416
  createViteLoader,
2084
2417
  createProductionLoader,
2085
2418
  STRIPPED_HEADERS,
@@ -2094,9 +2427,6 @@ export {
2094
2427
  extractTraceId,
2095
2428
  InMemoryStore,
2096
2429
  createRateLimiter,
2097
- compilePattern,
2098
- matchRoute,
2099
- scanServerRoutes,
2100
2430
  CSP_REPORT_PATH,
2101
2431
  normalizeLegacy,
2102
2432
  normalizeNew,
@@ -2115,7 +2445,6 @@ export {
2115
2445
  createPluginRunnerFromConfig,
2116
2446
  superjsonTransformer,
2117
2447
  jsonTransformer,
2118
- resolveTransformer,
2119
- scanWebSocketRoutes
2448
+ resolveTransformer
2120
2449
  };
2121
- //# sourceMappingURL=chunk-OEB3OIYP.js.map
2450
+ //# sourceMappingURL=chunk-OJHO2X7C.js.map