@volter/world-runtime 3.0.37 → 3.0.39
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/known-external-services.json +3 -10
- package/dist/src/actions-runner-cli.js +3 -3
- package/dist/src/actions-runner.js +2 -1
- package/dist/src/app-url.js +2 -1
- package/dist/src/backings.d.ts +10 -1
- package/dist/src/backings.js +66 -7
- package/dist/src/branch.js +85 -52
- package/dist/src/browser-routes.cjs +59 -0
- package/dist/src/ca-trust.js +4 -3
- package/dist/src/changeset.js +4 -3
- package/dist/src/cli-commands.js +25 -9
- package/dist/src/cli.js +2 -1
- package/dist/src/command-lifetime.d.mts +12 -0
- package/dist/src/command-lifetime.d.ts +1 -15
- package/dist/src/command-lifetime.js +1 -98
- package/dist/src/command-lifetime.mjs +90 -0
- package/dist/src/configs.js +1 -1
- package/dist/src/console-apart.js +3 -2
- package/dist/src/covers.d.ts +3 -2
- package/dist/src/covers.js +95 -18
- package/dist/src/error-message.d.mts +1 -0
- package/dist/src/error-message.d.ts +1 -0
- package/dist/src/error-message.js +2 -0
- package/dist/src/error-message.mjs +10 -0
- package/dist/src/guest-command.d.mts +4 -0
- package/dist/src/guest-command.mjs +26 -0
- package/dist/src/guest-control.d.mts +1 -0
- package/dist/src/guest-control.mjs +17 -0
- package/dist/src/guest-entry.mjs +146 -0
- package/dist/src/guest-processes.d.mts +9 -0
- package/dist/src/guest-processes.mjs +47 -0
- package/dist/src/guest-runner.d.ts +6 -0
- package/dist/src/guest-runner.js +567 -0
- package/dist/src/host.js +5 -4
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/infra-cli.js +4 -0
- package/dist/src/infra.js +133 -23
- package/dist/src/init.js +60 -23
- package/dist/src/lifecycle-record.d.ts +3 -1
- package/dist/src/lifecycle-record.js +29 -6
- package/dist/src/livekit-backing.mjs +37 -0
- package/dist/src/local-branches.js +11 -3
- package/dist/src/mongodb-backing.d.ts +7 -0
- package/dist/src/mongodb-backing.js +58 -0
- package/dist/src/native-postgres-backing.d.ts +4 -0
- package/dist/src/native-postgres-backing.js +404 -0
- package/dist/src/origin.js +2 -1
- package/dist/src/owned-path.d.ts +4 -0
- package/dist/src/owned-path.js +37 -0
- package/dist/src/pack-facts.d.ts +1 -0
- package/dist/src/pglite-backing.d.ts +4 -13
- package/dist/src/pglite-backing.js +198 -106
- package/dist/src/pglite-host.mjs +47 -17
- package/dist/src/placeholder.d.ts +3 -2
- package/dist/src/placeholder.js +9 -4
- package/dist/src/process-groups.d.mts +2 -0
- package/dist/src/process-groups.d.ts +1 -4
- package/dist/src/process-groups.js +1 -49
- package/dist/src/process-groups.mjs +59 -0
- package/dist/src/process-receipt.d.mts +6 -0
- package/dist/src/process-receipt.d.ts +11 -0
- package/dist/src/process-receipt.js +3 -0
- package/dist/src/process-receipt.mjs +45 -0
- package/dist/src/process-signal.d.mts +9 -0
- package/dist/src/process-signal.mjs +92 -0
- package/dist/src/project-inspect.d.ts +1 -1
- package/dist/src/project-inspect.js +4 -2
- package/dist/src/proxy-daemon.js +2 -1
- package/dist/src/redirect-proxy.js +186 -19
- package/dist/src/redis-backing.js +99 -35
- package/dist/src/reflect.js +2 -1
- package/dist/src/root.js +7 -6
- package/dist/src/run-task-worker.js +2 -1
- package/dist/src/runtime.d.ts +24 -7
- package/dist/src/runtime.js +703 -286
- package/dist/src/schema.d.ts +12 -1
- package/dist/src/schema.js +20 -5
- package/dist/src/served-world.d.ts +1 -0
- package/dist/src/served-world.js +26 -14
- package/dist/src/service-recorder.js +8 -3
- package/dist/src/signals.d.mts +1 -0
- package/dist/src/signals.d.ts +1 -1
- package/dist/src/signals.js +1 -11
- package/dist/src/signals.mjs +10 -0
- package/dist/src/tail.d.ts +2 -0
- package/dist/src/tail.js +9 -2
- package/dist/src/tcp-port.js +27 -13
- package/dist/src/temporal-backing.mjs +28 -0
- package/dist/src/up-cli.js +8 -3
- package/dist/src/up-task-worker.js +2 -1
- package/dist/src/websocket-relay.d.ts +1 -1
- package/dist/src/websocket-relay.js +23 -10
- package/dist/src/world-view.js +2 -2
- package/known-external-services.json +3 -10
- package/package.json +12 -7
- package/src/actions-runner-cli.ts +3 -3
- package/src/actions-runner.ts +2 -1
- package/src/app-url.ts +2 -1
- package/src/backings.ts +65 -7
- package/src/branch.ts +73 -34
- package/src/browser-routes.cjs +59 -0
- package/src/ca-trust.ts +4 -3
- package/src/changeset.ts +4 -3
- package/src/cli-commands.ts +21 -9
- package/src/cli.ts +2 -1
- package/src/command-lifetime.d.mts +12 -0
- package/src/command-lifetime.mjs +90 -0
- package/src/command-lifetime.ts +1 -79
- package/src/configs.ts +1 -1
- package/src/console-apart.ts +2 -1
- package/src/covers.ts +97 -19
- package/src/error-message.d.mts +1 -0
- package/src/error-message.mjs +10 -0
- package/src/error-message.ts +2 -0
- package/src/guest-command.d.mts +4 -0
- package/src/guest-command.mjs +26 -0
- package/src/guest-control.d.mts +1 -0
- package/src/guest-control.mjs +17 -0
- package/src/guest-entry.mjs +146 -0
- package/src/guest-processes.d.mts +9 -0
- package/src/guest-processes.mjs +47 -0
- package/src/guest-runner.ts +414 -0
- package/src/host.ts +5 -4
- package/src/index.ts +1 -0
- package/src/infra-cli.ts +4 -0
- package/src/infra.ts +103 -22
- package/src/init.ts +55 -22
- package/src/lifecycle-record.ts +21 -6
- package/src/livekit-backing.mjs +37 -0
- package/src/local-branches.ts +6 -3
- package/src/mongodb-backing.ts +55 -0
- package/src/native-postgres-backing.ts +329 -0
- package/src/origin.ts +2 -1
- package/src/owned-path.ts +32 -0
- package/src/pack-facts.ts +1 -0
- package/src/pglite-backing.ts +155 -79
- package/src/pglite-host.mjs +47 -17
- package/src/placeholder.ts +10 -5
- package/src/process-groups.d.mts +2 -0
- package/src/process-groups.mjs +59 -0
- package/src/process-groups.ts +1 -33
- package/src/process-receipt.d.mts +6 -0
- package/src/process-receipt.mjs +45 -0
- package/src/process-receipt.ts +5 -0
- package/src/process-signal.d.mts +9 -0
- package/src/process-signal.mjs +92 -0
- package/src/project-inspect.ts +4 -2
- package/src/proxy-daemon.ts +2 -1
- package/src/redirect-proxy.ts +153 -18
- package/src/redis-backing.ts +74 -25
- package/src/reflect.ts +2 -1
- package/src/root.ts +7 -6
- package/src/run-task-worker.ts +2 -1
- package/src/runtime.ts +596 -265
- package/src/schema.ts +30 -6
- package/src/served-world.ts +21 -15
- package/src/service-recorder.ts +4 -2
- package/src/signals.d.mts +1 -0
- package/src/signals.mjs +10 -0
- package/src/signals.ts +1 -10
- package/src/tail.ts +10 -3
- package/src/tcp-port.ts +24 -13
- package/src/temporal-backing.mjs +28 -0
- package/src/up-cli.ts +6 -3
- package/src/up-task-worker.ts +2 -1
- package/src/websocket-relay.ts +18 -11
- package/src/world-view.ts +2 -2
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
},
|
|
20
20
|
"@daily-co/daily-js": {
|
|
21
21
|
"disposition": "demanded",
|
|
22
|
-
"reason": "vendor daily (daily.co), its call client (the call machine from c.daily.co, signalling on gs.daily.co, WebRTC media): the
|
|
22
|
+
"reason": "vendor daily (daily.co), its call client (the call machine from c.daily.co, signalling on gs.daily.co, WebRTC media): the Daily REST spec does not describe call-client media or signalling, so this SDK remains an uncovered wire"
|
|
23
23
|
},
|
|
24
24
|
"@daily-co/daily-react": {
|
|
25
25
|
"disposition": "demanded",
|
|
26
|
-
"reason": "vendor daily (daily.co), React hooks over its call client (@daily-co/daily-js): the
|
|
26
|
+
"reason": "vendor daily (daily.co), React hooks over its call client (@daily-co/daily-js): the Daily REST spec does not describe call-client media or signalling, so this SDK remains an uncovered wire"
|
|
27
27
|
},
|
|
28
28
|
"@azure/communication-email": {
|
|
29
29
|
"disposition": "demanded",
|
|
@@ -193,10 +193,6 @@
|
|
|
193
193
|
"disposition": "demanded",
|
|
194
194
|
"reason": "vendor matrix, pack demanded by census 2026-08-31 (×1)"
|
|
195
195
|
},
|
|
196
|
-
"node-telegram-bot-api": {
|
|
197
|
-
"disposition": "demanded",
|
|
198
|
-
"reason": "vendor telegram, pack demanded by census 2026-08-31 (×2); already curated in covers.ts UNKNOWN_DEP_KNOWN_SERVICES"
|
|
199
|
-
},
|
|
200
196
|
"npm-registry@npm.jsr.io": {
|
|
201
197
|
"disposition": "acknowledged",
|
|
202
198
|
"reason": "acknowledged: registry destination — the npm-registry twin deliberately owns npmjs + GitHub only (census 2026-08-31, ×2)"
|
|
@@ -217,10 +213,6 @@
|
|
|
217
213
|
"disposition": "demanded",
|
|
218
214
|
"reason": "vendor shopify, well-known service client with no twin pack (curated in covers.ts UNKNOWN_DEP_KNOWN_SERVICES)"
|
|
219
215
|
},
|
|
220
|
-
"telegraf": {
|
|
221
|
-
"disposition": "demanded",
|
|
222
|
-
"reason": "vendor telegram, pack demanded by census 2026-08-31 (×1); already curated in covers.ts UNKNOWN_DEP_KNOWN_SERVICES"
|
|
223
|
-
},
|
|
224
216
|
"twenty-client-sdk": {
|
|
225
217
|
"disposition": "demanded",
|
|
226
218
|
"reason": "vendor twenty, pack demanded by census 2026-08-31 (×1)"
|
|
@@ -925,6 +917,7 @@
|
|
|
925
917
|
}
|
|
926
918
|
},
|
|
927
919
|
"notExternal": {
|
|
920
|
+
"@chat-adapter/state-memory": "Chat SDK local in-memory state adapter, no external connection: @chat-adapter/state-memory 4.39.0 dist/index.js stores subscriptions in Set and locks, cache and queues in Map; https://chat-sdk.dev/adapters/official/memory",
|
|
928
921
|
"@agentclientprotocol/sdk": "Agent Client Protocol library, not a vendor",
|
|
929
922
|
"@ai-sdk/devtools": "local dev-tools UI for the AI SDK",
|
|
930
923
|
"@ai-sdk/langchain": "LangChain interop adapter, not a service",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.39",
|
|
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",
|
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
".": {
|
|
42
42
|
"types": "./dist/src/index.d.ts",
|
|
43
43
|
"default": "./dist/src/index.js"
|
|
44
|
-
}
|
|
44
|
+
},
|
|
45
|
+
"./src/pglite-host.mjs": "./src/pglite-host.mjs",
|
|
46
|
+
"./browser-routes": "./src/browser-routes.cjs"
|
|
45
47
|
},
|
|
46
48
|
"bin": {
|
|
47
49
|
"volter-world": "dist/src/cli.js",
|
|
@@ -60,7 +62,6 @@
|
|
|
60
62
|
"devDependencies": {
|
|
61
63
|
"@types/bun": "^1.2.20",
|
|
62
64
|
"@types/node": "^24.0.0",
|
|
63
|
-
"mongodb": "~6.20.0",
|
|
64
65
|
"pg": "8.16.3",
|
|
65
66
|
"typescript": "^5.9.0"
|
|
66
67
|
},
|
|
@@ -68,17 +69,21 @@
|
|
|
68
69
|
"node": ">=22.3"
|
|
69
70
|
},
|
|
70
71
|
"peerDependencies": {
|
|
71
|
-
"@volter/world-core": "3.0.
|
|
72
|
-
"@volter/world-console": "3.0.
|
|
72
|
+
"@volter/world-core": "3.0.39",
|
|
73
|
+
"@volter/world-console": "3.0.34"
|
|
73
74
|
},
|
|
74
75
|
"dependencies": {
|
|
75
76
|
"@electric-sql/pglite": "0.5.8",
|
|
76
77
|
"@electric-sql/pglite-pgvector": "0.0.9",
|
|
77
78
|
"@volter/world-access": "3.0.1",
|
|
78
|
-
"@volter/world-core": "3.0.
|
|
79
|
+
"@volter/world-core": "3.0.39",
|
|
80
|
+
"mongodb": "~6.20.0",
|
|
79
81
|
"pg-gateway": "0.3.0-beta.4",
|
|
80
82
|
"smol-toml": "^1.8.0",
|
|
81
|
-
"yaml": "2.9.0"
|
|
83
|
+
"yaml": "2.9.0",
|
|
84
|
+
"@volter/browser-host-node": "0.1.84",
|
|
85
|
+
"@volter/browser-sandboxes": "0.1.84",
|
|
86
|
+
"@volter/browser-runtime": "0.1.104"
|
|
82
87
|
},
|
|
83
88
|
"peerDependenciesMeta": {
|
|
84
89
|
"@volter/world-console": {
|
|
@@ -8,9 +8,9 @@ import { mkdirSync } from 'node:fs';
|
|
|
8
8
|
import { runQueued } from './actions-runner.ts';
|
|
9
9
|
|
|
10
10
|
const args = process.argv.slice(2);
|
|
11
|
-
const github = optionValue(args, '--github')
|
|
12
|
-
const token = optionValue(args, '--token')
|
|
13
|
-
const work = optionValue(args, '--work'
|
|
11
|
+
const github = optionValue(args, '--github') || process.env.GITHUB_API_URL;
|
|
12
|
+
const token = optionValue(args, '--token') || process.env.GITHUB_TOKEN;
|
|
13
|
+
const work = optionValue(args, '--work', '.volter/actions-runner');
|
|
14
14
|
if (!github || !token) {
|
|
15
15
|
process.stderr.write('usage: volter-world-actions-runner --github <url> --token <token> [--work <dir>] [--once]\n');
|
|
16
16
|
process.exit(2);
|
package/src/actions-runner.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// What it does not do (named, so a workflow that needs them fails plainly): matrices beyond their first combination,
|
|
9
9
|
// services and containers, composite and JavaScript actions from the marketplace, reusable workflows, concurrency groups.
|
|
10
|
+
import { errorMessage } from './error-message.ts';
|
|
10
11
|
import { spawn } from 'node:child_process';
|
|
11
12
|
import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
13
|
import { join } from 'node:path';
|
|
@@ -222,7 +223,7 @@ export async function runOnce(opts: ActionsRunnerOptions, repo: string, run: Row
|
|
|
222
223
|
if (failed) conclusion = 'failure';
|
|
223
224
|
}
|
|
224
225
|
} catch (error) {
|
|
225
|
-
log(`run ${repo}#${id}: ${(error
|
|
226
|
+
log(`run ${repo}#${id}: ${errorMessage(error)}`);
|
|
226
227
|
conclusion = 'failure';
|
|
227
228
|
} finally {
|
|
228
229
|
await api(opts, 'POST', `/_twin/repos/${repo}/actions/runs/${id}/complete`, { conclusion });
|
package/src/app-url.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// once, at the caller's request, and stores what it saw. The record lives in the instance dir, so
|
|
20
20
|
// a re-`up` (which wipes the dir) or `down --purge` clears it: a recorded URL never outlives the
|
|
21
21
|
// instance it described.
|
|
22
|
+
import { errorMessage } from './error-message.ts';
|
|
22
23
|
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
23
24
|
import { withFileLock } from '@volter/world-core/runtime';
|
|
24
25
|
import { join } from 'node:path';
|
|
@@ -223,7 +224,7 @@ export function addAppHosts(name: string, hosts: string[], options: { root?: str
|
|
|
223
224
|
function listeningTcpPorts(pid: number): number[] {
|
|
224
225
|
const result = spawnSync('lsof', ['-a', '-p', String(pid), '-iTCP', '-sTCP:LISTEN', '-P', '-n', '-Fn'], { windowsHide: true, encoding: 'utf8' });
|
|
225
226
|
if (result.error) {
|
|
226
|
-
throw new Error(`volter-world app-url: \`lsof\` is unavailable (${result.error
|
|
227
|
+
throw new Error(`volter-world app-url: \`lsof\` is unavailable (${errorMessage(result.error)}) — pass the port, or --set the URL directly`);
|
|
227
228
|
}
|
|
228
229
|
const ports = new Set<number>();
|
|
229
230
|
for (const line of (result.stdout ?? '').split('\n')) {
|
package/src/backings.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
1
2
|
import type { WorldServiceConfig } from './schema.ts';
|
|
2
3
|
|
|
3
4
|
export type ServerBackingFacts = {
|
|
4
5
|
serverBacking: { command: string };
|
|
6
|
+
maxRequestBodySize?: number;
|
|
5
7
|
adoption: { sdks: string[]; pypi: string[]; envStems: string[] };
|
|
6
8
|
hostsNone: string;
|
|
7
9
|
endpointEnv: { name: string; templates: Record<string, string>; note: string };
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
/**
|
|
13
|
+
* MongoDB is declared infrastructure, not an API service in this table: its native server uses
|
|
14
|
+
* mongodb-backing.ts through the same managed lifecycle as Postgres (pglite-backing.ts).
|
|
15
|
+
*
|
|
11
16
|
* A vendor whose open-source server a World runs as its service instead of a twin (architecture, "A vendor's own
|
|
12
|
-
* server as a backing"): the media plane no pack can hold as data. The service is the vendor's own binary on an 'auto'
|
|
17
|
+
* server as a backing"): the wire or media plane no pack can hold as data. The service is the vendor's own binary on an 'auto'
|
|
13
18
|
* port, loopback only; the application's key and secret are the ones the server is started with, so its SDK signs
|
|
14
19
|
* what the server verifies. What the server does not do on its own (LiveKit's egress, a separate service) the
|
|
15
20
|
* service's note names.
|
|
@@ -19,6 +24,31 @@ export const VENDOR_SERVER_BACKINGS: Record<string, { service: WorldServiceConfi
|
|
|
19
24
|
// livekit-server wants a secret of at least 32 characters
|
|
20
25
|
const secret = 'twin-livekit-secret-for-the-worlds-own-server';
|
|
21
26
|
return {
|
|
27
|
+
temporal: {
|
|
28
|
+
service: {
|
|
29
|
+
'//': 'Temporal CLI development server on PATH, loopback only, SQLite in World state. No cluster, Cloud, TLS, World clock or branching; reset clears workflow history. See cookbook/temporal-backing.',
|
|
30
|
+
id: 'temporal', type: 'process', command: 'node',
|
|
31
|
+
args: [fileURLToPath(new URL('./temporal-backing.mjs', import.meta.url))],
|
|
32
|
+
port: 'auto', portArg: false, controlPlane: true, branchState: 'unsupported',
|
|
33
|
+
// Existing service readiness budgets, no new timeout. Native dev-server checkout took 0.84 s
|
|
34
|
+
// on 6e0123e + backing error handling changes with Node 23.9.0; SDK consumer only in this World, host not isolated.
|
|
35
|
+
// Reproduce: cookbook/temporal-backing/README.md, timed checkout and reset commands.
|
|
36
|
+
ready: { command: 'node', args: [fileURLToPath(new URL('./temporal-backing.mjs', import.meta.url)), '--health'] },
|
|
37
|
+
injectEnvTemplates: {
|
|
38
|
+
TEMPORAL_ADDRESS: '${host}:${port}', TEMPORAL_NAMESPACE: 'default', TEMPORAL_TLS: 'false', TEMPORAL_API_KEY: '',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
env: { TEMPORAL_NAMESPACE: 'default', TEMPORAL_TLS: 'false', TEMPORAL_API_KEY: '' },
|
|
42
|
+
facts: {
|
|
43
|
+
serverBacking: { command: 'temporal' },
|
|
44
|
+
adoption: { sdks: ['@temporalio/client', '@temporalio/worker', '@temporalio/workflow', 'nestjs-temporal-core'],
|
|
45
|
+
pypi: ['temporalio'], envStems: ['TEMPORAL'] },
|
|
46
|
+
hostsNone: 'The World runs the vendor development server at its declared loopback gRPC endpoint.',
|
|
47
|
+
endpointEnv: { name: 'TEMPORAL_ADDRESS', templates: {
|
|
48
|
+
TEMPORAL_ADDRESS: '${host}:${port}', TEMPORAL_NAMESPACE: 'default', TEMPORAL_TLS: 'false', TEMPORAL_API_KEY: '',
|
|
49
|
+
}, note: 'The local development server creates default; no TLS or Cloud API key.' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
22
52
|
livekit: {
|
|
23
53
|
service: {
|
|
24
54
|
'//': 'LiveKit\'s own server (livekit-server, Apache-2.0; `brew install livekit` or LiveKit\'s install script puts it '
|
|
@@ -27,11 +57,11 @@ export const VENDOR_SERVER_BACKINGS: Record<string, { service: WorldServiceConfi
|
|
|
27
57
|
+ `"webhook: {api_key: ${key}, urls: [<the app's webhook URL>]}" to args.`,
|
|
28
58
|
id: 'livekit',
|
|
29
59
|
type: 'twin',
|
|
30
|
-
command: '
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
args: ['--bind', '127.0.0.1', '--node-ip', '127.0.0.1', '--keys', `${key}: ${secret}`, '--rtc.tcp_port', '0'],
|
|
60
|
+
command: 'node',
|
|
61
|
+
// The wrapper supplies a local TURN relay using UDP at the allocated HTTP port.
|
|
62
|
+
args: [fileURLToPath(new URL('./livekit-backing.mjs', import.meta.url)), '--keys', `${key}: ${secret}`],
|
|
34
63
|
port: 'auto',
|
|
64
|
+
portArg: false,
|
|
35
65
|
rootArg: false,
|
|
36
66
|
controlPlane: true,
|
|
37
67
|
ready: { httpUrl: '${url}/' },
|
|
@@ -43,9 +73,11 @@ export const VENDOR_SERVER_BACKINGS: Record<string, { service: WorldServiceConfi
|
|
|
43
73
|
},
|
|
44
74
|
env: { LIVEKIT_API_KEY: key, LIVEKIT_API_SECRET: secret },
|
|
45
75
|
facts: {
|
|
46
|
-
//
|
|
76
|
+
// LiveKit's client and server SDKs use its own server as the backing.
|
|
47
77
|
serverBacking: { command: 'livekit-server' },
|
|
48
|
-
adoption: { sdks: ['livekit-client', 'livekit-server-sdk', '@livekit/rtc-node'
|
|
78
|
+
adoption: { sdks: ['livekit-client', 'livekit-server-sdk', '@livekit/rtc-node', '@livekit/react-native',
|
|
79
|
+
'@livekit/react-native-expo-plugin', '@livekit/react-native-webrtc'],
|
|
80
|
+
pypi: ['livekit', 'livekit-agents', 'livekit-api'], envStems: ['LIVEKIT'] },
|
|
49
81
|
hostsNone: 'The World runs the vendor server on its declared endpoint; SDKs read LIVEKIT_URL.',
|
|
50
82
|
endpointEnv: { name: 'LIVEKIT_URL', templates: { LIVEKIT_URL: 'ws://${host}:${port}', LIVEKIT_API_KEY: key, LIVEKIT_API_SECRET: secret }, note: 'Credentials and URL are the same ones the World starts its LiveKit server with.' },
|
|
51
83
|
},
|
|
@@ -53,3 +85,29 @@ export const VENDOR_SERVER_BACKINGS: Record<string, { service: WorldServiceConfi
|
|
|
53
85
|
};
|
|
54
86
|
})();
|
|
55
87
|
|
|
88
|
+
/** Declare a backing's application-owned env stems as service bindings; omitted stems use the vendor's defaults. */
|
|
89
|
+
export function serverBackingService(vendor: string, declaration: { envStems?: readonly string[]; service?: WorldServiceConfig } = {}): WorldServiceConfig {
|
|
90
|
+
const backing = VENDOR_SERVER_BACKINGS[vendor];
|
|
91
|
+
// Native infrastructure supplies its already-declared lifecycle rather than an API service from the table.
|
|
92
|
+
const service = declaration.service ?? backing?.service;
|
|
93
|
+
if (!service) throw new Error(`No server backing declared for ${vendor}`);
|
|
94
|
+
const base = backing?.facts.adoption.envStems[0] ?? vendor.toUpperCase();
|
|
95
|
+
const stems = declaration.envStems ?? backing?.facts.adoption.envStems ?? [base];
|
|
96
|
+
if (!stems.length || stems.some((stem) => !/^[A-Z][A-Z0-9_]*$/.test(stem))) throw new Error('Server backing env stems must be nonempty uppercase env names');
|
|
97
|
+
const unique = [...new Set(stems)];
|
|
98
|
+
const bindingName = (name: string, stem: string): string => {
|
|
99
|
+
if (!name.startsWith(`${base}_`)) throw new Error(`Server backing ${vendor} binding ${name} has no vendor stem`);
|
|
100
|
+
return `${stem}${name.slice(base.length)}`;
|
|
101
|
+
};
|
|
102
|
+
const injectEnvTemplates = Object.fromEntries(unique.flatMap((stem) =>
|
|
103
|
+
Object.entries(service.injectEnvTemplates ?? {}).map(([name, value]) => [bindingName(name, stem), value])));
|
|
104
|
+
const external = service.external === undefined ? undefined : {
|
|
105
|
+
...service.external,
|
|
106
|
+
...(service.external.discover === undefined ? {} : { discover: unique.flatMap((stem) => service.external!.discover!.map((mapping) => {
|
|
107
|
+
const as = bindingName(mapping.as, stem);
|
|
108
|
+
// The producer keeps its own keys; only the application binding gets the declared stem.
|
|
109
|
+
return { ...mapping, as };
|
|
110
|
+
})) }),
|
|
111
|
+
};
|
|
112
|
+
return { ...service, ...(external ? { external } : { injectEnvTemplates }) };
|
|
113
|
+
}
|
package/src/branch.ts
CHANGED
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
// `branch <world> <name>` boots a new world from the base's config and forks every twin's mirror
|
|
4
4
|
// into it (core's `forkTwin`: a POINTER to the base at its current position — the base's whole
|
|
5
5
|
// history, its own entries included, is the branch's parent; nothing is copied). A managed database (containerless
|
|
6
|
-
// Postgres) keeps no history, so it is the one thing copied: the stopped base's data, as of now. Two branches of one
|
|
6
|
+
// Postgres and MongoDB) keeps no history, so it is the one thing copied: the stopped base's data, as of now. Two branches of one
|
|
7
7
|
// base running at once share the database port its definition names; run one at a time (the SDK does). `checkout <name>`
|
|
8
8
|
// brings a stopped world back up from its own instance record, state intact.
|
|
9
|
+
import { runInfrastructure } from './infra.ts';
|
|
9
10
|
import { cpSync, existsSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
10
11
|
import { dirname, join, resolve } from 'node:path';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
13
|
import { captureHistory, historyAtInstant, forkTwin, stateGeneration, withHistoryLocks, worldPaths, worldStateRoot } from '@volter/world-core/runtime';
|
|
13
14
|
import { inheritWorldBrowsers, materializeBrowserFiles } from './browsers.ts';
|
|
14
|
-
import { downWorld, saveWorldInstance, statusWorld, upWorld } from './runtime.ts';
|
|
15
|
+
import { externalServiceEnv, downWorld, recordedWorldNetwork, saveWorldInstance, readWorldBootRecord, statusWorld, upWorld } from './runtime.ts';
|
|
15
16
|
import type { WorldInstance } from './schema.ts';
|
|
16
|
-
import {
|
|
17
|
+
import { loadWorldConfig } from './configs.ts';
|
|
18
|
+
import { containerlessDataDirectory, parseInfraDefinition, pgliteDown, pgliteUp } from './pglite-backing.ts';
|
|
17
19
|
import { redisTwinCli, redisTwinDown, redisTwinUp } from './redis-backing.ts';
|
|
18
20
|
|
|
19
21
|
export type BranchOptions = { root?: string; envFile?: string; forceEnvOutput?: boolean; now?: Date; /** branch from a point in the base's history: an instant, or a position per twin (contract "Just like Neon", 3) */ at?: { instant?: string; positions?: Record<string, number>; views?: Record<string, string> } };
|
|
@@ -27,7 +29,11 @@ function stateServices(controlRoot: string): string[] {
|
|
|
27
29
|
/** A new world over the base's mirrors. */
|
|
28
30
|
export async function branchWorld(base: string, name: string, options: BranchOptions = {}): Promise<{ instance: WorldInstance; forked: Record<string, string[]> }> {
|
|
29
31
|
const root = resolve(options.root ?? process.cwd());
|
|
30
|
-
const from =
|
|
32
|
+
const from = readWorldBootRecord(base, root);
|
|
33
|
+
const { config } = loadWorldConfig(existsSync(from.configPath) ? from.configPath : from.config, root);
|
|
34
|
+
const unsupported = [...new Set([...config.services, ...Object.values(from.services)]
|
|
35
|
+
.filter((service) => service.branchState === 'unsupported').map((service) => service.id))];
|
|
36
|
+
if (unsupported.length) throw new Error(`volter-world branch: services do not support carrying state to a branch: ${unsupported.join(', ')}; retain the original World or reset it explicitly`);
|
|
31
37
|
if (name === base) throw new Error(`volter-world branch: the branch needs a name other than the base's ("${base}")`);
|
|
32
38
|
const envFile = options.envFile ?? join(dirname(from.envFile), `${name}.world.env`);
|
|
33
39
|
const at = (options.now ?? new Date()).toISOString();
|
|
@@ -50,25 +56,9 @@ export async function branchWorld(base: string, name: string, options: BranchOpt
|
|
|
50
56
|
throw new Error(`volter-world branch: ${base} has a managed database, which keeps no history to branch from at an instant or position; branch from its current state`);
|
|
51
57
|
}
|
|
52
58
|
refuseLiveDatabases(from, databases);
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
const
|
|
56
|
-
const began = Date.now();
|
|
57
|
-
let instance: WorldInstance;
|
|
58
|
-
try {
|
|
59
|
-
instance = await upWorld(existsSync(from.configPath) ? from.configPath : from.config, { name, root, mode: from.mode, envFile, forceEnvOutput: options.forceEnvOutput });
|
|
60
|
-
} catch (error) {
|
|
61
|
-
// only a record made during this call is ours to remove, and it is removed by its own createdAt, as the fork's
|
|
62
|
-
// cleanup below does: a concurrent `branch` of the same name that recorded first keeps its World
|
|
63
|
-
let recorded: string | null = null;
|
|
64
|
-
if (!existed) { try { const made = statusWorld(name, root).createdAt; if (Date.parse(made) >= began) recorded = made; } catch { /* the boot recorded nothing */ } }
|
|
65
|
-
if (recorded) {
|
|
66
|
-
try { await downWorld(name, root, { purge: true, expectedCreatedAt: recorded }); } catch (cleanup) {
|
|
67
|
-
throw new Error(`${error instanceof Error ? error.message : String(error)} (and the half-made branch ${name} could not be removed: ${cleanup instanceof Error ? cleanup.message : String(cleanup)})`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
59
|
+
// up owns rollback. If boot throws, retain its diagnostics: a name/time lookup cannot prove
|
|
60
|
+
// that a partial record belongs to this invocation, especially under a frozen World clock.
|
|
61
|
+
const instance = await upWorld(existsSync(from.configPath) ? from.configPath : from.config, { name, root, mode: from.mode, owner: from.lifecycle?.owner ?? from.resources?.owner, networkCeiling: recordedWorldNetwork(from), envFile, forceEnvOutput: options.forceEnvOutput });
|
|
72
62
|
const forked: Record<string, string[]> = {};
|
|
73
63
|
try {
|
|
74
64
|
// the branch's origin is its base's, or none: never one a stopped World of the same name left
|
|
@@ -85,24 +75,30 @@ export async function branchWorld(base: string, name: string, options: BranchOpt
|
|
|
85
75
|
materializeBrowserFiles(instance.dirs.data, root, name, instance.config);
|
|
86
76
|
await branchManagedDatabases(from, instance, databases);
|
|
87
77
|
} catch (error) {
|
|
88
|
-
await downWorld(name, root, { purge: true,
|
|
78
|
+
try { await downWorld(name, root, { purge: true, expectedInstance: instance.instance }); }
|
|
79
|
+
catch (cleanup) { throw new AggregateError([error, cleanup], 'Copied database startup and branch cleanup failed'); }
|
|
89
80
|
throw error;
|
|
90
81
|
}
|
|
91
82
|
return { instance, forked };
|
|
92
83
|
}
|
|
93
84
|
|
|
94
|
-
/** The base's containerless managed databases: each PGlite `pglite-<kind>-data
|
|
95
|
-
* `
|
|
85
|
+
/** The base's containerless managed databases: each PGlite `pglite-<kind>-data`, MongoDB's
|
|
86
|
+
* `mongod-mongodb-data` (pglite-backing.ts), the redis twin's `redis-twin` tree (redis-backing.ts), and native PostgreSQL. A stopped World's
|
|
96
87
|
* hosts leave no pid file; a docker-backed World keeps its data under the container's volume, which a branch does not
|
|
97
88
|
* carry. */
|
|
98
89
|
function managedDatabases(from: WorldInstance): string[] {
|
|
99
90
|
const data = from.dirs.data;
|
|
100
91
|
if (!existsSync(data)) return [];
|
|
101
|
-
|
|
92
|
+
const known = (name: string) => /^pglite-[a-z0-9_-]+-data$/.test(name) || name === 'mongod-mongodb-data' || name === 'native-postgres-data';
|
|
93
|
+
if (readdirSync(data).some(name => name.endsWith('-data') && !known(name) && !from.services[name])) {
|
|
94
|
+
throw new Error('volter-world branch: unrecognised storage; retain the original World');
|
|
95
|
+
}
|
|
96
|
+
return readdirSync(data).filter((n) => /^pglite-[a-z0-9_-]+-data$/.test(n) || n === 'mongod-mongodb-data' || n === 'redis-twin' || n === 'native-postgres-data');
|
|
102
97
|
}
|
|
103
98
|
|
|
104
99
|
/** The pid files of the hosts serving one managed database directory. */
|
|
105
100
|
function hostPidFiles(data: string, db: string): string[] {
|
|
101
|
+
if (db === 'native-postgres-data') return [join(data, 'native-postgres.owner.json')];
|
|
106
102
|
if (db === 'redis-twin') return readdirSync(data).filter((n) => /^redis-twin-\d+\.pid$/.test(n)).map((n) => join(data, n));
|
|
107
103
|
return [join(data, `${db.slice(0, -'-data'.length)}.pid`)];
|
|
108
104
|
}
|
|
@@ -112,7 +108,8 @@ function hostPidFiles(data: string, db: string): string[] {
|
|
|
112
108
|
function refuseLiveDatabases(from: WorldInstance, dbs: string[]): void {
|
|
113
109
|
for (const db of dbs) {
|
|
114
110
|
for (const pidFile of hostPidFiles(from.dirs.data, db)) {
|
|
115
|
-
const
|
|
111
|
+
const recorded = existsSync(pidFile) ? readFileSync(pidFile, 'utf8').trim() : '';
|
|
112
|
+
const pid = db === 'native-postgres-data' && recorded ? JSON.parse(recorded).pid : Number(recorded);
|
|
116
113
|
let live = false;
|
|
117
114
|
if (Number.isInteger(pid) && pid > 0) { try { process.kill(pid, 0); live = true; } catch { live = false; } }
|
|
118
115
|
if (live) throw new Error(`volter-world branch: ${from.name}'s managed ${db} is running (pid ${pid}); stop ${from.name} first so its database can be copied into the branch consistently`);
|
|
@@ -120,16 +117,58 @@ function refuseLiveDatabases(from: WorldInstance, dbs: string[]): void {
|
|
|
120
117
|
}
|
|
121
118
|
}
|
|
122
119
|
|
|
120
|
+
/** Compare actual cold-storage formats, not the edited config's requested backing. */
|
|
121
|
+
function requireMatchingBackings(source: string[], child: string[]): void {
|
|
122
|
+
const kinds = {
|
|
123
|
+
postgres: ['native-postgres-data', 'pglite-postgres-data'],
|
|
124
|
+
mongodb: ['mongod-mongodb-data'],
|
|
125
|
+
};
|
|
126
|
+
for (const [kind, formats] of Object.entries(kinds)) {
|
|
127
|
+
const from = source.filter(directory => formats.includes(directory));
|
|
128
|
+
if (!from.length) continue;
|
|
129
|
+
const to = child.filter(directory => formats.includes(directory));
|
|
130
|
+
if (from.length !== 1 || to.length !== 1 || from[0] !== to[0]) {
|
|
131
|
+
throw new Error(`volter-world branch: ${kind} source backing ${from.join(', ')} differs from child backing ${to.join(', ') || 'none'}; refusing to copy database bytes`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const directory of source) {
|
|
135
|
+
if (!child.includes(directory)) throw new Error(`volter-world branch: source backing ${directory} differs from child backing ${child.join(', ') || 'none'}; refusing to copy database bytes`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
123
139
|
/** The branch's managed databases as the base holds them: the branch's own, just booted empty by `up`, are stopped,
|
|
124
140
|
* replaced by a copy of the base's data, and started again, so a branch of a migrated and seeded World serves that
|
|
125
141
|
* data, as its twins serve the base's history. */
|
|
126
142
|
async function branchManagedDatabases(from: WorldInstance, instance: WorldInstance, dbs: string[]): Promise<void> {
|
|
127
143
|
if (!dbs.length) return;
|
|
144
|
+
requireMatchingBackings(dbs, managedDatabases(instance));
|
|
128
145
|
const definition = join(dirname(instance.configPath), 'world.infrastructure.yml');
|
|
129
|
-
if (!existsSync(definition))
|
|
146
|
+
if (!existsSync(definition)) throw new Error('volter-world branch: source has managed database bytes but child has no infrastructure definition; refusing to copy');
|
|
130
147
|
const services = parseInfraDefinition(readFileSync(definition, 'utf8'));
|
|
131
148
|
const redis = services.filter((s) => s.kind === 'redis');
|
|
132
|
-
const hosted = services.filter((
|
|
149
|
+
const hosted = services.filter((service) => {
|
|
150
|
+
const directory = containerlessDataDirectory(service.kind);
|
|
151
|
+
if (directory === undefined || !dbs.includes(directory)) return false;
|
|
152
|
+
if (!existsSync(join(instance.dirs.data, directory))) {
|
|
153
|
+
throw new Error(`volter-world branch: ${service.kind}'s copied state belongs to a different backing than the branch booted; refusing to replace its database`);
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
});
|
|
157
|
+
const config = loadWorldConfig(instance.configPath, instance.root).config;
|
|
158
|
+
const infrastructure = config.services.find((service) => service.type === 'external' && (service.external?.up[0] === 'volter-world-infra' || service.id === 'infrastructure'));
|
|
159
|
+
if (!infrastructure) throw new Error(`volter-world branch: ${instance.name}'s managed databases have no declared infrastructure service`);
|
|
160
|
+
const launch = { env: externalServiceEnv(infrastructure, instance.dirs, instance.env, config.stripEnv) };
|
|
161
|
+
if (dbs.includes('native-postgres-data')) {
|
|
162
|
+
const io = { out: (text: string) => process.stdout.write(text), err: (text: string) => process.stderr.write(text) };
|
|
163
|
+
if (await runInfrastructure('down', launch.env, io) !== 0) throw new Error('managed native database branch could not stop its new backing');
|
|
164
|
+
for (const db of dbs) {
|
|
165
|
+
const to = join(instance.dirs.data, db);
|
|
166
|
+
rmSync(to, { recursive: true, force: true });
|
|
167
|
+
cpSync(join(from.dirs.data, db), to, { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
if (await runInfrastructure('up', launch.env, io) !== 0) throw new Error('managed native database branch could not resume its copied backing');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
133
172
|
const redisCli = dbs.includes('redis-twin') && redis.length ? redisTwinCli([dirname(fileURLToPath(import.meta.url)), dirname(instance.configPath)]) : undefined;
|
|
134
173
|
await pgliteDown(hosted, instance.dirs.data);
|
|
135
174
|
if (redisCli) await redisTwinDown(redis, instance.dirs.data);
|
|
@@ -139,13 +178,13 @@ async function branchManagedDatabases(from: WorldInstance, instance: WorldInstan
|
|
|
139
178
|
rmSync(to, { recursive: true, force: true });
|
|
140
179
|
cpSync(join(from.dirs.data, db), to, { recursive: true });
|
|
141
180
|
}
|
|
142
|
-
if (
|
|
143
|
-
if (redisCli) await redisTwinUp(redis, instance.dirs.data, redisCli);
|
|
181
|
+
if (hosted.length) await pgliteUp(hosted, instance.dirs.data, launch);
|
|
182
|
+
if (redisCli) await redisTwinUp(redis, instance.dirs.data, redisCli, launch);
|
|
144
183
|
}
|
|
145
184
|
|
|
146
185
|
/** A stopped world back up, from its own record. */
|
|
147
186
|
export async function checkoutWorld(name: string, options: { root?: string } = {}): Promise<WorldInstance> {
|
|
148
187
|
const root = resolve(options.root ?? process.cwd());
|
|
149
|
-
const was =
|
|
150
|
-
return upWorld(existsSync(was.configPath) ? was.configPath : was.config, { name, root, mode: was.mode, envFile: was.envFile, keepState: true });
|
|
188
|
+
const was = readWorldBootRecord(name, root);
|
|
189
|
+
return upWorld(existsSync(was.configPath) ? was.configPath : was.config, { name, root, mode: was.mode, owner: was.lifecycle?.owner ?? was.resources?.owner, envFile: was.envFile, keepState: true });
|
|
151
190
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Bind an existing browser page to a World-attached HTTP transport. A namespace gives the World its own page and cookie
|
|
2
|
+
// paths on that page's origin; vendor URLs remain the wire targets. Published external JavaScript is never rewritten.
|
|
3
|
+
exports.installWorldBrowserRoutes = async function (router, transport, { namespace, origin, onExchange, context = false } = {}) {
|
|
4
|
+
if (namespace && (!/^\/[A-Za-z0-9._~/-]+$/.test(namespace) || !origin)) throw new Error('A World page needs a path namespace and its browser origin');
|
|
5
|
+
const base = namespace?.replace(/\/$/, '');
|
|
6
|
+
const forward = async route => {
|
|
7
|
+
try {
|
|
8
|
+
const request = route.request();
|
|
9
|
+
if (context && namespace) {
|
|
10
|
+
let documentUrl;
|
|
11
|
+
try { documentUrl = request.frame().page().url(); } catch {}
|
|
12
|
+
const ownPath = `${origin}${base}/`;
|
|
13
|
+
if (!request.url().startsWith(ownPath) && !documentUrl?.startsWith(ownPath)) return route.fallback();
|
|
14
|
+
}
|
|
15
|
+
let url = new URL(request.url()), method = request.method(), response;
|
|
16
|
+
let prefix;
|
|
17
|
+
if (namespace && url.origin === origin && url.pathname.startsWith(base + '/')) {
|
|
18
|
+
const rest = url.pathname.slice(base.length + 1), at = rest.indexOf('/');
|
|
19
|
+
const host = at < 0 ? rest : rest.slice(0, at);
|
|
20
|
+
prefix = `${base}/${host}`;
|
|
21
|
+
url = new URL(`https://${host}${at < 0 ? '/' : rest.slice(at)}${url.search}`);
|
|
22
|
+
}
|
|
23
|
+
if (namespace && !prefix) prefix = `${base}/${url.host}`;
|
|
24
|
+
// Complete wire headers stay opaque: the abbreviated request header view omits authentication headers.
|
|
25
|
+
const headers = await request.allHeaders();
|
|
26
|
+
if (prefix) headers['x-forwarded-prefix'] = prefix;
|
|
27
|
+
const visited = new Set();
|
|
28
|
+
for (;;) {
|
|
29
|
+
if (visited.has(url.href)) throw new Error('World browser redirect loop');
|
|
30
|
+
visited.add(url.href);
|
|
31
|
+
response = await route.fetch({ url: transport + '/?url=' + encodeURIComponent(url.href), method, headers, maxRedirects: 0, timeout: 1000 });
|
|
32
|
+
const status = response.status(), location = response.headers().location;
|
|
33
|
+
onExchange?.({ method, path: url.pathname, status });
|
|
34
|
+
if (![301, 302, 303, 307, 308].includes(status) || !location) break;
|
|
35
|
+
const next = new URL(location, url);
|
|
36
|
+
// Chrome owns the application's HTTP loopback callback.
|
|
37
|
+
if (next.protocol !== 'https:') break;
|
|
38
|
+
if (next.origin !== url.origin) {
|
|
39
|
+
for (const name of Object.keys(headers)) if (/^(authorization|cookie|proxy-authorization|x-volter-twin-|x-forwarded-prefix)/i.test(name)) delete headers[name];
|
|
40
|
+
}
|
|
41
|
+
if (status === 303 || ([301, 302].includes(status) && method === 'POST')) method = 'GET';
|
|
42
|
+
url = next;
|
|
43
|
+
}
|
|
44
|
+
if (namespace && /text\/html/i.test(response.headers()['content-type'] ?? '')) {
|
|
45
|
+
// Input values are protocol data (OAuth redirect_uri, state and code). Keep them byte-for-byte. Links and inline
|
|
46
|
+
// scripts name browser destinations, mapped to this World's page; external script bytes remain as published.
|
|
47
|
+
const body = (await response.text()).split(/(<input\b[^>]*>)/gi).map(part => /^<input\b/i.test(part) ? part : part.replace(/https:\/\/([A-Za-z0-9.-]+(?::\d+)?)(?=[/\s"'&<]|$)/g, (_, host) => `${origin}${base}/${host}`)).join('');
|
|
48
|
+
const outHeaders = { ...response.headers() };
|
|
49
|
+
delete outHeaders['content-length'];
|
|
50
|
+
await route.fulfill({ response, headers: outHeaders, body });
|
|
51
|
+
} else await route.fulfill({ response });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
onExchange?.({ error: error.message });
|
|
54
|
+
await route.abort('blockedbyclient').catch(() => {});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
await router.route('https://**/*', forward);
|
|
58
|
+
return { message: 'World routing installed on auth tab', namespace: base ?? null, dispose: () => router.unroute('https://**/*', forward) };
|
|
59
|
+
};
|
package/src/ca-trust.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorMessage } from './error-message.ts';
|
|
1
2
|
import { createHash, randomUUID, X509Certificate } from 'node:crypto';
|
|
2
3
|
import { existsSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
|
|
3
4
|
import { dirname, join } from 'node:path';
|
|
@@ -21,7 +22,7 @@ export function sessionTrustEnv(caCertPath: string, inputs: CaTrustInputs = proc
|
|
|
21
22
|
files.set(path, pem);
|
|
22
23
|
return pem;
|
|
23
24
|
}
|
|
24
|
-
catch (error) { throw new CaTrustError(`Cannot read configured CA file ${path}: ${
|
|
25
|
+
catch (error) { throw new CaTrustError(`Cannot read configured CA file ${path}: ${errorMessage(error)}`); }
|
|
25
26
|
};
|
|
26
27
|
const session = read(caCertPath);
|
|
27
28
|
const extra = callers.flatMap(caller => caller.NODE_EXTRA_CA_CERTS ? [read(caller.NODE_EXTRA_CA_CERTS)] : []).join('\n');
|
|
@@ -41,8 +42,8 @@ export function sessionTrustEnv(caCertPath: string, inputs: CaTrustInputs = proc
|
|
|
41
42
|
}
|
|
42
43
|
} catch (error) {
|
|
43
44
|
try { rmSync(temporary, { force: true }); }
|
|
44
|
-
catch (cleanupError) { throw new CaTrustError(`Cannot materialize session trust bundle: ${
|
|
45
|
-
throw new CaTrustError(`Cannot materialize session trust bundle: ${
|
|
45
|
+
catch (cleanupError) { throw new CaTrustError(`Cannot materialize session trust bundle: ${errorMessage(error)}; cleanup: ${errorMessage(cleanupError)}`); }
|
|
46
|
+
throw new CaTrustError(`Cannot materialize session trust bundle: ${errorMessage(error)}`);
|
|
46
47
|
}
|
|
47
48
|
return path;
|
|
48
49
|
};
|
package/src/changeset.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorMessage } from './error-message.ts';
|
|
1
2
|
import { worldChangesetRules } from './configs.ts';
|
|
2
3
|
import { blobDigest, captureHistory, captureParentHistory, getActiveBlobStore, getActiveWorldStore, originHead, withHistoryLocks, worldPaths, type HistoryReference } from '@volter/world-core/runtime';
|
|
3
4
|
// `volter-world mark` / `diff` / `changeset` — the WORLD-side verbs over the changeset
|
|
@@ -66,7 +67,7 @@ function requireWorld(name: string, root: string): WorldInstance {
|
|
|
66
67
|
try {
|
|
67
68
|
return statusWorld(name, root);
|
|
68
69
|
} catch (error) {
|
|
69
|
-
if (!/World instance not found/.test(String((error
|
|
70
|
+
if (!/World instance not found/.test(String(errorMessage(error)))) throw error;
|
|
70
71
|
const known = listWorlds(root).map((world) => world.name);
|
|
71
72
|
throw new Error(`World "${name}" not found in ${root}${known.length ? ` (known worlds: ${known.join(', ')})` : ' (no worlds here)'} — start it with \`volter world up\` in the app's folder (the operator's own: \`volter-world up <config> --env-out <path> --name ${name}\`)`);
|
|
72
73
|
}
|
|
@@ -553,7 +554,7 @@ export async function pushWorldChangeset(name: string, options: PushWorldOptions
|
|
|
553
554
|
if (isPathRemote(url)) {
|
|
554
555
|
try { const target = localWorld(url);
|
|
555
556
|
const receipts = await landChangeset(target.name, target.root, target.instance, located.changeset, expect); const position = Object.fromEntries([...new Set(located.changeset.actions.map((a) => a.service))].map((twin) => { const st = located.changeset.actions.find((e) => e.service === twin)?.stateService ?? twin; return [twin, wholeLog(st, join(target.instance.dirs.data, twin)).length]; })); const snapshots = Object.fromEntries(Object.keys(position).map(twin => { const state = located.changeset.actions.find(a => a.service === twin)?.stateService ?? twin; const head = captureHistory(state, join(target.instance.dirs.data, twin)); return [twin, { view: head.view, position: head.position }]; })); body = { receipts, position, snapshots }; res = { ok: true, status: 200 }; }
|
|
556
|
-
catch (error) { body = { error:
|
|
557
|
+
catch (error) { body = { error: errorMessage(error) }; res = { ok: false, status: 409 }; }
|
|
557
558
|
} else {
|
|
558
559
|
// the client's position on each twin rides along (its fetch cursor): the door refuses when origin moved past it
|
|
559
560
|
const answer = await fetchReady(`${url}/-/${namespace}/push`, { method: 'POST', headers: { [TOKEN_HEADER]: options.key, 'content-type': 'application/json' }, body: JSON.stringify({ changeset: located.changeset, expect }) });
|
|
@@ -603,7 +604,7 @@ export async function pushWorldChangeset(name: string, options: PushWorldOptions
|
|
|
603
604
|
// the remote's record of who pushed it and who owns it, kept on this copy too (outside the hash)
|
|
604
605
|
const changeset = { ...withApplication(located.changeset, application), ...(body.owner && !located.changeset.owner ? { owner: body.owner } : {}), ...(body.pushedBy ? { pushedBy: body.pushedBy } : {}) };
|
|
605
606
|
writeChangesetFile(located.path, changeset);
|
|
606
|
-
const unsent = namedFailure ? `records the landed entries name did not reach the origin (${
|
|
607
|
+
const unsent = namedFailure ? `records the landed entries name did not reach the origin (${errorMessage(namedFailure)}); the next push from this branch that touches ${twins.map(([t]) => t).join(', ')} sends them` : undefined;
|
|
607
608
|
return { ...(unsent ? { unsent } : {}), application, changeset, world: located.world, path: located.path, remote: { url, namespace } };
|
|
608
609
|
}
|
|
609
610
|
|