@volter/world-runtime 3.0.6 → 3.0.7
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/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/pack-facts.d.ts +5 -0
- package/dist/src/served-world.d.ts +11 -1
- package/dist/src/served-world.js +58 -1
- package/dist/src/world-origins.d.ts +7 -0
- package/dist/src/world-origins.js +18 -0
- package/dist/src/world-view.js +5 -1
- package/package.json +4 -4
- package/src/index.ts +1 -1
- package/src/pack-facts.ts +2 -0
- package/src/served-world.ts +45 -2
- package/src/world-origins.ts +16 -0
- package/src/world-view.ts +4 -1
package/dist/src/index.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export { landReceipts, mountWorld, READ_ONLY_HEADER, worldOriginLabel, readServe
|
|
|
46
46
|
export type { BranchDoors, BranchRow, DoorHost, MountedWorld, ServeRecord, WorldLayout } from './served-world.js';
|
|
47
47
|
export { LocalBranches } from './local-branches.js';
|
|
48
48
|
export { serveWorldView, VIEW_CONSOLE_BASE } from './world-view.js';
|
|
49
|
-
export { LOCAL_ORIGIN_BASE, localWorldOrigin, pathOfWorld, worldDoorUnderConsole, worldOfHost } from './world-origins.js';
|
|
49
|
+
export { LOCAL_ORIGIN_BASE, localWorldOrigin, pathOfWorld, siteOfHost, worldDoorUnderConsole, worldOfHost } from './world-origins.js';
|
|
50
50
|
export type { ViewConsole, WorldView } from './world-view.js';
|
|
51
51
|
export type { LocalBranchesOptions } from './local-branches.js';
|
|
52
52
|
export type { ServedWorld } from './served-world.js';
|
package/dist/src/index.js
CHANGED
|
@@ -33,7 +33,7 @@ export { deployWorld, refreshTwin, adaptersFor, credentialPath, rewrapTwinCreden
|
|
|
33
33
|
export { landReceipts, mountWorld, READ_ONLY_HEADER, worldOriginLabel, readServeRecord, refreshSchedule, servedName, serveWorld, sessionsFile, keysFile, TOKEN_HEADER, WorldDoors } from "./served-world.js";
|
|
34
34
|
export { LocalBranches } from "./local-branches.js";
|
|
35
35
|
export { serveWorldView, VIEW_CONSOLE_BASE } from "./world-view.js";
|
|
36
|
-
export { LOCAL_ORIGIN_BASE, localWorldOrigin, pathOfWorld, worldDoorUnderConsole, worldOfHost } from "./world-origins.js";
|
|
36
|
+
export { LOCAL_ORIGIN_BASE, localWorldOrigin, pathOfWorld, siteOfHost, worldDoorUnderConsole, worldOfHost } from "./world-origins.js";
|
|
37
37
|
export { CONSOLE_BASE, consoleRedirect, serveConsoleApart, serveConsoleFor } from "./console-apart.js";
|
|
38
38
|
export { assertWorldStateRemovable } from "./ancestry.js";
|
|
39
39
|
export { withoutVaultSignIn } from "./runtime.js";
|
package/dist/src/pack-facts.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ export type PackFacts = {
|
|
|
42
42
|
body: Record<string, unknown>;
|
|
43
43
|
fill: Record<string, string>;
|
|
44
44
|
};
|
|
45
|
+
/** The door listing the hostnames the twin answers while it runs (TwinPack.hostsClaimed). */
|
|
46
|
+
hostsClaimed?: {
|
|
47
|
+
door: string;
|
|
48
|
+
note?: string;
|
|
49
|
+
};
|
|
45
50
|
credentialIssuer?: string;
|
|
46
51
|
/** The pack's colocatable `create<Name>TwinServer` factory export, derived (or descriptor-
|
|
47
52
|
* declared under ambiguity) by scripts/pack-facts.ts. Consumed by `init` to emit
|
|
@@ -54,7 +54,8 @@ export type MountedWorld = {
|
|
|
54
54
|
token: string;
|
|
55
55
|
readToken: string;
|
|
56
56
|
};
|
|
57
|
-
handle: (request: Request) => Promise<Response>;
|
|
57
|
+
handle: (request: Request) => Promise<Response>; /** a hostname this World's twins answer, as a browser reads it (siteOfHost) */
|
|
58
|
+
site: (request: Request, host: string) => Promise<Response>;
|
|
58
59
|
boot: (url: string) => Promise<void>;
|
|
59
60
|
stop: () => Promise<void>;
|
|
60
61
|
};
|
|
@@ -291,6 +292,15 @@ export declare class WorldDoors {
|
|
|
291
292
|
private refresh;
|
|
292
293
|
private controlRoot;
|
|
293
294
|
private stateOf;
|
|
295
|
+
/** The hostnames each of this World's twins answers while it runs (its hostsClaimed door). */
|
|
296
|
+
claimedHosts(origin: string): Promise<Array<{
|
|
297
|
+
vendor: string;
|
|
298
|
+
hosts: string[];
|
|
299
|
+
}>>;
|
|
300
|
+
/** A hostname one of this World's twins answers (its hostsClaimed door lists it), read as a browser reads it there:
|
|
301
|
+
* the request goes to that twin with the hostname as its original host, as the injector routes it inside the World.
|
|
302
|
+
* Reads only: the site is the twin's state as the World holds it, never a way to write it. */
|
|
303
|
+
site(request: Request, host: string): Promise<Response>;
|
|
294
304
|
handle(request: Request): Promise<Response>;
|
|
295
305
|
/** A twin's workspaces as its discovery door lists them (`GET /twin`'s `screens`: each built workspace screen's id and
|
|
296
306
|
* path), which a person is shown at `/<served>/<vendor><path>`. Asked again after a minute, as `enforcesRead` is. */
|
package/dist/src/served-world.js
CHANGED
|
@@ -158,6 +158,7 @@ export async function mountWorld(name, opts = {}) {
|
|
|
158
158
|
twins: () => world.twins(),
|
|
159
159
|
rotate: () => { world.retoken(mintToken('tok_'), mintToken('tok_r_')); keep(); return { token: world.token, readToken: world.readToken }; },
|
|
160
160
|
handle: (request) => world.handle(request),
|
|
161
|
+
site: (request, host) => world.site(request, host),
|
|
161
162
|
async boot(url) {
|
|
162
163
|
keep();
|
|
163
164
|
const port = Number(new URL(url).port) || 0;
|
|
@@ -746,6 +747,56 @@ export class WorldDoors {
|
|
|
746
747
|
stateOf(vendor) {
|
|
747
748
|
return stateServiceOf(this.controlRoot(vendor), vendor);
|
|
748
749
|
}
|
|
750
|
+
/** The hostnames each of this World's twins answers while it runs (its hostsClaimed door). */
|
|
751
|
+
async claimedHosts(origin) {
|
|
752
|
+
const out = [];
|
|
753
|
+
for (const vendor of Object.keys(this.up().services)) {
|
|
754
|
+
const door = packFacts()[vendor]?.hostsClaimed?.door;
|
|
755
|
+
const twin = door ? this.host.twinFetch(vendor) : undefined;
|
|
756
|
+
if (!door || !twin)
|
|
757
|
+
continue;
|
|
758
|
+
const listed = await twin(new Request(`${origin}${door}`, { headers: { [READ_ONLY_HEADER]: '1' } })).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
759
|
+
const hosts = Array.isArray(listed?.hosts) ? listed.hosts.map((h) => String(h).toLowerCase()) : [];
|
|
760
|
+
if (hosts.length)
|
|
761
|
+
out.push({ vendor, hosts });
|
|
762
|
+
}
|
|
763
|
+
return out;
|
|
764
|
+
}
|
|
765
|
+
/** A hostname one of this World's twins answers (its hostsClaimed door lists it), read as a browser reads it there:
|
|
766
|
+
* the request goes to that twin with the hostname as its original host, as the injector routes it inside the World.
|
|
767
|
+
* Reads only: the site is the twin's state as the World holds it, never a way to write it. */
|
|
768
|
+
async site(request, host) {
|
|
769
|
+
if (request.method !== 'GET' && request.method !== 'HEAD')
|
|
770
|
+
return Response.json({ error: 'a site in a World is read: GET and HEAD' }, { status: 405 });
|
|
771
|
+
const url = new URL(request.url);
|
|
772
|
+
for (const { vendor, hosts } of await this.claimedHosts(url.origin)) {
|
|
773
|
+
const twin = this.host.twinFetch(vendor);
|
|
774
|
+
if (!twin || !hosts.includes(host))
|
|
775
|
+
continue;
|
|
776
|
+
const headers = new Headers();
|
|
777
|
+
for (const name of ['accept', 'accept-language', 'if-none-match', 'range', 'user-agent']) {
|
|
778
|
+
const v = request.headers.get(name);
|
|
779
|
+
if (v !== null)
|
|
780
|
+
headers.set(name, v);
|
|
781
|
+
}
|
|
782
|
+
headers.set('x-volter-twin-original-host', host);
|
|
783
|
+
headers.set(READ_ONLY_HEADER, '1');
|
|
784
|
+
const answer = await twin(new Request(`${url.origin}${url.pathname}${url.search}`, { method: request.method, headers, redirect: 'manual' }));
|
|
785
|
+
// a redirect the site answers names its own hostname: in the World it stays under the World's origin
|
|
786
|
+
const out = new Headers(answer.headers);
|
|
787
|
+
const location = out.get('location');
|
|
788
|
+
if (location) {
|
|
789
|
+
try {
|
|
790
|
+
const to = new URL(location, `https://${host}`);
|
|
791
|
+
if (to.hostname === host)
|
|
792
|
+
out.set('location', `${to.pathname}${to.search}${to.hash}`);
|
|
793
|
+
}
|
|
794
|
+
catch { /* left as answered */ }
|
|
795
|
+
}
|
|
796
|
+
return new Response(request.method === 'HEAD' ? null : answer.body, { status: answer.status, statusText: answer.statusText, headers: out });
|
|
797
|
+
}
|
|
798
|
+
return Response.json({ error: `no twin of ${this.served} answers ${host}` }, { status: 404 });
|
|
799
|
+
}
|
|
749
800
|
async handle(request) {
|
|
750
801
|
try {
|
|
751
802
|
if (!this.host.ready() && !new URL(request.url).pathname.startsWith('/-/ping'))
|
|
@@ -1124,9 +1175,15 @@ export class WorldDoors {
|
|
|
1124
1175
|
return Response.json(await this.map());
|
|
1125
1176
|
if (kind === 'timeline' && !name && request.method === 'GET')
|
|
1126
1177
|
return this.timeline(url);
|
|
1178
|
+
// the sites this World's twins answer (a Workers Custom Domain, an R2 public domain), each read in a browser at
|
|
1179
|
+
// `<hostname>.<the World's origin>` (siteOfHost)
|
|
1180
|
+
if (kind === 'sites' && !name && request.method === 'GET')
|
|
1181
|
+
return Response.json({ world: this.served, sites: (await this.claimedHosts(url.origin)).flatMap((c) => c.hosts.map((host) => ({ twin: c.vendor, host }))) });
|
|
1127
1182
|
if (kind === 'diff' && !name && request.method === 'GET') {
|
|
1128
1183
|
const delta = diffWorld(this.name, { root: this.worldRoot });
|
|
1129
|
-
|
|
1184
|
+
// what another World pushed here is in its changeset already: never this World's uncut changes
|
|
1185
|
+
const received = new Set(listWorldChangesets({ root: this.worldRoot, world: this.name }).filter((l) => l.changeset.pushedBy).flatMap((l) => l.changeset.actions.map((a) => a.action.id)));
|
|
1186
|
+
return Response.json({ world: this.served, base: { id: delta.base.id, at: delta.base.createdAt ?? null }, changes: delta.actions.filter((a) => !received.has(a.action.id)).map((a) => ({ twin: a.service, entry: a.action })) });
|
|
1130
1187
|
}
|
|
1131
1188
|
if (kind === 'log' && name && request.method === 'GET') {
|
|
1132
1189
|
const state = this.stateOf(name);
|
|
@@ -11,3 +11,10 @@ export declare function worldDoorUnderConsole(path: string, served: (name: strin
|
|
|
11
11
|
/** Whether a path on a World's origin belongs to that World (its wire, its doors, its vendors' screens) or to
|
|
12
12
|
* what every origin carries (`/-/ping`, the console): a World's origin serves no other World. */
|
|
13
13
|
export declare function pathOfWorld(path: string, served: string, consoleBase: string): boolean;
|
|
14
|
+
/** A site a World's twins answer, reached under the World's origin: `<hostname>.<world>--<org>.localhost` names the
|
|
15
|
+
* hostname `<hostname>` as that World's twins answer it (a Workers Custom Domain, an R2 public domain). The World and
|
|
16
|
+
* the hostname, or undefined for any other host. Each site is an origin of its own, apart from the World's session. */
|
|
17
|
+
export declare function siteOfHost(hostname: string, served: Iterable<string>, base?: string): {
|
|
18
|
+
world: string;
|
|
19
|
+
site: string;
|
|
20
|
+
} | undefined;
|
|
@@ -35,3 +35,21 @@ export function worldDoorUnderConsole(path, served, consoleBase) {
|
|
|
35
35
|
export function pathOfWorld(path, served, consoleBase) {
|
|
36
36
|
return path === '/-/ping' || path === consoleBase || path.startsWith(`${consoleBase}/`) || path.startsWith(`/${served}/`) || path.startsWith(`/-/${served}/`) || path === `/${served}` || path === `/-/${served}`;
|
|
37
37
|
}
|
|
38
|
+
/** A site a World's twins answer, reached under the World's origin: `<hostname>.<world>--<org>.localhost` names the
|
|
39
|
+
* hostname `<hostname>` as that World's twins answer it (a Workers Custom Domain, an R2 public domain). The World and
|
|
40
|
+
* the hostname, or undefined for any other host. Each site is an origin of its own, apart from the World's session. */
|
|
41
|
+
export function siteOfHost(hostname, served, base = LOCAL_ORIGIN_BASE) {
|
|
42
|
+
const suffix = `.${base}`;
|
|
43
|
+
if (!hostname.endsWith(suffix))
|
|
44
|
+
return undefined;
|
|
45
|
+
const rest = hostname.slice(0, -suffix.length);
|
|
46
|
+
for (const name of served) {
|
|
47
|
+
const label = worldOriginLabel(name);
|
|
48
|
+
if (!label || !rest.endsWith(`.${label}`))
|
|
49
|
+
continue;
|
|
50
|
+
const site = rest.slice(0, -(label.length + 1));
|
|
51
|
+
if (site.includes('.'))
|
|
52
|
+
return { world: name, site };
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
package/dist/src/world-view.js
CHANGED
|
@@ -8,7 +8,7 @@ import { join, resolve } from 'node:path';
|
|
|
8
8
|
import { serveHttp, stateDirName } from '@volter/world-core/runtime';
|
|
9
9
|
import { LocalBranches } from "./local-branches.js";
|
|
10
10
|
import { mountWorld } from "./served-world.js";
|
|
11
|
-
import { localWorldOrigin, pathOfWorld, worldDoorUnderConsole, worldOfHost } from "./world-origins.js";
|
|
11
|
+
import { localWorldOrigin, pathOfWorld, siteOfHost, worldDoorUnderConsole, worldOfHost } from "./world-origins.js";
|
|
12
12
|
export const VIEW_CONSOLE_BASE = '/-/console';
|
|
13
13
|
/** The branch Worlds already under `dir` (`<dir>/<org>/<world>/.volter/world.json`). */
|
|
14
14
|
function branchRoots(dir) {
|
|
@@ -110,6 +110,10 @@ export async function serveWorldView(name, opts = {}) {
|
|
|
110
110
|
const at = new URL(request.url);
|
|
111
111
|
const path = at.pathname;
|
|
112
112
|
// a World's own origin serves that World (and the ping and the console) and no other
|
|
113
|
+
// a site the World's twins answer, under the World's local origin (`<hostname>.<world>--<org>.localhost`)
|
|
114
|
+
const site = origins && !proxied ? siteOfHost(at.hostname, worlds.keys()) : undefined;
|
|
115
|
+
if (site)
|
|
116
|
+
return worlds.get(site.world).site(request, site.site);
|
|
113
117
|
const own = origins ? (proxied && at.host === proxied.host ? base : worldOfHost(at.hostname, worlds.keys())) : undefined;
|
|
114
118
|
if (own === null)
|
|
115
119
|
return Response.json({ error: `no world at ${at.host}` }, { status: 404 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.7",
|
|
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.7",
|
|
72
|
+
"@volter/world-console": "3.0.2"
|
|
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.7",
|
|
79
79
|
"pg-gateway": "0.3.0-beta.4",
|
|
80
80
|
"smol-toml": "^1.8.0",
|
|
81
81
|
"yaml": "2.9.0"
|
package/src/index.ts
CHANGED
|
@@ -168,7 +168,7 @@ export { landReceipts, mountWorld, READ_ONLY_HEADER, worldOriginLabel, readServe
|
|
|
168
168
|
export type { BranchDoors, BranchRow, DoorHost, MountedWorld, ServeRecord, WorldLayout } from './served-world.ts';
|
|
169
169
|
export { LocalBranches } from './local-branches.ts';
|
|
170
170
|
export { serveWorldView, VIEW_CONSOLE_BASE } from './world-view.ts';
|
|
171
|
-
export { LOCAL_ORIGIN_BASE, localWorldOrigin, pathOfWorld, worldDoorUnderConsole, worldOfHost } from './world-origins.ts';
|
|
171
|
+
export { LOCAL_ORIGIN_BASE, localWorldOrigin, pathOfWorld, siteOfHost, worldDoorUnderConsole, worldOfHost } from './world-origins.ts';
|
|
172
172
|
export type { ViewConsole, WorldView } from './world-view.ts';
|
|
173
173
|
export type { LocalBranchesOptions } from './local-branches.ts';
|
|
174
174
|
export type { ServedWorld } from './served-world.ts';
|
package/src/pack-facts.ts
CHANGED
|
@@ -22,6 +22,8 @@ export type PackFacts = {
|
|
|
22
22
|
endpointEnvNone?: string;
|
|
23
23
|
/** The door that issues the application a credential the twin accepts (TwinPack.credentialDoor). */
|
|
24
24
|
credentialDoor?: { path: string; body: Record<string, unknown>; fill: Record<string, string> };
|
|
25
|
+
/** The door listing the hostnames the twin answers while it runs (TwinPack.hostsClaimed). */
|
|
26
|
+
hostsClaimed?: { door: string; note?: string };
|
|
25
27
|
credentialIssuer?: string;
|
|
26
28
|
/** The pack's colocatable `create<Name>TwinServer` factory export, derived (or descriptor-
|
|
27
29
|
* declared under ambiguity) by scripts/pack-facts.ts. Consumed by `init` to emit
|
package/src/served-world.ts
CHANGED
|
@@ -115,7 +115,7 @@ function configRefFor(worldRoot: string, name: string): string {
|
|
|
115
115
|
/** A world mounted by a host. `token`/`readToken` read the CURRENT tokens (rotate changes them);
|
|
116
116
|
* `twins()` names the world's twins once booted; `rotate()` mints both tokens anew — the old die
|
|
117
117
|
* with the call — and keeps them beside the world like `boot` does. */
|
|
118
|
-
export type MountedWorld = { name: string; served: string; root: string; readonly token: string; readonly readToken: string; twins: () => string[]; rotate: () => { token: string; readToken: string }; handle: (request: Request) => Promise<Response>; boot: (url: string) => Promise<void>; stop: () => Promise<void> };
|
|
118
|
+
export type MountedWorld = { name: string; served: string; root: string; readonly token: string; readonly readToken: string; twins: () => string[]; rotate: () => { token: string; readToken: string }; handle: (request: Request) => Promise<Response>; /** a hostname this World's twins answer, as a browser reads it (siteOfHost) */ site: (request: Request, host: string) => Promise<Response>; boot: (url: string) => Promise<void>; stop: () => Promise<void> };
|
|
119
119
|
/** A browser session a World keeps: its scope, when it ends, and the person a pass named, when one did. */
|
|
120
120
|
type HeldSession = { scope: 'read' | 'write'; until: number; /** opened by this machine's own page (localSession) */ local?: true; who?: string; /** the platform whose pass opened it */ issuer?: string; /** the person's subject there */ sub?: string; /** the read key a shared link opened it with: it lasts only while that key does */ key?: string };
|
|
121
121
|
|
|
@@ -169,6 +169,7 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
|
|
|
169
169
|
twins: () => world.twins(),
|
|
170
170
|
rotate: () => { world.retoken(mintToken('tok_'), mintToken('tok_r_')); keep(); return { token: world.token, readToken: world.readToken }; },
|
|
171
171
|
handle: (request) => world.handle(request),
|
|
172
|
+
site: (request, host) => world.site(request, host),
|
|
172
173
|
async boot(url) {
|
|
173
174
|
keep();
|
|
174
175
|
const port = Number(new URL(url).port) || 0;
|
|
@@ -671,6 +672,43 @@ export class WorldDoors {
|
|
|
671
672
|
return stateServiceOf(this.controlRoot(vendor), vendor);
|
|
672
673
|
}
|
|
673
674
|
|
|
675
|
+
/** The hostnames each of this World's twins answers while it runs (its hostsClaimed door). */
|
|
676
|
+
async claimedHosts(origin: string): Promise<Array<{ vendor: string; hosts: string[] }>> {
|
|
677
|
+
const out: Array<{ vendor: string; hosts: string[] }> = [];
|
|
678
|
+
for (const vendor of Object.keys(this.up().services)) {
|
|
679
|
+
const door = packFacts()[vendor]?.hostsClaimed?.door;
|
|
680
|
+
const twin = door ? this.host.twinFetch(vendor) : undefined;
|
|
681
|
+
if (!door || !twin) continue;
|
|
682
|
+
const listed = await twin(new Request(`${origin}${door}`, { headers: { [READ_ONLY_HEADER]: '1' } })).then((r) => (r.ok ? r.json() as Promise<{ hosts?: unknown }> : null)).catch(() => null);
|
|
683
|
+
const hosts = Array.isArray(listed?.hosts) ? (listed.hosts as unknown[]).map((h) => String(h).toLowerCase()) : [];
|
|
684
|
+
if (hosts.length) out.push({ vendor, hosts });
|
|
685
|
+
}
|
|
686
|
+
return out;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** A hostname one of this World's twins answers (its hostsClaimed door lists it), read as a browser reads it there:
|
|
690
|
+
* the request goes to that twin with the hostname as its original host, as the injector routes it inside the World.
|
|
691
|
+
* Reads only: the site is the twin's state as the World holds it, never a way to write it. */
|
|
692
|
+
async site(request: Request, host: string): Promise<Response> {
|
|
693
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') return Response.json({ error: 'a site in a World is read: GET and HEAD' }, { status: 405 });
|
|
694
|
+
const url = new URL(request.url);
|
|
695
|
+
for (const { vendor, hosts } of await this.claimedHosts(url.origin)) {
|
|
696
|
+
const twin = this.host.twinFetch(vendor);
|
|
697
|
+
if (!twin || !hosts.includes(host)) continue;
|
|
698
|
+
const headers = new Headers();
|
|
699
|
+
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); }
|
|
700
|
+
headers.set('x-volter-twin-original-host', host);
|
|
701
|
+
headers.set(READ_ONLY_HEADER, '1');
|
|
702
|
+
const answer = await twin(new Request(`${url.origin}${url.pathname}${url.search}`, { method: request.method, headers, redirect: 'manual' }));
|
|
703
|
+
// a redirect the site answers names its own hostname: in the World it stays under the World's origin
|
|
704
|
+
const out = new Headers(answer.headers);
|
|
705
|
+
const location = out.get('location');
|
|
706
|
+
if (location) { try { const to = new URL(location, `https://${host}`); if (to.hostname === host) out.set('location', `${to.pathname}${to.search}${to.hash}`); } catch { /* left as answered */ } }
|
|
707
|
+
return new Response(request.method === 'HEAD' ? null : answer.body, { status: answer.status, statusText: answer.statusText, headers: out });
|
|
708
|
+
}
|
|
709
|
+
return Response.json({ error: `no twin of ${this.served} answers ${host}` }, { status: 404 });
|
|
710
|
+
}
|
|
711
|
+
|
|
674
712
|
async handle(request: Request): Promise<Response> {
|
|
675
713
|
try {
|
|
676
714
|
if (!this.host.ready() && !new URL(request.url).pathname.startsWith('/-/ping')) return Response.json({ error: 'booting', world: this.served }, { status: 503, headers: { 'retry-after': '1' } });
|
|
@@ -975,9 +1013,14 @@ export class WorldDoors {
|
|
|
975
1013
|
}
|
|
976
1014
|
if (kind === 'map' && !name && request.method === 'GET') return Response.json(await this.map());
|
|
977
1015
|
if (kind === 'timeline' && !name && request.method === 'GET') return this.timeline(url);
|
|
1016
|
+
// the sites this World's twins answer (a Workers Custom Domain, an R2 public domain), each read in a browser at
|
|
1017
|
+
// `<hostname>.<the World's origin>` (siteOfHost)
|
|
1018
|
+
if (kind === 'sites' && !name && request.method === 'GET') return Response.json({ world: this.served, sites: (await this.claimedHosts(url.origin)).flatMap((c) => c.hosts.map((host) => ({ twin: c.vendor, host }))) });
|
|
978
1019
|
if (kind === 'diff' && !name && request.method === 'GET') {
|
|
979
1020
|
const delta = diffWorld(this.name, { root: this.worldRoot });
|
|
980
|
-
|
|
1021
|
+
// what another World pushed here is in its changeset already: never this World's uncut changes
|
|
1022
|
+
const received = new Set(listWorldChangesets({ root: this.worldRoot, world: this.name }).filter((l) => l.changeset.pushedBy).flatMap((l) => l.changeset.actions.map((a) => a.action.id)));
|
|
1023
|
+
return Response.json({ world: this.served, base: { id: delta.base.id, at: delta.base.createdAt ?? null }, changes: delta.actions.filter((a) => !received.has(a.action.id)).map((a) => ({ twin: a.service, entry: a.action })) });
|
|
981
1024
|
}
|
|
982
1025
|
|
|
983
1026
|
if (kind === 'log' && name && request.method === 'GET') {
|
package/src/world-origins.ts
CHANGED
|
@@ -36,3 +36,19 @@ export function worldDoorUnderConsole(path: string, served: (name: string) => bo
|
|
|
36
36
|
export function pathOfWorld(path: string, served: string, consoleBase: string): boolean {
|
|
37
37
|
return path === '/-/ping' || path === consoleBase || path.startsWith(`${consoleBase}/`) || path.startsWith(`/${served}/`) || path.startsWith(`/-/${served}/`) || path === `/${served}` || path === `/-/${served}`;
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/** A site a World's twins answer, reached under the World's origin: `<hostname>.<world>--<org>.localhost` names the
|
|
41
|
+
* hostname `<hostname>` as that World's twins answer it (a Workers Custom Domain, an R2 public domain). The World and
|
|
42
|
+
* the hostname, or undefined for any other host. Each site is an origin of its own, apart from the World's session. */
|
|
43
|
+
export function siteOfHost(hostname: string, served: Iterable<string>, base = LOCAL_ORIGIN_BASE): { world: string; site: string } | undefined {
|
|
44
|
+
const suffix = `.${base}`;
|
|
45
|
+
if (!hostname.endsWith(suffix)) return undefined;
|
|
46
|
+
const rest = hostname.slice(0, -suffix.length);
|
|
47
|
+
for (const name of served) {
|
|
48
|
+
const label = worldOriginLabel(name);
|
|
49
|
+
if (!label || !rest.endsWith(`.${label}`)) continue;
|
|
50
|
+
const site = rest.slice(0, -(label.length + 1));
|
|
51
|
+
if (site.includes('.')) return { world: name, site };
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
package/src/world-view.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { join, resolve } from 'node:path';
|
|
|
8
8
|
import { serveHttp, stateDirName } from '@volter/world-core/runtime';
|
|
9
9
|
import { LocalBranches } from './local-branches.ts';
|
|
10
10
|
import { mountWorld, type MountedWorld } from './served-world.ts';
|
|
11
|
-
import { localWorldOrigin, pathOfWorld, worldDoorUnderConsole, worldOfHost } from './world-origins.ts';
|
|
11
|
+
import { localWorldOrigin, pathOfWorld, siteOfHost, worldDoorUnderConsole, worldOfHost } from './world-origins.ts';
|
|
12
12
|
|
|
13
13
|
/** A console as a host mounts it (`@volter/world-console`): answer a request under `base`, or null. */
|
|
14
14
|
export type ViewConsole = { handle: (request: Request, base: string, opts?: { worldOrigin?: boolean }) => Promise<Response | null> };
|
|
@@ -99,6 +99,9 @@ export async function serveWorldView(name: string, opts: { /** the person signed
|
|
|
99
99
|
: incoming;
|
|
100
100
|
const at = new URL(request.url); const path = at.pathname;
|
|
101
101
|
// a World's own origin serves that World (and the ping and the console) and no other
|
|
102
|
+
// a site the World's twins answer, under the World's local origin (`<hostname>.<world>--<org>.localhost`)
|
|
103
|
+
const site = origins && !proxied ? siteOfHost(at.hostname, worlds.keys()) : undefined;
|
|
104
|
+
if (site) return worlds.get(site.world)!.site(request, site.site);
|
|
102
105
|
const own = origins ? (proxied && at.host === proxied.host ? base! : worldOfHost(at.hostname, worlds.keys())) : undefined;
|
|
103
106
|
if (own === null) return Response.json({ error: `no world at ${at.host}` }, { status: 404 });
|
|
104
107
|
// the bare address, typed into a browser, is the World's page: at its own origin when it has one
|