@volter/world-runtime 3.0.162 → 3.0.164

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.
Files changed (140) hide show
  1. package/dist/src/ancestry.js +2 -2
  2. package/dist/src/app-url.d.ts +5 -1
  3. package/dist/src/app-url.js +14 -1
  4. package/dist/src/branch.js +28 -8
  5. package/dist/src/browser-files.d.ts +5 -0
  6. package/dist/src/browser-files.js +14 -0
  7. package/dist/src/browsers.d.ts +2 -3
  8. package/dist/src/browsers.js +4 -4
  9. package/dist/src/catalog.d.ts +2 -1
  10. package/dist/src/catalog.js +3 -2
  11. package/dist/src/changeset.js +1 -1
  12. package/dist/src/cli-commands.js +71 -18
  13. package/dist/src/cli-load-clock.d.ts +1 -0
  14. package/dist/src/cli-load-clock.js +2 -0
  15. package/dist/src/cli.js +5 -1
  16. package/dist/src/clickhouse-backing.d.ts +2 -0
  17. package/dist/src/clickhouse-backing.js +145 -0
  18. package/dist/src/command-lifetime.d.mts +1 -1
  19. package/dist/src/command-lifetime.mjs +5 -2
  20. package/dist/src/configs.js +1 -1
  21. package/dist/src/consumers.js +1 -1
  22. package/dist/src/covers.d.ts +4 -2
  23. package/dist/src/covers.js +8 -4
  24. package/dist/src/go-mysql-binding.d.ts +19 -0
  25. package/dist/src/go-mysql-binding.js +51 -0
  26. package/dist/src/guest-processes.d.mts +1 -1
  27. package/dist/src/guest-processes.mjs +22 -14
  28. package/dist/src/host-cli.js +8 -1
  29. package/dist/src/host-ready.d.ts +16 -2
  30. package/dist/src/host-ready.js +66 -39
  31. package/dist/src/host-worker.js +7 -1
  32. package/dist/src/host.d.ts +26 -0
  33. package/dist/src/host.js +93 -3
  34. package/dist/src/infra-definition.d.ts +35 -0
  35. package/dist/src/infra-definition.js +146 -0
  36. package/dist/src/infra.d.ts +3 -0
  37. package/dist/src/infra.js +375 -105
  38. package/dist/src/init.d.ts +46 -0
  39. package/dist/src/init.js +445 -18
  40. package/dist/src/lifecycle-record.d.ts +5 -1
  41. package/dist/src/lifecycle-record.js +27 -4
  42. package/dist/src/local-branches.js +1 -1
  43. package/dist/src/mariadb-bootstrap.d.ts +32 -0
  44. package/dist/src/mariadb-bootstrap.js +90 -0
  45. package/dist/src/mariadb-supplier.d.ts +23 -0
  46. package/dist/src/mariadb-supplier.js +215 -0
  47. package/dist/src/mongodb-backing.d.ts +4 -0
  48. package/dist/src/mongodb-backing.js +71 -1
  49. package/dist/src/native-mariadb-backing.d.ts +13 -0
  50. package/dist/src/native-mariadb-backing.js +512 -0
  51. package/dist/src/native-postgres-backing.d.ts +4 -10
  52. package/dist/src/native-postgres-backing.js +428 -43
  53. package/dist/src/native-server-inputs.d.ts +16 -0
  54. package/dist/src/native-server-inputs.js +204 -0
  55. package/dist/src/origin.js +3 -3
  56. package/dist/src/original-inputs.d.ts +14 -0
  57. package/dist/src/original-inputs.js +84 -0
  58. package/dist/src/pack-facts.d.ts +9 -1
  59. package/dist/src/pglite-backing.d.ts +41 -17
  60. package/dist/src/pglite-backing.js +213 -79
  61. package/dist/src/pglite-host.mjs +482 -10
  62. package/dist/src/process-groups.d.mts +6 -1
  63. package/dist/src/process-groups.d.ts +2 -1
  64. package/dist/src/process-groups.js +1 -1
  65. package/dist/src/process-groups.mjs +82 -15
  66. package/dist/src/process-signal.d.mts +2 -2
  67. package/dist/src/process-signal.mjs +29 -9
  68. package/dist/src/project-inspect.d.ts +12 -0
  69. package/dist/src/project-inspect.js +41 -4
  70. package/dist/src/proxy-env.d.ts +34 -0
  71. package/dist/src/proxy-env.js +114 -0
  72. package/dist/src/redirect-proxy.d.ts +1 -31
  73. package/dist/src/redirect-proxy.js +3 -112
  74. package/dist/src/redis-backing.d.ts +6 -4
  75. package/dist/src/redis-backing.js +20 -168
  76. package/dist/src/runtime.d.ts +1 -0
  77. package/dist/src/runtime.js +503 -218
  78. package/dist/src/schema.d.ts +1 -0
  79. package/dist/src/schema.js +5 -3
  80. package/dist/src/served-world.js +15 -10
  81. package/dist/src/service-exit.d.ts +3 -2
  82. package/dist/src/service-exit.js +7 -5
  83. package/dist/src/twin-backing.d.ts +47 -0
  84. package/dist/src/twin-backing.js +343 -0
  85. package/dist/src/up-cli.js +56 -3
  86. package/package.json +4 -4
  87. package/src/ancestry.ts +2 -2
  88. package/src/app-url.ts +9 -2
  89. package/src/branch.ts +26 -8
  90. package/src/browser-files.ts +15 -0
  91. package/src/browsers.ts +4 -4
  92. package/src/catalog.ts +4 -3
  93. package/src/changeset.ts +1 -1
  94. package/src/cli-commands.ts +85 -18
  95. package/src/cli-load-clock.ts +2 -0
  96. package/src/cli.ts +5 -1
  97. package/src/clickhouse-backing.ts +70 -0
  98. package/src/command-lifetime.d.mts +1 -1
  99. package/src/command-lifetime.mjs +5 -2
  100. package/src/configs.ts +1 -1
  101. package/src/consumers.ts +1 -1
  102. package/src/covers.ts +9 -4
  103. package/src/go-mysql-binding.ts +49 -0
  104. package/src/guest-processes.d.mts +1 -1
  105. package/src/guest-processes.mjs +22 -14
  106. package/src/host-cli.ts +7 -1
  107. package/src/host-ready.ts +62 -31
  108. package/src/host-worker.ts +6 -2
  109. package/src/host.ts +88 -6
  110. package/src/infra-definition.ts +135 -0
  111. package/src/infra.ts +250 -45
  112. package/src/init.ts +416 -17
  113. package/src/lifecycle-record.ts +18 -4
  114. package/src/local-branches.ts +1 -1
  115. package/src/mariadb-bootstrap.ts +107 -0
  116. package/src/mariadb-supplier.ts +199 -0
  117. package/src/mongodb-backing.ts +59 -1
  118. package/src/native-mariadb-backing.ts +418 -0
  119. package/src/native-postgres-backing.ts +353 -41
  120. package/src/native-server-inputs.ts +169 -0
  121. package/src/origin.ts +3 -3
  122. package/src/original-inputs.ts +72 -0
  123. package/src/pack-facts.ts +1 -1
  124. package/src/pglite-backing.ts +192 -80
  125. package/src/pglite-host.mjs +482 -10
  126. package/src/process-groups.d.mts +6 -1
  127. package/src/process-groups.mjs +82 -15
  128. package/src/process-groups.ts +2 -1
  129. package/src/process-signal.d.mts +2 -2
  130. package/src/process-signal.mjs +29 -9
  131. package/src/project-inspect.ts +37 -4
  132. package/src/proxy-env.ts +128 -0
  133. package/src/redirect-proxy.ts +3 -125
  134. package/src/redis-backing.ts +19 -136
  135. package/src/runtime.ts +342 -107
  136. package/src/schema.ts +5 -4
  137. package/src/served-world.ts +14 -10
  138. package/src/service-exit.ts +5 -3
  139. package/src/twin-backing.ts +300 -0
  140. package/src/up-cli.ts +52 -3
