@volter/world-runtime 3.0.33 → 3.0.35

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.
@@ -30,7 +30,7 @@ import { boardFrameKey, getActiveWorldStore, withHistoryLocks, captureHistory, h
30
30
  // to `.volter/token` and `.volter/token.read` (0600).
31
31
  // One writer per world by construction: the doors run in this process, serialized per twin.
32
32
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
33
- import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, siteUrlOf, ORIGINAL_PATH_HEADER, validateRemoteOrigin } from '@volter/world-core/runtime';
33
+ import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, TWIN_SITE_HEADER, siteUrlOf, ORIGINAL_PATH_HEADER, validateRemoteOrigin } from '@volter/world-core/runtime';
34
34
  import { resolveTwinHost } from '@volter/world-core/host-routing';
35
35
  import { assertBrowserName, validateBrowserState, cookieMatchesHost } from '@volter/world-core';
36
36
  import { worldBrowser, worldBrowsers, putWorldBrowser, deleteWorldBrowser, materializeBrowserFiles, browserLoad, putBrowserLoad, browserLoadId } from "./browsers.js";
@@ -659,6 +659,9 @@ export class WorldDoors {
659
659
  }
660
660
  const answer = await this.site(request, site, browser, held);
661
661
  const out = new Headers(answer.headers);
662
+ // A document belongs to this live page session and browser state, not an earlier opening of its URL.
663
+ if (out.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() === 'text/html')
664
+ out.set('cache-control', 'private, no-store');
662
665
  out.append('content-security-policy', `frame-ancestors 'self'${held.framer ? ` ${held.framer}` : ''}`);
663
666
  this.keepCookiesHere(answer, out);
664
667
  return new Response(answer.body, { status: answer.status, statusText: answer.statusText, headers: out });
@@ -1095,8 +1098,8 @@ export class WorldDoors {
1095
1098
  ...(packFacts()[vendor]?.hosts ?? []).flatMap(rule => typeof rule.host === 'string' && !rule.exclude ? [rule.host] : []),
1096
1099
  ])] }));
1097
1100
  }
