@voyant-travel/hono 0.118.3 → 0.119.0

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/dist/app.d.ts CHANGED
@@ -17,6 +17,20 @@ export interface LazyMount {
17
17
  prefix: string;
18
18
  load: LazyRoutesLoader;
19
19
  }
20
+ /**
21
+ * A single module route mount recorded at mount time so a build-time generator
22
+ * can produce one self-contained OpenAPI document per module (voyant#2733).
23
+ * Mirrors `ModuleMount` in `./openapi.ts` (structurally identical, declared here
24
+ * to keep the build-time-only openapi module out of the runtime path). `load`
25
+ * returns the sub-app without serving it — eager mounts wrap the already-built
26
+ * routes as `() => routes`; lazy mounts pass their loader.
27
+ */
28
+ export interface ModuleMount {
29
+ moduleName: string;
30
+ /** Absolute surface mount prefix, or `"/"` for absolute `lazyRoutes`. */
31
+ prefix: string;
32
+ load: () => Hono<any, any, any> | Promise<Hono<any, any, any>>;
33
+ }
20
34
  /**
21
35
  * App handle returned alongside the Hono instance. Carries `ready()` for
22
36
  * headless / sibling-process deployments that need to fire the lazy
@@ -59,6 +73,14 @@ export interface VoyantAppExtensions<TBindings = unknown> {
59
73
  * `.openapi()` operations via `mergeLazyOpenApiPaths`. Never read at runtime.
60
74
  */
61
75
  lazyMounts: LazyMount[];
76
+ /**
77
+ * Every module route mount (admin + public, eager + lazy), tagged with its
78
+ * owning module name (voyant#2733). A build-time generator reads this to
79
+ * produce one self-contained OpenAPI document per module via
80
+ * `generateModuleOpenApiDocuments`, keeping the module boundary authoritative
81
+ * rather than guessed from path prefixes. Never read at runtime.
82
+ */
83
+ moduleMounts: ModuleMount[];
62
84
  }