@@ -0,0 +1,169 @@
1
+ // World init owns this copy. Custody never initializes a server or admits a program.
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
4
+ import { dirname, isAbsolute, join, posix, relative } from 'node:path';
5
+ import { nativeMariaDBReference, type NativeMariaDBReference } from './mariadb-supplier.ts';
6
+
7
+ export interface NativeServerInputs extends NativeMariaDBReference {
8
+ image: string;
9
+ declarationSha256: string;
10
+ snapshotSha256: string;
11
+ }
12
+ const sha = (value: Uint8Array): string => createHash('sha256').update(value).digest('hex');
13
+ const hex = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9]{64}$/u.test(value);
14
+ const record = (value: unknown): Record<string, unknown> => {
15
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Native managed inputs need object metadata');
16
+ return value as Record<string, unknown>;
17
+ };
18
+ const regular = (path: string): Buffer => {
19
+ const stat = lstatSync(path);
20
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Native input must be a regular owned file');
21
+ return readFileSync(path);
22
+ };
23
+ const confined = (root: string, name: string): string => {
24
+ if (!name || isAbsolute(name) || name.includes('\\') || name.includes('\0') || posix.normalize(name) !== name
25
+ || name.split('/').some(part => !part || part === '.' || part === '..')) throw new Error('Native input path is not confined');
26
+ const path = join(root, name), rel = relative(root, realpathSync(path));
27
+ if (!rel || rel === '..' || rel.startsWith('../') || isAbsolute(rel)) throw new Error('Native input path leaves its owned root');
28
+ return path;
29
+ };
30
+
31
+ export function nativeServerReference(value: unknown): NativeServerInputs {
32
+ const row = record(value);
33
+ const { image, declarationSha256, snapshotSha256, ...provider } = row;
34
+ const reference = nativeMariaDBReference(provider);
35
+ if (typeof image !== 'string' || !/^docker\.io\/library\/mariadb@sha256:[a-f0-9]{64}$/u.test(image)
36
+ || !hex(declarationSha256) || !hex(snapshotSha256))
37
+ throw new Error('Native managed source requires a resolved image and exact snapshot/declaration');
38
+ return { ...reference, image, declarationSha256, snapshotSha256 };
39
+ }
40
+
41
+ export function nativeServerRuntimeReference(reference: NativeServerInputs): NativeMariaDBReference {
42
+ const { image: _image, declarationSha256: _declaration, snapshotSha256: _snapshot, ...provider } = reference;
43
+ return { ...provider, directory: `native-inputs/${reference.receiptSha256}/${reference.declarationSha256}` };
44
+ }
45
+
46
+ /** Validate metadata and use these verified owned byte copies at the later write boundary. */
47
+ export function readNativeServerInputs(parent: string, value: unknown): { reference: NativeServerInputs; files: ReadonlyMap<string, Buffer>; aliases: string[] } {
48
+ const reference = nativeServerReference(value), base = realpathSync(parent);
49
+ const root = confined(base, reference.directory), stat = lstatSync(root);
50
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('Native managed snapshot must be a regular directory');
51
+ const manifestBytes = regular(confined(root, 'manifest.json'));
52
+ if (sha(manifestBytes) !== reference.snapshotSha256) throw new Error('Native managed snapshot changed');
53
+ const manifest = record(JSON.parse(manifestBytes.toString('utf8'))), measured = record(manifest.files);
54
+ if (manifest.kind !== 'volter.native-server-inputs' || manifest.version !== 1 || manifest.receiptSha256 !== reference.receiptSha256
55
+ || Object.keys(manifest).some(key => !['kind', 'version', 'receiptSha256', 'files'].includes(key)))
56
+ throw new Error('Invalid native managed snapshot');
57
+ const required = ['artifact.json', 'source-receipt.json', 'recipe-inputs/recipe.json', 'recipe-inputs/release.json',
58
+ 'recipe-inputs/image-index.json', 'recipe-inputs/image-manifest.json', 'recipe-inputs/image-config.json', 'declaration.json'];
59
+ if (Object.keys(measured).sort().join('\0') !== required.sort().join('\0')) throw new Error('Native runtime evidence subset differs');
60
+ const files = new Map<string, Buffer>();
61
+ for (const [name, value] of Object.entries(measured)) {
62
+ const pin = record(value), bytes = regular(confined(root, name));
63
+ if (Object.keys(pin).some(key => !['bytes', 'sha256'].includes(key)) || !Number.isSafeInteger(pin.bytes)
64
+ || pin.bytes !== bytes.length || !hex(pin.sha256) || sha(bytes) !== pin.sha256) throw new Error('Native managed input bytes changed: ' + name);
65
+ files.set(name, bytes);
66
+ }
67
+ const inventory: string[] = [];
68
+ const walk = (directory: string): void => {
69
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
70
+ const path = join(directory, entry.name);
71
+ if (entry.isDirectory()) walk(path); else inventory.push(relative(root, path).split(posix.sep).join('/'));
72
+ }
73
+ };
74
+ walk(root);
75
+ if (inventory.sort().join('\0') !== [...required, 'manifest.json'].sort().join('\0')) throw new Error('Native managed snapshot contains unmeasured files');
76
+ const receiptBytes = files.get('artifact.json')!, receipt = record(JSON.parse(receiptBytes.toString('utf8')));
77
+ const source = record(receipt.source), recipe = record(receipt.recipe), image = record(receipt.imageBinding), originalMap = record(receipt.files);
78
+ if (sha(receiptBytes) !== reference.receiptSha256 || receipt.kind !== 'sweep.wali-native-managed-server' || receipt.version !== 1
79
+ || receipt.complete !== true || receipt.sourceUnchanged !== true || receipt.qualified !== false || receipt.admitted !== false
80
+ || receipt.target !== 'wasm32-linux-muslwali' || receipt.prefix !== 'prefix/opt/programs/mariadb/' + reference.release
81
+ || source.version !== reference.release || source.sha256 !== reference.sourceSha256
82
+ || recipe.path !== reference.recipe.path || recipe.sha256 !== reference.recipe.sha256
83
+ || sha(files.get('recipe-inputs/recipe.json')!) !== reference.recipe.sha256
84
+ || ![image.indexDigest, image.manifestDigest].includes(reference.image.slice(reference.image.indexOf('@') + 1)))
85
+ throw new Error('Native managed snapshot differs from its actual original producer');
86
+ for (const [name, bytes] of files) if (name !== 'artifact.json' && name !== 'declaration.json') {
87
+ const original = record(originalMap[name]);
88
+ if (original.bytes !== bytes.length || original.sha256 !== sha(bytes)) throw new Error('Native runtime subset is absent from its original receipt map');
89
+ }
90
+ files.set('manifest.json', manifestBytes);
91
+ const declaration = files.get('declaration.json')!;
92
+ if (sha(declaration) !== reference.declarationSha256) throw new Error('Native source declaration bytes changed');
93
+ return { reference, files, aliases: sourceAliases(new TextDecoder('utf-8', { fatal: true }).decode(declaration), reference.release) };
94
+ }
95
+
96
+ /** Every parsed source requirement has a disposition; unsupported start behavior refuses before World writes. */
97
+ function sourceAliases(declaration: string, release: string): string[] {
98
+ const service = record(JSON.parse(declaration));
99
+ // annotated on the name, so each refuse() narrows what follows it (TypeScript's assertion-call rule)
100
+ const refuse: (field: string) => never = (field) => { throw new Error(`native MariaDB ${release} provider-start-input gap: ${field}`); };
101
+ const allowed = ['name', 'image', 'build', 'command', 'entrypoint', 'environment', 'envFiles', 'volumes', 'healthcheck', 'workingDir', 'hostname',
102
+ 'capAdd', 'capDrop', 'privileged', 'user', 'secrets', 'networkMode', 'limits', 'develop', 'ports', 'expose', 'dependsOn', 'hostNames', 'profiles',
103
+ 'ignored', 'connectionBindings', 'environmentInherited'];
104
+ for (const key of Object.keys(service)) if (!allowed.includes(key)) refuse(key);
105
+ for (const key of ['build', 'command', 'entrypoint', 'healthcheck', 'workingDir', 'hostname', 'user', 'networkMode', 'develop', 'connectionBindings'])
106
+ if (service[key] !== undefined) refuse(key);
107
+ if (service.privileged === true) refuse('privileged');
108
+ for (const key of ['envFiles', 'capAdd', 'capDrop', 'secrets', 'dependsOn', 'profiles', 'ignored', 'environmentInherited']) {
109
+ if (!Array.isArray(service[key]) && service[key] !== undefined) refuse(key);
110
+ if (Array.isArray(service[key]) && service[key].length) refuse(key);
111
+ }
112
+ if (Object.keys(record(service.limits)).length) refuse('limits');
113
+ if (!Array.isArray(service.volumes) || service.volumes.length > 1) refuse('volumes');
114
+ for (const item of service.volumes) {
115
+ const mount = record(item);
116
+ if (!['named', 'anonymous'].includes(String(mount.kind)) || mount.target !== '/var/lib/mysql' || mount.readOnly !== false) refuse('volume');
117
+ }
118
+ for (const key of ['ports', 'expose']) {
119
+ if (!Array.isArray(service[key])) refuse(key);
120
+ for (const value of service[key]) if (key === 'expose' ? value !== 3306 : record(value).container !== 3306 || record(value).protocol !== 'tcp') refuse(key);
121
+ }
122
+ const env = record(service.environment), aliases: string[] = [];
123
+ const values = new Map<string, string>();
124
+ for (const [name, value] of Object.entries(env)) {
125
+ const match = /^(MYSQL|MARIADB)_(DATABASE|USER|PASSWORD|ROOT_PASSWORD)$/u.exec(name);
126
+ if (!match || typeof value !== 'string' || !value || value.includes('\0')) refuse(name);
127
+ const field = match[2]!;
128
+ if (values.has(field) && values.get(field) !== value) refuse('conflicting credential aliases: ' + field);
129
+ values.set(field, value as string);
130
+ if (match[1] === 'MARIADB') aliases.push(name);
131
+ }
132
+ return aliases;
133
+ }
134
+
135
+ /** Only World init writes its configuration tree, using the exact bytes it reverified itself. */
136
+ export function copyNativeServerInputs(parent: string, reference: NativeServerInputs, world: string): NativeMariaDBReference {
137
+ const snapshot = readNativeServerInputs(parent, reference);
138
+ const directory = `native-inputs/${reference.receiptSha256}/${reference.declarationSha256}`;
139
+ const root = join(world, directory);
140
+ const collection = join(world, 'native-inputs');
141
+ try {
142
+ const stat = lstatSync(collection);
143
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('Native World input collection is not owned');
144
+ } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
145
+ let exists = true;
146
+ try { lstatSync(root); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; exists = false; }
147
+ if (exists) readNativeServerInputs(world, { ...reference, directory });
148
+ else {
149
+ mkdirSync(collection, { recursive: true, mode: 0o700 });
150
+ const producerDirectory = dirname(root);
151
+ try {
152
+ const stat = lstatSync(producerDirectory);
153
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('Native World producer collection is not owned');
154
+ } catch (error) {
155
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
156
+ mkdirSync(producerDirectory, { mode: 0o700 });
157
+ }
158
+ const staging = root + '.' + randomUUID() + '.partial';
159
+ mkdirSync(staging, { mode: 0o700 });
160
+ for (const [name, bytes] of snapshot.files) {
161
+ const file = join(staging, name);
162
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
163
+ writeFileSync(file, bytes, { flag: 'wx', mode: 0o400 });
164
+ }
165
+ renameSync(staging, root);
166
+ }
167
+ // This subset does not pretend the original map's archives/dependency outputs were copied into the World.
168
+ return nativeServerRuntimeReference(reference);
169
+ }
package/src/origin.ts CHANGED
@@ -62,7 +62,7 @@ export async function fetchFromOrigin(name: string, options: FetchOptions): Prom
62
62
  withHistoryLock(worldPaths(state, controlRoot).dir, () => {
63
63
  const meta = readBranchMeta(state, controlRoot);
64
64
  if (meta?.parent && !meta.origin) throw new Error('Cannot attach a first origin to an existing local fork; clone the origin before branching');
65
- });
65
+ }, { operation: 'origin fetch' });
66
66
  }
