@ontrails/hono 1.0.0-beta.15 → 1.0.0-beta.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/surface.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Hono connector for Trails HTTP routes.
2
+ * Hono adapter for Trails HTTP routes.
3
3
  *
4
4
  * Takes framework-agnostic HttpRouteDefinition[] and wires them into a
5
5
  * Hono application, handling request parsing, response mapping, and errors.
@@ -10,9 +10,14 @@
10
10
  * ```
11
11
  */
12
12
 
13
- import { isTrailsError, mapTransportError } from '@ontrails/core';
13
+ import {
14
+ isTrailsError,
15
+ projectErrorDiagnostics,
16
+ projectPublicSurfaceError,
17
+ ValidationError,
18
+ } from '@ontrails/core';
14
19
  import type {
15
- Intent,
20
+ BaseSurfaceOptions,
16
21
  Layer,
17
22
  ResourceOverrideMap,
18
23
  Topo,
@@ -22,33 +27,38 @@ import { Hono } from 'hono';
22
27
  import type { Context as HonoContext } from 'hono';
23
28
  import type { ContentfulStatusCode } from 'hono/utils/http-status';
24
29
  import { deriveHttpRoutes } from '@ontrails/http';
25
- import type { HttpMethod, HttpRouteDefinition } from '@ontrails/http';
30
+ import type {
31
+ HttpExecutionContext,
32
+ HttpMethod,
33
+ HttpRouteDefinition,
34
+ ResolveHttpPermit,
35
+ } from '@ontrails/http';
26
36
 
27
37
  // ---------------------------------------------------------------------------
28
38
  // Options
29
39
  // ---------------------------------------------------------------------------
30
40
 
31
- export interface CreateAppOptions {
41
+ export interface CreateAppOptions extends BaseSurfaceOptions {
32
42
  readonly basePath?: string | undefined;
33
- /** Config values for resources that declare a `config` schema, keyed by resource ID. */
34
- readonly configValues?:
35
- | Readonly<Record<string, Record<string, unknown>>>
36
- | undefined;
37
43
  readonly createContext?:
38
44
  | (() => TrailContextInit | Promise<TrailContextInit>)
39
45
  | undefined;
40
- readonly exclude?: readonly string[] | undefined;
41
46
  readonly hostname?: string | undefined;
42
- readonly include?: readonly string[] | undefined;
43
- readonly intent?: readonly Intent[] | undefined;
44
47
  readonly layers?: readonly Layer[] | undefined;
48
+ /** Maximum JSON request body size in bytes. Defaults to 1 MiB. */
49
+ readonly maxJsonBodyBytes?: number | undefined;
45
50
  readonly name?: string | undefined;
46
51
  readonly port?: number | undefined;
47
52
  readonly resources?: ResourceOverrideMap | undefined;
48
- /** Set to `false` to skip topo validation at startup. Defaults to `true`. */
49
- readonly validate?: boolean | undefined;
53
+ readonly resolvePermit?: ResolveHttpPermit | undefined;
54
+ }
55
+
56
+ interface RuntimeOptions {
57
+ readonly maxJsonBodyBytes: number;
50
58
  }
51
59
 
60
+ const DEFAULT_MAX_JSON_BODY_BYTES = 1024 * 1024;
61
+
52
62
  export interface SurfaceHttpResult {
53
63
  readonly close: () => Promise<void>;
54
64
  readonly url: string;
@@ -63,7 +73,7 @@ export interface SurfaceHttpResult {
63
73
  *
64
74
  * A single `?tag=one` stays a scalar string, while repeated keys like
65
75
  * `?tag=one&tag=two` become arrays. Schema validation owns whether that shape
66
- * is accepted; the connector does not coerce singleton values into arrays.
76
+ * is accepted; the adapter does not coerce singleton values into arrays.
67
77
  */
68
78
  const parseQueryParams = (c: HonoContext): Record<string, unknown> => {
69
79
  const result: Record<string, unknown> = {};
@@ -85,32 +95,226 @@ const parseQueryParams = (c: HonoContext): Record<string, unknown> => {
85
95
  /** Sentinel indicating a JSON parse failure. */
86
96
  const JSON_PARSE_ERROR = Symbol('JSON_PARSE_ERROR');
87
97
 
98
+ /** Sentinel indicating a JSON body rejected before parsing. */
99
+ const JSON_BODY_TOO_LARGE = Symbol('JSON_BODY_TOO_LARGE');
100
+
101
+ /** Sentinel indicating malformed body metadata. */
102
+ const JSON_BODY_INVALID_CONTENT_LENGTH = Symbol(
103
+ 'JSON_BODY_INVALID_CONTENT_LENGTH'
104
+ );
105
+
106
+ interface JsonObject {
107
+ readonly [key: string]: JsonValue;
108
+ }
109
+
110
+ type JsonValue =
111
+ | null
112
+ | boolean
113
+ | number
114
+ | string
115
+ | readonly JsonValue[]
116
+ | JsonObject;
117
+ type JsonBodyReadResult =
118
+ | JsonValue
119
+ | typeof JSON_BODY_INVALID_CONTENT_LENGTH
120
+ | typeof JSON_PARSE_ERROR
121
+ | typeof JSON_BODY_TOO_LARGE;
122
+ type JsonBodyTextReadResult = string | typeof JSON_BODY_TOO_LARGE;
123
+ type InputReadResult = Record<string, unknown> | JsonBodyReadResult;
124
+
125
+ const CONTENT_LENGTH_DECIMAL_PATTERN = /^\d+$/;
126
+
88
127
  /** Return true when the request has no body content. */
128
+ type ParsedContentLength =
129
+ | number
130
+ | typeof JSON_BODY_INVALID_CONTENT_LENGTH
131
+ | undefined;
132
+
133
+ const parseContentLength = (
134
+ contentLength: string | undefined
135
+ ): ParsedContentLength => {
136
+ if (contentLength === undefined) {
137
+ return undefined;
138
+ }
139
+ if (!CONTENT_LENGTH_DECIMAL_PATTERN.test(contentLength)) {
140
+ return JSON_BODY_INVALID_CONTENT_LENGTH;
141
+ }
142
+ const size = Number(contentLength);
143
+ return Number.isSafeInteger(size) ? size : Number.MAX_SAFE_INTEGER;
144
+ };
145
+
89
146
  const isEmptyBody = (c: HonoContext): boolean => {
90
- const contentLength = c.req.header('Content-Length');
147
+ const contentLength = parseContentLength(c.req.header('Content-Length'));
148
+ if (contentLength === JSON_BODY_INVALID_CONTENT_LENGTH) {
149
+ return false;
150
+ }
91
151
  if (contentLength !== undefined) {
92
- return Number.parseInt(contentLength, 10) === 0;
152
+ return contentLength === 0;
93
153
  }
94
154
  // No Content-Length header — treat as empty when Content-Type is also absent.
95
155
  return c.req.header('Content-Type') === undefined;
96
156
  };
97
157
 
158
+ const resolveMaxJsonBodyBytes = (value: number | undefined): number => {
159
+ const maxJsonBodyBytes = value ?? DEFAULT_MAX_JSON_BODY_BYTES;
160
+
161
+ if (!Number.isFinite(maxJsonBodyBytes) || maxJsonBodyBytes < 1) {
162
+ throw new ValidationError(
163
+ 'maxJsonBodyBytes must be a positive finite number'
164
+ );
165
+ }
166
+
167
+ return maxJsonBodyBytes;
168
+ };
169
+
170
+ const hasOversizedContentLength = (
171
+ c: HonoContext,
172
+ maxJsonBodyBytes: number
173
+ ): boolean => {
174
+ const contentLength = c.req.header('Content-Length');
175
+ if (contentLength === undefined) {
176
+ return false;
177
+ }
178
+ const size = parseContentLength(contentLength);
179
+ if (size === JSON_BODY_INVALID_CONTENT_LENGTH) {
180
+ return false;
181
+ }
182
+ return size !== undefined && size > maxJsonBodyBytes;
183
+ };
184
+
185
+ const measureBodyTextBytes = (text: string): number => new Blob([text]).size;
186
+
187
+ const validateCachedBodyText = (
188
+ text: string,
189
+ maxJsonBodyBytes: number
190
+ ): JsonBodyTextReadResult =>
191
+ measureBodyTextBytes(text) > maxJsonBodyBytes ? JSON_BODY_TOO_LARGE : text;
192
+
193
+ const readCachedBodyText = async (
194
+ c: HonoContext,
195
+ maxJsonBodyBytes: number
196
+ ): Promise<JsonBodyTextReadResult> =>
197
+ validateCachedBodyText(await c.req.text(), maxJsonBodyBytes);
198
+
199
+ const readBodyText = async (
200
+ c: HonoContext,
201
+ maxJsonBodyBytes: number
202
+ ): Promise<string | typeof JSON_BODY_TOO_LARGE> => {
203
+ if (c.req.raw.bodyUsed) {
204
+ return await readCachedBodyText(c, maxJsonBodyBytes);
205
+ }
206
+
207
+ const { body } = c.req.raw;
208
+ if (body === null) {
209
+ return '';
210
+ }
211
+
212
+ const reader = body.getReader();
213
+ const chunks: Uint8Array[] = [];
214
+ let totalBytes = 0;
215
+
216
+ try {
217
+ while (true) {
218
+ const { done, value } = await reader.read();
219
+ if (done) {
220
+ break;
221
+ }
222
+ if (value === undefined) {
223
+ continue;
224
+ }
225
+ totalBytes += value.byteLength;
226
+ if (totalBytes > maxJsonBodyBytes) {
227
+ await reader.cancel();
228
+ return JSON_BODY_TOO_LARGE;
229
+ }
230
+ chunks.push(value);
231
+ }
232
+ } finally {
233
+ reader.releaseLock();
234
+ }
235
+
236
+ const bytes = new Uint8Array(totalBytes);
237
+ let offset = 0;
238
+ for (const chunk of chunks) {
239
+ bytes.set(chunk, offset);
240
+ offset += chunk.byteLength;
241
+ }
242
+
243
+ return new TextDecoder().decode(bytes);
244
+ };
245
+
246
+ const readJsonBody = async (
247
+ c: HonoContext,
248
+ maxJsonBodyBytes: number
249
+ ): Promise<JsonBodyReadResult> => {
250
+ if (
251
+ parseContentLength(c.req.header('Content-Length')) ===
252
+ JSON_BODY_INVALID_CONTENT_LENGTH
253
+ ) {
254
+ return JSON_BODY_INVALID_CONTENT_LENGTH;
255
+ }
256
+
257
+ if (hasOversizedContentLength(c, maxJsonBodyBytes)) {
258
+ return JSON_BODY_TOO_LARGE;
259
+ }
260
+
261
+ const text = await readBodyText(c, maxJsonBodyBytes);
262
+ if (text === JSON_BODY_TOO_LARGE) {
263
+ return JSON_BODY_TOO_LARGE;
264
+ }
265
+
266
+ try {
267
+ return JSON.parse(text) as JsonValue;
268
+ } catch {
269
+ return JSON_PARSE_ERROR;
270
+ }
271
+ };
272
+
273
+ const parseJsonBodyText = (text: string): JsonBodyReadResult => {
274
+ try {
275
+ return JSON.parse(text) as JsonValue;
276
+ } catch {
277
+ return JSON_PARSE_ERROR;
278
+ }
279
+ };
280
+
281
+ const parseWebhookBodyText = (
282
+ c: HonoContext,
283
+ text: string
284
+ ): JsonBodyReadResult =>
285
+ isEmptyBody(c) || text.length === 0 ? {} : parseJsonBodyText(text);
286
+
287
+ const readWebhookBodyText = async (
288
+ c: HonoContext,
289
+ maxJsonBodyBytes: number
290
+ ): Promise<
291
+ string | typeof JSON_BODY_INVALID_CONTENT_LENGTH | typeof JSON_BODY_TOO_LARGE
292
+ > => {
293
+ if (
294
+ parseContentLength(c.req.header('Content-Length')) ===
295
+ JSON_BODY_INVALID_CONTENT_LENGTH
296
+ ) {
297
+ return JSON_BODY_INVALID_CONTENT_LENGTH;
298
+ }
299
+ if (hasOversizedContentLength(c, maxJsonBodyBytes)) {
300
+ return JSON_BODY_TOO_LARGE;
301
+ }
302
+ return await readBodyText(c, maxJsonBodyBytes);
303
+ };
304
+
98
305
  /** Read input from request based on input source. */
99
306
  const readInput = async (
100
307
  c: HonoContext,
101
- inputSource: 'query' | 'body'
102
- ): Promise<unknown> => {
308
+ inputSource: 'query' | 'body',
309
+ options: RuntimeOptions
310
+ ): Promise<InputReadResult> => {
103
311
  if (inputSource === 'query') {
104
312
  return parseQueryParams(c);
105
313
  }
106
314
  if (isEmptyBody(c)) {
107
315
  return {};
108
316
  }
109
- try {
110
- return await c.req.json();
111
- } catch {
112
- return JSON_PARSE_ERROR;
113
- }
317
+ return await readJsonBody(c, options.maxJsonBodyBytes);
114
318
  };
115
319
 
116
320
  // ---------------------------------------------------------------------------
@@ -121,30 +325,44 @@ const readInput = async (
121
325
  const mapErrorResponse = (
122
326
  error: Error
123
327
  ): { body: Record<string, unknown>; status: ContentfulStatusCode } => {
124
- if (isTrailsError(error)) {
125
- return {
126
- body: {
127
- error: {
128
- category: error.category,
129
- code: error.name,
130
- message: error.message,
131
- },
132
- },
133
- status: mapTransportError('http', error) as ContentfulStatusCode,
134
- };
135
- }
328
+ const projection = projectPublicSurfaceError('http', error);
136
329
  return {
137
330
  body: {
138
331
  error: {
139
- category: 'internal',
140
- code: 'InternalError',
141
- message: error.message,
332
+ category: projection.category,
333
+ code: projection.name,
334
+ message: projection.message,
142
335
  },
143
336
  },
144
- status: 500,
337
+ status: projection.code as ContentfulStatusCode,
145
338
  };
146
339
  };
147
340
 
341
+ const LOG_UNSAFE_LABEL_CHARACTERS = /[^\w:.-]/g;
342
+ const MAX_DIAGNOSTIC_LABEL_VALUE_LENGTH = 128;
343
+
344
+ const sanitizeDiagnosticLabelValue = (value: string): string =>
345
+ value
346
+ .replace(LOG_UNSAFE_LABEL_CHARACTERS, '_')
347
+ .slice(0, MAX_DIAGNOSTIC_LABEL_VALUE_LENGTH);
348
+
349
+ const reportInternalDiagnostics = (error: Error, c: HonoContext): void => {
350
+ if (isTrailsError(error)) {
351
+ return;
352
+ }
353
+
354
+ const requestId = c.req.header('X-Request-ID');
355
+ const safeRequestId =
356
+ requestId === undefined
357
+ ? undefined
358
+ : sanitizeDiagnosticLabelValue(requestId);
359
+ const label =
360
+ safeRequestId === undefined
361
+ ? '[ontrails:hono] Internal error'
362
+ : `[ontrails:hono] Internal error (${safeRequestId})`;
363
+ console.error(label, projectErrorDiagnostics(error));
364
+ };
365
+
148
366
  // ---------------------------------------------------------------------------
149
367
  // Route registration
150
368
  // ---------------------------------------------------------------------------
@@ -157,43 +375,193 @@ const mapResultToResponse = (
157
375
  if (result.isOk()) {
158
376
  return c.json({ data: result.value }, 200);
159
377
  }
160
- const { body, status } = mapErrorResponse(
161
- result.error ?? new Error('Unknown error')
162
- );
378
+ const error = result.error ?? new Error('Unknown error');
379
+ reportInternalDiagnostics(error, c);
380
+ const { body, status } = mapErrorResponse(error);
163
381
  return c.json(body, status);
164
382
  };
165
383
 
166
384
  /** Convert a caught unknown value to an error response. */
167
385
  const handleCaughtError = (error: unknown, c: HonoContext): Response => {
168
386
  const err = error instanceof Error ? error : new Error(String(error));
387
+ reportInternalDiagnostics(err, c);
169
388
  const { body, status } = mapErrorResponse(err);
170
389
  return c.json(body, status);
171
390
  };
172
391
 
392
+ const invalidJsonResponse = (c: HonoContext): Response =>
393
+ c.json(
394
+ {
395
+ error: {
396
+ category: 'validation',
397
+ code: 'ValidationError',
398
+ message: 'Invalid JSON in request body',
399
+ },
400
+ },
401
+ 400
402
+ );
403
+
404
+ const invalidContentLengthResponse = (c: HonoContext): Response =>
405
+ c.json(
406
+ {
407
+ error: {
408
+ category: 'validation',
409
+ code: 'ValidationError',
410
+ message: 'Invalid Content-Length header',
411
+ },
412
+ },
413
+ 400
414
+ );
415
+
416
+ const oversizedJsonBodyResponse = (
417
+ c: HonoContext,
418
+ options: RuntimeOptions
419
+ ): Response =>
420
+ c.json(
421
+ {
422
+ error: {
423
+ category: 'validation',
424
+ code: 'ValidationError',
425
+ message: `JSON request body exceeds ${options.maxJsonBodyBytes} bytes`,
426
+ },
427
+ },
428
+ 413
429
+ );
430
+
431
+ const collectHeaders = (c: HonoContext): Record<string, string> => {
432
+ const headers: Record<string, string> = {};
433
+ for (const [key, value] of c.req.raw.headers) {
434
+ headers[key] = value;
435
+ }
436
+ return headers;
437
+ };
438
+
439
+ const createHttpExecutionContext = (c: HonoContext): HttpExecutionContext => ({
440
+ headers: c.req.raw.headers,
441
+ });
442
+
443
+ const createWebhookVerifyRequest = (
444
+ c: HonoContext,
445
+ body: string
446
+ ): {
447
+ readonly body: string;
448
+ readonly headers: Record<string, string>;
449
+ readonly method: string;
450
+ readonly path: string;
451
+ } => ({
452
+ body,
453
+ headers: collectHeaders(c),
454
+ method: c.req.method,
455
+ path: new URL(c.req.url).pathname,
456
+ });
457
+
458
+ const recordInvalidWebhook = async (
459
+ route: HttpRouteDefinition,
460
+ errorCategory = 'validation'
461
+ ): Promise<void> => {
462
+ await route.recordWebhookInvalid?.(errorCategory);
463
+ };
464
+
465
+ const errorCategoryForWebhookFailure = (error: Error | undefined): string =>
466
+ error !== undefined && isTrailsError(error) ? error.category : 'internal';
467
+
468
+ const handleWebhookRoute = async (
469
+ route: HttpRouteDefinition,
470
+ options: RuntimeOptions,
471
+ c: HonoContext
472
+ ): Promise<Response> => {
473
+ const rawBody = await readWebhookBodyText(c, options.maxJsonBodyBytes);
474
+
475
+ if (rawBody === JSON_BODY_INVALID_CONTENT_LENGTH) {
476
+ await recordInvalidWebhook(route);
477
+ return invalidContentLengthResponse(c);
478
+ }
479
+
480
+ if (rawBody === JSON_BODY_TOO_LARGE) {
481
+ await recordInvalidWebhook(route);
482
+ return oversizedJsonBodyResponse(c, options);
483
+ }
484
+
485
+ const verified = await route.verifyWebhook?.(
486
+ createWebhookVerifyRequest(c, rawBody)
487
+ );
488
+ if (verified?.isErr()) {
489
+ await recordInvalidWebhook(
490
+ route,
491
+ errorCategoryForWebhookFailure(verified.error)
492
+ );
493
+ return mapResultToResponse(verified, c);
494
+ }
495
+
496
+ const jsonBody = parseWebhookBodyText(c, rawBody);
497
+ if (jsonBody === JSON_PARSE_ERROR) {
498
+ await recordInvalidWebhook(route);
499
+ return invalidJsonResponse(c);
500
+ }
501
+
502
+ const parsed = route.parseWebhookInput?.(jsonBody);
503
+ if (parsed === undefined) {
504
+ await recordInvalidWebhook(route);
505
+ return mapResultToResponse(
506
+ {
507
+ error: new ValidationError('Webhook route is missing parse handler'),
508
+ isOk: () => false,
509
+ },
510
+ c
511
+ );
512
+ }
513
+ if (parsed.isErr()) {
514
+ await recordInvalidWebhook(route);
515
+ return mapResultToResponse(parsed, c);
516
+ }
517
+
518
+ const requestId = c.req.header('X-Request-ID') ?? undefined;
519
+ const { signal: abortSignal } = c.req.raw;
520
+ const result = await route.execute(
521
+ parsed.value,
522
+ requestId,
523
+ abortSignal,
524
+ createHttpExecutionContext(c)
525
+ );
526
+ return mapResultToResponse(result, c);
527
+ };
528
+
173
529
  /** Create a Hono handler from a route definition. */
174
530
  const createHonoHandler =
175
- (route: HttpRouteDefinition) =>
531
+ (route: HttpRouteDefinition, options: RuntimeOptions) =>
176
532
  async (c: HonoContext): Promise<Response> => {
177
- const rawInput = await readInput(c, route.inputSource);
533
+ if (route.inputSource === 'webhook') {
534
+ try {
535
+ return await handleWebhookRoute(route, options, c);
536
+ } catch (error: unknown) {
537
+ return handleCaughtError(error, c);
538
+ }
539
+ }
540
+
541
+ const rawInput = await readInput(c, route.inputSource, options);
178
542
 
179
543
  if (rawInput === JSON_PARSE_ERROR) {
180
- return c.json(
181
- {
182
- error: {
183
- category: 'validation',
184
- code: 'ValidationError',
185
- message: 'Invalid JSON in request body',
186
- },
187
- },
188
- 400
189
- );
544
+ return invalidJsonResponse(c);
545
+ }
546
+
547
+ if (rawInput === JSON_BODY_INVALID_CONTENT_LENGTH) {
548
+ return invalidContentLengthResponse(c);
549
+ }
550
+
551
+ if (rawInput === JSON_BODY_TOO_LARGE) {
552
+ return oversizedJsonBodyResponse(c, options);
190
553
  }
191
554
 
192
555
  const requestId = c.req.header('X-Request-ID') ?? undefined;
193
556
  const { signal: abortSignal } = c.req.raw;
194
557
 
195
558
  try {
196
- const result = await route.execute(rawInput, requestId, abortSignal);
559
+ const result = await route.execute(
560
+ rawInput,
561
+ requestId,
562
+ abortSignal,
563
+ createHttpExecutionContext(c)
564
+ );
197
565
  return mapResultToResponse(result, c);
198
566
  } catch (error: unknown) {
199
567
  return handleCaughtError(error, c);
@@ -215,14 +583,24 @@ const routeRegistrars: Record<
215
583
  GET: (hono, path, handler) => {
216
584
  hono.get(path, handler);
217
585
  },
586
+ PATCH: (hono, path, handler) => {
587
+ hono.patch(path, handler);
588
+ },
218
589
  POST: (hono, path, handler) => {
219
590
  hono.post(path, handler);
220
591
  },
592
+ PUT: (hono, path, handler) => {
593
+ hono.put(path, handler);
594
+ },
221
595
  };
222
596
 
223
- const registerRoutes = (hono: Hono, routes: HttpRouteDefinition[]): void => {
597
+ const registerRoutes = (
598
+ hono: Hono,
599
+ routes: HttpRouteDefinition[],
600
+ options: RuntimeOptions
601
+ ): void => {
224
602
  for (const route of routes) {
225
- const handler = createHonoHandler(route);
603
+ const handler = createHonoHandler(route, options);
226
604
  routeRegistrars[route.method](hono, route.path, handler);
227
605
  }
228
606
  };
@@ -233,18 +611,7 @@ const registerRoutes = (hono: Hono, routes: HttpRouteDefinition[]): void => {
233
611
 
234
612
  const registerErrorHandler = (hono: Hono): void => {
235
613
  // oxlint-disable-next-line prefer-await-to-callbacks -- Hono's onError API requires a callback
236
- hono.onError((err, c) =>
237
- c.json(
238
- {
239
- error: {
240
- category: 'internal',
241
- code: 'InternalError',
242
- message: err.message,
243
- },
244
- },
245
- 500
246
- )
247
- );
614
+ hono.onError((err, c) => handleCaughtError(err, c));
248
615
  };
249
616
 
250
617
  // ---------------------------------------------------------------------------
@@ -257,12 +624,27 @@ const registerErrorHandler = (hono: Hono): void => {
257
624
 
258
625
  /**
259
626
  * Build HTTP routes from a topo and register them on a Hono app.
627
+ *
628
+ * @remarks This is a host materialization boundary. Derivation failures are
629
+ * thrown for HTTP bootstrap code after `deriveHttpRoutes` has already
630
+ * represented the framework error as a Result.
631
+ *
632
+ * @example
633
+ * ```ts
634
+ * import { createApp } from '@ontrails/hono';
635
+ *
636
+ * const app = createApp(graph, { basePath: '/api' });
637
+ * Bun.serve({ fetch: app.fetch, port: 3000 });
638
+ * ```
260
639
  */
261
640
  export const createApp = (
262
641
  graph: Topo,
263
642
  options: CreateAppOptions = {}
264
643
  ): Hono => {
265
644
  const hono = new Hono();
645
+ const runtimeOptions = {
646
+ maxJsonBodyBytes: resolveMaxJsonBodyBytes(options.maxJsonBodyBytes),
647
+ };
266
648
 
267
649
  registerErrorHandler(hono);
268
650
 
@@ -274,6 +656,7 @@ export const createApp = (
274
656
  include: options.include,
275
657
  intent: options.intent,
276
658
  layers: options.layers,
659
+ resolvePermit: options.resolvePermit,
277
660
  resources: options.resources,
278
661
  validate: options.validate,
279
662
  });
@@ -282,7 +665,7 @@ export const createApp = (
282
665
  throw routesResult.error;
283
666
  }
284
667
 
285
- registerRoutes(hono, routesResult.value);
668
+ registerRoutes(hono, routesResult.value, runtimeOptions);
286
669
  return hono;
287
670
  };
288
671
 
@@ -313,6 +696,14 @@ const startServer = (
313
696
  *
314
697
  * @remarks Always starts a Bun server. Use `createApp(graph)` for an
315
698
  * unserved Hono app that you can wire into your own server.
699
+ *
700
+ * @example
701
+ * ```ts
702
+ * import { surface } from '@ontrails/hono';
703
+ *
704
+ * const server = await surface(graph, { port: 3000 });
705
+ * console.log(server.url);
706
+ * ```
316
707
  */
317
708
  export const surface = async (
318
709
  graph: Topo,
@@ -1 +0,0 @@
1
- $ tsc -b
@@ -1,3 +0,0 @@
1
- $ oxlint ./src
2
- Found 0 warnings and 0 errors.
3
- Finished in 69ms on 3 files with 93 rules using 24 threads.
@@ -1 +0,0 @@
1
- $ tsc --noEmit
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export { createApp, surface, type CreateAppOptions, type SurfaceHttpResult, } from './surface.js';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,OAAO,EACP,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAC"}