63
85
  /**
64
86
  * Low-level app factory: given an already-resolved `modules`/`extensions` set
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAQhC,OAAO,EAAE,KAAK,gBAAgB,EAA0C,MAAM,kBAAkB,CAAA;AA+BhG,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAY,eAAe,EAAE,MAAM,YAAY,CAAA;AAI5F,4EAA4E;AAC5E,KAAK,QAAQ,CAAC,SAAS,SAAS,cAAc,IAAI;IAChD,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAA;AAuBD;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,gBAAgB,CAAA;CACvB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB,CAAC,SAAS,GAAG,OAAO;IACtD;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,QAAQ,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1C;;;;;;;;OAQG;IACH,QAAQ,EAAE,OAAO,qBAAqB,EAAE,QAAQ,CAAA;IAChD;;;;;OAKG;IACH,UAAU,EAAE,SAAS,EAAE,CAAA;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,SAAS,SAAS,cAAc,EACvD,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,GACjC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAue5D"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAQhC,OAAO,EAAE,KAAK,gBAAgB,EAA0C,MAAM,kBAAkB,CAAA;AA+BhG,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAY,eAAe,EAAE,MAAM,YAAY,CAAA;AAI5F,4EAA4E;AAC5E,KAAK,QAAQ,CAAC,SAAS,SAAS,cAAc,IAAI;IAChD,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAA;AAuBD;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,gBAAgB,CAAA;CACvB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAA;IAEd,IAAI,EAAE,MAAM,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;CAC/D;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB,CAAC,SAAS,GAAG,OAAO;IACtD;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,QAAQ,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1C;;;;;;;;OAQG;IACH,QAAQ,EAAE,OAAO,qBAAqB,EAAE,QAAQ,CAAA;IAChD;;;;;OAKG;IACH,UAAU,EAAE,SAAS,EAAE,CAAA;IACvB;;;;;;OAMG;IACH,YAAY,EAAE,WAAW,EAAE,CAAA;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,SAAS,SAAS,cAAc,EACvD,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,GACjC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,GAAG,mBAAmB,CAAC,SAAS,CAAC,CA+f5D"}
package/dist/app.js CHANGED
@@ -410,56 +410,79 @@ export function mountApp(config) {
410
410
  // them so the prefix logic is single-sourced here. Cheap array pushes — no
411
411
  // eager `import()` and no runtime read.
412
412
  const lazyMounts = [];
413
+ // Per-module OpenAPI generation manifest (voyant#2733). Recorded alongside the
414
+ // mount so the module name + real prefix are single-sourced here; `publicPath`
415
+ // overrides are captured with their actual mount, not re-derived. Eager routes
416
+ // are wrapped as `() => routes` so the generator's loader-based path is
417
+ // uniform across eager + lazy. Webhook routes are intentionally excluded — the
418
+ // per-module docs cover the admin/storefront surfaces only.
419
+ const moduleMounts = [];
413
420
  // Mount module routes
414
421
  for (const mod of allModules) {
415
- const adminPrefix = `/v1/admin/${mod.module.name}`;
416
- const publicPrefix = resolveSurfaceMountPath("/v1/public", mod.publicPath, mod.module.name);
422
+ const moduleName = mod.module.name;
423
+ const adminPrefix = `/v1/admin/${moduleName}`;
424
+ const publicPrefix = resolveSurfaceMountPath("/v1/public", mod.publicPath, moduleName);
417
425
  if (mod.adminRoutes) {
418
- app.route(adminPrefix, mod.adminRoutes);
426
+ const adminRoutes = mod.adminRoutes;
427
+ app.route(adminPrefix, adminRoutes);
428
+ moduleMounts.push({ moduleName, prefix: adminPrefix, load: () => adminRoutes });
419
429
  }
420
430
  if (mod.publicRoutes) {
421
- app.route(publicPrefix, mod.publicRoutes);
431
+ const publicRoutes = mod.publicRoutes;
432
+ app.route(publicPrefix, publicRoutes);
433
+ moduleMounts.push({ moduleName, prefix: publicPrefix, load: () => publicRoutes });
422
434
  }
423
435
  if (mod.lazyAdminRoutes) {
424
436
  mountLazyRoutesAt(app, adminPrefix, mod.lazyAdminRoutes);
425
437
  lazyMounts.push({ prefix: adminPrefix, load: mod.lazyAdminRoutes });
438
+ moduleMounts.push({ moduleName, prefix: adminPrefix, load: mod.lazyAdminRoutes });
426
439
  }
427
440
  if (mod.lazyPublicRoutes) {
428
441
  mountLazyRoutesAt(app, publicPrefix, mod.lazyPublicRoutes);
429
442
  lazyMounts.push({ prefix: publicPrefix, load: mod.lazyPublicRoutes });
443
+ moduleMounts.push({ moduleName, prefix: publicPrefix, load: mod.lazyPublicRoutes });
430
444
  }
431
445
  if (mod.lazyRoutes) {
432
446
  mountLazyRoutePaths(app, mod.lazyRoutes.paths, mod.lazyRoutes.load);
433
447
  lazyMounts.push({ prefix: "/", load: mod.lazyRoutes.load });
448
+ moduleMounts.push({ moduleName, prefix: "/", load: mod.lazyRoutes.load });
434
449
  }
435
450
  if (mod.webhookRoutes) {
436
- app.route(`/v1/${mod.module.name}`, mod.webhookRoutes);
451
+ app.route(`/v1/${moduleName}`, mod.webhookRoutes);
437
452
  }
438
453
  }
439
454
  // Mount extension routes
440
455
  for (const ext of allExtensions) {
441
- const adminPrefix = `/v1/admin/${ext.extension.module}`;
442
- const publicPrefix = resolveSurfaceMountPath("/v1/public", ext.publicPath, ext.extension.module);
456
+ const moduleName = ext.extension.module;
457
+ const adminPrefix = `/v1/admin/${moduleName}`;
458
+ const publicPrefix = resolveSurfaceMountPath("/v1/public", ext.publicPath, moduleName);
443
459
  if (ext.adminRoutes) {
444
- app.route(adminPrefix, ext.adminRoutes);
460
+ const adminRoutes = ext.adminRoutes;
461
+ app.route(adminPrefix, adminRoutes);
462
+ moduleMounts.push({ moduleName, prefix: adminPrefix, load: () => adminRoutes });
445
463
  }
446
464
  if (ext.publicRoutes) {
447
- app.route(publicPrefix, ext.publicRoutes);
465
+ const publicRoutes = ext.publicRoutes;
466
+ app.route(publicPrefix, publicRoutes);
467
+ moduleMounts.push({ moduleName, prefix: publicPrefix, load: () => publicRoutes });
448
468
  }
449
469
  if (ext.lazyAdminRoutes) {
450
470
  mountLazyRoutesAt(app, adminPrefix, ext.lazyAdminRoutes);
451
471
  lazyMounts.push({ prefix: adminPrefix, load: ext.lazyAdminRoutes });
472
+ moduleMounts.push({ moduleName, prefix: adminPrefix, load: ext.lazyAdminRoutes });
452
473
  }
453
474
  if (ext.lazyPublicRoutes) {
454
475
  mountLazyRoutesAt(app, publicPrefix, ext.lazyPublicRoutes);
455
476
  lazyMounts.push({ prefix: publicPrefix, load: ext.lazyPublicRoutes });
477
+ moduleMounts.push({ moduleName, prefix: publicPrefix, load: ext.lazyPublicRoutes });
456
478
  }
457
479
  if (ext.lazyRoutes) {
458
480
  mountLazyRoutePaths(app, ext.lazyRoutes.paths, ext.lazyRoutes.load);
459
481
  lazyMounts.push({ prefix: "/", load: ext.lazyRoutes.load });
482
+ moduleMounts.push({ moduleName, prefix: "/", load: ext.lazyRoutes.load });
460
483
  }
461
484
  if (ext.webhookRoutes) {
462
- app.route(`/v1/${ext.extension.module}`, ext.webhookRoutes);
485
+ app.route(`/v1/${moduleName}`, ext.webhookRoutes);
463
486
  }
464
487
  }
465
488
  // Additional routes
@@ -477,6 +500,7 @@ export function mountApp(config) {
477
500
  const augmented = app;
478
501
  augmented.eventBus = eventBus;
479
502
  augmented.lazyMounts = lazyMounts;
503
+ augmented.moduleMounts = moduleMounts;
480
504
  augmented.ready = (bindings) => ensureRuntimeBootstrapped(bindings ?? {});
481
505
  return augmented;
482
506
  }
@@ -10,9 +10,10 @@ import type { EventBus, OutboxEventStore } from "@voyant-travel/core";
10
10
  * With a `store` (transactional outbox), emits are also DURABLE: the
11
11
  * envelope is persisted before any handler runs, and failed deliveries
12
12
  * are retried by the drain. If the durable capture itself fails (DB
13
- * unreachable), the emit falls back to direct (non-durable) delivery
14
- * losing durability for that one event is strictly better than losing
15
- * the event, and an order of magnitude better than failing the request.
13
+ * unreachable), the emit falls back to direct (non-durable) delivery.
14
+ * Once capture has succeeded, the wrapper must not redeliver directly:
15
+ * the stored row owns completion/retry, and direct fallback would make
16
+ * subscriber side effects ambiguous.
16
17
  *
17
18
  * Buses that predate the {@link EmitOptions} parameter simply ignore it
18
19
  * and keep awaiting all handlers — a safe degradation.
@@ -1 +1 @@
1
- {"version":3,"file":"request-event-bus.d.ts","sourceRoot":"","sources":["../../src/lib/request-event-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,QAAQ,EAAiB,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEjG;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,QAAQ,EACb,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,EAC3D,KAAK,CAAC,EAAE,gBAAgB,GACvB,QAAQ,CA+BV"}
1
+ {"version":3,"file":"request-event-bus.d.ts","sourceRoot":"","sources":["../../src/lib/request-event-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,QAAQ,EAAiB,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEjG;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,QAAQ,EACb,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,EAC3D,KAAK,CAAC,EAAE,gBAAgB,GACvB,QAAQ,CAiDV"}
@@ -9,9 +9,10 @@
9
9
  * With a `store` (transactional outbox), emits are also DURABLE: the
10
10
  * envelope is persisted before any handler runs, and failed deliveries
11
11
  * are retried by the drain. If the durable capture itself fails (DB
12
- * unreachable), the emit falls back to direct (non-durable) delivery
13
- * losing durability for that one event is strictly better than losing
14
- * the event, and an order of magnitude better than failing the request.
12
+ * unreachable), the emit falls back to direct (non-durable) delivery.
13
+ * Once capture has succeeded, the wrapper must not redeliver directly:
14
+ * the stored row owns completion/retry, and direct fallback would make
15
+ * subscriber side effects ambiguous.
15
16
  *
16
17
  * Buses that predate the {@link EmitOptions} parameter simply ignore it
17
18
  * and keep awaiting all handlers — a safe degradation.
@@ -26,12 +27,28 @@ export function requestScopedEventBus(bus, schedule, store) {
26
27
  if (!store) {
27
28
  return bus.emit(event, data, metadata, { ...base, ...options });
28
29
  }
30
+ let captureFailed = false;
31
+ const guardedStore = {
32
+ async insert(envelope) {
33
+ try {
34
+ return await store.insert(envelope);
35
+ }
36
+ catch (err) {
37
+ captureFailed = true;
38
+ throw err;
39
+ }
40
+ },
41
+ complete: (id) => store.complete(id),
42
+ fail: (id, error) => store.fail(id, error),
43
+ };
29
44
  try {
30
- return await bus.emit(event, data, metadata, { ...base, store, ...options });
45
+ return await bus.emit(event, data, metadata, { ...base, store: guardedStore, ...options });
31
46
  }
32
47
  catch (err) {
33
- // Only the outbox insert can reject (handler errors are
34
- // collected, bookkeeping failures are swallowed inside the bus).
48
+ if (!captureFailed) {
49
+ console.error(`[events] durable delivery failed for "${event}" after outbox capture:`, err);
50
+ return;
51
+ }
35
52
  console.error(`[events] outbox capture failed for "${event}" — falling back to direct delivery:`, err);
36
53
  return bus.emit(event, data, metadata, { ...base, ...options });
37
54
  }
package/dist/openapi.d.ts CHANGED
@@ -66,6 +66,23 @@ export interface LazyMount {
66
66
  prefix: string;
67
67
  load: LazyRoutesLoader;
68
68
  }
69
+ /**
70
+ * A single module route mount recorded by `mountApp` for build-time per-module
71
+ * spec generation (voyant#2733). One module contributes several mounts (admin +
72
+ * public, eager + lazy), all tagged with the same `moduleName` — so the module
73
+ * boundary is the authoritative one from the registration, not a path-prefix
74
+ * guess. `prefix` is the real absolute mount (base path included), so the
75
+ * generated per-module doc carries correct absolute paths — including
76
+ * `publicPath` overrides whose prefix isn't the module name (e.g. a module that
77
+ * mounts its storefront routes under `/v1/public/booking-engine`). `load`
78
+ * returns the sub-app without serving it (eager mounts wrap the already-built
79
+ * app as `() => routes`; lazy mounts pass their loader).
80
+ */
81
+ export interface ModuleMount {
82
+ moduleName: string;
83
+ prefix: string;
84
+ load: () => AnyApp | Promise<AnyApp>;
85
+ }
69
86
  /**
70
87
  * Eager-merge lazy route families into a generated base document (voyant#2114).
71
88
  *
@@ -86,5 +103,51 @@ export interface LazyMount {
86
103
  * (`@asteasolutions/zod-to-openapi` stays out of the Worker bundle).
87
104
  */
