@rotorsoft/act-http 1.1.0 → 1.2.1

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 +12 -0
  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/start.d.ts +1 -1
  12. package/dist/@types/sse/apply-patch.d.ts +3 -3
  13. package/dist/@types/sse/broadcast.d.ts +6 -6
  14. package/dist/@types/sse/broadcast.d.ts.map +1 -1
  15. package/dist/@types/sse/presence.d.ts +7 -7
  16. package/dist/@types/sse/presence.d.ts.map +1 -1
  17. package/dist/@types/webhook/classify.d.ts +6 -6
  18. package/dist/@types/webhook/classify.d.ts.map +1 -1
  19. package/dist/@types/webhook/index.d.ts +1 -1
  20. package/dist/@types/webhook/index.d.ts.map +1 -1
  21. package/dist/@types/webhook/sign.d.ts +1 -1
  22. package/dist/@types/webhook/sign.d.ts.map +1 -1
  23. package/dist/api/index.cjs +85 -0
  24. package/dist/api/index.cjs.map +1 -0
  25. package/dist/api/index.js +62 -0
  26. package/dist/api/index.js.map +1 -0
  27. package/dist/{chunk-NOIXOF2I.js → chunk-4CGAUB5H.js} +13 -13
  28. package/dist/chunk-4CGAUB5H.js.map +1 -0
  29. package/dist/{chunk-F7VWYZ37.js → chunk-K4HAOBRF.js} +4 -4
  30. package/dist/{chunk-F7VWYZ37.js.map → chunk-K4HAOBRF.js.map} +1 -1
  31. package/dist/receiver/express/index.cjs +14 -14
  32. package/dist/receiver/express/index.cjs.map +1 -1
  33. package/dist/receiver/express/index.js +3 -3
  34. package/dist/receiver/express/index.js.map +1 -1
  35. package/dist/receiver/fastify/index.cjs +12 -12
  36. package/dist/receiver/fastify/index.cjs.map +1 -1
  37. package/dist/receiver/fastify/index.js +1 -1
  38. package/dist/receiver/hono/index.cjs +14 -14
  39. package/dist/receiver/hono/index.cjs.map +1 -1
  40. package/dist/receiver/hono/index.js +2 -2
  41. package/dist/receiver/index.cjs +19 -2746
  42. package/dist/receiver/index.cjs.map +1 -1
  43. package/dist/receiver/index.js +5 -2077
  44. package/dist/receiver/index.js.map +1 -1
  45. package/dist/receiver/trpc/index.cjs +12 -12
  46. package/dist/receiver/trpc/index.cjs.map +1 -1
  47. package/dist/receiver/trpc/index.js +1 -1
  48. package/dist/sse/index.cjs +21 -21
  49. package/dist/sse/index.cjs.map +1 -1
  50. package/dist/sse/index.js +24 -24
  51. package/dist/sse/index.js.map +1 -1
  52. package/dist/webhook/index.cjs +14 -34
  53. package/dist/webhook/index.cjs.map +1 -1
  54. package/dist/webhook/index.js +14 -32
  55. package/dist/webhook/index.js.map +1 -1
  56. package/package.json +34 -12
  57. package/dist/chunk-NOIXOF2I.js.map +0 -1
  58. package/dist/dist-NWMJQI4E.js +0 -647
  59. package/dist/dist-NWMJQI4E.js.map +0 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rotorsoft/act-http",
3
3
  "type": "module",
4
- "version": "1.1.0",
4
+ "version": "1.2.1",
5
5
  "description": "HTTP integrations for act apps — webhooks and SSE",
6
6
  "keywords": [
7
7
  "typescript",
@@ -60,19 +60,28 @@
60
60
  "types": "./dist/@types/receiver/hono/index.d.ts",
61
61
  "import": "./dist/receiver/hono/index.js",
62
62
  "require": "./dist/receiver/hono/index.cjs"
63
+ },
64
+ "./api": {
65
+ "types": "./dist/@types/api/index.d.ts",
66
+ "import": "./dist/api/index.js",
67
+ "require": "./dist/api/index.cjs"
63
68
  }
