@volter/world-runtime 2.0.36 → 2.0.37

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.
@@ -21,5 +21,7 @@ export declare function serveWorldView(name: string, opts?: {
21
21
  host?: string;
22
22
  console?: ViewConsole;
23
23
  announce?: (line: string) => void; /** each World a browser steps into at an origin of its own (on by default on loopback) */
24
- origins?: boolean;
24
+ origins?: boolean; /** the World's own origin as a browser reaches it through a proxy on this machine (a desktop serving it as an app):
25
+ * a request the proxy forwards from that host (X-Forwarded-Host) is answered as at that origin, its local session included */
26
+ origin?: string;
25
27
  }): Promise<WorldView>;
@@ -39,7 +39,15 @@ export async function serveWorldView(name, opts = {}) {
39
39
  // a non-loopback --host turns origins off by default: say so (the World and its branches then share one origin)
40
40
  if (opts.origins === undefined && !origins)
41
41
  opts.announce?.(`origins off (bound to ${host}): the World and its branches share one origin, where a browser session is only path-scoped; bind loopback for an origin per World`);
42
- const browserOrigin = origins ? (served) => (port ? localWorldOrigin(served, port) : null) : undefined;
42
+ const proxied = opts.origin ? new URL(opts.origin) : null;
43
+ if (proxied && (proxied.origin !== opts.origin || !/^https?:$/.test(proxied.protocol)))
44
+ throw new Error(`--origin is an origin (http(s)://host[:port]), got ${opts.origin}`);
45
+ if (proxied && !(loopbackBind && origins))
46
+ throw new Error('--origin is for a World served on loopback with origins on, behind a proxy on this machine');
47
+ // the World itself at the proxy's origin; its branches keep their own *.localhost origins
48
+ // (the first World mounted is the World; every later one is a branch of it)
49
+ let base = null;
50
+ const browserOrigin = origins ? (served) => (proxied && (base === null || served === base) ? proxied.origin : port ? localWorldOrigin(served, port) : null) : undefined;
43
51
  const mountAt = (at) => mountWorld(loadWorldConfig(join(at, stateDirName(), 'world.json'), at).config.id, { root: at, branches: (served) => branches.doorsFor(served), ...(browserOrigin ? { browserOrigin } : {}), ...(local ? { localTrust: true } : {}) });
44
52
  const branches = new LocalBranches({ dir: join(root, stateDirName(), 'branches'), origin: () => loopback, worlds, mount: mountAt, ...(opts.announce ? { announce: opts.announce } : {}) });
45
53
  // THE WORLD'S OWN BRANCHES, one served at a time: serving another stops this one's twins and starts that one's under the
@@ -84,17 +92,25 @@ export async function serveWorldView(name, opts = {}) {
84
92
  };
85
93
  const mountOpts = { root, branches: (served) => branches.doorsFor(served), ...(browserOrigin ? { browserOrigin } : {}), ...(local ? { localTrust: true } : {}), serveBranch };
86
94
  let world = await mountWorld(name, mountOpts);
95
+ base = world.served;
87
96
  worlds.set(world.served, world);
88
97
  const server = await serveHttp({
89
98
  // a pushed blob (a release video) is one PUT: bodies up to 1 GiB, as `serve` takes them
90
99
  hostname: host, port: opts.port ?? 0, maxRequestBodySize: 1024 * 1024 * 1024,
91
100
  // this front serves a World and its branches; each World's own twins journal their requests
92
101
  twinRequestJournal: false,
93
- async fetch(request) {
102
+ async fetch(incoming) {
103
+ // forwarded from the proxy's host: answered as at the World's own origin there (only that exact host; the proxy is
104
+ // on this machine, as everything that reaches a loopback front is)
105
+ const addressed = new URL(incoming.url).hostname.replace(/^\[|\]$/g, '');
106
+ const fromProxy = proxied && incoming.headers.get('x-forwarded-host') === proxied.host && ['localhost', '127.0.0.1', '::1'].includes(addressed);
107
+ const request = fromProxy
108
+ ? new Request(`${proxied.origin}${new URL(incoming.url).pathname}${new URL(incoming.url).search}`, incoming)
109
+ : incoming;
94
110
  const at = new URL(request.url);
95
111
  const path = at.pathname;
96
112
  // a World's own origin serves that World (and the ping and the console) and no other
97
- const own = origins ? worldOfHost(at.hostname, worlds.keys()) : undefined;
113
+ const own = origins ? (proxied && at.host === proxied.host ? base : worldOfHost(at.hostname, worlds.keys())) : undefined;
98
114
  if (own === null)
99
115
  return Response.json({ error: `no world at ${at.host}` }, { status: 404 });
100
116
  // the bare address, typed into a browser, is the World's page: at its own origin when it has one
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "2.0.36",
3
+ "version": "2.0.37",
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",
@@ -68,7 +68,7 @@
68
68
  },
69
69
  "peerDependencies": {
70
70
  "@volter/world-core": "2.0.21",
71
- "@volter/world-console": "2.0.12"
71
+ "@volter/world-console": "2.0.13"
72
72
  },
73
73
  "dependencies": {
74
74
  "@electric-sql/pglite": "0.5.8",
package/src/world-view.ts CHANGED
@@ -28,7 +28,8 @@ function branchRoots(dir: string): string[] {
28
28
  return out.sort();
29
29
  }
30
30
 
31
- export async function serveWorldView(name: string, opts: { root?: string; port?: number; host?: string; console?: ViewConsole; announce?: (line: string) => void; /** each World a browser steps into at an origin of its own (on by default on loopback) */ origins?: boolean } = {}): Promise<WorldView> {
31
+ export async function serveWorldView(name: string, opts: { root?: string; port?: number; host?: string; console?: ViewConsole; announce?: (line: string) => void; /** each World a browser steps into at an origin of its own (on by default on loopback) */ origins?: boolean; /** the World's own origin as a browser reaches it through a proxy on this machine (a desktop serving it as an app):
32
+ * a request the proxy forwards from that host (X-Forwarded-Host) is answered as at that origin, its local session included */ origin?: string } = {}): Promise<WorldView> {
32
33
  const root = resolve(opts.root ?? process.cwd());
33
34
  const host = opts.host ?? '127.0.0.1';
34
35
  const worlds = new Map<string, MountedWorld>();
@@ -41,7 +42,13 @@ export async function serveWorldView(name: string, opts: { root?: string; port?:
41
42
  const local = loopbackBind && origins;
42
43
  // a non-loopback --host turns origins off by default: say so (the World and its branches then share one origin)
43
44
  if (opts.origins === undefined && !origins) opts.announce?.(`origins off (bound to ${host}): the World and its branches share one origin, where a browser session is only path-scoped; bind loopback for an origin per World`);
44
- const browserOrigin = origins ? (served: string) => (port ? localWorldOrigin(served, port) : null) : undefined;
45
+ const proxied = opts.origin ? new URL(opts.origin) : null;
46
+ if (proxied && (proxied.origin !== opts.origin || !/^https?:$/.test(proxied.protocol))) throw new Error(`--origin is an origin (http(s)://host[:port]), got ${opts.origin}`);
47
+ if (proxied && !(loopbackBind && origins)) throw new Error('--origin is for a World served on loopback with origins on, behind a proxy on this machine');
48
+ // the World itself at the proxy's origin; its branches keep their own *.localhost origins
49
+ // (the first World mounted is the World; every later one is a branch of it)
50
+ let base: string | null = null;
51
+ const browserOrigin = origins ? (served: string) => (proxied && (base === null || served === base) ? proxied.origin : port ? localWorldOrigin(served, port) : null) : undefined;
45
52
  const mountAt = (at: string): Promise<MountedWorld> => mountWorld(loadWorldConfig(join(at, stateDirName(), 'world.json'), at).config.id, { root: at, branches: (served) => branches.doorsFor(served), ...(browserOrigin ? { browserOrigin } : {}), ...(local ? { localTrust: true } : {}) });
46
53
  const branches: LocalBranches = new LocalBranches({ dir: join(root, stateDirName(), 'branches'), origin: () => loopback, worlds, mount: mountAt, ...(opts.announce ? { announce: opts.announce } : {}) });
47
54
  // THE WORLD'S OWN BRANCHES, one served at a time: serving another stops this one's twins and starts that one's under the
@@ -75,16 +82,24 @@ export async function serveWorldView(name: string, opts: { root?: string; port?:
75
82
  };
76
83
  const mountOpts = { root, branches: (served: string) => branches.doorsFor(served), ...(browserOrigin ? { browserOrigin } : {}), ...(local ? { localTrust: true } : {}), serveBranch };
77
84
  let world = await mountWorld(name, mountOpts);
85
+ base = world.served;
78
86
  worlds.set(world.served, world);
79
87
  const server = await serveHttp({
80
88
  // a pushed blob (a release video) is one PUT: bodies up to 1 GiB, as `serve` takes them
81
89
  hostname: host, port: opts.port ?? 0, maxRequestBodySize: 1024 * 1024 * 1024,
82
90
  // this front serves a World and its branches; each World's own twins journal their requests
83
91
  twinRequestJournal: false,
84
- async fetch(request) {
92
+ async fetch(incoming) {
93
+ // forwarded from the proxy's host: answered as at the World's own origin there (only that exact host; the proxy is
94
+ // on this machine, as everything that reaches a loopback front is)
95
+ const addressed = new URL(incoming.url).hostname.replace(/^\[|\]$/g, '');
96
+ const fromProxy = proxied && incoming.headers.get('x-forwarded-host') === proxied.host && ['localhost', '127.0.0.1', '::1'].includes(addressed);
97
+ const request = fromProxy
98
+ ? new Request(`${proxied.origin}${new URL(incoming.url).pathname}${new URL(incoming.url).search}`, incoming)
99
+ : incoming;
85
100
  const at = new URL(request.url); const path = at.pathname;
86
101
  // a World's own origin serves that World (and the ping and the console) and no other
87
- const own = origins ? worldOfHost(at.hostname, worlds.keys()) : undefined;
102
+ const own = origins ? (proxied && at.host === proxied.host ? base! : worldOfHost(at.hostname, worlds.keys())) : undefined;
88
103
  if (own === null) return Response.json({ error: `no world at ${at.host}` }, { status: 404 });
89
104
  // the bare address, typed into a browser, is the World's page: at its own origin when it has one
90
105
  if (opts.console && path === '/' && request.method === 'GET' && (request.headers.get('accept') ?? '').includes('text/html')) {