@voyant-travel/hono 0.120.1 → 0.121.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.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;;;;;;;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"}
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,CA4gB5D"}
package/dist/app.js CHANGED
@@ -417,6 +417,21 @@ export function mountApp(config) {
417
417
  // uniform across eager + lazy. Webhook routes are intentionally excluded — the
418
418
  // per-module docs cover the admin/storefront surfaces only.
419
419
  const moduleMounts = [];
420
+ const deferredLazyRouteMounts = [];
421
+ function deferLazyRoutesAt(moduleName, prefix, load) {
422
+ lazyMounts.push({ prefix, load });
423
+ moduleMounts.push({ moduleName, prefix, load });
424
+ deferredLazyRouteMounts.push(() => {
425
+ mountLazyRoutesAt(app, prefix, load);
426
+ });
427
+ }
428
+ function deferLazyRoutePaths(moduleName, paths, load) {
429
+ lazyMounts.push({ prefix: "/", load });
430
+ moduleMounts.push({ moduleName, prefix: "/", load });
431
+ deferredLazyRouteMounts.push(() => {
432
+ mountLazyRoutePaths(app, paths, load);
433
+ });
434
+ }
420
435
  // Mount module routes
421
436
  for (const mod of allModules) {
422
437
  const moduleName = mod.module.name;
@@ -433,19 +448,13 @@ export function mountApp(config) {
433
448
  moduleMounts.push({ moduleName, prefix: publicPrefix, load: () => publicRoutes });
434
449
  }
435
450
  if (mod.lazyAdminRoutes) {
436
- mountLazyRoutesAt(app, adminPrefix, mod.lazyAdminRoutes);
437
- lazyMounts.push({ prefix: adminPrefix, load: mod.lazyAdminRoutes });
438
- moduleMounts.push({ moduleName, prefix: adminPrefix, load: mod.lazyAdminRoutes });
451
+ deferLazyRoutesAt(moduleName, adminPrefix, mod.lazyAdminRoutes);
439
452
  }
440
453
  if (mod.lazyPublicRoutes) {
441
- mountLazyRoutesAt(app, publicPrefix, mod.lazyPublicRoutes);
442
- lazyMounts.push({ prefix: publicPrefix, load: mod.lazyPublicRoutes });
443
- moduleMounts.push({ moduleName, prefix: publicPrefix, load: mod.lazyPublicRoutes });
454
+ deferLazyRoutesAt(moduleName, publicPrefix, mod.lazyPublicRoutes);
444
455
  }
445
456
  if (mod.lazyRoutes) {
446
- mountLazyRoutePaths(app, mod.lazyRoutes.paths, mod.lazyRoutes.load);
447
- lazyMounts.push({ prefix: "/", load: mod.lazyRoutes.load });
448
- moduleMounts.push({ moduleName, prefix: "/", load: mod.lazyRoutes.load });
457
+ deferLazyRoutePaths(moduleName, mod.lazyRoutes.paths, mod.lazyRoutes.load);
449
458
  }
450
459
  if (mod.webhookRoutes) {
451
460
  app.route(`/v1/${moduleName}`, mod.webhookRoutes);
@@ -467,24 +476,21 @@ export function mountApp(config) {
467
476
  moduleMounts.push({ moduleName, prefix: publicPrefix, load: () => publicRoutes });
468
477
  }
469
478
  if (ext.lazyAdminRoutes) {
470
- mountLazyRoutesAt(app, adminPrefix, ext.lazyAdminRoutes);
471
- lazyMounts.push({ prefix: adminPrefix, load: ext.lazyAdminRoutes });
472
- moduleMounts.push({ moduleName, prefix: adminPrefix, load: ext.lazyAdminRoutes });
479
+ deferLazyRoutesAt(moduleName, adminPrefix, ext.lazyAdminRoutes);
473
480
  }
474
481
  if (ext.lazyPublicRoutes) {
475
- mountLazyRoutesAt(app, publicPrefix, ext.lazyPublicRoutes);
476
- lazyMounts.push({ prefix: publicPrefix, load: ext.lazyPublicRoutes });
477
- moduleMounts.push({ moduleName, prefix: publicPrefix, load: ext.lazyPublicRoutes });
482
+ deferLazyRoutesAt(moduleName, publicPrefix, ext.lazyPublicRoutes);
478
483
  }
479
484
  if (ext.lazyRoutes) {
480
- mountLazyRoutePaths(app, ext.lazyRoutes.paths, ext.lazyRoutes.load);
481
- lazyMounts.push({ prefix: "/", load: ext.lazyRoutes.load });
482
- moduleMounts.push({ moduleName, prefix: "/", load: ext.lazyRoutes.load });
485
+ deferLazyRoutePaths(moduleName, ext.lazyRoutes.paths, ext.lazyRoutes.load);
483
486
  }
484
487
  if (ext.webhookRoutes) {
485
488
  app.route(`/v1/${moduleName}`, ext.webhookRoutes);
486
489
  }
487
490
  }
491
+ for (const mountLazyRoutes of deferredLazyRouteMounts) {
492
+ mountLazyRoutes();
493
+ }
488
494
  // Additional routes
489
495
  if (config.additionalRoutes) {
490
496
  config.additionalRoutes(app);
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAM7C,OAAO,EACL,KAAK,QAAQ,EAEb,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAA;AA2GpB,wBAAgB,WAAW,CAAC,SAAS,SAAS,cAAc,EAC1D,QAAQ,EAAE,QAAQ,CAAC,SAAS,CAAC,EAC7B,IAAI,CAAC,EAAE;IACL,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAA;CACxC,GACA,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CAwLD"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAM7C,OAAO,EACL,KAAK,QAAQ,EAEb,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAA;AAgIpB,wBAAgB,WAAW,CAAC,SAAS,SAAS,cAAc,EAC1D,QAAQ,EAAE,QAAQ,CAAC,SAAS,CAAC,EAC7B,IAAI,CAAC,EAAE;IACL,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAA;CACxC,GACA,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CA4LD"}
@@ -1,5 +1,5 @@
1
1
  import { apikeyTable } from "@voyant-travel/db/schema/iam";
2
- import { permissionsToStrings } from "@voyant-travel/types/api-keys";
2
+ import { API_KEY_AUDIENCES, permissionsToStrings } from "@voyant-travel/types/api-keys";
3
3
  import { and, eq, sql } from "drizzle-orm";
4
4
  import { constantTimeEqual, sha256Base64Url, sha256Hex } from "../auth/crypto.js";
5
5
  import { extractBearerToken, verifySession } from "../auth/session-jwt.js";
@@ -91,6 +91,26 @@ async function writeCachedApiKey(kv, keyHash, row) {
91
91
  // cache writes are best-effort
92
92
  }
93
93
  }
94
+ const API_KEY_AUDIENCE_SET = new Set(API_KEY_AUDIENCES);
95
+ /**
96
+ * Extract the grant audience from an API key's `metadata` JSON. Malformed or
97
+ * absent metadata falls back to `"staff"` (legacy server-to-server default).
98
+ */
99
+ function resolveApiKeyAudience(metadata) {
100
+ if (!metadata)
101
+ return "staff";
102
+ try {
103
+ const parsed = JSON.parse(metadata);
104
+ const value = parsed?.audience;
105
+ if (typeof value === "string" && API_KEY_AUDIENCE_SET.has(value)) {
106
+ return value;
107
+ }
108
+ }
109
+ catch {
110
+ // Ignore malformed metadata — fall back to the staff default.
111
+ }
112
+ return "staff";
113
+ }
94
114
  function applyAuthContext(c, auth) {
95
115
  if (auth.userId)
96
116
  c.set("userId", auth.userId);
@@ -102,6 +122,8 @@ function applyAuthContext(c, auth) {
102
122
  c.set("callerType", auth.callerType);
103
123
  if (auth.actor)
104
124
  c.set("actor", auth.actor);
125
+ if (auth.audience)
126
+ c.set("audience", auth.audience);
105
127
  if (auth.scopes !== undefined)
106
128
  c.set("scopes", auth.scopes);
107
129
  if (auth.isInternalRequest !== undefined)
@@ -209,16 +231,20 @@ export function requireAuth(dbSource, opts) {
209
231
  .catch(() => { });
210
232
  tryGetExecutionCtx(c)?.waitUntil(counterUpdate);
211
233
  const scopes = permissionsToStrings(row.permissions);
234
+ // Audience is a grant attribute carried on the key's `metadata`, not
235
+ // inferred from scopes (D3). Legacy keys with no audience default to
236
+ // `staff`, preserving prior server-to-server behaviour. The actor
237
+ // follows the audience so a non-staff key resolves to its own
238
+ // visibility pool instead of silently gaining operator privileges.
239
+ const audience = resolveApiKeyAudience(row.metadata);
212
240
  applyAuthContext(c, {
213
241
  organizationId: row.referenceId,
214
242
  scopes,
215
243
  callerType: "api_key",
216
244
  apiTokenId: row.id,
217
245
  apiKeyId: row.id,
218
- // Core-owned API keys (`voy_` prefix) are server-to-server credentials
219
- // issued to operator staff. The actor stays explicit here so that
220
- // `requireActor` doesn't have to default unset callers to "staff".
221
- actor: "staff",
246
+ actor: audience,
247
+ audience,
222
248
  });
223
249
  // `await` is load-bearing: with a bare `return next()` the
224
250
  // `finally` would run as soon as the downstream promise is
@@ -1 +1 @@
1
- {"version":3,"file":"require-actor.d.ts","sourceRoot":"","sources":["../../src/middleware/require-actor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAEhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAG7C,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AA8DlE;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAIzD;AAED,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAMD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc,EAC5E,GAAG,OAAO,EAAE,KAAK,EAAE,GAClB,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CAAA;AACF,wBAAgB,YAAY,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc,EAC5E,OAAO,EAAE,mBAAmB,EAC5B,GAAG,OAAO,EAAE,KAAK,EAAE,GAClB,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CAAA"}
1
+ {"version":3,"file":"require-actor.d.ts","sourceRoot":"","sources":["../../src/middleware/require-actor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAEhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAG7C,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AA0ElE;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAIzD;AAED,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAMD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc,EAC5E,GAAG,OAAO,EAAE,KAAK,EAAE,GAClB,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CAAA;AACF,wBAAgB,YAAY,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc,EAC5E,OAAO,EAAE,mBAAmB,EAC5B,GAAG,OAAO,EAAE,KAAK,EAAE,GAClB,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CAAA"}
@@ -1,5 +1,15 @@
1
1
  import { hasApiKeyPermission, permissionStringsToPermissions } from "@voyant-travel/types/api-keys";
2
2
  import { normalizePathname } from "../lib/public-paths.js";
3
+ /**
4
+ * Route surfaces exempt from the coarse method+path permission guard because
5
+ * they enforce authorization at a finer grain internally: `_meta` (capability
6
+ * discovery) and `mcp` (the agent tool server, which gates every tool by its own
7
+ * `requiredScopes`). Neither is a real module name.
8
+ */
9
+ const COARSE_GUARD_EXEMPT_RESOURCES = new Set(["_meta", "mcp"]);
10
+ function isCoarseGuardExempt(resource) {
11
+ return resource !== null && COARSE_GUARD_EXEMPT_RESOURCES.has(resource);
12
+ }
3
13
  function apiKeyResourceFromPath(pathname) {
4
14
  const surfaceMatch = pathname.match(/^\/v1\/(?:admin|public)\/([^/]+)/);
5
15
  if (surfaceMatch?.[1])
@@ -84,11 +94,13 @@ export function requireActor(...args) {
84
94
  basePath: options.basePath,
85
95
  });
86
96
  const resource = apiKeyResourceFromPath(pathname);
87
- // Meta/discovery endpoints (e.g. `/v1/admin/_meta/capabilities`) report
88
- // what the caller can do including the key's own granted scopes — so any
89
- // authenticated API key may read them, regardless of module scope. `_meta`
90
- // is a reserved namespace (no module is named `_meta`).
91
- if (resource === "_meta")
97
+ // Coarse-guard-exempt surfaces. `_meta` (capability discovery) and `mcp`
98
+ // (the agent tool server) are dispatch surfaces where authorization is
99
+ // enforced at a finer grain than method+path: the MCP server filters and
100
+ // gates each tool by its own `requiredScopes` (voyant#2792, D2). So any
101
+ // authenticated key reaches them; a key with no relevant scopes simply
102
+ // sees an empty tool list. Neither is a real module name.
103
+ if (isCoarseGuardExempt(resource))
92
104
  return next();
93
105
  const actions = apiKeyPermissionActionsForMethod(c.req.method, pathname);
94
106
  if (resource && hasAnyApiKeyPermission(c.get("scopes"), resource, actions)) {
@@ -119,7 +131,7 @@ export function requireActor(...args) {
119
131
  basePath: options.basePath,
120
132
  });
121
133
  const resource = apiKeyResourceFromPath(pathname);
122
- if (resource && resource !== "_meta") {
134
+ if (resource && !isCoarseGuardExempt(resource)) {
123
135
  const actions = apiKeyPermissionActionsForMethod(c.req.method, pathname);
124
136
  if (!hasAnyApiKeyPermission(scopes, resource, actions)) {
125
137
  return c.json({ error: "Forbidden: missing required permission" }, 403);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyant-travel/hono",
3
- "version": "0.120.1",
3
+ "version": "0.121.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -133,21 +133,21 @@
133
133
  "dependencies": {
134
134
  "@hono/zod-openapi": "^1.4.0",
135
135
  "drizzle-orm": "^0.45.2",
136
- "hono": "^4.12.25",
136
+ "hono": "^4.12.27",
137
137
  "zod": "^4.4.3",
138
- "@voyant-travel/core": "^0.111.1",
139
- "@voyant-travel/types": "^0.106.1",
138
+ "@voyant-travel/core": "^0.112.0",
139
+ "@voyant-travel/db": "^0.109.5",
140
140
  "@voyant-travel/storage": "^0.106.0",
141
- "@voyant-travel/db": "^0.109.4",
142
- "@voyant-travel/utils": "^0.105.5",
143
- "@voyant-travel/workflows": "^0.111.14"
141
+ "@voyant-travel/types": "^0.107.0",
142
+ "@voyant-travel/utils": "^0.105.6",
143
+ "@voyant-travel/workflows": "^0.111.15"
144
144
  },
145
145
  "devDependencies": {
146
- "@cloudflare/workers-types": "^4.20260628.1",
146
+ "@cloudflare/workers-types": "^4.20260702.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.14"
150
+ "@voyant-travel/workflows-orchestrator": "^0.111.15"
151
151
  },
152
152
  "files": [
153
153
  "dist"