@voyant-travel/hono 0.127.2 → 0.128.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.
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @voyant-travel/hono
2
2
 
3
- Hono transport adapter for Voyant. Provides `createApp()`, middleware, auth
4
- helpers, and plugin expansion for mounting Voyant modules behind a Hono app.
3
+ Voyant's sole server API runtime implementation. Provides `createApp()`,
4
+ middleware, auth helpers, and API bundle expansion for mounting Voyant modules
5
+ behind a Hono app.
5
6
 
6
7
  ## Install
7
8
 
@@ -52,8 +53,8 @@ The middleware chain is: container → requestId → logger → errorBoundary
52
53
  | --- | --- |
53
54
  | `.` | Barrel re-exports |
54
55
  | `./app` | `createApp` factory |
55
- | `./module` | `HonoModule`, `HonoExtension` contracts |
56
- | `./bundle` | `HonoBundle`, `defineHonoBundle`, `expandHonoBundles` |
56
+ | `./module` | `ApiModule`, `ApiExtension` contracts |
57
+ | `./bundle` | `ApiBundle`, `defineApiBundle`, `expandApiBundles` |
57
58
  | `./middleware` | All middleware re-exports |
58
59
  | `./middleware/auth` | `requireAuth` session/API-key/JWT auth |
59
60
  | `./middleware/cors` | CORS configuration |
@@ -1,4 +1,4 @@
1
- import type { HonoExtension, HonoModule } from "./module.js";
1
+ import type { ApiExtension, ApiModule } from "./module.js";
2
2
  /**
3
3
  * Assemble the anonymous-access allow-list (ADR-0008) from module/extension
4
4
  * `anonymous` declarations, unioned with any explicit `publicPaths` the
@@ -11,4 +11,4 @@ import type { HonoExtension, HonoModule } from "./module.js";
11
11
  * deterministic, snapshot-auditable result; the global list is what `requireAuth`
12
12
  * matches to skip auth (and stamp `actor: "customer"`).
13
13
  */
