@volter/world-core 2.0.37 → 3.0.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 (205) hide show
  1. package/README.md +4 -5
  2. package/app-route.cjs +12 -6
  3. package/app-route.d.cts +1 -1
  4. package/dist/app-route.cjs +12 -6
  5. package/dist/app-route.d.cts +1 -1
  6. package/dist/generated/pack-facts.json +1410 -3069
  7. package/dist/inject.cjs +64 -9
  8. package/dist/pack-facts.cjs +44 -0
  9. package/dist/src/actions.d.ts +3 -3
  10. package/dist/src/actions.js +22 -16
  11. package/dist/src/ancestry.d.ts +14 -2
  12. package/dist/src/ancestry.js +92 -2
  13. package/dist/src/anthropic-wire.d.ts +39 -0
  14. package/dist/src/anthropic-wire.js +136 -0
  15. package/dist/src/bytes.d.ts +7 -0
  16. package/dist/src/bytes.js +35 -0
  17. package/dist/src/changeset.d.ts +1 -1
  18. package/dist/src/changeset.js +0 -0
  19. package/dist/src/clickhouse/index.d.ts +3 -0
  20. package/dist/src/clickhouse/index.js +6 -0
  21. package/dist/src/clickhouse/sql.d.ts +233 -0
  22. package/dist/src/clickhouse/sql.js +4329 -0
  23. package/dist/src/clickhouse/types.d.ts +18 -0
  24. package/dist/src/clickhouse/types.js +47 -0
  25. package/dist/src/clickhouse/values.d.ts +146 -0
  26. package/dist/src/clickhouse/values.js +858 -0
  27. package/dist/src/client-bundle.js +2 -3
  28. package/dist/src/cors.d.ts +15 -0
  29. package/dist/src/cors.js +31 -0
  30. package/dist/src/derived-core.d.ts +487 -24
  31. package/dist/src/derived-core.js +788 -144
  32. package/dist/src/derived-real.d.ts +13 -0
  33. package/dist/src/derived-real.js +518 -0
  34. package/dist/src/derived.d.ts +35 -1
  35. package/dist/src/derived.js +61 -9
  36. package/dist/src/emit.js +1 -2
  37. package/dist/src/events.d.ts +206 -0
  38. package/dist/src/events.js +341 -0
  39. package/dist/src/executor.d.ts +3 -0
  40. package/dist/src/executor.js +19 -2
  41. package/dist/src/file-response.d.ts +6 -0
  42. package/dist/src/file-response.js +30 -0
  43. package/dist/src/fork.js +3 -2
  44. package/dist/src/git/history.d.ts +7 -0
  45. package/dist/src/git/history.js +24 -0
  46. package/dist/src/git/index.d.ts +1 -0
  47. package/dist/src/git/index.js +1 -0
  48. package/dist/src/git/lfs.d.ts +28 -0
  49. package/dist/src/git/lfs.js +66 -0
  50. package/dist/src/git/objects.js +3 -8
  51. package/dist/src/git/smart-http.d.ts +3 -1
  52. package/dist/src/git/smart-http.js +67 -6
  53. package/dist/src/graphql-wire.d.ts +29 -0
  54. package/dist/src/graphql-wire.js +101 -0
  55. package/dist/src/grpc-wire.d.ts +67 -0
  56. package/dist/src/grpc-wire.js +170 -0
  57. package/dist/src/h2.d.ts +40 -0
  58. package/dist/src/h2.js +656 -0
  59. package/dist/src/head.d.ts +32 -3
  60. package/dist/src/head.js +161 -40
  61. package/dist/src/history.d.ts +1 -1
  62. package/dist/src/history.js +6 -6
  63. package/dist/src/hpack.json +1 -0
  64. package/dist/src/index.d.ts +64 -75
  65. package/dist/src/index.js +58 -101
  66. package/dist/src/log.js +28 -19
  67. package/dist/src/machines.d.ts +50 -0
  68. package/dist/src/machines.js +151 -0
  69. package/dist/src/managed-database.d.ts +86 -0
  70. package/dist/src/managed-database.js +283 -0
  71. package/dist/src/multipart.d.ts +11 -0
  72. package/dist/src/multipart.js +51 -0
  73. package/dist/src/observe.d.ts +15 -5
  74. package/dist/src/observe.js +23 -9
  75. package/dist/src/openai-wire.d.ts +108 -0
  76. package/dist/src/openai-wire.js +337 -0
  77. package/dist/src/pack-assets.d.ts +3 -4
  78. package/dist/src/pack-assets.js +15 -10
  79. package/dist/src/pack-fetch.d.ts +77 -0
  80. package/dist/src/pack-fetch.js +449 -0
  81. package/dist/src/pack-paths.d.ts +12 -0
  82. package/dist/src/pack-paths.js +86 -0
  83. package/dist/src/packRegistry.d.ts +69 -162
  84. package/dist/src/packRegistry.js +55 -20
  85. package/dist/src/people.d.ts +13 -0
  86. package/dist/src/people.js +18 -0
  87. package/dist/src/placeholder-image.d.ts +5 -0
  88. package/dist/src/placeholder-image.js +114 -0
  89. package/dist/src/protobuf.d.ts +28 -0
  90. package/dist/src/protobuf.js +332 -0
  91. package/dist/src/redis/engine.js +1 -1
  92. package/dist/src/request-scope.d.ts +1 -1
  93. package/dist/src/request-scope.js +6 -4
  94. package/dist/src/resource-blob.d.ts +5 -0
  95. package/dist/src/resource-blob.js +11 -0
  96. package/dist/src/runtime.d.ts +85 -0
  97. package/dist/src/runtime.js +104 -0
  98. package/dist/src/s3/wire.d.ts +60 -0
  99. package/dist/src/s3/wire.js +157 -0
  100. package/dist/src/scenario.d.ts +3 -0
  101. package/dist/src/scenario.js +2 -0
  102. package/dist/src/schema-sample.d.ts +1 -0
  103. package/dist/src/schema-sample.js +21 -0
  104. package/dist/src/sealed-box.d.ts +14 -0
  105. package/dist/src/sealed-box.js +225 -0
  106. package/dist/src/serve-http.d.ts +14 -0
  107. package/dist/src/serve-http.js +27 -3
  108. package/dist/src/serve.d.ts +6 -0
  109. package/dist/src/serve.js +69 -14
  110. package/dist/src/signing.d.ts +135 -0
  111. package/dist/src/signing.js +222 -0
  112. package/dist/src/sigv4.d.ts +48 -0
  113. package/dist/src/sigv4.js +167 -0
  114. package/dist/src/smtp.d.ts +16 -0
  115. package/dist/src/smtp.js +72 -0
  116. package/dist/src/sockets.d.ts +51 -0
  117. package/dist/src/sockets.js +90 -0
  118. package/dist/src/state-system.d.ts +1 -0
  119. package/dist/src/state-system.js +1 -1
  120. package/dist/src/storage.d.ts +1 -1
  121. package/dist/src/storage.js +3 -3
  122. package/dist/src/trace-context.js +1 -1
  123. package/dist/src/twin-fetch.d.ts +0 -7
  124. package/dist/src/twin-fetch.js +0 -14
  125. package/dist/src/vendor-call.d.ts +6 -0
  126. package/dist/src/vendor-call.js +41 -0
  127. package/dist/src/world-store.js +1 -1
  128. package/dist/vendor-hosts.cjs +36 -125
  129. package/dist/vendor-hosts.d.cts +8 -0
  130. package/generated/pack-facts.json +1410 -3069
  131. package/inject.cjs +64 -9
  132. package/pack-facts.cjs +44 -0
  133. package/package.json +17 -3
  134. package/src/actions.ts +23 -16
  135. package/src/ancestry.ts +74 -2
  136. package/src/anthropic-wire.ts +137 -0
  137. package/src/bytes.ts +42 -0
  138. package/src/changeset.ts +5 -5
  139. package/src/clickhouse/index.ts +6 -0
  140. package/src/clickhouse/sql.ts +3059 -0
  141. package/src/clickhouse/types.ts +44 -0
  142. package/src/clickhouse/values.ts +697 -0
  143. package/src/client-bundle.ts +2 -3
  144. package/src/cors.ts +34 -0
  145. package/src/derived-core.ts +1013 -146
  146. package/src/derived-real.ts +434 -0
  147. package/src/derived.ts +73 -3
  148. package/src/emit.ts +1 -2
  149. package/src/events.ts +449 -0
  150. package/src/executor.ts +24 -2
  151. package/src/file-response.ts +27 -0
  152. package/src/fork.ts +3 -2
  153. package/src/git/history.ts +19 -0
  154. package/src/git/index.ts +1 -0
  155. package/src/git/lfs.ts +67 -0
  156. package/src/git/objects.ts +3 -5
  157. package/src/git/smart-http.ts +56 -6
  158. package/src/graphql-wire.ts +106 -0
  159. package/src/grpc-wire.ts +159 -0
  160. package/src/h2.ts +627 -0
  161. package/src/head.ts +132 -41
  162. package/src/history.ts +6 -6
  163. package/src/hpack.json +1 -0
  164. package/src/index.ts +82 -329
  165. package/src/log.ts +27 -18
  166. package/src/machines.ts +151 -0
  167. package/src/managed-database.ts +299 -0
  168. package/src/multipart.ts +51 -0
  169. package/src/observe.ts +31 -15
  170. package/src/openai-wire.ts +371 -0
  171. package/src/pack-assets.ts +15 -11
  172. package/src/pack-fetch.ts +458 -0
  173. package/src/pack-paths.ts +72 -0
  174. package/src/packRegistry.ts +79 -167
  175. package/src/people.ts +31 -0
  176. package/src/placeholder-image.ts +88 -0
  177. package/src/protobuf.ts +251 -0
  178. package/src/redis/engine.ts +1 -1
  179. package/src/request-scope.ts +8 -4
  180. package/src/resource-blob.ts +13 -0
  181. package/src/runtime.ts +344 -0
  182. package/src/s3/wire.ts +172 -0
  183. package/src/scenario.ts +4 -0
  184. package/src/schema-sample.ts +24 -0
  185. package/src/sealed-box.ts +182 -0
  186. package/src/serve-http.ts +31 -3
  187. package/src/serve.ts +58 -14
  188. package/src/signing.ts +231 -0
  189. package/src/sigv4.ts +158 -0
  190. package/src/smtp.ts +76 -0
  191. package/src/sockets.ts +140 -0
  192. package/src/state-system.ts +2 -2
  193. package/src/storage.ts +3 -3
  194. package/src/trace-context.ts +1 -1
  195. package/src/twin-fetch.ts +0 -20
  196. package/src/vendor-call.ts +41 -0
  197. package/src/world-store.ts +1 -1
  198. package/vendor-hosts.cjs +36 -125
  199. package/vendor-hosts.d.cts +8 -0
  200. package/dist/src/mirror-shell.d.ts +0 -2
  201. package/dist/src/mirror-shell.js +0 -13
  202. package/dist/src/v1-removed.d.ts +0 -159
  203. package/dist/src/v1-removed.js +0 -124
  204. package/src/mirror-shell.ts +0 -15
  205. package/src/v1-removed.ts +0 -172