88
105
  export declare function mergeLazyOpenApiPaths(base: OpenApiDocument, mounts: readonly LazyMount[], options: GenerateOpenApiOptions): Promise<OpenApiDocument>;
106
+ /**
107
+ * Generate one self-contained OpenAPI document per module from its recorded
108
+ * mounts (voyant#2733).
109
+ *
110
+ * Instead of building one giant composed document and splitting it by path
111
+ * prefix, this generates each module's spec directly from the routes the module
112
+ * registered — the authoritative module boundary. Each module's admin + public
113
+ * (+ lazy) sub-apps are re-mounted into a throwaway `OpenAPIHono` at their real
114
+ * absolute prefix (reusing the exact prefix-merge `OpenAPIHono.route(...)`
115
+ * applies), then its `getOpenAPI31Document()` is read. The returned docs are
116
+ * self-contained (each carries its own referenced components), so they render
117
+ * and diff cleanly on their own — unlike a 7 MB aggregate.
118
+ *
119
+ * A mount whose loader throws, or that returns a plain `Hono` with no
120
+ * `.openapi()` registry, is skipped without failing the module. Modules that
121
+ * contribute no documented operation are omitted from the result entirely.
122
+ *
123
+ * Build-time only — same constraint as the rest of this module
124
+ * (`@asteasolutions/zod-to-openapi` must stay out of the Worker bundle).
125
+ */
126
+ export declare function generateModuleOpenApiDocuments(mounts: readonly ModuleMount[], options: GenerateOpenApiOptions): Promise<Map<string, OpenApiDocument>>;
127
+ /**
128
+ * Partition a composed document into one document per module, covering EVERY
129
+ * admin/storefront path (voyant#2733).
130
+ *
131
+ * `generateModuleOpenApiDocuments` alone only sees routes recorded in the module
132
+ * mount manifest — it misses `additionalRoutes` and any route mounted directly
133
+ * on the composed app (e.g. the operator's workflow-runs admin surface, the
134
+ * `_meta/capabilities` route). Those were present in the old committed aggregate
135
+ * and must not silently vanish from the committed per-module specs.
136
+ *
137
+ * This uses the manifest as the *authoritative* owner for the routes it knows
138
+ * about — which is what makes `publicPath` overrides (whose prefix isn't the
139
+ * module name, e.g. `/v1/public/booking-engine`) land under the right module —
140
+ * and falls back to the path's own second segment for anything the manifest
141
+ * doesn't claim. The result therefore partitions the full surface exactly: every
142
+ * `/v1/admin/*` and `/v1/public/*` path lands in exactly one module document.
143
+ * Non-surface routes (`/v1/<name>` webhooks, legacy `/v1/*`) live only in the
144
+ * aggregate, as before.
145
+ *
146
+ * Each per-module document carries the aggregate's shared `components` verbatim
147
+ * (there is ~one), so it stays a valid, self-contained OpenAPI document.
148
+ *
149
+ * Build-time only.
150
+ */
151
+ export declare function splitDocumentByModule(full: OpenApiDocument, mounts: readonly ModuleMount[], options: GenerateOpenApiOptions): Promise<Map<string, OpenApiDocument>>;
89
152
  export {};
