@volter/world-runtime 3.0.123 → 3.0.125
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/configs.js +1 -1
- package/dist/src/root.js +9 -2
- package/dist/src/runtime.js +19 -8
- package/dist/src/schema.d.ts +1 -0
- package/dist/src/schema.js +3 -1
- package/dist/src/served-world.d.ts +1 -0
- package/dist/src/served-world.js +10 -2
- package/package.json +4 -4
- package/src/configs.ts +1 -1
- package/src/root.ts +7 -2
- package/src/runtime.ts +15 -7
- package/src/schema.ts +3 -2
- package/src/served-world.ts +12 -3
package/dist/src/configs.js
CHANGED
|
@@ -32,7 +32,7 @@ const LEGACY_TOP_LEVEL = new Set(['id', 'description', 'bare', 'remotes', 'resou
|
|
|
32
32
|
const LEGACY_SERVICE = new Set(['id', 'type', 'package', 'command', 'args', 'cwd', 'port', 'portReason', 'env', 'controlPlane', 'branchState', 'injectEnv', 'injectEnvTemplates', 'root', 'rootArg', 'portArg', 'cliRedirect', 'ready', 'preload', 'external', 'colocate', 'version']);
|
|
33
33
|
const LEGACY_NESTED = {
|
|
34
34
|
bare: new Set(['name']), resources: new Set(['memoryMiB', 'writableStorageMiB']), catalog: new Set(['sha', 'protocol']),
|
|
35
|
-
root: new Set(['url', 'deploy', 'refresh', 'scope']), refresh: new Set(['every', 'webhook']),
|
|
35
|
+
root: new Set(['url', 'deploy', 'refresh', 'scope']), refresh: new Set(['every', 'webhook', 'atMost']),
|
|
36
36
|
external: new Set(['up', 'status', 'down', 'discover', 'readyWhen']), discover: new Set(['as', 'source', 'jsonPath', 'pattern']),
|
|
37
37
|
ready: new Set(['command', 'args', 'httpUrl', 'timeoutMs', 'intervalMs']), colocate: new Set(['module', 'export', 'scenarioPath']),
|
|
38
38
|
share: new Set(['provider', 'command', 'args', 'ephemeral', 'services']), shareService: new Set(['id', 'verifyPath']),
|
package/dist/src/root.js
CHANGED
|
@@ -13,7 +13,7 @@ import { packEntry } from "./catalog.js";
|
|
|
13
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
16
|
-
import { authStrategyFor, currentSealer, destinationOf, sealsThroughVault, openSealedAt, getActiveWorldStore, getPack, buildRemoteExecute, clearRoot, readRoot, registerStateSystem, stateDirName, stateSystemFor, validateRemoteOrigin, writeRoot, } from '@volter/world-core/runtime';
|
|
16
|
+
import { authStrategyFor, currentSealer, destinationOf, sealsThroughVault, openSealedAt, getActiveWorldStore, getPack, buildRemoteExecute, clearRoot, readRoot, registerPack, registerStateSystem, stateDirName, stateSystemFor, validateRemoteOrigin, writeRoot, } from '@volter/world-core/runtime';
|
|
17
17
|
import { loadWorldConfig, worldChangesetRules, writeWorldConfig } from "./configs.js";
|
|
18
18
|
import { changesetReadiness, collectObservations, foldObservations, loadChecks, openRootCredential, performEntries, rootCustody, vendorPackageDir } from '@volter/world-core/runtime';
|
|
19
19
|
import { findWorldChangeset, listWorldChangesets } from "./changeset.js";
|
|
@@ -427,7 +427,14 @@ async function packModule(vendor, from) {
|
|
|
427
427
|
if (registered)
|
|
428
428
|
return { pack: registered };
|
|
429
429
|
const entry = packEntryPath(vendor, from);
|
|
430
|
-
|
|
430
|
+
if (!entry)
|
|
431
|
+
return null;
|
|
432
|
+
const mod = await importModule(entry);
|
|
433
|
+
// An app-local pack may import its own installed kernel copy. Arm the exported descriptor in this runtime's
|
|
434
|
+
// kernel too, so its executor sees the pack's budget, hosts and references rather than the fallback.
|
|
435
|
+
if (mod.pack)
|
|
436
|
+
registerPack(mod.pack);
|
|
437
|
+
return mod;
|
|
431
438
|
}
|
|
432
439
|
/** The pack's directory: installed beside the World, in its pack repository, or in the pack repository beside this
|
|
433
440
|
* platform checkout. */
|
package/dist/src/runtime.js
CHANGED
|
@@ -28,7 +28,7 @@ import { isEnvironmentName } from "./schema.js";
|
|
|
28
28
|
import { infraConnections, parseInfraDefinition } from "./pglite-backing.js";
|
|
29
29
|
import { describeServiceEnd, readServiceEnd } from "./service-exit.js";
|
|
30
30
|
import { Socket } from 'node:net';
|
|
31
|
-
import { getActiveWorldStore, withFileLock, stateDirName, PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding, WORLD_BOOT_PATH, WORLD_DATA_ENV, WORLD_ENV_NAMES_ENV } from '@volter/world-core/runtime';
|
|
31
|
+
import { beginPlaceholderPull, endPlaceholderPull, placeholderPullActive, getActiveWorldStore, withFileLock, stateDirName, PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding, WORLD_BOOT_PATH, WORLD_DATA_ENV, WORLD_ENV_NAMES_ENV } from '@volter/world-core/runtime';
|
|
32
32
|
import { loadWorldConfig } from "./configs.js";
|
|
33
33
|
import { createWorldNetworkPolicy, parseWorldNetworkPolicy, normalizeWorldNetwork, intersectWorldNetwork, WORLD_NETWORK_POLICY_ENV } from '@volter/world-core/network-policy';
|
|
34
34
|
import { overlayPackFactsFile, resolveServicePackage } from "./catalog.js";
|
|
@@ -1613,7 +1613,7 @@ const ISSUE_TIMEOUT_MS = 15_000;
|
|
|
1613
1613
|
/** A twin's issued credential (its `issues`, copied from its pack's credentialDoor by init): once it is up, the door is
|
|
1614
1614
|
* asked for one and each pending name of that service is set to it. A twin that answers no credential, or has no
|
|
1615
1615
|
* door, leaves the names unset and says so, rather than handing the application a fixture the twin would refuse. */
|
|
1616
|
-
async function issueCredentials(service, started, env, pending, unissued) {
|
|
1616
|
+
async function issueCredentials(service, started, env, pending, unissued, controlRoot) {
|
|
1617
1617
|
const direct = new Set([...pending].filter(([, id]) => id === `${service.id}@direct`).map(([name]) => name));
|
|
1618
1618
|
const wanted = [...pending].filter(([name, id]) => id === service.id || direct.has(name)).map(([name]) => name);
|
|
1619
1619
|
if (wanted.length === 0)
|
|
@@ -1631,10 +1631,21 @@ async function issueCredentials(service, started, env, pending, unissued) {
|
|
|
1631
1631
|
throw new Error('its service names no credential door');
|
|
1632
1632
|
if (!started.url)
|
|
1633
1633
|
throw new Error('it has no URL to ask');
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1634
|
+
// A boot's identity is existing account state, not an application write to deploy.
|
|
1635
|
+
const enclosingSeed = placeholderPullActive(service.id, controlRoot);
|
|
1636
|
+
if (!enclosingSeed)
|
|
1637
|
+
beginPlaceholderPull(service.id, controlRoot);
|
|
1638
|
+
let answer;
|
|
1639
|
+
try {
|
|
1640
|
+
const res = await fetch(new URL(door.path, started.url), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(door.body), signal: AbortSignal.timeout(ISSUE_TIMEOUT_MS) });
|
|
1641
|
+
if (!res.ok)
|
|
1642
|
+
throw new Error(`answered ${res.status}`);
|
|
1643
|
+
answer = (await res.json());
|
|
1644
|
+
}
|
|
1645
|
+
finally {
|
|
1646
|
+
if (!enclosingSeed)
|
|
1647
|
+
endPlaceholderPull(service.id, controlRoot);
|
|
1648
|
+
}
|
|
1638
1649
|
for (const name of wanted) {
|
|
1639
1650
|
const spec = door.fill[name] ?? '';
|
|
1640
1651
|
if (spec.startsWith('directory:') || spec.startsWith('file:')) {
|
|
@@ -2277,7 +2288,7 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
2277
2288
|
for (const started of host.services) {
|
|
2278
2289
|
const declared = colocated.find((s) => s.id === started.id);
|
|
2279
2290
|
if (declared)
|
|
2280
|
-
await issueCredentials(declared, started, env, issuing, unissued);
|
|
2291
|
+
await issueCredentials(declared, started, env, issuing, unissued, ownedPath(data, started.id));
|
|
2281
2292
|
}
|
|
2282
2293
|
writeProxyEnv(root, name, env);
|
|
2283
2294
|
if (colocated.length)
|
|
@@ -2347,7 +2358,7 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
2347
2358
|
recordService(started);
|
|
2348
2359
|
recordPidsSoFar();
|
|
2349
2360
|
Object.assign(env, started.env);
|
|
2350
|
-
await issueCredentials(service, started, env, issuing, unissued);
|
|
2361
|
+
await issueCredentials(service, started, env, issuing, unissued, ownedPath(data, started.id));
|
|
2351
2362
|
writeProxyEnv(root, name, env);
|
|
2352
2363
|
boot.progress(`${service.id}: started, its environment merged`);
|
|
2353
2364
|
}
|
package/dist/src/schema.d.ts
CHANGED
package/dist/src/schema.js
CHANGED
|
@@ -245,11 +245,13 @@ function assertLegacyWorldConfig(value, path) {
|
|
|
245
245
|
if (r.refresh !== undefined) {
|
|
246
246
|
if (!record(r.refresh))
|
|
247
247
|
throw new Error(`Service "${service.id}" root.refresh must be an object in ${path}`);
|
|
248
|
-
assertKeys(r.refresh, ['every', 'webhook'], `Service "${service.id}" root.refresh`, path);
|
|
248
|
+
assertKeys(r.refresh, ['every', 'webhook', 'atMost'], `Service "${service.id}" root.refresh`, path);
|
|
249
249
|
if (r.refresh.every !== undefined && typeof r.refresh.every !== 'string')
|
|
250
250
|
throw new Error(`Service "${service.id}" root.refresh.every must be a string in ${path}`);
|
|
251
251
|
if (r.refresh.webhook !== undefined && typeof r.refresh.webhook !== 'boolean')
|
|
252
252
|
throw new Error(`Service "${service.id}" root.refresh.webhook must be a boolean in ${path}`);
|
|
253
|
+
if (r.refresh.atMost !== undefined && typeof r.refresh.atMost !== 'string')
|
|
254
|
+
throw new Error(`Service "${service.id}" root.refresh.atMost must be a string in ${path}`);
|
|
253
255
|
}
|
|
254
256
|
if (r.deploy === undefined)
|
|
255
257
|
service.root.deploy = 'gated';
|
package/dist/src/served-world.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { errorMessage } from "./error-message.js";
|
|
2
|
-
import { boardFrameKey, getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant, historyDigest, historyEntries, readHistoryView, worldPaths } from '@volter/world-core/runtime';
|
|
2
|
+
import { boardFrameKey, getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant, historyDigest, historyEntries, readHistoryView, worldPaths, WORLD_ENV_NAMES_ENV } from '@volter/world-core/runtime';
|
|
3
3
|
// A SERVED WORLD (docs/reference/http-api.md#a-served-world): `volter world serve` puts a world at
|
|
4
4
|
// `http://<host>:<port>/<org>/<world>/`. Each twin's vendor API is under its vendor id — the
|
|
5
5
|
// addressed grammar RH2 depends on — and the world's own doors are under `/-/<org>/<world>/`:
|
|
@@ -1350,7 +1350,15 @@ export class WorldDoors {
|
|
|
1350
1350
|
...Object.entries(templates).filter(([, t]) => !/\$\{(?:host|port)\}/.test(t)).map(([n, t]) => [n, t.replaceAll('${url}', vendors[v])]),
|
|
1351
1351
|
];
|
|
1352
1352
|
}));
|
|
1353
|
-
|
|
1353
|
+
const runningEnv = this.up().env ?? {};
|
|
1354
|
+
const declaredNames = JSON.parse(runningEnv[WORLD_ENV_NAMES_ENV] ?? '[]');
|
|
1355
|
+
const ownedNames = new Set(Array.isArray(declaredNames) ? declaredNames.filter((name) => typeof name === 'string') : []);
|
|
1356
|
+
const services = loadWorldConfig(this.configRef, this.worldRoot).config.services;
|
|
1357
|
+
const credentials = Object.fromEntries(Object.keys(vendors).flatMap((vendor) => {
|
|
1358
|
+
const door = services.find((service) => service.id === vendor)?.issues ?? packFacts()[vendor]?.credentialDoor;
|
|
1359
|
+
return Object.entries(door?.fill ?? {}).flatMap(([name, field]) => !field.startsWith('file:') && ownedNames.has(name) && runningEnv[name] ? [[name, runningEnv[name]]] : []);
|
|
1360
|
+
}));
|
|
1361
|
+
return Response.json({ name: this.served, vendors, ca: null, proxy: null, env: { ...credentials, ...endpoints, VOLTER_TWINS_KEY: presented }, ...(Object.keys(streams).length ? { streams } : {}) });
|
|
1354
1362
|
}
|
|
1355
1363
|
if (url.pathname.startsWith(prefix)) {
|
|
1356
1364
|
const rest = url.pathname.slice(prefix.length);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.125",
|
|
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",
|
|
@@ -70,14 +70,14 @@
|
|
|
70
70
|
"node": ">=22.3"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@volter/world-core": "3.0.
|
|
74
|
-
"@volter/world-console": "3.0.
|
|
73
|
+
"@volter/world-core": "3.0.125",
|
|
74
|
+
"@volter/world-console": "3.0.120"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"@electric-sql/pglite": "0.5.8",
|
|
78
78
|
"@electric-sql/pglite-pgvector": "0.0.9",
|
|
79
79
|
"@volter/world-access": "3.0.1",
|
|
80
|
-
"@volter/world-core": "3.0.
|
|
80
|
+
"@volter/world-core": "3.0.125",
|
|
81
81
|
"mongodb": "~6.20.0",
|
|
82
82
|
"pg-gateway": "0.3.0-beta.4",
|
|
83
83
|
"smol-toml": "^1.8.0",
|
package/src/configs.ts
CHANGED
|
@@ -36,7 +36,7 @@ const LEGACY_TOP_LEVEL = new Set(['id', 'description', 'bare', 'remotes', 'resou
|
|
|
36
36
|
const LEGACY_SERVICE = new Set(['id', 'type', 'package', 'command', 'args', 'cwd', 'port', 'portReason', 'env', 'controlPlane', 'branchState', 'injectEnv', 'injectEnvTemplates', 'root', 'rootArg', 'portArg', 'cliRedirect', 'ready', 'preload', 'external', 'colocate', 'version']);
|
|
37
37
|
const LEGACY_NESTED = {
|
|
38
38
|
bare: new Set(['name']), resources: new Set(['memoryMiB', 'writableStorageMiB']), catalog: new Set(['sha', 'protocol']),
|
|
39
|
-
root: new Set(['url', 'deploy', 'refresh', 'scope']), refresh: new Set(['every', 'webhook']),
|
|
39
|
+
root: new Set(['url', 'deploy', 'refresh', 'scope']), refresh: new Set(['every', 'webhook', 'atMost']),
|
|
40
40
|
external: new Set(['up', 'status', 'down', 'discover', 'readyWhen']), discover: new Set(['as', 'source', 'jsonPath', 'pattern']),
|
|
41
41
|
ready: new Set(['command', 'args', 'httpUrl', 'timeoutMs', 'intervalMs']), colocate: new Set(['module', 'export', 'scenarioPath']),
|
|
42
42
|
share: new Set(['provider', 'command', 'args', 'ephemeral', 'services']), shareService: new Set(['id', 'verifyPath']),
|
package/src/root.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
16
16
|
import {
|
|
17
|
-
NO_SECRETS_CHECK, authStrategyFor, currentSealer, destinationOf, sealsThroughVault, openSealedAt, getActiveWorldStore, getPack, buildRemoteExecute, clearRoot, observeAppends, readRoot, registerStateSystem, stateDirName, stateSystemFor, type Check, type CredentialPayload, type PerformAction, type RemoteExecute, type RootConfig, type SealedCredential, type TwinAction, validateRemoteOrigin, writeRoot,
|
|
17
|
+
NO_SECRETS_CHECK, authStrategyFor, currentSealer, destinationOf, sealsThroughVault, openSealedAt, getActiveWorldStore, getPack, buildRemoteExecute, clearRoot, observeAppends, readRoot, registerPack, registerStateSystem, stateDirName, stateSystemFor, type Check, type CredentialPayload, type PerformAction, type RemoteExecute, type RootConfig, type SealedCredential, type TwinAction, type TwinPack, validateRemoteOrigin, writeRoot,
|
|
18
18
|
} from '@volter/world-core/runtime';
|
|
19
19
|
import { loadWorldConfig, worldChangesetRules, writeWorldConfig } from './configs.ts';
|
|
20
20
|
import { changesetReadiness, collectObservations, type Observation, foldObservations, loadChecks, openRootCredential, performEntries, rootCustody, protocolStanding, userKek, vendorPackageDir, type DeployReport, type ObserveReport } from '@volter/world-core/runtime';
|
|
@@ -356,7 +356,12 @@ async function packModule(vendor: string, from: string): Promise<Record<string,
|
|
|
356
356
|
const registered = getPack(vendor);
|
|
357
357
|
if (registered) return { pack: registered };
|
|
358
358
|
const entry = packEntryPath(vendor, from);
|
|
359
|
-
|
|
359
|
+
if (!entry) return null;
|
|
360
|
+
const mod = await importModule(entry);
|
|
361
|
+
// An app-local pack may import its own installed kernel copy. Arm the exported descriptor in this runtime's
|
|
362
|
+
// kernel too, so its executor sees the pack's budget, hosts and references rather than the fallback.
|
|
363
|
+
if (mod.pack) registerPack(mod.pack as TwinPack);
|
|
364
|
+
return mod;
|
|
360
365
|
}
|
|
361
366
|
/** The pack's directory: installed beside the World, in its pack repository, or in the pack repository beside this
|
|
362
367
|
* platform checkout. */
|
package/src/runtime.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { isEnvironmentName } from './schema.ts';
|
|
|
30
30
|
import { infraConnections, parseInfraDefinition } from './pglite-backing.ts';
|
|
31
31
|
import { describeServiceEnd, readServiceEnd } from './service-exit.ts';
|
|
32
32
|
import { Socket } from 'node:net';
|
|
33
|
-
import { getActiveWorldStore, withFileLock, stateDirName, PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding, WORLD_BOOT_PATH, WORLD_DATA_ENV, WORLD_ENV_NAMES_ENV } from '@volter/world-core/runtime';
|
|
33
|
+
import { beginPlaceholderPull, endPlaceholderPull, placeholderPullActive, getActiveWorldStore, withFileLock, stateDirName, PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding, WORLD_BOOT_PATH, WORLD_DATA_ENV, WORLD_ENV_NAMES_ENV } from '@volter/world-core/runtime';
|
|
34
34
|
import { loadWorldConfig } from './configs.ts';
|
|
35
35
|
import { createWorldNetworkPolicy, parseWorldNetworkPolicy, normalizeWorldNetwork, intersectWorldNetwork, type WorldNetwork, WORLD_NETWORK_POLICY_ENV } from '@volter/world-core/network-policy';
|
|
36
36
|
import { overlayPackFactsFile, resolveServicePackage } from './catalog.ts';
|
|
@@ -1587,7 +1587,7 @@ const ISSUE_TIMEOUT_MS = 15_000;
|
|
|
1587
1587
|
/** A twin's issued credential (its `issues`, copied from its pack's credentialDoor by init): once it is up, the door is
|
|
1588
1588
|
* asked for one and each pending name of that service is set to it. A twin that answers no credential, or has no
|
|
1589
1589
|
* door, leaves the names unset and says so, rather than handing the application a fixture the twin would refuse. */
|
|
1590
|
-
async function issueCredentials(service: WorldServiceConfig, started: WorldServiceInstance, env: Record<string, string>, pending: Map<string, string>, unissued: Set<string
|
|
1590
|
+
async function issueCredentials(service: WorldServiceConfig, started: WorldServiceInstance, env: Record<string, string>, pending: Map<string, string>, unissued: Set<string>, controlRoot: string): Promise<void> {
|
|
1591
1591
|
const direct = new Set([...pending].filter(([, id]) => id === `${service.id}@direct`).map(([name]) => name));
|
|
1592
1592
|
const wanted = [...pending].filter(([name, id]) => id === service.id || direct.has(name)).map(([name]) => name);
|
|
1593
1593
|
if (wanted.length === 0) return;
|
|
@@ -1598,9 +1598,17 @@ async function issueCredentials(service: WorldServiceConfig, started: WorldServi
|
|
|
1598
1598
|
try {
|
|
1599
1599
|
if (!door) throw new Error('its service names no credential door');
|
|
1600
1600
|
if (!started.url) throw new Error('it has no URL to ask');
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1601
|
+
// A boot's identity is existing account state, not an application write to deploy.
|
|
1602
|
+
const enclosingSeed = placeholderPullActive(service.id, controlRoot);
|
|
1603
|
+
if (!enclosingSeed) beginPlaceholderPull(service.id, controlRoot);
|
|
1604
|
+
let answer: Record<string, unknown>;
|
|
1605
|
+
try {
|
|
1606
|
+
const res = await fetch(new URL(door.path, started.url), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(door.body), signal: AbortSignal.timeout(ISSUE_TIMEOUT_MS) });
|
|
1607
|
+
if (!res.ok) throw new Error(`answered ${res.status}`);
|
|
1608
|
+
answer = (await res.json()) as Record<string, unknown>;
|
|
1609
|
+
} finally {
|
|
1610
|
+
if (!enclosingSeed) endPlaceholderPull(service.id, controlRoot);
|
|
1611
|
+
}
|
|
1604
1612
|
for (const name of wanted) {
|
|
1605
1613
|
const spec = door.fill[name] ?? '';
|
|
1606
1614
|
if (spec.startsWith('directory:') || spec.startsWith('file:')) {
|
|
@@ -2158,7 +2166,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
|
|
|
2158
2166
|
if (colocated.length) boot.progress('co-located host ready: receipt read, every port admitted as its own');
|
|
2159
2167
|
for (const started of host.services) {
|
|
2160
2168
|
const declared = colocated.find((s) => s.id === started.id);
|
|
2161
|
-
if (declared) await issueCredentials(declared, started, env, issuing, unissued);
|
|
2169
|
+
if (declared) await issueCredentials(declared, started, env, issuing, unissued, ownedPath(data, started.id));
|
|
2162
2170
|
}
|
|
2163
2171
|
writeProxyEnv(root, name, env);
|
|
2164
2172
|
if (colocated.length) boot.progress('co-located twins: credentials issued, proxy environment written');
|
|
@@ -2219,7 +2227,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
|
|
|
2219
2227
|
recordService(started);
|
|
2220
2228
|
recordPidsSoFar();
|
|
2221
2229
|
Object.assign(env, started.env);
|
|
2222
|
-
await issueCredentials(service, started, env, issuing, unissued);
|
|
2230
|
+
await issueCredentials(service, started, env, issuing, unissued, ownedPath(data, started.id));
|
|
2223
2231
|
writeProxyEnv(root, name, env);
|
|
2224
2232
|
boot.progress(`${service.id}: started, its environment merged`);
|
|
2225
2233
|
}
|
package/src/schema.ts
CHANGED
|
@@ -204,7 +204,7 @@ export type WorldServiceConfig = {
|
|
|
204
204
|
/** THE ROOT (contract "The model"): the vendor's real account behind this twin on a shared world —
|
|
205
205
|
* its API (or the one resource the account is), the deploy policy, how it is refreshed. Key-free;
|
|
206
206
|
* the credential is sealed under `.volter/credentials/`. Written by `volter twin <vendor> root`. */
|
|
207
|
-
root?: { url: string; deploy: 'auto' | 'gated' | 'hold'; refresh?: { every?: string; webhook?: boolean }; scope?: string };
|
|
207
|
+
root?: { url: string; deploy: 'auto' | 'gated' | 'hold'; refresh?: { every?: string; webhook?: boolean; atMost?: string }; scope?: string };
|
|
208
208
|
rootArg?: string | false;
|
|
209
209
|
portArg?: string | false;
|
|
210
210
|
/**
|
|
@@ -635,9 +635,10 @@ function assertLegacyWorldConfig(value: unknown, path: string): WorldConfig {
|
|
|
635
635
|
if ((r as { scope?: unknown }).scope !== undefined && typeof (r as { scope?: unknown }).scope !== 'string') throw new Error(`Service "${service.id}" root.scope must be a string in ${path}`);
|
|
636
636
|
if (r.refresh !== undefined) {
|
|
637
637
|
if (!record(r.refresh)) throw new Error(`Service "${service.id}" root.refresh must be an object in ${path}`);
|
|
638
|
-
assertKeys(r.refresh, ['every', 'webhook'], `Service "${service.id}" root.refresh`, path);
|
|
638
|
+
assertKeys(r.refresh, ['every', 'webhook', 'atMost'], `Service "${service.id}" root.refresh`, path);
|
|
639
639
|
if (r.refresh.every !== undefined && typeof r.refresh.every !== 'string') throw new Error(`Service "${service.id}" root.refresh.every must be a string in ${path}`);
|
|
640
640
|
if (r.refresh.webhook !== undefined && typeof r.refresh.webhook !== 'boolean') throw new Error(`Service "${service.id}" root.refresh.webhook must be a boolean in ${path}`);
|
|
641
|
+
if (r.refresh.atMost !== undefined && typeof r.refresh.atMost !== 'string') throw new Error(`Service "${service.id}" root.refresh.atMost must be a string in ${path}`);
|
|
641
642
|
}
|
|
642
643
|
if (r.deploy === undefined) (service.root as { deploy: string }).deploy = 'gated';
|
|
643
644
|
}
|
package/src/served-world.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { errorMessage } from './error-message.ts';
|
|
2
|
-
import { boardFrameKey, getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant, historyDigest, historyEntries, historyLength, readHistoryView, worldPaths, type HistoryReference } from '@volter/world-core/runtime';
|
|
2
|
+
import { boardFrameKey, getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant, historyDigest, historyEntries, historyLength, readHistoryView, worldPaths, WORLD_ENV_NAMES_ENV, type HistoryReference } from '@volter/world-core/runtime';
|
|
3
3
|
// A SERVED WORLD (docs/reference/http-api.md#a-served-world): `volter world serve` puts a world at
|
|
4
4
|
// `http://<host>:<port>/<org>/<world>/`. Each twin's vendor API is under its vendor id — the
|
|
5
5
|
// addressed grammar RH2 depends on — and the world's own doors are under `/-/<org>/<world>/`:
|
|
@@ -351,7 +351,7 @@ function streamEnv(id: string, protocol: string): Record<string, string> {
|
|
|
351
351
|
|
|
352
352
|
/** What the doors need of a world: where each twin's state lives and which twins it has. A booted
|
|
353
353
|
* local instance is one; a hosted World describes itself. */
|
|
354
|
-
export type WorldLayout = { dirs: { data: string }; envFile?: string; services: Record<string, { url?: string; protocol?: unknown; resolvedPackage?: string; resolvedVersion?: string; pinnedSource?: { package?: string; version?: string } }> };
|
|
354
|
+
export type WorldLayout = { dirs: { data: string }; envFile?: string; env?: Record<string, string>; services: Record<string, { url?: string; protocol?: unknown; resolvedPackage?: string; resolvedVersion?: string; pinnedSource?: { package?: string; version?: string } }> };
|
|
355
355
|
/** The host a world's doors run in: its layout, and how a request reaches a twin's wire — a local
|
|
356
356
|
* host forwards to the twin's own port, a hosted World calls the pack in-process. */
|
|
357
357
|
export type DoorHost = {
|
|
@@ -1194,7 +1194,16 @@ export class WorldDoors {
|
|
|
1194
1194
|
...Object.entries(templates).filter(([, t]) => !/\$\{(?:host|port)\}/.test(t)).map(([n, t]) => [n, t.replaceAll('${url}', vendors[v]!)]),
|
|
1195
1195
|
];
|
|
1196
1196
|
}));
|
|
1197
|
-
|
|
1197
|
+
const runningEnv = this.up().env ?? {};
|
|
1198
|
+
const declaredNames: unknown = JSON.parse(runningEnv[WORLD_ENV_NAMES_ENV] ?? '[]');
|
|
1199
|
+
const ownedNames = new Set(Array.isArray(declaredNames) ? declaredNames.filter((name): name is string => typeof name === 'string') : []);
|
|
1200
|
+
const services = loadWorldConfig(this.configRef, this.worldRoot).config.services;
|
|
1201
|
+
const credentials = Object.fromEntries(Object.keys(vendors).flatMap((vendor) => {
|
|
1202
|
+
const door = services.find((service) => service.id === vendor)?.issues ?? packFacts()[vendor]?.credentialDoor;
|
|
1203
|
+
return Object.entries(door?.fill ?? {}).flatMap(([name, field]) =>
|
|
1204
|
+
!field.startsWith('file:') && ownedNames.has(name) && runningEnv[name] ? [[name, runningEnv[name]]] : []);
|
|
1205
|
+
}));
|
|
1206
|
+
return Response.json({ name: this.served, vendors, ca: null, proxy: null, env: { ...credentials, ...endpoints, VOLTER_TWINS_KEY: presented }, ...(Object.keys(streams).length ? { streams } : {}) });
|
|
1198
1207
|
}
|
|
1199
1208
|
if (url.pathname.startsWith(prefix)) {
|
|
1200
1209
|
const rest = url.pathname.slice(prefix.length);
|