@volter/world-runtime 3.0.156 → 3.0.157

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.
@@ -1,6 +1,6 @@
1
1
  import { errorMessage } from "./error-message.js";
2
2
  import { worldChangesetRules } from "./configs.js";
3
- import { blobDigest, captureHistory, captureParentHistory, getActiveBlobStore, getActiveWorldStore, withHistoryLocks, worldPaths } from '@volter/world-core/runtime';
3
+ import { blobDigest, captureHistory, captureParentHistory, getActiveBlobStore, getActiveWorldStore, resourceBlobFile, resourceBlobKey, withHistoryLocks, worldPaths } from '@volter/world-core/runtime';
4
4
  // `volter-world mark` / `diff` / `changeset` — the WORLD-side verbs over the changeset
5
5
  // primitive (docs/concepts/the-model.md v0). Thin, exactly like `covers` and `tail` are thin: all
6
6
  // the state logic (markers, deltas, content hashing, replay) lives in @volter/world-core's
@@ -24,7 +24,7 @@ import { blobDigest, captureHistory, captureParentHistory, getActiveBlobStore, g
24
24
  // Read-only over the twins' data at rest, like `tail` — except `replay`, which is the one verb
25
25
  // here that WRITES, and it writes only through the control plane's kernel write path.
26
26
  import { tmpdir } from 'node:os';
27
- import { join, relative, resolve } from 'node:path';
27
+ import { join, resolve } from 'node:path';
28
28
  import { approveChangeset, rebaseChangeset, buildChangeset, captureMarker, changesetHashMatches, changesetReadiness, diffLedgers, normalizeChangeset, replayChangeset, runChangesetVerifiers, stateDirName, withApplication, withVerification, worldBootMarker, assertSafeChangesetName, assertMarkerBelongsTo, CHANGESET_KIND, MARKER_KIND, WORLD_BOOT_MARKER_ID, } from '@volter/world-core/runtime';
29
29
  import { instanceDir, isWorldOrigin, listWorlds, statusWorld } from "./runtime.js";
30
30
  import { fetchFromOrigin } from "./origin.js";
@@ -436,7 +436,7 @@ export async function pushWorldChangeset(name, options) {
436
436
  const twins = [...new Map(located.changeset.actions.map((a) => [a.service, a.stateService ?? a.service])).entries()];
437
437
  const ownBlobs = async (twin, state) => {
438
438
  const dir = worldPaths(state, join(instance.dirs.data, twin)).resources;
439
- return (await blobs.list(dir)).map((path) => ({ key: relative(dir, path).split('\\').join('/'), path }));
439
+ return (await blobs.list(dir)).map((path) => ({ key: resourceBlobKey(dir, path), path }));
440
440
  };
441
441
  // content-addressed means a segment of the key IS the digest of the bytes it holds: such bytes never
442
442
  // change. A name built on a digest of something else (a record keyed by a hash of an action id, a
@@ -465,12 +465,12 @@ export async function pushWorldChangeset(name, options) {
465
465
  for (const { key, path } of await ownBlobs(twin, state)) {
466
466
  if (!(await wanted(key, path)))
467
467
  continue;
468
- const held = await blobs.get(join(there, key));
468
+ const held = await blobs.get(resourceBlobFile(there, key));
469
469
  if (held !== null && blobDigest(held) === (await digestOf(path)))
470
470
  continue;
471
471
  const bytes = await blobs.get(path);
472
472
  if (bytes)
473
- await blobs.put(join(there, key), bytes);
473
+ await blobs.put(resourceBlobFile(there, key), bytes);
474
474
  }
475
475
  }
476
476
  return;
@@ -419,6 +419,11 @@ export declare class WorldDoors {
419
419
  private enforcesRead;
420
420
  /** The vendor wire: `/<org>/<world>/<vendor>/<rest>` → the twin's own URL. */
421
421
  private wire;
422
+ /** A page's links to sites this World serves (the hostnames its twins claim, a Worker's custom domain among them),
423
+ * rewritten to where the World shows each (siteUrlOf), so a person who follows one stays in the World. The page's own
424
+ * vendor's hosts (`own`) are its own to place, and a site the World does not hold keeps its real address. Null
425
+ * when there is nothing to do: not HTML, compressed, or no site served. */
426
+ private inWorldLinks;
422
427
  private linksPath;
423
428
  /** The World's links: name → the vendor origin it forwards to. */
424
429
  links(): Record<string, {
@@ -46,7 +46,7 @@ import { fetchFromOrigin } from "./origin.js";
46
46
  import { CONSOLE_BASE, consoleRedirect, serveConsoleApart } from "./console-apart.js";
47
47
  import { loadWorldConfig, worldChangesetRules, writeWorldConfig } from "./configs.js";
48
48
  import { adaptersFor, credentialPath, credentialPayloadFrom, deployTwin, deployWorld, loadWorldChecks, twinSigningSecret, materializeRoots, refreshPosture, refreshTwin, rootForControlRoot, rewrapTwinCredentials, sealTwinCredential, sealedCredentialInfo, bindLegacyCredentials, setTwinRoot, spanMs } from "./root.js";
49
- import { describeActionFields } from '@volter/world-core/runtime';
49
+ import { describeActionFields, resourceBlobFile } from '@volter/world-core/runtime';
50
50
  import { worldLedgers } from "./changeset.js";
51
51
  import { clockFile, downWorld, instanceDir, isWorldOrigin, listWorlds, readWorldBootRecord, statusWorld, upWorld } from "./runtime.js";
52
52
  import { assertChangesetRules } from "./schema.js";
@@ -1305,12 +1305,21 @@ export class WorldDoors {
1305
1305
  }
1306
1306
  catch { /* left as answered */ }
1307
1307
  }
1308
+ // A page a site answers links another site the World serves by its hostname (www.videogame.ai's link to
1309
+ // cyclotron.videogame.ai): it opens where the World shows that site, as a browser redirect does above.
1310
+ const at = request.method !== 'HEAD' ? this.host.sitesTemplate?.() : undefined;
1311
+ const linked = at ? await this.inWorldLinks(answer, out, url.origin, at, undefined, browser) : null;
1312
+ if (linked)
1313
+ return linked;
1308
1314
  return new Response(request.method === 'HEAD' ? null : answer.body, { status: answer.status, statusText: answer.statusText, headers: out });
1309
1315
  }
1310
1316
  // a person's browser is told in words (often the site is not there yet, or was not in a copy of the World as it was)
1311
1317
  if ((request.headers.get('accept') ?? '').includes('text/html')) {
1312
1318
  const shown = host.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
1313
- return new Response(`<!doctype html><meta charset="utf-8"><title>${shown}</title><body style="font:16px system-ui;margin:3em;color:#555"><p>Nothing is served at <b>${shown}</b> in this World.</p></body>`, { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } });
1319
+ // A link here from a page in the World (a post's URL) names a site the World does not hold: the person may open
1320
+ // the real one, by a click of their own (an answered redirect would make the World's origin an open redirect).
1321
+ const real = `https://${host}${path}${url.search}`.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
1322
+ return new Response(`<!doctype html><meta charset="utf-8"><title>${shown}</title><body style="font:16px system-ui;margin:3em;color:#555"><p>Nothing is served at <b>${shown}</b> in this World.</p><p><a href="${real}" rel="noopener noreferrer">Open ${shown} outside the World</a></p></body>`, { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } });
1314
1323
  }
