@voyant-travel/hono 0.129.2 → 0.130.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
@@ -41,6 +41,11 @@ const app = createApp({
41
41
  })
42
42
  ```
43
43
 
44
+ Custom `resolve` adapters must return an explicit realm alongside the actor:
45
+ admin sessions return `{ actor: "staff", realm: "admin" }`, while storefront
46
+ sessions return a non-staff actor with `realm: "customer"`. Realm/actor
47
+ mismatches are rejected before protected routes run.
48
+
44
49
  Use `modules`, `extensions`, and provider-backed route helpers as the default
45
50
  composition surface. Use `plugins` when you want to register a reusable
46
51
  distribution bundle that packages those pieces together.
package/dist/index.d.ts CHANGED
@@ -20,5 +20,5 @@ export { stampOpenApiRegistryApiId } from "./openapi-ownership.js";
20
20
  export { openApiValidationHook } from "./openapi-validation.js";
21
21
  export type { CreatePublicCapabilityOptions, PublicCapabilityCookieOptions, PublicCapabilityPayload, VerifyPublicCapabilityOptions, } from "./public-capability.js";
22
22
  export { createPublicCapabilityToken, extractPublicCapabilityToken, serializePublicCapabilityCookie, verifyPublicCapabilityToken, } from "./public-capability.js";
23
- export type { DbFactory, DbFactorySelector, DbSource, DbSurfaceSelection, LogEntry, LoggerProvider, VoyantAppConfig, VoyantAuthAppTokenResolveArgs, VoyantAuthIntegration, VoyantAuthPermissionArgs, VoyantAuthResolveArgs, VoyantBindings, VoyantDb, VoyantExecutionContext, VoyantQueryRuntime, VoyantRequestAuthContext, VoyantRouteHandler, VoyantVariables, } from "./types.js";
23
+ export type { DbFactory, DbFactorySelector, DbSource, DbSurfaceSelection, LogEntry, LoggerProvider, VoyantAppConfig, VoyantAuthAppTokenResolveArgs, VoyantAuthIntegration, VoyantAuthPermissionArgs, VoyantAuthResolveArgs, VoyantBindings, VoyantDb, VoyantExecutionContext, VoyantQueryRuntime, VoyantRequestAuthContext, VoyantResolvedSessionAuthContext, VoyantRouteHandler, VoyantVariables, } from "./types.js";
24
24
  export { ApiHttpError, ForbiddenApiError, normalizeValidationError, parseJsonBody, parseOptionalJsonBody, parseQuery, RequestValidationError, UnauthorizedApiError, } from "./validation.js";
@@ -162,6 +162,15 @@ function applyAuthContext(c, auth) {
162
162
  if (auth.appContextConstraint)
163
163
  c.set("appContextConstraint", auth.appContextConstraint);
164
164
  }
165
+ function matchesRequestRealm(path, auth) {
166
+ if (path === "/v1/admin" || path.startsWith("/v1/admin/")) {
167
+ return auth.actor === "staff" && auth.realm === "admin";
168
+ }
169
+ if (path === "/v1/public" || path.startsWith("/v1/public/")) {
170
+ return auth.actor !== "staff" && auth.realm === "customer";
171
+ }
172
+ return false;
173
+ }
165
174
  export function requireAuth(dbSource, opts) {
166
175
  const publicPaths = opts?.publicPaths ?? [];
167
176
  return async (c, next) => {
@@ -331,7 +340,7 @@ export function requireAuth(dbSource, opts) {
331
340
  // Guarded: Hono throws on `executionCtx` access outside Workers.
332
341
  ctx: tryGetExecutionCtx(c),
333
342
  });
334
- if (resolved?.userId) {
343
+ if (resolved?.userId && matchesRequestRealm(p, resolved)) {
335
344
  applyAuthContext(c, resolved);
336
345
  // `await` is load-bearing — see strategy 2: a bare
337
346
  // `return next()` would let the `finally` release the shared
@@ -343,14 +352,31 @@ export function requireAuth(dbSource, opts) {
343
352
  await lease.release();
344
353
  }
345
354
  }
346
- // Strategy 5: Generic session-claims bearer token support
347
- const sessionSecret = c.env.SESSION_CLAIMS_SECRET;
348
- if (token && sessionSecret && token.includes(".")) {
355
+ // Strategy 5: Realm-bound session-claims bearer token support. Ambiguous
356
+ // surfaces intentionally skip this strategy instead of trying both keys.
357
+ const sessionRealm = p === "/v1/admin" || p.startsWith("/v1/admin/")
358
+ ? "admin"
359
+ : p === "/v1/public" || p.startsWith("/v1/public/")
360
+ ? "customer"
361
+ : null;
362
+ const adminSessionSecret = c.env.SESSION_CLAIMS_ADMIN_SECRET?.trim();
363
+ const customerSessionSecret = c.env.SESSION_CLAIMS_CUSTOMER_SECRET?.trim();
364
+ const sessionRootsAreDistinct = !adminSessionSecret || !customerSessionSecret || adminSessionSecret !== customerSessionSecret;
365
+ const sessionSecret = !sessionRootsAreDistinct
366
+ ? undefined
367
+ : sessionRealm === "admin"
368
+ ? adminSessionSecret
369
+ : sessionRealm === "customer"
370
+ ? customerSessionSecret
371
+ : undefined;
372
+ if (token && sessionSecret && sessionSecret.length >= 32 && token.includes(".")) {
349
373
  try {
350
374
  const sessionAuth = await verifySession(token, sessionSecret);
351
375
  applyAuthContext(c, {
352
376
  ...sessionAuth,
353
377
  callerType: "session",
378
+ actor: sessionRealm === "admin" ? "staff" : "customer",
379
+ audience: sessionRealm === "admin" ? "staff" : "customer",
354
380
  });
355
381
  return next();
356
382
  }
package/dist/types.d.ts CHANGED
@@ -17,7 +17,8 @@ export interface VoyantExecutionContext {
17
17
  export interface VoyantBindings {
18
18
  INTERNAL_API_KEY?: string;
19
19
  INTERNAL_API_KEY_SCOPES?: string;
20
- SESSION_CLAIMS_SECRET?: string;
20
+ SESSION_CLAIMS_ADMIN_SECRET?: string;
21
+ SESSION_CLAIMS_CUSTOMER_SECRET?: string;
21
22
  BETTER_AUTH_ADMIN_SECRET?: string;
22
23
  BETTER_AUTH_CUSTOMER_SECRET?: string;
23
24
  DATABASE_URL: string;
@@ -117,9 +118,13 @@ export declare function resolveDbFactoryResult(value: VoyantDb | DisposableDb):
117
118
  export type VoyantRequestAuthContext = Omit<VoyantAuthContext, "actor"> & {
118
119
  userId: string;
119
120
  actor: Actor;
120
- /** Explicit security realm for session identities. */
121
+ /** Explicit security realm when the context represents a user session. */
121
122
  realm?: "admin" | "customer";
122
123
  };
124
+ /** User-session identity returned by a custom `auth.resolve` adapter. */
125
+ export type VoyantResolvedSessionAuthContext = VoyantRequestAuthContext & {
126
+ realm: "admin" | "customer";
127
+ };
123
128
  export interface LogEntry {
124
129
  method: string;
125
130
  path: string;
@@ -159,13 +164,13 @@ export interface VoyantAuthIntegration<TBindings extends VoyantBindings = Voyant
159
164
  /**
160
165
  * Resolve the request to an auth context, or return `null` for anonymous.
161
166
  *
162
- * The returned object MUST include `actor` `requireActor` is fail-closed,
163
- * so omitting it 401s every protected route. For single-tenant admin apps
164
- * where every authenticated session is staff, return `actor: "staff"`.
165
- * Customer/partner/supplier sessions should return the corresponding actor
166
- * so `/v1/public/*` route guards work.
167
+ * The returned object MUST include both `actor` and `realm`. Admin sessions
168
+ * use `realm: "admin"` with `actor: "staff"`; customer, partner, or supplier
169
+ * sessions use `realm: "customer"` with the corresponding non-staff actor.
170
+ * Realm/actor mismatches fail closed so credentials cannot cross between
171
+ * `/v1/admin/*` and `/v1/public/*`.
167
172
  */
