@volter/world-runtime 2.0.32 → 2.0.34

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) {
@@ -1184,6 +1189,15 @@ export class WorldDoors {
1184
1189
  }
1185
1190
  if (kind === 'links' && !name && request.method === 'GET')
1186
1191
  return Response.json({ links: this.links() });
1192
+ // OPENING A SEAL — pointing this World at a vendor (a link, a twin's root) or sealing the credential it uses there — is
1193
+ // the World token's act, or a person's at its page: never a key's, which an app or a script holds (an app that could
1194
+ // open its own seal would be unsealed by itself, not deliberately)
1195
+ const opensSeal = (kind === 'links' && name && (request.method === 'PUT' || request.method === 'DELETE'))
1196
+ || (kind === 'twins' && name && (verb === 'root' || verb === 'credential') && request.method === 'PUT')
1197
+ // a check runs in a local World's own process, with all it can reach: putting or removing one is the same act
1198
+ || (kind === 'checks' && name && (request.method === 'PUT' || request.method === 'DELETE'));
1199
+ if (opensSeal && via === 'key')
1200
+ return Response.json({ error: 'opening a seal is the World\'s token\'s act, or a person\'s at its page: a key cannot point this World at a vendor, seal a credential, or change its checks' }, { status: 403 });
1187
1201
  if (kind === 'links' && name && request.method === 'PUT') {
1188
1202
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name) || this.up().services[name])
1189
1203
  return Response.json({ error: 'a link is named [a-z0-9_-] and is not a twin of this World' }, { status: 400 });
@@ -1328,7 +1342,8 @@ export class WorldDoors {
1328
1342
  const since = Date.now() - sinceMs;
1329
1343
  const twins = [];
1330
1344
  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);
1345
+ // a link's request entries (written before it forwards) are its audit, not traffic: its outcomes are counted
1346
+ const entries = readTwinRequestJournal(vendor, this.controlRoot(vendor), { since }).filter((e) => e.phase !== 'request' && (!e.at || Date.parse(e.at) >= since));
1332
1347
  const byStatus = {};
1333
1348
  const routes = new Map();
1334
1349
  const times = [];
@@ -1344,7 +1359,7 @@ export class WorldDoors {
1344
1359
  const pct = (p) => (times.length ? times[Math.min(times.length - 1, Math.floor((p / 100) * times.length))] : null);
1345
1360
  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
1361
  }
1347
- return Response.json({ world: this.served, hours: sinceMs / 3_600_000, twins, requests: twins.reduce((n, t) => n + t.requests, 0) });
1362
+ 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
1363
  }
1349
1364
  if (kind === 'twins' && !name && request.method === 'GET') {
1350
1365
  // 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.34",
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
 
@@ -1032,6 +1037,14 @@ export class WorldDoors {
1032
1037
  return Response.json(approveWorldChangeset(name, { root: this.worldRoot, world: this.name, principal, ...(body.note ? { note: body.note } : {}) }));
1033
1038
  }
1034
1039
  if (kind === 'links' && !name && request.method === 'GET') return Response.json({ links: this.links() });
1040
+ // OPENING A SEAL — pointing this World at a vendor (a link, a twin's root) or sealing the credential it uses there — is
1041
+ // the World token's act, or a person's at its page: never a key's, which an app or a script holds (an app that could
1042
+ // open its own seal would be unsealed by itself, not deliberately)
1043
+ const opensSeal = (kind === 'links' && name && (request.method === 'PUT' || request.method === 'DELETE'))
1044
+ || (kind === 'twins' && name && (verb === 'root' || verb === 'credential') && request.method === 'PUT')
1045
+ // a check runs in a local World's own process, with all it can reach: putting or removing one is the same act
1046
+ || (kind === 'checks' && name && (request.method === 'PUT' || request.method === 'DELETE'));
1047
+ if (opensSeal && via === 'key') return Response.json({ error: 'opening a seal is the World\'s token\'s act, or a person\'s at its page: a key cannot point this World at a vendor, seal a credential, or change its checks' }, { status: 403 });
1035
1048
  if (kind === 'links' && name && request.method === 'PUT') {
1036
1049
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name) || this.up().services[name]) return Response.json({ error: 'a link is named [a-z0-9_-] and is not a twin of this World' }, { status: 400 });
1037
1050
  const body = (await request.json()) as { origin?: string };
@@ -1145,13 +1158,14 @@ export class WorldDoors {
1145
1158
  const sinceMs = Number(url.searchParams.get('hours') ?? '24') * 3_600_000; const since = Date.now() - sinceMs;
1146
1159
  const twins: Array<{ twin: string; requests: number; byStatus: Record<string, number>; p50Ms: number | null; p95Ms: number | null; topRoutes: Array<{ route: string; requests: number }> }> = [];
1147
1160
  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);
1161
+ // a link's request entries (written before it forwards) are its audit, not traffic: its outcomes are counted
1162
+ const entries = readTwinRequestJournal(vendor, this.controlRoot(vendor), { since }).filter((e) => e.phase !== 'request' && (!e.at || Date.parse(e.at) >= since));
1149
1163
  const byStatus: Record<string, number> = {}; const routes = new Map<string, number>(); const times: number[] = [];
1150
1164
  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
1165
  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
1166
  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
1167
  }
1154
- return Response.json({ world: this.served, hours: sinceMs / 3_600_000, twins, requests: twins.reduce((n, t) => n + t.requests, 0) });
1168
+ 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
1169
  }
1156
1170
  if (kind === 'twins' && !name && request.method === 'GET') {
1157
1171
  // 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 ?? '?'}`;