@volter/world-core 3.0.64 → 3.0.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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, decideCapability, 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,43 @@ 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, or a
331
+ // local light guest, whose HTTP leaves through its guest listener; what the World grants is its published policy. The
332
+ // seams supply what they observe: the twin route, the vendors sharing a host, the World's doors.
333
+ function worldGrant() {
334
+ let policy; let unreadable;
335
+ try { policy = worldNetworkPolicyFromEnv(process.env); } catch (error) { unreadable = error.message; }
336
+ return { policy, strict: strictEgressEnabled(), unreadable };
337
+ }
338
+ function decide(url, transport, facts = {}, port) {
339
+ const guestProxy = localGuestProxy();
340
+ return decideCapability({ kind: 'egress', url, transport, port },
341
+ { run: { guest: !!guestProxy }, declaration: OPEN_DECLARATION, grant: worldGrant(), facts: { guestProxy, ...facts } });
342
+ }
343
+ /** 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
344
+ * not mediated by its guest listener. */
345
+ function httpFacts(url, pathname) {
346
+ if (!url) return {};
347
+ const guestProxy = localGuestProxy();
348
+ return {
349
+ twin: resolveTwin(url.hostname, MAP, pathname === undefined ? url.pathname : pathname),
350
+ twinnedHostVendors: twinnedHostVendors(url.hostname, MAP),
351
+ ...(guestProxy && !/^https?:$/.test(url.protocol) ? { guestProxy: null } : {}),
352
+ endpoint: guestProxy && (addressedToWorld(url) || explicitTwinEndpoint(url) || url.origin === guestProxy.origin) ? { kind: 'door' } : null,
353
+ };
333
354
  }
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;
355
+ /** A refusal as an Error: the seam's own message, the side that refused, and the World's refusal record. */
356
+ function refusalError(answer, message) {
357
+ return Object.assign(new Error(`${message} (${refusalSide(answer)})`), { worldRefusal: refusalRecord(answer) });
358
+ }
359
+ /** The World's listing refused it (sealed, unlisted, an unreadable policy). */
360
+ const LISTING_RULES = new Set(['sealed', 'not-granted', 'policy-unreadable']);
361
+ /** The message an HTTP seam (`request`, `fetch`) gives a refusal. */
362
+ function httpRefusalMessage(answer, url, seam) {
363
+ if (answer.rule === 'unclaimed-path') return unclaimedTwinnedHostPathMessage(url.hostname, url.pathname, MAP);
364
+ if (LISTING_RULES.has(answer.rule)) return `[twin-inject] blocked untwinned external ${seam} to ${url.hostname}`;
365
+ return `[twin-inject] ${answer.reason}`;
339
366
  }
340
367
 
341
368
  // --- untwinned-host TLS failure naming (local mode) -----------------------------------------
@@ -475,10 +502,6 @@ function explicitTwinEndpoint(url) {
475
502
  } catch { return false; }
476
503
  });
