@volter/world-runtime 3.0.6 → 3.0.8
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 +64 -3
- 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 +49 -4
- 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
|
@@ -26,7 +26,7 @@ import { getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant
|
|
|
26
26
|
// to `.volter/token` and `.volter/token.read` (0600).
|
|
27
27
|
// One writer per world by construction: the doors run in this process, serialized per twin.
|
|
28
28
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
29
|
-
import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin } from '@volter/world-core/runtime';
|
|
29
|
+
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, validateRemoteOrigin } from '@volter/world-core/runtime';
|
|
30
30
|
import { APP_READ_ENDPOINT_ENV } from "./init.js";
|
|
31
31
|
import { packFacts } from "./pack-facts.js";
|
|
32
32
|
import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass } from '@volter/world-access';
|
|
@@ -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'))
|
|
@@ -890,6 +941,14 @@ export class WorldDoors {
|
|
|
890
941
|
headers.set(TWIN_PREFIX_HEADER, `/${this.served}/${vendor}`);
|
|
891
942
|
headers.set('x-forwarded-host', url.host);
|
|
892
943
|
headers.set('x-forwarded-proto', url.protocol.replace(':', ''));
|
|
944
|
+
// where this World shows the hostnames its twins answer, for a page that links one (twinSiteUrl): under its own
|
|
945
|
+
// origin on this machine (siteOfHost); a World without one shows none
|
|
946
|
+
const own = this.host.browserOrigin?.() ?? null;
|
|
947
|
+
const sites = own ? new URL(own) : null;
|
|
948
|
+
if (sites && sites.hostname.endsWith('.localhost'))
|
|
949
|
+
headers.set(TWIN_SITES_HEADER, `${sites.protocol}//{host}.${sites.host}`);
|
|
950
|
+
else
|
|
951
|
+
headers.delete(TWIN_SITES_HEADER);
|
|
893
952
|
const body = request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.arrayBuffer();
|
|
894
953
|
const answer = await twin(new Request(`${url.origin}/${tail.join('/')}${url.search}`, { method: request.method, headers, ...(body === undefined ? {} : { body }), redirect: 'manual' }));
|
|
895
954
|
// an upgrade the twin answered (a tunnel's control or visitor socket) goes back as it is
|
|
@@ -934,7 +993,7 @@ export class WorldDoors {
|
|
|
934
993
|
// what leaves for the vendor before the seal is applied: no World token in any header it can arrive
|
|
935
994
|
// under (an SDK's own `Authorization: Bearer` included), no cookie, no routing headers of this World
|
|
936
995
|
const caller = new Headers(request.headers);
|
|
937
|
-
for (const h of [TOKEN_HEADER, TWINS_KEY_HEADER, TWIN_PREFIX_HEADER, CALLER_HEADER, 'host', 'x-forwarded-host', 'x-forwarded-proto', 'cookie'])
|
|
996
|
+
for (const h of [TOKEN_HEADER, TWINS_KEY_HEADER, TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, CALLER_HEADER, 'host', 'x-forwarded-host', 'x-forwarded-proto', 'cookie'])
|
|
938
997
|
caller.delete(h);
|
|
939
998
|
// a secret this World minted (`tok_…`, live, revoked or expired) or one that opens it never leaves
|
|
940
999
|
const authorization = caller.get('authorization');
|
|
@@ -1126,7 +1185,9 @@ export class WorldDoors {
|
|
|
1126
1185
|
return this.timeline(url);
|
|
1127
1186
|
if (kind === 'diff' && !name && request.method === 'GET') {
|
|
1128
1187
|
const delta = diffWorld(this.name, { root: this.worldRoot });
|
|
1129
|
-
|
|
1188
|
+
// what another World pushed here is in its changeset already: never this World's uncut changes
|
|
1189
|
+
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)));
|
|
1190
|
+
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
1191
|
}
|
|
1131
1192
|
if (kind === 'log' && name && request.method === 'GET') {
|
|
1132
1193
|
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.8",
|
|
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.8",
|
|
72
|
+
"@volter/world-console": "3.0.3"
|
|
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.8",
|
|
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
|
@@ -26,7 +26,7 @@ import { getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant
|
|
|
26
26
|
// to `.volter/token` and `.volter/token.read` (0600).
|
|
27
27
|
// One writer per world by construction: the doors run in this process, serialized per twin.
|
|
28
28
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync , rmSync} from 'node:fs';
|
|
29
|
-
import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, type TwinRequestJournalCursor, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core/runtime';
|
|
29
|
+
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, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core/runtime';
|
|
30
30
|
import { APP_READ_ENDPOINT_ENV } from './init.ts';
|
|
31
31
|
import { packFacts } from './pack-facts.ts';
|
|
32
32
|
import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass, type TrustedIssuer, type WorldCredentials } from '@volter/world-access';
|
|
@@ -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' } });
|
|
@@ -789,6 +827,11 @@ export class WorldDoors {
|
|
|
789
827
|
// the twin is mounted under this World's path: live URLs it mints must carry it (twinPublicBase)
|
|
790
828
|
headers.set(TWIN_PREFIX_HEADER, `/${this.served}/${vendor}`);
|
|
791
829
|
headers.set('x-forwarded-host', url.host); headers.set('x-forwarded-proto', url.protocol.replace(':', ''));
|
|
830
|
+
// where this World shows the hostnames its twins answer, for a page that links one (twinSiteUrl): under its own
|
|
831
|
+
// origin on this machine (siteOfHost); a World without one shows none
|
|
832
|
+
const own = this.host.browserOrigin?.() ?? null;
|
|
833
|
+
const sites = own ? new URL(own) : null;
|
|
834
|
+
if (sites && sites.hostname.endsWith('.localhost')) headers.set(TWIN_SITES_HEADER, `${sites.protocol}//{host}.${sites.host}`); else headers.delete(TWIN_SITES_HEADER);
|
|
792
835
|
const body = request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.arrayBuffer();
|
|
793
836
|
const answer = await twin(new Request(`${url.origin}/${tail.join('/')}${url.search}`, { method: request.method, headers, ...(body === undefined ? {} : { body }), redirect: 'manual' }));
|
|
794
837
|
// an upgrade the twin answered (a tunnel's control or visitor socket) goes back as it is
|
|
@@ -830,7 +873,7 @@ export class WorldDoors {
|
|
|
830
873
|
// what leaves for the vendor before the seal is applied: no World token in any header it can arrive
|
|
831
874
|
// under (an SDK's own `Authorization: Bearer` included), no cookie, no routing headers of this World
|
|
832
875
|
const caller = new Headers(request.headers);
|
|
833
|
-
for (const h of [TOKEN_HEADER, TWINS_KEY_HEADER, TWIN_PREFIX_HEADER, CALLER_HEADER, 'host', 'x-forwarded-host', 'x-forwarded-proto', 'cookie']) caller.delete(h);
|
|
876
|
+
for (const h of [TOKEN_HEADER, TWINS_KEY_HEADER, TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, CALLER_HEADER, 'host', 'x-forwarded-host', 'x-forwarded-proto', 'cookie']) caller.delete(h);
|
|
834
877
|
// a secret this World minted (`tok_…`, live, revoked or expired) or one that opens it never leaves
|
|
835
878
|
const authorization = caller.get('authorization');
|
|
836
879
|
const offered = authorization ? presentedSecrets(new Headers({ authorization }), { token: TOKEN_HEADER, twinsKey: TWINS_KEY_HEADER }) : [];
|
|
@@ -977,7 +1020,9 @@ export class WorldDoors {
|
|
|
977
1020
|
if (kind === 'timeline' && !name && request.method === 'GET') return this.timeline(url);
|
|
978
1021
|
if (kind === 'diff' && !name && request.method === 'GET') {
|
|
979
1022
|
const delta = diffWorld(this.name, { root: this.worldRoot });
|
|
980
|
-
|
|
1023
|
+
// what another World pushed here is in its changeset already: never this World's uncut changes
|
|
1024
|
+
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)));
|
|
1025
|
+
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
1026
|
}
|
|
982
1027
|
|
|
983
1028
|
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
|