moflo 4.10.1 → 4.10.3

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.
@@ -1,393 +0,0 @@
1
- /**
2
- * RVFA (MoVector Format Appliance) — Binary format reader/writer
3
- * for self-contained MoFlo appliances.
4
- *
5
- * Binary layout:
6
- * [4B magic "RVFA"] [4B version u32LE] [4B header_len u32LE]
7
- * [header_len B JSON header] [section data ...] [32B SHA256 footer]
8
- */
9
- import { createHash } from 'node:crypto';
10
- import { gzipSync, gunzipSync } from 'node:zlib';
11
- import { readFile } from 'node:fs/promises';
12
- // ---------------------------------------------------------------------------
13
- // Constants
14
- // ---------------------------------------------------------------------------
15
- export const RVFA_MAGIC = Buffer.from('RVFA');
16
- export const RVFA_VERSION = 1;
17
- const MAGIC_SIZE = 4;
18
- const VERSION_SIZE = 4;
19
- const HEADER_LEN_SIZE = 4;
20
- const PREAMBLE_SIZE = MAGIC_SIZE + VERSION_SIZE + HEADER_LEN_SIZE; // 12
21
- const SHA256_SIZE = 32;
22
- const MAX_HEADER_JSON_SIZE = 1024 * 1024; // 1 MB
23
- // ---------------------------------------------------------------------------
24
- // Helpers
25
- // ---------------------------------------------------------------------------
26
- function sha256(data) {
27
- return createHash('sha256').update(data).digest('hex');
28
- }
29
- function sha256Bytes(data) {
30
- return createHash('sha256').update(data).digest();
31
- }
32
- /** Format bytes into a human-readable string (e.g. '2.3 GB'). */
33
- export function formatSize(bytes) {
34
- if (bytes < 0)
35
- return '0 B';
36
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
37
- let idx = 0;
38
- let value = bytes;
39
- while (value >= 1024 && idx < units.length - 1) {
40
- value /= 1024;
41
- idx++;
42
- }
43
- return idx === 0 ? `${value} ${units[idx]}` : `${value.toFixed(1)} ${units[idx]}`;
44
- }
45
- /** Create a sensible default header for a given profile. */
46
- export function createDefaultHeader(profile) {
47
- const modelDefaults = {
48
- cloud: { provider: 'api-vault', vaultEncryption: 'aes-256-gcm' },
49
- hybrid: {
50
- provider: 'hybrid',
51
- engine: 'ruvllm-0.1.0',
52
- models: ['phi-3-mini-q4'],
53
- vaultEncryption: 'aes-256-gcm',
54
- },
55
- offline: {
56
- provider: 'ruvllm',
57
- engine: 'ruvllm-0.1.0',
58
- models: ['phi-3-mini-q4'],
59
- },
60
- };
61
- const capDefaults = {
62
- cloud: ['mcp', 'swarm', 'memory', 'hooks', 'neural', 'api-vault'],
63
- hybrid: ['mcp', 'swarm', 'memory', 'hooks', 'neural', 'ruvllm', 'api-vault'],
64
- offline: ['mcp', 'swarm', 'memory', 'hooks', 'neural', 'ruvllm'],
65
- };
66
- return {
67
- magic: 'RVFA',
68
- version: RVFA_VERSION,
69
- name: '',
70
- appVersion: '3.5.0',
71
- arch: 'x86_64',
72
- platform: 'linux',
73
- profile,
74
- created: new Date().toISOString(),
75
- sections: [],
76
- boot: {
77
- entrypoint: '/opt/ruflo/bin/ruflo',
78
- args: ['--appliance'],
79
- env: {},
80
- isolation: profile === 'cloud' ? 'container' : 'native',
81
- },
82
- models: modelDefaults[profile],
83
- capabilities: capDefaults[profile],
84
- };
85
- }
86
- /** Type-guard that validates an unknown value is a well-formed RvfaHeader. */
87
- export function validateHeader(header) {
88
- if (typeof header !== 'object' || header === null)
89
- return false;
90
- const h = header;
91
- const str = (v) => typeof v === 'string';
92
- const obj = (v) => typeof v === 'object' && v !== null;
93
- const oneOf = (v, vals) => vals.includes(v);
94
- if (h.magic !== 'RVFA' || typeof h.version !== 'number' || h.version < 1)
95
- return false;
96
- if (!str(h.name) || !str(h.appVersion) || !str(h.arch) || !str(h.platform))
97
- return false;
98
- if (!oneOf(h.profile, ['cloud', 'hybrid', 'offline']))
99
- return false;
100
- if (!str(h.created) || !Array.isArray(h.sections) || !Array.isArray(h.capabilities))
101
- return false;
102
- if (!obj(h.boot))
103
- return false;
104
- const boot = h.boot;
105
- if (!str(boot.entrypoint) || !Array.isArray(boot.args) || !obj(boot.env))
106
- return false;
107
- if (!oneOf(boot.isolation, ['container', 'microvm', 'native']))
108
- return false;
109
- if (!obj(h.models))
110
- return false;
111
- if (!oneOf(h.models.provider, ['ruvllm', 'api-vault', 'hybrid']))
112
- return false;
113
- for (const sec of h.sections) {
114
- if (!obj(sec))
115
- return false;
116
- const s = sec;
117
- if (!str(s.id) || !str(s.type) || !str(s.sha256))
118
- return false;
119
- if (typeof s.offset !== 'number' || typeof s.size !== 'number')
120
- return false;
121
- if (typeof s.originalSize !== 'number')
122
- return false;
123
- if (!oneOf(s.compression, ['none', 'gzip', 'zstd']))
124
- return false;
125
- }
126
- return true;
127
- }
128
- export class RvfaWriter {
129
- header;
130
- staged = [];
131
- constructor(partial) {
132
- const profile = partial.profile ?? 'cloud';
133
- const defaults = createDefaultHeader(profile);
134
- this.header = { ...defaults, ...partial, sections: [] };
135
- }
136
- /**
137
- * Add a section to the appliance image.
138
- *
139
- * @param id Section identifier (e.g. 'kernel', 'runtime', 'ruflo').
140
- * @param data Raw (uncompressed) section payload.
141
- * @param options Optional compression and MIME type overrides.
142
- */
143
- addSection(id, data, options) {
144
- const compression = options?.compression ?? 'gzip';
145
- const mimeType = options?.type ?? 'application/octet-stream';
146
- let compressed;
147
- let actualCompression = compression;
148
- if (compression === 'gzip') {
149
- compressed = gzipSync(data);
150
- }
151
- else if (compression === 'zstd') {
152
- // zstd not available in core Node.js — fall back to gzip
153
- compressed = gzipSync(data);
154
- actualCompression = 'gzip';
155
- }
156
- else {
157
- compressed = data;
158
- }
159
- const hash = sha256(compressed);
160
- this.staged.push({
161
- id,
162
- type: mimeType,
163
- data: compressed,
164
- originalSize: data.length,
165
- sha256: hash,
166
- compression: actualCompression,
167
- });
168
- }
169
- /**
170
- * Assemble the final RVFA binary image.
171
- *
172
- * Layout:
173
- * [4B magic] [4B version] [4B header_len]
174
- * [header JSON bytes]
175
- * [section 0 bytes] [section 1 bytes] ...
176
- * [32B SHA256 of all section bytes combined]
177
- */
178
- build() {
179
- // Build section descriptors with placeholder offsets
180
- const sectionDescriptors = this.staged.map((s) => ({
181
- id: s.id,
182
- type: s.type,
183
- offset: 0,
184
- size: s.data.length,
185
- originalSize: s.originalSize,
186
- sha256: s.sha256,
187
- compression: s.compression,
188
- }));
189
- // Iteratively compute offsets: the header contains the offsets as JSON,
190
- // so changing offsets can change the header length. We converge quickly.
191
- this.header.sections = sectionDescriptors;
192
- let headerJson = Buffer.from(JSON.stringify(this.header), 'utf-8');
193
- let prevLen = -1;
194
- for (let attempt = 0; attempt < 5 && headerJson.length !== prevLen; attempt++) {
195
- prevLen = headerJson.length;
196
- const dataAreaStart = PREAMBLE_SIZE + headerJson.length;
197
- let cursor = dataAreaStart;
198
- for (let i = 0; i < this.staged.length; i++) {
199
- sectionDescriptors[i].offset = cursor;
200
- cursor += this.staged[i].data.length;
201
- }
202
- this.header.sections = sectionDescriptors;
203
- headerJson = Buffer.from(JSON.stringify(this.header), 'utf-8');
204
- }
205
- // Build preamble buffers
206
- const magicBuf = Buffer.from('RVFA');
207
- const versionBuf = Buffer.alloc(VERSION_SIZE);
208
- versionBuf.writeUInt32LE(RVFA_VERSION, 0);
209
- const headerLenBuf = Buffer.alloc(HEADER_LEN_SIZE);
210
- headerLenBuf.writeUInt32LE(headerJson.length, 0);
211
- // Concatenate section data
212
- const sectionBuffers = this.staged.map((s) => s.data);
213
- const allSectionData = Buffer.concat(sectionBuffers);
214
- // Footer: SHA256 of all section data combined
215
- const footer = sha256Bytes(allSectionData);
216
- return Buffer.concat([
217
- magicBuf,
218
- versionBuf,
219
- headerLenBuf,
220
- headerJson,
221
- allSectionData,
222
- footer,
223
- ]);
224
- }
225
- }
226
- // ---------------------------------------------------------------------------
227
- // RvfaReader
228
- // ---------------------------------------------------------------------------
229
- export class RvfaReader {
230
- buf;
231
- header;
232
- constructor(buf, header) {
233
- this.buf = buf;
234
- this.header = header;
235
- }
236
- /** Parse an RVFA image from an in-memory Buffer. */
237
- static fromBuffer(buf) {
238
- if (buf.length < PREAMBLE_SIZE) {
239
- throw new Error('Buffer too small to contain RVFA preamble');
240
- }
241
- // Magic
242
- const magic = buf.subarray(0, MAGIC_SIZE).toString('ascii');
243
- if (magic !== 'RVFA') {
244
- throw new Error(`Invalid RVFA magic: expected "RVFA", got "${magic}"`);
245
- }
246
- // Version
247
- const version = buf.readUInt32LE(MAGIC_SIZE);
248
- if (version !== RVFA_VERSION) {
249
- throw new Error(`Unsupported RVFA version: ${version} (expected ${RVFA_VERSION})`);
250
- }
251
- // Header length
252
- const headerLen = buf.readUInt32LE(MAGIC_SIZE + VERSION_SIZE);
253
- if (headerLen > MAX_HEADER_JSON_SIZE) {
254
- throw new Error(`Header JSON exceeds maximum size (${headerLen} > ${MAX_HEADER_JSON_SIZE})`);
255
- }
256
- if (PREAMBLE_SIZE + headerLen > buf.length) {
257
- throw new Error('Buffer too small to contain declared header');
258
- }
259
- // Parse header JSON
260
- const headerSlice = buf.subarray(PREAMBLE_SIZE, PREAMBLE_SIZE + headerLen);
261
- let parsed;
262
- try {
263
- parsed = JSON.parse(headerSlice.toString('utf-8'));
264
- }
265
- catch {
266
- throw new Error('Failed to parse RVFA header JSON');
267
- }
268
- if (!validateHeader(parsed)) {
269
- throw new Error('RVFA header failed validation');
270
- }
271
- const header = parsed;
272
- // Bounds-check every section offset
273
- const totalSize = buf.length;
274
- for (const sec of header.sections) {
275
- if (sec.offset < 0 || sec.size < 0) {
276
- throw new Error(`Section "${sec.id}" has negative offset or size`);
277
- }
278
- if (sec.offset + sec.size > totalSize - SHA256_SIZE) {
279
- throw new Error(`Section "${sec.id}" extends beyond buffer ` +
280
- `(offset=${sec.offset}, size=${sec.size}, bufLen=${totalSize})`);
281
- }
282
- }
283
- // Check for overlapping sections
284
- const sorted = [...header.sections].sort((a, b) => a.offset - b.offset);
285
- for (let i = 1; i < sorted.length; i++) {
286
- const prev = sorted[i - 1];
287
- const curr = sorted[i];
288
- if (prev.offset + prev.size > curr.offset) {
289
- throw new Error(`Sections "${prev.id}" and "${curr.id}" overlap ` +
290
- `(${prev.offset}+${prev.size} > ${curr.offset})`);
291
- }
292
- }
293
- return new RvfaReader(buf, header);
294
- }
295
- /** Read an RVFA image from a file path. */
296
- static async fromFile(path) {
297
- if (path.includes('\0')) {
298
- throw new Error('Path contains null bytes');
299
- }
300
- const data = await readFile(path);
301
- return RvfaReader.fromBuffer(data);
302
- }
303
- /** Return the parsed header. */
304
- getHeader() {
305
- return this.header;
306
- }
307
- /** List all sections declared in the header. */
308
- getSections() {
309
- return this.header.sections;
310
- }
311
- /**
312
- * Extract and decompress a section by its id.
313
- *
314
- * @param id The section identifier (e.g. 'kernel', 'runtime').
315
- * @returns The decompressed section payload.
316
- */
317
- extractSection(id) {
318
- const sec = this.header.sections.find((s) => s.id === id);
319
- if (!sec) {
320
- throw new Error(`Section "${id}" not found`);
321
- }
322
- if (sec.offset + sec.size > this.buf.length - SHA256_SIZE) {
323
- throw new Error(`Section "${id}" exceeds buffer bounds`);
324
- }
325
- const raw = this.buf.subarray(sec.offset, sec.offset + sec.size);
326
- if (sec.compression === 'gzip') {
327
- return gunzipSync(raw);
328
- }
329
- if (sec.compression === 'zstd') {
330
- // zstd not natively supported — attempt gzip fallback (mirrors writer)
331
- try {
332
- return gunzipSync(raw);
333
- }
334
- catch {
335
- throw new Error('zstd decompression is not supported in this environment');
336
- }
337
- }
338
- // compression === 'none'
339
- return Buffer.from(raw);
340
- }
341
- /**
342
- * Verify the integrity of the RVFA image.
343
- *
344
- * Checks:
345
- * 1. Magic bytes
346
- * 2. Version number
347
- * 3. SHA256 of each section's compressed data
348
- * 4. SHA256 footer (all section data combined)
349
- */
350
- verify() {
351
- const errors = [];
352
- // 1. Magic
353
- const magic = this.buf.subarray(0, MAGIC_SIZE).toString('ascii');
354
- if (magic !== 'RVFA') {
355
- errors.push(`Invalid magic: "${magic}"`);
356
- }
357
- // 2. Version
358
- const version = this.buf.readUInt32LE(MAGIC_SIZE);
359
- if (version !== RVFA_VERSION) {
360
- errors.push(`Unsupported version: ${version}`);
361
- }
362
- // 3. Per-section SHA256
363
- const sectionDataParts = [];
364
- for (const sec of this.header.sections) {
365
- if (sec.offset + sec.size > this.buf.length - SHA256_SIZE) {
366
- errors.push(`Section "${sec.id}" extends beyond buffer`);
367
- continue;
368
- }
369
- const raw = this.buf.subarray(sec.offset, sec.offset + sec.size);
370
- sectionDataParts.push(raw);
371
- const actual = sha256(raw);
372
- if (actual !== sec.sha256) {
373
- errors.push(`Section "${sec.id}" SHA256 mismatch: ` +
374
- `expected ${sec.sha256}, got ${actual}`);
375
- }
376
- }
377
- // 4. Footer SHA256
378
- if (this.buf.length >= SHA256_SIZE) {
379
- const allSections = Buffer.concat(sectionDataParts);
380
- const expectedFooter = sha256Bytes(allSections);
381
- const actualFooter = this.buf.subarray(this.buf.length - SHA256_SIZE);
382
- if (!expectedFooter.equals(actualFooter)) {
383
- errors.push(`Footer SHA256 mismatch: expected ${expectedFooter.toString('hex')}, ` +
384
- `got ${actualFooter.toString('hex')}`);
385
- }
386
- }
387
- else {
388
- errors.push('Buffer too small to contain SHA256 footer');
389
- }
390
- return { valid: errors.length === 0, errors };
391
- }
392
- }
393
- //# sourceMappingURL=rvfa-format.js.map
@@ -1,238 +0,0 @@
1
- /**
2
- * RVFA Runner -- Boot and run self-contained Ruflo appliances.
3
- *
4
- * Supports three run modes (cli, mcp, verify) and two isolation
5
- * strategies (native Node.js, container via Docker).
6
- *
7
- * @module moflo/appliance/rvfa-runner
8
- */
9
- import { writeFile, mkdir, rm } from 'node:fs/promises';
10
- import { join } from 'node:path';
11
- import { spawn } from 'node:child_process';
12
- import { tmpdir } from 'node:os';
13
- import { RvfaReader } from './rvfa-format.js';
14
- // ── Internal helpers ────────────────────────────────────────
15
- /** Spawn a child process and capture stdout/stderr. */
16
- function spawnAsync(cmd, args, opts) {
17
- return new Promise((resolve) => {
18
- const start = performance.now();
19
- const out = [];
20
- const err = [];
21
- const child = spawn(cmd, args, {
22
- cwd: opts.cwd, env: { ...process.env, ...opts.env }, stdio: ['pipe', 'pipe', 'pipe'],
23
- windowsHide: true,
24
- });
25
- child.stdout.on('data', (c) => { out.push(c); if (opts.verbose)
26
- process.stdout.write(c); });
27
- child.stderr.on('data', (c) => { err.push(c); if (opts.verbose)
28
- process.stderr.write(c); });
29
- child.on('close', (code) => resolve({
30
- exitCode: code ?? 1, stdout: Buffer.concat(out).toString(), stderr: Buffer.concat(err).toString(),
31
- duration: performance.now() - start,
32
- }));
33
- child.on('error', (e) => resolve({
34
- exitCode: 1, stdout: '', stderr: e.message, duration: performance.now() - start,
35
- }));
36
- });
37
- }
38
- const fail = (stderr) => ({ exitCode: 1, stdout: '', stderr, duration: 0 });
39
- const cleanup = (dir) => rm(dir, { recursive: true, force: true }).catch(() => { });
40
- /** Check whether the reader has a section with the given id. */
41
- function hasSection(reader, id) {
42
- return reader.getSections().some((s) => s.id === id);
43
- }
44
- /** Safely extract a section, returning null if absent. */
45
- function tryExtract(reader, id) {
46
- try {
47
- return reader.extractSection(id);
48
- }
49
- catch {
50
- return null;
51
- }
52
- }
53
- // ── Runner ──────────────────────────────────────────────────
54
- export class RvfaRunner {
55
- reader;
56
- header;
57
- constructor(reader) {
58
- this.reader = reader;
59
- this.header = reader.getHeader();
60
- }
61
- /** Read and parse an RVFA file from disk. Throws on invalid input. */
62
- static async fromFile(rvfaPath) {
63
- const reader = await RvfaReader.fromFile(rvfaPath);
64
- return new RvfaRunner(reader);
65
- }
66
- /** Create a runner from an already-loaded buffer. */
67
- static fromBuffer(buf) {
68
- return new RvfaRunner(RvfaReader.fromBuffer(buf));
69
- }
70
- /**
71
- * Boot the appliance: verify integrity, then dispatch to the
72
- * requested isolation strategy and run mode.
73
- */
74
- async boot(options) {
75
- const { valid, errors } = this.reader.verify();
76
- if (!valid) {
77
- return fail(`Integrity check failed:\n${errors.join('\n')}`);
78
- }
79
- if (options.mode === 'verify')
80
- return this.runVerify(options);
81
- if (options.isolation === 'container')
82
- return this.runContainer(options);
83
- return this.runNative(options);
84
- }
85
- /**
86
- * Run natively via Node.js: extract RUFLO section to a temp dir,
87
- * configure env vars, optionally decrypt API-key vault, and spawn.
88
- */
89
- async runNative(options) {
90
- const workDir = join(tmpdir(), `rvfa-${this.header.name}-${Date.now()}`);
91
- try {
92
- await mkdir(workDir, { recursive: true });
93
- const ruflo = tryExtract(this.reader, 'ruflo');
94
- if (!ruflo)
95
- return fail('RVFA appliance does not contain a "ruflo" section');
96
- const entryFile = join(workDir, 'ruflo-bundle.js');
97
- await writeFile(entryFile, ruflo);
98
- const env = {
99
- ...this.header.boot.env,
100
- RVFA_APPLIANCE_NAME: this.header.name,
101
- RVFA_APPLIANCE_VERSION: this.header.appVersion,
102
- RVFA_RUN_MODE: options.mode,
103
- RVFA_PROFILE: this.header.profile,
104
- };
105
- if (options.passphrase && this.header.models.provider !== 'ruvllm') {
106
- const vault = tryExtract(this.reader, 'models');
107
- if (vault) {
108
- const keys = await this.decryptVault(vault, options.passphrase);
109
- if (keys)
110
- Object.assign(env, keys);
111
- }
112
- }
113
- const args = [...this.header.boot.args];
114
- if (options.mode === 'mcp')
115
- args.push('--mcp', '--transport', 'stdio');
116
- return spawnAsync(this.header.boot.entrypoint || 'node', [entryFile, ...args], {
117
- cwd: workDir, env, verbose: options.verbose,
118
- });
119
- }
120
- finally {
121
- await cleanup(workDir);
122
- }
123
- }
124
- /**
125
- * Run in a Docker container: generate a Dockerfile from the
126
- * extracted sections, build the image, and run it.
127
- */
128
- async runContainer(options) {
129
- const dockerCheck = await spawnAsync('docker', ['info'], { verbose: false });
130
- if (dockerCheck.exitCode !== 0) {
131
- return fail('Docker is not available. Install Docker or use isolation: "native".');
132
- }
133
- const workDir = join(tmpdir(), `rvfa-container-${Date.now()}`);
134
- try {
135
- await mkdir(workDir, { recursive: true });
136
- const ruflo = tryExtract(this.reader, 'ruflo');
137
- if (!ruflo)
138
- return fail('RVFA appliance does not contain a "ruflo" section');
139
- await writeFile(join(workDir, 'ruflo-bundle.js'), ruflo);
140
- const data = tryExtract(this.reader, 'data');
141
- if (data)
142
- await writeFile(join(workDir, 'data.bin'), data);
143
- const envFlags = [];
144
- for (const [k, v] of Object.entries(this.header.boot.env))
145
- envFlags.push('-e', `${k}=${v}`);
146
- envFlags.push('-e', `RVFA_RUN_MODE=${options.mode}`, '-e', `RVFA_PROFILE=${this.header.profile}`);
147
- const baseImage = this.header.platform === 'alpine' ? 'node:20-alpine' : 'node:20-slim';
148
- const cmdArgs = this.header.boot.args.map((a) => `, "${a}"`).join('');
149
- const dockerfile = [
150
- `FROM ${baseImage}`, 'WORKDIR /app', 'COPY ruflo-bundle.js .',
151
- data ? 'COPY data.bin .' : '', `CMD ["node", "ruflo-bundle.js"${cmdArgs}]`,
152
- ].filter(Boolean).join('\n');
153
- await writeFile(join(workDir, 'Dockerfile'), dockerfile);
154
- const imageName = `rvfa-${this.header.name}:${this.header.appVersion}`.toLowerCase();
155
- const build = await spawnAsync('docker', ['build', '-t', imageName, '.'], {
156
- cwd: workDir, verbose: options.verbose,
157
- });
158
- if (build.exitCode !== 0) {
159
- return { ...build, stderr: `Docker build failed:\n${build.stderr}` };
160
- }
161
- return spawnAsync('docker', ['run', '--rm', ...envFlags, imageName], { verbose: options.verbose });
162
- }
163
- finally {
164
- await cleanup(workDir);
165
- }
166
- }
167
- /**
168
- * Run the verification suite. Extracts the VERIFY section and
169
- * executes it; falls back to a basic integrity report.
170
- */
171
- async runVerify(options) {
172
- const start = performance.now();
173
- const verifyPayload = tryExtract(this.reader, 'verify');
174
- if (!verifyPayload) {
175
- const { valid, errors } = this.reader.verify();
176
- const lines = [
177
- `Appliance: ${this.header.name} v${this.header.appVersion}`,
178
- `Profile: ${this.header.profile}`,
179
- `Sections: ${this.header.sections.length}`,
180
- `Integrity: ${valid ? 'PASS' : 'FAIL'}`,
181
- ...errors.map((e) => ` FAIL: ${e}`),
182
- errors.length === 0 ? ' All checks PASS' : '',
183
- ];
184
- return {
185
- exitCode: valid ? 0 : 1,
186
- stdout: lines.filter(Boolean).join('\n'), stderr: '',
187
- duration: performance.now() - start,
188
- };
189
- }
190
- const workDir = join(tmpdir(), `rvfa-verify-${Date.now()}`);
191
- try {
192
- await mkdir(workDir, { recursive: true });
193
- await writeFile(join(workDir, 'verify.js'), verifyPayload);
194
- return spawnAsync('node', [join(workDir, 'verify.js')], {
195
- cwd: workDir, verbose: options.verbose,
196
- env: { RVFA_APPLIANCE_NAME: this.header.name, RVFA_APPLIANCE_VERSION: this.header.appVersion },
197
- });
198
- }
199
- finally {
200
- await cleanup(workDir);
201
- }
202
- }
203
- /** Return appliance metadata without booting. */
204
- getInfo() {
205
- const sections = this.reader.getSections();
206
- const totalSize = sections.reduce((sum, s) => sum + s.size, 0);
207
- return {
208
- header: { ...this.header },
209
- sections: sections.map((s) => ({ id: s.id, size: s.size, originalSize: s.originalSize })),
210
- totalSize,
211
- };
212
- }
213
- // ── Private ───────────────────────────────────────────────
214
- /**
215
- * Decrypt an API-key vault (AES-256-GCM).
216
- * Layout: [16-byte IV][ciphertext][16-byte auth-tag]
217
- * Key derived via PBKDF2 with salt = "rvfa-vault-{name}".
218
- */
219
- async decryptVault(payload, passphrase) {
220
- try {
221
- const { createDecipheriv, pbkdf2Sync } = await import('node:crypto');
222
- if (payload.length < 33)
223
- return null;
224
- const iv = payload.subarray(0, 16);
225
- const tag = payload.subarray(payload.length - 16);
226
- const ciphertext = payload.subarray(16, payload.length - 16);
227
- const key = pbkdf2Sync(passphrase, Buffer.from(`rvfa-vault-${this.header.name}`), 100_000, 32, 'sha256');
228
- const decipher = createDecipheriv('aes-256-gcm', key, iv);
229
- decipher.setAuthTag(tag);
230
- const dec = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
231
- return JSON.parse(dec.toString('utf-8'));
232
- }
233
- catch {
234
- return null;
235
- }
236
- }
237
- }
238
- //# sourceMappingURL=rvfa-runner.js.map