@voyant-travel/hono 0.109.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +58 -0
  3. package/dist/app-workflows.d.ts +31 -0
  4. package/dist/app-workflows.d.ts.map +1 -0
  5. package/dist/app-workflows.js +110 -0
  6. package/dist/app.d.ts +45 -0
  7. package/dist/app.d.ts.map +1 -0
  8. package/dist/app.js +403 -0
  9. package/dist/auth/crypto.d.ts +16 -0
  10. package/dist/auth/crypto.d.ts.map +1 -0
  11. package/dist/auth/crypto.js +66 -0
  12. package/dist/auth/index.d.ts +5 -0
  13. package/dist/auth/index.d.ts.map +1 -0
  14. package/dist/auth/index.js +3 -0
  15. package/dist/auth/require-user.d.ts +3 -0
  16. package/dist/auth/require-user.d.ts.map +1 -0
  17. package/dist/auth/require-user.js +8 -0
  18. package/dist/auth/session-jwt.d.ts +7 -0
  19. package/dist/auth/session-jwt.d.ts.map +1 -0
  20. package/dist/auth/session-jwt.js +23 -0
  21. package/dist/composition.d.ts +67 -0
  22. package/dist/composition.d.ts.map +1 -0
  23. package/dist/composition.js +46 -0
  24. package/dist/document-download.d.ts +30 -0
  25. package/dist/document-download.d.ts.map +1 -0
  26. package/dist/document-download.js +102 -0
  27. package/dist/index.d.ts +17 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +9 -0
  30. package/dist/lib/db-selector.d.ts +24 -0
  31. package/dist/lib/db-selector.d.ts.map +1 -0
  32. package/dist/lib/db-selector.js +28 -0
  33. package/dist/lib/execution-ctx.d.ts +16 -0
  34. package/dist/lib/execution-ctx.d.ts.map +1 -0
  35. package/dist/lib/execution-ctx.js +16 -0
  36. package/dist/lib/public-paths.d.ts +19 -0
  37. package/dist/lib/public-paths.d.ts.map +1 -0
  38. package/dist/lib/public-paths.js +27 -0
  39. package/dist/lib/request-event-bus.d.ts +21 -0
  40. package/dist/lib/request-event-bus.d.ts.map +1 -0
  41. package/dist/lib/request-event-bus.js +43 -0
  42. package/dist/middleware/auth.d.ts +10 -0
  43. package/dist/middleware/auth.d.ts.map +1 -0
  44. package/dist/middleware/auth.js +280 -0
  45. package/dist/middleware/body-size.d.ts +7 -0
  46. package/dist/middleware/body-size.d.ts.map +1 -0
  47. package/dist/middleware/body-size.js +20 -0
  48. package/dist/middleware/cors.d.ts +6 -0
  49. package/dist/middleware/cors.d.ts.map +1 -0
  50. package/dist/middleware/cors.js +94 -0
  51. package/dist/middleware/db.d.ts +43 -0
  52. package/dist/middleware/db.d.ts.map +1 -0
  53. package/dist/middleware/db.js +78 -0
  54. package/dist/middleware/error-boundary.d.ts +5 -0
  55. package/dist/middleware/error-boundary.d.ts.map +1 -0
  56. package/dist/middleware/error-boundary.js +76 -0
  57. package/dist/middleware/idempotency-key.d.ts +97 -0
  58. package/dist/middleware/idempotency-key.d.ts.map +1 -0
  59. package/dist/middleware/idempotency-key.js +235 -0
  60. package/dist/middleware/index.d.ts +14 -0
  61. package/dist/middleware/index.d.ts.map +1 -0
  62. package/dist/middleware/index.js +13 -0
  63. package/dist/middleware/logger.d.ts +5 -0
  64. package/dist/middleware/logger.d.ts.map +1 -0
  65. package/dist/middleware/logger.js +27 -0
  66. package/dist/middleware/metrics.d.ts +55 -0
  67. package/dist/middleware/metrics.d.ts.map +1 -0
  68. package/dist/middleware/metrics.js +94 -0
  69. package/dist/middleware/public-cache.d.ts +44 -0
  70. package/dist/middleware/public-cache.d.ts.map +1 -0
  71. package/dist/middleware/public-cache.js +205 -0
  72. package/dist/middleware/rate-limit.d.ts +214 -0
  73. package/dist/middleware/rate-limit.d.ts.map +1 -0
  74. package/dist/middleware/rate-limit.js +240 -0
  75. package/dist/middleware/request-db.d.ts +42 -0
  76. package/dist/middleware/request-db.d.ts.map +1 -0
  77. package/dist/middleware/request-db.js +62 -0
  78. package/dist/middleware/require-actor.d.ts +28 -0
  79. package/dist/middleware/require-actor.d.ts.map +1 -0
  80. package/dist/middleware/require-actor.js +89 -0
  81. package/dist/middleware/require-permission.d.ts +9 -0
  82. package/dist/middleware/require-permission.d.ts.map +1 -0
  83. package/dist/middleware/require-permission.js +62 -0
  84. package/dist/middleware/security-headers.d.ts +10 -0
  85. package/dist/middleware/security-headers.d.ts.map +1 -0
  86. package/dist/middleware/security-headers.js +19 -0
  87. package/dist/module.d.ts +41 -0
  88. package/dist/module.d.ts.map +1 -0
  89. package/dist/module.js +1 -0
  90. package/dist/plugin.d.ts +66 -0
  91. package/dist/plugin.d.ts.map +1 -0
  92. package/dist/plugin.js +37 -0
  93. package/dist/public-capability.d.ts +46 -0
  94. package/dist/public-capability.d.ts.map +1 -0
  95. package/dist/public-capability.js +140 -0
  96. package/dist/public-document-delivery.d.ts +111 -0
  97. package/dist/public-document-delivery.d.ts.map +1 -0
  98. package/dist/public-document-delivery.js +234 -0
  99. package/dist/types.d.ts +318 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +29 -0
  102. package/dist/validation.d.ts +36 -0
  103. package/dist/validation.d.ts.map +1 -0
  104. package/dist/validation.js +106 -0
  105. package/package.json +156 -0
