@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
package/src/derived.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { parseExactJson } from './exact-json.ts';
1
2
  // THE DERIVED DISPATCH — the generated wire's router (docs/contributing/architecture.md, "The derived
2
3
  // pack"). A derived pack's surface (`src/generated/surface.gen.json`, written by scripts/derive-pack.ts)
3
4
  // names every operation the vendor publishes. This matches a request to one of them and hands it to its
@@ -38,11 +39,11 @@ export type DerivedOperation = {
38
39
  scopes?: ReadonlyArray<string>;
39
40
  credentials?: ReadonlyArray<string>;
40
41
  /** the body's declared fields: name, type, and the numeric bounds the spec states (a strict body, `ctx.fields`) */
41
- body?: ReadonlyArray<{ name: string; type: string; required?: boolean; minimum?: number; maximum?: number }>;
42
+ body?: ReadonlyArray<{ name: string; type: string; required?: boolean; minimum?: number | string; maximum?: number | string }>;
42
43
  };
43
44
  /** `basePath` is what the vendor's server URL puts before every operation path; `spanning` names the
44
45
  * path parameters whose values may hold slashes (a git ref, a file path), which no spec says. */
45
- export type DerivedSurface = { version: string; basePath?: string; spanning?: ReadonlyArray<string>; operations: ReadonlyArray<DerivedOperation> };
46
+ export type DerivedSurface = { graphql?: { sdl: string }; format?: string; commands?: ReadonlyArray<{ id: string }>; version: string; basePath?: string; spanning?: ReadonlyArray<string>; operations: ReadonlyArray<DerivedOperation> };
46
47
 
47
48
  /** What a semantics handler receives: the request, the operation it matched, and its path parameters. */
48
49
  export type DerivedCall = { request: Request; operation: DerivedOperation; params: Record<string, string> };
