@dsh-enhanced/assistant-isolation 0.1.30

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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/cordis.patch.yml +3 -0
  4. package/lib/audit-archive.d.ts +52 -0
  5. package/lib/audit-archive.d.ts.map +1 -0
  6. package/lib/audit-archive.js +775 -0
  7. package/lib/audit-archive.js.map +1 -0
  8. package/lib/cleanup-receipt.d.ts +26 -0
  9. package/lib/cleanup-receipt.d.ts.map +1 -0
  10. package/lib/cleanup-receipt.js +131 -0
  11. package/lib/cleanup-receipt.js.map +1 -0
  12. package/lib/cli.d.ts +16 -0
  13. package/lib/cli.d.ts.map +1 -0
  14. package/lib/cli.js +183 -0
  15. package/lib/cli.js.map +1 -0
  16. package/lib/config.d.ts +20 -0
  17. package/lib/config.d.ts.map +1 -0
  18. package/lib/config.js +61 -0
  19. package/lib/config.js.map +1 -0
  20. package/lib/daemon-binding.d.ts +13 -0
  21. package/lib/daemon-binding.d.ts.map +1 -0
  22. package/lib/daemon-binding.js +120 -0
  23. package/lib/daemon-binding.js.map +1 -0
  24. package/lib/diagnostics.d.ts +19 -0
  25. package/lib/diagnostics.d.ts.map +1 -0
  26. package/lib/diagnostics.js +122 -0
  27. package/lib/diagnostics.js.map +1 -0
  28. package/lib/index.d.ts +19 -0
  29. package/lib/index.d.ts.map +1 -0
  30. package/lib/index.js +13 -0
  31. package/lib/index.js.map +1 -0
  32. package/lib/ledger.d.ts +88 -0
  33. package/lib/ledger.d.ts.map +1 -0
  34. package/lib/ledger.js +623 -0
  35. package/lib/ledger.js.map +1 -0
  36. package/lib/probe.d.ts +5 -0
  37. package/lib/probe.d.ts.map +1 -0
  38. package/lib/probe.js +45 -0
  39. package/lib/probe.js.map +1 -0
  40. package/lib/reconcile.d.ts +5 -0
  41. package/lib/reconcile.d.ts.map +1 -0
  42. package/lib/reconcile.js +29 -0
  43. package/lib/reconcile.js.map +1 -0
  44. package/lib/runner.d.ts +12 -0
  45. package/lib/runner.d.ts.map +1 -0
  46. package/lib/runner.js +201 -0
  47. package/lib/runner.js.map +1 -0
  48. package/lib/runtime-witness.d.ts +32 -0
  49. package/lib/runtime-witness.d.ts.map +1 -0
  50. package/lib/runtime-witness.js +225 -0
  51. package/lib/runtime-witness.js.map +1 -0
  52. package/lib/service.d.ts +47 -0
  53. package/lib/service.d.ts.map +1 -0
  54. package/lib/service.js +361 -0
  55. package/lib/service.js.map +1 -0
  56. package/lib/storage-policy.d.ts +7 -0
  57. package/lib/storage-policy.d.ts.map +1 -0
  58. package/lib/storage-policy.js +82 -0
  59. package/lib/storage-policy.js.map +1 -0
  60. package/lib/storage.d.ts +18 -0
  61. package/lib/storage.d.ts.map +1 -0
  62. package/lib/storage.js +102 -0
  63. package/lib/storage.js.map +1 -0
  64. package/lib/tools.d.ts +4 -0
  65. package/lib/tools.d.ts.map +1 -0
  66. package/lib/tools.js +49 -0
  67. package/lib/tools.js.map +1 -0
  68. package/lib/types.d.ts +143 -0
  69. package/lib/types.d.ts.map +1 -0
  70. package/lib/types.js +2 -0
  71. package/lib/types.js.map +1 -0
  72. package/lib/verifier-runner.d.ts +21 -0
  73. package/lib/verifier-runner.d.ts.map +1 -0
  74. package/lib/verifier-runner.js +192 -0
  75. package/lib/verifier-runner.js.map +1 -0
  76. package/lib/version.d.ts +2 -0
  77. package/lib/version.d.ts.map +1 -0
  78. package/lib/version.js +2 -0
  79. package/lib/version.js.map +1 -0
  80. package/lib/workspace.d.ts +6 -0
  81. package/lib/workspace.d.ts.map +1 -0
  82. package/lib/workspace.js +140 -0
  83. package/lib/workspace.js.map +1 -0
  84. package/package.json +105 -0
  85. package/runtime/supervisor.mjs +290 -0
