@volter/world-core 3.0.38 → 3.0.39

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 (179) hide show
  1. package/app-route.cjs +26 -25
  2. package/app-route.d.cts +1 -0
  3. package/dist/app-route.cjs +26 -25
  4. package/dist/app-route.d.cts +1 -0
  5. package/dist/generated/pack-facts.json +409 -352
  6. package/dist/inject.cjs +474 -95
  7. package/dist/network-policy.cjs +108 -13
  8. package/dist/network-policy.d.cts +8 -3
  9. package/dist/src/actions.js +2 -1
  10. package/dist/src/anthropic-wire.d.ts +2 -0
  11. package/dist/src/anthropic-wire.js +5 -0
  12. package/dist/src/attestation.d.ts +6 -0
  13. package/dist/src/attestation.js +27 -0
  14. package/dist/src/bytes.d.ts +3 -0
  15. package/dist/src/bytes.js +17 -0
  16. package/dist/src/changeset.js +2 -1
  17. package/dist/src/clickhouse/sql.js +73 -14
  18. package/dist/src/derived-core.d.ts +210 -32
  19. package/dist/src/derived-core.js +510 -244
  20. package/dist/src/derived-real.d.ts +1 -1
  21. package/dist/src/derived-real.js +704 -93
  22. package/dist/src/derived.d.ts +18 -2
  23. package/dist/src/derived.js +21 -2
  24. package/dist/src/events.d.ts +28 -45
  25. package/dist/src/events.js +40 -58
  26. package/dist/src/exact-json.d.ts +10 -0
  27. package/dist/src/exact-json.js +98 -0
  28. package/dist/src/executor.d.ts +17 -0
  29. package/dist/src/executor.js +91 -30
  30. package/dist/src/file-response.js +1 -1
  31. package/dist/src/form.d.ts +8 -0
  32. package/dist/src/form.js +76 -0
  33. package/dist/src/git/format.d.ts +16 -0
  34. package/dist/src/git/format.js +110 -0
  35. package/dist/src/git/index.d.ts +1 -0
  36. package/dist/src/git/index.js +1 -0
  37. package/dist/src/git/lfs.d.ts +1 -1
  38. package/dist/src/git/lfs.js +4 -3
  39. package/dist/src/graphql-wire.d.ts +6 -0
  40. package/dist/src/graphql-wire.js +6 -3
  41. package/dist/src/grpc-wire.js +2 -1
  42. package/dist/src/head.d.ts +1 -0
  43. package/dist/src/head.js +15 -4
  44. package/dist/src/held-reads.d.ts +6 -0
  45. package/dist/src/held-reads.js +60 -0
  46. package/dist/src/history.js +5 -4
  47. package/dist/src/host-port.d.ts +5 -0
  48. package/dist/src/host-port.js +14 -0
  49. package/dist/src/index.d.ts +11 -3
  50. package/dist/src/index.js +9 -4
  51. package/dist/src/log.js +12 -7
  52. package/dist/src/machines.d.ts +81 -5
  53. package/dist/src/machines.js +125 -9
  54. package/dist/src/managed-database.d.ts +15 -11
  55. package/dist/src/managed-database.js +38 -37
  56. package/dist/src/multipart.d.ts +21 -3
  57. package/dist/src/multipart.js +112 -38
  58. package/dist/src/observe.d.ts +3 -0
  59. package/dist/src/observe.js +6 -2
  60. package/dist/src/openai-wire.d.ts +16 -0
  61. package/dist/src/openai-wire.js +43 -12
  62. package/dist/src/pack-fetch.d.ts +4 -6
  63. package/dist/src/pack-fetch.js +189 -57
  64. package/dist/src/packRegistry.d.ts +7 -1
  65. package/dist/src/packRegistry.js +20 -3
  66. package/dist/src/private-transport.d.ts +1 -0
  67. package/dist/src/private-transport.js +11 -0
  68. package/dist/src/protobuf.js +10 -4
  69. package/dist/src/rateBudget.d.ts +8 -1
  70. package/dist/src/rateBudget.js +1 -0
  71. package/dist/src/redis/engine.d.ts +14 -5
  72. package/dist/src/redis/engine.js +271 -72
  73. package/dist/src/redis/frames.d.ts +17 -0
  74. package/dist/src/redis/frames.js +123 -0
  75. package/dist/src/redis/index.d.ts +6 -0
  76. package/dist/src/redis/index.js +6 -0
  77. package/dist/src/redis/json.d.ts +2 -0
  78. package/dist/src/redis/json.js +14 -0
  79. package/dist/src/redis/lua-libs.d.ts +4 -0
  80. package/dist/src/redis/lua-libs.js +173 -0
  81. package/dist/src/redis/lua.d.ts +1 -0
  82. package/dist/src/redis/lua.js +36 -0
  83. package/dist/src/redis/member-storage.d.ts +5 -0
  84. package/dist/src/redis/member-storage.js +75 -0
  85. package/dist/src/redis/resp2.d.ts +4 -0
  86. package/dist/src/redis/resp2.js +29 -0
  87. package/dist/src/redis/stream.d.ts +14 -0
  88. package/dist/src/redis/stream.js +335 -0
  89. package/dist/src/remote-execute.d.ts +2 -2
  90. package/dist/src/request-body.d.ts +11 -0
  91. package/dist/src/request-body.js +11 -0
  92. package/dist/src/runtime.d.ts +6 -5
  93. package/dist/src/runtime.js +5 -5
  94. package/dist/src/s3/wire.d.ts +4 -4
  95. package/dist/src/s3/wire.js +5 -3
  96. package/dist/src/scenario.d.ts +22 -0
  97. package/dist/src/scenario.js +35 -31
  98. package/dist/src/serve-http.d.ts +2 -1
  99. package/dist/src/serve-http.js +10 -1
  100. package/dist/src/serve.js +7 -4
  101. package/dist/src/signing.d.ts +23 -5
  102. package/dist/src/signing.js +74 -11
  103. package/dist/src/sigv4.d.ts +3 -1
  104. package/dist/src/sigv4.js +4 -2
  105. package/dist/src/smtp.js +2 -1
  106. package/dist/src/sockets.js +8 -3
  107. package/dist/src/state-system.d.ts +3 -0
  108. package/dist/src/storage.js +5 -2
  109. package/dist/src/twin-fetch.d.ts +3 -1
  110. package/dist/src/twin-fetch.js +3 -2
  111. package/dist/src/vendor-call.d.ts +10 -3
  112. package/dist/src/vendor-call.js +58 -14
  113. package/dist/test-fixtures/attestation.SOURCE.md +1 -0
  114. package/dist/test-fixtures/attestation.json +50 -0
  115. package/generated/pack-facts.json +409 -352
  116. package/inject.cjs +474 -95
  117. package/network-policy.cjs +108 -13
  118. package/network-policy.d.cts +8 -3
  119. package/package.json +11 -4
  120. package/src/actions.ts +2 -1
  121. package/src/anthropic-wire.ts +6 -0
  122. package/src/attestation.ts +23 -0
  123. package/src/bytes.ts +21 -0
  124. package/src/changeset.ts +2 -1
  125. package/src/clickhouse/sql.ts +50 -16
  126. package/src/derived-core.ts +573 -249
  127. package/src/derived-real.ts +552 -42
  128. package/src/derived.ts +23 -4
  129. package/src/events.ts +54 -85
  130. package/src/exact-json.ts +71 -0
  131. package/src/executor.ts +86 -27
  132. package/src/file-response.ts +1 -1
  133. package/src/form.ts +66 -0
  134. package/src/git/format.ts +89 -0
  135. package/src/git/index.ts +1 -0
  136. package/src/git/lfs.ts +5 -4
  137. package/src/graphql-wire.ts +8 -3
  138. package/src/grpc-wire.ts +2 -1
  139. package/src/head.ts +15 -5
  140. package/src/held-reads.ts +54 -0
  141. package/src/history.ts +5 -4
  142. package/src/host-port.ts +9 -0
  143. package/src/index.ts +10 -4
  144. package/src/log.ts +11 -7
  145. package/src/machines.ts +135 -13
  146. package/src/managed-database.ts +48 -40
  147. package/src/multipart.ts +110 -35
  148. package/src/observe.ts +6 -3
  149. package/src/openai-wire.ts +45 -14
  150. package/src/pack-fetch.ts +173 -50
  151. package/src/packRegistry.ts +27 -4
  152. package/src/private-transport.ts +8 -0
  153. package/src/protobuf.ts +8 -4
  154. package/src/rateBudget.ts +6 -1
  155. package/src/redis/engine.ts +168 -63
  156. package/src/redis/frames.ts +53 -0
  157. package/src/redis/index.ts +6 -0
  158. package/src/redis/json.ts +13 -0
  159. package/src/redis/lua-libs.ts +62 -0
  160. package/src/redis/lua.ts +20 -0
  161. package/src/redis/member-storage.ts +58 -0
  162. package/src/redis/resp2.ts +25 -0
  163. package/src/redis/stream.ts +102 -0
  164. package/src/remote-execute.ts +2 -2
  165. package/src/request-body.ts +12 -0
  166. package/src/runtime.ts +6 -5
  167. package/src/s3/wire.ts +8 -5
  168. package/src/scenario.ts +34 -13
  169. package/src/serve-http.ts +11 -2
  170. package/src/serve.ts +4 -3
  171. package/src/signing.ts +71 -17
  172. package/src/sigv4.ts +5 -3
  173. package/src/smtp.ts +3 -2
  174. package/src/sockets.ts +8 -3
  175. package/src/state-system.ts +3 -0
  176. package/src/storage.ts +5 -2
  177. package/src/twin-fetch.ts +4 -3
  178. package/src/vendor-call.ts +59 -12
  179. package/test-fixtures/attestation.json +50 -0
