@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.
- package/dist/src/served-world.js +36 -12
- package/package.json +4 -4
- package/src/served-world.ts +35 -10
package/dist/src/served-world.js
CHANGED
|
@@ -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.
|
|
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
|
|
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,
|
|
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
|
|
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}${
|
|
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
|
-
|
|
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.
|
|
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.
|
|
72
|
-
"@volter/world-console": "3.0.
|
|
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.
|
|
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"
|
package/src/served-world.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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,
|
|
1084
|
-
else for (const row of
|
|
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}${
|
|
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 {
|
|
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)
|