1315
1324
  return Response.json({ error: `no twin of ${this.served} answers ${host}` }, { status: 404 });
1316
1325
  }
@@ -1566,8 +1575,32 @@ export class WorldDoors {
1566
1575
  if (!/^\s*(?:__Host-)?volter_(?:world|pages|browser)=/i.test(c))
1567
1576
  out.append('set-cookie', c);
1568
1577
  }
1578
+ // a twin's page links a site this World serves (a post's URL, a Worker's custom domain) by its hostname: the
1579
+ // person who follows it stays in the World
1580
+ if (sitesAt && request.method === 'GET' && answer.status < 300) {
1581
+ const linked = await this.inWorldLinks(answer, out, url.origin, sitesAt, vendor);
1582
+ if (linked)
1583
+ return linked;
1584
+ }
1569
1585
  return new Response(answer.body, { status: answer.status, headers: out });
1570
1586
  }
1587
+ /** A page's links to sites this World serves (the hostnames its twins claim, a Worker's custom domain among them),
1588
+ * rewritten to where the World shows each (siteUrlOf), so a person who follows one stays in the World. The page's own
1589
+ * vendor's hosts (`own`) are its own to place, and a site the World does not hold keeps its real address. Null
1590
+ * when there is nothing to do: not HTML, compressed, or no site served. */
1591
+ async inWorldLinks(answer, out, origin, template, own, browser) {
1592
+ if (!answer.body || !(out.get('content-type') ?? '').includes('text/html') || out.has('content-encoding'))
1593
+ return null;
1594
+ const served = new Set((await this.claimedHosts(origin)).filter(row => row.vendor !== own).flatMap(row => row.hosts.map(h => h.toLowerCase())));
1595
+ if (served.size === 0)
1596
+ return null;
1597
+ const html = (await answer.text()).replace(/(href|action)="https?:\/\/([A-Za-z0-9.-]+)(?::\d+)?((?:[/?#][^"]*)?)"/g, (match, attr, to, rest) => {
1598
+ const next = served.has(to.toLowerCase()) ? siteUrlOf(template, to.toLowerCase(), browser) : null;
1599
+ return next ? `${attr}="${next}${rest || '/'}"` : match;
1600
+ });
1601
+ out.delete('content-length');
1602
+ return new Response(html, { status: answer.status, statusText: answer.statusText, headers: out });
1603
+ }
1571
1604
  linksPath() { return join(this.worldRoot, stateDirName(), 'links.json'); }