14
- export declare function assembleAnonymousPaths(modules: readonly HonoModule[], extensions: readonly HonoExtension[], explicit?: readonly string[]): string[];
14
+ export declare function assembleAnonymousPaths(modules: readonly ApiModule[], extensions: readonly ApiExtension[], explicit?: readonly string[]): string[];
@@ -31,7 +31,7 @@ export function assembleAnonymousPaths(modules, extensions, explicit = []) {
31
31
  // at `/v1/{name}`, matching the mount in `app.ts`. Parameterized/wildcard paths
32
32
  // are skipped (the literal `matchesPublicPath` matcher can't match them) and
33
33
  // must be declared via `anonymous` if ever needed.
34
- // biome-ignore lint/suspicious/noExplicitAny: Hono sub-apps have varied env generics -- owner: hono; mirrors the HonoModule.webhookRoutes suppression.
34
+ // biome-ignore lint/suspicious/noExplicitAny: Hono sub-apps have varied env generics -- owner: hono; mirrors the ApiModule.webhookRoutes suppression.
35
35
  const addWebhooks = (name, routes) => {
36
36
  if (!routes)
37
37
  return;
package/dist/app.js CHANGED
@@ -5,7 +5,7 @@ import { createContainer, createEventBus, createQueryRunner, } from "@voyant-tra
5
5
  import { createLinkServiceFactory } from "@voyant-travel/db/links";
6
6
  import { assembleAnonymousPaths } from "./anonymous-paths.js";
7
7
  import { containerToServiceResolver, makeFrameworkLogger, wireWorkflowRuntime, } from "./app-workflows.js";
8
- import { expandHonoBundles, isLazyHonoBundle, } from "./bundle.js";
8
+ import { expandApiBundles, isLazyApiBundle, } from "./bundle.js";
9
9
  import { mountLazyRoutePaths, mountLazyRoutesAt } from "./lazy-routes.js";
10
10
  import { mountAuthForwarding } from "./lib/auth-forward.js";
11
11
  import { createPathDbSelector } from "./lib/db-selector.js";
@@ -19,7 +19,6 @@ import { cors } from "./middleware/cors.js";
19
19
  import { db } from "./middleware/db.js";
20
20
  import { handleApiError, requestId } from "./middleware/error-boundary.js";
21
21
  import { logger } from "./middleware/logger.js";
22
- import { metrics } from "./middleware/metrics.js";
23
22
  import { publicResponseCache } from "./middleware/public-cache.js";
24
23
  import { rateLimit, resolveRateLimitStore, } from "./middleware/rate-limit.js";
25
24
  import { requireActor } from "./middleware/require-actor.js";
@@ -80,14 +79,14 @@ export function mountApp(config) {
80
79
  throw new Error(`Duplicate bundle name: "${plugin.name}"`);
81
80
  }
82
81
  pluginNames.add(plugin.name);
83
- if (isLazyHonoBundle(plugin))
82
+ if (isLazyApiBundle(plugin))
84
83
  lazyPlugins.push(plugin);
85
84
  else
86
85
  eagerPlugins.push(plugin);
87
86
  }
88
87
  // Expand eager plugins into their constituent modules/extensions before
89
88
  // mounting. Lazy plugins keep only their static metadata in the eager closure.
90
- const expanded = eagerPlugins.length > 0 ? expandHonoBundles(eagerPlugins) : null;
89
+ const expanded = eagerPlugins.length > 0 ? expandApiBundles(eagerPlugins) : null;
91
90
  const allModules = [...(config.modules ?? []), ...(expanded?.modules ?? [])];
92
91
  const allExtensions = [...(config.extensions ?? []), ...(expanded?.extensions ?? [])];
93
92
  const linkDefinitions = [...(config.linkDefinitions ?? []), ...(expanded?.links ?? [])];
@@ -252,7 +251,7 @@ export function mountApp(config) {
252
251
  const pending = bundles.filter((bundle) => !expandedLazyBundleNames.has(bundle.name));
253
252
  if (pending.length === 0)
254
253
  return;
255
- const lazyExpanded = expandHonoBundles(pending);
254
+ const lazyExpanded = expandApiBundles(pending);
256
255
  for (const bundle of pending)
257
256
  expandedLazyBundleNames.add(bundle.name);
258
257
  allModules.push(...lazyExpanded.modules);
@@ -372,11 +371,6 @@ export function mountApp(config) {
372
371
  app.use("*", requestId);
373
372
  // Structured logger
374
373
  app.use("*", logger(config.logger));
375
- // Per-request metrics → Analytics Engine (no-op without the binding).
376
- // Mounted before the cache middleware so cache hits are measured too.
377
- if (config.metrics !== false) {
378
- app.use("*", metrics());
379
- }
380
374
  // CORS (allowlist via env CORS_ALLOWLIST)
381
375
  app.use("*", cors());
382
376
  if (config.securityHeaders !== false) {
@@ -604,7 +598,7 @@ export function mountApp(config) {
604
598
  }
605
599
  function buildBundleRouteApp(bundle) {
606
600
  const pluginRoutes = new OpenAPIHono();
607
- const bundleExpanded = expandHonoBundles([bundle]);
601
+ const bundleExpanded = expandApiBundles([bundle]);
608
602
  for (const mod of bundleExpanded.modules)
609
603
  mountModuleRoutesInto(pluginRoutes, mod);
610
604
  for (const ext of bundleExpanded.extensions)
package/dist/bundle.d.ts CHANGED
@@ -1,27 +1,27 @@
1
1
  import type { BootstrapHandler, EventFilterDescriptor, LinkDefinition, Subscriber, WorkflowDescriptor } from "@voyant-travel/core";
2
- import type { HonoExtension, HonoModule } from "./module.js";
2
+ import type { ApiExtension, ApiModule } from "./module.js";
3
3
  /**
4
- * Hono-flavoured bundle contribution surface.
4
+ * Reusable server API contribution surface.
5
5
  *
6
- * `@voyant-travel/hono` is the default HTTP transport adapter for Voyant. The
7
- * `HonoBundle` describes reusable packages that contribute {@link HonoModule}
8
- * / {@link HonoExtension} wrappers that can carry HTTP routes.
6
+ * `@voyant-travel/hono` is Voyant's sole server API runtime implementation.
7
+ * `ApiBundle` describes packages that contribute {@link ApiModule} /
8
+ * {@link ApiExtension} wrappers carrying HTTP routes.
9
9
  *
10
10
  * Registered via `createApp({ plugins: [...] })` — the app factory expands
11
11
  * each bundle into the underlying modules, extensions, subscribers, and link
12
12
  * definitions before mounting them.
13
13
  */
14
- export interface HonoBundle {
14
+ export interface ApiBundle {
15
15
  /** Unique bundle identifier (e.g. "payload-cms", "bokun"). */
16
16
  name: string;
17
17
  /** Optional version tag for diagnostics. */
18
18
  version?: string;
19
19
  /** Optional lazy runtime bootstrap executed once per app/isolate. */
20
20
  bootstrap?: BootstrapHandler;
21
- /** Hono modules (module + routes) contributed by the plugin. */
22
- modules?: HonoModule[];
23
- /** Hono extensions (extension + routes) contributed by the plugin. */
24
- extensions?: HonoExtension[];
21
+ /** API modules (module + routes) contributed by the plugin. */
22
+ modules?: ApiModule[];
23
+ /** API extensions (extension + routes) contributed by the plugin. */
24
+ extensions?: ApiExtension[];
25
25
  /** Event subscribers wired to the caller's event bus, when provided. */
26
26
  subscribers?: Subscriber[];
27
27
  /** Link definitions contributed by the plugin. */
@@ -49,15 +49,15 @@ export interface HonoBundle {
49
49
  */
50
50
  eventFilters?: readonly EventFilterDescriptor[];
51
51
  }
52
- declare const LAZY_HONO_BUNDLE: unique symbol;
52
+ declare const LAZY_API_BUNDLE: unique symbol;
53
53
  /**
54
54
  * Lazy bundle declaration. The bundle's heavy runtime graph is imported only
55
55
  * when a declared route matcher is hit, unless `loadOnBootstrap` asks the app
56
56
  * to load it during request/headless bootstrap. `name` and `anonymous` remain
57
57
  * eager metadata so duplicate checks and auth allow-lists stay fail-closed.
58
58
  */
59
- export interface LazyHonoBundle {
60
- readonly [LAZY_HONO_BUNDLE]: true;
59
+ export interface LazyApiBundle {
60
+ readonly [LAZY_API_BUNDLE]: true;
61
61
  /** Unique bundle identifier matching the loaded bundle's `name`. */
62
62
  name: string;
63
63
  /** Optional version tag for diagnostics. */
@@ -94,27 +94,27 @@ export interface LazyHonoBundle {
94
94
  */
95
95
  loadOnBootstrap?: boolean;
96
96
  /** Loads and constructs the real bundle. Memoized by `mountApp`. */
97
- load: () => Promise<HonoBundle>;
97
+ load: () => Promise<ApiBundle>;
98
98
  }
99
- export type HonoBundleInput = HonoBundle | LazyHonoBundle;
99
+ export type ApiBundleInput = ApiBundle | LazyApiBundle;
100
100
  /**
101
101
  * Identity helper — returns the bundle unchanged, purely for IDE inference.
102
102
  */
103
- export declare function defineHonoBundle<P extends HonoBundle>(bundle: P): P;
104
- export declare function defineLazyHonoBundle<P extends Omit<LazyHonoBundle, typeof LAZY_HONO_BUNDLE>>(bundle: P): P & LazyHonoBundle;
105
- export declare function isLazyHonoBundle(bundle: HonoBundleInput): bundle is LazyHonoBundle;
106
- export interface ExpandedHonoBundles {
107
- modules: HonoModule[];
108
- extensions: HonoExtension[];
103
+ export declare function defineApiBundle<P extends ApiBundle>(bundle: P): P;
104
+ export declare function defineLazyApiBundle<P extends Omit<LazyApiBundle, typeof LAZY_API_BUNDLE>>(bundle: P): P & LazyApiBundle;
105
+ export declare function isLazyApiBundle(bundle: ApiBundleInput): bundle is LazyApiBundle;
106
+ export interface ExpandedApiBundles {
107
+ modules: ApiModule[];
108
+ extensions: ApiExtension[];
109
109
  subscribers: Subscriber[];
110
110
  links: LinkDefinition[];
111
111
  /** Absolute anonymous-access paths declared by bundles (ADR-0008). */
112
112
  anonymousPaths: string[];
113
113
  }
114
114
  /**
115
- * Flatten a list of {@link HonoBundle} values into their constituent pieces.
115
+ * Flatten a list of {@link ApiBundle} values into their constituent pieces.
116
116
  *
117
117
  * Throws if two bundles declare the same `name`.
118
118
  */
119
- export declare function expandHonoBundles(bundles: ReadonlyArray<HonoBundle>): ExpandedHonoBundles;
119
+ export declare function expandApiBundles(bundles: ReadonlyArray<ApiBundle>): ExpandedApiBundles;
120
120
  export {};
package/dist/bundle.js CHANGED
@@ -1,22 +1,22 @@
1
- const LAZY_HONO_BUNDLE = Symbol.for("voyant.hono.lazyBundle");
1
+ const LAZY_API_BUNDLE = Symbol.for("voyant.api.lazyBundle");
2
2
  /**
3
3
  * Identity helper — returns the bundle unchanged, purely for IDE inference.
4
4
  */
5
- export function defineHonoBundle(bundle) {
5
+ export function defineApiBundle(bundle) {
6
6
  return bundle;
7
7
  }
8
- export function defineLazyHonoBundle(bundle) {
9
- return { ...bundle, [LAZY_HONO_BUNDLE]: true };
8
+ export function defineLazyApiBundle(bundle) {
9
+ return { ...bundle, [LAZY_API_BUNDLE]: true };
10
10
  }
11
- export function isLazyHonoBundle(bundle) {
12
- return bundle[LAZY_HONO_BUNDLE] === true;
11
+ export function isLazyApiBundle(bundle) {
12
+ return bundle[LAZY_API_BUNDLE] === true;
13
13
  }
14
14
  /**
15
- * Flatten a list of {@link HonoBundle} values into their constituent pieces.
15
+ * Flatten a list of {@link ApiBundle} values into their constituent pieces.
16
16
  *
17
17
  * Throws if two bundles declare the same `name`.
18
18
  */
19
- export function expandHonoBundles(bundles) {
19
+ export function expandApiBundles(bundles) {
20
20
  const seen = new Set();
21
21
  const modules = [];
22
22
  const extensions = [];
@@ -1,4 +1,4 @@
1
- import type { HonoExtension, HonoModule } from "./module.js";
1
+ import type { ApiExtension, ApiModule } from "./module.js";
2
2
  /**
3
3
  * Manifest-driven runtime composition.
4
4
  *
@@ -30,8 +30,8 @@ export interface CompositionContext<TCapabilities> {
30
30
  capabilities: TCapabilities;
31
31
  options: Record<string, unknown>;
32
32
  }
33
- export type ModuleFactory<TCapabilities> = (ctx: CompositionContext<TCapabilities>) => HonoModule | HonoModule[];
34
- export type ExtensionFactory<TCapabilities> = (ctx: CompositionContext<TCapabilities>) => HonoExtension;
33
+ export type ModuleFactory<TCapabilities> = (ctx: CompositionContext<TCapabilities>) => ApiModule | ApiModule[];
34
+ export type ExtensionFactory<TCapabilities> = (ctx: CompositionContext<TCapabilities>) => ApiExtension;
35
35
  /**
36
36
  * Maps manifest specifiers to the factory that builds the runtime unit. Keys
37
37
  * MUST match the `voyant.config.ts` `modules` / `extensions` specifiers.
@@ -41,8 +41,8 @@ export interface CompositionRegistry<TCapabilities> {
41
41
  extensions?: Record<string, ExtensionFactory<TCapabilities>>;
42
42
  }
43
43
  export interface ComposedApp {
44
- modules: HonoModule[];
45
- extensions: HonoExtension[];
44
+ modules: ApiModule[];
45
+ extensions: ApiExtension[];
46
46
  }
47
47
  /**
48
48
  * Derive the `createApp({ modules, extensions })` arrays from a manifest by
package/dist/index.d.ts CHANGED
@@ -3,17 +3,17 @@ export { assembleAnonymousPaths } from "./anonymous-paths.js";
3
3
  export { mountApp } from "./app.js";
4
4
  export type { SessionAuthContext } from "./auth/index.js";
5
5
  export { constantTimeEqual, extractBearerToken, generateNumericCode, randomBytesHex, requireUserId, sha256Base64Url, sha256Hex, unsignCookie, verifySession, } from "./auth/index.js";
6
- export type { ExpandedHonoBundles, HonoBundle, HonoBundleInput, LazyHonoBundle, } from "./bundle.js";
7
- export { defineHonoBundle, defineLazyHonoBundle, expandHonoBundles, isLazyHonoBundle, } from "./bundle.js";
6
+ export type { ApiBundle, ApiBundleInput, ExpandedApiBundles, LazyApiBundle, } from "./bundle.js";
7
+ export { defineApiBundle, defineLazyApiBundle, expandApiBundles, isLazyApiBundle, } from "./bundle.js";
8
8
  export { type CreateAppConfig, createApp } from "./create-app.js";
9
9
  export type { AuthenticatedDocumentDownloadResolver, AuthenticatedDocumentDownloadResolverOptions, DocumentDownloadEnvelope, DocumentDownloadResolution, DocumentDownloadResolver, DocumentDownloadResolverResult, StoredDocumentReference, } from "./document-download.js";
10
10
  export { createAuthenticatedDocumentDownloadResolver, encodeStorageKeyPath, resolveStoredDocumentDownload, } from "./document-download.js";
11
11
  export { type AsyncMethodProvider, lazyProvider } from "./lazy-provider.js";
12
- export { createLazyRouteHandler, type LazyHonoRoutes, type LazyRoutesLoader, mountLazyRoutePaths, mountLazyRoutesAt, } from "./lazy-routes.js";
12
+ export { createLazyRouteHandler, type LazyApiRoutes, type LazyRoutesLoader, mountLazyRoutePaths, mountLazyRoutesAt, } from "./lazy-routes.js";
13
13
  export { createPathDbSelector, type PathDbSelectorOptions } from "./lib/db-selector.js";
14
14
  export type { RateLimitStore } from "./middleware/index.js";
15
15
  export { clientIpKey, consoleLoggerProvider, cors, createMemoryRateLimitStore, createRedisRateLimitStore, DEFAULT_IDEMPOTENCY_TTL_MS, db, enforceRateLimit, errorBoundary, handleApiError, type IdempotencyKeyOptions, idempotencyKey, isStaffRbacEnforced, LIVE_LIMITS, logger, purgeExpiredIdempotencyKeys, rateLimit, requestId, requireActor, requireAuth, requirePermission, } from "./middleware/index.js";
16
- export type { HonoExtension, HonoModule } from "./module.js";
16
+ export type { ApiExtension, ApiModule } from "./module.js";
17
17
  export type { ErrorEvent, Reporter } from "./observability/index.js";
18
18
  export { consoleReporter, getRequestId, noopReporter, runWithRequestId, safeCaptureException, } from "./observability/index.js";
19
19
  export { stampOpenApiRegistryApiId } from "./openapi-ownership.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { assembleAnonymousPaths } from "./anonymous-paths.js";
2
2
  export { mountApp } from "./app.js";
3
3
  export { constantTimeEqual, extractBearerToken, generateNumericCode, randomBytesHex, requireUserId, sha256Base64Url, sha256Hex, unsignCookie, verifySession, } from "./auth/index.js";
4
- export { defineHonoBundle, defineLazyHonoBundle, expandHonoBundles, isLazyHonoBundle, } from "./bundle.js";
4
+ export { defineApiBundle, defineLazyApiBundle, expandApiBundles, isLazyApiBundle, } from "./bundle.js";
5
5
  export { createApp } from "./create-app.js";
6
6
  export { createAuthenticatedDocumentDownloadResolver, encodeStorageKeyPath, resolveStoredDocumentDownload, } from "./document-download.js";
7
7
  export { lazyProvider } from "./lazy-provider.js";
@@ -38,7 +38,7 @@ export type LazyRoutesLoader = () => Promise<AnyHono>;
38
38
  * matchers the framework installs up front (no bundle import until a request
39
39
  * matches).
40
40
  */
41
- export interface LazyHonoRoutes {
41
+ export interface LazyApiRoutes {
42
42
  paths: readonly string[];
43
43
  load: LazyRoutesLoader;
44
44
  }
@@ -5,9 +5,8 @@ export { db } from "./db.js";
5
5
  export { errorBoundary, type HandleApiErrorOptions, handleApiError, requestId, } from "./error-boundary.js";
6
6
  export { DEFAULT_IDEMPOTENCY_TTL_MS, type IdempotencyKeyOptions, idempotencyKey, purgeExpiredIdempotencyKeys, } from "./idempotency-key.js";
7
7
  export { consoleLoggerProvider, logger } from "./logger.js";
8
- export { type AnalyticsEngineDatasetLike, DB_METRICS_CONTEXT_KEY, type MetricsMiddlewareOptions, metrics, type RequestDbMetrics, withQueryCounting, } from "./metrics.js";
9
8
  export { type PublicCacheOptions, publicResponseCache, resetPublicCacheStateForTests, } from "./public-cache.js";
10
- export { type CloudflareRateLimiterBinding, clientIpKey, createCloudflareRateLimitStore, createKvRateLimitStore, createMemoryRateLimitStore, createRedisRateLimitStore, enforceRateLimit, LIVE_LIMITS, type RateLimitConfig, type RateLimitPolicy, type RateLimitRequestContext, type RateLimitResult, type RateLimitRule, type RateLimitStore, type RedisRateLimitStoreOptions, rateLimit, resolveRateLimitStore, } from "./rate-limit.js";
9
+ export { clientIpKey, createMemoryRateLimitStore, createRedisRateLimitStore, enforceRateLimit, LIVE_LIMITS, type RateLimitConfig, type RateLimitPolicy, type RateLimitRequestContext, type RateLimitResult, type RateLimitRule, type RateLimitStore, type RedisRateLimitStoreOptions, rateLimit, resolveRateLimitStore, } from "./rate-limit.js";
11
10
  export { isStaffRbacEnforced, requireActor } from "./require-actor.js";
12
11
  export { requirePermission } from "./require-permission.js";
13
12
  export { type SecurityHeadersOptions, securityHeaders } from "./security-headers.js";
@@ -5,9 +5,8 @@ export { db } from "./db.js";
5
5
  export { errorBoundary, handleApiError, requestId, } from "./error-boundary.js";
6
6
  export { DEFAULT_IDEMPOTENCY_TTL_MS, idempotencyKey, purgeExpiredIdempotencyKeys, } from "./idempotency-key.js";
7
7
  export { consoleLoggerProvider, logger } from "./logger.js";
8
- export { DB_METRICS_CONTEXT_KEY, metrics, withQueryCounting, } from "./metrics.js";
9
8
  export { publicResponseCache, resetPublicCacheStateForTests, } from "./public-cache.js";
10
- export { clientIpKey, createCloudflareRateLimitStore, createKvRateLimitStore, createMemoryRateLimitStore, createRedisRateLimitStore, enforceRateLimit, LIVE_LIMITS, rateLimit, resolveRateLimitStore, } from "./rate-limit.js";
9
+ export { clientIpKey, createMemoryRateLimitStore, createRedisRateLimitStore, enforceRateLimit, LIVE_LIMITS, rateLimit, resolveRateLimitStore, } from "./rate-limit.js";
11
10
  export { isStaffRbacEnforced, requireActor } from "./require-actor.js";
12
11
  export { requirePermission } from "./require-permission.js";
13
12
  export { securityHeaders } from "./security-headers.js";
@@ -1,10 +1,5 @@
1
1
  import type { MiddlewareHandler } from "hono";
2
- import type { VoyantVariables } from "../types.js";
3
- /**
4
- * Structural shape of a Workers Analytics Engine dataset binding —
5
- * declared locally so `@voyant-travel/hono` needs no `@cloudflare/workers-types`
6
- * dependency.
7
- */
2
+ /** Structural sink contract for request metrics. */
8
3
  export interface AnalyticsEngineDatasetLike {
9
4
  writeDataPoint(point: {
10
5
  blobs?: string[];
@@ -18,19 +13,11 @@ export interface RequestDbMetrics {
18
13
  }
19
14
  export declare const DB_METRICS_CONTEXT_KEY = "__voyantDbMetrics";
20
15
  export interface MetricsMiddlewareOptions {
21
- /**
22
- * Resolve the Analytics Engine dataset from bindings. Defaults to
23
- * `env.METRICS`. Returning undefined makes the middleware a no-op for
24
- * that request (deployments without the binding pay ~nothing).
25
- */
26
- dataset?: (env: unknown) => AnalyticsEngineDatasetLike | undefined;
16
+ /** Resolve the metrics sink for a request. Returning undefined is a no-op. */
17
+ dataset: (env: unknown) => AnalyticsEngineDatasetLike | undefined;
27
18
  }
28
19
  /**
29
- * Per-request metrics Workers Analytics Engine (RFC voyant#1687 Phase
30
- * 3.4, the in-worker half — the platform dispatcher's DISPATCH_METRICS
31
- * dataset already records hostname/plan/cache/duration per dispatch;
32
- * this adds what only the worker can see: the matched route pattern,
33
- * the db query count, and in-worker cache hits).
20
+ * Per-request metrics for an explicitly supplied deployment sink.
34
21
  *
35
22
  * One data point per request:
36
23
  * - blobs: [method, routePattern, surface, cacheStatus]
@@ -40,8 +27,10 @@ export interface MetricsMiddlewareOptions {
40
27
  * `writeDataPoint` is fire-and-forget and never throws into the
41
28
  * request; a missing binding short-circuits before timing overhead.
42
29
  */
43
- export declare function metrics(options?: MetricsMiddlewareOptions): MiddlewareHandler<{
44
- Variables: Pick<VoyantVariables, typeof DB_METRICS_CONTEXT_KEY>;
30
+ export declare function metrics(options: MetricsMiddlewareOptions): MiddlewareHandler<{
31
+ Variables: {
32
+ [DB_METRICS_CONTEXT_KEY]?: RequestDbMetrics;
33
+ };
45
34
  }>;
46
35
  /**
47
36
  * Wrap a drizzle client so top-level query-initiating calls increment
@@ -9,11 +9,7 @@ function surfaceOf(path) {
9
9
  return "other";
10
10
  }
11
11
  /**
12
- * Per-request metrics Workers Analytics Engine (RFC voyant#1687 Phase
13
- * 3.4, the in-worker half — the platform dispatcher's DISPATCH_METRICS
14
- * dataset already records hostname/plan/cache/duration per dispatch;
15
- * this adds what only the worker can see: the matched route pattern,
16
- * the db query count, and in-worker cache hits).
12
+ * Per-request metrics for an explicitly supplied deployment sink.
17
13
  *
18
14
  * One data point per request:
19
15
  * - blobs: [method, routePattern, surface, cacheStatus]
@@ -23,9 +19,8 @@ function surfaceOf(path) {
23
19
  * `writeDataPoint` is fire-and-forget and never throws into the
24
20
  * request; a missing binding short-circuits before timing overhead.
25
21
  */
26
- export function metrics(options = {}) {
27
- const resolveDataset = options.dataset ??
28
- ((env) => env?.METRICS);
22
+ export function metrics(options) {
23
+ const resolveDataset = options.dataset;
29
24
  return async (c, next) => {
30
25
  const dataset = resolveDataset(c.env);
31
26
  if (!dataset || typeof dataset.writeDataPoint !== "function") {
@@ -1,4 +1,3 @@
1
- import type { KVStore } from "@voyant-travel/utils/cache";
2
1
  import { type LazyRedisClient } from "@voyant-travel/utils/redis-client";
3
2
  import type { MiddlewareHandler } from "hono";
4
3
  /**
@@ -6,11 +5,9 @@ import type { MiddlewareHandler } from "hono";
6
5
  *
7
6
  * The limiter is split into two halves:
8
7
  *
9
- * - a {@link RateLimitStore} — the counting backend. Three implementations
10
- * ship with the framework: the Cloudflare native Rate Limiting binding
11
- * (truly distributed, limits configured in wrangler), a KV-backed
12
- * fixed-window counter (best-effort — see {@link createKvRateLimitStore}),
13
- * and an in-memory Map for Node/dev/tests (per-isolate).
8
+ * - a {@link RateLimitStore} — the counting backend. Composition injects the
9
+ * selected provider; an in-memory Map remains the zero-configuration
10
+ * fallback for Node/dev/tests.
14
11
  * - the enforcement surface — the {@link rateLimit} Hono middleware and the
15
12
  * imperative {@link enforceRateLimit} helper that route packages
16
13
  * (storefront, bookings, …) call directly inside handlers.
@@ -73,9 +70,8 @@ export interface RateLimitPolicy {
73
70
  export interface RateLimitConfig {
74
71
  /**
75
72
  * Explicit store, or a function-of-bindings for stores built from env
76
- * bindings (Workers env is only available per request). When omitted
77
- * or when the function returns `undefined` — resolution falls through
78
- * to `c.env.RATE_LIMITER` (CF binding) → `c.env.RATE_LIMIT` (KV) →
73
+ * bindings. When omitted or when the function returns `undefined`
74
+ * resolution falls through to the injected `RATE_LIMIT_STORE`, then
79
75
  * in-memory.
80
76
  */
81
77
  store?: RateLimitStore | ((env: unknown) => RateLimitStore | undefined);
@@ -92,21 +88,6 @@ export interface RateLimitRule {
92
88
  max: number;
93
89
  windowSeconds: number;
94
90
  }
95
- /**
96
- * The Cloudflare native Rate Limiting binding shape (wrangler
97
- * `[[ratelimits]]`). The binding's `limit`/`period` are fixed in wrangler
98
- * config — the policy's `max`/`windowSeconds` are advisory for headers
99
- * only. One shared binding works across policies because the bucket is
100
- * part of the key; deployments wanting different limits per policy bind
101
- * one ratelimiter per policy and pass it via `policy.store`.
102
- */
103
- export interface CloudflareRateLimiterBinding {
104
- limit(opts: {
105
- key: string;
106
- }): Promise<{
107
- success: boolean;
108
- }>;
109
- }
110
91
  /**
111
92
  * Minimal structural view of a Hono context — enough for key derivation,
112
93
  * store resolution, and response headers — so route packages can call
@@ -142,30 +123,6 @@ export declare function clientIpKey(c: {
142
123
  export declare function createMemoryRateLimitStore(options?: {
143
124
  maxEntries?: number;
144
125
  }): RateLimitStore;
145
- /**
146
- * KV-backed fixed-window store.
147
- *
148
- * **Best-effort by construction**: KV is eventually consistent and the
149
- * counter is a non-atomic read-modify-write, so concurrent requests
150
- * across PoPs undercount — a determined attacker gets somewhat more than
151
- * `max` through before the window converges. With per-client keys this
152
- * is still a real brake on brute force and write floods; deployments
153
- * needing exact distributed limits should bind the Cloudflare Rate
154
- * Limiting binding (`RATE_LIMITER`) instead.
155
- *
156
- * Stored keys are `lim:<bucket>:<clientKey>:<windowKey>` (the window
157
- * suffix is appended here) with a TTL of `max(60, windowSeconds * 2)` —
158
- * KV's TTL floor is 60s.
159
- */
160
- export declare function createKvRateLimitStore(kv: KVStore): RateLimitStore;
161
- /**
162
- * Adapter for the Cloudflare native Rate Limiting binding. Truly
163
- * distributed and atomic; the actual limit/period are fixed in wrangler
164
- * config, so the policy's `max`/`windowSeconds` only inform the
165
- * `Retry-After` hint. `remaining` is never reported (the binding does
166
- * not expose it).
167
- */
168
- export declare function createCloudflareRateLimitStore(binding: CloudflareRateLimiterBinding): RateLimitStore;
169
126
  export interface RedisRateLimitStoreOptions {
170
127
  client?: LazyRedisClient;
171
128
  }
@@ -174,8 +131,8 @@ export declare function createRedisRateLimitStore(redisUrl: string, options?: Re
174
131
  export declare function resetRateLimitWarningsForTests(): void;
175
132
  /**
176
133
  * Resolve the best available store from the environment:
177
- * `c.env.RATE_LIMITER` (CF Rate Limiting binding) `c.env.RATE_LIMIT`
178
- * (KV) → in-memory fallback. **Fails open into the memory store** —
134
+ * Injected `c.env.RATE_LIMIT_STORE` in-memory fallback. **Fails open into
135
+ * the memory store** —
179
136
  * rate limiting must never break Node/headless deployments that bind
180
137
  * neither — but warns once per isolate outside dev/test so a
181
138
  * production deploy without a distributed backend is visible in logs.
@@ -67,58 +67,6 @@ export function createMemoryRateLimitStore(options) {
67
67
  },
68
68
  };
69
69
  }
70
- /**
71
- * KV-backed fixed-window store.
72
- *
73
- * **Best-effort by construction**: KV is eventually consistent and the
74
- * counter is a non-atomic read-modify-write, so concurrent requests
75
- * across PoPs undercount — a determined attacker gets somewhat more than
76
- * `max` through before the window converges. With per-client keys this
77
- * is still a real brake on brute force and write floods; deployments
78
- * needing exact distributed limits should bind the Cloudflare Rate
79
- * Limiting binding (`RATE_LIMITER`) instead.
80
- *
81
- * Stored keys are `lim:<bucket>:<clientKey>:<windowKey>` (the window
82
- * suffix is appended here) with a TTL of `max(60, windowSeconds * 2)` —
83
- * KV's TTL floor is 60s.
84
- */
85
- export function createKvRateLimitStore(kv) {
86
- return {
87
- async limit(key, { max, windowSeconds }) {
88
- const nowSeconds = Math.floor(Date.now() / 1000);
89
- const windowKey = Math.floor(nowSeconds / windowSeconds);
90
- const storageKey = `${key}:${windowKey}`;
91
- const raw = await kv.get(storageKey);
92
- const current = raw ? Number(raw) || 0 : 0;
93
- const next = current + 1;
94
- await kv.put(storageKey, String(next), {
95
- expirationTtl: Math.max(60, windowSeconds * 2),
96
- });
97
- return {
98
- allowed: next <= max,
99
- remaining: Math.max(0, max - next),
100
- retryAfterSeconds: Math.max(1, windowSeconds - (nowSeconds % windowSeconds)),
101
- };
102
- },
103
- };
104
- }
105
- /**
106
- * Adapter for the Cloudflare native Rate Limiting binding. Truly
107
- * distributed and atomic; the actual limit/period are fixed in wrangler
108
- * config, so the policy's `max`/`windowSeconds` only inform the
109
- * `Retry-After` hint. `remaining` is never reported (the binding does
110
- * not expose it).
111
- */
112
- export function createCloudflareRateLimitStore(binding) {
113
- return {
114
- async limit(key, { windowSeconds }) {
115
- const { success } = await binding.limit({ key });
116
- return success
117
- ? { allowed: true }
118
- : { allowed: false, retryAfterSeconds: Math.max(1, windowSeconds) };
119
- },
120
- };
121
- }
122
70
  export function createRedisRateLimitStore(redisUrl, options = {}) {
123
71
  const lazyClient = options.client ?? createLazyRedisClient(redisUrl);
124
72
  return {
@@ -140,9 +88,7 @@ export function createRedisRateLimitStore(redisUrl, options = {}) {
140
88
  };
141
89
  }
142
90
  // ---- Store resolution ----
143
- const bindingStoreCache = new WeakMap();
144
91
  const explicitStoreCache = new WeakMap();
145
- const kvStoreCache = new WeakMap();
146
92
  const sharedMemoryStore = createMemoryRateLimitStore();
147
93
  let warnedNoDistributedStore = false;
148
94
  function isDevLikeEnv() {
@@ -156,23 +102,14 @@ export function resetRateLimitWarningsForTests() {
156
102
  }
157
103
  /**
158
104
  * Resolve the best available store from the environment:
159
- * `c.env.RATE_LIMITER` (CF Rate Limiting binding) `c.env.RATE_LIMIT`
160
- * (KV) → in-memory fallback. **Fails open into the memory store** —
105
+ * Injected `c.env.RATE_LIMIT_STORE` in-memory fallback. **Fails open into
106
+ * the memory store** —
161
107
  * rate limiting must never break Node/headless deployments that bind
162
108
  * neither — but warns once per isolate outside dev/test so a
163
109
  * production deploy without a distributed backend is visible in logs.
164
110
  */
165
111
  export function resolveRateLimitStore(c, memoryFallback = sharedMemoryStore) {
166
112
  const env = (c.env ?? {});
167
- const binding = env.RATE_LIMITER;
168
- if (binding && typeof binding.limit === "function") {
169
- let store = bindingStoreCache.get(binding);
170
- if (!store) {
171
- store = createCloudflareRateLimitStore(binding);
172
- bindingStoreCache.set(binding, store);
173
- }
174
- return store;
175
- }
176
113
  const explicit = env.RATE_LIMIT_STORE;
177
114
  if (explicit && typeof explicit.limit === "function") {
178
115
  let store = explicitStoreCache.get(explicit);
@@ -182,19 +119,10 @@ export function resolveRateLimitStore(c, memoryFallback = sharedMemoryStore) {
182
119
  }
183
120
  return store;
184
121
  }
185
- const kv = env.RATE_LIMIT;
186
- if (kv && typeof kv.get === "function" && typeof kv.put === "function") {
187
- let store = kvStoreCache.get(kv);
188
- if (!store) {
189
- store = createKvRateLimitStore(kv);
190
- kvStoreCache.set(kv, store);
191
- }
192
- return store;
193
- }
194
122
  if (!warnedNoDistributedStore && !isDevLikeEnv()) {
195
123
  warnedNoDistributedStore = true;
196
124
  console.warn("[voyant] rate-limit: no distributed store available (inject RATE_LIMIT_STORE " +
197
- "or a KV-compatible RATE_LIMIT fallback). " +
125
+ "). " +
198
126
  "Falling back to a per-isolate in-memory limiter — limits apply " +
199
127
  "per instance, not fleet-wide.");
200
128
  }
package/dist/module.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Extension, Module } from "@voyant-travel/core";
2
2
  import type { Hono } from "hono";
3
- import type { LazyHonoRoutes, LazyRoutesLoader } from "./lazy-routes.js";
4
- export interface HonoModule {
3
+ import type { LazyApiRoutes, LazyRoutesLoader } from "./lazy-routes.js";
4
+ export interface ApiModule {
5
5
  module: Module;
6
6
  /** Staff-facing routes — mounted at `/v1/admin/{module.name}`. */
7
7
  adminRoutes?: Hono<any>;
@@ -35,7 +35,7 @@ export interface HonoModule {
35
35
  * returns ABSOLUTE routes; the framework mounts + caches them with the request
36
36
  * context bridged in. Context-preserving replacement for `mountLazyRouteApp`.
37
37
  */
38
- lazyRoutes?: LazyHonoRoutes;
38
+ lazyRoutes?: LazyApiRoutes;
39
39
  /**
40
40
  * Optional override for the public mount path relative to `/v1/public`.
41
41
  *
@@ -69,7 +69,7 @@ export interface HonoModule {
69
69
  */
70
70
  transactionalPaths?: readonly string[];
71
71
  }
72
- export interface HonoExtension {
72
+ export interface ApiExtension {
73
73
  extension: Extension;
74
74
  /** Staff-facing routes — mounted at `/v1/admin/{extension.module}`. */
75
75
  adminRoutes?: Hono<any>;
@@ -77,15 +77,15 @@ export interface HonoExtension {
77
77
  publicRoutes?: Hono<any>;
78
78
  /**
79
79
  * Inbound webhook routes — mounted at `/v1/{extension.module}`, concrete paths
80
- * auto-added to the anonymous allow-list (ADR-0008). See `HonoModule.webhookRoutes`.
80
+ * auto-added to the anonymous allow-list (ADR-0008). See `ApiModule.webhookRoutes`.
81
81
  */
82
82
  webhookRoutes?: Hono<any>;
83
- /** Lazy variant of `adminRoutes` — mounted at `/v1/admin/{extension.module}` (see HonoModule). */
83
+ /** Lazy variant of `adminRoutes` — mounted at `/v1/admin/{extension.module}` (see ApiModule). */
84
84
  lazyAdminRoutes?: LazyRoutesLoader;
85
85
  /** Lazy variant of `publicRoutes` — mounted at `/v1/public/{publicPath ?? extension.module}`. */
86
86
  lazyPublicRoutes?: LazyRoutesLoader;
87
- /** Deployment-local lazy family at explicit absolute paths (see HonoModule). */
88
- lazyRoutes?: LazyHonoRoutes;
87
+ /** Deployment-local lazy family at explicit absolute paths (see ApiModule). */
88
+ lazyRoutes?: LazyApiRoutes;
89
89
  /**
90
90
  * Optional override for the public mount path relative to `/v1/public`.
91
91
  *
@@ -95,13 +95,13 @@ export interface HonoExtension {
95
95
  publicPath?: string;
96
96
  /**
97
97
  * Declares which of this extension's PUBLIC routes are reachable without a
98
- * session (ADR-0008). Same semantics as {@link HonoModule.anonymous}, relative
98
+ * session (ADR-0008). Same semantics as {@link ApiModule.anonymous}, relative
99
99
  * to the extension's public mount.
100
100
  */
101
101
  anonymous?: boolean | readonly string[];
102
102
  /**
103
103
  * Absolute transactional path prefixes — same semantics as
104
- * {@link HonoModule.transactionalPaths}.
104
+ * {@link ApiModule.transactionalPaths}.
105
105
  */
106
106
  transactionalPaths?: readonly string[];
107
107
  }
package/dist/types.d.ts CHANGED
@@ -7,8 +7,8 @@ import type { NeonHttpDatabase } from "drizzle-orm/neon-http";
7
7
  import type { NeonDatabase as NeonWsDatabase } from "drizzle-orm/neon-serverless";
8
8
  import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
9
9
  import type { Handler, Hono } from "hono";
10
- import type { HonoBundleInput } from "./bundle.js";
11
- import type { HonoExtension, HonoModule } from "./module.js";
10
+ import type { ApiBundleInput } from "./bundle.js";
11
+ import type { ApiExtension, ApiModule } from "./module.js";
12
12
  import type { Reporter } from "./observability/reporter.js";
13
13
  export interface VoyantExecutionContext {
14
14
  waitUntil?: (promise: Promise<unknown>) => void;
@@ -24,17 +24,9 @@ export interface VoyantBindings {
24
24
  APP_URL?: string;
25
25
  DASH_BASE_URL?: string;
26
26
  API_BASE_URL?: string;
27
- RATE_LIMIT?: KVStore;
28
27
  RATE_LIMIT_STORE?: import("./middleware/rate-limit.js").RateLimitStore;
29
28
  CACHE?: KVStore;
30
29
  SHARED_STATE?: KVStore;
31
- RATE_LIMITER?: import("./middleware/rate-limit.js").CloudflareRateLimiterBinding;
32
- /**
33
- * Workers Analytics Engine dataset receiving per-request metrics
34
- * (see the `metrics` middleware). Optional — without it the
35
- * middleware is a no-op.
36
- */
37
- METRICS?: import("./middleware/metrics.js").AnalyticsEngineDatasetLike;
38
30
  }
39
31
  export type VoyantDb = PostgresJsDatabase | NeonHttpDatabase | NeonWsDatabase;
40
32
  export type VoyantQueryRuntime = QueryRunner;
@@ -55,8 +47,6 @@ export type VoyantVariables = CoreVoyantVariables & {
55
47
  query?: VoyantQueryRuntime;
56
48
  /** Optional workflow driver surfaced to HTTP routes after lazy app bootstrap. */
57
49
  workflowDriver?: import("@voyant-travel/workflows/driver").WorkflowDriver;
58
- /** Per-request db metrics counter populated by the metrics middleware. */
59
- __voyantDbMetrics?: import("./middleware/metrics.js").RequestDbMetrics;
60
50
  };
61
51
  /** Handler contract for application-authored Hono API routes. */
62
52
  export type VoyantRouteHandler<TBindings extends VoyantBindings = VoyantBindings> = Handler<{
@@ -193,9 +183,9 @@ export interface VoyantAppConfig<TBindings extends VoyantBindings = VoyantBindin
193
183
  * together with `dbTransactional`.
194
184
  */
195
185
  dbTransactionalPaths?: string[];
196
- modules?: HonoModule[];
197
- extensions?: HonoExtension[];
198
- plugins?: HonoBundleInput[];
186
+ modules?: ApiModule[];
187
+ extensions?: ApiExtension[];
188
+ plugins?: ApiBundleInput[];
199
189
  eventBus?: EventBus;
200
190
  /**
201
191
  * Link definitions activated against each request's resolved database.
@@ -260,13 +250,6 @@ export interface VoyantAppConfig<TBindings extends VoyantBindings = VoyantBindin
260
250
  * `insertOutboxEvents(tx, ...)`. Default off.
261
251
  */
262
252
  outbox?: boolean;
263
- /**
264
- * Per-request metrics to the `env.METRICS` Analytics Engine dataset
265
- * (method, route pattern, surface, cache status, duration, status,
266
- * db query count). Enabled by default and inert without the binding;
267
- * set `false` to disable entirely.
268
- */
269
- metrics?: boolean;
270
253
  /**
271
254
  * Default request body limit enforced before route handlers parse
272
255
  * JSON/form data. Content-type-aware: JSON bodies are capped at 10 MiB
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyant-travel/hono",
3
- "version": "0.127.2",
3
+ "version": "0.128.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -130,18 +130,17 @@
130
130
  "drizzle-orm": "^0.45.2",
131
131
  "hono": "^4.12.27",
132
132
  "zod": "^4.4.3",
133
- "@voyant-travel/core": "^0.124.0",
134
- "@voyant-travel/db": "^0.114.7",
133
+ "@voyant-travel/core": "^0.125.0",
134
+ "@voyant-travel/db": "^0.114.9",
135
135
  "@voyant-travel/types": "^0.109.2",
136
136
  "@voyant-travel/utils": "^0.107.1",
137
- "@voyant-travel/workflows": "^0.121.0"
137
+ "@voyant-travel/workflows": "^0.122.2"
138
138
  },
139
139
  "devDependencies": {
140
- "@cloudflare/workers-types": "^4.20260702.1",
141
140
  "typescript": "^6.0.3",
142
141
  "vitest": "^4.1.9",
143
142
  "@voyant-travel/voyant-typescript-config": "^0.1.0",
144
- "@voyant-travel/workflows-orchestrator": "^0.121.0"
143
+ "@voyant-travel/workflows-orchestrator": "^0.122.2"
145
144
  },
146
145
  "files": [
147
146
  "dist"