@volter/world-runtime 2.0.39 → 2.0.41

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.
@@ -84,10 +84,12 @@ export async function branchWorld(base, name, options = {}) {
84
84
  }
85
85
  const forked = {};
86
86
  try {
87
- if (from.origin) {
87
+ // the branch's origin is its base's, or none: never one a stopped World of the same name left
88
+ if (from.origin)
88
89
  instance.origin = { ...from.origin };
89
- saveWorldInstance(instance);
90
- }
90
+ else
91
+ delete instance.origin;
92
+ saveWorldInstance(instance);
91
93
  for (const { service, fromRoot, states } of source) {
92
94
  const toRoot = join(instance.dirs.data, service);
93
95
  for (const { state, generation, view, position: pos } of states) {
@@ -8,6 +8,7 @@ import { join, resolve } from 'node:path';
8
8
  import { getActiveWorldStore, readBranchMeta, withAncestryLock, writeBranchMeta } from '@volter/world-core';
9
9
  import { saveWorldInstance, statusWorld } from "./runtime.js";
10
10
  import { fetchReady, isPathRemote, localWorld, TOKEN_HEADER } from "./served-world.js";
11
+ import { loadWorldConfig, writeWorldConfig } from "./configs.js";
11
12
  function originOf(instance, options) {
12
13
  const url = options.url ?? instance.origin?.url;
13
14
  const namespace = options.namespace ?? instance.origin?.namespace;
@@ -132,6 +133,20 @@ export async function fetchFromOrigin(name, options) {
132
133
  });
133
134
  }
134
135
  saveWorldInstance({ ...instance, origin: { ...origin, ...(options.full ? { clonedAt: at_ } : instance.origin?.clonedAt ? { clonedAt: instance.origin.clonedAt } : {}), fetchedAt: at_ } });
136
+ // a clone names its remote where `volter remote add` does (world.json, as git config holds a remote): one record of
137
+ // it, which every restart carries (runtime's namesOrigin) and `remote remove` ends. Only the World the config is
138
+ // for writes it: a runtime branch boots from its base's config, whose remote stays the base's
139
+ if (options.full && instance.name === instance.config) {
140
+ try {
141
+ const { path: configPath, config } = loadWorldConfig(instance.configPath, root);
142
+ const named = isPathRemote(origin.url) ? origin.url : `${origin.url}/${origin.namespace}`;
143
+ if (config.remotes?.origin !== named)
144
+ writeWorldConfig(configPath, { ...config, remotes: { ...(config.remotes ?? {}), origin: named } });
145
+ }
146
+ catch (error) {
147
+ console.error(`[origin] ${name}: the remote was not named in its config: ${error.message}`);
148
+ }
149
+ }
135
150
  return { world: name, origin, full: options.full ?? false, appended, behind, snapshots };
136
151
  }
137
152
  export function worldOrigin(name, root) {
@@ -728,6 +728,22 @@ function writePidsFromInstance(instance, fresh = []) {
728
728
  ].filter((pid) => Number.isInteger(pid) && pid > 0))];
729
729
  writeOwnedPids(instance.root, instance.name, pids, fresh);
730
730
  }
731
+ /** Whether a world.json remote (`https://<host>/<org>/<world>`, or a world's directory, relative to the World's root) is
732
+ * the recorded origin, read as the CLI's parseOriginUrl reads it (the URL's origin, then its two path parts). */
733
+ function namesOrigin(remote, origin, worldRoot) {
734
+ if (!remote)
735
+ return false;
736
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(remote))
737
+ return resolve(worldRoot, remote) === resolve(worldRoot, origin.url);
738
+ try {
739
+ const parsed = new URL(remote);
740
+ const parts = parsed.pathname.split('/').filter(Boolean);
741
+ return parts.length === 2 && parsed.origin === new URL(origin.url).origin && parts.join('/') === origin.namespace;
742
+ }
743
+ catch {
744
+ return false;
745
+ }
746
+ }
731
747
  export function saveWorldInstance(instance) {
732
748
  writeTextAtomic(instanceFile(instance.root, instance.name), `${JSON.stringify(instance, null, 2)}\n`);
733
749
  }
@@ -1483,6 +1499,7 @@ export async function upWorld(configId, options = {}, abort) {
1483
1499
  const pidsFile = join(instance, 'pids');
1484
1500
  const envFile = resolve(root, options.envFile ?? join(instance, 'world.env'));
1485
1501
  const instanceFile = join(instance, 'instance.json');
1502
+ let carriedOrigin;
1486
1503
  const priorOutputInstance = existsSync(instanceFile) ? JSON.parse(readFileSync(instanceFile, 'utf8')) : undefined;
1487
1504
  const priorOutput = priorOutputInstance?.name === name && resolve(priorOutputInstance.root) === root
1488
1505
  ? { path: resolve(priorOutputInstance.envFile), contents: envFileContents(priorOutputInstance.env) } : undefined;
@@ -1536,6 +1553,14 @@ export async function upWorld(configId, options = {}, abort) {
1536
1553
  || !Number.isFinite(Date.parse(receipt.stoppedAt))) {
1537
1554
  throw new Error(`World "${name}" has no verified teardown receipt for its prior instance; use its owning runtime to complete cleanup before replacement`);
1538
1555
  }
1556
+ // the origin belongs to the World, not to its running state (git: a remote outlives every checkout): an up of the
1557
+ // same config carries the one the last left while world.json's `origin` remote still names it (`remote remove`
1558
+ // ends it there; an instance under another World's config, a runtime branch, has only its own record); its cursors
1559
+ // only when its state (the mirror they count) is kept, as reset drops them
1560
+ if (prior.origin && prior.config === loaded.config.id && (name !== loaded.config.id || namesOrigin(loaded.config.remotes?.origin, prior.origin, root))) {
1561
+ carriedOrigin = options.keepState ? prior.origin
1562
+ : { url: prior.origin.url, namespace: prior.origin.namespace, ...(prior.origin.clonedAt ? { clonedAt: prior.origin.clonedAt } : {}) };
1563
+ }
1539
1564
  }
