@rotorsoft/act-http 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +127 -3
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/@types/api/actor.d.ts +20 -0
  4. package/dist/@types/api/actor.d.ts.map +1 -0
  5. package/dist/@types/api/errors.d.ts +73 -0
  6. package/dist/@types/api/errors.d.ts.map +1 -0
  7. package/dist/@types/api/idempotency.d.ts +36 -0
  8. package/dist/@types/api/idempotency.d.ts.map +1 -0
  9. package/dist/@types/api/index.d.ts +39 -0
  10. package/dist/@types/api/index.d.ts.map +1 -0
  11. package/dist/@types/receiver/check.d.ts +66 -0
  12. package/dist/@types/receiver/check.d.ts.map +1 -0
  13. package/dist/@types/receiver/express/index.d.ts +51 -0
  14. package/dist/@types/receiver/express/index.d.ts.map +1 -0
  15. package/dist/@types/receiver/extract.d.ts +24 -0
  16. package/dist/@types/receiver/extract.d.ts.map +1 -0
  17. package/dist/@types/receiver/fastify/index.d.ts +55 -0
  18. package/dist/@types/receiver/fastify/index.d.ts.map +1 -0
  19. package/dist/@types/receiver/hono/index.d.ts +60 -0
  20. package/dist/@types/receiver/hono/index.d.ts.map +1 -0
  21. package/dist/@types/receiver/index.d.ts +39 -0
  22. package/dist/@types/receiver/index.d.ts.map +1 -0
  23. package/dist/@types/receiver/start.d.ts +48 -0
  24. package/dist/@types/receiver/start.d.ts.map +1 -0
  25. package/dist/@types/receiver/trpc/index.d.ts +16 -0
  26. package/dist/@types/receiver/trpc/index.d.ts.map +1 -0
  27. package/dist/@types/receiver/verify.d.ts +57 -0
  28. package/dist/@types/receiver/verify.d.ts.map +1 -0
  29. package/dist/@types/webhook/classify.d.ts +59 -0
  30. package/dist/@types/webhook/classify.d.ts.map +1 -0
  31. package/dist/@types/webhook/index.d.ts +3 -2
  32. package/dist/@types/webhook/index.d.ts.map +1 -1
  33. package/dist/@types/webhook/sign.d.ts +25 -0
  34. package/dist/@types/webhook/sign.d.ts.map +1 -0
  35. package/dist/@types/webhook/types.d.ts +61 -20
  36. package/dist/@types/webhook/types.d.ts.map +1 -1
  37. package/dist/api/index.cjs +85 -0
  38. package/dist/api/index.cjs.map +1 -0
  39. package/dist/api/index.js +62 -0
  40. package/dist/api/index.js.map +1 -0
  41. package/dist/chunk-F7VWYZ37.js +29 -0
  42. package/dist/chunk-F7VWYZ37.js.map +1 -0
  43. package/dist/chunk-NOIXOF2I.js +78 -0
  44. package/dist/chunk-NOIXOF2I.js.map +1 -0
  45. package/dist/dist-NWMJQI4E.js +647 -0
  46. package/dist/dist-NWMJQI4E.js.map +1 -0
  47. package/dist/receiver/express/index.cjs +128 -0
  48. package/dist/receiver/express/index.cjs.map +1 -0
  49. package/dist/receiver/express/index.js +33 -0
  50. package/dist/receiver/express/index.js.map +1 -0
  51. package/dist/receiver/fastify/index.cjs +120 -0
  52. package/dist/receiver/fastify/index.cjs.map +1 -0
  53. package/dist/receiver/fastify/index.js +25 -0
  54. package/dist/receiver/fastify/index.js.map +1 -0
  55. package/dist/receiver/hono/index.cjs +123 -0
  56. package/dist/receiver/hono/index.cjs.map +1 -0
  57. package/dist/receiver/hono/index.js +8 -0
  58. package/dist/receiver/hono/index.js.map +1 -0
  59. package/dist/receiver/index.cjs +2943 -0
  60. package/dist/receiver/index.cjs.map +1 -0
  61. package/dist/receiver/index.js +2162 -0
  62. package/dist/receiver/index.js.map +1 -0
  63. package/dist/receiver/trpc/index.cjs +126 -0
  64. package/dist/receiver/trpc/index.cjs.map +1 -0
  65. package/dist/receiver/trpc/index.js +31 -0
  66. package/dist/receiver/trpc/index.js.map +1 -0
  67. package/dist/webhook/index.cjs +66 -6
  68. package/dist/webhook/index.cjs.map +1 -1
  69. package/dist/webhook/index.js +62 -6
  70. package/dist/webhook/index.js.map +1 -1
  71. package/package.json +52 -3
