@volter/world-runtime 3.0.162 → 3.0.163

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.
@@ -1883,20 +1883,23 @@ export class WorldDoors {
1883
1883
  // a browser session is opened by either token: the read token's is how a view-only link opens a vendor's screens
1884
1884
  // the session gate comes first: a page that holds only the cookie (a sibling World's, the same site) can neither
1885
1885
  // re-issue nor end the session through this door
1886
- // A HOSTED SITE OPENED (`GET sites/go?host=<hostname>&land=<path>`): a person's navigation, handed to the site's own
1887
- // address with a one-time read pass. A navigation from a sibling address (the site's own page, linking back here) is
1888
- // taken too: it only opens, in this person's own browser, a read of this World's own site
1889
- if (kind === 'sites' && name === 'go' && request.method === 'GET') {
1886
+ // A HOSTED SITE OPENED (`sites/go?host=<hostname>&land=<path>`): GET hands a navigation to the site's own address;
1887
+ // POST gives an authenticated console click that same opening URL, with the same scoped one-time pages pass.
1888
+ // A GET navigation from a sibling site may open a read; a POST handoff comes only from the console's own origin.
1889
+ if (kind === 'sites' && name === 'go' && (request.method === 'GET' || request.method === 'POST')) {
1890
1890
  const fetchSite = request.headers.get('sec-fetch-site');
1891
1891
  const dest = request.headers.get('sec-fetch-dest');
1892
+ const handoff = request.method === 'POST';
1893
+ if (handoff && fetchSite !== 'same-origin')
1894
+ return Response.json({ error: "a site opening URL is requested only from the World's own page (Sec-Fetch-Site: same-origin)" }, { status: 403 });
1892
1895
  const browser = url.searchParams.get('browser') ?? undefined;
1893
1896
  if (browser && fetchSite !== 'same-origin' && fetchSite !== 'none')
1894
1897
  return Response.json({ error: 'a browser site opens only from its World or a typed address (Sec-Fetch-Site: same-origin or none)' }, { status: 403 });
1895
1898
  // a person's navigation (from here, from the site's own page linking back, or typed in), or a frame of this World's
1896
1899
  // own console (its board, its comparison)
1897
- const navigated = (request.method === 'GET' || request.method === 'HEAD') && dest === 'document' && ['same-origin', 'same-site', 'none'].includes(fetchSite ?? '');
1900
+ const navigated = request.method === 'GET' && dest === 'document' && ['same-origin', 'same-site', 'none'].includes(fetchSite ?? '');
1898
1901
  const framed = fetchSite === 'same-origin' && dest === 'iframe';
1899
- if (via === 'session' && !navigated && !framed)
1902
+ if (via === 'session' && !handoff && !navigated && !framed)
1900
1903
  return Response.json({ error: "a hosted site opens on a person's navigation" }, { status: 403 });
1901
1904
  const template = this.host.sitesTemplate?.() ?? null;
1902
1905
  const hostname = (url.searchParams.get('host') ?? '').toLowerCase();
@@ -1924,7 +1927,9 @@ export class WorldDoors {
1924
1927
  const passed = this.mintPagesPass(browser ? scope : 'read', { site: hostname, ...(browser ? { browser } : {}), ...(asked.session ? { session: asked.session } : {}), ...(asked.who ? { who: asked.who } : {}), ...(this.host.browserOrigin?.() ? { framer: this.host.browserOrigin() } : {}) });
1925
1928
  if (!passed)
1926
1929
  return Response.json({ error: 'too many sites opened at once: try again in a minute' }, { status: 429 });
1927
- return new Response(null, { status: 303, headers: { location: `${at}/-/${this.served}/pages/open#${new URLSearchParams({ pass: passed, land: land.startsWith('/') ? land : '/' }).toString()}`, 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' } });
1930
+ const to = `${at}/-/${this.served}/pages/open#${new URLSearchParams({ pass: passed, land: land.startsWith('/') ? land : '/' }).toString()}`;
1931
+ const headers = { 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' };
1932
+ return handoff ? Response.json({ url: to }, { headers }) : new Response(null, { status: 303, headers: { ...headers, location: to } });
1928
1933
  }
1929
1934
  const foreign = this.crossOriginSession(request, via);
1930
1935
  if (foreign)
@@ -29,8 +29,9 @@ export type ServiceEnd = {
29
29
  * which has no runs to tell apart. A run is bounded by its start record or, for a spawn that failed before starting
30
30
  * or a log from before start records, by the previous run's exit. */
31
31
  export declare function readServiceEnd(log: string): ServiceEnd | undefined;
32
- /** One line for a service that is not live: how it ended, who stopped it, when, and after how long. */
33
- export declare function describeServiceEnd(end: ServiceEnd): string;
32
+ /** One line for a service that is not live: how it ended, who stopped it, when, and after how long.
33
+ * A caller with only an exit event cannot attribute its stop without the run's stop history. */
34
+ export declare function describeServiceEnd(end: ServiceEnd, stopContext?: 'known' | 'unknown'): string;
34
35
  export type WorldEvent = {
35
36
  event: string;
36
37
  service?: string;
@@ -139,8 +139,9 @@ const duration = (ms) => {
139
139
  const s = Math.round(ms / 1000);
140
140
  return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}s` : `${Math.floor(s / 3600)}h${String(Math.floor((s % 3600) / 60)).padStart(2, '0')}m`;
141
141
  };
142
- /** One line for a service that is not live: how it ended, who stopped it, when, and after how long. */
143
- export function describeServiceEnd(end) {
142
+ /** One line for a service that is not live: how it ended, who stopped it, when, and after how long.
143
+ * A caller with only an exit event cannot attribute its stop without the run's stop history. */
144
+ export function describeServiceEnd(end, stopContext = 'known') {
144
145
  const exit = end.exited;
145
146
  // A log from before exit records split code from signal: a status past 128 is 128 + the signal's number.
146
147
  if (exit && exit.code === undefined && exit.signal === undefined && exit.status > 128)
@@ -157,8 +158,9 @@ export function describeServiceEnd(end) {
157
158
  const ended = exit.signal ? `ended by ${exit.signal}` : `exit code ${exit.code ?? exit.status}`;
158
159
  const how = exit.error ? `failed to spawn (${exit.error})`
159
160
  : exit.stopRequested && end.stopped ? `was stopped by the World (${end.stopped.by}; ${ended})`
160
- : exit.stopRequested ? `was stopped by a ${exit.stopRequested} from outside the World (no World stop was recorded; ${ended})`
161
- : exit.signal ? `was killed by ${exit.signal}` : `exited with code ${exit.code ?? exit.status}`;
161
+ : exit.stopRequested && stopContext === 'unknown' ? `stopped after a ${exit.stopRequested} request (${ended})`
162
+ : exit.stopRequested ? `was stopped by a ${exit.stopRequested} from outside the World (no World stop was recorded; ${ended})`
163
+ : exit.signal ? `was killed by ${exit.signal}` : `exited with code ${exit.code ?? exit.status}`;
162
164
  const ran = exit.uptimeMs !== undefined ? ` after running ${duration(exit.uptimeMs)}` : '';
163
165
  return `${how}${ran}, at ${exit.at}`;
164
166
  }
@@ -181,7 +183,7 @@ export function describeWorldEvent(event) {
181
183
  if (event.event === 'world.service.started')
182
184
  return `${event.at} ${who} started (pid ${event.pid})`;
183
185
  if (event.event === 'world.service.exited')
184
- return `${event.at} ${who} ${describeServiceEnd({ exited: event, lastOutput: [] }).replace(/, at [^,]*$/, '')}`;
186
+ return `${event.at} ${who} ${describeServiceEnd({ exited: event, lastOutput: [] }, 'unknown').replace(/, at [^,]*$/, '')}`;
185
187
  if (event.event === 'world.service.stop')
186
188
  return `${event.at} ${who} stopped by the World (${event.by}, ${event.signal})`;
187
189
  if (event.event === 'world.service.killed')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "3.0.162",
3
+ "version": "3.0.163",
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,8 +70,8 @@
70
70
  "node": ">=22.3"
71
71
  },
72
72
  "peerDependencies": {
73
- "@volter/world-core": "3.0.154",
74
- "@volter/world-console": "3.0.150"
73
+ "@volter/world-core": "3.0.155",
74
+ "@volter/world-console": "3.0.151"
75
75
  },
76
76
  "dependencies": {
77
77
  "@electric-sql/pglite": "0.5.8",
@@ -80,7 +80,7 @@
80
80
  "@volter/browser-runtime": "0.1.104",
81
81
  "@volter/browser-sandboxes": "0.1.84",
82
82
  "@volter/world-access": "3.0.1",
83
- "@volter/world-core": "3.0.154",
83
+ "@volter/world-core": "3.0.155",
84
84
  "modern-screenshot": "4.7.0",
85
85
  "mongodb": "~6.20.0",
86
86
  "pg-gateway": "0.3.0-beta.4",
@@ -1630,19 +1630,21 @@ export class WorldDoors {
1630
1630
  // a browser session is opened by either token: the read token's is how a view-only link opens a vendor's screens
1631
1631
  // the session gate comes first: a page that holds only the cookie (a sibling World's, the same site) can neither
1632
1632
  // re-issue nor end the session through this door
1633
- // A HOSTED SITE OPENED (`GET sites/go?host=<hostname>&land=<path>`): a person's navigation, handed to the site's own
1634
- // address with a one-time read pass. A navigation from a sibling address (the site's own page, linking back here) is
1635
- // taken too: it only opens, in this person's own browser, a read of this World's own site
1636
- if (kind === 'sites' && name === 'go' && request.method === 'GET') {
1633
+ // A HOSTED SITE OPENED (`sites/go?host=<hostname>&land=<path>`): GET hands a navigation to the site's own address;
1634
+ // POST gives an authenticated console click that same opening URL, with the same scoped one-time pages pass.
1635
+ // A GET navigation from a sibling site may open a read; a POST handoff comes only from the console's own origin.
1636
+ if (kind === 'sites' && name === 'go' && (request.method === 'GET' || request.method === 'POST')) {
1637
1637
  const fetchSite = request.headers.get('sec-fetch-site');
1638
1638
  const dest = request.headers.get('sec-fetch-dest');
1639
+ const handoff = request.method === 'POST';
1640
+ if (handoff && fetchSite !== 'same-origin') return Response.json({ error: "a site opening URL is requested only from the World's own page (Sec-Fetch-Site: same-origin)" }, { status: 403 });
1639
1641
  const browser = url.searchParams.get('browser') ?? undefined;
1640
1642
  if (browser && fetchSite !== 'same-origin' && fetchSite !== 'none') return Response.json({ error: 'a browser site opens only from its World or a typed address (Sec-Fetch-Site: same-origin or none)' }, { status: 403 });
1641
1643
  // a person's navigation (from here, from the site's own page linking back, or typed in), or a frame of this World's
1642
1644
  // own console (its board, its comparison)
1643
- const navigated = (request.method === 'GET' || request.method === 'HEAD') && dest === 'document' && ['same-origin', 'same-site', 'none'].includes(fetchSite ?? '');
1645
+ const navigated = request.method === 'GET' && dest === 'document' && ['same-origin', 'same-site', 'none'].includes(fetchSite ?? '');
1644
1646
  const framed = fetchSite === 'same-origin' && dest === 'iframe';
1645
- if (via === 'session' && !navigated && !framed) return Response.json({ error: "a hosted site opens on a person's navigation" }, { status: 403 });
1647
+ if (via === 'session' && !handoff && !navigated && !framed) return Response.json({ error: "a hosted site opens on a person's navigation" }, { status: 403 });
1646
1648
  const template = this.host.sitesTemplate?.() ?? null;
1647
1649
  const hostname = (url.searchParams.get('host') ?? '').toLowerCase();
1648
1650
  if (browser) {
@@ -1660,7 +1662,9 @@ export class WorldDoors {
1660
1662
  const land = url.searchParams.get('land') ?? '/';
1661
1663
  const passed = this.mintPagesPass(browser ? scope : 'read', { site: hostname, ...(browser ? { browser } : {}), ...(asked.session ? { session: asked.session } : {}), ...(asked.who ? { who: asked.who } : {}), ...(this.host.browserOrigin?.() ? { framer: this.host.browserOrigin!()! } : {}) });
1662
1664
  if (!passed) return Response.json({ error: 'too many sites opened at once: try again in a minute' }, { status: 429 });
1663
- return new Response(null, { status: 303, headers: { location: `${at}/-/${this.served}/pages/open#${new URLSearchParams({ pass: passed, land: land.startsWith('/') ? land : '/' }).toString()}`, 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' } });
1665
+ const to = `${at}/-/${this.served}/pages/open#${new URLSearchParams({ pass: passed, land: land.startsWith('/') ? land : '/' }).toString()}`;
1666
+ const headers = { 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' };
1667
+ return handoff ? Response.json({ url: to }, { headers }) : new Response(null, { status: 303, headers: { ...headers, location: to } });
1664
1668
  }
1665
1669
  const foreign = this.crossOriginSession(request, via);
1666
1670
  if (foreign) return foreign;
@@ -129,8 +129,9 @@ const duration = (ms: number): string => {
129
129
  return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}s` : `${Math.floor(s / 3600)}h${String(Math.floor((s % 3600) / 60)).padStart(2, '0')}m`;
130
130
  };
131
131
 
132
- /** One line for a service that is not live: how it ended, who stopped it, when, and after how long. */
133
- export function describeServiceEnd(end: ServiceEnd): string {
132
+ /** One line for a service that is not live: how it ended, who stopped it, when, and after how long.
133
+ * A caller with only an exit event cannot attribute its stop without the run's stop history. */
134
+ export function describeServiceEnd(end: ServiceEnd, stopContext: 'known' | 'unknown' = 'known'): string {
134
135
  const exit = end.exited;
135
136
  // A log from before exit records split code from signal: a status past 128 is 128 + the signal's number.
136
137
  if (exit && exit.code === undefined && exit.signal === undefined && exit.status > 128) exit.signal = `signal ${exit.status - 128}`;
@@ -144,6 +145,7 @@ export function describeServiceEnd(end: ServiceEnd): string {
144
145
  const ended = exit.signal ? `ended by ${exit.signal}` : `exit code ${exit.code ?? exit.status}`;
145
146
  const how = exit.error ? `failed to spawn (${exit.error})`
146
147
  : exit.stopRequested && end.stopped ? `was stopped by the World (${end.stopped.by}; ${ended})`
148
+ : exit.stopRequested && stopContext === 'unknown' ? `stopped after a ${exit.stopRequested} request (${ended})`
147
149
  : exit.stopRequested ? `was stopped by a ${exit.stopRequested} from outside the World (no World stop was recorded; ${ended})`
148
150
  : exit.signal ? `was killed by ${exit.signal}` : `exited with code ${exit.code ?? exit.status}`;
149
151
  const ran = exit.uptimeMs !== undefined ? ` after running ${duration(exit.uptimeMs)}` : '';
@@ -162,7 +164,7 @@ export function readWorldEvents(path: string): WorldEvent[] {
162
164
  export function describeWorldEvent(event: WorldEvent): string {
163
165
  const who = event.service ?? '(world)';
164
166
  if (event.event === 'world.service.started') return `${event.at} ${who} started (pid ${event.pid})`;
165
- if (event.event === 'world.service.exited') return `${event.at} ${who} ${describeServiceEnd({ exited: event as unknown as ServiceExitRecord, lastOutput: [] }).replace(/, at [^,]*$/, '')}`;
167
+ if (event.event === 'world.service.exited') return `${event.at} ${who} ${describeServiceEnd({ exited: event as unknown as ServiceExitRecord, lastOutput: [] }, 'unknown').replace(/, at [^,]*$/, '')}`;
166
168
  if (event.event === 'world.service.stop') return `${event.at} ${who} stopped by the World (${event.by}, ${event.signal})`;
167
169
  if (event.event === 'world.service.killed') return `${event.at} ${who} killed by the World (${event.by}, SIGKILL after the grace)`;
168
170
  return `${event.at} ${who} ${event.event}`;