@voyantjs/hono 0.9.0 → 0.11.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/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export type { VoyantPermission } from "@voyantjs/core";
2
2
  export { createApp } from "./app.js";
3
3
  export type { SessionAuthContext } from "./auth/index.js";
4
4
  export { extractBearerToken, generateNumericCode, randomBytesHex, requireUserId, sha256Base64Url, sha256Hex, unsignCookie, verifySession, } from "./auth/index.js";
5
- export { consoleLoggerProvider, cors, db, errorBoundary, handleApiError, LIVE_LIMITS, logger, rateLimit, requestId, requireActor, requireAuth, requirePermission, } from "./middleware/index.js";
5
+ export { consoleLoggerProvider, cors, DEFAULT_IDEMPOTENCY_TTL_MS, db, errorBoundary, handleApiError, type IdempotencyKeyOptions, idempotencyKey, LIVE_LIMITS, logger, purgeExpiredIdempotencyKeys, rateLimit, requestId, requireActor, requireAuth, requirePermission, } from "./middleware/index.js";
6
6
  export type { HonoExtension, HonoModule } from "./module.js";
7
7
  export type { ExpandedHonoBundles, ExpandedHonoPlugins, HonoBundle, HonoPlugin, } from "./plugin.js";
8
8
  export { defineHonoBundle, defineHonoPlugin, expandHonoBundles, expandHonoPlugins, } from "./plugin.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACpC,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,eAAe,EACf,SAAS,EACT,YAAY,EACZ,aAAa,GACd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,qBAAqB,EACrB,IAAI,EACJ,EAAE,EACF,aAAa,EACb,cAAc,EACd,WAAW,EACX,MAAM,EACN,SAAS,EACT,SAAS,EACT,YAAY,EACZ,WAAW,EACX,iBAAiB,GAClB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC5D,YAAY,EACV,mBAAmB,EACnB,mBAAmB,EACnB,UAAU,EACV,UAAU,GACX,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AACpB,YAAY,EACV,SAAS,EACT,QAAQ,EACR,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,EACd,QAAQ,EACR,sBAAsB,EACtB,kBAAkB,EAClB,wBAAwB,EACxB,eAAe,GAChB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,wBAAwB,EACxB,aAAa,EACb,qBAAqB,EACrB,UAAU,EACV,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACpC,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,eAAe,EACf,SAAS,EACT,YAAY,EACZ,aAAa,GACd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,qBAAqB,EACrB,IAAI,EACJ,0BAA0B,EAC1B,EAAE,EACF,aAAa,EACb,cAAc,EACd,KAAK,qBAAqB,EAC1B,cAAc,EACd,WAAW,EACX,MAAM,EACN,2BAA2B,EAC3B,SAAS,EACT,SAAS,EACT,YAAY,EACZ,WAAW,EACX,iBAAiB,GAClB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC5D,YAAY,EACV,mBAAmB,EACnB,mBAAmB,EACnB,UAAU,EACV,UAAU,GACX,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AACpB,YAAY,EACV,SAAS,EACT,QAAQ,EACR,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,EACd,QAAQ,EACR,sBAAsB,EACtB,kBAAkB,EAClB,wBAAwB,EACxB,eAAe,GAChB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,wBAAwB,EACxB,aAAa,EACb,qBAAqB,EACrB,UAAU,EACV,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,iBAAiB,CAAA"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { createApp } from "./app.js";
2
2
  export { extractBearerToken, generateNumericCode, randomBytesHex, requireUserId, sha256Base64Url, sha256Hex, unsignCookie, verifySession, } from "./auth/index.js";
3
- export { consoleLoggerProvider, cors, db, errorBoundary, handleApiError, LIVE_LIMITS, logger, rateLimit, requestId, requireActor, requireAuth, requirePermission, } from "./middleware/index.js";
3
+ export { consoleLoggerProvider, cors, DEFAULT_IDEMPOTENCY_TTL_MS, db, errorBoundary, handleApiError, idempotencyKey, LIVE_LIMITS, logger, purgeExpiredIdempotencyKeys, rateLimit, requestId, requireActor, requireAuth, requirePermission, } from "./middleware/index.js";
4
4
  export { defineHonoBundle, defineHonoPlugin, expandHonoBundles, expandHonoPlugins, } from "./plugin.js";