1572
1605
  /** The World's links: name → the vendor origin it forwards to. */
1573
1606
  links() {
@@ -2266,7 +2299,7 @@ export class WorldDoors {
2266
2299
  return Response.json({ error: 'a digest in a blob key is lowercase hex' }, { status: 400 });
2267
2300
  if (!this.up().services[name])
2268
2301
  return Response.json({ error: `no twin "${name}" in ${this.served}` }, { status: 404 });
2269
- const path = join(worldPaths(this.stateOf(name), this.controlRoot(name)).resources, key);
2302
+ const path = resourceBlobFile(worldPaths(this.stateOf(name), this.controlRoot(name)).resources, key);
2270
2303
  const store = getActiveBlobStore();
2271
2304
  // HEAD names the blob's own digest, so a push can tell the same key holding other bytes from the same blob
2272
2305
  if (request.method === 'HEAD') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "3.0.156",
3
+ "version": "3.0.157",
4
4
  "description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
5
5
  "keywords": [
6
6
  "twin",
@@ -70,14 +70,14 @@
70
70
  "node": ">=22.3"
71
71
  },
72
72
  "peerDependencies": {
73
- "@volter/world-core": "3.0.148",
73
+ "@volter/world-core": "3.0.149",
74
74
  "@volter/world-console": "3.0.149"
75
75
  },
76
76
  "dependencies": {
77
77
  "@electric-sql/pglite": "0.5.8",
78
78
  "@electric-sql/pglite-pgvector": "0.0.9",
79
79
  "@volter/world-access": "3.0.1",
80
- "@volter/world-core": "3.0.148",
80
+ "@volter/world-core": "3.0.149",
81
81
  "mongodb": "~6.20.0",
82
82
  "pg-gateway": "0.3.0-beta.4",
83
83
  "smol-toml": "^1.8.0",
package/src/changeset.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { errorMessage } from './error-message.ts';
2
2
  import { worldChangesetRules } from './configs.ts';
3
- import { blobDigest, captureHistory, captureParentHistory, getActiveBlobStore, getActiveWorldStore, originHead, withHistoryLocks, worldPaths, type HistoryReference } from '@volter/world-core/runtime';
3
+ import { blobDigest, captureHistory, captureParentHistory, getActiveBlobStore, getActiveWorldStore, originHead, resourceBlobFile, resourceBlobKey, withHistoryLocks, worldPaths, type HistoryReference } from '@volter/world-core/runtime';
4
4
  // `volter-world mark` / `diff` / `changeset` — the WORLD-side verbs over the changeset
5
5
  // primitive (docs/concepts/the-model.md v0). Thin, exactly like `covers` and `tail` are thin: all
6
6
  // the state logic (markers, deltas, content hashing, replay) lives in @volter/world-core's
@@ -24,7 +24,7 @@ import { blobDigest, captureHistory, captureParentHistory, getActiveBlobStore, g
24
24
  // Read-only over the twins' data at rest, like `tail` — except `replay`, which is the one verb
25
25
  // here that WRITES, and it writes only through the control plane's kernel write path.
26
26
  import { tmpdir } from 'node:os';
27
- import { join, relative, resolve } from 'node:path';
27
+ import { join, resolve } from 'node:path';
28
28
  import {
29
29
  approveChangeset,
30
30
  rebaseChangeset,
@@ -524,7 +524,7 @@ export async function pushWorldChangeset(name: string, options: PushWorldOptions
524
524
  const twins = [...new Map(located.changeset.actions.map((a) => [a.service, a.stateService ?? a.service])).entries()];
525
525
  const ownBlobs = async (twin: string, state: string): Promise<Array<{ key: string; path: string }>> => {
526
526
  const dir = worldPaths(state, join(instance.dirs.data, twin)).resources;
527
- return (await blobs.list(dir)).map((path) => ({ key: relative(dir, path).split('\\').join('/'), path }));
527
+ return (await blobs.list(dir)).map((path) => ({ key: resourceBlobKey(dir, path), path }));
528
528
  };
529
529
  // content-addressed means a segment of the key IS the digest of the bytes it holds: such bytes never
530
530
  // change. A name built on a digest of something else (a record keyed by a hash of an action id, a
@@ -548,10 +548,10 @@ export async function pushWorldChangeset(name: string, options: PushWorldOptions
548
548
  const there = worldPaths(state, join(target.instance.dirs.data, twin)).resources;
549
549
  for (const { key, path } of await ownBlobs(twin, state)) {
550
550
  if (!(await wanted(key, path))) continue;
551
- const held = await blobs.get(join(there, key));
551
+ const held = await blobs.get(resourceBlobFile(there, key));
552
552
  if (held !== null && blobDigest(held) === (await digestOf(path))) continue;
553
553
  const bytes = await blobs.get(path);
554
- if (bytes) await blobs.put(join(there, key), bytes);
554
+ if (bytes) await blobs.put(resourceBlobFile(there, key), bytes);
555
555
  }
556
556
  }
557
557
  return;
@@ -49,7 +49,7 @@ import { fetchFromOrigin } from './origin.ts';
49
49
  import { CONSOLE_BASE, consoleRedirect, serveConsoleApart, type ConsoleMount } from './console-apart.ts';
50
50
  import { loadWorldConfig, worldChangesetRules, writeWorldConfig } from './configs.ts';
51
51
  import { adaptersFor, credentialPath, credentialPayloadFrom, deployTwin, deployWorld, loadWorldChecks, twinSigningSecret, materializeRoots, refreshPosture, refreshTwin, rootForControlRoot, rewrapTwinCredentials, sealTwinCredential, sealedCredentialInfo, bindLegacyCredentials, setTwinRoot, spanMs } from './root.ts';
52
- import { describeActionFields } from '@volter/world-core/runtime';
52
+ import { describeActionFields, resourceBlobFile } from '@volter/world-core/runtime';
53
53
  import { worldLedgers } from './changeset.ts';
54
54
  import { clockFile, downWorld, instanceDir, isWorldOrigin, listWorlds, readWorldBootRecord, statusWorld, upWorld } from './runtime.ts';
55
55
  import { assertChangesetRules, type WorldChangesetRules, type WorldInstance } from './schema.ts';
@@ -1155,12 +1155,20 @@ export class WorldDoors {
1155
1155
  else if (to.hostname === host) out.set('location', `${to.pathname}${to.search}${to.hash}`);
1156
1156
  else if (browser && routes && resolveTwinHost(routes.rules, to.hostname, routes.map, to.pathname, routes.claimed) && routes.sites.some(row => row.hosts.includes(to.hostname))) { const at = this.host.sitesTemplate?.(); const next = at ? siteUrlOf(at, to.hostname, browser) : null; if (next) out.set('location', `${next}${to.pathname}${to.search}${to.hash}`); }
1157
1157
  } catch { /* left as answered */ } }
1158
+ // A page a site answers links another site the World serves by its hostname (www.videogame.ai's link to
1159
+ // cyclotron.videogame.ai): it opens where the World shows that site, as a browser redirect does above.
1160
+ const at = request.method !== 'HEAD' ? this.host.sitesTemplate?.() : undefined;
1161
+ const linked = at ? await this.inWorldLinks(answer, out, url.origin, at, undefined, browser) : null;
1162
+ if (linked) return linked;
1158
1163
  return new Response(request.method === 'HEAD' ? null : answer.body, { status: answer.status, statusText: answer.statusText, headers: out });
1159
1164
  }
1160
1165
  // a person's browser is told in words (often the site is not there yet, or was not in a copy of the World as it was)
1161
1166
  if ((request.headers.get('accept') ?? '').includes('text/html')) {
1162
1167
  const shown = host.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
1163
- return new Response(`<!doctype html><meta charset="utf-8"><title>${shown}</title><body style="font:16px system-ui;margin:3em;color:#555"><p>Nothing is served at <b>${shown}</b> in this World.</p></body>`, { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } });
1168
+ // A link here from a page in the World (a post's URL) names a site the World does not hold: the person may open
1169
+ // the real one, by a click of their own (an answered redirect would make the World's origin an open redirect).
1170
+ const real = `https://${host}${path}${url.search}`.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
1171
+ return new Response(`<!doctype html><meta charset="utf-8"><title>${shown}</title><body style="font:16px system-ui;margin:3em;color:#555"><p>Nothing is served at <b>${shown}</b> in this World.</p><p><a href="${real}" rel="noopener noreferrer">Open ${shown} outside the World</a></p></body>`, { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } });
1164
1172
  }
1165
1173
  return Response.json({ error: `no twin of ${this.served} answers ${host}` }, { status: 404 });
1166
1174
  }
@@ -1369,9 +1377,31 @@ export class WorldDoors {
1369
1377
  out.delete('set-cookie');
1370
1378
  for (const c of setCookies) if (!/^\s*(?:__Host-)?volter_(?:world|pages|browser)=/i.test(c)) out.append('set-cookie', c);
1371
1379
  }
1380
+ // a twin's page links a site this World serves (a post's URL, a Worker's custom domain) by its hostname: the
1381
+ // person who follows it stays in the World
1382
+ if (sitesAt && request.method === 'GET' && answer.status < 300) {
1383
+ const linked = await this.inWorldLinks(answer, out, url.origin, sitesAt, vendor);
1384
+ if (linked) return linked;
1385
+ }
1372
1386
  return new Response(answer.body, { status: answer.status, headers: out });
1373
1387
  }
