@volter/world-core 3.0.11 → 3.0.12

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.
package/attach.cjs CHANGED
@@ -25,15 +25,40 @@ if (ref) {
25
25
  // a blocking fetch at module load: spawn a child that prints the manifest
26
26
  const probe = spawnSync(
27
27
  process.execPath,
28
- ['-e', 'fetch(process.argv[1],process.argv[2]?{headers:{authorization:"Bearer "+process.argv[2]}}:undefined).then(r=>{if(!r.ok)throw new Error(r.status);return r.text()}).then(t=>process.stdout.write(t)).catch(e=>{console.error(String(e));process.exit(1)})', url, ...(token ? [token] : [])],
28
+ // the token by the child's environment, never its arguments: another user on this machine can list arguments
29
+ ['-e', 'const t=process.env.VOLTER_ATTACH_PROBE_TOKEN;fetch(process.argv[1],t?{headers:{authorization:"Bearer "+t}}:undefined).then(r=>{if(!r.ok)throw new Error(r.status);return r.text()}).then(t=>process.stdout.write(t)).catch(e=>{console.error(String(e));process.exit(1)})', url],
29
30
  // the child must NOT inherit NODE_OPTIONS/VOLTER_WORLD — it would
30
31
  // re-run this very file at its own startup (a spawn recursion bomb)
31
- { encoding: 'utf8', env: { ...process.env, NODE_OPTIONS: '', VOLTER_WORLD: '' } },
32
+ { encoding: 'utf8', env: { ...process.env, NODE_OPTIONS: '', VOLTER_WORLD: '', ...(token ? { VOLTER_ATTACH_PROBE_TOKEN: token } : {}) } },
32
33
  );
33
34
  if (probe.status !== 0) {
34
35
  throw new Error(`[twin-attach] could not fetch the world manifest from ${url}: ${(probe.stderr || '').trim()}`);
35
36
  }
36
37
  const manifest = JSON.parse(probe.stdout);
38
+ // a served World's twins as loopback listeners that add its key (./http-bridge.cjs), so a client with its own HTTP
39
+ // stack (wrangler's undici, which neither patch reaches) reaches them as it would a local World's: the twin URLs and
40
+ // every endpoint variable naming a twin's place point at the listener. Ports are chosen by a blocking child, the
41
+ // listeners bound on the next turn, before any client can connect, and never hold the process open.
42
+ // the key the World echoed to this caller, as the injector carries it; else the token given
43
+ const key = manifest.env?.VOLTER_TWINS_KEY ?? token;
44
+ const served = Object.entries(manifest.vendors ?? {}).filter(([, origin]) => key && /^https?:\/\//.test(String(origin)));
45
+ if (served.length) {
46
+ const picked = spawnSync(
47
+ process.execPath,
48
+ ['-e', 'const n=require("node:net");const k=+process.argv[1];const s=[];const d=()=>{if(s.length<k){const v=n.createServer().listen(0,"127.0.0.1",()=>{s.push(v);d()})}else{process.stdout.write(s.map(v=>v.address().port).join(","));s.forEach(v=>v.close())}};d()', String(served.length)],
49
+ { encoding: 'utf8', env: { ...process.env, NODE_OPTIONS: '', VOLTER_WORLD: '' } },
50
+ );
51
+ if (picked.status !== 0) throw new Error(`[twin-attach] could not choose loopback ports for the world's twins: ${(picked.stderr || '').trim()}`);
52
+ const ports = picked.stdout.split(',').map(Number);
53
+ const { bridgeHttp, bridgeSecret, pointAt } = require('./http-bridge.cjs');
54
+ served.forEach(([vendor], i) => {
55
+ const secret = bridgeSecret();
56
+ const at = pointAt(manifest, vendor, `http://127.0.0.1:${ports[i]}/${secret}`);
57
+ bridgeHttp(at, key, ports[i], secret, manifest.ca || undefined).then((server) => server.unref(), (error) => {
58
+ process.stderr.write(`[twin-attach] ${at}: could not listen on 127.0.0.1:${ports[i]}: ${error.message}\n`);
59
+ });
60
+ });
61
+ }
37
62
  for (const [vendor, origin] of Object.entries(manifest.vendors ?? {})) {
38
63
  const key = `${vendor.toUpperCase()}_TWIN_URL`;
39
64
  if (process.env[key] === undefined) process.env[key] = origin;
@@ -55,7 +80,7 @@ if (ref) {
55
80
  if (picked.status !== 0) throw new Error(`[twin-attach] could not choose loopback ports for the world's streams: ${(picked.stderr || '').trim()}`);
56
81
  const ports = picked.stdout.split(',').map(Number);
57
82
  const { bridgeStream, streamEnv } = require('./stream-bridge.cjs');
58
- const streamToken = token ?? manifest.env?.VOLTER_TWINS_KEY ?? '';
83
+ const streamToken = manifest.env?.VOLTER_TWINS_KEY ?? token ?? '';
59
84
  streams.forEach((stream, i) => {
60
85
  for (const [key, value] of Object.entries(streamEnv(stream.env, ports[i]))) {
61
86
  if (process.env[key] === undefined) process.env[key] = value;
package/dist/attach.cjs CHANGED
@@ -25,15 +25,40 @@ if (ref) {
25
25
  // a blocking fetch at module load: spawn a child that prints the manifest
26
26
  const probe = spawnSync(
27
27
  process.execPath,
28
- ['-e', 'fetch(process.argv[1],process.argv[2]?{headers:{authorization:"Bearer "+process.argv[2]}}:undefined).then(r=>{if(!r.ok)throw new Error(r.status);return r.text()}).then(t=>process.stdout.write(t)).catch(e=>{console.error(String(e));process.exit(1)})', url, ...(token ? [token] : [])],
28
+ // the token by the child's environment, never its arguments: another user on this machine can list arguments
29
+ ['-e', 'const t=process.env.VOLTER_ATTACH_PROBE_TOKEN;fetch(process.argv[1],t?{headers:{authorization:"Bearer "+t}}:undefined).then(r=>{if(!r.ok)throw new Error(r.status);return r.text()}).then(t=>process.stdout.write(t)).catch(e=>{console.error(String(e));process.exit(1)})', url],
29
30
  // the child must NOT inherit NODE_OPTIONS/VOLTER_WORLD — it would
30
31
  // re-run this very file at its own startup (a spawn recursion bomb)
31
- { encoding: 'utf8', env: { ...process.env, NODE_OPTIONS: '', VOLTER_WORLD: '' } },
32
+ { encoding: 'utf8', env: { ...process.env, NODE_OPTIONS: '', VOLTER_WORLD: '', ...(token ? { VOLTER_ATTACH_PROBE_TOKEN: token } : {}) } },
32
33
  );
33
34
  if (probe.status !== 0) {
34
35
  throw new Error(`[twin-attach] could not fetch the world manifest from ${url}: ${(probe.stderr || '').trim()}`);
35
36
  }
36
37
  const manifest = JSON.parse(probe.stdout);
38
+ // a served World's twins as loopback listeners that add its key (./http-bridge.cjs), so a client with its own HTTP
39
+ // stack (wrangler's undici, which neither patch reaches) reaches them as it would a local World's: the twin URLs and
40
+ // every endpoint variable naming a twin's place point at the listener. Ports are chosen by a blocking child, the
41
+ // listeners bound on the next turn, before any client can connect, and never hold the process open.
42
+ // the key the World echoed to this caller, as the injector carries it; else the token given
43
+ const key = manifest.env?.VOLTER_TWINS_KEY ?? token;
44
+ const served = Object.entries(manifest.vendors ?? {}).filter(([, origin]) => key && /^https?:\/\//.test(String(origin)));
45
+ if (served.length) {
46
+ const picked = spawnSync(
47
+ process.execPath,
48
+ ['-e', 'const n=require("node:net");const k=+process.argv[1];const s=[];const d=()=>{if(s.length<k){const v=n.createServer().listen(0,"127.0.0.1",()=>{s.push(v);d()})}else{process.stdout.write(s.map(v=>v.address().port).join(","));s.forEach(v=>v.close())}};d()', String(served.length)],
49
+ { encoding: 'utf8', env: { ...process.env, NODE_OPTIONS: '', VOLTER_WORLD: '' } },
50
+ );
51
+ if (picked.status !== 0) throw new Error(`[twin-attach] could not choose loopback ports for the world's twins: ${(picked.stderr || '').trim()}`);
52
+ const ports = picked.stdout.split(',').map(Number);
53
+ const { bridgeHttp, bridgeSecret, pointAt } = require('./http-bridge.cjs');
54
+ served.forEach(([vendor], i) => {
55
+ const secret = bridgeSecret();
56
+ const at = pointAt(manifest, vendor, `http://127.0.0.1:${ports[i]}/${secret}`);
57
+ bridgeHttp(at, key, ports[i], secret, manifest.ca || undefined).then((server) => server.unref(), (error) => {
58
+ process.stderr.write(`[twin-attach] ${at}: could not listen on 127.0.0.1:${ports[i]}: ${error.message}\n`);
59
+ });
60
+ });
61
+ }
37
62
  for (const [vendor, origin] of Object.entries(manifest.vendors ?? {})) {
38
63
  const key = `${vendor.toUpperCase()}_TWIN_URL`;
39
64
  if (process.env[key] === undefined) process.env[key] = origin;
@@ -55,7 +80,7 @@ if (ref) {
55
80
  if (picked.status !== 0) throw new Error(`[twin-attach] could not choose loopback ports for the world's streams: ${(picked.stderr || '').trim()}`);
56
81
  const ports = picked.stdout.split(',').map(Number);
57
82
  const { bridgeStream, streamEnv } = require('./stream-bridge.cjs');
58
- const streamToken = token ?? manifest.env?.VOLTER_TWINS_KEY ?? '';
83
+ const streamToken = manifest.env?.VOLTER_TWINS_KEY ?? token ?? '';
59
84
  streams.forEach((stream, i) => {
60
85
  for (const [key, value] of Object.entries(streamEnv(stream.env, ports[i]))) {
61
86
  if (process.env[key] === undefined) process.env[key] = value;
@@ -0,0 +1,92 @@
1
+ // THE HTTP BRIDGE: a served World's twins as loopback listeners, for a client the injector cannot reach because it brings
2
+ // its own HTTP stack (wrangler bundles undici; a Go binary, curl). Each forwards to its twin's place in the World with
3
+ // the World's key added, so the client needs only its vendor credential, as at a local World's twin. Used by the attach
4
+ // entry, beside the stream bridge (./stream-bridge.cjs).
5
+ //
6
+ // The key is the World's, so the listener lends it only to whoever holds the listener's own address: a random path
7
+ // prefix the attaching process's environment carries, as it carries the key. A request without it (another user on
8
+ // this machine, a page in a browser) is refused, as is any that names a path outside the twin's place.
9
+ 'use strict';
10
+
11
+ const http = require('node:http');
12
+ const https = require('node:https');
13
+ const { randomBytes } = require('node:crypto');
14
+
15
+ // the transports as Node has them, taken before the injector patches them: the bridge adds the key itself
16
+ const request = { 'http:': http.request, 'https:': https.request };
17
+
18
+ /** A listener's secret prefix: the path every request to it begins with. */
19
+ function bridgeSecret() { return randomBytes(18).toString('hex'); }
20
+
21
+ /** Serve the twin at `target` (its place in the World, `<origin>/<org>/<world>/<vendor>`) on 127.0.0.1:`port` (0: any),
22
+ * under `/<secret>`. A redirect to the twin's place is answered at the bridge's own address. */
23
+ function bridgeHttp(target, key, port = 0, secret = bridgeSecret(), ca = undefined) {
24
+ const base = new URL(target.replace(/\/$/, ''));
25
+ const mine = `/${secret}`;
26
+ const refuse = (res, status, why) => { res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' }); res.end(`[twin-http] ${why}\n`); };
27
+ const server = http.createServer((req, res) => {
28
+ // a browser's request carries its page's origin: never the attaching process's own
29
+ if (req.headers.origin) return refuse(res, 403, 'a page in a browser does not borrow the World\'s key');
30
+ const raw = req.url ?? '';
31
+ if (!(raw === mine || raw.startsWith(`${mine}/`) || raw.startsWith(`${mine}?`))) return refuse(res, 404, 'not this listener\'s address');
32
+ const rest = raw.slice(mine.length);
33
+ const to = new URL(`${base.pathname}${rest.startsWith('/') || rest === '' ? rest : `/${rest}`}`, base.origin);
34
+ // the URL resolved (dot segments, encoded or not): still under the twin's place, or refused
35
+ if (to.pathname !== base.pathname && !to.pathname.startsWith(`${base.pathname}/`)) return refuse(res, 403, 'outside the twin\'s place');
36
+ // the host and path the client sent, which a signature over the request (SigV4's canonical request) covers
37
+ const headers = { ...req.headers, host: base.host, 'x-twins-key': key, 'x-volter-twin-original-host': req.headers.host ?? '', 'x-volter-original-path': raw.split('?')[0] };
38
+ for (const h of ['connection', 'keep-alive', 'proxy-connection', 'upgrade']) delete headers[h];
39
+ const here = `http://127.0.0.1:${server.address().port}${mine}`;
40
+ // a World served behind its session CA is trusted by it here, as the injector's forwards are
41
+ const up = request[base.protocol](to, { method: req.method, headers, ...(ca && base.protocol === 'https:' ? { ca } : {}) }, (answer) => {
42
+ const out = { ...answer.headers };
43
+ delete out.connection; delete out['keep-alive'];
44
+ if (typeof out.location === 'string') {
45
+ // a redirect within the twin's place, absolute or by path, is answered at the listener
46
+ const loc = out.location;
47
+ if (loc === base.href || loc.startsWith(`${base.href}/`) || loc.startsWith(`${base.href}?`)) out.location = `${here}${loc.slice(base.href.length)}`;
48
+ else if (loc === base.pathname || loc.startsWith(`${base.pathname}/`)) out.location = `${here}${loc.slice(base.pathname.length)}`;
49
+ }
50
+ res.writeHead(answer.statusCode ?? 502, answer.statusMessage, out);
51
+ answer.on('error', () => res.destroy());
52
+ answer.pipe(res);
53
+ });
54
+ up.on('error', (error) => { if (res.headersSent) res.destroy(); else refuse(res, 502, `${base.href}: ${error.message}`); });
55
+ req.on('error', () => up.destroy());
56
+ res.on('close', () => { if (!res.writableFinished) up.destroy(); });
57
+ req.pipe(up);
58
+ });
59
+ // a socket a client upgrades (a log tail's WebSocket) is not carried yet: said, not dropped
60
+ server.on('upgrade', (_req, socket) => { socket.end('HTTP/1.1 501 Not Implemented\r\ncontent-type: text/plain\r\n\r\n[twin-http] a WebSocket is not carried through the listener\n'); });
61
+ return new Promise((resolve, reject) => {
62
+ server.once('error', reject);
63
+ server.listen(port, '127.0.0.1', () => resolve(server));
64
+ });
65
+ }
66
+
67
+ /** Point a World manifest's twin at a local listener: its twin URL, and every endpoint variable naming its place (itself,
68
+ * or a path or query under it; never another twin whose name it begins). */
69
+ function pointAt(manifest, vendor, local) {
70
+ const at = String(manifest.vendors[vendor]).replace(/\/$/, '');
71
+ const under = (value) => value === at || value.startsWith(`${at}/`) || value.startsWith(`${at}?`);
72
+ manifest.vendors[vendor] = local;
73
+ for (const [name, value] of Object.entries(manifest.env ?? {})) if (typeof value === 'string' && under(value)) manifest.env[name] = `${local}${value.slice(at.length)}`;
74
+ return at;
75
+ }
76
+
77
+ /** Every served twin of a World manifest bridged (`volter-world attach`, a command in any language): its listener up, and
78
+ * the manifest pointed at it. Resolves to the listeners, unref'd. */
79
+ async function bridgeTwins(manifest, key, ca = undefined) {
80
+ const servers = [];
81
+ for (const [vendor, origin] of Object.entries(manifest.vendors ?? {})) {
82
+ if (!key || !/^https?:\/\//.test(String(origin))) continue;
83
+ const secret = bridgeSecret();
84
+ const server = await bridgeHttp(String(origin), key, 0, secret, ca);
85
+ server.unref();
86
+ pointAt(manifest, vendor, `http://127.0.0.1:${server.address().port}/${secret}`);
87
+ servers.push(server);
88
+ }
89
+ return servers;
90
+ }
91
+
92
+ module.exports = { bridgeHttp, bridgeSecret, bridgeTwins, pointAt };
@@ -325,6 +325,9 @@ function refreshOf(units, service) {
325
325
  return async (execute, opts) => {
326
326
  const observed = [];
327
327
  const complete = [];
328
+ // what the root's credential may not read (a token scoped to some of the account): left as the World holds it, said
329
+ const unread = [];
330
+ let answered = 0;
328
331
  for (const unit of units) {
329
332
  const { manifest: m, surface } = unit;
330
333
  // parents before children, however deep
@@ -344,17 +347,31 @@ function refreshOf(units, service) {
344
347
  const limit = m.list?.limit && (op.query ?? []).some((q) => q.name === m.list.limit.param) ? { [m.list.limit.param]: String(m.list.limit.max) } : {};
345
348
  let page = { ...limit };
346
349
  for (let taken = 0;;) {
347
- const path = `${op.basePath ?? surface.basePath ?? ''}${op.path}`.replace(/\{([^}]+)\}/g, (_, n) => encodeURIComponent(n === decl.parent?.param ? String(parent) : ''));
350
+ let unfilled = false;
351
+ const path = `${op.basePath ?? surface.basePath ?? ''}${op.path}`.replace(/\{([^}]+)\}/g, (_, n) => { const v = n === decl.parent?.param && parent !== undefined ? String(parent) : ''; if (!v)
352
+ unfilled = true; return encodeURIComponent(v); });
348
353
  // a parent the operation names in its query, not its path (Slack's conversations.history `channel`)
349
354
  const byQuery = decl.parent?.param && parent !== undefined && !op.path.includes(`{${decl.parent.param}}`) ? { [decl.parent.param]: String(parent) } : {};
350
355
  const q = new URLSearchParams({ ...page, ...byQuery });
351
356
  const answer = await execute({ method: op.method.toUpperCase(), path: [...q].length ? `${path}?${q}` : path, ...(unit.lane ? { lane: unit.lane } : {}) });
352
- if (answer.status === 401 || answer.status === 403)
353
- throw new Error(`${answer.status}: the vendor refused the root's credential; nothing folded`);
357
+ if (answer.status === 401)
358
+ throw new Error(`401: the vendor refused the root's credential (${op.method.toUpperCase()} ${path}); nothing folded`);
359
+ if (answer.status === 403) {
360
+ unread.push({ type, path, status: 403 });
361
+ whole = false;
362
+ break;
363
+ }
354
364
  if (answer.status === 429)
355
365
  throw new Error(`429: rate limited${answer.headers['retry-after'] ? `, retry after ${answer.headers['retry-after']}s` : ''}`);
366
+ // one subject read by its parent and not there (an account with no workers.dev subdomain): nothing to observe. A
367
+ // path missing a parameter (an empty segment) is the kernel's mistake, never the vendor's absence
368
+ if ('get' in scope && answer.status === 404 && !unfilled) {
369
+ answered += 1;
370
+ break;
371
+ }
356
372
  if (answer.status >= 400)
357
373
  throw new Error(`${answer.status}: ${refusalWords(m, parse(answer))}`);
374
+ answered += 1;
358
375
  const parsed = parse(answer);
359
376
  const items = 'get' in scope ? [object(op.answers?.key ? at(parsed, op.answers.key) : parsed) ?? {}]
360
377
  : 'items' in scope && scope.items ? (at(parsed, scope.items) ?? []).map((x) => object(x)).filter((x) => x !== undefined) : itemsOf(op, parsed);
@@ -411,8 +428,12 @@ function refreshOf(units, service) {
411
428
  complete.push({ type, field: decl.parent.field, within: parents.map(String) });
412
429
  }
413
430
  }
431
+ // every read refused is a credential the vendor refuses, never an account that holds nothing
432
+ if (unread.length && !answered)
433
+ throw new Error(`403: the vendor refused the root's credential for every read (${unread.map((u) => u.path).join(', ')}); nothing folded`);
414
434
  adopt(service, opts.root, units, observed);
415
- return observeResources(service, observed, { ...(opts.root !== undefined ? { root: opts.root } : {}), complete });
435
+ const report = observeResources(service, observed, { ...(opts.root !== undefined ? { root: opts.root } : {}), complete });
436
+ return unread.length ? { ...report, unread } : report;
416
437
  };
417
438
  }
418
439
  /** A vendor's subject's stored id: its key template filled from its fields (its parent's among them), else its id
@@ -69,12 +69,18 @@ const DECODE = { gzip: gunzipSync, zstd: zstdDecompressSync, deflate: inflateSyn
69
69
  /** A request with its body decoded as its Content-Encoding says, when the manifest's `encodings` reads that coding;
70
70
  * undefined when the bytes are not what the header names. */
71
71
  async function decoded(m, request) {
72
- const coding = (request.headers.get('content-encoding') ?? '').trim().toLowerCase();
73
- if (!m.encodings?.request?.includes(coding) || request.method === 'GET' || request.method === 'HEAD')
72
+ if (request.method === 'GET' || request.method === 'HEAD' || request.body === null)
74
73
  return request;
74
+ // the body read once, here, and every part of the pack given a request made again from its bytes: a served request's
75
+ // body read through a clone and then read again (an `around` that checks a body before its handler does) tees a stream
76
+ // whose second branch a Bun server never finishes, and the request hung
77
+ const raw = new Uint8Array(await request.arrayBuffer());
78
+ const coding = (request.headers.get('content-encoding') ?? '').trim().toLowerCase();
79
+ if (!m.encodings?.request?.includes(coding))
80
+ return new Request(request.url, { method: request.method, headers: request.headers, body: raw });
75
81
  let body;
76
82
  try {
77
- body = DECODE[coding](new Uint8Array(await request.arrayBuffer()));
83
+ body = DECODE[coding](raw);
78
84
  }
79
85
  catch {
80
86
  return undefined;
@@ -213,11 +219,16 @@ export function createPackFetch(parts, options = {}) {
213
219
  const workspaces = () => [...(m.screens ?? []).filter((s) => s.kind === 'workspace' && s.status === 'done' && parts.screens?.[s.id]).map((s) => ({ id: s.id, path: s.path })), ...lanesWorkspaces(parts.lanes ?? {})];
214
220
  /** Whether a built screen a person views (a workspace or a flow, never content an API client reads) draws a url: its host
215
221
  * one the screen names, its path the screen's or under it (a root screen draws its root alone). */
222
+ // by what the pack declares built (a screen drawn by another's handler, as X's profile is by its web pages, is built)
223
+ const viewed = (m.screens ?? []).filter((s) => s.status === 'done' && s.kind !== 'content').map((s) => {
224
+ const hosts = [s.host, ...(s.hosts ?? [])].map(hostMatcher);
225
+ return { root: s.path.replace(/\/+$/, '') === '', host: (h) => hosts.some((match) => match(h)), path: pathMatcher(s.path, true) };
226
+ });
216
227
  const drawsUrl = (at) => {
217
228
  try {
218
229
  const u = new URL(at);
219
230
  const path = u.pathname.replace(/\/+$/, '') || '/';
220
- return screens.some((s) => s.decl.kind !== 'content' && Boolean(s.host(u.hostname.toLowerCase())) && (s.decl.path.replace(/\/+$/, '') === '' ? path === '/' : Boolean(s.path(path))));
231
+ return viewed.some((s) => s.host(u.hostname.toLowerCase()) && (s.root ? path === '/' : Boolean(s.path(path))));
221
232
  }
222
233
  catch {
223
234
  return false;
@@ -246,7 +257,9 @@ export function createPackFetch(parts, options = {}) {
246
257
  return request;
247
258
  // the path the client sent, which a signature over the request (SigV4's canonical URI) covers
248
259
  const headers = new Headers(request.headers);
249
- headers.set(ORIGINAL_PATH_HEADER, url.pathname);
260
+ // the path as first sent: a front before this (an attach bridge) may have named the one its client signed
261
+ if (!headers.has(ORIGINAL_PATH_HEADER))
262
+ headers.set(ORIGINAL_PATH_HEADER, url.pathname);
250
263
  if (Object.keys(groups).length)
251
264
  headers.set(PREFIX_PARAMS_HEADER, JSON.stringify(groups));
252
265
  url.pathname = path;
@@ -3,6 +3,7 @@ export type { PackTransport, PullPosture, PullTrigger, PullVendor, RoundTripWrit
3
3
  export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest } from './scenario.js';
4
4
  export { WORLD_CLOCK_ENV, worldNow } from './world-clock.js';
5
5
  export { WORLD_ENV_NAMES_ENV, worldEnvValue } from './world-env.js';
6
+ export { ORIGINAL_PATH_HEADER } from './sigv4.js';
6
7
  export { createTwinFetchFromHandler, TWIN_PREFIX_HEADER, TWIN_REQUEST_SCOPES, TWIN_SITES_HEADER, twinPublicBase, twinSiteUrl, withRequestScopes } from './twin-fetch.js';
7
8
  export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, ReadOnlyRequestError, runAsReadOnlyRequest, runAsVendorMove, writesRefused } from './request-scope.js';
8
9
  export { compileSurface, createDerivedFetch, matchOperation } from './derived.js';
@@ -11,6 +11,7 @@ export { clearRegistry, getPack, hasPack, listPacks, packOf, registerPack, pullP
11
11
  export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest } from "./scenario.js";
12
12
  export { WORLD_CLOCK_ENV, worldNow } from "./world-clock.js";
13
13
  export { WORLD_ENV_NAMES_ENV, worldEnvValue } from "./world-env.js";
14
+ export { ORIGINAL_PATH_HEADER } from "./sigv4.js";
14
15
  export { createTwinFetchFromHandler, TWIN_PREFIX_HEADER, TWIN_REQUEST_SCOPES, TWIN_SITES_HEADER, twinPublicBase, twinSiteUrl, withRequestScopes } from "./twin-fetch.js";
15
16
  export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, ReadOnlyRequestError, runAsReadOnlyRequest, runAsVendorMove, writesRefused } from "./request-scope.js";
16
17
  export { compileSurface, createDerivedFetch, matchOperation } from "./derived.js";
@@ -0,0 +1,92 @@
1
+ // THE HTTP BRIDGE: a served World's twins as loopback listeners, for a client the injector cannot reach because it brings
2
+ // its own HTTP stack (wrangler bundles undici; a Go binary, curl). Each forwards to its twin's place in the World with
3
+ // the World's key added, so the client needs only its vendor credential, as at a local World's twin. Used by the attach
4
+ // entry, beside the stream bridge (./stream-bridge.cjs).
5
+ //
6
+ // The key is the World's, so the listener lends it only to whoever holds the listener's own address: a random path
7
+ // prefix the attaching process's environment carries, as it carries the key. A request without it (another user on
8
+ // this machine, a page in a browser) is refused, as is any that names a path outside the twin's place.
9
+ 'use strict';
10
+
11
+ const http = require('node:http');
12
+ const https = require('node:https');
13
+ const { randomBytes } = require('node:crypto');
14
+
15
+ // the transports as Node has them, taken before the injector patches them: the bridge adds the key itself
16
+ const request = { 'http:': http.request, 'https:': https.request };
17
+
18
+ /** A listener's secret prefix: the path every request to it begins with. */
19
+ function bridgeSecret() { return randomBytes(18).toString('hex'); }
20
+
21
+ /** Serve the twin at `target` (its place in the World, `<origin>/<org>/<world>/<vendor>`) on 127.0.0.1:`port` (0: any),
22
+ * under `/<secret>`. A redirect to the twin's place is answered at the bridge's own address. */
23
+ function bridgeHttp(target, key, port = 0, secret = bridgeSecret(), ca = undefined) {
24
+ const base = new URL(target.replace(/\/$/, ''));
25
+ const mine = `/${secret}`;
26
+ const refuse = (res, status, why) => { res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' }); res.end(`[twin-http] ${why}\n`); };
27
+ const server = http.createServer((req, res) => {
28
+ // a browser's request carries its page's origin: never the attaching process's own
29
+ if (req.headers.origin) return refuse(res, 403, 'a page in a browser does not borrow the World\'s key');
30
+ const raw = req.url ?? '';
31
+ if (!(raw === mine || raw.startsWith(`${mine}/`) || raw.startsWith(`${mine}?`))) return refuse(res, 404, 'not this listener\'s address');
32
+ const rest = raw.slice(mine.length);
33
+ const to = new URL(`${base.pathname}${rest.startsWith('/') || rest === '' ? rest : `/${rest}`}`, base.origin);
34
+ // the URL resolved (dot segments, encoded or not): still under the twin's place, or refused
35
+ if (to.pathname !== base.pathname && !to.pathname.startsWith(`${base.pathname}/`)) return refuse(res, 403, 'outside the twin\'s place');
36
+ // the host and path the client sent, which a signature over the request (SigV4's canonical request) covers
37
+ const headers = { ...req.headers, host: base.host, 'x-twins-key': key, 'x-volter-twin-original-host': req.headers.host ?? '', 'x-volter-original-path': raw.split('?')[0] };
38
+ for (const h of ['connection', 'keep-alive', 'proxy-connection', 'upgrade']) delete headers[h];
39
+ const here = `http://127.0.0.1:${server.address().port}${mine}`;
40
+ // a World served behind its session CA is trusted by it here, as the injector's forwards are
41
+ const up = request[base.protocol](to, { method: req.method, headers, ...(ca && base.protocol === 'https:' ? { ca } : {}) }, (answer) => {
42
+ const out = { ...answer.headers };
43
+ delete out.connection; delete out['keep-alive'];
44
+ if (typeof out.location === 'string') {
45
+ // a redirect within the twin's place, absolute or by path, is answered at the listener
46
+ const loc = out.location;
47
+ if (loc === base.href || loc.startsWith(`${base.href}/`) || loc.startsWith(`${base.href}?`)) out.location = `${here}${loc.slice(base.href.length)}`;
48
+ else if (loc === base.pathname || loc.startsWith(`${base.pathname}/`)) out.location = `${here}${loc.slice(base.pathname.length)}`;
49
+ }
50
+ res.writeHead(answer.statusCode ?? 502, answer.statusMessage, out);
51
+ answer.on('error', () => res.destroy());
52
+ answer.pipe(res);
53
+ });
54
+ up.on('error', (error) => { if (res.headersSent) res.destroy(); else refuse(res, 502, `${base.href}: ${error.message}`); });
55
+ req.on('error', () => up.destroy());
56
+ res.on('close', () => { if (!res.writableFinished) up.destroy(); });
57
+ req.pipe(up);
58
+ });
59
+ // a socket a client upgrades (a log tail's WebSocket) is not carried yet: said, not dropped
60
+ server.on('upgrade', (_req, socket) => { socket.end('HTTP/1.1 501 Not Implemented\r\ncontent-type: text/plain\r\n\r\n[twin-http] a WebSocket is not carried through the listener\n'); });
61
+ return new Promise((resolve, reject) => {
62
+ server.once('error', reject);
63
+ server.listen(port, '127.0.0.1', () => resolve(server));
64
+ });
65
+ }
66
+
67
+ /** Point a World manifest's twin at a local listener: its twin URL, and every endpoint variable naming its place (itself,
68
+ * or a path or query under it; never another twin whose name it begins). */
69
+ function pointAt(manifest, vendor, local) {
70
+ const at = String(manifest.vendors[vendor]).replace(/\/$/, '');
71
+ const under = (value) => value === at || value.startsWith(`${at}/`) || value.startsWith(`${at}?`);
72
+ manifest.vendors[vendor] = local;
73
+ for (const [name, value] of Object.entries(manifest.env ?? {})) if (typeof value === 'string' && under(value)) manifest.env[name] = `${local}${value.slice(at.length)}`;
74
+ return at;
75
+ }
76
+
77
+ /** Every served twin of a World manifest bridged (`volter-world attach`, a command in any language): its listener up, and
78
+ * the manifest pointed at it. Resolves to the listeners, unref'd. */
79
+ async function bridgeTwins(manifest, key, ca = undefined) {
80
+ const servers = [];
81
+ for (const [vendor, origin] of Object.entries(manifest.vendors ?? {})) {
82
+ if (!key || !/^https?:\/\//.test(String(origin))) continue;
83
+ const secret = bridgeSecret();
84
+ const server = await bridgeHttp(String(origin), key, 0, secret, ca);
85
+ server.unref();
86
+ pointAt(manifest, vendor, `http://127.0.0.1:${server.address().port}/${secret}`);
87
+ servers.push(server);
88
+ }
89
+ return servers;
90
+ }
91
+
92
+ module.exports = { bridgeHttp, bridgeSecret, bridgeTwins, pointAt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-core",
3
- "version": "3.0.11",
3
+ "version": "3.0.12",
4
4
  "description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
5
5
  "keywords": [
6
6
  "twin",
@@ -24,6 +24,7 @@
24
24
  "inject.cjs",
25
25
  "attach.cjs",
26
26
  "stream-bridge.cjs",
27
+ "http-bridge.cjs",
27
28
  "network-policy.cjs",
28
29
  "network-policy.d.cts",
29
30
  "app-route.cjs",
@@ -75,6 +76,7 @@
75
76
  "./inject": "./inject.cjs",
76
77
  "./attach": "./attach.cjs",
77
78
  "./stream-bridge": "./stream-bridge.cjs",
79
+ "./http-bridge": "./http-bridge.cjs",
78
80
  "./network-policy": {
79
81
  "types": "./network-policy.d.cts",
80
82
  "default": "./network-policy.cjs"
@@ -280,6 +280,9 @@ function refreshOf(units: Unit[], service: string): NonNullable<StateSystemAdapt
280
280
  return async (execute, opts) => {
281
281
  const observed: ObservedResource[] = [];
282
282
  const complete: Completed[] = [];
283
+ // what the root's credential may not read (a token scoped to some of the account): left as the World holds it, said
284
+ const unread: Array<{ type: string; path: string; status: number }> = [];
285
+ let answered = 0;
283
286
  for (const unit of units) {
284
287
  const { manifest: m, surface } = unit;
285
288
  // parents before children, however deep
@@ -298,14 +301,20 @@ function refreshOf(units: Unit[], service: string): NonNullable<StateSystemAdapt
298
301
  const limit = m.list?.limit && (op.query ?? []).some((q) => q.name === m.list!.limit.param) ? { [m.list.limit.param]: String(m.list.limit.max) } : {};
299
302
  let page: Record<string, string> = { ...limit };
300
303
  for (let taken = 0; ;) {
301
- const path = `${op.basePath ?? surface.basePath ?? ''}${op.path}`.replace(/\{([^}]+)\}/g, (_, n: string) => encodeURIComponent(n === decl.parent?.param ? String(parent) : ''));
304
+ let unfilled = false;
305
+ const path = `${op.basePath ?? surface.basePath ?? ''}${op.path}`.replace(/\{([^}]+)\}/g, (_, n: string) => { const v = n === decl.parent?.param && parent !== undefined ? String(parent) : ''; if (!v) unfilled = true; return encodeURIComponent(v); });
302
306
  // a parent the operation names in its query, not its path (Slack's conversations.history `channel`)
303
307
  const byQuery = decl.parent?.param && parent !== undefined && !op.path.includes(`{${decl.parent.param}}`) ? { [decl.parent.param]: String(parent) } : {};
304
308
  const q = new URLSearchParams({ ...page, ...byQuery });
305
309
  const answer = await execute({ method: op.method.toUpperCase(), path: [...q].length ? `${path}?${q}` : path, ...(unit.lane ? { lane: unit.lane } : {}) });
306
- if (answer.status === 401 || answer.status === 403) throw new Error(`${answer.status}: the vendor refused the root's credential; nothing folded`);
310
+ if (answer.status === 401) throw new Error(`401: the vendor refused the root's credential (${op.method.toUpperCase()} ${path}); nothing folded`);
311
+ if (answer.status === 403) { unread.push({ type, path, status: 403 }); whole = false; break; }
307
312
  if (answer.status === 429) throw new Error(`429: rate limited${answer.headers['retry-after'] ? `, retry after ${answer.headers['retry-after']}s` : ''}`);
313
+ // one subject read by its parent and not there (an account with no workers.dev subdomain): nothing to observe. A
314
+ // path missing a parameter (an empty segment) is the kernel's mistake, never the vendor's absence
315
+ if ('get' in scope && answer.status === 404 && !unfilled) { answered += 1; break; }
308
316
  if (answer.status >= 400) throw new Error(`${answer.status}: ${refusalWords(m, parse(answer))}`);
317
+ answered += 1;
309
318
  const parsed = parse(answer);
310
319
  const items = 'get' in scope ? [object(op.answers?.key ? at(parsed, op.answers.key) : parsed) ?? {}]
311
320
  : 'items' in scope && scope.items ? ((at(parsed, scope.items) as unknown[] | undefined) ?? []).map((x) => object(x)).filter((x): x is Row => x !== undefined) : itemsOf(op, parsed);
@@ -344,8 +353,11 @@ function refreshOf(units: Unit[], service: string): NonNullable<StateSystemAdapt
344
353
  else if (whole && parents.length) complete.push({ type, field: decl.parent!.field, within: parents.map(String) });
345
354
  }
346
355
  }
356
+ // every read refused is a credential the vendor refuses, never an account that holds nothing
357
+ if (unread.length && !answered) throw new Error(`403: the vendor refused the root's credential for every read (${unread.map((u) => u.path).join(', ')}); nothing folded`);
347
358
  adopt(service, opts.root, units, observed);
348
- return observeResources(service, observed, { ...(opts.root !== undefined ? { root: opts.root } : {}), complete });
359
+ const report = observeResources(service, observed, { ...(opts.root !== undefined ? { root: opts.root } : {}), complete });
360
+ return unread.length ? { ...(report as object), unread } : report;
349
361
  };
350
362
  }
351
363
 
package/src/pack-fetch.ts CHANGED
@@ -124,10 +124,15 @@ const DECODE = { gzip: gunzipSync, zstd: zstdDecompressSync, deflate: inflateSyn
124
124
  /** A request with its body decoded as its Content-Encoding says, when the manifest's `encodings` reads that coding;
125
125
  * undefined when the bytes are not what the header names. */
126
126
  async function decoded(m: DerivedManifest, request: Request): Promise<Request | undefined> {
127
+ if (request.method === 'GET' || request.method === 'HEAD' || request.body === null) return request;
128
+ // the body read once, here, and every part of the pack given a request made again from its bytes: a served request's
129
+ // body read through a clone and then read again (an `around` that checks a body before its handler does) tees a stream
130
+ // whose second branch a Bun server never finishes, and the request hung
131
+ const raw = new Uint8Array(await request.arrayBuffer());
127
132
  const coding = (request.headers.get('content-encoding') ?? '').trim().toLowerCase() as keyof typeof DECODE;
128
- if (!m.encodings?.request?.includes(coding) || request.method === 'GET' || request.method === 'HEAD') return request;
133
+ if (!m.encodings?.request?.includes(coding)) return new Request(request.url, { method: request.method, headers: request.headers, body: raw });
129
134
  let body: Uint8Array;
130
- try { body = DECODE[coding](new Uint8Array(await request.arrayBuffer())); } catch { return undefined; }
135
+ try { body = DECODE[coding](raw); } catch { return undefined; }
131
136
  const headers = new Headers(request.headers);
132
137
  headers.delete('content-encoding');
133
138
  headers.delete('content-length');
@@ -249,10 +254,15 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
249
254
  const workspaces = (): Workspace[] => [...(m.screens ?? []).filter((s) => s.kind === 'workspace' && s.status === 'done' && parts.screens?.[s.id]).map((s) => ({ id: s.id, path: s.path })), ...lanesWorkspaces(parts.lanes ?? {})];
250
255
  /** Whether a built screen a person views (a workspace or a flow, never content an API client reads) draws a url: its host
251
256
  * one the screen names, its path the screen's or under it (a root screen draws its root alone). */
257
+ // by what the pack declares built (a screen drawn by another's handler, as X's profile is by its web pages, is built)
258
+ const viewed = (m.screens ?? []).filter((s) => s.status === 'done' && s.kind !== 'content').map((s) => {
259
+ const hosts = [s.host, ...(s.hosts ?? [])].map(hostMatcher);
260
+ return { root: s.path.replace(/\/+$/, '') === '', host: (h: string) => hosts.some((match) => match(h)), path: pathMatcher(s.path, true) };
261
+ });
252
262
  const drawsUrl = (at: string): boolean => {
253
263
  try {
254
264
  const u = new URL(at); const path = u.pathname.replace(/\/+$/, '') || '/';
255
- return screens.some((s) => s.decl.kind !== 'content' && Boolean(s.host(u.hostname.toLowerCase())) && (s.decl.path.replace(/\/+$/, '') === '' ? path === '/' : Boolean(s.path(path))));
265
+ return viewed.some((s) => s.host(u.hostname.toLowerCase()) && (s.root ? path === '/' : Boolean(s.path(path))));
256
266
  } catch { return false; }
257
267
  };
258
268
  // a context the kernel opens for what no surface operation names: a door, a screen, the clock, the board
@@ -277,7 +287,8 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
277
287
  if (path === url.pathname && !Object.keys(groups).length) return request;
278
288
  // the path the client sent, which a signature over the request (SigV4's canonical URI) covers
279
289
  const headers = new Headers(request.headers);
280
- headers.set(ORIGINAL_PATH_HEADER, url.pathname);
290
+ // the path as first sent: a front before this (an attach bridge) may have named the one its client signed
291
+ if (!headers.has(ORIGINAL_PATH_HEADER)) headers.set(ORIGINAL_PATH_HEADER, url.pathname);
281
292
  if (Object.keys(groups).length) headers.set(PREFIX_PARAMS_HEADER, JSON.stringify(groups));
282
293
  url.pathname = path;
283
294
  return new Request(url, { method: request.method, headers, body: request.body, ...(request.body ? { duplex: 'half' } : {}) } as RequestInit);
package/src/runtime.ts CHANGED
@@ -12,6 +12,7 @@ export type { PackTransport, PullPosture, PullTrigger, PullVendor, RoundTripWrit
12
12
  export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest } from './scenario.ts';
13
13
  export { WORLD_CLOCK_ENV, worldNow } from './world-clock.ts';
14
14
  export { WORLD_ENV_NAMES_ENV, worldEnvValue } from './world-env.ts';
15
+ export { ORIGINAL_PATH_HEADER } from './sigv4.ts';
15
16
  export { createTwinFetchFromHandler, TWIN_PREFIX_HEADER, TWIN_REQUEST_SCOPES, TWIN_SITES_HEADER, twinPublicBase, twinSiteUrl, withRequestScopes } from './twin-fetch.ts';
16
17
  export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, ReadOnlyRequestError, runAsReadOnlyRequest, runAsVendorMove, writesRefused } from './request-scope.ts';
17
18
  export { compileSurface, createDerivedFetch, matchOperation } from './derived.ts';