90
153
  //# sourceMappingURL=openapi.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAEhC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8EAA8E;IAC9E,CAAC,SAAS,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAA;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,CAAC,SAAS,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAA;CACpC;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,WAAW,CAAA;IACjB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAA;CAC1B;AAGD,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;AAEjC;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC,CAAA;AAE7E;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CASjB;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,YAAY,CAAA;AAO/C;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,UAAU,GAAG,eAAe,CAMxF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,gBAAgB,CAAA;CACvB;AAYD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,eAAe,CAAC,CA8D1B"}
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAEhC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8EAA8E;IAC9E,CAAC,SAAS,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAA;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,CAAC,SAAS,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAA;CACpC;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,WAAW,CAAA;IACjB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAA;CAC1B;AAGD,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;AAEjC;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC,CAAA;AAE7E;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CASjB;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,YAAY,CAAA;AAO/C;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,UAAU,GAAG,eAAe,CAMxF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,gBAAgB,CAAA;CACvB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACrC;AAYD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,eAAe,CAAC,CA8D1B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAsCvC;AAOD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CA4BvC"}
package/dist/openapi.js CHANGED
@@ -112,3 +112,121 @@ export async function mergeLazyOpenApiPaths(base, mounts, options) {
112
112
  }
113
113
  return merged;
