@volter/world-core 3.0.64 → 3.0.66

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.
@@ -0,0 +1,60 @@
1
+ import type { WorldNetworkPolicy } from './network-policy.cjs';
2
+
3
+ export type CapabilityName = 'network.egress' | 'network.ownLoopback' | 'network.services' | 'network.host' | 'files.persist';
4
+ export type RefusalSide = 'declaration' | 'grant' | 'transport' | 'twin';
5
+ export type DeclaredBy = { readonly capability: CapabilityName; readonly file: string; readonly line: number };
6
+
7
+ /** What a guest declares (an image's runtime binding `capabilities`, a browser program's hosts). */
8
+ export interface CapabilityDeclaration {
9
+ readonly network?: {
10
+ readonly egress?: '*' | readonly string[];
11
+ readonly ownLoopback?: boolean;
12
+ readonly listen?: readonly number[];
13
+ readonly services?: '*' | readonly string[];
14
+ readonly host?: boolean;
15
+ };
16
+ readonly files?: { readonly persist?: readonly string[] };
17
+ readonly derivedFrom?: readonly DeclaredBy[];
18
+ }
19
+
20
+ export interface WorldGrant { readonly policy?: WorldNetworkPolicy; readonly strict?: boolean; readonly unreadable?: string }
21
+ export interface WorldEndpoint { readonly host: string; readonly port: number; readonly kind: 'door' | 'service'; readonly name?: string }
22
+ export interface CapabilityFacts {
23
+ readonly twin?: { readonly vendor: string; readonly origin: string } | null;
24
+ readonly twinnedHostVendors?: readonly string[];
25
+ readonly twinOrigin?: boolean;
26
+ readonly invalid?: boolean;
27
+ readonly guestProxy?: URL | null;
28
+ readonly endpoint?: Pick<WorldEndpoint, 'kind' | 'name'> | null;
29
+ readonly ownListener?: boolean;
30
+ }
31
+ export interface CapabilityContext { readonly run?: { readonly guest?: boolean }; readonly declaration?: CapabilityDeclaration; readonly grant?: WorldGrant; readonly facts?: CapabilityFacts }
32
+
33
+ export type CapabilityRequest =
34
+ | { readonly kind: 'egress'; readonly url: string | URL | null; readonly method?: string; readonly transport?: 'http' | 'upgrade' | 'http2' | 'dispatch' | 'socket' | 'datagram'; readonly port?: number }
35
+ | { readonly kind: 'loopback'; readonly host: string; readonly port?: number; readonly transport?: 'tcp' | 'udp' }
36
+ | { readonly kind: 'persist'; readonly path: string };
37
+
38
+ export type CapabilityAnswer =
39
+ | { readonly outcome: 'pass'; readonly capability: CapabilityName; readonly target: string; readonly via: 'direct' | 'inspected' | 'passthrough' | 'guest-proxy' | 'run-scoped' }
40
+ | { readonly outcome: 'twin'; readonly capability: CapabilityName; readonly target: string; readonly twin: { readonly vendor: string; readonly origin: string } }
41
+ | { readonly outcome: 'refuse'; readonly capability: CapabilityName; readonly target: string; readonly missing: RefusalSide; readonly rule: string; readonly reason: string; readonly grantField?: string; readonly declaredBy?: DeclaredBy };
42
+
43
+ export interface WorldRefusalRecord { readonly kind: 'volter-world-refusal'; readonly version: 1; readonly capability: CapabilityName; readonly target: string; readonly missing: RefusalSide; readonly reason: string; readonly grantField?: string; readonly declaredBy?: DeclaredBy }
44
+
45
+ export interface CapabilitySet {
46
+ readonly network: { readonly egress: 'none' | 'proxy'; readonly loopback: 'none' | 'own'; readonly listen: readonly number[]; readonly endpoints: readonly WorldEndpoint[] };
47
+ readonly files: { readonly persist: readonly string[] };
48
+ readonly refusals: readonly Extract<CapabilityAnswer, { outcome: 'refuse' }>[];
49
+ }
50
+
51
+ export const OPEN_DECLARATION: CapabilityDeclaration;
52
+ export const EMPTY_DECLARATION: CapabilityDeclaration;
53
+ export const WORLD_DECLARATION_ENV: 'VOLTER_WORLD_DECLARATION';
54
+ export function worldDeclarationFromEnv(env: Readonly<Record<string, string | undefined>>): CapabilityDeclaration | undefined;
55
+ export function sandboxRefusal(target: string): Extract<CapabilityAnswer, { outcome: 'refuse' }>;
56
+ export const GRANT_FIELDS: Readonly<Partial<Record<CapabilityName, string>>>;
57
+ export function decideCapability(request: CapabilityRequest, context: CapabilityContext): CapabilityAnswer;
58
+ export function decideCapabilitySet(declaration: CapabilityDeclaration | undefined, grant?: WorldGrant, world?: { readonly endpoints?: readonly WorldEndpoint[] }): CapabilitySet;
59
+ export function refusalRecord(answer: Extract<CapabilityAnswer, { outcome: 'refuse' }>): WorldRefusalRecord;
60
+ export function refusalSide(answer: Extract<CapabilityAnswer, { outcome: 'refuse' }>): string;
package/dist/inject.cjs CHANGED
@@ -45,7 +45,8 @@ const { Readable } = require('stream');
45
45
  const Module = require('module');
46
46
  const { urlToHttpOptions } = require('node:url');
47
47
  const claimsRefreshChannel = require('node:diagnostics_channel').channel('volter.inject.claims-refresh');
48
- const { allowsWorldNetworkEgress, worldNetworkPolicyFromEnv, isWorldInternalHost, worldEgressRefusal, worldNetworkNeedsPathInspection, worldNetworkUsesPassthrough } = require('./network-policy.cjs');
48
+ const { worldNetworkPolicyFromEnv, isWorldInternalHost } = require('./network-policy.cjs');
49
+ const { OPEN_DECLARATION, worldDeclarationFromEnv, decideCapability, sandboxRefusal, refusalRecord, refusalSide } = require('./capability-decision.cjs');
49
50
  const { createPublicKey, createVerify } = require('crypto');