1098
- async browserRoutes(origin) {
1099
- const listed = await this.claimedHosts(origin);
1101
+ async browserRoutes(origin, hosts) {
1102
+ const listed = hosts ?? await this.claimedHosts(origin);
1100
1103
  const facts = packFacts();
1101
1104
  const map = Object.fromEntries(Object.keys(this.up().services).map(vendor => [vendor, vendor]));
1102
1105
  const claimed = {};
@@ -1153,7 +1156,7 @@ export class WorldDoors {
1153
1156
  }
1154
1157
  putBrowserLoad(this.up().dirs.data, loadId, { name, site, version: browser.version, cookies: cookies.filter(c => !protectedCookie(c.name)).map(c => ({ name: c.name, path: c.path })) });
1155
1158
  headers.append('set-cookie', `${marker}=${loadId}; Path=/; HttpOnly; Secure; SameSite=None; Partitioned`);
1156
- const script = `localStorage.clear(); for (const entry of ${JSON.stringify(storage).replace(/</g, '\\u003c')}) localStorage.setItem(entry.name, entry.value); location.replace(location.href);`;
1159
+ const script = `localStorage.clear(); for (const entry of ${JSON.stringify(storage).replace(/</g, '\\u003c')}) localStorage.setItem(entry.name, entry.value); location.reload();`;
1157
1160
  const hash = createHash('sha256').update(script).digest('base64');
1158
1161
  const framer = this.pageSession(request, url, site, name)?.framer;
1159
1162
  headers.set('content-security-policy', `default-src 'none'; script-src 'sha256-${hash}'; base-uri 'none'; frame-ancestors 'self'${framer ? ` ${framer}` : ''}`);
@@ -1196,17 +1199,32 @@ export class WorldDoors {
1196
1199
  if (browser && !grant)
1197
1200
  return Response.json({ error: "open this browser site from the World's board" }, { status: 401 });
1198
1201
  const url = new URL(request.url);
1199
- const routes = browser ? await this.browserRoutes(url.origin) : null;
1202
+ const pageHost = host;
1203
+ // This prefix belongs only to a hosted site's session, never to a bare twin or the content plane.
1204
+ const resource = grant && this.siteShaped(url) && url.pathname.startsWith('/_host/');
1205
+ const listed = await this.claimedHosts(url.origin);
1206
+ const routes = browser || resource ? await this.browserRoutes(url.origin, listed) : null;
1207
+ let path = url.pathname;
1200
1208
  let vendor;
1201
- if (routes)
1202
- vendor = resolveTwinHost(routes.rules, host, routes.map, url.pathname, routes.claimed)?.origin;
1209
+ if (browser && routes)
1210
+ vendor = resolveTwinHost(routes.rules, host, routes.map, resource ? '/' : path, routes.claimed)?.origin;
1203
1211
  else
1204
- for (const row of await this.claimedHosts(url.origin)) {
1212
+ for (const row of listed) {
1205
1213
  if (!this.host.twinFetch(row.vendor) || !row.hosts.includes(host))
1206
1214
  continue;
1207
1215
  vendor = row.vendor;
1208
1216
  break;
1209
1217
  }
1218
+ if (resource && routes) {
1219
+ const target = /^\/_host\/([a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)(\/.*)?$/.exec(path);
1220
+ const targetHost = target?.[1];
1221
+ const targetPath = target?.[2] ?? '/';
1222
+ const targetVendor = targetHost ? resolveTwinHost(routes.rules, targetHost, routes.map, targetPath, routes.claimed)?.origin : undefined;
1223
+ if (!vendor || targetVendor !== vendor)
1224
+ return Response.json({ error: 'this site serves only its own vendor hosts' }, { status: 404 });
1225
+ host = targetHost;
1226
+ path = targetPath;
1227
+ }
1210
1228
  const twin = vendor ? this.host.twinFetch(vendor) : undefined;
1211
1229
  if (vendor && twin) {
1212
1230
  const reads = request.method === 'GET' || request.method === 'HEAD';
@@ -1214,7 +1232,7 @@ export class WorldDoors {
1214
1232
  if (browser && grant?.scope === 'read' && !reads && !enforced)
1215
1233
  return Response.json({ error: 'the read token opens only reads' }, { status: 403 });
1216
1234
  const headers = browser ? new Headers(request.headers) : new Headers();
1217
- for (const key of [TOKEN_HEADER, TWINS_KEY_HEADER, CALLER_HEADER, ORIGINAL_PATH_HEADER, READ_ONLY_HEADER, 'host', TWIN_PREFIX_HEADER])
1235
+ for (const key of [TOKEN_HEADER, TWINS_KEY_HEADER, CALLER_HEADER, ORIGINAL_PATH_HEADER, READ_ONLY_HEADER, 'host', TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, TWIN_SITE_HEADER])
1218
1236
  headers.delete(key);
1219
1237
  for (const name of ['accept', 'accept-language', 'if-none-match', 'range', 'user-agent']) {
1220
1238
  const v = request.headers.get(name);
@@ -1226,9 +1244,12 @@ export class WorldDoors {
1226
1244
  if (cookies.length)
1227
1245
  headers.set('cookie', cookies.join('; '));
1228
1246
  headers.set('x-volter-world-browser', browser);
1247
+ }
1248
+ if (grant && this.siteShaped(url)) {
1229
1249
  const template = this.host.sitesTemplate?.();
1230
1250
  if (template)
1231
1251
  headers.set(TWIN_SITES_HEADER, template);
1252
+ headers.set(TWIN_SITE_HEADER, pageHost);
1232
1253
  headers.set('x-forwarded-host', url.host);
1233
1254
  headers.set('x-forwarded-proto', url.protocol.slice(0, -1));
1234
1255
  }
@@ -1241,14 +1262,17 @@ export class WorldDoors {
1241
1262
  headers.set('traceparent', traceparentForDelivery(request.headers.get('traceparent')) ?? newTraceparent());
1242
1263
  }
1243
1264
  const body = reads ? undefined : await request.arrayBuffer();
1244
- const answer = await twin(new Request(`${url.origin}${url.pathname}${url.search}`, { method: request.method, headers, ...(body === undefined ? {} : { body }), redirect: 'manual' }));
1265
+ const answer = await twin(new Request(`${url.origin}${path}${url.search}`, { method: request.method, headers, ...(body === undefined ? {} : { body }), redirect: 'manual' }));
1245
1266
  // a redirect the site answers names its own hostname: in the World it stays under the World's origin
1246
1267
  const out = new Headers(answer.headers);
1247
1268
  const location = out.get('location');
1248
1269
  if (location) {
1249
1270
  try {
1250
- const to = new URL(location, `https://${host}`);
1251
- if (to.hostname === host)
1271
+ const to = new URL(location, `https://${host}${path}${url.search}`);
1272
+ const sameVendor = resource && routes && resolveTwinHost(routes.rules, to.hostname, routes.map, to.pathname, routes.claimed)?.origin === vendor;
1273
+ if (sameVendor)
1274
+ out.set('location', `${to.hostname === pageHost ? '' : `/_host/${to.hostname}`}${to.pathname}${to.search}${to.hash}`);
1275
+ else if (to.hostname === host)
1252
1276
  out.set('location', `${to.pathname}${to.search}${to.hash}`);
1253
1277
  else if (browser && routes && resolveTwinHost(routes.rules, to.hostname, routes.map, to.pathname, routes.claimed) && routes.sites.some(row => row.hosts.includes(to.hostname))) {
1254
1278
  const at = this.host.sitesTemplate?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "3.0.33",
3
+ "version": "3.0.35",
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",
@@ -68,14 +68,14 @@
68
68
  "node": ">=22.3"
69
69
  },
70
70
  "peerDependencies": {
71
- "@volter/world-core": "3.0.33",
72
- "@volter/world-console": "3.0.28"
71
+ "@volter/world-core": "3.0.35",
72
+ "@volter/world-console": "3.0.30"
73
73
  },
74
74
  "dependencies": {
75
75
  "@electric-sql/pglite": "0.5.8",
76
76
  "@electric-sql/pglite-pgvector": "0.0.9",
77
77
  "@volter/world-access": "3.0.1",
78
- "@volter/world-core": "3.0.33",
78
+ "@volter/world-core": "3.0.35",
79
79
  "pg-gateway": "0.3.0-beta.4",
80
80
  "smol-toml": "^1.8.0",
81
81
  "yaml": "2.9.0"
@@ -30,7 +30,7 @@ import { boardFrameKey, getActiveWorldStore, withHistoryLocks, captureHistory, h
30
30
  // to `.volter/token` and `.volter/token.read` (0600).
31
31
  // One writer per world by construction: the doors run in this process, serialized per twin.
32
32
  import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync , rmSync} from 'node:fs';
33
- import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, type TwinRequestJournalCursor, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, siteLabel, siteUrlOf, ORIGINAL_PATH_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink, type BoardFrame } from '@volter/world-core/runtime';
33
+ import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, type TwinRequestJournalCursor, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, TWIN_SITE_HEADER, siteLabel, siteUrlOf, ORIGINAL_PATH_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink, type BoardFrame } from '@volter/world-core/runtime';
34
34
  import { resolveTwinHost, type HostPredicate } from '@volter/world-core/host-routing';
35
35
  import { assertBrowserName, validateBrowserState, cookieMatchesHost } from '@volter/world-core';
36
36
  import { worldBrowser, worldBrowsers, putWorldBrowser, deleteWorldBrowser, materializeBrowserFiles, browserLoad, putBrowserLoad, browserLoadId } from './browsers.ts';
@@ -645,6 +645,8 @@ export class WorldDoors {
645
645
  }
646
646
  const answer = await this.site(request, site, browser, held);
647
647
  const out = new Headers(answer.headers);
648
+ // A document belongs to this live page session and browser state, not an earlier opening of its URL.
649
+ if (out.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() === 'text/html') out.set('cache-control', 'private, no-store');
648
650
  out.append('content-security-policy', `frame-ancestors 'self'${held.framer ? ` ${held.framer}` : ''}`);
649
651
  this.keepCookiesHere(answer, out);
650
652
  return new Response(answer.body, { status: answer.status, statusText: answer.statusText, headers: out });
@@ -997,8 +999,8 @@ export class WorldDoors {
997
999
  ])] }));
998
1000
  }
999
1001
 
1000
- private async browserRoutes(origin: string): Promise<{ sites: Array<{ vendor: string; hosts: string[] }>; rules: Record<string, HostPredicate>; map: Record<string, string>; claimed: Record<string, Set<string>> }> {
1001
- const listed = await this.claimedHosts(origin);
1002
+ private async browserRoutes(origin: string, hosts?: Array<{ vendor: string; hosts: string[] }>): Promise<{ sites: Array<{ vendor: string; hosts: string[] }>; rules: Record<string, HostPredicate>; map: Record<string, string>; claimed: Record<string, Set<string>> }> {
1003
+ const listed = hosts ?? await this.claimedHosts(origin);
1002
1004
  const facts = packFacts();
1003
1005
  const map: Record<string, string> = Object.fromEntries(Object.keys(this.up().services).map(vendor => [vendor, vendor]));
1004
1006
  const claimed: Record<string, Set<string>> = {};
@@ -1043,7 +1045,7 @@ export class WorldDoors {
1043
1045
  }
1044
1046
  putBrowserLoad(this.up().dirs.data, loadId, { name, site, version: browser.version, cookies: cookies.filter(c => !protectedCookie(c.name)).map(c => ({ name: c.name, path: c.path })) });
1045
1047
  headers.append('set-cookie', `${marker}=${loadId}; Path=/; HttpOnly; Secure; SameSite=None; Partitioned`);
1046
- const script = `localStorage.clear(); for (const entry of ${JSON.stringify(storage).replace(/</g, '\\u003c')}) localStorage.setItem(entry.name, entry.value); location.replace(location.href);`;
1048
+ const script = `localStorage.clear(); for (const entry of ${JSON.stringify(storage).replace(/</g, '\\u003c')}) localStorage.setItem(entry.name, entry.value); location.reload();`;
1047
1049
  const hash = createHash('sha256').update(script).digest('base64');
1048
1050
  const framer = this.pageSession(request, url, site, name)?.framer;
1049
1051
  headers.set('content-security-policy', `default-src 'none'; script-src 'sha256-${hash}'; base-uri 'none'; frame-ancestors 'self'${framer ? ` ${framer}` : ''}`);
@@ -1078,28 +1080,45 @@ export class WorldDoors {
1078
1080
  if (!browser && request.method !== 'GET' && request.method !== 'HEAD') return Response.json({ error: 'a site in a World is read: GET and HEAD' }, { status: 405 });
1079
1081
  if (browser && !grant) return Response.json({ error: "open this browser site from the World's board" }, { status: 401 });
1080
1082
  const url = new URL(request.url);
1081
- const routes = browser ? await this.browserRoutes(url.origin) : null;
1083
+ const pageHost = host;
1084
+ // This prefix belongs only to a hosted site's session, never to a bare twin or the content plane.
1085
+ const resource = grant && this.siteShaped(url) && url.pathname.startsWith('/_host/');
1086
+ const listed = await this.claimedHosts(url.origin);
1087
+ const routes = browser || resource ? await this.browserRoutes(url.origin, listed) : null;
1088
+ let path = url.pathname;
1082
1089
  let vendor: string | undefined;
1083
- if (routes) vendor = resolveTwinHost(routes.rules, host, routes.map, url.pathname, routes.claimed)?.origin;
1084
- else for (const row of await this.claimedHosts(url.origin)) {
1090
+ if (browser && routes) vendor = resolveTwinHost(routes.rules, host, routes.map, resource ? '/' : path, routes.claimed)?.origin;
1091
+ else for (const row of listed) {
1085
1092
  if (!this.host.twinFetch(row.vendor) || !row.hosts.includes(host)) continue;
1086
1093
  vendor = row.vendor;
1087
1094
  break;
1088
1095
  }
1096
+ if (resource && routes) {
1097
+ const target = /^\/_host\/([a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)(\/.*)?$/.exec(path);
1098
+ const targetHost = target?.[1];
1099
+ const targetPath = target?.[2] ?? '/';
1100
+ const targetVendor = targetHost ? resolveTwinHost(routes.rules, targetHost, routes.map, targetPath, routes.claimed)?.origin : undefined;
1101
+ if (!vendor || targetVendor !== vendor) return Response.json({ error: 'this site serves only its own vendor hosts' }, { status: 404 });
1102
+ host = targetHost!;
1103
+ path = targetPath;
1104
+ }
1089
1105
  const twin = vendor ? this.host.twinFetch(vendor) : undefined;
1090
1106
  if (vendor && twin) {
1091
1107
  const reads = request.method === 'GET' || request.method === 'HEAD';
1092
1108
  const enforced = browser && grant?.scope === 'read' && await this.enforcesRead(vendor, twin, url.origin);
1093
1109
  if (browser && grant?.scope === 'read' && !reads && !enforced) return Response.json({ error: 'the read token opens only reads' }, { status: 403 });
1094
1110
  const headers = browser ? new Headers(request.headers) : new Headers();
1095
- for (const key of [TOKEN_HEADER, TWINS_KEY_HEADER, CALLER_HEADER, ORIGINAL_PATH_HEADER, READ_ONLY_HEADER, 'host', TWIN_PREFIX_HEADER]) headers.delete(key);
1111
+ for (const key of [TOKEN_HEADER, TWINS_KEY_HEADER, CALLER_HEADER, ORIGINAL_PATH_HEADER, READ_ONLY_HEADER, 'host', TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, TWIN_SITE_HEADER]) headers.delete(key);
1096
1112
  for (const name of ['accept', 'accept-language', 'if-none-match', 'range', 'user-agent']) { const v = request.headers.get(name); if (v !== null) headers.set(name, v); }
1097
1113
  if (browser) {
1098
1114
  const cookies = (request.headers.get('cookie') ?? '').split(/;\s*/).filter(c => !/^(?:__Host-)?volter_(?:world|pages|browser)=/.test(c));
1099
1115
  if (cookies.length) headers.set('cookie', cookies.join('; '));
1100
1116
  headers.set('x-volter-world-browser', browser);
1117
+ }
1118
+ if (grant && this.siteShaped(url)) {
1101
1119
  const template = this.host.sitesTemplate?.();
1102
1120
  if (template) headers.set(TWIN_SITES_HEADER, template);
1121
+ headers.set(TWIN_SITE_HEADER, pageHost);
1103
1122
  headers.set('x-forwarded-host', url.host);
1104
1123
  headers.set('x-forwarded-proto', url.protocol.slice(0, -1));
1105
1124
  }
@@ -1110,11 +1129,17 @@ export class WorldDoors {
1110
1129
  headers.set('traceparent', traceparentForDelivery(request.headers.get('traceparent')) ?? newTraceparent());
1111
1130
  }
1112
1131
  const body = reads ? undefined : await request.arrayBuffer();
1113
- const answer = await twin(new Request(`${url.origin}${url.pathname}${url.search}`, { method: request.method, headers, ...(body === undefined ? {} : { body }), redirect: 'manual' }));
1132
+ const answer = await twin(new Request(`${url.origin}${path}${url.search}`, { method: request.method, headers, ...(body === undefined ? {} : { body }), redirect: 'manual' }));
1114
1133
  // a redirect the site answers names its own hostname: in the World it stays under the World's origin
1115
1134
  const out = new Headers(answer.headers);
1116
1135
  const location = out.get('location');
1117
- if (location) { try { const to = new URL(location, `https://${host}`); if (to.hostname === host) out.set('location', `${to.pathname}${to.search}${to.hash}`); 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}`); } } catch { /* left as answered */ } }
1136
+ if (location) { try {
1137
+ const to = new URL(location, `https://${host}${path}${url.search}`);
1138
+ const sameVendor = resource && routes && resolveTwinHost(routes.rules, to.hostname, routes.map, to.pathname, routes.claimed)?.origin === vendor;
1139
+ if (sameVendor) out.set('location', `${to.hostname === pageHost ? '' : `/_host/${to.hostname}`}${to.pathname}${to.search}${to.hash}`);
1140
+ else if (to.hostname === host) out.set('location', `${to.pathname}${to.search}${to.hash}`);
1141
+ 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}`); }
1142
+ } catch { /* left as answered */ } }
1118
1143
  return new Response(request.method === 'HEAD' ? null : answer.body, { status: answer.status, statusText: answer.statusText, headers: out });
1119
1144
  }
1120
1145
  // 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)