@@ -0,0 +1,20 @@
1
+ import type { Actor } from "@rotorsoft/act";
2
+ /**
3
+ * Extractor function the host supplies to resolve an {@link Actor}
4
+ * from an incoming request. The framework keeps auth out of the
5
+ * package — JWT vs session vs API key is the host's call — and asks
6
+ * for this single closure that every transport (tRPC, Hono, OpenAPI
7
+ * docs) composes against.
8
+ *
9
+ * The `request` argument is intentionally generic. Each transport
10
+ * narrows it at the call site (`IncomingMessage` for Hono, the tRPC
11
+ * context object for tRPC, etc.) — keeping the contract here
12
+ * transport-agnostic means one extractor implementation plugs into
13
+ * every adapter unchanged.
14
+ *
15
+ * Async is allowed so the extractor can verify a JWT against a
16
+ * remote JWKS endpoint without forcing every host to synchronously
17
+ * cache.
18
+ */
19
+ export type ActorExtractor = (request: unknown) => Actor | Promise<Actor>;
20
+ //# sourceMappingURL=actor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actor.d.ts","sourceRoot":"","sources":["../../../src/api/actor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Uniform error envelope shipped over the wire by every act-http
3
+ * transport. Hosts get the same shape from REST, tRPC, and OpenAPI —
4
+ * a client that talks to two transports doesn't have to invent two
5
+ * error parsers.
6
+ *
7
+ * - `error` — the framework error name (`"ValidationError"`,
8
+ * `"InvariantError"`, …). Stable identifier, safe to switch on.
9
+ * - `detail` — the framework's message text. Human-readable; not
10
+ * parsed by clients.
11
+ * - `code` — a machine-readable status code from {@link ERROR_MAP}
12
+ * for clients that prefer enum-style branching over name strings.
13
+ */
14
+ export type ApiError = {
15
+ error: string;
16
+ detail?: string;
17
+ code?: string;
18
+ };
19
+ /**
20
+ * Status + code pair for one known framework error.
21
+ */
22
+ export type ErrorMapEntry = {
23
+ status: number;
24
+ code: string;
25
+ };
26
+ /**
27
+ * The single table that maps framework error types to HTTP status
28
+ * codes and machine-readable codes. One table, three consumers
29
+ * (Hono, tRPC, OpenAPI) — cross-transport consistency by
30
+ * construction.
31
+ *
32
+ * Operators wanting different mappings wrap the generated transport
33
+ * rather than mutating this — the consistency is the load-bearing
34
+ * property, not the specific status codes.
35
+ */
36
+ export declare const ERROR_MAP: {
37
+ readonly ValidationError: {
38
+ readonly status: 422;
39
+ readonly code: "VALIDATION";
40
+ };
41
+ readonly InvariantError: {
42
+ readonly status: 409;
43
+ readonly code: "INVARIANT";
44
+ };
45
+ readonly ConcurrencyError: {
46
+ readonly status: 412;
47
+ readonly code: "CONCURRENCY";
48
+ };
49
+ readonly StreamClosedError: {
50
+ readonly status: 410;
51
+ readonly code: "STREAM_CLOSED";
52
+ };
53
+ readonly NonRetryableError: {
54
+ readonly status: 400;
55
+ readonly code: "NON_RETRYABLE";
56
+ };
57
+ };
58
+ /**
59
+ * Translate an unknown thrown value into the canonical
60
+ * {@link ApiError} envelope plus HTTP status. Each transport's error
61
+ * boundary calls this once and forwards the result to the wire.
62
+ *
63
+ * Known framework errors map per {@link ERROR_MAP}. Everything else
64
+ * surfaces as a 500 with `code: "INTERNAL"`; the `detail` field is
65
+ * populated when the throw was an `Error` instance, omitted
66
+ * otherwise (a thrown string or object doesn't get to leak its
67
+ * payload to the client).
68
+ */
69
+ export declare function toApiError(err: unknown): {
70
+ status: number;
71
+ body: ApiError;
72
+ };
73
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/api/errors.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;CAM4B,CAAC;AAWnD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAuB3E"}
@@ -0,0 +1,36 @@
1
+ import type { IdempotencyStore } from "@rotorsoft/act-ops/idempotency";
2
+ /**
3
+ * Result of a {@link withIdempotency} call.
4
+ *
5
+ * - `{ deduped: false, result }` — the claim was fresh; the handler
6
+ * ran and produced `result`.
7
+ * - `{ deduped: true }` — the claim was already taken; the handler
8
+ * was *not* invoked. The caller decides how to respond (typically
9
+ * a 2xx with no body, matching the receiver-side convention).
10
+ *
11
+ * Note: the contract does not cache the previous response. A
12
+ * duplicate call returns the deduped marker only — replaying the
13
+ * original handler's output would require a response-caching
14
+ * adapter, which is out of scope here. The receiver-side convention
15
+ * (and the convention the generated transports follow) is "ack the
16
+ * duplicate; do nothing else."
17
+ */
18
+ export type IdempotencyResult<T> = {
19
+ deduped: false;
20
+ result: T;
21
+ } | {
22
+ deduped: true;
23
+ };
24
+ /**
25
+ * Wrap an action handler so the framework honors `Idempotency-Key`
26
+ * dedup. Acquires the key via {@link IdempotencyStore.claim}, runs
27
+ * the handler exactly when the claim was fresh, and skips the
28
+ * handler entirely on a duplicate.
29
+ *
30
+ * Reuses the contract `@rotorsoft/act-ops/idempotency` already
31
+ * defines for the receiver-side `Idempotency-Key` story. A single
32
+ * `IdempotencyStore` implementation covers both halves of the "Act
33
+ * over the wire" surface — receiver and generated API.
34
+ */
35
+ export declare function withIdempotency<T>(store: IdempotencyStore, key: string, handler: () => Promise<T>): Promise<IdempotencyResult<T>>;
36
+ //# sourceMappingURL=idempotency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../../src/api/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAEvE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAC3B;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,GAC7B;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,CAAC;AAEtB;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,KAAK,EAAE,gBAAgB,EACvB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAM/B"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * @module act-http/api
4
+ *
5
+ * Shared utilities for the act-http auto-generated API surfaces.
6
+ * Three concerns that every transport (tRPC, Hono, OpenAPI) has to
7
+ * address — actor extraction, error envelope mapping,
8
+ * `Idempotency-Key` wiring — defined once here and composed by each
9
+ * transport sibling subpath.
10
+ *
11
+ * - {@link ActorExtractor} — the host-supplied closure that resolves
12
+ * an `Actor` from an incoming request. Auth (JWT, session, API
13
+ * key) stays in the host; the package only asks for this one
14
+ * function.
15
+ * - {@link ApiError}, {@link ERROR_MAP}, {@link toApiError} — the
16
+ * uniform error envelope and the status/code mapping every
17
+ * transport uses. Cross-transport consistency by construction.
18
+ * - {@link withIdempotency} — the helper that wraps action handlers
19
+ * in an `Idempotency-Key` claim. Reuses the
20
+ * `@rotorsoft/act-ops/idempotency` contract that
21
+ * `@rotorsoft/act-http/receiver` already speaks, so receivers and
22
+ * generated APIs share one `IdempotencyStore` implementation.
23
+ *
24
+ * Sibling subpaths in the same package consume the utilities here:
25
+ *
26
+ * - `@rotorsoft/act-http/trpc` — tRPC adapter (#843).
27
+ * - `@rotorsoft/act-http/hono` — Hono adapter (#844).
28
+ * - `@rotorsoft/act-http/openapi` — OpenAPI emitter (#845).
29
+ *
30
+ * Existing siblings unrelated to the generated-API work:
31
+ *
32
+ * - `@rotorsoft/act-http/webhook` — outbound POST delivery.
33
+ * - `@rotorsoft/act-http/sse` — incremental state broadcast.
34
+ * - `@rotorsoft/act-http/receiver` — inbound webhook ingestion.
35
+ */
36
+ export type { ActorExtractor } from "./actor.js";
37
+ export { type ApiError, ERROR_MAP, type ErrorMapEntry, toApiError, } from "./errors.js";
38
+ export { type IdempotencyResult, withIdempotency } from "./idempotency.js";
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACL,KAAK,QAAQ,EACb,SAAS,EACT,KAAK,aAAa,EAClB,UAAU,GACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,KAAK,iBAAiB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC"}
@@ -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"}