@volter/world-runtime 3.0.166 → 3.0.168

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.
Files changed (82) hide show
  1. package/dist/known-external-services.json +0 -1
  2. package/dist/src/actions-runner.js +53 -14
  3. package/dist/src/attach.d.ts +9 -0
  4. package/dist/src/attach.js +9 -2
  5. package/dist/src/cli-commands.js +19 -3
  6. package/dist/src/console-apart.js +3 -2
  7. package/dist/src/credential-door.d.ts +8 -0
  8. package/dist/src/credential-door.js +54 -0
  9. package/dist/src/fixture-env.js +2 -2
  10. package/dist/src/forwardable-headers.d.ts +1 -0
  11. package/dist/src/forwardable-headers.js +19 -0
  12. package/dist/src/go-mysql-binding.d.ts +2 -0
  13. package/dist/src/go-mysql-binding.js +7 -1
  14. package/dist/src/guest-runner.js +8 -4
  15. package/dist/src/host-ready.d.ts +7 -3
  16. package/dist/src/host-ready.js +16 -3
  17. package/dist/src/index.d.ts +6 -1
  18. package/dist/src/index.js +7 -1
  19. package/dist/src/infra.js +10 -5
  20. package/dist/src/init.d.ts +5 -0
  21. package/dist/src/init.js +157 -23
  22. package/dist/src/managed-api-needs.d.ts +101 -0
  23. package/dist/src/managed-api-needs.js +312 -0
  24. package/dist/src/managed-api-source.d.ts +24 -0
  25. package/dist/src/managed-api-source.js +59 -0
  26. package/dist/src/mongodb-backing.js +33 -58
  27. package/dist/src/mongodb-command.d.ts +21 -0
  28. package/dist/src/mongodb-command.js +317 -0
  29. package/dist/src/pglite-host.mjs +2 -2
  30. package/dist/src/placeholder.d.ts +24 -1
  31. package/dist/src/placeholder.js +105 -13
  32. package/dist/src/process-signal.d.mts +2 -0
  33. package/dist/src/process-signal.mjs +44 -2
  34. package/dist/src/project-inspect.d.ts +1 -1
  35. package/dist/src/project-inspect.js +8 -1
  36. package/dist/src/redirect-proxy.js +4 -19
  37. package/dist/src/reflect.js +2 -2
  38. package/dist/src/runtime.d.ts +5 -1
  39. package/dist/src/runtime.js +311 -59
  40. package/dist/src/schema.d.ts +10 -0
  41. package/dist/src/schema.js +76 -3
  42. package/dist/src/serve.js +24 -4
  43. package/dist/src/served-world.d.ts +2 -1
  44. package/dist/src/served-world.js +6 -4
  45. package/dist/src/service-command.d.ts +7 -3
  46. package/dist/src/service-command.js +53 -22
  47. package/dist/src/service-environment.d.ts +4 -0
  48. package/dist/src/service-environment.js +21 -0
  49. package/dist/src/service-recorder.js +9 -13
  50. package/known-external-services.json +0 -1
  51. package/package.json +5 -5
  52. package/src/actions-runner.ts +38 -10
  53. package/src/attach.ts +15 -2
  54. package/src/cli-commands.ts +18 -3
  55. package/src/console-apart.ts +3 -2
  56. package/src/credential-door.ts +54 -0
  57. package/src/fixture-env.ts +2 -2
  58. package/src/forwardable-headers.ts +15 -0
  59. package/src/go-mysql-binding.ts +8 -1
  60. package/src/guest-runner.ts +8 -4
  61. package/src/host-ready.ts +12 -3
  62. package/src/index.ts +8 -1
  63. package/src/infra.ts +8 -3
  64. package/src/init.ts +145 -23
  65. package/src/managed-api-needs.ts +285 -0
  66. package/src/managed-api-source.ts +67 -0
  67. package/src/mongodb-backing.ts +32 -48
  68. package/src/mongodb-command.ts +238 -0
  69. package/src/pglite-host.mjs +2 -2
  70. package/src/placeholder.ts +90 -13
  71. package/src/process-signal.d.mts +2 -0
  72. package/src/process-signal.mjs +44 -2
  73. package/src/project-inspect.ts +9 -2
  74. package/src/redirect-proxy.ts +4 -15
  75. package/src/reflect.ts +2 -2
  76. package/src/runtime.ts +272 -45
  77. package/src/schema.ts +73 -3
  78. package/src/serve.ts +22 -4
  79. package/src/served-world.ts +7 -5
  80. package/src/service-command.ts +41 -19
  81. package/src/service-environment.ts +21 -0
  82. package/src/service-recorder.ts +9 -9
package/src/schema.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { errorMessage } from './error-message.ts';
2
+ import { isScalarCredentialField } from './credential-door.ts';
2
3
  import { normalizeWorldNetwork, type WorldNetwork } from '@volter/world-core/network-policy';