168
- resolve?: (args: VoyantAuthResolveArgs<TBindings>) => Promise<VoyantRequestAuthContext | null> | VoyantRequestAuthContext | null;
173
+ resolve?: (args: VoyantAuthResolveArgs<TBindings>) => Promise<VoyantResolvedSessionAuthContext | null> | VoyantResolvedSessionAuthContext | null;
169
174
  resolveAppToken?: (args: VoyantAuthAppTokenResolveArgs<TBindings>) => Promise<VoyantAuthContext | null> | VoyantAuthContext | null;
170
175
  hasPermission?: (args: VoyantAuthPermissionArgs<TBindings>) => Promise<boolean> | boolean;
171
176
  validateApiKey?: (args: VoyantAuthApiKeyValidationArgs<TBindings>) => Promise<boolean> | boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyant-travel/hono",
3
- "version": "0.129.2",
3
+ "version": "0.130.1",
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/core": "^0.127.1",
134
- "@voyant-travel/db": "^0.114.13",
135
- "@voyant-travel/types": "^0.109.4",
136
- "@voyant-travel/utils": "^0.107.1",
137
- "@voyant-travel/workflows": "^0.122.6"
133
+ "@voyant-travel/core": "^0.129.0",
134
+ "@voyant-travel/db": "^0.114.15",
135
+ "@voyant-travel/types": "^0.109.5",
136
+ "@voyant-travel/utils": "^0.108.0",
137
+ "@voyant-travel/workflows": "^0.122.8"
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.6"
143
+ "@voyant-travel/workflows-orchestrator": "^0.122.8"
144
144
  },
145
145
  "files": [
146
146
  "dist"