@@ -0,0 +1,775 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { closeSync, constants, existsSync, fchmodSync, fstatSync, fsyncSync, linkSync, lstatSync, openSync, readFileSync, readdirSync, realpathSync, unlinkSync, writeSync, } from 'node:fs';
3
+ import { isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ export const ISOLATION_AUDIT_ARCHIVE_PROTOCOL = 'dsh-isolation-audit-archive/v1';
6
+ const LEDGER_SCHEMA = 6;
7
+ const DEFAULT_BATCH_SIZE = 100;
8
+ const MAX_BATCH_SIZE = 1_000;
9
+ const MAX_TEXT_LENGTH = 16_384;
10
+ const MAX_ARCHIVE_FILE_BYTES = 32 * 1_024 * 1_024;
11
+ const ARCHIVE_PREFIX = 'isolation-audit-v1-';
12
+ const ARCHIVE_SUFFIX = '.ndjson';
13
+ const CONTROL_FILE = '.archive-control.sqlite';
14
+ const DIGEST = /^[a-f0-9]{64}$/u;
15
+ const ARCHIVE_FILE = /^isolation-audit-v1-([a-f0-9]{64})\.ndjson$/u;
16
+ const TEMP_FILE = /^\.isolation-audit-v1-([a-f0-9]{64})\.[1-9][0-9]*\.[0-9a-f-]{36}\.tmp$/u;
17
+ const CONTROL_FILES = new Set([CONTROL_FILE, `${CONTROL_FILE}-journal`, `${CONTROL_FILE}-wal`, `${CONTROL_FILE}-shm`]);
18
+ const ARCHIVE_INSTANCE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
19
+ export class IsolationAuditArchiveError extends Error {
20
+ code;
21
+ constructor(code, message = code) {
22
+ super(message);
23
+ this.code = code;
24
+ this.name = 'IsolationAuditArchiveError';
25
+ }
26
+ }
27
+ function fail(code, message) { throw new IsolationAuditArchiveError(code, message); }
28
+ function safeNatural(value) { return Number.isSafeInteger(value) && value >= 0; }
29
+ function safePositive(value) { return Number.isSafeInteger(value) && value > 0; }
30
+ function plain(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype; }
31
+ function rowObject(value) {
32
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
33
+ return false;
34
+ const prototype = Object.getPrototypeOf(value);
35
+ return prototype === null || prototype === Object.prototype;
36
+ }
37
+ function exactKeys(value, expected) {
38
+ const actual = Object.keys(value).sort();
39
+ const wanted = [...expected].sort();
40
+ return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
41
+ }
42
+ function boundedText(value, nullable = false) {
43
+ return (nullable && value === null) || (typeof value === 'string' && value.length > 0 && value.length <= MAX_TEXT_LENGTH);
44
+ }
45
+ function boundedDetail(value) {
46
+ return typeof value === 'string' && value.length > 0 && Buffer.byteLength(value, 'utf8') <= MAX_ARCHIVE_FILE_BYTES;
47
+ }
48
+ function freeze(value) {
49
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
50
+ for (const item of Object.values(value))
51
+ freeze(item);
52
+ Object.freeze(value);
53
+ }
54
+ return value;
55
+ }
56
+ function canonical(value) {
57
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
58
+ return JSON.stringify(value);
59
+ if (typeof value === 'number') {
60
+ if (!Number.isSafeInteger(value))
61
+ fail('invalid-archive', 'archive JSON contains a non-safe integer');
62
+ return JSON.stringify(value);
63
+ }
64
+ if (Array.isArray(value))
65
+ return `[${value.map(canonical).join(',')}]`;
66
+ if (!plain(value))
67
+ fail('invalid-archive', 'archive JSON must contain only plain data');
68
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
69
+ }
70
+ function digest(content) { return createHash('sha256').update(content).digest('hex'); }
71
+ function sameInode(left, right) {
72
+ return BigInt(left.dev) === BigInt(right.dev) && BigInt(left.ino) === BigInt(right.ino);
73
+ }
74
+ function secureDirectory(path, label) {
75
+ if (typeof path !== 'string' || !isAbsolute(path) || resolve(path) !== path || path === '/' || /[\p{Cc}]/u.test(path))
76
+ fail('invalid-path', `${label} must be an absolute normalized directory`);
77
+ let descriptor;
78
+ try {
79
+ const entry = lstatSync(path, { bigint: true });
80
+ if (!entry.isDirectory() || entry.isSymbolicLink() || entry.uid !== BigInt(process.getuid?.() ?? -1) || (entry.mode & 4095n) !== 448n || realpathSync.native(path) !== path)
81
+ fail('invalid-path', `${label} must be a canonical private owned directory`);
82
+ descriptor = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
83
+ const opened = fstatSync(descriptor, { bigint: true });
84
+ const after = lstatSync(path, { bigint: true });
85
+ if (!opened.isDirectory() || opened.uid !== entry.uid || (opened.mode & 4095n) !== 448n || !sameInode(entry, opened) || !sameInode(opened, after))
86
+ fail('invalid-path', `${label} changed while being opened`);
87
+ return { path, descriptor, device: opened.dev, inode: opened.ino };
88
+ }
89
+ catch (error) {
90
+ if (descriptor !== undefined)
91
+ closeSync(descriptor);
92
+ if (error instanceof IsolationAuditArchiveError)
93
+ throw error;
94
+ fail('invalid-path', `${label} is unavailable`);
95
+ }
96
+ }
97
+ function closeDirectory(directory) { closeSync(directory.descriptor); }
98
+ function stillSameDirectory(directory) {
99
+ try {
100
+ const opened = fstatSync(directory.descriptor, { bigint: true });
101
+ const linked = lstatSync(directory.path, { bigint: true });
102
+ if (!sameInode(opened, linked) || opened.dev !== directory.device || opened.ino !== directory.inode)
103
+ fail('invalid-path', 'archive directory changed during operation');
104
+ }
105
+ catch (error) {
106
+ if (error instanceof IsolationAuditArchiveError)
107
+ throw error;
108
+ fail('invalid-path', 'archive directory changed during operation');
109
+ }
110
+ }
111
+ function disjoint(left, right) {
112
+ if (left === right)
113
+ return false;
114
+ const leftToRight = relative(left, right);
115
+ const rightToLeft = relative(right, left);
116
+ return (leftToRight.startsWith('..') || isAbsolute(leftToRight)) && (rightToLeft.startsWith('..') || isAbsolute(rightToLeft));
117
+ }
118
+ function inspectPrivateFile(path, label, maximum = Number.MAX_SAFE_INTEGER) {
119
+ let descriptor;
120
+ try {
121
+ const before = lstatSync(path, { bigint: true });
122
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.uid !== BigInt(process.getuid?.() ?? -1) || (before.mode & 4095n) !== 384n || before.size > BigInt(maximum))
123
+ fail('invalid-archive', `${label} must be one bounded private owned regular file`);
124
+ descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
125
+ const opened = fstatSync(descriptor, { bigint: true });
126
+ const after = lstatSync(path, { bigint: true });
127
+ if (!opened.isFile() || opened.nlink !== 1n || opened.uid !== before.uid || (opened.mode & 4095n) !== 384n || opened.size !== before.size || !sameInode(before, opened) || !sameInode(opened, after))
128
+ fail('invalid-archive', `${label} changed while being opened`);
129
+ return { descriptor, size: Number(opened.size) };
130
+ }
131
+ catch (error) {
132
+ if (descriptor !== undefined)
133
+ closeSync(descriptor);
134
+ if (error instanceof IsolationAuditArchiveError)
135
+ throw error;
136
+ fail('invalid-archive', `${label} is unavailable`);
137
+ }
138
+ }
139
+ function readPrivateFile(path, label, maximum = Number.MAX_SAFE_INTEGER) {
140
+ const opened = inspectPrivateFile(path, label, maximum);
141
+ try {
142
+ const content = readFileSync(opened.descriptor);
143
+ const after = fstatSync(opened.descriptor, { bigint: true });
144
+ if (content.byteLength !== opened.size || after.size !== BigInt(opened.size) || after.nlink !== 1n)
145
+ fail('invalid-archive', `${label} changed while being read`);
146
+ return content;
147
+ }
148
+ finally {
149
+ closeSync(opened.descriptor);
150
+ }
151
+ }
152
+ function validateControlArtifact(path, name) {
153
+ if (!existsSync(path))
154
+ return;
155
+ const opened = inspectPrivateFile(path, name);
156
+ closeSync(opened.descriptor);
157
+ }
158
+ function mutableControlFileIdentity(path) {
159
+ let descriptor;
160
+ try {
161
+ const before = lstatSync(path, { bigint: true });
162
+ const owner = BigInt(process.getuid?.() ?? -1);
163
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.uid !== owner || (before.mode & 4095n) !== 384n)
164
+ fail('invalid-archive', 'archive control database must be one private owned regular file');
165
+ descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
166
+ const opened = fstatSync(descriptor, { bigint: true });
167
+ const after = lstatSync(path, { bigint: true });
168
+ if (!opened.isFile() || opened.nlink !== 1n || opened.uid !== owner || (opened.mode & 4095n) !== 384n
169
+ || !after.isFile() || after.isSymbolicLink() || after.nlink !== 1n || after.uid !== owner || (after.mode & 4095n) !== 384n
170
+ || !sameInode(before, opened) || !sameInode(opened, after))
171
+ fail('invalid-archive', 'archive control database changed while being opened');
172
+ return { path, device: opened.dev, inode: opened.ino, owner: opened.uid };
173
+ }
174
+ catch (error) {
175
+ if (error instanceof IsolationAuditArchiveError)
176
+ throw error;
177
+ return fail('invalid-archive', 'archive control database is unavailable');
178
+ }
179
+ finally {
180
+ if (descriptor !== undefined)
181
+ closeSync(descriptor);
182
+ }
183
+ }
184
+ function validateLockedControlPath(identity) {
185
+ try {
186
+ const linked = lstatSync(identity.path, { bigint: true });
187
+ if (!linked.isFile() || linked.isSymbolicLink() || linked.nlink !== 1n || linked.uid !== identity.owner || (linked.mode & 4095n) !== 384n
188
+ || linked.dev !== identity.device || linked.ino !== identity.inode)
189
+ fail('invalid-archive', 'archive control database changed while locked');
190
+ }
191
+ catch (error) {
192
+ if (error instanceof IsolationAuditArchiveError)
193
+ throw error;
194
+ fail('invalid-archive', 'archive control database changed while locked');
195
+ }
196
+ }
197
+ function decodeManifest(value) {
198
+ const keys = ['archiveInstanceId', 'count', 'firstSequence', 'lastSequence', 'ledgerSchema', 'previousDigest', 'protocol', 'snapshotThroughSequence', 'sourceStateRootDigest', 'type', 'version'];
199
+ if (!plain(value) || !exactKeys(value, keys) || typeof value.archiveInstanceId !== 'string' || !ARCHIVE_INSTANCE_ID.test(value.archiveInstanceId) || typeof value.sourceStateRootDigest !== 'string' || !DIGEST.test(value.sourceStateRootDigest) || !safeNatural(value.count) || value.count > MAX_BATCH_SIZE
200
+ || !safePositive(value.firstSequence) || !safeNatural(value.lastSequence) || value.ledgerSchema !== LEDGER_SCHEMA
201
+ || (value.previousDigest !== null && (typeof value.previousDigest !== 'string' || !DIGEST.test(value.previousDigest)))
202
+ || value.protocol !== ISOLATION_AUDIT_ARCHIVE_PROTOCOL || !safeNatural(value.snapshotThroughSequence) || value.type !== 'manifest' || value.version !== 1)
203
+ fail('invalid-archive', 'invalid audit archive manifest');
204
+ if (value.count === 0) {
205
+ if (value.firstSequence !== 1 || value.lastSequence !== 0 || value.snapshotThroughSequence !== 0 || value.previousDigest !== null)
206
+ fail('invalid-archive', 'invalid audit archive genesis manifest');
207
+ }
208
+ else if (value.lastSequence !== value.firstSequence + value.count - 1 || value.lastSequence > value.snapshotThroughSequence)
209
+ fail('invalid-archive', 'inconsistent audit archive manifest bounds');
210
+ return freeze({ archiveInstanceId: value.archiveInstanceId, count: value.count, firstSequence: value.firstSequence, lastSequence: value.lastSequence, ledgerSchema: 6, previousDigest: value.previousDigest, protocol: ISOLATION_AUDIT_ARCHIVE_PROTOCOL, snapshotThroughSequence: value.snapshotThroughSequence, sourceStateRootDigest: value.sourceStateRootDigest, type: 'manifest', version: 1 });
211
+ }
212
+ function decodeRecord(value) {
213
+ const keys = ['action', 'detail', 'grantId', 'jobId', 'occurredAt', 'sequence'];
214
+ if (!plain(value) || !exactKeys(value, keys) || !boundedText(value.action) || !boundedDetail(value.detail) || !boundedText(value.grantId, true) || !boundedText(value.jobId, true) || !safeNatural(value.occurredAt) || !safePositive(value.sequence))
215
+ fail('invalid-archive', 'invalid audit archive record');
216
+ return freeze({ action: value.action, detail: value.detail, grantId: value.grantId, jobId: value.jobId, occurredAt: value.occurredAt, sequence: value.sequence });
217
+ }
218
+ function parseArchiveFile(directory, name, expectedDigest) {
219
+ const content = readPrivateFile(join(directory, name), `archive file ${name}`, MAX_ARCHIVE_FILE_BYTES);
220
+ if (content.byteLength === 0 || content[content.byteLength - 1] !== 0x0a)
221
+ fail('invalid-archive', `archive file ${name} is truncated`);
222
+ const calculated = digest(content);
223
+ if (calculated !== expectedDigest)
224
+ fail('invalid-archive', `archive file ${name} digest does not match its filename`);
225
+ const text = content.toString('utf8');
226
+ if (!Buffer.from(text, 'utf8').equals(content) || text.includes('\r'))
227
+ fail('invalid-archive', `archive file ${name} is not canonical UTF-8 NDJSON`);
228
+ const lines = text.slice(0, -1).split('\n');
229
+ if (lines.some(line => line.length === 0))
230
+ fail('invalid-archive', `archive file ${name} has an empty NDJSON line`);
231
+ let rawManifest;
232
+ try {
233
+ rawManifest = JSON.parse(lines[0]);
234
+ }
235
+ catch {
236
+ fail('invalid-archive', `archive file ${name} has malformed JSON`);
237
+ }
238
+ if (canonical(rawManifest) !== lines[0])
239
+ fail('invalid-archive', `archive file ${name} manifest is not canonical JSON`);
240
+ const manifest = decodeManifest(rawManifest);
241
+ const records = [];
242
+ for (const line of lines.slice(1)) {
243
+ let raw;
244
+ try {
245
+ raw = JSON.parse(line);
246
+ }
247
+ catch {
248
+ fail('invalid-archive', `archive file ${name} has malformed JSON`);
249
+ }
250
+ if (canonical(raw) !== line)
251
+ fail('invalid-archive', `archive file ${name} record is not canonical JSON`);
252
+ records.push(decodeRecord(raw));
253
+ }
254
+ if (records.length !== manifest.count)
255
+ fail('invalid-archive', `archive file ${name} count does not match its records`);
256
+ if (records.length > 0 && (records[0].sequence !== manifest.firstSequence || records.at(-1).sequence !== manifest.lastSequence))
257
+ fail('invalid-archive', `archive file ${name} record bounds do not match its manifest`);
258
+ for (let index = 0; index < records.length; index += 1)
259
+ if (records[index].sequence !== manifest.firstSequence + index)
260
+ fail('invalid-archive', `archive file ${name} has a sequence gap or reorder`);
261
+ return { name, digest: expectedDigest, manifest, records };
262
+ }
263
+ function readControlIdentity(directory) {
264
+ const path = join(directory.path, CONTROL_FILE);
265
+ if (!existsSync(path))
266
+ return null;
267
+ const opened = inspectPrivateFile(path, 'archive control database');
268
+ closeSync(opened.descriptor);
269
+ let database;
270
+ try {
271
+ database = new DatabaseSync(path, { readOnly: true });
272
+ database.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=5000; BEGIN;');
273
+ const version = database.prepare('PRAGMA user_version').get().user_version;
274
+ const row = database.prepare('SELECT archive_instance_id, source_state_root_digest FROM archive_control WHERE singleton=1').get();
275
+ if (version !== 1 || typeof row?.archive_instance_id !== 'string' || !ARCHIVE_INSTANCE_ID.test(row.archive_instance_id) || typeof row.source_state_root_digest !== 'string' || !DIGEST.test(row.source_state_root_digest))
276
+ fail('invalid-archive', 'invalid archive control identity');
277
+ database.exec('COMMIT');
278
+ return { archiveInstanceId: row.archive_instance_id, sourceStateRootDigest: row.source_state_root_digest };
279
+ }
280
+ catch (error) {
281
+ try {
282
+ database?.exec('ROLLBACK');
283
+ }
284
+ catch { }
285
+ if (error instanceof IsolationAuditArchiveError)
286
+ throw error;
287
+ return fail('invalid-archive', 'invalid archive control database');
288
+ }
289
+ finally {
290
+ database?.close();
291
+ }
292
+ }
293
+ function verifyArchive(directory, expectedIdentity, allowIncompleteTail = false, allowUninitialized = false, lockedControl) {
294
+ stillSameDirectory(directory);
295
+ const parsed = [];
296
+ let names;
297
+ try {
298
+ names = readdirSync(directory.path);
299
+ }
300
+ catch {
301
+ fail('invalid-archive', 'archive directory cannot be read');
302
+ }
303
+ for (const name of names) {
304
+ const match = ARCHIVE_FILE.exec(name);
305
+ if (match) {
306
+ parsed.push(parseArchiveFile(directory.path, name, match[1]));
307
+ continue;
308
+ }
309
+ if (CONTROL_FILES.has(name)) {
310
+ if (name === CONTROL_FILE && lockedControl !== undefined)
311
+ validateLockedControlPath(lockedControl);
312
+ else
313
+ validateControlArtifact(join(directory.path, name), name);
314
+ continue;
315
+ }
316
+ if (TEMP_FILE.test(name)) {
317
+ validateControlArtifact(join(directory.path, name), name);
318
+ continue;
319
+ }
320
+ fail('invalid-archive', `unknown file in audit archive: ${name}`);
321
+ }
322
+ const controlIdentity = expectedIdentity ?? readControlIdentity(directory) ?? undefined;
323
+ if (parsed.length === 0) {
324
+ if (!allowUninitialized || controlIdentity === undefined)
325
+ fail('invalid-archive', 'audit archive has no genesis file');
326
+ return { files: [], verification: freeze({ valid: true, protocol: ISOLATION_AUDIT_ARCHIVE_PROTOCOL, fileCount: 0, archiveInstanceId: controlIdentity.archiveInstanceId, recordCount: 0, highestSequence: 0, snapshotThroughSequence: 0, headDigest: null, sourceStateRootDigest: controlIdentity.sourceStateRootDigest }) };
327
+ }
328
+ const genesis = parsed.filter(file => file.manifest.previousDigest === null);
329
+ if (genesis.length !== 1)
330
+ fail('invalid-archive', 'audit archive must have exactly one chain root');
331
+ const successors = new Map();
332
+ for (const file of parsed)
333
+ if (file.manifest.previousDigest !== null) {
334
+ if (successors.has(file.manifest.previousDigest))
335
+ fail('invalid-archive', 'audit archive contains a fork');
336
+ successors.set(file.manifest.previousDigest, file);
337
+ }
338
+ const ordered = [];
339
+ const consumed = new Set();
340
+ let current = genesis[0];
341
+ while (current) {
342
+ if (consumed.has(current.digest))
343
+ fail('invalid-archive', 'audit archive contains a cycle');
344
+ consumed.add(current.digest);
345
+ ordered.push(current);
346
+ current = successors.get(current.digest);
347
+ }
348
+ if (consumed.size !== parsed.length)
349
+ fail('invalid-archive', 'audit archive contains an orphan or invalid previous digest');
350
+ let expectedSequence = 1;
351
+ let snapshotThroughSequence = 0;
352
+ let recordCount = 0;
353
+ for (let fileIndex = 0; fileIndex < ordered.length; fileIndex += 1) {
354
+ const file = ordered[fileIndex];
355
+ const { manifest } = file;
356
+ if (manifest.archiveInstanceId !== ordered[0].manifest.archiveInstanceId)
357
+ fail('invalid-archive', 'audit archive instance identity changed within the chain');
358
+ if (manifest.sourceStateRootDigest !== ordered[0].manifest.sourceStateRootDigest)
359
+ fail('invalid-archive', 'audit archive source identity changed within the chain');
360
+ if (fileIndex === 0 && manifest.count === 0) {
361
+ if (ordered.length > 1 && ordered[1].manifest.firstSequence !== 1)
362
+ fail('invalid-archive', 'audit archive does not continue after genesis');
363
+ }
364
+ else {
365
+ if (manifest.count === 0 || manifest.firstSequence !== expectedSequence)
366
+ fail('invalid-archive', 'audit archive has a gap, reorder, or duplicate sequence');
367
+ expectedSequence = manifest.lastSequence + 1;
368
+ recordCount += manifest.count;
369
+ }
370
+ if (manifest.snapshotThroughSequence < snapshotThroughSequence)
371
+ fail('invalid-archive', 'audit archive snapshot high-water regressed');
372
+ snapshotThroughSequence = manifest.snapshotThroughSequence;
373
+ }
374
+ const head = ordered.at(-1);
375
+ const highestSequence = expectedSequence - 1;
376
+ if (!allowIncompleteTail && head.manifest.snapshotThroughSequence !== highestSequence)
377
+ fail('invalid-archive', 'audit archive has an incomplete snapshot tail');
378
+ const archiveInstanceId = ordered[0].manifest.archiveInstanceId;
379
+ const sourceStateRootDigest = ordered[0].manifest.sourceStateRootDigest;
380
+ if (controlIdentity !== undefined && (controlIdentity.archiveInstanceId !== archiveInstanceId || controlIdentity.sourceStateRootDigest !== sourceStateRootDigest))
381
+ fail('invalid-archive', 'archive chain identity differs from its control identity');
382
+ return { files: ordered, verification: freeze({ valid: true, protocol: ISOLATION_AUDIT_ARCHIVE_PROTOCOL, fileCount: ordered.length, archiveInstanceId, recordCount, highestSequence, snapshotThroughSequence, headDigest: head.digest, sourceStateRootDigest }) };
383
+ }
384
+ /** Verify a complete local hash chain. External anchoring is required to detect valid tail deletion or full recomputation. */
385
+ export function verifyIsolationAuditArchive(archiveDirectory) {
386
+ const directory = secureDirectory(archiveDirectory, 'isolation audit archive directory');
387
+ try {
388
+ const verification = verifyArchive(directory).verification;
389
+ if (verification.archiveInstanceId === null || verification.headDigest === null || verification.sourceStateRootDigest === null)
390
+ fail('invalid-archive', 'audit archive has no anchorable genesis');
391
+ return verification;
392
+ }
393
+ finally {
394
+ closeDirectory(directory);
395
+ }
396
+ }
397
+ function ensureControlFile(directory) {
398
+ const path = join(directory.path, CONTROL_FILE);
399
+ if (!existsSync(path)) {
400
+ let descriptor;
401
+ try {
402
+ descriptor = openSync(path, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
403
+ fchmodSync(descriptor, 0o600);
404
+ fsyncSync(descriptor);
405
+ }
406
+ catch (error) {
407
+ if (error.code !== 'EEXIST')
408
+ fail('io', 'cannot create archive control database');
409
+ }
410
+ finally {
411
+ if (descriptor !== undefined)
412
+ closeSync(descriptor);
413
+ }
414
+ fsyncSync(directory.descriptor);
415
+ }
416
+ return mutableControlFileIdentity(path);
417
+ }
418
+ function recoverTemporaryPublications(directory) {
419
+ let changed = false;
420
+ for (const name of readdirSync(directory.path)) {
421
+ const match = TEMP_FILE.exec(name);
422
+ if (!match)
423
+ continue;
424
+ const temporaryPath = join(directory.path, name);
425
+ let temporary;
426
+ try {
427
+ temporary = lstatSync(temporaryPath, { bigint: true });
428
+ }
429
+ catch {
430
+ continue;
431
+ }
432
+ if (!temporary.isFile() || temporary.isSymbolicLink() || temporary.uid !== BigInt(process.getuid?.() ?? -1) || (temporary.mode & 4095n) !== 384n || (temporary.nlink !== 1n && temporary.nlink !== 2n))
433
+ fail('invalid-archive', `unsafe archive temporary file ${name}`);
434
+ if (temporary.nlink === 2n) {
435
+ const targetName = `${ARCHIVE_PREFIX}${match[1]}${ARCHIVE_SUFFIX}`;
436
+ const targetPath = join(directory.path, targetName);
437
+ let target;
438
+ try {
439
+ target = lstatSync(targetPath, { bigint: true });
440
+ }
441
+ catch {
442
+ fail('invalid-archive', `archive temporary hardlink ${name} has no matching target`);
443
+ }
444
+ if (!target.isFile() || target.isSymbolicLink() || target.nlink !== 2n || target.uid !== temporary.uid || (target.mode & 4095n) !== 384n || !sameInode(temporary, target))
445
+ fail('invalid-archive', `archive temporary hardlink ${name} does not match its target`);
446
+ let descriptor;
447
+ try {
448
+ descriptor = openSync(temporaryPath, constants.O_RDONLY | constants.O_NOFOLLOW);
449
+ const opened = fstatSync(descriptor, { bigint: true });
450
+ const after = lstatSync(temporaryPath, { bigint: true });
451
+ if (!sameInode(temporary, opened) || !sameInode(opened, after) || opened.nlink !== 2n)
452
+ fail('invalid-archive', `archive temporary hardlink ${name} changed while being opened`);
453
+ const content = readFileSync(descriptor);
454
+ if (content.byteLength > MAX_ARCHIVE_FILE_BYTES || digest(content) !== match[1])
455
+ fail('invalid-archive', `archive temporary hardlink ${name} has invalid content`);
456
+ }
457
+ finally {
458
+ if (descriptor !== undefined)
459
+ closeSync(descriptor);
460
+ }
461
+ }
462
+ try {
463
+ unlinkSync(temporaryPath);
464
+ changed = true;
465
+ }
466
+ catch {
467
+ fail('io', `cannot remove archive temporary file ${name}`);
468
+ }
469
+ if (temporary.nlink === 2n) {
470
+ const final = inspectPrivateFile(join(directory.path, `${ARCHIVE_PREFIX}${match[1]}${ARCHIVE_SUFFIX}`), 'recovered archive file', MAX_ARCHIVE_FILE_BYTES);
471
+ closeSync(final.descriptor);
472
+ }
473
+ }
474
+ if (changed)
475
+ fsyncSync(directory.descriptor);
476
+ }
477
+ function beginArchiveControl(directory, sourceStateRootDigest) {
478
+ // All open/close based checks on the main control inode must happen before
479
+ // BEGIN IMMEDIATE. POSIX process locks may be released by closing any fd for
480
+ // that inode, even when SQLite owns a different descriptor.
481
+ const file = ensureControlFile(directory);
482
+ let database;
483
+ try {
484
+ database = new DatabaseSync(file.path);
485
+ database.exec('PRAGMA busy_timeout=30000; PRAGMA journal_mode=DELETE; PRAGMA synchronous=FULL;');
486
+ database.exec('BEGIN IMMEDIATE;');
487
+ const version = database.prepare('PRAGMA user_version').get().user_version;
488
+ if (version === 0) {
489
+ const archiveInstanceId = randomUUID();
490
+ database.exec('CREATE TABLE archive_control (singleton INTEGER PRIMARY KEY CHECK(singleton=1), archive_instance_id TEXT NOT NULL, source_state_root_digest TEXT NOT NULL) STRICT;');
491
+ database.prepare('INSERT INTO archive_control(singleton, archive_instance_id, source_state_root_digest) VALUES (1, ?, ?)').run(archiveInstanceId, sourceStateRootDigest);
492
+ database.exec('PRAGMA user_version=1; COMMIT; BEGIN IMMEDIATE;');
493
+ }
494
+ else if (version !== 1)
495
+ fail('invalid-archive', 'unsupported archive control database');
496
+ validateLockedControlPath(file);
497
+ const row = database.prepare('SELECT archive_instance_id, source_state_root_digest FROM archive_control WHERE singleton=1').get();
498
+ if (typeof row?.archive_instance_id !== 'string' || !ARCHIVE_INSTANCE_ID.test(row.archive_instance_id) || row.source_state_root_digest !== sourceStateRootDigest)
499
+ fail('conflict', 'archive is bound to another isolation state root');
500
+ return { database, archiveInstanceId: row.archive_instance_id, sourceStateRootDigest, file };
501
+ }
502
+ catch (error) {
503
+ try {
504
+ database?.exec('ROLLBACK');
505
+ }
506
+ catch { }
507
+ database?.close();
508
+ if (error instanceof IsolationAuditArchiveError)
509
+ throw error;
510
+ fail('io', 'cannot lock archive control database');
511
+ }
512
+ }
513
+ function validateLedgerFile(state) {
514
+ const path = join(state.path, 'ledger.sqlite');
515
+ try {
516
+ const inspected = inspectPrivateFile(path, 'isolation ledger');
517
+ return { path, descriptor: inspected.descriptor };
518
+ }
519
+ catch (error) {
520
+ if (error instanceof IsolationAuditArchiveError)
521
+ fail('invalid-source', error.message);
522
+ throw error;
523
+ }
524
+ }
525
+ function validateLedgerSnapshot(database) {
526
+ const userVersion = database.prepare('PRAGMA user_version').get().user_version;
527
+ const schema = database.prepare("SELECT value FROM schema_meta WHERE key='schema-version'").get();
528
+ if (userVersion !== LEDGER_SCHEMA || schema?.value !== String(LEDGER_SCHEMA))
529
+ fail('invalid-source', 'isolation ledger schema 6 is required');
530
+ const highWater = database.prepare("SELECT COALESCE((SELECT seq FROM sqlite_sequence WHERE name='isolation_audit'), 0) AS sequence").get().sequence;
531
+ if (!safeNatural(highWater))
532
+ fail('invalid-source', 'isolation audit high-water is invalid');
533
+ const bounds = database.prepare('SELECT COUNT(*) AS count, COALESCE(MIN(sequence), 0) AS first_sequence, COALESCE(MAX(sequence), 0) AS last_sequence FROM isolation_audit').get();
534
+ if (!safeNatural(bounds.count) || bounds.count !== highWater || bounds.first_sequence !== (highWater === 0 ? 0 : 1) || bounds.last_sequence !== highWater)
535
+ fail('invalid-source', 'isolation audit sequence history is incomplete');
536
+ return highWater;
537
+ }
538
+ function openLedgerSnapshot(state) {
539
+ const ledger = validateLedgerFile(state);
540
+ let database;
541
+ try {
542
+ database = new DatabaseSync(ledger.path, { readOnly: true });
543
+ database.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=5000; BEGIN;');
544
+ const linked = lstatSync(ledger.path, { bigint: true });
545
+ const opened = fstatSync(ledger.descriptor, { bigint: true });
546
+ if (!sameInode(linked, opened))
547
+ fail('invalid-source', 'isolation ledger changed while being opened');
548
+ return { database, descriptor: ledger.descriptor, highWater: validateLedgerSnapshot(database) };
549
+ }
550
+ catch (error) {
551
+ try {
552
+ database?.exec('ROLLBACK');
553
+ }
554
+ catch { }
555
+ database?.close();
556
+ closeSync(ledger.descriptor);
557
+ if (error instanceof IsolationAuditArchiveError)
558
+ throw error;
559
+ return fail('invalid-source', error instanceof Error ? error.message : 'isolation ledger is unavailable');
560
+ }
561
+ }
562
+ function preflightLedger(state) {
563
+ const snapshot = openLedgerSnapshot(state);
564
+ try {
565
+ snapshot.database.exec('COMMIT');
566
+ }
567
+ catch (error) {
568
+ return fail('invalid-source', error instanceof Error ? error.message : 'isolation ledger preflight failed');
569
+ }
570
+ finally {
571
+ snapshot.database.close();
572
+ closeSync(snapshot.descriptor);
573
+ }
574
+ }
575
+ function rowRecord(row, expectedSequence) {
576
+ if (!rowObject(row) || !exactKeys(row, ['sequence', 'occurred_at', 'action', 'job_id', 'grant_id', 'detail']) || row.sequence !== expectedSequence
577
+ || !safePositive(row.sequence) || !safeNatural(row.occurred_at) || !boundedText(row.action) || !boundedDetail(row.detail) || !boundedText(row.job_id, true) || !boundedText(row.grant_id, true))
578
+ fail('invalid-source', 'isolation audit contains an invalid or discontinuous row');
579
+ return freeze({ action: row.action, detail: row.detail, grantId: row.grant_id, jobId: row.job_id, occurredAt: row.occurred_at, sequence: row.sequence });
580
+ }
581
+ function recordsEqual(left, right) { return canonical(left) === canonical(right); }
582
+ function makeManifest(archiveInstanceId, sourceStateRootDigest, firstSequence, lastSequence, count, snapshotThroughSequence, previousDigest) {
583
+ return freeze({ archiveInstanceId, count, firstSequence, lastSequence, ledgerSchema: 6, previousDigest, protocol: ISOLATION_AUDIT_ARCHIVE_PROTOCOL, snapshotThroughSequence, sourceStateRootDigest, type: 'manifest', version: 1 });
584
+ }
585
+ function encodeFile(manifest, records) {
586
+ return Buffer.from(`${[canonical(manifest), ...records.map(record => canonical(record))].join('\n')}\n`, 'utf8');
587
+ }
588
+ function publish(directory, content) {
589
+ stillSameDirectory(directory);
590
+ if (content.byteLength > MAX_ARCHIVE_FILE_BYTES)
591
+ fail('invalid-source', 'one audit record exceeds the archive file bound');
592
+ const contentDigest = digest(content);
593
+ const name = `${ARCHIVE_PREFIX}${contentDigest}${ARCHIVE_SUFFIX}`;
594
+ const target = join(directory.path, name);
595
+ if (existsSync(target)) {
596
+ if (!readPrivateFile(target, `archive file ${name}`, MAX_ARCHIVE_FILE_BYTES).equals(content))
597
+ fail('conflict', `conflicting archive target ${name}`);
598
+ stillSameDirectory(directory);
599
+ return { name, created: false, digest: contentDigest };
600
+ }
601
+ const temporaryName = `.${ARCHIVE_PREFIX}${contentDigest}.${process.pid}.${randomUUID()}.tmp`;
602
+ const temporary = join(directory.path, temporaryName);
603
+ let descriptor;
604
+ let linked = false;
605
+ try {
606
+ descriptor = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
607
+ fchmodSync(descriptor, 0o600);
608
+ let offset = 0;
609
+ while (offset < content.length)
610
+ offset += writeSync(descriptor, content, offset, content.length - offset);
611
+ fsyncSync(descriptor);
612
+ const staged = fstatSync(descriptor, { bigint: true });
613
+ const stagedPath = lstatSync(temporary, { bigint: true });
614
+ if (!staged.isFile() || staged.uid !== BigInt(process.getuid?.() ?? -1) || (staged.mode & 4095n) !== 384n || staged.nlink !== 1n || staged.size !== BigInt(content.length) || !sameInode(staged, stagedPath))
615
+ fail('io', 'unsafe archive temporary file');
616
+ closeSync(descriptor);
617
+ descriptor = undefined;
618
+ try {
619
+ linkSync(temporary, target);
620
+ linked = true;
621
+ const source = lstatSync(temporary, { bigint: true });
622
+ const destination = lstatSync(target, { bigint: true });
623
+ if (!sameInode(source, destination) || source.nlink !== 2n || destination.nlink !== 2n)
624
+ fail('io', 'archive publication link is unsafe');
625
+ }
626
+ catch (error) {
627
+ if (error.code !== 'EEXIST')
628
+ throw error;
629
+ if (!readPrivateFile(target, `archive file ${name}`, MAX_ARCHIVE_FILE_BYTES).equals(content))
630
+ fail('conflict', `conflicting archive target ${name}`);
631
+ }
632
+ unlinkSync(temporary);
633
+ fsyncSync(directory.descriptor);
634
+ if (linked) {
635
+ const final = inspectPrivateFile(target, `archive file ${name}`, MAX_ARCHIVE_FILE_BYTES);
636
+ closeSync(final.descriptor);
637
+ }
638
+ stillSameDirectory(directory);
639
+ return { name, created: linked, digest: contentDigest };
640
+ }
641
+ catch (error) {
642
+ if (descriptor !== undefined)
643
+ closeSync(descriptor);
644
+ try {
645
+ if (existsSync(temporary))
646
+ unlinkSync(temporary);
647
+ }
648
+ catch { }
649
+ if (error instanceof IsolationAuditArchiveError)
650
+ throw error;
651
+ fail('io', `cannot publish audit archive file ${name}`);
652
+ }
653
+ }
654
+ function validateOptions(options) {
655
+ if (!plain(options) || !exactKeys(options, options.batchSize === undefined ? ['stateRoot', 'archiveDirectory'] : ['stateRoot', 'archiveDirectory', 'batchSize'])
656
+ || typeof options.stateRoot !== 'string' || typeof options.archiveDirectory !== 'string')
657
+ fail('invalid-input', 'invalid audit archive options');
658
+ const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
659
+ if (!safePositive(batchSize) || batchSize > MAX_BATCH_SIZE)
660
+ fail('invalid-input', 'audit archive batch size must be between 1 and 1000');
661
+ return { stateRoot: options.stateRoot, archiveDirectory: options.archiveDirectory, batchSize };
662
+ }
663
+ /** Copy a fixed, read-only ledger snapshot into immutable, content-addressed archive batches. */
664
+ export function archiveIsolationAudit(options) {
665
+ const input = validateOptions(options);
666
+ const state = secureDirectory(input.stateRoot, 'isolation state root');
667
+ let archive;
668
+ let control;
669
+ let ledgerDescriptor;
670
+ let database;
671
+ let transaction = false;
672
+ try {
673
+ archive = secureDirectory(input.archiveDirectory, 'isolation audit archive directory');
674
+ if (!disjoint(state.path, archive.path) || (state.device === archive.device && state.inode === archive.inode))
675
+ fail('invalid-path', 'state root and archive directory must be disjoint');
676
+ // Do not initialize or bind an archive when the requested source cannot
677
+ // first prove that it is a complete schema-6 audit ledger. The locked
678
+ // snapshot below repeats these checks authoritatively.
679
+ preflightLedger(state);
680
+ const controlLock = beginArchiveControl(archive, digest(state.path));
681
+ control = controlLock.database;
682
+ recoverTemporaryPublications(archive);
683
+ const existing = verifyArchive(archive, controlLock, true, true, controlLock.file);
684
+ const snapshot = openLedgerSnapshot(state);
685
+ database = snapshot.database;
686
+ ledgerDescriptor = snapshot.descriptor;
687
+ transaction = true;
688
+ const highWater = snapshot.highWater;
689
+ if (existing.verification.highestSequence > highWater || existing.verification.snapshotThroughSequence > highWater)
690
+ fail('conflict', 'archive does not match this isolation ledger');
691
+ let archivedIndex = 0;
692
+ for (const file of existing.files)
693
+ for (const record of file.records) {
694
+ const row = database.prepare('SELECT sequence, occurred_at, action, job_id, grant_id, detail FROM isolation_audit WHERE sequence=?').get(record.sequence);
695
+ if (!row || !recordsEqual(record, rowRecord(row, record.sequence)))
696
+ fail('conflict', 'archived audit prefix differs from the isolation ledger');
697
+ archivedIndex += 1;
698
+ }
699
+ if (archivedIndex !== existing.verification.highestSequence)
700
+ fail('invalid-archive', 'archive record count is inconsistent');
701
+ const createdFiles = [];
702
+ let createdRecords = 0;
703
+ let afterSequence = existing.verification.highestSequence;
704
+ let previousDigest = existing.verification.headDigest;
705
+ if (highWater === 0 && previousDigest === null) {
706
+ const publication = publish(archive, encodeFile(makeManifest(controlLock.archiveInstanceId, controlLock.sourceStateRootDigest, 1, 0, 0, 0, null), []));
707
+ if (publication.created)
708
+ createdFiles.push(publication.name);
709
+ previousDigest = publication.digest;
710
+ }
711
+ while (afterSequence < highWater) {
712
+ const rows = database.prepare('SELECT sequence, occurred_at, action, job_id, grant_id, detail FROM isolation_audit WHERE sequence>? AND sequence<=? ORDER BY sequence ASC LIMIT ?').all(afterSequence, highWater, input.batchSize);
713
+ if (rows.length === 0)
714
+ fail('invalid-source', 'isolation audit has a sequence gap');
715
+ const decoded = rows.map((row, index) => rowRecord(row, afterSequence + index + 1));
716
+ let cursor = 0;
717
+ while (cursor < decoded.length) {
718
+ const records = [];
719
+ while (cursor < decoded.length && records.length < input.batchSize) {
720
+ const candidate = decoded[cursor];
721
+ const next = [...records, candidate];
722
+ const provisional = makeManifest(controlLock.archiveInstanceId, controlLock.sourceStateRootDigest, next[0].sequence, next.at(-1).sequence, next.length, highWater, previousDigest);
723
+ if (encodeFile(provisional, next).byteLength > MAX_ARCHIVE_FILE_BYTES) {
724
+ if (records.length === 0)
725
+ fail('invalid-source', 'one audit record exceeds the archive file bound');
726
+ break;
727
+ }
728
+ records.push(candidate);
729
+ cursor += 1;
730
+ }
731
+ const manifest = makeManifest(controlLock.archiveInstanceId, controlLock.sourceStateRootDigest, records[0].sequence, records.at(-1).sequence, records.length, highWater, previousDigest);
732
+ const publication = publish(archive, encodeFile(manifest, records));
733
+ if (publication.created) {
734
+ createdFiles.push(publication.name);
735
+ createdRecords += records.length;
736
+ }
737
+ previousDigest = publication.digest;
738
+ afterSequence = records.at(-1).sequence;
739
+ }
740
+ }
741
+ const verification = verifyArchive(archive, controlLock, false, false, controlLock.file).verification;
742
+ if (verification.highestSequence !== highWater || verification.headDigest !== previousDigest || verification.archiveInstanceId === null || verification.sourceStateRootDigest === null)
743
+ fail('invalid-archive', 'published archive failed final verification');
744
+ stillSameDirectory(archive);
745
+ database.exec('COMMIT');
746
+ transaction = false;
747
+ control.exec('COMMIT');
748
+ stillSameDirectory(archive);
749
+ return freeze({ createdFiles: freeze(createdFiles), createdRecords, verification: verification });
750
+ }
751
+ catch (error) {
752
+ if (transaction)
753
+ try {
754
+ database?.exec('ROLLBACK');
755
+ }
756
+ catch { }
757
+ try {
758
+ control?.exec('ROLLBACK');
759
+ }
760
+ catch { }
761
+ if (error instanceof IsolationAuditArchiveError)
762
+ throw error;
763
+ return fail('io', error instanceof Error ? error.message : 'audit archive failed');
764
+ }
765
+ finally {
766
+ database?.close();
767
+ if (ledgerDescriptor !== undefined)
768
+ closeSync(ledgerDescriptor);
769
+ control?.close();
770
+ if (archive)
771
+ closeDirectory(archive);
772
+ closeDirectory(state);
773
+ }
774
+ }
775
+ //# sourceMappingURL=audit-archive.js.map