5
5
  export { ApiHttpError, ForbiddenApiError, normalizeValidationError, parseJsonBody, parseOptionalJsonBody, parseQuery, RequestValidationError, UnauthorizedApiError, } from "./validation.js";
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAI7C,OAAO,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAsCpG,wBAAgB,WAAW,CAAC,SAAS,SAAS,cAAc,EAC1D,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC,EAC/B,IAAI,CAAC,EAAE;IACL,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,IAAI,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAA;CACxC,GACA,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CAwID"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAI7C,OAAO,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAsCpG,wBAAgB,WAAW,CAAC,SAAS,SAAS,cAAc,EAC1D,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC,EAC/B,IAAI,CAAC,EAAE;IACL,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,IAAI,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAA;CACxC,GACA,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CA4ID"}
@@ -118,6 +118,10 @@ export function requireAuth(dbFactory, opts) {
118
118
  scopes,
119
119
  callerType: "api_key",
120
120
  apiKeyId: row.id,
121
+ // Core-owned API keys (`voy_` prefix) are server-to-server credentials
122
+ // issued to operator staff. The actor stays explicit here so that
123
+ // `requireActor` doesn't have to default unset callers to "staff".
124
+ actor: "staff",
121
125
  });
122
126
  return next();
123
127
  }
@@ -0,0 +1,71 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ import type { DbFactory, VoyantBindings, VoyantVariables } from "../types.js";
3
+ /**
4
+ * Twenty-four hours, in milliseconds. Default TTL for stored idempotency
5
+ * keys. Tunable per middleware instance.
6
+ */
7
+ export declare const DEFAULT_IDEMPOTENCY_TTL_MS: number;
8
+ export interface IdempotencyKeyOptions {
9
+ /**
10
+ * Namespacing label so unrelated endpoints can safely accept overlapping
11
+ * client keys. Defaults to the request method + pathname when omitted.
12
+ */
13
+ scope?: string;
14
+ /**
15
+ * How long stored responses are replayable. Defaults to
16
+ * {@link DEFAULT_IDEMPOTENCY_TTL_MS}.
17
+ */
18
+ ttlMs?: number;
19
+ /**
20
+ * Whether the header is required. When `true`, requests without the header
21
+ * are rejected with 400. Defaults to `false` so existing clients keep
22
+ * working through a deprecation window.
23
+ */
24
+ required?: boolean;
25
+ /**
26
+ * Optional callback that, given the response body that's about to be
27
+ * stored, returns a `referenceId` (typically the booking id). Used for
28
+ * operational queries to correlate replays with the underlying entity.
29
+ */
30
+ extractReferenceId?: (body: unknown) => string | null | undefined;
31
+ }
32
+ interface ContextWithIdempotency {
33
+ idempotencyKey?: string;
34
+ idempotencyReplayed?: boolean;
35
+ }
36
+ declare module "hono" {
37
+ interface ContextVariableMap extends ContextWithIdempotency {
38
+ }
39
+ }
40
+ /**
41
+ * Idempotency-Key middleware.
42
+ *
43
+ * On request:
44
+ * - reads the `Idempotency-Key` header
45
+ * - if absent and `required` is false, passes through
46
+ * - if absent and `required` is true, returns 400
47
+ * - looks up `(scope, key)` in `idempotency_keys`:
48
+ * - hit + same body hash → returns the stored response (replay)
49
+ * - hit + different body hash → returns 409 (conflict)
50
+ * - miss → runs the handler, then stores the response on the way out
51
+ *
52
+ * The handler's response body is captured by cloning the response. The
53
+ * `Idempotency-Key` and a `Idempotency-Replayed: true` header are echoed
54
+ * on replay so callers can detect replays in client-side logging.
55
+ *
56
+ * The middleware reads the `db` instance off the request context (set by
57
+ * the `db` middleware that ships with `createApp`) — caller is responsible
58
+ * for ordering this middleware after `db`.
59
+ */
60
+ export declare function idempotencyKey<TBindings extends VoyantBindings = VoyantBindings>(options?: IdempotencyKeyOptions): MiddlewareHandler<{
61
+ Bindings: TBindings;
62
+ Variables: VoyantVariables;
63
+ }>;
64
+ /**
65
+ * Sweep expired idempotency rows. Call from a daily cron.
66
+ */
67
+ export declare function purgeExpiredIdempotencyKeys<TBindings extends VoyantBindings>(dbFactory: DbFactory<TBindings>, env: TBindings): Promise<{
68
+ removed: number;
69
+ }>;
70
+ export {};
71
+ //# sourceMappingURL=idempotency-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency-key.d.ts","sourceRoot":"","sources":["../../src/middleware/idempotency-key.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAE7C,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7E;;;GAGG;AACH,eAAO,MAAM,0BAA0B,QAAsB,CAAA;AAmB7D,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAClE;AAED,UAAU,sBAAsB;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAED,OAAO,QAAQ,MAAM,CAAC;IACpB,UAAU,kBAAmB,SAAQ,sBAAsB;KAAG;CAC/D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc,EAC9E,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CAkID;AAiBD;;GAEG;AACH,wBAAsB,2BAA2B,CAAC,SAAS,SAAS,cAAc,EAChF,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC,EAC/B,GAAG,EAAE,SAAS,GACb,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAO9B"}
@@ -0,0 +1,179 @@
1
+ import { infraIdempotencyKeysTable } from "@voyantjs/db/schema/infra";
2
+ import { and, eq, lt } from "drizzle-orm";
3
+ /**
4
+ * Twenty-four hours, in milliseconds. Default TTL for stored idempotency
5
+ * keys. Tunable per middleware instance.
6
+ */
7
+ export const DEFAULT_IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1000;
8
+ const HEADER_NAME = "Idempotency-Key";
9
+ /**
10
+ * Hashes a UTF-8 string with SHA-256 and returns the hex digest. Uses Web
11
+ * Crypto so the middleware works in Cloudflare Workers and Node.
12
+ */
13
+ async function sha256Hex(value) {
14
+ const data = new TextEncoder().encode(value);
15
+ const digest = await crypto.subtle.digest("SHA-256", data);
16
+ const bytes = new Uint8Array(digest);
17
+ let out = "";
18
+ for (const byte of bytes) {
19
+ out += byte.toString(16).padStart(2, "0");
20
+ }
21
+ return out;
22
+ }
23
+ /**
24
+ * Idempotency-Key middleware.
25
+ *
26
+ * On request:
27
+ * - reads the `Idempotency-Key` header
28
+ * - if absent and `required` is false, passes through
29
+ * - if absent and `required` is true, returns 400
30
+ * - looks up `(scope, key)` in `idempotency_keys`:
31
+ * - hit + same body hash → returns the stored response (replay)
32
+ * - hit + different body hash → returns 409 (conflict)
33
+ * - miss → runs the handler, then stores the response on the way out
34
+ *
35
+ * The handler's response body is captured by cloning the response. The
36
+ * `Idempotency-Key` and a `Idempotency-Replayed: true` header are echoed
37
+ * on replay so callers can detect replays in client-side logging.
38
+ *
39
+ * The middleware reads the `db` instance off the request context (set by
40
+ * the `db` middleware that ships with `createApp`) — caller is responsible
41
+ * for ordering this middleware after `db`.
42
+ */
43
+ export function idempotencyKey(options = {}) {
44
+ const ttlMs = options.ttlMs ?? DEFAULT_IDEMPOTENCY_TTL_MS;
45
+ return async (c, next) => {
46
+ if (c.req.method === "OPTIONS")
47
+ return next();
48
+ const headerKey = c.req.header(HEADER_NAME) ?? c.req.header(HEADER_NAME.toLowerCase());
49
+ if (!headerKey) {
50
+ if (options.required) {
51
+ return c.json({ error: `${HEADER_NAME} header is required for this endpoint` }, 400);
52
+ }
53
+ return next();
54
+ }
55
+ if (headerKey.length > 255) {
56
+ return c.json({ error: `${HEADER_NAME} must be 255 characters or fewer` }, 400);
57
+ }
58
+ const scope = options.scope ?? `${c.req.method} ${new URL(c.req.url).pathname}`;
59
+ const rawBody = await c.req.text();
60
+ const bodyHash = await sha256Hex(rawBody);
61
+ const db = c.get("db");
62
+ if (!db) {
63
+ throw new Error("idempotencyKey middleware requires `db` on the request context. Mount `db()` (or `createApp`) before this middleware.");
64
+ }
65
+ // Look up an existing record. If the (scope, key) was used recently we
66
+ // either replay or 409.
67
+ const [existing] = await db
68
+ .select()
69
+ .from(infraIdempotencyKeysTable)
70
+ .where(and(eq(infraIdempotencyKeysTable.scope, scope), eq(infraIdempotencyKeysTable.key, headerKey)))
71
+ .limit(1);
72
+ if (existing) {
73
+ if (existing.expiresAt < new Date()) {
74
+ // The row is past its TTL — clean it up and proceed as if missed.
75
+ await db
76
+ .delete(infraIdempotencyKeysTable)
77
+ .where(eq(infraIdempotencyKeysTable.id, existing.id));
78
+ }
79
+ else if (existing.bodyHash !== bodyHash) {
80
+ return c.json({
81
+ error: `${HEADER_NAME} ${headerKey} was previously used with a different request body`,
82
+ }, 409);
83
+ }
84
+ else {
85
+ c.set("idempotencyKey", headerKey);
86
+ c.set("idempotencyReplayed", true);
87
+ const replayed = c.json(existing.responseBody, existing.responseStatus);
88
+ replayed.headers.set("Idempotency-Replayed", "true");
89
+ replayed.headers.set("Idempotency-Key", headerKey);
90
+ return replayed;
91
+ }
92
+ }
93
+ // Re-attach the body so downstream handlers can re-parse it. Hono's
94
+ // `c.req.text()` consumed the original; rebuild a Request whose body
95
+ // matches what we hashed.
96
+ c.req.raw = new Request(c.req.raw, { body: rawBody });
97
+ c.set("idempotencyKey", headerKey);
98
+ c.set("idempotencyReplayed", false);
99
+ await next();
100
+ // Capture the response without consuming the original.
101
+ const response = c.res;
102
+ if (!response)
103
+ return;
104
+ const cloned = response.clone();
105
+ let parsedBody = null;
106
+ try {
107
+ const text = await cloned.text();
108
+ parsedBody = text.length > 0 ? JSON.parse(text) : null;
109
+ }
110
+ catch {
111
+ // Non-JSON responses are not stored — idempotency replay only works
112
+ // for JSON endpoints. Pass through silently.
113
+ return;
114
+ }
115
+ // Only persist successful, replayable responses (2xx). 4xx/5xx leave
116
+ // the slot open so the client can retry with a corrected body.
117
+ if (response.status < 200 || response.status >= 300)
118
+ return;
119
+ const referenceId = options.extractReferenceId?.(parsedBody) ??
120
+ pickStringField(parsedBody, ["bookingId", "id"]) ??
121
+ null;
122
+ try {
123
+ await db
124
+ .insert(infraIdempotencyKeysTable)
125
+ .values({
126
+ scope,
127
+ key: headerKey,
128
+ bodyHash,
129
+ responseStatus: response.status,
130
+ responseBody: parsedBody,
131
+ referenceId,
132
+ expiresAt: new Date(Date.now() + ttlMs),
133
+ })
134
+ .onConflictDoNothing({
135
+ target: [infraIdempotencyKeysTable.scope, infraIdempotencyKeysTable.key],
136
+ });
137
+ // Echo the header so callers can confirm idempotent storage.
138
+ c.res = new Response(c.res.body, {
139
+ status: c.res.status,
140
+ statusText: c.res.statusText,
141
+ headers: new Headers(c.res.headers),
142
+ });
143
+ c.res.headers.set("Idempotency-Key", headerKey);
144
+ }
145
+ catch {
146
+ // Best-effort storage. If the write fails we still return the
147
+ // response — duplicate-detection just degrades to "no protection
148
+ // for this request." Logging is the deployment's responsibility.
149
+ }
150
+ };
151
+ }
152
+ function pickStringField(body, keys) {
153
+ if (!body || typeof body !== "object")
154
+ return null;
155
+ const obj = body;
156
+ for (const key of keys) {
157
+ const value = obj[key];
158
+ if (typeof value === "string" && value.length > 0)
159
+ return value;
160
+ // Common shape: `{ data: { id: ... } }`
161
+ if (key === "id" && obj.data && typeof obj.data === "object") {
162
+ const nested = obj.data.id;
163
+ if (typeof nested === "string" && nested.length > 0)
164
+ return nested;
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+ /**
170
+ * Sweep expired idempotency rows. Call from a daily cron.
171
+ */
172
+ export async function purgeExpiredIdempotencyKeys(dbFactory, env) {
173
+ const db = dbFactory(env);
174
+ const result = await db
175
+ .delete(infraIdempotencyKeysTable)
176
+ .where(lt(infraIdempotencyKeysTable.expiresAt, new Date()))
177
+ .returning();
178
+ return { removed: result.length };
179
+ }
@@ -2,6 +2,7 @@ export { requireAuth } from "./auth.js";
2
2
  export { cors } from "./cors.js";
3
3
  export { db } from "./db.js";
4
4
  export { errorBoundary, handleApiError, requestId } from "./error-boundary.js";
5
+ export { DEFAULT_IDEMPOTENCY_TTL_MS, type IdempotencyKeyOptions, idempotencyKey, purgeExpiredIdempotencyKeys, } from "./idempotency-key.js";
5
6
  export { consoleLoggerProvider, logger } from "./logger.js";
6
7
  export { LIVE_LIMITS, rateLimit } from "./rate-limit.js";
7
8
  export { requireActor } from "./require-actor.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC9E,OAAO,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC3D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC9E,OAAO,EACL,0BAA0B,EAC1B,KAAK,qBAAqB,EAC1B,cAAc,EACd,2BAA2B,GAC5B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC3D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA"}
@@ -2,6 +2,7 @@ export { requireAuth } from "./auth.js";
2
2
  export { cors } from "./cors.js";
3
3
  export { db } from "./db.js";
4
4
  export { errorBoundary, handleApiError, requestId } from "./error-boundary.js";
5
+ export { DEFAULT_IDEMPOTENCY_TTL_MS, idempotencyKey, purgeExpiredIdempotencyKeys, } from "./idempotency-key.js";
5
6
  export { consoleLoggerProvider, logger } from "./logger.js";
6
7
  export { LIVE_LIMITS, rateLimit } from "./rate-limit.js";
7
8
  export { requireActor } from "./require-actor.js";
@@ -12,9 +12,11 @@ import type { VoyantBindings, VoyantVariables } from "../types.js";
12
12
  * custom `auth.resolve` integration. Internal requests
13
13
  * (`isInternalRequest === true`) bypass the check.
14
14
  *
15
- * When the caller has no explicit actor, this middleware treats them as
16
- * `"staff"` to preserve backwards compatibility with existing deployments
17
- * that predate the actor concept.
15
+ * When the caller has no resolved actor, this middleware returns `401
16
+ * Unauthorized`. Earlier versions defaulted unset callers to `"staff"` for
17
+ * backwards compatibility, but that meant a misordered or missing auth
18
+ * middleware silently granted operator privileges to anonymous traffic.
19
+ * The default is now fail-closed.
18
20
  *
19
21
  * @example
20
22
  * app.use("/v1/admin/*", requireActor("staff"))
@@ -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,gBAAgB,CAAA;AAC3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAE7C,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElE;;;;;;;;;;;;;;;;;;GAkBG;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,CAoBD"}
1
+ {"version":3,"file":"require-actor.d.ts","sourceRoot":"","sources":["../../src/middleware/require-actor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAE7C,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElE;;;;;;;;;;;;;;;;;;;;GAoBG;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,CAuBD"}
@@ -9,9 +9,11 @@
9
9
  * custom `auth.resolve` integration. Internal requests
10
10
  * (`isInternalRequest === true`) bypass the check.
11
11
  *
12
- * When the caller has no explicit actor, this middleware treats them as
13
- * `"staff"` to preserve backwards compatibility with existing deployments
14
- * that predate the actor concept.
12
+ * When the caller has no resolved actor, this middleware returns `401
13
+ * Unauthorized`. Earlier versions defaulted unset callers to `"staff"` for
14
+ * backwards compatibility, but that meant a misordered or missing auth
15
+ * middleware silently granted operator privileges to anonymous traffic.
16
+ * The default is now fail-closed.
15
17
  *
16
18
  * @example
17
19
  * app.use("/v1/admin/*", requireActor("staff"))
@@ -28,7 +30,10 @@ export function requireActor(...allowed) {
28
30
  if (c.get("isInternalRequest")) {
29
31
  return next();
30
32
  }
31
- const actor = c.get("actor") ?? "staff";
33
+ const actor = c.get("actor");
34
+ if (!actor) {
35
+ return c.json({ error: "Unauthorized: actor not resolved" }, 401);
36
+ }
32
37
  if (!allowSet.has(actor)) {
33
38
  return c.json({ error: "Forbidden: actor not permitted on this surface" }, 403);
34
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/hono",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "license": "FSL-1.1-Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -77,10 +77,10 @@
77
77
  "drizzle-orm": "^0.45.2",
78
78
  "hono": "^4.12.10",
79
79
  "zod": "^4.3.6",
80
- "@voyantjs/core": "0.9.0",
81
- "@voyantjs/db": "0.9.0",
82
- "@voyantjs/types": "0.9.0",
83
- "@voyantjs/utils": "0.9.0"
80
+ "@voyantjs/core": "0.11.0",
81
+ "@voyantjs/db": "0.11.0",
82
+ "@voyantjs/types": "0.11.0",
83
+ "@voyantjs/utils": "0.11.0"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@cloudflare/workers-types": "^4.20260403.1",