@volter/world-runtime 2.0.46 → 2.0.48

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.
@@ -39,7 +39,7 @@ export { fetchFromOrigin, worldOrigin } from './origin.js';
39
39
  export type { FetchOptions, FetchOutcome } from './origin.js';
40
40
  export { branchWorld, checkoutWorld } from './branch.js';
41
41
  export type { BranchOptions } from './branch.js';
42
- export { deployWorld, refreshTwin, adaptersFor, credentialPath, credentialPayloadFrom, rootCredentialInput, bindLegacyCredentials, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from './root.js';
42
+ export { deployWorld, refreshTwin, adaptersFor, credentialPath, rewrapTwinCredentials, credentialPayloadFrom, rootCredentialInput, bindLegacyCredentials, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from './root.js';
43
43
  export type { DeployTwinOutcome, MaterializedRoot } from './root.js';
44
44
  export { landReceipts, mountWorld, READ_ONLY_HEADER, worldOriginLabel, readServeRecord, refreshSchedule, servedName, serveWorld, sessionsFile, keysFile, TOKEN_HEADER, WorldDoors } from './served-world.js';
45
45
  export type { BranchDoors, BranchRow, DoorHost, MountedWorld, ServeRecord, WorldLayout } from './served-world.js';
package/dist/src/index.js CHANGED
@@ -29,7 +29,7 @@ export { advertiseWorldManifest, fetchRemoteManifest, MANIFEST_PATH, remoteAttac
29
29
  export { resetWorld, seedEntryFor, seedWorld } from "./placeholder.js";
30
30
  export { fetchFromOrigin, worldOrigin } from "./origin.js";
31
31
  export { branchWorld, checkoutWorld } from "./branch.js";
32
- export { deployWorld, refreshTwin, adaptersFor, credentialPath, credentialPayloadFrom, rootCredentialInput, bindLegacyCredentials, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from "./root.js";
32
+ export { deployWorld, refreshTwin, adaptersFor, credentialPath, rewrapTwinCredentials, credentialPayloadFrom, rootCredentialInput, bindLegacyCredentials, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from "./root.js";
33
33
  export { landReceipts, mountWorld, READ_ONLY_HEADER, worldOriginLabel, readServeRecord, refreshSchedule, servedName, serveWorld, sessionsFile, keysFile, TOKEN_HEADER, WorldDoors } from "./served-world.js";
34
34
  export { LocalBranches } from "./local-branches.js";
35
35
  export { serveWorldView, VIEW_CONSOLE_BASE } from "./world-view.js";
@@ -5,6 +5,16 @@ import type { WorldConfig } from './schema.js';
5
5
  * seals is wrapped under. Made on first use, 0600, never printed. */
6
6
  export { userKek, userKekPath } from '@volter/world-core';
7
7
  export declare function credentialPath(worldRoot: string, vendor: string): string;
8
+ /** Rewrap every credential sealed beside the World (each vendor's and each link's) under the newest version of the key
9
+ * that wrapped it: an operator's act after rotating the key, so no record still needs an old version. `current` is only
10
+ * what the sealer vouched for; a record it cannot (sealed under a host key, or by another vault key, a version it
11
+ * could not read), one changed while it was rewrapped (sealed again meanwhile, never overwritten) or one that does not
12
+ * read is `left`, by name. */
13
+ export declare function rewrapTwinCredentials(worldRoot: string): Promise<{
14
+ rewrapped: string[];
15
+ current: string[];
16
+ left: string[];
17
+ }>;
8
18
  /** Seal a vendor's credential beside the World, bound to `bound` (destinationOf the root's or link's origin): it opens
9
19
  * for that destination only, so pointing the root or link elsewhere never sends it there. */
10
20
  export declare function sealTwinCredential(worldRoot: string, vendor: string, payload: CredentialPayload, at: string | undefined, bound: string): Promise<{
package/dist/src/root.js CHANGED
@@ -30,6 +30,53 @@ export { userKek, userKekPath } from '@volter/world-core';
30
30
  export function credentialPath(worldRoot, vendor) {
31
31
  return join(resolve(worldRoot), stateDirName(), 'credentials', `${vendor}.json`);
32
32
  }
33
+ /** Rewrap every credential sealed beside the World (each vendor's and each link's) under the newest version of the key
34
+ * that wrapped it: an operator's act after rotating the key, so no record still needs an old version. `current` is only
35
+ * what the sealer vouched for; a record it cannot (sealed under a host key, or by another vault key, a version it
36
+ * could not read), one changed while it was rewrapped (sealed again meanwhile, never overwritten) or one that does not
37
+ * read is `left`, by name. */
38
+ export async function rewrapTwinCredentials(worldRoot) {
39
+ const store = getActiveWorldStore();
40
+ const sealer = currentSealer();
41
+ const dir = dirname(credentialPath(worldRoot, 'x'));
42
+ const names = store.list(dir).filter((f) => /^[a-z0-9][a-z0-9_-]*\.json$/.test(f)).map((f) => f.replace(/\.json$/, ''));
43
+ const out = { rewrapped: [], current: [], left: [] };
44
+ for (const name of names) {
45
+ const path = credentialPath(worldRoot, name);
46
+ const text = store.read(path);
47
+ if (text === null)
48
+ continue;
49
+ let sealed;
50
+ try {
51
+ sealed = JSON.parse(text);
52
+ }
53
+ catch {
54
+ out.left.push(name);
55
+ continue;
56
+ }
57
+ if (!sealer.rewrap) {
58
+ out.left.push(name);
59
+ continue;
60
+ }
61
+ const result = await sealer.rewrap(sealed);
62
+ if (result.status === 'current') {
63
+ out.current.push(name);
64
+ continue;
65
+ }
66
+ if (result.status === 'foreign') {
67
+ out.left.push(name);
68
+ continue;
69
+ }
70
+ // sealed again while the vault rewrapped it: the new credential stands, and this record is not reported moved
71
+ if (store.read(path) !== text) {
72
+ out.left.push(name);
73
+ continue;
74
+ }
75
+ store.write(path, `${JSON.stringify(result.sealed, null, 2)}\n`, { secret: true });
76
+ out.rewrapped.push(name);
77
+ }
78
+ return out;
79
+ }
33
80
  /** Seal a vendor's credential beside the World, bound to `bound` (destinationOf the root's or link's origin): it opens
34
81
  * for that destination only, so pointing the root or link elsewhere never sends it there. */
35
82
  export async function sealTwinCredential(worldRoot, vendor, payload, at, bound) {
@@ -36,7 +36,7 @@ import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChange
36
36
  import { fetchFromOrigin } from "./origin.js";
37
37
  import { CONSOLE_BASE, consoleRedirect, serveConsoleApart } from "./console-apart.js";
38
38
  import { loadWorldConfig } from "./configs.js";
39
- import { adaptersFor, credentialPath, packMirror, credentialPayloadFrom, deployTwin, deployWorld, loadWorldChecks, twinSigningSecret, materializeRoots, refreshPosture, refreshTwin, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, bindLegacyCredentials, setTwinRoot, spanMs } from "./root.js";
39
+ import { adaptersFor, credentialPath, packMirror, credentialPayloadFrom, deployTwin, deployWorld, loadWorldChecks, twinSigningSecret, materializeRoots, refreshPosture, refreshTwin, rootForControlRoot, rewrapTwinCredentials, sealTwinCredential, sealedCredentialInfo, bindLegacyCredentials, setTwinRoot, spanMs } from "./root.js";
40
40
  import { clockFile, downWorld, listWorlds, statusWorld, upWorld } from "./runtime.js";
41
41
  export const TOKEN_HEADER = 'x-volter-token';
42
42
  /** The header the injector carries a hosted World's token in (inject.cjs, VOLTER_TWINS_KEY). */
@@ -1256,9 +1256,19 @@ export class WorldDoors {
1256
1256
  const opensSeal = (kind === 'links' && name && (request.method === 'PUT' || request.method === 'DELETE'))
1257
1257
  || (kind === 'twins' && name && (verb === 'root' || verb === 'credential') && request.method === 'PUT')
1258
1258
  // a check runs in a local World's own process, with all it can reach: putting or removing one is the same act
1259
- || (kind === 'checks' && name && (request.method === 'PUT' || request.method === 'DELETE'));
1259
+ || (kind === 'checks' && name && (request.method === 'PUT' || request.method === 'DELETE'))
1260
+ // rewrapping the World's sealed credentials is its holder's act, after a rotation of the key that wrapped them
1261
+ || (kind === 'credentials' && name === 'rewrap' && request.method === 'POST');
1260
1262
  if (opensSeal && via === 'key')
1261
1263
  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 });
1264
+ // THE REWRAP: after an operator rotates the vault key this World's credentials are wrapped under, each record's DEK
1265
+ // is wrapped again under the newest version (transit/rewrap: the DEK never leaves the vault); the answer names
1266
+ // which moved, which were already current, and which this sealer does not wrap (sealed under a host key)
1267
+ if (kind === 'credentials' && name === 'rewrap' && request.method === 'POST') {
1268
+ if (scope !== 'write')
1269
+ return Response.json({ error: 'rewrapping needs the World\'s write access' }, { status: 403 });
1270
+ return Response.json(await this.serialized('credentials', () => rewrapTwinCredentials(this.worldRoot)));
1271
+ }
1262
1272
  if (kind === 'links' && name && request.method === 'PUT') {
1263
1273
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name) || this.up().services[name])
1264
1274
  return Response.json({ error: 'a link is named [a-z0-9_-] and is not a twin of this World' }, { status: 400 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "2.0.46",
3
+ "version": "2.0.48",
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.28",
70
+ "@volter/world-core": "2.0.30",
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.28",
77
+ "@volter/world-core": "2.0.30",
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.28"
87
+ "@volter/twin-mongodb": "0.1.30"
88
88
  }
89
89
  }
package/src/index.ts CHANGED
@@ -161,7 +161,7 @@ export type { FetchOptions, FetchOutcome } from './origin.ts';
161
161
 
162
162
  export { branchWorld, checkoutWorld } from './branch.ts';
163
163
  export type { BranchOptions } from './branch.ts';
164
- export { deployWorld, refreshTwin, adaptersFor, credentialPath, credentialPayloadFrom, rootCredentialInput, bindLegacyCredentials, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from './root.ts';
164
+ export { deployWorld, refreshTwin, adaptersFor, credentialPath, rewrapTwinCredentials, credentialPayloadFrom, rootCredentialInput, bindLegacyCredentials, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from './root.ts';
165
165
  export type { DeployTwinOutcome, MaterializedRoot } from './root.ts';
166
166
  export { landReceipts, mountWorld, READ_ONLY_HEADER, worldOriginLabel, readServeRecord, refreshSchedule, servedName, serveWorld, sessionsFile, keysFile, TOKEN_HEADER, WorldDoors } from './served-world.ts';
167
167
  export type { BranchDoors, BranchRow, DoorHost, MountedWorld, ServeRecord, WorldLayout } from './served-world.ts';
package/src/root.ts CHANGED
@@ -37,6 +37,33 @@ export { userKek, userKekPath } from '@volter/world-core';
37
37
  export function credentialPath(worldRoot: string, vendor: string): string {
38
38
  return join(resolve(worldRoot), stateDirName(), 'credentials', `${vendor}.json`);
39
39
  }
40
+ /** Rewrap every credential sealed beside the World (each vendor's and each link's) under the newest version of the key
41
+ * that wrapped it: an operator's act after rotating the key, so no record still needs an old version. `current` is only
42
+ * what the sealer vouched for; a record it cannot (sealed under a host key, or by another vault key, a version it
43
+ * could not read), one changed while it was rewrapped (sealed again meanwhile, never overwritten) or one that does not
44
+ * read is `left`, by name. */
45
+ export async function rewrapTwinCredentials(worldRoot: string): Promise<{ rewrapped: string[]; current: string[]; left: string[] }> {
46
+ const store = getActiveWorldStore(); const sealer = currentSealer();
47
+ const dir = dirname(credentialPath(worldRoot, 'x'));
48
+ const names = store.list(dir).filter((f) => /^[a-z0-9][a-z0-9_-]*\.json$/.test(f)).map((f) => f.replace(/\.json$/, ''));
49
+ const out = { rewrapped: [] as string[], current: [] as string[], left: [] as string[] };
50
+ for (const name of names) {
51
+ const path = credentialPath(worldRoot, name);
52
+ const text = store.read(path); if (text === null) continue;
53
+ let sealed: SealedCredential;
54
+ try { sealed = JSON.parse(text) as SealedCredential; } catch { out.left.push(name); continue; }
55
+ if (!sealer.rewrap) { out.left.push(name); continue; }
56
+ const result = await sealer.rewrap(sealed);
57
+ if (result.status === 'current') { out.current.push(name); continue; }
58
+ if (result.status === 'foreign') { out.left.push(name); continue; }
59
+ // sealed again while the vault rewrapped it: the new credential stands, and this record is not reported moved
60
+ if (store.read(path) !== text) { out.left.push(name); continue; }
61
+ store.write(path, `${JSON.stringify(result.sealed, null, 2)}\n`, { secret: true });
62
+ out.rewrapped.push(name);
63
+ }
64
+ return out;
65
+ }
66
+
40
67
  /** Seal a vendor's credential beside the World, bound to `bound` (destinationOf the root's or link's origin): it opens
41
68
  * for that destination only, so pointing the root or link elsewhere never sends it there. */
42
69
  export async function sealTwinCredential(worldRoot: string, vendor: string, payload: CredentialPayload, at: string | undefined, bound: string): Promise<{ placedAt: string; fingerprint: string }> {
@@ -39,7 +39,7 @@ import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChange
39
39
  import { fetchFromOrigin } from './origin.ts';
40
40
  import { CONSOLE_BASE, consoleRedirect, serveConsoleApart, type ConsoleMount } from './console-apart.ts';
41
41
  import { loadWorldConfig } from './configs.ts';
42
- import { adaptersFor, credentialPath, packMirror, type PackMirror, credentialPayloadFrom, deployTwin, deployWorld, loadWorldChecks, twinSigningSecret, materializeRoots, refreshPosture, refreshTwin, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, bindLegacyCredentials, setTwinRoot, spanMs } from './root.ts';
42
+ import { adaptersFor, credentialPath, packMirror, type PackMirror, credentialPayloadFrom, deployTwin, deployWorld, loadWorldChecks, twinSigningSecret, materializeRoots, refreshPosture, refreshTwin, rootForControlRoot, rewrapTwinCredentials, sealTwinCredential, sealedCredentialInfo, bindLegacyCredentials, setTwinRoot, spanMs } from './root.ts';
43
43
  import { clockFile, downWorld, listWorlds, statusWorld, upWorld } from './runtime.ts';
44
44
  import type { WorldInstance } from './schema.ts';
45
45
 
@@ -1094,8 +1094,17 @@ export class WorldDoors {
1094
1094
  const opensSeal = (kind === 'links' && name && (request.method === 'PUT' || request.method === 'DELETE'))
1095
1095
  || (kind === 'twins' && name && (verb === 'root' || verb === 'credential') && request.method === 'PUT')
1096
1096
  // a check runs in a local World's own process, with all it can reach: putting or removing one is the same act
1097
- || (kind === 'checks' && name && (request.method === 'PUT' || request.method === 'DELETE'));
1097
+ || (kind === 'checks' && name && (request.method === 'PUT' || request.method === 'DELETE'))
1098
+ // rewrapping the World's sealed credentials is its holder's act, after a rotation of the key that wrapped them
1099
+ || (kind === 'credentials' && name === 'rewrap' && request.method === 'POST');
1098
1100
  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 });
1101
+ // THE REWRAP: after an operator rotates the vault key this World's credentials are wrapped under, each record's DEK
1102
+ // is wrapped again under the newest version (transit/rewrap: the DEK never leaves the vault); the answer names
1103
+ // which moved, which were already current, and which this sealer does not wrap (sealed under a host key)
1104
+ if (kind === 'credentials' && name === 'rewrap' && request.method === 'POST') {
1105
+ if (scope !== 'write') return Response.json({ error: 'rewrapping needs the World\'s write access' }, { status: 403 });
1106
+ return Response.json(await this.serialized('credentials', () => rewrapTwinCredentials(this.worldRoot)));
1107
+ }
1099
1108
  if (kind === 'links' && name && request.method === 'PUT') {
1100
1109
  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 });
1101
1110
  const body = (await request.json()) as { origin?: string };