@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.
Files changed (59) hide show
  1. package/README.md +115 -3
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/@types/receiver/check.d.ts +66 -0
  4. package/dist/@types/receiver/check.d.ts.map +1 -0
  5. package/dist/@types/receiver/express/index.d.ts +51 -0
  6. package/dist/@types/receiver/express/index.d.ts.map +1 -0
  7. package/dist/@types/receiver/extract.d.ts +24 -0
  8. package/dist/@types/receiver/extract.d.ts.map +1 -0
  9. package/dist/@types/receiver/fastify/index.d.ts +55 -0
  10. package/dist/@types/receiver/fastify/index.d.ts.map +1 -0
  11. package/dist/@types/receiver/hono/index.d.ts +60 -0
  12. package/dist/@types/receiver/hono/index.d.ts.map +1 -0
  13. package/dist/@types/receiver/index.d.ts +39 -0
  14. package/dist/@types/receiver/index.d.ts.map +1 -0
  15. package/dist/@types/receiver/start.d.ts +48 -0
  16. package/dist/@types/receiver/start.d.ts.map +1 -0
  17. package/dist/@types/receiver/trpc/index.d.ts +16 -0
  18. package/dist/@types/receiver/trpc/index.d.ts.map +1 -0
  19. package/dist/@types/receiver/verify.d.ts +57 -0
  20. package/dist/@types/receiver/verify.d.ts.map +1 -0
  21. package/dist/@types/webhook/classify.d.ts +59 -0
  22. package/dist/@types/webhook/classify.d.ts.map +1 -0
  23. package/dist/@types/webhook/index.d.ts +3 -2
  24. package/dist/@types/webhook/index.d.ts.map +1 -1
  25. package/dist/@types/webhook/sign.d.ts +25 -0
  26. package/dist/@types/webhook/sign.d.ts.map +1 -0
  27. package/dist/@types/webhook/types.d.ts +61 -20
  28. package/dist/@types/webhook/types.d.ts.map +1 -1
  29. package/dist/chunk-F7VWYZ37.js +29 -0
  30. package/dist/chunk-F7VWYZ37.js.map +1 -0
  31. package/dist/chunk-NOIXOF2I.js +78 -0
  32. package/dist/chunk-NOIXOF2I.js.map +1 -0
  33. package/dist/dist-NWMJQI4E.js +647 -0
  34. package/dist/dist-NWMJQI4E.js.map +1 -0
  35. package/dist/receiver/express/index.cjs +128 -0
  36. package/dist/receiver/express/index.cjs.map +1 -0
  37. package/dist/receiver/express/index.js +33 -0
  38. package/dist/receiver/express/index.js.map +1 -0
  39. package/dist/receiver/fastify/index.cjs +120 -0
  40. package/dist/receiver/fastify/index.cjs.map +1 -0
  41. package/dist/receiver/fastify/index.js +25 -0
  42. package/dist/receiver/fastify/index.js.map +1 -0
  43. package/dist/receiver/hono/index.cjs +123 -0
  44. package/dist/receiver/hono/index.cjs.map +1 -0
  45. package/dist/receiver/hono/index.js +8 -0
  46. package/dist/receiver/hono/index.js.map +1 -0
  47. package/dist/receiver/index.cjs +2943 -0
  48. package/dist/receiver/index.cjs.map +1 -0
  49. package/dist/receiver/index.js +2162 -0
  50. package/dist/receiver/index.js.map +1 -0
  51. package/dist/receiver/trpc/index.cjs +126 -0
  52. package/dist/receiver/trpc/index.cjs.map +1 -0
  53. package/dist/receiver/trpc/index.js +31 -0
  54. package/dist/receiver/trpc/index.js.map +1 -0
  55. package/dist/webhook/index.cjs +66 -6
  56. package/dist/webhook/index.cjs.map +1 -1
  57. package/dist/webhook/index.js +62 -6
  58. package/dist/webhook/index.js.map +1 -1
  59. 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
- Two independent subpath exports:
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
- - **`WebhookError`** — thrown on 5xx, network errors, and timeouts. Carries `status` (`0` for network/timeout) and `url`. Retryable by drain.
79
- - **`NonRetryableWebhookError`** — thrown on 4xx. Extends `NonRetryableError` from `@rotorsoft/act`; the drain finalizer blocks the stream on first attempt without consuming the retry budget.
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