@volter/world-runtime 3.0.129 → 3.0.130
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/dist/src/redirect-proxy.js +21 -3
- package/package.json +4 -4
- package/src/redirect-proxy.ts +17 -3
|
@@ -288,6 +288,24 @@ export function twinUnreachableMessage(vendor, origin, err, world) {
|
|
|
288
288
|
const cause = err.cause;
|
|
289
289
|
return `twin "${vendor}" at ${origin} is unreachable${world ? ` in World "${world}"` : ''}: ${errorMessage(err)}${cause?.code ? ` (${cause.code})` : cause?.message ? ` (${errorMessage(cause)})` : ''}; check \`volter-world doctor${world ? ` ${world}` : ''}\`\n`;
|
|
290
290
|
}
|
|
291
|
+
/** The headers of one connection, not of the request (RFC 9110 §7.6.1), with `expect`: the hop that received them
|
|
292
|
+
* answered them (Node's server sends the 100 Continue itself). Node's fetch refuses `transfer-encoding`,
|
|
293
|
+
* `keep-alive`, `upgrade`, `expect` and a `connection` other than close/keep-alive outright, so a `git push` whose
|
|
294
|
+
* pack came chunked (larger than `http.postBuffer`) failed `fetch failed (UND_ERR_INVALID_ARG)`; the body streams
|
|
295
|
+
* on and fetch frames it afresh. */
|
|
296
|
+
const HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-connection', 'transfer-encoding', 'te', 'trailer', 'upgrade', 'expect'];
|
|
297
|
+
const TOKEN = /^[!#$%&'*+.^_`|~0-9a-z-]+$/i;
|
|
298
|
+
function forwardableHeaders(incoming) {
|
|
299
|
+
const headers = new Headers(incoming);
|
|
300
|
+
for (const listed of (incoming.get('connection') ?? '').split(',')) {
|
|
301
|
+
const name = listed.trim();
|
|
302
|
+
if (TOKEN.test(name))
|
|
303
|
+
headers.delete(name);
|
|
304
|
+
}
|
|
305
|
+
for (const h of HOP_BY_HOP)
|
|
306
|
+
headers.delete(h);
|
|
307
|
+
return headers;
|
|
308
|
+
}
|
|
291
309
|
/** Start the ambient redirect proxy. Returns a handle with the listen port + the env to export.
|
|
292
310
|
* - Plain `http://` proxied requests to a vendor host → forwarded to the twin.
|
|
293
311
|
* - `CONNECT host:443` to a vendor host → MITM-terminated with a session-signed leaf, decrypted, and
|
|
@@ -489,8 +507,8 @@ export async function startRedirectProxy(options) {
|
|
|
489
507
|
return new Response(note(noTwinMessage(hostHeader, u.pathname)) + refusalText(gap).trimStart(), { status: 502, headers: { 'content-type': 'text/plain; charset=utf-8', [REFUSAL_HEADER]: gap.missing } });
|
|
490
508
|
}
|
|
491
509
|
const origin = new URL(twin.origin);
|
|
492
|
-
const headers =
|
|
493
|
-
for (const h of ['
|
|
510
|
+
const headers = forwardableHeaders(req.headers);
|
|
511
|
+
for (const h of ['accept-encoding', 'x-volter-world-browser'])
|
|
494
512
|
headers.delete(h);
|
|
495
513
|
// the application is not a twin: it gets no World key, keeps its Host and hears it was reached over TLS
|
|
496
514
|
if (twin.vendor === 'app') {
|
|
@@ -521,7 +539,7 @@ export async function startRedirectProxy(options) {
|
|
|
521
539
|
return new Response(upRes.body, { status: upRes.status, headers: out });
|
|
522
540
|
}
|
|
523
541
|
catch (err) {
|
|
524
|
-
return new Response(twinUnreachable(twin.vendor, origin.origin, err), { status: 502 });
|
|
542
|
+
return new Response(twinUnreachable(twin.vendor, origin.origin, err), { status: 502, headers: { 'content-type': 'text/plain; charset=utf-8' } });
|
|
525
543
|
}
|
|
526
544
|
},
|
|
527
545
|
upgrade: websocketRelay(req => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.130",
|
|
4
4
|
"description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -70,14 +70,14 @@
|
|
|
70
70
|
"node": ">=22.3"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@volter/world-core": "3.0.
|
|
74
|
-
"@volter/world-console": "3.0.
|
|
73
|
+
"@volter/world-core": "3.0.130",
|
|
74
|
+
"@volter/world-console": "3.0.125"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"@electric-sql/pglite": "0.5.8",
|
|
78
78
|
"@electric-sql/pglite-pgvector": "0.0.9",
|
|
79
79
|
"@volter/world-access": "3.0.1",
|
|
80
|
-
"@volter/world-core": "3.0.
|
|
80
|
+
"@volter/world-core": "3.0.130",
|
|
81
81
|
"mongodb": "~6.20.0",
|
|
82
82
|
"pg-gateway": "0.3.0-beta.4",
|
|
83
83
|
"smol-toml": "^1.8.0",
|
package/src/redirect-proxy.ts
CHANGED
|
@@ -336,6 +336,20 @@ export function twinUnreachableMessage(vendor: string, origin: string, err: Erro
|
|
|
336
336
|
return `twin "${vendor}" at ${origin} is unreachable${world ? ` in World "${world}"` : ''}: ${errorMessage(err)}${cause?.code ? ` (${cause.code})` : cause?.message ? ` (${errorMessage(cause)})` : ''}; check \`volter-world doctor${world ? ` ${world}` : ''}\`\n`;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/** The headers of one connection, not of the request (RFC 9110 §7.6.1), with `expect`: the hop that received them
|
|
340
|
+
* answered them (Node's server sends the 100 Continue itself). Node's fetch refuses `transfer-encoding`,
|
|
341
|
+
* `keep-alive`, `upgrade`, `expect` and a `connection` other than close/keep-alive outright, so a `git push` whose
|
|
342
|
+
* pack came chunked (larger than `http.postBuffer`) failed `fetch failed (UND_ERR_INVALID_ARG)`; the body streams
|
|
343
|
+
* on and fetch frames it afresh. */
|
|
344
|
+
const HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-connection', 'transfer-encoding', 'te', 'trailer', 'upgrade', 'expect'];
|
|
345
|
+
const TOKEN = /^[!#$%&'*+.^_`|~0-9a-z-]+$/i;
|
|
346
|
+
function forwardableHeaders(incoming: Headers): Headers {
|
|
347
|
+
const headers = new Headers(incoming);
|
|
348
|
+
for (const listed of (incoming.get('connection') ?? '').split(',')) { const name = listed.trim(); if (TOKEN.test(name)) headers.delete(name); }
|
|
349
|
+
for (const h of HOP_BY_HOP) headers.delete(h);
|
|
350
|
+
return headers;
|
|
351
|
+
}
|
|
352
|
+
|
|
339
353
|
/** Start the ambient redirect proxy. Returns a handle with the listen port + the env to export.
|
|
340
354
|
* - Plain `http://` proxied requests to a vendor host → forwarded to the twin.
|
|
341
355
|
* - `CONNECT host:443` to a vendor host → MITM-terminated with a session-signed leaf, decrypted, and
|
|
@@ -503,8 +517,8 @@ export async function startRedirectProxy(options: RedirectProxyOptions): Promise
|
|
|
503
517
|
}
|
|
504
518
|
const origin = new URL(twin.origin);
|
|
505
519
|
|
|
506
|
-
const headers =
|
|
507
|
-
for (const h of ['
|
|
520
|
+
const headers = forwardableHeaders(req.headers);
|
|
521
|
+
for (const h of ['accept-encoding', 'x-volter-world-browser']) headers.delete(h);
|
|
508
522
|
// the application is not a twin: it gets no World key, keeps its Host and hears it was reached over TLS
|
|
509
523
|
if (twin.vendor === 'app') {
|
|
510
524
|
headers.delete('x-twins-key');
|
|
@@ -529,7 +543,7 @@ export async function startRedirectProxy(options: RedirectProxyOptions): Promise
|
|
|
529
543
|
for (const h of ['content-encoding', 'content-length', 'transfer-encoding', 'connection', 'keep-alive']) out.delete(h);
|
|
530
544
|
return new Response(upRes.body, { status: upRes.status, headers: out });
|
|
531
545
|
} catch (err) {
|
|
532
|
-
return new Response(twinUnreachable(twin.vendor, origin.origin, err as Error), { status: 502 });
|
|
546
|
+
return new Response(twinUnreachable(twin.vendor, origin.origin, err as Error), { status: 502, headers: { 'content-type': 'text/plain; charset=utf-8' } });
|
|
533
547
|
}
|
|
534
548
|
},
|
|
535
549
|
upgrade: websocketRelay(req => {
|