50
51
 
51
52
  // the vendor host table (VENDOR_HOSTS: a hostname, and a path where two vendors share a host, to the vendor whose twin
@@ -325,17 +326,50 @@ function strictEgressEnabled() {
325
326
  return process.env.VOLTER_TWIN_STRICT_EGRESS === '1' || process.env.VOLTER_TWIN_STRICT_EGRESS === 'true';
326
327
  }
327
328
 
328
- // Only an explicit public build grant takes precedence over vendor substitution.
329
- function publicBuildRequest(url) {
330
- if (!url || strictEgressEnabled()) return false;
331
- const policy = worldNetworkPolicyFromEnv(process.env);
332
- return !!policy && policy.phase === 'build' && allowsWorldNetworkEgress(policy, url);
329
+ // THE WORLD'S DECISION (docs/contributing/architecture.md, "Capability decisions"): every seam below asks
330
+ // capability-decision.cjs and enforces its answer. This process is a host process, whose declaration is the open one; a
331
+ // local light guest, whose HTTP leaves through its guest listener; or a process of an image run, which carries its
332
+ // image's declaration (VOLTER_WORLD_DECLARATION) and is confined from the host's network. What the World grants is its
333
+ // published policy. The seams supply what they observe: the twin route, the vendors sharing a host, the World's doors.
334
+ function worldGrant() {
335
+ let policy; let unreadable;
336
+ try { policy = worldNetworkPolicyFromEnv(process.env); } catch (error) { unreadable = error.message; }
337
+ return { policy, strict: strictEgressEnabled(), unreadable };
338
+ }
339
+ /** This process's run: its declaration, and whether it is confined from the host's network. */
340
+ function worldRun() {
341
+ const declared = worldDeclarationFromEnv(process.env);
342
+ return declared ? { guest: true, declaration: declared } : { guest: !!localGuestProxy(), declaration: OPEN_DECLARATION };
343
+ }
344
+ function decide(url, transport, facts = {}, port) {
345
+ const { guest, declaration } = worldRun();
346
+ return decideCapability({ kind: 'egress', url, transport, port },
347
+ { run: { guest }, declaration, grant: worldGrant(), facts: { guestProxy: localGuestProxy(), ...facts } });
348
+ }
349
+ /** What an HTTP seam observes about `url`. A local guest's request to one of the World's doors, or not over HTTP(S), is
350
+ * not mediated by its guest listener. */
351
+ function httpFacts(url, pathname) {
352
+ if (!url) return {};
353
+ const guestProxy = localGuestProxy();
354
+ const proxy = worldProxy();
355
+ return {
356
+ twin: resolveTwin(url.hostname, MAP, pathname === undefined ? url.pathname : pathname),
357
+ twinnedHostVendors: twinnedHostVendors(url.hostname, MAP),
358
+ ...(guestProxy && !/^https?:$/.test(url.protocol) ? { guestProxy: null } : {}),
359
+ endpoint: worldRun().guest && (addressedToWorld(url) || explicitTwinEndpoint(url) || (proxy && url.origin === proxy.origin)) ? { kind: 'door' } : null,
360
+ };
333
361
  }
334
-
335
- function shouldBlockUntwinned(url) {
336
- const routed = url && resolveTwin(url.hostname, MAP, url.pathname);
337
- if (!url || (routed && routed.vendor !== 'app')) return false;
338
- return worldEgressRefusal(url) !== null;
362
+ /** A refusal as an Error: the seam's own message, the side that refused, and the World's refusal record. */
363
+ function refusalError(answer, message) {
364
+ return Object.assign(new Error(`${message} (${refusalSide(answer)})`), { worldRefusal: refusalRecord(answer) });
365
+ }
366
+ /** The World's listing refused it (sealed, unlisted, an unreadable policy). */
367
+ const LISTING_RULES = new Set(['sealed', 'not-granted', 'policy-unreadable']);
368
+ /** The message an HTTP seam (`request`, `fetch`) gives a refusal. */
369
+ function httpRefusalMessage(answer, url, seam) {
370
+ if (answer.rule === 'unclaimed-path') return unclaimedTwinnedHostPathMessage(url.hostname, url.pathname, MAP);
371
+ if (LISTING_RULES.has(answer.rule)) return `[twin-inject] blocked untwinned external ${seam} to ${url.hostname}`;
372
+ return `[twin-inject] ${answer.reason}`;
339
373
  }
340
374
 
341
375
  // --- untwinned-host TLS failure naming (local mode) -----------------------------------------
@@ -462,6 +496,10 @@ function worldBase() {
462
496
  }
463
497
  // The local Substrate agent keeps host control routing; its product commands select the
464
498
  // existing guest listener. Route original destinations through that listener before rewriting.
499
+ /** The World proxy this process is pointed at, or null. */
500
+ function worldProxy() {
501
+ try { return new URL(process.env.VOLTER_WORLD_PROXY || ''); } catch { return null; }
502
+ }
465
503
  function localGuestProxy() {
466
504
  const guest = process.env.VOLTER_WORLD_GUEST_PROXY;
467
505
  if (!guest || process.env.VOLTER_WORLD_PROXY !== guest) return null;
@@ -475,10 +513,6 @@ function explicitTwinEndpoint(url) {
475
513
  } catch { return false; }
476
514
  });
477
515
  }
