@volter/world-core 3.0.31 → 3.0.32
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/browser.js +65 -3
- package/package.json +1 -1
- package/src/browser.ts +52 -3
package/dist/src/browser.js
CHANGED
|
@@ -3,6 +3,67 @@ import { assertBrowserName, browserOrigin, validateBrowserState } from "./browse
|
|
|
3
3
|
import { nodeBuiltin } from "./serve-http.js";
|
|
4
4
|
import { worldNow } from "./world-clock.js";
|
|
5
5
|
export { cookieMatchesHost } from "./browser-state.js";
|
|
6
|
+
/** Undo the request's path mount before its answer reaches a vendor-addressed client. */
|
|
7
|
+
function vendorResponse(response, vendor, twin, mount) {
|
|
8
|
+
if (!mount)
|
|
9
|
+
return response;
|
|
10
|
+
const pathAtVendor = (path) => path === mount ? '/' : path.startsWith(`${mount}/`) ? path.slice(mount.length) : path;
|
|
11
|
+
const urlAtVendor = (value) => {
|
|
12
|
+
if (!value.trim())
|
|
13
|
+
return value;
|
|
14
|
+
let at;
|
|
15
|
+
try {
|
|
16
|
+
at = new URL(value, twin);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
if (at.origin !== twin.origin && at.origin !== vendor.origin)
|
|
22
|
+
return value;
|
|
23
|
+
const path = pathAtVendor(at.pathname);
|
|
24
|
+
if (path === at.pathname)
|
|
25
|
+
return value;
|
|
26
|
+
const tail = `${path}${at.search}${at.hash}`;
|
|
27
|
+
return /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(value.trim()) ? `${vendor.origin}${tail}` : tail;
|
|
28
|
+
};
|
|
29
|
+
const headers = new Headers(response.headers);
|
|
30
|
+
let changed = false;
|
|
31
|
+
const cookies = response.headers.getSetCookie();
|
|
32
|
+
const mapped = cookies.map(cookie => cookie.replace(/(;\s*path\s*=)([^;]*)/gi, (attribute, lead, path) => {
|
|
33
|
+
const at = pathAtVendor(path.trim());
|
|
34
|
+
return at === path.trim() ? attribute : `${lead}${at}`;
|
|
35
|
+
}));
|
|
36
|
+
if (mapped.some((cookie, i) => cookie !== cookies[i])) {
|
|
37
|
+
headers.delete('set-cookie');
|
|
38
|
+
for (const cookie of mapped)
|
|
39
|
+
headers.append('set-cookie', cookie);
|
|
40
|
+
changed = true;
|
|
41
|
+
}
|
|
42
|
+
for (const name of ['location', 'content-location']) {
|
|
43
|
+
const value = headers.get(name);
|
|
44
|
+
if (value === null)
|
|
45
|
+
continue;
|
|
46
|
+
const at = urlAtVendor(value);
|
|
47
|
+
if (at !== value) {
|
|
48
|
+
headers.set(name, at);
|
|
49
|
+
changed = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const refresh = headers.get('refresh');
|
|
53
|
+
if (refresh !== null) {
|
|
54
|
+
const parts = /^(\s*\d+(?:\.\d+)?\s*[;,]\s*(?:url\s*=\s*)?)(?:"([^"]*)"|'([^']*)'|([^"']*?))(\s*)$/i.exec(refresh);
|
|
55
|
+
if (parts) {
|
|
56
|
+
const value = parts[2] ?? parts[3] ?? parts[4];
|
|
57
|
+
const at = urlAtVendor(value);
|
|
58
|
+
if (at !== value) {
|
|
59
|
+
const quote = parts[2] !== undefined ? '"' : parts[3] !== undefined ? "'" : '';
|
|
60
|
+
headers.set('refresh', `${parts[1]}${quote}${at}${quote}${parts[5]}`);
|
|
61
|
+
changed = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return changed ? new Response(response.body, { status: response.status, statusText: response.statusText, headers }) : response;
|
|
66
|
+
}
|
|
6
67
|
/** A seed's named browser. Every state write goes through the World's single HTTP loader. */
|
|
7
68
|
export async function browser(name) {
|
|
8
69
|
assertBrowserName(name);
|
|
@@ -91,20 +152,21 @@ export async function browser(name) {
|
|
|
91
152
|
if (!target || target.vendor === 'app')
|
|
92
153
|
throw new Error(`browser ${name}: no twin in this World serves ${url.hostname}${url.pathname}`);
|
|
93
154
|
const at = new URL(target.origin);
|
|
94
|
-
|
|
155
|
+
const mount = at.pathname.replace(/\/+$/, '');
|
|
156
|
+
at.pathname = `${mount}${url.pathname}`;
|
|
95
157
|
at.search = url.search;
|
|
96
158
|
const sent = new Headers(headers);
|
|
97
159
|
const cookies = jar.header(url, new Date(worldNow()).getTime());
|
|
98
160
|
if (cookies)
|
|
99
161
|
sent.set('cookie', cookies);
|
|
100
|
-
sent.set('host',
|
|
162
|
+
sent.set('host', mount ? at.host : url.host);
|
|
101
163
|
sent.set('x-volter-twin-original-host', url.host);
|
|
102
164
|
sent.set('x-forwarded-host', url.host);
|
|
103
165
|
sent.set('x-forwarded-proto', url.protocol.slice(0, -1));
|
|
104
166
|
if (process.env.VOLTER_TWINS_KEY)
|
|
105
167
|
sent.set('x-twins-key', process.env.VOLTER_TWINS_KEY);
|
|
106
168
|
const before = JSON.stringify(snapshot());
|
|
107
|
-
const response = await fetch(at, { method, headers: sent, body, redirect: 'manual', signal: request.signal });
|
|
169
|
+
const response = vendorResponse(await fetch(at, { method, headers: sent, body, redirect: 'manual', signal: request.signal }), url, at, mount);
|
|
108
170
|
for (const cookie of response.headers.getSetCookie())
|
|
109
171
|
jar.set(cookie, url, new Date(worldNow()).getTime());
|
|
110
172
|
if (JSON.stringify(snapshot()) !== before)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.32",
|
|
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",
|
package/src/browser.ts
CHANGED
|
@@ -11,6 +11,54 @@ export type SeedBrowser = {
|
|
|
11
11
|
state(): Promise<BrowserState>;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
/** Undo the request's path mount before its answer reaches a vendor-addressed client. */
|
|
15
|
+
function vendorResponse(response: Response, vendor: URL, twin: URL, mount: string): Response {
|
|
16
|
+
if (!mount) return response;
|
|
17
|
+
const pathAtVendor = (path: string): string => path === mount ? '/' : path.startsWith(`${mount}/`) ? path.slice(mount.length) : path;
|
|
18
|
+
const urlAtVendor = (value: string): string => {
|
|
19
|
+
if (!value.trim()) return value;
|
|
20
|
+
let at: URL;
|
|
21
|
+
try { at = new URL(value, twin); } catch { return value; }
|
|
22
|
+
if (at.origin !== twin.origin && at.origin !== vendor.origin) return value;
|
|
23
|
+
const path = pathAtVendor(at.pathname);
|
|
24
|
+
if (path === at.pathname) return value;
|
|
25
|
+
const tail = `${path}${at.search}${at.hash}`;
|
|
26
|
+
return /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(value.trim()) ? `${vendor.origin}${tail}` : tail;
|
|
27
|
+
};
|
|
28
|
+
const headers = new Headers(response.headers);
|
|
29
|
+
let changed = false;
|
|
30
|
+
const cookies = response.headers.getSetCookie();
|
|
31
|
+
const mapped = cookies.map(cookie => cookie.replace(/(;\s*path\s*=)([^;]*)/gi, (attribute, lead: string, path: string) => {
|
|
32
|
+
const at = pathAtVendor(path.trim());
|
|
33
|
+
return at === path.trim() ? attribute : `${lead}${at}`;
|
|
34
|
+
}));
|
|
35
|
+
if (mapped.some((cookie, i) => cookie !== cookies[i])) {
|
|
36
|
+
headers.delete('set-cookie');
|
|
37
|
+
for (const cookie of mapped) headers.append('set-cookie', cookie);
|
|
38
|
+
changed = true;
|
|
39
|
+
}
|
|
40
|
+
for (const name of ['location', 'content-location']) {
|
|
41
|
+
const value = headers.get(name);
|
|
42
|
+
if (value === null) continue;
|
|
43
|
+
const at = urlAtVendor(value);
|
|
44
|
+
if (at !== value) { headers.set(name, at); changed = true; }
|
|
45
|
+
}
|
|
46
|
+
const refresh = headers.get('refresh');
|
|
47
|
+
if (refresh !== null) {
|
|
48
|
+
const parts = /^(\s*\d+(?:\.\d+)?\s*[;,]\s*(?:url\s*=\s*)?)(?:"([^"]*)"|'([^']*)'|([^"']*?))(\s*)$/i.exec(refresh);
|
|
49
|
+
if (parts) {
|
|
50
|
+
const value = parts[2] ?? parts[3] ?? parts[4]!;
|
|
51
|
+
const at = urlAtVendor(value);
|
|
52
|
+
if (at !== value) {
|
|
53
|
+
const quote = parts[2] !== undefined ? '"' : parts[3] !== undefined ? "'" : '';
|
|
54
|
+
headers.set('refresh', `${parts[1]}${quote}${at}${quote}${parts[5]}`);
|
|
55
|
+
changed = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return changed ? new Response(response.body, { status: response.status, statusText: response.statusText, headers }) : response;
|
|
60
|
+
}
|
|
61
|
+
|
|
14
62
|
/** A seed's named browser. Every state write goes through the World's single HTTP loader. */
|
|
15
63
|
export async function browser(name: string): Promise<SeedBrowser> {
|
|
16
64
|
assertBrowserName(name);
|
|
@@ -83,18 +131,19 @@ export async function browser(name: string): Promise<SeedBrowser> {
|
|
|
83
131
|
const target = await resolve(url);
|
|
84
132
|
if (!target || target.vendor === 'app') throw new Error(`browser ${name}: no twin in this World serves ${url.hostname}${url.pathname}`);
|
|
85
133
|
const at = new URL(target.origin);
|
|
86
|
-
|
|
134
|
+
const mount = at.pathname.replace(/\/+$/, '');
|
|
135
|
+
at.pathname = `${mount}${url.pathname}`;
|
|
87
136
|
at.search = url.search;
|
|
88
137
|
const sent = new Headers(headers);
|
|
89
138
|
const cookies = jar.header(url, new Date(worldNow()).getTime());
|
|
90
139
|
if (cookies) sent.set('cookie', cookies);
|
|
91
|
-
sent.set('host',
|
|
140
|
+
sent.set('host', mount ? at.host : url.host);
|
|
92
141
|
sent.set('x-volter-twin-original-host', url.host);
|
|
93
142
|
sent.set('x-forwarded-host', url.host);
|
|
94
143
|
sent.set('x-forwarded-proto', url.protocol.slice(0, -1));
|
|
95
144
|
if (process.env.VOLTER_TWINS_KEY) sent.set('x-twins-key', process.env.VOLTER_TWINS_KEY);
|
|
96
145
|
const before = JSON.stringify(snapshot());
|
|
97
|
-
const response = await fetch(at, { method, headers: sent, body, redirect: 'manual', signal: request.signal });
|
|
146
|
+
const response = vendorResponse(await fetch(at, { method, headers: sent, body, redirect: 'manual', signal: request.signal }), url, at, mount);
|
|
98
147
|
for (const cookie of response.headers.getSetCookie()) jar.set(cookie, url, new Date(worldNow()).getTime());
|
|
99
148
|
if (JSON.stringify(snapshot()) !== before) await save();
|
|
100
149
|
const location = response.headers.get('location');
|