@beignet/core 0.0.34 → 0.0.36

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 (120) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +145 -15
  3. package/dist/agent-capabilities/index.d.ts +197 -0
  4. package/dist/agent-capabilities/index.d.ts.map +1 -0
  5. package/dist/agent-capabilities/index.js +282 -0
  6. package/dist/agent-capabilities/index.js.map +1 -0
  7. package/dist/application/index.d.ts +2 -2
  8. package/dist/application/index.d.ts.map +1 -1
  9. package/dist/application/index.js +13 -3
  10. package/dist/application/index.js.map +1 -1
  11. package/dist/client/client.d.ts +2 -0
  12. package/dist/client/client.d.ts.map +1 -1
  13. package/dist/client/client.js +33 -16
  14. package/dist/client/client.js.map +1 -1
  15. package/dist/contracts/contract-builder.d.ts +5 -3
  16. package/dist/contracts/contract-builder.d.ts.map +1 -1
  17. package/dist/contracts/contract-builder.js +3 -12
  18. package/dist/contracts/contract-builder.js.map +1 -1
  19. package/dist/contracts/contract-group.d.ts +2 -2
  20. package/dist/contracts/contract-group.d.ts.map +1 -1
  21. package/dist/contracts/contract-group.js +2 -1
  22. package/dist/contracts/contract-group.js.map +1 -1
  23. package/dist/contracts/metadata.d.ts +3 -0
  24. package/dist/contracts/metadata.d.ts.map +1 -0
  25. package/dist/contracts/metadata.js +14 -0
  26. package/dist/contracts/metadata.js.map +1 -0
  27. package/dist/contracts/types.d.ts +20 -0
  28. package/dist/contracts/types.d.ts.map +1 -1
  29. package/dist/contracts/types.js.map +1 -1
  30. package/dist/events/index.d.ts +4 -4
  31. package/dist/events/index.d.ts.map +1 -1
  32. package/dist/events/index.js +5 -5
  33. package/dist/events/index.js.map +1 -1
  34. package/dist/idempotency/index.d.ts.map +1 -1
  35. package/dist/idempotency/index.js +14 -9
  36. package/dist/idempotency/index.js.map +1 -1
  37. package/dist/jobs/index.d.ts.map +1 -1
  38. package/dist/jobs/index.js +10 -4
  39. package/dist/jobs/index.js.map +1 -1
  40. package/dist/openapi/index.js +15 -8
  41. package/dist/openapi/index.js.map +1 -1
  42. package/dist/ports/auth.d.ts +2 -0
  43. package/dist/ports/auth.d.ts.map +1 -1
  44. package/dist/ports/auth.js +2 -0
  45. package/dist/ports/auth.js.map +1 -1
  46. package/dist/providers/index.d.ts +1 -1
  47. package/dist/providers/index.d.ts.map +1 -1
  48. package/dist/providers/index.js.map +1 -1
  49. package/dist/providers/provider.d.ts +9 -1
  50. package/dist/providers/provider.d.ts.map +1 -1
  51. package/dist/providers/provider.js.map +1 -1
  52. package/dist/schedules/index.d.ts +6 -13
  53. package/dist/schedules/index.d.ts.map +1 -1
  54. package/dist/schedules/index.js +24 -44
  55. package/dist/schedules/index.js.map +1 -1
  56. package/dist/server/index.d.ts +2 -2
  57. package/dist/server/index.d.ts.map +1 -1
  58. package/dist/server/index.js +1 -1
  59. package/dist/server/index.js.map +1 -1
  60. package/dist/server/request-executor.d.ts +72 -0
  61. package/dist/server/request-executor.d.ts.map +1 -0
  62. package/dist/server/request-executor.js +781 -0
  63. package/dist/server/request-executor.js.map +1 -0
  64. package/dist/server/request-preparation.d.ts +39 -0
  65. package/dist/server/request-preparation.d.ts.map +1 -0
  66. package/dist/server/request-preparation.js +248 -0
  67. package/dist/server/request-preparation.js.map +1 -0
  68. package/dist/server/response-finalization.d.ts +34 -0
  69. package/dist/server/response-finalization.d.ts.map +1 -0
  70. package/dist/server/response-finalization.js +279 -0
  71. package/dist/server/response-finalization.js.map +1 -0
  72. package/dist/server/route-definitions.d.ts +139 -0
  73. package/dist/server/route-definitions.d.ts.map +1 -0
  74. package/dist/server/route-definitions.js +94 -0
  75. package/dist/server/route-definitions.js.map +1 -0
  76. package/dist/server/route-matching.d.ts +15 -0
  77. package/dist/server/route-matching.d.ts.map +1 -0
  78. package/dist/server/route-matching.js +46 -0
  79. package/dist/server/route-matching.js.map +1 -0
  80. package/dist/server/server.d.ts +6 -147
  81. package/dist/server/server.d.ts.map +1 -1
  82. package/dist/server/server.js +29 -1350
  83. package/dist/server/server.js.map +1 -1
  84. package/dist/server/use-case-route.d.ts +1 -1
  85. package/dist/tracing/index.d.ts +1 -1
  86. package/dist/tracing/index.d.ts.map +1 -1
  87. package/dist/tracing/index.js.map +1 -1
  88. package/dist/uploads/client.js +4 -7
  89. package/dist/uploads/client.js.map +1 -1
  90. package/dist/uploads/index.d.ts.map +1 -1
  91. package/dist/uploads/index.js +59 -12
  92. package/dist/uploads/index.js.map +1 -1
  93. package/package.json +5 -1
  94. package/skills/app-architecture/SKILL.md +27 -1
  95. package/src/agent-capabilities/index.ts +643 -0
  96. package/src/application/index.ts +19 -5
  97. package/src/client/client.ts +41 -17
  98. package/src/contracts/contract-builder.ts +8 -16
  99. package/src/contracts/contract-group.ts +4 -6
  100. package/src/contracts/metadata.ts +20 -0
  101. package/src/contracts/types.ts +39 -0
  102. package/src/events/index.ts +16 -20
  103. package/src/idempotency/index.ts +17 -9
  104. package/src/jobs/index.ts +8 -3
  105. package/src/openapi/index.ts +24 -12
  106. package/src/ports/auth.ts +2 -0
  107. package/src/providers/index.ts +1 -0
  108. package/src/providers/provider.ts +17 -2
  109. package/src/schedules/index.ts +57 -63
  110. package/src/server/index.ts +4 -2
  111. package/src/server/request-executor.ts +1170 -0
  112. package/src/server/request-preparation.ts +409 -0
  113. package/src/server/response-finalization.ts +385 -0
  114. package/src/server/route-definitions.ts +338 -0
  115. package/src/server/route-matching.ts +64 -0
  116. package/src/server/server.ts +63 -2139
  117. package/src/server/use-case-route.ts +1 -1
  118. package/src/tracing/index.ts +1 -0
  119. package/src/uploads/client.ts +4 -7
  120. package/src/uploads/index.ts +70 -12