478
- function mediateLocalGuest(url) {
479
- const proxy = localGuestProxy();
480
- return proxy && url && /^https?:$/.test(url.protocol) && !addressedToWorld(url) && !explicitTwinEndpoint(url) && url.origin !== proxy.origin ? proxy : null;
481
- }
482
516
  function addressedToWorld(url) {
483
517
  const base = worldBase();
484
518
  if (!base || !url || !base.path || url.origin.replace(/^ws/, 'http') !== base.origin) return false;
@@ -545,30 +579,30 @@ function keyedView(dispatcher) {
545
579
  if (name === 'dispatch') {
546
580
  return function keyedDispatch(opts, handler) {
547
581
  const key = ORIGINAL_DEFINE_PROPERTY ? twinsKey() : null;
582
+ let answer = null;
548
583
  if (ORIGINAL_DEFINE_PROPERTY && opts?.origin != null) {
549
- const policy = worldNetworkPolicyFromEnv(process.env);
550
- let at;
584
+ let at = null; let invalid = false;
551
585
  try {
552
586
  const origin = new URL(String(opts.origin));
553
587
  const path = String(opts.path ?? '/');
554
588
  if (!path.startsWith('/') || /\\/.test(path)) throw new Error('invalid dispatch path');
555
589
  at = new URL(origin.origin + path);
556
- } catch {
557
- if (policy) { process.nextTick(() => handler.onError(new Error('[twin-inject] invalid effective undici request'))); return false; }
558
- }
559
- if (at && !isLocalOrPrivateHost(at.hostname) && !isTwinOriginHost(at.hostname) && (!resolveTwin(at.hostname, MAP, at.pathname) || publicBuildRequest(at))
560
- && (shouldBlockUntwinned(at) || (policy && worldNetworkNeedsPathInspection(policy, at)))) {
561
- process.nextTick(() => handler.onError(new Error('[twin-inject] unmediated undici hop cannot use inspected egress')));
590
+ } catch { invalid = true; }
591
+ answer = decide(at, 'dispatch', at ? { twin: resolveTwin(at.hostname, MAP, at.pathname), twinnedHostVendors: twinnedHostVendors(at.hostname, MAP), twinOrigin: isTwinOriginHost(at.hostname) } : { invalid });
592
+ if (answer.outcome === 'refuse') {
593
+ const message = answer.rule === 'invalid' ? '[twin-inject] invalid effective undici request'
594
+ : answer.rule === 'unclaimed-path' ? unclaimedTwinnedHostPathMessage(at.hostname, at.pathname, MAP)
595
+ : '[twin-inject] unmediated undici hop cannot use inspected egress';
596
+ process.nextTick(() => handler.onError(refusalError(answer, message)));
562
597
  return false;
563
598
  }
564
599
  }
565
600
  // Undici's shared dispatcher is an HTTP seam even when its caller bypasses global fetch.
566
601
  // Rewrite the hop, not its socket: the original host remains available to the twin.
567
- if (ORIGINAL_DEFINE_PROPERTY && opts?.origin != null) {
568
- let at;
569
- try { at = new URL(new URL(String(opts.origin)).origin + String(opts.path ?? '/')); } catch {}
570
- const twin = at && !publicBuildRequest(at) && resolveTwin(at.hostname, MAP, at.pathname);
571
- if (twin && twin.vendor !== 'app') {
602
+ if (answer && answer.outcome === 'twin') {
603
+ const at = new URL(answer.target);
604
+ const twin = answer.twin;
605
+ if (twin.vendor !== 'app') {
572
606
  const endpoint = new URL(twin.origin);
573
607
  const headers = undiciHeaderObject(opts.headers);
574
608
  headers['host'] = at.host;
@@ -605,6 +639,12 @@ function keyUndiciDispatches() {
605
639
  };
606
640
  for (const name of UNDICI_DISPATCHERS) if (globalThis[name]) globalThis[name] = keyedView(globalThis[name]);
607
641
  }
642
+ /** The twin the World routes `url` to, or null: its other answers are the dispatcher's and the sockets' to enforce. */
643
+ function routedTwin(url) {
644
+ if (!url) return null;
645
+ const answer = decide(url, 'dispatch', { twin: resolveTwin(url.hostname, MAP, url.pathname) });
646
+ return answer.outcome === 'twin' ? answer.twin : null;
647
+ }
608
648
  // Imported Undici APIs may hold a dispatcher obtained before the global view was installed.
609
649
  // Rewrite their public HTTP doors as well; an independent connector still faces the socket guards.
610
650
  let UNDICI_LOAD = null;
@@ -620,7 +660,7 @@ function routeImportedUndici() {
620
660
  view.fetch = function routedUndiciFetch(input, init) {
621
661
  if (!installed) return exports.fetch(input, init);
622
662
  let url; try { url = new URL(typeof input === 'object' && input?.url ? input.url : String(input)); } catch {}
623
- const twin = url && !publicBuildRequest(url) && resolveTwin(url.hostname, MAP, url.pathname);
663
+ const twin = routedTwin(url);
624
664
  // Local control planes may use their own Request class and dispatcher. Preserve both;
625
665
  // connections still pass the socket guards, and claimed vendor hops still enter routing.
626
666
  if (url && isLocalOrPrivateHost(url.hostname) && (!twin || twin.vendor === 'app')) return exports.fetch(input, init);
@@ -639,7 +679,7 @@ function routeImportedUndici() {
639
679
  if (!installed) return exports.request.apply(exports, arguments);
640
680
  const opts = typeof options === 'function' ? {} : options ?? {};
641
681
  let url; try { url = new URL(String(input)); if (opts.path !== undefined) url = new URL(url.origin + String(opts.path)); } catch {}
642
- const twin = url && !publicBuildRequest(url) && resolveTwin(url.hostname, MAP, url.pathname);
682
+ const twin = routedTwin(url);
643
683
  if (!twin || twin.vendor === 'app') return exports.request.apply(exports, arguments);
644
684
  const headers = undiciHeaderObject(opts.headers);
645
685
  headers.host = url.host; headers['x-volter-twin-original-host'] = url.host;
@@ -962,7 +1002,7 @@ function unclaimedTwinnedHostPathMessage(hostname, pathname, map) {
962
1002
 
963
1003
  function blockedClientRequest(message) {
964
1004
  const request = new EventEmitter();
965
- const error = new Error(message);
1005
+ const error = message instanceof Error ? message : new Error(message);
966
1006
  const socket = new EventEmitter();
967
1007
  socket.connecting = false;
968
1008
  socket.setKeepAlive = () => socket;
@@ -1154,46 +1194,53 @@ function socketTargetPort(args) {
1154
1194
  return Number.isInteger(port) && port > 0 && port < 65536 ? port : undefined;
1155
1195
  }
1156
1196
 
1157
- function socketRefusal(hostname, port, isTls) {
1197
+ /** Why the World refuses a socket to `hostname`:`port` (an Error carrying the refusal record), or null. */
1198
+ function socketRefusal(hostname, port, isTls, datagram = false) {
1158
1199
  const host = String(hostname || '').toLowerCase();
1200
+ // an addressing mistake, not a capability: the World's DNS answer for the names it routes leads nowhere
1159
1201
  if (host === WORLD_ADDRESS[4] || host === WORLD_ADDRESS[6]) {
1160
- return `[twin-inject] refused a socket to ${host}, the address this World's DNS gives the names it routes: `
1161
- + 'nothing listens there; connect by the name, which the World routes to its twin or its application.';
1202
+ return new Error(`[twin-inject] refused a socket to ${host}, the address this World's DNS gives the names it routes: `
1203
+ + 'nothing listens there; connect by the name, which the World routes to its twin or its application.');
1162
1204
  }
1163
- if (localGuestProxy() && isLocalOrPrivateHost(host)) {
1164
- const endpoints = [localGuestProxy(), worldBase() && new URL(worldBase().origin), ...Object.values(MAP).map(origin => { try { return new URL(origin); } catch { return null; } })].filter(Boolean);
1165
- if (!endpoints.some(endpoint => endpoint.hostname.replace(/^\[|\]$/g, '') === host.replace(/^\[|\]$/g, '') && Number(endpoint.port || (endpoint.protocol === 'https:' ? 443 : 80)) === port)) {
1166
- return `[twin-inject] local guest private socket to ${host}:${port} requires its guest proxy`;
1167
- }
1168
- }
1169
- if (!host || isLocalOrPrivateHost(host) || isTwinOriginHost(host)) return null;
1205
+ if (!host) return null;
1170
1206
  // the destination as an origin, its port and its protocol included: a policy that lists https://host allows TLS to
1171
1207
  // host:443, never another port or a plaintext connection to it
1172
1208
  const authority = `${isIP(host) === 6 ? `[${host}]` : host}${port !== undefined && !(isTls && port === 443) ? `:${port}` : ''}`;
1173
1209
  const url = (() => { try { return new URL(`${isTls ? 'https' : 'http'}://${authority}/`); } catch { return null; } })();
1174
- const policy = worldNetworkPolicyFromEnv(process.env);
1175
- if (url && policy && !strictEgressEnabled() && worldNetworkUsesPassthrough(policy, url)) return null;
1176
- const vendors = twinnedHostVendors(host, MAP);
1177
- if (vendors.length > 0) {
1210
+ if (!url) return null;
1211
+ const internal = isLocalOrPrivateHost(host);
1212
+ const facts = internal
1213
+ ? { endpoint: worldRun().guest && guestDoorAt(host, port) ? { kind: 'door' } : null }
1214
+ : { twin: resolveTwin(host, MAP), twinnedHostVendors: twinnedHostVendors(host, MAP), twinOrigin: isTwinOriginHost(host) };
1215
+ const answer = decide(url, datagram ? 'datagram' : 'socket', facts, port);
1216
+ if (answer.outcome !== 'refuse') return null;
1217
+ let message = `[twin-inject] ${answer.reason}`;
1218
+ if (answer.rule === 'guest-private') message = `[twin-inject] local guest private socket to ${host}:${port} requires its guest proxy`;
1219
+ else if (answer.rule === 'raw-socket-twinned') {
1220
+ const vendors = facts.twinnedHostVendors;
1178
1221
  const doors = vendors.map(endpointEnvFor).filter(Boolean);
1179
- return `[twin-inject] refused a raw socket to ${host}, which is twinned in this world (${vendors.join(', ')}): `
1222
+ message = `[twin-inject] refused a raw socket to ${host}, which is twinned in this world (${vendors.join(', ')}): `
1180
1223
  + `the client opened its own connection instead of going through http/https or fetch, so the twin never saw it. `
1181
1224
  + (doors.length > 0
1182
1225
  ? `The SDK reaches the twin through ${doors.join('; ')}, which the world sets when it runs the app.`
1183
1226
  : `This client needs an endpoint door on the pack before the world can serve it.`);
1184
- }
1185
- if (resolveTwin(host, MAP)?.vendor === 'app') {
1186
- return `[twin-inject] refused a raw socket to ${host}, which this world routes to the application (volter-world app-url --host): `
1227
+ } else if (answer.rule === 'raw-socket-app') {
1228
+ message = `[twin-inject] refused a raw socket to ${host}, which this world routes to the application (volter-world app-url --host): `
1187
1229
  + 'the client opened its own connection instead of going through http/https or fetch, so it could not be routed.';
1188
- }
1189
- if (url && policy && !strictEgressEnabled() && worldNetworkNeedsPathInspection(policy, url)) return `[twin-inject] inspected egress requires a mediated HTTP request to ${authority}`;
1190
- if (url && shouldBlockUntwinned(url)) return `[twin-inject] blocked untwinned external socket to ${authority}`;
1191
- return null;
1230
+ } else if (answer.rule === 'inspected-transport') message = `[twin-inject] inspected egress requires a mediated HTTP request to ${authority}`;
1231
+ else if (LISTING_RULES.has(answer.rule)) message = `[twin-inject] blocked untwinned external socket to ${authority}`;
1232
+ return refusalError(answer, message);
1233
+ }
1234
+
1235
+ /** One of the World's doors a guest connects to directly: its proxy (a local guest's listener), the World's origin, a twin. */
1236
+ function guestDoorAt(host, port) {
1237
+ const doors = [worldProxy(), localGuestProxy(), worldBase() && new URL(worldBase().origin), ...Object.values(MAP).map(origin => { try { return new URL(origin); } catch { return null; } })].filter(Boolean);
1238
+ return doors.some(door => door.hostname.replace(/^\[|\]$/g, '') === host.replace(/^\[|\]$/g, '') && Number(door.port || (door.protocol === 'https:' ? 443 : 80)) === port);
1192
1239
  }
1193
1240
 
1194
- function refusedSocket(message) {
1241
+ function refusedSocket(refusal) {
1195
1242
  const socket = new net.Socket();
1196
- process.nextTick(() => socket.destroy(Object.assign(new Error(message), { code: 'ECONNREFUSED' })));
1243
+ process.nextTick(() => socket.destroy(Object.assign(refusal, { code: 'ECONNREFUSED' })));
1197
1244
  return socket;
1198
1245
  }
1199
1246
 
@@ -1226,18 +1273,39 @@ function patchedInstanceConnect(originalConnect) {
1226
1273
  return function guardedInstanceConnect(...args) {
1227
1274
  const target = Array.isArray(args[0]) ? args[0] : args;
1228
1275
  const isTls = this instanceof tls.TLSSocket;
1229
- for (const host of socketTargetHosts(target, isTls)) {
1276
+ const hosts = socketTargetHosts(target, isTls);
1277
+ for (const host of hosts) {
1230
1278
  const refusal = socketRefusal(host, socketTargetPort(target), isTls);
1231
1279
  if (refusal) {
1232
1280
  // Preserve the instance and its error listeners; never return a different socket.
1233
- process.nextTick(() => this.destroy(Object.assign(new Error(refusal), { code: 'ECONNREFUSED' })));
1281
+ process.nextTick(() => this.destroy(Object.assign(refusal, { code: 'ECONNREFUSED' })));
1234
1282
  return this;
1235
1283
  }
1236
1284
  }
1285
+ if (hosts.length > 0) reportSandboxRefusal(this, `${hosts[0]}:${socketTargetPort(target) ?? ''}`);
1237
1286
  return originalConnect.apply(this, args);
1238
1287
  };
1239
1288
  }
1240
1289
 
1290
+ // A process of an image run is confined by its sandbox (a native run's Seatbelt) as well as by this decision. Seatbelt
1291
+ // answers a connection it refuses with EPERM and, for Node, writes no kernel record (images lane, measured on Homebrew
1292
+ // Node 24), so a connection this decision let through and the sandbox refused is reported here, as the World's record.
1293
+ // The emit is wrapped rather than an 'error' listener added: a listener would make an unhandled error handled.
1294
+ const SANDBOX_REPORTED = Symbol('volter.inject.sandboxReported');
1295
+ function reportSandboxRefusal(socket, target) {
1296
+ if (!worldDeclarationFromEnv(process.env) || socket[SANDBOX_REPORTED] || typeof socket.emit !== 'function') return;
1297
+ socket[SANDBOX_REPORTED] = true;
1298
+ const emit = socket.emit;
1299
+ socket.emit = function sandboxReportingEmit(event, error, ...rest) {
1300
+ if (event === 'error' && error && error.code === 'EPERM' && !error.worldRefusal) {
1301
+ const answer = sandboxRefusal(target);
1302
+ error.message = `${error.message} (${answer.reason}; ${refusalSide(answer)})`;
1303
+ error.worldRefusal = refusalRecord(answer);
1304
+ }
1305
+ return emit.call(this, event, error, ...rest);
1306
+ };
1307
+ }
1308
+
1241
1309
  function patchedDatagram(original, connecting) {
1242
1310
  return function guardedDatagram(...args) {
1243
1311
  let peer;
@@ -1245,9 +1313,9 @@ function patchedDatagram(original, connecting) {
1245
1313
  const tail = connecting ? args : args.slice(1);
1246
1314
  const host = peer?.address ?? tail.findLast(arg => typeof arg === 'string') ?? 'localhost';
1247
1315
  const port = peer?.port ?? tail.findLast(arg => typeof arg === 'number');
1248
- const refusal = socketRefusal(host, port, false);
1316
+ const refusal = socketRefusal(host, port, false, true);
1249
1317
  if (refusal) {
1250
- const error = Object.assign(new Error(refusal), { code: 'ECONNREFUSED' });
1318
+ const error = Object.assign(refusal, { code: 'ECONNREFUSED' });
1251
1319
  const callback = !connecting && typeof args.at(-1) === 'function' ? args.at(-1) : null;
1252
1320
  process.nextTick(() => callback ? callback(error) : this.emit('error', error));
1253
1321
  return;
@@ -1289,33 +1357,27 @@ function upgradeToTwin(origin, url, method, headers, args, callback) {
1289
1357
  function patchedFactory(originalRequest, defaultProtocol) {
1290
1358
  return function patchedRequest(...args) {
1291
1359
  const { url, method, headers, callback, options: given, invalid } = describe(args, defaultProtocol);
1292
- if (invalid && worldNetworkPolicyFromEnv(process.env)) return blockedClientRequest('[twin-inject] request has an invalid effective authority or path');
1293
- const guest = mediateLocalGuest(url);
1294
- if (guest) {
1295
- if (isUpgrade(headers) || method === 'CONNECT') return blockedClientRequest('[twin-inject] local guest upgrade requires a mediated transport');
1296
- return requestApp({ proxy: guest }, url, method, headers, { signal: given.signal, timeout: given.timeout }, callback);
1360
+ const upgrade = isUpgrade(headers) || method === 'CONNECT';
1361
+ const answer = decide(url, upgrade ? 'upgrade' : 'http', { ...httpFacts(url), invalid });
1362
+ if (answer.outcome === 'refuse') {
1363
+ const message = answer.rule === 'invalid' ? '[twin-inject] request has an invalid effective authority or path'
1364
+ : answer.rule === 'guest-upgrade' ? '[twin-inject] local guest upgrade requires a mediated transport'
1365
+ : answer.rule === 'inspected-transport' ? '[twin-inject] inspected egress cannot upgrade its transport'
1366
+ : httpRefusalMessage(answer, url, 'request');
1367
+ return blockedClientRequest(refusalError(answer, message));
1297
1368
  }
1298
- const twin = url && !publicBuildRequest(url) && resolveTwin(url.hostname, MAP, url.pathname);
1299
- if (twin && twin.vendor === 'app') {
1369
+ if (answer.via === 'guest-proxy') return requestApp({ proxy: localGuestProxy() }, url, method, headers, { signal: given.signal, timeout: given.timeout }, callback);
1370
+ if (answer.outcome === 'twin' && answer.twin.vendor === 'app') {
1300
1371
  const given = args.find((a) => a && typeof a === 'object' && !(a instanceof URL)) || {};
1301
- return requestApp(twin, url, method, headers, { signal: given.signal, timeout: given.timeout }, callback);
1372
+ return requestApp(answer.twin, url, method, headers, { signal: given.signal, timeout: given.timeout }, callback);
1302
1373
  }
1303
- if (twin) {
1374
+ if (answer.outcome === 'twin') {
1304
1375
  const fwdHeaders = Object.assign({}, headers);
1305
1376
  if (!fwdHeaders.host && !fwdHeaders.Host) fwdHeaders.host = url.host;
1306
- if (isUpgrade(headers)) return upgradeToTwin(twin.origin, url, method, fwdHeaders, args, callback);
1307
- return fakeClientRequest(twin.origin, method, url.pathname + url.search, fwdHeaders, callback);
1308
- }
1309
- // No twin claims this path — but if configured twins claim the HOST, the path is an unclaimed
1310
- // slice of a shared, virtualized host (the Cal.com /calendar/v3 case): refuse loudly, in every
1311
- // mode, before the strict-egress check gets a chance to phrase it as an ordinary untwinned host.
1312
- if (url && !publicBuildRequest(url) && twinnedHostVendors(url.hostname, MAP).length > 0) {
1313
- return blockedClientRequest(unclaimedTwinnedHostPathMessage(url.hostname, url.pathname, MAP));
1377
+ if (isUpgrade(headers)) return upgradeToTwin(answer.twin.origin, url, method, fwdHeaders, args, callback);
1378
+ return fakeClientRequest(answer.twin.origin, method, url.pathname + url.search, fwdHeaders, callback);
1314
1379
  }
1315
- if (shouldBlockUntwinned(url)) return blockedClientRequest(`[twin-inject] blocked untwinned external request to ${url.hostname}`);
1316
- const policy = worldNetworkPolicyFromEnv(process.env);
1317
- if (url && policy && worldNetworkNeedsPathInspection(policy, url)) {
1318
- if (isUpgrade(headers) || method === 'CONNECT') return blockedClientRequest('[twin-inject] inspected egress cannot upgrade its transport');
1380
+ if (answer.via === 'inspected') {
1319
1381
  try {
1320
1382
  return requestApp({ proxy: pathGrantProxy() }, url, method, headers, { signal: given.signal, timeout: given.timeout }, callback);
1321
1383
  } catch (error) { return blockedClientRequest(`[twin-inject] ${error.message}`); }
@@ -1345,7 +1407,7 @@ function install(mapOverride) {
1345
1407
  // the claiming twins' doors are read from the start, so a process's first request finds what they claim
1346
1408
  for (const vendor of Object.keys(CLAIMERS)) if (MAP[vendor]) claimsAt(MAP[vendor], CLAIMERS[vendor]);
1347
1409
  const active = Object.keys(MAP);
1348
- if (installed || (!active.length && !worldBase() && !MAP[APP_INSTANCE] && !strictEgressEnabled() && !worldNetworkPolicyFromEnv(process.env))) return { active };
1410
+ if (installed || (!active.length && !worldBase() && !MAP[APP_INSTANCE] && !strictEgressEnabled() && !worldNetworkPolicyFromEnv(process.env) && !worldDeclarationFromEnv(process.env))) return { active };
1349
1411
 
1350
1412
  ORIGINALS = {
1351
1413
  httpRequest: http.request.bind(http),
@@ -1384,13 +1446,17 @@ function install(mapOverride) {
1384
1446
  http2.connect = function patchedHttp2Connect(authority, options, listener) {
1385
1447
  let url = null;
1386
1448
  try { url = new URL(typeof authority === 'string' ? authority : authority && authority.href ? authority.href : `https://${authority.hostname || authority.host}`); } catch { url = null; }
1387
- const twin = url && !publicBuildRequest(url) && resolveTwin(url.hostname, MAP, '/');
1388
- if (!twin) {
1389
- if (mediateLocalGuest(url)) throw new Error('[twin-inject] local guest HTTP/2 requires a mediated transport');
1390
- const policy = worldNetworkPolicyFromEnv(process.env);
1391
- if (url && policy && !isLocalOrPrivateHost(url.hostname) && worldNetworkNeedsPathInspection(policy, url)) throw new Error('[twin-inject] HTTP/2 cannot use inspected egress');
1449
+ // a session names no path at connect, so a twinned host's unclaimed paths cannot be told apart here: host-level only
1450
+ const { twinnedHostVendors: _paths, ...facts } = httpFacts(url, '/');
1451
+ const answer = decide(url, 'http2', facts);
1452
+ if (answer.outcome !== 'twin') {
1453
+ // the World's listing is enforced on the session's own TLS connect, by the socket guard, with this same decision
1454
+ if (answer.outcome === 'refuse' && !LISTING_RULES.has(answer.rule)) {
1455
+ throw refusalError(answer, answer.rule === 'guest-http2' ? '[twin-inject] local guest HTTP/2 requires a mediated transport' : '[twin-inject] HTTP/2 cannot use inspected egress');
1456
+ }
1392
1457
  return ORIGINALS.http2Connect.apply(this, arguments);
1393
1458
  }
1459
+ const twin = answer.twin;
1394
1460
  const cb = typeof options === 'function' ? options : listener;
1395
1461
  const opts = typeof options === 'function' || !options ? {} : Object.fromEntries(Object.entries(options).filter(([k]) => !['secureContext', 'servername', 'ca', 'cert', 'key', 'checkServerIdentity', 'ALPNProtocols', 'createConnection', 'rejectUnauthorized'].includes(k)));
1396
1462
  return ORIGINALS.http2Connect.call(this, new URL(twin.origin).origin, opts, cb);
@@ -1403,26 +1469,23 @@ function install(mapOverride) {
1403
1469
  let urlStr;
1404
1470
  try { urlStr = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input && input.url; } catch { urlStr = undefined; }
1405
1471
  let url = null;
1472
+ let invalid = false;
1406
1473
  if (urlStr) {
1407
- if (/\\/.test(String(urlStr).split(/[?#]/, 1)[0]) && worldNetworkPolicyFromEnv(process.env)) return Promise.reject(new Error('[twin-inject] backslash in fetch target'));
1474
+ invalid = /\\/.test(String(urlStr).split(/[?#]/, 1)[0]);
1408
1475
  try { url = new URL(urlStr); } catch { url = null; }
1409
1476
  }
1410
- const guest = mediateLocalGuest(url);
1411
- if (guest) return fetchApp({ proxy: guest }, url, input, init, chain);
1412
- // Resolve refreshed claims before evaluating the public build-artifact exception.
1413
- const claimed = url && resolveTwin(url.hostname, MAP, url.pathname);
1414
- const twin = claimed && !publicBuildRequest(url) ? claimed : null;
1477
+ // the claims are read as they stand; a miss refreshes them once below and asks again
1478
+ const answer = decide(url, 'http', { ...httpFacts(url), invalid });
1479
+ if (answer.rule === 'invalid') return Promise.reject(refusalError(answer, '[twin-inject] backslash in fetch target'));
1480
+ if (answer.via === 'guest-proxy') return fetchApp({ proxy: localGuestProxy() }, url, input, init, chain);
1481
+ const twin = answer.outcome === 'twin' ? answer.twin : null;
1415
1482
  if (!twin) {
1416
1483
  // A host may have been claimed since startup (including by a child process).
1417
1484
  // Refresh once at most: a retry goes on with the claims actually returned.
1418
1485
  const pending = !claimsWaited && url && !isLocalOrPrivateHost(url.hostname) && !isTwinOriginHost(url.hostname) ? claimsRefreshBriefly() : null;
1419
1486
  if (pending) return pending.then(() => patchedFetch(input, init, true, chain));
1420
- // Same shared-host rule as patchedFactory: an unclaimed path on a twinned host refuses
1421
- // loudly in every mode — never mis-routed, never leaked to the real vendor.
1422
- if (url && !publicBuildRequest(url) && twinnedHostVendors(url.hostname, MAP).length > 0) {
1423
- return Promise.reject(new Error(unclaimedTwinnedHostPathMessage(url.hostname, url.pathname, MAP)));
1424
- }
1425
- if (shouldBlockUntwinned(url)) return Promise.reject(new Error(`[twin-inject] blocked untwinned external fetch to ${url.hostname}`));
1487
+ // an unclaimed path on a twinned host refuses loudly in every mode: never mis-routed, never leaked to the vendor
1488
+ if (answer.outcome === 'refuse') return Promise.reject(refusalError(answer, httpRefusalMessage(answer, url, 'fetch')));
1426
1489
  // Each followed redirect re-enters authorization; native fetch only receives manual hops under a policy.
1427
1490
  if (addressedToWorld(url) && twinsKey()) {
1428
1491
  const request = typeof input === 'object' && input !== null ? input : undefined;
@@ -1437,9 +1500,9 @@ function install(mapOverride) {
1437
1500
  }
1438
1501
  return originalFetch(input, keyed);
1439
1502
  }
1440
- const policy = worldNetworkPolicyFromEnv(process.env);
1503
+ const policy = worldGrant().policy;
1441
1504
  const redirect = init?.redirect ?? (typeof input === 'object' && input?.redirect) ?? 'follow';
1442
- if (url && policy && worldNetworkNeedsPathInspection(policy, url)) {
1505
+ if (answer.via === 'inspected') {
1443
1506
  try { return fetchApp({ proxy: pathGrantProxy() }, url, input, init, chain); }
1444
1507
  catch (error) { return Promise.reject(error); }
1445
1508
  }
@@ -55,12 +55,13 @@ function origins(value) {
55
55
  }
56
56
 
57
57
  function normalizeWorldNetwork(value) {
58
- const network = object(value, ['egress', 'writes', 'phase', 'passthrough', 'publicReads'], 'World network');
58
+ const network = object(value, ['egress', 'writes', 'phase', 'passthrough', 'publicReads', 'ownLoopback'], 'World network');
59
59
  const egress = origins(network.egress);
60
60
  const writes = origins(network.writes ?? []);
61
61
  const phase = network.phase ?? 'runtime';
62
62
  if (phase !== 'build' && phase !== 'runtime') throw new Error('World network phase must be build or runtime');
63
63
  if (network.publicReads !== undefined && typeof network.publicReads !== 'boolean') throw new Error('World network publicReads must be a boolean');
64
+ if (network.ownLoopback !== undefined && typeof network.ownLoopback !== 'boolean') throw new Error('World network ownLoopback must be a boolean');
64
65
  const publicReads = phase === 'build' && network.publicReads !== false;
65
66
  const passthrough = origins(network.passthrough ?? []);
66
67
  if (passthrough.length && phase !== 'build') throw new Error('World TLS passthrough requires phase: build');
@@ -69,7 +70,9 @@ function normalizeWorldNetwork(value) {
69
70
  // an opaque tunnel cannot show its method, so it cannot carry a write
70
71
  if (writes.some(grant => new URL(grant).origin === origin)) throw new Error(`TLS passthrough cannot carry a write grant: ${origin}`);
71
72
  }
72
- return Object.freeze({ egress, writes, phase, passthrough, publicReads });
73
+ // a run reaches its own listeners unless the World says otherwise (architecture, "Capability decisions"); only the
74
+ // refusal is written, so a policy that grants it reads as it did before the field existed
75
+ return Object.freeze({ egress, writes, phase, passthrough, publicReads, ...(network.ownLoopback === false ? { ownLoopback: false } : {}) });
73
76
  }
74
77
 
75
78
  function createWorldNetworkPolicy(world, network) {
@@ -78,9 +81,9 @@ function createWorldNetworkPolicy(world, network) {
78
81
  }
79
82
 
80
83
  function parseWorldNetworkPolicy(value) {
81
- const policy = object(value, ['version', 'world', 'egress', 'writes', 'phase', 'passthrough', 'publicReads'], 'World network policy');
84
+ const policy = object(value, ['version', 'world', 'egress', 'writes', 'phase', 'passthrough', 'publicReads', 'ownLoopback'], 'World network policy');
82
85
  if (policy.version !== 1) throw new Error('Unsupported World network policy version');
83
- return createWorldNetworkPolicy(policy.world, { egress: policy.egress, writes: policy.writes, phase: policy.phase, passthrough: policy.passthrough, publicReads: policy.publicReads });
86
+ return createWorldNetworkPolicy(policy.world, { egress: policy.egress, writes: policy.writes, phase: policy.phase, passthrough: policy.passthrough, publicReads: policy.publicReads, ownLoopback: policy.ownLoopback });
84
87
  }
85
88
 
86
89
  function worldNetworkPolicyFromEnv(env) {
@@ -152,7 +155,8 @@ function intersectWorldNetwork(own, ceiling) {
152
155
  ? left.passthrough.filter(origin => right.passthrough.includes(origin) && egress.includes(origin)) : [];
153
156
  // public reads are permission too: only a build ceiling that reads them passes them on
154
157
  const publicReads = left.publicReads && right.publicReads;
155
- return normalizeWorldNetwork({ egress, writes, phase: left.phase, passthrough, publicReads });
158
+ const ownLoopback = left.ownLoopback !== false && right.ownLoopback !== false;
159
+ return normalizeWorldNetwork({ egress, writes, phase: left.phase, passthrough, publicReads, ownLoopback });
156
160
  }
157
161
 
158
162
  /** Loopback and private addresses stay inside the machine a World runs on: never refused. */
@@ -171,21 +175,21 @@ function isWorldInternalHost(hostname) {
171
175
  }
172
176
 
173
177
  /**
174
- * Why this World refuses an outbound request to `value`, or null when it allows it. The one rule
175
- * every enforcement point applies: the injector for application processes, and a twin's own
176
- * World-internal outbound calls (a queue delivering to its destination), which run in a host the
177
- * injector does not attach to. Internal addresses pass; strict egress refuses everything else; a
178
+ * Why this World refuses an outbound request to `value`, or null when it allows it: the World's capability decision
179
+ * (capability-decision.cjs) for a twin's own World-internal outbound calls (a queue delivering to its destination), which
180
+ * run in a host the injector does not attach to. Internal addresses pass; strict egress refuses everything else; a
178
181
  * published network policy refuses what it does not list. An unreadable policy refuses.
179
182
  */
180
183
  function worldEgressRefusal(value, env = process.env, method = 'GET') {
181
184
  let url;
182
185
  try { url = new URL(String(value)); } catch { return `unparseable destination ${String(value)}`; }
183
- if (isWorldInternalHost(url.hostname)) return null;
184
- if (env.VOLTER_TWIN_STRICT_EGRESS === '1' || env.VOLTER_TWIN_STRICT_EGRESS === 'true') return `sealed World refuses untwinned host ${url.hostname}`;
185
- let policy;
186
- try { policy = worldNetworkPolicyFromEnv(env); } catch (error) { return `World network policy unreadable (${error.message}); refusing ${url.hostname}`; }
187
- if (policy !== undefined && !allowsWorldNetworkEgress(policy, url, method)) return `World network policy does not allow ${String(method).toUpperCase()} ${url.origin}`;
188
- return null;
186
+ let policy; let unreadable;
187
+ try { policy = worldNetworkPolicyFromEnv(env); } catch (error) { unreadable = error.message; }
188
+ const strict = env.VOLTER_TWIN_STRICT_EGRESS === '1' || env.VOLTER_TWIN_STRICT_EGRESS === 'true';
189
+ const { OPEN_DECLARATION, decideCapability, refusalSide } = require('./capability-decision.cjs');
190
+ const answer = decideCapability({ kind: 'egress', url, method, transport: 'http' },
191
+ { run: { guest: false }, declaration: OPEN_DECLARATION, grant: { policy, strict, unreadable }, facts: {} });
192
+ return answer.outcome === 'refuse' ? `${answer.reason} (${refusalSide(answer)})` : null;
189
193
  }
190
194
 
191
195
  module.exports = { PUBLIC_BUILD_READS, READ_METHODS, intersectWorldNetwork, worldNetworkNeedsPathInspection, worldNetworkUsesPassthrough, isWorldInternalHost, worldEgressRefusal, WORLD_NETWORK_POLICY_ENV, normalizeWorldNetwork, createWorldNetworkPolicy, parseWorldNetworkPolicy, worldNetworkPolicyFromEnv, allowsWorldNetworkEgress };
@@ -1,4 +1,4 @@
1
- export interface WorldNetwork { readonly egress: readonly string[]; readonly writes?: readonly string[]; readonly phase?: 'build' | 'runtime'; readonly passthrough?: readonly string[]; readonly publicReads?: boolean }
1
+ export interface WorldNetwork { readonly egress: readonly string[]; readonly writes?: readonly string[]; readonly phase?: 'build' | 'runtime'; readonly passthrough?: readonly string[]; readonly publicReads?: boolean; readonly ownLoopback?: boolean }
2
2
  export interface WorldNetworkPolicy extends WorldNetwork { readonly version: 1; readonly world: string }
3
3
  export const WORLD_NETWORK_POLICY_ENV: 'VOLTER_WORLD_NETWORK_POLICY';
4
4
  export function normalizeWorldNetwork(value: unknown): WorldNetwork;