@voyant-travel/hono 0.128.5 → 0.129.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.js +15 -3
- package/dist/auth-augmentation.d.ts +4 -0
- package/dist/auth-augmentation.js +21 -0
- package/dist/client-authenticated-routes.d.ts +8 -0
- package/dist/client-authenticated-routes.js +20 -0
- package/dist/middleware/auth.d.ts +2 -0
- package/dist/middleware/auth.js +11 -2
- package/dist/middleware/require-actor.d.ts +3 -0
- package/dist/middleware/require-actor.js +7 -6
- package/dist/module.d.ts +26 -0
- package/package.json +5 -5
package/dist/app.js
CHANGED
|
@@ -5,7 +5,9 @@ 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 { composeAuthAugmentations } from "./auth-augmentation.js";
|
|
8
9
|
import { expandApiBundles, isLazyApiBundle, } from "./bundle.js";
|
|
10
|
+
import { assembleClientAuthenticatedRoutes, matchesClientAuthenticatedRoute, } from "./client-authenticated-routes.js";
|
|
9
11
|
import { mountLazyRoutePaths, mountLazyRoutesAt } from "./lazy-routes.js";
|
|
10
12
|
import { mountAuthForwarding } from "./lib/auth-forward.js";
|
|
11
13
|
import { createPathDbSelector } from "./lib/db-selector.js";
|
|
@@ -104,6 +106,8 @@ export function mountApp(config) {
|
|
|
104
106
|
...(expanded?.anonymousPaths ?? []),
|
|
105
107
|
...lazyPlugins.flatMap((plugin) => plugin.anonymous ?? []),
|
|
106
108
|
]);
|
|
109
|
+
const clientAuthenticatedRoutes = assembleClientAuthenticatedRoutes(allModules);
|
|
110
|
+
const composedAuth = composeAuthAugmentations(config.auth, allModules);
|
|
107
111
|
// When the framework owns the bus, route subscriber-dispatch failures
|
|
108
112
|
// (including the workflow forwarder) to the reporter — they're otherwise
|
|
109
113
|
// only console-logged per the fire-and-forget EventBus contract (RFC #1553).
|
|
@@ -404,7 +408,9 @@ export function mountApp(config) {
|
|
|
404
408
|
const pathname = normalizePathname(new URL(c.req.url).pathname, {
|
|
405
409
|
basePath: config.basePath,
|
|
406
410
|
});
|
|
407
|
-
const isPublicWrite = pathname.startsWith("/v1/public/") ||
|
|
411
|
+
const isPublicWrite = pathname.startsWith("/v1/public/") ||
|
|
412
|
+
matchesPublicPath(pathname, anonymousPaths) ||
|
|
413
|
+
matchesClientAuthenticatedRoute(c.req.method, pathname, clientAuthenticatedRoutes);
|
|
408
414
|
if (!isPublicWrite)
|
|
409
415
|
return next();
|
|
410
416
|
return rateLimit(buildRateLimitPolicy(rateLimitConfig, c.env, "public-write", publicWriteRule))(c, next);
|
|
@@ -460,7 +466,7 @@ export function mountApp(config) {
|
|
|
460
466
|
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
461
467
|
// App-owned auth handler (must be before auth middleware — these routes are
|
|
462
468
|
// public). Forwarding + observability bridging live in `mountAuthForwarding`.
|
|
463
|
-
const authHandler =
|
|
469
|
+
const authHandler = composedAuth?.handler;
|
|
464
470
|
if (authHandler) {
|
|
465
471
|
mountAuthForwarding(app, authHandler, { reporter, appName });
|
|
466
472
|
}
|
|
@@ -475,7 +481,12 @@ export function mountApp(config) {
|
|
|
475
481
|
transactionalPrefixes: txPrefixes,
|
|
476
482
|
})
|
|
477
483
|
: config.db;
|
|
478
|
-
const authOptions = {
|
|
484
|
+
const authOptions = {
|
|
485
|
+
publicPaths: anonymousPaths,
|
|
486
|
+
clientAuthenticatedRoutes,
|
|
487
|
+
basePath: config.basePath,
|
|
488
|
+
auth: composedAuth,
|
|
489
|
+
};
|
|
479
490
|
// Auth middleware for all other routes
|
|
480
491
|
app.use("*", requireAuth(dbSource, authOptions));
|
|
481
492
|
// DB middleware — sets c.var.db for all downstream handlers.
|
|
@@ -502,6 +513,7 @@ export function mountApp(config) {
|
|
|
502
513
|
basePath: config.basePath,
|
|
503
514
|
resources: config.accessResources,
|
|
504
515
|
accessCatalog: config.accessCatalog,
|
|
516
|
+
clientAuthenticatedRoutes,
|
|
505
517
|
};
|
|
506
518
|
app.use("/v1/admin/*", requireActor(actorOptions, "staff"));
|
|
507
519
|
app.use("/v1/public/*", requireActor(actorOptions, "customer", "partner", "supplier"));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ApiModule } from "./module.js";
|
|
2
|
+
import type { VoyantAuthIntegration, VoyantBindings } from "./types.js";
|
|
3
|
+
/** Compose trusted package app-token resolvers while preserving host auth. */
|
|
4
|
+
export declare function composeAuthAugmentations<TBindings extends VoyantBindings>(hostAuth: VoyantAuthIntegration<TBindings> | undefined, modules: readonly ApiModule[]): VoyantAuthIntegration<TBindings> | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Compose trusted package app-token resolvers while preserving host auth. */
|
|
2
|
+
export function composeAuthAugmentations(hostAuth, modules) {
|
|
3
|
+
const packageResolvers = modules.flatMap((module) => module.authAugmentation ? [module.authAugmentation.resolveAppToken] : []);
|
|
4
|
+
if (packageResolvers.length === 0)
|
|
5
|
+
return hostAuth;
|
|
6
|
+
const hostResolver = hostAuth?.resolveAppToken;
|
|
7
|
+
return {
|
|
8
|
+
...hostAuth,
|
|
9
|
+
resolveAppToken: async (args) => {
|
|
10
|
+
const hostResult = await hostResolver?.(args);
|
|
11
|
+
if (hostResult)
|
|
12
|
+
return hostResult;
|
|
13
|
+
for (const resolve of packageResolvers) {
|
|
14
|
+
const result = await resolve(args);
|
|
15
|
+
if (result)
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ApiModule, ClientAuthenticatedRoute } from "./module.js";
|
|
2
|
+
export interface AbsoluteClientAuthenticatedRoute {
|
|
3
|
+
method: ClientAuthenticatedRoute["method"];
|
|
4
|
+
path: string;
|
|
5
|
+
}
|
|
6
|
+
/** Resolve trusted module declarations to exact, app-local admin routes. */
|
|
7
|
+
export declare function assembleClientAuthenticatedRoutes(modules: readonly ApiModule[]): AbsoluteClientAuthenticatedRoute[];
|
|
8
|
+
export declare function matchesClientAuthenticatedRoute(method: string, pathname: string, routes: readonly AbsoluteClientAuthenticatedRoute[]): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Resolve trusted module declarations to exact, app-local admin routes. */
|
|
2
|
+
export function assembleClientAuthenticatedRoutes(modules) {
|
|
3
|
+
const routes = new Map();
|
|
4
|
+
for (const module of modules) {
|
|
5
|
+
const prefix = `/v1/admin/${module.module.name}`;
|
|
6
|
+
for (const route of module.clientAuthenticated ?? []) {
|
|
7
|
+
const trimmed = route.path.trim();
|
|
8
|
+
if (!trimmed.startsWith("/") || trimmed.includes(":") || trimmed.includes("*")) {
|
|
9
|
+
throw new Error(`clientAuthenticated route for module "${module.module.name}" must be a concrete relative path beginning with "/"`);
|
|
10
|
+
}
|
|
11
|
+
const path = `${prefix}${trimmed === "/" ? "" : trimmed.replace(/\/+$/g, "")}`;
|
|
12
|
+
const resolved = { method: route.method, path };
|
|
13
|
+
routes.set(`${resolved.method} ${resolved.path}`, resolved);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return [...routes.values()].sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
|
|
17
|
+
}
|
|
18
|
+
export function matchesClientAuthenticatedRoute(method, pathname, routes) {
|
|
19
|
+
return routes.some((route) => route.method === method.toUpperCase() && route.path === pathname);
|
|
20
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { MiddlewareHandler } from "hono";
|
|
2
|
+
import type { AbsoluteClientAuthenticatedRoute } from "../client-authenticated-routes.js";
|
|
2
3
|
import { type DbSource, type VoyantAuthIntegration, type VoyantBindings, type VoyantVariables } from "../types.js";
|
|
3
4
|
export declare function requireAuth<TBindings extends VoyantBindings>(dbSource: DbSource<TBindings>, opts?: {
|
|
4
5
|
publicPaths?: string[];
|
|
6
|
+
clientAuthenticatedRoutes?: readonly AbsoluteClientAuthenticatedRoute[];
|
|
5
7
|
basePath?: string;
|
|
6
8
|
auth?: VoyantAuthIntegration<TBindings>;
|
|
7
9
|
}): MiddlewareHandler<{
|
package/dist/middleware/auth.js
CHANGED
|
@@ -3,6 +3,7 @@ import { API_KEY_AUDIENCES, permissionsToStrings } from "@voyant-travel/types/ap
|
|
|
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";
|
|
6
|
+
import { matchesClientAuthenticatedRoute } from "../client-authenticated-routes.js";
|
|
6
7
|
import { tryGetExecutionCtx } from "../lib/execution-ctx.js";
|
|
7
8
|
import { matchesPublicPath, normalizePathname } from "../lib/public-paths.js";
|
|
8
9
|
import { selectDbFactory, } from "../types.js";
|
|
@@ -152,6 +153,8 @@ function applyAuthContext(c, auth) {
|
|
|
152
153
|
c.set("appTokenMode", auth.appTokenMode);
|
|
153
154
|
if (auth.appViewerId)
|
|
154
155
|
c.set("appViewerId", auth.appViewerId);
|
|
156
|
+
if (auth.appContextConstraint)
|
|
157
|
+
c.set("appContextConstraint", auth.appContextConstraint);
|
|
155
158
|
}
|
|
156
159
|
export function requireAuth(dbSource, opts) {
|
|
157
160
|
const publicPaths = opts?.publicPaths ?? [];
|
|
@@ -167,6 +170,9 @@ export function requireAuth(dbSource, opts) {
|
|
|
167
170
|
const isHealthCheck = p === "/health";
|
|
168
171
|
if (isPublicAuth || isHealthCheck)
|
|
169
172
|
return next();
|
|
173
|
+
if (matchesClientAuthenticatedRoute(c.req.method, p, opts?.clientAuthenticatedRoutes ?? [])) {
|
|
174
|
+
return next();
|
|
175
|
+
}
|
|
170
176
|
if (matchesPublicPath(p, publicPaths)) {
|
|
171
177
|
if (p.startsWith("/v1/public/")) {
|
|
172
178
|
c.set("actor", "customer");
|
|
@@ -283,8 +289,11 @@ export function requireAuth(dbSource, opts) {
|
|
|
283
289
|
await lease.release();
|
|
284
290
|
}
|
|
285
291
|
}
|
|
286
|
-
// Strategy 3: Remote app access token support
|
|
287
|
-
|
|
292
|
+
// Strategy 3: Remote app access token support. App grants authorize only
|
|
293
|
+
// the installation-scoped App API; never resolve them on admin/public
|
|
294
|
+
// surfaces where similarly named scopes may protect broader host routes.
|
|
295
|
+
const isAppApi = p === "/v1/app" || p.startsWith("/v1/app/");
|
|
296
|
+
if (token && isAppApi && opts?.auth?.resolveAppToken) {
|
|
288
297
|
const lease = acquireRequestDb(c, dbFactory);
|
|
289
298
|
try {
|
|
290
299
|
const resolved = await opts.auth.resolveAppToken({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Actor } from "@voyant-travel/core";
|
|
2
2
|
import { type AccessCatalog } from "@voyant-travel/types/api-keys";
|
|
3
3
|
import type { MiddlewareHandler } from "hono";
|
|
4
|
+
import type { AbsoluteClientAuthenticatedRoute } from "../client-authenticated-routes.js";
|
|
4
5
|
import type { VoyantBindings, VoyantVariables } from "../types.js";
|
|
5
6
|
/**
|
|
6
7
|
* Staff-session RBAC enforcement (member-rbac-rfc, voyant#2085). Enforced **by
|
|
@@ -24,6 +25,8 @@ export interface RequireActorOptions {
|
|
|
24
25
|
authorization?: "coarse" | "route";
|
|
25
26
|
}[];
|
|
26
27
|
accessCatalog?: AccessCatalog;
|
|
28
|
+
/** Exact admin protocol routes that authenticate their client in-band. */
|
|
29
|
+
clientAuthenticatedRoutes?: readonly AbsoluteClientAuthenticatedRoute[];
|
|
27
30
|
}
|
|
28
31
|
/**
|
|
29
32
|
* Guards a route surface by actor type.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasApiKeyPermission, permissionStringsToPermissions, } from "@voyant-travel/types/api-keys";
|
|
2
|
+
import { matchesClientAuthenticatedRoute } from "../client-authenticated-routes.js";
|
|
2
3
|
import { normalizePathname } from "../lib/public-paths.js";
|
|
3
4
|
/**
|
|
4
5
|
* Route surfaces exempt from the coarse method+path permission guard because
|
|
@@ -98,12 +99,15 @@ export function requireActor(...args) {
|
|
|
98
99
|
return async (c, next) => {
|
|
99
100
|
if (c.req.method === "OPTIONS")
|
|
100
101
|
return next();
|
|
102
|
+
const pathname = normalizePathname(new URL(c.req.url).pathname, {
|
|
103
|
+
basePath: options.basePath,
|
|
104
|
+
});
|
|
105
|
+
if (matchesClientAuthenticatedRoute(c.req.method, pathname, options.clientAuthenticatedRoutes ?? [])) {
|
|
106
|
+
return next();
|
|
107
|
+
}
|
|
101
108
|
if (c.get("callerType") === "api_key" ||
|
|
102
109
|
c.get("callerType") === "app" ||
|
|
103
110
|
c.get("callerType") === "internal") {
|
|
104
|
-
const pathname = normalizePathname(new URL(c.req.url).pathname, {
|
|
105
|
-
basePath: options.basePath,
|
|
106
|
-
});
|
|
107
111
|
const { resource, authorization } = authorizationForRequest(pathname, options);
|
|
108
112
|
// Coarse-guard-exempt surfaces. `_meta` (capability discovery) and `mcp`
|
|
109
113
|
// (the agent tool server) are dispatch surfaces where authorization is
|
|
@@ -139,9 +143,6 @@ export function requireActor(...args) {
|
|
|
139
143
|
// resource (e.g. `_meta`) stay open until a module is explicitly covered.
|
|
140
144
|
if (actor === "staff" && c.get("callerType") === "session" && isStaffRbacEnforced(c.env)) {
|
|
141
145
|
const scopes = c.get("scopes");
|
|
142
|
-
const pathname = normalizePathname(new URL(c.req.url).pathname, {
|
|
143
|
-
basePath: options.basePath,
|
|
144
|
-
});
|
|
145
146
|
const { resource, authorization } = authorizationForRequest(pathname, options);
|
|
146
147
|
if (resource && authorization !== "route" && !isCoarseGuardExempt(resource)) {
|
|
147
148
|
const actions = apiKeyPermissionActionsForMethod(c.req.method, pathname);
|
package/dist/module.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Extension, Module } from "@voyant-travel/core";
|
|
2
2
|
import type { Hono } from "hono";
|
|
3
3
|
import type { LazyApiRoutes, LazyRoutesLoader } from "./lazy-routes.js";
|
|
4
|
+
import type { VoyantAuthIntegration } from "./types.js";
|
|
4
5
|
export interface ApiModule {
|
|
5
6
|
module: Module;
|
|
6
7
|
/** Staff-facing routes — mounted at `/v1/admin/{module.name}`. */
|
|
@@ -56,6 +57,23 @@ export interface ApiModule {
|
|
|
56
57
|
* require a `staff` actor.
|
|
57
58
|
*/
|
|
58
59
|
anonymous?: boolean | readonly string[];
|
|
60
|
+
/**
|
|
61
|
+
* Concrete admin endpoints whose credential is validated by the route itself
|
|
62
|
+
* instead of by the staff-session middleware. Each declaration is matched by
|
|
63
|
+
* exact HTTP method and exact path; it never opens sibling or child routes.
|
|
64
|
+
*
|
|
65
|
+
* This is intentionally narrower than `anonymous`: use it only for protocol
|
|
66
|
+
* endpoints such as an OAuth token exchange where client authentication is
|
|
67
|
+
* part of the request body or Authorization header. The handler remains
|
|
68
|
+
* responsible for validating that client credential.
|
|
69
|
+
*/
|
|
70
|
+
clientAuthenticated?: readonly ClientAuthenticatedRoute[];
|
|
71
|
+
/**
|
|
72
|
+
* Trusted package-owned additions to the host authentication pipeline.
|
|
73
|
+
* Augmentations are composed after any host app-token resolver and do not
|
|
74
|
+
* replace staff session handling or any other host auth hook.
|
|
75
|
+
*/
|
|
76
|
+
authAugmentation?: ApiAuthAugmentation;
|
|
59
77
|
/**
|
|
60
78
|
* Absolute API path prefixes whose requests must be served by the
|
|
61
79
|
* transaction-capable db client (ADR-0008). For modules whose
|
|
@@ -69,6 +87,14 @@ export interface ApiModule {
|
|
|
69
87
|
*/
|
|
70
88
|
transactionalPaths?: readonly string[];
|
|
71
89
|
}
|
|
90
|
+
export interface ClientAuthenticatedRoute {
|
|
91
|
+
method: "POST";
|
|
92
|
+
/** Concrete path relative to `/v1/admin/{module.name}`. */
|
|
93
|
+
path: string;
|
|
94
|
+
}
|
|
95
|
+
export interface ApiAuthAugmentation {
|
|
96
|
+
resolveAppToken: NonNullable<VoyantAuthIntegration["resolveAppToken"]>;
|
|
97
|
+
}
|
|
72
98
|
export interface ApiExtension {
|
|
73
99
|
extension: Extension;
|
|
74
100
|
/** Staff-facing routes — mounted at `/v1/admin/{extension.module}`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyant-travel/hono",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.129.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -130,17 +130,17 @@
|
|
|
130
130
|
"drizzle-orm": "^0.45.2",
|
|
131
131
|
"hono": "^4.12.27",
|
|
132
132
|
"zod": "^4.4.3",
|
|
133
|
-
"@voyant-travel/
|
|
134
|
-
"@voyant-travel/
|
|
133
|
+
"@voyant-travel/db": "^0.114.13",
|
|
134
|
+
"@voyant-travel/core": "^0.127.0",
|
|
135
135
|
"@voyant-travel/types": "^0.109.4",
|
|
136
136
|
"@voyant-travel/utils": "^0.107.1",
|
|
137
|
-
"@voyant-travel/workflows": "^0.122.
|
|
137
|
+
"@voyant-travel/workflows": "^0.122.6"
|
|
138
138
|
},
|
|
139
139
|
"devDependencies": {
|
|
140
140
|
"typescript": "^6.0.3",
|
|
141
141
|
"vitest": "^4.1.9",
|
|
142
142
|
"@voyant-travel/voyant-typescript-config": "^0.1.0",
|
|
143
|
-
"@voyant-travel/workflows-orchestrator": "^0.122.
|
|
143
|
+
"@voyant-travel/workflows-orchestrator": "^0.122.6"
|
|
144
144
|
},
|
|
145
145
|
"files": [
|
|
146
146
|
"dist"
|