@volter/world-runtime 2.0.5 → 2.0.6
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.
- package/dist/src/served-world.js +26 -11
- package/package.json +2 -2
- package/src/served-world.ts +24 -10
package/dist/src/served-world.js
CHANGED
|
@@ -31,7 +31,7 @@ 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';
|
|
33
33
|
import { dirname, join, resolve } from 'node:path';
|
|
34
|
-
import { appendActionIfAbsent, branchEntries, branchLogPath, branchMetaPath, confirmAction, parentLogPath, readTree, stateDirName, wholeLog, worldNow, parentEntries, rebaseBranch, } from '@volter/world-core';
|
|
34
|
+
import { appendActionIfAbsent, branchEntries, branchLogPath, branchMetaPath, changesetContentHash, confirmAction, parentLogPath, readTree, stateDirName, wholeLog, worldNow, parentEntries, rebaseBranch, } from '@volter/world-core';
|
|
35
35
|
import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChangeset, listWorldChangesets, pushWorldChangeset, verifyWorldChangeset, worldChangesetsDir } from "./changeset.js";
|
|
36
36
|
import { fetchFromOrigin } from "./origin.js";
|
|
37
37
|
import { CONSOLE_BASE, consoleRedirect, serveConsoleApart } from "./console-apart.js";
|
|
@@ -415,6 +415,9 @@ export class WorldDoors {
|
|
|
415
415
|
for (const k of also)
|
|
416
416
|
ids.add(k.id);
|
|
417
417
|
this.keepKeys(this.keys().filter((k) => !ids.has(k.id)));
|
|
418
|
+
// a session a revoked key opened ends with it
|
|
419
|
+
if (Object.values(this.sessions()).some((x) => x.key !== undefined && ids.has(x.key)))
|
|
420
|
+
this.keepSessions(Object.fromEntries(Object.entries(this.sessions()).filter(([, x]) => x.key === undefined || !ids.has(x.key))));
|
|
418
421
|
// the keys are gone whatever a removal answers; a branch that stays is said, not hidden (its parent key no longer opens this World)
|
|
419
422
|
for (const k of [...gone, ...also])
|
|
420
423
|
if (k.branch)
|
|
@@ -472,8 +475,10 @@ export class WorldDoors {
|
|
|
472
475
|
const who = new URL(request.url).searchParams.get('person');
|
|
473
476
|
if (via !== 'token' || !who)
|
|
474
477
|
return Response.json({ error: 'DELETE /keys?person=<subject>, with the World\'s token' }, { status: via !== 'token' ? 403 : 400 });
|
|
475
|
-
// their keys, the parent keys of the branches they made, and those branches
|
|
476
|
-
|
|
478
|
+
// their keys, the parent keys of the branches they made, and those branches; and the sessions their passes opened
|
|
479
|
+
const revoked = await this.revokeKeys((k) => person(k) === who);
|
|
480
|
+
this.keepSessions(Object.fromEntries(Object.entries(this.sessions()).filter(([, s]) => s.sub !== who)));
|
|
481
|
+
return Response.json({ revoked });
|
|
477
482
|
}
|
|
478
483
|
if (id && request.method === 'DELETE') {
|
|
479
484
|
const found = this.keys().find((k) => k.id === id);
|
|
@@ -1053,21 +1058,29 @@ export class WorldDoors {
|
|
|
1053
1058
|
return Response.json({ twin: name, state, view: cut.view, position: cut.position, tree: readTree(state, controlRoot, { view: cut.view }) });
|
|
1054
1059
|
}
|
|
1055
1060
|
if (kind === 'changesets' && request.method === 'GET') {
|
|
1061
|
+
// with the body's hash as it stands, so a reader sees an approval or a verification made on other bytes as stale
|
|
1056
1062
|
if (name) {
|
|
1057
1063
|
const l = findWorldChangeset(name, { root: this.worldRoot, world: this.name });
|
|
1058
|
-
return Response.json(l.changeset);
|
|
1064
|
+
return Response.json({ ...l.changeset, currentHash: changesetContentHash(l.changeset) });
|
|
1059
1065
|
}
|
|
1060
|
-
return Response.json({ changesets: listWorldChangesets({ root: this.worldRoot, world: this.name }).map((l) => l.changeset) });
|
|
1066
|
+
return Response.json({ changesets: listWorldChangesets({ root: this.worldRoot, world: this.name }).map((l) => ({ ...l.changeset, currentHash: changesetContentHash(l.changeset) })) });
|
|
1061
1067
|
}
|
|
1062
1068
|
if (kind === 'changesets' && name && verb === 'verify' && request.method === 'POST') {
|
|
1063
1069
|
const outcome = await this.serialized('changesets', () => verifyWorldChangeset(name, { root: this.worldRoot, world: this.name, ephemeral: true }));
|
|
1064
1070
|
return Response.json(outcome);
|
|
1065
1071
|
}
|
|
1066
1072
|
if (kind === 'changesets' && name && verb === 'approve' && request.method === 'POST') {
|
|
1067
|
-
const body = (await request.json());
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1073
|
+
const body = (await request.json().catch(() => ({})));
|
|
1074
|
+
// a browser's session signs as whom it was opened for (a platform's pass names the person; this machine's own page
|
|
1075
|
+
// is the person at it), never as whatever the page says; a token or a key names its principal, as before, and so
|
|
1076
|
+
// does a session the World's write token opened (it is that token's holder, who names themselves)
|
|
1077
|
+
const held = via === 'session' ? this.sessions()[this.credential(request).session ?? ''] : undefined;
|
|
1078
|
+
const named = typeof body.as === 'string' && body.as.trim() ? body.as.trim().slice(0, 80) : undefined;
|
|
1079
|
+
const tokenSession = held !== undefined && !held.who && held.key === undefined && held.scope === 'write';
|
|
1080
|
+
const principal = via === 'session' ? held?.who ?? (this.host.localTrust?.() ? 'this machine' : tokenSession ? named : undefined) : named;
|
|
1081
|
+
if (!principal)
|
|
1082
|
+
return Response.json({ error: via === 'session' && !tokenSession ? 'this session names no person to sign as: open the World from its platform, or approve with its token' : 'approve: { as, note? } — name who approves' }, { status: via === 'session' && !tokenSession ? 403 : 400 });
|
|
1083
|
+
return Response.json(approveWorldChangeset(name, { root: this.worldRoot, world: this.name, principal, ...(body.note ? { note: body.note } : {}) }));
|
|
1071
1084
|
}
|
|
1072
1085
|
if (kind === 'links' && !name && request.method === 'GET')
|
|
1073
1086
|
return Response.json({ links: this.links() });
|
|
@@ -1361,8 +1374,10 @@ export class WorldDoors {
|
|
|
1361
1374
|
const cookie = this.sessionCookie(url, held ?? this.openSession(scope, undefined, key));
|
|
1362
1375
|
const who = held ? this.sessions()[held]?.who : undefined;
|
|
1363
1376
|
const issuer = held ? this.sessions()[held]?.issuer : undefined;
|
|
1364
|
-
// `local`: this page is the local World's own (localPage), so the console offers no sign-in or sign-out
|
|
1365
|
-
|
|
1377
|
+
// `local`: this page is the local World's own (localPage), so the console offers no sign-in or sign-out;
|
|
1378
|
+
// `signsAs`: whom an approval from this session is recorded as, or null when the page must name them (a token's)
|
|
1379
|
+
const signsAs = who ?? (this.host.localTrust?.() ? 'this machine' : null);
|
|
1380
|
+
return Response.json({ world: this.served, scope, signsAs, ...(who ? { who } : {}), ...(issuer ? { issuer } : {}), ...(own ? { origin: own } : {}), ...(this.localPage(request, url) ? { local: true } : {}) }, { headers: { 'set-cookie': cookie } });
|
|
1366
1381
|
}
|
|
1367
1382
|
/** THE CLOCK: the frozen instant every twin stamps from, kept in the World's store. It moves only
|
|
1368
1383
|
* forward once the World has entries (a twin's catch-up stamps each move at its due time, so a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@volter/world-core": "2.0.4",
|
|
71
|
-
"@volter/world-console": "2.0.
|
|
71
|
+
"@volter/world-console": "2.0.6"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@electric-sql/pglite": "0.5.8",
|
package/src/served-world.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass, type Tr
|
|
|
32
32
|
import { createHash } from 'node:crypto';
|
|
33
33
|
import { dirname, join, resolve } from 'node:path';
|
|
34
34
|
import {
|
|
35
|
-
appendActionIfAbsent, branchEntries, branchLogPath, branchMetaPath, confirmAction, parentLogPath, readTree, stateDirName, wholeLog, worldNow,
|
|
35
|
+
appendActionIfAbsent, branchEntries, branchLogPath, branchMetaPath, changesetContentHash, confirmAction, parentLogPath, readTree, stateDirName, wholeLog, worldNow,
|
|
36
36
|
parentEntries, rebaseBranch, type Changeset, type Entry, type Receipt, type RootConfig,
|
|
37
37
|
} from '@volter/world-core';
|
|
38
38
|
import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChangeset, listWorldChangesets, pushWorldChangeset, verifyWorldChangeset, worldChangesetsDir } from './changeset.ts';
|
|
@@ -407,6 +407,8 @@ export class WorldDoors {
|
|
|
407
407
|
const also = this.keys().filter((k) => !ids.has(k.id) && k.madeWith !== undefined && ids.has(k.madeWith)); // branches a revoked key made
|
|
408
408
|
for (const k of also) ids.add(k.id);
|
|
409
409
|
this.keepKeys(this.keys().filter((k) => !ids.has(k.id)));
|
|
410
|
+
// a session a revoked key opened ends with it
|
|
411
|
+
if (Object.values(this.sessions()).some((x) => x.key !== undefined && ids.has(x.key))) this.keepSessions(Object.fromEntries(Object.entries(this.sessions()).filter(([, x]) => x.key === undefined || !ids.has(x.key))));
|
|
410
412
|
// the keys are gone whatever a removal answers; a branch that stays is said, not hidden (its parent key no longer opens this World)
|
|
411
413
|
for (const k of [...gone, ...also]) if (k.branch) await this.host.branches?.remove(k.branch).catch((error: unknown) => { console.error(`${this.served}: branch ${k.branch} stays after its key was revoked: ${error instanceof Error ? error.message : String(error)}`); return false; });
|
|
412
414
|
return [...ids];
|
|
@@ -447,8 +449,10 @@ export class WorldDoors {
|
|
|
447
449
|
// every key made by or for a person: their leaving the org ends what they hold here (the token's act)
|
|
448
450
|
const who = new URL(request.url).searchParams.get('person');
|
|
449
451
|
if (via !== 'token' || !who) return Response.json({ error: 'DELETE /keys?person=<subject>, with the World\'s token' }, { status: via !== 'token' ? 403 : 400 });
|
|
450
|
-
// their keys, the parent keys of the branches they made, and those branches
|
|
451
|
-
|
|
452
|
+
// their keys, the parent keys of the branches they made, and those branches; and the sessions their passes opened
|
|
453
|
+
const revoked = await this.revokeKeys((k) => person(k) === who);
|
|
454
|
+
this.keepSessions(Object.fromEntries(Object.entries(this.sessions()).filter(([, s]) => s.sub !== who)));
|
|
455
|
+
return Response.json({ revoked });
|
|
452
456
|
}
|
|
453
457
|
if (id && request.method === 'DELETE') {
|
|
454
458
|
const found = this.keys().find((k) => k.id === id);
|
|
@@ -931,17 +935,25 @@ export class WorldDoors {
|
|
|
931
935
|
return Response.json({ twin: name, state, view: cut.view, position: cut.position, tree: readTree(state, controlRoot, { view: cut.view }) });
|
|
932
936
|
}
|
|
933
937
|
if (kind === 'changesets' && request.method === 'GET') {
|
|
934
|
-
|
|
935
|
-
|
|
938
|
+
// with the body's hash as it stands, so a reader sees an approval or a verification made on other bytes as stale
|
|
939
|
+
if (name) { const l = findWorldChangeset(name, { root: this.worldRoot, world: this.name }); return Response.json({ ...l.changeset, currentHash: changesetContentHash(l.changeset) }); }
|
|
940
|
+
return Response.json({ changesets: listWorldChangesets({ root: this.worldRoot, world: this.name }).map((l) => ({ ...l.changeset, currentHash: changesetContentHash(l.changeset) })) });
|
|
936
941
|
}
|
|
937
942
|
if (kind === 'changesets' && name && verb === 'verify' && request.method === 'POST') {
|
|
938
943
|
const outcome = await this.serialized('changesets', () => verifyWorldChangeset(name, { root: this.worldRoot, world: this.name, ephemeral: true }));
|
|
939
944
|
return Response.json(outcome);
|
|
940
945
|
}
|
|
941
946
|
if (kind === 'changesets' && name && verb === 'approve' && request.method === 'POST') {
|
|
942
|
-
const body = (await request.json()) as { as?: string; note?: string };
|
|
943
|
-
|
|
944
|
-
|
|
947
|
+
const body = (await request.json().catch(() => ({}))) as { as?: string; note?: string };
|
|
948
|
+
// a browser's session signs as whom it was opened for (a platform's pass names the person; this machine's own page
|
|
949
|
+
// is the person at it), never as whatever the page says; a token or a key names its principal, as before, and so
|
|
950
|
+
// does a session the World's write token opened (it is that token's holder, who names themselves)
|
|
951
|
+
const held = via === 'session' ? this.sessions()[this.credential(request).session ?? ''] : undefined;
|
|
952
|
+
const named = typeof body.as === 'string' && body.as.trim() ? body.as.trim().slice(0, 80) : undefined;
|
|
953
|
+
const tokenSession = held !== undefined && !held.who && held.key === undefined && held.scope === 'write';
|
|
954
|
+
const principal = via === 'session' ? held?.who ?? (this.host.localTrust?.() ? 'this machine' : tokenSession ? named : undefined) : named;
|
|
955
|
+
if (!principal) return Response.json({ error: via === 'session' && !tokenSession ? 'this session names no person to sign as: open the World from its platform, or approve with its token' : 'approve: { as, note? } — name who approves' }, { status: via === 'session' && !tokenSession ? 403 : 400 });
|
|
956
|
+
return Response.json(approveWorldChangeset(name, { root: this.worldRoot, world: this.name, principal, ...(body.note ? { note: body.note } : {}) }));
|
|
945
957
|
}
|
|
946
958
|
if (kind === 'links' && !name && request.method === 'GET') return Response.json({ links: this.links() });
|
|
947
959
|
if (kind === 'links' && name && request.method === 'PUT') {
|
|
@@ -1165,8 +1177,10 @@ export class WorldDoors {
|
|
|
1165
1177
|
if (request.method === 'DELETE') { this.endSession(held); return Response.json({ world: this.served, ended: true }, { headers: { 'set-cookie': this.sessionCookie(url, '', '; Max-Age=0')! } }); }
|
|
1166
1178
|
const cookie = this.sessionCookie(url, held ?? this.openSession(scope, undefined, key))!;
|
|
1167
1179
|
const who = held ? this.sessions()[held]?.who : undefined; const issuer = held ? this.sessions()[held]?.issuer : undefined;
|
|
1168
|
-
// `local`: this page is the local World's own (localPage), so the console offers no sign-in or sign-out
|
|
1169
|
-
|
|
1180
|
+
// `local`: this page is the local World's own (localPage), so the console offers no sign-in or sign-out;
|
|
1181
|
+
// `signsAs`: whom an approval from this session is recorded as, or null when the page must name them (a token's)
|
|
1182
|
+
const signsAs = who ?? (this.host.localTrust?.() ? 'this machine' : null);
|
|
1183
|
+
return Response.json({ world: this.served, scope, signsAs, ...(who ? { who } : {}), ...(issuer ? { issuer } : {}), ...(own ? { origin: own } : {}), ...(this.localPage(request, url) ? { local: true } : {}) }, { headers: { 'set-cookie': cookie } });
|
|
1170
1184
|
}
|
|
1171
1185
|
|
|
1172
1186
|
/** THE CLOCK: the frozen instant every twin stamps from, kept in the World's store. It moves only
|