@ontrails/http 1.0.0-beta.12 → 1.0.0-beta.13

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.
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Hono connector for Trails HTTP routes.
3
+ *
4
+ * Takes framework-agnostic HttpRouteDefinition[] and wires them into a
5
+ * Hono application, handling request parsing, response mapping, and errors.
6
+ *
7
+ * ```ts
8
+ * const app = topo("myapp", entity);
9
+ * await trailhead(app, { port: 3000 });
10
+ * ```
11
+ */
12
+ import { isTrailsError, statusCodeMap, validateTopo } from '@ontrails/core';
13
+ import { Hono } from 'hono';
14
+ import { buildHttpRoutes } from '../build.js';
15
+ /** Unwrap optional/default wrappers to reach the underlying Zod type name. */
16
+ const unwrapZodType = (node) => {
17
+ let current = node;
18
+ while (current._zod.def['type'] === 'optional' ||
19
+ current._zod.def['type'] === 'default') {
20
+ current = current._zod.def['innerType'];
21
+ }
22
+ return current._zod.def['type'];
23
+ };
24
+ /** Extract the object shape from a Zod schema, or undefined if not an object. */
25
+ const extractShape = (schema) => {
26
+ const s = schema;
27
+ if (s._zod.def['type'] !== 'object') {
28
+ return undefined;
29
+ }
30
+ return s._zod.def['shape'];
31
+ };
32
+ /** Collect top-level field names whose underlying type is array. */
33
+ const collectArrayKeys = (inputSchema) => {
34
+ const shape = inputSchema ? extractShape(inputSchema) : undefined;
35
+ if (!shape) {
36
+ return new Set();
37
+ }
38
+ const keys = new Set();
39
+ for (const [key, value] of Object.entries(shape)) {
40
+ if (unwrapZodType(value) === 'array') {
41
+ keys.add(key);
42
+ }
43
+ }
44
+ return keys;
45
+ };
46
+ /** Parse query params into a plain object, preserving raw strings for Zod. */
47
+ const parseQueryParams = (c, inputSchema) => {
48
+ const result = {};
49
+ const url = new URL(c.req.url);
50
+ const arrayKeys = collectArrayKeys(inputSchema);
51
+ for (const key of url.searchParams.keys()) {
52
+ // Already collected via getAll
53
+ if (key in result) {
54
+ continue;
55
+ }
56
+ const all = url.searchParams.getAll(key);
57
+ result[key] = all.length > 1 || arrayKeys.has(key) ? all : all[0];
58
+ }
59
+ return result;
60
+ };
61
+ /** Sentinel indicating a JSON parse failure. */
62
+ const JSON_PARSE_ERROR = Symbol('JSON_PARSE_ERROR');
63
+ /** Return true when the request has no body content. */
64
+ const isEmptyBody = (c) => {
65
+ const contentLength = c.req.header('Content-Length');
66
+ if (contentLength !== undefined) {
67
+ return Number.parseInt(contentLength, 10) === 0;
68
+ }
69
+ // No Content-Length header — treat as empty when Content-Type is also absent.
70
+ return c.req.header('Content-Type') === undefined;
71
+ };
72
+ /** Read input from request based on input source. */
73
+ const readInput = async (c, inputSource, inputSchema) => {
74
+ if (inputSource === 'query') {
75
+ return parseQueryParams(c, inputSchema);
76
+ }
77
+ if (isEmptyBody(c)) {
78
+ return {};
79
+ }
80
+ try {
81
+ return await c.req.json();
82
+ }
83
+ catch {
84
+ return JSON_PARSE_ERROR;
85
+ }
86
+ };
87
+ // ---------------------------------------------------------------------------
88
+ // Response mapping
89
+ // ---------------------------------------------------------------------------
90
+ /** Map a TrailsError or generic Error to an HTTP error response. */
91
+ const mapErrorResponse = (error) => {
92
+ if (isTrailsError(error)) {
93
+ return {
94
+ body: {
95
+ error: {
96
+ category: error.category,
97
+ code: error.name,
98
+ message: error.message,
99
+ },
100
+ },
101
+ status: statusCodeMap[error.category],
102
+ };
103
+ }
104
+ return {
105
+ body: {
106
+ error: {
107
+ category: 'internal',
108
+ code: 'InternalError',
109
+ message: error.message,
110
+ },
111
+ },
112
+ status: 500,
113
+ };
114
+ };
115
+ // ---------------------------------------------------------------------------
116
+ // Route registration
117
+ // ---------------------------------------------------------------------------
118
+ /** Map a Result to an HTTP response via Hono context. */
119
+ const mapResultToResponse = (result, c) => {
120
+ if (result.isOk()) {
121
+ return c.json({ data: result.value }, 200);
122
+ }
123
+ const { body, status } = mapErrorResponse(result.error ?? new Error('Unknown error'));
124
+ return c.json(body, status);
125
+ };
126
+ /** Convert a caught unknown value to an error response. */
127
+ const handleCaughtError = (error, c) => {
128
+ const err = error instanceof Error ? error : new Error(String(error));
129
+ const { body, status } = mapErrorResponse(err);
130
+ return c.json(body, status);
131
+ };
132
+ /** Create a Hono handler from a route definition. */
133
+ const createHonoHandler = (route) => async (c) => {
134
+ const rawInput = await readInput(c, route.inputSource, route.trail.input);
135
+ if (rawInput === JSON_PARSE_ERROR) {
136
+ return c.json({
137
+ error: {
138
+ category: 'validation',
139
+ code: 'ValidationError',
140
+ message: 'Invalid JSON in request body',
141
+ },
142
+ }, 400);
143
+ }
144
+ const requestId = c.req.header('X-Request-ID') ?? undefined;
145
+ const { signal: abortSignal } = c.req.raw;
146
+ try {
147
+ const result = await route.execute(rawInput, requestId, abortSignal);
148
+ return mapResultToResponse(result, c);
149
+ }
150
+ catch (error) {
151
+ return handleCaughtError(error, c);
152
+ }
153
+ };
154
+ /** Route registration keyed by HTTP method. */
155
+ const routeRegistrars = {
156
+ DELETE: (hono, path, handler) => {
157
+ hono.delete(path, handler);
158
+ },
159
+ GET: (hono, path, handler) => {
160
+ hono.get(path, handler);
161
+ },
162
+ POST: (hono, path, handler) => {
163
+ hono.post(path, handler);
164
+ },
165
+ };
166
+ const registerRoutes = (hono, routes) => {
167
+ for (const route of routes) {
168
+ const handler = createHonoHandler(route);
169
+ routeRegistrars[route.method](hono, route.path, handler);
170
+ }
171
+ };
172
+ // ---------------------------------------------------------------------------
173
+ // Global error handler
174
+ // ---------------------------------------------------------------------------
175
+ const registerErrorHandler = (hono) => {
176
+ // oxlint-disable-next-line prefer-await-to-callbacks -- Hono's onError API requires a callback
177
+ hono.onError((err, c) => c.json({
178
+ error: {
179
+ category: 'internal',
180
+ code: 'InternalError',
181
+ message: err.message,
182
+ },
183
+ }, 500));
184
+ };
185
+ // ---------------------------------------------------------------------------
186
+ // Validation
187
+ // ---------------------------------------------------------------------------
188
+ /**
189
+ * Throw a ValidationError if the topo has structural issues.
190
+ * Pass `skip: true` to bypass validation (e.g. when `validate: false` is set).
191
+ */
192
+ const assertValidTopo = (app, skip = false) => {
193
+ if (skip) {
194
+ return;
195
+ }
196
+ const validated = validateTopo(app);
197
+ if (validated.isErr()) {
198
+ throw validated.error;
199
+ }
200
+ };
201
+ // ---------------------------------------------------------------------------
202
+ // trailhead
203
+ // ---------------------------------------------------------------------------
204
+ /**
205
+ * Build HTTP routes from a topo, create a Hono app, and optionally start serving.
206
+ */
207
+ // oxlint-disable-next-line require-await -- async for consistency with other trailhead() entrypoints
208
+ export const trailhead = async (app, options = {}) => {
209
+ assertValidTopo(app, options.validate === false);
210
+ const hono = new Hono();
211
+ registerErrorHandler(hono);
212
+ const routesResult = buildHttpRoutes(app, {
213
+ basePath: options.basePath,
214
+ configValues: options.configValues,
215
+ createContext: options.createContext,
216
+ gates: options.gates,
217
+ provisions: options.provisions,
218
+ });
219
+ if (routesResult.isErr()) {
220
+ throw routesResult.error;
221
+ }
222
+ registerRoutes(hono, routesResult.value);
223
+ if (options.serve !== false) {
224
+ Bun.serve({
225
+ fetch: hono.fetch,
226
+ hostname: options.hostname ?? '0.0.0.0',
227
+ port: options.port ?? 3000,
228
+ });
229
+ }
230
+ return hono;
231
+ };
232
+ //# sourceMappingURL=trailhead.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trailhead.js","sourceRoot":"","sources":["../../src/hono/trailhead.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/http",
3
- "version": "1.0.0-beta.12",
3
+ "version": "1.0.0-beta.13",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -4,13 +4,13 @@ import {
4
4
  InternalError,
5
5
  NotFoundError,
6
6
  Result,
7
- SURFACE_KEY,
8
- service,
7
+ TRAILHEAD_KEY,
8
+ provision,
9
9
  ValidationError,
10
10
  trail,
11
11
  topo,
12
12
  } from '@ontrails/core';