@@ -1,11 +1,54 @@
1
- // THE DERIVED DISPATCH — the generated wire's router (docs/contributing/architecture.md, "The derived
2
- // pack"). A derived pack's surface (`src/generated/surface.gen.json`, written by scripts/derive-pack.ts)
3
- // names every operation the vendor publishes. This matches a request to one of them and hands it to its
4
- // owner: the pack's semantics handler for that operationId when there is one, otherwise the pack's
5
- // existing fetch, which keeps serving everything not yet moved (and every route outside the spec, such
6
- // as the `/twin` door). `owners` says, per operation, which of the two serves it: the count a pack's
7
- // move is measured by. Workerd-clean: no fs, no clock, nothing at import.
8
1
  import { runWithRequestTrace } from "./trace-context.js";
2
+ /**
3
+ * A request's body as text, read once per request: the dispatch reads it and tags the request with it, and every clone
4
+ * made from a tagged request carries the tag, so the malformed-JSON guard, the idempotency signature, the handler's
5
+ * context and the parameters all take the one string (a publish's body is megabytes, and each read made a copy).
6
+ */
7
+ const BODY_TEXT = Symbol('volter.bodyText');
8
+ function tagBody(request, text, key = {}) {
9
+ const tagged = request;
10
+ tagged[BODY_TEXT] = text;
11
+ BODY_PARSE_KEY.set(request, key);
12
+ const clone = request.clone.bind(request);
13
+ tagged.clone = () => tagBody(clone(), text, key);
14
+ return request;
15
+ }
16
+ export async function bodyTextOf(request) {
17
+ const held = request[BODY_TEXT];
18
+ return held !== undefined ? held : request.text();
19
+ }
20
+ /** The body parsed by the malformed-JSON guard, which only checks it: handed once to the handler's context (that parse
21
+ * would be the same), and never shared further, so no reader sees another's changes. */
22
+ const BODY_PARSED = new WeakMap();
23
+ /** The body's text, as JSON, or undefined when it is not JSON; the parse is kept for the first taker. */
24
+ export async function checkJsonBody(request) {
25
+ const text = await bodyTextOf(request);
26
+ if (!text)
27
+ return true;
28
+ // a body labelled otherwise and not shaped as JSON is the form it is labelled (an OAuth token request), not bad JSON
29
+ if (!(request.headers.get('content-type') ?? '').includes('json') && !/^\s*[[{]/.test(text))
30
+ return true;
31
+ try {
32
+ const value = JSON.parse(text);
33
+ const tag = request[BODY_TEXT] !== undefined ? BODY_PARSE_KEY.get(request) : undefined;
34
+ if (tag)
35
+ BODY_PARSED.set(tag, { value });
36
+ return true;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ /** The guard's parse of this request's body, taken once (a clone of a tagged request shares its key). */
43
+ export function takeParsedBody(request) {
44
+ const tag = BODY_PARSE_KEY.get(request);
45
+ const held = tag ? BODY_PARSED.get(tag) : undefined;
46
+ if (tag && held)
47
+ BODY_PARSED.delete(tag);
48
+ return held;
49
+ }
50
+ /** One key per request as the dispatch received it, shared by every clone made from it. */
51
+ const BODY_PARSE_KEY = new WeakMap();
9
52
  class Unmodeled extends Error {
10
53
  }
11
54
  function compile(operation, basePath, spanning) {
@@ -32,7 +75,9 @@ function compile(operation, basePath, spanning) {
32
75
  }).join('');
33
76
  })
34
77
  .join('/');
35
- return { operation, pattern: new RegExp(`^${body}/?$`), names, literals };
78
+ // a path the spec writes with its trailing slash (Django's `/persons/`) is the same path without it, as a router that
79
+ // takes both answers it (PostHog's `trailing_slash = r"/?"`)
80
+ return { operation, pattern: new RegExp(`^${body.replace(/\/$/, '')}/?$`), names, literals };
36
81
  }
37
82
  const compiled = new WeakMap(); // cache: compiled routes per surface; a surface is immutable generated data
38
83
  /** Routes per method, most literal segments first, so `/v1/customers/search` wins over `/v1/customers/{customer}`. */
@@ -78,6 +123,8 @@ export function matchOperation(routes, method, pathname, query, headers) {
78
123
  }
79
124
  return undefined;
80
125
  }