3
4
 
4
5
  export type WorldServiceType = 'twin' | 'process' | 'external';
@@ -172,7 +173,7 @@ export type WorldServiceConfig = {
172
173
  /** The credential this twin issues the World's applications at boot (its pack's `credentialDoor`, copied by `init`):
173
174
  * `up` POSTs `body` to `path` on the running twin and sets each name of `fill` whose value is `$issue:<this id>` to
174
175
  * the answer's field `fill` names for it. */
175
- issues?: { path: string; body: Record<string, unknown>; fill: Record<string, string> };
176
+ issues?: { path: string; body: Record<string, unknown>; fill: Record<string, string>; required?: string[] };
176
177
  /**
177
178
  * The twin PACKAGE this service boots from (`@volter/twin-<vendor>`), resolved from the world
178
179
  * root at boot by node's own `node_modules` walk (docs/concepts/worlds.md#the-config-and-the-running-world).
@@ -458,6 +459,9 @@ export type WorldInstance = {
458
459
  };
459
460
  services: Record<string, WorldServiceInstance>;
460
461
  env: Record<string, string>;
462
+ /** Required owners fixed at this boot; the digest binds their actual scalar responses in env without duplicating values.
463
+ * Omitted digest means issuance has not finished. Absent metadata is readable for cleanup, but not a live application declaration. */
464
+ requiredCredentials?: { version: 1; owners: Record<string, string>; valuesSha256?: string };
461
465
  /** Always written at boot; optional only so cleanup can read incomplete legacy records. */
462
466
  networkCeiling?: WorldNetwork | 'unrestricted';
463
467
  bootServices?: string[];
@@ -632,7 +636,7 @@ function assertLegacyWorldConfig(value: unknown, path: string): WorldConfig {
632
636
  if (service.branchState !== undefined && service.branchState !== 'unsupported') throw new Error(`Service ${service.id} branchState must be unsupported in ${path}`);
633
637
  const exports = [service.injectEnv, ...Object.keys(service.injectEnvTemplates ?? {}), ...Object.keys(service.cliRedirect ?? {}), ...(Array.isArray(service.external?.discover) ? service.external.discover.map(entry => entry?.as) : [])];
634
638
  if (exports.some(name => name !== undefined && !isEnvironmentName(name))) throw new Error(`Service ${service.id} bindings must name environment variables in ${path}`);
635
- if (service.issues !== undefined && (!record(service.issues) || typeof service.issues.path !== 'string' || !record(service.issues.body) || !environmentMap(service.issues.fill))) throw new Error(`Service ${service.id} issues must define path, body and string→string fill in ${path}`);
639
+ if (service.issues !== undefined) readCredentialDoor(service.issues, service.id, path);
636
640
  if (service.env?.VOLTER_WORLD_NETWORK_POLICY !== undefined || exports.includes('VOLTER_WORLD_NETWORK_POLICY')) throw new Error(`Service ${service.id} cannot replace World network permission in ${path}`);
637
641
  if (!/^[a-z0-9][a-z0-9._-]*$/.test(service.id)) throw new Error(`Invalid service id "${service.id}" in ${path}`);
638
642
  if (ids.has(service.id)) throw new Error(`Duplicate service id "${service.id}" in ${path}`);
@@ -762,6 +766,7 @@ function assertLegacyWorldConfig(value: unknown, path: string): WorldConfig {
762
766
  }
763
767
  }
764
768
  }
769
+ assertRequiredCredentialBindings(config as WorldConfig, path);
765
770
  return config as WorldConfig;
766
771
  }
767
772
 
@@ -774,6 +779,67 @@ function assertKeys(value: Record<string, unknown>, allowed: string[], what: str
774
779
  if (unexpected.length) throw new Error(`${what} has unknown field(s) ${unexpected.join(', ')} in ${path}`);
775
780
  }
776
781
 
