@volter/world-core 3.0.38 → 3.0.39
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/app-route.cjs +26 -25
- package/app-route.d.cts +1 -0
- package/dist/app-route.cjs +26 -25
- package/dist/app-route.d.cts +1 -0
- package/dist/generated/pack-facts.json +409 -352
- package/dist/inject.cjs +474 -95
- package/dist/network-policy.cjs +108 -13
- package/dist/network-policy.d.cts +8 -3
- package/dist/src/actions.js +2 -1
- package/dist/src/anthropic-wire.d.ts +2 -0
- package/dist/src/anthropic-wire.js +5 -0
- package/dist/src/attestation.d.ts +6 -0
- package/dist/src/attestation.js +27 -0
- package/dist/src/bytes.d.ts +3 -0
- package/dist/src/bytes.js +17 -0
- package/dist/src/changeset.js +2 -1
- package/dist/src/clickhouse/sql.js +73 -14
- package/dist/src/derived-core.d.ts +210 -32
- package/dist/src/derived-core.js +510 -244
- package/dist/src/derived-real.d.ts +1 -1
- package/dist/src/derived-real.js +704 -93
- package/dist/src/derived.d.ts +18 -2
- package/dist/src/derived.js +21 -2
- package/dist/src/events.d.ts +28 -45
- package/dist/src/events.js +40 -58
- package/dist/src/exact-json.d.ts +10 -0
- package/dist/src/exact-json.js +98 -0
- package/dist/src/executor.d.ts +17 -0
- package/dist/src/executor.js +91 -30
- package/dist/src/file-response.js +1 -1
- package/dist/src/form.d.ts +8 -0
- package/dist/src/form.js +76 -0
- package/dist/src/git/format.d.ts +16 -0
- package/dist/src/git/format.js +110 -0
- package/dist/src/git/index.d.ts +1 -0
- package/dist/src/git/index.js +1 -0
- package/dist/src/git/lfs.d.ts +1 -1
- package/dist/src/git/lfs.js +4 -3
- package/dist/src/graphql-wire.d.ts +6 -0
- package/dist/src/graphql-wire.js +6 -3
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/head.d.ts +1 -0
- package/dist/src/head.js +15 -4
- package/dist/src/held-reads.d.ts +6 -0
- package/dist/src/held-reads.js +60 -0
- package/dist/src/history.js +5 -4
- package/dist/src/host-port.d.ts +5 -0
- package/dist/src/host-port.js +14 -0
- package/dist/src/index.d.ts +11 -3
- package/dist/src/index.js +9 -4
- package/dist/src/log.js +12 -7
- package/dist/src/machines.d.ts +81 -5
- package/dist/src/machines.js +125 -9
- package/dist/src/managed-database.d.ts +15 -11
- package/dist/src/managed-database.js +38 -37
- package/dist/src/multipart.d.ts +21 -3
- package/dist/src/multipart.js +112 -38
- package/dist/src/observe.d.ts +3 -0
- package/dist/src/observe.js +6 -2
- package/dist/src/openai-wire.d.ts +16 -0
- package/dist/src/openai-wire.js +43 -12
- package/dist/src/pack-fetch.d.ts +4 -6
- package/dist/src/pack-fetch.js +189 -57
- package/dist/src/packRegistry.d.ts +7 -1
- package/dist/src/packRegistry.js +20 -3
- package/dist/src/private-transport.d.ts +1 -0
- package/dist/src/private-transport.js +11 -0
- package/dist/src/protobuf.js +10 -4
- package/dist/src/rateBudget.d.ts +8 -1
- package/dist/src/rateBudget.js +1 -0
- package/dist/src/redis/engine.d.ts +14 -5
- package/dist/src/redis/engine.js +271 -72
- package/dist/src/redis/frames.d.ts +17 -0
- package/dist/src/redis/frames.js +123 -0
- package/dist/src/redis/index.d.ts +6 -0
- package/dist/src/redis/index.js +6 -0
- package/dist/src/redis/json.d.ts +2 -0
- package/dist/src/redis/json.js +14 -0
- package/dist/src/redis/lua-libs.d.ts +4 -0
- package/dist/src/redis/lua-libs.js +173 -0
- package/dist/src/redis/lua.d.ts +1 -0
- package/dist/src/redis/lua.js +36 -0
- package/dist/src/redis/member-storage.d.ts +5 -0
- package/dist/src/redis/member-storage.js +75 -0
- package/dist/src/redis/resp2.d.ts +4 -0
- package/dist/src/redis/resp2.js +29 -0
- package/dist/src/redis/stream.d.ts +14 -0
- package/dist/src/redis/stream.js +335 -0
- package/dist/src/remote-execute.d.ts +2 -2
- package/dist/src/request-body.d.ts +11 -0
- package/dist/src/request-body.js +11 -0
- package/dist/src/runtime.d.ts +6 -5
- package/dist/src/runtime.js +5 -5
- package/dist/src/s3/wire.d.ts +4 -4
- package/dist/src/s3/wire.js +5 -3
- package/dist/src/scenario.d.ts +22 -0
- package/dist/src/scenario.js +35 -31
- package/dist/src/serve-http.d.ts +2 -1
- package/dist/src/serve-http.js +10 -1
- package/dist/src/serve.js +7 -4
- package/dist/src/signing.d.ts +23 -5
- package/dist/src/signing.js +74 -11
- package/dist/src/sigv4.d.ts +3 -1
- package/dist/src/sigv4.js +4 -2
- package/dist/src/smtp.js +2 -1
- package/dist/src/sockets.js +8 -3
- package/dist/src/state-system.d.ts +3 -0
- package/dist/src/storage.js +5 -2
- package/dist/src/twin-fetch.d.ts +3 -1
- package/dist/src/twin-fetch.js +3 -2
- package/dist/src/vendor-call.d.ts +10 -3
- package/dist/src/vendor-call.js +58 -14
- package/dist/test-fixtures/attestation.SOURCE.md +1 -0
- package/dist/test-fixtures/attestation.json +50 -0
- package/generated/pack-facts.json +409 -352
- package/inject.cjs +474 -95
- package/network-policy.cjs +108 -13
- package/network-policy.d.cts +8 -3
- package/package.json +11 -4
- package/src/actions.ts +2 -1
- package/src/anthropic-wire.ts +6 -0
- package/src/attestation.ts +23 -0
- package/src/bytes.ts +21 -0
- package/src/changeset.ts +2 -1
- package/src/clickhouse/sql.ts +50 -16
- package/src/derived-core.ts +573 -249
- package/src/derived-real.ts +552 -42
- package/src/derived.ts +23 -4
- package/src/events.ts +54 -85
- package/src/exact-json.ts +71 -0
- package/src/executor.ts +86 -27
- package/src/file-response.ts +1 -1
- package/src/form.ts +66 -0
- package/src/git/format.ts +89 -0
- package/src/git/index.ts +1 -0
- package/src/git/lfs.ts +5 -4
- package/src/graphql-wire.ts +8 -3
- package/src/grpc-wire.ts +2 -1
- package/src/head.ts +15 -5
- package/src/held-reads.ts +54 -0
- package/src/history.ts +5 -4
- package/src/host-port.ts +9 -0
- package/src/index.ts +10 -4
- package/src/log.ts +11 -7
- package/src/machines.ts +135 -13
- package/src/managed-database.ts +48 -40
- package/src/multipart.ts +110 -35
- package/src/observe.ts +6 -3
- package/src/openai-wire.ts +45 -14
- package/src/pack-fetch.ts +173 -50
- package/src/packRegistry.ts +27 -4
- package/src/private-transport.ts +8 -0
- package/src/protobuf.ts +8 -4
- package/src/rateBudget.ts +6 -1
- package/src/redis/engine.ts +168 -63
- package/src/redis/frames.ts +53 -0
- package/src/redis/index.ts +6 -0
- package/src/redis/json.ts +13 -0
- package/src/redis/lua-libs.ts +62 -0
- package/src/redis/lua.ts +20 -0
- package/src/redis/member-storage.ts +58 -0
- package/src/redis/resp2.ts +25 -0
- package/src/redis/stream.ts +102 -0
- package/src/remote-execute.ts +2 -2
- package/src/request-body.ts +12 -0
- package/src/runtime.ts +6 -5
- package/src/s3/wire.ts +8 -5
- package/src/scenario.ts +34 -13
- package/src/serve-http.ts +11 -2
- package/src/serve.ts +4 -3
- package/src/signing.ts +71 -17
- package/src/sigv4.ts +5 -3
- package/src/smtp.ts +3 -2
- package/src/sockets.ts +8 -3
- package/src/state-system.ts +3 -0
- package/src/storage.ts +5 -2
- package/src/twin-fetch.ts +4 -3
- package/src/vendor-call.ts +59 -12
- package/test-fixtures/attestation.json +50 -0
package/dist/inject.cjs
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
//
|
|
30
30
|
// A client can still open its own sockets (npm `undici`'s own Agent, which
|
|
31
31
|
// @turbopuffer/turbopuffer uses under Node, honours neither patch nor HTTPS_PROXY).
|
|
32
|
-
// `net.connect`/`tls.connect`
|
|
32
|
+
// Node TCP clients using `net.connect`/`tls.connect` or Socket instances reach guarded seams, which carry
|
|
33
33
|
// the backstop: a connect to a twinned vendor host went around the rewrite and is
|
|
34
34
|
// refused, never sent to the real vendor; a connect to an untwinned host is refused
|
|
35
35
|
// wherever strict egress or the World's network policy refuses it. The refusal
|
|
@@ -38,11 +38,14 @@ const http = require('http');
|
|
|
38
38
|
const https = require('https');
|
|
39
39
|
const net = require('net');
|
|
40
40
|
const tls = require('tls');
|
|
41
|
+
const dgram = require('node:dgram');
|
|
41
42
|
const { isIP } = require('node:net');
|
|
42
43
|
const { EventEmitter } = require('events');
|
|
43
44
|
const { Readable } = require('stream');
|
|
44
45
|
const Module = require('module');
|
|
45
|
-
const {
|
|
46
|
+
const { urlToHttpOptions } = require('node:url');
|
|
47
|
+
const claimsRefreshChannel = require('node:diagnostics_channel').channel('volter.inject.claims-refresh');
|
|
48
|
+
const { allowsWorldNetworkEgress, worldNetworkPolicyFromEnv, isWorldInternalHost, worldEgressRefusal, worldNetworkNeedsPathInspection, worldNetworkUsesPassthrough } = require('./network-policy.cjs');
|
|
46
49
|
const { createPublicKey, createVerify } = require('crypto');
|
|
47
50
|
|
|
48
51
|
// the vendor host table (VENDOR_HOSTS: a hostname, and a path where two vendors share a host, to the vendor whose twin
|
|
@@ -247,8 +250,8 @@ const { appRouteFor, appForwardHeaders, appRequestOptions, sessionCa } = require
|
|
|
247
250
|
// HOSTS A TWIN CLAIMS WHILE IT RUNS (packRegistry `hostsClaimed`): no descriptor can list a name a person makes answer
|
|
248
251
|
// to a vendor, so each claiming twin lists its own at its door, read here every five seconds from the moment the
|
|
249
252
|
// injector installs (or, in a process that only resolves, the proxy, from its first resolve). A fetch that would be
|
|
250
|
-
// refused
|
|
251
|
-
//
|
|
253
|
+
// refused refreshes the claims once before deciding, so a name connected by a child process is usable immediately.
|
|
254
|
+
// The door's `appHosts` are the names a hosting twin serves as the
|
|
252
255
|
// customer's own application (a domain added to a Vercel project): those route to the application, not the twin.
|
|
253
256
|
const CLAIMERS = {};
|
|
254
257
|
{
|
|
@@ -256,30 +259,45 @@ const CLAIMERS = {};
|
|
|
256
259
|
for (const vendor of Object.keys(packs)) if (packs[vendor].hostsClaimed) CLAIMERS[vendor] = packs[vendor].hostsClaimed.door;
|
|
257
260
|
}
|
|
258
261
|
const CLAIMS = new Map(); // cache: origin -> the hosts its twin last listed; refreshed by the poll below
|
|
262
|
+
// dfb9058, Node 23.11.0 build-policy World, 15 co-located twins, no app/build concurrent there:
|
|
263
|
+
// attach cookbook-dub-build -- node $T/cookbook/dub/claim-refresh.mjs measured five batches
|
|
264
|
+
// at 2.34–14.08 ms with one claiming twin. Retain the existing first-read ceiling for headroom.
|
|
265
|
+
// The host was shared; other actors' workloads were not controlled or measured.
|
|
266
|
+
// Full command, conditions and transcript: cookbook/dub/README.md, Claim refresh measurement.
|
|
267
|
+
const CLAIMS_WAIT_MS = 1500;
|
|
259
268
|
function claimsAt(origin, door) {
|
|
260
269
|
const held = CLAIMS.get(origin);
|
|
261
270
|
if (held) return held;
|
|
262
|
-
const entry = { hosts: new Set(), appHosts: new Set(), loaded: false, ready: null };
|
|
271
|
+
const entry = { hosts: new Set(), appHosts: new Set(), loaded: false, ready: null, refresh: null };
|
|
263
272
|
let settle;
|
|
264
273
|
entry.ready = new Promise((resolve) => { settle = resolve; });
|
|
265
274
|
CLAIMS.set(origin, entry);
|
|
275
|
+
let reading = null;
|
|
266
276
|
const read = () => {
|
|
277
|
+
if (reading) return reading;
|
|
267
278
|
// the unpatched fetch: the door is the twin's own origin, never a vendor host to redirect
|
|
268
279
|
const f = (ORIGINALS && ORIGINALS.fetch) || globalThis.fetch;
|
|
269
|
-
if (typeof f !== 'function') return;
|
|
270
|
-
|
|
271
|
-
|
|
280
|
+
if (typeof f !== 'function') { entry.loaded = true; settle(); return Promise.resolve(); }
|
|
281
|
+
const controller = new AbortController();
|
|
282
|
+
const deadline = setTimeout(() => controller.abort(), CLAIMS_WAIT_MS);
|
|
283
|
+
reading = Promise.resolve()
|
|
284
|
+
.then(() => f(`${origin}${door}`, { headers: attachTwinsKey({}), signal: controller.signal }))
|
|
272
285
|
.then((r) => (r.ok ? r.json() : null))
|
|
273
286
|
.then((body) => {
|
|
274
287
|
if (body && Array.isArray(body.hosts)) entry.hosts = new Set(body.hosts.map((h) => String(h).toLowerCase()));
|
|
275
288
|
if (body) entry.appHosts = new Set(Array.isArray(body.appHosts) ? body.appHosts.map((h) => String(h).toLowerCase()) : []);
|
|
276
289
|
})
|
|
277
290
|
.catch(() => {})
|
|
278
|
-
.then(() => { entry.loaded = true; settle(); })
|
|
291
|
+
.then(() => { entry.loaded = true; settle(); reading = null; })
|
|
292
|
+
.finally(() => clearTimeout(deadline));
|
|
293
|
+
return reading;
|
|
279
294
|
};
|
|
295
|
+
let refreshing = null;
|
|
296
|
+
// An in-flight read may precede a mutation made while this process was blocked in a child.
|
|
297
|
+
// Finish that read, then obtain a new snapshot; simultaneous misses share that refresh.
|
|
298
|
+
entry.refresh = () => refreshing ??= Promise.resolve(reading).then(read).finally(() => { refreshing = null; });
|
|
280
299
|
read();
|
|
281
|
-
//
|
|
282
|
-
// takes Cloudflare minutes to turn Active, so a faster read buys nothing and loads the twin
|
|
300
|
+
// Periodic discovery keeps already known claims current; a fetch miss refreshes on demand.
|
|
283
301
|
const timer = setInterval(read, 5000);
|
|
284
302
|
if (timer.unref) timer.unref();
|
|
285
303
|
return entry;
|
|
@@ -307,6 +325,13 @@ function strictEgressEnabled() {
|
|
|
307
325
|
return process.env.VOLTER_TWIN_STRICT_EGRESS === '1' || process.env.VOLTER_TWIN_STRICT_EGRESS === 'true';
|
|
308
326
|
}
|
|
309
327
|
|
|
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);
|
|
333
|
+
}
|
|
334
|
+
|
|
310
335
|
function shouldBlockUntwinned(url) {
|
|
311
336
|
const routed = url && resolveTwin(url.hostname, MAP, url.pathname);
|
|
312
337
|
if (!url || (routed && routed.vendor !== 'app')) return false;
|
|
@@ -355,37 +380,59 @@ function annotateUntwinnedTlsError(error, hostname) {
|
|
|
355
380
|
// --- request-args → { url, method, headers, callback } across every overload ---
|
|
356
381
|
function describe(args, defaultProtocol) {
|
|
357
382
|
const input = args[0];
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const proto = String(merged.protocol || defaultProtocol);
|
|
377
|
-
const host = merged.hostname || merged.host;
|
|
378
|
-
const path = merged.path || '/';
|
|
379
|
-
// A request addressed to a proxy (axios and others honour the World's HTTPS_PROXY) names the proxy as its host
|
|
380
|
-
// and carries the real target as its path: "When making a request to a proxy ... a client MUST send the target
|
|
381
|
-
// URI in "absolute-form" as the request-target" (RFC 9112 §3.2.2). That target is the request's URL.
|
|
383
|
+
const options = args[1] && typeof args[1] === 'object' ? args[1] : {};
|
|
384
|
+
const callback = typeof args[1] === 'function' ? args[1] : typeof args[2] === 'function' ? args[2] : undefined;
|
|
385
|
+
let merged;
|
|
386
|
+
try {
|
|
387
|
+
merged = Object.assign({}, input instanceof URL || typeof input === 'string'
|
|
388
|
+
? urlToHttpOptions(input instanceof URL ? input : new URL(input)) : input || {}, options);
|
|
389
|
+
} catch { merged = {}; }
|
|
390
|
+
const headers = merged.headers || {};
|
|
391
|
+
const proto = String(merged.protocol || defaultProtocol);
|
|
392
|
+
const host = merged.hostname || merged.host || 'localhost';
|
|
393
|
+
// The mediated transport owns its agent. Do not invoke a caller getter (agent-base infers
|
|
394
|
+
// protocol from Node's native HTTPS stack, which is absent during this authorization).
|
|
395
|
+
const port = merged.port || merged.defaultPort || (proto === 'https:' ? 443 : 80);
|
|
396
|
+
const path = merged.path ?? '/';
|
|
397
|
+
let url = null; let invalid = false;
|
|
398
|
+
try {
|
|
399
|
+
const authority = isIP(String(host)) === 6 ? `[${host}]` : host;
|
|
400
|
+
const physical = new URL(`${proto.endsWith(':') ? proto : `${proto}:`}//${authority}:${port}`);
|
|
382
401
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path)) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
402
|
+
url = new URL(path);
|
|
403
|
+
// Absolute-form names a target only on this World's own proxy. Else retain both authorities.
|
|
404
|
+
let proxy;
|
|
405
|
+
try { proxy = new URL(process.env.VOLTER_WORLD_PROXY || ''); } catch { /* no proxy */ }
|
|
406
|
+
invalid = physical.origin !== url.origin && physical.origin !== proxy?.origin;
|
|
407
|
+
} else {
|
|
408
|
+
if (!String(path).startsWith('/') || /\\/.test(path)) invalid = true;
|
|
409
|
+
url = new URL(String(path), physical.origin);
|
|
410
|
+
if (url.origin !== physical.origin) invalid = true;
|
|
386
411
|
}
|
|
412
|
+
} catch { invalid = true; }
|
|
413
|
+
return { url, method: String(merged.method || 'GET').toUpperCase(), headers, callback, options: merged, invalid };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function headersForProxy(headers, url) {
|
|
417
|
+
const entries = Array.isArray(headers) ? headers.reduce((out, _v, i) => i % 2 ? out : [...out, [headers[i], headers[i + 1]]], []) : Object.entries(headers);
|
|
418
|
+
const out = Object.create(null);
|
|
419
|
+
for (const [key, value] of entries) {
|
|
420
|
+
const name = String(key).toLowerCase();
|
|
421
|
+
if (name === 'host') {
|
|
422
|
+
for (const host of Array.isArray(value) ? value : [value]) {
|
|
423
|
+
const at = new URL(`${url.protocol}//${String(host)}`);
|
|
424
|
+
if (at.host !== url.host || at.username || at.password || at.pathname !== '/' || at.search || at.hash) throw new Error('path-granted request Host differs from its authority');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (!['x-twins-key', 'x-volter-twin-original-host', 'proxy-authorization', 'proxy-connection', 'connection', 'host'].includes(name)) out[key] = value;
|
|
387
428
|
}
|
|
388
|
-
return {
|
|
429
|
+
return Object.assign(out, { host: url.host, connection: 'close' });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function pathGrantProxy() {
|
|
433
|
+
const proxy = new URL(process.env.VOLTER_WORLD_PROXY || '');
|
|
434
|
+
if (proxy.protocol !== 'http:' || !isLocalOrPrivateHost(proxy.hostname) || proxy.username || proxy.password) throw new Error('inspected egress needs the World proxy');
|
|
435
|
+
return proxy;
|
|
389
436
|
}
|
|
390
437
|
|
|
391
438
|
let ORIGINALS = null; // captured once, used to forward to the twin without re-entering the patch
|
|
@@ -413,6 +460,25 @@ function worldBase() {
|
|
|
413
460
|
if (!ref || !/^https?:\/\//i.test(ref)) return null;
|
|
414
461
|
try { const base = new URL(ref.replace(/\/\.well-known\/volter-world\/?$/, '').replace(/\/+$/, '')); return { origin: base.origin, path: base.pathname.replace(/\/+$/, '') }; } catch { return null; }
|
|
415
462
|
}
|
|
463
|
+
// The local Substrate agent keeps host control routing; its product commands select the
|
|
464
|
+
// existing guest listener. Route original destinations through that listener before rewriting.
|
|
465
|
+
function localGuestProxy() {
|
|
466
|
+
const guest = process.env.VOLTER_WORLD_GUEST_PROXY;
|
|
467
|
+
if (!guest || process.env.VOLTER_WORLD_PROXY !== guest) return null;
|
|
468
|
+
try { return new URL(guest); } catch { return null; }
|
|
469
|
+
}
|
|
470
|
+
function explicitTwinEndpoint(url) {
|
|
471
|
+
return url && Object.values(MAP).some(origin => {
|
|
472
|
+
try {
|
|
473
|
+
const endpoint = new URL(origin), path = endpoint.pathname.replace(/\/$/, '');
|
|
474
|
+
return endpoint.origin === url.origin && (!path || url.pathname === path || url.pathname.startsWith(path + '/'));
|
|
475
|
+
} catch { return false; }
|
|
476
|
+
});
|
|
477
|
+
}
|
|
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
|
+
}
|
|
416
482
|
function addressedToWorld(url) {
|
|
417
483
|
const base = worldBase();
|
|
418
484
|
if (!base || !url || !base.path || url.origin.replace(/^ws/, 'http') !== base.origin) return false;
|
|
@@ -467,6 +533,11 @@ function hopToWorld(opts) {
|
|
|
467
533
|
const at = String(opts.path == null ? '/' : opts.path);
|
|
468
534
|
return origin === base.origin && (at.startsWith(`${base.path}/`) || at.startsWith(`/-${base.path}/`));
|
|
469
535
|
}
|
|
536
|
+
function undiciHeaderObject(raw) {
|
|
537
|
+
const entries = Array.isArray(raw) && raw.length && !Array.isArray(raw[0])
|
|
538
|
+
? Array.from({ length: raw.length / 2 }, (_, i) => [String(raw[i * 2]), String(raw[i * 2 + 1])]) : raw;
|
|
539
|
+
return Object.fromEntries(new Headers(entries ?? {}));
|
|
540
|
+
}
|
|
470
541
|
function keyedView(dispatcher) {
|
|
471
542
|
if (!dispatcher || typeof dispatcher.dispatch !== 'function' || KEYED_VIEWS.has(dispatcher)) return dispatcher;
|
|
472
543
|
const view = new Proxy(dispatcher, {
|
|
@@ -474,6 +545,42 @@ function keyedView(dispatcher) {
|
|
|
474
545
|
if (name === 'dispatch') {
|
|
475
546
|
return function keyedDispatch(opts, handler) {
|
|
476
547
|
const key = ORIGINAL_DEFINE_PROPERTY ? twinsKey() : null;
|
|
548
|
+
if (ORIGINAL_DEFINE_PROPERTY && opts?.origin != null) {
|
|
549
|
+
const policy = worldNetworkPolicyFromEnv(process.env);
|
|
550
|
+
let at;
|
|
551
|
+
try {
|
|
552
|
+
const origin = new URL(String(opts.origin));
|
|
553
|
+
const path = String(opts.path ?? '/');
|
|
554
|
+
if (!path.startsWith('/') || /\\/.test(path)) throw new Error('invalid dispatch path');
|
|
555
|
+
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')));
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Undici's shared dispatcher is an HTTP seam even when its caller bypasses global fetch.
|
|
566
|
+
// 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') {
|
|
572
|
+
const endpoint = new URL(twin.origin);
|
|
573
|
+
const headers = undiciHeaderObject(opts.headers);
|
|
574
|
+
headers['host'] = at.host;
|
|
575
|
+
headers['x-volter-twin-original-host'] = at.host;
|
|
576
|
+
if (key) headers['x-twins-key'] = key;
|
|
577
|
+
return target.dispatch(Object.assign({}, opts, {
|
|
578
|
+
origin: endpoint.origin,
|
|
579
|
+
path: endpoint.pathname.replace(/\/$/, '') + at.pathname + at.search,
|
|
580
|
+
headers, maxRedirections: 0,
|
|
581
|
+
}), handler);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
477
584
|
// a hop the caller asked undici to follow is not keyed; a keyed hop is sent with maxRedirections 0, so no
|
|
478
585
|
// redirect follower below this view (a composed interceptor, a RetryAgent or proxy agent around one, undici
|
|
479
586
|
// 6's option or interceptors) carries its headers to the next origin: the redirect comes back to the caller
|
|
@@ -490,7 +597,7 @@ function keyedView(dispatcher) {
|
|
|
490
597
|
}
|
|
491
598
|
let ORIGINAL_DEFINE_PROPERTY = null;
|
|
492
599
|
function keyUndiciDispatches() {
|
|
493
|
-
if (ORIGINAL_DEFINE_PROPERTY || !worldBase() || Object.defineProperty.name === 'keyingDefineProperty') return;
|
|
600
|
+
if (ORIGINAL_DEFINE_PROPERTY || (!worldBase() && !worldNetworkPolicyFromEnv(process.env)) || Object.defineProperty.name === 'keyingDefineProperty') return;
|
|
494
601
|
const define = ORIGINAL_DEFINE_PROPERTY = Object.defineProperty;
|
|
495
602
|
Object.defineProperty = function keyingDefineProperty(target, name, descriptor) {
|
|
496
603
|
if (ORIGINAL_DEFINE_PROPERTY && target === globalThis && UNDICI_DISPATCHERS.includes(name) && descriptor && 'value' in descriptor) descriptor = Object.assign({}, descriptor, { value: keyedView(descriptor.value) });
|
|
@@ -498,6 +605,53 @@ function keyUndiciDispatches() {
|
|
|
498
605
|
};
|
|
499
606
|
for (const name of UNDICI_DISPATCHERS) if (globalThis[name]) globalThis[name] = keyedView(globalThis[name]);
|
|
500
607
|
}
|
|
608
|
+
// Imported Undici APIs may hold a dispatcher obtained before the global view was installed.
|
|
609
|
+
// Rewrite their public HTTP doors as well; an independent connector still faces the socket guards.
|
|
610
|
+
let UNDICI_LOAD = null;
|
|
611
|
+
function routeImportedUndici() {
|
|
612
|
+
if (UNDICI_LOAD) return;
|
|
613
|
+
const original = UNDICI_LOAD = Module._load;
|
|
614
|
+
const seen = new WeakMap();
|
|
615
|
+
Module._load = function volterUndiciLoad(request, parent, isMain) {
|
|
616
|
+
const exports = original.apply(this, arguments);
|
|
617
|
+
if (request !== 'undici' || !exports || typeof exports.request !== 'function') return exports;
|
|
618
|
+
if (seen.has(exports)) return seen.get(exports);
|
|
619
|
+
const view = Object.assign({}, exports);
|
|
620
|
+
view.fetch = function routedUndiciFetch(input, init) {
|
|
621
|
+
if (!installed) return exports.fetch(input, init);
|
|
622
|
+
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);
|
|
624
|
+
// Local control planes may use their own Request class and dispatcher. Preserve both;
|
|
625
|
+
// connections still pass the socket guards, and claimed vendor hops still enter routing.
|
|
626
|
+
if (url && isLocalOrPrivateHost(url.hostname) && (!twin || twin.vendor === 'app')) return exports.fetch(input, init);
|
|
627
|
+
if (input && typeof input === 'object' && typeof input.url === 'string' && !(input instanceof globalThis.Request)) {
|
|
628
|
+
const options = {};
|
|
629
|
+
for (const name of ['method', 'signal', 'redirect', 'cache', 'credentials', 'integrity', 'keepalive', 'referrer', 'referrerPolicy']) {
|
|
630
|
+
if (input[name] !== undefined) options[name] = input[name];
|
|
631
|
+
}
|
|
632
|
+
options.headers = Array.from(input.headers);
|
|
633
|
+
if (input.body !== null && input.body !== undefined) { options.body = input.body; options.duplex = 'half'; }
|
|
634
|
+
return globalThis.fetch(input.url, { ...options, ...init });
|
|
635
|
+
}
|
|
636
|
+
return globalThis.fetch(input, init);
|
|
637
|
+
};
|
|
638
|
+
view.request = function routedUndiciRequest(input, options, callback) {
|
|
639
|
+
if (!installed) return exports.request.apply(exports, arguments);
|
|
640
|
+
const opts = typeof options === 'function' ? {} : options ?? {};
|
|
641
|
+
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);
|
|
643
|
+
if (!twin || twin.vendor === 'app') return exports.request.apply(exports, arguments);
|
|
644
|
+
const headers = undiciHeaderObject(opts.headers);
|
|
645
|
+
headers.host = url.host; headers['x-volter-twin-original-host'] = url.host;
|
|
646
|
+
const key = twinsKey(); if (key) headers['x-twins-key'] = key;
|
|
647
|
+
const target = twin.origin.replace(/\/$/, '') + url.pathname + url.search;
|
|
648
|
+
const { path: _path, ...requestOptions } = opts; // target already carries the effective path and endpoint prefix
|
|
649
|
+
return exports.request(target, { ...requestOptions, headers, maxRedirections: 0 }, typeof options === 'function' ? options : callback);
|
|
650
|
+
};
|
|
651
|
+
seen.set(exports, view);
|
|
652
|
+
return view;
|
|
653
|
+
};
|
|
654
|
+
}
|
|
501
655
|
function unkeyUndiciDispatches() {
|
|
502
656
|
if (!ORIGINAL_DEFINE_PROPERTY) return;
|
|
503
657
|
// only what this put in place is put back: a wrapper installed over it since stays
|
|
@@ -510,55 +664,94 @@ function unkeyUndiciDispatches() {
|
|
|
510
664
|
// twin (or the World) answering 30x to an external URL would carry x-twins-key to a REAL host. A
|
|
511
665
|
// keyed fetch follows redirects itself: a hop that is a twin or the World re-enters the patched
|
|
512
666
|
// fetch (the key re-attached there); any other hop is fetched WITHOUT the key.
|
|
513
|
-
|
|
667
|
+
// The HTTPS cache first needed the vendor URL rather than its loopback transport destination.
|
|
668
|
+
function fetchLocation(response, url) {
|
|
669
|
+
Object.defineProperty(response, 'url', { value: String(url), configurable: true });
|
|
670
|
+
return response;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// WHATWG Fetch, HTTP-redirect fetch, step 13 removes Authorization when URL origins differ:
|
|
674
|
+
// source: https://fetch.spec.whatwg.org/#http-redirect-fetch "delete headerName from request’s header list"
|
|
675
|
+
// Cookie and Proxy-Authorization are also scoped credentials; do not copy them to another origin.
|
|
676
|
+
const CROSS_ORIGIN_DROPPED = ['authorization', 'cookie', 'proxy-authorization', 'x-twins-key', 'x-volter-twin-original-host'];
|
|
677
|
+
function dropCrossOriginHeaders(headers, from, to) {
|
|
678
|
+
if (new URL(from).origin === new URL(to).origin) return;
|
|
679
|
+
if (headers instanceof Headers) {
|
|
680
|
+
for (const name of CROSS_ORIGIN_DROPPED) headers.delete(name);
|
|
681
|
+
} else {
|
|
682
|
+
for (const name of Object.keys(headers)) if (CROSS_ORIGIN_DROPPED.includes(name.toLowerCase())) delete headers[name];
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// A chain's bound belongs to the request, not to one transport follower.
|
|
687
|
+
const redirectChains = new WeakSet();
|
|
688
|
+
let patchedWorldFetch;
|
|
689
|
+
function redirectChain(known) {
|
|
690
|
+
if (known && redirectChains.has(known)) return known;
|
|
691
|
+
const chain = { hops: 0 }; redirectChains.add(chain); return chain;
|
|
692
|
+
}
|
|
693
|
+
function takeRedirect(chain) {
|
|
694
|
+
// Fetch Standard, HTTP-redirect fetch: a redirect count of 20 returns a network error.
|
|
695
|
+
// https://fetch.spec.whatwg.org/#http-redirect-fetch
|
|
696
|
+
if (chain.hops >= 20) throw new TypeError('fetch failed: redirect count exceeded');
|
|
697
|
+
chain.hops++;
|
|
698
|
+
}
|
|
699
|
+
function reenterFetch(input, init, chain) {
|
|
700
|
+
return patchedWorldFetch(input, init, false, chain);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function followKeyless(first, target, reqInit, chain) {
|
|
514
704
|
let hop = await first();
|
|
515
705
|
let hopUrl = target;
|
|
516
706
|
let hopMethod = String(reqInit.method || 'GET').toUpperCase();
|
|
517
707
|
let hopBody = reqInit.body;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
708
|
+
const hopHeaders = new Headers(reqInit.headers || {});
|
|
709
|
+
for (;;) {
|
|
710
|
+
const location = [301, 302, 303, 307, 308].includes(hop.status) ? hop.headers.get('location') : null;
|
|
711
|
+
if (!location) return fetchLocation(hop, hopUrl);
|
|
712
|
+
try { takeRedirect(chain); } catch (error) { try { await hop.body?.cancel(); } catch {} throw error; }
|
|
521
713
|
// RFC semantics per hop (audit M7): 303 → GET; 301/302 demote POST to GET
|
|
522
714
|
// (the browser convention SDKs expect); 307/308 preserve method AND body.
|
|
523
|
-
if (hop.status === 303 || ((hop.status === 301 || hop.status === 302) && hopMethod === 'POST')) {
|
|
715
|
+
if ((hop.status === 303 && hopMethod !== 'HEAD') || ((hop.status === 301 || hop.status === 302) && hopMethod === 'POST')) {
|
|
524
716
|
hopMethod = 'GET';
|
|
525
717
|
hopBody = undefined;
|
|
526
718
|
}
|
|
527
719
|
// Resolve against the CURRENT hop (a relative Location from hop 2 is relative to hop 2),
|
|
528
|
-
// cancel the interim body, and re-enter the PATCHED fetch with the
|
|
529
|
-
//
|
|
720
|
+
// cancel the interim body, and re-enter the PATCHED fetch with the surviving headers.
|
|
721
|
+
// Keep that reduced set across hops, so credentials never return on a later same-origin hop.
|
|
530
722
|
const nextUrl = new URL(location, hopUrl);
|
|
531
723
|
try { hop.body && (await hop.body.cancel()); } catch { /* interim body */ }
|
|
532
|
-
|
|
724
|
+
dropCrossOriginHeaders(hopHeaders, hopUrl, nextUrl);
|
|
533
725
|
hopHeaders.delete('x-twins-key');
|
|
534
726
|
hopHeaders.delete('x-volter-twin-original-host');
|
|
535
|
-
hop = await
|
|
727
|
+
hop = await reenterFetch(nextUrl, {
|
|
536
728
|
method: hopMethod,
|
|
537
729
|
headers: hopHeaders,
|
|
538
730
|
...(hopBody === undefined ? {} : { body: hopBody }),
|
|
539
731
|
redirect: 'manual',
|
|
540
|
-
|
|
732
|
+
...(reqInit.signal ? { signal: reqInit.signal } : {}),
|
|
733
|
+
}, chain);
|
|
541
734
|
hopUrl = nextUrl;
|
|
542
735
|
}
|
|
543
|
-
return hop;
|
|
736
|
+
return fetchLocation(hop, hopUrl);
|
|
544
737
|
}
|
|
545
738
|
|
|
546
739
|
// A request routed to the application is a real request to where it listens (the original http/https request, so
|
|
547
740
|
// streaming, aborts and timeouts behave as the client expects); its options are app-route.cjs's, shared with the twins'
|
|
548
741
|
// deliveries.
|
|
549
742
|
function requestApp(route, url, method, headers, extra, callback) {
|
|
550
|
-
const options =
|
|
743
|
+
const options = route.proxy
|
|
744
|
+
? { protocol: 'http:', hostname: route.proxy.hostname, port: route.proxy.port, path: url.href, method,
|
|
745
|
+
headers: headersForProxy(headers, url), agent: false, ...extra }
|
|
746
|
+
: appRequestOptions(route, url, method, headers, extra);
|
|
551
747
|
const make = options.protocol === 'https:' ? ORIGINALS.httpsRequest : ORIGINALS.httpRequest;
|
|
552
748
|
return callback ? make(options, callback) : make(options);
|
|
553
749
|
}
|
|
554
750
|
|
|
555
|
-
// What leaves the application with a redirect to anywhere else: what native fetch drops crossing origins, and the World's own.
|
|
556
|
-
const CROSS_ORIGIN_DROPPED = ['authorization', 'cookie', 'proxy-authorization', 'x-twins-key', 'x-volter-twin-original-host'];
|
|
557
|
-
|
|
558
751
|
/** fetch() routed to the application, as native fetch behaves: the caller's redirect mode, its signal, a streamed
|
|
559
752
|
* body, decoded content; redirects followed (at most 20) within the application while they stay on its hosts, and
|
|
560
753
|
* through the patched fetch, without credentials or the World's headers, once they leave. */
|
|
561
|
-
async function fetchApp(route, url, input, init) {
|
|
754
|
+
async function fetchApp(route, url, input, init, chain) {
|
|
562
755
|
const request = typeof input === 'object' && input !== null && !(input instanceof URL) && typeof input.url === 'string' ? input : null;
|
|
563
756
|
const mode = (init && init.redirect) || (request && request.redirect) || 'follow';
|
|
564
757
|
const signal = (init && init.signal) || (request && request.signal) || undefined;
|
|
@@ -573,8 +766,8 @@ async function fetchApp(route, url, input, init) {
|
|
|
573
766
|
if (type && !Object.keys(headers).some((k) => k.toLowerCase() === 'content-type')) headers['content-type'] = type;
|
|
574
767
|
body = Buffer.from(await read.arrayBuffer());
|
|
575
768
|
} else if (typeof body === 'string' && !Object.keys(headers).some((k) => k.toLowerCase() === 'content-type')) headers['content-type'] = 'text/plain;charset=UTF-8';
|
|
576
|
-
let at = url; let current = route;
|
|
577
|
-
for (
|
|
769
|
+
let at = url; let current = route; const firstHop = chain.hops;
|
|
770
|
+
for (;;) {
|
|
578
771
|
if (signal && signal.aborted) throw signal.reason ?? new DOMException('This operation was aborted', 'AbortError');
|
|
579
772
|
const res = await new Promise((resolve, reject) => {
|
|
580
773
|
const req = requestApp(current, at, method, headers, { signal }, resolve);
|
|
@@ -586,17 +779,19 @@ async function fetchApp(route, url, input, init) {
|
|
|
586
779
|
if (res.statusCode >= 300 && res.statusCode < 400 && location && [301, 302, 303, 307, 308].includes(res.statusCode) && mode !== 'manual') {
|
|
587
780
|
res.resume();
|
|
588
781
|
if (mode === 'error') throw new TypeError('fetch failed: unexpected redirect');
|
|
589
|
-
|
|
782
|
+
takeRedirect(chain);
|
|
590
783
|
const next = new URL(String(location), at);
|
|
591
784
|
// 303 answers with a GET (a HEAD stays one); 301 and 302 turn only a POST into a GET, as fetch does
|
|
592
785
|
if ((res.statusCode === 303 && method !== 'HEAD') || ((res.statusCode === 301 || res.statusCode === 302) && method === 'POST')) {
|
|
593
786
|
method = 'GET'; body = undefined;
|
|
594
787
|
for (const k of Object.keys(headers)) if (/^content-/i.test(k)) delete headers[k];
|
|
595
788
|
}
|
|
789
|
+
dropCrossOriginHeaders(headers, at, next);
|
|
596
790
|
const nextRoute = resolveTwin(next.hostname, MAP, next.pathname);
|
|
597
|
-
if (!nextRoute || nextRoute.vendor !== 'app') {
|
|
598
|
-
|
|
599
|
-
|
|
791
|
+
if ((current.proxy && localGuestProxy()) || !nextRoute || nextRoute.vendor !== 'app') {
|
|
792
|
+
delete headers['x-twins-key'];
|
|
793
|
+
delete headers['x-volter-twin-original-host'];
|
|
794
|
+
const onward = await reenterFetch(next.toString(), { method, headers, redirect: mode, ...(signal ? { signal } : {}), ...(body != null ? { body } : {}) }, chain);
|
|
600
795
|
return onward;
|
|
601
796
|
}
|
|
602
797
|
at = next; current = nextRoute;
|
|
@@ -613,7 +808,7 @@ async function fetchApp(route, url, input, init) {
|
|
|
613
808
|
if (empty) res.resume();
|
|
614
809
|
const response = new Response(empty ? null : Readable.toWeb(stream), { status: res.statusCode, statusText: res.statusMessage, headers: out });
|
|
615
810
|
Object.defineProperty(response, 'url', { value: at.toString() });
|
|
616
|
-
if (hops >
|
|
811
|
+
if (chain.hops > firstHop) Object.defineProperty(response, 'redirected', { value: true });
|
|
617
812
|
return response;
|
|
618
813
|
}
|
|
619
814
|
}
|
|
@@ -869,7 +1064,30 @@ function lookupWaitsForClaims(hostname) {
|
|
|
869
1064
|
* slow resolver would, and a name it claims is known from its next read. Null when every first read has landed. */
|
|
870
1065
|
function claimsPendingBriefly() {
|
|
871
1066
|
const pending = claimsPending(MAP);
|
|
872
|
-
return pending ?
|
|
1067
|
+
return pending ? waitForClaims(pending) : null;
|
|
1068
|
+
}
|
|
1069
|
+
/** The caller's whole wait is bounded independently of reads already in flight. */
|
|
1070
|
+
function waitForClaims(pending, claimingTwins) {
|
|
1071
|
+
return new Promise((resolve) => {
|
|
1072
|
+
const started = performance.now();
|
|
1073
|
+
let settled = false;
|
|
1074
|
+
const finish = (outcome) => {
|
|
1075
|
+
if (settled) return;
|
|
1076
|
+
settled = true;
|
|
1077
|
+
clearTimeout(deadline);
|
|
1078
|
+
if (claimingTwins !== undefined && claimsRefreshChannel.hasSubscribers) {
|
|
1079
|
+
claimsRefreshChannel.publish({ outcome, elapsedMs: performance.now() - started, limitMs: CLAIMS_WAIT_MS, claimingTwins });
|
|
1080
|
+
}
|
|
1081
|
+
resolve();
|
|
1082
|
+
};
|
|
1083
|
+
const deadline = setTimeout(() => finish('expired'), CLAIMS_WAIT_MS);
|
|
1084
|
+
pending.then(() => finish('complete'), () => finish('failed'));
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
/** Refresh unresolved fetch destinations once; a claim acquired after startup must not await the poll. */
|
|
1088
|
+
function claimsRefreshBriefly() {
|
|
1089
|
+
const reads = Object.keys(CLAIMERS).filter((v) => MAP[v]).map((v) => claimsAt(MAP[v], CLAIMERS[v]).refresh());
|
|
1090
|
+
return reads.length ? waitForClaims(Promise.all(reads), reads.length) : null;
|
|
873
1091
|
}
|
|
874
1092
|
function patchedLookup(original) {
|
|
875
1093
|
return function patchedDnsLookup(hostname, options, callback) {
|
|
@@ -898,6 +1116,7 @@ function patchedPromisesLookup(original) {
|
|
|
898
1116
|
|
|
899
1117
|
/** The port a net/tls connect names (`connect(port, host)`, `connect({ port, host })`), or undefined. */
|
|
900
1118
|
function socketTargetPort(args) {
|
|
1119
|
+
if (Array.isArray(args[0])) args = args[0]; // Node factories pass their normalized argument tuple to the instance.
|
|
901
1120
|
const [first] = args;
|
|
902
1121
|
const raw = first !== null && typeof first === 'object' ? first.port : first;
|
|
903
1122
|
const port = Number(raw);
|
|
@@ -910,7 +1129,19 @@ function socketRefusal(hostname, port, isTls) {
|
|
|
910
1129
|
return `[twin-inject] refused a socket to ${host}, the address this World's DNS gives the names it routes: `
|
|
911
1130
|
+ 'nothing listens there; connect by the name, which the World routes to its twin or its application.';
|
|
912
1131
|
}
|
|
1132
|
+
if (localGuestProxy() && isLocalOrPrivateHost(host)) {
|
|
1133
|
+
const endpoints = [localGuestProxy(), worldBase() && new URL(worldBase().origin), ...Object.values(MAP).map(origin => { try { return new URL(origin); } catch { return null; } })].filter(Boolean);
|
|
1134
|
+
if (!endpoints.some(endpoint => endpoint.hostname.replace(/^\[|\]$/g, '') === host.replace(/^\[|\]$/g, '') && Number(endpoint.port || (endpoint.protocol === 'https:' ? 443 : 80)) === port)) {
|
|
1135
|
+
return `[twin-inject] local guest private socket to ${host}:${port} requires its guest proxy`;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
913
1138
|
if (!host || isLocalOrPrivateHost(host) || isTwinOriginHost(host)) return null;
|
|
1139
|
+
// the destination as an origin, its port and its protocol included: a policy that lists https://host allows TLS to
|
|
1140
|
+
// host:443, never another port or a plaintext connection to it
|
|
1141
|
+
const authority = `${isIP(host) === 6 ? `[${host}]` : host}${port !== undefined && !(isTls && port === 443) ? `:${port}` : ''}`;
|
|
1142
|
+
const url = (() => { try { return new URL(`${isTls ? 'https' : 'http'}://${authority}/`); } catch { return null; } })();
|
|
1143
|
+
const policy = worldNetworkPolicyFromEnv(process.env);
|
|
1144
|
+
if (url && policy && !strictEgressEnabled() && worldNetworkUsesPassthrough(policy, url)) return null;
|
|
914
1145
|
const vendors = twinnedHostVendors(host, MAP);
|
|
915
1146
|
if (vendors.length > 0) {
|
|
916
1147
|
const doors = vendors.map(endpointEnvFor).filter(Boolean);
|
|
@@ -924,10 +1155,7 @@ function socketRefusal(hostname, port, isTls) {
|
|
|
924
1155
|
return `[twin-inject] refused a raw socket to ${host}, which this world routes to the application (volter-world app-url --host): `
|
|
925
1156
|
+ 'the client opened its own connection instead of going through http/https or fetch, so it could not be routed.';
|
|
926
1157
|
}
|
|
927
|
-
|
|
928
|
-
// host:443, never another port or a plaintext connection to it
|
|
929
|
-
const authority = `${isIP(host) === 6 ? `[${host}]` : host}${port !== undefined && !(isTls && port === 443) ? `:${port}` : ''}`;
|
|
930
|
-
const url = (() => { try { return new URL(`${isTls ? 'https' : 'http'}://${authority}/`); } catch { return null; } })();
|
|
1158
|
+
if (url && policy && !strictEgressEnabled() && worldNetworkNeedsPathInspection(policy, url)) return `[twin-inject] inspected egress requires a mediated HTTP request to ${authority}`;
|
|
931
1159
|
if (url && shouldBlockUntwinned(url)) return `[twin-inject] blocked untwinned external socket to ${authority}`;
|
|
932
1160
|
return null;
|
|
933
1161
|
}
|
|
@@ -938,8 +1166,22 @@ function refusedSocket(message) {
|
|
|
938
1166
|
return socket;
|
|
939
1167
|
}
|
|
940
1168
|
|
|
1169
|
+
// tls.connect over an existing World proxy socket creates no external connection. Verify the
|
|
1170
|
+
// actual socket peer, never its caller-supplied host/servername, before permitting that SNI.
|
|
1171
|
+
function isWorldProxySocket(options) {
|
|
1172
|
+
if (!options || !(options.socket instanceof net.Socket) || options.socket.destroyed) return false;
|
|
1173
|
+
let proxy;
|
|
1174
|
+
try { proxy = new URL(process.env.VOLTER_WORLD_PROXY || ''); } catch { return false; }
|
|
1175
|
+
const local = ['localhost', '127.0.0.1', '[::1]', '::1'];
|
|
1176
|
+
if (!local.includes(proxy.hostname)) return false;
|
|
1177
|
+
const peer = options.socket.remoteAddress;
|
|
1178
|
+
if (!['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(peer)) return false;
|
|
1179
|
+
return options.socket.remotePort === Number(proxy.port || (proxy.protocol === 'https:' ? 443 : 80));
|
|
1180
|
+
}
|
|
1181
|
+
|
|
941
1182
|
function patchedConnect(originalConnect, isTls) {
|
|
942
1183
|
return function patchedSocketConnect(...args) {
|
|
1184
|
+
if (isTls && isWorldProxySocket(args.find((a) => a && typeof a === 'object' && a.socket))) return originalConnect.apply(this, args);
|
|
943
1185
|
const port = socketTargetPort(args);
|
|
944
1186
|
for (const host of socketTargetHosts(args, isTls)) {
|
|
945
1187
|
const refusal = socketRefusal(host, port, isTls);
|
|
@@ -949,6 +1191,40 @@ function patchedConnect(originalConnect, isTls) {
|
|
|
949
1191
|
};
|
|
950
1192
|
}
|
|
951
1193
|
|
|
1194
|
+
function patchedInstanceConnect(originalConnect) {
|
|
1195
|
+
return function guardedInstanceConnect(...args) {
|
|
1196
|
+
const target = Array.isArray(args[0]) ? args[0] : args;
|
|
1197
|
+
const isTls = this instanceof tls.TLSSocket;
|
|
1198
|
+
for (const host of socketTargetHosts(target, isTls)) {
|
|
1199
|
+
const refusal = socketRefusal(host, socketTargetPort(target), isTls);
|
|
1200
|
+
if (refusal) {
|
|
1201
|
+
// Preserve the instance and its error listeners; never return a different socket.
|
|
1202
|
+
process.nextTick(() => this.destroy(Object.assign(new Error(refusal), { code: 'ECONNREFUSED' })));
|
|
1203
|
+
return this;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return originalConnect.apply(this, args);
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function patchedDatagram(original, connecting) {
|
|
1211
|
+
return function guardedDatagram(...args) {
|
|
1212
|
+
let peer;
|
|
1213
|
+
if (!connecting) { try { peer = this.remoteAddress(); } catch { /* unconnected UDP socket */ } }
|
|
1214
|
+
const tail = connecting ? args : args.slice(1);
|
|
1215
|
+
const host = peer?.address ?? tail.findLast(arg => typeof arg === 'string') ?? 'localhost';
|
|
1216
|
+
const port = peer?.port ?? tail.findLast(arg => typeof arg === 'number');
|
|
1217
|
+
const refusal = socketRefusal(host, port, false);
|
|
1218
|
+
if (refusal) {
|
|
1219
|
+
const error = Object.assign(new Error(refusal), { code: 'ECONNREFUSED' });
|
|
1220
|
+
const callback = !connecting && typeof args.at(-1) === 'function' ? args.at(-1) : null;
|
|
1221
|
+
process.nextTick(() => callback ? callback(error) : this.emit('error', error));
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
return original.apply(this, args);
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
952
1228
|
let MAP = {};
|
|
953
1229
|
let installed = false;
|
|
954
1230
|
|
|
@@ -981,8 +1257,14 @@ function upgradeToTwin(origin, url, method, headers, args, callback) {
|
|
|
981
1257
|
|
|
982
1258
|
function patchedFactory(originalRequest, defaultProtocol) {
|
|
983
1259
|
return function patchedRequest(...args) {
|
|
984
|
-
const { url, method, headers, callback } = describe(args, defaultProtocol);
|
|
985
|
-
|
|
1260
|
+
const { url, method, headers, callback, options: given, invalid } = describe(args, defaultProtocol);
|
|
1261
|
+
if (invalid && worldNetworkPolicyFromEnv(process.env)) return blockedClientRequest('[twin-inject] request has an invalid effective authority or path');
|
|
1262
|
+
const guest = mediateLocalGuest(url);
|
|
1263
|
+
if (guest) {
|
|
1264
|
+
if (isUpgrade(headers) || method === 'CONNECT') return blockedClientRequest('[twin-inject] local guest upgrade requires a mediated transport');
|
|
1265
|
+
return requestApp({ proxy: guest }, url, method, headers, { signal: given.signal, timeout: given.timeout }, callback);
|
|
1266
|
+
}
|
|
1267
|
+
const twin = url && !publicBuildRequest(url) && resolveTwin(url.hostname, MAP, url.pathname);
|
|
986
1268
|
if (twin && twin.vendor === 'app') {
|
|
987
1269
|
const given = args.find((a) => a && typeof a === 'object' && !(a instanceof URL)) || {};
|
|
988
1270
|
return requestApp(twin, url, method, headers, { signal: given.signal, timeout: given.timeout }, callback);
|
|
@@ -996,10 +1278,17 @@ function patchedFactory(originalRequest, defaultProtocol) {
|
|
|
996
1278
|
// No twin claims this path — but if configured twins claim the HOST, the path is an unclaimed
|
|
997
1279
|
// slice of a shared, virtualized host (the Cal.com /calendar/v3 case): refuse loudly, in every
|
|
998
1280
|
// mode, before the strict-egress check gets a chance to phrase it as an ordinary untwinned host.
|
|
999
|
-
if (url && twinnedHostVendors(url.hostname, MAP).length > 0) {
|
|
1281
|
+
if (url && !publicBuildRequest(url) && twinnedHostVendors(url.hostname, MAP).length > 0) {
|
|
1000
1282
|
return blockedClientRequest(unclaimedTwinnedHostPathMessage(url.hostname, url.pathname, MAP));
|
|
1001
1283
|
}
|
|
1002
1284
|
if (shouldBlockUntwinned(url)) return blockedClientRequest(`[twin-inject] blocked untwinned external request to ${url.hostname}`);
|
|
1285
|
+
const policy = worldNetworkPolicyFromEnv(process.env);
|
|
1286
|
+
if (url && policy && worldNetworkNeedsPathInspection(policy, url)) {
|
|
1287
|
+
if (isUpgrade(headers) || method === 'CONNECT') return blockedClientRequest('[twin-inject] inspected egress cannot upgrade its transport');
|
|
1288
|
+
try {
|
|
1289
|
+
return requestApp({ proxy: pathGrantProxy() }, url, method, headers, { signal: given.signal, timeout: given.timeout }, callback);
|
|
1290
|
+
} catch (error) { return blockedClientRequest(`[twin-inject] ${error.message}`); }
|
|
1291
|
+
}
|
|
1003
1292
|
const request = originalRequest.apply(this, addressedToWorld(url) ? withWorldKey(args) : args);
|
|
1004
1293
|
// Local mode, untwinned external host, world CA in force: the TLS handshake against the
|
|
1005
1294
|
// real vendor is about to fail with a bare OpenSSL message — annotate it in-place via an
|
|
@@ -1035,6 +1324,9 @@ function install(mapOverride) {
|
|
|
1035
1324
|
fetch: typeof globalThis.fetch === 'function' ? globalThis.fetch : null,
|
|
1036
1325
|
netConnect: net.connect,
|
|
1037
1326
|
netCreateConnection: net.createConnection,
|
|
1327
|
+
socketConnect: net.Socket.prototype.connect,
|
|
1328
|
+
datagramConnect: dgram.Socket.prototype.connect,
|
|
1329
|
+
datagramSend: dgram.Socket.prototype.send,
|
|
1038
1330
|
tlsConnect: tls.connect,
|
|
1039
1331
|
dnsLookup: dns.lookup,
|
|
1040
1332
|
dnsPromisesLookup: dns.promises.lookup,
|
|
@@ -1047,18 +1339,27 @@ function install(mapOverride) {
|
|
|
1047
1339
|
https.get = function patchedHttpsGet(...args) { const req = https.request(...args); req.end(); return req; };
|
|
1048
1340
|
net.connect = patchedConnect(ORIGINALS.netConnect, false);
|
|
1049
1341
|
net.createConnection = patchedConnect(ORIGINALS.netCreateConnection, false);
|
|
1342
|
+
net.Socket.prototype.connect = patchedInstanceConnect(ORIGINALS.socketConnect);
|
|
1343
|
+
dgram.Socket.prototype.connect = patchedDatagram(ORIGINALS.datagramConnect, true);
|
|
1344
|
+
dgram.Socket.prototype.send = patchedDatagram(ORIGINALS.datagramSend, false);
|
|
1050
1345
|
tls.connect = patchedConnect(ORIGINALS.tlsConnect, true);
|
|
1051
1346
|
dns.lookup = patchedLookup(ORIGINALS.dnsLookup);
|
|
1052
1347
|
dns.promises.lookup = patchedPromisesLookup(ORIGINALS.dnsPromisesLookup);
|
|
1053
1348
|
keyUndiciDispatches();
|
|
1349
|
+
routeImportedUndici();
|
|
1054
1350
|
// gRPC (architecture, "Other wires": gRPC): an HTTP/2 session to a host a twin serves (grpc-js on google-gax: TLS to
|
|
1055
1351
|
// webrisk.googleapis.com) connects to that twin's origin in plain text, where the kernel's HTTP/2 server answers it;
|
|
1056
1352
|
// the client carries on unchanged. Any other authority is the real one's.
|
|
1057
1353
|
http2.connect = function patchedHttp2Connect(authority, options, listener) {
|
|
1058
1354
|
let url = null;
|
|
1059
1355
|
try { url = new URL(typeof authority === 'string' ? authority : authority && authority.href ? authority.href : `https://${authority.hostname || authority.host}`); } catch { url = null; }
|
|
1060
|
-
const twin = url && resolveTwin(url.hostname, MAP, '/');
|
|
1061
|
-
if (!twin)
|
|
1356
|
+
const twin = url && !publicBuildRequest(url) && resolveTwin(url.hostname, MAP, '/');
|
|
1357
|
+
if (!twin) {
|
|
1358
|
+
if (mediateLocalGuest(url)) throw new Error('[twin-inject] local guest HTTP/2 requires a mediated transport');
|
|
1359
|
+
const policy = worldNetworkPolicyFromEnv(process.env);
|
|
1360
|
+
if (url && policy && !isLocalOrPrivateHost(url.hostname) && worldNetworkNeedsPathInspection(policy, url)) throw new Error('[twin-inject] HTTP/2 cannot use inspected egress');
|
|
1361
|
+
return ORIGINALS.http2Connect.apply(this, arguments);
|
|
1362
|
+
}
|
|
1062
1363
|
const cb = typeof options === 'function' ? options : listener;
|
|
1063
1364
|
const opts = typeof options === 'function' || !options ? {} : Object.fromEntries(Object.entries(options).filter(([k]) => !['secureContext', 'servername', 'ca', 'cert', 'key', 'checkServerIdentity', 'ALPNProtocols', 'createConnection', 'rejectUnauthorized'].includes(k)));
|
|
1064
1365
|
return ORIGINALS.http2Connect.call(this, new URL(twin.origin).origin, opts, cb);
|
|
@@ -1066,27 +1367,32 @@ function install(mapOverride) {
|
|
|
1066
1367
|
|
|
1067
1368
|
if (ORIGINALS.fetch) {
|
|
1068
1369
|
const originalFetch = ORIGINALS.fetch;
|
|
1069
|
-
globalThis.fetch = function patchedFetch(input, init, claimsWaited) {
|
|
1370
|
+
globalThis.fetch = patchedWorldFetch = function patchedFetch(input, init, claimsWaited, knownChain) {
|
|
1371
|
+
const chain = redirectChain(knownChain);
|
|
1070
1372
|
let urlStr;
|
|
1071
1373
|
try { urlStr = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input && input.url; } catch { urlStr = undefined; }
|
|
1072
1374
|
let url = null;
|
|
1073
|
-
if (urlStr) {
|
|
1074
|
-
|
|
1375
|
+
if (urlStr) {
|
|
1376
|
+
if (/\\/.test(String(urlStr).split(/[?#]/, 1)[0]) && worldNetworkPolicyFromEnv(process.env)) return Promise.reject(new Error('[twin-inject] backslash in fetch target'));
|
|
1377
|
+
try { url = new URL(urlStr); } catch { url = null; }
|
|
1378
|
+
}
|
|
1379
|
+
const guest = mediateLocalGuest(url);
|
|
1380
|
+
if (guest) return fetchApp({ proxy: guest }, url, input, init, chain);
|
|
1381
|
+
// Resolve refreshed claims before evaluating the public build-artifact exception.
|
|
1382
|
+
const claimed = url && resolveTwin(url.hostname, MAP, url.pathname);
|
|
1383
|
+
const twin = claimed && !publicBuildRequest(url) ? claimed : null;
|
|
1075
1384
|
if (!twin) {
|
|
1076
|
-
//
|
|
1077
|
-
//
|
|
1078
|
-
const pending = !claimsWaited && url && !isLocalOrPrivateHost(url.hostname) && !isTwinOriginHost(url.hostname) ?
|
|
1079
|
-
if (pending) return pending.then(() => patchedFetch(input, init, true));
|
|
1385
|
+
// A host may have been claimed since startup (including by a child process).
|
|
1386
|
+
// Refresh once at most: a retry goes on with the claims actually returned.
|
|
1387
|
+
const pending = !claimsWaited && url && !isLocalOrPrivateHost(url.hostname) && !isTwinOriginHost(url.hostname) ? claimsRefreshBriefly() : null;
|
|
1388
|
+
if (pending) return pending.then(() => patchedFetch(input, init, true, chain));
|
|
1080
1389
|
// Same shared-host rule as patchedFactory: an unclaimed path on a twinned host refuses
|
|
1081
1390
|
// loudly in every mode — never mis-routed, never leaked to the real vendor.
|
|
1082
|
-
if (url && twinnedHostVendors(url.hostname, MAP).length > 0) {
|
|
1391
|
+
if (url && !publicBuildRequest(url) && twinnedHostVendors(url.hostname, MAP).length > 0) {
|
|
1083
1392
|
return Promise.reject(new Error(unclaimedTwinnedHostPathMessage(url.hostname, url.pathname, MAP)));
|
|
1084
1393
|
}
|
|
1085
1394
|
if (shouldBlockUntwinned(url)) return Promise.reject(new Error(`[twin-inject] blocked untwinned external fetch to ${url.hostname}`));
|
|
1086
|
-
//
|
|
1087
|
-
// point. Under an explicit World policy they must not open an unchecked
|
|
1088
|
-
// second origin. Manual redirects remain readable for an SDK to follow
|
|
1089
|
-
// through this same authorization door.
|
|
1395
|
+
// Each followed redirect re-enters authorization; native fetch only receives manual hops under a policy.
|
|
1090
1396
|
if (addressedToWorld(url) && twinsKey()) {
|
|
1091
1397
|
const request = typeof input === 'object' && input !== null ? input : undefined;
|
|
1092
1398
|
const headers = new Headers(init?.headers ?? request?.headers ?? {});
|
|
@@ -1095,16 +1401,23 @@ function install(mapOverride) {
|
|
|
1095
1401
|
if (!keyed.method && request?.method) keyed.method = request.method;
|
|
1096
1402
|
const redirect = keyed.redirect ?? request?.redirect ?? 'follow';
|
|
1097
1403
|
if (redirect === 'follow') {
|
|
1098
|
-
if (worldNetworkPolicyFromEnv(process.env)) return originalFetch(input, Object.assign(keyed, { redirect: 'error' }));
|
|
1099
1404
|
keyed.redirect = 'manual';
|
|
1100
|
-
return followKeyless(() => originalFetch(input, keyed), url, keyed);
|
|
1405
|
+
return followKeyless(() => originalFetch(input, keyed), url, keyed, chain);
|
|
1101
1406
|
}
|
|
1102
1407
|
return originalFetch(input, keyed);
|
|
1103
1408
|
}
|
|
1104
1409
|
const policy = worldNetworkPolicyFromEnv(process.env);
|
|
1105
1410
|
const redirect = init?.redirect ?? (typeof input === 'object' && input?.redirect) ?? 'follow';
|
|
1106
|
-
|
|
1107
|
-
|
|
1411
|
+
if (url && policy && worldNetworkNeedsPathInspection(policy, url)) {
|
|
1412
|
+
try { return fetchApp({ proxy: pathGrantProxy() }, url, input, init, chain); }
|
|
1413
|
+
catch (error) { return Promise.reject(error); }
|
|
1414
|
+
}
|
|
1415
|
+
const passthrough = policy && redirect === 'follow'
|
|
1416
|
+
? followKeyless(() => originalFetch(input, { ...init, redirect: 'manual' }), url, {
|
|
1417
|
+
method: init?.method ?? input?.method, headers: init?.headers ?? input?.headers,
|
|
1418
|
+
body: init?.body ?? input?.body, signal: init?.signal ?? input?.signal,
|
|
1419
|
+
}, chain)
|
|
1420
|
+
: originalFetch(input, init);
|
|
1108
1421
|
// Same untwinned-host TLS naming as patchedFactory: fetch wraps the issuer error in
|
|
1109
1422
|
// "fetch failed" with the code down the `cause` chain — annotate the surfaced error.
|
|
1110
1423
|
if (url && !isLocalOrPrivateHost(url.hostname) && worldCaIsConfigured() && passthrough && typeof passthrough.catch === 'function') {
|
|
@@ -1116,7 +1429,7 @@ function install(mapOverride) {
|
|
|
1116
1429
|
}
|
|
1117
1430
|
return passthrough;
|
|
1118
1431
|
}
|
|
1119
|
-
if (twin.vendor === 'app') return fetchApp(twin, url, input, init);
|
|
1432
|
+
if (twin.vendor === 'app') return fetchApp(twin, url, input, init, chain);
|
|
1120
1433
|
const target = `${twin.origin.replace(/\/$/, '')}${url.pathname}${url.search}`;
|
|
1121
1434
|
const reqInit = Object.assign({}, init);
|
|
1122
1435
|
if (!reqInit.method && typeof input === 'object' && input && input.method) reqInit.method = input.method;
|
|
@@ -1128,7 +1441,6 @@ function install(mapOverride) {
|
|
|
1128
1441
|
if (reqInit.duplex === undefined) reqInit.duplex = 'half';
|
|
1129
1442
|
}
|
|
1130
1443
|
if (reqInit.redirect === undefined && typeof input === 'object' && input?.redirect) reqInit.redirect = input.redirect;
|
|
1131
|
-
if (worldNetworkPolicyFromEnv(process.env) && (reqInit.redirect === undefined || reqInit.redirect === 'follow')) reqInit.redirect = 'error';
|
|
1132
1444
|
// Preserve the vendor destination for twins that serve more than one registry flavor from
|
|
1133
1445
|
// one local origin. Host is honored by node:http; this explicit internal header is the
|
|
1134
1446
|
// fetch-safe fallback when a runtime normalizes the forbidden Host header itself.
|
|
@@ -1141,9 +1453,9 @@ function install(mapOverride) {
|
|
|
1141
1453
|
// through this same patch, so its next host is routed on its own.
|
|
1142
1454
|
// Redirects are followed by followKeyless, so the key never reaches another origin. The
|
|
1143
1455
|
// node path already behaves this way (the SDK sees the 30x and retries through the patch).
|
|
1144
|
-
if (twinsKey() && (reqInit.redirect === undefined || reqInit.redirect === 'follow')) {
|
|
1456
|
+
if ((twinsKey() || /^http:/i.test(twin.origin)) && (reqInit.redirect === undefined || reqInit.redirect === 'follow')) {
|
|
1145
1457
|
reqInit.redirect = 'manual';
|
|
1146
|
-
return followKeyless(() => originalFetch(target, reqInit),
|
|
1458
|
+
return followKeyless(() => originalFetch(target, reqInit), url, reqInit, chain);
|
|
1147
1459
|
}
|
|
1148
1460
|
// an https twin origin (a served world's door): undici cannot use a
|
|
1149
1461
|
// session CA set after process start, so route through forward() — the
|
|
@@ -1161,10 +1473,10 @@ function install(mapOverride) {
|
|
|
1161
1473
|
// branch used to prefix too, doubling the namespace on pathed https skins).
|
|
1162
1474
|
const fullPath = `${url.pathname}${url.search}`;
|
|
1163
1475
|
return forward(twin.origin, String(reqInit.method || 'GET').toUpperCase(), fullPath, headerObj, bodyValue === undefined ? undefined : Buffer.from(bodyValue))
|
|
1164
|
-
.then((res) => new Response(res.body, { status: res.statusCode, headers: res.headers }));
|
|
1476
|
+
.then((res) => fetchLocation(new Response(res.body, { status: res.statusCode, headers: res.headers }), url));
|
|
1165
1477
|
}
|
|
1166
1478
|
}
|
|
1167
|
-
return originalFetch(target, reqInit);
|
|
1479
|
+
return originalFetch(target, reqInit).then((response) => fetchLocation(response, url));
|
|
1168
1480
|
};
|
|
1169
1481
|
}
|
|
1170
1482
|
|
|
@@ -1207,6 +1519,9 @@ function restore() {
|
|
|
1207
1519
|
if (ORIGINALS.fetch) globalThis.fetch = ORIGINALS.fetch;
|
|
1208
1520
|
net.connect = ORIGINALS.netConnect;
|
|
1209
1521
|
net.createConnection = ORIGINALS.netCreateConnection;
|
|
1522
|
+
net.Socket.prototype.connect = ORIGINALS.socketConnect;
|
|
1523
|
+
dgram.Socket.prototype.connect = ORIGINALS.datagramConnect;
|
|
1524
|
+
dgram.Socket.prototype.send = ORIGINALS.datagramSend;
|
|
1210
1525
|
tls.connect = ORIGINALS.tlsConnect;
|
|
1211
1526
|
dns.lookup = ORIGINALS.dnsLookup;
|
|
1212
1527
|
dns.promises.lookup = ORIGINALS.dnsPromisesLookup;
|
|
@@ -1262,7 +1577,71 @@ function installWorldTime() {
|
|
|
1262
1577
|
globalThis.Date = WorldDate;
|
|
1263
1578
|
}
|
|
1264
1579
|
|
|
1580
|
+
// A Cloudflare Worker started through miniflare (`wrangler dev`, Vitest's Workers pool) makes its outbound requests from
|
|
1581
|
+
// workerd, which neither the patched fetch nor the proxy sees. Each miniflare Worker hands them to its
|
|
1582
|
+
// `outboundService`; one that names none is given the patched fetch here, at construction and at setOptions, so its
|
|
1583
|
+
// vendor calls reach their twins as the process's do (architecture, the injector). Its own outbound service stands.
|
|
1584
|
+
function installMiniflareOutbound() {
|
|
1585
|
+
if (Module.__volterMiniflareOutboundInstalled) return;
|
|
1586
|
+
Module.__volterMiniflareOutboundInstalled = true;
|
|
1587
|
+
// A host this process maps to a twin goes through the patched fetch. Any other goes where the World routes it: through
|
|
1588
|
+
// its proxy (HTTPS_PROXY, trusting the World's CA), which Node's own fetch does not read, so through miniflare's undici
|
|
1589
|
+
// with the environment's proxy agent; loopback stays direct.
|
|
1590
|
+
const outboundFor = (miniflarePath) => {
|
|
1591
|
+
let proxied = null;
|
|
1592
|
+
try {
|
|
1593
|
+
if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY) {
|
|
1594
|
+
const undici = Module.createRequire(miniflarePath)('undici');
|
|
1595
|
+
const dispatcher = new undici.EnvHttpProxyAgent({ noProxy: localGuestProxy() ? '' : process.env.NO_PROXY || process.env.no_proxy || 'localhost,127.0.0.1,::1' });
|
|
1596
|
+
proxied = (request) => undici.fetch(request, { dispatcher });
|
|
1597
|
+
}
|
|
1598
|
+
} catch { proxied = null; }
|
|
1599
|
+
const mapped = (request) => { try { const url = new URL(request.url); return Boolean(resolveTwin(url.hostname, MAP, url.pathname)); } catch { return false; } };
|
|
1600
|
+
// The Request itself is handed on: its method, headers and body are getters, which a copy of it as `init` loses.
|
|
1601
|
+
return (request) => (!proxied || (installed && mapped(request)) ? globalThis.fetch(request) : proxied(request));
|
|
1602
|
+
};
|
|
1603
|
+
let outbound = null;
|
|
1604
|
+
const routed = (options) => {
|
|
1605
|
+
if (!options || typeof options !== 'object') return options;
|
|
1606
|
+
// miniflare 5's Worker is `{ config, legacy, dev }`, its outbound a fetcher under `dev` (wrangler's own ProxyWorker, the
|
|
1607
|
+
// one that turns the Cache API off, forwards to the app's Worker and keeps its own path); miniflare 4's names it inline.
|
|
1608
|
+
const worker = (w) => {
|
|
1609
|
+
if (!w || typeof w !== 'object') return w;
|
|
1610
|
+
if ('config' in w) {
|
|
1611
|
+
const dev = w.dev && typeof w.dev === 'object' ? w.dev : {};
|
|
1612
|
+
return dev.outboundService === undefined && dev.cacheAPI !== false ? { ...w, dev: { ...dev, outboundService: { type: 'fetcher', handler: outbound } } } : w;
|
|
1613
|
+
}
|
|
1614
|
+
return w.outboundService === undefined ? { ...w, outboundService: outbound } : w;
|
|
1615
|
+
};
|
|
1616
|
+
return Array.isArray(options.workers) ? { ...options, workers: options.workers.map(worker) } : worker(options);
|
|
1617
|
+
};
|
|
1618
|
+
const originalLoad = Module._load;
|
|
1619
|
+
const seen = new WeakMap();
|
|
1620
|
+
Module._load = function volterMiniflareLoad(request, parent) {
|
|
1621
|
+
const exports = originalLoad.apply(this, arguments);
|
|
1622
|
+
if (request !== 'miniflare' || !exports || typeof exports.Miniflare !== 'function' || exports.Miniflare.__volterRouted) return exports;
|
|
1623
|
+
if (seen.has(exports)) return seen.get(exports);
|
|
1624
|
+
if (!outbound) {
|
|
1625
|
+
let at = null;
|
|
1626
|
+
try { at = Module._resolveFilename(request, parent); } catch { at = null; }
|
|
1627
|
+
outbound = at ? outboundFor(at) : (request) => globalThis.fetch(request);
|
|
1628
|
+
}
|
|
1629
|
+
const Original = exports.Miniflare;
|
|
1630
|
+
class Miniflare extends Original {
|
|
1631
|
+
constructor(options) { super(routed(options)); }
|
|
1632
|
+
setOptions(options) { return super.setOptions(routed(options)); }
|
|
1633
|
+
}
|
|
1634
|
+
Miniflare.__volterRouted = true;
|
|
1635
|
+
// miniflare's build exports `Miniflare` as a getter that can be neither assigned nor redefined: its importer is given
|
|
1636
|
+
// a view of the module whose `Miniflare` is this one, the same view each time it is required.
|
|
1637
|
+
const view = new Proxy(exports, { get: (target, key, receiver) => (key === 'Miniflare' ? Miniflare : Reflect.get(target, key, receiver)) });
|
|
1638
|
+
seen.set(exports, view);
|
|
1639
|
+
return view;
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1265
1643
|
// Auto-install as a preload (`--require`); strict refusal also applies without twin URLs.
|
|
1266
1644
|
install();
|
|
1267
1645
|
installWorldTime();
|
|
1268
1646
|
installPrismaAdapterShim();
|
|
1647
|
+
installMiniflareOutbound();
|