@volter/world-core 2.0.36 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +4 -5
  2. package/app-route.cjs +12 -6
  3. package/app-route.d.cts +1 -1
  4. package/dist/app-route.cjs +12 -6
  5. package/dist/app-route.d.cts +1 -1
  6. package/dist/generated/pack-facts.json +1410 -3069
  7. package/dist/inject.cjs +64 -9
  8. package/dist/pack-facts.cjs +44 -0
  9. package/dist/src/actions.d.ts +3 -3
  10. package/dist/src/actions.js +22 -16
  11. package/dist/src/ancestry.d.ts +14 -2
  12. package/dist/src/ancestry.js +92 -2
  13. package/dist/src/anthropic-wire.d.ts +39 -0
  14. package/dist/src/anthropic-wire.js +136 -0
  15. package/dist/src/bytes.d.ts +7 -0
  16. package/dist/src/bytes.js +35 -0
  17. package/dist/src/changeset.d.ts +1 -1
  18. package/dist/src/changeset.js +0 -0
  19. package/dist/src/clickhouse/index.d.ts +3 -0
  20. package/dist/src/clickhouse/index.js +6 -0
  21. package/dist/src/clickhouse/sql.d.ts +233 -0
  22. package/dist/src/clickhouse/sql.js +4329 -0
  23. package/dist/src/clickhouse/types.d.ts +18 -0
  24. package/dist/src/clickhouse/types.js +47 -0
  25. package/dist/src/clickhouse/values.d.ts +146 -0
  26. package/dist/src/clickhouse/values.js +858 -0
  27. package/dist/src/client-bundle.js +2 -3
  28. package/dist/src/cors.d.ts +15 -0
  29. package/dist/src/cors.js +31 -0
  30. package/dist/src/derived-core.d.ts +487 -24
  31. package/dist/src/derived-core.js +788 -144
  32. package/dist/src/derived-real.d.ts +13 -0
  33. package/dist/src/derived-real.js +518 -0
  34. package/dist/src/derived.d.ts +35 -1
  35. package/dist/src/derived.js +61 -9
  36. package/dist/src/emit.js +1 -2
  37. package/dist/src/events.d.ts +206 -0
  38. package/dist/src/events.js +341 -0
  39. package/dist/src/executor.d.ts +3 -0
  40. package/dist/src/executor.js +19 -2
  41. package/dist/src/file-response.d.ts +6 -0
  42. package/dist/src/file-response.js +30 -0
  43. package/dist/src/fork.js +3 -2
  44. package/dist/src/git/history.d.ts +7 -0
  45. package/dist/src/git/history.js +24 -0
  46. package/dist/src/git/index.d.ts +1 -0
  47. package/dist/src/git/index.js +1 -0
  48. package/dist/src/git/lfs.d.ts +28 -0
  49. package/dist/src/git/lfs.js +66 -0
  50. package/dist/src/git/objects.js +3 -8
  51. package/dist/src/git/smart-http.d.ts +3 -1
  52. package/dist/src/git/smart-http.js +67 -6
  53. package/dist/src/graphql-wire.d.ts +29 -0
  54. package/dist/src/graphql-wire.js +101 -0
  55. package/dist/src/grpc-wire.d.ts +67 -0
  56. package/dist/src/grpc-wire.js +170 -0
  57. package/dist/src/h2.d.ts +40 -0
  58. package/dist/src/h2.js +656 -0
  59. package/dist/src/head.d.ts +32 -3
  60. package/dist/src/head.js +161 -40
  61. package/dist/src/history.d.ts +1 -1
  62. package/dist/src/history.js +6 -6
  63. package/dist/src/hpack.json +1 -0
  64. package/dist/src/index.d.ts +64 -75
  65. package/dist/src/index.js +58 -101
  66. package/dist/src/log.js +28 -19
  67. package/dist/src/machines.d.ts +50 -0
  68. package/dist/src/machines.js +151 -0
  69. package/dist/src/managed-database.d.ts +86 -0
  70. package/dist/src/managed-database.js +283 -0
  71. package/dist/src/multipart.d.ts +11 -0
  72. package/dist/src/multipart.js +51 -0
  73. package/dist/src/observe.d.ts +15 -5
  74. package/dist/src/observe.js +23 -9
  75. package/dist/src/openai-wire.d.ts +108 -0
  76. package/dist/src/openai-wire.js +337 -0
  77. package/dist/src/pack-assets.d.ts +3 -4
  78. package/dist/src/pack-assets.js +15 -10
  79. package/dist/src/pack-fetch.d.ts +77 -0
  80. package/dist/src/pack-fetch.js +449 -0
  81. package/dist/src/pack-paths.d.ts +12 -0
  82. package/dist/src/pack-paths.js +86 -0
  83. package/dist/src/packRegistry.d.ts +69 -162
  84. package/dist/src/packRegistry.js +55 -20
  85. package/dist/src/people.d.ts +13 -0
  86. package/dist/src/people.js +18 -0
  87. package/dist/src/placeholder-image.d.ts +5 -0
  88. package/dist/src/placeholder-image.js +114 -0
  89. package/dist/src/protobuf.d.ts +28 -0
  90. package/dist/src/protobuf.js +332 -0
  91. package/dist/src/redis/engine.js +1 -1
  92. package/dist/src/request-scope.d.ts +1 -1
  93. package/dist/src/request-scope.js +6 -4
  94. package/dist/src/resource-blob.d.ts +5 -0
  95. package/dist/src/resource-blob.js +11 -0
  96. package/dist/src/runtime.d.ts +85 -0
  97. package/dist/src/runtime.js +104 -0
  98. package/dist/src/s3/wire.d.ts +60 -0
  99. package/dist/src/s3/wire.js +157 -0
  100. package/dist/src/scenario.d.ts +3 -0
  101. package/dist/src/scenario.js +2 -0
  102. package/dist/src/schema-sample.d.ts +1 -0
  103. package/dist/src/schema-sample.js +21 -0
  104. package/dist/src/sealed-box.d.ts +14 -0
  105. package/dist/src/sealed-box.js +225 -0
  106. package/dist/src/serve-http.d.ts +14 -0
  107. package/dist/src/serve-http.js +27 -3
  108. package/dist/src/serve.d.ts +6 -0
  109. package/dist/src/serve.js +69 -14
  110. package/dist/src/signing.d.ts +135 -0
  111. package/dist/src/signing.js +222 -0
  112. package/dist/src/sigv4.d.ts +48 -0
  113. package/dist/src/sigv4.js +167 -0
  114. package/dist/src/smtp.d.ts +16 -0
  115. package/dist/src/smtp.js +72 -0
  116. package/dist/src/sockets.d.ts +51 -0
  117. package/dist/src/sockets.js +90 -0
  118. package/dist/src/state-system.d.ts +1 -0
  119. package/dist/src/state-system.js +1 -1
  120. package/dist/src/storage.d.ts +1 -1
  121. package/dist/src/storage.js +3 -3
  122. package/dist/src/trace-context.js +1 -1
  123. package/dist/src/twin-fetch.d.ts +0 -7
  124. package/dist/src/twin-fetch.js +0 -14
  125. package/dist/src/vendor-call.d.ts +6 -0
  126. package/dist/src/vendor-call.js +41 -0
  127. package/dist/src/world-store.js +1 -1
  128. package/dist/vendor-hosts.cjs +36 -125
  129. package/dist/vendor-hosts.d.cts +8 -0
  130. package/generated/pack-facts.json +1410 -3069
  131. package/inject.cjs +64 -9
  132. package/pack-facts.cjs +44 -0
  133. package/package.json +17 -3
  134. package/src/actions.ts +23 -16
  135. package/src/ancestry.ts +74 -2
  136. package/src/anthropic-wire.ts +137 -0
  137. package/src/bytes.ts +42 -0
  138. package/src/changeset.ts +5 -5
  139. package/src/clickhouse/index.ts +6 -0
  140. package/src/clickhouse/sql.ts +3059 -0
  141. package/src/clickhouse/types.ts +44 -0
  142. package/src/clickhouse/values.ts +697 -0
  143. package/src/client-bundle.ts +2 -3
  144. package/src/cors.ts +34 -0
  145. package/src/derived-core.ts +1013 -146
  146. package/src/derived-real.ts +434 -0
  147. package/src/derived.ts +73 -3
  148. package/src/emit.ts +1 -2
  149. package/src/events.ts +449 -0
  150. package/src/executor.ts +24 -2
  151. package/src/file-response.ts +27 -0
  152. package/src/fork.ts +3 -2
  153. package/src/git/history.ts +19 -0
  154. package/src/git/index.ts +1 -0
  155. package/src/git/lfs.ts +67 -0
  156. package/src/git/objects.ts +3 -5
  157. package/src/git/smart-http.ts +56 -6
  158. package/src/graphql-wire.ts +106 -0
  159. package/src/grpc-wire.ts +159 -0
  160. package/src/h2.ts +627 -0
  161. package/src/head.ts +132 -41
  162. package/src/history.ts +6 -6
  163. package/src/hpack.json +1 -0
  164. package/src/index.ts +82 -329
  165. package/src/log.ts +27 -18
  166. package/src/machines.ts +151 -0
  167. package/src/managed-database.ts +299 -0
  168. package/src/multipart.ts +51 -0
  169. package/src/observe.ts +31 -15
  170. package/src/openai-wire.ts +371 -0
  171. package/src/pack-assets.ts +15 -11
  172. package/src/pack-fetch.ts +458 -0
  173. package/src/pack-paths.ts +72 -0
  174. package/src/packRegistry.ts +79 -167
  175. package/src/people.ts +31 -0
  176. package/src/placeholder-image.ts +88 -0
  177. package/src/protobuf.ts +251 -0
  178. package/src/redis/engine.ts +1 -1
  179. package/src/request-scope.ts +8 -4
  180. package/src/resource-blob.ts +13 -0
  181. package/src/runtime.ts +344 -0
  182. package/src/s3/wire.ts +172 -0
  183. package/src/scenario.ts +4 -0
  184. package/src/schema-sample.ts +24 -0
  185. package/src/sealed-box.ts +182 -0
  186. package/src/serve-http.ts +31 -3
  187. package/src/serve.ts +58 -14
  188. package/src/signing.ts +231 -0
  189. package/src/sigv4.ts +158 -0
  190. package/src/smtp.ts +76 -0
  191. package/src/sockets.ts +140 -0
  192. package/src/state-system.ts +2 -2
  193. package/src/storage.ts +3 -3
  194. package/src/trace-context.ts +1 -1
  195. package/src/twin-fetch.ts +0 -20
  196. package/src/vendor-call.ts +41 -0
  197. package/src/world-store.ts +1 -1
  198. package/vendor-hosts.cjs +36 -125
  199. package/vendor-hosts.d.cts +8 -0
  200. package/dist/src/mirror-shell.d.ts +0 -2
  201. package/dist/src/mirror-shell.js +0 -13
  202. package/dist/src/v1-removed.d.ts +0 -159
  203. package/dist/src/v1-removed.js +0 -124
  204. package/src/mirror-shell.ts +0 -15
  205. package/src/v1-removed.ts +0 -172