package/dist/app.js ADDED
@@ -0,0 +1,403 @@
1
+ import { createContainer, createEventBus, createQueryRunner, } from "@voyant-travel/core";
2
+ import { createOutboxEventStore } from "@voyant-travel/db/outbox";
3
+ import { Hono } from "hono";
4
+ import { containerToServiceResolver, makeFrameworkLogger, wireWorkflowRuntime, } from "./app-workflows.js";
5
+ import { createPathDbSelector } from "./lib/db-selector.js";
6
+ import { tryGetExecutionCtx } from "./lib/execution-ctx.js";
7
+ import { matchesPublicPath, normalizePathname } from "./lib/public-paths.js";
8
+ import { requestScopedEventBus } from "./lib/request-event-bus.js";
9
+ import { requireAuth } from "./middleware/auth.js";
10
+ import { DEFAULT_REQUEST_BODY_LIMIT_BYTES, requestBodyLimit } from "./middleware/body-size.js";
11
+ import { cors } from "./middleware/cors.js";
12
+ import { db } from "./middleware/db.js";
13
+ import { handleApiError, requestId } from "./middleware/error-boundary.js";
14
+ import { logger } from "./middleware/logger.js";
15
+ import { metrics } from "./middleware/metrics.js";
16
+ import { publicResponseCache } from "./middleware/public-cache.js";
17
+ import { rateLimit, resolveRateLimitStore, } from "./middleware/rate-limit.js";
18
+ import { requireActor } from "./middleware/require-actor.js";
19
+ import { securityHeaders } from "./middleware/security-headers.js";
20
+ import { expandHonoPlugins } from "./plugin.js";
21
+ function resolveSurfaceMountPath(prefix, path, fallback) {
22
+ const normalized = path?.trim();
23
+ if (!normalized) {
24
+ return `${prefix}/${fallback}`;
25
+ }
26
+ if (normalized === "/") {
27
+ return prefix;
28
+ }
29
+ return `${prefix}/${normalized.replace(/^\/+|\/+$/g, "")}`;
30
+ }
31
+ const WRITE_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
32
+ function resolveConfiguredRateLimitStore(config, env) {
33
+ if (!config?.store)
34
+ return undefined;
35
+ return typeof config.store === "function" ? config.store(env) : config.store;
36
+ }
37
+ function buildRateLimitPolicy(config, env, bucket, defaults) {
38
+ return {
39
+ bucket,
40
+ ...defaults,
41
+ store: resolveConfiguredRateLimitStore(config, env) ?? resolveRateLimitStore({ env }),
42
+ };
43
+ }
44
+ export function createApp(config) {
45
+ const app = new Hono();
46
+ app.onError(handleApiError);
47
+ // Expand plugins into their constituent modules/extensions before mounting
48
+ const expanded = config.plugins ? expandHonoPlugins(config.plugins) : null;
49
+ const allModules = [...(config.modules ?? []), ...(expanded?.modules ?? [])];
50
+ const allExtensions = [...(config.extensions ?? []), ...(expanded?.extensions ?? [])];
51
+ const eventBus = config.eventBus ?? createEventBus();
52
+ const query = typeof config.query === "function"
53
+ ? config.query
54
+ : config.query
55
+ ? createQueryRunner(config.query)
56
+ : undefined;
57
+ // Module container — registered services are resolvable from routes
58
+ const container = createContainer();
59
+ for (const mod of allModules) {
60
+ if (mod.module.service !== undefined) {
61
+ container.register(mod.module.name, mod.module.service);
62
+ }
63
+ }
64
+ for (const sub of expanded?.subscribers ?? []) {
65
+ eventBus.subscribe(sub.event, sub.handler, { inline: sub.inline ?? false });
66
+ }
67
+ // ---- Workflow runtime wiring (synchronous setup; manifest registration
68
+ // + EventBus forwarder run inside the lazy bootstrap below) ----
69
+ //
70
+ // We collect `workflows` + `eventFilters` from every module and plugin
71
+ // here so the failure mode for "duplicate workflow id across modules"
72
+ // surfaces at construction time (per architecture doc §18, the workflow
73
+ // runtime is fail-closed).
74
+ const collectedWorkflows = [];
75
+ const collectedFilters = [];
76
+ for (const mod of allModules) {
77
+ if (mod.module.workflows)
78
+ collectedWorkflows.push(...mod.module.workflows);
79
+ if (mod.module.eventFilters)
80
+ collectedFilters.push(...mod.module.eventFilters);
81
+ }
82
+ for (const plugin of config.plugins ?? []) {
83
+ if (plugin.workflows)
84
+ collectedWorkflows.push(...plugin.workflows);
85
+ if (plugin.eventFilters)
86
+ collectedFilters.push(...plugin.eventFilters);
87
+ }
88
+ // Validate duplicate workflow ids across modules + plugins. Same id from
89
+ // re-imports (HMR / shared bundles) is fine because identity is by id —
90
+ // we only flag genuinely-different definitions sharing an id.
91
+ if (config.workflows && collectedWorkflows.length > 0) {
92
+ const seen = new Map();
93
+ for (const wf of collectedWorkflows) {
94
+ const existing = seen.get(wf.id);
95
+ if (existing && existing !== wf) {
96
+ throw new Error(`[voyant] duplicate workflow id "${wf.id}" registered by multiple modules/plugins. ` +
97
+ `Workflow ids must be unique across the app — use a module-scoped prefix ` +
98
+ `(e.g. "${wf.id.includes(".") ? wf.id : `<module>.${wf.id}`}").`);
99
+ }
100
+ seen.set(wf.id, wf);
101
+ }
102
+ }
103
+ // Workflow driver construction is **deferred** to the lazy bootstrap
104
+ // path so CF-edge users (whose driver options come from `env.*`
105
+ // bindings only available at request time) can pass a function-of-
106
+ // bindings shape — `(env) => createCloudflareEdgeDriver({...})` —
107
+ // rather than constructing at module-load time. Mode 2 / InMemory
108
+ // users pass a direct factory; the framework adapts both shapes.
109
+ // See reviewer feedback P2.1 + architecture doc §6.3.
110
+ let workflowDriver;
111
+ let bootstrapPromise = null;
112
+ function ensureRuntimeBootstrapped(bindings) {
113
+ if (!bootstrapPromise) {
114
+ bootstrapPromise = (async () => {
115
+ const ctx = { bindings, container, eventBus };
116
+ // ---- Workflow runtime FIRST — fail-closed manifest registration
117
+ // and EventBus forwarder must be in place before any module
118
+ // bootstrap can emit. Otherwise a `module.bootstrap` that
119
+ // emits an event during its own bootstrap would route through
120
+ // a bus with no workflow forwarder yet, silently losing the
121
+ // event. Per architecture doc §21.22 + reviewer feedback P2.3.
122
+ if (config.workflows) {
123
+ // `driver` is always a function-of-bindings (per
124
+ // VoyantWorkflowsConfig — see types.ts + reviewer feedback P2.1).
125
+ // Mode 2 / InMemory users wrap with `() => createXxxDriver({...})`.
126
+ // CF-edge users use `(env) => createCloudflareEdgeDriver({ env.* })`.
127
+ // We invoke with bindings, then the resulting DriverFactory
128
+ // with framework deps.
129
+ const factoryDeps = {
130
+ services: containerToServiceResolver(container),
131
+ logger: makeFrameworkLogger(config.logger),
132
+ };
133
+ const factory = config.workflows.driver(bindings);
134
+ workflowDriver = factory(factoryDeps);
135
+ await wireWorkflowRuntime({
136
+ modules: allModules.map((m) => m.module),
137
+ collectedWorkflows,
138
+ collectedFilters,
139
+ driver: workflowDriver,
140
+ environment: config.workflows.environment ?? "development",
141
+ projectId: config.workflows.projectId ?? "default",
142
+ eventBus,
143
+ });
144
+ }
145
+ // Run each bootstrap in isolation — a single failing plugin/module/extension
146
+ // must not poison the cached promise and kill the whole app's request pipeline.
147
+ const runIsolated = async (label, fn) => {
148
+ if (!fn)
149
+ return;
150
+ try {
151
+ await fn(ctx);
152
+ }
153
+ catch (error) {
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ console.error(`[voyant] bootstrap failed for ${label}: ${message}`);
156
+ }
157
+ };
158
+ for (const plugin of config.plugins ?? []) {
159
+ await runIsolated(`plugin:${plugin.name}`, plugin.bootstrap);
160
+ }
161
+ for (const mod of allModules) {
162
+ await runIsolated(`module:${mod.module.name}`, mod.module.bootstrap);
163
+ }
164
+ for (const ext of allExtensions) {
165
+ await runIsolated(`extension:${ext.extension.module}/${ext.extension.name}`, ext.extension.bootstrap);
166
+ }
167
+ })();
168
+ }
169
+ return bootstrapPromise;
170
+ }
171
+ // Request ID header
172
+ app.use("*", requestId);
173
+ // Structured logger
174
+ app.use("*", logger(config.logger));
175
+ // Per-request metrics → Analytics Engine (no-op without the binding).
176
+ // Mounted before the cache middleware so cache hits are measured too.
177
+ if (config.metrics !== false) {
178
+ app.use("*", metrics());
179
+ }
180
+ // CORS (allowlist via env CORS_ALLOWLIST)
181
+ app.use("*", cors());
182
+ if (config.securityHeaders !== false) {
183
+ app.use("*", securityHeaders(config.securityHeaders));
184
+ }
185
+ if (config.requestBodyLimit !== false) {
186
+ app.use("*", requestBodyLimit({
187
+ maxBytes: config.requestBodyLimit?.maxBytes ?? DEFAULT_REQUEST_BODY_LIMIT_BYTES,
188
+ }));
189
+ }
190
+ if (config.rateLimit !== false) {
191
+ const rateLimitConfig = config.rateLimit;
192
+ if (rateLimitConfig?.auth !== false) {
193
+ const authRule = rateLimitConfig?.auth ?? { max: 10, windowSeconds: 60 };
194
+ app.use("/auth/*", async (c, next) => {
195
+ if (c.req.method !== "POST")
196
+ return next();
197
+ return rateLimit(buildRateLimitPolicy(rateLimitConfig, c.env, "auth", authRule))(c, next);
198
+ });
199
+ }
200
+ if (rateLimitConfig?.publicWrite !== false) {
201
+ const publicWriteRule = rateLimitConfig?.publicWrite ?? { max: 60, windowSeconds: 60 };
202
+ app.use("*", async (c, next) => {
203
+ if (!WRITE_METHODS.has(c.req.method))
204
+ return next();
205
+ const pathname = normalizePathname(new URL(c.req.url).pathname);
206
+ const isPublicWrite = pathname.startsWith("/v1/public/") ||
207
+ matchesPublicPath(pathname, config.publicPaths ?? []);
208
+ if (!isPublicWrite)
209
+ return next();
210
+ return rateLimit(buildRateLimitPolicy(rateLimitConfig, c.env, "public-write", publicWriteRule))(c, next);
211
+ });
212
+ }
213
+ }
214
+ // Shared response cache for the public surface. Mounted BEFORE the
215
+ // runtime bootstrap on purpose: a cache hit skips module-graph
216
+ // instantiation, auth, and the per-request db client entirely — the
217
+ // hit path allocates nothing but the cached body. Only responses a
218
+ // route explicitly marks `Cache-Control: public, s-maxage=…` are
219
+ // stored (see middleware docs).
220
+ if (config.publicCache !== false) {
221
+ app.use("*", publicResponseCache(config.publicCache ?? {}));
222
+ }
223
+ // Per-request outbox store factory: emits happen in route handlers,
224
+ // after the db middleware ran, so the request's db client is resolved
225
+ // lazily at capture time. Outbox writes are single statements — the
226
+ // cheap http client on non-transactional surfaces handles them fine.
227
+ const buildOutboxStore = config.outbox
228
+ ? (c) => createOutboxEventStore(() => {
229
+ const requestDb = c.get("db");
230
+ if (!requestDb) {
231
+ throw new Error("[voyant] outbox capture needs the per-request db — emit ran before the db middleware");
232
+ }
233
+ return requestDb;
234
+ })
235
+ : undefined;
236
+ app.use("*", async (c, next) => {
237
+ c.set("container", container);
238
+ // Request-scoped bus: emits defer non-`inline` subscribers past the
239
+ // response via waitUntil, so handlers doing outbound HTTP (CMS sync,
240
+ // e-invoicing) no longer add their latency to every mutation.
241
+ //
242
+ // With `outbox: true`, emits are also durable (persisted before
243
+ // delivery, retried by `drainOutbox` from @voyant-travel/db/outbox) —
244
+ // INCLUDING on runtimes without an ExecutionContext (Node/headless),
245
+ // where emits await handlers inline but still capture through the
246
+ // store. Only when there's neither a scheduler nor a store does the
247
+ // raw bus go on the context unwrapped.
248
+ const executionCtx = tryGetExecutionCtx(c);
249
+ const outboxStore = buildOutboxStore?.(c);
250
+ c.set("eventBus", executionCtx || outboxStore
251
+ ? requestScopedEventBus(eventBus, executionCtx ? (pending) => executionCtx.waitUntil(pending) : undefined, outboxStore)
252
+ : eventBus);
253
+ if (config.link) {
254
+ c.set("link", config.link);
255
+ }
256
+ if (query) {
257
+ c.set("query", query);
258
+ }
259
+ // Bootstrap (fires once, idempotent) — resolves the workflow driver
260
+ // with c.env-supplied bindings on the first request, so deferred
261
+ // driver construction sees real runtime bindings (reviewer P2.1).
262
+ await ensureRuntimeBootstrapped(c.env);
263
+ if (workflowDriver) {
264
+ // Surfaced on `c.var.workflowDriver` so HTTP route handlers can
265
+ // call `driver.trigger(...)` directly without re-resolving from
266
+ // the container. Also used by the optional HTTP ingest adapter
267
+ // (`mountHttpIngestAdapter` from `@voyant-travel/workflows/http-ingest`).
268
+ c.set("workflowDriver", workflowDriver);
269
+ }
270
+ return next();
271
+ });
272
+ // Health check (public, no auth)
273
+ app.get("/health", (c) => c.json({ status: "ok" }));
274
+ // App-owned auth handler (must be before auth middleware — these routes are public)
275
+ const authHandler = config.auth?.handler;
276
+ if (authHandler) {
277
+ app.all("/auth/*", async (c) => {
278
+ const authApp = authHandler(c.env);
279
+ return authApp.fetch(c.req.raw, c.env, c.executionCtx);
280
+ });
281
+ }
282
+ // Transactional surface map: a request must be served by a
283
+ // transaction-capable db client when its path belongs to (a) a module
284
+ // declaring `requiresTransactionalDb`, (b) a module targeted by an
285
+ // extension that declares it (extensions mount under the target
286
+ // module's prefix — e.g. catalog-authoring's compose routes live under
287
+ // /v1/admin/products), or (c) a template-supplied extra path
288
+ // (`dbTransactionalPaths` — for additionalRoutes / adapter-wired flows
289
+ // like the catalog booking engine whose transactionality depends on
290
+ // starter wiring).
291
+ const txModuleNames = new Set(allModules.filter((m) => m.module.requiresTransactionalDb).map((m) => m.module.name));
292
+ for (const ext of allExtensions) {
293
+ if (ext.extension.requiresTransactionalDb)
294
+ txModuleNames.add(ext.extension.module);
295
+ }
296
+ const txRequiringModules = [...txModuleNames];
297
+ const txPrefixes = [...(config.dbTransactionalPaths ?? [])];
298
+ for (const name of txModuleNames) {
299
+ // `/v1/public/<name>` is added unconditionally (not only when the
300
+ // module mounts publicRoutes): other modules mounted at the public
301
+ // root can serve paths under a flagged module's segment — e.g.
302
+ // storefront (publicPath "/") handles
303
+ // /v1/public/bookings/sessions/bootstrap, which reaches
304
+ // bookings' transactional reserve flow.
305
+ txPrefixes.push(`/v1/admin/${name}`, `/v1/${name}`, `/v1/public/${name}`);
306
+ }
307
+ for (const mod of allModules) {
308
+ if (txModuleNames.has(mod.module.name) && mod.publicRoutes) {
309
+ txPrefixes.push(resolveSurfaceMountPath("/v1/public", mod.publicPath, mod.module.name));
310
+ }
311
+ }
312
+ for (const ext of allExtensions) {
313
+ if (txModuleNames.has(ext.extension.module) && ext.publicRoutes) {
314
+ txPrefixes.push(resolveSurfaceMountPath("/v1/public", ext.publicPath, ext.extension.module));
315
+ }
316
+ }
317
+ // With a `dbTransactional` factory, requests are routed per surface:
318
+ // transactional prefixes get it, everything else gets the cheap
319
+ // default (typically neon-http — no per-request connection handshake).
320
+ // Without it, `config.db` serves everything as before.
321
+ const dbSource = config.dbTransactional
322
+ ? createPathDbSelector({
323
+ defaultFactory: config.db,
324
+ transactionalFactory: config.dbTransactional,
325
+ transactionalPrefixes: txPrefixes,
326
+ })
327
+ : config.db;
328
+ // Auth middleware for all other routes
329
+ app.use("*", requireAuth(dbSource, { publicPaths: config.publicPaths, auth: config.auth }));
330
+ // DB middleware — sets c.var.db for all downstream handlers.
331
+ // Pass the list of modules that need interactive transactions so the
332
+ // middleware can throw a clear startup-style error on first request if
333
+ // the wired adapter is neon-http (which doesn't support db.transaction).
334
+ app.use("*", db(dbSource, { requiresTransactionalDb: txRequiringModules }));
335
+ // Actor guards for the two API surfaces
336
+ app.use("/v1/admin/*", requireActor("staff"));
337
+ app.use("/v1/public/*", requireActor("customer", "partner", "supplier"));
338
+ const requireLegacyActor = requireActor("staff");
339
+ app.use("/v1/*", (c, next) => {
340
+ const pathname = new URL(c.req.url).pathname;
341
+ if (pathname.startsWith("/v1/admin/") || pathname.startsWith("/v1/public/")) {
342
+ return next();
343
+ }
344
+ return requireLegacyActor(c, next);
345
+ });
346
+ // Admin capability discovery — GET /v1/admin/_meta/capabilities. A built-in
347
+ // framework route (like /health), mounted only when the deployment supplies
348
+ // the operation catalogue via `config.adminMeta` (from
349
+ // `@voyant-travel/admin-contracts`) — keeping `@voyant-travel/hono` decoupled from it.
350
+ // Guarded by the `/v1/admin/*` staff actor guard above.
351
+ if (config.adminMeta) {
352
+ const adminMeta = config.adminMeta;
353
+ app.get("/v1/admin/_meta/capabilities", (c) => c.json({
354
+ contractVersion: adminMeta.contractVersion,
355
+ ...(adminMeta.deploymentVersion ? { deploymentVersion: adminMeta.deploymentVersion } : {}),
356
+ modules: allModules.map((m) => m.module.name),
357
+ operations: adminMeta.operations,
358
+ actor: c.get("actor"),
359
+ scopes: c.get("scopes"),
360
+ }));
361
+ }
362
+ // Mount module routes
363
+ for (const mod of allModules) {
364
+ if (mod.adminRoutes) {
365
+ app.route(`/v1/admin/${mod.module.name}`, mod.adminRoutes);
366
+ }
367
+ if (mod.publicRoutes) {
368
+ app.route(resolveSurfaceMountPath("/v1/public", mod.publicPath, mod.module.name), mod.publicRoutes);
369
+ }
370
+ if (mod.routes) {
371
+ app.route(`/v1/${mod.module.name}`, mod.routes);
372
+ }
373
+ }
374
+ // Mount extension routes
375
+ for (const ext of allExtensions) {
376
+ if (ext.adminRoutes) {
377
+ app.route(`/v1/admin/${ext.extension.module}`, ext.adminRoutes);
378
+ }
379
+ if (ext.publicRoutes) {
380
+ app.route(resolveSurfaceMountPath("/v1/public", ext.publicPath, ext.extension.module), ext.publicRoutes);
381
+ }
382
+ if (ext.routes) {
383
+ app.route(`/v1/${ext.extension.module}`, ext.routes);
384
+ }
385
+ }
386
+ // Additional routes
387
+ if (config.additionalRoutes) {
388
+ config.additionalRoutes(app);
389
+ }
390
+ // Attach `ready()` directly to the Hono instance. Fires the lazy
391
+ // bootstrap with the supplied bindings (or `{}` for back-compat with
392
+ // Mode 2 / InMemory drivers that ignore them). Production code never
393
+ // calls `ready()` — the first request triggers the same boot via
394
+ // `ensureRuntimeBootstrapped(c.env)`. Tests + Mode 2 sibling processes
395
+ // use this so the time wheel + manifest registration happen without
396
+ // traffic; CF-edge users that want eager boot must pass the real `env`
397
+ // (otherwise the memoized bootstrap promise locks in a driver built
398
+ // from `{}` and every later request reuses that broken instance).
399
+ const augmented = app;
400
+ augmented.eventBus = eventBus;
401
+ augmented.ready = (bindings) => ensureRuntimeBootstrapped(bindings ?? {});
402
+ return augmented;
403
+ }
@@ -0,0 +1,16 @@
1
+ export declare function randomBytesHex(lengthBytes: number): string;
2
+ export declare function sha256Hex(input: string | Uint8Array): Promise<string>;
3
+ export declare function constantTimeEqual(a: string, b: string): boolean;
4
+ export declare function generateNumericCode(length: number): string;
5
+ /**
6
+ * SHA-256 hash a string using Web Crypto API.
7
+ * Returns the hash as a base64url string without padding,
8
+ * matching Better Auth's `defaultKeyHasher` format.
9
+ */
10
+ export declare function sha256Base64Url(input: string): Promise<string>;
11
+ /**
12
+ * Unsign a Better Auth session cookie.
13
+ * Better Auth signs cookies as: encodeURIComponent(value + "." + base64(HMAC-SHA256(value, secret)))
14
+ */
15
+ export declare function unsignCookie(rawCookieValue: string, secret: string): Promise<string | null>;
16
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/auth/crypto.ts"],"names":[],"mappings":"AAAA,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAK3E;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAO/D;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUpE;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwBjG"}
@@ -0,0 +1,66 @@
1
+ export function randomBytesHex(lengthBytes) {
2
+ const bytes = new Uint8Array(lengthBytes);
3
+ crypto.getRandomValues(bytes);
4
+ return Array.from(bytes)
5
+ .map((b) => b.toString(16).padStart(2, "0"))
6
+ .join("");
7
+ }
8
+ export async function sha256Hex(input) {
9
+ const data = typeof input === "string" ? new TextEncoder().encode(input) : input;
10
+ const hash = await crypto.subtle.digest("SHA-256", data.buffer);
11
+ const arr = Array.from(new Uint8Array(hash));
12
+ return arr.map((b) => b.toString(16).padStart(2, "0")).join("");
13
+ }
14
+ export function constantTimeEqual(a, b) {
15
+ const length = Math.max(a.length, b.length, 1);
16
+ let diff = a.length === b.length ? 0 : 1;
17
+ for (let i = 0; i < length; i++) {
18
+ diff |= (a.charCodeAt(i) | 0) ^ (b.charCodeAt(i) | 0);
19
+ }
20
+ return diff === 0;
21
+ }
22
+ export function generateNumericCode(length) {
23
+ const max = 10 ** length;
24
+ const buf = new Uint32Array(1);
25
+ crypto.getRandomValues(buf);
26
+ const code = Number((buf[0] ?? 0) % max);
27
+ return String(code).padStart(length, "0");
28
+ }
29
+ /**
30
+ * SHA-256 hash a string using Web Crypto API.
31
+ * Returns the hash as a base64url string without padding,
32
+ * matching Better Auth's `defaultKeyHasher` format.
33
+ */
34
+ export async function sha256Base64Url(input) {
35
+ const data = new TextEncoder().encode(input);
36
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
37
+ const bytes = new Uint8Array(hashBuffer);
38
+ let binary = "";
39
+ for (const byte of bytes) {
40
+ binary += String.fromCharCode(byte);
41
+ }
42
+ const base64 = btoa(binary);
43
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
44
+ }
45
+ /**
46
+ * Unsign a Better Auth session cookie.
47
+ * Better Auth signs cookies as: encodeURIComponent(value + "." + base64(HMAC-SHA256(value, secret)))
48
+ */
49
+ export async function unsignCookie(rawCookieValue, secret) {
50
+ const decoded = decodeURIComponent(rawCookieValue);
51
+ const lastDot = decoded.lastIndexOf(".");
52
+ if (lastDot < 1)
53
+ return null;
54
+ const value = decoded.substring(0, lastDot);
55
+ const signature = decoded.substring(lastDot + 1);
56
+ if (signature.length !== 44 || !signature.endsWith("="))
57
+ return null;
58
+ const encoder = new TextEncoder();
59
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
60
+ const sigBinStr = atob(signature);
61
+ const sigBytes = new Uint8Array(sigBinStr.length);
62
+ for (let i = 0; i < sigBinStr.length; i++)
63
+ sigBytes[i] = sigBinStr.charCodeAt(i);
64
+ const valid = await crypto.subtle.verify("HMAC", key, sigBytes, encoder.encode(value));
65
+ return valid ? value : null;
66
+ }
@@ -0,0 +1,5 @@
1
+ export { constantTimeEqual, generateNumericCode, randomBytesHex, sha256Base64Url, sha256Hex, unsignCookie, } from "./crypto.js";
2
+ export { requireUserId } from "./require-user.js";
3
+ export type { SessionAuthContext } from "./session-jwt.js";
4
+ export { extractBearerToken, verifySession } from "./session-jwt.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,SAAS,EACT,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,3 @@
1
+ export { constantTimeEqual, generateNumericCode, randomBytesHex, sha256Base64Url, sha256Hex, unsignCookie, } from "./crypto.js";
2
+ export { requireUserId } from "./require-user.js";
3
+ export { extractBearerToken, verifySession } from "./session-jwt.js";
@@ -0,0 +1,3 @@
1
+ import type { Context } from "hono";
2
+ export declare function requireUserId(c: Context): string;
3
+ //# sourceMappingURL=require-user.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-user.d.ts","sourceRoot":"","sources":["../../src/auth/require-user.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAInC,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAOhD"}
@@ -0,0 +1,8 @@
1
+ import { UnauthorizedApiError } from "../validation.js";
2
+ export function requireUserId(c) {
3
+ const userId = c.get("userId");
4
+ if (!userId) {
5
+ throw new UnauthorizedApiError();
6
+ }
7
+ return userId;
8
+ }
@@ -0,0 +1,7 @@
1
+ export interface SessionAuthContext {
2
+ userId: string;
3
+ sessionId?: string;
4
+ }
5
+ export declare function verifySession(token: string, secretKey: string): Promise<SessionAuthContext>;
6
+ export declare function extractBearerToken(authHeader: string | undefined): string | null;
7
+ //# sourceMappingURL=session-jwt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-jwt.d.ts","sourceRoot":"","sources":["../../src/auth/session-jwt.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAWjG;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAWhF"}
@@ -0,0 +1,23 @@
1
+ import { verifySessionClaims } from "@voyant-travel/utils/session-claims";
2
+ export async function verifySession(token, secretKey) {
3
+ const payload = await verifySessionClaims(token, secretKey);
4
+ if (!payload) {
5
+ throw new Error("Invalid or expired token");
6
+ }
7
+ return {
8
+ userId: payload.userId,
9
+ sessionId: payload.sessionId,
10
+ };
11
+ }
12
+ export function extractBearerToken(authHeader) {
13
+ if (!authHeader)
14
+ return null;
15
+ const parts = authHeader.trim().split(/\s+/);
16
+ if (parts.length !== 2)
17
+ return null;
18
+ const scheme = parts[0];
19
+ const token = parts[1];
20
+ if (!scheme || !token || !/^bearer$/i.test(scheme))
21
+ return null;
22
+ return token;
23
+ }
@@ -0,0 +1,67 @@
1
+ import type { HonoExtension, HonoModule } from "./module.js";
2
+ /**
3
+ * Manifest-driven runtime composition.
4
+ *
5
+ * The `voyant.config.ts` manifest already drives the migration/schema side
6
+ * (see `@voyant-travel/cli` `db doctor` and `docs/architecture/migration-resilience-rfc.md`).
7
+ * This module lets the SAME manifest drive runtime composition: instead of a
8
+ * template hand-listing `createApp({ modules, extensions })`, it registers a
9
+ * factory per manifest entry and derives the arrays from the manifest.
10
+ *
11
+ * The factories receive a typed **capability container** — the template's
12
+ * deployment-specific capabilities (storage, FX, notification providers,
13
+ * document-download resolvers, …) gathered in one place. Because Voyant runs
14
+ * on Cloudflare Workers (per-request `bindings`), capabilities are typically
15
+ * bindings-deferred closures (`(bindings) => T`), so the container is a plain
16
+ * typed value resolved per request rather than a boot-time singleton.
17
+ */
18
+ /** A manifest entry: a bare specifier or `{ resolve, options }`. */
19
+ export type CompositionEntry = string | {
20
+ resolve: string;
21
+ options?: Record<string, unknown>;
22
+ };
23
+ /** The subset of `VoyantConfig` this composer reads. */
24
+ export interface CompositionManifest {
25
+ modules?: CompositionEntry[];
26
+ extensions?: CompositionEntry[];
27
+ }
28
+ /** Context handed to every factory: the capability container + per-entry options. */
29
+ export interface CompositionContext<TCapabilities> {
30
+ capabilities: TCapabilities;
31
+ options: Record<string, unknown>;
32
+ }
33
+ export type ModuleFactory<TCapabilities> = (ctx: CompositionContext<TCapabilities>) => HonoModule | HonoModule[];
34
+ export type ExtensionFactory<TCapabilities> = (ctx: CompositionContext<TCapabilities>) => HonoExtension;
35
+ /**
36
+ * Maps manifest specifiers to the factory that builds the runtime unit. Keys
37
+ * MUST match the `voyant.config.ts` `modules` / `extensions` specifiers.
38
+ */
39
+ export interface CompositionRegistry<TCapabilities> {
40
+ modules: Record<string, ModuleFactory<TCapabilities>>;
41
+ extensions?: Record<string, ExtensionFactory<TCapabilities>>;
42
+ }
43
+ export interface ComposedApp {
44
+ modules: HonoModule[];
45
+ extensions: HonoExtension[];
46
+ }
47
+ /**
48
+ * Derive the `createApp({ modules, extensions })` arrays from a manifest by
49
+ * looking each entry up in the registry, **preserving manifest order** (mount
50
+ * + hook-registration order is significant). Throws if the manifest lists an
51
+ * entry the registry has no factory for — so "added to the manifest but not
52
+ * wired" fails loudly at boot rather than silently dropping a module.
53
+ */
54
+ export declare function composeFromManifest<TCapabilities>(manifest: CompositionManifest, registry: CompositionRegistry<TCapabilities>, capabilities: TCapabilities): ComposedApp;
55
+ export interface ManifestRegistryDiff {
56
+ /** In the manifest but with no registered factory. */
57
+ missingFactories: string[];
58
+ /** Registered factories not referenced by the manifest. */
59
+ orphanFactories: string[];
60
+ }
61
+ /**
62
+ * Pure parity check between a manifest's entries and a registry's keys, for
63
+ * tooling (`voyant db doctor`). Reports manifest entries with no factory and
64
+ * factories the manifest never references.
65
+ */
66
+ export declare function diffManifestRegistry(manifestEntries: CompositionEntry[], registryKeys: string[]): ManifestRegistryDiff;
67
+ //# sourceMappingURL=composition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composition.d.ts","sourceRoot":"","sources":["../src/composition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE5D;;;;;;;;;;;;;;;GAeG;AAEH,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAA;AAE9F,wDAAwD;AACxD,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAChC;AAED,qFAAqF;AACrF,MAAM,WAAW,kBAAkB,CAAC,aAAa;IAC/C,YAAY,EAAE,aAAa,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED,MAAM,MAAM,aAAa,CAAC,aAAa,IAAI,CACzC,GAAG,EAAE,kBAAkB,CAAC,aAAa,CAAC,KACnC,UAAU,GAAG,UAAU,EAAE,CAAA;AAC9B,MAAM,MAAM,gBAAgB,CAAC,aAAa,IAAI,CAC5C,GAAG,EAAE,kBAAkB,CAAC,aAAa,CAAC,KACnC,aAAa,CAAA;AAElB;;;GAGG;AACH,MAAM,WAAW,mBAAmB,CAAC,aAAa;IAChD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC,CAAA;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAC,CAAA;CAC7D;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,UAAU,EAAE,aAAa,EAAE,CAAA;CAC5B;AAUD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAC/C,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,mBAAmB,CAAC,aAAa,CAAC,EAC5C,YAAY,EAAE,aAAa,GAC1B,WAAW,CA0Bb;AAED,MAAM,WAAW,oBAAoB;IACnC,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,2DAA2D;IAC3D,eAAe,EAAE,MAAM,EAAE,CAAA;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,eAAe,EAAE,gBAAgB,EAAE,EACnC,YAAY,EAAE,MAAM,EAAE,GACrB,oBAAoB,CAOtB"}