@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,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"}
@@ -54,48 +54,89 @@ export type WebhookConfig<TEvents extends Schemas = Schemas> = {
54
54
  * Injection point for tests. Defaults to global `fetch`.
55
55
  */
56
56
  readonly fetch?: typeof fetch;
57
+ /**
58
+ * HMAC-SHA256 signing key. When set, the webhook helper attaches
59
+ * two headers to every request:
60
+ *
61
+ * - `X-Webhook-Signature: sha256=<hex>` — HMAC of
62
+ * `${timestamp}.${body}` (`body` is the final serialized payload)
63
+ * - `X-Webhook-Timestamp: <unix-seconds>`
64
+ *
65
+ * Pair with `verifyWebhook` from `@rotorsoft/act-http/receiver` on
66
+ * the receiving side. When undefined, no signature headers are
67
+ * added — back-compat with consumers that don't need signing.
68
+ *
69
+ * Callers can override either header by returning it from the
70
+ * `headers` resolver (case-insensitive), the same way the
71
+ * `Idempotency-Key` and `Content-Type` defaults yield to caller
72
+ * intent.
73
+ */
74
+ readonly secret?: string;
57
75
  };
58
76
  /**
59
- * Common fields carried on both webhook error subclasses.
77
+ * Common fields carried on every HTTP delivery error in this package.
60
78
  */
