@volter/world-runtime 3.0.124 → 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.
@@ -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
- return entry ? await importModule(entry) : null;
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. */
@@ -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
- 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) });
1635
- if (!res.ok)
1636
- throw new Error(`answered ${res.status}`);
1637
- const answer = (await res.json());
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
  }
@@ -230,6 +230,7 @@ export type WorldServiceConfig = {
230
230
  refresh?: {
231
231
  every?: string;
232
232
  webhook?: boolean;
233
+ atMost?: string;
233
234
  };
234
235
  scope?: string;
235
236
  };
@@ -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';
@@ -117,6 +117,7 @@ export type WorldLayout = {
117
117
  data: string;
118
118
  };
119
119
  envFile?: string;
120
+ env?: Record<string, string>;
120
121
  services: Record<string, {
121
122
  url?: string;
122
123
  protocol?: unknown;
@@ -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
- return Response.json({ name: this.served, vendors, ca: null, proxy: null, env: { ...endpoints, VOLTER_TWINS_KEY: presented }, ...(Object.keys(streams).length ? { streams } : {}) });
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.124",
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.124",
74
- "@volter/world-console": "3.0.119"
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.124",
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
- return entry ? await importModule(entry) : null;
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>): Promise<void> {
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
- 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) });
1602
- if (!res.ok) throw new Error(`answered ${res.status}`);
1603
- const answer = (await res.json()) as Record<string, unknown>;
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
  }
@@ -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
- return Response.json({ name: this.served, vendors, ca: null, proxy: null, env: { ...endpoints, VOLTER_TWINS_KEY: presented }, ...(Object.keys(streams).length ? { streams } : {}) });
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);