1540
1565
  assertLifecycleStartable(root, name);
1541
1566
  beginWorldLifecycle(root, name, createdAt, options.owner);
@@ -1643,6 +1668,7 @@ export async function upWorld(configId, options = {}, abort) {
1643
1668
  ...(loaded.config.actors ? { actors: loaded.config.actors } : {}),
1644
1669
  ...(loaded.config.fixtures ? { fixtures: loaded.config.fixtures } : {}),
1645
1670
  lifecycle: { owner: options.owner, log: lifecycleLogPath(root, name) },
1671
+ ...(carriedOrigin ? { origin: carriedOrigin } : {}),
1646
1672
  };
1647
1673
  saveWorldInstance(worldInstance);
1648
1674
  const cancelled = new AbortController();
@@ -268,6 +268,14 @@ export declare class WorldDoors {
268
268
  /** What this World issued, for world-access to decide a grant against: its two tokens, its named keys, its sessions. */
269
269
  private issued;
270
270
  private credential;
271
+ /** Each changeset's actions' receipts, from the landed copies on each twin's log: a deploy appends one per entry it
272
+ * performed, and a push one per entry it landed at the remote (`landed` while the remote waits, beside the
273
+ * changeset's own `applied`). */
274
+ private deployedOf;
275
+ /** Who made a request, as this World names it: `key:<name>` for a named key, `person:<who>` for a browser's session
276
+ * (`person` when it names no one), `token` / `token:read` for the World's own tokens; null when it presented nothing
277
+ * this World issued. */
278
+ private callerOf;
271
279
  /** The session cookie's name where `url` reached this World, or null where a session opens nothing:
272
280
  * on the World's own origin a host-only cookie (`__Host-` over https); without an origin of its own,
273
281
  * the path-scoped cookie a shared origin has always carried. */
@@ -352,6 +360,10 @@ export declare class WorldDoors {
352
360
  * twin, then position. The World keeps no order across twins finer than its clock, and this claims
353
361
  * none. `before` is the cursor the previous page answered; `twin` and `trace` narrow it. */
354
362
  private timeline;
363
+ /** Each twin's callers by the World's span, from its request journal: its newest CALLER_SEGMENTS once, then from
364
+ * where the last read stopped (readTwinRequestJournalFrom follows a rotation). The newest CALLERS_KEPT are kept. */
365
+ private readonly callersByTwin;
366
+ private callersOf;
355
367
  /** Each twin's timeline rows, kept until its logs change: a poll of an unchanged World reads no log
356
368
  * (a twin is read again only when the size or time of its parent log, branch log or branch record
357
369
  * moved), and the merged order is sorted again only when a twin was. */
@@ -26,7 +26,7 @@ import { getActiveWorldStore, withAncestryLock, captureHistory, historyAtInstant
26
26
  // to `.volter/token` and `.volter/token.read` (0600).
27
27
  // One writer per world by construction: the doors run in this process, serialized per twin.
28
28
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
29
- import { assertSafeChangesetName, blobDigest, destinationOf, openSealedAt, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, mirrorShellUnder, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin } from '@volter/world-core';
29
+ import { CALLER_HEADER, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, assertSafeChangesetName, blobDigest, destinationOf, openSealedAt, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, mirrorShellUnder, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin } from '@volter/world-core';
30
30
  import { APP_READ_ENDPOINT_ENV } from "./init.js";
31
31
  import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass } from '@volter/world-access';
32
32
  import { createHash } from 'node:crypto';
@@ -369,6 +369,11 @@ function traceIdOf(traceparent) {
369
369
  const m = typeof traceparent === 'string' ? /^[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$/.exec(traceparent) : null;
370
370
  return m && !/^0+$/.test(m[1]) ? m[1] : null;
371
371
  }
372
+ const CALLERS_KEPT = 50_000;
373
+ /** The closed journal segments a World reads for callers when it first answers a timeline (~1 MB each): older entries show no caller. */
374
+ const CALLER_SEGMENTS = 8;
375
+ /** A W3C traceparent's parent-id (the span the World minted for the request), or null. */
376
+ function spanOf(traceparent) { return parseTraceparent(traceparent)?.parentId ?? null; }
372
377
  /** Timeline order, newest first: by the entry's instant, then twin, then position. */
373
378
  function newerFirst(a, b) {
374
379
  return b.at.localeCompare(a.at) || b.twin.localeCompare(a.twin) || b.position - a.position;
@@ -653,6 +658,49 @@ export class WorldDoors {
653
658
  return { scope: grant.scope, via: 'session', token: null, session: grant.session, ...(grant.who ? { who: grant.who } : {}) };
654
659
  return { scope: grant.scope, via: grant.via === 'key' ? 'key' : 'token', token: grant.presented ?? null };
655
660
  }
661
+ /** Each changeset's actions' receipts, from the landed copies on each twin's log: a deploy appends one per entry it
662
+ * performed, and a push one per entry it landed at the remote (`landed` while the remote waits, beside the
663
+ * changeset's own `applied`). */
664
+ deployedOf(changesets) {
665
+ // by the state each action was written in (a twin may hold several), and a settled receipt wins over a provisional
666
+ // `landed` one whichever comes later in the log (a push's local copy, then the remote's deploy fetched in)
667
+ const landed = new Map();
668
+ const of = (service, state) => {
669
+ const at = `${service}\u0000${state}`;
670
+ let held = landed.get(at);
671
+ if (!held) {
672
+ held = new Map();
673
+ if (this.up().services[service]) {
674
+ for (const e of parentEntries(state, this.controlRoot(service))) {
675
+ if (!e.landsId || !e.receipt)
676
+ continue;
677
+ const before = held.get(e.landsId);
678
+ if (!before || before.status === 'landed' || e.receipt.status !== 'landed')
679
+ held.set(e.landsId, e.receipt);
680
+ }
681
+ }
682
+ landed.set(at, held);
683
+ }
684
+ return held;
685
+ };
686
+ return changesets.map((c) => c.actions.flatMap((a) => { const r = of(a.service, a.stateService ?? this.stateOf(a.service)).get(a.action.id); return r ? [{ service: a.service, actionId: a.action.id, ...r }] : []; }));
687
+ }
688
+ /** Who made a request, as this World names it: `key:<name>` for a named key, `person:<who>` for a browser's session
689
+ * (`person` when it names no one), `token` / `token:read` for the World's own tokens; null when it presented nothing
690
+ * this World issued. */
691
+ callerOf(request) {
692
+ const cred = this.credential(request);
693
+ if (cred.scope === null)
694
+ return null;
695
+ if (cred.via === 'session')
696
+ return cred.who ? `person:${cred.who}` : 'person';
697
+ if (cred.via === 'key') {
698
+ const token = cred.token;
699
+ const held = token ? this.keys().find((k) => sameSecret(keyHash(token), k.hash)) : undefined;
700
+ return held ? `key:${held.name}` : 'key';
701
+ }
702
+ return cred.scope === 'read' ? 'token:read' : 'token';
703
+ }
656
704
  /** The session cookie's name where `url` reached this World, or null where a session opens nothing:
657
705
  * on the World's own origin a host-only cookie (`__Host-` over https); without an origin of its own,
658
706
  * the path-scoped cookie a shared origin has always carried. */
@@ -872,6 +920,14 @@ export class WorldDoors {
872
920
  headers.delete(TOKEN_HEADER);
873
921
  headers.delete(TWINS_KEY_HEADER);
874
922
  headers.delete('host');
923
+ // who made it is the World's word, never the caller's: the twin's request journal records it. The World is a hop of
924
+ // the request's trace: the caller's trace-id goes on (or a new one), under a parent-id the World mints, which the
925
+ // request's log entries carry, so each entry is joined to its caller by an id no caller chose
926
+ headers.delete(CALLER_HEADER);
927
+ const who = this.callerOf(request);
928
+ if (who)
929
+ headers.set(CALLER_HEADER, who);
930
+ headers.set('traceparent', traceparentForDelivery(headers.get('traceparent')) ?? newTraceparent());
875
931
  // at a twin that enforces the read scope, every request of the read token is marked read-only (a GET
876
932
  // that would write for its caller is refused too); at any other, its GET passes as it always has
877
933
  if (enforced)
@@ -942,7 +998,7 @@ export class WorldDoors {
942
998
  // what leaves for the vendor before the seal is applied: no World token in any header it can arrive
943
999
  // under (an SDK's own `Authorization: Bearer` included), no cookie, no routing headers of this World
944
1000
  const caller = new Headers(request.headers);
945
- for (const h of [TOKEN_HEADER, TWINS_KEY_HEADER, TWIN_PREFIX_HEADER, 'host', 'x-forwarded-host', 'x-forwarded-proto', 'cookie'])
1001
+ for (const h of [TOKEN_HEADER, TWINS_KEY_HEADER, TWIN_PREFIX_HEADER, CALLER_HEADER, 'host', 'x-forwarded-host', 'x-forwarded-proto', 'cookie'])
946
1002
  caller.delete(h);
947
1003
  // a secret this World minted (`tok_…`, live, revoked or expired) or one that opens it never leaves
948
1004
  const authorization = caller.get('authorization');
@@ -953,7 +1009,10 @@ export class WorldDoors {
953
1009
  // name, with the credential shape of what the caller sent on. As an audit device does, the request is
954
1010
  // written before the real credential is used, and a request whose audit cannot be written is refused.
955
1011
  const credentials = twinRequestCredentials(caller, url);
956
- if (!journalTwinRequest(name, { phase: 'request', method: request.method, path: `/${rest}`, status: 0, credentials }, this.controlRoot(name))) {
1012
+ const who = this.callerOf(request);
1013
+ const trace = parseTraceparent(request.headers.get('traceparent'))?.traceId;
1014
+ const by = { ...(who ? { caller: who } : {}), ...(trace ? { trace } : {}) };
1015
+ if (!journalTwinRequest(name, { phase: 'request', method: request.method, path: `/${rest}`, status: 0, credentials, ...by }, this.controlRoot(name))) {
957
1016
  return Response.json({ error: `the ${name} link's audit could not be written: nothing was sent` }, { status: 503 });
958
1017
  }
959
1018
  const startedAt = Date.now();
@@ -964,7 +1023,7 @@ export class WorldDoors {
964
1023
  return answer;
965
1024
  }
966
1025
  finally {
967
- journalTwinRequest(name, { method: request.method, path: `/${rest}`, status, ms: Date.now() - startedAt, credentials }, this.controlRoot(name));
1026
+ journalTwinRequest(name, { method: request.method, path: `/${rest}`, status, ms: Date.now() - startedAt, credentials, ...by }, this.controlRoot(name));
968
1027
  }
969
1028
  }
970
1029
  async forwardSealed(request, caller, url, rest, name, origin) {
@@ -1166,9 +1225,11 @@ export class WorldDoors {
1166
1225
  // with the body's hash as it stands, so a reader sees an approval or a verification made on other bytes as stale
1167
1226
  if (name) {
1168
1227
  const l = findWorldChangeset(name, { root: this.worldRoot, world: this.name });
1169
- return Response.json({ ...l.changeset, currentHash: changesetContentHash(l.changeset) });
1228
+ return Response.json({ ...l.changeset, currentHash: changesetContentHash(l.changeset), deployed: this.deployedOf([l.changeset])[0] });
1170
1229
  }
1171
- return Response.json({ changesets: listWorldChangesets({ root: this.worldRoot, world: this.name }).map((l) => ({ ...l.changeset, currentHash: changesetContentHash(l.changeset) })) });
1230
+ const all = listWorldChangesets({ root: this.worldRoot, world: this.name }).map((l) => l.changeset);
1231
+ const deployed = this.deployedOf(all);
1232
+ return Response.json({ changesets: all.map((c, i) => ({ ...c, currentHash: changesetContentHash(c), deployed: deployed[i] })) });
1172
1233
  }
1173
1234
  if (kind === 'changesets' && name && verb === 'verify' && request.method === 'POST') {
1174
1235
  const outcome = await this.serialized('changesets', () => verifyWorldChangeset(name, { root: this.worldRoot, world: this.name, ephemeral: true }));
@@ -1705,7 +1766,33 @@ export class WorldDoors {
1705
1766
  const shown = page.slice(0, limit);
1706
1767
  const last = shown[shown.length - 1];
1707
1768
  const next = more && last ? Buffer.from(JSON.stringify([last.entry.occurredAt, last.twin, last.position])).toString('base64url') : null;
1708
- return Response.json({ world: this.served, entries: shown, next });
1769
+ // who made each entry: the request it was written in (the World's span on it) as the twin's journal recorded it
1770
+ const entries = shown.map((r) => { const by = this.callersOf(r.twin).get(spanOf(r.entry.traceparent) ?? ''); return by ? { ...r, caller: by } : r; });
1771
+ return Response.json({ world: this.served, entries, next });
1772
+ }
1773
+ /** Each twin's callers by the World's span, from its request journal: its newest CALLER_SEGMENTS once, then from
1774
+ * where the last read stopped (readTwinRequestJournalFrom follows a rotation). The newest CALLERS_KEPT are kept. */
1775
+ callersByTwin = new Map();
1776
+ callersOf(twin) {
1777
+ const root = this.controlRoot(twin);
1778
+ let held = this.callersByTwin.get(twin);
1779
+ if (!held || held.root !== root) {
1780
+ held = { root, cursor: null, callers: new Map() };
1781
+ this.callersByTwin.set(twin, held);
1782
+ }
1783
+ const read = readTwinRequestJournalFrom(twin, root, held.cursor, { segments: CALLER_SEGMENTS });
1784
+ for (const e of read.entries)
1785
+ if (e.span && e.caller) {
1786
+ held.callers.delete(e.span);
1787
+ held.callers.set(e.span, e.caller);
1788
+ }
1789
+ held.cursor = read.cursor;
1790
+ for (const span of held.callers.keys()) {
1791
+ if (held.callers.size <= CALLERS_KEPT)
1792
+ break;
1793
+ held.callers.delete(span);
1794
+ }
1795
+ return held.callers;
1709
1796
  }
1710
1797
  /** Each twin's timeline rows, kept until its logs change: a poll of an unchanged World reads no log
1711
1798
  * (a twin is read again only when the size or time of its parent log, branch log or branch record
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "2.0.39",
3
+ "version": "2.0.41",
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",
@@ -67,14 +67,14 @@
67
67
  "node": ">=22.3"
68
68
  },
69
69
  "peerDependencies": {
70
- "@volter/world-core": "2.0.21",
70
+ "@volter/world-core": "2.0.23",
71
71
  "@volter/world-console": "2.0.13"
72
72
  },
73
73
  "dependencies": {
74
74
  "@electric-sql/pglite": "0.5.8",
75
75
  "@electric-sql/pglite-pgvector": "0.0.9",
76
76
  "@volter/world-access": "2.0.2",
77
- "@volter/world-core": "2.0.21",
77
+ "@volter/world-core": "2.0.23",
78
78
  "pg-gateway": "0.3.0-beta.4",
79
79
  "smol-toml": "^1.8.0"
80
80
  },
@@ -84,6 +84,6 @@
84
84
  }
85
85
  },
86
86
  "optionalDependencies": {
87
- "@volter/twin-mongodb": "0.1.21"
87
+ "@volter/twin-mongodb": "0.1.23"
88
88
  }
89
89
  }
package/src/branch.ts CHANGED
@@ -69,7 +69,9 @@ export async function branchWorld(base: string, name: string, options: BranchOpt
69
69
  }
70
70
  const forked: Record<string, string[]> = {};
71
71
  try {
72
- if (from.origin) { instance.origin = { ...from.origin }; saveWorldInstance(instance); }
72
+ // the branch's origin is its base's, or none: never one a stopped World of the same name left
73
+ if (from.origin) instance.origin = { ...from.origin }; else delete instance.origin;
74
+ saveWorldInstance(instance);
73
75
  for (const { service, fromRoot, states } of source) {
74
76
  const toRoot = join(instance.dirs.data, service);
75
77
  for (const { state, generation, view, position: pos } of states) {
package/src/origin.ts CHANGED
@@ -9,6 +9,7 @@ import { appendOriginEntry, appendParentEntry, branchEntries, getActiveWorldStor
9
9
  import { saveWorldInstance, statusWorld } from './runtime.ts';
10
10
  import type { WorldInstance } from './schema.ts';
11
11
  import { fetchReady, isPathRemote, localWorld, TOKEN_HEADER } from './served-world.ts';
12
+ import { loadWorldConfig, writeWorldConfig } from './configs.ts';
12
13
 
13
14
  export type FetchOptions = { root?: string; url?: string; namespace?: string; key: string; full?: boolean; services?: string[]; views?: Record<string, HistoryReference>; now?: Date };
14
15
  export type FetchOutcome = {
@@ -126,6 +127,16 @@ export async function fetchFromOrigin(name: string, options: FetchOptions): Prom
126
127
 
127
128
  }
128
129
  saveWorldInstance({ ...instance, origin: { ...origin, ...(options.full ? { clonedAt: at_ } : instance.origin?.clonedAt ? { clonedAt: instance.origin.clonedAt } : {}), fetchedAt: at_ } });
130
+ // a clone names its remote where `volter remote add` does (world.json, as git config holds a remote): one record of
131
+ // it, which every restart carries (runtime's namesOrigin) and `remote remove` ends. Only the World the config is
132
+ // for writes it: a runtime branch boots from its base's config, whose remote stays the base's
133
+ if (options.full && instance.name === instance.config) {
134
+ try {
135
+ const { path: configPath, config } = loadWorldConfig(instance.configPath, root);
136
+ const named = isPathRemote(origin.url) ? origin.url : `${origin.url}/${origin.namespace}`;
137
+ if (config.remotes?.origin !== named) writeWorldConfig(configPath, { ...config, remotes: { ...(config.remotes ?? {}), origin: named } });
138
+ } catch (error) { console.error(`[origin] ${name}: the remote was not named in its config: ${(error as Error).message}`); }
139
+ }
129
140
  return { world: name, origin, full: options.full ?? false, appended, behind, snapshots };
130
141
  }
131
142
 
package/src/runtime.ts CHANGED
@@ -757,6 +757,17 @@ function writePidsFromInstance(instance: WorldInstance, fresh: number[] = []): v
757
757
  writeOwnedPids(instance.root, instance.name, pids, fresh);
758
758
  }
759
759
 
760
+ /** Whether a world.json remote (`https://<host>/<org>/<world>`, or a world's directory, relative to the World's root) is
761
+ * the recorded origin, read as the CLI's parseOriginUrl reads it (the URL's origin, then its two path parts). */
762
+ function namesOrigin(remote: string | undefined, origin: { url: string; namespace: string }, worldRoot: string): boolean {
763
+ if (!remote) return false;
764
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(remote)) return resolve(worldRoot, remote) === resolve(worldRoot, origin.url);
765
+ try {
766
+ const parsed = new URL(remote); const parts = parsed.pathname.split('/').filter(Boolean);
767
+ return parts.length === 2 && parsed.origin === new URL(origin.url).origin && parts.join('/') === origin.namespace;
768
+ } catch { return false; }
769
+ }
770
+
760
771
  export function saveWorldInstance(instance: WorldInstance): void {
761
772
  writeTextAtomic(instanceFile(instance.root, instance.name), `${JSON.stringify(instance, null, 2)}\n`);
762
773
  }
@@ -1483,6 +1494,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
1483
1494
  const pidsFile = join(instance, 'pids');
1484
1495
  const envFile = resolve(root, options.envFile ?? join(instance, 'world.env'));
1485
1496
  const instanceFile = join(instance, 'instance.json');
1497
+ let carriedOrigin: WorldInstance['origin'] | undefined;
1486
1498
  const priorOutputInstance = existsSync(instanceFile) ? JSON.parse(readFileSync(instanceFile, 'utf8')) as WorldInstance : undefined;
1487
1499
  const priorOutput = priorOutputInstance?.name === name && resolve(priorOutputInstance.root) === root
1488
1500
  ? { path: resolve(priorOutputInstance.envFile), contents: envFileContents(priorOutputInstance.env) } : undefined;
@@ -1537,6 +1549,14 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
1537
1549
  || !Number.isFinite(Date.parse(receipt.stoppedAt))) {
1538
1550
  throw new Error(`World "${name}" has no verified teardown receipt for its prior instance; use its owning runtime to complete cleanup before replacement`);
1539
1551
  }
1552
+ // the origin belongs to the World, not to its running state (git: a remote outlives every checkout): an up of the
1553
+ // same config carries the one the last left while world.json's `origin` remote still names it (`remote remove`
1554
+ // ends it there; an instance under another World's config, a runtime branch, has only its own record); its cursors
1555
+ // only when its state (the mirror they count) is kept, as reset drops them
1556
+ if (prior.origin && prior.config === loaded.config.id && (name !== loaded.config.id || namesOrigin(loaded.config.remotes?.origin, prior.origin, root))) {
1557
+ carriedOrigin = options.keepState ? prior.origin
1558
+ : { url: prior.origin.url, namespace: prior.origin.namespace, ...(prior.origin.clonedAt ? { clonedAt: prior.origin.clonedAt } : {}) };
1559
+ }
1540
1560
  }
1541
1561
  assertLifecycleStartable(root, name);
1542
1562
  beginWorldLifecycle(root, name, createdAt, options.owner);
@@ -1622,6 +1642,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
1622
1642
  ...(loaded.config.actors ? { actors: loaded.config.actors } : {}),
1623
1643
  ...(loaded.config.fixtures ? { fixtures: loaded.config.fixtures } : {}),
1624
1644
  lifecycle: { owner: options.owner, log: lifecycleLogPath(root, name) },
1645
+ ...(carriedOrigin ? { origin: carriedOrigin } : {}),
1625
1646
  };
1626
1647
  saveWorldInstance(worldInstance);
1627
1648
  const cancelled = new AbortController();
@@ -26,7 +26,7 @@ import { getActiveWorldStore, withAncestryLock, captureHistory, historyAtInstant
26
26
  // to `.volter/token` and `.volter/token.read` (0600).
27
27
  // One writer per world by construction: the doors run in this process, serialized per twin.
28
28
  import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync , rmSync} from 'node:fs';
29
- import { assertSafeChangesetName, blobDigest, destinationOf, openSealedAt, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, mirrorShellUnder, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core';
29
+ import { CALLER_HEADER, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, type TwinRequestJournalCursor, assertSafeChangesetName, blobDigest, destinationOf, openSealedAt, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, mirrorShellUnder, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core';
30
30
  import { APP_READ_ENDPOINT_ENV } from './init.ts';
31
31
  import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass, type TrustedIssuer, type WorldCredentials } from '@volter/world-access';
32
32
  import { createHash } from 'node:crypto';
@@ -380,6 +380,11 @@ function traceIdOf(traceparent: unknown): string | null {
380
380
  return m && !/^0+$/.test(m[1]!) ? m[1]! : null;
381
381
  }
382
382
  type TimelineRow = { twin: string; position: number; entry: Entry };
383
+ const CALLERS_KEPT = 50_000;
384
+ /** The closed journal segments a World reads for callers when it first answers a timeline (~1 MB each): older entries show no caller. */
385
+ const CALLER_SEGMENTS = 8;
386
+ /** A W3C traceparent's parent-id (the span the World minted for the request), or null. */
387
+ function spanOf(traceparent: unknown): string | null { return parseTraceparent(traceparent)?.parentId ?? null; }
383
388
  /** Timeline order, newest first: by the entry's instant, then twin, then position. */
384
389
  function newerFirst(a: { at: string; twin: string; position: number }, b: { at: string; twin: string; position: number }): number {
385
390
  return b.at.localeCompare(a.at) || b.twin.localeCompare(a.twin) || b.position - a.position;
@@ -590,6 +595,42 @@ export class WorldDoors {
590
595
  if (grant.via === 'session') return { scope: grant.scope, via: 'session', token: null, session: grant.session!, ...(grant.who ? { who: grant.who } : {}) };
591
596
  return { scope: grant.scope, via: grant.via === 'key' ? 'key' : 'token', token: grant.presented ?? null };
592
597
  }
598
+ /** Each changeset's actions' receipts, from the landed copies on each twin's log: a deploy appends one per entry it
599
+ * performed, and a push one per entry it landed at the remote (`landed` while the remote waits, beside the
600
+ * changeset's own `applied`). */
601
+ private deployedOf(changesets: Changeset[]): Array<Array<{ service: string; actionId: string } & Receipt>> {
602
+ // by the state each action was written in (a twin may hold several), and a settled receipt wins over a provisional
603
+ // `landed` one whichever comes later in the log (a push's local copy, then the remote's deploy fetched in)
604
+ const landed = new Map<string, Map<string, Receipt>>();
605
+ const of = (service: string, state: string): Map<string, Receipt> => {
606
+ const at = `${service}\u0000${state}`;
607
+ let held = landed.get(at);
608
+ if (!held) {
609
+ held = new Map();
610
+ if (this.up().services[service]) {
611
+ for (const e of parentEntries(state, this.controlRoot(service))) {
612
+ if (!e.landsId || !e.receipt) continue;
613
+ const before = held.get(e.landsId);
614
+ if (!before || before.status === 'landed' || e.receipt.status !== 'landed') held.set(e.landsId, e.receipt);
615
+ }
616
+ }
617
+ landed.set(at, held);
618
+ }
619
+ return held;
620
+ };
621
+ return changesets.map((c) => c.actions.flatMap((a) => { const r = of(a.service, a.stateService ?? this.stateOf(a.service)).get(a.action.id); return r ? [{ service: a.service, actionId: a.action.id, ...r }] : []; }));
622
+ }
623
+
624
+ /** Who made a request, as this World names it: `key:<name>` for a named key, `person:<who>` for a browser's session
625
+ * (`person` when it names no one), `token` / `token:read` for the World's own tokens; null when it presented nothing
626
+ * this World issued. */
627
+ private callerOf(request: Request): string | null {
628
+ const cred = this.credential(request);
629
+ if (cred.scope === null) return null;
630
+ if (cred.via === 'session') return cred.who ? `person:${cred.who}` : 'person';
631
+ if (cred.via === 'key') { const token = cred.token; const held = token ? this.keys().find((k) => sameSecret(keyHash(token), k.hash)) : undefined; return held ? `key:${held.name}` : 'key'; }
632
+ return cred.scope === 'read' ? 'token:read' : 'token';
633
+ }
593
634
  /** The session cookie's name where `url` reached this World, or null where a session opens nothing:
594
635
  * on the World's own origin a host-only cookie (`__Host-` over https); without an origin of its own,
595
636
  * the path-scoped cookie a shared origin has always carried. */
@@ -781,6 +822,12 @@ export class WorldDoors {
781
822
  const enforced = scope === 'read' && (await this.enforcesRead(vendor!, twin, url.origin));
782
823
  if (scope === 'read' && !reads && !enforced) return Response.json({ error: 'the read token opens only reads' }, { status: 403 });
783
824
  const headers = new Headers(request.headers); headers.delete(TOKEN_HEADER); headers.delete(TWINS_KEY_HEADER); headers.delete('host');
825
+ // who made it is the World's word, never the caller's: the twin's request journal records it. The World is a hop of
826
+ // the request's trace: the caller's trace-id goes on (or a new one), under a parent-id the World mints, which the
827
+ // request's log entries carry, so each entry is joined to its caller by an id no caller chose
828
+ headers.delete(CALLER_HEADER);
829
+ const who = this.callerOf(request); if (who) headers.set(CALLER_HEADER, who);
830
+ headers.set('traceparent', traceparentForDelivery(headers.get('traceparent')) ?? newTraceparent());
784
831
  // at a twin that enforces the read scope, every request of the read token is marked read-only (a GET
785
832
  // that would write for its caller is refused too); at any other, its GET passes as it always has
786
833
  if (enforced) headers.set(READ_ONLY_HEADER, '1');
@@ -843,7 +890,7 @@ export class WorldDoors {
843
890
  // what leaves for the vendor before the seal is applied: no World token in any header it can arrive
844
891
  // under (an SDK's own `Authorization: Bearer` included), no cookie, no routing headers of this World
845
892
  const caller = new Headers(request.headers);
846
- for (const h of [TOKEN_HEADER, TWINS_KEY_HEADER, TWIN_PREFIX_HEADER, 'host', 'x-forwarded-host', 'x-forwarded-proto', 'cookie']) caller.delete(h);
893
+ for (const h of [TOKEN_HEADER, TWINS_KEY_HEADER, TWIN_PREFIX_HEADER, CALLER_HEADER, 'host', 'x-forwarded-host', 'x-forwarded-proto', 'cookie']) caller.delete(h);
847
894
  // a secret this World minted (`tok_…`, live, revoked or expired) or one that opens it never leaves
848
895
  const authorization = caller.get('authorization');
849
896
  const offered = authorization ? presentedSecrets(new Headers({ authorization }), { token: TOKEN_HEADER, twinsKey: TWINS_KEY_HEADER }) : [];
@@ -852,7 +899,9 @@ export class WorldDoors {
852
899
  // name, with the credential shape of what the caller sent on. As an audit device does, the request is
853
900
  // written before the real credential is used, and a request whose audit cannot be written is refused.
854
901
  const credentials = twinRequestCredentials(caller, url);
855
- if (!journalTwinRequest(name, { phase: 'request', method: request.method, path: `/${rest}`, status: 0, credentials }, this.controlRoot(name))) {
902
+ const who = this.callerOf(request); const trace = parseTraceparent(request.headers.get('traceparent'))?.traceId;
903
+ const by = { ...(who ? { caller: who } : {}), ...(trace ? { trace } : {}) };
904
+ if (!journalTwinRequest(name, { phase: 'request', method: request.method, path: `/${rest}`, status: 0, credentials, ...by }, this.controlRoot(name))) {
856
905
  return Response.json({ error: `the ${name} link's audit could not be written: nothing was sent` }, { status: 503 });
857
906
  }
858
907
  const startedAt = Date.now(); let status = 500;
@@ -861,7 +910,7 @@ export class WorldDoors {
861
910
  status = answer.status;
862
911
  return answer;
863
912
  } finally {
864
- journalTwinRequest(name, { method: request.method, path: `/${rest}`, status, ms: Date.now() - startedAt, credentials }, this.controlRoot(name));
913
+ journalTwinRequest(name, { method: request.method, path: `/${rest}`, status, ms: Date.now() - startedAt, credentials, ...by }, this.controlRoot(name));
865
914
  }
866
915
  }
867
916
 
@@ -1017,8 +1066,10 @@ export class WorldDoors {
1017
1066
  }
1018
1067
  if (kind === 'changesets' && request.method === 'GET') {
1019
1068
  // with the body's hash as it stands, so a reader sees an approval or a verification made on other bytes as stale
1020
- if (name) { const l = findWorldChangeset(name, { root: this.worldRoot, world: this.name }); return Response.json({ ...l.changeset, currentHash: changesetContentHash(l.changeset) }); }
1021
- return Response.json({ changesets: listWorldChangesets({ root: this.worldRoot, world: this.name }).map((l) => ({ ...l.changeset, currentHash: changesetContentHash(l.changeset) })) });
1069
+ if (name) { const l = findWorldChangeset(name, { root: this.worldRoot, world: this.name }); return Response.json({ ...l.changeset, currentHash: changesetContentHash(l.changeset), deployed: this.deployedOf([l.changeset])[0] }); }
1070
+ const all = listWorldChangesets({ root: this.worldRoot, world: this.name }).map((l) => l.changeset);
1071
+ const deployed = this.deployedOf(all);
1072
+ return Response.json({ changesets: all.map((c, i) => ({ ...c, currentHash: changesetContentHash(c), deployed: deployed[i] })) });
1022
1073
  }
1023
1074
  if (kind === 'changesets' && name && verb === 'verify' && request.method === 'POST') {
1024
1075
  const outcome = await this.serialized('changesets', () => verifyWorldChangeset(name, { root: this.worldRoot, world: this.name, ephemeral: true }));
@@ -1442,7 +1493,23 @@ export class WorldDoors {
1442
1493
  const more = page.length > limit; const shown = page.slice(0, limit);
1443
1494
  const last = shown[shown.length - 1];
1444
1495
  const next = more && last ? Buffer.from(JSON.stringify([last.entry.occurredAt, last.twin, last.position])).toString('base64url') : null;
1445
- return Response.json({ world: this.served, entries: shown, next });
1496
+ // who made each entry: the request it was written in (the World's span on it) as the twin's journal recorded it
1497
+ const entries = shown.map((r) => { const by = this.callersOf(r.twin).get(spanOf(r.entry.traceparent) ?? ''); return by ? { ...r, caller: by } : r; });
1498
+ return Response.json({ world: this.served, entries, next });
1499
+ }
1500
+
1501
+ /** Each twin's callers by the World's span, from its request journal: its newest CALLER_SEGMENTS once, then from
1502
+ * where the last read stopped (readTwinRequestJournalFrom follows a rotation). The newest CALLERS_KEPT are kept. */
1503
+ private readonly callersByTwin = new Map<string, { root: string; cursor: TwinRequestJournalCursor | null; callers: Map<string, string> }>();
1504
+ private callersOf(twin: string): Map<string, string> {
1505
+ const root = this.controlRoot(twin);
1506
+ let held = this.callersByTwin.get(twin);
1507
+ if (!held || held.root !== root) { held = { root, cursor: null, callers: new Map() }; this.callersByTwin.set(twin, held); }
1508
+ const read = readTwinRequestJournalFrom(twin, root, held.cursor, { segments: CALLER_SEGMENTS });
1509
+ for (const e of read.entries) if (e.span && e.caller) { held.callers.delete(e.span); held.callers.set(e.span, e.caller); }
1510
+ held.cursor = read.cursor;
1511
+ for (const span of held.callers.keys()) { if (held.callers.size <= CALLERS_KEPT) break; held.callers.delete(span); }
1512
+ return held.callers;
1446
1513
  }
1447
1514
 
1448
1515
  /** Each twin's timeline rows, kept until its logs change: a poll of an unchanged World reads no log