126
+ /** The header the kernel carries a path prefix's named groups in, from routing to the call (never a client's). */
127
+ export const PREFIX_PARAMS_HEADER = 'x-volter-path-prefix-params';
81
128
  export function createDerivedFetch(options) {
82
129
  const handlers = options.handlers ?? {};
83
130
  const known = new Set(options.surface.operations.map((o) => o.id));
@@ -95,7 +142,12 @@ export function createDerivedFetch(options) {
95
142
  : matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers);
96
143
  if (!matched)
97
144
  return fallback(request, undefined, 'no operation at this path');
98
- const call = { request, operation: matched.operation, params: matched.params };
145
+ // the body read once, as text, for everything the dispatch does with it (bodyTextOf)
146
+ if (request.method !== 'GET' && request.method !== 'HEAD' && request.body && !(request.headers.get('content-type') ?? '').includes('multipart'))
147
+ tagBody(request, await request.clone().text());
148
+ // the path prefix's named groups (pack-fetch.ts, `pathPrefix`) are the call's parameters too
149
+ const prefixed = request.headers.get(PREFIX_PARAMS_HEADER);
150
+ const call = { request, operation: matched.operation, params: prefixed ? { ...JSON.parse(prefixed), ...matched.params } : matched.params };
99
151
  const around = options.around ?? ((_c, next) => next());
100
152
  const handler = handlers[matched.operation.id];
101
153
  if (handler)
package/dist/src/emit.js CHANGED
@@ -6,8 +6,7 @@
6
6
  // `TwinEmitter` and registered on its `TwinPack.emitter` (same argument as `browserRouting`:
7
7
  // vendor knowledge in the descriptor, kernel stays vendor-agnostic).
8
8
  //
9
- // The operator surface is the PACK's bin (`world-stripe emit …`), exactly like the mirror
10
- // UIs — the kernel never imports packs, so `volter-twin emit <service>` only works when a
9
+ // The operator surface is the PACK's bin (`world-stripe emit …`) — the kernel never imports packs, so `volter-twin emit <service>` only works when a
11
10
  // consumer registered the pack in-process, and otherwise fails loudly pointing at the
12
11
  // vendor bin. Unlike the twin's own background emission on state changes (fire-and-forget,
13
12
  // vendor-faithful), an explicit DELIVER is an operator asking for exactly this delivery, so
@@ -0,0 +1,206 @@
1
+ /** How the vendor signs a delivery. */
2
+ export type EventScheme =
3
+ /** A message id, a timestamp and a signature header (the Standard Webhooks shape): `<prefix>-id`,
4
+ * `<prefix>-timestamp` (seconds) and `<prefix>-signature: v1,<base64 HMAC-SHA256>` over `${id}.${timestamp}.${body}`,
5
+ * keyed by the base64 part of the secret after `secretPrefix`, or by the secret's own UTF-8 bytes (`secretKey: 'utf8'`:
6
+ * Polar's, whose SDK base64-encodes the secret as it is before verifying). */
7
+ {
8
+ kind: 'webhook-id';
9
+ prefix: string;
10
+ secretPrefix?: string;
11
+ secretKey?: 'base64' | 'utf8';
12
+ }
13
+ /** One header of a timestamp and its signatures: `<header>: t=<seconds>,v1=<hex HMAC-SHA256>` over `${t}.${body}`,
14
+ * keyed by the secret as it is. */
15
+ | {
16
+ kind: 'timestamp-v1';
17
+ header: string;
18
+ }
19
+ /** One HMAC header over the body (`X-Hub-Signature-256: sha256=<hex>`, GitHub's), or over `${timestamp}.${body}` when
20
+ * the vendor sends its timestamp in `timestampHeader`; `signed` names what is signed otherwise, `{t}` the timestamp
21
+ * and `{body}` the body (Slack's `v0:{t}:{body}`), and `algorithm` the hash (SHA-256 unless named: GitHub's older
22
+ * `X-Hub-Signature` is SHA-1). */
23
+ | {
24
+ kind: 'hmac';
25
+ header: string;
26
+ encoding: 'hex' | 'base64';
27
+ prefix?: string;
28
+ timestampHeader?: string;
29
+ signed?: string;
30
+ algorithm?: 'sha1' | 'sha256';
31
+ secretKey?: 'base64' | 'utf8';
32
+ }
33
+ /** One header of a plain hash (not an HMAC) of the secret and the body (HubSpot's v1 `X-HubSpot-Signature`: the SHA-256
34
+ * of the app's client secret followed by the request body); `signed` names the input, `{secret}` and `{body}` */
35
+ | {
36
+ kind: 'hash';
37
+ header: string;
38
+ signed: string;
39
+ encoding: 'hex' | 'base64';
40
+ algorithm?: 'sha256';
41
+ };
42
+ /** Where the World's endpoints of one kind are kept (a door the vendor's dashboard stands in for writes them): their
43
+ * stored type and the fields holding the URL, the signing secret, the event types an endpoint takes (all when absent,
44
+ * `*` and `<family>.*` patterns) and whether it is disabled (`disabled` true), not enabled (`enabled` false) or not live
45
+ * (`status`). A field may be a dotted path into the row (`config.url`). */
46
+ export type EndpointsDecl = {
47
+ storedAs: string;
48
+ url: string;
49
+ secret: string;
50
+ filter?: string;
51
+ disabled?: string;
52
+ enabled?: string;
53
+ /** An endpoint that is live only while a field holds one value (Stripe's `status: 'enabled'`). */
54
+ status?: {
55
+ field: string;
56
+ live: string;
57
+ };
58
+ /** Which events an endpoint takes by whom they are about: one whose `field` is true takes only events whose
59
+ * envelope value `value` is set (Stripe's Connect endpoint, `connect: true`, takes connected accounts' events, those
60
+ * with an `$account`), and any other only events where it is not. */
61
+ scope?: {
62
+ field: string;
63
+ value: string;
64
+ };
65
+ /** Endpoint fields that must equal an event's values (field → `$value`, or a literal): a repository's hook takes only
66
+ * its repository's events (GitHub's `{ repository: '$repository' }`), an organization's its organization's. A list
67
+ * field takes the event when it holds the value. Several matches: the endpoint takes the event when any holds (a
68
+ * GitHub App's installation: its own events, its account's when it covers all the account's repositories, a listed
69
+ * repository's). */
70
+ match?: Record<string, string> | ReadonlyArray<Record<string, string>>;
71
+ /** Related rows an endpoint's fields read through (`app.hook_url`): each join's name, the stored type it names and the
72
+ * endpoint's field holding its id (a GitHub installation's URL, secret and events are its App's). */
73
+ joins?: Record<string, {
74
+ storedAs: string;
75
+ by: string;
76
+ }>;
77
+ /** The event types this kind of endpoint takes (every type when absent), or those it does not (`skip`): two kinds over
78
+ * the same rows, one taking a type under a `match` the other has no need of (a Slack app is sent `app_mention` only
79
+ * when a message mentions it, and every `message` its subscription names). */
80
+ types?: ReadonlyArray<string>;
81
+ skip?: ReadonlyArray<string>;
82
+ /** What each delivery's answer writes on the endpoint, when the vendor keeps score (Daily's webhook): a success (2xx)
83
+ * resets `failures` and writes `success` (templates: `$time.iso`, `$time.s`), a failure adds one to `failures`, and
84
+ * `after` failures in a row make `move`, the vendor's transition of a state field, asked of the endpoint's machine,
85
+ * on every endpoint but one matching `unless` (Daily's `exponential` retry never breaks). */
86
+ outcome?: EndpointOutcome;
87
+ };
88
+ /** An endpoint kind's delivery outcome (EndpointsDecl.outcome). */
89
+ export type EndpointOutcome = {
90
+ failures: string;
91
+ success?: Record<string, string>;
92
+ after?: number;
93
+ move?: {
94
+ field: string;
95
+ to: string;
96
+ operation: string;
97
+ };
98
+ unless?: {
99
+ field: string;
100
+ equals: unknown;
101
+ };
102
+ };
103
+ /** A delivery's answer as the kernel reads it: the endpoint kind and row it went to, and the receiver's status (0 when
104
+ * the World refused it or nothing answered). */
105
+ export type DeliveryAnswer = {
106
+ kind: EndpointsDecl;
107
+ row: Record<string, unknown>;
108
+ status: number;
109
+ occurredAt: string;
110
+ };
111
+ /** The events a pack's writes send. */
112
+ export type EventsDecl = {
113
+ /** how each delivery is signed; several schemes each add their headers (GitHub's SHA-256 and SHA-1 signatures) */
114
+ scheme: EventScheme | ReadonlyArray<EventScheme>;
115
+ /** further headers each delivery carries, templates as the envelope's are, plus `$endpoint.<field>` for the endpoint
116
+ * it goes to (GitHub's `X-GitHub-Event`, `X-GitHub-Delivery` and `X-GitHub-Hook-ID`) */
117
+ headers?: Record<string, string>;
118
+ /** A write's kernel operation (`user.create`) → the vendor's event type (`user.created`), or several (a Slack message
119
+ * sends `message` and `app_mention`; `values` withholds one the write does not warrant); a write not named sends none. */
120
+ types: Record<string, string | ReadonlyArray<string>>;
121
+ /** The event types the vendor enumerates, when it names every write's event by its resource: an operation that is
122
+ * one of them sends it, and `<resource>.create|update|delete` sends `<resource>.created|updated|deleted` when that is
123
+ * one of them (a vendor whose spec lists its event types: Stripe's). `types` is read first. */
124
+ known?: ReadonlyArray<string>;
125
+ /** The vendor's own resource each event is also kept as (its Events API: Stripe's `GET /v1/events`), the envelope as
126
+ * its fields under the event's id, whether or not an endpoint takes it. */
127
+ store?: string;
128
+ /** The event as the vendor sends it, a template: a string `$type`, `$data`, `$id`, `$time.ms`, `$time.s`, `$time.iso`,
129
+ * `$request.client_ip` or `$request.user_agent` is replaced by that value; everything else is sent as written. */
130
+ envelope: unknown;
131
+ /** Where the World's endpoints are kept: one kind, or several (GitHub's repository and organization hooks). */
132
+ endpoints: EndpointsDecl | ReadonlyArray<EndpointsDecl>;
133
+ /** The bookkeeping type each delivery is kept as (`_webhook_message`): what a World reads to see what was sent. */
134
+ record?: string;
135
+ /** The bookkeeping type a World states its receivers' answers in (`{url, status}`, Daily's `_receiver`), for receivers
136
+ * it runs no server for: a delivery whose URL starts with a stated `url` (the longest) is answered that status, and
137
+ * nothing is sent. */
138
+ receivers?: string;
139
+ };
140
+ /** An event's further envelope values (`$`-named) the pack computes for a write, from the vendor's own state (the
141
+ * connected account it is about, the API version it is rendered in): the pack's `semantics/events.ts` `values`. A
142
+ * value left undefined drops its key from the envelope; `$send: false` keeps the event (stored) but sends it to no
143
+ * endpoint (an account the vendor no longer reports to). */
144
+ export type EventValues<C> = (ctx: C, write: EventWrite, type: string) => Record<string, unknown>;
145
+ /** The write an event reports, as the kernel's write hook sees it. */
146
+ export type EventWrite = {
147
+ operation: string;
148
+ storedType: string;
149
+ body: Record<string, unknown>;
150
+ root?: string;
151
+ occurredAt: string;
152
+ request?: Request;
153
+ };
154
+ /** How the vendor renders an event's `data` for a write, when it is not the written object as its operation answered
155
+ * it: the pack's `semantics/events.ts`, under the handler contract's read-only context, for each type the write sends
156
+ * (a Slack message renders as `message` and as `app_mention`). */
157
+ export type EventRender<C> = (ctx: C, write: EventWrite, type: string) => Record<string, unknown>;
158
+ /** A delivery made: where it went and exactly what was sent. */
159
+ export type EventDelivery = {
160
+ url: string;
161
+ body: string;
162
+ headers: Record<string, string>;
163
+ };
164
+ /** The transport a delivery is made over; the World's by default, a test's own when given. */
165
+ export type EventTransport = (url: string, body: string, headers: Record<string, string>) => Promise<number | void>;
166
+ /** A signature scheme's headers for one delivery: deterministic (the id is derived from the delivery itself). */
167
+ /** `seq` tells apart two identical deliveries at one instant (a frozen World clock): the World's count of deliveries
168
+ * made before this one, so a receiver that deduplicates by id drops neither. */
169
+ export declare function signEvent(scheme: EventScheme, secret: string, body: string, seconds: number, url: string, seq?: number): Record<string, string>;
170
+ /** Every scheme's headers for one delivery, merged. */
171
+ export declare function signEventAll(schemes: EventScheme | ReadonlyArray<EventScheme>, secret: string, body: string, seconds: number, url: string, seq?: number): Record<string, string>;
172
+ /** A delivery's signature checked as its receiver checks it (a Standard Webhooks verifier, a timestamp-v1 verifier, a plain
173
+ * HMAC header): true when one of its signatures is the one the secret makes (constant time), and its timestamp,
174
+ * when the scheme sends one, is within `tolerance` seconds of `now`. */
175
+ export declare function verifyEvent(scheme: EventScheme, secret: string, body: string, headers: Record<string, string>, opts?: {
176
+ tolerance?: number;
177
+ now?: number;
178
+ }): boolean;
179
+ /** The envelope template filled for one event, and for one endpoint when it names `$endpoint.<field>` (left out when
180
+ * there is none: the event as kept). A `...` key spreads its filled value into the object. */
181
+ export declare function fillEnvelope(template: unknown, values: Record<string, unknown>, endpoint?: Record<string, unknown>): unknown;
182
+ /** The event types a write's operation sends: `types`, else the vendor's enumerated types (`known`). */
183
+ export declare function eventTypesOf(decl: Pick<EventsDecl, 'types' | 'known'>, operation: string): string[];
184
+ /** The event type a write's operation sends, if any: its first under `types`, else the vendor's enumerated types
185
+ * (`known`). */
186
+ export declare function eventTypeOf(decl: Pick<EventsDecl, 'types' | 'known'>, operation: string): string | undefined;
187
+ /** Whether an endpoint's list of event types takes a type: every type when the list is empty, `*` or `<family>.*`
188
+ * its patterns, else the type itself. */
189
+ export declare function takesEvent(list: unknown, type: string): boolean;
190
+ /** A message a vendor sends the application's own server that is no declared event (Google Calendar's push notification:
191
+ * a bodiless POST with its X-Goog headers), over the same transport: the receiver's status, or 0 when the World lets
192
+ * no request out to it or it cannot be reached. The vendor waits on it, as the sender of such a message does. */
193
+ export declare function deliverToApplication(url: string, init: RequestInit): Promise<number>;
194
+ /** What the application's server answered a message the vendor waits on, or why it did not answer: `missed` when the
195
+ * World let no request out to it or it could not be reached (`unreachable`), or it did not answer in time (`timeout`). */
196
+ export type ApplicationAnswer = {
197
+ status: number;
198
+ body: string;
199
+ missed?: 'timeout' | 'unreachable';
200
+ };
201
+ /** A message the vendor sends the application's server and decides by its answer (Stripe's real-time authorization
202
+ * request, answered within its window): over the same transport, waited on for at most `within` milliseconds. */
203
+ export declare function askApplication(url: string, init: RequestInit, within: number): Promise<ApplicationAnswer>;
204
+ /** Every event a write sends, rendered, stored when the vendor keeps its events, signed, delivered to each endpoint
205
+ * that takes it, and recorded. */
206
+ export declare function deliverEvents(service: string, decl: EventsDecl, write: EventWrite, data: (type: string) => Promise<Record<string, unknown>>, transport?: EventTransport, values?: (type: string) => Promise<Record<string, unknown>>, onAnswer?: (answer: DeliveryAnswer) => Promise<void>): Promise<EventDelivery[]>;
@@ -0,0 +1,341 @@
1
+ // DECLARED EVENTS (docs/contributing/architecture.md, "What an author writes, and how": the events row) — the webhook a
2
+ // vendor sends for a write, as data the kernel executes. A pack declares which write sends which event type, the
3
+ // event's envelope, the scheme that signs it and where the World's endpoints are kept; the kernel renders, signs,
4
+ // delivers (through the World's application route and egress rule) and records each delivery. The one thing a pack
5
+ // may write is how its vendor renders an event's `data` (`semantics/events.ts`), when it is not the written object.
6
+ //
7
+ // Imports stay free of side effects: the delivery's transport (Node's http, the World's egress policy) is loaded when a
8
+ // delivery is made, never when a browser client imports the kernel.
9
+ import { createHash, createHmac } from 'node:crypto';
10
+ import { applyTwinWrite, twinResources } from "./serve.js";
11
+ import { equalSecrets as same } from "./signing.js";
12
+ import { deliveryTraceHeaders } from "./trace-context.js";
13
+ /** Whether an endpoint takes an event by its `match`: every field of one match (any, when several) equal to the event's
14
+ * value, or, a list, holding it. */
15
+ function matches(row, match, filled) {
16
+ if (match === undefined)
17
+ return true;
18
+ const one = (m) => Object.entries(m).every(([field, value]) => {
19
+ const want = value.startsWith('$') ? filled[value] : value;
20
+ const have = at(row, field);
21
+ // a list value takes a row whose field is one of it (the apps a Slack message mentions)
22
+ if (Array.isArray(want))
23
+ return want.some((w) => String(w) === String(have));
24
+ return Array.isArray(have) ? have.some((h) => String(h) === String(want)) : have === want || (have !== undefined && want !== undefined && String(have) === String(want));
25
+ });
26
+ return Array.isArray(match) ? match.some(one) : one(match);
27
+ }
28
+ const digest = (s) => createHash('sha256').update(s).digest('hex');
29
+ /** A `webhook-id` scheme's key: the base64 part of the secret, after its prefix when it carries one. */
30
+ const webhookKey = (scheme, secret) => scheme.secretKey === 'utf8' ? Buffer.from(secret, 'utf8') : Buffer.from(scheme.secretPrefix && secret.startsWith(scheme.secretPrefix) ? secret.slice(scheme.secretPrefix.length) : secret, 'base64');
31
+ /** A signature scheme's headers for one delivery: deterministic (the id is derived from the delivery itself). */
32
+ /** `seq` tells apart two identical deliveries at one instant (a frozen World clock): the World's count of deliveries
33
+ * made before this one, so a receiver that deduplicates by id drops neither. */
34
+ export function signEvent(scheme, secret, body, seconds, url, seq = 0) {
35
+ if (scheme.kind === 'webhook-id') {
36
+ const id = `msg_twin_${digest(`${url}|${seconds}|${seq}|${body}`).slice(0, 24)}`;
37
+ const p = scheme.prefix.toLowerCase();
38
+ const signature = createHmac('sha256', webhookKey(scheme, secret)).update(`${id}.${seconds}.${body}`, 'utf8').digest('base64');
39
+ return { [`${p}-id`]: id, [`${p}-timestamp`]: String(seconds), [`${p}-signature`]: `v1,${signature}` };
40
+ }
41
+ if (scheme.kind === 'timestamp-v1') {
42
+ const signature = createHmac('sha256', secret).update(`${seconds}.${body}`, 'utf8').digest('hex');
43
+ return { [scheme.header.toLowerCase()]: `t=${seconds},v1=${signature}` };
44
+ }
45
+ if (scheme.kind === 'hash')
46
+ return { [scheme.header.toLowerCase()]: hashOf(scheme, secret, body) };
47
+ // the key is the secret as it is, or its base64 decoded (`secretKey: 'base64'`, Daily's)
48
+ const mac = createHmac(scheme.algorithm ?? 'sha256', scheme.secretKey === 'base64' ? Buffer.from(secret, 'base64') : secret).update(hmacBase(scheme, seconds, body), 'utf8').digest(scheme.encoding);
49
+ return { [scheme.header.toLowerCase()]: `${scheme.prefix ?? ''}${mac}`, ...(scheme.timestampHeader ? { [scheme.timestampHeader.toLowerCase()]: String(seconds) } : {}) };
50
+ }
51
+ /** A `hash` scheme's value: the digest of its `signed` template with the secret and the body put in. */
52
+ const hashOf = (scheme, secret, body) => createHash(scheme.algorithm ?? 'sha256').update(scheme.signed.replaceAll('{secret}', secret).replaceAll('{body}', body), 'utf8').digest(scheme.encoding);
53
+ /** What an `hmac` scheme signs: its `signed` template, else `${t}.${body}` with a timestamp header, else the body. */
54
+ const hmacBase = (scheme, seconds, body) => scheme.signed !== undefined ? scheme.signed.replaceAll('{t}', String(seconds)).replaceAll('{body}', body) : scheme.timestampHeader ? `${seconds}.${body}` : body;
55
+ /** Every scheme's headers for one delivery, merged. */
56
+ export function signEventAll(schemes, secret, body, seconds, url, seq = 0) {
57
+ return Object.assign({}, ...(Array.isArray(schemes) ? schemes : [schemes]).map((scheme) => signEvent(scheme, secret, body, seconds, url, seq)));
58
+ }
59
+ /** A delivery's signature checked as its receiver checks it (a Standard Webhooks verifier, a timestamp-v1 verifier, a plain
60
+ * HMAC header): true when one of its signatures is the one the secret makes (constant time), and its timestamp,
61
+ * when the scheme sends one, is within `tolerance` seconds of `now`. */
62
+ export function verifyEvent(scheme, secret, body, headers, opts = {}) {
63
+ const h = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
64
+ // a check given the time holds the timestamp to five minutes unless it says otherwise (Standard Webhooks' default)
65
+ const fresh = (seconds) => opts.now === undefined || Math.abs(opts.now - seconds) <= (opts.tolerance ?? 300);
66
+ if (secret === '')
67
+ return false;
68
+ if (scheme.kind === 'webhook-id') {
69
+ const p = scheme.prefix.toLowerCase();
70
+ const id = h[`${p}-id`];
71
+ const seconds = Number(h[`${p}-timestamp`]);
72
+ const given = h[`${p}-signature`];
73
+ if (!id || !given || !Number.isFinite(seconds) || !fresh(seconds))
74
+ return false;
75
+ const expected = createHmac('sha256', webhookKey(scheme, secret)).update(`${id}.${seconds}.${body}`, 'utf8').digest('base64');
76
+ return given.split(' ').some((part) => part.startsWith('v1,') && same(part.slice('v1,'.length), expected));
77
+ }
78
+ if (scheme.kind === 'timestamp-v1') {
79
+ const parts = (h[scheme.header.toLowerCase()] ?? '').split(',').map((p) => p.split('='));
80
+ const seconds = Number(parts.find(([k]) => k === 't')?.[1]);
81
+ if (!Number.isFinite(seconds) || !fresh(seconds))
82
+ return false;
83
+ const expected = createHmac('sha256', secret).update(`${seconds}.${body}`, 'utf8').digest('hex');
84
+ return parts.some(([k, v]) => k === 'v1' && v !== undefined && same(v, expected));
85
+ }
86
+ if (scheme.kind === 'hash') {
87
+ const given = h[scheme.header.toLowerCase()];
88
+ return given !== undefined && same(given, hashOf(scheme, secret, body));
89
+ }
90
+ const seconds = scheme.timestampHeader ? Number(h[scheme.timestampHeader.toLowerCase()]) : undefined;
91
+ if (seconds !== undefined && (!Number.isFinite(seconds) || !fresh(seconds)))
92
+ return false;
93
+ const expected = `${scheme.prefix ?? ''}${createHmac(scheme.algorithm ?? 'sha256', secret).update(hmacBase(scheme, seconds ?? 0, body), 'utf8').digest(scheme.encoding)}`;
94
+ return same(h[scheme.header.toLowerCase()] ?? '', expected);
95
+ }
96
+ /** The envelope template filled for one event, and for one endpoint when it names `$endpoint.<field>` (left out when
97
+ * there is none: the event as kept). A `...` key spreads its filled value into the object. */
98
+ export function fillEnvelope(template, values, endpoint) {
99
+ if (typeof template === 'string') {
100
+ if (template.startsWith('$endpoint.'))
101
+ return endpoint ? at(endpoint, template.slice('$endpoint.'.length)) ?? undefined : undefined;
102
+ return template.startsWith('$') && template in values ? values[template] : template;
103
+ }
104
+ if (Array.isArray(template))
105
+ return template.map((item) => fillEnvelope(item, values, endpoint));
106
+ // a key whose value is a `$` value left undefined is not sent (an event about no connected account has no `account`)
107
+ if (template && typeof template === 'object') {
108
+ const out = {};
109
+ for (const [key, value] of Object.entries(template)) {
110
+ const filled = fillEnvelope(value, values, endpoint);
111
+ if (key === '...') {
112
+ if (filled && typeof filled === 'object' && !Array.isArray(filled))
113
+ Object.assign(out, filled);
114
+ continue;
115
+ }
116
+ if (filled !== undefined)
117
+ out[key] = filled;
118
+ }
119
+ return out;
120
+ }
121
+ return template;
122
+ }
123
+ /** Whether an envelope template names an endpoint's fields (so each delivery's body is its own). */
124
+ const perEndpoint = (template) => JSON.stringify(template ?? null).includes('"$endpoint.');
125
+ /** A row's field by a dotted path (`config.url`). */
126
+ function at(row, path) {
127
+ let v = row;
128
+ for (const key of path.split('.'))
129
+ v = v && typeof v === 'object' ? v[key] : undefined;
130
+ return v;
131
+ }
132
+ /** A delivery's further headers: each a `$` value, an endpoint's `$endpoint.<field>`, or written as it is. */
133
+ function headersFor(templates, values, endpoint) {
134
+ const out = {};
135
+ for (const [name, template] of Object.entries(templates)) {
136
+ // a literal may stand before an endpoint's field (`Basic $endpoint.basicAuth`); the header is dropped when it is empty
137
+ const prefixed = /^(.+?)\$endpoint\.([\w.]+)$/.exec(template);
138
+ const field = prefixed ? at(endpoint, prefixed[2]) : undefined;
139
+ const value = prefixed ? (field === undefined || field === null || field === '' ? undefined : `${prefixed[1]}${String(field)}`)
140
+ : template.startsWith('$endpoint.') ? at(endpoint, template.slice('$endpoint.'.length)) : template.startsWith('$') ? values[template] : template;
141
+ if (value !== undefined && value !== null)
142
+ out[name.toLowerCase()] = String(value);
143
+ }
144
+ return out;
145
+ }
146
+ const knownSets = new WeakMap();
147
+ /** The event types a write's operation sends: `types`, else the vendor's enumerated types (`known`). */
148
+ export function eventTypesOf(decl, operation) {
149
+ const named = decl.types[operation];
150
+ if (named !== undefined)
151
+ return typeof named === 'string' ? [named] : [...named];
152
+ const one = eventTypeOf(decl, operation);
153
+ return one === undefined ? [] : [one];
154
+ }
155
+ /** The event type a write's operation sends, if any: its first under `types`, else the vendor's enumerated types
156
+ * (`known`). */
157
+ export function eventTypeOf(decl, operation) {
158
+ const given = decl.types[operation];
159
+ const named = typeof given === 'string' || given === undefined ? given : given[0];
160
+ if (named !== undefined || !decl.known)
161
+ return named;
162
+ let known = knownSets.get(decl.known);
163
+ if (!known)
164
+ knownSets.set(decl.known, known = new Set(decl.known));
165
+ if (known.has(operation))
166
+ return operation;
167
+ const verb = /^(.+)\.(create|update|delete)$/.exec(operation);
168
+ return verb && known.has(`${verb[1]}.${verb[2]}d`) ? `${verb[1]}.${verb[2]}d` : undefined;
169
+ }
170
+ /** Whether an endpoint's list of event types takes a type: every type when the list is empty, `*` or `<family>.*`
171
+ * its patterns, else the type itself. */
172
+ export function takesEvent(list, type) {
173
+ if (!Array.isArray(list) || list.length === 0)
174
+ return true;
175
+ return list.some((p) => { const s = String(p); return s === '*' || s === type || (s.endsWith('.*') && type.startsWith(s.slice(0, -1))); });
176
+ }
177
+ /** The request that made the write, as an event's attributes name it: the client's address (the first hop a proxy
178
+ * names) and its user agent. */
179
+ function requestValues(request) {
180
+ const forwarded = request?.headers.get('x-forwarded-for')?.split(',')[0]?.trim();
181
+ return {
182
+ '$request.client_ip': forwarded ?? request?.headers.get('x-real-ip') ?? '',
183
+ '$request.user_agent': request?.headers.get('user-agent') ?? '',
184
+ };
185
+ }
186
+ /** The World's transport: its application route (the app's own hostnames reach it inside the World) and its egress
187
+ * rule (a refused destination is dropped as an unreachable endpoint is), never awaited by the write that sent it (deliverEvents schedules it after the answer). Loaded at
188
+ * the first delivery, so importing the kernel stays free of Node's http. */
189
+ const worldTransport = async (url, body, headers) => {
190
+ const [{ appDestination, appFetch }, { worldEgressRefusal }] = await Promise.all([
191
+ import('../app-route.cjs'),
192
+ import('../network-policy.cjs'),
193
+ ]);
194
+ if (!appDestination(url) && worldEgressRefusal(url) !== null)
195
+ return 0;
196
+ try {
197
+ return (await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body })).status;
198
+ }
199
+ catch {
200
+ return 0;
201
+ }
202
+ };
203
+ /** A message a vendor sends the application's own server that is no declared event (Google Calendar's push notification:
204
+ * a bodiless POST with its X-Goog headers), over the same transport: the receiver's status, or 0 when the World lets
205
+ * no request out to it or it cannot be reached. The vendor waits on it, as the sender of such a message does. */
206
+ export async function deliverToApplication(url, init) {
207
+ const [{ appDestination, appFetch }, { worldEgressRefusal }] = await Promise.all([
208
+ import('../app-route.cjs'),
209
+ import('../network-policy.cjs'),
210
+ ]);
211
+ if (!appDestination(url) && worldEgressRefusal(url) !== null)
212
+ return 0;
213
+ // the request that caused the message is its trace's parent (trace-context.ts)
214
+ const headers = new Headers(init.headers);
215
+ for (const [k, v] of Object.entries(deliveryTraceHeaders()))
216
+ if (!headers.has(k))
217
+ headers.set(k, v);
218
+ try {
219
+ return (await appFetch(url, { ...init, headers })).status;
220
+ }
221
+ catch {
222
+ return 0;
223
+ }
224
+ }
225
+ /** A message the vendor sends the application's server and decides by its answer (Stripe's real-time authorization
226
+ * request, answered within its window): over the same transport, waited on for at most `within` milliseconds. */
227
+ export async function askApplication(url, init, within) {
228
+ const [{ appDestination, appFetch }, { worldEgressRefusal }] = await Promise.all([
229
+ import('../app-route.cjs'),
230
+ import('../network-policy.cjs'),
231
+ ]);
232
+ if (!appDestination(url) && worldEgressRefusal(url) !== null)
233
+ return { status: 0, body: '', missed: 'unreachable' };
234
+ const headers = new Headers(init.headers);
235
+ for (const [k, v] of Object.entries(deliveryTraceHeaders()))
236
+ if (!headers.has(k))
237
+ headers.set(k, v);
238
+ try {
239
+ const res = await appFetch(url, { ...init, headers, signal: AbortSignal.timeout(within) });
240
+ return { status: res.status, body: await res.text() };
241
+ }
242
+ catch (e) {
243
+ return { status: 0, body: '', missed: e instanceof Error && (e.name === 'TimeoutError' || e.name === 'AbortError') ? 'timeout' : 'unreachable' };
244
+ }
245
+ }
246
+ /** Every event a write sends, rendered, stored when the vendor keeps its events, signed, delivered to each endpoint
247
+ * that takes it, and recorded. */
248
+ export async function deliverEvents(service, decl, write, data, transport = worldTransport, values, onAnswer) {
249
+ if (write.storedType === decl.store)
250
+ return [];
251
+ const sent = [];
252
+ for (const type of eventTypesOf(decl, write.operation))
253
+ sent.push(...(await deliverEventType(service, decl, write, data, transport, values, type, onAnswer)));
254
+ return sent;
255
+ }
256
+ /** One event type a write sends: to the live endpoints that take it, stored when the vendor keeps its events. */
257
+ async function deliverEventType(service, decl, write, data, transport, values, type, onAnswer) {
258
+ const all = twinResources(service, write.root);
259
+ const kinds = Array.isArray(decl.endpoints) ? decl.endpoints : [decl.endpoints];
260
+ // an endpoint's row with its joined rows under their names (`app.hook_url` reads the installation's App)
261
+ const joined = (kind, r) => {
262
+ if (!kind.joins)
263
+ return r;
264
+ const out = { ...r, id: r.id };
265
+ for (const [name, j] of Object.entries(kind.joins))
266
+ out[name] = all.find((x) => x.type === j.storedAs && x.deleted !== true && String(x.id) === String(at(r, j.by)));
267
+ return out;
268
+ };
269
+ const live = kinds.filter((kind) => (!kind.types || kind.types.includes(type)) && !kind.skip?.includes(type)).flatMap((kind) => all
270
+ .filter((r) => r.type === kind.storedAs && r.deleted !== true)
271
+ .map((r) => joined(kind, r))
272
+ .filter((r) => (kind.disabled === undefined || at(r, kind.disabled) !== true) && (kind.enabled === undefined || at(r, kind.enabled) !== false) && (kind.status === undefined || at(r, kind.status.field) === kind.status.live))
273
+ .filter((r) => typeof at(r, kind.url) === 'string' && typeof at(r, kind.secret) === 'string')
274
+ .filter((r) => kind.filter === undefined || takesEvent(at(r, kind.filter), type))
275
+ .map((row) => ({ row, kind })));
276
+ if (live.length === 0 && decl.store === undefined)
277
+ return [];
278
+ const millis = Date.parse(write.occurredAt) || 0;
279
+ const seconds = Math.floor(millis / 1000);
280
+ const eventData = await data(type);
281
+ const extra = values ? await values(type) : {};
282
+ // the World's count of events kept so far tells apart two identical events at one instant (a frozen clock)
283
+ const kept = decl.store !== undefined ? all.filter((r) => r.type === decl.store).length : 0;
284
+ const filled = {
285
+ $type: type, $data: eventData,
286
+ $id: `evt_twin_${digest(`${type}|${write.occurredAt}|${kept}|${JSON.stringify(eventData)}`).slice(0, 24)}`,
287
+ '$time.ms': millis, '$time.s': seconds, '$time.iso': write.occurredAt,
288
+ ...requestValues(write.request),
289
+ ...extra,
290
+ };
291
+ const envelope = fillEnvelope(decl.envelope, filled);
292
+ if (decl.store !== undefined) {
293
+ await applyTwinWrite(service, { operation: `${decl.store}.record`, subjectType: decl.store, subjectId: String(filled.$id), fields: envelope, occurredAt: write.occurredAt, actor: { kind: 'system' } }, write.root);
294
+ }
295
+ if (extra.$send === false)
296
+ return [];
297
+ const endpoints = live
298
+ .filter(({ row, kind }) => kind.scope === undefined || (at(row, kind.scope.field) === true) === (filled[kind.scope.value] !== undefined))
299
+ .filter(({ row, kind }) => matches(row, kind.match, filled));
300
+ if (endpoints.length === 0)
301
+ return [];
302
+ const shared = JSON.stringify(envelope);
303
+ const each = perEndpoint(decl.envelope);
304
+ const sent = [];
305
+ const to = [];
306
+ const before = decl.record !== undefined ? all.filter((r) => r.type === decl.record).length : 0;
307
+ for (const [i, { row, kind }] of endpoints.entries()) {
308
+ const body = each ? JSON.stringify(fillEnvelope(decl.envelope, filled, row)) : shared;
309
+ const url = String(at(row, kind.url));
310
+ const seq = before + i;
311
+ const extraHeaders = decl.headers ? headersFor(decl.headers, filled, row) : {};
312
+ const headers = { ...extraHeaders, ...signEventAll(decl.scheme, String(at(row, kind.secret)), body, seconds, url, seq) };
313
+ sent.push({ url, body, headers });
314
+ to.push({ kind, row });
315
+ // kept with the write, by an id of the delivery itself (two made at once never take the same one)
316
+ if (decl.record !== undefined) {
317
+ await applyTwinWrite(service, {
318
+ operation: `${decl.record.slice(1)}.record`, subjectType: decl.record, subjectId: `${decl.record.slice(1)}_${digest(`${url}|${seconds}|${seq}|${body}`).slice(0, 16)}`,
319
+ fields: { url, body, headers, event_type: type, sent_at: millis }, occurredAt: write.occurredAt, actor: { kind: 'system' },
320
+ }, write.root);
321
+ }
322
+ }
323
+ // sent after the write's answer, as a vendor sends it: the caller never waits on its receiver, and a receiver that is
324
+ // slow or down fails no request
325
+ // the write's trace is read now, inside its request, and carried on each delivery (W3C trace continuation)
326
+ const trace = deliveryTraceHeaders();
327
+ // each answer is read as the vendor reads it, when the endpoint kind keeps score (its `outcome`)
328
+ const stated = (url) => {
329
+ const r = decl.receivers === undefined ? undefined : all.filter((x) => x.type === decl.receivers && x.deleted !== true && url.startsWith(String(x.url)))
330
+ .sort((x, y) => String(y.url).length - String(x.url).length)[0];
331
+ return r === undefined ? undefined : Number(r.status);
332
+ };
333
+ for (const [i, { url, body: payload, headers }] of sent.entries())
334
+ setTimeout(() => {
335
+ const answered = stated(url);
336
+ (answered !== undefined ? Promise.resolve(answered) : transport(url, payload, { ...headers, ...trace }))
337
+ .then((status) => (onAnswer && to[i].kind.outcome ? onAnswer({ ...to[i], status: typeof status === 'number' ? status : 0, occurredAt: write.occurredAt }) : undefined))
338
+ .catch(() => { });
339
+ }, 0);
340
+ return sent;
341
+ }