67
67
  for (const service of services) {
68
68
  const controlRoot = join(instance.dirs.data, service);
@@ -93,7 +93,7 @@ export async function fetchFromOrigin(name: string, options: FetchOptions): Prom
93
93
  const pointer = selected?.origin ?? selected?.parent;
94
94
  appended[service] = Math.max(0, parentLength - before);
95
95
  behind[service] = pointer ? Math.max(0, parentLength - pointer.position) || (pointer.view === head.view ? 0 : 1) : 0;
96
- });
96
+ }, { operation: 'origin fetch' });
97
97
  continue;
98
98
  }
99
99
  // Download one immutable view, then publish its complete cache head. No mixed pages.
@@ -126,7 +126,7 @@ export async function fetchFromOrigin(name: string, options: FetchOptions): Prom
126
126
  const pointer = selected?.origin ?? selected?.parent;
127
127
  // Changed same-length snapshots are still drift; offsets are scoped to a view.
128
128
  behind[service] = pointer ? Math.max(0, entries.length - pointer.position) || (pointer.remoteView === first.view ? 0 : 1) : 0;
129
- });
129
+ }, { operation: 'origin fetch' });
130
130
 
131
131
  }
132
132
  saveWorldInstance({ ...instance, origin: { ...origin, ...(options.full ? { clonedAt: at_ } : instance.origin?.clonedAt ? { clonedAt: instance.origin.clonedAt } : {}), fetchedAt: at_ } });
