@rotorsoft/act-http 1.2.1 → 1.4.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.
@@ -0,0 +1,109 @@
1
+ import type { Act, Actor, Schema, Schemas } from "@rotorsoft/act";
2
+ import type { IdempotencyStore } from "@rotorsoft/act-ops/idempotency";
3
+ import type { Context, MiddlewareHandler } from "hono";
4
+ import { Hono } from "hono";
5
+ import { type ActorExtractor } from "../api/index.js";
6
+ /**
7
+ * Hono context variables this subpath contributes. The generic on
8
+ * the returned middleware threads it through so routes downstream
9
+ * of {@link authenticated} see `c.get("actor")` typed without a
10
+ * manual cast.
11
+ */
12
+ export type ActMiddlewareVariables = {
13
+ actor: Actor;
14
+ };
15
+ /**
16
+ * Per-call options for {@link hono}. The host supplies the four
17
+ * seams the package can't make decisions on:
18
+ *
19
+ * - `actor` — auth resolver. Receives the Hono context, returns the
20
+ * `Actor` that flows onto `c.get("actor")` for every generated
21
+ * route (via an internal {@link authenticated}).
22
+ * - `stream` — stream-id resolver. Receives the action name, the
23
+ * validated body, and the Hono context; returns the target
24
+ * stream. Singleton aggregates return a constant; per-tenant
25
+ * aggregates pull from headers, path, or body.
26
+ * - `expectedVersion` (optional) — optimistic-concurrency resolver.
27
+ * Hosts typically read it from an `If-Match` header on the
28
+ * request, or from the client's last-known snapshot. Returning
29
+ * `undefined` skips the check for that call — handy when only
30
+ * some actions are concurrency-sensitive.
31
+ * - `idempotency` (optional) — when set, the route honors
32
+ * `Idempotency-Key` via {@link withIdempotency}. The host
33
+ * supplies the `IdempotencyStore`; `keyFrom` defaults to reading
34
+ * the `Idempotency-Key` header. On a duplicate claim, the route
35
+ * responds `409 Conflict` — the contract intentionally doesn't
36
+ * cache the original handler's result, matching the
37
+ * receiver-side "ack the duplicate" semantics.
38
+ * - `basePath` (optional, default `"/api"`) — Hono basePath the
39
+ * routes mount under.
40
+ */
41
+ export type HonoOptions = {
42
+ readonly actor: ActorExtractor;
43
+ readonly stream: (action_name: string, input: unknown, c: Context) => string | Promise<string>;
44
+ readonly expectedVersion?: (action_name: string, input: unknown, c: Context) => number | undefined | Promise<number | undefined>;
45
+ readonly idempotency?: {
46
+ readonly store: IdempotencyStore;
47
+ readonly keyFrom?: (c: Context) => string | undefined;
48
+ };
49
+ readonly basePath?: string;
50
+ };
51
+ /**
52
+ * Build a Hono middleware that runs an {@link ActorExtractor} once
53
+ * per call and stashes the resolved {@link Actor} under
54
+ * `c.set("actor", ...)` so downstream routes can read it via
55
+ * `c.get("actor")`. The generator at {@link hono} uses this
56
+ * internally; it's also exported so hosts that want to compose
57
+ * their own route chain (logging, tracing, custom auth flavors)
58
+ * can wire it directly:
59
+ *
60
+ * ```ts
61
+ * import { Hono } from "hono";
62
+ * import { authenticated, type ActMiddlewareVariables } from "@rotorsoft/act-http/hono";
63
+ *
64
+ * const api = new Hono<{ Variables: ActMiddlewareVariables }>();
65
+ * api.use("*", authenticated((c) => resolveUserFromJwt(c)));
66
+ * api.get("/me", (c) => c.json(c.get("actor"))); // typed
67
+ * ```
68
+ *
69
+ * Errors thrown by the extractor surface as `401 Unauthorized` with
70
+ * the shared {@link ApiError} envelope.
71
+ */
72
+ export declare function authenticated(extractor: ActorExtractor): MiddlewareHandler<{
73
+ Variables: ActMiddlewareVariables;
74
+ }>;
75
+ /**
76
+ * Build a typed Hono REST surface from a built `Act` instance.
77
+ *
78
+ * Walks `app.registry.actions` once and emits one
79
+ * `POST /actions/<actionName>` per action under the configured
80
+ * `basePath` (default `/api`). Each route:
81
+ *
82
+ * 1. Runs the internal {@link authenticated} middleware —
83
+ * `options.actor` resolves the {@link Actor} once and stashes
84
+ * it under `c.get("actor")`.
85
+ * 2. Validates the JSON body against the action's Zod schema via
86
+ * `@hono/zod-validator`. Failures short-circuit with `422`.
87
+ * 3. Resolves the target stream via
88
+ * `options.stream(name, input, c)`.
89
+ * 4. (Optionally) resolves `expectedVersion` for optimistic
90
+ * concurrency.
91
+ * 5. (Optionally) claims the `Idempotency-Key` via
92
+ * {@link withIdempotency} — responds `409` on duplicate.
93
+ * 6. Calls
94
+ * `app.do(name, { stream, actor, expectedVersion? }, input)`.
95
+ * 7. Maps any thrown framework error onto the shared
96
+ * {@link ApiError} envelope via {@link toApiError}, returning
97
+ * the conventional HTTP status (`409` / `412` for concurrency,
98
+ * `422` for validation, `400` for non-retryable, etc.).
99
+ *
100
+ * @param app A built `Act` orchestrator.
101
+ * @param options Auth, stream, expected-version, idempotency, and
102
+ * base-path seams.
103
+ * @returns A Hono app covering every registered action under
104
+ * `<basePath>/actions/<name>`.
105
+ */
106
+ export declare function hono(app: Act<Schemas, Schemas, Schemas, Record<string, Schema>>, options: HonoOptions): Hono<{
107
+ Variables: ActMiddlewareVariables;
108
+ }>;
109
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hono/index.ts"],"names":[],"mappings":"AAgDA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EACL,KAAK,cAAc,EAIpB,MAAM,iBAAiB,CAAC;AAEzB;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,EAAE,KAAK,CAAC;CACd,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,CACf,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,OAAO,EACd,CAAC,EAAE,OAAO,KACP,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9B,QAAQ,CAAC,eAAe,CAAC,EAAE,CACzB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,OAAO,EACd,CAAC,EAAE,OAAO,KACP,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACtD,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;QACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;KACvD,CAAC;IACF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,cAAc,GACxB,iBAAiB,CAAC;IAAE,SAAS,EAAE,sBAAsB,CAAA;CAAE,CAAC,CAe1D;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,IAAI,CAClB,GAAG,EAAE,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC3D,OAAO,EAAE,WAAW,GACnB,IAAI,CAAC;IAAE,SAAS,EAAE,sBAAsB,CAAA;CAAE,CAAC,CAyE7C"}
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * @module act-http/trpc
4
+ *
5
+ * tRPC subpath for the auto-generated API epic (#835). Walks the
6
+ * registry of a built `Act` once at function call and emits a typed
7
+ * router that exposes every registered action as a flat `mutation`,
8
+ * matching the shape hand-written tRPC routers in the Act ecosystem
9
+ * already use.
10
+ *
11
+ * Usage:
12
+ *
13
+ * ```ts
14
+ * import { trpc } from "@rotorsoft/act-http/trpc";
15
+ *
16
+ * const router = trpc(app, {
17
+ * actor: (ctx) => ({ id: ctx.user.id, name: ctx.user.name }),
18
+ * stream: (action, input, ctx) => input.stream ?? ctx.tenant,
19
+ * });
20
+ *
21
+ * // Client sees:
22
+ * // appRouter.PressKey.mutate({ key: "5" })
23
+ * // appRouter.OpenTicket.mutate({ title: "..." })
24
+ * ```
25
+ *
26
+ * The route shape is flat: one procedure per registered action at
27
+ * the top level. Action names are the unique identifiers across
28
+ * the registry (the framework already enforces no duplicates), so
29
+ * an extra namespace tier would be overhead in the common case.
30
+ * Pure mutations for this slice; query/projection wiring lives in
31
+ * a follow-up. See `libs/act-http/README.md` for the full surface
32
+ * walkthrough.
33
+ *
34
+ * **Actor lives on the context.** The generator wires an internal
35
+ * tRPC middleware that runs the host's
36
+ * {@link TrpcOptions.actor | actor extractor} once per call and
37
+ * injects the resolved {@link Actor} onto the downstream context as
38
+ * `ctx.actor`. Every generated mutation reads from there — auth
39
+ * resolves at the procedure boundary, not inside each action body,
40
+ * and any composed middleware (logging, tracing) sees the same
41
+ * `ctx.actor`. The middleware factory is also exported as
42
+ * {@link authenticated} for hosts that need to compose it into
43
+ * their own procedure chain.
44
+ *
45
+ * Composes the shared utilities at `@rotorsoft/act-http/api`:
46
+ * {@link ActorExtractor} for the auth seam, {@link toApiError} for
47
+ * the uniform error→status/code mapping, {@link withIdempotency}
48
+ * for `Idempotency-Key` dedup. Three transports (this one, Hono,
49
+ * OpenAPI) ride the same utilities so a client talking to two
50
+ * transports never sees two error shapes for the same framework
51
+ * error.
52
+ */
53
+ import type { Act, Schema, Schemas } from "@rotorsoft/act";
54
+ import type { IdempotencyStore } from "@rotorsoft/act-ops/idempotency";
55
+ import { type ActorExtractor } from "../api/index.js";
56
+ /**
57
+ * Per-call options for {@link trpc}. The host supplies the three
58
+ * seams the package can't make decisions on:
59
+ *
60
+ * - `actor` — auth resolver. Receives the tRPC context, returns the
61
+ * `Actor` that flows onto `ctx.actor` for every generated
62
+ * procedure (via an internal {@link authenticated}).
63
+ * - `stream` — stream-id resolver. Receives the action name, the
64
+ * validated action input, and the original tRPC context; returns
65
+ * the target stream. Singleton aggregates return a constant;
66
+ * per-tenant aggregates pull from input or context.
67
+ * - `expectedVersion` (optional) — optimistic-concurrency resolver.
68
+ * When set, the procedure threads the resolved value through
69
+ * `target.expectedVersion` so `app.do` refuses to commit if the
70
+ * stream has moved. Hosts typically read it from an `If-Match`
71
+ * header on the underlying HTTP request, or pull it from the
72
+ * client's last-known snapshot. Returning `undefined` skips the
73
+ * check for that call — handy when only some actions are
74
+ * concurrency-sensitive.
75
+ * - `idempotency` (optional) — when set, the procedure honors
76
+ * `Idempotency-Key` via the shared
77
+ * {@link withIdempotency} helper. The host supplies the
78
+ * `IdempotencyStore` and a `keyFrom` extractor that reads the key
79
+ * out of the tRPC context (typically a header). On a duplicate
80
+ * claim, the procedure throws a `CONFLICT` `TRPCError` — the
81
+ * receiver-side convention of "ack the duplicate" doesn't carry
82
+ * over because the contract intentionally does not cache the
83
+ * original handler's result.
84
+ *
85
+ * @template Ctx The host's tRPC context shape. Flows end-to-end —
86
+ * procedures see this exact context type (plus `actor`) at
87
+ * runtime.
88
+ */
89
+ export type TrpcOptions<Ctx> = {
90
+ readonly actor: ActorExtractor;
91
+ readonly stream: (action_name: string, input: unknown, ctx: Ctx) => string | Promise<string>;
92
+ readonly expectedVersion?: (action_name: string, input: unknown, ctx: Ctx) => number | undefined | Promise<number | undefined>;
93
+ readonly idempotency?: {
94
+ readonly store: IdempotencyStore;
95
+ readonly keyFrom: (ctx: Ctx) => string | undefined;
96
+ };
97
+ };
98
+ /**
99
+ * Build a tRPC middleware that runs an {@link ActorExtractor} once
100
+ * per call and forwards the resolved {@link Actor} on the
101
+ * downstream context as `ctx.actor`. The generator at
102
+ * {@link trpc} uses this internally; it's also exported so hosts
103
+ * that want to compose their own procedure chain (logging,
104
+ * tracing, custom auth flavors) can wire it directly:
105
+ *
106
+ * ```ts
107
+ * import { initTRPC } from "@trpc/server";
108
+ * import { authenticated } from "@rotorsoft/act-http/trpc";
109
+ *
110
+ * type Ctx = { user?: { id: string; name: string } };
111
+ * const t = initTRPC.context<Ctx>().create();
112
+ *
113
+ * const authed = t.procedure.use(
114
+ * authenticated((ctx) => {
115
+ * if (!ctx.user) throw new Error("not authenticated");
116
+ * return { id: ctx.user.id, name: ctx.user.name };
117
+ * })
118
+ * );
119
+ *
120
+ * // Inside any procedure built off `authed`, `ctx.actor: Actor`.
121
+ * ```
122
+ *
123
+ * The middleware is unaware of the host's `t` instance — it returns
124
+ * a structurally-typed function that any tRPC `procedure.use(...)`
125
+ * accepts. Errors thrown by the extractor surface unchanged
126
+ * (typically wrap them as `TRPCError({ code: "UNAUTHORIZED" })`
127
+ * in the extractor body).
128
+ */
129
+ export declare function authenticated(extractor: ActorExtractor): any;
130
+ /**
131
+ * Build a typed tRPC router from a built `Act` instance.
132
+ *
133
+ * Walks `app.registry.actions` once and emits one flat mutation per
134
+ * action under `router.<actionName>`. The framework already enforces
135
+ * no duplicate action names across states, so a flat tier reads
136
+ * cleanly at the client and matches the hand-written router shape
137
+ * adopters already have. Each mutation:
138
+ *
139
+ * 1. Runs the internal {@link authenticated} — `options.actor`
140
+ * resolves the {@link Actor} once and injects it onto
141
+ * `ctx.actor`.
142
+ * 2. Resolves the target stream via `options.stream(name, input, ctx)`.
143
+ * 3. (Optionally) claims the `Idempotency-Key` via
144
+ * {@link withIdempotency} — throws `CONFLICT` on duplicate.
145
+ * 4. Calls `app.do(name, { stream, actor: ctx.actor }, input)`.
146
+ * 5. Maps any thrown framework error onto a `TRPCError` via
147
+ * {@link toApiError}.
148
+ *
149
+ * The returned router's procedure inputs come straight from each
150
+ * action's Zod schema; outputs are the framework's
151
+ * `Snapshot<TState, TEvents>[]` shape. Strong typing flows
152
+ * end-to-end via tRPC's normal inference path — the client sees
153
+ * `appRouter.Calculator.PressKey.mutate({ key: "5" })` as a typed
154
+ * call without any codegen.
155
+ *
156
+ * @template Ctx The tRPC context shape the host's runtime feeds in.
157
+ * Flows through `actor`, `stream`, and `idempotency.keyFrom` so
158
+ * the operator's existing context types just work. The internal
159
+ * middleware augments `Ctx` with `{ actor: Actor }` on the
160
+ * downstream procedure context.
161
+ *
162
+ * @param app A built `Act` orchestrator.
163
+ * @param options Auth, stream, and (optional) idempotency seams.
164
+ * @returns A tRPC router covering every registered action, grouped
165
+ * by owning state.
166
+ */
167
+ export declare function trpc<Ctx extends object = object>(app: Act<any, Schemas, Schemas, Record<string, Schema>>, options: TrpcOptions<Ctx>): never;
168
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/trpc/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,OAAO,KAAK,EAAE,GAAG,EAAS,MAAM,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAEvE,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,MAAM,WAAW,CAAC,GAAG,IAAI;IAC7B,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,CACf,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,GAAG,KACL,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9B,QAAQ,CAAC,eAAe,CAAC,EAAE,CACzB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,GAAG,KACL,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACtD,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;QACjC,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,GAAG,SAAS,CAAC;KACpD,CAAC;CACH,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,wBAAgB,aAAa,CAAC,SAAS,EAAE,cAAc,GAAG,GAAG,CAW5D;AA+CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,IAAI,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,EAE9C,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACvD,OAAO,EAAE,WAAW,CAAC,GAAG,CAAC,SAkE1B"}
package/dist/api/index.js CHANGED
@@ -1,59 +1,8 @@
1
- // src/api/errors.ts
2
1
  import {
3
- ConcurrencyError,
4
- InvariantError,
5
- NonRetryableError,
6
- StreamClosedError,
7
- ValidationError
8
- } from "@rotorsoft/act";
9
- var ERROR_MAP = {
10
- ValidationError: { status: 422, code: "VALIDATION" },
11
- InvariantError: { status: 409, code: "INVARIANT" },
12
- ConcurrencyError: { status: 412, code: "CONCURRENCY" },
13
- StreamClosedError: { status: 410, code: "STREAM_CLOSED" },
14
- NonRetryableError: { status: 400, code: "NON_RETRYABLE" }
15
- };
16
- var lookup_known = (err) => {
17
- if (err instanceof ValidationError) return { name: "ValidationError" };
18
- if (err instanceof InvariantError) return { name: "InvariantError" };
19
- if (err instanceof ConcurrencyError) return { name: "ConcurrencyError" };
20
- if (err instanceof StreamClosedError) return { name: "StreamClosedError" };
21
- if (err instanceof NonRetryableError) return { name: "NonRetryableError" };
22
- return null;
23
- };
24
- function toApiError(err) {
25
- const known = lookup_known(err);
26
- if (known) {
27
- const entry = ERROR_MAP[known.name];
28
- return {
29
- status: entry.status,
30
- body: {
31
- error: known.name,
32
- detail: err.message,
33
- code: entry.code
34
- }
35
- };
36
- }
37
- if (err instanceof Error) {
38
- return {
39
- status: 500,
40
- body: { error: "InternalError", detail: err.message, code: "INTERNAL" }
41
- };
42
- }
43
- return {
44
- status: 500,
45
- body: { error: "InternalError", code: "INTERNAL" }
46
- };
47
- }
48
-
49
- // src/api/idempotency.ts
50
- async function withIdempotency(store, key, handler) {
51
- const fresh = await store.claim(key);
52
- if (!fresh) {
53
- return { deduped: true };
54
- }
55
- return { deduped: false, result: await handler() };
56
- }
2
+ ERROR_MAP,
3
+ toApiError,
4
+ withIdempotency
5
+ } from "../chunk-Z6FVXDDU.js";
57
6
  export {
58
7
  ERROR_MAP,
59
8
  toApiError,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/api/errors.ts","../../src/api/idempotency.ts"],"sourcesContent":["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":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;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,gBAAiB,QAAO,EAAE,MAAM,kBAAkB;AACrE,MAAI,eAAe,eAAgB,QAAO,EAAE,MAAM,iBAAiB;AACnE,MAAI,eAAe,iBAAkB,QAAO,EAAE,MAAM,mBAAmB;AACvE,MAAI,eAAe,kBAAmB,QAAO,EAAE,MAAM,oBAAoB;AACzE,MAAI,eAAe,kBAAmB,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;","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,63 @@
1
+ // src/api/errors.ts
2
+ import {
3
+ ConcurrencyError,
4
+ InvariantError,
5
+ NonRetryableError,
6
+ StreamClosedError,
7
+ ValidationError
8
+ } from "@rotorsoft/act";
9
+ var ERROR_MAP = {
10
+ ValidationError: { status: 422, code: "VALIDATION" },
11
+ InvariantError: { status: 409, code: "INVARIANT" },
12
+ ConcurrencyError: { status: 412, code: "CONCURRENCY" },
13
+ StreamClosedError: { status: 410, code: "STREAM_CLOSED" },
14
+ NonRetryableError: { status: 400, code: "NON_RETRYABLE" }
15
+ };
16
+ var lookup_known = (err) => {
17
+ if (err instanceof ValidationError) return { name: "ValidationError" };
18
+ if (err instanceof InvariantError) return { name: "InvariantError" };
19
+ if (err instanceof ConcurrencyError) return { name: "ConcurrencyError" };
20
+ if (err instanceof StreamClosedError) return { name: "StreamClosedError" };
21
+ if (err instanceof NonRetryableError) return { name: "NonRetryableError" };
22
+ return null;
23
+ };
24
+ function toApiError(err) {
25
+ const known = lookup_known(err);
26
+ if (known) {
27
+ const entry = ERROR_MAP[known.name];
28
+ return {
29
+ status: entry.status,
30
+ body: {
31
+ error: known.name,
32
+ detail: err.message,
33
+ code: entry.code
34
+ }
35
+ };
36
+ }
37
+ if (err instanceof Error) {
38
+ return {
39
+ status: 500,
40
+ body: { error: "InternalError", detail: err.message, code: "INTERNAL" }
41
+ };
42
+ }
43
+ return {
44
+ status: 500,
45
+ body: { error: "InternalError", code: "INTERNAL" }
46
+ };
47
+ }
48
+
49
+ // src/api/idempotency.ts
50
+ async function withIdempotency(store, key, handler) {
51
+ const fresh = await store.claim(key);
52
+ if (!fresh) {
53
+ return { deduped: true };
54
+ }
55
+ return { deduped: false, result: await handler() };
56
+ }
57
+
58
+ export {
59
+ ERROR_MAP,
60
+ toApiError,
61
+ withIdempotency
62
+ };
63
+ //# sourceMappingURL=chunk-Z6FVXDDU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api/errors.ts","../src/api/idempotency.ts"],"sourcesContent":["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":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;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,gBAAiB,QAAO,EAAE,MAAM,kBAAkB;AACrE,MAAI,eAAe,eAAgB,QAAO,EAAE,MAAM,iBAAiB;AACnE,MAAI,eAAe,iBAAkB,QAAO,EAAE,MAAM,mBAAmB;AACvE,MAAI,eAAe,kBAAmB,QAAO,EAAE,MAAM,oBAAoB;AACzE,MAAI,eAAe,kBAAmB,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;","names":[]}
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/hono/index.ts
21
+ var hono_exports = {};
22
+ __export(hono_exports, {
23
+ authenticated: () => authenticated,
24
+ hono: () => hono
25
+ });
26
+ module.exports = __toCommonJS(hono_exports);
27
+
28
+ // ../../node_modules/.pnpm/@hono+zod-validator@0.8.0_hono@4.12.23_zod@4.4.3/node_modules/@hono/zod-validator/dist/index.js
29
+ var import_validator = require("hono/validator");
30
+ function zValidatorFunction(target, schema, hook, options) {
31
+ return (0, import_validator.validator)(target, async (value, c) => {
32
+ let validatorValue = value;
33
+ if (target === "header" && "_def" in schema || target === "header" && "_zod" in schema) {
34
+ const schemaKeys = Object.keys("in" in schema ? schema.in.shape : schema.shape);
35
+ const caseInsensitiveKeymap = Object.fromEntries(schemaKeys.map((key) => [key.toLowerCase(), key]));
36
+ validatorValue = Object.fromEntries(Object.entries(value).map(([key, value$1]) => [caseInsensitiveKeymap[key] || key, value$1]));
37
+ }
38
+ const result = options && options.validationFunction ? await options.validationFunction(schema, validatorValue) : await schema.safeParseAsync(validatorValue);
39
+ if (hook) {
40
+ const hookResult = await hook({
41
+ data: validatorValue,
42
+ ...result,
43
+ target
44
+ }, c);
45
+ if (hookResult) {
46
+ if (hookResult instanceof Response) return hookResult;
47
+ if ("response" in hookResult) return hookResult.response;
48
+ }
49
+ }
50
+ if (!result.success) return c.json(result, 400);
51
+ return result.data;
52
+ });
53
+ }
54
+ var zValidator = zValidatorFunction;
55
+
56
+ // src/hono/index.ts
57
+ var import_hono = require("hono");
58
+
59
+ // src/api/errors.ts
60
+ var import_act = require("@rotorsoft/act");
61
+ var ERROR_MAP = {
62
+ ValidationError: { status: 422, code: "VALIDATION" },
63
+ InvariantError: { status: 409, code: "INVARIANT" },
64
+ ConcurrencyError: { status: 412, code: "CONCURRENCY" },
65
+ StreamClosedError: { status: 410, code: "STREAM_CLOSED" },
66
+ NonRetryableError: { status: 400, code: "NON_RETRYABLE" }
67
+ };
68
+ var lookup_known = (err) => {
69
+ if (err instanceof import_act.ValidationError) return { name: "ValidationError" };
70
+ if (err instanceof import_act.InvariantError) return { name: "InvariantError" };
71
+ if (err instanceof import_act.ConcurrencyError) return { name: "ConcurrencyError" };
72
+ if (err instanceof import_act.StreamClosedError) return { name: "StreamClosedError" };
73
+ if (err instanceof import_act.NonRetryableError) return { name: "NonRetryableError" };
74
+ return null;
75
+ };
76
+ function toApiError(err) {
77
+ const known = lookup_known(err);
78
+ if (known) {
79
+ const entry = ERROR_MAP[known.name];
80
+ return {
81
+ status: entry.status,
82
+ body: {
83
+ error: known.name,
84
+ detail: err.message,
85
+ code: entry.code
86
+ }
87
+ };
88
+ }
89
+ if (err instanceof Error) {
90
+ return {
91
+ status: 500,
92
+ body: { error: "InternalError", detail: err.message, code: "INTERNAL" }
93
+ };
94
+ }
95
+ return {
96
+ status: 500,
97
+ body: { error: "InternalError", code: "INTERNAL" }
98
+ };
99
+ }
100
+
101
+ // src/api/idempotency.ts
102
+ async function withIdempotency(store, key, handler) {
103
+ const fresh = await store.claim(key);
104
+ if (!fresh) {
105
+ return { deduped: true };
106
+ }
107
+ return { deduped: false, result: await handler() };
108
+ }
109
+
110
+ // src/hono/index.ts
111
+ function authenticated(extractor) {
112
+ return async (c, next) => {
113
+ try {
114
+ const actor = await extractor(c);
115
+ c.set("actor", actor);
116
+ await next();
117
+ } catch (err) {
118
+ const body = {
119
+ error: "Unauthorized",
120
+ detail: err instanceof Error ? err.message : void 0,
121
+ code: "UNAUTHORIZED"
122
+ };
123
+ return c.json(body, 401);
124
+ }
125
+ };
126
+ }
127
+ var default_key_from = (c) => c.req.header("idempotency-key");
128
+ function hono(app, options) {
129
+ const base_path = options.basePath ?? "/api";
130
+ const api = new import_hono.Hono().basePath(
131
+ base_path
132
+ );
133
+ api.use("*", authenticated(options.actor));
134
+ const key_from = options.idempotency?.keyFrom ?? default_key_from;
135
+ for (const [action_name, state] of Object.entries(app.registry.actions)) {
136
+ const schema = state.actions[action_name];
137
+ api.post(
138
+ `/actions/${action_name}`,
139
+ zValidator("json", schema),
140
+ async (c) => {
141
+ try {
142
+ const input = c.req.valid("json");
143
+ const actor = c.get("actor");
144
+ const stream = await options.stream(action_name, input, c);
145
+ const expected_version = options.expectedVersion ? await options.expectedVersion(action_name, input, c) : void 0;
146
+ const target = expected_version === void 0 ? { stream, actor } : { stream, actor, expectedVersion: expected_version };
147
+ if (options.idempotency) {
148
+ const key = key_from(c);
149
+ if (!key) {
150
+ const body = {
151
+ error: "BadRequest",
152
+ detail: "Idempotency-Key required",
153
+ code: "BAD_REQUEST"
154
+ };
155
+ return c.json(body, 400);
156
+ }
157
+ const outcome = await withIdempotency(
158
+ options.idempotency.store,
159
+ key,
160
+ () => app.do(action_name, target, input)
161
+ );
162
+ if (outcome.deduped) {
163
+ const body = {
164
+ error: "Conflict",
165
+ detail: "Idempotency-Key already used; original result not cached",
166
+ code: "CONFLICT"
167
+ };
168
+ return c.json(body, 409);
169
+ }
170
+ return c.json(outcome.result);
171
+ }
172
+ const snapshots = await app.do(
173
+ action_name,
174
+ target,
175
+ input
176
+ );
177
+ return c.json(snapshots);
178
+ } catch (err) {
179
+ const { status, body } = toApiError(err);
180
+ return c.json(body, status);
181
+ }
182
+ }
183
+ );
184
+ }
185
+ return api;
186
+ }
187
+ // Annotate the CommonJS export names for ESM import in node:
188
+ 0 && (module.exports = {
189
+ authenticated,
190
+ hono
191
+ });
192
+ //# sourceMappingURL=index.cjs.map