@@ -0,0 +1,409 @@
1
+ import {
2
+ getContractHeaderSchemas,
3
+ type HttpContractConfig,
4
+ methodSupportsRequestBody,
5
+ type StandardSchema,
6
+ } from "../contracts/index.js";
7
+ import type { HttpRequestLike, HttpResponseLike } from "./http.js";
8
+ import {
9
+ parseStandardSchema,
10
+ SchemaValidationError,
11
+ } from "./providers/index.js";
12
+ import { errorResponse } from "./response-finalization.js";
13
+
14
+ /**
15
+ * Request body limits enforced before contract body validation.
16
+ */
17
+ export interface RequestBodyOptions {
18
+ /**
19
+ * Maximum request body size in bytes for JSON/text route bodies.
20
+ *
21
+ * @default 1048576
22
+ */
23
+ maxBytes?: number;
24
+ }
25
+
26
+ export type PreparedRequestInputs = {
27
+ path: unknown;
28
+ query: unknown;
29
+ headers: unknown;
30
+ body: unknown;
31
+ rawHeaders: Record<string, string>;
32
+ };
33
+
34
+ export type RequestPreparationResult =
35
+ | { ok: true; inputs: PreparedRequestInputs }
36
+ | { ok: false; response: HttpResponseLike };
37
+
38
+ type RequestValidationLocation = "query" | "path" | "headers" | "body";
39
+
40
+ const DEFAULT_REQUEST_BODY_MAX_BYTES = 1024 * 1024;
41
+
42
+ class RequestBodyTooLargeError extends Error {
43
+ readonly maxBytes: number;
44
+ readonly actualBytes?: number;
45
+
46
+ constructor(maxBytes: number, actualBytes?: number) {
47
+ super("Request body exceeds the configured size limit.");
48
+ this.name = "RequestBodyTooLargeError";
49
+ this.maxBytes = maxBytes;
50
+ this.actualBytes = actualBytes;
51
+ }
52
+ }
53
+
54
+ function contractDiagnostics(contract: HttpContractConfig) {
55
+ return {
56
+ contract: contract.name,
57
+ method: contract.method,
58
+ path: contract.path,
59
+ };
60
+ }
61
+
62
+ function requestValidationDetails(
63
+ contract: HttpContractConfig,
64
+ location: RequestValidationLocation,
65
+ error?: unknown,
66
+ additionalDetails?: Record<string, unknown>,
67
+ ) {
68
+ const details = {
69
+ ...contractDiagnostics(contract),
70
+ location,
71
+ ...additionalDetails,
72
+ };
73
+
74
+ if (error instanceof SchemaValidationError) {
75
+ return {
76
+ ...details,
77
+ issues: error.issues,
78
+ };
79
+ }
80
+
81
+ if (error instanceof Error) {
82
+ return {
83
+ ...details,
84
+ message: error.message,
85
+ };
86
+ }
87
+
88
+ return details;
89
+ }
90
+
91
+ function requestValidationError(
92
+ contract: HttpContractConfig,
93
+ status: number,
94
+ code: string,
95
+ message: string,
96
+ location: RequestValidationLocation,
97
+ error?: unknown,
98
+ additionalDetails?: Record<string, unknown>,
99
+ ): HttpResponseLike {
100
+ return errorResponse(
101
+ status,
102
+ code,
103
+ message,
104
+ requestValidationDetails(contract, location, error, additionalDetails),
105
+ );
106
+ }
107
+
108
+ function missingJsonContentTypeHint(
109
+ req: HttpRequestLike,
110
+ body: unknown,
111
+ ): Record<string, unknown> | undefined {
112
+ if (req.headers.get("content-type") || typeof body !== "string") {
113
+ return undefined;
114
+ }
115
+
116
+ const trimmed = body.trim();
117
+ if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) {
118
+ return undefined;
119
+ }
120
+
121
+ try {
122
+ const parsed = JSON.parse(trimmed);
123
+ if (typeof parsed !== "object" || parsed === null) return undefined;
124
+ } catch {
125
+ return undefined;
126
+ }
127
+
128
+ return {
129
+ hint: 'The request body looks like JSON. Set "Content-Type: application/json" to parse it as JSON.',
130
+ };
131
+ }
132
+
133
+ export function requestHeadersToRecord(
134
+ headers: Headers,
135
+ ): Record<string, string> {
136
+ const record: Record<string, string> = {};
137
+ headers.forEach((value, key) => {
138
+ record[key.toLowerCase()] = value;
139
+ });
140
+ return record;
141
+ }
142
+
143
+ async function parseHeaderSchemas(
144
+ schemas: readonly StandardSchema[],
145
+ rawHeaders: Record<string, string>,
146
+ ): Promise<Record<string, unknown>> {
147
+ let parsedHeaders: Record<string, unknown> = rawHeaders;
148
+
149
+ for (const schema of schemas) {
150
+ const parsed = await parseStandardSchema(schema, rawHeaders);
151
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
152
+ parsedHeaders = {
153
+ ...parsedHeaders,
154
+ ...(parsed as Record<string, unknown>),
155
+ };
156
+ } else {
157
+ parsedHeaders = parsed as Record<string, unknown>;
158
+ }
159
+ }
160
+
161
+ return parsedHeaders;
162
+ }
163
+
164
+ export function requestBodyLimit(
165
+ options: RequestBodyOptions | undefined,
166
+ ): number {
167
+ const maxBytes = options?.maxBytes ?? DEFAULT_REQUEST_BODY_MAX_BYTES;
168
+
169
+ if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
170
+ throw new Error(
171
+ "createServer requestBody.maxBytes must be a positive number.",
172
+ );
173
+ }
174
+
175
+ return maxBytes;
176
+ }
177
+
178
+ function assertContentLengthWithinLimit(
179
+ headers: Headers,
180
+ maxBytes: number,
181
+ ): void {
182
+ const contentLength = headers.get("content-length");
183
+ if (contentLength === null) return;
184
+
185
+ const actualBytes = Number(contentLength);
186
+ if (!Number.isFinite(actualBytes) || actualBytes < 0) return;
187
+ if (actualBytes > maxBytes) {
188
+ throw new RequestBodyTooLargeError(maxBytes, actualBytes);
189
+ }
190
+ }
191
+
192
+ async function readLimitedRequestText(
193
+ req: HttpRequestLike,
194
+ maxBytes: number,
195
+ ): Promise<string> {
196
+ assertContentLengthWithinLimit(req.headers, maxBytes);
197
+
198
+ const body = req.raw?.body;
199
+ if (!body) {
200
+ const text = await req.text();
201
+ const actualBytes = new TextEncoder().encode(text).byteLength;
202
+ if (actualBytes > maxBytes) {
203
+ throw new RequestBodyTooLargeError(maxBytes, actualBytes);
204
+ }
205
+ return text;
206
+ }
207
+
208
+ const reader = body.getReader();
209
+ const decoder = new TextDecoder();
210
+ let received = 0;
211
+ let text = "";
212
+
213
+ try {
214
+ while (true) {
215
+ const result = await reader.read();
216
+ if (result.done) break;
217
+ received += result.value.byteLength;
218
+ if (received > maxBytes) {
219
+ throw new RequestBodyTooLargeError(maxBytes, received);
220
+ }
221
+ text += decoder.decode(result.value, { stream: true });
222
+ }
223
+ text += decoder.decode();
224
+ } finally {
225
+ reader.releaseLock();
226
+ }
227
+
228
+ return text;
229
+ }
230
+
231
+ async function parseBody(
232
+ req: HttpRequestLike,
233
+ maxBytes: number,
234
+ ): Promise<unknown> {
235
+ const method = req.method.toUpperCase() as HttpContractConfig["method"];
236
+ if (!methodSupportsRequestBody(method)) {
237
+ return undefined;
238
+ }
239
+
240
+ const bodyReq = req.clone?.() ?? req;
241
+ const contentType = req.headers.get("content-type") || "";
242
+ if (contentType.includes("application/json")) {
243
+ const text = await readLimitedRequestText(bodyReq, maxBytes);
244
+ if (text === "") return undefined;
245
+ return JSON.parse(text);
246
+ }
247
+
248
+ try {
249
+ const text = await readLimitedRequestText(bodyReq, maxBytes);
250
+ return text === "" ? undefined : text;
251
+ } catch (error) {
252
+ if (error instanceof RequestBodyTooLargeError) throw error;
253
+ return undefined;
254
+ }
255
+ }
256
+
257
+ export async function prepareRequestInputs(args: {
258
+ contract: HttpContractConfig;
259
+ req: HttpRequestLike;
260
+ url: URL;
261
+ rawHeaders: Record<string, string>;
262
+ matchedParams: Record<string, string>;
263
+ maxRequestBodyBytes: number;
264
+ rawRoute?: boolean;
265
+ }): Promise<RequestPreparationResult> {
266
+ const {
267
+ contract,
268
+ req,
269
+ url,
270
+ rawHeaders,
271
+ matchedParams,
272
+ maxRequestBodyBytes,
273
+ rawRoute,
274
+ } = args;
275
+ const rawQuery: Record<string, string | string[]> = {};
276
+ for (const key of new Set(url.searchParams.keys())) {
277
+ const values = url.searchParams.getAll(key);
278
+ rawQuery[key] = values.length === 1 ? values[0] : values;
279
+ }
280
+
281
+ let query: unknown = rawQuery;
282
+ if (contract.query) {
283
+ try {
284
+ query = await parseStandardSchema(contract.query, query);
285
+ } catch (error) {
286
+ return {
287
+ ok: false,
288
+ response: requestValidationError(
289
+ contract,
290
+ 422,
291
+ "VALIDATION_ERROR",
292
+ "Invalid query parameters",
293
+ "query",
294
+ error,
295
+ ),
296
+ };
297
+ }
298
+ }
299
+
300
+ let path: unknown = matchedParams;
301
+ if (contract.pathParams) {
302
+ try {
303
+ path = await parseStandardSchema(contract.pathParams, matchedParams);
304
+ } catch (error) {
305
+ return {
306
+ ok: false,
307
+ response: requestValidationError(
308
+ contract,
309
+ 422,
310
+ "VALIDATION_ERROR",
311
+ "Invalid path parameters",
312
+ "path",
313
+ error,
314
+ ),
315
+ };
316
+ }
317
+ }
318
+
319
+ let headers: unknown = rawHeaders;
320
+ const headerSchemas = getContractHeaderSchemas(contract.headers);
321
+ if (headerSchemas.length > 0) {
322
+ try {
323
+ headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
324
+ } catch (error) {
325
+ return {
326
+ ok: false,
327
+ response: requestValidationError(
328
+ contract,
329
+ 422,
330
+ "VALIDATION_ERROR",
331
+ "Invalid request headers",
332
+ "headers",
333
+ error,
334
+ ),
335
+ };
336
+ }
337
+ }
338
+
339
+ let body: unknown;
340
+ // Raw routes own body consumption: the handler reads `req` itself, for
341
+ // example to verify a webhook signature over the exact bytes.
342
+ if (!rawRoute) {
343
+ try {
344
+ body = await parseBody(req, maxRequestBodyBytes);
345
+ } catch (error) {
346
+ if (error instanceof RequestBodyTooLargeError) {
347
+ return {
348
+ ok: false,
349
+ response: requestValidationError(
350
+ contract,
351
+ 413,
352
+ "PAYLOAD_TOO_LARGE",
353
+ "Request body is too large",
354
+ "body",
355
+ error,
356
+ ),
357
+ };
358
+ }
359
+ return {
360
+ ok: false,
361
+ response: requestValidationError(
362
+ contract,
363
+ 400,
364
+ "INVALID_BODY",
365
+ "Malformed JSON",
366
+ "body",
367
+ error,
368
+ ),
369
+ };
370
+ }
371
+ }
372
+
373
+ if (contract.body) {
374
+ try {
375
+ body = await parseStandardSchema(contract.body, body);
376
+ } catch (error) {
377
+ if (body === undefined && error instanceof SchemaValidationError) {
378
+ return {
379
+ ok: false,
380
+ response: requestValidationError(
381
+ contract,
382
+ 400,
383
+ "MISSING_BODY",
384
+ "Request body is required",
385
+ "body",
386
+ error,
387
+ ),
388
+ };
389
+ }
390
+ return {
391
+ ok: false,
392
+ response: requestValidationError(
393
+ contract,
394
+ 422,
395
+ "VALIDATION_ERROR",
396
+ "Invalid request body",
397
+ "body",
398
+ error,
399
+ missingJsonContentTypeHint(req, body),
400
+ ),
401
+ };
402
+ }
403
+ }
404
+
405
+ return {
406
+ ok: true,
407
+ inputs: { path, query, headers, body, rawHeaders },
408
+ };
409
+ }