@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
@@ -0,0 +1,66 @@
1
+ import type { IdempotencyStore } from "@rotorsoft/act-ops/idempotency";
2
+ import { type VerifyOptions } from "./verify.js";
3
+ /**
4
+ * Failure reasons returned by {@link checkWebhook}. The shape splits
5
+ * `missing-key` (a client error, mapped to HTTP 400) from the five
6
+ * verification failures (authentication errors, HTTP 401) so each
7
+ * maps to its own telemetry bucket.
8
+ */
9
+ export type CheckFailureReason = "missing-key" | "missing-signature" | "missing-timestamp" | "stale" | "future" | "bad-signature";
10
+ /**
11
+ * Outcome of {@link checkWebhook}. Either the request passed every
12
+ * configured check and carries a usable idempotency key, or it
13
+ * failed one of them and the framework adapter should reply with the
14
+ * corresponding HTTP status.
15
+ */
16
+ export type CheckResult = {
17
+ ok: false;
18
+ status: 400 | 401;
19
+ reason: CheckFailureReason;
20
+ } | {
21
+ ok: true;
22
+ key: string;
23
+ deduped: boolean;
24
+ };
25
+ /** Options for {@link checkWebhook}. */
26
+ export type CheckWebhookOptions = {
27
+ /** Idempotency store the framework-agnostic core claims the key on. */
28
+ store: IdempotencyStore;
29
+ /**
30
+ * Optional HMAC-SHA256 secret. When set, the request's
31
+ * `X-Webhook-Signature` and `X-Webhook-Timestamp` headers are
32
+ * verified before the dedup claim. When omitted, signature
33
+ * verification is skipped (unsigned receivers).
34
+ */
35
+ secret?: string;
36
+ /**
37
+ * Verification options forwarded to {@link verifyWebhook}. Only
38
+ * meaningful when `secret` is set. Defaults to a ±300-second
39
+ * timestamp window.
40
+ */
41
+ verify?: VerifyOptions;
42
+ };
43
+ /**
44
+ * Framework-agnostic receiver check: verify the signature (when a
45
+ * secret is configured), extract the `Idempotency-Key`, and claim
46
+ * it on the store. Returns the request's fate as a discriminated
47
+ * union the per-framework adapter translates into the framework's
48
+ * idiomatic 4xx response or context injection.
49
+ *
50
+ * **Order of checks** (matters):
51
+ *
52
+ * 1. Verify signature + timestamp window (when `secret` is set).
53
+ * Rejecting bad signatures *before* extracting and claiming the
54
+ * key keeps attacker-supplied keys out of the dedup store —
55
+ * otherwise a flood of spoofed POSTs would pollute the LRU.
56
+ * 2. Extract the `Idempotency-Key`. Missing → reject with 400.
57
+ * 3. Claim the key on the store. If already seen, return
58
+ * `{ ok: true; deduped: true }` so the framework adapter can
59
+ * short-circuit the handler without re-running side effects.
60
+ *
61
+ * The dedup store may be sync (`InMemoryIdempotencyStore`) or async
62
+ * (durable adapters like a future `PostgresIdempotencyStore`); the
63
+ * core awaits unconditionally so both shapes compose cleanly.
64
+ */
65
+ export declare function checkWebhook(headers: Record<string, string | string[] | undefined>, body: string, options: CheckWebhookOptions): Promise<CheckResult>;
66
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../src/receiver/check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAEvE,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,aAAa,CAAC;AAEhE;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAC1B,aAAa,GACb,mBAAmB,GACnB,mBAAmB,GACnB,OAAO,GACP,QAAQ,GACR,eAAe,CAAC;AAEpB;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAC5D;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAEhD,wCAAwC;AACxC,MAAM,MAAM,mBAAmB,GAAG;IAChC,uEAAuE;IACvE,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EACtD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,WAAW,CAAC,CAkBtB"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * @module act-http/receiver/express
4
+ *
5
+ * Express adapter for the receiver-side webhook check.
6
+ *
7
+ * Usage:
8
+ *
9
+ * ```ts
10
+ * import express from "express";
11
+ * import { webhookMiddleware } from "@rotorsoft/act-http/receiver/express";
12
+ * import { InMemoryIdempotencyStore } from "@rotorsoft/act-ops/idempotency";
13
+ *
14
+ * const app = express();
15
+ * const dedup = new InMemoryIdempotencyStore();
16
+ *
17
+ * // Raw body capture required when signing is enabled.
18
+ * app.use(express.raw({ type: "application/json" }));
19
+ *
20
+ * app.post(
21
+ * "/webhooks/orders",
22
+ * webhookMiddleware({ store: dedup, secret: process.env.WEBHOOK_SECRET }),
23
+ * (req, res) => {
24
+ * const { key, deduped } = (req as any).idempotency;
25
+ * if (deduped) return res.json({ status: "dedup-skipped", key });
26
+ * // ... process the inbound event ...
27
+ * res.json({ status: "processed", key });
28
+ * }
29
+ * );
30
+ * ```
31
+ *
32
+ * On failure: responds with the framework-idiomatic JSON shape
33
+ * `{ error: <reason> }` at status 400 (missing-key) or 401
34
+ * (verification failures), and does not call `next()`. On success:
35
+ * attaches `req.idempotency = { key, deduped }` and calls `next()`.
36
+ *
37
+ * **Raw body requirement**: when `secret` is configured, mount
38
+ * `express.raw({ type: "application/json" })` (or whatever
39
+ * content-type your webhooks use) ahead of the receiver middleware.
40
+ * The middleware reads `req.body` as a `Buffer | string` and converts
41
+ * to a UTF-8 string for hashing. Skip when unsigned.
42
+ */
43
+ import type { RequestHandler } from "express";
44
+ import { type CheckWebhookOptions } from "../check.js";
45
+ /**
46
+ * Build an Express middleware that verifies the request signature
47
+ * (when `secret` is set), enforces `Idempotency-Key`, and claims the
48
+ * key on the configured store. See the module-level docs for usage.
49
+ */
50
+ export declare function webhookMiddleware(options: CheckWebhookOptions): RequestHandler;
51
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/receiver/express/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,OAAO,KAAK,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAC/E,OAAO,EAAE,KAAK,mBAAmB,EAAgB,MAAM,aAAa,CAAC;AAErE;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,mBAAmB,GAC3B,cAAc,CAwBhB"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Pull the `Idempotency-Key` header from a Node-style headers bag,
3
+ * case-insensitive. Returns `undefined` when any of the following
4
+ * carries no usable key:
5
+ *
6
+ * - the header is missing
7
+ * - its value is an array (ambiguous — can't pick one without a
8
+ * policy the receiver hasn't declared)
9
+ * - its value is the empty string (carries no idempotency
10
+ * information; structurally equivalent to "no header at all")
11
+ *
12
+ * Pair with `IdempotencyStore.claim` from
13
+ * `@rotorsoft/act-ops/idempotency`: extract the key from the inbound
14
+ * request, claim it on the store, return a `deduped` marker when the
15
+ * claim fails. The framework-agnostic middleware that wires these
16
+ * together lands in #744.
17
+ *
18
+ * Validation beyond "is there a usable key?" (length bounds, format
19
+ * checks, normalization) is intentionally out of scope. Receivers
20
+ * picking a policy can layer it on top — or, when #744 ships, opt
21
+ * into the middleware's opinionated defaults.
22
+ */
23
+ export declare function extractIdempotencyKey(headers: Record<string, string | string[] | undefined>): string | undefined;
24
+ //# sourceMappingURL=extract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../../src/receiver/extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GACrD,MAAM,GAAG,SAAS,CAQpB"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * @module act-http/receiver/fastify
4
+ *
5
+ * Fastify adapter for the receiver-side webhook check.
6
+ *
7
+ * Usage:
8
+ *
9
+ * ```ts
10
+ * import Fastify from "fastify";
11
+ * import { webhookMiddleware } from "@rotorsoft/act-http/receiver/fastify";
12
+ * import { InMemoryIdempotencyStore } from "@rotorsoft/act-ops/idempotency";
13
+ *
14
+ * const app = Fastify();
15
+ * const dedup = new InMemoryIdempotencyStore();
16
+ *
17
+ * app.post(
18
+ * "/webhooks/orders",
19
+ * {
20
+ * preHandler: webhookMiddleware({
21
+ * store: dedup,
22
+ * secret: process.env.WEBHOOK_SECRET,
23
+ * }),
24
+ * },
25
+ * async (request, reply) => {
26
+ * const { key, deduped } = (request as any).idempotency;
27
+ * if (deduped) return { status: "dedup-skipped", key };
28
+ * // ... process the inbound event ...
29
+ * return { status: "processed", key };
30
+ * }
31
+ * );
32
+ * ```
33
+ *
34
+ * On failure: replies with `{ error: <reason> }` at status 400
35
+ * (missing-key) or 401 (verification failures). On success: attaches
36
+ * `request.idempotency = { key, deduped }` and lets the route handler
37
+ * run.
38
+ *
39
+ * **Raw body requirement**: when `secret` is configured, register a
40
+ * content-type parser that preserves the raw body string. Fastify's
41
+ * default JSON parser eats the bytes — register a custom parser via
42
+ * `app.addContentTypeParser("application/json", { parseAs: "string" }, …)`
43
+ * and stash the string on `request.rawBody` (Fastify pattern). The
44
+ * middleware reads `request.rawBody` for hashing. Skip when unsigned.
45
+ */
46
+ import type { FastifyReply, FastifyRequest } from "fastify";
47
+ import { type CheckWebhookOptions } from "../check.js";
48
+ /**
49
+ * Build a Fastify `preHandler` hook that verifies the request
50
+ * signature (when `secret` is set), enforces `Idempotency-Key`, and
51
+ * claims the key on the configured store. See the module-level docs
52
+ * for usage.
53
+ */
54
+ export declare function webhookMiddleware(options: CheckWebhookOptions): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
55
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/receiver/fastify/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,KAAK,mBAAmB,EAAgB,MAAM,aAAa,CAAC;AAOrE;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,mBAAmB,GAC3B,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAkBjE"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * @module act-http/receiver/hono
4
+ *
5
+ * Hono adapter for the receiver-side webhook check.
6
+ *
7
+ * Usage:
8
+ *
9
+ * ```ts
10
+ * import { Hono } from "hono";
11
+ * import { webhookMiddleware } from "@rotorsoft/act-http/receiver/hono";
12
+ * import { InMemoryIdempotencyStore } from "@rotorsoft/act-ops/idempotency";
13
+ *
14
+ * const app = new Hono();
15
+ * const dedup = new InMemoryIdempotencyStore();
16
+ *
17
+ * app.post(
18
+ * "/webhooks/orders",
19
+ * webhookMiddleware({ store: dedup, secret: process.env.WEBHOOK_SECRET }),
20
+ * async (c) => {
21
+ * const idem = c.get("idempotency") as { key: string; deduped: boolean };
22
+ * if (idem.deduped) return c.json({ status: "dedup-skipped", key: idem.key });
23
+ * // ... process the inbound event ...
24
+ * return c.json({ status: "processed", key: idem.key });
25
+ * }
26
+ * );
27
+ * ```
28
+ *
29
+ * On failure: returns `c.json({ error: <reason> }, status)` directly
30
+ * (Hono short-circuits when middleware returns a Response). On
31
+ * success: stashes `c.set("idempotency", { key, deduped })` and
32
+ * continues with `await next()`.
33
+ *
34
+ * **Raw body**: Hono exposes `await c.req.text()` natively, which
35
+ * the middleware reads when `secret` is configured. No extra setup
36
+ * needed.
37
+ */
38
+ import type { MiddlewareHandler } from "hono";
39
+ import { type CheckWebhookOptions } from "../check.js";
40
+ /**
41
+ * Variables this middleware contributes to the Hono context. The
42
+ * generic on the returned {@link MiddlewareHandler} threads it
43
+ * through so route handlers downstream of `app.post(..., webhookMiddleware(...), handler)`
44
+ * see `c.get("idempotency")` typed without a manual cast.
45
+ */
46
+ export type WebhookVariables = {
47
+ idempotency: {
48
+ key: string;
49
+ deduped: boolean;
50
+ };
51
+ };
52
+ /**
53
+ * Build a Hono middleware that verifies the request signature (when
54
+ * `secret` is set), enforces `Idempotency-Key`, and claims the key
55
+ * on the configured store. See the module-level docs for usage.
56
+ */
57
+ export declare function webhookMiddleware(options: CheckWebhookOptions): MiddlewareHandler<{
58
+ Variables: WebhookVariables;
59
+ }>;
60
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/receiver/hono/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,EAAE,KAAK,mBAAmB,EAAgB,MAAM,aAAa,CAAC;AAErE;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CAChD,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,mBAAmB,GAC3B,iBAAiB,CAAC;IAAE,SAAS,EAAE,gBAAgB,CAAA;CAAE,CAAC,CAWpD"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * @module act-http/receiver
4
+ *
5
+ * Server-side helpers for the inbound HTTP role — the receiver that
6
+ * sits on the other end of an `@rotorsoft/act-http/webhook` POST.
7
+ *
8
+ * The subpath hosts two primitives today:
9
+ *
10
+ * - {@link extractIdempotencyKey} — case-insensitive
11
+ * `Idempotency-Key` parser; pair with `IdempotencyStore.claim`
12
+ * from `@rotorsoft/act-ops/idempotency` for dedup.
13
+ * - {@link verifyWebhook} — HMAC-SHA256 signature + timestamp
14
+ * verifier; pair with `webhook({ secret })` from
15
+ * `@rotorsoft/act-http/webhook` for authenticated, replay-resistant
16
+ * delivery.
17
+ *
18
+ * The framework-agnostic middleware that wires these into request
19
+ * handlers, plus per-framework adapters (tRPC / Express / Fastify /
20
+ * Hono), lands in #744 (ACT-1116).
21
+ *
22
+ * Sibling subpaths in the same package:
23
+ *
24
+ * - `@rotorsoft/act-http/webhook` — the sender side: outbound POSTs,
25
+ * automatic `Idempotency-Key`, status-classified retries, optional
26
+ * HMAC signing.
27
+ * - `@rotorsoft/act-http/sse` — incremental state broadcast over
28
+ * Server-Sent Events.
29
+ *
30
+ * The receiver subpath ships from the same package as `/webhook` so
31
+ * a service that both sends and receives webhooks installs one
32
+ * dependency. The dedup contract that links them lives in
33
+ * `@rotorsoft/act-ops/idempotency`.
34
+ */
35
+ export { type CheckFailureReason, type CheckResult, type CheckWebhookOptions, checkWebhook, } from "./check.js";
36
+ export { extractIdempotencyKey } from "./extract.js";
37
+ export { receiver } from "./start.js";
38
+ export { type VerifyOptions, type VerifyResult, verifyWebhook, } from "./verify.js";
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/receiver/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,aAAa,GACd,MAAM,aAAa,CAAC"}
@@ -0,0 +1,48 @@
1
+ import type { ReceiverBuilder, ReceiverOptions } from "@rotorsoft/act-ops/receiver";
2
+ /**
3
+ * Recommended factory for "I want to receive webhooks." Returns a
4
+ * {@link ReceiverBuilder} the operator configures fluently:
5
+ *
6
+ * ```ts
7
+ * import { receiver } from "@rotorsoft/act-http/receiver";
8
+ * import { InMemoryIdempotencyStore } from "@rotorsoft/act-ops/idempotency";
9
+ * import { z } from "zod";
10
+ *
11
+ * const r = receiver({
12
+ * port: 4001,
13
+ * store: new InMemoryIdempotencyStore(),
14
+ * secret: process.env.WEBHOOK_SECRET,
15
+ * })
16
+ * .on("OrderConfirmed", z.object({
17
+ * orderId: z.string(),
18
+ * total: z.number(),
19
+ * }), async (event, ctx) => {
20
+ * // event.orderId and event.total are typed
21
+ * // ctx.key is the deduplicated Idempotency-Key
22
+ * await processOrder(event.orderId, event.total);
23
+ * })
24
+ * .build();
25
+ *
26
+ * await r.listen();
27
+ * ```
28
+ *
29
+ * Matches Act's builder pattern: `receiver(...)` is the factory,
30
+ * `.on()` registers handlers fluently, `.build()` finalizes and
31
+ * produces an immutable {@link Receiver} — at which point the type
32
+ * loses `.on()` and gains the runtime methods (`listen` / `close` /
33
+ * `fetch`). The lifecycle phases are split at the type level.
34
+ *
35
+ * Internally uses Hono for routing — the universal-runtime choice
36
+ * that gives one code path coverage across Node, AWS Lambda,
37
+ * Cloudflare Workers, Vercel Edge, Bun, and Deno. For operators
38
+ * with an existing tRPC / Express / Fastify / Hono app who need to
39
+ * compose the receiver with their own middleware stack, the
40
+ * lower-level `webhookMiddleware` from
41
+ * `@rotorsoft/act-http/receiver/<framework>` is the escape hatch.
42
+ *
43
+ * `@hono/node-server` is imported lazily inside `.listen()` so
44
+ * Lambda / edge consumers (who never call `.listen()`) don't need
45
+ * it installed.
46
+ */
47
+ export declare function receiver(options: ReceiverOptions): ReceiverBuilder;
48
+ //# sourceMappingURL=start.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../../src/receiver/start.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,eAAe,EAEf,eAAe,EAEhB,MAAM,6BAA6B,CAAC;AAIrC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,eAAe,CA2FlE"}
@@ -0,0 +1,16 @@
1
+ import { type CheckWebhookOptions } from "../check.js";
2
+ /**
3
+ * Build a tRPC middleware that verifies the request signature (when
4
+ * `secret` is set), enforces `Idempotency-Key`, and claims the key on
5
+ * the configured store. See the module-level docs for usage.
6
+ *
7
+ * The returned function uses permissive `any` typing because tRPC's
8
+ * `MiddlewareFunction` type lives in `unstable-core-do-not-import`
9
+ * (internal namespace, not for external import). Type-safety at the
10
+ * call site comes from `t.procedure.use(...)` validating the
11
+ * middleware shape against the procedure's context — the operator's
12
+ * tRPC context must include `headers` and `rawBody`, and downstream
13
+ * handlers see `ctx.idempotency = { key, deduped }`.
14
+ */
15
+ export declare function webhookMiddleware(options: CheckWebhookOptions): any;
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/receiver/trpc/index.ts"],"names":[],"mappings":"AAyCA,OAAO,EAAE,KAAK,mBAAmB,EAAgB,MAAM,aAAa,CAAC;AAErE;;;;;;;;;;;;GAYG;AAEH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,mBAAmB,GAAG,GAAG,CA2BnE"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Outcome of {@link verifyWebhook}. Either the request signature
3
+ * checks out, or one of five distinct failure reasons applies. Each
4
+ * reason maps to an operator-meaningful telemetry bucket — separated
5
+ * deliberately so dashboards can distinguish "client lost its secret"
6
+ * from "client clock is wrong" from "this is a replay attack."
7
+ */
8
+ export type VerifyResult = {
9
+ ok: true;
10
+ } | {
11
+ ok: false;
12
+ reason: "missing-signature" | "missing-timestamp" | "stale" | "future" | "bad-signature";
13
+ };
14
+ /** Options for {@link verifyWebhook}. */
15
+ export type VerifyOptions = {
16
+ /**
17
+ * Maximum acceptable timestamp drift in either direction, in
18
+ * seconds. Default: 300 (±5 minutes) — matches Stripe / GitHub /
19
+ * Slack conventions. Tightening narrows the replay window;
20
+ * loosening accommodates clients with worse clock sync.
21
+ */
22
+ maxAgeSeconds?: number;
23
+ /**
24
+ * Current Unix-seconds time. Exposed for tests; production
25
+ * callers should leave it undefined so wall-clock is used.
26
+ */
27
+ now?: number;
28
+ };
29
+ /**
30
+ * Verify an inbound webhook's signature and timestamp against the
31
+ * shared secret. Pair with the sender side: configure
32
+ * `webhook({ secret })` from `@rotorsoft/act-http/webhook`.
33
+ *
34
+ * Returns `{ ok: true }` on success or `{ ok: false; reason }` on
35
+ * failure. The reasons are:
36
+ *
37
+ * - `missing-signature` — no `X-Webhook-Signature` header, value
38
+ * was an array, or value was empty.
39
+ * - `missing-timestamp` — no `X-Webhook-Timestamp` header, value
40
+ * was empty, or value isn't a parseable integer.
41
+ * - `stale` — timestamp older than `maxAgeSeconds` from `now`.
42
+ * - `future` — timestamp more than `maxAgeSeconds` ahead of `now`.
43
+ * - `bad-signature` — signature header didn't start with `sha256=`,
44
+ * wasn't 64 hex chars, or the recomputed HMAC didn't match
45
+ * (constant-time compare).
46
+ *
47
+ * The signed payload is `${timestamp}.${body}`, so `body` must be
48
+ * the **raw request body bytes**. Any pre-parse normalization
49
+ * (whitespace trimming, JSON re-stringification) would change the
50
+ * hash and reject every otherwise-valid request. Framework adapters
51
+ * in #744 will provide the raw body alongside the parsed one.
52
+ *
53
+ * Uses Node's `crypto.timingSafeEqual` for the final comparison to
54
+ * avoid signature-equality timing attacks.
55
+ */
56
+ export declare function verifyWebhook(headers: Record<string, string | string[] | undefined>, body: string, secret: string, options?: VerifyOptions): VerifyResult;
57
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/receiver/verify.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EACF,mBAAmB,GACnB,mBAAmB,GACnB,OAAO,GACP,QAAQ,GACR,eAAe,CAAC;CACrB,CAAC;AAEN,yCAAyC;AACzC,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EACtD,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,aAAa,GACtB,YAAY,CAqCd"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Three buckets for an HTTP response from an outbound delivery:
3
+ *
4
+ * - `ok` — the receiver accepted the delivery (2xx). Stop and return.
5
+ * - `retry` — the receiver had a transient problem (5xx). Throw a
6
+ * retryable error; drain will pace the next attempt per `backoff`.
7
+ * - `block` — the receiver rejected the delivery permanently (3xx
8
+ * or 4xx). Throw a non-retryable error; drain blocks the stream
9
+ * on the first failed attempt (when `blockOnError` is true) and
10
+ * surfaces it via the `"blocked"` lifecycle event.
11
+ *
12
+ * The 3xx → `block` mapping is intentional: a redirect at the
13
+ * delivery layer means the configured URL is wrong, and retrying
14
+ * the same URL won't fix that. Manual operator review is the right
15
+ * next step, which is what the block path produces.
16
+ */
17
+ export type HttpDisposition = "ok" | "retry" | "block";
18
+ /**
19
+ * Classify an HTTP response as `ok` (2xx), `retry` (5xx), or
20
+ * `block` (3xx, 4xx). The classification {@link webhook} uses
21
+ * internally, lifted here so custom integrations (gRPC bridges,
22
+ * SDK-based reactions, etc.) can apply the same retry semantics
23
+ * without inventing a parallel rule.
24
+ */
25
+ export declare function classifyHttpResponse(response: Response): HttpDisposition;
26
+ /** Options for {@link tryOk}. */
27
+ export type TryOkOptions = {
28
+ /** The endpoint that received the request. Surfaced on the thrown error and in its message. */
29
+ url: string;
30
+ /**
31
+ * Label prefixed onto the error message — typically the
32
+ * integration's identity (`"webhook"`, `"mySdk"`, `"grpc"`).
33
+ * Default: `"request"`.
34
+ */
35
+ label?: string;
36
+ };
37
+ /**
38
+ * If `response` is 2xx, return. Otherwise, capture the response body
39
+ * (best-effort) and throw a {@link RetryableHttpError} (for 5xx) or
40
+ * {@link NonRetryableHttpError} (for 3xx/4xx). Collapses the
41
+ * classify-and-throw boilerplate every custom HTTP-like reaction
42
+ * would otherwise write into one line:
43
+ *
44
+ * ```ts
45
+ * .on("OrderConfirmed").do(async (event) => {
46
+ * const response = await mySdk.deliver(event);
47
+ * await tryOk(response, { url: mySdk.url, label: "mySdk" });
48
+ * // ...response was 2xx; continue with downstream work...
49
+ * });
50
+ * ```
51
+ *
52
+ * The {@link webhook} helper throws webhook-specific subclasses
53
+ * ({@link WebhookError} / {@link NonRetryableWebhookError}) for
54
+ * backward compatibility — both extend the generic classes thrown
55
+ * here, so `instanceof RetryableHttpError` matches both webhook and
56
+ * custom-integration errors uniformly.
57
+ */
58
+ export declare function tryOk(response: Response, options: TryOkOptions): Promise<void>;
59
+ //# sourceMappingURL=classify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../../src/webhook/classify.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC;AAEvD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,eAAe,CAIxE;AAED,iCAAiC;AACjC,MAAM,MAAM,YAAY,GAAG;IACzB,+FAA+F;IAC/F,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,KAAK,CACzB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CAmBf"}
@@ -26,8 +26,9 @@
26
26
  */
27
27
  import type { ReactionHandler, Schemas } from "@rotorsoft/act";
28
28
  import { type WebhookConfig } from "./types.js";
29
- export type { WebhookBody, WebhookConfig, WebhookResolver } from "./types.js";
30
- export { NonRetryableWebhookError, WebhookError } from "./types.js";
29
+ export { classifyHttpResponse, type HttpDisposition, type TryOkOptions, tryOk, } from "./classify.js";
30
+ export type { HttpDeliveryErrorInit, WebhookBody, WebhookConfig, WebhookResolver, } from "./types.js";
31
+ export { NonRetryableHttpError, NonRetryableWebhookError, RetryableHttpError, WebhookError, } from "./types.js";
31
32
  /**
32
33
  * Build a reaction handler that POSTs each event to an external URL.
33
34
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/webhook/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAa,eAAe,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC1E,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC9E,OAAO,EAAE,wBAAwB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAsBpE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,OAAO,SAAS,OAAO,GAAG,OAAO,EACvD,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,GAC7B,eAAe,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,CAsEzC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/webhook/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAa,eAAe,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAG1E,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,GACN,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,qBAAqB,EACrB,wBAAwB,EACxB,kBAAkB,EAClB,YAAY,GACb,MAAM,YAAY,CAAC;AAsBpB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,OAAO,SAAS,OAAO,GAAG,OAAO,EACvD,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,GAC7B,eAAe,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,CA+EzC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Compute the HMAC-SHA256 signature for an outbound webhook request.
3
+ *
4
+ * The signed payload is `${timestamp}.${body}` — Stripe-style. The
5
+ * timestamp is included so the receiver can reject replays via a
6
+ * window check, and the dot separator prevents `timestamp + body`
7
+ * ambiguity (12 + 345 vs 123 + 45).
8
+ *
9
+ * Returns `{ signature, timestamp }` so the webhook helper can attach
10
+ * both as headers — `X-Webhook-Signature: sha256=<hex>` and
11
+ * `X-Webhook-Timestamp: <unix-seconds>` — for the receiver to verify
12
+ * via `verifyWebhook` from `@rotorsoft/act-http/receiver`.
13
+ *
14
+ * `now` is exposed for tests; production callers should leave it
15
+ * undefined so wall-clock is used.
16
+ *
17
+ * @internal Reachable from tests via the source path. Not re-exported
18
+ * from the package's `./webhook` entry — the webhook helper calls
19
+ * it internally, and operators don't need it directly.
20
+ */
21
+ export declare function signRequest(body: string, secret: string, now?: number): {
22
+ signature: string;
23
+ timestamp: string;
24
+ };
25
+ //# sourceMappingURL=sign.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../../../src/webhook/sign.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAsC,GAC1C;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAK1C"}