64
69
  },
65
70
  "sideEffects": false,
66
71
  "engines": {
67
- "node": ">=22.18.0"
72
+ "node": ">=22.22.3"
68
73
  },
69
74
  "publishConfig": {
70
75
  "access": "public"
71
76
  },
72
77
  "peerDependencies": {
73
- "@trpc/server": ">=11",
74
- "@rotorsoft/act": "^1.6.0",
75
- "@rotorsoft/act-ops": "^0.1.0"
78
+ "@trpc/server": ">=11.17.0",
79
+ "@hono/node-server": "^2.0.4",
80
+ "express": "^5.2.1",
81
+ "fastify": "^5.8.5",
82
+ "hono": "^4.12.23",
83
+ "@rotorsoft/act": "^1.10.1",
84
+ "@rotorsoft/act-ops": "^0.2.0"
76
85
  },
77
86
  "peerDependenciesMeta": {
78
87
  "@rotorsoft/act-ops": {
@@ -80,19 +89,32 @@
80
89
  },
81
90
  "@trpc/server": {
82
91
  "optional": true
92
+ },
93
+ "@hono/node-server": {
94
+ "optional": true
95
+ },
96
+ "express": {
97
+ "optional": true
98
+ },
99
+ "fastify": {
100
+ "optional": true
101
+ },
102
+ "hono": {
103
+ "optional": true
83
104
  }
84
105
  },
85
106
  "dependencies": {
86
- "@rotorsoft/act-patch": "^1.2.3"
107
+ "@rotorsoft/act-patch": "^1.2.4"
87
108
  },
88
109
  "devDependencies": {
89
- "@hono/node-server": "^1",
110
+ "@hono/node-server": "^2.0.4",
90
111
  "@trpc/server": "^11.17.0",
91
- "@types/express": "^5",
92
- "fastify": "^5",
93
- "hono": "^4",
94
- "zod": "^4",
95
- "@rotorsoft/act-ops": "^0.1.0"
112
+ "@types/express": "^5.0.6",
113
+ "express": "^5.2.1",
114
+ "fastify": "^5.8.5",
115
+ "hono": "^4.12.23",
116
+ "zod": "^4.4.3",
117
+ "@rotorsoft/act-ops": "^0.2.0"
96
118
  },
