@volter/world-runtime 3.0.33 → 3.0.34

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";
@@ -1095,8 +1095,8 @@ export class WorldDoors {
1095
1095
  ...(packFacts()[vendor]?.hosts ?? []).flatMap(rule => typeof rule.host === 'string' && !rule.exclude ? [rule.host] : []),
1096
1096
  ])] }));
1097
1097
  }
1098
- async browserRoutes(origin) {
1099
- const listed = await this.claimedHosts(origin);
1098
+ async browserRoutes(origin, hosts) {
1099
+ const listed = hosts ?? await this.claimedHosts(origin);
1100
1100
  const facts = packFacts();
1101
1101
  const map = Object.fromEntries(Object.keys(this.up().services).map(vendor => [vendor, vendor]));
1102
1102
  const claimed = {};
@@ -1196,17 +1196,32 @@ export class WorldDoors {
1196
1196
  if (browser && !grant)
1197
1197
  return Response.json({ error: "open this browser site from the World's board" }, { status: 401 });
1198
1198
  const url = new URL(request.url);
1199
- const routes = browser ? await this.browserRoutes(url.origin) : null;
1199
+ const pageHost = host;
1200
+ // This prefix belongs only to a hosted site's session, never to a bare twin or the content plane.
1201
+ const resource = grant && this.siteShaped(url) && url.pathname.startsWith('/_host/');
1202
+ const listed = await this.claimedHosts(url.origin);
1203
+ const routes = browser || resource ? await this.browserRoutes(url.origin, listed) : null;
1204
+ let path = url.pathname;
1200
1205
  let vendor;
1201
- if (routes)
1202
- vendor = resolveTwinHost(routes.rules, host, routes.map, url.pathname, routes.claimed)?.origin;
1206
+ if (browser && routes)
1207
+ vendor = resolveTwinHost(routes.rules, host, routes.map, resource ? '/' : path, routes.claimed)?.origin;
1203
1208
  else
1204
- for (const row of await this.claimedHosts(url.origin)) {
1209
+ for (const row of listed) {
1205
1210
  if (!this.host.twinFetch(row.vendor) || !row.hosts.includes(host))
1206
1211
  continue;
1207
1212
  vendor = row.vendor;
1208
1213
  break;
1209
1214
  }
1215
+ if (resource && routes) {
1216
+ const target = /^\/_host\/([a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)(\/.*)?$/.exec(path);
1217
+ const targetHost = target?.[1];
1218
+ const targetPath = target?.[2] ?? '/';
1219
+ const targetVendor = targetHost ? resolveTwinHost(routes.rules, targetHost, routes.map, targetPath, routes.claimed)?.origin : undefined;
1220
+ if (!vendor || targetVendor !== vendor)
1221
+ return Response.json({ error: 'this site serves only its own vendor hosts' }, { status: 404 });
1222
+ host = targetHost;
1223
+ path = targetPath;
1224
+ }
1210
1225
  const twin = vendor ? this.host.twinFetch(vendor) : undefined;
1211
1226
  if (vendor && twin) {
1212
1227
  const reads = request.method === 'GET' || request.method === 'HEAD';
@@ -1214,7 +1229,7 @@ export class WorldDoors {
1214
1229
  if (browser && grant?.scope === 'read' && !reads && !enforced)
1215
1230
  return Response.json({ error: 'the read token opens only reads' }, { status: 403 });
1216
1231
  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])
1232
+ 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
1233
  headers.delete(key);
1219
1234
  for (const name of ['accept', 'accept-language', 'if-none-match', 'range', 'user-agent']) {
1220
1235
  const v = request.headers.get(name);
@@ -1226,9 +1241,12 @@ export class WorldDoors {
1226
1241
  if (cookies.length)
1227
1242
  headers.set('cookie', cookies.join('; '));
1228
1243
  headers.set('x-volter-world-browser', browser);
1244
+ }
1245
+ if (grant && this.siteShaped(url)) {
1229
1246
  const template = this.host.sitesTemplate?.();
1230
1247
  if (template)
1231
1248
  headers.set(TWIN_SITES_HEADER, template);
1249
+ headers.set(TWIN_SITE_HEADER, pageHost);
1232
1250
  headers.set('x-forwarded-host', url.host);
1233
1251
  headers.set('x-forwarded-proto', url.protocol.slice(0, -1));
1234
1252
  }
@@ -1241,14 +1259,17 @@ export class WorldDoors {
1241
1259
  headers.set('traceparent', traceparentForDelivery(request.headers.get('traceparent')) ?? newTraceparent());
1242
1260
  }
1243
1261
  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' }));
1262
+ const answer = await twin(new Request(`${url.origin}${path}${url.search}`, { method: request.method, headers, ...(body === undefined ? {} : { body }), redirect: 'manual' }));
1245
1263
  // a redirect the site answers names its own hostname: in the World it stays under the World's origin
1246
1264
  const out = new Headers(answer.headers);
1247
1265
  const location = out.get('location');
1248
1266
  if (location) {
1249
1267
  try {
1250
- const to = new URL(location, `https://${host}`);
1251
- if (to.hostname === host)
1268
+ const to = new URL(location, `https://${host}${path}${url.search}`);
1269
+ const sameVendor = resource && routes && resolveTwinHost(routes.rules, to.hostname, routes.map, to.pathname, routes.claimed)?.origin === vendor;
1270
+ if (sameVendor)
1271
+ out.set('location', `${to.hostname === pageHost ? '' : `/_host/${to.hostname}`}${to.pathname}${to.search}${to.hash}`);
1272
+ else if (to.hostname === host)
1252
1273
  out.set('location', `${to.pathname}${to.search}${to.hash}`);
1253
1274
  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
1275
  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.34",
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.34",
72
+ "@volter/world-console": "3.0.29"
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.34",
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';
@@ -997,8 +997,8 @@ export class WorldDoors {
997
997
  ])] }));
998
998
  }
999
999
 
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);
1000
+ 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>> }> {
1001
+ const listed = hosts ?? await this.claimedHosts(origin);
1002
1002
  const facts = packFacts();
1003
1003
  const map: Record<string, string> = Object.fromEntries(Object.keys(this.up().services).map(vendor => [vendor, vendor]));
1004
1004
  const claimed: Record<string, Set<string>> = {};
@@ -1078,28 +1078,45 @@ export class WorldDoors {
1078
1078
  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
1079
  if (browser && !grant) return Response.json({ error: "open this browser site from the World's board" }, { status: 401 });
1080
1080
  const url = new URL(request.url);
1081
- const routes = browser ? await this.browserRoutes(url.origin) : null;
1081
+ const pageHost = host;
1082
+ // This prefix belongs only to a hosted site's session, never to a bare twin or the content plane.
1083
+ const resource = grant && this.siteShaped(url) && url.pathname.startsWith('/_host/');
1084
+ const listed = await this.claimedHosts(url.origin);
1085
+ const routes = browser || resource ? await this.browserRoutes(url.origin, listed) : null;
1086
+ let path = url.pathname;
1082
1087
  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)) {
1088
+ if (browser && routes) vendor = resolveTwinHost(routes.rules, host, routes.map, resource ? '/' : path, routes.claimed)?.origin;
1089
+ else for (const row of listed) {
1085
1090
  if (!this.host.twinFetch(row.vendor) || !row.hosts.includes(host)) continue;
1086
1091
  vendor = row.vendor;
1087
1092
  break;
1088
1093
  }
1094
+ if (resource && routes) {
1095
+ const target = /^\/_host\/([a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)(\/.*)?$/.exec(path);
1096
+ const targetHost = target?.[1];
1097
+ const targetPath = target?.[2] ?? '/';
1098
+ const targetVendor = targetHost ? resolveTwinHost(routes.rules, targetHost, routes.map, targetPath, routes.claimed)?.origin : undefined;
1099
+ if (!vendor || targetVendor !== vendor) return Response.json({ error: 'this site serves only its own vendor hosts' }, { status: 404 });
1100
+ host = targetHost!;
1101
+ path = targetPath;
1102
+ }
1089
1103
  const twin = vendor ? this.host.twinFetch(vendor) : undefined;
1090
1104
  if (vendor && twin) {
1091
1105
  const reads = request.method === 'GET' || request.method === 'HEAD';
1092
1106
  const enforced = browser && grant?.scope === 'read' && await this.enforcesRead(vendor, twin, url.origin);
1093
1107
  if (browser && grant?.scope === 'read' && !reads && !enforced) return Response.json({ error: 'the read token opens only reads' }, { status: 403 });
1094
1108
  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);
1109
+ 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
1110
  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
1111
  if (browser) {
1098
1112
  const cookies = (request.headers.get('cookie') ?? '').split(/;\s*/).filter(c => !/^(?:__Host-)?volter_(?:world|pages|browser)=/.test(c));
1099
1113
  if (cookies.length) headers.set('cookie', cookies.join('; '));
1100
1114
  headers.set('x-volter-world-browser', browser);
1115
+ }
1116
+ if (grant && this.siteShaped(url)) {
1101
1117
  const template = this.host.sitesTemplate?.();
1102
1118
  if (template) headers.set(TWIN_SITES_HEADER, template);
1119
+ headers.set(TWIN_SITE_HEADER, pageHost);
1103
1120
  headers.set('x-forwarded-host', url.host);
1104
1121
  headers.set('x-forwarded-proto', url.protocol.slice(0, -1));
1105
1122
  }
@@ -1110,11 +1127,17 @@ export class WorldDoors {
1110
1127
  headers.set('traceparent', traceparentForDelivery(request.headers.get('traceparent')) ?? newTraceparent());
1111
1128
  }
1112
1129
  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' }));
1130
+ const answer = await twin(new Request(`${url.origin}${path}${url.search}`, { method: request.method, headers, ...(body === undefined ? {} : { body }), redirect: 'manual' }));
1114
1131
  // a redirect the site answers names its own hostname: in the World it stays under the World's origin
1115
1132
  const out = new Headers(answer.headers);
1116
1133
  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 */ } }
1134
+ if (location) { try {
1135
+ const to = new URL(location, `https://${host}${path}${url.search}`);
1136
+ const sameVendor = resource && routes && resolveTwinHost(routes.rules, to.hostname, routes.map, to.pathname, routes.claimed)?.origin === vendor;
1137
+ if (sameVendor) out.set('location', `${to.hostname === pageHost ? '' : `/_host/${to.hostname}`}${to.pathname}${to.search}${to.hash}`);
1138
+ else if (to.hostname === host) out.set('location', `${to.pathname}${to.search}${to.hash}`);
1139
+ 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}`); }
1140
+ } catch { /* left as answered */ } }
1118
1141
  return new Response(request.method === 'HEAD' ? null : answer.body, { status: answer.status, statusText: answer.statusText, headers: out });
1119
1142
  }
1120
1143
  // 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)