782
+ /** Credential metadata is read completely before normalization or serialization can discard a field. */
783
+ function readCredentialDoor(value: unknown, service: string, path: string): NonNullable<WorldServiceConfig['issues']> {
784
+ const what = `Service ${service} issues`;
785
+ if (!record(value)) throw new Error(`${what} must be an object in ${path}`);
786
+ assertKeys(value, ['path', 'body', 'fill', 'required'], what, path);
787
+ if (!Object.hasOwn(value, 'path') || typeof value.path !== 'string' || value.path === ''
788
+ || !Object.hasOwn(value, 'body') || !record(value.body)
789
+ || !Object.hasOwn(value, 'fill') || !record(value.fill)) {
790
+ throw new Error(`${what} must define non-empty path, object body and string→string fill in ${path}`);
791
+ }
792
+ const fill: Array<[string, string]> = [];
793
+ for (const [name, field] of Object.entries(value.fill)) {
794
+ if (!isEnvironmentName(name) || typeof field !== 'string') throw new Error(`${what} fill must map environment names to strings in ${path}`);
795
+ fill.push([name, field]);
796
+ }
797
+ const required: string[] = [];
798
+ if (Object.hasOwn(value, 'required')) {
799
+ if (!Array.isArray(value.required)) throw new Error(`${what} required must be an array in ${path}`);
800
+ const seen = new Set<string>();
801
+ for (const name of value.required) {
802
+ if (!isEnvironmentName(name) || seen.has(name)) throw new Error(`${what} required must contain unique environment names in ${path}`);
803
+ const field = value.fill[name];
804
+ if (!Object.hasOwn(value.fill, name) || typeof field !== 'string' || !isScalarCredentialField(field)) {
805
+ throw new Error(`${what} required ${name} must name its direct scalar fill in ${path}`);
806
+ }
807
+ seen.add(name); required.push(name);
808
+ }
809
+ }
810
+ return { path: value.path, body: value.body, fill: Object.fromEntries(fill),
811
+ ...(Object.hasOwn(value, 'required') ? { required } : {}) };
812
+ }
813
+
814
+ /** Required names belong to one twin's exact pending reference, with no competing output or local override. */
815
+ export function assertRequiredCredentialBindings(config: WorldConfig, path: string): void {
816
+ const owners = new Map<string, string>();
817
+ for (const service of config.services) {
818
+ if (service.issues === undefined) continue;
819
+ const door = readCredentialDoor(service.issues, service.id, path);
820
+ if (door.required !== undefined && (service.type ?? 'twin') !== 'twin') {
821
+ throw new Error(`Service ${service.id} required credential bindings belong to a twin in ${path}`);
822
+ }
823
+ for (const name of door.required ?? []) {
824
+ if (owners.has(name)) throw new Error(`Required credential ${name} has competing issuers in ${path}`);
825
+ if (!Object.hasOwn(config.env ?? {}, name) || config.env?.[name] !== `${ISSUE_ENV_PREFIX}${service.id}`) {
826
+ throw new Error(`Required credential ${name} must be exactly ${ISSUE_ENV_PREFIX}${service.id} in ${path}`);
827
+ }
828
+ owners.set(name, service.id);
829
+ }
830
+ }
831
+ for (const service of config.services) {
832
+ const endpoints = new Set([service.injectEnv, ...Object.keys(service.injectEnvTemplates ?? {}),
833
+ ...Object.keys(service.cliRedirect ?? {}), ...(service.external?.discover ?? []).map(entry => entry.as)]);
834
+ for (const [name, owner] of owners) {
835
+ if (endpoints.has(name) || Object.hasOwn(service.env ?? {}, name)
836
+ || (service.id !== owner && Object.hasOwn(service.issues?.fill ?? {}, name))) {
837
+ throw new Error(`Required credential ${name} owned by ${owner} conflicts with service ${service.id} in ${path}`);
838
+ }
839
+ }
840
+ }
841
+ }
842
+
777
843
  export const DEFAULT_WORLD_SELECTION: WorldSelection = {
778
844
  include: [{ usage: 'application' }],
779
845
  exclude: [],
@@ -810,7 +876,7 @@ function serviceV2ToLegacy(value: unknown, path: string): WorldServiceConfig {
810
876
  assertKeys(source, ['package', 'version'], 'service.source', path);
811
877
  assertKeys(execution, ['process', 'colocate', 'lifecycle', 'cwd', 'environment', 'preload', 'controlPlane', 'branchState'], 'service.execution', path);
812
878
  assertKeys(endpoint, ['port', 'portReason', 'ready'], 'service.endpoint', path);
813
- assertKeys(bindings, ['injectEnv', 'injectEnvTemplates', 'cliRedirect', 'discover'], 'service.bindings', path);
879
+ assertKeys(bindings, ['injectEnv', 'injectEnvTemplates', 'cliRedirect', 'discover', 'issues'], 'service.bindings', path);
814
880
  const process = execution.process === undefined ? {} : execution.process;
815
881
  const environment = execution.environment === undefined ? {} : execution.environment;
816
882
  if (!record(process) || !record(environment)) throw new Error(`Service execution process/environment must be objects in ${path}`);
@@ -867,6 +933,7 @@ function serviceV2ToLegacy(value: unknown, path: string): WorldServiceConfig {
867
933
  ...(bindings.injectEnv === undefined ? {} : { injectEnv: bindings.injectEnv as string }),
868
934
  ...(bindings.injectEnvTemplates === undefined ? {} : { injectEnvTemplates: bindings.injectEnvTemplates as Record<string, string> }),
869
935
  ...(bindings.cliRedirect === undefined ? {} : { cliRedirect: bindings.cliRedirect as Record<string, string> }),
936
+ ...(!Object.hasOwn(bindings, 'issues') ? {} : { issues: readCredentialDoor(bindings.issues, value.id, path) }),
870
937
  ...(value.root === undefined ? {} : { root: value.root as WorldServiceConfig['root'] }),
871
938
  ...(external === undefined ? {} : { external }),
872
939
  };
@@ -984,6 +1051,7 @@ export function assertWorldConfig(value: unknown, path: string): WorldConfig {
984
1051
  return normalizeV2(value, path);
985
1052
  }
986
1053
  const legacy = assertLegacyWorldConfig(value, path);
1054
+ if (legacy.services.some(service => service.issues?.required !== undefined)) throw new Error(`Required credential bindings require a format-2 World manifest in ${path}`);
987
1055
  return { ...legacy, manifestVersion: 1 };
988
1056
  }
989
1057
 
@@ -1021,6 +1089,7 @@ function serviceToV2(service: WorldServiceConfig): Record<string, unknown> {
1021
1089
  ...(service.ready === undefined ? {} : { ready: service.ready }),
1022
1090
  };
1023
1091
  const bindings = {
1092
+ ...(service.issues === undefined ? {} : { issues: readCredentialDoor(service.issues, service.id, 'World serialization') }),
1024
1093
  ...(service.injectEnv === undefined ? {} : { injectEnv: service.injectEnv }),
1025
1094
  ...(service.injectEnvTemplates === undefined ? {} : { injectEnvTemplates: service.injectEnvTemplates }),
1026
1095
  ...(service.cliRedirect === undefined ? {} : { cliRedirect: service.cliRedirect }),
@@ -1044,6 +1113,7 @@ function serviceToV2(service: WorldServiceConfig): Record<string, unknown> {
1044
1113
 
1045
1114
  /** Serialize normalized intent as the committed format-2 manifest. */
1046
1115
  export function worldConfigDocument(config: WorldConfig, selection: WorldSelection = config.selection ?? DEFAULT_WORLD_SELECTION): WorldManifestV2 & Record<string, unknown> {
1116
+ assertLegacyWorldConfig({ ...config, manifestVersion: 2 }, 'World serialization');
1047
1117
  return {
1048
1118
  schemaVersion: WORLD_SCHEMA_VERSION,
1049
1119
  metadata: { id: config.id, ...(config.description === undefined ? {} : { description: config.description }) },
package/src/serve.ts CHANGED
@@ -8,7 +8,8 @@
8
8
  import { timingSafeEqual } from 'node:crypto';
9
9
  import { createServer as createHttpServer, type Server as HttpServer } from 'node:http';
10
10
  import { deriveWorldManifest, type WorldManifest } from './attach.ts';
11
- import { statusWorld } from './runtime.ts';
11
+ import { refuseRequiredCredentialTransport, statusWorld } from './runtime.ts';
12
+ import { errorMessage } from './error-message.ts';
12
13
  import { worldAttachmentNetworkEnv, worldStrictEgress } from '@volter/world-core/network-policy';
13
14
 
14
15
  export const MANIFEST_PATH = '/.well-known/volter-world';
@@ -16,7 +17,9 @@ export const MANIFEST_PATH = '/.well-known/volter-world';
16
17
  /** The manifest a REMOTE attacher sees: every vendor behind the one advertised
17
18
  * door. The serving side keeps routing against its own loopback env. */
18
19
  export function advertiseWorldManifest(name: string, root: string, advertisedOrigin: string): WorldManifest {
19
- const local = deriveWorldManifest(name, statusWorld(name, root).env);
20
+ const status = statusWorld(name, root);
21
+ refuseRequiredCredentialTransport(status, 'remote WorldManifest publication');
22
+ const local = deriveWorldManifest(name, status.env);
20
23
  const vendors: Record<string, string> = {};
21
24
  // /__vendor/<id> keeps the fetch flow routable through the one door (the
22
25
  // injector's fetch patch rewrites URLs wholesale and loses the vendor Host);
@@ -57,8 +60,15 @@ export async function startManifestServer(options: ManifestServerOptions): Promi
57
60
  response.end('volter-world: GET ' + MANIFEST_PATH + '\n');
58
61
  return;
59
62
  }
63
+ let body: string;
64
+ try { body = `${JSON.stringify(options.manifest(), null, 2)}\n`; }
65
+ catch (error) {
66
+ response.writeHead(503, { 'content-type': 'text/plain' });
67
+ response.end(`World manifest unavailable: ${errorMessage(error)}\n`);
68
+ return;
69
+ }
60
70
  response.writeHead(200, { 'content-type': 'application/json' });
61
- response.end(`${JSON.stringify(options.manifest(), null, 2)}\n`);
71
+ response.end(body);
62
72
  });
63
73
  await new Promise<void>((resolveListen, reject) => {
64
74
  server.once('error', reject);
@@ -81,18 +91,26 @@ export async function fetchRemoteManifest(ref: string, options: { token?: string
81
91
  const response = await fetch(url, options.token === undefined ? undefined : { headers: { authorization: `Bearer ${options.token}` } });
82
92
  if (!response.ok) throw new Error(`remote world manifest: ${url} → ${response.status}`);
83
93
  const manifest = (await response.json()) as WorldManifest;
84
- if (typeof manifest.name !== 'string' || typeof manifest.vendors !== 'object' || manifest.vendors === null) {
94
+ if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)
95
+ || typeof manifest.name !== 'string' || typeof manifest.vendors !== 'object' || manifest.vendors === null) {
85
96
  throw new Error(`remote world manifest: ${url} returned no manifest`);
86
97
  }
98
+ refuseRequiredRemoteManifest(manifest);
87
99
  return manifest;
88
100
  }
89
101
 
102
+ /** Required credentials have no approved remote transport; their field must never be silently ignored. */
103
+ function refuseRequiredRemoteManifest(manifest: WorldManifest): void {
104
+ if (Object.hasOwn(manifest, 'requiredCredentials')) throw new Error('Remote WorldManifest required credential transport is unsupported; refusing attachment');
105
+ }
106
+
90
107
  /** The env a remote env-attachment exports: vendor twin URLs (the advertised
91
108
  * door), suggested fake credentials, CA trust, and the injector preload. */
92
109
  export function remoteAttachEnv(
93
110
  manifest: WorldManifest,
94
111
  options: { caFile?: string; injectPath: string; worldRef?: string; callerEnv?: Readonly<Record<string, string | undefined>> },
95
112
  ): Record<string, string> {
113
+ refuseRequiredRemoteManifest(manifest);
96
114
  const env: Record<string, string> = { ...manifest.env,
97
115
  ...worldAttachmentNetworkEnv(options.worldRef ?? manifest.name, manifest.network, options.callerEnv ?? process.env, worldStrictEgress(manifest.env)),
98
116
  };
@@ -53,6 +53,7 @@ import { describeActionFields, resourceBlobFile } from '@volter/world-core/runti
53
53
  import { worldLedgers } from './changeset.ts';
54
54
  import { clockFile, downWorld, instanceDir, isWorldOrigin, listWorlds, readWorldBootRecord, statusWorld, upWorld } from './runtime.ts';
55
55
  import { assertChangesetRules, type WorldChangesetRules, type WorldInstance } from './schema.ts';
56
+ import { forwardableHeaders } from './forwardable-headers.ts';
56
57
 
57
58
  export const TOKEN_HEADER = 'x-volter-token';
58
59
  /** The header the injector carries a hosted World's token in (inject.cjs, VOLTER_TWINS_KEY). */
@@ -194,7 +195,7 @@ function configRefFor(worldRoot: string, name: string): string {
194
195
  /** A world mounted by a host. `token`/`readToken` read the CURRENT tokens (rotate changes them);
195
196
  * `twins()` names the world's twins once booted; `rotate()` mints both tokens anew — the old die
196
197
  * with the call — and keeps them beside the world like `boot` does. */
197
- export type MountedWorld = { maxRequestBodySize: number; name: string; served: string; root: string; readonly token: string; readonly readToken: string; twins: () => string[]; rotate: () => { token: string; readToken: string }; handle: (request: Request) => Promise<Response>; /** a hostname this World's twins answer, as a browser reads it (siteOfHost) */ site: (request: Request, host: string) => Promise<Response>; /** the same site at its local address, its framed pages carrying the board's picture script (localSite) */ localSite: (request: Request, host: string) => Promise<Response>; /** WorldDoors.pagesPass */ pagesPass: (scope: 'read' | 'write', land: string, grant?: { who?: string; framer?: string; asker?: string }) => string | null; branchBoard: (frame?: string) => Promise<Response>; boot: (url: string) => Promise<void>; stop: () => Promise<void> };
198
+ export type MountedWorld = { maxRequestBodySize: number; name: string; served: string; root: string; readonly token: string; readonly readToken: string; /** whether its boot has finished: until then its twins are not known */ ready: () => boolean; twins: () => string[]; rotate: () => { token: string; readToken: string }; handle: (request: Request) => Promise<Response>; /** a hostname this World's twins answer, as a browser reads it (siteOfHost) */ site: (request: Request, host: string) => Promise<Response>; /** the same site at its local address, its framed pages carrying the board's picture script (localSite) */ localSite: (request: Request, host: string) => Promise<Response>; /** WorldDoors.pagesPass */ pagesPass: (scope: 'read' | 'write', land: string, grant?: { who?: string; framer?: string; asker?: string }) => string | null; branchBoard: (frame?: string) => Promise<Response>; boot: (url: string) => Promise<void>; stop: () => Promise<void> };
198
199
  /** A browser session a World keeps: its scope, when it ends, and the person a pass named, when one did. */
199
200
  /** What a content-plane pass, and the session it opens, carry: its scope; the control session that asked (the content
200
201
  * session ends with it; none when the World's token asked); the person; the console that frames it; and whether it
@@ -244,7 +245,7 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
244
245
  if (!target) return null;
245
246
  // the body as bytes, none when empty: an empty body streamed is sent chunked with no last chunk (Bun 1.2), and the
246
247
  // twin's server reads the next request on the connection as its continuation (HPE_INVALID_CHUNK_SIZE)
247
- return async (request) => { const at = new URL(request.url); const body = request.body ? new Uint8Array(await request.arrayBuffer()) : undefined; return fetch(`${target.replace(/\/+$/, '')}${at.pathname}${at.search}`, { method: request.method, headers: request.headers, ...(body?.byteLength ? { body } : {}), redirect: 'manual' } as RequestInit); };
248
+ return async (request) => { const at = new URL(request.url); const body = request.body ? new Uint8Array(await request.arrayBuffer()) : undefined; return fetch(`${target.replace(/\/+$/, '')}${at.pathname}${at.search}`, { method: request.method, headers: forwardableHeaders(request.headers), ...(body?.byteLength ? { body } : {}), redirect: 'manual' } as RequestInit); };
248
249
  },
249
250
  };
250
251
  const world = new WorldDoors(name, worldRoot, configRef, served, host, held('token') ?? mintToken('tok_'), held('token.read') ?? mintToken('tok_r_'));
@@ -255,6 +256,7 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
255
256
  name, served, root: worldRoot,
256
257
  maxRequestBodySize: Math.max(1024 * 1024 * 1024, ...loadWorldConfig(configRef, worldRoot).config.services.map((service) => packFacts()[service.package?.replace(/^@volter\/twin-/, '') ?? service.id]?.maxRequestBodySize ?? 0)),
257
258
  get token() { return world.token; }, get readToken() { return world.readToken; },
259
+ ready: () => host.ready(),
258
260
  twins: () => world.twins(),
259
261
  rotate: () => { world.retoken(mintToken('tok_'), mintToken('tok_r_')); keep(); return { token: world.token, readToken: world.readToken }; },
260
262
  handle: (request) => world.handle(request),
@@ -1209,7 +1211,7 @@ export class WorldDoors {
1209
1211
  const reads = request.method === 'GET' || request.method === 'HEAD';
1210
1212
  const enforced = browser && grant?.scope === 'read' && await this.enforcesRead(vendor, twin, url.origin);
1211
1213
  if (browser && grant?.scope === 'read' && !reads && !enforced) return Response.json({ error: 'the read token opens only reads' }, { status: 403 });
1212
- const headers = browser ? new Headers(request.headers) : new Headers();
1214
+ const headers = browser ? forwardableHeaders(request.headers) : new Headers();
1213
1215
  for (const key of [TOKEN_HEADER, TWINS_KEY_HEADER, CALLER_HEADER, ORIGINAL_PATH_HEADER, READ_ONLY_HEADER, 'host', TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, TWIN_SITE_HEADER]) headers.delete(key);
1214
1216
  for (const name of ['accept', 'accept-language', 'if-none-match', 'range', 'user-agent']) { const v = request.headers.get(name); if (v !== null) headers.set(name, v); }
1215
1217
  if (browser) {
@@ -1421,7 +1423,7 @@ export class WorldDoors {
1421
1423
  // (a Slack read over POST passes, a post is refused); a twin that cannot enforce that gets reads only
1422
1424
  const enforced = scope === 'read' && (await this.enforcesRead(vendor!, twin, url.origin));
1423
1425
  if (scope === 'read' && !reads && !enforced) return Response.json({ error: 'the read token opens only reads' }, { status: 403 });
1424
- const headers = new Headers(request.headers); headers.delete(TOKEN_HEADER); headers.delete(TWINS_KEY_HEADER); headers.delete('host');
1426
+ const headers = forwardableHeaders(request.headers); headers.delete(TOKEN_HEADER); headers.delete(TWINS_KEY_HEADER); headers.delete('host');
1425
1427
  // who made it is the World's word, never the caller's: the twin's request journal records it. The World is a hop of
1426
1428
  // the request's trace: the caller's trace-id goes on (or a new one), under a parent-id the World mints, which the
1427
1429
  // request's log entries carry, so each entry is joined to its caller by an id no caller chose
@@ -1507,7 +1509,7 @@ export class WorldDoors {
1507
1509
  private async forward(request: Request, url: URL, rest: string, name: string, origin: string): Promise<Response> {
1508
1510
  // what leaves for the vendor before the seal is applied: no World token in any header it can arrive
1509
1511
  // under (an SDK's own `Authorization: Bearer` included), no cookie, no routing headers of this World
1510
- const caller = new Headers(request.headers);
1512
+ const caller = forwardableHeaders(request.headers);
1511
1513
  for (const h of [TOKEN_HEADER, TWINS_KEY_HEADER, TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, CALLER_HEADER, 'host', 'x-forwarded-host', 'x-forwarded-proto', 'cookie']) caller.delete(h);
1512
1514
  // a secret this World minted (`tok_…`, live, revoked or expired) or one that opens it never leaves
1513
1515
  const authorization = caller.get('authorization');
@@ -1,29 +1,51 @@
1
- // Process services run without a shell. On Windows npm's Bun command is a .cmd
2
- // shim, whose native package executable must be selected before Node spawns it.
1
+ // Process services run without a shell. On Windows a bare command name is found the way a shell would find it, and an
2
+ // npm command shim (`.cmd`, which Node's shell-free spawn cannot execute) runs as what it launches: its package's
3
+ // script under Node, or the native executable of npm's Bun.
3
4
  import { readFileSync, statSync } from 'node:fs';
4
- import { join, resolve } from 'node:path';
5
+ import { isAbsolute, join, resolve } from 'node:path';
5
6
 
6
7
  const file = (path: string): boolean => {
7
8
  try { return statSync(path).isFile(); } catch { return false; }
8
9
  };
9
10
 
10
- /** Resolve only the known npm Bun layout, never execute or interpret a command
11
- * script. Keep explicit paths, other commands and non-Windows behavior intact. */
12
- export function serviceExecutable(command: string, env: NodeJS.ProcessEnv): string {
13
- if (process.platform !== 'win32' || command.toLowerCase() !== 'bun') return command;
11
+ /** npm's Bun package: `bun.cmd` beside `node_modules/bun/bin/bun.exe`. */
12
+ function npmBun(directory: string): string | undefined {
13
+ const installed = resolve(directory, 'node_modules', 'bun');
14
+ const executable = join(installed, 'bin', 'bun.exe');
15
+ if (!file(executable)) return undefined;
16
+ try {
17
+ const manifest = JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8')) as { name?: unknown; bin?: { bun?: unknown } };
18
+ if (manifest.name === 'bun' && manifest.bin?.bun === 'bin/bun.exe') return executable;
19
+ } catch { /* An incomplete installation follows the ordinary spawn-error path. */ }
20
+ return undefined;
21
+ }
22
+
23
+ /** The script an npm cmd-shim launches (`"%dp0%\node_modules\<package>\bin\<tool>.js" %*`), when it names one under
24
+ * its own directory. Only the shim's text is read; nothing in it is executed. */
25
+ function shimScript(shim: string): string | undefined {
26
+ let text: string;
27
+ try { text = readFileSync(shim, 'utf8'); } catch { return undefined; }
28
+ const match = /"%dp0%\\([^"%]+\.[cm]?js)"\s+%\*/i.exec(text) ?? /"%~dp0\\([^"%]+\.[cm]?js)"\s+%\*/i.exec(text);
29
+ if (!match) return undefined;
30
+ const script = resolve(shim, '..', match[1]!);
31
+ return file(script) ? script : undefined;
32
+ }
33
+
34
+ /** The program and arguments a service command runs as. Explicit paths, commands with an extension and every
35
+ * non-Windows command keep their meaning; a command that cannot be resolved is spawned as given, and fails through
36
+ * the ordinary recorded spawn error. */
37
+ export function serviceInvocation(command: string, args: string[], env: NodeJS.ProcessEnv): { command: string; args: string[] } {
38
+ if (process.platform !== 'win32' || !command || isAbsolute(command) || /[\\/]/.test(command) || /\.[a-z0-9]+$/i.test(command)) return { command, args };
14
39
  const path = Object.entries(env).find(([key]) => key.toUpperCase() === 'PATH')?.[1] ?? '';
15
- const directories = [process.cwd(), ...path.split(';').map((value) => value.replace(/^"(.*)"$/, '$1'))];
40
+ const directories = [process.cwd(), ...path.split(';').filter(Boolean).map((value) => value.replace(/^"(.*)"$/, '$1'))];
16
41
  for (const directory of directories) {
17
- const native = resolve(directory, 'bun.exe');
18
- if (file(native)) return native;
19
- if (!file(resolve(directory, 'bun.cmd'))) continue;
20
- const installed = resolve(directory, 'node_modules', 'bun');
21
- const executable = join(installed, 'bin', 'bun.exe');
22
- if (!file(executable)) continue;
23
- try {
24
- const manifest = JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8')) as { name?: unknown; bin?: { bun?: unknown } };
25
- if (manifest.name === 'bun' && manifest.bin?.bun === 'bin/bun.exe') return executable;
26
- } catch { /* An incomplete installation follows the ordinary spawn-error path. */ }
42
+ const native = resolve(directory, `${command}.exe`);
43
+ if (file(native)) return { command: native, args };
44
+ const shim = resolve(directory, `${command}.cmd`);
45
+ if (!file(shim)) continue;
46
+ if (command.toLowerCase() === 'bun') { const bun = npmBun(directory); if (bun) return { command: bun, args }; }
47
+ const script = shimScript(shim);
48
+ if (script) return { command: process.execPath, args: [script, ...args] };
27
49
  }
28
- return command;
50
+ return { command, args };
29
51
  }
@@ -0,0 +1,21 @@
1
+ /** The recorder's environment and its child's are computed here so the boot gate sees the exact final names. */
2
+ export function recorderEnvironment(env: NodeJS.ProcessEnv, service: string, log: string, logDir: string, events: string): NodeJS.ProcessEnv {
3
+ const out: NodeJS.ProcessEnv = { ...env, VOLTER_WORLD_SERVICE_ID: service, VOLTER_WORLD_SERVICE_LOG: log,
4
+ VOLTER_WORLD_SERVICE_LOG_DIR: logDir, VOLTER_WORLD_EVENTS: events };
5
+ if (out.NODE_OPTIONS !== undefined) {
6
+ out.VOLTER_WORLD_SERVICE_NODE_OPTIONS = out.NODE_OPTIONS;
7
+ delete out.NODE_OPTIONS;
8
+ }
9
+ return out;
10
+ }
11
+
12
+ /** Restore the injector for the real child and omit only the recorder's existing private transport names. */
13
+ export function recordedApplicationEnvironment(recorded: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
14
+ const out: NodeJS.ProcessEnv = { ...recorded };
15
+ if (out.VOLTER_WORLD_SERVICE_NODE_OPTIONS !== undefined) {
16
+ out.NODE_OPTIONS = out.VOLTER_WORLD_SERVICE_NODE_OPTIONS;
17
+ delete out.VOLTER_WORLD_SERVICE_NODE_OPTIONS;
18
+ }
19
+ for (const key of ['VOLTER_WORLD_EVENTS', 'VOLTER_WORLD_SERVICE_LOG_MAX']) delete out[key];
20
+ return out;
21
+ }
@@ -17,19 +17,18 @@
17
17
  // The World's NODE_OPTIONS (the injector) is for the service, not this parent: it arrives renamed and is restored.
18
18
  import { signalChild } from './process-signal.mjs';
19
19
  import { errorMessage } from './error-message.ts';
20
- import { serviceExecutable } from './service-command.ts';
20
+ import { serviceInvocation } from './service-command.ts';
21
+ import { recordedApplicationEnvironment } from './service-environment.ts';
21
22
  import { spawn } from 'node:child_process';
22
23
  import { constants } from 'node:os';
23
24
  import { appendFileSync, closeSync, fstatSync, openSync, readSync, statSync, truncateSync, writeFileSync } from 'node:fs';
24
25
 
25
26
  const [command = '', ...args] = process.argv.slice(2);
26
- const env = { ...process.env };
27
- if (env.VOLTER_WORLD_SERVICE_NODE_OPTIONS !== undefined) { env.NODE_OPTIONS = env.VOLTER_WORLD_SERVICE_NODE_OPTIONS; delete env.VOLTER_WORLD_SERVICE_NODE_OPTIONS; }
28
- const service = env.VOLTER_WORLD_SERVICE_ID;
29
- const logPath = env.VOLTER_WORLD_SERVICE_LOG;
30
- const eventsPath = env.VOLTER_WORLD_EVENTS;
31
- const cap = Number(env.VOLTER_WORLD_SERVICE_LOG_MAX) > 0 ? Number(env.VOLTER_WORLD_SERVICE_LOG_MAX) : 64 * 1024 * 1024;
32
- for (const key of ['VOLTER_WORLD_EVENTS', 'VOLTER_WORLD_SERVICE_LOG_MAX']) delete env[key];
27
+ const env = recordedApplicationEnvironment(process.env);
28
+ const service = process.env.VOLTER_WORLD_SERVICE_ID;
29
+ const logPath = process.env.VOLTER_WORLD_SERVICE_LOG;
30
+ const eventsPath = process.env.VOLTER_WORLD_EVENTS;
31
+ const cap = Number(process.env.VOLTER_WORLD_SERVICE_LOG_MAX) > 0 ? Number(process.env.VOLTER_WORLD_SERVICE_LOG_MAX) : 64 * 1024 * 1024;
33
32
 
34
33
  /** The log's size and whether its last byte ends a line (a record never lands inside a service's line). */
35
34
  const logEnd = (): { size: number; atLineStart: boolean } => {
@@ -49,7 +48,8 @@ const record = (entry: Record<string, unknown>, then?: () => void): void => {
49
48
  };
50
49
 
51
50
  const started = Date.now();
52
- const child = spawn(serviceExecutable(command, env), args, { windowsHide: true, stdio: 'inherit', env });
51
+ const invocation = serviceInvocation(command, args, env);
52
+ const child = spawn(invocation.command, invocation.args, { windowsHide: true, stdio: 'inherit', env });
53
53
  if (child.pid !== undefined) record({ event: 'world.service.started', pid: child.pid, parentPid: process.pid });
54
54
 
55
55
  /** The log's last `bytes` (from a line start) as the rotation `<log>.1`: however far a runaway writer overshot the cap,