@rotorsoft/act-http 1.0.0 → 1.1.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 +115 -3
- package/dist/.tsbuildinfo +1 -1
- 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/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 +47 -3
package/README.md
CHANGED
|
@@ -20,12 +20,17 @@ 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. |
|
|
29
34
|
|
|
30
35
|
## Quick start
|
|
31
36
|
|
|
@@ -50,6 +55,85 @@ import { webhook } from "@rotorsoft/act-http/webhook";
|
|
|
50
55
|
.to(resolver)
|
|
51
56
|
```
|
|
52
57
|
|
|
58
|
+
### `receiver` — high-level builder (the canonical path)
|
|
59
|
+
|
|
60
|
+
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:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { receiver } from "@rotorsoft/act-http/receiver";
|
|
64
|
+
import { InMemoryIdempotencyStore } from "@rotorsoft/act-ops/idempotency";
|
|
65
|
+
import { z } from "zod";
|
|
66
|
+
|
|
67
|
+
const escalations = receiver({
|
|
68
|
+
port: 4001,
|
|
69
|
+
store: new InMemoryIdempotencyStore(),
|
|
70
|
+
secret: process.env.WEBHOOK_SECRET,
|
|
71
|
+
})
|
|
72
|
+
.on("OrderConfirmed", z.object({ orderId: z.string(), total: z.number() }),
|
|
73
|
+
async (event, ctx) => { await processOrder(event.orderId, event.total); })
|
|
74
|
+
.build();
|
|
75
|
+
|
|
76
|
+
await escalations.listen(); // Node
|
|
77
|
+
// export default { fetch: escalations.fetch }; // Cloudflare / Vercel / Bun / Deno
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Naming convention: type `Receiver` (PascalCase), factory `receiver` (lowercase) — matches Act's existing `act` / `state` / `slice` / `projection` builder analogs.
|
|
81
|
+
|
|
82
|
+
### `receiver/<framework>` — low-level middleware
|
|
83
|
+
|
|
84
|
+
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:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
// tRPC
|
|
88
|
+
import { webhookMiddleware } from "@rotorsoft/act-http/receiver/trpc";
|
|
89
|
+
import { InMemoryIdempotencyStore } from "@rotorsoft/act-ops/idempotency";
|
|
90
|
+
|
|
91
|
+
const dedup = new InMemoryIdempotencyStore();
|
|
92
|
+
const idempotent = t.procedure.use(
|
|
93
|
+
webhookMiddleware({ store: dedup, secret: process.env.WEBHOOK_SECRET })
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const router = t.router({
|
|
97
|
+
webhook: idempotent.input(Schema).mutation(({ input, ctx }) => {
|
|
98
|
+
const { key, deduped } = ctx.idempotency;
|
|
99
|
+
if (deduped) return { status: "dedup-skipped", key };
|
|
100
|
+
return { status: "processed", key };
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Each adapter follows the same shape:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { webhookMiddleware } from "@rotorsoft/act-http/receiver/express";
|
|
109
|
+
app.post("/webhook", webhookMiddleware({ store, secret }), (req, res) => {
|
|
110
|
+
const { key, deduped } = (req as any).idempotency;
|
|
111
|
+
// …
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { webhookMiddleware } from "@rotorsoft/act-http/receiver/fastify";
|
|
117
|
+
app.post("/webhook", { preHandler: webhookMiddleware({ store, secret }) }, async (req) => {
|
|
118
|
+
const { key, deduped } = (req as any).idempotency;
|
|
119
|
+
// …
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { webhookMiddleware } from "@rotorsoft/act-http/receiver/hono";
|
|
125
|
+
app.post("/webhook", webhookMiddleware({ store, secret }), (c) => {
|
|
126
|
+
const { key, deduped } = c.get("idempotency");
|
|
127
|
+
// …
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
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.
|
|
132
|
+
|
|
133
|
+
### `receiver` primitives — when neither builder nor middleware fits
|
|
134
|
+
|
|
135
|
+
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.
|
|
136
|
+
|
|
53
137
|
### `sse` — live state broadcast
|
|
54
138
|
|
|
55
139
|
```ts
|
|
@@ -75,9 +159,34 @@ onData: (msg) => {
|
|
|
75
159
|
### `/webhook` subpath
|
|
76
160
|
|
|
77
161
|
- **`webhook(config)`** — reaction-handler factory. Returns a function compatible with `.do(handler, opts)`.
|
|
78
|
-
- **`
|
|
79
|
-
- **`
|
|
162
|
+
- **`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.
|
|
163
|
+
- **`classifyHttpResponse(response)`** — the underlying `"ok" | "retry" | "block"` classifier. Reach for it directly when you need custom error classes; otherwise `tryOk` wraps it.
|
|
164
|
+
- **`RetryableHttpError`** — generic retryable delivery error. Extends `Error`. Thrown by `tryOk` on 5xx. `WebhookError` extends it.
|
|
165
|
+
- **`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.
|
|
166
|
+
- **`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.
|
|
167
|
+
- **`NonRetryableWebhookError`** — webhook-specific subclass of `NonRetryableHttpError`, thrown by `webhook` on 3xx/4xx. Same backward-compat story as `WebhookError`.
|
|
80
168
|
- **`WebhookConfig`** — TypeScript type for the helper options.
|
|
169
|
+
- **`HttpDisposition`** — the `"ok" | "retry" | "block"` discriminator returned by `classifyHttpResponse`.
|
|
170
|
+
- **`HttpDeliveryErrorInit`** — common `{ status, url, responseBody? }` shape passed to every HTTP error class.
|
|
171
|
+
- **`TryOkOptions`** — `{ url, label? }` shape passed to `tryOk`.
|
|
172
|
+
|
|
173
|
+
### `/receiver` subpath
|
|
174
|
+
|
|
175
|
+
- **`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.
|
|
176
|
+
- **`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.
|
|
177
|
+
- **`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.
|
|
178
|
+
- **Types**: `CheckResult`, `CheckWebhookOptions`, `CheckFailureReason`, `VerifyResult`, `VerifyOptions`.
|
|
179
|
+
|
|
180
|
+
### `/receiver/<framework>` subpaths
|
|
181
|
+
|
|
182
|
+
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:
|
|
183
|
+
|
|
184
|
+
| Subpath | Injection site | Failure response |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| `/receiver/trpc` | `ctx.idempotency` | throws `TRPCError({ code, message: reason })` |
|
|
187
|
+
| `/receiver/express` | `req.idempotency` | `res.status(...).json({ error: reason })` |
|
|
188
|
+
| `/receiver/fastify` | `request.idempotency` | `reply.status(...).send({ error: reason })` |
|
|
189
|
+
| `/receiver/hono` | `c.get("idempotency")` (typed via `Variables`) | `c.json({ error: reason }, status)` |
|
|
81
190
|
|
|
82
191
|
### `/sse` subpath
|
|
83
192
|
|
|
@@ -100,12 +209,15 @@ onData: (msg) => {
|
|
|
100
209
|
| `body` | `unknown` or `(event) => unknown` | the committed event (JSON-serialized) |
|
|
101
210
|
| `timeoutMs` | `number` | `5000` |
|
|
102
211
|
| `idempotencyKey` | `(event) => string | null` | `String(event.id)` |
|
|
212
|
+
| `secret` | `string` | unset (unsigned) |
|
|
103
213
|
| `fetch` | `typeof fetch` | `globalThis.fetch` |
|
|
104
214
|
|
|
105
215
|
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
216
|
|
|
107
217
|
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
218
|
|
|
219
|
+
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.
|
|
220
|
+
|
|
109
221
|
## Common patterns
|
|
110
222
|
|
|
111
223
|
### Retry & block semantics
|