1374
1388
 
1389
+ /** A page's links to sites this World serves (the hostnames its twins claim, a Worker's custom domain among them),
1390
+ * rewritten to where the World shows each (siteUrlOf), so a person who follows one stays in the World. The page's own
1391
+ * vendor's hosts (`own`) are its own to place, and a site the World does not hold keeps its real address. Null
1392
+ * when there is nothing to do: not HTML, compressed, or no site served. */
1393
+ private async inWorldLinks(answer: Response, out: Headers, origin: string, template: string, own: string | undefined, browser?: string): Promise<Response | null> {
1394
+ if (!answer.body || !(out.get('content-type') ?? '').includes('text/html') || out.has('content-encoding')) return null;
1395
+ const served = new Set((await this.claimedHosts(origin)).filter(row => row.vendor !== own).flatMap(row => row.hosts.map(h => h.toLowerCase())));
1396
+ if (served.size === 0) return null;
1397
+ const html = (await answer.text()).replace(/(href|action)="https?:\/\/([A-Za-z0-9.-]+)(?::\d+)?((?:[/?#][^"]*)?)"/g, (match, attr: string, to: string, rest: string) => {
1398
+ const next = served.has(to.toLowerCase()) ? siteUrlOf(template, to.toLowerCase(), browser) : null;
1399
+ return next ? `${attr}="${next}${rest || '/'}"` : match;
1400
+ });
1401
+ out.delete('content-length');
1402
+ return new Response(html, { status: answer.status, statusText: answer.statusText, headers: out });
1403
+ }
1404
+
1375
1405
  private linksPath(): string { return join(this.worldRoot, stateDirName(), 'links.json'); }
1376
1406
  /** The World's links: name → the vendor origin it forwards to. */
1377
1407
  links(): Record<string, { origin: string }> {
@@ -1906,7 +1936,7 @@ export class WorldDoors {
1906
1936
  // a digest-shaped segment is lowercase, so a case-insensitive filesystem can't take unchecked bytes for it
1907
1937
  if (key.split('/').some((seg) => /^[0-9a-fA-F]{64}$/.test(seg) && seg !== seg.toLowerCase())) return Response.json({ error: 'a digest in a blob key is lowercase hex' }, { status: 400 });
1908
1938
  if (!this.up().services[name]) return Response.json({ error: `no twin "${name}" in ${this.served}` }, { status: 404 });
1909
- const path = join(worldPaths(this.stateOf(name), this.controlRoot(name)).resources, key);
1939
+ const path = resourceBlobFile(worldPaths(this.stateOf(name), this.controlRoot(name)).resources, key);
1910
1940
  const store = getActiveBlobStore();
1911
1941
  // HEAD names the blob's own digest, so a push can tell the same key holding other bytes from the same blob
1912
1942
  if (request.method === 'HEAD') {