97
119
  "scripts": {
98
120
  "clean": "rm -rf dist",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/receiver/extract.ts","../src/receiver/verify.ts","../src/receiver/check.ts"],"sourcesContent":["/**\n * Pull the `Idempotency-Key` header from a Node-style headers bag,\n * case-insensitive. Returns `undefined` when any of the following\n * carries no usable key:\n *\n * - the header is missing\n * - its value is an array (ambiguous — can't pick one without a\n * policy the receiver hasn't declared)\n * - its value is the empty string (carries no idempotency\n * information; structurally equivalent to \"no header at all\")\n *\n * Pair with `IdempotencyStore.claim` from\n * `@rotorsoft/act-ops/idempotency`: extract the key from the inbound\n * request, claim it on the store, return a `deduped` marker when the\n * claim fails. The framework-agnostic middleware that wires these\n * together lands in #744.\n *\n * Validation beyond \"is there a usable key?\" (length bounds, format\n * checks, normalization) is intentionally out of scope. Receivers\n * picking a policy can layer it on top — or, when #744 ships, opt\n * into the middleware's opinionated defaults.\n */\nexport function extractIdempotencyKey(\n headers: Record<string, string | string[] | undefined>\n): string | undefined {\n for (const [name, value] of Object.entries(headers)) {\n if (name.toLowerCase() !== \"idempotency-key\") continue;\n if (Array.isArray(value)) return undefined;\n if (value === \"\") return undefined;\n return value;\n }\n return undefined;\n}\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Outcome of {@link verifyWebhook}. Either the request signature\n * checks out, or one of five distinct failure reasons applies. Each\n * reason maps to an operator-meaningful telemetry bucket — separated\n * deliberately so dashboards can distinguish \"client lost its secret\"\n * from \"client clock is wrong\" from \"this is a replay attack.\"\n */\nexport type VerifyResult =\n | { ok: true }\n | {\n ok: false;\n reason:\n | \"missing-signature\"\n | \"missing-timestamp\"\n | \"stale\"\n | \"future\"\n | \"bad-signature\";\n };\n\n/** Options for {@link verifyWebhook}. */\nexport type VerifyOptions = {\n /**\n * Maximum acceptable timestamp drift in either direction, in\n * seconds. Default: 300 (±5 minutes) — matches Stripe / GitHub /\n * Slack conventions. Tightening narrows the replay window;\n * loosening accommodates clients with worse clock sync.\n */\n maxAgeSeconds?: number;\n /**\n * Current Unix-seconds time. Exposed for tests; production\n * callers should leave it undefined so wall-clock is used.\n */\n now?: number;\n};\n\n/**\n * Verify an inbound webhook's signature and timestamp against the\n * shared secret. Pair with the sender side: configure\n * `webhook({ secret })` from `@rotorsoft/act-http/webhook`.\n *\n * Returns `{ ok: true }` on success or `{ ok: false; reason }` on\n * failure. The reasons are:\n *\n * - `missing-signature` — no `X-Webhook-Signature` header, value\n * was an array, or value was empty.\n * - `missing-timestamp` — no `X-Webhook-Timestamp` header, value\n * was empty, or value isn't a parseable integer.\n * - `stale` — timestamp older than `maxAgeSeconds` from `now`.\n * - `future` — timestamp more than `maxAgeSeconds` ahead of `now`.\n * - `bad-signature` — signature header didn't start with `sha256=`,\n * wasn't 64 hex chars, or the recomputed HMAC didn't match\n * (constant-time compare).\n *\n * The signed payload is `${timestamp}.${body}`, so `body` must be\n * the **raw request body bytes**. Any pre-parse normalization\n * (whitespace trimming, JSON re-stringification) would change the\n * hash and reject every otherwise-valid request. Framework adapters\n * in #744 will provide the raw body alongside the parsed one.\n *\n * Uses Node's `crypto.timingSafeEqual` for the final comparison to\n * avoid signature-equality timing attacks.\n */\nexport function verifyWebhook(\n headers: Record<string, string | string[] | undefined>,\n body: string,\n secret: string,\n options?: VerifyOptions\n): VerifyResult {\n const maxAgeSeconds = options?.maxAgeSeconds ?? 300;\n const now = options?.now ?? Math.floor(Date.now() / 1000);\n\n const signature = pickHeader(headers, \"x-webhook-signature\");\n if (!signature) return { ok: false, reason: \"missing-signature\" };\n\n const timestampStr = pickHeader(headers, \"x-webhook-timestamp\");\n if (!timestampStr) return { ok: false, reason: \"missing-timestamp\" };\n const timestamp = Number.parseInt(timestampStr, 10);\n if (Number.isNaN(timestamp) || String(timestamp) !== timestampStr) {\n return { ok: false, reason: \"missing-timestamp\" };\n }\n\n const delta = now - timestamp;\n if (delta > maxAgeSeconds) return { ok: false, reason: \"stale\" };\n if (delta < -maxAgeSeconds) return { ok: false, reason: \"future\" };\n\n if (!signature.startsWith(\"sha256=\")) {\n return { ok: false, reason: \"bad-signature\" };\n }\n const providedHex = signature.slice(\"sha256=\".length);\n if (!/^[0-9a-fA-F]{64}$/.test(providedHex)) {\n return { ok: false, reason: \"bad-signature\" };\n }\n\n const expectedHex = createHmac(\"sha256\", secret)\n .update(`${timestampStr}.${body}`)\n .digest(\"hex\");\n\n const a = Buffer.from(providedHex, \"hex\");\n const b = Buffer.from(expectedHex, \"hex\");\n if (!timingSafeEqual(a, b)) {\n return { ok: false, reason: \"bad-signature\" };\n }\n\n return { ok: true };\n}\n\nfunction pickHeader(\n headers: Record<string, string | string[] | undefined>,\n lowerName: string\n): string | undefined {\n for (const [name, value] of Object.entries(headers)) {\n if (name.toLowerCase() !== lowerName) continue;\n if (Array.isArray(value) || value === undefined || value === \"\") {\n return undefined;\n }\n return value;\n }\n return undefined;\n}\n","import type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\nimport { extractIdempotencyKey } from \"./extract.js\";\nimport { type VerifyOptions, verifyWebhook } from \"./verify.js\";\n\n/**\n * Failure reasons returned by {@link checkWebhook}. The shape splits\n * `missing-key` (a client error, mapped to HTTP 400) from the five\n * verification failures (authentication errors, HTTP 401) so each\n * maps to its own telemetry bucket.\n */\nexport type CheckFailureReason =\n | \"missing-key\"\n | \"missing-signature\"\n | \"missing-timestamp\"\n | \"stale\"\n | \"future\"\n | \"bad-signature\";\n\n/**\n * Outcome of {@link checkWebhook}. Either the request passed every\n * configured check and carries a usable idempotency key, or it\n * failed one of them and the framework adapter should reply with the\n * corresponding HTTP status.\n */\nexport type CheckResult =\n | { ok: false; status: 400 | 401; reason: CheckFailureReason }\n | { ok: true; key: string; deduped: boolean };\n\n/** Options for {@link checkWebhook}. */\nexport type CheckWebhookOptions = {\n /** Idempotency store the framework-agnostic core claims the key on. */\n store: IdempotencyStore;\n /**\n * Optional HMAC-SHA256 secret. When set, the request's\n * `X-Webhook-Signature` and `X-Webhook-Timestamp` headers are\n * verified before the dedup claim. When omitted, signature\n * verification is skipped (unsigned receivers).\n */\n secret?: string;\n /**\n * Verification options forwarded to {@link verifyWebhook}. Only\n * meaningful when `secret` is set. Defaults to a ±300-second\n * timestamp window.\n */\n verify?: VerifyOptions;\n};\n\n/**\n * Framework-agnostic receiver check: verify the signature (when a\n * secret is configured), extract the `Idempotency-Key`, and claim\n * it on the store. Returns the request's fate as a discriminated\n * union the per-framework adapter translates into the framework's\n * idiomatic 4xx response or context injection.\n *\n * **Order of checks** (matters):\n *\n * 1. Verify signature + timestamp window (when `secret` is set).\n * Rejecting bad signatures *before* extracting and claiming the\n * key keeps attacker-supplied keys out of the dedup store —\n * otherwise a flood of spoofed POSTs would pollute the LRU.\n * 2. Extract the `Idempotency-Key`. Missing → reject with 400.\n * 3. Claim the key on the store. If already seen, return\n * `{ ok: true; deduped: true }` so the framework adapter can\n * short-circuit the handler without re-running side effects.\n *\n * The dedup store may be sync (`InMemoryIdempotencyStore`) or async\n * (durable adapters like a future `PostgresIdempotencyStore`); the\n * core awaits unconditionally so both shapes compose cleanly.\n */\nexport async function checkWebhook(\n headers: Record<string, string | string[] | undefined>,\n body: string,\n options: CheckWebhookOptions\n): Promise<CheckResult> {\n if (options.secret !== undefined) {\n const verification = verifyWebhook(\n headers,\n body,\n options.secret,\n options.verify\n );\n if (!verification.ok) {\n return { ok: false, status: 401, reason: verification.reason };\n }\n }\n\n const key = extractIdempotencyKey(headers);\n if (!key) return { ok: false, status: 400, reason: \"missing-key\" };\n\n const claimed = await options.store.claim(key);\n return { ok: true, key, deduped: !claimed };\n}\n"],"mappings":";AAsBO,SAAS,sBACd,SACoB;AACpB,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AACnD,QAAI,KAAK,YAAY,MAAM,kBAAmB;AAC9C,QAAI,MAAM,QAAQ,KAAK,EAAG,QAAO;AACjC,QAAI,UAAU,GAAI,QAAO;AACzB,WAAO;AAAA,EACT;AACA,SAAO;AACT;;;AChCA,SAAS,YAAY,uBAAuB;AAgErC,SAAS,cACd,SACA,MACA,QACA,SACc;AACd,QAAM,gBAAgB,SAAS,iBAAiB;AAChD,QAAM,MAAM,SAAS,OAAO,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAExD,QAAM,YAAY,WAAW,SAAS,qBAAqB;AAC3D,MAAI,CAAC,UAAW,QAAO,EAAE,IAAI,OAAO,QAAQ,oBAAoB;AAEhE,QAAM,eAAe,WAAW,SAAS,qBAAqB;AAC9D,MAAI,CAAC,aAAc,QAAO,EAAE,IAAI,OAAO,QAAQ,oBAAoB;AACnE,QAAM,YAAY,OAAO,SAAS,cAAc,EAAE;AAClD,MAAI,OAAO,MAAM,SAAS,KAAK,OAAO,SAAS,MAAM,cAAc;AACjE,WAAO,EAAE,IAAI,OAAO,QAAQ,oBAAoB;AAAA,EAClD;AAEA,QAAM,QAAQ,MAAM;AACpB,MAAI,QAAQ,cAAe,QAAO,EAAE,IAAI,OAAO,QAAQ,QAAQ;AAC/D,MAAI,QAAQ,CAAC,cAAe,QAAO,EAAE,IAAI,OAAO,QAAQ,SAAS;AAEjE,MAAI,CAAC,UAAU,WAAW,SAAS,GAAG;AACpC,WAAO,EAAE,IAAI,OAAO,QAAQ,gBAAgB;AAAA,EAC9C;AACA,QAAM,cAAc,UAAU,MAAM,UAAU,MAAM;AACpD,MAAI,CAAC,oBAAoB,KAAK,WAAW,GAAG;AAC1C,WAAO,EAAE,IAAI,OAAO,QAAQ,gBAAgB;AAAA,EAC9C;AAEA,QAAM,cAAc,WAAW,UAAU,MAAM,EAC5C,OAAO,GAAG,YAAY,IAAI,IAAI,EAAE,EAChC,OAAO,KAAK;AAEf,QAAM,IAAI,OAAO,KAAK,aAAa,KAAK;AACxC,QAAM,IAAI,OAAO,KAAK,aAAa,KAAK;AACxC,MAAI,CAAC,gBAAgB,GAAG,CAAC,GAAG;AAC1B,WAAO,EAAE,IAAI,OAAO,QAAQ,gBAAgB;AAAA,EAC9C;AAEA,SAAO,EAAE,IAAI,KAAK;AACpB;AAEA,SAAS,WACP,SACA,WACoB;AACpB,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AACnD,QAAI,KAAK,YAAY,MAAM,UAAW;AACtC,QAAI,MAAM,QAAQ,KAAK,KAAK,UAAU,UAAa,UAAU,IAAI;AAC/D,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;;;ACnDA,eAAsB,aACpB,SACA,MACA,SACsB;AACtB,MAAI,QAAQ,WAAW,QAAW;AAChC,UAAM,eAAe;AAAA,MACnB;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AACA,QAAI,CAAC,aAAa,IAAI;AACpB,aAAO,EAAE,IAAI,OAAO,QAAQ,KAAK,QAAQ,aAAa,OAAO;AAAA,IAC/D;AAAA,EACF;AAEA,QAAM,MAAM,sBAAsB,OAAO;AACzC,MAAI,CAAC,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,KAAK,QAAQ,cAAc;AAEjE,QAAM,UAAU,MAAM,QAAQ,MAAM,MAAM,GAAG;AAC7C,SAAO,EAAE,IAAI,MAAM,KAAK,SAAS,CAAC,QAAQ;AAC5C;","names":[]}