@rotorsoft/act-http 1.6.0 → 1.7.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/README.md +40 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/api/index.d.ts +1 -0
- package/dist/@types/api/index.d.ts.map +1 -1
- package/dist/@types/api/sse-wiring.d.ts +166 -0
- package/dist/@types/api/sse-wiring.d.ts.map +1 -0
- package/dist/@types/hono/index.d.ts +18 -1
- package/dist/@types/hono/index.d.ts.map +1 -1
- package/dist/@types/trpc/index.d.ts +17 -1
- package/dist/@types/trpc/index.d.ts.map +1 -1
- package/dist/api/index.cjs +104 -0
- package/dist/api/index.cjs.map +1 -1
- package/dist/api/index.js +13 -1
- package/dist/chunk-QOHCL5GN.js +161 -0
- package/dist/chunk-QOHCL5GN.js.map +1 -0
- package/dist/chunk-WGFX4G27.js +14522 -0
- package/dist/chunk-WGFX4G27.js.map +1 -0
- package/dist/hono/index.cjs +148 -0
- package/dist/hono/index.cjs.map +1 -1
- package/dist/hono/index.js +61 -1
- package/dist/hono/index.js.map +1 -1
- package/dist/openapi/index.js +3 -14516
- package/dist/openapi/index.js.map +1 -1
- package/dist/trpc/index.cjs +14629 -0
- package/dist/trpc/index.cjs.map +1 -1
- package/dist/trpc/index.js +33 -1
- package/dist/trpc/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-Z6FVXDDU.js +0 -63
- package/dist/chunk-Z6FVXDDU.js.map +0 -1
package/dist/hono/index.cjs
CHANGED
|
@@ -55,6 +55,7 @@ var zValidator = zValidatorFunction;
|
|
|
55
55
|
|
|
56
56
|
// src/hono/index.ts
|
|
57
57
|
var import_hono = require("hono");
|
|
58
|
+
var import_streaming = require("hono/streaming");
|
|
58
59
|
|
|
59
60
|
// src/api/errors.ts
|
|
60
61
|
var import_act = require("@rotorsoft/act");
|
|
@@ -107,6 +108,98 @@ async function withIdempotency(store, key, handler) {
|
|
|
107
108
|
return { deduped: false, result: await handler() };
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
// src/api/sse-wiring.ts
|
|
112
|
+
var DEFAULT_SSE_MAX_CONNECTIONS = 500;
|
|
113
|
+
var DEFAULT_SSE_HEARTBEAT_MS = 3e4;
|
|
114
|
+
var MAX_CONNECTIONS_MIN = 1;
|
|
115
|
+
var MAX_CONNECTIONS_MAX = 1e4;
|
|
116
|
+
var HEARTBEAT_MS_MIN = 15e3;
|
|
117
|
+
var HEARTBEAT_MS_MAX = 3e5;
|
|
118
|
+
function resolveSseConfig(options) {
|
|
119
|
+
const max_connections = options.maxConnections ?? DEFAULT_SSE_MAX_CONNECTIONS;
|
|
120
|
+
const heartbeat_ms = options.heartbeatMs ?? DEFAULT_SSE_HEARTBEAT_MS;
|
|
121
|
+
if (!Number.isFinite(max_connections) || max_connections < MAX_CONNECTIONS_MIN || max_connections > MAX_CONNECTIONS_MAX) {
|
|
122
|
+
throw new RangeError(
|
|
123
|
+
`sse.maxConnections must be in [${MAX_CONNECTIONS_MIN}, ${MAX_CONNECTIONS_MAX}], got ${max_connections}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (!Number.isFinite(heartbeat_ms) || heartbeat_ms < HEARTBEAT_MS_MIN || heartbeat_ms > HEARTBEAT_MS_MAX) {
|
|
127
|
+
throw new RangeError(
|
|
128
|
+
`sse.heartbeatMs must be in [${HEARTBEAT_MS_MIN}, ${HEARTBEAT_MS_MAX}], got ${heartbeat_ms}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
channel: options.channel,
|
|
133
|
+
maxConnections: max_connections,
|
|
134
|
+
heartbeatMs: heartbeat_ms
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
var SseConnectionCounter = class {
|
|
138
|
+
limit;
|
|
139
|
+
_open = 0;
|
|
140
|
+
constructor(limit) {
|
|
141
|
+
this.limit = limit;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Reserve one slot. Returns `true` when accepted (caller must
|
|
145
|
+
* eventually {@link release} the slot, even on error paths) or
|
|
146
|
+
* `false` when the cap is already reached.
|
|
147
|
+
*/
|
|
148
|
+
acquire() {
|
|
149
|
+
if (this._open >= this.limit) return false;
|
|
150
|
+
this._open++;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Free a previously-{@link acquire}d slot. Always runs from a
|
|
155
|
+
* `finally` block in the transport's subscription handler so
|
|
156
|
+
* crashed handlers don't leak count.
|
|
157
|
+
*/
|
|
158
|
+
release() {
|
|
159
|
+
if (this._open > 0) this._open--;
|
|
160
|
+
}
|
|
161
|
+
/** Currently-open count. Test/observability hook. */
|
|
162
|
+
get open() {
|
|
163
|
+
return this._open;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
function fireAndForget(op) {
|
|
167
|
+
return op().catch(() => void 0);
|
|
168
|
+
}
|
|
169
|
+
async function* runSseSubscription(channel, stream_id, accounting, signal, on_cap_exceeded) {
|
|
170
|
+
if (accounting && !accounting.acquire() && on_cap_exceeded) {
|
|
171
|
+
on_cap_exceeded();
|
|
172
|
+
}
|
|
173
|
+
const pending = [];
|
|
174
|
+
let resolve_wait = null;
|
|
175
|
+
const unsubscribe = channel.subscribe(stream_id, (msg) => {
|
|
176
|
+
pending.push(msg);
|
|
177
|
+
resolve_wait?.();
|
|
178
|
+
});
|
|
179
|
+
const on_abort = () => resolve_wait?.();
|
|
180
|
+
signal?.addEventListener("abort", on_abort);
|
|
181
|
+
try {
|
|
182
|
+
const cached = channel.get_state(stream_id);
|
|
183
|
+
if (cached !== void 0) {
|
|
184
|
+
yield { kind: "state", data: cached };
|
|
185
|
+
}
|
|
186
|
+
while (!signal?.aborted) {
|
|
187
|
+
if (pending.length === 0) {
|
|
188
|
+
await new Promise((resolve) => {
|
|
189
|
+
resolve_wait = resolve;
|
|
190
|
+
});
|
|
191
|
+
if (signal?.aborted) break;
|
|
192
|
+
}
|
|
193
|
+
const msg = pending.shift();
|
|
194
|
+
yield { kind: "patch", data: msg };
|
|
195
|
+
}
|
|
196
|
+
} finally {
|
|
197
|
+
signal?.removeEventListener("abort", on_abort);
|
|
198
|
+
unsubscribe();
|
|
199
|
+
accounting?.release();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
110
203
|
// src/hono/index.ts
|
|
111
204
|
function authenticated(extractor) {
|
|
112
205
|
return async (c, next) => {
|
|
@@ -132,6 +225,61 @@ function hono(app, options) {
|
|
|
132
225
|
);
|
|
133
226
|
api.use("*", authenticated(options.actor));
|
|
134
227
|
const key_from = options.idempotency?.keyFrom ?? default_key_from;
|
|
228
|
+
if (options.sse) {
|
|
229
|
+
const sse_config = resolveSseConfig(options.sse);
|
|
230
|
+
const sse_counter = new SseConnectionCounter(sse_config.maxConnections);
|
|
231
|
+
const state_names = /* @__PURE__ */ new Set();
|
|
232
|
+
for (const action of Object.values(app.registry.actions)) {
|
|
233
|
+
state_names.add(action.name);
|
|
234
|
+
}
|
|
235
|
+
for (const state_name of state_names) {
|
|
236
|
+
api.get(`/sse/${state_name}`, async (c) => {
|
|
237
|
+
const stream_id = c.req.query("stream");
|
|
238
|
+
if (!stream_id) {
|
|
239
|
+
const body = {
|
|
240
|
+
error: "BadRequest",
|
|
241
|
+
detail: "stream query parameter is required",
|
|
242
|
+
code: "BAD_REQUEST"
|
|
243
|
+
};
|
|
244
|
+
return c.json(body, 400);
|
|
245
|
+
}
|
|
246
|
+
if (!sse_counter.acquire()) {
|
|
247
|
+
c.header("Retry-After", "1");
|
|
248
|
+
const body = {
|
|
249
|
+
error: "ServiceUnavailable",
|
|
250
|
+
detail: "max concurrent SSE connections reached",
|
|
251
|
+
code: "SSE_BUSY"
|
|
252
|
+
};
|
|
253
|
+
return c.json(body, 503);
|
|
254
|
+
}
|
|
255
|
+
const controller = new AbortController();
|
|
256
|
+
return (0, import_streaming.streamSSE)(c, async (sse_stream) => {
|
|
257
|
+
sse_stream.onAbort(() => controller.abort());
|
|
258
|
+
const heartbeat = setInterval(() => {
|
|
259
|
+
fireAndForget(
|
|
260
|
+
() => sse_stream.writeSSE({ event: "ping", data: "" })
|
|
261
|
+
);
|
|
262
|
+
}, sse_config.heartbeatMs);
|
|
263
|
+
try {
|
|
264
|
+
for await (const frame of runSseSubscription(
|
|
265
|
+
sse_config.channel,
|
|
266
|
+
stream_id,
|
|
267
|
+
void 0,
|
|
268
|
+
controller.signal
|
|
269
|
+
)) {
|
|
270
|
+
await sse_stream.writeSSE({
|
|
271
|
+
event: frame.kind,
|
|
272
|
+
data: JSON.stringify(frame.data)
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
} finally {
|
|
276
|
+
clearInterval(heartbeat);
|
|
277
|
+
sse_counter.release();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
135
283
|
for (const [action_name, state] of Object.entries(app.registry.actions)) {
|
|
136
284
|
const schema = state.actions[action_name];
|
|
137
285
|
api.post(
|
package/dist/hono/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hono/index.ts","../../../../node_modules/.pnpm/@hono+zod-validator@0.8.0_hono@4.12.25_zod@4.4.3/node_modules/@hono/zod-validator/src/index.ts","../../src/api/errors.ts","../../src/api/idempotency.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/hono\n *\n * Hono subpath for the auto-generated API epic (#835). Walks the\n * registry of a built `Act` once at function call and emits a Hono\n * app with one `POST /actions/<name>` route per registered action,\n * Zod-validated bodies, and the shared error envelope.\n *\n * Usage:\n *\n * ```ts\n * import { hono } from \"@rotorsoft/act-http/hono\";\n *\n * const api = hono(app, {\n * actor: (c) => ({ id: c.req.header(\"x-user-id\")!, name: \"...\" }),\n * stream: (action, input, c) => `tenant-${c.req.header(\"x-tenant\")}`,\n * });\n *\n * // Mounts at /api by default — pass `basePath` to override.\n * // POST /api/actions/OpenTicket body: { title } → 200 Snapshot[] | 4xx ApiError\n * ```\n *\n * The route shape matches the flat tRPC sibling at\n * `@rotorsoft/act-http/trpc`: one endpoint per action, no\n * state-name tier. Action names are unique across the registry —\n * the framework enforces no duplicates — so an extra namespace\n * level would be overhead. Queries live in a follow-up; this\n * slice is mutations only.\n *\n * **Actor lives on the context.** The generator wires an internal\n * middleware that runs the host's\n * {@link HonoOptions.actor | actor extractor} once per call and\n * stashes the resolved {@link Actor} under `c.get(\"actor\")`. Every\n * generated route reads from there — auth resolves at one seam,\n * downstream middleware (logging, tracing) sees the same actor.\n * The middleware factory is also exported as {@link authenticated}\n * for hosts that want to compose it into their own Hono chain\n * alongside hand-written routes.\n *\n * Composes the shared utilities at `@rotorsoft/act-http/api`:\n * {@link ActorExtractor} for the auth seam, {@link toApiError} for\n * the uniform error → status / code mapping, {@link withIdempotency}\n * for `Idempotency-Key` dedup. Three transports (tRPC, this one,\n * OpenAPI) ride the same utilities so a client speaking two of\n * them never sees two shapes for the same framework error.\n */\nimport { zValidator } from \"@hono/zod-validator\";\nimport type { Actor, Target } from \"@rotorsoft/act\";\nimport type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { Hono } from \"hono\";\nimport {\n type ActorExtractor,\n type ApiError,\n toApiError,\n withIdempotency,\n} from \"../api/index.js\";\n\n/**\n * Hono context variables this subpath contributes. The generic on\n * the returned middleware threads it through so routes downstream\n * of {@link authenticated} see `c.get(\"actor\")` typed without a\n * manual cast.\n */\nexport type ActMiddlewareVariables = {\n actor: Actor;\n};\n\n/**\n * Per-call options for {@link hono}. The host supplies the four\n * seams the package can't make decisions on:\n *\n * - `actor` — auth resolver. Receives the Hono context, returns the\n * `Actor` that flows onto `c.get(\"actor\")` for every generated\n * route (via an internal {@link authenticated}).\n * - `stream` — stream-id resolver. Receives the action name, the\n * validated body, and the Hono context; returns the target\n * stream. Singleton aggregates return a constant; per-tenant\n * aggregates pull from headers, path, or body.\n * - `expectedVersion` (optional) — optimistic-concurrency resolver.\n * Hosts typically read it from an `If-Match` header on the\n * request, or from the client's last-known snapshot. Returning\n * `undefined` skips the check for that call — handy when only\n * some actions are concurrency-sensitive.\n * - `idempotency` (optional) — when set, the route honors\n * `Idempotency-Key` via {@link withIdempotency}. The host\n * supplies the `IdempotencyStore`; `keyFrom` defaults to reading\n * the `Idempotency-Key` header. On a duplicate claim, the route\n * responds `409 Conflict` — the contract intentionally doesn't\n * cache the original handler's result, matching the\n * receiver-side \"ack the duplicate\" semantics.\n * - `basePath` (optional, default `\"/api\"`) — Hono basePath the\n * routes mount under.\n */\nexport type HonoOptions = {\n readonly actor: ActorExtractor;\n readonly stream: (\n action_name: string,\n input: unknown,\n c: Context\n ) => string | Promise<string>;\n readonly expectedVersion?: (\n action_name: string,\n input: unknown,\n c: Context\n ) => number | undefined | Promise<number | undefined>;\n readonly idempotency?: {\n readonly store: IdempotencyStore;\n readonly keyFrom?: (c: Context) => string | undefined;\n };\n readonly basePath?: string;\n};\n\n/**\n * Build a Hono middleware that runs an {@link ActorExtractor} once\n * per call and stashes the resolved {@link Actor} under\n * `c.set(\"actor\", ...)` so downstream routes can read it via\n * `c.get(\"actor\")`. The generator at {@link hono} uses this\n * internally; it's also exported so hosts that want to compose\n * their own route chain (logging, tracing, custom auth flavors)\n * can wire it directly:\n *\n * ```ts\n * import { Hono } from \"hono\";\n * import { authenticated, type ActMiddlewareVariables } from \"@rotorsoft/act-http/hono\";\n *\n * const api = new Hono<{ Variables: ActMiddlewareVariables }>();\n * api.use(\"*\", authenticated((c) => resolveUserFromJwt(c)));\n * api.get(\"/me\", (c) => c.json(c.get(\"actor\"))); // typed\n * ```\n *\n * Errors thrown by the extractor surface as `401 Unauthorized` with\n * the shared {@link ApiError} envelope.\n */\nexport function authenticated(\n extractor: ActorExtractor\n): MiddlewareHandler<{ Variables: ActMiddlewareVariables }> {\n return async (c, next) => {\n try {\n const actor = await extractor(c);\n c.set(\"actor\", actor);\n await next();\n } catch (err) {\n const body: ApiError = {\n error: \"Unauthorized\",\n detail: err instanceof Error ? err.message : undefined,\n code: \"UNAUTHORIZED\",\n };\n return c.json(body, 401);\n }\n };\n}\n\nconst default_key_from = (c: Context): string | undefined =>\n c.req.header(\"idempotency-key\");\n\n/**\n * Build a typed Hono REST surface from a built `Act` instance.\n *\n * Walks `app.registry.actions` once and emits one\n * `POST /actions/<actionName>` per action under the configured\n * `basePath` (default `/api`). Each route:\n *\n * 1. Runs the internal {@link authenticated} middleware —\n * `options.actor` resolves the {@link Actor} once and stashes\n * it under `c.get(\"actor\")`.\n * 2. Validates the JSON body against the action's Zod schema via\n * `@hono/zod-validator`. Failures short-circuit with `422`.\n * 3. Resolves the target stream via\n * `options.stream(name, input, c)`.\n * 4. (Optionally) resolves `expectedVersion` for optimistic\n * concurrency.\n * 5. (Optionally) claims the `Idempotency-Key` via\n * {@link withIdempotency} — responds `409` on duplicate.\n * 6. Calls\n * `app.do(name, { stream, actor, expectedVersion? }, input)`.\n * 7. Maps any thrown framework error onto the shared\n * {@link ApiError} envelope via {@link toApiError}, returning\n * the conventional HTTP status (`409` / `412` for concurrency,\n * `422` for validation, `400` for non-retryable, etc.).\n *\n * @param app A built `Act` orchestrator.\n * @param options Auth, stream, expected-version, idempotency, and\n * base-path seams.\n * @returns A Hono app covering every registered action under\n * `<basePath>/actions/<name>`.\n */\n/**\n * Structural shape of the Act surface this generator walks at\n * runtime — the registry's action-name → owning-state map plus the\n * `do(...)` dispatch. Letting TApp infer to the caller's concrete\n * `Act<...>` against this structural bound keeps nested variance\n * out of the signature and avoids `any` in either direction.\n *\n * @internal\n */\ntype ActSurface = {\n readonly registry: {\n actions: Record<\n string,\n {\n readonly name: string;\n readonly actions: Record<string, unknown>;\n }\n >;\n };\n do(action: string, target: Target, payload: unknown): Promise<unknown>;\n};\n\nexport function hono<TApp extends ActSurface = ActSurface>(\n app: TApp,\n options: HonoOptions\n): Hono<{ Variables: ActMiddlewareVariables }> {\n const base_path = options.basePath ?? \"/api\";\n const api = new Hono<{ Variables: ActMiddlewareVariables }>().basePath(\n base_path\n );\n api.use(\"*\", authenticated(options.actor));\n\n const key_from = options.idempotency?.keyFrom ?? default_key_from;\n\n for (const [action_name, state] of Object.entries(app.registry.actions)) {\n const schema = state.actions[action_name];\n api.post(\n `/actions/${action_name}`,\n zValidator(\"json\", schema as never),\n async (c) => {\n try {\n const input = c.req.valid(\"json\" as never) as unknown;\n const actor = c.get(\"actor\");\n const stream = await options.stream(action_name, input, c);\n const expected_version = options.expectedVersion\n ? await options.expectedVersion(action_name, input, c)\n : undefined;\n const target =\n expected_version === undefined\n ? { stream, actor }\n : { stream, actor, expectedVersion: expected_version };\n\n if (options.idempotency) {\n const key = key_from(c);\n if (!key) {\n const body: ApiError = {\n error: \"BadRequest\",\n detail: \"Idempotency-Key required\",\n code: \"BAD_REQUEST\",\n };\n return c.json(body, 400);\n }\n const outcome = await withIdempotency(\n options.idempotency.store,\n key,\n () =>\n app.do(action_name as never, target as never, input as never)\n );\n if (outcome.deduped) {\n const body: ApiError = {\n error: \"Conflict\",\n detail:\n \"Idempotency-Key already used; original result not cached\",\n code: \"CONFLICT\",\n };\n return c.json(body, 409);\n }\n return c.json(outcome.result);\n }\n\n const snapshots = await app.do(\n action_name as never,\n target as never,\n input as never\n );\n return c.json(snapshots);\n } catch (err) {\n const { status, body } = toApiError(err);\n // Hono types narrow `status` to its known status-number union;\n // `toApiError` returns a numeric status from a closed set that\n // matches but TypeScript can't prove it through the union.\n return c.json(body, status as 400 | 409 | 410 | 412 | 422 | 500);\n }\n }\n );\n }\n\n return api;\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono'\nimport { validator } from 'hono/validator'\nimport type * as v3 from 'zod/v3'\nimport type { ZodSafeParseResult as v4ZodSafeParseResult } from 'zod/v4'\nimport type * as v4 from 'zod/v4/core'\nimport type { InferInput } from './utils'\n\ntype ZodSchema = v3.ZodType | v4.$ZodType\ntype ZodError<T extends ZodSchema> = T extends v4.$ZodType\n ? v4.$ZodError<v4.output<T>>\n : v3.ZodError\ntype ZodSafeParseResult<T, T2, T3 extends ZodSchema> = T3 extends v4.$ZodType\n ? v4ZodSafeParseResult<T>\n : v3.SafeParseReturnType<T, T2>\ntype zInput<T> = T extends v3.ZodType ? v3.input<T> : T extends v4.$ZodType ? v4.input<T> : never\ntype zOutput<T> = T extends v3.ZodType ? v3.output<T> : T extends v4.$ZodType ? v4.output<T> : never\ntype zInfer<T> = T extends v3.ZodType ? v3.infer<T> : T extends v4.$ZodType ? v4.infer<T> : never\n\n// Failure variant of `safeParseAsync`'s return value.\n// Mirrors the runtime body returned by `c.json(result, 400)` on validation failure.\ntype ZodValidatorFailureBody<T extends ZodSchema> = T extends v4.$ZodType\n ? Extract<v4ZodSafeParseResult<v4.output<T>>, { success: false }>\n : T extends v3.ZodType\n ? Extract<v3.SafeParseReturnType<v3.input<T>, v3.output<T>>, { success: false }>\n : never\n\ntype ZodValidatorFailureResponse<T extends ZodSchema> = Response &\n TypedResponse<ZodValidatorFailureBody<T>, 400, 'json'>\n\nexport type Hook<\n T,\n E extends Env,\n P extends string,\n Target extends keyof ValidationTargets = keyof ValidationTargets,\n O = {},\n Schema extends ZodSchema = any,\n> = (\n result: ({ success: true; data: T } | { success: false; error: ZodError<Schema>; data: T }) & {\n target: Target\n },\n c: Context<E, P>\n) => Response | void | TypedResponse<O> | Promise<Response | void | TypedResponse<O>>\n\ntype HasUndefined<T> = undefined extends T ? true : false\n\ntype ExtractValidationResponse<VF> = VF extends (value: any, c: any) => infer R\n ? R extends Promise<infer PR>\n ? PR extends TypedResponse<infer T, infer S, infer F>\n ? Response & TypedResponse<T, S, F>\n : PR extends Response\n ? PR\n : PR extends undefined\n ? never\n : never\n : R extends TypedResponse<infer T, infer S, infer F>\n ? Response & TypedResponse<T, S, F>\n : R extends Response\n ? R\n : R extends undefined\n ? never\n : never\n : never\n\ntype ValidationFunctionOption<T extends ZodSchema, Target extends keyof ValidationTargets> = {\n validationFunction: (\n schema: T,\n value: ValidationTargets[Target]\n ) => ZodSafeParseResult<any, any, T> | Promise<ZodSafeParseResult<any, any, T>>\n}\n\ntype DefaultInput<Target extends keyof ValidationTargets, In, Out> = {\n in: HasUndefined<In> extends true\n ? {\n [K in Target]?: [In] extends [ValidationTargets[K]] ? In : InferInput<In, K>\n }\n : {\n [K in Target]: [In] extends [ValidationTargets[K]] ? In : InferInput<In, K>\n }\n out: { [K in Target]: Out }\n}\n\n// without hook (with or without options)\nfunction zValidatorFunction<\n T extends ZodSchema,\n Target extends keyof ValidationTargets,\n E extends Env,\n P extends string,\n In = zInput<T>,\n Out = zOutput<T>,\n I extends Input = DefaultInput<Target, In, Out>,\n V extends I = I,\n>(\n target: Target,\n schema: T,\n hook?: undefined,\n options?: ValidationFunctionOption<T, Target>\n): MiddlewareHandler<E, P, V, ZodValidatorFailureResponse<T>>\n\n// with hook and options\nfunction zValidatorFunction<\n T extends ZodSchema,\n Target extends keyof ValidationTargets,\n E extends Env,\n P extends string,\n HookFn extends Hook<InferredValue, E, P, Target, {}, T>,\n In = zInput<T>,\n Out = zOutput<T>,\n I extends Input = DefaultInput<Target, In, Out>,\n V extends I = I,\n InferredValue = zInfer<T>,\n>(\n target: Target,\n schema: T,\n hook?: HookFn,\n options?: ValidationFunctionOption<T, Target>\n): MiddlewareHandler<E, P, V, ExtractValidationResponse<HookFn>>\n\n// implementation\nfunction zValidatorFunction<\n T extends ZodSchema,\n Target extends keyof ValidationTargets,\n E extends Env,\n P extends string,\n HookFn extends Hook<InferredValue, E, P, Target, {}, T>,\n In = zInput<T>,\n Out = zOutput<T>,\n I extends Input = DefaultInput<Target, In, Out>,\n V extends I = I,\n InferredValue = zInfer<T>,\n>(\n target: Target,\n schema: T,\n hook?: HookFn,\n options?: ValidationFunctionOption<T, Target>\n):\n | MiddlewareHandler<E, P, V, ZodValidatorFailureResponse<T>>\n | MiddlewareHandler<E, P, V, ExtractValidationResponse<HookFn>> {\n // @ts-expect-error not typed well\n return validator(target, async (value: ValidationTargets[Target], c) => {\n let validatorValue = value\n\n // in case where our `target` === `header`, Hono parses all of the headers into lowercase.\n // this might not match the Zod schema, so we want to make sure that we account for that when parsing the schema.\n if ((target === 'header' && '_def' in schema) || (target === 'header' && '_zod' in schema)) {\n // create an object that maps lowercase schema keys to lowercase\n // @ts-expect-error the schema is a Zod Schema\n const schemaKeys = Object.keys('in' in schema ? schema.in.shape : schema.shape)\n const caseInsensitiveKeymap = Object.fromEntries(\n schemaKeys.map((key) => [key.toLowerCase(), key])\n )\n\n validatorValue = Object.fromEntries(\n Object.entries(value).map(([key, value]) => [caseInsensitiveKeymap[key] || key, value])\n )\n }\n\n const result =\n options && options.validationFunction\n ? await options.validationFunction(schema, validatorValue)\n : // @ts-expect-error z4.$ZodType has safeParseAsync\n await schema.safeParseAsync(validatorValue)\n\n if (hook) {\n const hookResult = await hook({ data: validatorValue, ...result, target }, c)\n if (hookResult) {\n if (hookResult instanceof Response) {\n return hookResult\n }\n\n if ('response' in hookResult) {\n return hookResult.response\n }\n }\n }\n\n if (!result.success) {\n return c.json(result, 400)\n }\n\n return result.data as zInfer<T>\n })\n}\n\nexport const zValidator: typeof zValidatorFunction = zValidatorFunction\n","import {\n ConcurrencyError,\n InvariantError,\n NonRetryableError,\n StreamClosedError,\n ValidationError,\n} from \"@rotorsoft/act\";\n\n/**\n * Uniform error envelope shipped over the wire by every act-http\n * transport. Hosts get the same shape from REST, tRPC, and OpenAPI —\n * a client that talks to two transports doesn't have to invent two\n * error parsers.\n *\n * - `error` — the framework error name (`\"ValidationError\"`,\n * `\"InvariantError\"`, …). Stable identifier, safe to switch on.\n * - `detail` — the framework's message text. Human-readable; not\n * parsed by clients.\n * - `code` — a machine-readable status code from {@link ERROR_MAP}\n * for clients that prefer enum-style branching over name strings.\n */\nexport type ApiError = {\n error: string;\n detail?: string;\n code?: string;\n};\n\n/**\n * Status + code pair for one known framework error.\n */\nexport type ErrorMapEntry = {\n status: number;\n code: string;\n};\n\n/**\n * The single table that maps framework error types to HTTP status\n * codes and machine-readable codes. One table, three consumers\n * (Hono, tRPC, OpenAPI) — cross-transport consistency by\n * construction.\n *\n * Operators wanting different mappings wrap the generated transport\n * rather than mutating this — the consistency is the load-bearing\n * property, not the specific status codes.\n */\nexport const ERROR_MAP = {\n ValidationError: { status: 422, code: \"VALIDATION\" },\n InvariantError: { status: 409, code: \"INVARIANT\" },\n ConcurrencyError: { status: 412, code: \"CONCURRENCY\" },\n StreamClosedError: { status: 410, code: \"STREAM_CLOSED\" },\n NonRetryableError: { status: 400, code: \"NON_RETRYABLE\" },\n} as const satisfies Record<string, ErrorMapEntry>;\n\nconst lookup_known = (\n err: unknown\n): { name: keyof typeof ERROR_MAP } | null => {\n if (err instanceof ValidationError) return { name: \"ValidationError\" };\n if (err instanceof InvariantError) return { name: \"InvariantError\" };\n if (err instanceof ConcurrencyError) return { name: \"ConcurrencyError\" };\n if (err instanceof StreamClosedError) return { name: \"StreamClosedError\" };\n if (err instanceof NonRetryableError) return { name: \"NonRetryableError\" };\n return null;\n};\n\n/**\n * Translate an unknown thrown value into the canonical\n * {@link ApiError} envelope plus HTTP status. Each transport's error\n * boundary calls this once and forwards the result to the wire.\n *\n * Known framework errors map per {@link ERROR_MAP}. Everything else\n * surfaces as a 500 with `code: \"INTERNAL\"`; the `detail` field is\n * populated when the throw was an `Error` instance, omitted\n * otherwise (a thrown string or object doesn't get to leak its\n * payload to the client).\n */\nexport function toApiError(err: unknown): { status: number; body: ApiError } {\n const known = lookup_known(err);\n if (known) {\n const entry = ERROR_MAP[known.name];\n return {\n status: entry.status,\n body: {\n error: known.name,\n detail: (err as Error).message,\n code: entry.code,\n },\n };\n }\n if (err instanceof Error) {\n return {\n status: 500,\n body: { error: \"InternalError\", detail: err.message, code: \"INTERNAL\" },\n };\n }\n return {\n status: 500,\n body: { error: \"InternalError\", code: \"INTERNAL\" },\n };\n}\n","import type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\n\n/**\n * Result of a {@link withIdempotency} call.\n *\n * - `{ deduped: false, result }` — the claim was fresh; the handler\n * ran and produced `result`.\n * - `{ deduped: true }` — the claim was already taken; the handler\n * was *not* invoked. The caller decides how to respond (typically\n * a 2xx with no body, matching the receiver-side convention).\n *\n * Note: the contract does not cache the previous response. A\n * duplicate call returns the deduped marker only — replaying the\n * original handler's output would require a response-caching\n * adapter, which is out of scope here. The receiver-side convention\n * (and the convention the generated transports follow) is \"ack the\n * duplicate; do nothing else.\"\n */\nexport type IdempotencyResult<T> =\n | { deduped: false; result: T }\n | { deduped: true };\n\n/**\n * Wrap an action handler so the framework honors `Idempotency-Key`\n * dedup. Acquires the key via {@link IdempotencyStore.claim}, runs\n * the handler exactly when the claim was fresh, and skips the\n * handler entirely on a duplicate.\n *\n * Reuses the contract `@rotorsoft/act-ops/idempotency` already\n * defines for the receiver-side `Idempotency-Key` story. A single\n * `IdempotencyStore` implementation covers both halves of the \"Act\n * over the wire\" surface — receiver and generated API.\n */\nexport async function withIdempotency<T>(\n store: IdempotencyStore,\n key: string,\n handler: () => Promise<T>\n): Promise<IdempotencyResult<T>> {\n const fresh = await store.claim(key);\n if (!fresh) {\n return { deduped: true };\n }\n return { deduped: false, result: await handler() };\n}\n"],"mappings":"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","names":["value","zValidator: typeof zValidatorFunction"]}
|
|
1
|
+
{"version":3,"sources":["../../src/hono/index.ts","../../../../node_modules/.pnpm/@hono+zod-validator@0.8.0_hono@4.12.25_zod@4.4.3/node_modules/@hono/zod-validator/src/index.ts","../../src/api/errors.ts","../../src/api/idempotency.ts","../../src/api/sse-wiring.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/hono\n *\n * Hono subpath for the auto-generated API epic (#835). Walks the\n * registry of a built `Act` once at function call and emits a Hono\n * app with one `POST /actions/<name>` route per registered action,\n * Zod-validated bodies, and the shared error envelope.\n *\n * Usage:\n *\n * ```ts\n * import { hono } from \"@rotorsoft/act-http/hono\";\n *\n * const api = hono(app, {\n * actor: (c) => ({ id: c.req.header(\"x-user-id\")!, name: \"...\" }),\n * stream: (action, input, c) => `tenant-${c.req.header(\"x-tenant\")}`,\n * });\n *\n * // Mounts at /api by default — pass `basePath` to override.\n * // POST /api/actions/OpenTicket body: { title } → 200 Snapshot[] | 4xx ApiError\n * ```\n *\n * The route shape matches the flat tRPC sibling at\n * `@rotorsoft/act-http/trpc`: one endpoint per action, no\n * state-name tier. Action names are unique across the registry —\n * the framework enforces no duplicates — so an extra namespace\n * level would be overhead. Queries live in a follow-up; this\n * slice is mutations only.\n *\n * **Actor lives on the context.** The generator wires an internal\n * middleware that runs the host's\n * {@link HonoOptions.actor | actor extractor} once per call and\n * stashes the resolved {@link Actor} under `c.get(\"actor\")`. Every\n * generated route reads from there — auth resolves at one seam,\n * downstream middleware (logging, tracing) sees the same actor.\n * The middleware factory is also exported as {@link authenticated}\n * for hosts that want to compose it into their own Hono chain\n * alongside hand-written routes.\n *\n * Composes the shared utilities at `@rotorsoft/act-http/api`:\n * {@link ActorExtractor} for the auth seam, {@link toApiError} for\n * the uniform error → status / code mapping, {@link withIdempotency}\n * for `Idempotency-Key` dedup. Three transports (tRPC, this one,\n * OpenAPI) ride the same utilities so a client speaking two of\n * them never sees two shapes for the same framework error.\n */\nimport { zValidator } from \"@hono/zod-validator\";\nimport type { Actor, Target } from \"@rotorsoft/act\";\nimport type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { Hono } from \"hono\";\nimport { streamSSE } from \"hono/streaming\";\nimport {\n type ActorExtractor,\n type ApiError,\n fireAndForget,\n resolveSseConfig,\n runSseSubscription,\n SseConnectionCounter,\n type SseOptions,\n toApiError,\n withIdempotency,\n} from \"../api/index.js\";\n\n/**\n * Hono context variables this subpath contributes. The generic on\n * the returned middleware threads it through so routes downstream\n * of {@link authenticated} see `c.get(\"actor\")` typed without a\n * manual cast.\n */\nexport type ActMiddlewareVariables = {\n actor: Actor;\n};\n\n/**\n * Per-call options for {@link hono}. The host supplies the four\n * seams the package can't make decisions on:\n *\n * - `actor` — auth resolver. Receives the Hono context, returns the\n * `Actor` that flows onto `c.get(\"actor\")` for every generated\n * route (via an internal {@link authenticated}).\n * - `stream` — stream-id resolver. Receives the action name, the\n * validated body, and the Hono context; returns the target\n * stream. Singleton aggregates return a constant; per-tenant\n * aggregates pull from headers, path, or body.\n * - `expectedVersion` (optional) — optimistic-concurrency resolver.\n * Hosts typically read it from an `If-Match` header on the\n * request, or from the client's last-known snapshot. Returning\n * `undefined` skips the check for that call — handy when only\n * some actions are concurrency-sensitive.\n * - `idempotency` (optional) — when set, the route honors\n * `Idempotency-Key` via {@link withIdempotency}. The host\n * supplies the `IdempotencyStore`; `keyFrom` defaults to reading\n * the `Idempotency-Key` header. On a duplicate claim, the route\n * responds `409 Conflict` — the contract intentionally doesn't\n * cache the original handler's result, matching the\n * receiver-side \"ack the duplicate\" semantics.\n * - `basePath` (optional, default `\"/api\"`) — Hono basePath the\n * routes mount under.\n */\nexport type HonoOptions = {\n readonly actor: ActorExtractor;\n readonly stream: (\n action_name: string,\n input: unknown,\n c: Context\n ) => string | Promise<string>;\n readonly expectedVersion?: (\n action_name: string,\n input: unknown,\n c: Context\n ) => number | undefined | Promise<number | undefined>;\n readonly idempotency?: {\n readonly store: IdempotencyStore;\n readonly keyFrom?: (c: Context) => string | undefined;\n };\n /**\n * Optional SSE wiring. When set, the generator emits one\n * `GET <basePath>/sse/<stateName>?stream=<streamId>` per unique\n * state name in the registry. The endpoint runs the host\n * {@link actor} extractor, looks up the streamId from\n * `?stream=...`, opens a `text/event-stream`, yields the cached\n * state (if any), and forwards every patch published to the\n * shared {@link SseOptions.channel}. A heartbeat keeps proxies\n * from idling the connection out; the per-process connection cap\n * returns `503 Service Unavailable` (with `Retry-After: 1`) when\n * full so operators never see silent stalls.\n *\n * Off by default — most APIs don't expose live state to every\n * client, and opening one is widening both the auth and cost\n * surface.\n */\n readonly sse?: SseOptions;\n readonly basePath?: string;\n};\n\n/**\n * Build a Hono middleware that runs an {@link ActorExtractor} once\n * per call and stashes the resolved {@link Actor} under\n * `c.set(\"actor\", ...)` so downstream routes can read it via\n * `c.get(\"actor\")`. The generator at {@link hono} uses this\n * internally; it's also exported so hosts that want to compose\n * their own route chain (logging, tracing, custom auth flavors)\n * can wire it directly:\n *\n * ```ts\n * import { Hono } from \"hono\";\n * import { authenticated, type ActMiddlewareVariables } from \"@rotorsoft/act-http/hono\";\n *\n * const api = new Hono<{ Variables: ActMiddlewareVariables }>();\n * api.use(\"*\", authenticated((c) => resolveUserFromJwt(c)));\n * api.get(\"/me\", (c) => c.json(c.get(\"actor\"))); // typed\n * ```\n *\n * Errors thrown by the extractor surface as `401 Unauthorized` with\n * the shared {@link ApiError} envelope.\n */\nexport function authenticated(\n extractor: ActorExtractor\n): MiddlewareHandler<{ Variables: ActMiddlewareVariables }> {\n return async (c, next) => {\n try {\n const actor = await extractor(c);\n c.set(\"actor\", actor);\n await next();\n } catch (err) {\n const body: ApiError = {\n error: \"Unauthorized\",\n detail: err instanceof Error ? err.message : undefined,\n code: \"UNAUTHORIZED\",\n };\n return c.json(body, 401);\n }\n };\n}\n\nconst default_key_from = (c: Context): string | undefined =>\n c.req.header(\"idempotency-key\");\n\n/**\n * Build a typed Hono REST surface from a built `Act` instance.\n *\n * Walks `app.registry.actions` once and emits one\n * `POST /actions/<actionName>` per action under the configured\n * `basePath` (default `/api`). Each route:\n *\n * 1. Runs the internal {@link authenticated} middleware —\n * `options.actor` resolves the {@link Actor} once and stashes\n * it under `c.get(\"actor\")`.\n * 2. Validates the JSON body against the action's Zod schema via\n * `@hono/zod-validator`. Failures short-circuit with `422`.\n * 3. Resolves the target stream via\n * `options.stream(name, input, c)`.\n * 4. (Optionally) resolves `expectedVersion` for optimistic\n * concurrency.\n * 5. (Optionally) claims the `Idempotency-Key` via\n * {@link withIdempotency} — responds `409` on duplicate.\n * 6. Calls\n * `app.do(name, { stream, actor, expectedVersion? }, input)`.\n * 7. Maps any thrown framework error onto the shared\n * {@link ApiError} envelope via {@link toApiError}, returning\n * the conventional HTTP status (`409` / `412` for concurrency,\n * `422` for validation, `400` for non-retryable, etc.).\n *\n * @param app A built `Act` orchestrator.\n * @param options Auth, stream, expected-version, idempotency, and\n * base-path seams.\n * @returns A Hono app covering every registered action under\n * `<basePath>/actions/<name>`.\n */\n/**\n * Structural shape of the Act surface this generator walks at\n * runtime — the registry's action-name → owning-state map plus the\n * `do(...)` dispatch. Letting TApp infer to the caller's concrete\n * `Act<...>` against this structural bound keeps nested variance\n * out of the signature and avoids `any` in either direction.\n *\n * @internal\n */\ntype ActSurface = {\n readonly registry: {\n actions: Record<\n string,\n {\n readonly name: string;\n readonly actions: Record<string, unknown>;\n }\n >;\n };\n do(action: string, target: Target, payload: unknown): Promise<unknown>;\n};\n\nexport function hono<TApp extends ActSurface = ActSurface>(\n app: TApp,\n options: HonoOptions\n): Hono<{ Variables: ActMiddlewareVariables }> {\n const base_path = options.basePath ?? \"/api\";\n const api = new Hono<{ Variables: ActMiddlewareVariables }>().basePath(\n base_path\n );\n api.use(\"*\", authenticated(options.actor));\n\n const key_from = options.idempotency?.keyFrom ?? default_key_from;\n\n // Wire SSE subscriptions before the mutation routes so the\n // `/sse/*` paths sit alongside `/actions/*`. The cap counter is\n // shared by every state-name's GET route on this generator\n // instance so a noisy room on one state doesn't get a free pass\n // on another.\n if (options.sse) {\n const sse_config = resolveSseConfig(options.sse);\n const sse_counter = new SseConnectionCounter(sse_config.maxConnections);\n const state_names = new Set<string>();\n for (const action of Object.values(app.registry.actions)) {\n state_names.add(action.name);\n }\n for (const state_name of state_names) {\n api.get(`/sse/${state_name}`, async (c) => {\n const stream_id = c.req.query(\"stream\");\n if (!stream_id) {\n const body: ApiError = {\n error: \"BadRequest\",\n detail: \"stream query parameter is required\",\n code: \"BAD_REQUEST\",\n };\n return c.json(body, 400);\n }\n if (!sse_counter.acquire()) {\n c.header(\"Retry-After\", \"1\");\n const body: ApiError = {\n error: \"ServiceUnavailable\",\n detail: \"max concurrent SSE connections reached\",\n code: \"SSE_BUSY\",\n };\n return c.json(body, 503);\n }\n // The route already acquired the slot above so streamSSE\n // could return `503` before writing headers if the cap was\n // full. The shared loop runs without accounting; the route\n // releases the slot in `finally`.\n const controller = new AbortController();\n return streamSSE(c, async (sse_stream) => {\n sse_stream.onAbort(() => controller.abort());\n const heartbeat = setInterval(() => {\n fireAndForget(() =>\n sse_stream.writeSSE({ event: \"ping\", data: \"\" })\n );\n }, sse_config.heartbeatMs);\n try {\n for await (const frame of runSseSubscription(\n sse_config.channel,\n stream_id,\n undefined,\n controller.signal\n )) {\n await sse_stream.writeSSE({\n event: frame.kind,\n data: JSON.stringify(frame.data),\n });\n }\n } finally {\n clearInterval(heartbeat);\n sse_counter.release();\n }\n });\n });\n }\n }\n\n for (const [action_name, state] of Object.entries(app.registry.actions)) {\n const schema = state.actions[action_name];\n api.post(\n `/actions/${action_name}`,\n zValidator(\"json\", schema as never),\n async (c) => {\n try {\n const input = c.req.valid(\"json\" as never) as unknown;\n const actor = c.get(\"actor\");\n const stream = await options.stream(action_name, input, c);\n const expected_version = options.expectedVersion\n ? await options.expectedVersion(action_name, input, c)\n : undefined;\n const target =\n expected_version === undefined\n ? { stream, actor }\n : { stream, actor, expectedVersion: expected_version };\n\n if (options.idempotency) {\n const key = key_from(c);\n if (!key) {\n const body: ApiError = {\n error: \"BadRequest\",\n detail: \"Idempotency-Key required\",\n code: \"BAD_REQUEST\",\n };\n return c.json(body, 400);\n }\n const outcome = await withIdempotency(\n options.idempotency.store,\n key,\n () =>\n app.do(action_name as never, target as never, input as never)\n );\n if (outcome.deduped) {\n const body: ApiError = {\n error: \"Conflict\",\n detail:\n \"Idempotency-Key already used; original result not cached\",\n code: \"CONFLICT\",\n };\n return c.json(body, 409);\n }\n return c.json(outcome.result);\n }\n\n const snapshots = await app.do(\n action_name as never,\n target as never,\n input as never\n );\n return c.json(snapshots);\n } catch (err) {\n const { status, body } = toApiError(err);\n // Hono types narrow `status` to its known status-number union;\n // `toApiError` returns a numeric status from a closed set that\n // matches but TypeScript can't prove it through the union.\n return c.json(body, status as 400 | 409 | 410 | 412 | 422 | 500);\n }\n }\n );\n }\n\n return api;\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono'\nimport { validator } from 'hono/validator'\nimport type * as v3 from 'zod/v3'\nimport type { ZodSafeParseResult as v4ZodSafeParseResult } from 'zod/v4'\nimport type * as v4 from 'zod/v4/core'\nimport type { InferInput } from './utils'\n\ntype ZodSchema = v3.ZodType | v4.$ZodType\ntype ZodError<T extends ZodSchema> = T extends v4.$ZodType\n ? v4.$ZodError<v4.output<T>>\n : v3.ZodError\ntype ZodSafeParseResult<T, T2, T3 extends ZodSchema> = T3 extends v4.$ZodType\n ? v4ZodSafeParseResult<T>\n : v3.SafeParseReturnType<T, T2>\ntype zInput<T> = T extends v3.ZodType ? v3.input<T> : T extends v4.$ZodType ? v4.input<T> : never\ntype zOutput<T> = T extends v3.ZodType ? v3.output<T> : T extends v4.$ZodType ? v4.output<T> : never\ntype zInfer<T> = T extends v3.ZodType ? v3.infer<T> : T extends v4.$ZodType ? v4.infer<T> : never\n\n// Failure variant of `safeParseAsync`'s return value.\n// Mirrors the runtime body returned by `c.json(result, 400)` on validation failure.\ntype ZodValidatorFailureBody<T extends ZodSchema> = T extends v4.$ZodType\n ? Extract<v4ZodSafeParseResult<v4.output<T>>, { success: false }>\n : T extends v3.ZodType\n ? Extract<v3.SafeParseReturnType<v3.input<T>, v3.output<T>>, { success: false }>\n : never\n\ntype ZodValidatorFailureResponse<T extends ZodSchema> = Response &\n TypedResponse<ZodValidatorFailureBody<T>, 400, 'json'>\n\nexport type Hook<\n T,\n E extends Env,\n P extends string,\n Target extends keyof ValidationTargets = keyof ValidationTargets,\n O = {},\n Schema extends ZodSchema = any,\n> = (\n result: ({ success: true; data: T } | { success: false; error: ZodError<Schema>; data: T }) & {\n target: Target\n },\n c: Context<E, P>\n) => Response | void | TypedResponse<O> | Promise<Response | void | TypedResponse<O>>\n\ntype HasUndefined<T> = undefined extends T ? true : false\n\ntype ExtractValidationResponse<VF> = VF extends (value: any, c: any) => infer R\n ? R extends Promise<infer PR>\n ? PR extends TypedResponse<infer T, infer S, infer F>\n ? Response & TypedResponse<T, S, F>\n : PR extends Response\n ? PR\n : PR extends undefined\n ? never\n : never\n : R extends TypedResponse<infer T, infer S, infer F>\n ? Response & TypedResponse<T, S, F>\n : R extends Response\n ? R\n : R extends undefined\n ? never\n : never\n : never\n\ntype ValidationFunctionOption<T extends ZodSchema, Target extends keyof ValidationTargets> = {\n validationFunction: (\n schema: T,\n value: ValidationTargets[Target]\n ) => ZodSafeParseResult<any, any, T> | Promise<ZodSafeParseResult<any, any, T>>\n}\n\ntype DefaultInput<Target extends keyof ValidationTargets, In, Out> = {\n in: HasUndefined<In> extends true\n ? {\n [K in Target]?: [In] extends [ValidationTargets[K]] ? In : InferInput<In, K>\n }\n : {\n [K in Target]: [In] extends [ValidationTargets[K]] ? In : InferInput<In, K>\n }\n out: { [K in Target]: Out }\n}\n\n// without hook (with or without options)\nfunction zValidatorFunction<\n T extends ZodSchema,\n Target extends keyof ValidationTargets,\n E extends Env,\n P extends string,\n In = zInput<T>,\n Out = zOutput<T>,\n I extends Input = DefaultInput<Target, In, Out>,\n V extends I = I,\n>(\n target: Target,\n schema: T,\n hook?: undefined,\n options?: ValidationFunctionOption<T, Target>\n): MiddlewareHandler<E, P, V, ZodValidatorFailureResponse<T>>\n\n// with hook and options\nfunction zValidatorFunction<\n T extends ZodSchema,\n Target extends keyof ValidationTargets,\n E extends Env,\n P extends string,\n HookFn extends Hook<InferredValue, E, P, Target, {}, T>,\n In = zInput<T>,\n Out = zOutput<T>,\n I extends Input = DefaultInput<Target, In, Out>,\n V extends I = I,\n InferredValue = zInfer<T>,\n>(\n target: Target,\n schema: T,\n hook?: HookFn,\n options?: ValidationFunctionOption<T, Target>\n): MiddlewareHandler<E, P, V, ExtractValidationResponse<HookFn>>\n\n// implementation\nfunction zValidatorFunction<\n T extends ZodSchema,\n Target extends keyof ValidationTargets,\n E extends Env,\n P extends string,\n HookFn extends Hook<InferredValue, E, P, Target, {}, T>,\n In = zInput<T>,\n Out = zOutput<T>,\n I extends Input = DefaultInput<Target, In, Out>,\n V extends I = I,\n InferredValue = zInfer<T>,\n>(\n target: Target,\n schema: T,\n hook?: HookFn,\n options?: ValidationFunctionOption<T, Target>\n):\n | MiddlewareHandler<E, P, V, ZodValidatorFailureResponse<T>>\n | MiddlewareHandler<E, P, V, ExtractValidationResponse<HookFn>> {\n // @ts-expect-error not typed well\n return validator(target, async (value: ValidationTargets[Target], c) => {\n let validatorValue = value\n\n // in case where our `target` === `header`, Hono parses all of the headers into lowercase.\n // this might not match the Zod schema, so we want to make sure that we account for that when parsing the schema.\n if ((target === 'header' && '_def' in schema) || (target === 'header' && '_zod' in schema)) {\n // create an object that maps lowercase schema keys to lowercase\n // @ts-expect-error the schema is a Zod Schema\n const schemaKeys = Object.keys('in' in schema ? schema.in.shape : schema.shape)\n const caseInsensitiveKeymap = Object.fromEntries(\n schemaKeys.map((key) => [key.toLowerCase(), key])\n )\n\n validatorValue = Object.fromEntries(\n Object.entries(value).map(([key, value]) => [caseInsensitiveKeymap[key] || key, value])\n )\n }\n\n const result =\n options && options.validationFunction\n ? await options.validationFunction(schema, validatorValue)\n : // @ts-expect-error z4.$ZodType has safeParseAsync\n await schema.safeParseAsync(validatorValue)\n\n if (hook) {\n const hookResult = await hook({ data: validatorValue, ...result, target }, c)\n if (hookResult) {\n if (hookResult instanceof Response) {\n return hookResult\n }\n\n if ('response' in hookResult) {\n return hookResult.response\n }\n }\n }\n\n if (!result.success) {\n return c.json(result, 400)\n }\n\n return result.data as zInfer<T>\n })\n}\n\nexport const zValidator: typeof zValidatorFunction = zValidatorFunction\n","import {\n ConcurrencyError,\n InvariantError,\n NonRetryableError,\n StreamClosedError,\n ValidationError,\n} from \"@rotorsoft/act\";\n\n/**\n * Uniform error envelope shipped over the wire by every act-http\n * transport. Hosts get the same shape from REST, tRPC, and OpenAPI —\n * a client that talks to two transports doesn't have to invent two\n * error parsers.\n *\n * - `error` — the framework error name (`\"ValidationError\"`,\n * `\"InvariantError\"`, …). Stable identifier, safe to switch on.\n * - `detail` — the framework's message text. Human-readable; not\n * parsed by clients.\n * - `code` — a machine-readable status code from {@link ERROR_MAP}\n * for clients that prefer enum-style branching over name strings.\n */\nexport type ApiError = {\n error: string;\n detail?: string;\n code?: string;\n};\n\n/**\n * Status + code pair for one known framework error.\n */\nexport type ErrorMapEntry = {\n status: number;\n code: string;\n};\n\n/**\n * The single table that maps framework error types to HTTP status\n * codes and machine-readable codes. One table, three consumers\n * (Hono, tRPC, OpenAPI) — cross-transport consistency by\n * construction.\n *\n * Operators wanting different mappings wrap the generated transport\n * rather than mutating this — the consistency is the load-bearing\n * property, not the specific status codes.\n */\nexport const ERROR_MAP = {\n ValidationError: { status: 422, code: \"VALIDATION\" },\n InvariantError: { status: 409, code: \"INVARIANT\" },\n ConcurrencyError: { status: 412, code: \"CONCURRENCY\" },\n StreamClosedError: { status: 410, code: \"STREAM_CLOSED\" },\n NonRetryableError: { status: 400, code: \"NON_RETRYABLE\" },\n} as const satisfies Record<string, ErrorMapEntry>;\n\nconst lookup_known = (\n err: unknown\n): { name: keyof typeof ERROR_MAP } | null => {\n if (err instanceof ValidationError) return { name: \"ValidationError\" };\n if (err instanceof InvariantError) return { name: \"InvariantError\" };\n if (err instanceof ConcurrencyError) return { name: \"ConcurrencyError\" };\n if (err instanceof StreamClosedError) return { name: \"StreamClosedError\" };\n if (err instanceof NonRetryableError) return { name: \"NonRetryableError\" };\n return null;\n};\n\n/**\n * Translate an unknown thrown value into the canonical\n * {@link ApiError} envelope plus HTTP status. Each transport's error\n * boundary calls this once and forwards the result to the wire.\n *\n * Known framework errors map per {@link ERROR_MAP}. Everything else\n * surfaces as a 500 with `code: \"INTERNAL\"`; the `detail` field is\n * populated when the throw was an `Error` instance, omitted\n * otherwise (a thrown string or object doesn't get to leak its\n * payload to the client).\n */\nexport function toApiError(err: unknown): { status: number; body: ApiError } {\n const known = lookup_known(err);\n if (known) {\n const entry = ERROR_MAP[known.name];\n return {\n status: entry.status,\n body: {\n error: known.name,\n detail: (err as Error).message,\n code: entry.code,\n },\n };\n }\n if (err instanceof Error) {\n return {\n status: 500,\n body: { error: \"InternalError\", detail: err.message, code: \"INTERNAL\" },\n };\n }\n return {\n status: 500,\n body: { error: \"InternalError\", code: \"INTERNAL\" },\n };\n}\n","import type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\n\n/**\n * Result of a {@link withIdempotency} call.\n *\n * - `{ deduped: false, result }` — the claim was fresh; the handler\n * ran and produced `result`.\n * - `{ deduped: true }` — the claim was already taken; the handler\n * was *not* invoked. The caller decides how to respond (typically\n * a 2xx with no body, matching the receiver-side convention).\n *\n * Note: the contract does not cache the previous response. A\n * duplicate call returns the deduped marker only — replaying the\n * original handler's output would require a response-caching\n * adapter, which is out of scope here. The receiver-side convention\n * (and the convention the generated transports follow) is \"ack the\n * duplicate; do nothing else.\"\n */\nexport type IdempotencyResult<T> =\n | { deduped: false; result: T }\n | { deduped: true };\n\n/**\n * Wrap an action handler so the framework honors `Idempotency-Key`\n * dedup. Acquires the key via {@link IdempotencyStore.claim}, runs\n * the handler exactly when the claim was fresh, and skips the\n * handler entirely on a duplicate.\n *\n * Reuses the contract `@rotorsoft/act-ops/idempotency` already\n * defines for the receiver-side `Idempotency-Key` story. A single\n * `IdempotencyStore` implementation covers both halves of the \"Act\n * over the wire\" surface — receiver and generated API.\n */\nexport async function withIdempotency<T>(\n store: IdempotencyStore,\n key: string,\n handler: () => Promise<T>\n): Promise<IdempotencyResult<T>> {\n const fresh = await store.claim(key);\n if (!fresh) {\n return { deduped: true };\n }\n return { deduped: false, result: await handler() };\n}\n","import type { BroadcastChannel } from \"../sse/broadcast.js\";\nimport type { BroadcastState } from \"../sse/types.js\";\n\n/**\n * SSE wiring options shared by the auto-generated `trpc` and `hono`\n * transports. When supplied, each transport emits one streaming\n * subscription per registered state name, all reading from the same\n * host-supplied {@link BroadcastChannel}.\n *\n * The host owns publication: every `app.do(...)` call site must\n * forward the resulting snapshots to `channel.publish(...)` for\n * subscribers to see updates. The framework deliberately doesn't\n * auto-publish because the derived state usually needs app-specific\n * overlays (presence, computed fields) that only the host can supply.\n *\n * Defaults are sized for typical business-app dashboards (dozens to\n * hundreds of human viewers per process). Both numeric knobs are\n * validated at transport-function construction; out-of-range values\n * throw `RangeError` immediately so misconfiguration surfaces at\n * startup instead of at first connection.\n */\nexport type SseOptions = {\n /**\n * The {@link BroadcastChannel} the generator's subscriptions hook\n * into. Host owns its lifecycle; the generator does not construct\n * one for you because the channel's `<S>` shape is app-specific and\n * passing it in keeps the type chain typed end-to-end at the host.\n */\n // biome-ignore lint/suspicious/noExplicitAny: the channel's BroadcastState shape is opaque to the generator — narrowing it would force every state into one structural bound that doesn't fit real apps\n readonly channel: BroadcastChannel<any>;\n /**\n * Hard cap on concurrent open subscriptions per generator\n * instance. The 501st simultaneous open returns `503` (Hono) or\n * throws `TOO_MANY_REQUESTS` (tRPC) — never a silent stall.\n *\n * Default `500`. Validated range `[1, 10_000]`. Above 10k the\n * framework refuses to construct: at that point the FD ceiling\n * and per-connection memory force horizontal scaling regardless\n * of tuning, so the right move is sticky LB + per-process cap,\n * not a higher single-process limit.\n */\n readonly maxConnections?: number;\n /**\n * Keep-alive cadence in milliseconds. Most reverse proxies idle\n * connections out after ~60 seconds; the default `30_000` stays\n * comfortably under.\n *\n * Validated range `[15_000, 300_000]`. Under 15 s wastes\n * bandwidth on a business workload; over 5 min risks proxy drops.\n */\n readonly heartbeatMs?: number;\n};\n\n/**\n * Resolved SSE configuration after validation and default expansion.\n * Internal — the resolved values are what every transport actually\n * runs against.\n *\n * @internal\n */\nexport type SseConfig = {\n readonly channel: BroadcastChannel<BroadcastState>;\n readonly maxConnections: number;\n readonly heartbeatMs: number;\n};\n\n/**\n * Default cap on concurrent open subscriptions per generator\n * instance. Sized for typical business-app dashboards (dozens to a\n * few hundred viewers); high enough not to surprise small\n * deployments, low enough not to mask a missing horizontal-scale\n * strategy on big ones.\n */\nexport const DEFAULT_SSE_MAX_CONNECTIONS = 500;\n\n/**\n * Default keep-alive cadence (30 s). Sits comfortably under the\n * 60 s idle timeout most reverse proxies impose.\n */\nexport const DEFAULT_SSE_HEARTBEAT_MS = 30_000;\n\nconst MAX_CONNECTIONS_MIN = 1;\nconst MAX_CONNECTIONS_MAX = 10_000;\nconst HEARTBEAT_MS_MIN = 15_000;\nconst HEARTBEAT_MS_MAX = 300_000;\n\n/**\n * Validate and apply defaults to host-supplied {@link SseOptions}.\n *\n * Throws `RangeError` when either numeric knob is outside its\n * documented range. Pure: same input → same output, no side\n * effects, no I/O. Each transport calls this once at the start of\n * `hono(...)` / `trpc(...)`.\n */\nexport function resolveSseConfig(options: SseOptions): SseConfig {\n const max_connections = options.maxConnections ?? DEFAULT_SSE_MAX_CONNECTIONS;\n const heartbeat_ms = options.heartbeatMs ?? DEFAULT_SSE_HEARTBEAT_MS;\n if (\n !Number.isFinite(max_connections) ||\n max_connections < MAX_CONNECTIONS_MIN ||\n max_connections > MAX_CONNECTIONS_MAX\n ) {\n throw new RangeError(\n `sse.maxConnections must be in [${MAX_CONNECTIONS_MIN}, ${MAX_CONNECTIONS_MAX}], got ${max_connections}`\n );\n }\n if (\n !Number.isFinite(heartbeat_ms) ||\n heartbeat_ms < HEARTBEAT_MS_MIN ||\n heartbeat_ms > HEARTBEAT_MS_MAX\n ) {\n throw new RangeError(\n `sse.heartbeatMs must be in [${HEARTBEAT_MS_MIN}, ${HEARTBEAT_MS_MAX}], got ${heartbeat_ms}`\n );\n }\n return {\n channel: options.channel,\n maxConnections: max_connections,\n heartbeatMs: heartbeat_ms,\n };\n}\n\n/**\n * Per-generator concurrent-open counter. Constructed once inside\n * `hono(...)` / `trpc(...)` and consulted at every subscription\n * open: failure to acquire means the cap has been hit and the\n * transport reports `503` / `TOO_MANY_REQUESTS` to the caller.\n *\n * The counter is intentionally process-local. SSE doesn't multiplex\n * across processes; operators wanting more than `maxConnections`\n * viewers run multiple processes behind a sticky load balancer.\n */\nexport class SseConnectionCounter {\n public readonly limit: number;\n private _open = 0;\n\n constructor(limit: number) {\n this.limit = limit;\n }\n\n /**\n * Reserve one slot. Returns `true` when accepted (caller must\n * eventually {@link release} the slot, even on error paths) or\n * `false` when the cap is already reached.\n */\n acquire(): boolean {\n if (this._open >= this.limit) return false;\n this._open++;\n return true;\n }\n\n /**\n * Free a previously-{@link acquire}d slot. Always runs from a\n * `finally` block in the transport's subscription handler so\n * crashed handlers don't leak count.\n */\n release(): void {\n if (this._open > 0) this._open--;\n }\n\n /** Currently-open count. Test/observability hook. */\n get open(): number {\n return this._open;\n }\n}\n\n/**\n * Wrap a fire-and-forget Promise-returning operation so its\n * rejection is swallowed in place of leaking an unhandled\n * rejection. Used by the Hono heartbeat tick (the interval callback\n * isn't `await`ed, so its rejection has nowhere to go) and by any\n * other \"best-effort write\" the transports run.\n *\n * Pure: same input → same output, no side effects on the caller.\n * Returns the chained Promise so the caller can also `await`\n * completion if they want to (the Hono heartbeat doesn't).\n */\nexport function fireAndForget<T>(op: () => Promise<T>): Promise<T | undefined> {\n return op().catch(() => undefined);\n}\n\n/**\n * Frame yielded by the per-state SSE subscription generator. The\n * initial cached state (when present) arrives as `kind: \"state\"`;\n * every subsequent broadcast publication arrives as `kind: \"patch\"`\n * with the channel's `PatchMessage` payload.\n */\nexport type SseSubscriptionFrame<S> =\n | { readonly kind: \"state\"; readonly data: S }\n | { readonly kind: \"patch\"; readonly data: unknown };\n\n/**\n * Subscription accounting hook. Implementations have a chance to\n * acquire a slot before the loop starts and to release it once the\n * loop's `finally` block runs — used by the tRPC side to enforce\n * `maxConnections` from inside the generator, where it can throw\n * `TRPCError` cleanly. Hono enforces the cap outside the loop (so a\n * full counter returns `503` *before* streamSSE writes headers), then\n * passes `undefined` here.\n */\nexport type SseAccounting = {\n acquire(): boolean;\n release(): void;\n};\n\n/**\n * The shared subscription loop both transports run. Optionally\n * acquires one slot on the supplied {@link SseAccounting}, yields the\n * cached state (when present), then forwards every channel\n * publication as a `kind: \"patch\"` frame until either the consumer\n * breaks (calling `iter.return()`) or the supplied abort signal fires.\n *\n * Exported as the testable seam — the Hono and tRPC sibling\n * generators wrap this so the signal-driven cleanup and counter\n * accounting live in one place.\n */\n// biome-ignore lint/suspicious/noExplicitAny: the channel's BroadcastState shape is opaque to the loop — narrowing it would force every state into one structural bound that doesn't fit real apps\nexport async function* runSseSubscription<S extends { _v: number } = any>(\n channel: BroadcastChannel<S>,\n stream_id: string,\n accounting: SseAccounting | undefined,\n signal: AbortSignal | undefined,\n on_cap_exceeded?: () => never\n): AsyncGenerator<SseSubscriptionFrame<S>> {\n if (accounting && !accounting.acquire() && on_cap_exceeded) {\n on_cap_exceeded();\n }\n const pending: unknown[] = [];\n let resolve_wait: (() => void) | null = null;\n const unsubscribe = channel.subscribe(stream_id, (msg) => {\n pending.push(msg);\n resolve_wait?.();\n });\n const on_abort = () => resolve_wait?.();\n signal?.addEventListener(\"abort\", on_abort);\n try {\n const cached = channel.get_state(stream_id);\n if (cached !== undefined) {\n yield { kind: \"state\", data: cached };\n }\n while (!signal?.aborted) {\n if (pending.length === 0) {\n await new Promise<void>((resolve) => {\n resolve_wait = resolve;\n });\n if (signal?.aborted) break;\n }\n const msg = pending.shift();\n yield { kind: \"patch\", data: msg };\n }\n } finally {\n signal?.removeEventListener(\"abort\", on_abort);\n unsubscribe();\n accounting?.release();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;;ACuHA,SAAS,mBAYP,QACA,QACA,MACA,SAGgE;AAEhE,aAAO,4BAAU,QAAQ,OAAO,OAAkC,MAAM;AACtE,QAAI,iBAAiB;AAIrB,QAAK,WAAW,YAAY,UAAU,UAAY,WAAW,YAAY,UAAU,QAAS;AAG1F,YAAM,aAAa,OAAO,KAAK,QAAQ,SAAS,OAAO,GAAG,QAAQ,OAAO,KAAA;AACzE,YAAM,wBAAwB,OAAO,YACnC,WAAW,IAAA,CAAK,QAAQ,CAAC,IAAI,YAAA,GAAe,GAAA,CAAI,CAAC;AAGnD,uBAAiB,OAAO,YACtB,OAAO,QAAQ,KAAA,EAAO,IAAA,CAAK,CAAC,KAAKA,OAAAA,MAAW,CAAC,sBAAsB,GAAA,KAAQ,KAAKA,OAAAA,CAAM,CAAC;;AAI3F,UAAM,SACJ,WAAW,QAAQ,qBACf,MAAM,QAAQ,mBAAmB,QAAQ,cAAA,IAEzC,MAAM,OAAO,eAAe,cAAA;AAElC,QAAI,MAAM;AACR,YAAM,aAAa,MAAM,KAAK;QAAE,MAAM;QAAgB,GAAG;QAAQ;SAAU,CAAA;AAC3E,UAAI,YAAY;AACd,YAAI,sBAAsB,SACxB,QAAO;AAGT,YAAI,cAAc,WAChB,QAAO,WAAW;;;AAKxB,QAAI,CAAC,OAAO,QACV,QAAO,EAAE,KAAK,QAAQ,GAAA;AAGxB,WAAO,OAAO;;;AAIlB,IAAaC,aAAwC;;;ADrIrD,kBAAqB;AACrB,uBAA0B;;;AEpD1B,iBAMO;AAuCA,IAAM,YAAY;AAAA,EACvB,iBAAiB,EAAE,QAAQ,KAAK,MAAM,aAAa;AAAA,EACnD,gBAAgB,EAAE,QAAQ,KAAK,MAAM,YAAY;AAAA,EACjD,kBAAkB,EAAE,QAAQ,KAAK,MAAM,cAAc;AAAA,EACrD,mBAAmB,EAAE,QAAQ,KAAK,MAAM,gBAAgB;AAAA,EACxD,mBAAmB,EAAE,QAAQ,KAAK,MAAM,gBAAgB;AAC1D;AAEA,IAAM,eAAe,CACnB,QAC4C;AAC5C,MAAI,eAAe,2BAAiB,QAAO,EAAE,MAAM,kBAAkB;AACrE,MAAI,eAAe,0BAAgB,QAAO,EAAE,MAAM,iBAAiB;AACnE,MAAI,eAAe,4BAAkB,QAAO,EAAE,MAAM,mBAAmB;AACvE,MAAI,eAAe,6BAAmB,QAAO,EAAE,MAAM,oBAAoB;AACzE,MAAI,eAAe,6BAAmB,QAAO,EAAE,MAAM,oBAAoB;AACzE,SAAO;AACT;AAaO,SAAS,WAAW,KAAkD;AAC3E,QAAM,QAAQ,aAAa,GAAG;AAC9B,MAAI,OAAO;AACT,UAAM,QAAQ,UAAU,MAAM,IAAI;AAClC,WAAO;AAAA,MACL,QAAQ,MAAM;AAAA,MACd,MAAM;AAAA,QACJ,OAAO,MAAM;AAAA,QACb,QAAS,IAAc;AAAA,QACvB,MAAM,MAAM;AAAA,MACd;AAAA,IACF;AAAA,EACF;AACA,MAAI,eAAe,OAAO;AACxB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,EAAE,OAAO,iBAAiB,QAAQ,IAAI,SAAS,MAAM,WAAW;AAAA,IACxE;AAAA,EACF;AACA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM,EAAE,OAAO,iBAAiB,MAAM,WAAW;AAAA,EACnD;AACF;;;ACjEA,eAAsB,gBACpB,OACA,KACA,SAC+B;AAC/B,QAAM,QAAQ,MAAM,MAAM,MAAM,GAAG;AACnC,MAAI,CAAC,OAAO;AACV,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AACA,SAAO,EAAE,SAAS,OAAO,QAAQ,MAAM,QAAQ,EAAE;AACnD;;;AC8BO,IAAM,8BAA8B;AAMpC,IAAM,2BAA2B;AAExC,IAAM,sBAAsB;AAC5B,IAAM,sBAAsB;AAC5B,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AAUlB,SAAS,iBAAiB,SAAgC;AAC/D,QAAM,kBAAkB,QAAQ,kBAAkB;AAClD,QAAM,eAAe,QAAQ,eAAe;AAC5C,MACE,CAAC,OAAO,SAAS,eAAe,KAChC,kBAAkB,uBAClB,kBAAkB,qBAClB;AACA,UAAM,IAAI;AAAA,MACR,kCAAkC,mBAAmB,KAAK,mBAAmB,UAAU,eAAe;AAAA,IACxG;AAAA,EACF;AACA,MACE,CAAC,OAAO,SAAS,YAAY,KAC7B,eAAe,oBACf,eAAe,kBACf;AACA,UAAM,IAAI;AAAA,MACR,+BAA+B,gBAAgB,KAAK,gBAAgB,UAAU,YAAY;AAAA,IAC5F;AAAA,EACF;AACA,SAAO;AAAA,IACL,SAAS,QAAQ;AAAA,IACjB,gBAAgB;AAAA,IAChB,aAAa;AAAA,EACf;AACF;AAYO,IAAM,uBAAN,MAA2B;AAAA,EAChB;AAAA,EACR,QAAQ;AAAA,EAEhB,YAAY,OAAe;AACzB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAmB;AACjB,QAAI,KAAK,SAAS,KAAK,MAAO,QAAO;AACrC,SAAK;AACL,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAgB;AACd,QAAI,KAAK,QAAQ,EAAG,MAAK;AAAA,EAC3B;AAAA;AAAA,EAGA,IAAI,OAAe;AACjB,WAAO,KAAK;AAAA,EACd;AACF;AAaO,SAAS,cAAiB,IAA8C;AAC7E,SAAO,GAAG,EAAE,MAAM,MAAM,MAAS;AACnC;AAsCA,gBAAuB,mBACrB,SACA,WACA,YACA,QACA,iBACyC;AACzC,MAAI,cAAc,CAAC,WAAW,QAAQ,KAAK,iBAAiB;AAC1D,oBAAgB;AAAA,EAClB;AACA,QAAM,UAAqB,CAAC;AAC5B,MAAI,eAAoC;AACxC,QAAM,cAAc,QAAQ,UAAU,WAAW,CAAC,QAAQ;AACxD,YAAQ,KAAK,GAAG;AAChB,mBAAe;AAAA,EACjB,CAAC;AACD,QAAM,WAAW,MAAM,eAAe;AACtC,UAAQ,iBAAiB,SAAS,QAAQ;AAC1C,MAAI;AACF,UAAM,SAAS,QAAQ,UAAU,SAAS;AAC1C,QAAI,WAAW,QAAW;AACxB,YAAM,EAAE,MAAM,SAAS,MAAM,OAAO;AAAA,IACtC;AACA,WAAO,CAAC,QAAQ,SAAS;AACvB,UAAI,QAAQ,WAAW,GAAG;AACxB,cAAM,IAAI,QAAc,CAAC,YAAY;AACnC,yBAAe;AAAA,QACjB,CAAC;AACD,YAAI,QAAQ,QAAS;AAAA,MACvB;AACA,YAAM,MAAM,QAAQ,MAAM;AAC1B,YAAM,EAAE,MAAM,SAAS,MAAM,IAAI;AAAA,IACnC;AAAA,EACF,UAAE;AACA,YAAQ,oBAAoB,SAAS,QAAQ;AAC7C,gBAAY;AACZ,gBAAY,QAAQ;AAAA,EACtB;AACF;;;AJjGO,SAAS,cACd,WAC0D;AAC1D,SAAO,OAAO,GAAG,SAAS;AACxB,QAAI;AACF,YAAM,QAAQ,MAAM,UAAU,CAAC;AAC/B,QAAE,IAAI,SAAS,KAAK;AACpB,YAAM,KAAK;AAAA,IACb,SAAS,KAAK;AACZ,YAAM,OAAiB;AAAA,QACrB,OAAO;AAAA,QACP,QAAQ,eAAe,QAAQ,IAAI,UAAU;AAAA,QAC7C,MAAM;AAAA,MACR;AACA,aAAO,EAAE,KAAK,MAAM,GAAG;AAAA,IACzB;AAAA,EACF;AACF;AAEA,IAAM,mBAAmB,CAAC,MACxB,EAAE,IAAI,OAAO,iBAAiB;AAuDzB,SAAS,KACd,KACA,SAC6C;AAC7C,QAAM,YAAY,QAAQ,YAAY;AACtC,QAAM,MAAM,IAAI,iBAA4C,EAAE;AAAA,IAC5D;AAAA,EACF;AACA,MAAI,IAAI,KAAK,cAAc,QAAQ,KAAK,CAAC;AAEzC,QAAM,WAAW,QAAQ,aAAa,WAAW;AAOjD,MAAI,QAAQ,KAAK;AACf,UAAM,aAAa,iBAAiB,QAAQ,GAAG;AAC/C,UAAM,cAAc,IAAI,qBAAqB,WAAW,cAAc;AACtE,UAAM,cAAc,oBAAI,IAAY;AACpC,eAAW,UAAU,OAAO,OAAO,IAAI,SAAS,OAAO,GAAG;AACxD,kBAAY,IAAI,OAAO,IAAI;AAAA,IAC7B;AACA,eAAW,cAAc,aAAa;AACpC,UAAI,IAAI,QAAQ,UAAU,IAAI,OAAO,MAAM;AACzC,cAAM,YAAY,EAAE,IAAI,MAAM,QAAQ;AACtC,YAAI,CAAC,WAAW;AACd,gBAAM,OAAiB;AAAA,YACrB,OAAO;AAAA,YACP,QAAQ;AAAA,YACR,MAAM;AAAA,UACR;AACA,iBAAO,EAAE,KAAK,MAAM,GAAG;AAAA,QACzB;AACA,YAAI,CAAC,YAAY,QAAQ,GAAG;AAC1B,YAAE,OAAO,eAAe,GAAG;AAC3B,gBAAM,OAAiB;AAAA,YACrB,OAAO;AAAA,YACP,QAAQ;AAAA,YACR,MAAM;AAAA,UACR;AACA,iBAAO,EAAE,KAAK,MAAM,GAAG;AAAA,QACzB;AAKA,cAAM,aAAa,IAAI,gBAAgB;AACvC,mBAAO,4BAAU,GAAG,OAAO,eAAe;AACxC,qBAAW,QAAQ,MAAM,WAAW,MAAM,CAAC;AAC3C,gBAAM,YAAY,YAAY,MAAM;AAClC;AAAA,cAAc,MACZ,WAAW,SAAS,EAAE,OAAO,QAAQ,MAAM,GAAG,CAAC;AAAA,YACjD;AAAA,UACF,GAAG,WAAW,WAAW;AACzB,cAAI;AACF,6BAAiB,SAAS;AAAA,cACxB,WAAW;AAAA,cACX;AAAA,cACA;AAAA,cACA,WAAW;AAAA,YACb,GAAG;AACD,oBAAM,WAAW,SAAS;AAAA,gBACxB,OAAO,MAAM;AAAA,gBACb,MAAM,KAAK,UAAU,MAAM,IAAI;AAAA,cACjC,CAAC;AAAA,YACH;AAAA,UACF,UAAE;AACA,0BAAc,SAAS;AACvB,wBAAY,QAAQ;AAAA,UACtB;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,EACF;AAEA,aAAW,CAAC,aAAa,KAAK,KAAK,OAAO,QAAQ,IAAI,SAAS,OAAO,GAAG;AACvE,UAAM,SAAS,MAAM,QAAQ,WAAW;AACxC,QAAI;AAAA,MACF,YAAY,WAAW;AAAA,MACvB,WAAW,QAAQ,MAAe;AAAA,MAClC,OAAO,MAAM;AACX,YAAI;AACF,gBAAM,QAAQ,EAAE,IAAI,MAAM,MAAe;AACzC,gBAAM,QAAQ,EAAE,IAAI,OAAO;AAC3B,gBAAM,SAAS,MAAM,QAAQ,OAAO,aAAa,OAAO,CAAC;AACzD,gBAAM,mBAAmB,QAAQ,kBAC7B,MAAM,QAAQ,gBAAgB,aAAa,OAAO,CAAC,IACnD;AACJ,gBAAM,SACJ,qBAAqB,SACjB,EAAE,QAAQ,MAAM,IAChB,EAAE,QAAQ,OAAO,iBAAiB,iBAAiB;AAEzD,cAAI,QAAQ,aAAa;AACvB,kBAAM,MAAM,SAAS,CAAC;AACtB,gBAAI,CAAC,KAAK;AACR,oBAAM,OAAiB;AAAA,gBACrB,OAAO;AAAA,gBACP,QAAQ;AAAA,gBACR,MAAM;AAAA,cACR;AACA,qBAAO,EAAE,KAAK,MAAM,GAAG;AAAA,YACzB;AACA,kBAAM,UAAU,MAAM;AAAA,cACpB,QAAQ,YAAY;AAAA,cACpB;AAAA,cACA,MACE,IAAI,GAAG,aAAsB,QAAiB,KAAc;AAAA,YAChE;AACA,gBAAI,QAAQ,SAAS;AACnB,oBAAM,OAAiB;AAAA,gBACrB,OAAO;AAAA,gBACP,QACE;AAAA,gBACF,MAAM;AAAA,cACR;AACA,qBAAO,EAAE,KAAK,MAAM,GAAG;AAAA,YACzB;AACA,mBAAO,EAAE,KAAK,QAAQ,MAAM;AAAA,UAC9B;AAEA,gBAAM,YAAY,MAAM,IAAI;AAAA,YAC1B;AAAA,YACA;AAAA,YACA;AAAA,UACF;AACA,iBAAO,EAAE,KAAK,SAAS;AAAA,QACzB,SAAS,KAAK;AACZ,gBAAM,EAAE,QAAQ,KAAK,IAAI,WAAW,GAAG;AAIvC,iBAAO,EAAE,KAAK,MAAM,MAA2C;AAAA,QACjE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":["value","zValidator: typeof zValidatorFunction"]}
|
package/dist/hono/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
SseConnectionCounter,
|
|
3
|
+
fireAndForget,
|
|
4
|
+
resolveSseConfig,
|
|
5
|
+
runSseSubscription,
|
|
2
6
|
toApiError,
|
|
3
7
|
withIdempotency
|
|
4
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-QOHCL5GN.js";
|
|
5
9
|
import "../chunk-PZ5AY32C.js";
|
|
6
10
|
|
|
7
11
|
// ../../node_modules/.pnpm/@hono+zod-validator@0.8.0_hono@4.12.25_zod@4.4.3/node_modules/@hono/zod-validator/dist/index.js
|
|
@@ -34,6 +38,7 @@ var zValidator = zValidatorFunction;
|
|
|
34
38
|
|
|
35
39
|
// src/hono/index.ts
|
|
36
40
|
import { Hono } from "hono";
|
|
41
|
+
import { streamSSE } from "hono/streaming";
|
|
37
42
|
function authenticated(extractor) {
|
|
38
43
|
return async (c, next) => {
|
|
39
44
|
try {
|
|
@@ -58,6 +63,61 @@ function hono(app, options) {
|
|
|
58
63
|
);
|
|
59
64
|
api.use("*", authenticated(options.actor));
|
|
60
65
|
const key_from = options.idempotency?.keyFrom ?? default_key_from;
|
|
66
|
+
if (options.sse) {
|
|
67
|
+
const sse_config = resolveSseConfig(options.sse);
|
|
68
|
+
const sse_counter = new SseConnectionCounter(sse_config.maxConnections);
|
|
69
|
+
const state_names = /* @__PURE__ */ new Set();
|
|
70
|
+
for (const action of Object.values(app.registry.actions)) {
|
|
71
|
+
state_names.add(action.name);
|
|
72
|
+
}
|
|
73
|
+
for (const state_name of state_names) {
|
|
74
|
+
api.get(`/sse/${state_name}`, async (c) => {
|
|
75
|
+
const stream_id = c.req.query("stream");
|
|
76
|
+
if (!stream_id) {
|
|
77
|
+
const body = {
|
|
78
|
+
error: "BadRequest",
|
|
79
|
+
detail: "stream query parameter is required",
|
|
80
|
+
code: "BAD_REQUEST"
|
|
81
|
+
};
|
|
82
|
+
return c.json(body, 400);
|
|
83
|
+
}
|
|
84
|
+
if (!sse_counter.acquire()) {
|
|
85
|
+
c.header("Retry-After", "1");
|
|
86
|
+
const body = {
|
|
87
|
+
error: "ServiceUnavailable",
|
|
88
|
+
detail: "max concurrent SSE connections reached",
|
|
89
|
+
code: "SSE_BUSY"
|
|
90
|
+
};
|
|
91
|
+
return c.json(body, 503);
|
|
92
|
+
}
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
return streamSSE(c, async (sse_stream) => {
|
|
95
|
+
sse_stream.onAbort(() => controller.abort());
|
|
96
|
+
const heartbeat = setInterval(() => {
|
|
97
|
+
fireAndForget(
|
|
98
|
+
() => sse_stream.writeSSE({ event: "ping", data: "" })
|
|
99
|
+
);
|
|
100
|
+
}, sse_config.heartbeatMs);
|
|
101
|
+
try {
|
|
102
|
+
for await (const frame of runSseSubscription(
|
|
103
|
+
sse_config.channel,
|
|
104
|
+
stream_id,
|
|
105
|
+
void 0,
|
|
106
|
+
controller.signal
|
|
107
|
+
)) {
|
|
108
|
+
await sse_stream.writeSSE({
|
|
109
|
+
event: frame.kind,
|
|
110
|
+
data: JSON.stringify(frame.data)
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
clearInterval(heartbeat);
|
|
115
|
+
sse_counter.release();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
61
121
|
for (const [action_name, state] of Object.entries(app.registry.actions)) {
|
|
62
122
|
const schema = state.actions[action_name];
|
|
63
123
|
api.post(
|
package/dist/hono/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../node_modules/.pnpm/@hono+zod-validator@0.8.0_hono@4.12.25_zod@4.4.3/node_modules/@hono/zod-validator/src/index.ts","../../src/hono/index.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono'\nimport { validator } from 'hono/validator'\nimport type * as v3 from 'zod/v3'\nimport type { ZodSafeParseResult as v4ZodSafeParseResult } from 'zod/v4'\nimport type * as v4 from 'zod/v4/core'\nimport type { InferInput } from './utils'\n\ntype ZodSchema = v3.ZodType | v4.$ZodType\ntype ZodError<T extends ZodSchema> = T extends v4.$ZodType\n ? v4.$ZodError<v4.output<T>>\n : v3.ZodError\ntype ZodSafeParseResult<T, T2, T3 extends ZodSchema> = T3 extends v4.$ZodType\n ? v4ZodSafeParseResult<T>\n : v3.SafeParseReturnType<T, T2>\ntype zInput<T> = T extends v3.ZodType ? v3.input<T> : T extends v4.$ZodType ? v4.input<T> : never\ntype zOutput<T> = T extends v3.ZodType ? v3.output<T> : T extends v4.$ZodType ? v4.output<T> : never\ntype zInfer<T> = T extends v3.ZodType ? v3.infer<T> : T extends v4.$ZodType ? v4.infer<T> : never\n\n// Failure variant of `safeParseAsync`'s return value.\n// Mirrors the runtime body returned by `c.json(result, 400)` on validation failure.\ntype ZodValidatorFailureBody<T extends ZodSchema> = T extends v4.$ZodType\n ? Extract<v4ZodSafeParseResult<v4.output<T>>, { success: false }>\n : T extends v3.ZodType\n ? Extract<v3.SafeParseReturnType<v3.input<T>, v3.output<T>>, { success: false }>\n : never\n\ntype ZodValidatorFailureResponse<T extends ZodSchema> = Response &\n TypedResponse<ZodValidatorFailureBody<T>, 400, 'json'>\n\nexport type Hook<\n T,\n E extends Env,\n P extends string,\n Target extends keyof ValidationTargets = keyof ValidationTargets,\n O = {},\n Schema extends ZodSchema = any,\n> = (\n result: ({ success: true; data: T } | { success: false; error: ZodError<Schema>; data: T }) & {\n target: Target\n },\n c: Context<E, P>\n) => Response | void | TypedResponse<O> | Promise<Response | void | TypedResponse<O>>\n\ntype HasUndefined<T> = undefined extends T ? true : false\n\ntype ExtractValidationResponse<VF> = VF extends (value: any, c: any) => infer R\n ? R extends Promise<infer PR>\n ? PR extends TypedResponse<infer T, infer S, infer F>\n ? Response & TypedResponse<T, S, F>\n : PR extends Response\n ? PR\n : PR extends undefined\n ? never\n : never\n : R extends TypedResponse<infer T, infer S, infer F>\n ? Response & TypedResponse<T, S, F>\n : R extends Response\n ? R\n : R extends undefined\n ? never\n : never\n : never\n\ntype ValidationFunctionOption<T extends ZodSchema, Target extends keyof ValidationTargets> = {\n validationFunction: (\n schema: T,\n value: ValidationTargets[Target]\n ) => ZodSafeParseResult<any, any, T> | Promise<ZodSafeParseResult<any, any, T>>\n}\n\ntype DefaultInput<Target extends keyof ValidationTargets, In, Out> = {\n in: HasUndefined<In> extends true\n ? {\n [K in Target]?: [In] extends [ValidationTargets[K]] ? In : InferInput<In, K>\n }\n : {\n [K in Target]: [In] extends [ValidationTargets[K]] ? In : InferInput<In, K>\n }\n out: { [K in Target]: Out }\n}\n\n// without hook (with or without options)\nfunction zValidatorFunction<\n T extends ZodSchema,\n Target extends keyof ValidationTargets,\n E extends Env,\n P extends string,\n In = zInput<T>,\n Out = zOutput<T>,\n I extends Input = DefaultInput<Target, In, Out>,\n V extends I = I,\n>(\n target: Target,\n schema: T,\n hook?: undefined,\n options?: ValidationFunctionOption<T, Target>\n): MiddlewareHandler<E, P, V, ZodValidatorFailureResponse<T>>\n\n// with hook and options\nfunction zValidatorFunction<\n T extends ZodSchema,\n Target extends keyof ValidationTargets,\n E extends Env,\n P extends string,\n HookFn extends Hook<InferredValue, E, P, Target, {}, T>,\n In = zInput<T>,\n Out = zOutput<T>,\n I extends Input = DefaultInput<Target, In, Out>,\n V extends I = I,\n InferredValue = zInfer<T>,\n>(\n target: Target,\n schema: T,\n hook?: HookFn,\n options?: ValidationFunctionOption<T, Target>\n): MiddlewareHandler<E, P, V, ExtractValidationResponse<HookFn>>\n\n// implementation\nfunction zValidatorFunction<\n T extends ZodSchema,\n Target extends keyof ValidationTargets,\n E extends Env,\n P extends string,\n HookFn extends Hook<InferredValue, E, P, Target, {}, T>,\n In = zInput<T>,\n Out = zOutput<T>,\n I extends Input = DefaultInput<Target, In, Out>,\n V extends I = I,\n InferredValue = zInfer<T>,\n>(\n target: Target,\n schema: T,\n hook?: HookFn,\n options?: ValidationFunctionOption<T, Target>\n):\n | MiddlewareHandler<E, P, V, ZodValidatorFailureResponse<T>>\n | MiddlewareHandler<E, P, V, ExtractValidationResponse<HookFn>> {\n // @ts-expect-error not typed well\n return validator(target, async (value: ValidationTargets[Target], c) => {\n let validatorValue = value\n\n // in case where our `target` === `header`, Hono parses all of the headers into lowercase.\n // this might not match the Zod schema, so we want to make sure that we account for that when parsing the schema.\n if ((target === 'header' && '_def' in schema) || (target === 'header' && '_zod' in schema)) {\n // create an object that maps lowercase schema keys to lowercase\n // @ts-expect-error the schema is a Zod Schema\n const schemaKeys = Object.keys('in' in schema ? schema.in.shape : schema.shape)\n const caseInsensitiveKeymap = Object.fromEntries(\n schemaKeys.map((key) => [key.toLowerCase(), key])\n )\n\n validatorValue = Object.fromEntries(\n Object.entries(value).map(([key, value]) => [caseInsensitiveKeymap[key] || key, value])\n )\n }\n\n const result =\n options && options.validationFunction\n ? await options.validationFunction(schema, validatorValue)\n : // @ts-expect-error z4.$ZodType has safeParseAsync\n await schema.safeParseAsync(validatorValue)\n\n if (hook) {\n const hookResult = await hook({ data: validatorValue, ...result, target }, c)\n if (hookResult) {\n if (hookResult instanceof Response) {\n return hookResult\n }\n\n if ('response' in hookResult) {\n return hookResult.response\n }\n }\n }\n\n if (!result.success) {\n return c.json(result, 400)\n }\n\n return result.data as zInfer<T>\n })\n}\n\nexport const zValidator: typeof zValidatorFunction = zValidatorFunction\n","/**\n * @packageDocumentation\n * @module act-http/hono\n *\n * Hono subpath for the auto-generated API epic (#835). Walks the\n * registry of a built `Act` once at function call and emits a Hono\n * app with one `POST /actions/<name>` route per registered action,\n * Zod-validated bodies, and the shared error envelope.\n *\n * Usage:\n *\n * ```ts\n * import { hono } from \"@rotorsoft/act-http/hono\";\n *\n * const api = hono(app, {\n * actor: (c) => ({ id: c.req.header(\"x-user-id\")!, name: \"...\" }),\n * stream: (action, input, c) => `tenant-${c.req.header(\"x-tenant\")}`,\n * });\n *\n * // Mounts at /api by default — pass `basePath` to override.\n * // POST /api/actions/OpenTicket body: { title } → 200 Snapshot[] | 4xx ApiError\n * ```\n *\n * The route shape matches the flat tRPC sibling at\n * `@rotorsoft/act-http/trpc`: one endpoint per action, no\n * state-name tier. Action names are unique across the registry —\n * the framework enforces no duplicates — so an extra namespace\n * level would be overhead. Queries live in a follow-up; this\n * slice is mutations only.\n *\n * **Actor lives on the context.** The generator wires an internal\n * middleware that runs the host's\n * {@link HonoOptions.actor | actor extractor} once per call and\n * stashes the resolved {@link Actor} under `c.get(\"actor\")`. Every\n * generated route reads from there — auth resolves at one seam,\n * downstream middleware (logging, tracing) sees the same actor.\n * The middleware factory is also exported as {@link authenticated}\n * for hosts that want to compose it into their own Hono chain\n * alongside hand-written routes.\n *\n * Composes the shared utilities at `@rotorsoft/act-http/api`:\n * {@link ActorExtractor} for the auth seam, {@link toApiError} for\n * the uniform error → status / code mapping, {@link withIdempotency}\n * for `Idempotency-Key` dedup. Three transports (tRPC, this one,\n * OpenAPI) ride the same utilities so a client speaking two of\n * them never sees two shapes for the same framework error.\n */\nimport { zValidator } from \"@hono/zod-validator\";\nimport type { Actor, Target } from \"@rotorsoft/act\";\nimport type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { Hono } from \"hono\";\nimport {\n type ActorExtractor,\n type ApiError,\n toApiError,\n withIdempotency,\n} from \"../api/index.js\";\n\n/**\n * Hono context variables this subpath contributes. The generic on\n * the returned middleware threads it through so routes downstream\n * of {@link authenticated} see `c.get(\"actor\")` typed without a\n * manual cast.\n */\nexport type ActMiddlewareVariables = {\n actor: Actor;\n};\n\n/**\n * Per-call options for {@link hono}. The host supplies the four\n * seams the package can't make decisions on:\n *\n * - `actor` — auth resolver. Receives the Hono context, returns the\n * `Actor` that flows onto `c.get(\"actor\")` for every generated\n * route (via an internal {@link authenticated}).\n * - `stream` — stream-id resolver. Receives the action name, the\n * validated body, and the Hono context; returns the target\n * stream. Singleton aggregates return a constant; per-tenant\n * aggregates pull from headers, path, or body.\n * - `expectedVersion` (optional) — optimistic-concurrency resolver.\n * Hosts typically read it from an `If-Match` header on the\n * request, or from the client's last-known snapshot. Returning\n * `undefined` skips the check for that call — handy when only\n * some actions are concurrency-sensitive.\n * - `idempotency` (optional) — when set, the route honors\n * `Idempotency-Key` via {@link withIdempotency}. The host\n * supplies the `IdempotencyStore`; `keyFrom` defaults to reading\n * the `Idempotency-Key` header. On a duplicate claim, the route\n * responds `409 Conflict` — the contract intentionally doesn't\n * cache the original handler's result, matching the\n * receiver-side \"ack the duplicate\" semantics.\n * - `basePath` (optional, default `\"/api\"`) — Hono basePath the\n * routes mount under.\n */\nexport type HonoOptions = {\n readonly actor: ActorExtractor;\n readonly stream: (\n action_name: string,\n input: unknown,\n c: Context\n ) => string | Promise<string>;\n readonly expectedVersion?: (\n action_name: string,\n input: unknown,\n c: Context\n ) => number | undefined | Promise<number | undefined>;\n readonly idempotency?: {\n readonly store: IdempotencyStore;\n readonly keyFrom?: (c: Context) => string | undefined;\n };\n readonly basePath?: string;\n};\n\n/**\n * Build a Hono middleware that runs an {@link ActorExtractor} once\n * per call and stashes the resolved {@link Actor} under\n * `c.set(\"actor\", ...)` so downstream routes can read it via\n * `c.get(\"actor\")`. The generator at {@link hono} uses this\n * internally; it's also exported so hosts that want to compose\n * their own route chain (logging, tracing, custom auth flavors)\n * can wire it directly:\n *\n * ```ts\n * import { Hono } from \"hono\";\n * import { authenticated, type ActMiddlewareVariables } from \"@rotorsoft/act-http/hono\";\n *\n * const api = new Hono<{ Variables: ActMiddlewareVariables }>();\n * api.use(\"*\", authenticated((c) => resolveUserFromJwt(c)));\n * api.get(\"/me\", (c) => c.json(c.get(\"actor\"))); // typed\n * ```\n *\n * Errors thrown by the extractor surface as `401 Unauthorized` with\n * the shared {@link ApiError} envelope.\n */\nexport function authenticated(\n extractor: ActorExtractor\n): MiddlewareHandler<{ Variables: ActMiddlewareVariables }> {\n return async (c, next) => {\n try {\n const actor = await extractor(c);\n c.set(\"actor\", actor);\n await next();\n } catch (err) {\n const body: ApiError = {\n error: \"Unauthorized\",\n detail: err instanceof Error ? err.message : undefined,\n code: \"UNAUTHORIZED\",\n };\n return c.json(body, 401);\n }\n };\n}\n\nconst default_key_from = (c: Context): string | undefined =>\n c.req.header(\"idempotency-key\");\n\n/**\n * Build a typed Hono REST surface from a built `Act` instance.\n *\n * Walks `app.registry.actions` once and emits one\n * `POST /actions/<actionName>` per action under the configured\n * `basePath` (default `/api`). Each route:\n *\n * 1. Runs the internal {@link authenticated} middleware —\n * `options.actor` resolves the {@link Actor} once and stashes\n * it under `c.get(\"actor\")`.\n * 2. Validates the JSON body against the action's Zod schema via\n * `@hono/zod-validator`. Failures short-circuit with `422`.\n * 3. Resolves the target stream via\n * `options.stream(name, input, c)`.\n * 4. (Optionally) resolves `expectedVersion` for optimistic\n * concurrency.\n * 5. (Optionally) claims the `Idempotency-Key` via\n * {@link withIdempotency} — responds `409` on duplicate.\n * 6. Calls\n * `app.do(name, { stream, actor, expectedVersion? }, input)`.\n * 7. Maps any thrown framework error onto the shared\n * {@link ApiError} envelope via {@link toApiError}, returning\n * the conventional HTTP status (`409` / `412` for concurrency,\n * `422` for validation, `400` for non-retryable, etc.).\n *\n * @param app A built `Act` orchestrator.\n * @param options Auth, stream, expected-version, idempotency, and\n * base-path seams.\n * @returns A Hono app covering every registered action under\n * `<basePath>/actions/<name>`.\n */\n/**\n * Structural shape of the Act surface this generator walks at\n * runtime — the registry's action-name → owning-state map plus the\n * `do(...)` dispatch. Letting TApp infer to the caller's concrete\n * `Act<...>` against this structural bound keeps nested variance\n * out of the signature and avoids `any` in either direction.\n *\n * @internal\n */\ntype ActSurface = {\n readonly registry: {\n actions: Record<\n string,\n {\n readonly name: string;\n readonly actions: Record<string, unknown>;\n }\n >;\n };\n do(action: string, target: Target, payload: unknown): Promise<unknown>;\n};\n\nexport function hono<TApp extends ActSurface = ActSurface>(\n app: TApp,\n options: HonoOptions\n): Hono<{ Variables: ActMiddlewareVariables }> {\n const base_path = options.basePath ?? \"/api\";\n const api = new Hono<{ Variables: ActMiddlewareVariables }>().basePath(\n base_path\n );\n api.use(\"*\", authenticated(options.actor));\n\n const key_from = options.idempotency?.keyFrom ?? default_key_from;\n\n for (const [action_name, state] of Object.entries(app.registry.actions)) {\n const schema = state.actions[action_name];\n api.post(\n `/actions/${action_name}`,\n zValidator(\"json\", schema as never),\n async (c) => {\n try {\n const input = c.req.valid(\"json\" as never) as unknown;\n const actor = c.get(\"actor\");\n const stream = await options.stream(action_name, input, c);\n const expected_version = options.expectedVersion\n ? await options.expectedVersion(action_name, input, c)\n : undefined;\n const target =\n expected_version === undefined\n ? { stream, actor }\n : { stream, actor, expectedVersion: expected_version };\n\n if (options.idempotency) {\n const key = key_from(c);\n if (!key) {\n const body: ApiError = {\n error: \"BadRequest\",\n detail: \"Idempotency-Key required\",\n code: \"BAD_REQUEST\",\n };\n return c.json(body, 400);\n }\n const outcome = await withIdempotency(\n options.idempotency.store,\n key,\n () =>\n app.do(action_name as never, target as never, input as never)\n );\n if (outcome.deduped) {\n const body: ApiError = {\n error: \"Conflict\",\n detail:\n \"Idempotency-Key already used; original result not cached\",\n code: \"CONFLICT\",\n };\n return c.json(body, 409);\n }\n return c.json(outcome.result);\n }\n\n const snapshots = await app.do(\n action_name as never,\n target as never,\n input as never\n );\n return c.json(snapshots);\n } catch (err) {\n const { status, body } = toApiError(err);\n // Hono types narrow `status` to its known status-number union;\n // `toApiError` returns a numeric status from a closed set that\n // matches but TypeScript can't prove it through the union.\n return c.json(body, status as 400 | 409 | 410 | 412 | 422 | 500);\n }\n }\n );\n }\n\n return api;\n}\n"],"mappings":"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","names":["value","zValidator: typeof zValidatorFunction"]}
|
|
1
|
+
{"version":3,"sources":["../../../../node_modules/.pnpm/@hono+zod-validator@0.8.0_hono@4.12.25_zod@4.4.3/node_modules/@hono/zod-validator/src/index.ts","../../src/hono/index.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono'\nimport { validator } from 'hono/validator'\nimport type * as v3 from 'zod/v3'\nimport type { ZodSafeParseResult as v4ZodSafeParseResult } from 'zod/v4'\nimport type * as v4 from 'zod/v4/core'\nimport type { InferInput } from './utils'\n\ntype ZodSchema = v3.ZodType | v4.$ZodType\ntype ZodError<T extends ZodSchema> = T extends v4.$ZodType\n ? v4.$ZodError<v4.output<T>>\n : v3.ZodError\ntype ZodSafeParseResult<T, T2, T3 extends ZodSchema> = T3 extends v4.$ZodType\n ? v4ZodSafeParseResult<T>\n : v3.SafeParseReturnType<T, T2>\ntype zInput<T> = T extends v3.ZodType ? v3.input<T> : T extends v4.$ZodType ? v4.input<T> : never\ntype zOutput<T> = T extends v3.ZodType ? v3.output<T> : T extends v4.$ZodType ? v4.output<T> : never\ntype zInfer<T> = T extends v3.ZodType ? v3.infer<T> : T extends v4.$ZodType ? v4.infer<T> : never\n\n// Failure variant of `safeParseAsync`'s return value.\n// Mirrors the runtime body returned by `c.json(result, 400)` on validation failure.\ntype ZodValidatorFailureBody<T extends ZodSchema> = T extends v4.$ZodType\n ? Extract<v4ZodSafeParseResult<v4.output<T>>, { success: false }>\n : T extends v3.ZodType\n ? Extract<v3.SafeParseReturnType<v3.input<T>, v3.output<T>>, { success: false }>\n : never\n\ntype ZodValidatorFailureResponse<T extends ZodSchema> = Response &\n TypedResponse<ZodValidatorFailureBody<T>, 400, 'json'>\n\nexport type Hook<\n T,\n E extends Env,\n P extends string,\n Target extends keyof ValidationTargets = keyof ValidationTargets,\n O = {},\n Schema extends ZodSchema = any,\n> = (\n result: ({ success: true; data: T } | { success: false; error: ZodError<Schema>; data: T }) & {\n target: Target\n },\n c: Context<E, P>\n) => Response | void | TypedResponse<O> | Promise<Response | void | TypedResponse<O>>\n\ntype HasUndefined<T> = undefined extends T ? true : false\n\ntype ExtractValidationResponse<VF> = VF extends (value: any, c: any) => infer R\n ? R extends Promise<infer PR>\n ? PR extends TypedResponse<infer T, infer S, infer F>\n ? Response & TypedResponse<T, S, F>\n : PR extends Response\n ? PR\n : PR extends undefined\n ? never\n : never\n : R extends TypedResponse<infer T, infer S, infer F>\n ? Response & TypedResponse<T, S, F>\n : R extends Response\n ? R\n : R extends undefined\n ? never\n : never\n : never\n\ntype ValidationFunctionOption<T extends ZodSchema, Target extends keyof ValidationTargets> = {\n validationFunction: (\n schema: T,\n value: ValidationTargets[Target]\n ) => ZodSafeParseResult<any, any, T> | Promise<ZodSafeParseResult<any, any, T>>\n}\n\ntype DefaultInput<Target extends keyof ValidationTargets, In, Out> = {\n in: HasUndefined<In> extends true\n ? {\n [K in Target]?: [In] extends [ValidationTargets[K]] ? In : InferInput<In, K>\n }\n : {\n [K in Target]: [In] extends [ValidationTargets[K]] ? In : InferInput<In, K>\n }\n out: { [K in Target]: Out }\n}\n\n// without hook (with or without options)\nfunction zValidatorFunction<\n T extends ZodSchema,\n Target extends keyof ValidationTargets,\n E extends Env,\n P extends string,\n In = zInput<T>,\n Out = zOutput<T>,\n I extends Input = DefaultInput<Target, In, Out>,\n V extends I = I,\n>(\n target: Target,\n schema: T,\n hook?: undefined,\n options?: ValidationFunctionOption<T, Target>\n): MiddlewareHandler<E, P, V, ZodValidatorFailureResponse<T>>\n\n// with hook and options\nfunction zValidatorFunction<\n T extends ZodSchema,\n Target extends keyof ValidationTargets,\n E extends Env,\n P extends string,\n HookFn extends Hook<InferredValue, E, P, Target, {}, T>,\n In = zInput<T>,\n Out = zOutput<T>,\n I extends Input = DefaultInput<Target, In, Out>,\n V extends I = I,\n InferredValue = zInfer<T>,\n>(\n target: Target,\n schema: T,\n hook?: HookFn,\n options?: ValidationFunctionOption<T, Target>\n): MiddlewareHandler<E, P, V, ExtractValidationResponse<HookFn>>\n\n// implementation\nfunction zValidatorFunction<\n T extends ZodSchema,\n Target extends keyof ValidationTargets,\n E extends Env,\n P extends string,\n HookFn extends Hook<InferredValue, E, P, Target, {}, T>,\n In = zInput<T>,\n Out = zOutput<T>,\n I extends Input = DefaultInput<Target, In, Out>,\n V extends I = I,\n InferredValue = zInfer<T>,\n>(\n target: Target,\n schema: T,\n hook?: HookFn,\n options?: ValidationFunctionOption<T, Target>\n):\n | MiddlewareHandler<E, P, V, ZodValidatorFailureResponse<T>>\n | MiddlewareHandler<E, P, V, ExtractValidationResponse<HookFn>> {\n // @ts-expect-error not typed well\n return validator(target, async (value: ValidationTargets[Target], c) => {\n let validatorValue = value\n\n // in case where our `target` === `header`, Hono parses all of the headers into lowercase.\n // this might not match the Zod schema, so we want to make sure that we account for that when parsing the schema.\n if ((target === 'header' && '_def' in schema) || (target === 'header' && '_zod' in schema)) {\n // create an object that maps lowercase schema keys to lowercase\n // @ts-expect-error the schema is a Zod Schema\n const schemaKeys = Object.keys('in' in schema ? schema.in.shape : schema.shape)\n const caseInsensitiveKeymap = Object.fromEntries(\n schemaKeys.map((key) => [key.toLowerCase(), key])\n )\n\n validatorValue = Object.fromEntries(\n Object.entries(value).map(([key, value]) => [caseInsensitiveKeymap[key] || key, value])\n )\n }\n\n const result =\n options && options.validationFunction\n ? await options.validationFunction(schema, validatorValue)\n : // @ts-expect-error z4.$ZodType has safeParseAsync\n await schema.safeParseAsync(validatorValue)\n\n if (hook) {\n const hookResult = await hook({ data: validatorValue, ...result, target }, c)\n if (hookResult) {\n if (hookResult instanceof Response) {\n return hookResult\n }\n\n if ('response' in hookResult) {\n return hookResult.response\n }\n }\n }\n\n if (!result.success) {\n return c.json(result, 400)\n }\n\n return result.data as zInfer<T>\n })\n}\n\nexport const zValidator: typeof zValidatorFunction = zValidatorFunction\n","/**\n * @packageDocumentation\n * @module act-http/hono\n *\n * Hono subpath for the auto-generated API epic (#835). Walks the\n * registry of a built `Act` once at function call and emits a Hono\n * app with one `POST /actions/<name>` route per registered action,\n * Zod-validated bodies, and the shared error envelope.\n *\n * Usage:\n *\n * ```ts\n * import { hono } from \"@rotorsoft/act-http/hono\";\n *\n * const api = hono(app, {\n * actor: (c) => ({ id: c.req.header(\"x-user-id\")!, name: \"...\" }),\n * stream: (action, input, c) => `tenant-${c.req.header(\"x-tenant\")}`,\n * });\n *\n * // Mounts at /api by default — pass `basePath` to override.\n * // POST /api/actions/OpenTicket body: { title } → 200 Snapshot[] | 4xx ApiError\n * ```\n *\n * The route shape matches the flat tRPC sibling at\n * `@rotorsoft/act-http/trpc`: one endpoint per action, no\n * state-name tier. Action names are unique across the registry —\n * the framework enforces no duplicates — so an extra namespace\n * level would be overhead. Queries live in a follow-up; this\n * slice is mutations only.\n *\n * **Actor lives on the context.** The generator wires an internal\n * middleware that runs the host's\n * {@link HonoOptions.actor | actor extractor} once per call and\n * stashes the resolved {@link Actor} under `c.get(\"actor\")`. Every\n * generated route reads from there — auth resolves at one seam,\n * downstream middleware (logging, tracing) sees the same actor.\n * The middleware factory is also exported as {@link authenticated}\n * for hosts that want to compose it into their own Hono chain\n * alongside hand-written routes.\n *\n * Composes the shared utilities at `@rotorsoft/act-http/api`:\n * {@link ActorExtractor} for the auth seam, {@link toApiError} for\n * the uniform error → status / code mapping, {@link withIdempotency}\n * for `Idempotency-Key` dedup. Three transports (tRPC, this one,\n * OpenAPI) ride the same utilities so a client speaking two of\n * them never sees two shapes for the same framework error.\n */\nimport { zValidator } from \"@hono/zod-validator\";\nimport type { Actor, Target } from \"@rotorsoft/act\";\nimport type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { Hono } from \"hono\";\nimport { streamSSE } from \"hono/streaming\";\nimport {\n type ActorExtractor,\n type ApiError,\n fireAndForget,\n resolveSseConfig,\n runSseSubscription,\n SseConnectionCounter,\n type SseOptions,\n toApiError,\n withIdempotency,\n} from \"../api/index.js\";\n\n/**\n * Hono context variables this subpath contributes. The generic on\n * the returned middleware threads it through so routes downstream\n * of {@link authenticated} see `c.get(\"actor\")` typed without a\n * manual cast.\n */\nexport type ActMiddlewareVariables = {\n actor: Actor;\n};\n\n/**\n * Per-call options for {@link hono}. The host supplies the four\n * seams the package can't make decisions on:\n *\n * - `actor` — auth resolver. Receives the Hono context, returns the\n * `Actor` that flows onto `c.get(\"actor\")` for every generated\n * route (via an internal {@link authenticated}).\n * - `stream` — stream-id resolver. Receives the action name, the\n * validated body, and the Hono context; returns the target\n * stream. Singleton aggregates return a constant; per-tenant\n * aggregates pull from headers, path, or body.\n * - `expectedVersion` (optional) — optimistic-concurrency resolver.\n * Hosts typically read it from an `If-Match` header on the\n * request, or from the client's last-known snapshot. Returning\n * `undefined` skips the check for that call — handy when only\n * some actions are concurrency-sensitive.\n * - `idempotency` (optional) — when set, the route honors\n * `Idempotency-Key` via {@link withIdempotency}. The host\n * supplies the `IdempotencyStore`; `keyFrom` defaults to reading\n * the `Idempotency-Key` header. On a duplicate claim, the route\n * responds `409 Conflict` — the contract intentionally doesn't\n * cache the original handler's result, matching the\n * receiver-side \"ack the duplicate\" semantics.\n * - `basePath` (optional, default `\"/api\"`) — Hono basePath the\n * routes mount under.\n */\nexport type HonoOptions = {\n readonly actor: ActorExtractor;\n readonly stream: (\n action_name: string,\n input: unknown,\n c: Context\n ) => string | Promise<string>;\n readonly expectedVersion?: (\n action_name: string,\n input: unknown,\n c: Context\n ) => number | undefined | Promise<number | undefined>;\n readonly idempotency?: {\n readonly store: IdempotencyStore;\n readonly keyFrom?: (c: Context) => string | undefined;\n };\n /**\n * Optional SSE wiring. When set, the generator emits one\n * `GET <basePath>/sse/<stateName>?stream=<streamId>` per unique\n * state name in the registry. The endpoint runs the host\n * {@link actor} extractor, looks up the streamId from\n * `?stream=...`, opens a `text/event-stream`, yields the cached\n * state (if any), and forwards every patch published to the\n * shared {@link SseOptions.channel}. A heartbeat keeps proxies\n * from idling the connection out; the per-process connection cap\n * returns `503 Service Unavailable` (with `Retry-After: 1`) when\n * full so operators never see silent stalls.\n *\n * Off by default — most APIs don't expose live state to every\n * client, and opening one is widening both the auth and cost\n * surface.\n */\n readonly sse?: SseOptions;\n readonly basePath?: string;\n};\n\n/**\n * Build a Hono middleware that runs an {@link ActorExtractor} once\n * per call and stashes the resolved {@link Actor} under\n * `c.set(\"actor\", ...)` so downstream routes can read it via\n * `c.get(\"actor\")`. The generator at {@link hono} uses this\n * internally; it's also exported so hosts that want to compose\n * their own route chain (logging, tracing, custom auth flavors)\n * can wire it directly:\n *\n * ```ts\n * import { Hono } from \"hono\";\n * import { authenticated, type ActMiddlewareVariables } from \"@rotorsoft/act-http/hono\";\n *\n * const api = new Hono<{ Variables: ActMiddlewareVariables }>();\n * api.use(\"*\", authenticated((c) => resolveUserFromJwt(c)));\n * api.get(\"/me\", (c) => c.json(c.get(\"actor\"))); // typed\n * ```\n *\n * Errors thrown by the extractor surface as `401 Unauthorized` with\n * the shared {@link ApiError} envelope.\n */\nexport function authenticated(\n extractor: ActorExtractor\n): MiddlewareHandler<{ Variables: ActMiddlewareVariables }> {\n return async (c, next) => {\n try {\n const actor = await extractor(c);\n c.set(\"actor\", actor);\n await next();\n } catch (err) {\n const body: ApiError = {\n error: \"Unauthorized\",\n detail: err instanceof Error ? err.message : undefined,\n code: \"UNAUTHORIZED\",\n };\n return c.json(body, 401);\n }\n };\n}\n\nconst default_key_from = (c: Context): string | undefined =>\n c.req.header(\"idempotency-key\");\n\n/**\n * Build a typed Hono REST surface from a built `Act` instance.\n *\n * Walks `app.registry.actions` once and emits one\n * `POST /actions/<actionName>` per action under the configured\n * `basePath` (default `/api`). Each route:\n *\n * 1. Runs the internal {@link authenticated} middleware —\n * `options.actor` resolves the {@link Actor} once and stashes\n * it under `c.get(\"actor\")`.\n * 2. Validates the JSON body against the action's Zod schema via\n * `@hono/zod-validator`. Failures short-circuit with `422`.\n * 3. Resolves the target stream via\n * `options.stream(name, input, c)`.\n * 4. (Optionally) resolves `expectedVersion` for optimistic\n * concurrency.\n * 5. (Optionally) claims the `Idempotency-Key` via\n * {@link withIdempotency} — responds `409` on duplicate.\n * 6. Calls\n * `app.do(name, { stream, actor, expectedVersion? }, input)`.\n * 7. Maps any thrown framework error onto the shared\n * {@link ApiError} envelope via {@link toApiError}, returning\n * the conventional HTTP status (`409` / `412` for concurrency,\n * `422` for validation, `400` for non-retryable, etc.).\n *\n * @param app A built `Act` orchestrator.\n * @param options Auth, stream, expected-version, idempotency, and\n * base-path seams.\n * @returns A Hono app covering every registered action under\n * `<basePath>/actions/<name>`.\n */\n/**\n * Structural shape of the Act surface this generator walks at\n * runtime — the registry's action-name → owning-state map plus the\n * `do(...)` dispatch. Letting TApp infer to the caller's concrete\n * `Act<...>` against this structural bound keeps nested variance\n * out of the signature and avoids `any` in either direction.\n *\n * @internal\n */\ntype ActSurface = {\n readonly registry: {\n actions: Record<\n string,\n {\n readonly name: string;\n readonly actions: Record<string, unknown>;\n }\n >;\n };\n do(action: string, target: Target, payload: unknown): Promise<unknown>;\n};\n\nexport function hono<TApp extends ActSurface = ActSurface>(\n app: TApp,\n options: HonoOptions\n): Hono<{ Variables: ActMiddlewareVariables }> {\n const base_path = options.basePath ?? \"/api\";\n const api = new Hono<{ Variables: ActMiddlewareVariables }>().basePath(\n base_path\n );\n api.use(\"*\", authenticated(options.actor));\n\n const key_from = options.idempotency?.keyFrom ?? default_key_from;\n\n // Wire SSE subscriptions before the mutation routes so the\n // `/sse/*` paths sit alongside `/actions/*`. The cap counter is\n // shared by every state-name's GET route on this generator\n // instance so a noisy room on one state doesn't get a free pass\n // on another.\n if (options.sse) {\n const sse_config = resolveSseConfig(options.sse);\n const sse_counter = new SseConnectionCounter(sse_config.maxConnections);\n const state_names = new Set<string>();\n for (const action of Object.values(app.registry.actions)) {\n state_names.add(action.name);\n }\n for (const state_name of state_names) {\n api.get(`/sse/${state_name}`, async (c) => {\n const stream_id = c.req.query(\"stream\");\n if (!stream_id) {\n const body: ApiError = {\n error: \"BadRequest\",\n detail: \"stream query parameter is required\",\n code: \"BAD_REQUEST\",\n };\n return c.json(body, 400);\n }\n if (!sse_counter.acquire()) {\n c.header(\"Retry-After\", \"1\");\n const body: ApiError = {\n error: \"ServiceUnavailable\",\n detail: \"max concurrent SSE connections reached\",\n code: \"SSE_BUSY\",\n };\n return c.json(body, 503);\n }\n // The route already acquired the slot above so streamSSE\n // could return `503` before writing headers if the cap was\n // full. The shared loop runs without accounting; the route\n // releases the slot in `finally`.\n const controller = new AbortController();\n return streamSSE(c, async (sse_stream) => {\n sse_stream.onAbort(() => controller.abort());\n const heartbeat = setInterval(() => {\n fireAndForget(() =>\n sse_stream.writeSSE({ event: \"ping\", data: \"\" })\n );\n }, sse_config.heartbeatMs);\n try {\n for await (const frame of runSseSubscription(\n sse_config.channel,\n stream_id,\n undefined,\n controller.signal\n )) {\n await sse_stream.writeSSE({\n event: frame.kind,\n data: JSON.stringify(frame.data),\n });\n }\n } finally {\n clearInterval(heartbeat);\n sse_counter.release();\n }\n });\n });\n }\n }\n\n for (const [action_name, state] of Object.entries(app.registry.actions)) {\n const schema = state.actions[action_name];\n api.post(\n `/actions/${action_name}`,\n zValidator(\"json\", schema as never),\n async (c) => {\n try {\n const input = c.req.valid(\"json\" as never) as unknown;\n const actor = c.get(\"actor\");\n const stream = await options.stream(action_name, input, c);\n const expected_version = options.expectedVersion\n ? await options.expectedVersion(action_name, input, c)\n : undefined;\n const target =\n expected_version === undefined\n ? { stream, actor }\n : { stream, actor, expectedVersion: expected_version };\n\n if (options.idempotency) {\n const key = key_from(c);\n if (!key) {\n const body: ApiError = {\n error: \"BadRequest\",\n detail: \"Idempotency-Key required\",\n code: \"BAD_REQUEST\",\n };\n return c.json(body, 400);\n }\n const outcome = await withIdempotency(\n options.idempotency.store,\n key,\n () =>\n app.do(action_name as never, target as never, input as never)\n );\n if (outcome.deduped) {\n const body: ApiError = {\n error: \"Conflict\",\n detail:\n \"Idempotency-Key already used; original result not cached\",\n code: \"CONFLICT\",\n };\n return c.json(body, 409);\n }\n return c.json(outcome.result);\n }\n\n const snapshots = await app.do(\n action_name as never,\n target as never,\n input as never\n );\n return c.json(snapshots);\n } catch (err) {\n const { status, body } = toApiError(err);\n // Hono types narrow `status` to its known status-number union;\n // `toApiError` returns a numeric status from a closed set that\n // matches but TypeScript can't prove it through the union.\n return c.json(body, status as 400 | 409 | 410 | 412 | 422 | 500);\n }\n }\n );\n }\n\n return api;\n}\n"],"mappings":"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","names":["value","zValidator: typeof zValidatorFunction"]}
|