@@ -0,0 +1,72 @@
1
+ /** Retained original-input custody, separate from the managed service's mutable data. */
2
+ import * as managedOriginalInputs from '@volter/world-core/managed-original-inputs';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
5
+ import { join, resolve } from 'node:path';
6
+ export type OriginalInputReference = managedOriginalInputs.OriginalInputReference;
7
+
8
+ export function readOriginalInputSnapshot(parent: string, value: unknown): { reference: OriginalInputReference; root: string; bytes: Uint8Array; snapshot: ReturnType<typeof managedOriginalInputs.readSnapshot> } {
9
+ const reference = managedOriginalInputs.referenceOf(value);
10
+ let root = resolve(parent);
11
+ for (const part of reference.root.split('/')) {
12
+ root = join(root, part);
13
+ const stat = lstatSync(root);
14
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('Original-input root traverses a host symlink');
15
+ }
16
+ const file = join(root, reference.manifest), stat = lstatSync(file);
17
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024 * 1024) throw new Error('Invalid original-input manifest file');
18
+ const bytes = readFileSync(file), objectsDir = join(root, 'objects'), directory = lstatSync(objectsDir);
19
+ if (!directory.isDirectory() || directory.isSymbolicLink()) throw new Error('Invalid original-input objects directory');
20
+ const snapshot = managedOriginalInputs.readSnapshot(bytes, reference.sha256, (path, size) => {
21
+ const file = join(root, path), stat = lstatSync(file);
22
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== size) throw new Error('Original-input object differs');
23
+ return readFileSync(file);
24
+ });
25
+ if (snapshot.manifest.inventoryDigest !== reference.inventoryDigest || readdirSync(root).sort().join('\0') !== 'manifest.json\0objects'
26
+ || readdirSync(objectsDir).sort().join('\0') !== [...snapshot.objects.keys()].sort().join('\0')) throw new Error('Original-input closed inventory differs');
27
+ return { reference, root, bytes, snapshot };
28
+ }
29
+
30
+ export function copyOriginalInputSnapshot(from: string, reference: OriginalInputReference, out: string): OriginalInputReference {
31
+ const input = readOriginalInputSnapshot(from, reference);
32
+ const next: OriginalInputReference = { ...reference, root: `original-inputs/${reference.sha256}` };
33
+ const parent = join(out, 'original-inputs');
34
+ if (existsSync(parent) && (!lstatSync(parent).isDirectory() || lstatSync(parent).isSymbolicLink())) throw new Error('Original-input output parent is not an owned directory');
35
+ mkdirSync(parent, { recursive: true, mode: 0o700 });
36
+ const destination = join(out, next.root);
37
+ if (existsSync(destination)) {
38
+ readOriginalInputSnapshot(out, next);
39
+ return next;
40
+ }
41
+ const staging = destination + '.' + randomUUID() + '.partial';
42
+ mkdirSync(join(staging, 'objects'), { recursive: true, mode: 0o700 });
43
+ for (const [sha256, bytes] of input.snapshot.objects) writeFileSync(join(staging, 'objects', sha256), bytes, { mode: 0o400, flag: 'wx' });
44
+ writeFileSync(join(staging, 'manifest.json'), input.bytes, { mode: 0o400, flag: 'wx' });
45
+ renameSync(staging, destination);
46
+ chmodSync(join(destination, "objects"), 0o500); chmodSync(destination, 0o500);
47
+ return next;
48
+ }
49
+
50
+ /** A retained store cannot silently acquire another source declaration on resume. */
51
+ export function verifyOriginalInputState(root: string, reference: OriginalInputReference): void {
52
+ const marker = join(root, '.original-inputs.json');
53
+ const stat = lstatSync(marker);
54
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096) throw new Error('Invalid retained original-input state identity');
55
+ const value = JSON.parse(readFileSync(marker, 'utf8')) as { kind?: unknown; version?: unknown; sha256?: unknown };
56
+ if (value.kind !== 'volter.managed-original-input-state' || value.version !== 1 || value.sha256 !== reference.sha256) throw new Error('Retained managed state belongs to different original inputs; use an explicit fresh World or reset');
57
+ }
58
+
59
+ export function bindOriginalInputState(root: string, reference: OriginalInputReference): void {
60
+ const marker = join(root, '.original-inputs.json');
61
+ if (existsSync(marker)) { verifyOriginalInputState(root, reference); return; }
62
+ if (readdirSync(root).length) throw new Error('Retained managed state has no original-input identity; a fresh source-bound state is required');
63
+ writeFileSync(marker, JSON.stringify({ kind: 'volter.managed-original-input-state', version: 1, sha256: reference.sha256 }) + '\n', { flag: 'wx', mode: 0o600 });
64
+ }
65
+
66
+ export function preflightOriginalInputState(root: string, reference: OriginalInputReference): void {
67
+ if (!existsSync(root)) return;
68
+ const directory = lstatSync(root);
69
+ if (!directory.isDirectory() || directory.isSymbolicLink()) throw new Error('Managed original-input state root is not an owned directory');
70
+ if (existsSync(join(root, '.original-inputs.json'))) verifyOriginalInputState(root, reference);
71
+ else if (readdirSync(root).length) throw new Error('Retained managed state has no original-input identity; a fresh source-bound state is required');
72
+ }
package/src/pack-facts.ts CHANGED
@@ -34,7 +34,7 @@ export type PackFacts = {
34
34
  * `colocate:` service entries so a world's twins boot inside ONE host process (R2a). */
35
35
  serveExport?: string;
36
36
  /** The managed-infrastructure kind this pack is the World's backing for (architecture.md, "The descriptor"). */
37
- managedService?: { kind: 'redis' | 'mongodb'; scheme: string; note: string };
37
+ managedService?: { kind: 'redis' | 'mongodb' | 'clickhouse'; scheme: string; note: string; configurationArg?: '--configuration'; originalInputs?: { version: 1; manifestArg: '--original-inputs'; rootArg: '--original-root'; digestArg: '--original-inputs-sha256'; paths: string[] } };
38
38
  /** The World's managed database the runtime binds the twin to at boot (TwinPack.managedDatabase). */
39
39
  managedDatabase?: { kind: 'postgres'; arg: string; note: string };
40
40
  /** Listeners the twin opens beside its main port, allocated and recorded by the runtime (TwinPack.sidePorts). */