13
- import type { Layer } from '@ontrails/core';
13
+ import type { Gate } from '@ontrails/core';
14
14
  import { z } from 'zod';
15
15
 
16
16
  import { buildHttpRoutes } from '../build.js';
@@ -20,48 +20,48 @@ import { buildHttpRoutes } from '../build.js';
20
20
  // ---------------------------------------------------------------------------
21
21
 
22
22
  const echoTrail = trail('echo', {
23
+ blaze: (input) => Result.ok({ reply: input.message }),
23
24
  description: 'Echo a message back',
24
25
  input: z.object({ message: z.string() }),
25
26
  intent: 'read',
26
27
  output: z.object({ reply: z.string() }),
27
- run: (input) => Result.ok({ reply: input.message }),
28
28
  });
29
29
 
30
30
  const createTrail = trail('item.create', {
31
+ blaze: (input) => Result.ok({ id: '123', name: input.name }),
31
32
  description: 'Create an item',
32
33
  input: z.object({ name: z.string() }),
33
34
  output: z.object({ id: z.string(), name: z.string() }),
34
- run: (input) => Result.ok({ id: '123', name: input.name }),
35
35
  });
36
36
 
37
37
  const deleteTrail = trail('item.delete', {
38
+ blaze: (_input) => Result.ok({ deleted: true }),
38
39
  description: 'Delete an item',
39
40
  input: z.object({ id: z.string() }),
40
41
  intent: 'destroy',
41
- run: (_input) => Result.ok({ deleted: true }),
42
42
  });