@@ -41,13 +41,20 @@ export type DerivedOperation = {
41
41
  name: string;
42
42
  type: string;
43
43
  required?: boolean;
44
- minimum?: number;
45
- maximum?: number;
44
+ minimum?: number | string;
45
+ maximum?: number | string;
46
46
  }>;
47
47
  };
48
48
  /** `basePath` is what the vendor's server URL puts before every operation path; `spanning` names the
49
49
  * path parameters whose values may hold slashes (a git ref, a file path), which no spec says. */
50
50
  export type DerivedSurface = {
51
+ graphql?: {
52
+ sdl: string;
53
+ };
54
+ format?: string;
55
+ commands?: ReadonlyArray<{
56
+ id: string;
57
+ }>;
51
58
  version: string;
52
59
  basePath?: string;
53
60
  spanning?: ReadonlyArray<string>;
@@ -102,6 +109,10 @@ export declare function matchOperation(routes: Map<string, Route[]>, method: str
102
109
  operation: DerivedOperation;
103
110
  params: Record<string, string>;
104
111
  } | undefined;
112
+ /** The path's template parameters for a request no operation matches (a CORS preflight, a method the path is not
113
+ * served by): read from the most literal route of any method whose path the request's path fits, discriminators
114
+ * aside, so `around` and `gap` know the resource the URL names without parsing it (S3's OPTIONS on `/{Bucket}/{Key+}`). */
115
+ export declare function pathParams(routes: Map<string, Route[]>, pathname: string): Record<string, string>;
105
116
  export type DerivedOwner = 'handler' | 'core' | 'gap';
106
117
  /** The header the kernel carries a path prefix's named groups in, from routing to the call (never a client's). */
107
118
  export declare const PREFIX_PARAMS_HEADER = "x-volter-path-prefix-params";
@@ -109,8 +120,13 @@ export type DerivedFetch = ((request: Request) => Promise<Response>) & {
109
120
  /** Per operationId, who serves it: a semantics handler, the derived core, the pack's existing
110
121
  * fetch while it moves, or nothing (the vendor's gap). */
111
122
  owners(): Record<string, DerivedOwner>;
123
+ /** A vendor's owners by unit: its own API's under `''`, each lane's under the lane's name, each keyed by the unit's own
124
+ * operation ids (an id two units share is two operations, each served or the gap on its own). `owners()` is this map
125
+ * flat, a lane's ids written `<lane>:<id>`. A fetch with no lanes may leave it out: its one unit is `owners()`. */
126
+ ownersByUnit?(): Record<string, Record<string, DerivedOwner>>;
112
127
  /** The vendor's sockets (sockets.ts), for the serve seam to upgrade to, when its manifest declares any. */
113
128
  upgrade?: WebSocketUpgrade;
129
+ maxRequestBodySize?: number;
114
130
  /** The workspace screens it draws, its lanes' too (`GET /twin`'s `screens`). */
115
131
  workspaces?(): Workspace[];
116
132
  /** Whether it lays out frames of the World's board (`semantics/board.ts`). */
@@ -1,3 +1,4 @@
1
+ import { parseExactJson } from "./exact-json.js";
1
2
  import { runWithRequestTrace } from "./trace-context.js";
2
3
  /**
3
4
  * A request's body as text, read once per request: the dispatch reads it and tags the request with it, and every clone
@@ -29,7 +30,7 @@ export async function checkJsonBody(request) {
29
30
  if (!(request.headers.get('content-type') ?? '').includes('json') && !/^\s*[[{]/.test(text))
30
31
  return true;
31
32
  try {
32
- const value = JSON.parse(text);
33
+ const value = parseExactJson(text);
33
34
  const tag = request[BODY_TEXT] !== undefined ? BODY_PARSE_KEY.get(request) : undefined;
34
35
  if (tag)
35
36
  BODY_PARSED.set(tag, { value });
@@ -123,6 +124,24 @@ export function matchOperation(routes, method, pathname, query, headers) {
123
124
  }
124
125
  return undefined;
125
126
  }
127
+ /** The path's template parameters for a request no operation matches (a CORS preflight, a method the path is not
128
+ * served by): read from the most literal route of any method whose path the request's path fits, discriminators
129
+ * aside, so `around` and `gap` know the resource the URL names without parsing it (S3's OPTIONS on `/{Bucket}/{Key+}`). */
130
+ export function pathParams(routes, pathname) {
131
+ const fits = [...routes.values()].flat().filter((r) => r.pattern.test(pathname)).sort((a, b) => b.literals - a.literals || b.names.length - a.names.length);
132
+ const route = fits[0];
133
+ const m = route?.pattern.exec(pathname);
134
+ if (!route || !m)
135
+ return {};
136
+ const params = {};
137
+ try {
138
+ route.names.forEach((name, i) => { params[name] = decodeURIComponent(m[i + 1]); });
139
+ }
140
+ catch {
141
+ return {};
142
+ }
143
+ return params;
144
+ }
126
145
  /** The header the kernel carries a path prefix's named groups in, from routing to the call (never a client's). */
127
146
  export const PREFIX_PARAMS_HEADER = 'x-volter-path-prefix-params';
128
147
  export function createDerivedFetch(options) {
@@ -147,7 +166,7 @@ export function createDerivedFetch(options) {
147
166
  tagBody(request, await request.clone().text());
148
167
  // the path prefix's named groups (pack-fetch.ts, `pathPrefix`) are the call's parameters too
149
168
  const prefixed = request.headers.get(PREFIX_PARAMS_HEADER);
150
- const call = { request, operation: matched.operation, params: prefixed ? { ...JSON.parse(prefixed), ...matched.params } : matched.params };
169
+ const call = { request, operation: matched.operation, params: prefixed ? { ...parseExactJson(prefixed), ...matched.params } : matched.params };
151
170
  const around = options.around ?? ((_c, next) => next());
152
171
  const handler = handlers[matched.operation.id];
153
172
  if (handler)
@@ -1,20 +1,28 @@
1
1
  /** How the vendor signs a delivery. */
2
2
  export type EventScheme =
3
+ /** One header carrying the destination's secret as it is, no signature over the body (Cloudflare Notifications'
4
+ * `cf-webhook-auth`, the secret a webhook destination was made with). */
5
+ {
6
+ kind: 'secret';
7
+ header: string;
8
+ }
3
9
  /** A message id, a timestamp and a signature header (the Standard Webhooks shape): `<prefix>-id`,
4
10
  * `<prefix>-timestamp` (seconds) and `<prefix>-signature: v1,<base64 HMAC-SHA256>` over `${id}.${timestamp}.${body}`,
5
11
  * keyed by the base64 part of the secret after `secretPrefix`, or by the secret's own UTF-8 bytes (`secretKey: 'utf8'`:
6
12
  * Polar's, whose SDK base64-encodes the secret as it is before verifying). */
7
- {
13
+ | {
8
14
  kind: 'webhook-id';
9
15
  prefix: string;
10
16
  secretPrefix?: string;
11
17
  secretKey?: 'base64' | 'utf8';
12
18
  }
13
19
  /** 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. */
20
+ * keyed by the secret as it is; `version` names the signature's key when the vendor's is not `v1` (ElevenLabs'
21
+ * `ElevenLabs-Signature: t=…,v0=…`). */
15
22
  | {
16
23
  kind: 'timestamp-v1';
17
24
  header: string;
25
+ version?: string;
18
26
  }
19
27
  /** One HMAC header over the body (`X-Hub-Signature-256: sha256=<hex>`, GitHub's), or over `${timestamp}.${body}` when
20
28
  * the vendor sends its timestamp in `timestampHeader`; `signed` names what is signed otherwise, `{t}` the timestamp
@@ -28,15 +36,15 @@ export type EventScheme =
28
36
  timestampHeader?: string;
29
37
  signed?: string;
30
38
  algorithm?: 'sha1' | 'sha256';
31
- secretKey?: 'base64' | 'utf8';
32
39
  }
33
40
  /** 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}` */
41
+ * of the app's client secret followed by the request body); `signed` names the input, `{secret}` and `{body}`;
42
+ * `base64-unpadded` is base64 with its trailing `=` removed (E2B's `e2b-signature`) */
35
43
  | {
36
44
  kind: 'hash';
37
45
  header: string;
38
46
  signed: string;
39
- encoding: 'hex' | 'base64';
47
+ encoding: 'hex' | 'base64' | 'base64-unpadded';
40
48
  algorithm?: 'sha256';
41
49
  };
42
50
  /** Where the World's endpoints of one kind are kept (a door the vendor's dashboard stands in for writes them): their
@@ -79,34 +87,6 @@ export type EndpointsDecl = {
79
87
  * when a message mentions it, and every `message` its subscription names). */
80
88
  types?: ReadonlyArray<string>;
81
89
  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
90
  };
111
91
  /** The events a pack's writes send. */
112
92
  export type EventsDecl = {
@@ -132,10 +112,6 @@ export type EventsDecl = {
132
112
  endpoints: EndpointsDecl | ReadonlyArray<EndpointsDecl>;
133
113
  /** The bookkeeping type each delivery is kept as (`_webhook_message`): what a World reads to see what was sent. */
134
114
  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
115
  };
140
116
  /** An event's further envelope values (`$`-named) the pack computes for a write, from the vendor's own state (the
141
117
  * connected account it is about, the API version it is rendered in): the pack's `semantics/events.ts` `values`. A
@@ -187,14 +163,18 @@ export declare function eventTypeOf(decl: Pick<EventsDecl, 'types' | 'known'>, o
187
163
  /** Whether an endpoint's list of event types takes a type: every type when the list is empty, `*` or `<family>.*`
188
164
  * its patterns, else the type itself. */
189
165
  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`). */
166
+ /** Who watches what the World sends (a walk reading each delivery as its receiver would): told of every delivery as it
167
+ * is made, before its transport. Set and restored by the walk, as its World store is; unset, nothing watches. */
168
+ export type DeliveryObserver = (delivery: EventDelivery & {
169
+ service: string;
170
+ }) => void;
171
+ export declare function setDeliveryObserver(observer: DeliveryObserver | undefined): DeliveryObserver | undefined;
172
+ /** What the application's server answered a message the vendor waits on (its status, its headers by lower-cased name,
173
+ * its body), or why it did not answer: `missed` when the World let no request out to it or it could not be reached
174
+ * (`unreachable`), or it did not answer in time (`timeout`). */
196
175
  export type ApplicationAnswer = {
197
176
  status: number;
177
+ headers: Record<string, string>;
198
178
  body: string;
199
179
  missed?: 'timeout' | 'unreachable';
200
180
  };
@@ -203,10 +183,13 @@ export type ApplicationAnswer = {
203
183
  * application's; `'unreachable'` is a question nobody answers (a walk's life, a sealed World, takes no other); undefined
204
184
  * is a question it does not take, which goes on as any other does: the application route, the World's egress rule, the
205
185
  * network. A walk's stand-in answers every question (its life's answers, else unreachable); a hosted World's router
206
- * answers only what it can answer truthfully (WorldDoors.askInWorld) and leaves the rest. */
186
+ * answers only what it can answer truthfully (WorldDoors.askInWorld) and leaves the rest. `bodyError` models a
187
+ * disconnect after the response headers; a caller needing the complete body receives no complete answer. */
207
188
  export type ApplicationStandIn = (url: string, init: RequestInit) => Promise<{
208
189
  status: number;
190
+ headers?: Record<string, string>;
209
191
  body: string;
192
+ bodyError?: string;
210
193
  } | 'unreachable' | undefined>;
211
194
  /** A message the vendor sends the application's server and decides by its answer (Stripe's real-time authorization
212
195
  * request, answered within its window): over the same transport, waited on for at most `within` milliseconds, the
@@ -214,4 +197,4 @@ export type ApplicationStandIn = (url: string, init: RequestInit) => Promise<{
214
197
  export declare function askApplication(url: string, init: RequestInit, within: number, standIn?: ApplicationStandIn): Promise<ApplicationAnswer>;
215
198
  /** Every event a write sends, rendered, stored when the vendor keeps its events, signed, delivered to each endpoint
216
199
  * that takes it, and recorded. */
217
- 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[]>;
200
+ 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>>): Promise<EventDelivery[]>;
@@ -8,7 +8,7 @@
8
8
  // delivery is made, never when a browser client imports the kernel.
9
9
  import { createHash, createHmac } from 'node:crypto';
10
10
  import { applyTwinWrite, twinResources } from "./serve.js";
11
- import { equalSecrets as same } from "./signing.js";
11
+ import { equalSecrets as same, ksuidFrom } from "./signing.js";
12
12
  import { deliveryTraceHeaders } from "./trace-context.js";
13
13
  /** Whether an endpoint takes an event by its `match`: every field of one match (any, when several) equal to the event's
14
14
  * value, or, a list, holding it. */
@@ -33,23 +33,28 @@ const webhookKey = (scheme, secret) => scheme.secretKey === 'utf8' ? Buffer.from
33
33
  * made before this one, so a receiver that deduplicates by id drops neither. */
34
34
  export function signEvent(scheme, secret, body, seconds, url, seq = 0) {
35
35
  if (scheme.kind === 'webhook-id') {
36
- const id = `msg_twin_${digest(`${url}|${seconds}|${seq}|${body}`).slice(0, 24)}`;
36
+ // Svix's `msg_` and a KSUID (its ids' form), derived from the delivery itself
37
+ const id = `msg_${ksuidFrom(seconds, seq, `${url}|${seconds}|${seq}|${body}`)}`;
37
38
  const p = scheme.prefix.toLowerCase();
38
39
  const signature = createHmac('sha256', webhookKey(scheme, secret)).update(`${id}.${seconds}.${body}`, 'utf8').digest('base64');
39
40
  return { [`${p}-id`]: id, [`${p}-timestamp`]: String(seconds), [`${p}-signature`]: `v1,${signature}` };
40
41
  }
41
42
  if (scheme.kind === 'timestamp-v1') {
42
43
  const signature = createHmac('sha256', secret).update(`${seconds}.${body}`, 'utf8').digest('hex');
43
- return { [scheme.header.toLowerCase()]: `t=${seconds},v1=${signature}` };
44
+ return { [scheme.header.toLowerCase()]: `t=${seconds},${scheme.version ?? 'v1'}=${signature}` };
44
45
  }
46
+ if (scheme.kind === 'secret')
47
+ return { [scheme.header.toLowerCase()]: secret };
45
48
  if (scheme.kind === 'hash')
46
49
  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);
50
+ const mac = createHmac(scheme.algorithm ?? 'sha256', secret).update(hmacBase(scheme, seconds, body), 'utf8').digest(scheme.encoding);
49
51
  return { [scheme.header.toLowerCase()]: `${scheme.prefix ?? ''}${mac}`, ...(scheme.timestampHeader ? { [scheme.timestampHeader.toLowerCase()]: String(seconds) } : {}) };
50
52
  }
51
53
  /** 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);
54
+ const hashOf = (scheme, secret, body) => {
55
+ const digest = createHash(scheme.algorithm ?? 'sha256').update(scheme.signed.replaceAll('{secret}', secret).replaceAll('{body}', body), 'utf8');
56
+ return scheme.encoding === 'base64-unpadded' ? digest.digest('base64').replace(/=+$/, '') : digest.digest(scheme.encoding);
57
+ };
53
58
  /** What an `hmac` scheme signs: its `signed` template, else `${t}.${body}` with a timestamp header, else the body. */
54
59
  const hmacBase = (scheme, seconds, body) => scheme.signed !== undefined ? scheme.signed.replaceAll('{t}', String(seconds)).replaceAll('{body}', body) : scheme.timestampHeader ? `${seconds}.${body}` : body;
55
60
  /** Every scheme's headers for one delivery, merged. */
@@ -81,7 +86,11 @@ export function verifyEvent(scheme, secret, body, headers, opts = {}) {
81
86
  if (!Number.isFinite(seconds) || !fresh(seconds))
82
87
  return false;
83
88
  const expected = createHmac('sha256', secret).update(`${seconds}.${body}`, 'utf8').digest('hex');
84
- return parts.some(([k, v]) => k === 'v1' && v !== undefined && same(v, expected));
89
+ return parts.some(([k, v]) => k === (scheme.version ?? 'v1') && v !== undefined && same(v, expected));
90
+ }
91
+ if (scheme.kind === 'secret') {
92
+ const given = h[scheme.header.toLowerCase()];
93
+ return given !== undefined && same(given, secret);
85
94
  }
86
95
  if (scheme.kind === 'hash') {
87
96
  const given = h[scheme.header.toLowerCase()];
@@ -133,11 +142,7 @@ function at(row, path) {
133
142
  function headersFor(templates, values, endpoint) {
134
143
  const out = {};
135
144
  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;
145
+ const value = template.startsWith('$endpoint.') ? at(endpoint, template.slice('$endpoint.'.length)) : template.startsWith('$') ? values[template] : template;
141
146
  if (value !== undefined && value !== null)
142
147
  out[name.toLowerCase()] = String(value);
143
148
  }
@@ -191,7 +196,7 @@ const worldTransport = async (url, body, headers) => {
191
196
  import('../app-route.cjs'),
192
197
  import('../network-policy.cjs'),
193
198
  ]);
194
- if (!appDestination(url) && worldEgressRefusal(url) !== null)
199
+ if (!appDestination(url) && worldEgressRefusal(url, process.env, 'POST') !== null)
195
200
  return 0;
196
201
  try {
197
202
  return (await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body })).status;
@@ -200,27 +205,11 @@ const worldTransport = async (url, body, headers) => {
200
205
  return 0;
201
206
  }
202
207
  };
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
- }
208
+ let deliveryObserver;
209
+ export function setDeliveryObserver(observer) {
210
+ const previous = deliveryObserver;
211
+ deliveryObserver = observer;
212
+ return previous;
224
213
  }
225
214
  /** A message the vendor sends the application's server and decides by its answer (Stripe's real-time authorization
226
215
  * request, answered within its window): over the same transport, waited on for at most `within` milliseconds, the
@@ -231,42 +220,44 @@ export async function askApplication(url, init, within, standIn) {
231
220
  const late = new Promise((done) => { timer = setTimeout(() => done('late'), within); });
232
221
  const stood = await Promise.race([standIn(url, init), late]).finally(() => clearTimeout(timer));
233
222
  if (stood === 'late')
234
- return { status: 0, body: '', missed: 'timeout' };
223
+ return { status: 0, headers: {}, body: '', missed: 'timeout' };
235
224
  if (stood === 'unreachable')
236
- return { status: 0, body: '', missed: 'unreachable' };
225
+ return { status: 0, headers: {}, body: '', missed: 'unreachable' };
226
+ if (stood !== undefined && stood.bodyError !== undefined)
227
+ return { status: 0, headers: {}, body: '', missed: 'unreachable' };
237
228
  if (stood !== undefined)
238
- return stood;
229
+ return { status: stood.status, headers: Object.fromEntries(Object.entries(stood.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v])), body: stood.body };
239
230
  }
240
231
  const [{ appDestination, appFetch }, { worldEgressRefusal }] = await Promise.all([
241
232
  import('../app-route.cjs'),
242
233
  import('../network-policy.cjs'),
243
234
  ]);
244
- if (!appDestination(url) && worldEgressRefusal(url) !== null)
245
- return { status: 0, body: '', missed: 'unreachable' };
235
+ if (!appDestination(url) && worldEgressRefusal(url, process.env, init.method ?? 'GET') !== null)
236
+ return { status: 0, headers: {}, body: '', missed: 'unreachable' };
246
237
  const headers = new Headers(init.headers);
247
238
  for (const [k, v] of Object.entries(deliveryTraceHeaders()))
248
239
  if (!headers.has(k))
249
240
  headers.set(k, v);
250
241
  try {
251
242
  const res = await appFetch(url, { ...init, headers, signal: AbortSignal.timeout(within) });
252
- return { status: res.status, body: await res.text() };
243
+ return { status: res.status, headers: Object.fromEntries(res.headers), body: await res.text() };
253
244
  }
254
245
  catch (e) {
255
- return { status: 0, body: '', missed: e instanceof Error && (e.name === 'TimeoutError' || e.name === 'AbortError') ? 'timeout' : 'unreachable' };
246
+ return { status: 0, headers: {}, body: '', missed: e instanceof Error && (e.name === 'TimeoutError' || e.name === 'AbortError') ? 'timeout' : 'unreachable' };
256
247
  }
257
248
  }
258
249
  /** Every event a write sends, rendered, stored when the vendor keeps its events, signed, delivered to each endpoint
259
250
  * that takes it, and recorded. */
260
- export async function deliverEvents(service, decl, write, data, transport = worldTransport, values, onAnswer) {
251
+ export async function deliverEvents(service, decl, write, data, transport = worldTransport, values) {
261
252
  if (write.storedType === decl.store)
262
253
  return [];
263
254
  const sent = [];
264
255
  for (const type of eventTypesOf(decl, write.operation))
265
- sent.push(...(await deliverEventType(service, decl, write, data, transport, values, type, onAnswer)));
256
+ sent.push(...(await deliverEventType(service, decl, write, data, transport, values, type)));
266
257
  return sent;
267
258
  }
268
259
  /** One event type a write sends: to the live endpoints that take it, stored when the vendor keeps its events. */
269
- async function deliverEventType(service, decl, write, data, transport, values, type, onAnswer) {
260
+ async function deliverEventType(service, decl, write, data, transport, values, type) {
270
261
  const all = twinResources(service, write.root);
271
262
  const kinds = Array.isArray(decl.endpoints) ? decl.endpoints : [decl.endpoints];
272
263
  // an endpoint's row with its joined rows under their names (`app.hook_url` reads the installation's App)
@@ -295,7 +286,7 @@ async function deliverEventType(service, decl, write, data, transport, values, t
295
286
  const kept = decl.store !== undefined ? all.filter((r) => r.type === decl.store).length : 0;
296
287
  const filled = {
297
288
  $type: type, $data: eventData,
298
- $id: `evt_twin_${digest(`${type}|${write.occurredAt}|${kept}|${JSON.stringify(eventData)}`).slice(0, 24)}`,
289
+ $id: `evt_${ksuidFrom(seconds, kept, `${type}|${write.occurredAt}|${kept}|${JSON.stringify(eventData)}`)}`,
299
290
  '$time.ms': millis, '$time.s': seconds, '$time.iso': write.occurredAt,
300
291
  ...requestValues(write.request),
301
292
  ...extra,
@@ -314,7 +305,6 @@ async function deliverEventType(service, decl, write, data, transport, values, t
314
305
  const shared = JSON.stringify(envelope);
315
306
  const each = perEndpoint(decl.envelope);
316
307
  const sent = [];
317
- const to = [];
318
308
  const before = decl.record !== undefined ? all.filter((r) => r.type === decl.record).length : 0;
319
309
  for (const [i, { row, kind }] of endpoints.entries()) {
320
310
  const body = each ? JSON.stringify(fillEnvelope(decl.envelope, filled, row)) : shared;
@@ -323,7 +313,6 @@ async function deliverEventType(service, decl, write, data, transport, values, t
323
313
  const extraHeaders = decl.headers ? headersFor(decl.headers, filled, row) : {};
324
314
  const headers = { ...extraHeaders, ...signEventAll(decl.scheme, String(at(row, kind.secret)), body, seconds, url, seq) };
325
315
  sent.push({ url, body, headers });
326
- to.push({ kind, row });
327
316
  // kept with the write, by an id of the delivery itself (two made at once never take the same one)
328
317
  if (decl.record !== undefined) {
329
318
  await applyTwinWrite(service, {
@@ -336,18 +325,11 @@ async function deliverEventType(service, decl, write, data, transport, values, t
336
325
  // slow or down fails no request
337
326
  // the write's trace is read now, inside its request, and carried on each delivery (W3C trace continuation)
338
327
  const trace = deliveryTraceHeaders();
339
- // each answer is read as the vendor reads it, when the endpoint kind keeps score (its `outcome`)
340
- const stated = (url) => {
341
- const r = decl.receivers === undefined ? undefined : all.filter((x) => x.type === decl.receivers && x.deleted !== true && url.startsWith(String(x.url)))
342
- .sort((x, y) => String(y.url).length - String(x.url).length)[0];
343
- return r === undefined ? undefined : Number(r.status);
344
- };
345
- for (const [i, { url, body: payload, headers }] of sent.entries())
328
+ for (const d of sent)
329
+ deliveryObserver?.({ ...d, service });
330
+ for (const { url, body: payload, headers } of sent)
346
331
  setTimeout(() => {
347
- const answered = stated(url);
348
- (answered !== undefined ? Promise.resolve(answered) : transport(url, payload, { ...headers, ...trace }))
349
- .then((status) => (onAnswer && to[i].kind.outcome ? onAnswer({ ...to[i], status: typeof status === 'number' ? status : 0, occurredAt: write.occurredAt }) : undefined))
350
- .catch(() => { });
332
+ void transport(url, payload, { ...headers, ...trace }).catch(() => { });
351
333
  }, 0);
352
334
  return sent;
353
335
  }
@@ -0,0 +1,10 @@
1
+ /** JSON and wire integers outside JavaScript's safe range retain their decimal digits.
2
+ * Safe integers stay numbers; floating-point tokens keep their wire's double semantics.
3
+ * Quoted strings are consumed whole, so their contents are never rewritten.
4
+ */
5
+ export declare function exactInteger(digits: string): number | string;
6
+ export declare function parseExactJson(text: string, integer?: (digits: string) => number | string | bigint): any;
7
+ /** Ordering integer versions and stored fields must not round adjacent int64 values. */
8
+ export declare function compareExactNumbers(a: unknown, b: unknown): number;
9
+ /** Unsafe integer tokens use decimal strings at kernel interfaces; schema checks retain that meaning. */
10
+ export declare function isExactIntegerValue(value: unknown): boolean;
@@ -0,0 +1,98 @@
1
+ /** JSON and wire integers outside JavaScript's safe range retain their decimal digits.
2
+ * Safe integers stay numbers; floating-point tokens keep their wire's double semantics.
3
+ * Quoted strings are consumed whole, so their contents are never rewritten.
4
+ */
5
+ export function exactInteger(digits) {
6
+ const n = BigInt(digits);
7
+ return n >= -9007199254740991n && n <= 9007199254740991n ? Number(n) : n.toString();
8
+ }
9
+ export function parseExactJson(text, integer = exactInteger) {
10
+ let at = 0;
11
+ const number = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
12
+ const space = () => { while (/[\x20\t\r\n]/.test(text[at] ?? '') && at < text.length)
13
+ at++; };
14
+ const invalid = () => { throw new SyntaxError(`Invalid JSON at position ${at}`); };
15
+ const quoted = () => {
16
+ const begin = at++;
17
+ for (; at < text.length; at++) {
18
+ if (text[at] === '\\') {
19
+ at++;
20
+ continue;
21
+ }
22
+ if (text[at] === '"') {
23
+ const token = text.slice(begin, ++at);
24
+ // JSON.parse reads only a quoted string, never an integer token.
25
+ return JSON.parse(token);
26
+ }
27
+ }
28
+ return invalid();
29
+ };
30
+ const value = () => {
31
+ space();
32
+ if (text[at] === '"')
33
+ return quoted();
34
+ if (text[at] === '[' || text[at] === '{') {
35
+ const array = text[at++] === '[';
36
+ const out = array ? [] : {};
37
+ const end = array ? ']' : '}';
38
+ space();
39
+ if (text[at] === end) {
40
+ at++;
41
+ return out;
42
+ }
43
+ for (;;) {
44
+ space();
45
+ let key;
46
+ if (!array) {
47
+ if (text[at] !== '"')
48
+ return invalid();
49
+ key = quoted();
50
+ space();
51
+ if (text[at++] !== ':')
52
+ return invalid();
53
+ }
54
+ const item = value();
55
+ if (array)
56
+ out.push(item);
57
+ else
58
+ Object.defineProperty(out, key, { value: item, enumerable: true, writable: true, configurable: true });
59
+ space();
60
+ if (text[at] === end) {
61
+ at++;
62
+ return out;
63
+ }
64
+ if (text[at++] !== ',')
65
+ return invalid();
66
+ }
67
+ }
68
+ for (const [literal, decoded] of [['true', true], ['false', false], ['null', null]]) {
69
+ if (text.startsWith(literal, at)) {
70
+ at += literal.length;
71
+ return decoded;
72
+ }
73
+ }
74
+ number.lastIndex = at;
75
+ const token = number.exec(text)?.[0];
76
+ if (token === undefined)
77
+ return invalid();
78
+ at += token.length;
79
+ return /^-?\d+$/.test(token) && !Number.isSafeInteger(Number(token)) ? integer(token) : Number(token);
80
+ };
81
+ const out = value();
82
+ space();
83
+ if (at !== text.length)
84
+ return invalid();
85
+ return out;
86
+ }
87
+ /** Ordering integer versions and stored fields must not round adjacent int64 values. */
88
+ export function compareExactNumbers(a, b) {
89
+ if (/^-?\d+$/.test(String(a)) && /^-?\d+$/.test(String(b))) {
90
+ const x = BigInt(String(a)), y = BigInt(String(b));
91
+ return x < y ? -1 : x > y ? 1 : 0;
92
+ }
93
+ return Number(a) - Number(b);
94
+ }
95
+ /** Unsafe integer tokens use decimal strings at kernel interfaces; schema checks retain that meaning. */
96
+ export function isExactIntegerValue(value) {
97
+ return Number.isInteger(value) || typeof value === 'string' && /^-?\d+$/.test(value) && !Number.isSafeInteger(Number(value));
98
+ }
@@ -17,12 +17,26 @@ export type TwinAuthStrategy =
17
17
  {
18
18
  in: 'lanes';
19
19
  lanes: Record<string, TwinAuthStrategy>;
20
+ } | {
21
+ in: 'choice';
22
+ choices: Array<{
23
+ credential: string;
24
+ strategy: TwinAuthStrategy;
25
+ }>;
26
+ } | {
27
+ in: 'signature';
28
+ algorithm: 'oauth1-hmac-sha1';
20
29
  }
21
30
  /** the key rides in the query string (`?key=…`, `?appid=…`) — no header will do */
22
31
  | {
23
32
  in: 'query';
24
33
  name: string;
25
34
  }
35
+ /** the key rides in the path (Telegram's `/bot<token>/METHOD_NAME`): `pattern`'s first group is its place */
36
+ | {
37
+ in: 'path';
38
+ pattern: string;
39
+ }
26
40
  /** AWS Signature Version 4 — a signature computed per request over the whole canonical request.
27
41
  * `scope` answers which region and service THIS request is for, because a pack may route several
28
42
  * services over one executor (aws routes six); it is pure and is handed no secret. */
@@ -125,8 +139,11 @@ export type CredentialCustody = {
125
139
  };
126
140
  /** `vendor`: the vendor whose rate budget every call is charged to (D8): the call is priced and reserved before it is
127
141
  * made, refused when the budget is spent, and settled with the vendor's answer (its back-off honoured). */
142
+ /** `now`: the instant a signature is made at (the wall clock unless given: an acceptance whose vendor is a fixture World
143
+ * signs at that World's clock, which its twin checks a signature's date against). */
128
144
  export type RemoteExecuteOptions = {
129
145
  custody?: CredentialCustody;
130
146
  vendor?: string;
147
+ now?: () => Date;
131
148
  };
132
149
  export declare function buildRemoteExecute(origin: URL, credential: CredentialPayload, auth?: TwinAuthStrategy, vendorHosts?: readonly HostRule[], options?: RemoteExecuteOptions): RemoteExecute;