114
114
  }
115
+ /**
116
+ * Generate one self-contained OpenAPI document per module from its recorded
117
+ * mounts (voyant#2733).
118
+ *
119
+ * Instead of building one giant composed document and splitting it by path
120
+ * prefix, this generates each module's spec directly from the routes the module
121
+ * registered — the authoritative module boundary. Each module's admin + public
122
+ * (+ lazy) sub-apps are re-mounted into a throwaway `OpenAPIHono` at their real
123
+ * absolute prefix (reusing the exact prefix-merge `OpenAPIHono.route(...)`
124
+ * applies), then its `getOpenAPI31Document()` is read. The returned docs are
125
+ * self-contained (each carries its own referenced components), so they render
126
+ * and diff cleanly on their own — unlike a 7 MB aggregate.
127
+ *
128
+ * A mount whose loader throws, or that returns a plain `Hono` with no
129
+ * `.openapi()` registry, is skipped without failing the module. Modules that
130
+ * contribute no documented operation are omitted from the result entirely.
131
+ *
132
+ * Build-time only — same constraint as the rest of this module
133
+ * (`@asteasolutions/zod-to-openapi` must stay out of the Worker bundle).
134
+ */
135
+ export async function generateModuleOpenApiDocuments(mounts, options) {
136
+ const byModule = new Map();
137
+ for (const mount of mounts) {
138
+ const list = byModule.get(mount.moduleName);
139
+ if (list)
140
+ list.push(mount);
141
+ else
142
+ byModule.set(mount.moduleName, [mount]);
143
+ }
144
+ const result = new Map();
145
+ for (const [moduleName, moduleMounts] of byModule) {
146
+ const throwaway = new OpenAPIHono();
147
+ let mountedAny = false;
148
+ for (const { prefix, load } of moduleMounts) {
149
+ let subApp;
150
+ try {
151
+ subApp = await load();
152
+ }
153
+ catch (error) {
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ console.warn(`[voyant] openapi: module "${moduleName}" mount at "${prefix}" failed to load, skipping: ${message}`);
156
+ continue;
157
+ }
158
+ if (!isOpenApiHono(subApp))
159
+ continue;
160
+ throwaway.route(prefix === "/" ? "/" : prefix, subApp);
161
+ mountedAny = true;
162
+ }
163
+ if (!mountedAny)
164
+ continue;
165
+ const doc = throwaway.getOpenAPI31Document({
166
+ openapi: "3.1.0",
167
+ info: options.info,
168
+ ...(options.servers ? { servers: options.servers } : {}),
169
+ });
170
+ if (!doc.paths || Object.keys(doc.paths).length === 0)
171
+ continue;
172
+ result.set(moduleName, doc);
173
+ }
174
+ return result;
175
+ }
176
+ /** Second path segment after the surface prefix — `/v1/admin/<seg>/...`. */
177
+ function moduleSegment(path) {
178
+ return path.split("/")[3] ?? "misc";
179
+ }
180
+ /**
181
+ * Partition a composed document into one document per module, covering EVERY
182
+ * admin/storefront path (voyant#2733).
183
+ *
184
+ * `generateModuleOpenApiDocuments` alone only sees routes recorded in the module
185
+ * mount manifest — it misses `additionalRoutes` and any route mounted directly
186
+ * on the composed app (e.g. the operator's workflow-runs admin surface, the
187
+ * `_meta/capabilities` route). Those were present in the old committed aggregate
188
+ * and must not silently vanish from the committed per-module specs.
189
+ *
190
+ * This uses the manifest as the *authoritative* owner for the routes it knows
191
+ * about — which is what makes `publicPath` overrides (whose prefix isn't the
192
+ * module name, e.g. `/v1/public/booking-engine`) land under the right module —
193
+ * and falls back to the path's own second segment for anything the manifest
194
+ * doesn't claim. The result therefore partitions the full surface exactly: every
195
+ * `/v1/admin/*` and `/v1/public/*` path lands in exactly one module document.
196
+ * Non-surface routes (`/v1/<name>` webhooks, legacy `/v1/*`) live only in the
197
+ * aggregate, as before.
198
+ *
199
+ * Each per-module document carries the aggregate's shared `components` verbatim
200
+ * (there is ~one), so it stays a valid, self-contained OpenAPI document.
201
+ *
202
+ * Build-time only.
203
+ */
204
+ export async function splitDocumentByModule(full, mounts, options) {
205
+ const moduleDocs = await generateModuleOpenApiDocuments(mounts, options);
206
+ const owner = new Map();
207
+ for (const [moduleName, doc] of moduleDocs) {
208
+ for (const path of Object.keys(doc.paths ?? {}))
209
+ owner.set(path, moduleName);
210
+ }
211
+ const buckets = new Map();
212
+ for (const [path, item] of Object.entries(full.paths ?? {})) {
213
+ if (!path.startsWith("/v1/admin/") && !path.startsWith("/v1/public/"))
214
+ continue;
215
+ const moduleName = owner.get(path) ?? moduleSegment(path);
216
+ let bucket = buckets.get(moduleName);
217
+ if (!bucket) {
218
+ bucket = {};
219
+ buckets.set(moduleName, bucket);
220
+ }
221
+ bucket[path] = item;
222
+ }
223
+ const result = new Map();
224
+ for (const [moduleName, paths] of buckets) {
225
+ result.set(moduleName, {
226
+ ...full,
227
+ paths,
228
+ ...(full.components ? { components: full.components } : {}),
229
+ });
230
+ }
231
+ return result;
232
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyant-travel/hono",
3
- "version": "0.118.3",
3
+ "version": "0.119.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -135,19 +135,19 @@
135
135
  "drizzle-orm": "^0.45.2",
136
136
  "hono": "^4.12.25",
137
137
  "zod": "^4.4.3",
138
- "@voyant-travel/core": "^0.111.0",
138
+ "@voyant-travel/core": "^0.111.1",
139
139
  "@voyant-travel/db": "^0.109.4",
140
140
  "@voyant-travel/storage": "^0.106.0",
141
141
  "@voyant-travel/types": "^0.106.1",
142
142
  "@voyant-travel/utils": "^0.105.5",
143
- "@voyant-travel/workflows": "^0.111.12"
143
+ "@voyant-travel/workflows": "^0.111.13"
144
144
  },
145
145
  "devDependencies": {
146
146
  "@cloudflare/workers-types": "^4.20260628.1",
147
147
  "typescript": "^6.0.3",
148
148
  "vitest": "^4.1.9",
149
149
  "@voyant-travel/voyant-typescript-config": "^0.1.0",
150
- "@voyant-travel/workflows-orchestrator": "^0.111.12"
150
+ "@voyant-travel/workflows-orchestrator": "^0.111.13"
151
151
  },
152
152
  "files": [
153
153
  "dist"