43
43
 
44
44
  const notFoundTrail = trail('item.get', {
45
+ blaze: (_input) => Result.err(new NotFoundError('Item not found')),
45
46
  description: 'Get an item that does not exist',
46
47
  input: z.object({ id: z.string() }),
47
48
  intent: 'read',
48
- run: (_input) => Result.err(new NotFoundError('Item not found')),
49
49
  });
50
50
 
51
51
  const internalTrail = trail('crash', {
52
+ blaze: () => Result.err(new InternalError('Something broke')),
52
53
  description: 'Always fails with internal error',
53
54
  input: z.object({}),
54
- run: () => Result.err(new InternalError('Something broke')),
55
55
  });
56
56
 
57
57
  const internalMetaTrail = trail('secret', {
58
+ blaze: () => Result.ok({ ok: true }),
58
59
  description: 'Internal trail that should be skipped',
59
60
  input: z.object({}),
60
- metadata: { internal: true },
61
- run: () => Result.ok({ ok: true }),
61
+ meta: { internal: true },
62
62
  });
63
63
 
64
- const dbService = service('db.main', {
64
+ const dbProvision = provision('db.main', {
65
65
  create: () =>
66
66
  Result.ok({
67
67
  source: 'factory',
@@ -245,10 +245,10 @@ describe('buildHttpRoutes', () => {
245
245
 
246
246
  test('returns err Result when run function throws', async () => {
247
247
  const throwingTrail = trail('throwing', {
248
- input: z.object({}),
249
- run: () => {
248
+ blaze: () => {
250
249
  throw new Error('unexpected throw');
251
250
  },
251
+ input: z.object({}),
252
252
  });
253
253
  const app = topo('testapp', { throwingTrail });
254
254
  const buildResult = buildHttpRoutes(app);
@@ -283,12 +283,12 @@ describe('buildHttpRoutes', () => {
283
283
  let capturedRequestId: string | undefined;
284
284
 
285
285
  const ctxTrail = trail('ctx.check', {
286
- input: z.object({}),
287
- intent: 'read',
288
- run: (_input, ctx) => {
286
+ blaze: (_input, ctx) => {
289
287
  capturedRequestId = ctx.requestId;
290
288
  return Result.ok({ ok: true });
291
289
  },
290
+ input: z.object({}),
291
+ intent: 'read',
292
292
  });
293
293
 
294
294
  const app = topo('testapp', { ctxTrail });
@@ -305,12 +305,12 @@ describe('buildHttpRoutes', () => {
305
305
  let capturedRequestId: string | undefined;
306
306
 
307
307
  const ctxTrail = trail('ctx.default', {
308
- input: z.object({}),
309
- intent: 'read',
310
- run: (_input, ctx) => {
308
+ blaze: (_input, ctx) => {
311
309
  capturedRequestId = ctx.requestId;
312
310
  return Result.ok({ ok: true });
313
311
  },
312
+ input: z.object({}),
313
+ intent: 'read',
314
314
  });
315
315
 
316
316
  const app = topo('testapp', { ctxTrail });
@@ -324,18 +324,18 @@ describe('buildHttpRoutes', () => {
324
324
  expect(capturedRequestId).not.toBe('');
325
325
  });
326
326
 
327
- test('forwards service overrides into executeTrail', async () => {
328
- const serviceTrail = trail('service.check', {
327
+ test('forwards provision overrides into executeTrail', async () => {
328
+ const provisionTrail = trail('provision.check', {
329
+ blaze: (_input, ctx) =>
330
+ Result.ok({ source: dbProvision.from(ctx).source as string }),
329
331
  input: z.object({}),
330
332
  output: z.object({ source: z.string() }),
331
- run: (_input, ctx) =>
332
- Result.ok({ source: dbService.from(ctx).source as string }),
333
- services: [dbService],
333
+ provisions: [dbProvision],
334
334
  });
335
335
 
336
- const app = topo('testapp', { serviceTrail });
336
+ const app = topo('testapp', { provisionTrail });
337
337
  const buildResult = buildHttpRoutes(app, {
338
- services: { 'db.main': { source: 'override' } },
338
+ provisions: { 'db.main': { source: 'override' } },
339
339
  });
340
340
 
341
341
  expect(buildResult.isOk()).toBe(true);
@@ -347,12 +347,12 @@ describe('buildHttpRoutes', () => {
347
347
  });
348
348
  });
349
349
 
350
- describe('layers', () => {
351
- test('layers compose around trail execution', async () => {
350
+ describe('gates', () => {
351
+ test('gates compose around trail execution', async () => {
352
352
  const calls: string[] = [];
353
353
 
354
- const testLayer: Layer = {
355
- name: 'test-layer',
354
+ const testGate: Gate = {
355
+ name: 'test-gate',
356
356
  wrap(_trail, impl) {
357
357
  return async (input, ctx) => {
358
358
  calls.push('before');
@@ -364,7 +364,7 @@ describe('buildHttpRoutes', () => {
364
364
  };
365
365
 
366
366
  const app = topo('testapp', { echoTrail });
367
- const buildResult = buildHttpRoutes(app, { layers: [testLayer] });
367
+ const buildResult = buildHttpRoutes(app, { gates: [testGate] });
368
368
 
369
369
  expect(buildResult.isOk()).toBe(true);
370
370
  const [route] = buildResult.value;
@@ -377,24 +377,25 @@ describe('buildHttpRoutes', () => {
377
377
 
378
378
  describe('custom createContext', () => {
379
379
  test('custom createContext is used when provided', async () => {
380
- const contextState = { custom: false, surface: false };
380
+ const contextState = { custom: false, trailheadMarker: false };
381
381
 
382
382
  const ctxTrail = trail('ctx.custom', {
383
- input: z.object({}),
384
- intent: 'read',
385
- run: (_input, ctx) => {
383
+ blaze: (_input, ctx) => {
386
384
  contextState.custom = ctx.extensions?.['custom'] === true;
387
- contextState.surface = ctx.extensions?.[SURFACE_KEY] === 'http';
385
+ contextState.trailheadMarker =
386
+ ctx.extensions?.[TRAILHEAD_KEY] === 'http';
388
387
  return Result.ok({ ok: true });
389
388
  },
389
+ input: z.object({}),
390
+ intent: 'read',
390
391
  });
391
392
 
392
393
  const app = topo('testapp', { ctxTrail });
393
394
  const buildResult = buildHttpRoutes(app, {
394
395
  createContext: () => ({
396
+ abortSignal: new AbortController().signal,
395
397
  extensions: { custom: true },
396
398
  requestId: 'test-id',
397
- signal: new AbortController().signal,
398
399
  }),
399
400
  });
400
401
 
@@ -404,7 +405,7 @@ describe('buildHttpRoutes', () => {
404
405
  const result = await route?.execute({});
405
406
  expect(result?.isOk()).toBe(true);
406
407
  expect(contextState.custom).toBe(true);
407
- expect(contextState.surface).toBe(true);
408
+ expect(contextState.trailheadMarker).toBe(true);
408
409
  });
409
410
  });
410
411
 
@@ -414,16 +415,16 @@ describe('buildHttpRoutes', () => {
414
415
  // "entity/show" derives path /entity/show (slashes are preserved)
415
416
  // Both have intent: read -> GET, so they collide on GET /entity/show
416
417
  const dotTrail = trail('entity.show', {
418
+ blaze: () => Result.ok({ dot: true }),
417
419
  description: 'Show entity (dot notation)',
418
420
  input: z.object({}),
419
421
  intent: 'read',
420
- run: () => Result.ok({ dot: true }),
421
422
  });
422
423
  const slashTrail = trail('entity/show', {
424
+ blaze: () => Result.ok({ slash: true }),
423
425
  description: 'Show entity (slash notation)',
424
426
  input: z.object({}),
425
427
  intent: 'read',
426
- run: () => Result.ok({ slash: true }),
427
428
  });
428
429
  const app = topo('testapp', { dotTrail, slashTrail });
429
430
  const result = buildHttpRoutes(app);
@@ -438,15 +439,15 @@ describe('buildHttpRoutes', () => {
438
439
  // "item/resource" derives POST /item/resource (default intent: write)
439
440
  // Same path, different methods — no collision
440
441
  const getItem = trail('item.resource', {
442
+ blaze: () => Result.ok({ get: true }),
441
443
  description: 'Get item',
442
444
  input: z.object({}),
443
445
  intent: 'read',
444
- run: () => Result.ok({ get: true }),
445
446
  });
446
447
  const createItem = trail('item/resource', {
448
+ blaze: () => Result.ok({ created: true }),
447
449
  description: 'Create item',
448
450
  input: z.object({ name: z.string() }),
449
- run: () => Result.ok({ created: true }),
450
451
  });
451
452
  const app = topo('testapp', { createItem, getItem });
452
453
  const result = buildHttpRoutes(app);
@@ -457,16 +458,16 @@ describe('buildHttpRoutes', () => {
457
458
 
458
459
  test('collision error message identifies both trail IDs', () => {
459
460
  const dotTrail = trail('entity.show', {
461
+ blaze: () => Result.ok({ one: true }),
460
462
  description: 'Trail one',
461
463
  input: z.object({}),
462
464
  intent: 'read',
463
- run: () => Result.ok({ one: true }),
464
465
  });
465
466
  const slashTrail = trail('entity/show', {
467
+ blaze: () => Result.ok({ two: true }),
466
468
  description: 'Trail two',
467
469
  input: z.object({}),
468
470
  intent: 'read',
469
- run: () => Result.ok({ two: true }),
470
471
  });
471
472
  const app = topo('testapp', { dotTrail, slashTrail });
472
473
  const result = buildHttpRoutes(app);
package/src/build.ts CHANGED
@@ -2,19 +2,19 @@
2
2
  * Build framework-agnostic HTTP route definitions from a Trails topo.
3
3
  *
4
4
  * Each route definition describes the path, method, input source, and an
5
- * `execute` function that validates input, composes layers, and runs the
5
+ * `execute` function that validates input, composes gates, and runs the
6
6
  * implementation -- all without referencing any HTTP framework types.
7
7
  */
8
8
 
9
9
  import {
10
10
  Result,
11
- SURFACE_KEY,
11
+ TRAILHEAD_KEY,
12
12
  ValidationError,
13
13
  executeTrail,
14
14
  } from '@ontrails/core';
15
15
  import type {
16
- Layer,
17
- ServiceOverrideMap,
16
+ Gate,
17
+ ProvisionOverrideMap,
18
18
  Topo,
19
19
  Trail,
20
20
  TrailContextInit,
@@ -26,15 +26,15 @@ import type {
26
26
 
27
27
  export interface BuildHttpRoutesOptions {
28
28
  readonly basePath?: string | undefined;
29
- /** Config values for services that declare a `config` schema, keyed by service ID. */
29
+ /** Config values for provisions that declare a `config` schema, keyed by provision ID. */
30
30
  readonly configValues?:
31
31
  | Readonly<Record<string, Record<string, unknown>>>
32
32
  | undefined;
33
33
  readonly createContext?:
34
34
  | (() => TrailContextInit | Promise<TrailContextInit>)
35
35
  | undefined;
36
- readonly layers?: readonly Layer[] | undefined;
37
- readonly services?: ServiceOverrideMap | undefined;
36
+ readonly gates?: readonly Gate[] | undefined;
37
+ readonly provisions?: ProvisionOverrideMap | undefined;
38
38
  }
39
39
 
40
40
  export type HttpMethod = 'GET' | 'POST' | 'DELETE';
@@ -49,19 +49,19 @@ export interface HttpRouteDefinition {
49
49
  readonly inputSource: InputSource;
50
50
  readonly trail: Trail<unknown, unknown>;
51
51
  /**
52
- * Validate input, compose layers, and execute the trail implementation.
52
+ * Validate input, compose gates, and execute the trail implementation.
53
53
  *
54
54
  * The caller is responsible for parsing raw input from the request and
55
55
  * mapping the Result to an HTTP response. This function is framework-agnostic.
56
56
  *
57
- * @param signal - Optional AbortSignal from the HTTP request. When provided,
57
+ * @param abortSignal - Optional AbortSignal from the HTTP request. When provided,
58
58
  * it takes final precedence over any context factory signal, allowing
59
59
  * client-initiated cancellation to propagate into trail execution.
60
60
  */
61
61
  readonly execute: (
62
62
  input: unknown,
63
63
  requestId?: string | undefined,
64
- signal?: AbortSignal | undefined
64
+ abortSignal?: AbortSignal | undefined
65
65
  ) => Promise<Result<unknown, Error>>;
66
66
  }
67
67
 
@@ -93,15 +93,15 @@ const deriveInputSource = (method: HttpMethod): InputSource =>
93
93
 
94
94
  /** Check if a trail should be included (skip internal trails). */
95
95
  const shouldInclude = (trail: Trail<unknown, unknown>): boolean =>
96
- trail.metadata?.['internal'] !== true;
96
+ trail.meta?.['internal'] !== true;
97
97
 
98
- /** Build per-request context overrides with the HTTP surface marker. */
99
- const withHttpSurface = (
98
+ /** Build per-request context overrides with the HTTP trailhead marker. */
99
+ const withHttpTrailhead = (
100
100
  requestId: string | undefined
101
101
  ): Partial<TrailContextInit> => ({
102
102
  ...(requestId === undefined ? {} : { requestId }),
103
103
  extensions: {
104
- [SURFACE_KEY]: 'http' as const,
104
+ [TRAILHEAD_KEY]: 'http' as const,
105
105
  },
106
106
  });
107
107
 
@@ -118,17 +118,17 @@ const withHttpSurface = (
118
118
  const createExecute =
119
119
  (
120
120
  t: Trail<unknown, unknown>,
121
- layers: readonly Layer[],
121
+ gates: readonly Gate[],
122
122
  options: BuildHttpRoutesOptions
123
123
  ): HttpRouteDefinition['execute'] =>
124
- (input, requestId, signal) =>
124
+ (input, requestId, abortSignal) =>
125
125
  executeTrail(t, input, {
126
+ abortSignal,
126
127
  configValues: options.configValues,
127
128
  createContext: options.createContext,
128
- ctx: withHttpSurface(requestId),
129
- layers,
130
- services: options.services,
131
- signal,
129
+ ctx: withHttpTrailhead(requestId),
130
+ gates,
131
+ provisions: options.provisions,
132
132
  });
133
133
 
134
134
  // ---------------------------------------------------------------------------
@@ -143,13 +143,13 @@ const eligibleTrails = (app: Topo): Trail<unknown, unknown>[] =>
143
143
  const buildRoute = (
144
144
  trail: Trail<unknown, unknown>,
145
145
  basePath: string,
146
- layers: readonly Layer[],
146
+ gates: readonly Gate[],
147
147
  options: BuildHttpRoutesOptions
148
148
  ): HttpRouteDefinition => {
149
149
  const method = deriveMethod(trail);
150
150
  const path = derivePath(basePath, trail.id);
151
151
  return {
152
- execute: createExecute(trail, layers, options),
152
+ execute: createExecute(trail, gates, options),
153
153
  inputSource: deriveInputSource(method),
154
154
  method,
155
155
  path,
@@ -190,14 +190,14 @@ const registerRoute = (
190
190
  const accumulateRoutes = (
191
191
  trails: Trail<unknown, unknown>[],
192
192
  basePath: string,
193
- layers: readonly Layer[],
193
+ gates: readonly Gate[],
194
194
  options: BuildHttpRoutesOptions
195
195
  ): Result<HttpRouteDefinition[], Error> => {
196
196
  const routes: HttpRouteDefinition[] = [];
197
197
  const seenRoutes = new Map<string, string>();
198
198
 
199
199
  for (const trail of trails) {
200
- const route = buildRoute(trail, basePath, layers, options);
200
+ const route = buildRoute(trail, basePath, gates, options);
201
201
  const registered = registerRoute(route, seenRoutes, routes);
202
202
  if (registered.isErr()) {
203
203
  return registered;
@@ -218,7 +218,7 @@ const accumulateRoutes = (
218
218
  * - An HTTP method derived from intent (read -> GET, write -> POST, destroy -> DELETE)
219
219
  * - A path derived from the trail ID (dots become slashes)
220
220
  * - An input source derived from the method (GET -> query, others -> body)
221
- * - An `execute` function that validates, layers, and runs the implementation
221
+ * - An `execute` function that validates, gates, and runs the implementation
222
222
  *
223
223
  * Returns `Result.err(ValidationError)` if two trails derive the same
224
224
  * (method, path) pair. Returns `Result.ok(routes)` on success.
@@ -228,6 +228,6 @@ export const buildHttpRoutes = (
228
228
  options: BuildHttpRoutesOptions = {}
229
229
  ): Result<HttpRouteDefinition[], Error> => {
230
230
  const basePath = (options.basePath ?? '').replace(/\/+$/, '');
231
- const layers = options.layers ?? [];
232
- return accumulateRoutes(eligibleTrails(app), basePath, layers, options);
231
+ const gates = options.gates ?? [];
232
+ return accumulateRoutes(eligibleTrails(app), basePath, gates, options);
233
233
  };