@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
|
@@ -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
|
|
30
|
-
export {
|
|
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;
|
|
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"}
|