@rotorsoft/act-http 1.0.0 → 1.2.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 +127 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/api/actor.d.ts +20 -0
- package/dist/@types/api/actor.d.ts.map +1 -0
- package/dist/@types/api/errors.d.ts +73 -0
- package/dist/@types/api/errors.d.ts.map +1 -0
- package/dist/@types/api/idempotency.d.ts +36 -0
- package/dist/@types/api/idempotency.d.ts.map +1 -0
- package/dist/@types/api/index.d.ts +39 -0
- package/dist/@types/api/index.d.ts.map +1 -0
- package/dist/@types/receiver/check.d.ts +66 -0
- package/dist/@types/receiver/check.d.ts.map +1 -0
- package/dist/@types/receiver/express/index.d.ts +51 -0
- package/dist/@types/receiver/express/index.d.ts.map +1 -0
- package/dist/@types/receiver/extract.d.ts +24 -0
- package/dist/@types/receiver/extract.d.ts.map +1 -0
- package/dist/@types/receiver/fastify/index.d.ts +55 -0
- package/dist/@types/receiver/fastify/index.d.ts.map +1 -0
- package/dist/@types/receiver/hono/index.d.ts +60 -0
- package/dist/@types/receiver/hono/index.d.ts.map +1 -0
- package/dist/@types/receiver/index.d.ts +39 -0
- package/dist/@types/receiver/index.d.ts.map +1 -0
- package/dist/@types/receiver/start.d.ts +48 -0
- package/dist/@types/receiver/start.d.ts.map +1 -0
- package/dist/@types/receiver/trpc/index.d.ts +16 -0
- package/dist/@types/receiver/trpc/index.d.ts.map +1 -0
- package/dist/@types/receiver/verify.d.ts +57 -0
- package/dist/@types/receiver/verify.d.ts.map +1 -0
- package/dist/@types/webhook/classify.d.ts +59 -0
- package/dist/@types/webhook/classify.d.ts.map +1 -0
- package/dist/@types/webhook/index.d.ts +3 -2
- package/dist/@types/webhook/index.d.ts.map +1 -1
- package/dist/@types/webhook/sign.d.ts +25 -0
- package/dist/@types/webhook/sign.d.ts.map +1 -0
- package/dist/@types/webhook/types.d.ts +61 -20
- package/dist/@types/webhook/types.d.ts.map +1 -1
- package/dist/api/index.cjs +85 -0
- package/dist/api/index.cjs.map +1 -0
- package/dist/api/index.js +62 -0
- package/dist/api/index.js.map +1 -0
- package/dist/chunk-F7VWYZ37.js +29 -0
- package/dist/chunk-F7VWYZ37.js.map +1 -0
- package/dist/chunk-NOIXOF2I.js +78 -0
- package/dist/chunk-NOIXOF2I.js.map +1 -0
- package/dist/dist-NWMJQI4E.js +647 -0
- package/dist/dist-NWMJQI4E.js.map +1 -0
- package/dist/receiver/express/index.cjs +128 -0
- package/dist/receiver/express/index.cjs.map +1 -0
- package/dist/receiver/express/index.js +33 -0
- package/dist/receiver/express/index.js.map +1 -0
- package/dist/receiver/fastify/index.cjs +120 -0
- package/dist/receiver/fastify/index.cjs.map +1 -0
- package/dist/receiver/fastify/index.js +25 -0
- package/dist/receiver/fastify/index.js.map +1 -0
- package/dist/receiver/hono/index.cjs +123 -0
- package/dist/receiver/hono/index.cjs.map +1 -0
- package/dist/receiver/hono/index.js +8 -0
- package/dist/receiver/hono/index.js.map +1 -0
- package/dist/receiver/index.cjs +2943 -0
- package/dist/receiver/index.cjs.map +1 -0
- package/dist/receiver/index.js +2162 -0
- package/dist/receiver/index.js.map +1 -0
- package/dist/receiver/trpc/index.cjs +126 -0
- package/dist/receiver/trpc/index.cjs.map +1 -0
- package/dist/receiver/trpc/index.js +31 -0
- package/dist/receiver/trpc/index.js.map +1 -0
- package/dist/webhook/index.cjs +66 -6
- package/dist/webhook/index.cjs.map +1 -1
- package/dist/webhook/index.js +62 -6
- package/dist/webhook/index.js.map +1 -1
- package/package.json +52 -3
package/README.md
CHANGED
|
@@ -20,12 +20,18 @@ Most Act apps reach beyond their own process eventually — POSTing committed ev
|
|
|
20
20
|
pnpm add @rotorsoft/act-http
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Three independent subpath exports:
|
|
24
24
|
|
|
25
25
|
| Import path | What you get |
|
|
26
26
|
|---|---|
|
|
27
27
|
| `@rotorsoft/act-http/webhook` | `webhook()` — reaction handler that POSTs committed events with timeout, auto `Idempotency-Key`, and status-classified errors. |
|
|
28
28
|
| `@rotorsoft/act-http/sse` | `BroadcastChannel`, `PresenceTracker`, `StateCache`, `applyPatchMessage` — server-side broadcast + client-side patch applicator for incremental state sync. |
|
|
29
|
+
| `@rotorsoft/act-http/receiver` | `receiver()` builder (high-level Hono-backed runtime) + `extractIdempotencyKey` + `verifyWebhook` + `checkWebhook` (framework-agnostic core composing both with `IdempotencyStore.claim`). |
|
|
30
|
+
| `@rotorsoft/act-http/receiver/trpc` | `webhookMiddleware` — tRPC middleware adapter. |
|
|
31
|
+
| `@rotorsoft/act-http/receiver/express` | `webhookMiddleware` — Express middleware adapter. |
|
|
32
|
+
| `@rotorsoft/act-http/receiver/fastify` | `webhookMiddleware` — Fastify `preHandler` adapter. |
|
|
33
|
+
| `@rotorsoft/act-http/receiver/hono` | `webhookMiddleware` — Hono middleware adapter. |
|
|
34
|
+
| `@rotorsoft/act-http/api` | `ActorExtractor` type, `ApiError` + `ERROR_MAP` + `toApiError` envelope mapping, `withIdempotency` wrapper. Shared utilities for the auto-generated API surfaces (`/trpc`, `/hono`, `/openapi` subpaths, landing under issues #843/#844/#845). |
|
|
29
35
|
|
|
30
36
|
## Quick start
|
|
31
37
|
|
|
@@ -50,6 +56,85 @@ import { webhook } from "@rotorsoft/act-http/webhook";
|
|
|
50
56
|
.to(resolver)
|
|
51
57
|
```
|
|
52
58
|
|
|
59
|
+
### `receiver` — high-level builder (the canonical path)
|
|
60
|
+
|
|
61
|
+
The `receiver` builder from `@rotorsoft/act-http/receiver` is Hono-backed and runs on every fetch-shaped runtime — long-running Node (via `.listen()`), AWS Lambda, Cloudflare Workers, Vercel Edge, Bun, Deno (all via `.fetch()`). Declare typed handlers with Zod schemas, call `.build()`, and the runtime handles signature verification, dedup, raw-body capture, schema validation, and HTTP server lifecycle:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { receiver } from "@rotorsoft/act-http/receiver";
|
|
65
|
+
import { InMemoryIdempotencyStore } from "@rotorsoft/act-ops/idempotency";
|
|
66
|
+
import { z } from "zod";
|
|
67
|
+
|
|
68
|
+
const escalations = receiver({
|
|
69
|
+
port: 4001,
|
|
70
|
+
store: new InMemoryIdempotencyStore(),
|
|
71
|
+
secret: process.env.WEBHOOK_SECRET,
|
|
72
|
+
})
|
|
73
|
+
.on("OrderConfirmed", z.object({ orderId: z.string(), total: z.number() }),
|
|
74
|
+
async (event, ctx) => { await processOrder(event.orderId, event.total); })
|
|
75
|
+
.build();
|
|
76
|
+
|
|
77
|
+
await escalations.listen(); // Node
|
|
78
|
+
// export default { fetch: escalations.fetch }; // Cloudflare / Vercel / Bun / Deno
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Naming convention: type `Receiver` (PascalCase), factory `receiver` (lowercase) — matches Act's existing `act` / `state` / `slice` / `projection` builder analogs.
|
|
82
|
+
|
|
83
|
+
### `receiver/<framework>` — low-level middleware
|
|
84
|
+
|
|
85
|
+
When the receiver needs to compose with an existing HTTP stack (auth middleware, route-level rate limiting, an app already serving other routes), reach for the per-framework `webhookMiddleware` factories. They compose `extractIdempotencyKey` + `verifyWebhook` + `IdempotencyStore.claim` and translate the result into the framework's idiomatic 400/401 response:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// tRPC
|
|
89
|
+
import { webhookMiddleware } from "@rotorsoft/act-http/receiver/trpc";
|
|
90
|
+
import { InMemoryIdempotencyStore } from "@rotorsoft/act-ops/idempotency";
|
|
91
|
+
|
|
92
|
+
const dedup = new InMemoryIdempotencyStore();
|
|
93
|
+
const idempotent = t.procedure.use(
|
|
94
|
+
webhookMiddleware({ store: dedup, secret: process.env.WEBHOOK_SECRET })
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const router = t.router({
|
|
98
|
+
webhook: idempotent.input(Schema).mutation(({ input, ctx }) => {
|
|
99
|
+
const { key, deduped } = ctx.idempotency;
|
|
100
|
+
if (deduped) return { status: "dedup-skipped", key };
|
|
101
|
+
return { status: "processed", key };
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Each adapter follows the same shape:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { webhookMiddleware } from "@rotorsoft/act-http/receiver/express";
|
|
110
|
+
app.post("/webhook", webhookMiddleware({ store, secret }), (req, res) => {
|
|
111
|
+
const { key, deduped } = (req as any).idempotency;
|
|
112
|
+
// …
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { webhookMiddleware } from "@rotorsoft/act-http/receiver/fastify";
|
|
118
|
+
app.post("/webhook", { preHandler: webhookMiddleware({ store, secret }) }, async (req) => {
|
|
119
|
+
const { key, deduped } = (req as any).idempotency;
|
|
120
|
+
// …
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { webhookMiddleware } from "@rotorsoft/act-http/receiver/hono";
|
|
126
|
+
app.post("/webhook", webhookMiddleware({ store, secret }), (c) => {
|
|
127
|
+
const { key, deduped } = c.get("idempotency");
|
|
128
|
+
// …
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
On failure: the adapter responds with the framework's idiomatic 400 (`missing-key`) or 401 (one of five verification reasons — `missing-signature`, `missing-timestamp`, `stale`, `future`, `bad-signature`) and short-circuits the handler. On success: `{ key, deduped }` is injected into the request context.
|
|
133
|
+
|
|
134
|
+
### `receiver` primitives — when neither builder nor middleware fits
|
|
135
|
+
|
|
136
|
+
The framework-agnostic core (`checkWebhook`) and the underlying primitives (`extractIdempotencyKey`, `verifyWebhook`) are exported from `@rotorsoft/act-http/receiver` for receivers whose framework isn't in the adapter list (Koa, raw Node `http`, gRPC-over-HTTP, …) or for receivers with custom policy (e.g. "missing key falls back to body-derived dedup"). Use the `receiver` builder when you can; fall back to the framework `webhookMiddleware`, then the primitives.
|
|
137
|
+
|
|
53
138
|
### `sse` — live state broadcast
|
|
54
139
|
|
|
55
140
|
```ts
|
|
@@ -75,9 +160,45 @@ onData: (msg) => {
|
|
|
75
160
|
### `/webhook` subpath
|
|
76
161
|
|
|
77
162
|
- **`webhook(config)`** — reaction-handler factory. Returns a function compatible with `.do(handler, opts)`.
|
|
78
|
-
- **`
|
|
79
|
-
- **`
|
|
163
|
+
- **`tryOk(response, { url, label? })`** — collapses the classify-and-throw block to one line for **custom HTTP-like reactions** (gRPC bridges, SDK-based deliveries). Returns void on 2xx; throws `RetryableHttpError` on 5xx; throws `NonRetryableHttpError` on 3xx/4xx. Captures the response body (best-effort) onto the thrown error.
|
|
164
|
+
- **`classifyHttpResponse(response)`** — the underlying `"ok" | "retry" | "block"` classifier. Reach for it directly when you need custom error classes; otherwise `tryOk` wraps it.
|
|
165
|
+
- **`RetryableHttpError`** — generic retryable delivery error. Extends `Error`. Thrown by `tryOk` on 5xx. `WebhookError` extends it.
|
|
166
|
+
- **`NonRetryableHttpError`** — generic non-retryable delivery error. Extends `NonRetryableError` from `@rotorsoft/act`, so the drain finalizer blocks the stream on first failed attempt. Thrown by `tryOk` on 3xx/4xx. `NonRetryableWebhookError` extends it.
|
|
167
|
+
- **`WebhookError`** — webhook-specific subclass of `RetryableHttpError`, thrown by the `webhook` helper. Existing `instanceof WebhookError` checks continue to work; new code targeting any HTTP integration can catch `RetryableHttpError` to handle both webhook + custom-integration errors uniformly.
|
|
168
|
+
- **`NonRetryableWebhookError`** — webhook-specific subclass of `NonRetryableHttpError`, thrown by `webhook` on 3xx/4xx. Same backward-compat story as `WebhookError`.
|
|
80
169
|
- **`WebhookConfig`** — TypeScript type for the helper options.
|
|
170
|
+
- **`HttpDisposition`** — the `"ok" | "retry" | "block"` discriminator returned by `classifyHttpResponse`.
|
|
171
|
+
- **`HttpDeliveryErrorInit`** — common `{ status, url, responseBody? }` shape passed to every HTTP error class.
|
|
172
|
+
- **`TryOkOptions`** — `{ url, label? }` shape passed to `tryOk`.
|
|
173
|
+
|
|
174
|
+
### `/receiver` subpath
|
|
175
|
+
|
|
176
|
+
- **`checkWebhook(headers, body, options)`** — framework-agnostic core. Composes `verifyWebhook` (when `options.secret` is set) + `extractIdempotencyKey` + `options.store.claim`. Returns `{ ok: false; status: 400|401; reason }` on failure or `{ ok: true; key; deduped }` on success. The per-framework adapters wrap this and translate the outcome into the framework's idiomatic response.
|
|
177
|
+
- **`extractIdempotencyKey(headers)`** — case-insensitive `Idempotency-Key` header parser. Returns `undefined` when the header carries no usable key: missing, array-valued (ambiguous), or empty string. Validation beyond "is there a usable key?" (length, format) is intentionally out of scope.
|
|
178
|
+
- **`verifyWebhook(headers, body, secret, opts?)`** — HMAC-SHA256 signature + timestamp window verifier. Returns `{ ok: true }` or `{ ok: false; reason }` where reason is one of `missing-signature` / `missing-timestamp` / `stale` / `future` / `bad-signature`. Default timestamp window is ±300 seconds; override via `opts.maxAgeSeconds`. Uses `crypto.timingSafeEqual` to avoid timing attacks. Pair with `webhook({ secret })` on the sender side.
|
|
179
|
+
- **Types**: `CheckResult`, `CheckWebhookOptions`, `CheckFailureReason`, `VerifyResult`, `VerifyOptions`.
|
|
180
|
+
|
|
181
|
+
### `/receiver/<framework>` subpaths
|
|
182
|
+
|
|
183
|
+
Each framework adapter exports a single function `webhookMiddleware(options)` that returns the framework's native middleware shape. Options are `{ store, secret?, verify? }` — the same `CheckWebhookOptions` as the core. Failure → 400/401 with `{ error: <reason> }`; success → `{ key, deduped }` is injected:
|
|
184
|
+
|
|
185
|
+
| Subpath | Injection site | Failure response |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| `/receiver/trpc` | `ctx.idempotency` | throws `TRPCError({ code, message: reason })` |
|
|
188
|
+
| `/receiver/express` | `req.idempotency` | `res.status(...).json({ error: reason })` |
|
|
189
|
+
| `/receiver/fastify` | `request.idempotency` | `reply.status(...).send({ error: reason })` |
|
|
190
|
+
| `/receiver/hono` | `c.get("idempotency")` (typed via `Variables`) | `c.json({ error: reason }, status)` |
|
|
191
|
+
|
|
192
|
+
### `/api` subpath
|
|
193
|
+
|
|
194
|
+
Shared utilities consumed by every transport in the auto-generated API umbrella (act-http-api epic #835). Three concerns surfaced once, not per-transport:
|
|
195
|
+
|
|
196
|
+
- **`ActorExtractor`** — type alias `(request: unknown) => Actor | Promise<Actor>`. The host-supplied closure resolving an `Actor` from an incoming request. Required on every transport (`trpc(app, { actor })`, `hono(app, { actor })`). Auth (JWT, session, API key) stays in the host; the package only asks for this function.
|
|
197
|
+
- **`ApiError`** — uniform envelope `{ error, detail?, code? }` shipped over the wire by every transport. Hosts get the same shape from REST, tRPC, and OpenAPI.
|
|
198
|
+
- **`ERROR_MAP`** — `as const` table mapping framework error types to `{ status, code }`. `ValidationError → 422 / VALIDATION`, `InvariantError → 409 / INVARIANT`, `ConcurrencyError → 412 / CONCURRENCY`, `StreamClosedError → 410 / STREAM_CLOSED`, `NonRetryableError → 400 / NON_RETRYABLE`.
|
|
199
|
+
- **`toApiError(err) → { status, body }`** — the single mapping helper every transport calls in its error boundary. Known framework errors map per `ERROR_MAP`; everything else surfaces as 500 / `INTERNAL` (with `detail` only when the throw was an `Error` — thrown strings or objects don't leak payloads).
|
|
200
|
+
- **`withIdempotency(store, key, handler)`** — wraps an action handler in an `Idempotency-Key` claim. Reuses `@rotorsoft/act-ops/idempotency` — same contract `@rotorsoft/act-http/receiver` already speaks, so one `IdempotencyStore` covers both halves of the "Act over the wire" surface. Returns `{ deduped: false, result }` on fresh claim, `{ deduped: true }` on duplicate (handler is not called).
|
|
201
|
+
- **Types**: `IdempotencyResult<T>`, `ErrorMapEntry`.
|
|
81
202
|
|
|
82
203
|
### `/sse` subpath
|
|
83
204
|
|
|
@@ -100,12 +221,15 @@ onData: (msg) => {
|
|
|
100
221
|
| `body` | `unknown` or `(event) => unknown` | the committed event (JSON-serialized) |
|
|
101
222
|
| `timeoutMs` | `number` | `5000` |
|
|
102
223
|
| `idempotencyKey` | `(event) => string | null` | `String(event.id)` |
|
|
224
|
+
| `secret` | `string` | unset (unsigned) |
|
|
103
225
|
| `fetch` | `typeof fetch` | `globalThis.fetch` |
|
|
104
226
|
|
|
105
227
|
Strings as `body` are sent as-is; anything else is `JSON.stringify`'d and `Content-Type: application/json` is set automatically (unless the caller supplies it).
|
|
106
228
|
|
|
107
229
|
A caller-supplied `Idempotency-Key` header (case-insensitive) always wins; the auto-derived `event.id` is only applied when the header is absent. `event.id` is the framework's immutable, per-event monotonic integer — well-suited to downstream dedup.
|
|
108
230
|
|
|
231
|
+
When `secret` is set, the helper signs each request with HMAC-SHA256 over `${timestamp}.${body}` (the final serialized body) and attaches `X-Webhook-Signature: sha256=<hex>` + `X-Webhook-Timestamp: <unix-seconds>`. Caller-supplied versions of either header (case-insensitive) win, the same way the `Idempotency-Key` and `Content-Type` defaults yield to caller intent. Pair with `verifyWebhook` from `@rotorsoft/act-http/receiver` on the receiving side — the protocol matches Stripe / GitHub / Slack conventions modulo the `X-Webhook-*` prefix.
|
|
232
|
+
|
|
109
233
|
## Common patterns
|
|
110
234
|
|
|
111
235
|
### Retry & block semantics
|