@volter/world-runtime 2.0.32 → 2.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.
@@ -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, mirrorShellUnder, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin } from '@volter/world-core';
29
+ import { 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';
@@ -950,7 +950,12 @@ export class WorldDoors {
950
950
  if (offered.some((p) => p.startsWith('tok_')) || (offered.length && grantOf(offered, null, this.issued())))
951
951
  caller.delete('authorization');
952
952
  // an opened seal keeps the audit a twin keeps: the same line in the same journal under the link's
953
- // name, with the credential shape of what the caller sent on
953
+ // name, with the credential shape of what the caller sent on. As an audit device does, the request is
954
+ // written before the real credential is used, and a request whose audit cannot be written is refused.
955
+ const credentials = twinRequestCredentials(caller, url);
956
+ if (!journalTwinRequest(name, { phase: 'request', method: request.method, path: `/${rest}`, status: 0, credentials }, this.controlRoot(name))) {
957
+ return Response.json({ error: `the ${name} link's audit could not be written: nothing was sent` }, { status: 503 });
958
+ }
954
959
  const startedAt = Date.now();
955
960
  let status = 500;
956
961
  try {
@@ -959,7 +964,7 @@ export class WorldDoors {
959
964
  return answer;
960
965
  }
961
966
  finally {
962
- journalTwinRequest(name, { method: request.method, path: `/${rest}`, status, ms: Date.now() - startedAt, credentials: twinRequestCredentials(caller, url) }, this.controlRoot(name));
967
+ journalTwinRequest(name, { method: request.method, path: `/${rest}`, status, ms: Date.now() - startedAt, credentials }, this.controlRoot(name));
963
968
  }
964
969
  }
965
970
  async forwardSealed(request, caller, url, rest, name, origin) {
@@ -1328,7 +1333,8 @@ export class WorldDoors {
1328
1333
  const since = Date.now() - sinceMs;
1329
1334
  const twins = [];
1330
1335
  for (const vendor of [...new Set([...Object.keys(this.up().services), ...Object.keys(this.links())])]) {
1331
- const entries = readTwinRequestJournal(vendor, this.controlRoot(vendor)).filter((e) => !e.at || Date.parse(e.at) >= since);
1336
+ // a link's request entries (written before it forwards) are its audit, not traffic: its outcomes are counted
1337
+ const entries = readTwinRequestJournal(vendor, this.controlRoot(vendor), { since }).filter((e) => e.phase !== 'request' && (!e.at || Date.parse(e.at) >= since));
1332
1338
  const byStatus = {};
1333
1339
  const routes = new Map();
1334
1340
  const times = [];
@@ -1344,7 +1350,7 @@ export class WorldDoors {
1344
1350
  const pct = (p) => (times.length ? times[Math.min(times.length - 1, Math.floor((p / 100) * times.length))] : null);
1345
1351
  twins.push({ twin: vendor, requests: entries.length, byStatus, p50Ms: pct(50), p95Ms: pct(95), topRoutes: [...routes.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([route, requests]) => ({ route, requests })) });
1346
1352
  }
1347
- return Response.json({ world: this.served, hours: sinceMs / 3_600_000, twins, requests: twins.reduce((n, t) => n + t.requests, 0) });
1353
+ return Response.json({ world: this.served, hours: sinceMs / 3_600_000, twins, requests: twins.reduce((n, t) => n + t.requests, 0), journalFailures: twins.reduce((n, t) => n + twinRequestJournalFailures(t.twin, this.controlRoot(t.twin)), 0) });
1348
1354
  }
1349
1355
  if (kind === 'twins' && !name && request.method === 'GET') {
1350
1356
  // every twin of the world as a consumer sees it — the status door mapped over the world; what a
package/dist/src/tail.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // records its first action, so discovery re-runs on every poll and a missing ledger is a
8
8
  // notice, never an error.
9
9
  import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
10
- import { join } from 'node:path';
10
+ import { basename, dirname, join } from 'node:path';
11
11
  import { stateDirName } from '@volter/world-core';
12
12
  import { statusWorld } from "./runtime.js";
13
13
  /** Every action ledger a world service currently owns. One world service can record actions
@@ -43,10 +43,24 @@ function readNewLines(path, offsets) {
43
43
  catch {
44
44
  return []; // vanished between discovery and read (e.g. a purge mid-tail) — next poll rediscovers
45
45
  }
46
- if (size < offset)
47
- offset = 0; // rewritten shorter (scrub/rebuild) — replay rather than silently skip
48
- if (size === offset)
49
- return [];
46
+ let closed = [];
47
+ if (size < offset) {
48
+ // a request journal that closed into its next segment (world-core serve.ts): what was appended there
49
+ // after the last poll is read from the segment first; any other shorter rewrite (scrub/rebuild) replays
50
+ const segment = newestSegment(path);
51
+ let closedSize = -1;
52
+ try {
53
+ closedSize = segment ? statSync(segment).size : -1;
54
+ }
55
+ catch { /* gone between list and stat */ }
56
+ if (segment && closedSize >= offset)
57
+ closed = readNewLines(segment, new Map([[segment, offset]]));
58
+ offset = 0;
59
+ }
60
+ if (size === offset) {
61
+ offsets.set(path, offset);
62
+ return closed;
63
+ }
50
64
  const fd = openSync(path, 'r');
51
65
  const buffer = Buffer.alloc(size - offset);
52
66
  try {
@@ -56,10 +70,29 @@ function readNewLines(path, offsets) {
56
70
  closeSync(fd);
57
71
  }
58
72
  const lastNewline = buffer.lastIndexOf(0x0a);
59
- if (lastNewline < 0)
60
- return [];
73
+ if (lastNewline < 0) {
74
+ offsets.set(path, offset);
75
+ return closed;
76
+ }
61
77
  offsets.set(path, offset + lastNewline + 1);
62
- return buffer.subarray(0, lastNewline).toString('utf8').split('\n').filter((line) => line.trim() !== '');
78
+ return [...closed, ...buffer.subarray(0, lastNewline).toString('utf8').split('\n').filter((line) => line.trim() !== '')];
79
+ }
80
+ /** A journal's newest closed segment (`<file>.<n>`), or null. */
81
+ function newestSegment(path) {
82
+ const dir = dirname(path);
83
+ const base = basename(path);
84
+ let best = 0;
85
+ try {
86
+ for (const name of readdirSync(dir)) {
87
+ const n = name.startsWith(`${base}.`) ? Number(name.slice(base.length + 1)) : NaN;
88
+ if (Number.isInteger(n) && n > best)
89
+ best = n;
90
+ }
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ return best ? `${path}.${best}` : null;
63
96
  }
64
97
  /** Local wall-clock `HH:MM:SS.mmm` — the operator is watching a live feed on this machine. */
65
98
  function formatOccurredAt(occurredAt) {
@@ -85,6 +118,9 @@ function formatEntry(entry, json) {
85
118
  const creds = (record.credentials ?? [])
86
119
  .map((c) => `${c.name ?? '?'}${c.in === 'query' ? '(query)' : ''}=${c.empty ? 'EMPTY' : `${c.scheme ? `${c.scheme} ` : ''}${(c.fp ?? '').replace(/^sha256:/, '').slice(0, 8)}`}`)
87
120
  .join(' ');
121
+ // a link's request entry, written before it forwards: the request on its way, its outcome follows
122
+ if (record.phase === 'request')
123
+ return `${formatOccurredAt(entryInstant(entry))} ${entry.service} → ${record.method ?? '?'} ${record.path ?? '?'}${creds ? ` [${creds}]` : ''}`;
88
124
  return `${formatOccurredAt(entryInstant(entry))} ${entry.service} ${record.method ?? '?'} ${record.path ?? '?'} ${record.status ?? '?'}${typeof record.ms === 'number' ? ` in ${record.ms}ms` : ''}${creds ? ` [${creds}]` : ''}`;
89
125
  }
90
126
  return `${formatOccurredAt(record.occurredAt)} ${entry.service} ${record.operation ?? record.op ?? '?'} ${record.subject?.id ?? '?'}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "2.0.32",
3
+ "version": "2.0.33",
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.19",
71
- "@volter/world-console": "2.0.11"
70
+ "@volter/world-core": "2.0.20",
71
+ "@volter/world-console": "2.0.12"
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.19",
77
+ "@volter/world-core": "2.0.20",
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.19"
87
+ "@volter/twin-mongodb": "0.1.20"
88
88
  }
89
89
  }
@@ -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, mirrorShellUnder, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core';
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';
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';
@@ -849,14 +849,19 @@ export class WorldDoors {
849
849
  const offered = authorization ? presentedSecrets(new Headers({ authorization }), { token: TOKEN_HEADER, twinsKey: TWINS_KEY_HEADER }) : [];
850
850
  if (offered.some((p) => p.startsWith('tok_')) || (offered.length && grantOf(offered, null, this.issued()))) caller.delete('authorization');
851
851
  // an opened seal keeps the audit a twin keeps: the same line in the same journal under the link's
852
- // name, with the credential shape of what the caller sent on
852
+ // name, with the credential shape of what the caller sent on. As an audit device does, the request is
853
+ // written before the real credential is used, and a request whose audit cannot be written is refused.
854
+ const credentials = twinRequestCredentials(caller, url);
855
+ if (!journalTwinRequest(name, { phase: 'request', method: request.method, path: `/${rest}`, status: 0, credentials }, this.controlRoot(name))) {
856
+ return Response.json({ error: `the ${name} link's audit could not be written: nothing was sent` }, { status: 503 });
857
+ }
853
858
  const startedAt = Date.now(); let status = 500;
854
859
  try {
855
860
  const answer = await this.forwardSealed(request, caller, url, rest, name, origin);
856
861
  status = answer.status;
857
862
  return answer;
858
863
  } finally {
859
- journalTwinRequest(name, { method: request.method, path: `/${rest}`, status, ms: Date.now() - startedAt, credentials: twinRequestCredentials(caller, url) }, this.controlRoot(name));
864
+ journalTwinRequest(name, { method: request.method, path: `/${rest}`, status, ms: Date.now() - startedAt, credentials }, this.controlRoot(name));
860
865
  }
861
866
  }
862
867
 
@@ -1145,13 +1150,14 @@ export class WorldDoors {
1145
1150
  const sinceMs = Number(url.searchParams.get('hours') ?? '24') * 3_600_000; const since = Date.now() - sinceMs;
1146
1151
  const twins: Array<{ twin: string; requests: number; byStatus: Record<string, number>; p50Ms: number | null; p95Ms: number | null; topRoutes: Array<{ route: string; requests: number }> }> = [];
1147
1152
  for (const vendor of [...new Set([...Object.keys(this.up().services), ...Object.keys(this.links())])]) {
1148
- const entries = readTwinRequestJournal(vendor, this.controlRoot(vendor)).filter((e) => !e.at || Date.parse(e.at) >= since);
1153
+ // a link's request entries (written before it forwards) are its audit, not traffic: its outcomes are counted
1154
+ const entries = readTwinRequestJournal(vendor, this.controlRoot(vendor), { since }).filter((e) => e.phase !== 'request' && (!e.at || Date.parse(e.at) >= since));
1149
1155
  const byStatus: Record<string, number> = {}; const routes = new Map<string, number>(); const times: number[] = [];
1150
1156
  for (const e of entries) { const cls = `${String(e.status)[0]}xx`; byStatus[cls] = (byStatus[cls] ?? 0) + 1; const route = `${e.method} ${e.path.replace(/\/[0-9]+(?=\/|$)/g, '/{id}')}`; routes.set(route, (routes.get(route) ?? 0) + 1); if (typeof e.ms === 'number') times.push(e.ms); }
1151
1157
  times.sort((a, b) => a - b); const pct = (p: number): number | null => (times.length ? times[Math.min(times.length - 1, Math.floor((p / 100) * times.length))]! : null);
1152
1158
  twins.push({ twin: vendor, requests: entries.length, byStatus, p50Ms: pct(50), p95Ms: pct(95), topRoutes: [...routes.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([route, requests]) => ({ route, requests })) });
1153
1159
  }
1154
- return Response.json({ world: this.served, hours: sinceMs / 3_600_000, twins, requests: twins.reduce((n, t) => n + t.requests, 0) });
1160
+ return Response.json({ world: this.served, hours: sinceMs / 3_600_000, twins, requests: twins.reduce((n, t) => n + t.requests, 0), journalFailures: twins.reduce((n, t) => n + twinRequestJournalFailures(t.twin, this.controlRoot(t.twin)), 0) });
1155
1161
  }
1156
1162
  if (kind === 'twins' && !name && request.method === 'GET') {
1157
1163
  // every twin of the world as a consumer sees it — the status door mapped over the world; what a
package/src/tail.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  // records its first action, so discovery re-runs on every poll and a missing ledger is a
8
8
  // notice, never an error.
9
9
  import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
10
- import { join } from 'node:path';
10
+ import { basename, dirname, join } from 'node:path';
11
11
  import { stateDirName } from '@volter/world-core';
12
12
  import { statusWorld } from './runtime.ts';
13
13
 
@@ -84,8 +84,16 @@ function readNewLines(path: string, offsets: Map<string, number>): string[] {
84
84
  } catch {
85
85
  return []; // vanished between discovery and read (e.g. a purge mid-tail) — next poll rediscovers
86
86
  }
87
- if (size < offset) offset = 0; // rewritten shorter (scrub/rebuild) — replay rather than silently skip
88
- if (size === offset) return [];
87
+ let closed: string[] = [];
88
+ if (size < offset) {
89
+ // a request journal that closed into its next segment (world-core serve.ts): what was appended there
90
+ // after the last poll is read from the segment first; any other shorter rewrite (scrub/rebuild) replays
91
+ const segment = newestSegment(path);
92
+ let closedSize = -1; try { closedSize = segment ? statSync(segment).size : -1; } catch { /* gone between list and stat */ }
93
+ if (segment && closedSize >= offset) closed = readNewLines(segment, new Map([[segment, offset]]));
94
+ offset = 0;
95
+ }
96
+ if (size === offset) { offsets.set(path, offset); return closed; }
89
97
  const fd = openSync(path, 'r');
90
98
  const buffer = Buffer.alloc(size - offset);
91
99
  try {
@@ -94,9 +102,17 @@ function readNewLines(path: string, offsets: Map<string, number>): string[] {
94
102
  closeSync(fd);
95
103
  }
96
104
  const lastNewline = buffer.lastIndexOf(0x0a);
97
- if (lastNewline < 0) return [];
105
+ if (lastNewline < 0) { offsets.set(path, offset); return closed; }
98
106
  offsets.set(path, offset + lastNewline + 1);
99
- return buffer.subarray(0, lastNewline).toString('utf8').split('\n').filter((line) => line.trim() !== '');
107
+ return [...closed, ...buffer.subarray(0, lastNewline).toString('utf8').split('\n').filter((line) => line.trim() !== '')];
108
+ }
109
+
110
+ /** A journal's newest closed segment (`<file>.<n>`), or null. */
111
+ function newestSegment(path: string): string | null {
112
+ const dir = dirname(path); const base = basename(path);
113
+ let best = 0;
114
+ try { for (const name of readdirSync(dir)) { const n = name.startsWith(`${base}.`) ? Number(name.slice(base.length + 1)) : NaN; if (Number.isInteger(n) && n > best) best = n; } } catch { return null; }
115
+ return best ? `${path}.${best}` : null;
100
116
  }
101
117
 
102
118
  /** Local wall-clock `HH:MM:SS.mmm` — the operator is watching a live feed on this machine. */
@@ -123,6 +139,8 @@ function formatEntry(entry: TailEntry, json: boolean): string {
123
139
  const creds = (record.credentials ?? [])
124
140
  .map((c) => `${c.name ?? '?'}${c.in === 'query' ? '(query)' : ''}=${c.empty ? 'EMPTY' : `${c.scheme ? `${c.scheme} ` : ''}${(c.fp ?? '').replace(/^sha256:/, '').slice(0, 8)}`}`)
125
141
  .join(' ');
142
+ // a link's request entry, written before it forwards: the request on its way, its outcome follows
143
+ if (record.phase === 'request') return `${formatOccurredAt(entryInstant(entry))} ${entry.service} → ${record.method ?? '?'} ${record.path ?? '?'}${creds ? ` [${creds}]` : ''}`;
126
144
  return `${formatOccurredAt(entryInstant(entry))} ${entry.service} ${record.method ?? '?'} ${record.path ?? '?'} ${record.status ?? '?'}${typeof record.ms === 'number' ? ` in ${record.ms}ms` : ''}${creds ? ` [${creds}]` : ''}`;
127
145
  }
128
146
  return `${formatOccurredAt(record.occurredAt)} ${entry.service} ${record.operation ?? record.op ?? '?'} ${record.subject?.id ?? '?'}`;