@@ -0,0 +1,48 @@
1
+ export type SigV4Verdict = {
2
+ ok: true;
3
+ keyId: string;
4
+ region: string;
5
+ service: string;
6
+ } | {
7
+ ok: false;
8
+ reason: 'unsigned' | 'malformed' | 'unknown_key' | 'bad_signature' | 'expired';
9
+ keyId?: string;
10
+ };
11
+ /** The path a request arrived with, where the kernel routed it by another (a virtual-hosted bucket put in front of the
12
+ * path: pack-fetch's `routed`); a client signed the path it sent. */
13
+ export declare const ORIGINAL_PATH_HEADER = "x-volter-original-path";
14
+ /**
15
+ * Whether `request` is signed with SigV4 by a key `secretOf` knows (its secret, or undefined for a key the twin never
16
+ * issued). `body` is the bytes it carries (the payload hash of
17
+ * a service that sends no `x-amz-content-sha256`); `now` is the World's instant, in epoch milliseconds, for a presigned
18
+ * URL's expiry. The caller decides what an unsigned request is (a public read, or a refusal).
19
+ */
20
+ export declare function verifySigV4(request: Request, body: Uint8Array | string | undefined, secretOf: (keyId: string) => string | undefined | Promise<string | undefined>, now: number): Promise<SigV4Verdict>;
21
+ /**
22
+ * The SigV4 headers a client adds (`authorization`, `x-amz-date`, and for S3 `x-amz-content-sha256`), signed over the
23
+ * request as it will be sent: its method, URL, the headers it carries (host, `x-amz-*` and the content type are signed)
24
+ * and its body, at `at`. What a walk sends as an AWS SDK sends it; the verifier above is its other side.
25
+ */
26
+ export declare function signSigV4(input: {
27
+ method: string;
28
+ url: string;
29
+ headers?: Record<string, string>;
30
+ body?: string | Uint8Array;
31
+ keyId: string;
32
+ secret: string;
33
+ region: string;
34
+ service: string;
35
+ at: Date;
36
+ }): Record<string, string>;
37
+ /**
38
+ * A presigned URL signed again with a key: its date, expiry, region, service and signed headers are the URL's own
39
+ * (X-Amz-Date, X-Amz-Expires, X-Amz-Credential's scope, X-Amz-SignedHeaders, `host` when it names none), its key and
40
+ * signature this key's. What an SDK's getSignedUrl makes, for a walk that wrote the URL as its client printed it.
41
+ */
42
+ export declare function presignSigV4(input: {
43
+ method: string;
44
+ url: string;
45
+ keyId: string;
46
+ secret: string;
47
+ host?: string;
48
+ }): string;
@@ -0,0 +1,167 @@
1
+ // AWS Signature Version 4, VERIFIED: what an AWS-shaped twin (S3, Secrets Manager, R2's S3 endpoint) checks before it
2
+ // answers a signed request. The published algorithm (https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html,
3
+ // "Create a signed AWS API request"), the same one executor.ts signs with, run from the other side: the request names
4
+ // its access key, its credential scope and the headers it signed; the twin finds the key's secret, builds the canonical
5
+ // request from what arrived, and compares signatures. A presigned URL carries the same parts in its query
6
+ // (X-Amz-Credential, X-Amz-SignedHeaders, X-Amz-Signature; its payload `UNSIGNED-PAYLOAD`).
7
+ //
8
+ // Where the request reached the twin rather than the vendor, the host it signed is the vendor's: the World's injector
9
+ // names it (`x-volter-twin-original-host`), else the Host the request carries, else its URL's. S3 signs its path as
10
+ // sent and every other service encodes it a second time (the specification's own clause, as in executor.ts). A
11
+ // streaming upload (`STREAMING-AWS4-HMAC-SHA256-PAYLOAD`) is checked by its seed signature; its chunk signatures are
12
+ // not (the twin's reading: the seed binds the request, and the chunks are the body the twin keeps).
13
+ import * as nodeCrypto from 'node:crypto';
14
+ /** The path a request arrived with, where the kernel routed it by another (a virtual-hosted bucket put in front of the
15
+ * path: pack-fetch's `routed`); a client signed the path it sent. */
16
+ export const ORIGINAL_PATH_HEADER = 'x-volter-original-path';
17
+ const rfc3986 = (v) => encodeURIComponent(v).replace(/[!\x27()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
18
+ const sha256Hex = (data) => nodeCrypto.createHash('sha256').update(data).digest('hex');
19
+ const hmac = (key, data) => nodeCrypto.createHmac('sha256', key).update(data).digest();
20
+ /** The time a SigV4 date names (`20260105T093000Z`), in epoch milliseconds. */
21
+ const dateOf = (amz) => Date.parse(`${amz.slice(0, 4)}-${amz.slice(4, 6)}-${amz.slice(6, 8)}T${amz.slice(9, 11)}:${amz.slice(11, 13)}:${amz.slice(13, 15)}Z`);
22
+ /**
23
+ * Whether `request` is signed with SigV4 by a key `secretOf` knows (its secret, or undefined for a key the twin never
24
+ * issued). `body` is the bytes it carries (the payload hash of
25
+ * a service that sends no `x-amz-content-sha256`); `now` is the World's instant, in epoch milliseconds, for a presigned
26
+ * URL's expiry. The caller decides what an unsigned request is (a public read, or a refusal).
27
+ */
28
+ export async function verifySigV4(request, body, secretOf, now) {
29
+ const url = new URL(request.url);
30
+ const header = request.headers.get('authorization') ?? '';
31
+ const presigned = url.searchParams.get('X-Amz-Algorithm') === 'AWS4-HMAC-SHA256';
32
+ let credential;
33
+ let signedHeaders;
34
+ let signature;
35
+ let amzDate;
36
+ let payloadHash;
37
+ if (/^AWS4-HMAC-SHA256\s/i.test(header)) {
38
+ const parts = Object.fromEntries(header.replace(/^AWS4-HMAC-SHA256\s+/i, '').split(/,\s*/).map((p) => { const at = p.indexOf('='); return [p.slice(0, at).trim(), p.slice(at + 1).trim()]; }));
39
+ if (!parts.Credential || !parts.SignedHeaders || !parts.Signature)
40
+ return { ok: false, reason: 'malformed' };
41
+ credential = parts.Credential;
42
+ signedHeaders = parts.SignedHeaders;
43
+ signature = parts.Signature;
44
+ amzDate = request.headers.get('x-amz-date') ?? '';
45
+ const declared = request.headers.get('x-amz-content-sha256');
46
+ payloadHash = declared ?? sha256Hex(body ?? '');
47
+ }
48
+ else if (presigned) {
49
+ credential = url.searchParams.get('X-Amz-Credential') ?? '';
50
+ signedHeaders = url.searchParams.get('X-Amz-SignedHeaders') ?? '';
51
+ signature = url.searchParams.get('X-Amz-Signature') ?? '';
52
+ amzDate = url.searchParams.get('X-Amz-Date') ?? '';
53
+ payloadHash = 'UNSIGNED-PAYLOAD';
54
+ const expires = Number(url.searchParams.get('X-Amz-Expires') ?? '');
55
+ if (!Number.isFinite(expires) || !/^\d{8}T\d{6}Z$/.test(amzDate))
56
+ return { ok: false, reason: 'malformed' };
57
+ if (now > dateOf(amzDate) + expires * 1000)
58
+ return { ok: false, reason: 'expired' };
59
+ }
60
+ else {
61
+ return { ok: false, reason: 'unsigned' };
62
+ }
63
+ const [keyId = '', dateStamp = '', region = '', service = '', terminal = ''] = credential.split('/');
64
+ if (!keyId || terminal !== 'aws4_request' || !/^\d{8}$/.test(dateStamp) || !/^\d{8}T\d{6}Z$/.test(amzDate))
65
+ return { ok: false, reason: 'malformed', ...(keyId ? { keyId } : {}) };
66
+ const secret = await secretOf(keyId);
67
+ if (secret === undefined)
68
+ return { ok: false, reason: 'unknown_key', keyId };
69
+ // A canonical request binds the declared hash; the declaration must also describe the bytes received.
70
+ // S3's UNSIGNED-PAYLOAD and streaming markers are separate wire modes, not ordinary payload digests.
71
+ // https://docs.aws.amazon.com/AmazonS3/latest/developerguide/sig-v4-header-based-auth.html
72
+ const declaredPayload = request.headers.get('x-amz-content-sha256');
73
+ if (declaredPayload !== null && /^[0-9a-f]{64}$/i.test(declaredPayload)
74
+ && declaredPayload.toLowerCase() !== sha256Hex(body ?? '')) {
75
+ return { ok: false, reason: 'bad_signature', keyId };
76
+ }
77
+ const host = request.headers.get('x-volter-twin-original-host') ?? request.headers.get('host') ?? url.host;
78
+ const sentPath = request.headers.get(ORIGINAL_PATH_HEADER) ?? url.pathname;
79
+ const raw = sentPath === '' ? '/' : sentPath;
80
+ const canonicalUri = service === 's3' ? raw : raw.split('/').map((segment) => rfc3986(decodeURIComponent(segment))).map(rfc3986).join('/');
81
+ const canonicalQuery = [...url.searchParams.entries()]
82
+ .filter(([k]) => !(presigned && k === 'X-Amz-Signature'))
83
+ .map(([k, v]) => [rfc3986(k), rfc3986(v)])
84
+ .sort((left, right) => (left[0] === right[0] ? (left[1] < right[1] ? -1 : 1) : left[0] < right[0] ? -1 : 1))
85
+ .map(([k, v]) => `${k}=${v}`)
86
+ .join('&');
87
+ const names = signedHeaders.toLowerCase().split(';').filter(Boolean);
88
+ const value = (name) => (name === 'host' ? host : (request.headers.get(name) ?? '')).trim().replace(/\s+/g, ' ');
89
+ const canonicalHeaders = names.map((n) => `${n}:${value(n)}\n`).join('');
90
+ const canonicalRequest = [request.method.toUpperCase(), canonicalUri, canonicalQuery, canonicalHeaders, names.join(';'), payloadHash].join('\n');
91
+ const scope = `${dateStamp}/${region}/${service}/aws4_request`;
92
+ const stringToSign = ['AWS4-HMAC-SHA256', amzDate, scope, sha256Hex(canonicalRequest)].join('\n');
93
+ let key = hmac(`AWS4${secret}`, dateStamp);
94
+ for (const part of [region, service, 'aws4_request'])
95
+ key = hmac(key, part);
96
+ const expected = hmac(key, stringToSign).toString('hex');
97
+ const given = Buffer.from(signature.toLowerCase(), 'utf8');
98
+ const want = Buffer.from(expected, 'utf8');
99
+ if (given.length !== want.length || !nodeCrypto.timingSafeEqual(given, want))
100
+ return { ok: false, reason: 'bad_signature', keyId };
101
+ return { ok: true, keyId, region, service };
102
+ }
103
+ /**
104
+ * The SigV4 headers a client adds (`authorization`, `x-amz-date`, and for S3 `x-amz-content-sha256`), signed over the
105
+ * request as it will be sent: its method, URL, the headers it carries (host, `x-amz-*` and the content type are signed)
106
+ * and its body, at `at`. What a walk sends as an AWS SDK sends it; the verifier above is its other side.
107
+ */
108
+ export function signSigV4(input) {
109
+ const url = new URL(input.url);
110
+ const amzDate = `${input.at.toISOString().slice(0, 19).replace(/[:-]/g, '')}Z`;
111
+ const dateStamp = amzDate.slice(0, 8);
112
+ const payloadHash = sha256Hex(input.body ?? '');
113
+ const raw = url.pathname === '' ? '/' : url.pathname;
114
+ const canonicalUri = input.service === 's3' ? raw : raw.split('/').map((segment) => rfc3986(decodeURIComponent(segment))).map(rfc3986).join('/');
115
+ const canonicalQuery = [...url.searchParams.entries()]
116
+ .map(([k, v]) => [rfc3986(k), rfc3986(v)])
117
+ .sort((left, right) => (left[0] === right[0] ? (left[1] < right[1] ? -1 : 1) : left[0] < right[0] ? -1 : 1))
118
+ .map(([k, v]) => `${k}=${v}`)
119
+ .join('&');
120
+ const added = { 'x-amz-date': amzDate, ...(input.service === 's3' ? { 'x-amz-content-sha256': payloadHash } : {}) };
121
+ const signable = new Map([['host', url.host]]);
122
+ for (const [name, value] of Object.entries({ ...(input.headers ?? {}), ...added })) {
123
+ const n = name.toLowerCase();
124
+ if (n.startsWith('x-amz-') || n === 'content-type')
125
+ signable.set(n, value.trim().replace(/\s+/g, ' '));
126
+ }
127
+ const names = [...signable.keys()].sort();
128
+ const canonicalRequest = [input.method.toUpperCase(), canonicalUri, canonicalQuery, names.map((n) => `${n}:${signable.get(n)}\n`).join(''), names.join(';'), payloadHash].join('\n');
129
+ const scope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
130
+ const stringToSign = ['AWS4-HMAC-SHA256', amzDate, scope, sha256Hex(canonicalRequest)].join('\n');
131
+ let key = hmac(`AWS4${input.secret}`, dateStamp);
132
+ for (const part of [input.region, input.service, 'aws4_request'])
133
+ key = hmac(key, part);
134
+ return { ...added, authorization: `AWS4-HMAC-SHA256 Credential=${input.keyId}/${scope}, SignedHeaders=${names.join(';')}, Signature=${hmac(key, stringToSign).toString('hex')}` };
135
+ }
136
+ /**
137
+ * A presigned URL signed again with a key: its date, expiry, region, service and signed headers are the URL's own
138
+ * (X-Amz-Date, X-Amz-Expires, X-Amz-Credential's scope, X-Amz-SignedHeaders, `host` when it names none), its key and
139
+ * signature this key's. What an SDK's getSignedUrl makes, for a walk that wrote the URL as its client printed it.
140
+ */
141
+ export function presignSigV4(input) {
142
+ const url = new URL(input.url);
143
+ const q = url.searchParams;
144
+ const [, dateStamp = '', region = '', service = ''] = (q.get('X-Amz-Credential') ?? '').split('/');
145
+ const amzDate = q.get('X-Amz-Date') ?? '';
146
+ q.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256');
147
+ q.set('X-Amz-Credential', `${input.keyId}/${dateStamp}/${region}/${service}/aws4_request`);
148
+ if (!q.has('X-Amz-SignedHeaders'))
149
+ q.set('X-Amz-SignedHeaders', 'host');
150
+ q.delete('X-Amz-Signature');
151
+ const raw = url.pathname === '' ? '/' : url.pathname;
152
+ const canonicalUri = service === 's3' ? raw : raw.split('/').map((segment) => rfc3986(decodeURIComponent(segment))).map(rfc3986).join('/');
153
+ const canonicalQuery = [...q.entries()]
154
+ .map(([k, v]) => [rfc3986(k), rfc3986(v)])
155
+ .sort((left, right) => (left[0] === right[0] ? (left[1] < right[1] ? -1 : 1) : left[0] < right[0] ? -1 : 1))
156
+ .map(([k, v]) => `${k}=${v}`)
157
+ .join('&');
158
+ const names = (q.get('X-Amz-SignedHeaders') ?? 'host').toLowerCase().split(';').filter(Boolean);
159
+ const canonicalHeaders = names.map((n) => `${n}:${n === 'host' ? (input.host ?? url.host) : ''}\n`).join('');
160
+ const canonicalRequest = [input.method.toUpperCase(), canonicalUri, canonicalQuery, canonicalHeaders, names.join(';'), 'UNSIGNED-PAYLOAD'].join('\n');
161
+ const stringToSign = ['AWS4-HMAC-SHA256', amzDate, `${dateStamp}/${region}/${service}/aws4_request`, sha256Hex(canonicalRequest)].join('\n');
162
+ let key = hmac(`AWS4${input.secret}`, dateStamp);
163
+ for (const part of [region, service, 'aws4_request'])
164
+ key = hmac(key, part);
165
+ q.set('X-Amz-Signature', hmac(key, stringToSign).toString('hex'));
166
+ return url.toString();
167
+ }
@@ -0,0 +1,16 @@
1
+ /** Where a mail goes: the SMTP host and port, and the sender. */
2
+ export type SmtpRoute = {
3
+ host: string;
4
+ port: number;
5
+ from: string;
6
+ senderName?: string;
7
+ };
8
+ /** One mail: its recipient, subject and HTML body. */
9
+ export type Mail = {
10
+ to: string;
11
+ subject: string;
12
+ html: string;
13
+ };
14
+ /** Send one mail. Throws when it cannot be delivered (refused by the World's egress rule, unreachable, or refused by the
15
+ * server), so the vendor answers its own mailer failure. */
16
+ export declare function sendMail(route: SmtpRoute, mail: Mail, headers?: Record<string, string>): Promise<void>;
@@ -0,0 +1,72 @@
1
+ // A vendor's mail to a person, sent over SMTP (plain, no authentication) to the host the vendor's settings name — in a
2
+ // World, the World's mail twin — as the vendor's own mailer sends it (GoTrue's confirmation and magic links). A handler
3
+ // reaches it as `ctx.mail`. The World's egress rule is applied before the socket opens: a sealed World or one with a
4
+ // network policy refuses a host outside it; a local World with neither lets a configured outside host be reached, as it
5
+ // lets the application reach one. The socket is opened only when a mail is sent (nothing at import).
6
+ class SmtpSession {
7
+ socket;
8
+ buffer = '';
9
+ waiting = [];
10
+ constructor(socket) {
11
+ this.socket = socket;
12
+ socket.setEncoding('utf8');
13
+ socket.on('data', (chunk) => {
14
+ this.buffer += chunk;
15
+ let end;
16
+ // a reply is complete at a line whose code is followed by a space (multi-line replies use a dash)
17
+ while ((end = this.buffer.search(/^\d{3} .*\r?\n/m)) >= 0) {
18
+ const lineEnd = this.buffer.indexOf('\n', end);
19
+ const reply = this.buffer.slice(0, lineEnd + 1);
20
+ this.buffer = this.buffer.slice(lineEnd + 1);
21
+ this.waiting.shift()?.(reply.trim().split(/\r?\n/).pop());
22
+ }
23
+ });
24
+ }
25
+ reply() { return new Promise((resolve) => this.waiting.push(resolve)); }
26
+ async command(line, expect) {
27
+ const next = this.reply();
28
+ this.socket.write(`${line}\r\n`);
29
+ const reply = await next;
30
+ if (!reply.startsWith(String(expect)))
31
+ throw new Error(`SMTP ${line.split(' ')[0]}: ${reply}`);
32
+ }
33
+ }
34
+ /** How long a mail may take, connect to QUIT (a host that never answers must not hold the request open). */
35
+ const SMTP_TIMEOUT_MS = 10_000;
36
+ /** Send one mail. Throws when it cannot be delivered (refused by the World's egress rule, unreachable, or refused by the
37
+ * server), so the vendor answers its own mailer failure. */
38
+ export async function sendMail(route, mail, headers = {}) {
39
+ const [{ connect }, { worldEgressRefusal }] = await Promise.all([
40
+ import('node:net'),
41
+ import('../network-policy.cjs'),
42
+ ]);
43
+ const refusal = worldEgressRefusal(`http://${route.host}:${route.port}/`);
44
+ if (refusal !== null)
45
+ throw new Error(refusal);
46
+ const socket = connect(route.port, route.host);
47
+ socket.setTimeout(SMTP_TIMEOUT_MS, () => socket.destroy(new Error(`SMTP ${route.host}:${route.port}: no answer in ${SMTP_TIMEOUT_MS} ms`)));
48
+ const session = new SmtpSession(socket);
49
+ const greeting = session.reply();
50
+ const failed = new Promise((_, reject) => { socket.once('error', reject); socket.once('close', () => reject(new Error('SMTP connection closed'))); });
51
+ failed.catch(() => { });
52
+ const step = (p) => Promise.race([p, failed]);
53
+ await step(new Promise((resolve) => { socket.once('connect', () => resolve()); }));
54
+ try {
55
+ if (!(await step(greeting)).startsWith('220'))
56
+ throw new Error('SMTP: no greeting');
57
+ await step(session.command(`EHLO ${route.from.split('@')[1] ?? 'localhost'}`, 250));
58
+ await step(session.command(`MAIL FROM:<${route.from}>`, 250));
59
+ await step(session.command(`RCPT TO:<${mail.to}>`, 250));
60
+ await step(session.command('DATA', 354));
61
+ const from = route.senderName ? `"${route.senderName.replace(/"/g, '')}" <${route.from}>` : route.from;
62
+ const lines = [
63
+ `From: ${from}`, `To: ${mail.to}`, `Subject: ${mail.subject}`, 'MIME-Version: 1.0', 'Content-Type: text/html; charset=utf-8',
64
+ ...Object.entries(headers).map(([k, v]) => `${k}: ${v}`), '', ...mail.html.split(/\r?\n/).map((l) => (l.startsWith('.') ? `.${l}` : l)),
65
+ ];
66
+ await step(session.command(`${lines.join('\r\n')}\r\n.`, 250));
67
+ await step(session.command('QUIT', 221)).catch(() => { });
68
+ }
69
+ finally {
70
+ socket.destroy();
71
+ }
72
+ }
@@ -0,0 +1,51 @@
1
+ import type { EventWrite } from './events.js';
2
+ import type { ScenarioEngine } from './scenario.js';
3
+ import type { WebSocketUpgrade } from './serve-http.js';
4
+ /** A socket the vendor serves: its path (and host, when the vendor serves it on a host of its own), as the manifest
5
+ * declares it. */
6
+ /** A vendor's socket: its id, path and host; `protocols` are the subprotocols its server selects from those a client
7
+ * offers in `Sec-WebSocket-Protocol` (OpenAI Realtime's `realtime`), the first offered that it lists. */
8
+ export type SocketDecl = {
9
+ id: string;
10
+ path: string;
11
+ host?: string;
12
+ note: string;
13
+ source?: string;
14
+ protocols?: ReadonlyArray<string>;
15
+ };
16
+ /** One open connection, as its engine sees it: its own state, and the frames it sends (an object is sent as JSON). */
17
+ export type SocketSession = {
18
+ readonly id: number;
19
+ readonly socket: string;
20
+ state: Record<string, unknown>;
21
+ send(frame: unknown): void;
22
+ close(code?: number, reason?: string): void;
23
+ /** A reply this session will send, by its correlation key: settles when the engine calls `fulfil(key, value)`, and
24
+ * rejects when `ms` pass or the session closes (a request answered by a session: the tunnel's relay). */
25
+ expect(key: string, ms: number): Promise<unknown>;
26
+ /** Settles what `expect(key)` awaits; false when nothing awaits it (a late or unknown reply). */
27
+ fulfil(key: string, value: unknown): boolean;
28
+ /** The World's scenario's decision for a turn this session asks (a model vendor's turn spoken over a socket: a voice
29
+ * agent's reply), the turn given as the pack's scenario adapter reads a request; undefined when the pack has no
30
+ * scenario or the World carries none. A fault decision is the engine's to answer in its own frames. */
31
+ decide(asked: unknown): Promise<ScenarioServed | undefined>;
32
+ };
33
+ /** What the World's scenario serves a turn: a handler's decision, a miss, or a fault (scenario.ts `serve`). */
34
+ export type ScenarioServed = Awaited<ReturnType<ScenarioEngine<unknown>['serve']>>;
35
+ /** A socket's protocol, over the handler contract's context (a read of the World as the request that opened it). */
36
+ export type SocketEngine<C> = {
37
+ open?(session: SocketSession, ctx: C): void | Promise<void>;
38
+ message(session: SocketSession, data: string, ctx: C): void | Promise<void>;
39
+ /** A write of the World: the frames it sends this session, if any (the engine decides who is told what). */
40
+ write?(session: SocketSession, write: EventWrite, ctx: C): void | Promise<void>;
41
+ /** The session ended (the client hung up, or the engine closed it): the World is read and written as at a frame, so
42
+ * what the end settles is written (a call's final status). */
43
+ close?(session: SocketSession, ctx: C): void | Promise<void>;
44
+ };
45
+ /** The open sessions of one socket of the vendor in a World (a handler reaches them as `ctx.sockets(id)`). */
46
+ export declare function openSessions(service: string, root: string | undefined, socket: string): SocketSession[];
47
+ /** Offer a write to every open session of the vendor, each in its turn after what it is already doing. Queued, never
48
+ * awaited: the write may be one a session's own frame made, which that session finishes before it is told. */
49
+ export declare function socketWrite(service: string, root: string | undefined, write: EventWrite): void;
50
+ /** The upgrade the serve seam takes: a request to a declared socket's path (and host) opens a session of its engine. */
51
+ export declare function socketUpgrade<C>(service: string, root: string | undefined, decls: ReadonlyArray<SocketDecl>, engines: Record<string, SocketEngine<C>>, context: (request: Request, decl: SocketDecl) => Promise<C>, hostOf: (request: Request) => string, pathOf?: (request: Request) => string, decide?: (asked: unknown) => Promise<ScenarioServed | undefined>): WebSocketUpgrade;
@@ -0,0 +1,90 @@
1
+ const sessions = new Map();
2
+ let nextSession = 1;
3
+ const keyOf = (service, root) => `${service}\0${root ?? ''}`;
4
+ /** The open sessions of one socket of the vendor in a World (a handler reaches them as `ctx.sockets(id)`). */
5
+ export function openSessions(service, root, socket) {
6
+ return [...(sessions.get(keyOf(service, root)) ?? [])].filter((o) => o.session.socket === socket).map((o) => o.session);
7
+ }
8
+ /** Offer a write to every open session of the vendor, each in its turn after what it is already doing. Queued, never
9
+ * awaited: the write may be one a session's own frame made, which that session finishes before it is told. */
10
+ export function socketWrite(service, root, write) {
11
+ const open = sessions.get(keyOf(service, root));
12
+ if (!open?.size)
13
+ return;
14
+ for (const o of open) {
15
+ const { engine } = o;
16
+ if (!engine.write)
17
+ continue;
18
+ o.queue = o.queue.then(async () => engine.write(o.session, write, await o.context())).catch((error) => o.session.close(1011, String(error.message ?? error).slice(0, 120)));
19
+ }
20
+ }
21
+ /** The upgrade the serve seam takes: a request to a declared socket's path (and host) opens a session of its engine. */
22
+ export function socketUpgrade(service, root, decls, engines, context, hostOf, pathOf = (request) => new URL(request.url).pathname, decide = async () => undefined) {
23
+ const declFor = (request) => decls.find((d) => {
24
+ const path = pathOf(request).replace(/\/+$/, '') || '/';
25
+ return (d.path.replace(/\/+$/, '') || '/') === path && (!d.host || d.host.toLowerCase() === hostOf(request)) && engines[d.id] !== undefined;
26
+ });
27
+ const byPeer = new WeakMap();
28
+ const key = keyOf(service, root);
29
+ return {
30
+ accepts: (request) => declFor(request) !== undefined,
31
+ // the subprotocol the server answers with: the first the client offers that the socket declares, or, for a socket
32
+ // that declares none, the first offered (what the ws library and Bun answer by themselves)
33
+ protocol: (request) => {
34
+ const offered = (request.headers.get('sec-websocket-protocol') ?? '').split(',').map((p) => p.trim()).filter(Boolean);
35
+ const declared = declFor(request)?.protocols;
36
+ return declared ? offered.find((p) => declared.includes(p)) : offered[0];
37
+ },
38
+ open: (peer, request) => {
39
+ const decl = declFor(request);
40
+ const engine = engines[decl.id];
41
+ const pending = new Map();
42
+ const session = {
43
+ id: nextSession++, socket: decl.id, state: {},
44
+ send: (frame) => { peer.send(typeof frame === 'string' || frame instanceof Uint8Array ? frame : JSON.stringify(frame)); },
45
+ close: (code, reason) => { peer.close(code, reason); },
46
+ expect: (key, ms) => new Promise((resolve, reject) => {
47
+ const timer = setTimeout(() => { pending.delete(key); reject(new Error('no reply in time')); }, ms);
48
+ pending.set(key, { resolve, reject, timer });
49
+ }),
50
+ fulfil: (key, value) => {
51
+ const p = pending.get(key);
52
+ if (!p)
53
+ return false;
54
+ clearTimeout(p.timer);
55
+ pending.delete(key);
56
+ p.resolve(value);
57
+ return true;
58
+ },
59
+ decide,
60
+ };
61
+ const o = { session, engine, context: () => context(request.clone(), decl), queue: Promise.resolve(), pending };
62
+ byPeer.set(peer, o);
63
+ (sessions.get(key) ?? sessions.set(key, new Set()).get(key)).add(o);
64
+ if (engine.open)
65
+ o.queue = o.queue.then(async () => engine.open(session, await o.context())).catch((error) => session.close(1011, String(error.message ?? error).slice(0, 120)));
66
+ },
67
+ message: (peer, data) => {
68
+ const o = byPeer.get(peer);
69
+ if (!o)
70
+ return;
71
+ const text = typeof data === 'string' ? data : new TextDecoder().decode(data);
72
+ o.queue = o.queue.then(async () => o.engine.message(o.session, text, await o.context())).catch((error) => o.session.close(1011, String(error.message ?? error).slice(0, 120)));
73
+ },
74
+ close: (peer) => {
75
+ const o = byPeer.get(peer);
76
+ if (!o)
77
+ return;
78
+ sessions.get(key)?.delete(o);
79
+ // what awaited this session's replies is told it closed
80
+ for (const [k, p] of o.pending) {
81
+ clearTimeout(p.timer);
82
+ p.reject(new Error('the session closed'));
83
+ o.pending.delete(k);
84
+ }
85
+ // the end is written after what the session is still doing, over its own context
86
+ if (o.engine.close)
87
+ o.queue = o.queue.then(async () => o.engine.close(o.session, await o.context())).catch(() => { });
88
+ },
89
+ };
90
+ }
@@ -32,6 +32,7 @@ export type StateSystemAdapters = {
32
32
  root?: string;
33
33
  origin?: string;
34
34
  credential?: string;
35
+ scope?: string;
35
36
  }) => Promise<unknown>;
36
37
  ingest?: (request: Request, ctx: {
37
38
  root?: string;
@@ -9,7 +9,7 @@
9
9
  // from the vendor (the pack's refresh adapter). Reads never egress.
10
10
  // The binding is a file in the twin's state dir (`root.json`), written by `volter twin <vendor>
11
11
  // root`, read here — no env var, no pack code. The adapters come from the pack's descriptor
12
- // (protocol 2 `stateSystem`) or its anchored exports, registered by whoever mounts the twin.
12
+ // (`stateSystem`), registered by whoever mounts the twin.
13
13
  // CHECKS run before any entry is performed: a check that fails lands the entry as `refused` with
14
14
  // its reason and the deploy stops there. Checks are the world's files; the runtime loads them.
15
15
  import { dirname, join } from 'node:path';
@@ -61,7 +61,7 @@ export declare function twinLog(kind: string, details: Record<string, unknown>):
61
61
  * critical sections atomic. Public so world-runtime can guard concurrent `upWorld`
62
62
  * claims of one instance dir with the SAME lock semantics the event log uses (TWIN-36). */
63
63
  export declare function withFileLock<T>(lockPath: string, fn: () => T): T;
64
- /** The parent log as a protocol 1 pack reads it: every entry the branch inherited or landed, as
64
+ /** The parent log as observed rows: every entry the branch inherited or landed, as
65
65
  * observed rows (log.ts `toEvent`). A branched world sees its base world's logs to the branch
66
66
  * position first. */
67
67
  export declare function listEvents(service: string, root?: string): WorldServiceEvent[];
@@ -1,5 +1,5 @@
1
1
  import { dirname, isAbsolute, join, resolve } from 'node:path';
2
- import { assertNotBeingRemoved, withAncestryLock, withStateRemoval } from "./ancestry.js";
2
+ import { withStateLock, withStateRemoval } from "./ancestry.js";
3
3
  import { appendParentEntry, toEntry } from "./log.js";
4
4
  import { GenericWorldStateSchema, WorldServiceEventSchema, } from "./schemas.js";
5
5
  import { getActiveWorldStore } from "./world-store.js";
@@ -181,12 +181,12 @@ function appendJsonl(path, value) {
181
181
  * critical sections atomic. Public so world-runtime can guard concurrent `upWorld`
182
182
  * claims of one instance dir with the SAME lock semantics the event log uses (TWIN-36). */
183
183
  export function withFileLock(lockPath, fn) {
184
- return withAncestryLock(() => { assertNotBeingRemoved(dirname(lockPath)); return getActiveWorldStore().withLock(lockPath, fn); });
184
+ return withStateLock(lockPath, fn);
185
185
  }
186
186
  function writeJsonAtomic(path, value) {
187
187
  getActiveWorldStore().writeAtomic(path, `${JSON.stringify(value, null, 2)}\n`);
188
188
  }
189
- /** The parent log as a protocol 1 pack reads it: every entry the branch inherited or landed, as
189
+ /** The parent log as observed rows: every entry the branch inherited or landed, as
190
190
  * observed rows (log.ts `toEvent`). A branched world sees its base world's logs to the branch
191
191
  * position first. */
192
192
  export function listEvents(service, root) {
@@ -8,7 +8,7 @@
8
8
  // same trace-id, a new parent-id — so the app's handler continues the same trace.
9
9
  //
10
10
  // Nothing here is vendor knowledge, and nothing runs at import: the async-context store is made on
11
- // first use (a browser bundle of a mirror client carries the kernel and has no AsyncLocalStorage).
11
+ // first use (a browser bundle carrying the kernel and has no AsyncLocalStorage).
12
12
  import { AsyncLocalStorage } from 'node:async_hooks';
13
13
  export const TRACEPARENT_HEADER = 'traceparent';
14
14
  // version-traceid-parentid-flags, lowercase hex only (the spec's HEXDIGLC). Version ff is invalid;
@@ -62,13 +62,6 @@ export interface TwinFetchAdapterConfig {
62
62
  manifest: unknown | (() => unknown);
63
63
  /** GET /twin/scenario body (thunk) — scenario-carrying packs only. */
64
64
  scenarioStatus?: () => unknown;
65
- /** THE STORE DOOR (R5c, mirror purity R3): named, deterministic projections over stored
66
- * state, served at `GET /twin/store/<name>` and listed in the manifest as `stores`. A
67
- * mirror reads twin state ONLY through this door — never by importing the handler or a
68
- * `-twin-internal` module — so the vendor API lacking a listing endpoint (resend has no
69
- * list-emails) no longer breeds a bespoke `/_twin/*` route per mirror. Keyed in the skin
70
- * like every state read (only bare `GET /twin` is keyless). */
71
- stores?: Record<string, () => unknown | Promise<unknown>>;
72
65
  /** Per-request vendor extras (header threading: stripe-version, notion-version, …). */
73
66
  extras?: (request: Request, url: URL) => Record<string, unknown>;
74
67
  /** Static per-instance handler options (e.g. rateLimitPerSecond). */
@@ -91,25 +91,11 @@ export function createTwinFetchFromHandler(handler, config) {
91
91
  const cleanPath = url.pathname.replace(/\/+$/, '') || '/';
92
92
  if (request.method === 'GET' && cleanPath === '/twin') {
93
93
  const manifest = typeof config.manifest === 'function' ? config.manifest() : config.manifest;
94
- const storeNames = Object.keys(config.stores ?? {}).sort();
95
- if (storeNames.length > 0 && manifest !== null && typeof manifest === 'object') {
96
- const m = manifest;
97
- const doors = m.doors !== null && typeof m.doors === 'object' ? m.doors : {};
98
- // The manifest EDUCATES: it lists the store names and names the door's path.
99
- return Response.json({ ...m, stores: storeNames, doors: { ...doors, store: 'GET /twin/store/<name>' } });
100
- }
101
94
  return Response.json(manifest);
102
95
  }
103
96
  if (request.method === 'GET' && cleanPath === '/twin/scenario' && config.scenarioStatus !== undefined) {
104
97
  return Response.json(config.scenarioStatus());
105
98
  }
106
- if (request.method === 'GET' && cleanPath.startsWith('/twin/store/')) {
107
- const name = cleanPath.slice('/twin/store/'.length);
108
- const store = config.stores?.[name];
109
- if (store === undefined)
110
- return Response.json({ error: 'unknown store', store: name, stores: Object.keys(config.stores ?? {}).sort() }, { status: 404 });
111
- return Response.json(await store()); // a projection may read through the handler (async) — the door awaits it
112
- }
113
99
  const body = request.method === 'GET' || request.method === 'HEAD' ? '' : await request.text();
114
100
  const headers = {};
115
101
  request.headers.forEach((value, key) => { headers[key] = value; });
@@ -0,0 +1,6 @@
1
+ /** A call to a vendor the World runs no twin of. */
2
+ export declare class VendorUnreachableError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /** `url`, a vendor's own address, answered by the World's twin of that vendor. */
6
+ export declare function vendorFetch(url: string | URL, init?: RequestInit): Promise<Response>;
@@ -0,0 +1,41 @@
1
+ // A TWIN'S CALL TO ANOTHER VENDOR (architecture.md, "What an author writes, and how": `ctx.vendorFetch`). A vendor's
2
+ // service calls another vendor as part of its own behaviour — Clerk trades a Google sign-in's code for the person at
3
+ // Google's token endpoint — and in a World that other vendor is the World's twin of it. The call is addressed as the
4
+ // vendor's own URL and reaches that twin by the same host rules the injector routes an application by (vendor-hosts.cjs),
5
+ // carrying the World's twins key; a vendor the World runs no twin of is refused, never reached: a twin makes no direct
6
+ // vendor call (simulated execution is deterministic and stays inside the World). This is the one network a handler has;
7
+ // importing this module does nothing.
8
+ import { twinOrigins, VENDOR_HOSTS } from '../vendor-hosts.cjs';
9
+ /** A call to a vendor the World runs no twin of. */
10
+ export class VendorUnreachableError extends Error {
11
+ constructor(message) { super(message); this.name = 'VendorUnreachableError'; }
12
+ }
13
+ /** The World's twins by vendor: the instance's env (every twin's URL, whenever this process started) over this
14
+ * process's own. */
15
+ async function worldTwins() {
16
+ const env = process.env;
17
+ let instanceEnv = {};
18
+ const file = env.VOLTER_WORLD_INSTANCE;
19
+ if (file) {
20
+ try {
21
+ const { readFile } = await import('node:fs/promises');
22
+ instanceEnv = JSON.parse(await readFile(file, 'utf8')).env ?? {};
23
+ }
24
+ catch { /* no instance yet: this process's env alone */ }
25
+ }
26
+ return { ...twinOrigins(env), ...twinOrigins(instanceEnv) };
27
+ }
28
+ /** `url`, a vendor's own address, answered by the World's twin of that vendor. */
29
+ export async function vendorFetch(url, init = {}) {
30
+ const target = new URL(String(url));
31
+ const twins = await worldTwins();
32
+ const vendor = Object.keys(VENDOR_HOSTS).find((v) => twins[v] !== undefined && VENDOR_HOSTS[v](target.hostname, target.pathname));
33
+ if (vendor === undefined)
34
+ throw new VendorUnreachableError(`${target.host}${target.pathname}: this World runs no twin of it, and a twin calls only the World's twins`);
35
+ const headers = new Headers(init.headers);
36
+ headers.set('x-volter-twin-original-host', target.host);
37
+ const key = process.env.VOLTER_TWINS_KEY;
38
+ if (key && !headers.has('x-twins-key'))
39
+ headers.set('x-twins-key', key);
40
+ return fetch(`${twins[vendor].replace(/\/$/, '')}${target.pathname}${target.search}`, { ...init, headers });
41
+ }
@@ -10,7 +10,7 @@
10
10
  // without touching a line of twin business logic.
11
11
  //
12
12
  // The interface is SYNCHRONOUS on purpose. The kernel's internals (append-then-
13
- // project, read-then-append critical sections) are synchronous, and `clerk-twin.ts`'s
13
+ // project, read-then-append critical sections) are synchronous, and a pack's
14
14
  // request handler is a pure sync function keyed by `root`. Making the store async
15
15
  // would ripple `await` through every one of those call sites and change the handler's
16
16
  // shape — precisely the blast radius this seam exists to avoid. An async backend is