@braedonsaunders/appkit-desk 0.10.0 → 0.11.0
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/README.md +117 -0
- package/backend.d.ts +13 -1
- package/backend.d.ts.map +1 -1
- package/backend.js +23 -9
- package/backend.js.map +1 -1
- package/host.d.ts +33 -5
- package/host.d.ts.map +1 -1
- package/host.js +216 -18
- package/host.js.map +1 -1
- package/index.d.ts +3 -0
- package/index.d.ts.map +1 -1
- package/index.js +3 -0
- package/index.js.map +1 -1
- package/machine.d.ts +4 -2
- package/machine.d.ts.map +1 -1
- package/machine.js +5 -3
- package/machine.js.map +1 -1
- package/package.json +1 -1
- package/plan.d.ts +6 -1
- package/plan.d.ts.map +1 -1
- package/plan.js +7 -3
- package/plan.js.map +1 -1
- package/template-mapping.d.ts +5 -0
- package/template-mapping.d.ts.map +1 -0
- package/template-mapping.js +48 -0
- package/template-mapping.js.map +1 -0
- package/template-plan.d.ts +18 -0
- package/template-plan.d.ts.map +1 -0
- package/template-plan.js +52 -0
- package/template-plan.js.map +1 -0
- package/template-registry.d.ts +37 -0
- package/template-registry.d.ts.map +1 -0
- package/template-registry.js +190 -0
- package/template-registry.js.map +1 -0
- package/template-source.d.ts +15 -0
- package/template-source.d.ts.map +1 -0
- package/template-source.js +95 -0
- package/template-source.js.map +1 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants, createReadStream } from 'node:fs';
|
|
3
|
+
import { chmod, copyFile, link, lstat, mkdir, open, readFile, stat } from 'node:fs/promises';
|
|
4
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
5
|
+
import { cleanDeskId } from './plan.js';
|
|
6
|
+
import { planTemplateConfig } from './template-plan.js';
|
|
7
|
+
import { assertPinnedTemplateMemory, pinTemplateMemory } from './template-source.js';
|
|
8
|
+
async function digest(path, immutable) {
|
|
9
|
+
if (!isAbsolute(path))
|
|
10
|
+
throw new Error('Template paths must be absolute');
|
|
11
|
+
const before = await lstat(path);
|
|
12
|
+
if (!before.isFile() || before.isSymbolicLink() || (immutable && (before.mode & 0o222) !== 0))
|
|
13
|
+
throw new Error(`Template file must be ${immutable ? 'read-only and ' : ''}regular: ${path}`);
|
|
14
|
+
const hash = createHash('sha256');
|
|
15
|
+
for await (const chunk of createReadStream(path))
|
|
16
|
+
hash.update(chunk);
|
|
17
|
+
const after = await lstat(path);
|
|
18
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
|
|
19
|
+
throw new Error(`Template file changed during verification: ${path}`);
|
|
20
|
+
return hash.digest('hex');
|
|
21
|
+
}
|
|
22
|
+
async function measure(snapshot, diskPath, compatibility) {
|
|
23
|
+
if (!isAbsolute(snapshot) || !compatibility.cpuSignature || !compatibility.vmmVersion || !compatibility.componentVersion)
|
|
24
|
+
throw new Error('Invalid template compatibility identity');
|
|
25
|
+
const diskFiles = [join(snapshot, 'config.json'), join(snapshot, 'state.json'), join(snapshot, 'memory-ranges'), diskPath];
|
|
26
|
+
const boundFiles = [compatibility.vmmPath, compatibility.baseImagePath, compatibility.kernelPath, compatibility.initramfsPath, ...compatibility.attachmentPaths];
|
|
27
|
+
const all = [...diskFiles, ...boundFiles];
|
|
28
|
+
if (new Set(all).size !== all.length)
|
|
29
|
+
throw new Error('Template file identities must be distinct');
|
|
30
|
+
const files = {};
|
|
31
|
+
for (const [index, path] of all.entries())
|
|
32
|
+
files[path] = await digest(path, index < diskFiles.length);
|
|
33
|
+
return { version: 1, diskPath, compatibility, files };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Register a stopped, specially prepared template. The caller must ensure the
|
|
37
|
+
* guest was fenced and contains no user data or credentials; hashing cannot
|
|
38
|
+
* prove provenance. Every source file is kept read-only and protected against
|
|
39
|
+
* writers for as long as any child can map its memory.
|
|
40
|
+
*/
|
|
41
|
+
export async function registerDeskTemplate(snapshot, diskPath, compatibility) {
|
|
42
|
+
const registration = await measure(snapshot, diskPath, compatibility);
|
|
43
|
+
const config = JSON.parse(await readFile(join(snapshot, 'config.json'), 'utf8'));
|
|
44
|
+
if (config.payload?.kernel !== compatibility.kernelPath || config.payload.initramfs !== compatibility.initramfsPath)
|
|
45
|
+
throw new Error('Template kernel or initramfs does not match its configuration');
|
|
46
|
+
if (config.disks?.[0]?.path !== diskPath || config.disks[0].readonly === true || JSON.stringify(config.disks.slice(1).map(disk => disk.path)) !== JSON.stringify(compatibility.attachmentPaths) || config.disks.slice(1).some(disk => disk.readonly !== true))
|
|
47
|
+
throw new Error('Template disk and attachments do not match its configuration');
|
|
48
|
+
const file = await open(join(snapshot, 'appkit-template.json'), 'wx', 0o600);
|
|
49
|
+
try {
|
|
50
|
+
await file.writeFile(JSON.stringify(registration));
|
|
51
|
+
await file.sync();
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await file.close();
|
|
55
|
+
}
|
|
56
|
+
const directory = await open(snapshot, 'r');
|
|
57
|
+
try {
|
|
58
|
+
await directory.sync();
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await directory.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Prepare a child in a new private directory, without starting it. The caller
|
|
66
|
+
* must hold a network/workload fence until guest reinitialization succeeds.
|
|
67
|
+
* A failed or interrupted child directory is deliberately left for recovery.
|
|
68
|
+
*/
|
|
69
|
+
export async function forkDeskTemplate(snapshot, childDirectory, child, compatibility) {
|
|
70
|
+
if (!isAbsolute(childDirectory))
|
|
71
|
+
throw new Error('Child directory must be absolute');
|
|
72
|
+
const registered = JSON.parse(await readFile(join(snapshot, 'appkit-template.json'), 'utf8'));
|
|
73
|
+
if (registered.version !== 1 || typeof registered.diskPath !== 'string' || JSON.stringify(registered.compatibility) !== JSON.stringify(compatibility))
|
|
74
|
+
throw new Error('Template compatibility does not match');
|
|
75
|
+
const measured = await measure(snapshot, registered.diskPath, compatibility);
|
|
76
|
+
if (JSON.stringify(measured) !== JSON.stringify(registered))
|
|
77
|
+
throw new Error('Template source changed since registration');
|
|
78
|
+
const diskPath = join(childDirectory, 'disk.raw');
|
|
79
|
+
const snapshotPath = join(childDirectory, `${cleanDeskId(child.deskId)}.snapshot`);
|
|
80
|
+
const config = planTemplateConfig(JSON.parse(await readFile(join(snapshot, 'config.json'), 'utf8')), { ...child, diskPath });
|
|
81
|
+
await mkdir(childDirectory, { mode: 0o700 });
|
|
82
|
+
const parent = await open(dirname(childDirectory), 'r');
|
|
83
|
+
try {
|
|
84
|
+
await parent.sync();
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await parent.close();
|
|
88
|
+
}
|
|
89
|
+
await copyFile(registered.diskPath, diskPath, constants.COPYFILE_EXCL | constants.COPYFILE_FICLONE);
|
|
90
|
+
await chmod(diskPath, 0o600);
|
|
91
|
+
const [sourceDisk, childDisk] = await Promise.all([stat(registered.diskPath), stat(diskPath)]);
|
|
92
|
+
if (sourceDisk.dev === childDisk.dev && sourceDisk.ino === childDisk.ino)
|
|
93
|
+
throw new Error('Child disk must be independent');
|
|
94
|
+
if (await digest(diskPath, false) !== registered.files[registered.diskPath])
|
|
95
|
+
throw new Error('Child disk does not match the registered template');
|
|
96
|
+
const disk = await open(diskPath, 'r');
|
|
97
|
+
try {
|
|
98
|
+
await disk.sync();
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await disk.close();
|
|
102
|
+
}
|
|
103
|
+
await pinTemplateMemory(snapshot, snapshotPath, config);
|
|
104
|
+
await assertPinnedTemplateMemory(snapshot, snapshotPath);
|
|
105
|
+
// Retain the shared source across both resident and parked descendants.
|
|
106
|
+
await link(join(snapshotPath, 'memory-ranges'), join(childDirectory, '.template-memory'));
|
|
107
|
+
const attachments = compatibility.attachmentPaths.map(path => ({ path, sha256: registered.files[path], kind: 'block' }));
|
|
108
|
+
for (const [name, value] of [['appkit-cpu-signature', compatibility.cpuSignature], ['attachments.json', JSON.stringify(attachments)]]) {
|
|
109
|
+
const file = await open(join(snapshotPath, name), 'wx', 0o600);
|
|
110
|
+
try {
|
|
111
|
+
await file.writeFile(value);
|
|
112
|
+
await file.sync();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await file.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const snapshotDir = await open(snapshotPath, 'r');
|
|
119
|
+
try {
|
|
120
|
+
await snapshotDir.sync();
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await snapshotDir.close();
|
|
124
|
+
}
|
|
125
|
+
const binding = await open(join(childDirectory, '.template-binding.json'), 'wx', 0o600);
|
|
126
|
+
try {
|
|
127
|
+
await binding.writeFile(JSON.stringify({ version: 1, snapshot, compatibility }));
|
|
128
|
+
await binding.sync();
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await binding.close();
|
|
132
|
+
}
|
|
133
|
+
const marker = await open(join(childDirectory, '.prepared'), 'wx', 0o600);
|
|
134
|
+
try {
|
|
135
|
+
await marker.writeFile('1\n');
|
|
136
|
+
await marker.sync();
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
await marker.close();
|
|
140
|
+
}
|
|
141
|
+
const directory = await open(childDirectory, 'r');
|
|
142
|
+
try {
|
|
143
|
+
await directory.sync();
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
await directory.close();
|
|
147
|
+
}
|
|
148
|
+
return { diskPath, snapshotPath };
|
|
149
|
+
}
|
|
150
|
+
async function validatedBinding(childDirectory, compatibility) {
|
|
151
|
+
const binding = JSON.parse(await readFile(join(childDirectory, '.template-binding.json'), 'utf8'));
|
|
152
|
+
if (binding.version !== 1 || typeof binding.snapshot !== 'string' || !isAbsolute(binding.snapshot) || JSON.stringify(binding.compatibility) !== JSON.stringify(compatibility))
|
|
153
|
+
throw new Error('Template fork compatibility does not match');
|
|
154
|
+
const registered = JSON.parse(await readFile(join(binding.snapshot, 'appkit-template.json'), 'utf8'));
|
|
155
|
+
if (registered.version !== 1 || typeof registered.diskPath !== 'string' || JSON.stringify(registered.compatibility) !== JSON.stringify(compatibility))
|
|
156
|
+
throw new Error('Template registration does not match');
|
|
157
|
+
if (JSON.stringify(await measure(binding.snapshot, registered.diskPath, compatibility)) !== JSON.stringify(registered))
|
|
158
|
+
throw new Error('Template source changed since registration');
|
|
159
|
+
const [source, pin] = await Promise.all([stat(join(binding.snapshot, 'memory-ranges')), stat(join(childDirectory, '.template-memory'))]);
|
|
160
|
+
if (source.dev !== pin.dev || source.ino !== pin.ino)
|
|
161
|
+
throw new Error('Template lifetime pin does not match');
|
|
162
|
+
return { snapshot: binding.snapshot, registered };
|
|
163
|
+
}
|
|
164
|
+
/** Revalidate the registered source and lifetime pin for a parked descendant. */
|
|
165
|
+
export async function verifyDeskTemplateCompatibility(childDirectory, compatibility) {
|
|
166
|
+
await validatedBinding(childDirectory, compatibility);
|
|
167
|
+
}
|
|
168
|
+
/** Revalidate an unconsumed prepared child after an interrupted host start. */
|
|
169
|
+
export async function verifyDeskTemplateFork(childDirectory, child, compatibility) {
|
|
170
|
+
if (await readFile(join(childDirectory, '.prepared'), 'utf8') !== '1\n')
|
|
171
|
+
throw new Error('Incomplete template fork');
|
|
172
|
+
const binding = await validatedBinding(childDirectory, compatibility);
|
|
173
|
+
const registered = binding.registered;
|
|
174
|
+
const diskPath = join(childDirectory, 'disk.raw');
|
|
175
|
+
const snapshotPath = join(childDirectory, `${cleanDeskId(child.deskId)}.snapshot`);
|
|
176
|
+
await assertPinnedTemplateMemory(binding.snapshot, snapshotPath);
|
|
177
|
+
if (await digest(diskPath, false) !== registered.files[registered.diskPath])
|
|
178
|
+
throw new Error('Prepared child disk changed');
|
|
179
|
+
if (await digest(join(snapshotPath, 'state.json'), false) !== registered.files[join(binding.snapshot, 'state.json')])
|
|
180
|
+
throw new Error('Prepared child device state changed');
|
|
181
|
+
const expectedConfig = planTemplateConfig(JSON.parse(await readFile(join(binding.snapshot, 'config.json'), 'utf8')), { ...child, diskPath });
|
|
182
|
+
if (JSON.stringify(JSON.parse(await readFile(join(snapshotPath, 'config.json'), 'utf8'))) !== JSON.stringify(expectedConfig))
|
|
183
|
+
throw new Error('Prepared child configuration changed');
|
|
184
|
+
if ((await readFile(join(snapshotPath, 'appkit-cpu-signature'), 'utf8')).trim() !== compatibility.cpuSignature)
|
|
185
|
+
throw new Error('Prepared child CPU identity changed');
|
|
186
|
+
const attachments = compatibility.attachmentPaths.map(path => ({ path, sha256: registered.files[path], kind: 'block' }));
|
|
187
|
+
if (await readFile(join(snapshotPath, 'attachments.json'), 'utf8') !== JSON.stringify(attachments))
|
|
188
|
+
throw new Error('Prepared child attachments changed');
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=template-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-registry.js","sourceRoot":"","sources":["../src/template-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto'\nimport { constants, createReadStream } from 'node:fs'\nimport { chmod, copyFile, link, lstat, mkdir, open, readFile, stat } from 'node:fs/promises'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport { cleanDeskId } from './plan'\nimport { planTemplateConfig, type DeskTemplateChildIdentity } from './template-plan'\nimport { assertPinnedTemplateMemory, pinTemplateMemory } from './template-source'\n\n/** Compatibility inputs must describe the actual VMM and host CPU, not labels. */\nexport interface DeskTemplateCompatibility {\n vmmVersion: string\n vmmPath: string\n cpuSignature: string\n componentVersion: string\n baseImagePath: string\n kernelPath: string\n initramfsPath: string\n attachmentPaths: readonly string[]\n}\n\ninterface Registration {\n version: 1\n diskPath: string\n compatibility: DeskTemplateCompatibility\n files: Record<string, string>\n}\n\nasync function digest(path: string, immutable: boolean): Promise<string> {\n if (!isAbsolute(path)) throw new Error('Template paths must be absolute')\n const before = await lstat(path)\n if (!before.isFile() || before.isSymbolicLink() || (immutable && (before.mode & 0o222) !== 0)) throw new Error(`Template file must be ${immutable ? 'read-only and ' : ''}regular: ${path}`)\n const hash = createHash('sha256')\n for await (const chunk of createReadStream(path)) hash.update(chunk as Buffer)\n const after = await lstat(path)\n if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) throw new Error(`Template file changed during verification: ${path}`)\n return hash.digest('hex')\n}\n\nasync function measure(snapshot: string, diskPath: string, compatibility: DeskTemplateCompatibility): Promise<Registration> {\n if (!isAbsolute(snapshot) || !compatibility.cpuSignature || !compatibility.vmmVersion || !compatibility.componentVersion) throw new Error('Invalid template compatibility identity')\n const diskFiles = [join(snapshot, 'config.json'), join(snapshot, 'state.json'), join(snapshot, 'memory-ranges'), diskPath]\n const boundFiles = [compatibility.vmmPath, compatibility.baseImagePath, compatibility.kernelPath, compatibility.initramfsPath, ...compatibility.attachmentPaths]\n const all = [...diskFiles, ...boundFiles]\n if (new Set(all).size !== all.length) throw new Error('Template file identities must be distinct')\n const files: Record<string, string> = {}\n for (const [index, path] of all.entries()) files[path] = await digest(path, index < diskFiles.length)\n return { version: 1, diskPath, compatibility, files }\n}\n\n/**\n * Register a stopped, specially prepared template. The caller must ensure the\n * guest was fenced and contains no user data or credentials; hashing cannot\n * prove provenance. Every source file is kept read-only and protected against\n * writers for as long as any child can map its memory.\n */\nexport async function registerDeskTemplate(snapshot: string, diskPath: string, compatibility: DeskTemplateCompatibility): Promise<void> {\n const registration = await measure(snapshot, diskPath, compatibility)\n const config = JSON.parse(await readFile(join(snapshot, 'config.json'), 'utf8')) as { disks?: { path?: string; readonly?: boolean }[]; payload?: { kernel?: string; initramfs?: string } }\n if (config.payload?.kernel !== compatibility.kernelPath || config.payload.initramfs !== compatibility.initramfsPath) throw new Error('Template kernel or initramfs does not match its configuration')\n if (config.disks?.[0]?.path !== diskPath || config.disks[0].readonly === true || JSON.stringify(config.disks.slice(1).map(disk => disk.path)) !== JSON.stringify(compatibility.attachmentPaths) || config.disks.slice(1).some(disk => disk.readonly !== true)) throw new Error('Template disk and attachments do not match its configuration')\n const file = await open(join(snapshot, 'appkit-template.json'), 'wx', 0o600)\n try { await file.writeFile(JSON.stringify(registration)); await file.sync() } finally { await file.close() }\n const directory = await open(snapshot, 'r')\n try { await directory.sync() } finally { await directory.close() }\n}\n\n/**\n * Prepare a child in a new private directory, without starting it. The caller\n * must hold a network/workload fence until guest reinitialization succeeds.\n * A failed or interrupted child directory is deliberately left for recovery.\n */\nexport async function forkDeskTemplate(snapshot: string, childDirectory: string, child: Omit<DeskTemplateChildIdentity, 'diskPath'> & { deskId: string }, compatibility: DeskTemplateCompatibility): Promise<{ diskPath: string; snapshotPath: string }> {\n if (!isAbsolute(childDirectory)) throw new Error('Child directory must be absolute')\n const registered = JSON.parse(await readFile(join(snapshot, 'appkit-template.json'), 'utf8')) as Registration\n if (registered.version !== 1 || typeof registered.diskPath !== 'string' || JSON.stringify(registered.compatibility) !== JSON.stringify(compatibility)) throw new Error('Template compatibility does not match')\n const measured = await measure(snapshot, registered.diskPath, compatibility)\n if (JSON.stringify(measured) !== JSON.stringify(registered)) throw new Error('Template source changed since registration')\n const diskPath = join(childDirectory, 'disk.raw')\n const snapshotPath = join(childDirectory, `${cleanDeskId(child.deskId)}.snapshot`)\n const config = planTemplateConfig(JSON.parse(await readFile(join(snapshot, 'config.json'), 'utf8')), { ...child, diskPath })\n await mkdir(childDirectory, { mode: 0o700 })\n const parent = await open(dirname(childDirectory), 'r')\n try { await parent.sync() } finally { await parent.close() }\n await copyFile(registered.diskPath, diskPath, constants.COPYFILE_EXCL | constants.COPYFILE_FICLONE)\n await chmod(diskPath, 0o600)\n const [sourceDisk, childDisk] = await Promise.all([stat(registered.diskPath), stat(diskPath)])\n if (sourceDisk.dev === childDisk.dev && sourceDisk.ino === childDisk.ino) throw new Error('Child disk must be independent')\n if (await digest(diskPath, false) !== registered.files[registered.diskPath]) throw new Error('Child disk does not match the registered template')\n const disk = await open(diskPath, 'r')\n try { await disk.sync() } finally { await disk.close() }\n await pinTemplateMemory(snapshot, snapshotPath, config)\n await assertPinnedTemplateMemory(snapshot, snapshotPath)\n // Retain the shared source across both resident and parked descendants.\n await link(join(snapshotPath, 'memory-ranges'), join(childDirectory, '.template-memory'))\n const attachments = compatibility.attachmentPaths.map(path => ({ path, sha256: registered.files[path], kind: 'block' }))\n for (const [name, value] of [['appkit-cpu-signature', compatibility.cpuSignature], ['attachments.json', JSON.stringify(attachments)]] as const) {\n const file = await open(join(snapshotPath, name), 'wx', 0o600)\n try { await file.writeFile(value); await file.sync() } finally { await file.close() }\n }\n const snapshotDir = await open(snapshotPath, 'r')\n try { await snapshotDir.sync() } finally { await snapshotDir.close() }\n const binding = await open(join(childDirectory, '.template-binding.json'), 'wx', 0o600)\n try { await binding.writeFile(JSON.stringify({ version: 1, snapshot, compatibility })); await binding.sync() } finally { await binding.close() }\n const marker = await open(join(childDirectory, '.prepared'), 'wx', 0o600)\n try { await marker.writeFile('1\\n'); await marker.sync() } finally { await marker.close() }\n const directory = await open(childDirectory, 'r')\n try { await directory.sync() } finally { await directory.close() }\n return { diskPath, snapshotPath }\n}\n\nasync function validatedBinding(childDirectory: string, compatibility: DeskTemplateCompatibility): Promise<{ snapshot: string; registered: Registration }> {\n const binding = JSON.parse(await readFile(join(childDirectory, '.template-binding.json'), 'utf8')) as { version?: unknown; snapshot?: unknown; compatibility?: unknown }\n if (binding.version !== 1 || typeof binding.snapshot !== 'string' || !isAbsolute(binding.snapshot) || JSON.stringify(binding.compatibility) !== JSON.stringify(compatibility)) throw new Error('Template fork compatibility does not match')\n const registered = JSON.parse(await readFile(join(binding.snapshot, 'appkit-template.json'), 'utf8')) as Registration\n if (registered.version !== 1 || typeof registered.diskPath !== 'string' || JSON.stringify(registered.compatibility) !== JSON.stringify(compatibility)) throw new Error('Template registration does not match')\n if (JSON.stringify(await measure(binding.snapshot, registered.diskPath, compatibility)) !== JSON.stringify(registered)) throw new Error('Template source changed since registration')\n const [source, pin] = await Promise.all([stat(join(binding.snapshot, 'memory-ranges')), stat(join(childDirectory, '.template-memory'))])\n if (source.dev !== pin.dev || source.ino !== pin.ino) throw new Error('Template lifetime pin does not match')\n return { snapshot: binding.snapshot, registered }\n}\n\n/** Revalidate the registered source and lifetime pin for a parked descendant. */\nexport async function verifyDeskTemplateCompatibility(childDirectory: string, compatibility: DeskTemplateCompatibility): Promise<void> {\n await validatedBinding(childDirectory, compatibility)\n}\n\n/** Revalidate an unconsumed prepared child after an interrupted host start. */\nexport async function verifyDeskTemplateFork(childDirectory: string, child: Omit<DeskTemplateChildIdentity, 'diskPath'> & { deskId: string }, compatibility: DeskTemplateCompatibility): Promise<void> {\n if (await readFile(join(childDirectory, '.prepared'), 'utf8') !== '1\\n') throw new Error('Incomplete template fork')\n const binding = await validatedBinding(childDirectory, compatibility)\n const registered = binding.registered\n const diskPath = join(childDirectory, 'disk.raw')\n const snapshotPath = join(childDirectory, `${cleanDeskId(child.deskId)}.snapshot`)\n await assertPinnedTemplateMemory(binding.snapshot, snapshotPath)\n if (await digest(diskPath, false) !== registered.files[registered.diskPath]) throw new Error('Prepared child disk changed')\n if (await digest(join(snapshotPath, 'state.json'), false) !== registered.files[join(binding.snapshot, 'state.json')]) throw new Error('Prepared child device state changed')\n const expectedConfig = planTemplateConfig(JSON.parse(await readFile(join(binding.snapshot, 'config.json'), 'utf8')), { ...child, diskPath })\n if (JSON.stringify(JSON.parse(await readFile(join(snapshotPath, 'config.json'), 'utf8'))) !== JSON.stringify(expectedConfig)) throw new Error('Prepared child configuration changed')\n if ((await readFile(join(snapshotPath, 'appkit-cpu-signature'), 'utf8')).trim() !== compatibility.cpuSignature) throw new Error('Prepared child CPU identity changed')\n const attachments = compatibility.attachmentPaths.map(path => ({ path, sha256: registered.files[path], kind: 'block' }))\n if (await readFile(join(snapshotPath, 'attachments.json'), 'utf8') !== JSON.stringify(attachments)) throw new Error('Prepared child attachments changed')\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create an exclusive snapshot directory whose memory file is the SAME inode
|
|
3
|
+
* as the source. The caller owns the source and must keep its bytes immutable
|
|
4
|
+
* until every linked child has exited. Reflinking memory would not share the
|
|
5
|
+
* page cache. The caller must validate and rewrite the configuration and fence
|
|
6
|
+
* the guest before this directory can be restored.
|
|
7
|
+
*
|
|
8
|
+
* The source and destination must be on the same filesystem. A hard link is a
|
|
9
|
+
* durable pin: unlinking the source name cannot invalidate a resident mapping.
|
|
10
|
+
* This function never overwrites an existing destination.
|
|
11
|
+
*/
|
|
12
|
+
export declare function pinTemplateMemory(source: string, destination: string, config: unknown): Promise<void>;
|
|
13
|
+
/** Reject incomplete fork publication before handing it to a VMM. */
|
|
14
|
+
export declare function assertPinnedTemplateMemory(source: string, destination: string): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=template-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-source.d.ts","sourceRoot":"","sources":["../src/template-source.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA+C3G;AAED,qEAAqE;AACrE,wBAAsB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMnG"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { copyFile, link, lstat, mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
/**
|
|
6
|
+
* Create an exclusive snapshot directory whose memory file is the SAME inode
|
|
7
|
+
* as the source. The caller owns the source and must keep its bytes immutable
|
|
8
|
+
* until every linked child has exited. Reflinking memory would not share the
|
|
9
|
+
* page cache. The caller must validate and rewrite the configuration and fence
|
|
10
|
+
* the guest before this directory can be restored.
|
|
11
|
+
*
|
|
12
|
+
* The source and destination must be on the same filesystem. A hard link is a
|
|
13
|
+
* durable pin: unlinking the source name cannot invalidate a resident mapping.
|
|
14
|
+
* This function never overwrites an existing destination.
|
|
15
|
+
*/
|
|
16
|
+
export async function pinTemplateMemory(source, destination, config) {
|
|
17
|
+
const sourceMemory = join(source, 'memory-ranges');
|
|
18
|
+
const memory = await lstat(sourceMemory);
|
|
19
|
+
if (!memory.isFile() || memory.isSymbolicLink() || (memory.mode & 0o222) !== 0) {
|
|
20
|
+
throw new Error('Template memory must be a read-only regular file');
|
|
21
|
+
}
|
|
22
|
+
const parent = dirname(destination);
|
|
23
|
+
const stage = join(parent, `.${basename(destination)}.${randomUUID()}.tmp`);
|
|
24
|
+
await mkdir(stage, { mode: 0o700 });
|
|
25
|
+
try {
|
|
26
|
+
await link(sourceMemory, join(stage, 'memory-ranges'));
|
|
27
|
+
const pinned = await stat(join(stage, 'memory-ranges'));
|
|
28
|
+
const current = await lstat(sourceMemory);
|
|
29
|
+
if (!current.isFile() || current.dev !== memory.dev || current.ino !== memory.ino || pinned.dev !== memory.dev || pinned.ino !== memory.ino || (current.mode & 0o222) !== 0) {
|
|
30
|
+
throw new Error('Template memory changed while pinning');
|
|
31
|
+
}
|
|
32
|
+
// state.json is private to the child, so it cannot mutate the template.
|
|
33
|
+
await copyFile(join(source, 'state.json'), join(stage, 'state.json'), constants.COPYFILE_EXCL);
|
|
34
|
+
await writeFile(join(stage, 'config.json'), JSON.stringify(config), { flag: 'wx', mode: 0o600 });
|
|
35
|
+
for (const name of ['state.json', 'config.json']) {
|
|
36
|
+
const file = await open(join(stage, name), 'r');
|
|
37
|
+
try {
|
|
38
|
+
await file.sync();
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await file.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const dir = await open(stage, 'r');
|
|
45
|
+
try {
|
|
46
|
+
await dir.sync();
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await dir.close();
|
|
50
|
+
}
|
|
51
|
+
// Reserve the destination first: POSIX rename would otherwise replace an
|
|
52
|
+
// existing empty directory, potentially invalidating a competing fork.
|
|
53
|
+
await mkdir(destination, { mode: 0o700 });
|
|
54
|
+
try {
|
|
55
|
+
// Publishing individual entries into the reserved directory keeps the
|
|
56
|
+
// hard-link pin even if the process dies halfway through publication.
|
|
57
|
+
for (const name of ['memory-ranges', 'state.json', 'config.json']) {
|
|
58
|
+
await rename(join(stage, name), join(destination, name));
|
|
59
|
+
}
|
|
60
|
+
await writeFile(join(destination, '.complete'), '1\n', { flag: 'wx', mode: 0o600 });
|
|
61
|
+
const published = await open(destination, 'r');
|
|
62
|
+
try {
|
|
63
|
+
await published.sync();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await published.close();
|
|
67
|
+
}
|
|
68
|
+
const parentDir = await open(parent, 'r');
|
|
69
|
+
try {
|
|
70
|
+
await parentDir.sync();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await parentDir.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// An incomplete destination is deliberately retained; a recovery owner
|
|
78
|
+
// must establish that no VMM uses it before removing it.
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await rm(stage, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Reject incomplete fork publication before handing it to a VMM. */
|
|
87
|
+
export async function assertPinnedTemplateMemory(source, destination) {
|
|
88
|
+
if (await readFile(join(destination, '.complete'), 'utf8') !== '1\n')
|
|
89
|
+
throw new Error('Incomplete template fork');
|
|
90
|
+
const [original, pinned] = await Promise.all([lstat(join(source, 'memory-ranges')), lstat(join(destination, 'memory-ranges'))]);
|
|
91
|
+
if (!original.isFile() || !pinned.isFile() || original.dev !== pinned.dev || original.ino !== pinned.ino || (original.mode & 0o222) !== 0) {
|
|
92
|
+
throw new Error('Template fork does not pin the immutable memory source');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=template-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-source.js","sourceRoot":"","sources":["../src/template-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { constants } from 'node:fs'\nimport { copyFile, link, lstat, mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'\nimport { basename, dirname, join } from 'node:path'\nimport { randomUUID } from 'node:crypto'\n\n/**\n * Create an exclusive snapshot directory whose memory file is the SAME inode\n * as the source. The caller owns the source and must keep its bytes immutable\n * until every linked child has exited. Reflinking memory would not share the\n * page cache. The caller must validate and rewrite the configuration and fence\n * the guest before this directory can be restored.\n *\n * The source and destination must be on the same filesystem. A hard link is a\n * durable pin: unlinking the source name cannot invalidate a resident mapping.\n * This function never overwrites an existing destination.\n */\nexport async function pinTemplateMemory(source: string, destination: string, config: unknown): Promise<void> {\n const sourceMemory = join(source, 'memory-ranges')\n const memory = await lstat(sourceMemory)\n if (!memory.isFile() || memory.isSymbolicLink() || (memory.mode & 0o222) !== 0) {\n throw new Error('Template memory must be a read-only regular file')\n }\n const parent = dirname(destination)\n const stage = join(parent, `.${basename(destination)}.${randomUUID()}.tmp`)\n await mkdir(stage, { mode: 0o700 })\n try {\n await link(sourceMemory, join(stage, 'memory-ranges'))\n const pinned = await stat(join(stage, 'memory-ranges'))\n const current = await lstat(sourceMemory)\n if (!current.isFile() || current.dev !== memory.dev || current.ino !== memory.ino || pinned.dev !== memory.dev || pinned.ino !== memory.ino || (current.mode & 0o222) !== 0) {\n throw new Error('Template memory changed while pinning')\n }\n // state.json is private to the child, so it cannot mutate the template.\n await copyFile(join(source, 'state.json'), join(stage, 'state.json'), constants.COPYFILE_EXCL)\n await writeFile(join(stage, 'config.json'), JSON.stringify(config), { flag: 'wx', mode: 0o600 })\n for (const name of ['state.json', 'config.json']) {\n const file = await open(join(stage, name), 'r')\n try { await file.sync() } finally { await file.close() }\n }\n const dir = await open(stage, 'r')\n try { await dir.sync() } finally { await dir.close() }\n // Reserve the destination first: POSIX rename would otherwise replace an\n // existing empty directory, potentially invalidating a competing fork.\n await mkdir(destination, { mode: 0o700 })\n try {\n // Publishing individual entries into the reserved directory keeps the\n // hard-link pin even if the process dies halfway through publication.\n for (const name of ['memory-ranges', 'state.json', 'config.json']) {\n await rename(join(stage, name), join(destination, name))\n }\n await writeFile(join(destination, '.complete'), '1\\n', { flag: 'wx', mode: 0o600 })\n const published = await open(destination, 'r')\n try { await published.sync() } finally { await published.close() }\n const parentDir = await open(parent, 'r')\n try { await parentDir.sync() } finally { await parentDir.close() }\n } catch (error) {\n // An incomplete destination is deliberately retained; a recovery owner\n // must establish that no VMM uses it before removing it.\n throw error\n }\n } finally {\n await rm(stage, { recursive: true, force: true })\n }\n}\n\n/** Reject incomplete fork publication before handing it to a VMM. */\nexport async function assertPinnedTemplateMemory(source: string, destination: string): Promise<void> {\n if (await readFile(join(destination, '.complete'), 'utf8') !== '1\\n') throw new Error('Incomplete template fork')\n const [original, pinned] = await Promise.all([lstat(join(source, 'memory-ranges')), lstat(join(destination, 'memory-ranges'))])\n if (!original.isFile() || !pinned.isFile() || original.dev !== pinned.dev || original.ino !== pinned.ino || (original.mode & 0o222) !== 0) {\n throw new Error('Template fork does not pin the immutable memory source')\n }\n}\n"]}
|