477
504
  }
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
505
  function addressedToWorld(url) {
483
506
  const base = worldBase();
484
507
  if (!base || !url || !base.path || url.origin.replace(/^ws/, 'http') !== base.origin) return false;
@@ -545,30 +568,30 @@ function keyedView(dispatcher) {
545
568
  if (name === 'dispatch') {
546
569
  return function keyedDispatch(opts, handler) {
547
570
  const key = ORIGINAL_DEFINE_PROPERTY ? twinsKey() : null;
571
+ let answer = null;
548
572
  if (ORIGINAL_DEFINE_PROPERTY && opts?.origin != null) {
549
- const policy = worldNetworkPolicyFromEnv(process.env);
550
- let at;
573
+ let at = null; let invalid = false;
551
574
  try {
552
575
  const origin = new URL(String(opts.origin));
553
576
  const path = String(opts.path ?? '/');
554
577
  if (!path.startsWith('/') || /\\/.test(path)) throw new Error('invalid dispatch path');
555
578
  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')));
579
+ } catch { invalid = true; }
580
+ answer = decide(at, 'dispatch', at ? { twin: resolveTwin(at.hostname, MAP, at.pathname), twinnedHostVendors: twinnedHostVendors(at.hostname, MAP), twinOrigin: isTwinOriginHost(at.hostname) } : { invalid });
581
+ if (answer.outcome === 'refuse') {
582
+ const message = answer.rule === 'invalid' ? '[twin-inject] invalid effective undici request'
583
+ : answer.rule === 'unclaimed-path' ? unclaimedTwinnedHostPathMessage(at.hostname, at.pathname, MAP)
584
+ : '[twin-inject] unmediated undici hop cannot use inspected egress';
585
+ process.nextTick(() => handler.onError(refusalError(answer, message)));
562
586
  return false;
563
587
  }
564
588
  }
565
589
  // Undici's shared dispatcher is an HTTP seam even when its caller bypasses global fetch.
566
590
  // 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') {
591
+ if (answer && answer.outcome === 'twin') {
592
+ const at = new URL(answer.target);
593
+ const twin = answer.twin;
594
+ if (twin.vendor !== 'app') {
572
595
  const endpoint = new URL(twin.origin);
573
596
  const headers = undiciHeaderObject(opts.headers);
574
597
  headers['host'] = at.host;
@@ -605,6 +628,12 @@ function keyUndiciDispatches() {
605
628
  };
606
629
  for (const name of UNDICI_DISPATCHERS) if (globalThis[name]) globalThis[name] = keyedView(globalThis[name]);
607
630
  }
631
+ /** The twin the World routes `url` to, or null: its other answers are the dispatcher's and the sockets' to enforce. */
632
+ function routedTwin(url) {
633
+ if (!url) return null;
634
+ const answer = decide(url, 'dispatch', { twin: resolveTwin(url.hostname, MAP, url.pathname) });
635
+ return answer.outcome === 'twin' ? answer.twin : null;
636
+ }
608
637
  // Imported Undici APIs may hold a dispatcher obtained before the global view was installed.
609
638
  // Rewrite their public HTTP doors as well; an independent connector still faces the socket guards.
610
639
  let UNDICI_LOAD = null;
@@ -620,7 +649,7 @@ function routeImportedUndici() {
620
649
  view.fetch = function routedUndiciFetch(input, init) {
621
650
  if (!installed) return exports.fetch(input, init);
622
651
  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);
652
+ const twin = routedTwin(url);
624
653
  // Local control planes may use their own Request class and dispatcher. Preserve both;
625
654
  // connections still pass the socket guards, and claimed vendor hops still enter routing.
626
655
  if (url && isLocalOrPrivateHost(url.hostname) && (!twin || twin.vendor === 'app')) return exports.fetch(input, init);
@@ -639,7 +668,7 @@ function routeImportedUndici() {
639
668
  if (!installed) return exports.request.apply(exports, arguments);
640
669
  const opts = typeof options === 'function' ? {} : options ?? {};
641
670
  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);
671
+ const twin = routedTwin(url);
643
672
  if (!twin || twin.vendor === 'app') return exports.request.apply(exports, arguments);
644
673
  const headers = undiciHeaderObject(opts.headers);
645
674
  headers.host = url.host; headers['x-volter-twin-original-host'] = url.host;
@@ -962,7 +991,7 @@ function unclaimedTwinnedHostPathMessage(hostname, pathname, map) {
962
991
 
963
992
  function blockedClientRequest(message) {
964
993
  const request = new EventEmitter();
965
- const error = new Error(message);
994
+ const error = message instanceof Error ? message : new Error(message);
966
995
  const socket = new EventEmitter();
967
996
  socket.connecting = false;
968
997
  socket.setKeepAlive = () => socket;
@@ -1154,46 +1183,54 @@ function socketTargetPort(args) {
1154
1183
  return Number.isInteger(port) && port > 0 && port < 65536 ? port : undefined;
1155
1184
  }
1156
1185
 
1157
- function socketRefusal(hostname, port, isTls) {
1186
+ /** Why the World refuses a socket to `hostname`:`port` (an Error carrying the refusal record), or null. */
1187
+ function socketRefusal(hostname, port, isTls, datagram = false) {
1158
1188
  const host = String(hostname || '').toLowerCase();
1189
+ // an addressing mistake, not a capability: the World's DNS answer for the names it routes leads nowhere
1159
1190
  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.';
1162
- }
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
- }
1191
+ return new Error(`[twin-inject] refused a socket to ${host}, the address this World's DNS gives the names it routes: `
1192
+ + 'nothing listens there; connect by the name, which the World routes to its twin or its application.');
1168
1193
  }
1169
- if (!host || isLocalOrPrivateHost(host) || isTwinOriginHost(host)) return null;
1194
+ if (!host) return null;
1170
1195
  // the destination as an origin, its port and its protocol included: a policy that lists https://host allows TLS to
1171
1196
  // host:443, never another port or a plaintext connection to it
1172
1197
  const authority = `${isIP(host) === 6 ? `[${host}]` : host}${port !== undefined && !(isTls && port === 443) ? `:${port}` : ''}`;
1173
1198
  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) {
1199
+ if (!url) return null;
1200
+ const guestProxy = localGuestProxy();
1201
+ const internal = isLocalOrPrivateHost(host);
1202
+ const facts = internal
1203
+ ? { endpoint: guestProxy && guestDoorAt(host, port) ? { kind: 'door' } : null }
1204
+ : { twin: resolveTwin(host, MAP), twinnedHostVendors: twinnedHostVendors(host, MAP), twinOrigin: isTwinOriginHost(host) };
1205
+ const answer = decide(url, datagram ? 'datagram' : 'socket', facts, port);
1206
+ if (answer.outcome !== 'refuse') return null;
1207
+ let message = `[twin-inject] ${answer.reason}`;
1208
+ if (answer.rule === 'guest-private') message = `[twin-inject] local guest private socket to ${host}:${port} requires its guest proxy`;
1209
+ else if (answer.rule === 'raw-socket-twinned') {
1210
+ const vendors = facts.twinnedHostVendors;
1178
1211
  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(', ')}): `
1212
+ message = `[twin-inject] refused a raw socket to ${host}, which is twinned in this world (${vendors.join(', ')}): `
1180
1213
  + `the client opened its own connection instead of going through http/https or fetch, so the twin never saw it. `
1181
1214
  + (doors.length > 0
1182
1215
  ? `The SDK reaches the twin through ${doors.join('; ')}, which the world sets when it runs the app.`
1183
1216
  : `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): `
1217
+ } else if (answer.rule === 'raw-socket-app') {
1218
+ message = `[twin-inject] refused a raw socket to ${host}, which this world routes to the application (volter-world app-url --host): `
1187
1219
  + '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;
1220
+ } else if (answer.rule === 'inspected-transport') message = `[twin-inject] inspected egress requires a mediated HTTP request to ${authority}`;
1221
+ else if (LISTING_RULES.has(answer.rule)) message = `[twin-inject] blocked untwinned external socket to ${authority}`;
1222
+ return refusalError(answer, message);
1223
+ }
1224
+
1225
+ /** One of the World's doors a local guest connects to directly: its guest listener, the World's origin, a twin. */
1226
+ function guestDoorAt(host, port) {
1227
+ const doors = [localGuestProxy(), worldBase() && new URL(worldBase().origin), ...Object.values(MAP).map(origin => { try { return new URL(origin); } catch { return null; } })].filter(Boolean);
1228
+ return doors.some(door => door.hostname.replace(/^\[|\]$/g, '') === host.replace(/^\[|\]$/g, '') && Number(door.port || (door.protocol === 'https:' ? 443 : 80)) === port);
1192
1229
  }
1193
1230
 
1194
- function refusedSocket(message) {
1231
+ function refusedSocket(refusal) {
1195
1232
  const socket = new net.Socket();
1196
- process.nextTick(() => socket.destroy(Object.assign(new Error(message), { code: 'ECONNREFUSED' })));
1233
+ process.nextTick(() => socket.destroy(Object.assign(refusal, { code: 'ECONNREFUSED' })));
1197
1234
  return socket;
1198
1235
  }
1199
1236
 
@@ -1230,7 +1267,7 @@ function patchedInstanceConnect(originalConnect) {
1230
1267
  const refusal = socketRefusal(host, socketTargetPort(target), isTls);
1231
1268
  if (refusal) {
1232
1269
  // Preserve the instance and its error listeners; never return a different socket.
1233
- process.nextTick(() => this.destroy(Object.assign(new Error(refusal), { code: 'ECONNREFUSED' })));
1270
+ process.nextTick(() => this.destroy(Object.assign(refusal, { code: 'ECONNREFUSED' })));
1234
1271
  return this;
1235
1272
  }
1236
1273
  }
@@ -1245,9 +1282,9 @@ function patchedDatagram(original, connecting) {
1245
1282
  const tail = connecting ? args : args.slice(1);
1246
1283
  const host = peer?.address ?? tail.findLast(arg => typeof arg === 'string') ?? 'localhost';
1247
1284
  const port = peer?.port ?? tail.findLast(arg => typeof arg === 'number');
1248
- const refusal = socketRefusal(host, port, false);
1285
+ const refusal = socketRefusal(host, port, false, true);
1249
1286
  if (refusal) {
1250
- const error = Object.assign(new Error(refusal), { code: 'ECONNREFUSED' });
1287
+ const error = Object.assign(refusal, { code: 'ECONNREFUSED' });
1251
1288
  const callback = !connecting && typeof args.at(-1) === 'function' ? args.at(-1) : null;
1252
1289
  process.nextTick(() => callback ? callback(error) : this.emit('error', error));
1253
1290
  return;
@@ -1289,33 +1326,27 @@ function upgradeToTwin(origin, url, method, headers, args, callback) {
1289
1326
  function patchedFactory(originalRequest, defaultProtocol) {
1290
1327
  return function patchedRequest(...args) {
1291
1328
  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);
1329
+ const upgrade = isUpgrade(headers) || method === 'CONNECT';
1330
+ const answer = decide(url, upgrade ? 'upgrade' : 'http', { ...httpFacts(url), invalid });
1331
+ if (answer.outcome === 'refuse') {
1332
+ const message = answer.rule === 'invalid' ? '[twin-inject] request has an invalid effective authority or path'
1333
+ : answer.rule === 'guest-upgrade' ? '[twin-inject] local guest upgrade requires a mediated transport'
1334
+ : answer.rule === 'inspected-transport' ? '[twin-inject] inspected egress cannot upgrade its transport'
1335
+ : httpRefusalMessage(answer, url, 'request');
1336
+ return blockedClientRequest(refusalError(answer, message));
1297
1337
  }
1298
- const twin = url && !publicBuildRequest(url) && resolveTwin(url.hostname, MAP, url.pathname);
1299
- if (twin && twin.vendor === 'app') {
1338
+ if (answer.via === 'guest-proxy') return requestApp({ proxy: localGuestProxy() }, url, method, headers, { signal: given.signal, timeout: given.timeout }, callback);
1339
+ if (answer.outcome === 'twin' && answer.twin.vendor === 'app') {
1300
1340
  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);
1341
+ return requestApp(answer.twin, url, method, headers, { signal: given.signal, timeout: given.timeout }, callback);
1302
1342
  }
1303
- if (twin) {
1343
+ if (answer.outcome === 'twin') {
1304
1344
  const fwdHeaders = Object.assign({}, headers);
1305
1345
  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));
1346
+ if (isUpgrade(headers)) return upgradeToTwin(answer.twin.origin, url, method, fwdHeaders, args, callback);
1347
+ return fakeClientRequest(answer.twin.origin, method, url.pathname + url.search, fwdHeaders, callback);
1314
1348
  }
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');
1349
+ if (answer.via === 'inspected') {
1319
1350
  try {
1320
1351
  return requestApp({ proxy: pathGrantProxy() }, url, method, headers, { signal: given.signal, timeout: given.timeout }, callback);
1321
1352
  } catch (error) { return blockedClientRequest(`[twin-inject] ${error.message}`); }
@@ -1384,13 +1415,17 @@ function install(mapOverride) {
1384
1415
  http2.connect = function patchedHttp2Connect(authority, options, listener) {
1385
1416
  let url = null;
1386
1417
  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');
1418
+ // a session names no path at connect, so a twinned host's unclaimed paths cannot be told apart here: host-level only
1419
+ const { twinnedHostVendors: _paths, ...facts } = httpFacts(url, '/');
1420
+ const answer = decide(url, 'http2', facts);
1421
+ if (answer.outcome !== 'twin') {
1422
+ // the World's listing is enforced on the session's own TLS connect, by the socket guard, with this same decision
1423
+ if (answer.outcome === 'refuse' && !LISTING_RULES.has(answer.rule)) {
1424
+ 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');
1425
+ }
1392
1426
  return ORIGINALS.http2Connect.apply(this, arguments);
1393
1427
  }
1428
+ const twin = answer.twin;
1394
1429
  const cb = typeof options === 'function' ? options : listener;
1395
1430
  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
1431
  return ORIGINALS.http2Connect.call(this, new URL(twin.origin).origin, opts, cb);
@@ -1403,26 +1438,23 @@ function install(mapOverride) {
1403
1438
  let urlStr;
1404
1439
  try { urlStr = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input && input.url; } catch { urlStr = undefined; }
1405
1440
  let url = null;
1441
+ let invalid = false;
1406
1442
  if (urlStr) {
1407
- if (/\\/.test(String(urlStr).split(/[?#]/, 1)[0]) && worldNetworkPolicyFromEnv(process.env)) return Promise.reject(new Error('[twin-inject] backslash in fetch target'));
1443
+ invalid = /\\/.test(String(urlStr).split(/[?#]/, 1)[0]);
1408
1444
  try { url = new URL(urlStr); } catch { url = null; }
1409
1445
  }
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;
1446
+ // the claims are read as they stand; a miss refreshes them once below and asks again
1447
+ const answer = decide(url, 'http', { ...httpFacts(url), invalid });
1448
+ if (answer.rule === 'invalid') return Promise.reject(refusalError(answer, '[twin-inject] backslash in fetch target'));
1449
+ if (answer.via === 'guest-proxy') return fetchApp({ proxy: localGuestProxy() }, url, input, init, chain);
1450
+ const twin = answer.outcome === 'twin' ? answer.twin : null;
1415
1451
  if (!twin) {
1416
1452
  // A host may have been claimed since startup (including by a child process).
1417
1453
  // Refresh once at most: a retry goes on with the claims actually returned.
1418
1454
  const pending = !claimsWaited && url && !isLocalOrPrivateHost(url.hostname) && !isTwinOriginHost(url.hostname) ? claimsRefreshBriefly() : null;
1419
1455
  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}`));
1456
+ // an unclaimed path on a twinned host refuses loudly in every mode: never mis-routed, never leaked to the vendor
1457
+ if (answer.outcome === 'refuse') return Promise.reject(refusalError(answer, httpRefusalMessage(answer, url, 'fetch')));
1426
1458
  // Each followed redirect re-enters authorization; native fetch only receives manual hops under a policy.
1427
1459
  if (addressedToWorld(url) && twinsKey()) {
1428
1460
  const request = typeof input === 'object' && input !== null ? input : undefined;
@@ -1437,9 +1469,9 @@ function install(mapOverride) {
1437
1469
  }
1438
1470
  return originalFetch(input, keyed);
1439
1471
  }
1440
- const policy = worldNetworkPolicyFromEnv(process.env);
1472
+ const policy = worldGrant().policy;
1441
1473
  const redirect = init?.redirect ?? (typeof input === 'object' && input?.redirect) ?? 'follow';
1442
- if (url && policy && worldNetworkNeedsPathInspection(policy, url)) {
1474
+ if (answer.via === 'inspected') {
1443
1475
  try { return fetchApp({ proxy: pathGrantProxy() }, url, input, init, chain); }
1444
1476
  catch (error) { return Promise.reject(error); }
1445
1477
  }
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-core",
3
- "version": "3.0.64",
3
+ "version": "3.0.65",
4
4
  "description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
5
5
  "keywords": [
6
6
  "twin",
@@ -27,6 +27,8 @@
27
27
  "http-bridge.cjs",
28
28
  "network-policy.cjs",
29
29
  "network-policy.d.cts",
30
+ "capability-decision.cjs",
31
+ "capability-decision.d.cts",
30
32
  "app-route.cjs",
31
33
  "vendor-hosts.cjs",
32
34
  "host-routing.cjs",
@@ -91,6 +93,10 @@
91
93
  "types": "./network-policy.d.cts",
92
94
  "default": "./network-policy.cjs"
93
95
  },
96
+ "./capability-decision": {
97
+ "types": "./capability-decision.d.cts",
98
+ "default": "./capability-decision.cjs"
99
+ },
94
100
  "./app-route": {
95
101
  "types": "./app-route.d.cts",
96
102
  "default": "./app-route.cjs"