@volter/world-runtime 3.0.11 → 3.0.12
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/cli-commands.js +5 -1
- package/dist/src/served-world.js +36 -1
- package/package.json +4 -4
- package/src/cli-commands.ts +5 -1
- package/src/served-world.ts +34 -1
package/dist/src/cli-commands.js
CHANGED
|
@@ -920,11 +920,15 @@ export async function main() {
|
|
|
920
920
|
caFile = join(mkdtempSync(join(tmpdir(), 'volter-attach-')), 'ca.pem');
|
|
921
921
|
writeFileSync(caFile, manifest.ca);
|
|
922
922
|
}
|
|
923
|
+
// the World's twins, each a loopback listener here that adds its key, so a command with its own HTTP stack reaches
|
|
924
|
+
// them as it would a local World's (the manifest pointed at the listeners before its env is made)
|
|
925
|
+
const { bridgeTwins } = createRequire(import.meta.url)('@volter/world-core/http-bridge');
|
|
926
|
+
await bridgeTwins(manifest, manifest.env.VOLTER_TWINS_KEY ?? attachToken ?? '', manifest.ca ?? undefined);
|
|
923
927
|
const injectPath = createRequire(import.meta.url).resolve('@volter/world-core/inject');
|
|
924
928
|
const env = remoteAttachEnv(manifest, { injectPath, ...(caFile === undefined ? {} : { caFile }) });
|
|
925
929
|
// the world's byte-stream doors, bridged to loopback listeners here, beside a command in any language
|
|
926
930
|
const { bridgeStreams, drainStreams } = createRequire(import.meta.url)('@volter/world-core/stream-bridge');
|
|
927
|
-
const bridged = manifest.streams ? await bridgeStreams(manifest.streams,
|
|
931
|
+
const bridged = manifest.streams ? await bridgeStreams(manifest.streams, manifest.env.VOLTER_TWINS_KEY ?? attachToken ?? '') : { env: {} };
|
|
928
932
|
// VOLTER_WORLD names the World to the injector: a request addressed to its paths carries its key
|
|
929
933
|
const commandEnv = withoutVaultSignIn({ ...process.env, ...env, ...bridged.env, VOLTER_WORLD: resolved.ref });
|
|
930
934
|
commandEnv.VOLTER_TWIN_INJECT_QUIET = head.includes('--verbose') ? '0' : commandEnv.VOLTER_TWIN_INJECT_QUIET ?? '1';
|
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, TWIN_SITES_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, ORIGINAL_PATH_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';
|
|
@@ -982,6 +982,12 @@ export class WorldDoors {
|
|
|
982
982
|
// the request's trace: the caller's trace-id goes on (or a new one), under a parent-id the World mints, which the
|
|
983
983
|
// request's log entries carry, so each entry is joined to its caller by an id no caller chose
|
|
984
984
|
headers.delete(CALLER_HEADER);
|
|
985
|
+
// the address a request was signed for (a CLI's loopback bridge says it, @volter/world-core/http-bridge) is a
|
|
986
|
+
// credential holder's word: a browser's session cannot name another, so a signature is checked on the path it reached
|
|
987
|
+
if (via === 'session') {
|
|
988
|
+
headers.delete(ORIGINAL_PATH_HEADER);
|
|
989
|
+
headers.delete('x-volter-twin-original-host');
|
|
990
|
+
}
|
|
985
991
|
const who = this.callerOf(request);
|
|
986
992
|
if (who)
|
|
987
993
|
headers.set(CALLER_HEADER, callerForHeader(who));
|
|
@@ -1144,6 +1150,35 @@ export class WorldDoors {
|
|
|
1144
1150
|
// ending a browser's session needs no token: it only clears the caller's own cookie, which never
|
|
1145
1151
|
// reaches these /-/ paths, so a page holding just the session could not otherwise sign out
|
|
1146
1152
|
const sessionPath = path.replace(/\/+$/, '');
|
|
1153
|
+
// THE HAND-OFF: a page of no UI that opens this World's session from the token in its fragment, then goes to the page
|
|
1154
|
+
// of this World it names (`land`): how a page elsewhere links to this World's own pages (the console's comparison
|
|
1155
|
+
// opening a vendor's page in a copy of the World, read only). The token never reaches the server in a URL; its script
|
|
1156
|
+
// is a file of this origin, so the page keeps a strict policy. It is never framed: a session opened in another
|
|
1157
|
+
// site's frame is not this World's own page's
|
|
1158
|
+
if (sessionPath === 'session/hand-off' && request.method === 'GET') {
|
|
1159
|
+
return new Response(`<!doctype html><meta charset="utf-8"><title>${this.served}</title><body data-world="${this.served}"><script src="/-/${this.served}/session/hand-off.js"></script></body>`, { headers: { 'content-type': 'text/html; charset=utf-8', 'content-security-policy': "default-src 'none'; script-src 'self'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'", 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' } });
|
|
1160
|
+
}
|
|
1161
|
+
if (sessionPath === 'session/hand-off.js' && request.method === 'GET') {
|
|
1162
|
+
const script = `(() => {
|
|
1163
|
+
const world = document.body.dataset.world;
|
|
1164
|
+
const carried = new URLSearchParams(location.hash.slice(1));
|
|
1165
|
+
history.replaceState(null, '', location.pathname);
|
|
1166
|
+
const token = carried.get('token') || '';
|
|
1167
|
+
const land = carried.get('land') || '';
|
|
1168
|
+
// resolved as the browser will (encoded dots too), on this origin and under this World
|
|
1169
|
+
let to = null;
|
|
1170
|
+
try { const u = new URL(land, location.origin); if (land.startsWith('/') && !/[\\\\\\s]/.test(land) && u.origin === location.origin && u.pathname.startsWith('/' + world + '/')) to = u.pathname + u.search; } catch {}
|
|
1171
|
+
const open = () => fetch('/-/' + world + '/session', { method: 'POST', headers: { 'x-volter-token': token } });
|
|
1172
|
+
if (!token || !to) { document.body.textContent = 'Nothing to open.'; return; }
|
|
1173
|
+
// a session this browser already holds here is kept: a page elsewhere cannot replace, downgrade or fix it
|
|
1174
|
+
fetch('/-/' + world + '/session')
|
|
1175
|
+
.then((held) => (held.ok ? held : open()))
|
|
1176
|
+
.then((r) => { if (r.ok) location.replace(to); else document.body.textContent = 'This World did not open.'; })
|
|
1177
|
+
.catch(() => { document.body.textContent = 'This World did not answer.'; });
|
|
1178
|
+
})();
|
|
1179
|
+
`;
|
|
1180
|
+
return new Response(script, { headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-store' } });
|
|
1181
|
+
}
|
|
1147
1182
|
// a person signing in from a page (the console's form, posted to this origin): the token in the
|
|
1148
1183
|
// form body, the session set here, then on to a page of this World
|
|
1149
1184
|
// a form or beacon from another site never opens or ends a session here (the console, on this host's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.12",
|
|
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.12",
|
|
72
|
+
"@volter/world-console": "3.0.7"
|
|
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.12",
|
|
79
79
|
"pg-gateway": "0.3.0-beta.4",
|
|
80
80
|
"smol-toml": "^1.8.0",
|
|
81
81
|
"yaml": "2.9.0"
|
package/src/cli-commands.ts
CHANGED
|
@@ -802,11 +802,15 @@ export async function main(): Promise<void> {
|
|
|
802
802
|
caFile = join(mkdtempSync(join(tmpdir(), 'volter-attach-')), 'ca.pem');
|
|
803
803
|
writeFileSync(caFile, manifest.ca);
|
|
804
804
|
}
|
|
805
|
+
// the World's twins, each a loopback listener here that adds its key, so a command with its own HTTP stack reaches
|
|
806
|
+
// them as it would a local World's (the manifest pointed at the listeners before its env is made)
|
|
807
|
+
const { bridgeTwins } = createRequire(import.meta.url)('@volter/world-core/http-bridge') as { bridgeTwins: (manifest: WorldManifest, key: string, ca?: string) => Promise<unknown[]> };
|
|
808
|
+
await bridgeTwins(manifest, manifest.env.VOLTER_TWINS_KEY ?? attachToken ?? '', manifest.ca ?? undefined);
|
|
805
809
|
const injectPath = createRequire(import.meta.url).resolve('@volter/world-core/inject');
|
|
806
810
|
const env = remoteAttachEnv(manifest, { injectPath, ...(caFile === undefined ? {} : { caFile }) });
|
|
807
811
|
// the world's byte-stream doors, bridged to loopback listeners here, beside a command in any language
|
|
808
812
|
const { bridgeStreams, drainStreams } = createRequire(import.meta.url)('@volter/world-core/stream-bridge') as { bridgeStreams: (streams: WorldManifest['streams'], token: string) => Promise<{ env: Record<string, string> }>; drainStreams: (ms?: number) => Promise<void> };
|
|
809
|
-
const bridged = manifest.streams ? await bridgeStreams(manifest.streams,
|
|
813
|
+
const bridged = manifest.streams ? await bridgeStreams(manifest.streams, manifest.env.VOLTER_TWINS_KEY ?? attachToken ?? '') : { env: {} };
|
|
810
814
|
// VOLTER_WORLD names the World to the injector: a request addressed to its paths carries its key
|
|
811
815
|
const commandEnv: Record<string, string | undefined> = withoutVaultSignIn({ ...process.env, ...env, ...bridged.env, VOLTER_WORLD: resolved.ref });
|
|
812
816
|
commandEnv.VOLTER_TWIN_INJECT_QUIET = head.includes('--verbose') ? '0' : commandEnv.VOLTER_TWIN_INJECT_QUIET ?? '1';
|
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, TWIN_SITES_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink, type BoardFrame } 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, ORIGINAL_PATH_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink, type BoardFrame } 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';
|
|
@@ -869,6 +869,9 @@ export class WorldDoors {
|
|
|
869
869
|
// the request's trace: the caller's trace-id goes on (or a new one), under a parent-id the World mints, which the
|
|
870
870
|
// request's log entries carry, so each entry is joined to its caller by an id no caller chose
|
|
871
871
|
headers.delete(CALLER_HEADER);
|
|
872
|
+
// the address a request was signed for (a CLI's loopback bridge says it, @volter/world-core/http-bridge) is a
|
|
873
|
+
// credential holder's word: a browser's session cannot name another, so a signature is checked on the path it reached
|
|
874
|
+
if (via === 'session') { headers.delete(ORIGINAL_PATH_HEADER); headers.delete('x-volter-twin-original-host'); }
|
|
872
875
|
const who = this.callerOf(request); if (who) headers.set(CALLER_HEADER, callerForHeader(who));
|
|
873
876
|
headers.set('traceparent', traceparentForDelivery(headers.get('traceparent')) ?? newTraceparent());
|
|
874
877
|
// at a twin that enforces the read scope, every request of the read token is marked read-only (a GET
|
|
@@ -997,6 +1000,36 @@ export class WorldDoors {
|
|
|
997
1000
|
// ending a browser's session needs no token: it only clears the caller's own cookie, which never
|
|
998
1001
|
// reaches these /-/ paths, so a page holding just the session could not otherwise sign out
|
|
999
1002
|
const sessionPath = path.replace(/\/+$/, '');
|
|
1003
|
+
// THE HAND-OFF: a page of no UI that opens this World's session from the token in its fragment, then goes to the page
|
|
1004
|
+
// of this World it names (`land`): how a page elsewhere links to this World's own pages (the console's comparison
|
|
1005
|
+
// opening a vendor's page in a copy of the World, read only). The token never reaches the server in a URL; its script
|
|
1006
|
+
// is a file of this origin, so the page keeps a strict policy. It is never framed: a session opened in another
|
|
1007
|
+
// site's frame is not this World's own page's
|
|
1008
|
+
if (sessionPath === 'session/hand-off' && request.method === 'GET') {
|
|
1009
|
+
return new Response(`<!doctype html><meta charset="utf-8"><title>${this.served}</title><body data-world="${this.served}"><script src="/-/${this.served}/session/hand-off.js"></script></body>`,
|
|
1010
|
+
{ headers: { 'content-type': 'text/html; charset=utf-8', 'content-security-policy': "default-src 'none'; script-src 'self'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'", 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' } });
|
|
1011
|
+
}
|
|
1012
|
+
if (sessionPath === 'session/hand-off.js' && request.method === 'GET') {
|
|
1013
|
+
const script = `(() => {
|
|
1014
|
+
const world = document.body.dataset.world;
|
|
1015
|
+
const carried = new URLSearchParams(location.hash.slice(1));
|
|
1016
|
+
history.replaceState(null, '', location.pathname);
|
|
1017
|
+
const token = carried.get('token') || '';
|
|
1018
|
+
const land = carried.get('land') || '';
|
|
1019
|
+
// resolved as the browser will (encoded dots too), on this origin and under this World
|
|
1020
|
+
let to = null;
|
|
1021
|
+
try { const u = new URL(land, location.origin); if (land.startsWith('/') && !/[\\\\\\s]/.test(land) && u.origin === location.origin && u.pathname.startsWith('/' + world + '/')) to = u.pathname + u.search; } catch {}
|
|
1022
|
+
const open = () => fetch('/-/' + world + '/session', { method: 'POST', headers: { 'x-volter-token': token } });
|
|
1023
|
+
if (!token || !to) { document.body.textContent = 'Nothing to open.'; return; }
|
|
1024
|
+
// a session this browser already holds here is kept: a page elsewhere cannot replace, downgrade or fix it
|
|
1025
|
+
fetch('/-/' + world + '/session')
|
|
1026
|
+
.then((held) => (held.ok ? held : open()))
|
|
1027
|
+
.then((r) => { if (r.ok) location.replace(to); else document.body.textContent = 'This World did not open.'; })
|
|
1028
|
+
.catch(() => { document.body.textContent = 'This World did not answer.'; });
|
|
1029
|
+
})();
|
|
1030
|
+
`;
|
|
1031
|
+
return new Response(script, { headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-store' } });
|
|
1032
|
+
}
|
|
1000
1033
|
// a person signing in from a page (the console's form, posted to this origin): the token in the
|
|
1001
1034
|
// form body, the session set here, then on to a page of this World
|
|
1002
1035
|
// a form or beacon from another site never opens or ends a session here (the console, on this host's
|