@volter/world-core 3.0.31 → 3.0.33

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.
@@ -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
- at.pathname = `${at.pathname.replace(/\/$/, '')}${url.pathname}`;
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', new URL(target.origin).pathname.replace(/\/$/, '') ? at.host : url.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.31",
3
+ "version": "3.0.33",
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
- at.pathname = `${at.pathname.replace(/\/$/, '')}${url.pathname}`;
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', new URL(target.origin).pathname.replace(/\/$/, '') ? at.host : url.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');