@@ -77,7 +78,7 @@ export async function checkJsonBody(request: Request): Promise<boolean> {
77
78
  // a body labelled otherwise and not shaped as JSON is the form it is labelled (an OAuth token request), not bad JSON
78
79
  if (!(request.headers.get('content-type') ?? '').includes('json') && !/^\s*[[{]/.test(text)) return true;
79
80
  try {
80
- const value = JSON.parse(text);
81
+ const value = parseExactJson(text);
81
82
  const tag = (request as Tagged)[BODY_TEXT] !== undefined ? BODY_PARSE_KEY.get(request) : undefined;
82
83
  if (tag) BODY_PARSED.set(tag, { value });
83
84
  return true;
@@ -185,6 +186,19 @@ export function matchOperation(routes: Map<string, Route[]>, method: string, pat
185
186
  return undefined;
186
187
  }
187
188
 
189
+ /** The path's template parameters for a request no operation matches (a CORS preflight, a method the path is not
190
+ * served by): read from the most literal route of any method whose path the request's path fits, discriminators
191
+ * aside, so `around` and `gap` know the resource the URL names without parsing it (S3's OPTIONS on `/{Bucket}/{Key+}`). */
192
+ export function pathParams(routes: Map<string, Route[]>, pathname: string): Record<string, string> {
193
+ const fits = [...routes.values()].flat().filter((r) => r.pattern.test(pathname)).sort((a, b) => b.literals - a.literals || b.names.length - a.names.length);
194
+ const route = fits[0];
195
+ const m = route?.pattern.exec(pathname);
196
+ if (!route || !m) return {};
197
+ const params: Record<string, string> = {};
198
+ try { route.names.forEach((name, i) => { params[name] = decodeURIComponent(m[i + 1]!); }); } catch { return {}; }
199
+ return params;
200
+ }
201
+
188
202
  export type DerivedOwner = 'handler' | 'core' | 'gap';
189
203
 
190
204
  /** The header the kernel carries a path prefix's named groups in, from routing to the call (never a client's). */
@@ -194,8 +208,13 @@ export type DerivedFetch = ((request: Request) => Promise<Response>) & {
194
208
  /** Per operationId, who serves it: a semantics handler, the derived core, the pack's existing
195
209
  * fetch while it moves, or nothing (the vendor's gap). */
196
210
  owners(): Record<string, DerivedOwner>;
211
+ /** 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
212
+ * operation ids (an id two units share is two operations, each served or the gap on its own). `owners()` is this map
213
+ * flat, a lane's ids written `<lane>:<id>`. A fetch with no lanes may leave it out: its one unit is `owners()`. */
214
+ ownersByUnit?(): Record<string, Record<string, DerivedOwner>>;
197
215
  /** The vendor's sockets (sockets.ts), for the serve seam to upgrade to, when its manifest declares any. */
198
216
  upgrade?: WebSocketUpgrade;
217
+ maxRequestBodySize?: number;
199
218
  /** The workspace screens it draws, its lanes' too (`GET /twin`'s `screens`). */
200
219
  workspaces?(): Workspace[];
201
220
  /** Whether it lays out frames of the World's board (`semantics/board.ts`). */
@@ -256,7 +275,7 @@ export function createDerivedFetch(options: DerivedFetchOptions): DerivedFetch {
256
275
  if (request.method !== 'GET' && request.method !== 'HEAD' && request.body && !(request.headers.get('content-type') ?? '').includes('multipart')) tagBody(request, await request.clone().text());
257
276
  // the path prefix's named groups (pack-fetch.ts, `pathPrefix`) are the call's parameters too
258
277
  const prefixed = request.headers.get(PREFIX_PARAMS_HEADER);
259
- const call: DerivedCall = { request, operation: matched.operation, params: prefixed ? { ...(JSON.parse(prefixed) as Record<string, string>), ...matched.params } : matched.params };
278
+ const call: DerivedCall = { request, operation: matched.operation, params: prefixed ? { ...(parseExactJson(prefixed) as Record<string, string>), ...matched.params } : matched.params };
260
279
  const around = options.around ?? ((_c: DerivedCall, next: () => Promise<Response>) => next());
261
280
  const handler = handlers[matched.operation.id];
262
281
  if (handler) return around(call, () => handler(call));
package/src/events.ts CHANGED
@@ -8,27 +8,32 @@
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.ts';
11
- import { equalSecrets as same } from './signing.ts';
11
+ import { equalSecrets as same, ksuidFrom } from './signing.ts';
12
12
  import { deliveryTraceHeaders } from './trace-context.ts';
13
13
 
14
14
  /** How the vendor signs a delivery. */
15
15
  export type EventScheme =
16
+ /** One header carrying the destination's secret as it is, no signature over the body (Cloudflare Notifications'
17
+ * `cf-webhook-auth`, the secret a webhook destination was made with). */
18
+ | { kind: 'secret'; header: string }
16
19
  /** A message id, a timestamp and a signature header (the Standard Webhooks shape): `<prefix>-id`,
17
20
  * `<prefix>-timestamp` (seconds) and `<prefix>-signature: v1,<base64 HMAC-SHA256>` over `${id}.${timestamp}.${body}`,
18
21
  * keyed by the base64 part of the secret after `secretPrefix`, or by the secret's own UTF-8 bytes (`secretKey: 'utf8'`:
19
22
  * Polar's, whose SDK base64-encodes the secret as it is before verifying). */
20
23
  | { kind: 'webhook-id'; prefix: string; secretPrefix?: string; secretKey?: 'base64' | 'utf8' }
21
24
  /** One header of a timestamp and its signatures: `<header>: t=<seconds>,v1=<hex HMAC-SHA256>` over `${t}.${body}`,
22
- * keyed by the secret as it is. */
23
- | { kind: 'timestamp-v1'; header: string }
25
+ * keyed by the secret as it is; `version` names the signature's key when the vendor's is not `v1` (ElevenLabs'
26
+ * `ElevenLabs-Signature: t=…,v0=…`). */
27
+ | { kind: 'timestamp-v1'; header: string; version?: string }
24
28
  /** One HMAC header over the body (`X-Hub-Signature-256: sha256=<hex>`, GitHub's), or over `${timestamp}.${body}` when
25
29
  * the vendor sends its timestamp in `timestampHeader`; `signed` names what is signed otherwise, `{t}` the timestamp
26
30
  * and `{body}` the body (Slack's `v0:{t}:{body}`), and `algorithm` the hash (SHA-256 unless named: GitHub's older
27
31
  * `X-Hub-Signature` is SHA-1). */
28
- | { kind: 'hmac'; header: string; encoding: 'hex' | 'base64'; prefix?: string; timestampHeader?: string; signed?: string; algorithm?: 'sha1' | 'sha256'; secretKey?: 'base64' | 'utf8' }
32
+ | { kind: 'hmac'; header: string; encoding: 'hex' | 'base64'; prefix?: string; timestampHeader?: string; signed?: string; algorithm?: 'sha1' | 'sha256' }
29
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
30
- * of the app's client secret followed by the request body); `signed` names the input, `{secret}` and `{body}` */
31
- | { kind: 'hash'; header: string; signed: string; encoding: 'hex' | 'base64'; algorithm?: 'sha256' };
34
+ * of the app's client secret followed by the request body); `signed` names the input, `{secret}` and `{body}`;
35
+ * `base64-unpadded` is base64 with its trailing `=` removed (E2B's `e2b-signature`) */
36
+ | { kind: 'hash'; header: string; signed: string; encoding: 'hex' | 'base64' | 'base64-unpadded'; algorithm?: 'sha256' };
32
37
 
33
38
  /** Where the World's endpoints of one kind are kept (a door the vendor's dashboard stands in for writes them): their
34
39
  * stored type and the fields holding the URL, the signing secret, the event types an endpoint takes (all when absent,
@@ -56,26 +61,8 @@ export type EndpointsDecl = {
56
61
  * when a message mentions it, and every `message` its subscription names). */
57
62
  types?: ReadonlyArray<string>;
58
63
  skip?: ReadonlyArray<string>;
59
- /** What each delivery's answer writes on the endpoint, when the vendor keeps score (Daily's webhook): a success (2xx)
60
- * resets `failures` and writes `success` (templates: `$time.iso`, `$time.s`), a failure adds one to `failures`, and
61
- * `after` failures in a row make `move`, the vendor's transition of a state field, asked of the endpoint's machine,
62
- * on every endpoint but one matching `unless` (Daily's `exponential` retry never breaks). */
63
- outcome?: EndpointOutcome;
64
64
  };
65
65
 
66
- /** An endpoint kind's delivery outcome (EndpointsDecl.outcome). */
67
- export type EndpointOutcome = {
68
- failures: string;
69
- success?: Record<string, string>;
70
- after?: number;
71
- move?: { field: string; to: string; operation: string };
72
- unless?: { field: string; equals: unknown };
73
- };
74
-
75
- /** A delivery's answer as the kernel reads it: the endpoint kind and row it went to, and the receiver's status (0 when
76
- * the World refused it or nothing answered). */
77
- export type DeliveryAnswer = { kind: EndpointsDecl; row: Record<string, unknown>; status: number; occurredAt: string };
78
-
79
66
  /** Whether an endpoint takes an event by its `match`: every field of one match (any, when several) equal to the event's
80
67
  * value, or, a list, holding it. */
81
68
  function matches(row: Record<string, unknown>, match: EndpointsDecl['match'], filled: Record<string, unknown>): boolean {
@@ -114,10 +101,6 @@ export type EventsDecl = {
114
101
  endpoints: EndpointsDecl | ReadonlyArray<EndpointsDecl>;
115
102
  /** The bookkeeping type each delivery is kept as (`_webhook_message`): what a World reads to see what was sent. */
116
103
  record?: string;
117
- /** The bookkeeping type a World states its receivers' answers in (`{url, status}`, Daily's `_receiver`), for receivers
118
- * it runs no server for: a delivery whose URL starts with a stated `url` (the longest) is answered that status, and
119
- * nothing is sent. */
120
- receivers?: string;
121
104
  };
122
105
 
123
106
  /** An event's further envelope values (`$`-named) the pack computes for a write, from the vendor's own state (the
@@ -151,24 +134,27 @@ const webhookKey = (scheme: { secretPrefix?: string; secretKey?: 'base64' | 'utf
151
134
  * made before this one, so a receiver that deduplicates by id drops neither. */
152
135
  export function signEvent(scheme: EventScheme, secret: string, body: string, seconds: number, url: string, seq = 0): Record<string, string> {
153
136
  if (scheme.kind === 'webhook-id') {
154
- const id = `msg_twin_${digest(`${url}|${seconds}|${seq}|${body}`).slice(0, 24)}`;
137
+ // Svix's `msg_` and a KSUID (its ids' form), derived from the delivery itself
138
+ const id = `msg_${ksuidFrom(seconds, seq, `${url}|${seconds}|${seq}|${body}`)}`;
155
139
  const p = scheme.prefix.toLowerCase();
156
140
  const signature = createHmac('sha256', webhookKey(scheme, secret)).update(`${id}.${seconds}.${body}`, 'utf8').digest('base64');
157
141
  return { [`${p}-id`]: id, [`${p}-timestamp`]: String(seconds), [`${p}-signature`]: `v1,${signature}` };
158
142
  }
159
143
  if (scheme.kind === 'timestamp-v1') {
160
144
  const signature = createHmac('sha256', secret).update(`${seconds}.${body}`, 'utf8').digest('hex');
161
- return { [scheme.header.toLowerCase()]: `t=${seconds},v1=${signature}` };
145
+ return { [scheme.header.toLowerCase()]: `t=${seconds},${scheme.version ?? 'v1'}=${signature}` };
162
146
  }
147
+ if (scheme.kind === 'secret') return { [scheme.header.toLowerCase()]: secret };
163
148
  if (scheme.kind === 'hash') return { [scheme.header.toLowerCase()]: hashOf(scheme, secret, body) };
164
- // the key is the secret as it is, or its base64 decoded (`secretKey: 'base64'`, Daily's)
165
- const mac = createHmac(scheme.algorithm ?? 'sha256', scheme.secretKey === 'base64' ? Buffer.from(secret, 'base64') : secret).update(hmacBase(scheme, seconds, body), 'utf8').digest(scheme.encoding);
149
+ const mac = createHmac(scheme.algorithm ?? 'sha256', secret).update(hmacBase(scheme, seconds, body), 'utf8').digest(scheme.encoding);
166
150
  return { [scheme.header.toLowerCase()]: `${scheme.prefix ?? ''}${mac}`, ...(scheme.timestampHeader ? { [scheme.timestampHeader.toLowerCase()]: String(seconds) } : {}) };
167
151
  }
168
152
 
169
153
  /** A `hash` scheme's value: the digest of its `signed` template with the secret and the body put in. */
170
- const hashOf = (scheme: { signed: string; encoding: 'hex' | 'base64'; algorithm?: 'sha256' }, secret: string, body: string): string =>
171
- createHash(scheme.algorithm ?? 'sha256').update(scheme.signed.replaceAll('{secret}', secret).replaceAll('{body}', body), 'utf8').digest(scheme.encoding);
154
+ const hashOf = (scheme: { signed: string; encoding: 'hex' | 'base64' | 'base64-unpadded'; algorithm?: 'sha256' }, secret: string, body: string): string => {
155
+ const digest = createHash(scheme.algorithm ?? 'sha256').update(scheme.signed.replaceAll('{secret}', secret).replaceAll('{body}', body), 'utf8');
156
+ return scheme.encoding === 'base64-unpadded' ? digest.digest('base64').replace(/=+$/, '') : digest.digest(scheme.encoding);
157
+ };
172
158
 
173
159
  /** What an `hmac` scheme signs: its `signed` template, else `${t}.${body}` with a timestamp header, else the body. */
174
160
  const hmacBase = (scheme: { timestampHeader?: string; signed?: string }, seconds: number, body: string): string =>
@@ -199,8 +185,9 @@ export function verifyEvent(scheme: EventScheme, secret: string, body: string, h
199
185
  const seconds = Number(parts.find(([k]) => k === 't')?.[1]);
200
186
  if (!Number.isFinite(seconds) || !fresh(seconds)) return false;
201
187
  const expected = createHmac('sha256', secret).update(`${seconds}.${body}`, 'utf8').digest('hex');
202
- return parts.some(([k, v]) => k === 'v1' && v !== undefined && same(v, expected));
188
+ return parts.some(([k, v]) => k === (scheme.version ?? 'v1') && v !== undefined && same(v, expected));
203
189
  }
190
+ if (scheme.kind === 'secret') { const given = h[scheme.header.toLowerCase()]; return given !== undefined && same(given, secret); }
204
191
  if (scheme.kind === 'hash') { const given = h[scheme.header.toLowerCase()]; return given !== undefined && same(given, hashOf(scheme, secret, body)); }
205
192
  const seconds = scheme.timestampHeader ? Number(h[scheme.timestampHeader.toLowerCase()]) : undefined;
206
193
  if (seconds !== undefined && (!Number.isFinite(seconds) || !fresh(seconds))) return false;
@@ -243,11 +230,7 @@ function at(row: Record<string, unknown>, path: string): unknown {
243
230
  function headersFor(templates: Record<string, string>, values: Record<string, unknown>, endpoint: Record<string, unknown>): Record<string, string> {
244
231
  const out: Record<string, string> = {};
245
232
  for (const [name, template] of Object.entries(templates)) {
246
- // a literal may stand before an endpoint's field (`Basic $endpoint.basicAuth`); the header is dropped when it is empty
247
- const prefixed = /^(.+?)\$endpoint\.([\w.]+)$/.exec(template);
248
- const field = prefixed ? at(endpoint, prefixed[2]!) : undefined;
249
- const value = prefixed ? (field === undefined || field === null || field === '' ? undefined : `${prefixed[1]}${String(field)}`)
250
- : template.startsWith('$endpoint.') ? at(endpoint, template.slice('$endpoint.'.length)) : template.startsWith('$') ? values[template] : template;
233
+ const value = template.startsWith('$endpoint.') ? at(endpoint, template.slice('$endpoint.'.length)) : template.startsWith('$') ? values[template] : template;
251
234
  if (value !== undefined && value !== null) out[name.toLowerCase()] = String(value);
252
235
  }
253
236
  return out;
@@ -298,38 +281,35 @@ function requestValues(request: Request | undefined): Record<string, string> {
298
281
  const worldTransport: EventTransport = async (url, body, headers) => {
299
282
  const [{ appDestination, appFetch }, { worldEgressRefusal }] = await Promise.all([
300
283
  import('../app-route.cjs') as Promise<{ appDestination: (value: string) => unknown; appFetch: (url: string, init: RequestInit) => Promise<unknown> }>,
301
- import('../network-policy.cjs') as Promise<{ worldEgressRefusal: (value: string) => unknown }>,
284
+ import('../network-policy.cjs') as Promise<{ worldEgressRefusal: (value: string, env?: NodeJS.ProcessEnv, method?: string) => unknown }>,
302
285
  ]);
303
- if (!appDestination(url) && worldEgressRefusal(url) !== null) return 0;
286
+ if (!appDestination(url) && worldEgressRefusal(url, process.env, 'POST') !== null) return 0;
304
287
  try { return ((await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body })) as { status: number }).status; } catch { return 0; }
305
288
  };
306
289
 
307
- /** A message a vendor sends the application's own server that is no declared event (Google Calendar's push notification:
308
- * a bodiless POST with its X-Goog headers), over the same transport: the receiver's status, or 0 when the World lets
309
- * no request out to it or it cannot be reached. The vendor waits on it, as the sender of such a message does. */
310
- export async function deliverToApplication(url: string, init: RequestInit): Promise<number> {
311
- const [{ appDestination, appFetch }, { worldEgressRefusal }] = await Promise.all([
312
- import('../app-route.cjs') as Promise<{ appDestination: (value: string) => unknown; appFetch: (url: string, init: RequestInit) => Promise<{ status: number }> }>,
313
- import('../network-policy.cjs') as Promise<{ worldEgressRefusal: (value: string) => unknown }>,
314
- ]);
315
- if (!appDestination(url) && worldEgressRefusal(url) !== null) return 0;
316
- // the request that caused the message is its trace's parent (trace-context.ts)
317
- const headers = new Headers(init.headers);
318
- for (const [k, v] of Object.entries(deliveryTraceHeaders())) if (!headers.has(k)) headers.set(k, v);
319
- try { return (await appFetch(url, { ...init, headers })).status; } catch { return 0; }
290
+ /** Who watches what the World sends (a walk reading each delivery as its receiver would): told of every delivery as it
291
+ * is made, before its transport. Set and restored by the walk, as its World store is; unset, nothing watches. */
292
+ export type DeliveryObserver = (delivery: EventDelivery & { service: string }) => void;
293
+ let deliveryObserver: DeliveryObserver | undefined;
294
+ export function setDeliveryObserver(observer: DeliveryObserver | undefined): DeliveryObserver | undefined {
295
+ const previous = deliveryObserver;
296
+ deliveryObserver = observer;
297
+ return previous;
320
298
  }
321
299
 
322
- /** What the application's server answered a message the vendor waits on, or why it did not answer: `missed` when the
323
- * World let no request out to it or it could not be reached (`unreachable`), or it did not answer in time (`timeout`). */
324
- export type ApplicationAnswer = { status: number; body: string; missed?: 'timeout' | 'unreachable' };
300
+ /** What the application's server answered a message the vendor waits on (its status, its headers by lower-cased name,
301
+ * its body), or why it did not answer: `missed` when the World let no request out to it or it could not be reached
302
+ * (`unreachable`), or it did not answer in time (`timeout`). */
303
+ export type ApplicationAnswer = { status: number; headers: Record<string, string>; body: string; missed?: 'timeout' | 'unreachable' };
325
304
 
326
305
  /** Who answers what the vendor asks the application (`ctx.ask`) before the World's application route does, given to a
327
306
  * pack's fetch (`PackFetchOptions.application`) and belonging to that fetch alone: an answer it gives is the
328
307
  * application's; `'unreachable'` is a question nobody answers (a walk's life, a sealed World, takes no other); undefined
329
308
  * is a question it does not take, which goes on as any other does: the application route, the World's egress rule, the
330
309
  * network. A walk's stand-in answers every question (its life's answers, else unreachable); a hosted World's router
331
- * answers only what it can answer truthfully (WorldDoors.askInWorld) and leaves the rest. */
332
- export type ApplicationStandIn = (url: string, init: RequestInit) => Promise<{ status: number; body: string } | 'unreachable' | undefined>;
310
+ * answers only what it can answer truthfully (WorldDoors.askInWorld) and leaves the rest. `bodyError` models a
311
+ * disconnect after the response headers; a caller needing the complete body receives no complete answer. */
312
+ export type ApplicationStandIn = (url: string, init: RequestInit) => Promise<{ status: number; headers?: Record<string, string>; body: string; bodyError?: string } | 'unreachable' | undefined>;
333
313
 
334
314
  /** A message the vendor sends the application's server and decides by its answer (Stripe's real-time authorization
335
315
  * request, answered within its window): over the same transport, waited on for at most `within` milliseconds, the
@@ -339,22 +319,23 @@ export async function askApplication(url: string, init: RequestInit, within: num
339
319
  let timer: ReturnType<typeof setTimeout> | undefined;
340
320
  const late = new Promise<'late'>((done) => { timer = setTimeout(() => done('late'), within); });
341
321
  const stood = await Promise.race([standIn(url, init), late]).finally(() => clearTimeout(timer));
342
- if (stood === 'late') return { status: 0, body: '', missed: 'timeout' };
343
- if (stood === 'unreachable') return { status: 0, body: '', missed: 'unreachable' };
344
- if (stood !== undefined) return stood;
322
+ if (stood === 'late') return { status: 0, headers: {}, body: '', missed: 'timeout' };
323
+ if (stood === 'unreachable') return { status: 0, headers: {}, body: '', missed: 'unreachable' };
324
+ if (stood !== undefined && stood.bodyError !== undefined) return { status: 0, headers: {}, body: '', missed: 'unreachable' };
325
+ if (stood !== undefined) return { status: stood.status, headers: Object.fromEntries(Object.entries(stood.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v])), body: stood.body };
345
326
  }
346
327
  const [{ appDestination, appFetch }, { worldEgressRefusal }] = await Promise.all([
347
- import('../app-route.cjs') as Promise<{ appDestination: (value: string) => unknown; appFetch: (url: string, init: RequestInit) => Promise<{ status: number; text(): Promise<string> }> }>,
348
- import('../network-policy.cjs') as Promise<{ worldEgressRefusal: (value: string) => unknown }>,
328
+ import('../app-route.cjs') as Promise<{ appDestination: (value: string) => unknown; appFetch: (url: string, init: RequestInit) => Promise<{ status: number; headers: Headers; text(): Promise<string> }> }>,
329
+ import('../network-policy.cjs') as Promise<{ worldEgressRefusal: (value: string, env?: NodeJS.ProcessEnv, method?: string) => unknown }>,
349
330
  ]);
350
- if (!appDestination(url) && worldEgressRefusal(url) !== null) return { status: 0, body: '', missed: 'unreachable' };
331
+ if (!appDestination(url) && worldEgressRefusal(url, process.env, init.method ?? 'GET') !== null) return { status: 0, headers: {}, body: '', missed: 'unreachable' };
351
332
  const headers = new Headers(init.headers);
352
333
  for (const [k, v] of Object.entries(deliveryTraceHeaders())) if (!headers.has(k)) headers.set(k, v);
353
334
  try {
354
335
  const res = await appFetch(url, { ...init, headers, signal: AbortSignal.timeout(within) });
355
- return { status: res.status, body: await res.text() };
336
+ return { status: res.status, headers: Object.fromEntries(res.headers), body: await res.text() };
356
337
  } catch (e) {
357
- return { status: 0, body: '', missed: e instanceof Error && (e.name === 'TimeoutError' || e.name === 'AbortError') ? 'timeout' : 'unreachable' };
338
+ return { status: 0, headers: {}, body: '', missed: e instanceof Error && (e.name === 'TimeoutError' || e.name === 'AbortError') ? 'timeout' : 'unreachable' };
358
339
  }
359
340
  }
360
341
 
@@ -367,11 +348,10 @@ export async function deliverEvents(
367
348
  data: (type: string) => Promise<Record<string, unknown>>,
368
349
  transport: EventTransport = worldTransport,
369
350
  values?: (type: string) => Promise<Record<string, unknown>>,
370
- onAnswer?: (answer: DeliveryAnswer) => Promise<void>,
371
351
  ): Promise<EventDelivery[]> {
372
352
  if (write.storedType === decl.store) return [];
373
353
  const sent: EventDelivery[] = [];
374
- for (const type of eventTypesOf(decl, write.operation)) sent.push(...(await deliverEventType(service, decl, write, data, transport, values, type, onAnswer)));
354
+ for (const type of eventTypesOf(decl, write.operation)) sent.push(...(await deliverEventType(service, decl, write, data, transport, values, type)));
375
355
  return sent;
376
356
  }
377
357
 
@@ -384,7 +364,6 @@ async function deliverEventType(
384
364
  transport: EventTransport,
385
365
  values: ((type: string) => Promise<Record<string, unknown>>) | undefined,
386
366
  type: string,
387
- onAnswer?: (answer: DeliveryAnswer) => Promise<void>,
388
367
  ): Promise<EventDelivery[]> {
389
368
  const all = twinResources(service, write.root) as unknown as Array<Record<string, unknown>>;
390
369
  const kinds = Array.isArray(decl.endpoints) ? decl.endpoints : [decl.endpoints as EndpointsDecl];
@@ -411,7 +390,7 @@ async function deliverEventType(
411
390
  const kept = decl.store !== undefined ? all.filter((r) => r.type === decl.store).length : 0;
412
391
  const filled: Record<string, unknown> = {
413
392
  $type: type, $data: eventData,
414
- $id: `evt_twin_${digest(`${type}|${write.occurredAt}|${kept}|${JSON.stringify(eventData)}`).slice(0, 24)}`,
393
+ $id: `evt_${ksuidFrom(seconds, kept, `${type}|${write.occurredAt}|${kept}|${JSON.stringify(eventData)}`)}`,
415
394
  '$time.ms': millis, '$time.s': seconds, '$time.iso': write.occurredAt,
416
395
  ...requestValues(write.request),
417
396
  ...extra,
@@ -428,7 +407,6 @@ async function deliverEventType(
428
407
  const shared = JSON.stringify(envelope);
429
408
  const each = perEndpoint(decl.envelope);
430
409
  const sent: EventDelivery[] = [];
431
- const to: Array<{ kind: EndpointsDecl; row: Record<string, unknown> }> = [];
432
410
  const before = decl.record !== undefined ? all.filter((r) => r.type === decl.record).length : 0;
433
411
  for (const [i, { row, kind }] of endpoints.entries()) {
434
412
  const body = each ? JSON.stringify(fillEnvelope(decl.envelope, filled, row)) : shared;
@@ -437,7 +415,6 @@ async function deliverEventType(
437
415
  const extraHeaders = decl.headers ? headersFor(decl.headers, filled, row) : {};
438
416
  const headers = { ...extraHeaders, ...signEventAll(decl.scheme, String(at(row, kind.secret)), body, seconds, url, seq) };
439
417
  sent.push({ url, body, headers });
440
- to.push({ kind, row });
441
418
  // kept with the write, by an id of the delivery itself (two made at once never take the same one)
442
419
  if (decl.record !== undefined) {
443
420
  await applyTwinWrite(service, {
@@ -450,17 +427,9 @@ async function deliverEventType(
450
427
  // slow or down fails no request
451
428
  // the write's trace is read now, inside its request, and carried on each delivery (W3C trace continuation)
452
429
  const trace = deliveryTraceHeaders();
453
- // each answer is read as the vendor reads it, when the endpoint kind keeps score (its `outcome`)
454
- const stated = (url: string): number | undefined => {
455
- const r = decl.receivers === undefined ? undefined : all.filter((x) => x.type === decl.receivers && x.deleted !== true && url.startsWith(String(x.url)))
456
- .sort((x, y) => String(y.url).length - String(x.url).length)[0];
457
- return r === undefined ? undefined : Number(r.status);
458
- };
459
- for (const [i, { url, body: payload, headers }] of sent.entries()) setTimeout(() => {
460
- const answered = stated(url);
461
- (answered !== undefined ? Promise.resolve(answered) : transport(url, payload, { ...headers, ...trace }))
462
- .then((status) => (onAnswer && to[i]!.kind.outcome ? onAnswer({ ...to[i]!, status: typeof status === 'number' ? status : 0, occurredAt: write.occurredAt }) : undefined))
463
- .catch(() => {});
430
+ for (const d of sent) deliveryObserver?.({ ...d, service });
431
+ for (const { url, body: payload, headers } of sent) setTimeout(() => {
432
+ void transport(url, payload, { ...headers, ...trace }).catch(() => {});
464
433
  }, 0);
465
434
  return sent;
466
435
  }
@@ -0,0 +1,71 @@
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: string): number | string {
6
+ const n = BigInt(digits);
7
+ return n >= -9007199254740991n && n <= 9007199254740991n ? Number(n) : n.toString();
8
+ }
9
+
10
+ export function parseExactJson(text: string, integer: (digits: string) => number | string | bigint = exactInteger): any {
11
+ let at = 0;
12
+ const number = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
13
+ const space = (): void => { while (/[\x20\t\r\n]/.test(text[at] ?? '') && at < text.length) at++; };
14
+ const invalid = (): never => { throw new SyntaxError(`Invalid JSON at position ${at}`); };
15
+ const quoted = (): string => {
16
+ const begin = at++;
17
+ for (; at < text.length; at++) {
18
+ if (text[at] === '\\') { at++; continue; }
19
+ if (text[at] === '"') {
20
+ const token = text.slice(begin, ++at);
21
+ // JSON.parse reads only a quoted string, never an integer token.
22
+ return JSON.parse(token);
23
+ }
24
+ }
25
+ return invalid();
26
+ };
27
+ const value = (): any => {
28
+ space();
29
+ if (text[at] === '"') return quoted();
30
+ if (text[at] === '[' || text[at] === '{') {
31
+ const array = text[at++] === '[';
32
+ const out: any = array ? [] : {};
33
+ const end = array ? ']' : '}';
34
+ space(); if (text[at] === end) { at++; return out; }
35
+ for (;;) {
36
+ space();
37
+ let key: string | undefined;
38
+ if (!array) { if (text[at] !== '"') return invalid(); key = quoted(); space(); if (text[at++] !== ':') return invalid(); }
39
+ const item = value();
40
+ if (array) out.push(item);
41
+ else Object.defineProperty(out, key!, { value: item, enumerable: true, writable: true, configurable: true });
42
+ space();
43
+ if (text[at] === end) { at++; return out; }
44
+ if (text[at++] !== ',') return invalid();
45
+ }
46
+ }
47
+ for (const [literal, decoded] of [['true', true], ['false', false], ['null', null]] as const) {
48
+ if (text.startsWith(literal, at)) { at += literal.length; return decoded; }
49
+ }
50
+ number.lastIndex = at;
51
+ const token = number.exec(text)?.[0];
52
+ if (token === undefined) return invalid();
53
+ at += token.length;
54
+ return /^-?\d+$/.test(token) && !Number.isSafeInteger(Number(token)) ? integer(token) : Number(token);
55
+ };
56
+ const out = value(); space(); if (at !== text.length) return invalid(); return out;
57
+ }
58
+
59
+ /** Ordering integer versions and stored fields must not round adjacent int64 values. */
60
+ export function compareExactNumbers(a: unknown, b: unknown): number {
61
+ if (/^-?\d+$/.test(String(a)) && /^-?\d+$/.test(String(b))) {
62
+ const x = BigInt(String(a)), y = BigInt(String(b));
63
+ return x < y ? -1 : x > y ? 1 : 0;
64
+ }
65
+ return Number(a) - Number(b);
66
+ }
67
+
68
+ /** Unsafe integer tokens use decimal strings at kernel interfaces; schema checks retain that meaning. */
69
+ export function isExactIntegerValue(value: unknown): boolean {
70
+ return Number.isInteger(value) || typeof value === 'string' && /^-?\d+$/.test(value) && !Number.isSafeInteger(Number(value));
71
+ }
package/src/executor.ts CHANGED
@@ -24,8 +24,12 @@ export type TwinAuthStrategy =
24
24
  /** A vendor of lanes whose lanes authenticate apart (Cloudflare's API by bearer, its R2 by SigV4): a lane named takes
25
25
  * its own strategy, every other call the sealed headers; the request names its lane (`RemoteExecuteRequest.lane`). */
26
26
  | { in: 'lanes'; lanes: Record<string, TwinAuthStrategy> }
27
+ | { in: 'choice'; choices: Array<{ credential: string; strategy: TwinAuthStrategy }> }
28
+ | { in: 'signature'; algorithm: 'oauth1-hmac-sha1' }
27
29
  /** the key rides in the query string (`?key=…`, `?appid=…`) — no header will do */
28
30
  | { in: 'query'; name: string }
31
+ /** the key rides in the path (Telegram's `/bot<token>/METHOD_NAME`): `pattern`'s first group is its place */
32
+ | { in: 'path'; pattern: string }
29
33
  /** AWS Signature Version 4 — a signature computed per request over the whole canonical request.
30
34
  * `scope` answers which region and service THIS request is for, because a pack may route several
31
35
  * services over one executor (aws routes six); it is pure and is handed no secret. */
@@ -76,6 +80,29 @@ export type TwinAuthStrategy =
76
80
  rotate?: { answer: string; credential: string };
77
81
  };
78
82
 
83
+ async function oauth1SignedHeader(credential: CredentialPayload, method: string, target: URL, headers: Headers, body?: string | Uint8Array): Promise<string> {
84
+ const field = (name: string): string => {
85
+ const value = credential[name];
86
+ if (typeof value !== 'string' || !value) throw new Error(`remote execute: oauth1-hmac-sha1 needs sealed ${name}`);
87
+ return value;
88
+ };
89
+ const consumerKey = field('consumerKey'), consumerSecret = field('consumerSecret');
90
+ const token = field('token'), tokenSecret = field('tokenSecret');
91
+ const encode = (value: string): string => encodeURIComponent(value).replace(/[!\x27()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
92
+ const protocol: Array<[string, string]> = [
93
+ ['oauth_consumer_key', consumerKey], ['oauth_token', token], ['oauth_signature_method', 'HMAC-SHA1'],
94
+ ['oauth_timestamp', String(Math.floor(Date.now() / 1000))], ['oauth_nonce', crypto.randomUUID()], ['oauth_version', '1.0'],
95
+ ];
96
+ const form = typeof body === 'string' && /^application\/x-www-form-urlencoded/i.test(headers.get('content-type') ?? '') ? [...new URLSearchParams(body)] : [];
97
+ const pairs = [...protocol, ...target.searchParams, ...form].filter(([name]) => name !== 'oauth_signature').map(([name, value]) => [encode(name), encode(value)]);
98
+ pairs.sort(([a, av], [b, bv]) => a < b ? -1 : a > b ? 1 : av < bv ? -1 : av > bv ? 1 : 0);
99
+ const base = `${method.toUpperCase()}&${encode(`${target.protocol}//${target.host.toLowerCase()}${target.pathname}`)}&${encode(pairs.map(([name, value]) => `${name}=${value}`).join('&'))}`;
100
+ const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(`${encode(consumerSecret)}&${encode(tokenSecret)}`), { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
101
+ const bytes = new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(base)));
102
+ const signature = btoa(Array.from(bytes, byte => String.fromCharCode(byte)).join(''));
103
+ return `OAuth ${[...protocol, ['oauth_signature', signature]].map(([name, value]) => `${encode(name)}="${encode(value)}"`).join(', ')}`;
104
+ }
105
+
79
106
  const MAX_ORIGIN_LENGTH = 2048;
80
107
 
81
108
  function isLoopbackHost(hostname: string): boolean {
@@ -224,7 +251,9 @@ async function awsSigV4Headers(input: {
224
251
  export type CredentialCustody = { key: string; open: () => Promise<CredentialPayload>; seal: (credential: CredentialPayload) => Promise<void> | void };
225
252
  /** `vendor`: the vendor whose rate budget every call is charged to (D8): the call is priced and reserved before it is
226
253
  * made, refused when the budget is spent, and settled with the vendor's answer (its back-off honoured). */
227
- export type RemoteExecuteOptions = { custody?: CredentialCustody; vendor?: string };
254
+ /** `now`: the instant a signature is made at (the wall clock unless given: an acceptance whose vendor is a fixture World
255
+ * signs at that World's clock, which its twin checks a signature's date against). */
256
+ export type RemoteExecuteOptions = { custody?: CredentialCustody; vendor?: string; now?: () => Date };
228
257
  export function buildRemoteExecute(origin: URL, credential: CredentialPayload, auth?: TwinAuthStrategy, vendorHosts?: readonly HostRule[], options: RemoteExecuteOptions = {}): RemoteExecute {
229
258
  if (auth?.in === 'lanes') {
230
259
  // each lane's strategy over the same origin and credential, the budget charged once, around them all
@@ -234,7 +263,12 @@ export function buildRemoteExecute(origin: URL, credential: CredentialPayload, a
234
263
  const execute: RemoteExecute = (request) => (byLane.get(request.lane ?? '') ?? headers)(request);
235
264
  return options.vendor === undefined ? execute : budgeted(options.vendor, execute, options.custody?.key ?? JSON.stringify(credential.headers));
236
265
  }
237
- const execute = auth?.in === 'exchange' ? exchangeExecute(origin, credential, auth, vendorHosts, options.custody) : direct(origin, credential, auth, vendorHosts);
266
+ if (auth?.in === 'choice') {
267
+ const picked = auth.choices.find(c => typeof credential[c.credential] === 'string' && credential[c.credential] !== '');
268
+ if (!picked) throw new Error('remote execute: no credential matches the declared strategy choices');
269
+ return buildRemoteExecute(origin, credential, picked.strategy, vendorHosts, options);
270
+ }
271
+ const execute = auth?.in === 'exchange' ? exchangeExecute(origin, credential, auth, vendorHosts, options.custody) : direct(origin, credential, auth, vendorHosts, options.now);
238
272
  return options.vendor === undefined ? execute : budgeted(options.vendor, execute, options.custody?.key ?? JSON.stringify(credential.headers));
239
273
  }
240
274
 
@@ -253,7 +287,7 @@ function budgeted(vendor: string, execute: RemoteExecute, token: string): Remote
253
287
  }
254
288
 
255
289
  /** The call made as the root's credential is applied by the strategy (header replacement when none). */
256
- function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinAuthStrategy, { in: 'exchange' | 'lanes' }>, vendorHosts?: readonly HostRule[]): RemoteExecute {
290
+ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinAuthStrategy, { in: 'exchange' | 'choice' | 'lanes' }>, vendorHosts?: readonly HostRule[], now: () => Date = () => new Date()): RemoteExecute {
257
291
  return async ({ method, path, headers, body, responseType, presigned, issued }) => {
258
292
  // A PRESIGNED UPLOAD URL the vendor returned: its own signature is its authorization, so the
259
293
  // sealed credential never goes with it, whatever host it names. A host other than the root's
@@ -284,13 +318,23 @@ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinA
284
318
  // the origin into USERINFO and ships the credential to evil.com. A path is a path:
285
319
  // it starts with '/', and the parse must land on the validated origin, asserted.
286
320
  let target: URL;
321
+ // the vendor's own URL, where a loopback root stands in for it: what a signature is made over
322
+ let vendorUrl: URL | undefined;
287
323
  if (/^https:\/\//i.test(path) && vendorHosts?.length) {
288
324
  // a second host the pack declares for this vendor, on a path its rule names: validated as the root is
289
325
  target = new URL(path);
290
326
  if (target.username || target.password) throw new Error('remote execute: a vendor URL carries no userinfo');
291
327
  validateRemoteOrigin(target.origin);
292
- const declared = vendorHosts.some((r) => 'host' in r && !r.exclude && r.host === target.hostname && (r.pathPattern === undefined || new RegExp(r.pathPattern).test(target.pathname)));
328
+ const named = (r: HostRule): boolean => ('host' in r ? r.host === target.hostname : 'suffix' in r ? target.hostname.endsWith(r.suffix) : new RegExp(r.hostPattern).test(target.hostname))
329
+ && (r.pathPattern === undefined || new RegExp(r.pathPattern).test(target.pathname));
330
+ const declared = vendorHosts.some((r) => !r.exclude && named(r)) && !vendorHosts.some((r) => r.exclude && named(r));
293
331
  if (!declared) throw new Error(`remote execute: ${target.host}${target.pathname.slice(0, 64)} is not a host this vendor's pack declares for this path`);
332
+ // A loopback root is the vendor substitute. Preserve the vendor Host while retaining
333
+ // the already-validated root's mounted path; an absolute URL never escapes that root.
334
+ if (isLoopbackHost(origin.hostname)) {
335
+ vendorUrl = target;
336
+ target = new URL(origin.href.replace(/\/+$/, '') + target.pathname + target.search);
337
+ }
294
338
  } else {
295
339
  if (!path.startsWith('/')) throw new Error(`remote execute: path must start with '/' (got ${JSON.stringify(path.slice(0, 64))})`);
296
340
  // the base keeps its path: a served world's twin lives under /<org>/<world>/<vendor>, github.com under /
@@ -302,6 +346,8 @@ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinA
302
346
  // a credential the vendor issued in this perform is the request's authorization; the sealed one stays home
303
347
  if (issued !== undefined) outbound.set('authorization', issued);
304
348
  else for (const [name, value] of Object.entries(credential.headers)) outbound.set(name, value);
349
+ // the vendor's host, kept over any the credential carries (a loopback twin routes by it, as the World's proxy's)
350
+ if (vendorUrl) { outbound.set('host', vendorUrl.host); outbound.set('x-volter-twin-original-host', vendorUrl.host); }
305
351
 
306
352
  // ── THE STRATEGIES BEYOND HEADER REPLACEMENT ──────────────────────────────────────────────
307
353
  // Applied AFTER the anchoring check above, so nothing here can move the request off the
@@ -310,30 +356,43 @@ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinA
310
356
  // FAIL CLOSED. A strategy with no secret must never send a request that merely LOOKS
311
357
  // authenticated — that silence is the whole defect this exists to remove. The refusals name the
312
358
  // strategy and the parameter, never the secret.
313
- if (auth !== undefined && issued === undefined) {
314
- const secret = credential.secret;
315
- if (typeof secret !== 'string' || secret === '') {
316
- throw new Error(`remote execute: the ${auth.in} credential strategy needs \`secret\` on the sealed credential, and it is absent`);
317
- }
318
- if (auth.in === 'query') {
319
- // `set`, not `append`: it REPLACES whatever placeholder the pack put in its own path,
320
- // exactly as `outbound.set` replaces a placeholder authorization header.
321
- target.searchParams.set(auth.name, secret);
322
- } else if (auth.algorithm === 'aws-sigv4') {
323
- const keyId = credential.keyId;
324
- if (typeof keyId !== 'string' || keyId === '') {
325
- throw new Error('remote execute: the aws-sigv4 credential strategy needs `keyId` on the sealed credential, and it is absent');
359
+ // an issued credential is the authorization: nothing of the sealed one signs the request
360
+ if (issued === undefined) {
361
+ if (auth?.in === 'signature' && auth.algorithm === 'oauth1-hmac-sha1') {
362
+ outbound.set('authorization', await oauth1SignedHeader(credential, method, target, outbound, body));
363
+ } else if (auth !== undefined) {
364
+ const secret = credential.secret;
365
+ if (typeof secret !== 'string' || secret === '') {
366
+ throw new Error(`remote execute: the ${auth.in} credential strategy needs \`secret\` on the sealed credential, and it is absent`);
367
+ }
368
+ if (auth.in === 'query') {
369
+ // `set`, not `append`: it REPLACES whatever placeholder the pack put in its own path,
370
+ // exactly as `outbound.set` replaces a placeholder authorization header.
371
+ target.searchParams.set(auth.name, secret);
372
+ } else if (auth.in === 'path') {
373
+ // the key's place in the path, filled after the origin is validated; a secret that could end the segment or
374
+ // the path (a slash, a query, a fragment, an escape) is refused, so the request cannot leave the validated root
375
+ if (/[/?#%\\]/.test(secret)) throw new Error('remote execute: the path credential strategy refuses a secret that is not one path segment');
376
+ const place = new RegExp(auth.pattern, 'd').exec(target.pathname)?.indices?.[1];
377
+ if (!place) throw new Error(`remote execute: the path credential strategy's pattern does not name the key's place in ${target.pathname.slice(0, 64)}`);
378
+ target.pathname = target.pathname.slice(0, place[0]) + secret + target.pathname.slice(place[1]);
379
+ } else if (auth.algorithm === 'aws-sigv4') {
380
+ const keyId = credential.keyId;
381
+ if (typeof keyId !== 'string' || keyId === '') {
382
+ throw new Error('remote execute: the aws-sigv4 credential strategy needs `keyId` on the sealed credential, and it is absent');
383
+ }
384
+ // the HOST goes with it: an AWS endpoint names its own region, and only the pack knows how
385
+ const signedUrl = vendorUrl ?? target;
386
+ const { region, service } = typeof auth.scope === 'function' ? auth.scope({ method, path, host: signedUrl.host, headers: Object.fromEntries(outbound.entries()), ...(body === undefined ? {} : { body }) }) : auth.scope;
387
+ const signed = await awsSigV4Headers({ method, url: signedUrl, ...(body === undefined ? {} : { body }), keyId, secret, region, service, headers: outbound, at: now() });
388
+ for (const [name, value] of Object.entries(signed)) outbound.set(name, value);
389
+ } else {
390
+ // `null` is the vendor's own exemption, declared by the pack — not an error, and not a
391
+ // reason to send an unsigned header.
392
+ if (body instanceof Uint8Array) throw new Error('remote execute: this signature canonicalizer accepts text bodies only');
393
+ const payload = auth.canonical({ method, path, ...(body === undefined ? {} : { body }) });
394
+ if (payload !== null) outbound.set(auth.header, await hmacSha256(secret, payload, auth.encoding));
326
395
  }
327
- // the HOST goes with it: an AWS endpoint names its own region, and only the pack knows how
328
- const { region, service } = typeof auth.scope === 'function' ? auth.scope({ method, path, host: target.host, headers: Object.fromEntries(outbound.entries()), ...(body === undefined ? {} : { body }) }) : auth.scope;
329
- const signed = await awsSigV4Headers({ method, url: target, ...(body === undefined ? {} : { body }), keyId, secret, region, service, headers: outbound, at: new Date() });
330
- for (const [name, value] of Object.entries(signed)) outbound.set(name, value);
331
- } else {
332
- // `null` is the vendor's own exemption, declared by the pack — not an error, and not a
333
- // reason to send an unsigned header.
334
- if (body instanceof Uint8Array) throw new Error('remote execute: this signature canonicalizer accepts text bodies only');
335
- const payload = auth.canonical({ method, path, ...(body === undefined ? {} : { body }) });
336
- if (payload !== null) outbound.set(auth.header, await hmacSha256(secret, payload, auth.encoding));
337
396
  }
338
397
  }
339
398