61
- type WebhookErrorInit = {
79
+ export type HttpDeliveryErrorInit = {
62
80
  status: number;
63
81
  url: string;
64
82
  responseBody?: string;
65
83
  };
66
84
  /**
67
- * Thrown when a webhook request fails in a way the drain pipeline
85
+ * Thrown when an HTTP delivery fails in a way the drain pipeline
68
86
  * should retry: network failure, timeout, or 5xx response. `status` is
69
87
  * `0` for network / timeout errors, the HTTP status code otherwise.
70
88
  *
71
- * The class itself is the retry signal — if the helper throws this,
89
+ * The class itself is the retry signal — if a reaction throws this,
72
90
  * drain treats it like any other error (counts against `maxRetries`,
73
- * paces with `backoff`). For permanent failures, the helper throws
74
- * {@link NonRetryableWebhookError} instead.
91
+ * paces with `backoff`). For permanent failures, throw
92
+ * {@link NonRetryableHttpError} instead.
93
+ *
94
+ * Generic enough to cover any custom HTTP-like integration (gRPC
95
+ * bridges, SDK-based reactions). {@link WebhookError} is a
96
+ * webhook-specific subclass kept for backward compatibility.
75
97
  */
76
- export declare class WebhookError extends Error {
98
+ export declare class RetryableHttpError extends Error {
77
99
  readonly status: number;
78
100
  readonly url: string;
79
101
  readonly responseBody?: string;
80
- constructor(message: string, init: WebhookErrorInit);
102
+ constructor(message: string, init: HttpDeliveryErrorInit);
81
103
  }
82
104
  /**
83
- * Thrown when a webhook returns a 4xx response. Extends
84
- * {@link NonRetryableError} so the drain finalizer blocks the stream on
85
- * the first failed attempt (when `blockOnError` is true) — no wasted
86
- * retries on permanent client errors.
105
+ * Thrown when an HTTP delivery returns a 3xx or 4xx response
106
+ * permanent client errors that won't recover on retry. Extends
107
+ * {@link NonRetryableError} so the drain finalizer blocks the stream
108
+ * on the first failed attempt (when `blockOnError` is true) — no
109
+ * wasted retries on a malformed payload or wrong URL.
87
110
  *
88
- * Carries the same `status` / `url` / `responseBody` fields as
89
- * {@link WebhookError}; not a subclass of it (a single instance can't
90
- * be both `WebhookError` and `NonRetryableError`). Callers catching
91
- * either retryable or non-retryable webhook failures should check both
92
- * classes, or check the shared fields directly.
111
+ * Generic enough to cover any custom HTTP-like integration.
112
+ * {@link NonRetryableWebhookError} is a webhook-specific subclass kept
113
+ * for backward compatibility.
93
114
  */
94
- export declare class NonRetryableWebhookError extends NonRetryableError {
115
+ export declare class NonRetryableHttpError extends NonRetryableError {
95
116
  readonly status: number;
96
117
  readonly url: string;
97
118
  readonly responseBody?: string;
98
- constructor(message: string, init: WebhookErrorInit);
119
+ constructor(message: string, init: HttpDeliveryErrorInit);
120
+ }
121
+ /**
122
+ * Webhook-specific subclass of {@link RetryableHttpError}. Thrown by
123
+ * the {@link webhook} helper on 5xx responses, network failures, and
124
+ * timeouts. Existing `instanceof WebhookError` checks continue to
125
+ * work; new code targeting the generic HTTP integration shape can
126
+ * catch {@link RetryableHttpError} instead and handle webhook +
127
+ * custom integrations uniformly.
128
+ */
129
+ export declare class WebhookError extends RetryableHttpError {
130
+ constructor(message: string, init: HttpDeliveryErrorInit);
131
+ }
132
+ /**
133
+ * Webhook-specific subclass of {@link NonRetryableHttpError}. Thrown
134
+ * by the {@link webhook} helper on 3xx/4xx responses. Existing
135
+ * `instanceof NonRetryableWebhookError` checks continue to work; new
136
+ * code can catch {@link NonRetryableHttpError} or
137
+ * {@link NonRetryableError} for broader coverage.
138
+ */
139
+ export declare class NonRetryableWebhookError extends NonRetryableHttpError {
140
+ constructor(message: string, init: HttpDeliveryErrorInit);
99
141
  }
100
- export {};
101
142
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/webhook/types.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,iBAAiB,EACjB,KAAK,OAAO,EACb,MAAM,gBAAgB,CAAC;AAExB;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,OAAO,EAAE,CAAC,IAClD,CAAC,GACD,CAAC,CAAC,KAAK,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;AAEtD;;;;GAIG;AACH,MAAM,MAAM,WAAW,GACnB,MAAM,GACN;IAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GACjC,SAAS,OAAO,EAAE,CAAC;AAEvB;;;;GAIG;AACH,MAAM,MAAM,aAAa,CAAC,OAAO,SAAS,OAAO,GAAG,OAAO,IAAI;IAC7D,wDAAwD;IACxD,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC/C,yCAAyC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACtD;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpE;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EACV,WAAW,GACX,CAAC,CAAC,KAAK,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,KAAK,WAAW,CAAC,CAAC;IAChE;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,CACxB,KAAK,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,KACrC,MAAM,GAAG,IAAI,CAAC;IACnB;;OAEG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CAC/B,CAAC;AAEF;;GAEG;AACH,KAAK,gBAAgB,GAAG;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;;;;GASG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB;CAOpD;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,wBAAyB,SAAQ,iBAAiB;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB;CAOpD"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/webhook/types.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,iBAAiB,EACjB,KAAK,OAAO,EACb,MAAM,gBAAgB,CAAC;AAExB;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,OAAO,EAAE,CAAC,IAClD,CAAC,GACD,CAAC,CAAC,KAAK,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;AAEtD;;;;GAIG;AACH,MAAM,MAAM,WAAW,GACnB,MAAM,GACN;IAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GACjC,SAAS,OAAO,EAAE,CAAC;AAEvB;;;;GAIG;AACH,MAAM,MAAM,aAAa,CAAC,OAAO,SAAS,OAAO,GAAG,OAAO,IAAI;IAC7D,wDAAwD;IACxD,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC/C,yCAAyC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACtD;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpE;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EACV,WAAW,GACX,CAAC,CAAC,KAAK,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,KAAK,WAAW,CAAC,CAAC;IAChE;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,CACxB,KAAK,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,KACrC,MAAM,GAAG,IAAI,CAAC;IACnB;;OAEG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB;CAOzD;AAED;;;;;;;;;;GAUG;AACH,qBAAa,qBAAsB,SAAQ,iBAAiB;IAC1D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB;CAOzD;AAED;;;;;;;GAOG;AACH,qBAAa,YAAa,SAAQ,kBAAkB;gBACtC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB;CAIzD;AAED;;;;;;GAMG;AACH,qBAAa,wBAAyB,SAAQ,qBAAqB;gBACrD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB;CAIzD"}
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/api/index.ts
21
+ var api_exports = {};
22
+ __export(api_exports, {
23
+ ERROR_MAP: () => ERROR_MAP,
24
+ toApiError: () => toApiError,
25
+ withIdempotency: () => withIdempotency
26
+ });
27
+ module.exports = __toCommonJS(api_exports);
28
+
29
+ // src/api/errors.ts
30
+ var import_act = require("@rotorsoft/act");
31
+ var ERROR_MAP = {
32
+ ValidationError: { status: 422, code: "VALIDATION" },
33
+ InvariantError: { status: 409, code: "INVARIANT" },
34
+ ConcurrencyError: { status: 412, code: "CONCURRENCY" },
35
+ StreamClosedError: { status: 410, code: "STREAM_CLOSED" },
36
+ NonRetryableError: { status: 400, code: "NON_RETRYABLE" }
37
+ };
38
+ var lookupKnown = (err) => {
39
+ if (err instanceof import_act.ValidationError) return { name: "ValidationError" };
40
+ if (err instanceof import_act.InvariantError) return { name: "InvariantError" };
41
+ if (err instanceof import_act.ConcurrencyError) return { name: "ConcurrencyError" };
42
+ if (err instanceof import_act.StreamClosedError) return { name: "StreamClosedError" };
43
+ if (err instanceof import_act.NonRetryableError) return { name: "NonRetryableError" };
44
+ return null;
45
+ };
46
+ function toApiError(err) {
47
+ const known = lookupKnown(err);
48
+ if (known) {
49
+ const entry = ERROR_MAP[known.name];
50
+ return {
51
+ status: entry.status,
52
+ body: {
53
+ error: known.name,
54
+ detail: err.message,
55
+ code: entry.code
56
+ }
57
+ };
58
+ }
59
+ if (err instanceof Error) {
60
+ return {
61
+ status: 500,
62
+ body: { error: "InternalError", detail: err.message, code: "INTERNAL" }
63
+ };
64
+ }
65
+ return {
66
+ status: 500,
67
+ body: { error: "InternalError", code: "INTERNAL" }
68
+ };
69
+ }
70
+
71
+ // src/api/idempotency.ts
72
+ async function withIdempotency(store, key, handler) {
73
+ const fresh = await store.claim(key);
74
+ if (!fresh) {
75
+ return { deduped: true };
76
+ }
77
+ return { deduped: false, result: await handler() };
78
+ }
79
+ // Annotate the CommonJS export names for ESM import in node:
80
+ 0 && (module.exports = {
81
+ ERROR_MAP,
82
+ toApiError,
83
+ withIdempotency
84
+ });
85
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/api/index.ts","../../src/api/errors.ts","../../src/api/idempotency.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/api\n *\n * Shared utilities for the act-http auto-generated API surfaces.\n * Three concerns that every transport (tRPC, Hono, OpenAPI) has to\n * address — actor extraction, error envelope mapping,\n * `Idempotency-Key` wiring — defined once here and composed by each\n * transport sibling subpath.\n *\n * - {@link ActorExtractor} — the host-supplied closure that resolves\n * an `Actor` from an incoming request. Auth (JWT, session, API\n * key) stays in the host; the package only asks for this one\n * function.\n * - {@link ApiError}, {@link ERROR_MAP}, {@link toApiError} — the\n * uniform error envelope and the status/code mapping every\n * transport uses. Cross-transport consistency by construction.\n * - {@link withIdempotency} — the helper that wraps action handlers\n * in an `Idempotency-Key` claim. Reuses the\n * `@rotorsoft/act-ops/idempotency` contract that\n * `@rotorsoft/act-http/receiver` already speaks, so receivers and\n * generated APIs share one `IdempotencyStore` implementation.\n *\n * Sibling subpaths in the same package consume the utilities here:\n *\n * - `@rotorsoft/act-http/trpc` — tRPC adapter (#843).\n * - `@rotorsoft/act-http/hono` — Hono adapter (#844).\n * - `@rotorsoft/act-http/openapi` — OpenAPI emitter (#845).\n *\n * Existing siblings unrelated to the generated-API work:\n *\n * - `@rotorsoft/act-http/webhook` — outbound POST delivery.\n * - `@rotorsoft/act-http/sse` — incremental state broadcast.\n * - `@rotorsoft/act-http/receiver` — inbound webhook ingestion.\n */\n\nexport type { ActorExtractor } from \"./actor.js\";\nexport {\n type ApiError,\n ERROR_MAP,\n type ErrorMapEntry,\n toApiError,\n} from \"./errors.js\";\nexport { type IdempotencyResult, withIdempotency } from \"./idempotency.js\";\n","import {\n ConcurrencyError,\n InvariantError,\n NonRetryableError,\n StreamClosedError,\n ValidationError,\n} from \"@rotorsoft/act\";\n\n/**\n * Uniform error envelope shipped over the wire by every act-http\n * transport. Hosts get the same shape from REST, tRPC, and OpenAPI —\n * a client that talks to two transports doesn't have to invent two\n * error parsers.\n *\n * - `error` — the framework error name (`\"ValidationError\"`,\n * `\"InvariantError\"`, …). Stable identifier, safe to switch on.\n * - `detail` — the framework's message text. Human-readable; not\n * parsed by clients.\n * - `code` — a machine-readable status code from {@link ERROR_MAP}\n * for clients that prefer enum-style branching over name strings.\n */\nexport type ApiError = {\n error: string;\n detail?: string;\n code?: string;\n};\n\n/**\n * Status + code pair for one known framework error.\n */\nexport type ErrorMapEntry = {\n status: number;\n code: string;\n};\n\n/**\n * The single table that maps framework error types to HTTP status\n * codes and machine-readable codes. One table, three consumers\n * (Hono, tRPC, OpenAPI) — cross-transport consistency by\n * construction.\n *\n * Operators wanting different mappings wrap the generated transport\n * rather than mutating this — the consistency is the load-bearing\n * property, not the specific status codes.\n */\nexport const ERROR_MAP = {\n ValidationError: { status: 422, code: \"VALIDATION\" },\n InvariantError: { status: 409, code: \"INVARIANT\" },\n ConcurrencyError: { status: 412, code: \"CONCURRENCY\" },\n StreamClosedError: { status: 410, code: \"STREAM_CLOSED\" },\n NonRetryableError: { status: 400, code: \"NON_RETRYABLE\" },\n} as const satisfies Record<string, ErrorMapEntry>;\n\nconst lookupKnown = (err: unknown): { name: keyof typeof ERROR_MAP } | null => {\n if (err instanceof ValidationError) return { name: \"ValidationError\" };\n if (err instanceof InvariantError) return { name: \"InvariantError\" };\n if (err instanceof ConcurrencyError) return { name: \"ConcurrencyError\" };\n if (err instanceof StreamClosedError) return { name: \"StreamClosedError\" };\n if (err instanceof NonRetryableError) return { name: \"NonRetryableError\" };\n return null;\n};\n\n/**\n * Translate an unknown thrown value into the canonical\n * {@link ApiError} envelope plus HTTP status. Each transport's error\n * boundary calls this once and forwards the result to the wire.\n *\n * Known framework errors map per {@link ERROR_MAP}. Everything else\n * surfaces as a 500 with `code: \"INTERNAL\"`; the `detail` field is\n * populated when the throw was an `Error` instance, omitted\n * otherwise (a thrown string or object doesn't get to leak its\n * payload to the client).\n */\nexport function toApiError(err: unknown): { status: number; body: ApiError } {\n const known = lookupKnown(err);\n if (known) {\n const entry = ERROR_MAP[known.name];\n return {\n status: entry.status,\n body: {\n error: known.name,\n detail: (err as Error).message,\n code: entry.code,\n },\n };\n }\n if (err instanceof Error) {\n return {\n status: 500,\n body: { error: \"InternalError\", detail: err.message, code: \"INTERNAL\" },\n };\n }\n return {\n status: 500,\n body: { error: \"InternalError\", code: \"INTERNAL\" },\n };\n}\n","import type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\n\n/**\n * Result of a {@link withIdempotency} call.\n *\n * - `{ deduped: false, result }` — the claim was fresh; the handler\n * ran and produced `result`.\n * - `{ deduped: true }` — the claim was already taken; the handler\n * was *not* invoked. The caller decides how to respond (typically\n * a 2xx with no body, matching the receiver-side convention).\n *\n * Note: the contract does not cache the previous response. A\n * duplicate call returns the deduped marker only — replaying the\n * original handler's output would require a response-caching\n * adapter, which is out of scope here. The receiver-side convention\n * (and the convention the generated transports follow) is \"ack the\n * duplicate; do nothing else.\"\n */\nexport type IdempotencyResult<T> =\n | { deduped: false; result: T }\n | { deduped: true };\n\n/**\n * Wrap an action handler so the framework honors `Idempotency-Key`\n * dedup. Acquires the key via {@link IdempotencyStore.claim}, runs\n * the handler exactly when the claim was fresh, and skips the\n * handler entirely on a duplicate.\n *\n * Reuses the contract `@rotorsoft/act-ops/idempotency` already\n * defines for the receiver-side `Idempotency-Key` story. A single\n * `IdempotencyStore` implementation covers both halves of the \"Act\n * over the wire\" surface — receiver and generated API.\n */\nexport async function withIdempotency<T>(\n store: IdempotencyStore,\n key: string,\n handler: () => Promise<T>\n): Promise<IdempotencyResult<T>> {\n const fresh = await store.claim(key);\n if (!fresh) {\n return { deduped: true };\n }\n return { deduped: false, result: await handler() };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,iBAMO;AAuCA,IAAM,YAAY;AAAA,EACvB,iBAAiB,EAAE,QAAQ,KAAK,MAAM,aAAa;AAAA,EACnD,gBAAgB,EAAE,QAAQ,KAAK,MAAM,YAAY;AAAA,EACjD,kBAAkB,EAAE,QAAQ,KAAK,MAAM,cAAc;AAAA,EACrD,mBAAmB,EAAE,QAAQ,KAAK,MAAM,gBAAgB;AAAA,EACxD,mBAAmB,EAAE,QAAQ,KAAK,MAAM,gBAAgB;AAC1D;AAEA,IAAM,cAAc,CAAC,QAA0D;AAC7E,MAAI,eAAe,2BAAiB,QAAO,EAAE,MAAM,kBAAkB;AACrE,MAAI,eAAe,0BAAgB,QAAO,EAAE,MAAM,iBAAiB;AACnE,MAAI,eAAe,4BAAkB,QAAO,EAAE,MAAM,mBAAmB;AACvE,MAAI,eAAe,6BAAmB,QAAO,EAAE,MAAM,oBAAoB;AACzE,MAAI,eAAe,6BAAmB,QAAO,EAAE,MAAM,oBAAoB;AACzE,SAAO;AACT;AAaO,SAAS,WAAW,KAAkD;AAC3E,QAAM,QAAQ,YAAY,GAAG;AAC7B,MAAI,OAAO;AACT,UAAM,QAAQ,UAAU,MAAM,IAAI;AAClC,WAAO;AAAA,MACL,QAAQ,MAAM;AAAA,MACd,MAAM;AAAA,QACJ,OAAO,MAAM;AAAA,QACb,QAAS,IAAc;AAAA,QACvB,MAAM,MAAM;AAAA,MACd;AAAA,IACF;AAAA,EACF;AACA,MAAI,eAAe,OAAO;AACxB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,EAAE,OAAO,iBAAiB,QAAQ,IAAI,SAAS,MAAM,WAAW;AAAA,IACxE;AAAA,EACF;AACA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM,EAAE,OAAO,iBAAiB,MAAM,WAAW;AAAA,EACnD;AACF;;;AC/DA,eAAsB,gBACpB,OACA,KACA,SAC+B;AAC/B,QAAM,QAAQ,MAAM,MAAM,MAAM,GAAG;AACnC,MAAI,CAAC,OAAO;AACV,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AACA,SAAO,EAAE,SAAS,OAAO,QAAQ,MAAM,QAAQ,EAAE;AACnD;","names":[]}
@@ -0,0 +1,62 @@
1
+ // src/api/errors.ts
2
+ import {
3
+ ConcurrencyError,
4
+ InvariantError,
5
+ NonRetryableError,
6
+ StreamClosedError,
7
+ ValidationError
8
+ } from "@rotorsoft/act";
9
+ var ERROR_MAP = {
10
+ ValidationError: { status: 422, code: "VALIDATION" },
11
+ InvariantError: { status: 409, code: "INVARIANT" },
12
+ ConcurrencyError: { status: 412, code: "CONCURRENCY" },
13
+ StreamClosedError: { status: 410, code: "STREAM_CLOSED" },
14
+ NonRetryableError: { status: 400, code: "NON_RETRYABLE" }
15
+ };
16
+ var lookupKnown = (err) => {
17
+ if (err instanceof ValidationError) return { name: "ValidationError" };
18
+ if (err instanceof InvariantError) return { name: "InvariantError" };
19
+ if (err instanceof ConcurrencyError) return { name: "ConcurrencyError" };
20
+ if (err instanceof StreamClosedError) return { name: "StreamClosedError" };
21
+ if (err instanceof NonRetryableError) return { name: "NonRetryableError" };
22
+ return null;
23
+ };
24
+ function toApiError(err) {
25
+ const known = lookupKnown(err);
26
+ if (known) {
27
+ const entry = ERROR_MAP[known.name];
28
+ return {
29
+ status: entry.status,
30
+ body: {
31
+ error: known.name,
32
+ detail: err.message,
33
+ code: entry.code
34
+ }
35
+ };
36
+ }
37
+ if (err instanceof Error) {
38
+ return {
39
+ status: 500,
40
+ body: { error: "InternalError", detail: err.message, code: "INTERNAL" }
41
+ };
42
+ }
43
+ return {
44
+ status: 500,
45
+ body: { error: "InternalError", code: "INTERNAL" }
46
+ };
47
+ }
48
+
49
+ // src/api/idempotency.ts
50
+ async function withIdempotency(store, key, handler) {
51
+ const fresh = await store.claim(key);
52
+ if (!fresh) {
53
+ return { deduped: true };
54
+ }
55
+ return { deduped: false, result: await handler() };
56
+ }
57
+ export {
58
+ ERROR_MAP,
59
+ toApiError,
60
+ withIdempotency
61
+ };
62
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/api/errors.ts","../../src/api/idempotency.ts"],"sourcesContent":["import {\n ConcurrencyError,\n InvariantError,\n NonRetryableError,\n StreamClosedError,\n ValidationError,\n} from \"@rotorsoft/act\";\n\n/**\n * Uniform error envelope shipped over the wire by every act-http\n * transport. Hosts get the same shape from REST, tRPC, and OpenAPI —\n * a client that talks to two transports doesn't have to invent two\n * error parsers.\n *\n * - `error` — the framework error name (`\"ValidationError\"`,\n * `\"InvariantError\"`, …). Stable identifier, safe to switch on.\n * - `detail` — the framework's message text. Human-readable; not\n * parsed by clients.\n * - `code` — a machine-readable status code from {@link ERROR_MAP}\n * for clients that prefer enum-style branching over name strings.\n */\nexport type ApiError = {\n error: string;\n detail?: string;\n code?: string;\n};\n\n/**\n * Status + code pair for one known framework error.\n */\nexport type ErrorMapEntry = {\n status: number;\n code: string;\n};\n\n/**\n * The single table that maps framework error types to HTTP status\n * codes and machine-readable codes. One table, three consumers\n * (Hono, tRPC, OpenAPI) — cross-transport consistency by\n * construction.\n *\n * Operators wanting different mappings wrap the generated transport\n * rather than mutating this — the consistency is the load-bearing\n * property, not the specific status codes.\n */\nexport const ERROR_MAP = {\n ValidationError: { status: 422, code: \"VALIDATION\" },\n InvariantError: { status: 409, code: \"INVARIANT\" },\n ConcurrencyError: { status: 412, code: \"CONCURRENCY\" },\n StreamClosedError: { status: 410, code: \"STREAM_CLOSED\" },\n NonRetryableError: { status: 400, code: \"NON_RETRYABLE\" },\n} as const satisfies Record<string, ErrorMapEntry>;\n\nconst lookupKnown = (err: unknown): { name: keyof typeof ERROR_MAP } | null => {\n if (err instanceof ValidationError) return { name: \"ValidationError\" };\n if (err instanceof InvariantError) return { name: \"InvariantError\" };\n if (err instanceof ConcurrencyError) return { name: \"ConcurrencyError\" };\n if (err instanceof StreamClosedError) return { name: \"StreamClosedError\" };\n if (err instanceof NonRetryableError) return { name: \"NonRetryableError\" };\n return null;\n};\n\n/**\n * Translate an unknown thrown value into the canonical\n * {@link ApiError} envelope plus HTTP status. Each transport's error\n * boundary calls this once and forwards the result to the wire.\n *\n * Known framework errors map per {@link ERROR_MAP}. Everything else\n * surfaces as a 500 with `code: \"INTERNAL\"`; the `detail` field is\n * populated when the throw was an `Error` instance, omitted\n * otherwise (a thrown string or object doesn't get to leak its\n * payload to the client).\n */\nexport function toApiError(err: unknown): { status: number; body: ApiError } {\n const known = lookupKnown(err);\n if (known) {\n const entry = ERROR_MAP[known.name];\n return {\n status: entry.status,\n body: {\n error: known.name,\n detail: (err as Error).message,\n code: entry.code,\n },\n };\n }\n if (err instanceof Error) {\n return {\n status: 500,\n body: { error: \"InternalError\", detail: err.message, code: \"INTERNAL\" },\n };\n }\n return {\n status: 500,\n body: { error: \"InternalError\", code: \"INTERNAL\" },\n };\n}\n","import type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\n\n/**\n * Result of a {@link withIdempotency} call.\n *\n * - `{ deduped: false, result }` — the claim was fresh; the handler\n * ran and produced `result`.\n * - `{ deduped: true }` — the claim was already taken; the handler\n * was *not* invoked. The caller decides how to respond (typically\n * a 2xx with no body, matching the receiver-side convention).\n *\n * Note: the contract does not cache the previous response. A\n * duplicate call returns the deduped marker only — replaying the\n * original handler's output would require a response-caching\n * adapter, which is out of scope here. The receiver-side convention\n * (and the convention the generated transports follow) is \"ack the\n * duplicate; do nothing else.\"\n */\nexport type IdempotencyResult<T> =\n | { deduped: false; result: T }\n | { deduped: true };\n\n/**\n * Wrap an action handler so the framework honors `Idempotency-Key`\n * dedup. Acquires the key via {@link IdempotencyStore.claim}, runs\n * the handler exactly when the claim was fresh, and skips the\n * handler entirely on a duplicate.\n *\n * Reuses the contract `@rotorsoft/act-ops/idempotency` already\n * defines for the receiver-side `Idempotency-Key` story. A single\n * `IdempotencyStore` implementation covers both halves of the \"Act\n * over the wire\" surface — receiver and generated API.\n */\nexport async function withIdempotency<T>(\n store: IdempotencyStore,\n key: string,\n handler: () => Promise<T>\n): Promise<IdempotencyResult<T>> {\n const fresh = await store.claim(key);\n if (!fresh) {\n return { deduped: true };\n }\n return { deduped: false, result: await handler() };\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAuCA,IAAM,YAAY;AAAA,EACvB,iBAAiB,EAAE,QAAQ,KAAK,MAAM,aAAa;AAAA,EACnD,gBAAgB,EAAE,QAAQ,KAAK,MAAM,YAAY;AAAA,EACjD,kBAAkB,EAAE,QAAQ,KAAK,MAAM,cAAc;AAAA,EACrD,mBAAmB,EAAE,QAAQ,KAAK,MAAM,gBAAgB;AAAA,EACxD,mBAAmB,EAAE,QAAQ,KAAK,MAAM,gBAAgB;AAC1D;AAEA,IAAM,cAAc,CAAC,QAA0D;AAC7E,MAAI,eAAe,gBAAiB,QAAO,EAAE,MAAM,kBAAkB;AACrE,MAAI,eAAe,eAAgB,QAAO,EAAE,MAAM,iBAAiB;AACnE,MAAI,eAAe,iBAAkB,QAAO,EAAE,MAAM,mBAAmB;AACvE,MAAI,eAAe,kBAAmB,QAAO,EAAE,MAAM,oBAAoB;AACzE,MAAI,eAAe,kBAAmB,QAAO,EAAE,MAAM,oBAAoB;AACzE,SAAO;AACT;AAaO,SAAS,WAAW,KAAkD;AAC3E,QAAM,QAAQ,YAAY,GAAG;AAC7B,MAAI,OAAO;AACT,UAAM,QAAQ,UAAU,MAAM,IAAI;AAClC,WAAO;AAAA,MACL,QAAQ,MAAM;AAAA,MACd,MAAM;AAAA,QACJ,OAAO,MAAM;AAAA,QACb,QAAS,IAAc;AAAA,QACvB,MAAM,MAAM;AAAA,MACd;AAAA,IACF;AAAA,EACF;AACA,MAAI,eAAe,OAAO;AACxB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,EAAE,OAAO,iBAAiB,QAAQ,IAAI,SAAS,MAAM,WAAW;AAAA,IACxE;AAAA,EACF;AACA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM,EAAE,OAAO,iBAAiB,MAAM,WAAW;AAAA,EACnD;AACF;;;AC/DA,eAAsB,gBACpB,OACA,KACA,SAC+B;AAC/B,QAAM,QAAQ,MAAM,MAAM,MAAM,GAAG;AACnC,MAAI,CAAC,OAAO;AACV,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AACA,SAAO,EAAE,SAAS,OAAO,QAAQ,MAAM,QAAQ,EAAE;AACnD;","names":[]}
@@ -0,0 +1,29 @@
1
+ import {
2
+ checkWebhook
3
+ } from "./chunk-NOIXOF2I.js";
4
+
5
+ // src/receiver/hono/index.ts
6
+ function webhookMiddleware(options) {
7
+ return async function check(c, next) {
8
+ const headers = headersBag(c.req.raw.headers);
9
+ const rawBody = await c.req.text();
10
+ const result = await checkWebhook(headers, rawBody, options);
11
+ if (!result.ok) {
12
+ return c.json({ error: result.reason }, result.status);
13
+ }
14
+ c.set("idempotency", { key: result.key, deduped: result.deduped });
15
+ await next();
16
+ };
17
+ }
18
+ function headersBag(headers) {
19
+ const out = {};
20
+ headers.forEach((value, key) => {
21
+ out[key] = value;
22
+ });
23
+ return out;
24
+ }
25
+
26
+ export {
27
+ webhookMiddleware
28
+ };
29
+ //# sourceMappingURL=chunk-F7VWYZ37.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/receiver/hono/index.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/receiver/hono\n *\n * Hono adapter for the receiver-side webhook check.\n *\n * Usage:\n *\n * ```ts\n * import { Hono } from \"hono\";\n * import { webhookMiddleware } from \"@rotorsoft/act-http/receiver/hono\";\n * import { InMemoryIdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\n *\n * const app = new Hono();\n * const dedup = new InMemoryIdempotencyStore();\n *\n * app.post(\n * \"/webhooks/orders\",\n * webhookMiddleware({ store: dedup, secret: process.env.WEBHOOK_SECRET }),\n * async (c) => {\n * const idem = c.get(\"idempotency\") as { key: string; deduped: boolean };\n * if (idem.deduped) return c.json({ status: \"dedup-skipped\", key: idem.key });\n * // ... process the inbound event ...\n * return c.json({ status: \"processed\", key: idem.key });\n * }\n * );\n * ```\n *\n * On failure: returns `c.json({ error: <reason> }, status)` directly\n * (Hono short-circuits when middleware returns a Response). On\n * success: stashes `c.set(\"idempotency\", { key, deduped })` and\n * continues with `await next()`.\n *\n * **Raw body**: Hono exposes `await c.req.text()` natively, which\n * the middleware reads when `secret` is configured. No extra setup\n * needed.\n */\nimport type { MiddlewareHandler } from \"hono\";\nimport { type CheckWebhookOptions, checkWebhook } from \"../check.js\";\n\n/**\n * Variables this middleware contributes to the Hono context. The\n * generic on the returned {@link MiddlewareHandler} threads it\n * through so route handlers downstream of `app.post(..., webhookMiddleware(...), handler)`\n * see `c.get(\"idempotency\")` typed without a manual cast.\n */\nexport type WebhookVariables = {\n idempotency: { key: string; deduped: boolean };\n};\n\n/**\n * Build a Hono middleware that verifies the request signature (when\n * `secret` is set), enforces `Idempotency-Key`, and claims the key\n * on the configured store. See the module-level docs for usage.\n */\nexport function webhookMiddleware(\n options: CheckWebhookOptions\n): MiddlewareHandler<{ Variables: WebhookVariables }> {\n return async function check(c, next) {\n const headers = headersBag(c.req.raw.headers);\n const rawBody = await c.req.text();\n const result = await checkWebhook(headers, rawBody, options);\n if (!result.ok) {\n return c.json({ error: result.reason }, result.status);\n }\n c.set(\"idempotency\", { key: result.key, deduped: result.deduped });\n await next();\n };\n}\n\nfunction headersBag(\n headers: Headers\n): Record<string, string | string[] | undefined> {\n const out: Record<string, string | string[] | undefined> = {};\n headers.forEach((value, key) => {\n out[key] = value;\n });\n return out;\n}\n"],"mappings":";;;;;AAuDO,SAAS,kBACd,SACoD;AACpD,SAAO,eAAe,MAAM,GAAG,MAAM;AACnC,UAAM,UAAU,WAAW,EAAE,IAAI,IAAI,OAAO;AAC5C,UAAM,UAAU,MAAM,EAAE,IAAI,KAAK;AACjC,UAAM,SAAS,MAAM,aAAa,SAAS,SAAS,OAAO;AAC3D,QAAI,CAAC,OAAO,IAAI;AACd,aAAO,EAAE,KAAK,EAAE,OAAO,OAAO,OAAO,GAAG,OAAO,MAAM;AAAA,IACvD;AACA,MAAE,IAAI,eAAe,EAAE,KAAK,OAAO,KAAK,SAAS,OAAO,QAAQ,CAAC;AACjE,UAAM,KAAK;AAAA,EACb;AACF;AAEA,SAAS,WACP,SAC+C;AAC/C,QAAM,MAAqD,CAAC;AAC5D,UAAQ,QAAQ,CAAC,OAAO,QAAQ;AAC9B,QAAI,GAAG,IAAI;AAAA,EACb,CAAC;AACD,SAAO;AACT;","names":[]}
@@ -0,0 +1,78 @@
1
+ // src/receiver/extract.ts
2
+ function extractIdempotencyKey(headers) {
3
+ for (const [name, value] of Object.entries(headers)) {
4
+ if (name.toLowerCase() !== "idempotency-key") continue;
5
+ if (Array.isArray(value)) return void 0;
6
+ if (value === "") return void 0;
7
+ return value;
8
+ }
9
+ return void 0;
10
+ }
11
+
12
+ // src/receiver/verify.ts
13
+ import { createHmac, timingSafeEqual } from "crypto";
14
+ function verifyWebhook(headers, body, secret, options) {
15
+ const maxAgeSeconds = options?.maxAgeSeconds ?? 300;
16
+ const now = options?.now ?? Math.floor(Date.now() / 1e3);
17
+ const signature = pickHeader(headers, "x-webhook-signature");
18
+ if (!signature) return { ok: false, reason: "missing-signature" };
19
+ const timestampStr = pickHeader(headers, "x-webhook-timestamp");
20
+ if (!timestampStr) return { ok: false, reason: "missing-timestamp" };
21
+ const timestamp = Number.parseInt(timestampStr, 10);
22
+ if (Number.isNaN(timestamp) || String(timestamp) !== timestampStr) {
23
+ return { ok: false, reason: "missing-timestamp" };
24
+ }
25
+ const delta = now - timestamp;
26
+ if (delta > maxAgeSeconds) return { ok: false, reason: "stale" };
27
+ if (delta < -maxAgeSeconds) return { ok: false, reason: "future" };
28
+ if (!signature.startsWith("sha256=")) {
29
+ return { ok: false, reason: "bad-signature" };
30
+ }
31
+ const providedHex = signature.slice("sha256=".length);
32
+ if (!/^[0-9a-fA-F]{64}$/.test(providedHex)) {
33
+ return { ok: false, reason: "bad-signature" };
34
+ }
35
+ const expectedHex = createHmac("sha256", secret).update(`${timestampStr}.${body}`).digest("hex");
36
+ const a = Buffer.from(providedHex, "hex");
37
+ const b = Buffer.from(expectedHex, "hex");
38
+ if (!timingSafeEqual(a, b)) {
39
+ return { ok: false, reason: "bad-signature" };
40
+ }
41
+ return { ok: true };
42
+ }
43
+ function pickHeader(headers, lowerName) {
44
+ for (const [name, value] of Object.entries(headers)) {
45
+ if (name.toLowerCase() !== lowerName) continue;
46
+ if (Array.isArray(value) || value === void 0 || value === "") {
47
+ return void 0;
48
+ }
49
+ return value;
50
+ }
51
+ return void 0;
52
+ }
53
+
54
+ // src/receiver/check.ts
55
+ async function checkWebhook(headers, body, options) {
56
+ if (options.secret !== void 0) {
57
+ const verification = verifyWebhook(
58
+ headers,
59
+ body,
60
+ options.secret,
61
+ options.verify
62
+ );
63
+ if (!verification.ok) {
64
+ return { ok: false, status: 401, reason: verification.reason };
65
+ }
66
+ }
67
+ const key = extractIdempotencyKey(headers);
68
+ if (!key) return { ok: false, status: 400, reason: "missing-key" };
69
+ const claimed = await options.store.claim(key);
70
+ return { ok: true, key, deduped: !claimed };
71
+ }
72
+
73
+ export {
74
+ extractIdempotencyKey,
75
+ verifyWebhook,
76
+ checkWebhook
77
+ };
78
+ //# sourceMappingURL=chunk-NOIXOF2I.js.map
@@ -0,0 +1 @@
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":[]}