swarm-deploy 0.0.0 → 0.1.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE.md +201 -0
  3. package/NOTICE.md +13 -0
  4. package/README.md +652 -0
  5. package/RELEASING.md +71 -0
  6. package/SECURITY.md +41 -0
  7. package/dist/abort.d.ts +16 -0
  8. package/dist/abort.js +52 -0
  9. package/dist/abort.js.map +1 -0
  10. package/dist/allowlist.d.ts +2 -0
  11. package/dist/allowlist.js +21 -0
  12. package/dist/allowlist.js.map +1 -0
  13. package/dist/bin/swarm-deploy.d.ts +2 -0
  14. package/dist/bin/swarm-deploy.js +41 -0
  15. package/dist/bin/swarm-deploy.js.map +1 -0
  16. package/dist/cli.d.ts +25 -0
  17. package/dist/cli.js +694 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/client.d.ts +121 -0
  20. package/dist/client.js +285 -0
  21. package/dist/client.js.map +1 -0
  22. package/dist/direct-dht.d.ts +81 -0
  23. package/dist/direct-dht.js +254 -0
  24. package/dist/direct-dht.js.map +1 -0
  25. package/dist/error-code.d.ts +15 -0
  26. package/dist/error-code.js +30 -0
  27. package/dist/error-code.js.map +1 -0
  28. package/dist/errors.d.ts +27 -0
  29. package/dist/errors.js +33 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/events-node.d.ts +2 -0
  32. package/dist/events-node.js +7 -0
  33. package/dist/events-node.js.map +1 -0
  34. package/dist/files.d.ts +47 -0
  35. package/dist/files.js +150 -0
  36. package/dist/files.js.map +1 -0
  37. package/dist/fs-node.d.ts +2 -0
  38. package/dist/fs-node.js +7 -0
  39. package/dist/fs-node.js.map +1 -0
  40. package/dist/identity.d.ts +7 -0
  41. package/dist/identity.js +50 -0
  42. package/dist/identity.js.map +1 -0
  43. package/dist/index.d.ts +8 -0
  44. package/dist/index.js +19 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/os-node.d.ts +2 -0
  47. package/dist/os-node.js +7 -0
  48. package/dist/os-node.js.map +1 -0
  49. package/dist/path-node.d.ts +2 -0
  50. package/dist/path-node.js +7 -0
  51. package/dist/path-node.js.map +1 -0
  52. package/dist/process-node.d.ts +2 -0
  53. package/dist/process-node.js +7 -0
  54. package/dist/process-node.js.map +1 -0
  55. package/dist/server.d.ts +153 -0
  56. package/dist/server.js +576 -0
  57. package/dist/server.js.map +1 -0
  58. package/dist/storage/atomic-file.d.ts +15 -0
  59. package/dist/storage/atomic-file.js +172 -0
  60. package/dist/storage/atomic-file.js.map +1 -0
  61. package/dist/storage/commit-journal.d.ts +83 -0
  62. package/dist/storage/commit-journal.js +279 -0
  63. package/dist/storage/commit-journal.js.map +1 -0
  64. package/dist/storage/commit-store.d.ts +246 -0
  65. package/dist/storage/commit-store.js +1339 -0
  66. package/dist/storage/commit-store.js.map +1 -0
  67. package/dist/storage/layout.d.ts +16 -0
  68. package/dist/storage/layout.js +403 -0
  69. package/dist/storage/layout.js.map +1 -0
  70. package/dist/storage/recovery.d.ts +99 -0
  71. package/dist/storage/recovery.js +253 -0
  72. package/dist/storage/recovery.js.map +1 -0
  73. package/dist/storage/retention.d.ts +163 -0
  74. package/dist/storage/retention.js +465 -0
  75. package/dist/storage/retention.js.map +1 -0
  76. package/dist/storage/root-coordinator.d.ts +7 -0
  77. package/dist/storage/root-coordinator.js +27 -0
  78. package/dist/storage/root-coordinator.js.map +1 -0
  79. package/dist/storage/session-store.d.ts +2 -0
  80. package/dist/storage/session-store.js +7 -0
  81. package/dist/storage/session-store.js.map +1 -0
  82. package/dist/storage/tar-session-store.d.ts +116 -0
  83. package/dist/storage/tar-session-store.js +816 -0
  84. package/dist/storage/tar-session-store.js.map +1 -0
  85. package/dist/storage/types.d.ts +62 -0
  86. package/dist/storage/types.js +3 -0
  87. package/dist/storage/types.js.map +1 -0
  88. package/dist/tar-protocol/controls.d.ts +48 -0
  89. package/dist/tar-protocol/controls.js +185 -0
  90. package/dist/tar-protocol/controls.js.map +1 -0
  91. package/dist/tar-protocol/direct-wire.d.ts +43 -0
  92. package/dist/tar-protocol/direct-wire.js +194 -0
  93. package/dist/tar-protocol/direct-wire.js.map +1 -0
  94. package/dist/tar-protocol/extract.d.ts +18 -0
  95. package/dist/tar-protocol/extract.js +124 -0
  96. package/dist/tar-protocol/extract.js.map +1 -0
  97. package/dist/tar-protocol/hash.d.ts +10 -0
  98. package/dist/tar-protocol/hash.js +44 -0
  99. package/dist/tar-protocol/hash.js.map +1 -0
  100. package/dist/tar-protocol/lifecycle.d.ts +13 -0
  101. package/dist/tar-protocol/lifecycle.js +90 -0
  102. package/dist/tar-protocol/lifecycle.js.map +1 -0
  103. package/dist/tar-protocol/manifest.d.ts +46 -0
  104. package/dist/tar-protocol/manifest.js +287 -0
  105. package/dist/tar-protocol/manifest.js.map +1 -0
  106. package/dist/tar-protocol/ustar.d.ts +22 -0
  107. package/dist/tar-protocol/ustar.js +87 -0
  108. package/dist/tar-protocol/ustar.js.map +1 -0
  109. package/dist/types.d.ts +58 -0
  110. package/dist/types.js +3 -0
  111. package/dist/types.js.map +1 -0
  112. package/dist/validation.d.ts +4 -0
  113. package/dist/validation.js +26 -0
  114. package/dist/validation.js.map +1 -0
  115. package/package.json +101 -1
@@ -0,0 +1,1339 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MAX_COMMIT_METADATA_BYTES = exports.COMMIT_VERSION = exports.CorruptJournalError = exports.CommitStore = void 0;
7
+ const b4a_1 = __importDefault(require("b4a"));
8
+ const error_code_js_1 = require("../error-code.js");
9
+ const _fs_1 = __importDefault(require("#fs"));
10
+ const _path_1 = __importDefault(require("#path"));
11
+ const sodium_native_1 = __importDefault(require("sodium-native"));
12
+ const errors_js_1 = require("../errors.js");
13
+ const files_js_1 = require("../files.js");
14
+ const hash_js_1 = require("../tar-protocol/hash.js");
15
+ const manifest_js_1 = require("../tar-protocol/manifest.js");
16
+ const validation_js_1 = require("../validation.js");
17
+ const layout_js_1 = require("./layout.js");
18
+ const atomic_file_js_1 = require("./atomic-file.js");
19
+ const commit_journal_js_1 = require("./commit-journal.js");
20
+ Object.defineProperty(exports, "CorruptJournalError", { enumerable: true, get: function () { return commit_journal_js_1.CorruptJournalError; } });
21
+ Object.defineProperty(exports, "COMMIT_VERSION", { enumerable: true, get: function () { return commit_journal_js_1.COMMIT_VERSION; } });
22
+ Object.defineProperty(exports, "MAX_COMMIT_METADATA_BYTES", { enumerable: true, get: function () { return commit_journal_js_1.MAX_COMMIT_METADATA_BYTES; } });
23
+ const root_coordinator_js_1 = require("./root-coordinator.js");
24
+ function storageError(message, cause = null) {
25
+ return new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.PROTOCOL_INVALID, message, cause);
26
+ }
27
+ function commitError(message, cause = null) {
28
+ return new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.COMMIT_FAILED, message, cause);
29
+ }
30
+ function existsError(message) {
31
+ return new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_EXISTS, message);
32
+ }
33
+ function assertNotAborted(signal) {
34
+ if (signal?.aborted)
35
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.ABORTED, 'Upload was aborted');
36
+ }
37
+ function toHex(bytes) {
38
+ return b4a_1.default.toString(bytes, 'hex');
39
+ }
40
+ function sameHex32(left, right) {
41
+ return (/^[0-9a-f]{64}$/.test(left) &&
42
+ /^[0-9a-f]{64}$/.test(right) &&
43
+ sodium_native_1.default.sodium_memcmp(b4a_1.default.from(left, 'hex'), b4a_1.default.from(right, 'hex')));
44
+ }
45
+ function isCommitRecordName(name) {
46
+ return typeof name === 'string' && /^[0-9a-f]{64}\.json$/.test(name);
47
+ }
48
+ function attemptId() {
49
+ const bytes = b4a_1.default.allocUnsafe(32);
50
+ sodium_native_1.default.randombytes_buf(bytes);
51
+ return toHex(bytes);
52
+ }
53
+ function fingerprint(ownerKey) {
54
+ return toHex((0, hash_js_1.sodiumSha256)(ownerKey));
55
+ }
56
+ function replacementsEqual(left, right) {
57
+ if (left === undefined || right === undefined)
58
+ return left === right;
59
+ return (left.name === right.name &&
60
+ sameHex32(left.transferId, right.transferId) &&
61
+ left.historyName === right.historyName);
62
+ }
63
+ function recordsEqual(left, right) {
64
+ return (left.version === right.version &&
65
+ left.name === right.name &&
66
+ left.size === right.size &&
67
+ sameHex32(left.sha256, right.sha256) &&
68
+ left.committedAt === right.committedAt &&
69
+ left.uploaderFingerprint === right.uploaderFingerprint &&
70
+ sameHex32(left.transferId, right.transferId) &&
71
+ replacementsEqual(left.replaces, right.replaces));
72
+ }
73
+ function sameContent(left, right) {
74
+ return left.size === right.size && sameHex32(left.sha256, right.sha256);
75
+ }
76
+ /** The record the old sidecar becomes once its inode is only reachable as history. */
77
+ function historyRecord(oldRecord, name) {
78
+ return (0, commit_journal_js_1.assertCommitRecord)({ ...oldRecord, name });
79
+ }
80
+ function assertSession(session) {
81
+ if (!session || typeof session !== 'object')
82
+ throw storageError('Invalid commit session');
83
+ const candidate = session;
84
+ if (candidate.state !== 'verified')
85
+ throw storageError('Session is not verified');
86
+ if (!(0, commit_journal_js_1.isHex)(candidate.id))
87
+ throw storageError('Invalid session ID');
88
+ (0, validation_js_1.assertFixed32)(candidate.transferId, 'session transfer ID');
89
+ (0, validation_js_1.assertFixed32)(candidate.ownerKey, 'session owner key');
90
+ if (typeof candidate.name !== 'string')
91
+ throw storageError('Invalid filename');
92
+ (0, files_js_1.validateBasename)(candidate.name);
93
+ (0, validation_js_1.assertSafeUint)(candidate.size, 'session size');
94
+ (0, validation_js_1.assertFixed32)(candidate.digest, 'session digest');
95
+ if (!sameHex32(candidate.id, toHex(candidate.transferId))) {
96
+ throw storageError('Session ID mismatch');
97
+ }
98
+ (0, validation_js_1.assertSafeUint)(candidate.tarSize, 'session TAR size');
99
+ (0, validation_js_1.assertFixed32)(candidate.tarDigest, 'session TAR digest');
100
+ (0, manifest_js_1.assertMetadataTransferId)(candidate.ownerKey, {
101
+ v: 1,
102
+ name: candidate.name,
103
+ fileSize: candidate.size,
104
+ fileSha256: toHex(candidate.digest),
105
+ tarSize: candidate.tarSize,
106
+ tarSha256: toHex(candidate.tarDigest),
107
+ transferId: candidate.id,
108
+ reset: false
109
+ });
110
+ }
111
+ async function readExactly(handle, bytes, position) {
112
+ let offset = 0;
113
+ while (offset < bytes.byteLength) {
114
+ const read = await handle.read(bytes, offset, bytes.byteLength - offset, position + offset);
115
+ const count = typeof read === 'number' ? read : read.bytesRead;
116
+ if (!Number.isSafeInteger(count) || count <= 0)
117
+ return false;
118
+ offset += count;
119
+ }
120
+ return true;
121
+ }
122
+ async function writeAll(handle, bytes) {
123
+ let offset = 0;
124
+ while (offset < bytes.byteLength) {
125
+ const written = await handle.write(bytes, offset, bytes.byteLength - offset, offset);
126
+ const count = typeof written === 'number' ? written : written.bytesWritten;
127
+ if (!Number.isSafeInteger(count) || count <= 0) {
128
+ throw storageError('Unable to write commit journal');
129
+ }
130
+ offset += count;
131
+ }
132
+ }
133
+ async function syncDirectory(directory, storage) {
134
+ const handle = await storage.open(directory, 'r');
135
+ try {
136
+ await handle.sync();
137
+ }
138
+ finally {
139
+ await handle.close();
140
+ }
141
+ }
142
+ function digestExactFile(filePath, size, storage, syncFirst = false) {
143
+ return (0, layout_js_1.withSafeDirectoryIdentity)(_path_1.default.dirname(filePath), storage, async () => {
144
+ const handle = await (0, layout_js_1.openSafeRegularFile)(filePath, 'read', storage);
145
+ try {
146
+ if (syncFirst)
147
+ await handle.sync();
148
+ const before = await handle.stat();
149
+ if (!before.isFile() || before.size !== size)
150
+ return null;
151
+ const hash = new hash_js_1.SodiumSha256();
152
+ let position = 0;
153
+ while (position < size) {
154
+ const bytes = b4a_1.default.alloc(Math.min(64 * 1024, size - position));
155
+ if (!(await readExactly(handle, bytes, position)))
156
+ return null;
157
+ hash.update(bytes);
158
+ position += bytes.byteLength;
159
+ }
160
+ const after = await handle.stat();
161
+ if (!after.isFile() ||
162
+ after.size !== size ||
163
+ after.dev !== before.dev ||
164
+ after.ino !== before.ino) {
165
+ return null;
166
+ }
167
+ return {
168
+ digest: hash.digest(),
169
+ identity: (0, commit_journal_js_1.fileIdentity)(before)
170
+ };
171
+ }
172
+ finally {
173
+ await handle.close();
174
+ }
175
+ });
176
+ }
177
+ class CommitStore {
178
+ layout;
179
+ clock;
180
+ storage;
181
+ logger;
182
+ journalAttempts;
183
+ constructor({ layout, clock = Date, storage = _fs_1.default.promises, logger = null }) {
184
+ if (!layout || typeof layout !== 'object')
185
+ throw storageError('Invalid storage layout');
186
+ if (!clock || typeof clock.now !== 'function')
187
+ throw storageError('Invalid clock');
188
+ if (!storage || typeof storage !== 'object')
189
+ throw storageError('Invalid storage adapter');
190
+ this.layout = layout;
191
+ this.clock = clock;
192
+ this.storage = storage;
193
+ this.logger = logger;
194
+ this.journalAttempts = new Map();
195
+ }
196
+ _stagingPath(id) {
197
+ return _path_1.default.join(this.layout.staging, `${id}.part`);
198
+ }
199
+ _tarStagingPath(id) {
200
+ return _path_1.default.join(this.layout.staging, `${id}.tar.part`);
201
+ }
202
+ _sessionPath(id) {
203
+ return _path_1.default.join(this.layout.sessions, `${id}.json`);
204
+ }
205
+ _journalPath(id) {
206
+ return _path_1.default.join(this.layout.journals, `${id}.json`);
207
+ }
208
+ _recordPath(id) {
209
+ return _path_1.default.join(this.layout.commits, `${id}.json`);
210
+ }
211
+ _finalPath(name) {
212
+ return _path_1.default.join(this.layout.root, name);
213
+ }
214
+ _publicationPath(name) {
215
+ return _path_1.default.join(this.layout.publications, name);
216
+ }
217
+ async _assertLayout() {
218
+ for (const directory of (0, layout_js_1.protectedDirectories)(this.layout)) {
219
+ await (0, layout_js_1.assertSafeDirectory)(directory, this.storage);
220
+ }
221
+ }
222
+ /** Inspects one top-level path without following or trusting symlinks. */
223
+ _rootPathState(name) {
224
+ const filePath = this._finalPath(name);
225
+ return (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.root, this.storage, async () => {
226
+ let stat;
227
+ try {
228
+ stat = await this.storage.lstat(filePath);
229
+ }
230
+ catch (err) {
231
+ if ((0, error_code_js_1.isMissing)(err))
232
+ return { present: false, stat: null };
233
+ throw err;
234
+ }
235
+ if (stat.isSymbolicLink() || !stat.isFile())
236
+ return { present: true, stat: null };
237
+ return { present: true, stat };
238
+ });
239
+ }
240
+ async _syncRoot() {
241
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.root, this.storage, () => syncDirectory(this.layout.root, this.storage));
242
+ }
243
+ async _safeFileOrAbsent(filePath, directory) {
244
+ try {
245
+ return await (0, layout_js_1.withSafeDirectoryIdentity)(directory, this.storage, () => (0, layout_js_1.assertSafeFile)(filePath, this.storage));
246
+ }
247
+ catch (err) {
248
+ if ((0, error_code_js_1.isMissing)(err))
249
+ return null;
250
+ throw err;
251
+ }
252
+ }
253
+ async _readRecordOrAbsent(recordPath) {
254
+ const stat = await this._safeFileOrAbsent(recordPath, this.layout.commits);
255
+ if (!stat)
256
+ return null;
257
+ return (0, commit_journal_js_1.assertCommitRecord)(await (0, atomic_file_js_1.readJson)(recordPath, this.storage, commit_journal_js_1.MAX_COMMIT_METADATA_BYTES));
258
+ }
259
+ async _writeRecord(record) {
260
+ const recordPath = this._recordPath(record.transferId);
261
+ const existing = await this._readRecordOrAbsent(recordPath);
262
+ if (existing) {
263
+ if (!recordsEqual(existing, record))
264
+ throw storageError('Conflicting commit record');
265
+ return false;
266
+ }
267
+ await (0, atomic_file_js_1.writeAtomic)(recordPath, b4a_1.default.from(JSON.stringify(record)), this.storage);
268
+ return true;
269
+ }
270
+ /** Persists a sidecar transition over its own previous durable content. */
271
+ async _rewriteRecord(record) {
272
+ (0, commit_journal_js_1.assertCommitRecord)(record);
273
+ await (0, atomic_file_js_1.writeAtomic)(this._recordPath(record.transferId), b4a_1.default.from(JSON.stringify(record)), this.storage);
274
+ }
275
+ async _removeFile(filePath, directory) {
276
+ let removed = false;
277
+ await (0, layout_js_1.withSafeDirectoryIdentity)(directory, this.storage, async () => {
278
+ try {
279
+ await (0, layout_js_1.assertSafeFile)(filePath, this.storage);
280
+ }
281
+ catch (err) {
282
+ if ((0, error_code_js_1.isMissing)(err))
283
+ return;
284
+ throw err;
285
+ }
286
+ await this.storage.unlink(filePath);
287
+ removed = true;
288
+ });
289
+ if (removed) {
290
+ await (0, layout_js_1.withSafeDirectoryIdentity)(directory, this.storage, () => syncDirectory(directory, this.storage));
291
+ }
292
+ return removed;
293
+ }
294
+ async _removeManagedFinalOrAbsent(record) {
295
+ let removed = false;
296
+ let preservedPath = false;
297
+ const finalPath = this._finalPath(record.name);
298
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.root, this.storage, async () => {
299
+ let stat;
300
+ try {
301
+ stat = await this.storage.lstat(finalPath);
302
+ }
303
+ catch (err) {
304
+ if ((0, error_code_js_1.isMissing)(err))
305
+ return;
306
+ throw err;
307
+ }
308
+ if (stat.isDirectory()) {
309
+ try {
310
+ await this.storage.rmdir(finalPath);
311
+ removed = true;
312
+ }
313
+ catch (err) {
314
+ if ((0, error_code_js_1.errorCode)(err) === 'ENOTEMPTY' || (0, error_code_js_1.errorCode)(err) === 'EEXIST') {
315
+ preservedPath = true;
316
+ return;
317
+ }
318
+ throw err;
319
+ }
320
+ return;
321
+ }
322
+ await this.storage.unlink(finalPath);
323
+ removed = true;
324
+ });
325
+ if (removed) {
326
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.root, this.storage, () => syncDirectory(this.layout.root, this.storage));
327
+ }
328
+ return { removed, preservedPath };
329
+ }
330
+ async _linkJournal(id, journal, attempt) {
331
+ const journalPath = this._journalPath(id);
332
+ const temporary = _path_1.default.join(this.layout.journals, `.${id}.${attempt}.tmp`);
333
+ const bytes = b4a_1.default.from(JSON.stringify((0, commit_journal_js_1.serializeJournal)(journal)));
334
+ try {
335
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.journals, this.storage, async () => {
336
+ const handle = await (0, layout_js_1.openSafeRegularFile)(temporary, 'create', this.storage);
337
+ try {
338
+ await writeAll(handle, bytes);
339
+ await handle.sync();
340
+ }
341
+ finally {
342
+ await handle.close();
343
+ }
344
+ await this.storage.link(temporary, journalPath);
345
+ this.journalAttempts.set(id, attempt);
346
+ await syncDirectory(this.layout.journals, this.storage);
347
+ });
348
+ }
349
+ finally {
350
+ await this._removeFile(temporary, this.layout.journals).catch(() => { });
351
+ }
352
+ }
353
+ async _writeJournal(record, sourceStagingIdentity) {
354
+ const journalPath = this._journalPath(record.transferId);
355
+ if (await this._safeFileOrAbsent(journalPath, this.layout.journals)) {
356
+ let existing;
357
+ try {
358
+ existing = await this._readJournal(record.transferId);
359
+ }
360
+ catch (err) {
361
+ if (!(err instanceof commit_journal_js_1.CorruptJournalError))
362
+ throw err;
363
+ await this.retireCorruptJournal(record.transferId);
364
+ }
365
+ if (existing) {
366
+ if (existing.version !== commit_journal_js_1.JOURNAL_VERSION ||
367
+ existing.state !== 'committing' ||
368
+ !recordsEqual(existing.record, record) ||
369
+ !(0, commit_journal_js_1.identitiesEqual)(existing.sourceStagingIdentity, sourceStagingIdentity) ||
370
+ this.journalAttempts.get(record.transferId) !== existing.attemptId) {
371
+ throw storageError('Foreign commit journal already exists');
372
+ }
373
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.journals, this.storage, () => syncDirectory(this.layout.journals, this.storage));
374
+ return existing.attemptId;
375
+ }
376
+ }
377
+ const id = attemptId();
378
+ await this._linkJournal(record.transferId, {
379
+ version: commit_journal_js_1.JOURNAL_VERSION,
380
+ state: 'committing',
381
+ attemptId: id,
382
+ sourceStagingIdentity,
383
+ record
384
+ }, id);
385
+ return id;
386
+ }
387
+ /** A replacement always starts a fresh attempt; leftovers belong to recovery. */
388
+ async _writeReplacementJournal(journal) {
389
+ const id = journal.transferId;
390
+ if (await this._safeFileOrAbsent(this._journalPath(id), this.layout.journals)) {
391
+ let existing = null;
392
+ try {
393
+ existing = await this._readJournal(id);
394
+ }
395
+ catch (err) {
396
+ if (!(err instanceof commit_journal_js_1.CorruptJournalError))
397
+ throw err;
398
+ await this.retireCorruptJournal(id);
399
+ }
400
+ if (existing)
401
+ throw storageError('Foreign commit journal already exists');
402
+ }
403
+ await this._linkJournal(id, journal, journal.attemptId);
404
+ }
405
+ /**
406
+ * Records the boundary an attempt has already passed. The observable
407
+ * filesystem state stays authoritative during recovery; the phase is the
408
+ * durable intent trail the journal must carry.
409
+ */
410
+ async _setReplacementPhase(journal, phase) {
411
+ const next = { ...journal, phase };
412
+ await (0, atomic_file_js_1.writeAtomic)(this._journalPath(journal.transferId), b4a_1.default.from(JSON.stringify((0, commit_journal_js_1.serializeJournal)(next))), this.storage);
413
+ return next;
414
+ }
415
+ async retireCorruptJournal(id) {
416
+ if (!(0, commit_journal_js_1.isHex)(id))
417
+ throw storageError('Invalid corrupt journal ID');
418
+ const journalPath = this._journalPath(id);
419
+ const quarantine = _path_1.default.join(this.layout.journals, `.${id}.corrupt-${attemptId()}`);
420
+ let retired = false;
421
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.journals, this.storage, async () => {
422
+ try {
423
+ await (0, layout_js_1.assertSafeFile)(journalPath, this.storage);
424
+ }
425
+ catch (err) {
426
+ if ((0, error_code_js_1.isMissing)(err))
427
+ return;
428
+ throw err;
429
+ }
430
+ await this.storage.rename(journalPath, quarantine);
431
+ retired = true;
432
+ });
433
+ if (retired) {
434
+ this.journalAttempts.delete(id);
435
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.journals, this.storage, () => syncDirectory(this.layout.journals, this.storage));
436
+ }
437
+ return retired;
438
+ }
439
+ async retireCorruptAttempt(id) {
440
+ if (!(0, commit_journal_js_1.isHex)(id))
441
+ throw storageError('Invalid corrupt journal ID');
442
+ const session = await this._safeFileOrAbsent(this._sessionPath(id), this.layout.sessions);
443
+ let removedStaging = false;
444
+ if (!session) {
445
+ removedStaging = await this._removeFile(this._stagingPath(id), this.layout.staging);
446
+ }
447
+ const retired = await this.retireCorruptJournal(id);
448
+ return { retired, removedStaging };
449
+ }
450
+ async _discardJournal(id, expectedAttemptId) {
451
+ const journal = await this._readJournal(id);
452
+ if (!journal) {
453
+ if (this.journalAttempts.get(id) === expectedAttemptId)
454
+ this.journalAttempts.delete(id);
455
+ return false;
456
+ }
457
+ if (journal.attemptId !== expectedAttemptId)
458
+ return false;
459
+ const removed = await this._removeFile(this._journalPath(id), this.layout.journals);
460
+ if (removed && this.journalAttempts.get(id) === expectedAttemptId) {
461
+ this.journalAttempts.delete(id);
462
+ }
463
+ return removed;
464
+ }
465
+ async _markAttemptAborting(id, expectedAttemptId) {
466
+ const journal = await this._readJournal(id);
467
+ if (!journal || journal.attemptId !== expectedAttemptId) {
468
+ throw storageError('Commit journal changed before abort');
469
+ }
470
+ if (journal.state === 'aborting')
471
+ return journal;
472
+ const bytes = b4a_1.default.from(JSON.stringify({ ...(0, commit_journal_js_1.serializeJournal)(journal), state: 'aborting' }));
473
+ await (0, atomic_file_js_1.writeAtomic)(this._journalPath(id), bytes, this.storage);
474
+ const marked = await this._readJournal(id);
475
+ if (!marked ||
476
+ marked.version !== journal.version ||
477
+ marked.attemptId !== expectedAttemptId ||
478
+ marked.state !== 'aborting') {
479
+ throw storageError('Commit journal changed while marking abort');
480
+ }
481
+ return marked;
482
+ }
483
+ _linkStaging(stagingPath, finalPath, expectedIdentity, signal) {
484
+ return (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.root, this.storage, () => (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, async () => {
485
+ const staging = await (0, layout_js_1.assertSafeFile)(stagingPath, this.storage);
486
+ if (!(0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(staging), expectedIdentity)) {
487
+ throw storageError('Staging file changed before commit');
488
+ }
489
+ assertNotAborted(signal);
490
+ await this.storage.link(stagingPath, finalPath);
491
+ assertNotAborted(signal);
492
+ }));
493
+ }
494
+ async _removeAbortedPublication(record, expectedIdentity) {
495
+ const errors = [];
496
+ try {
497
+ const finalPath = this._finalPath(record.name);
498
+ const final = await this._safeFileOrAbsent(finalPath, this.layout.root);
499
+ if (final && (0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(final), expectedIdentity)) {
500
+ await this._removeFile(finalPath, this.layout.root);
501
+ }
502
+ }
503
+ catch (err) {
504
+ errors.push(err);
505
+ }
506
+ try {
507
+ const recordPath = this._recordPath(record.transferId);
508
+ const sidecar = await this._readRecordOrAbsent(recordPath);
509
+ if (sidecar && recordsEqual(sidecar, record)) {
510
+ await this._removeFile(recordPath, this.layout.commits);
511
+ }
512
+ }
513
+ catch (err) {
514
+ errors.push(err);
515
+ }
516
+ if (errors.length)
517
+ throw new AggregateError(errors, 'Unable to roll back publication');
518
+ }
519
+ _reportCleanupPending(record, err) {
520
+ try {
521
+ this.logger?.warn?.('Committed artifact cleanup remains pending', {
522
+ fingerprint: record.uploaderFingerprint,
523
+ name: record.name,
524
+ code: (0, error_code_js_1.errorCode)(err),
525
+ reason: err instanceof Error ? err.message : String(err)
526
+ });
527
+ }
528
+ catch { }
529
+ }
530
+ async _abortAttempt(record, expectedIdentity, attemptId, aborted) {
531
+ try {
532
+ await this._markAttemptAborting(record.transferId, attemptId);
533
+ await this._removeAbortedPublication(record, expectedIdentity);
534
+ await this._discardJournal(record.transferId, attemptId);
535
+ }
536
+ catch (cleanupError) {
537
+ throw new AggregateError([aborted, cleanupError], 'Unable to clean up aborted commit');
538
+ }
539
+ throw aborted;
540
+ }
541
+ async _removeOwnedFile(filePath, directory, identity) {
542
+ const stat = await this._safeFileOrAbsent(filePath, directory);
543
+ if (!stat || !(0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(stat), identity))
544
+ return false;
545
+ return this._removeFile(filePath, directory);
546
+ }
547
+ async _matchesRecord(filePath, directory, record, syncFirst = false) {
548
+ const stat = await this._safeFileOrAbsent(filePath, directory);
549
+ if (!stat)
550
+ return false;
551
+ const result = await digestExactFile(filePath, record.size, this.storage, syncFirst);
552
+ return result !== null && (0, hash_js_1.digestMatches)(result.digest, b4a_1.default.from(record.sha256, 'hex'));
553
+ }
554
+ _recordFromSession(session) {
555
+ assertSession(session);
556
+ const record = {
557
+ version: commit_journal_js_1.COMMIT_VERSION,
558
+ name: session.name,
559
+ size: session.size,
560
+ sha256: toHex(session.digest),
561
+ committedAt: this.clock.now(),
562
+ uploaderFingerprint: fingerprint(session.ownerKey),
563
+ transferId: session.id
564
+ };
565
+ return (0, commit_journal_js_1.assertCommitRecord)(record);
566
+ }
567
+ async inspect(name, offer, { replaceNames } = {}) {
568
+ await this._assertLayout();
569
+ (0, files_js_1.validateBasename)(name);
570
+ if ((0, files_js_1.isReservedHistoryName)(name)) {
571
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.INVALID_FILENAME, 'Reserved artifact name');
572
+ }
573
+ if (!offer || typeof offer !== 'object')
574
+ throw storageError('Invalid commit offer');
575
+ if (offer.name !== name)
576
+ throw storageError('Commit offer name mismatch');
577
+ (0, validation_js_1.assertSafeUint)(offer.size, 'commit offer size');
578
+ (0, validation_js_1.assertFixed32)(offer.digest, 'commit offer digest');
579
+ (0, validation_js_1.assertFixed32)(offer.transferId, 'commit offer transfer ID');
580
+ const mutable = (0, files_js_1.validateReplaceNames)(replaceNames);
581
+ return (0, root_coordinator_js_1.withNameLease)(this.layout.root, name, () => this._inspect(name, offer, mutable));
582
+ }
583
+ async _inspect(name, offer, mutable) {
584
+ const finalPath = this._finalPath(name);
585
+ const final = await this._safeFileOrAbsent(finalPath, this.layout.root);
586
+ if (!final)
587
+ return { status: 'AVAILABLE' };
588
+ const id = toHex(offer.transferId);
589
+ const record = await this._readRecordOrAbsent(this._recordPath(id));
590
+ if (record &&
591
+ record.name === name &&
592
+ record.size === offer.size &&
593
+ sameHex32(record.sha256, toHex(offer.digest)) &&
594
+ sameHex32(record.transferId, id) &&
595
+ (await this._matchesRecord(finalPath, this.layout.root, record))) {
596
+ return { status: 'ALREADY_COMMITTED', record };
597
+ }
598
+ if (!mutable.has(name))
599
+ return { status: 'FILE_EXISTS' };
600
+ const current = await this._managedCurrent(name);
601
+ if (!current || !(0, commit_journal_js_1.identitiesEqual)(current.identity, (0, commit_journal_js_1.fileIdentity)(final))) {
602
+ return { status: 'FILE_EXISTS' };
603
+ }
604
+ if (current.record.size === offer.size &&
605
+ sameHex32(current.record.sha256, toHex(offer.digest))) {
606
+ return { status: 'ALREADY_COMMITTED', record: current.record };
607
+ }
608
+ return { status: 'REPLACEABLE', record: current.record };
609
+ }
610
+ /** The managed record whose verified content currently owns a visible name. */
611
+ async _managedCurrent(name) {
612
+ const finalPath = this._finalPath(name);
613
+ const matches = (await this._scanRecords()).filter((record) => record.name === name);
614
+ if (matches.length === 0)
615
+ return null;
616
+ if (matches.length > 1)
617
+ throw storageError('Duplicate managed commit filename');
618
+ const record = matches[0];
619
+ const digested = await digestExactFile(finalPath, record.size, this.storage);
620
+ if (!digested || !(0, hash_js_1.digestMatches)(digested.digest, b4a_1.default.from(record.sha256, 'hex')))
621
+ return null;
622
+ return { record, identity: digested.identity };
623
+ }
624
+ _assertSessionMatchesRecord(session, record, message) {
625
+ assertSession(session);
626
+ if (session.name !== record.name ||
627
+ session.size !== record.size ||
628
+ toHex(session.digest) !== record.sha256 ||
629
+ fingerprint(session.ownerKey) !== record.uploaderFingerprint) {
630
+ throw new commit_journal_js_1.CorruptJournalError(message);
631
+ }
632
+ }
633
+ async _validateAttemptLeftovers(id, journal, sessionStore) {
634
+ const { record, sourceStagingIdentity } = journal;
635
+ const sessionPath = this._sessionPath(id);
636
+ const stagingPath = this._stagingPath(id);
637
+ const sessionFile = await this._safeFileOrAbsent(sessionPath, this.layout.sessions);
638
+ const staging = await this._safeFileOrAbsent(stagingPath, this.layout.staging);
639
+ if (sessionFile) {
640
+ if (!sessionStore || typeof sessionStore.readVerified !== 'function') {
641
+ throw storageError('Session store cannot validate commit cleanup');
642
+ }
643
+ const session = await sessionStore.readVerified(b4a_1.default.from(id, 'hex'));
644
+ this._assertSessionMatchesRecord(session, record, 'Commit journal does not match verified session');
645
+ }
646
+ if (staging &&
647
+ (!(0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(staging), sourceStagingIdentity) ||
648
+ !(await this._matchesRecord(stagingPath, this.layout.staging, record)))) {
649
+ throw storageError('Staging file does not match commit journal');
650
+ }
651
+ return { sessionFile, staging };
652
+ }
653
+ commit(session, { retentionManager = null, signal = null, replaceNames } = {}) {
654
+ try {
655
+ assertSession(session);
656
+ if (retentionManager !== null &&
657
+ (typeof retentionManager.run !== 'function' ||
658
+ typeof retentionManager.afterCommit !== 'function' ||
659
+ typeof retentionManager._runUnlocked !== 'function' ||
660
+ typeof retentionManager._afterCommitUnlocked !== 'function')) {
661
+ return Promise.reject(storageError('Invalid retention manager'));
662
+ }
663
+ if (signal !== null && (typeof signal !== 'object' || typeof signal.aborted !== 'boolean')) {
664
+ return Promise.reject(storageError('Invalid commit abort signal'));
665
+ }
666
+ const mutable = (0, files_js_1.validateReplaceNames)(replaceNames);
667
+ return (0, root_coordinator_js_1.withNameLease)(this.layout.root, session.name, () => (0, root_coordinator_js_1.withRootLease)(this.layout.root, () => this._commit(session, retentionManager, signal, mutable)));
668
+ }
669
+ catch (error) {
670
+ return Promise.reject(error);
671
+ }
672
+ }
673
+ async _commit(session, retentionManager, signal, mutable = new Set()) {
674
+ assertNotAborted(signal);
675
+ await this._assertLayout();
676
+ const record = this._recordFromSession(session);
677
+ if ((0, files_js_1.isReservedHistoryName)(record.name)) {
678
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.INVALID_FILENAME, 'Reserved artifact name');
679
+ }
680
+ const stagingPath = this._stagingPath(record.transferId);
681
+ const finalPath = this._finalPath(record.name);
682
+ const stagingDigest = await digestExactFile(stagingPath, record.size, this.storage, true);
683
+ if (stagingDigest === null ||
684
+ !(0, hash_js_1.digestMatches)(stagingDigest.digest, b4a_1.default.from(record.sha256, 'hex'))) {
685
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.CHECKSUM_MISMATCH, 'Staging file checksum mismatch');
686
+ }
687
+ if (mutable.has(record.name)) {
688
+ const plan = await this._planReplacement(record);
689
+ if (plan.mode === 'idempotent') {
690
+ await this._removeFile(this._sessionPath(record.transferId), this.layout.sessions);
691
+ await this._removeFile(stagingPath, this.layout.staging);
692
+ return plan.record;
693
+ }
694
+ if (plan.mode === 'replace') {
695
+ return this._replace(record, stagingDigest.identity, plan, retentionManager, signal);
696
+ }
697
+ }
698
+ if (retentionManager) {
699
+ await retentionManager._runUnlocked({ incomingBytes: record.size, trigger: 'commit' });
700
+ }
701
+ assertNotAborted(signal);
702
+ const journalAttemptId = await this._writeJournal(record, stagingDigest.identity);
703
+ let linked = false;
704
+ let linearized = false;
705
+ try {
706
+ assertNotAborted(signal);
707
+ await this._linkStaging(stagingPath, finalPath, stagingDigest.identity, signal);
708
+ linked = true;
709
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.root, this.storage, () => syncDirectory(this.layout.root, this.storage));
710
+ assertNotAborted(signal);
711
+ await this._writeRecord(record);
712
+ linearized = true;
713
+ assertNotAborted(signal);
714
+ await this._removeFile(this._sessionPath(record.transferId), this.layout.sessions);
715
+ assertNotAborted(signal);
716
+ await this._removeFile(stagingPath, this.layout.staging);
717
+ await this._removeFile(this._tarStagingPath(record.transferId), this.layout.staging);
718
+ assertNotAborted(signal);
719
+ await this._discardJournal(record.transferId, journalAttemptId);
720
+ if (retentionManager)
721
+ await retentionManager._afterCommitUnlocked();
722
+ return record;
723
+ }
724
+ catch (err) {
725
+ if (linearized) {
726
+ this._reportCleanupPending(record, err);
727
+ return record;
728
+ }
729
+ if ((0, error_code_js_1.errorCode)(err) === errors_js_1.ERRORS.ABORTED) {
730
+ return this._abortAttempt(record, stagingDigest.identity, journalAttemptId, err);
731
+ }
732
+ if (!linked && (0, error_code_js_1.errorCode)(err) !== 'EEXIST') {
733
+ const final = await this._safeFileOrAbsent(finalPath, this.layout.root);
734
+ linked =
735
+ !!final &&
736
+ (0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(final), stagingDigest.identity) &&
737
+ (await this._matchesRecord(finalPath, this.layout.root, record));
738
+ }
739
+ if (linked) {
740
+ try {
741
+ await this._removeAbortedPublication(record, stagingDigest.identity);
742
+ await this._discardJournal(record.transferId, journalAttemptId);
743
+ }
744
+ catch (cleanupError) {
745
+ throw new AggregateError([err, cleanupError], 'Unable to roll back failed publication');
746
+ }
747
+ throw err;
748
+ }
749
+ try {
750
+ await this._discardJournal(record.transferId, journalAttemptId);
751
+ }
752
+ catch (cleanupCause) {
753
+ throw Object.assign(commitError('Unable to create final artifact', err), { cleanupCause });
754
+ }
755
+ if ((0, error_code_js_1.errorCode)(err) === 'EEXIST') {
756
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_EXISTS, 'Destination already exists', err);
757
+ }
758
+ throw commitError('Unable to create final artifact', err);
759
+ }
760
+ }
761
+ /**
762
+ * Classifies a configured mutable name. Only an exact managed current record
763
+ * whose verified content owns the visible path may be replaced; every other
764
+ * occupied path is an existing-name conflict.
765
+ */
766
+ async _planReplacement(record) {
767
+ const state = await this._rootPathState(record.name);
768
+ if (!state.present)
769
+ return { mode: 'create' };
770
+ if (!state.stat)
771
+ throw existsError('Destination already exists');
772
+ const current = await this._managedCurrent(record.name);
773
+ if (!current || !(0, commit_journal_js_1.identitiesEqual)(current.identity, (0, commit_journal_js_1.fileIdentity)(state.stat))) {
774
+ throw existsError('Destination already exists');
775
+ }
776
+ if (sameContent(current.record, record))
777
+ return { mode: 'idempotent', record: current.record };
778
+ return { mode: 'replace', oldRecord: current.record, finalIdentity: current.identity };
779
+ }
780
+ /**
781
+ * A repeated A→B→A sequence reuses a transfer ID that already owns a history
782
+ * artifact of identical content. The superseded duplicate is dropped inside
783
+ * the transaction so the returning content becomes the only copy.
784
+ */
785
+ async _dedupHistory(record) {
786
+ const name = (0, files_js_1.historyName)(record.transferId);
787
+ const existing = await this._readRecordOrAbsent(this._recordPath(record.transferId));
788
+ if (!existing)
789
+ return null;
790
+ if (existing.name !== name || !sameContent(existing, record)) {
791
+ throw storageError('Conflicting commit record');
792
+ }
793
+ const state = await this._rootPathState(name);
794
+ if (!state.present)
795
+ return { name, identity: null };
796
+ if (!state.stat)
797
+ throw existsError('History destination already exists');
798
+ const digested = await digestExactFile(this._finalPath(name), existing.size, this.storage);
799
+ if (!digested || !(0, hash_js_1.digestMatches)(digested.digest, b4a_1.default.from(existing.sha256, 'hex'))) {
800
+ throw existsError('History destination already exists');
801
+ }
802
+ return { name, identity: digested.identity };
803
+ }
804
+ async _replace(record, stagingIdentity, plan, retentionManager, signal) {
805
+ const { oldRecord, finalIdentity } = plan;
806
+ const history = (0, files_js_1.historyName)(oldRecord.transferId);
807
+ const newRecord = (0, commit_journal_js_1.assertCommitRecord)({
808
+ ...record,
809
+ version: commit_journal_js_1.REPLACEMENT_COMMIT_VERSION,
810
+ replaces: { name: record.name, transferId: oldRecord.transferId, historyName: history }
811
+ });
812
+ const finalPath = this._finalPath(record.name);
813
+ const historyPath = this._finalPath(history);
814
+ const stagingPath = this._stagingPath(record.transferId);
815
+ const historyState = await this._rootPathState(history);
816
+ if (historyState.present &&
817
+ !(historyState.stat && (0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(historyState.stat), finalIdentity))) {
818
+ throw existsError('History destination already exists');
819
+ }
820
+ const dedup = await this._dedupHistory(newRecord);
821
+ if (retentionManager) {
822
+ await retentionManager._runUnlocked({ incomingBytes: newRecord.size, trigger: 'commit' });
823
+ }
824
+ assertNotAborted(signal);
825
+ const pinned = await this._rootPathState(record.name);
826
+ if (!pinned.stat || !(0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(pinned.stat), finalIdentity)) {
827
+ throw commitError('Managed artifact changed before replacement');
828
+ }
829
+ const attempt = attemptId();
830
+ let journal = {
831
+ version: commit_journal_js_1.REPLACEMENT_JOURNAL_VERSION,
832
+ intent: 'replace',
833
+ state: 'committing',
834
+ phase: 'journaled',
835
+ transferId: newRecord.transferId,
836
+ attemptId: attempt,
837
+ name: newRecord.name,
838
+ historyName: history,
839
+ publicationName: `${attempt}.part`,
840
+ sourceStagingIdentity: stagingIdentity,
841
+ finalIdentity,
842
+ historyIdentity: finalIdentity,
843
+ publicationIdentity: stagingIdentity,
844
+ dedupHistoryName: dedup ? dedup.name : null,
845
+ oldRecord,
846
+ record: newRecord
847
+ };
848
+ const publicationPath = this._publicationPath(journal.publicationName);
849
+ await this._writeReplacementJournal(journal);
850
+ let linearized = false;
851
+ try {
852
+ assertNotAborted(signal);
853
+ if (dedup) {
854
+ if (dedup.identity) {
855
+ await this._removeOwnedFile(this._finalPath(dedup.name), this.layout.root, dedup.identity);
856
+ }
857
+ await this._removeFile(this._recordPath(newRecord.transferId), this.layout.commits);
858
+ journal = await this._setReplacementPhase(journal, 'deduped');
859
+ }
860
+ assertNotAborted(signal);
861
+ if (!(await this._rootPathState(history)).present) {
862
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.root, this.storage, () => this.storage.link(finalPath, historyPath));
863
+ await this._syncRoot();
864
+ }
865
+ journal = await this._setReplacementPhase(journal, 'history-linked');
866
+ assertNotAborted(signal);
867
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.publications, this.storage, () => (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, () => this.storage.link(stagingPath, publicationPath)));
868
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.publications, this.storage, () => syncDirectory(this.layout.publications, this.storage));
869
+ journal = await this._setReplacementPhase(journal, 'publication-linked');
870
+ assertNotAborted(signal);
871
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.root, this.storage, () => (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.publications, this.storage, () => this.storage.rename(publicationPath, finalPath)));
872
+ await this._syncRoot();
873
+ journal = await this._setReplacementPhase(journal, 'final-renamed');
874
+ assertNotAborted(signal);
875
+ await this._writeRecord(newRecord);
876
+ linearized = true;
877
+ journal = await this._setReplacementPhase(journal, 'current-sidecar');
878
+ await this._rewriteRecord(historyRecord(oldRecord, history));
879
+ journal = await this._setReplacementPhase(journal, 'history-sidecar');
880
+ await this._removeFile(this._sessionPath(newRecord.transferId), this.layout.sessions);
881
+ await this._removeFile(stagingPath, this.layout.staging);
882
+ await this._removeFile(this._tarStagingPath(newRecord.transferId), this.layout.staging);
883
+ await this._discardJournal(newRecord.transferId, attempt);
884
+ if (retentionManager)
885
+ await retentionManager._afterCommitUnlocked();
886
+ return newRecord;
887
+ }
888
+ catch (err) {
889
+ if (linearized) {
890
+ this._reportCleanupPending(newRecord, err);
891
+ return newRecord;
892
+ }
893
+ const aborted = (0, error_code_js_1.errorCode)(err) === errors_js_1.ERRORS.ABORTED;
894
+ try {
895
+ if (aborted)
896
+ await this._markAttemptAborting(newRecord.transferId, attempt);
897
+ await this._rollbackReplacement(journal);
898
+ await this._discardJournal(newRecord.transferId, attempt);
899
+ }
900
+ catch (cleanupError) {
901
+ throw new AggregateError([err, cleanupError], aborted
902
+ ? 'Unable to clean up aborted replacement'
903
+ : 'Unable to roll back failed replacement');
904
+ }
905
+ throw err;
906
+ }
907
+ }
908
+ /**
909
+ * Restores the exact pinned old inode at the mutable name and removes only
910
+ * transaction-owned history, publication, and sidecar paths. A foreign
911
+ * destination is never overwritten or removed.
912
+ */
913
+ async _rollbackReplacement(journal) {
914
+ const { name, historyName: history, sourceStagingIdentity, finalIdentity } = journal;
915
+ const finalPath = this._finalPath(name);
916
+ const historyPath = this._finalPath(history);
917
+ const publicationPath = this._publicationPath(journal.publicationName);
918
+ const final = await this._rootPathState(name);
919
+ const historyState = await this._rootPathState(history);
920
+ const ownsHistory = historyState.present &&
921
+ historyState.stat !== null &&
922
+ (0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(historyState.stat), finalIdentity);
923
+ if (historyState.present && !ownsHistory) {
924
+ throw storageError('Foreign artifact at replacement history name');
925
+ }
926
+ if (final.stat && (0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(final.stat), finalIdentity)) {
927
+ if (ownsHistory)
928
+ await this._removeFile(historyPath, this.layout.root);
929
+ }
930
+ else if ((final.stat && (0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(final.stat), sourceStagingIdentity)) ||
931
+ !final.present) {
932
+ if (!ownsHistory) {
933
+ if (final.present)
934
+ throw storageError('Replacement cannot restore the old artifact');
935
+ }
936
+ else {
937
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.root, this.storage, () => this.storage.rename(historyPath, finalPath));
938
+ await this._syncRoot();
939
+ }
940
+ }
941
+ else {
942
+ throw storageError('Foreign artifact at replacement destination');
943
+ }
944
+ await this._removeOwnedFile(publicationPath, this.layout.publications, sourceStagingIdentity);
945
+ const sidecar = await this._readRecordOrAbsent(this._recordPath(journal.record.transferId));
946
+ if (sidecar && recordsEqual(sidecar, journal.record)) {
947
+ await this._removeFile(this._recordPath(journal.record.transferId), this.layout.commits);
948
+ }
949
+ }
950
+ _readJournal(id) {
951
+ return (0, commit_journal_js_1.readCommitJournal)(id, this.layout, this.storage);
952
+ }
953
+ /** Every parsable v2 journal, used to explain transient duplicate names. */
954
+ async _pendingReplacements() {
955
+ const names = await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.journals, this.storage, () => this.storage.readdir(this.layout.journals));
956
+ const pending = [];
957
+ for (const filename of names.filter(isCommitRecordName).sort()) {
958
+ let journal;
959
+ try {
960
+ journal = await this._readJournal(filename.slice(0, -'.json'.length));
961
+ }
962
+ catch (err) {
963
+ if (err instanceof commit_journal_js_1.CorruptJournalError)
964
+ continue;
965
+ throw err;
966
+ }
967
+ if ((0, commit_journal_js_1.isReplacementJournal)(journal))
968
+ pending.push(journal);
969
+ }
970
+ return pending;
971
+ }
972
+ /**
973
+ * Finalizes or rolls back one v2 attempt. The durable new current sidecar is
974
+ * the linearization point: before it the old content is restored, after it
975
+ * the new content is preserved while history metadata and cleanup converge.
976
+ */
977
+ async _convergeReplacement(id, journal, sessionStore, { forceAbort = false } = {}) {
978
+ const { record, name, attemptId: attempt } = journal;
979
+ const finalPath = this._finalPath(name);
980
+ const newSidecar = await this._readRecordOrAbsent(this._recordPath(record.transferId));
981
+ if (newSidecar && !recordsEqual(newSidecar, record)) {
982
+ throw storageError('Commit sidecar does not match journal');
983
+ }
984
+ if (newSidecar) {
985
+ const final = await this._rootPathState(name);
986
+ if (!final.stat ||
987
+ !(0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(final.stat), journal.sourceStagingIdentity) ||
988
+ !(await this._matchesRecord(finalPath, this.layout.root, record))) {
989
+ throw storageError('Linearized replacement does not own its final artifact');
990
+ }
991
+ await this._repairHistorySidecar(journal);
992
+ await this._removeOwnedFile(this._publicationPath(journal.publicationName), this.layout.publications, journal.sourceStagingIdentity);
993
+ await this._cleanupReplacementLeftovers(id, journal, sessionStore);
994
+ await this._discardJournal(id, attempt);
995
+ return { status: 'COMMITTED', record };
996
+ }
997
+ if (forceAbort && journal.state === 'committing') {
998
+ const marked = await this._markAttemptAborting(id, attempt);
999
+ if (!(0, commit_journal_js_1.isReplacementJournal)(marked))
1000
+ throw storageError('Commit journal changed before abort');
1001
+ journal = marked;
1002
+ }
1003
+ if (!(await this._assertReplacementRetryable(id, journal, sessionStore))) {
1004
+ throw new commit_journal_js_1.CorruptJournalError('Replacement journal does not own verified staging');
1005
+ }
1006
+ await this._rollbackReplacement(journal);
1007
+ await this._discardJournal(id, attempt);
1008
+ return { status: journal.state === 'aborting' ? 'ABORTED' : 'RESUMABLE', record };
1009
+ }
1010
+ /**
1011
+ * Startup recovery for one v2 attempt. Revocation of a still-live session
1012
+ * aborts the attempt first so the same rollback path converges.
1013
+ */
1014
+ _recoverReplacementJournal(id, journal, sessionStore, isAuthorized) {
1015
+ let forceAbort = journal.state === 'aborting';
1016
+ if (isAuthorized && journal.state === 'committing') {
1017
+ if (!sessionStore || !(sessionStore.sessions instanceof Map)) {
1018
+ throw storageError('Session store cannot validate commit journal');
1019
+ }
1020
+ const session = sessionStore.sessions.get(id);
1021
+ if (session && !isAuthorized(session.ownerKey))
1022
+ forceAbort = true;
1023
+ }
1024
+ return this._convergeReplacement(id, journal, sessionStore, { forceAbort });
1025
+ }
1026
+ /** Writes or drops the history sidecar for an already linearized attempt. */
1027
+ async _repairHistorySidecar(journal) {
1028
+ const { oldRecord, historyName: history, finalIdentity } = journal;
1029
+ const expected = historyRecord(oldRecord, history);
1030
+ const sidecar = await this._readRecordOrAbsent(this._recordPath(oldRecord.transferId));
1031
+ if (sidecar && !recordsEqual(sidecar, expected) && !recordsEqual(sidecar, oldRecord)) {
1032
+ throw storageError('Replacement history sidecar does not match journal');
1033
+ }
1034
+ const state = await this._rootPathState(history);
1035
+ if (state.present &&
1036
+ (!state.stat || !(0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(state.stat), finalIdentity))) {
1037
+ throw storageError('Foreign artifact at replacement history name');
1038
+ }
1039
+ if (!state.present) {
1040
+ const sidecarPath = this._recordPath(oldRecord.transferId);
1041
+ if (sidecar)
1042
+ await this._removeFile(sidecarPath, this.layout.commits);
1043
+ return;
1044
+ }
1045
+ if (!(await this._matchesRecord(this._finalPath(history), this.layout.root, expected))) {
1046
+ throw storageError('Replacement history artifact does not match its record');
1047
+ }
1048
+ if (!sidecar || !recordsEqual(sidecar, expected))
1049
+ await this._rewriteRecord(expected);
1050
+ }
1051
+ async _cleanupReplacementLeftovers(id, journal, sessionStore) {
1052
+ const sessionFile = await this._safeFileOrAbsent(this._sessionPath(id), this.layout.sessions);
1053
+ if (sessionFile) {
1054
+ if (!sessionStore || typeof sessionStore.readVerified !== 'function') {
1055
+ throw storageError('Session store cannot validate commit cleanup');
1056
+ }
1057
+ this._assertSessionMatchesRecord(await sessionStore.readVerified(b4a_1.default.from(id, 'hex')), journal.record, 'Commit journal does not match verified session');
1058
+ await this._removeFile(this._sessionPath(id), this.layout.sessions);
1059
+ }
1060
+ const stagingPath = this._stagingPath(id);
1061
+ const staging = await this._safeFileOrAbsent(stagingPath, this.layout.staging);
1062
+ if (staging) {
1063
+ if (!(0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(staging), journal.sourceStagingIdentity)) {
1064
+ throw storageError('Staging file does not match commit journal');
1065
+ }
1066
+ await this._removeFile(stagingPath, this.layout.staging);
1067
+ }
1068
+ await this._removeFile(this._tarStagingPath(id), this.layout.staging);
1069
+ }
1070
+ /** Proves the verified session and staging an aborted attempt must retain. */
1071
+ async _assertReplacementRetryable(id, journal, sessionStore) {
1072
+ if (!sessionStore || typeof sessionStore.readVerified !== 'function') {
1073
+ throw storageError('Session store cannot validate commit journal');
1074
+ }
1075
+ const session = await sessionStore.readVerified(b4a_1.default.from(id, 'hex'));
1076
+ this._assertSessionMatchesRecord(session, journal.record, 'Replacement journal does not match verified session');
1077
+ const stagingPath = this._stagingPath(id);
1078
+ const staging = await this._safeFileOrAbsent(stagingPath, this.layout.staging);
1079
+ if (!staging || !(0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(staging), journal.sourceStagingIdentity)) {
1080
+ return false;
1081
+ }
1082
+ return this._matchesRecord(stagingPath, this.layout.staging, journal.record);
1083
+ }
1084
+ async recoverJournal(id, sessionStore, { isAuthorized = null } = {}) {
1085
+ await this._assertLayout();
1086
+ if (isAuthorized !== null && typeof isAuthorized !== 'function') {
1087
+ throw storageError('Invalid recovery authorization callback');
1088
+ }
1089
+ const journal = await this._readJournal(id);
1090
+ if (!journal)
1091
+ return { status: 'MISSING' };
1092
+ if ((0, commit_journal_js_1.isReplacementJournal)(journal)) {
1093
+ return (0, root_coordinator_js_1.withNameLease)(this.layout.root, journal.name, () => this._recoverReplacementJournal(id, journal, sessionStore, isAuthorized));
1094
+ }
1095
+ const { record, attemptId: journalAttemptId, sourceStagingIdentity } = journal;
1096
+ const finalPath = this._finalPath(record.name);
1097
+ const final = await this._safeFileOrAbsent(finalPath, this.layout.root);
1098
+ const sidecar = await this._readRecordOrAbsent(this._recordPath(id));
1099
+ if (final &&
1100
+ sidecar &&
1101
+ recordsEqual(sidecar, record) &&
1102
+ (0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(final), sourceStagingIdentity) &&
1103
+ (await this._matchesRecord(finalPath, this.layout.root, record))) {
1104
+ await this._removeFile(this._sessionPath(id), this.layout.sessions);
1105
+ await this._removeFile(this._stagingPath(id), this.layout.staging);
1106
+ await this._removeFile(this._tarStagingPath(id), this.layout.staging);
1107
+ await this._discardJournal(id, journalAttemptId);
1108
+ return { status: 'COMMITTED', record };
1109
+ }
1110
+ if (journal.state === 'aborting') {
1111
+ if (!sessionStore || typeof sessionStore.readVerified !== 'function') {
1112
+ throw storageError('Session store cannot validate aborted commit journal');
1113
+ }
1114
+ const session = await sessionStore.readVerified(b4a_1.default.from(id, 'hex'));
1115
+ if (session.name !== record.name ||
1116
+ session.size !== record.size ||
1117
+ toHex(session.digest) !== record.sha256 ||
1118
+ fingerprint(session.ownerKey) !== record.uploaderFingerprint) {
1119
+ throw new commit_journal_js_1.CorruptJournalError('Revoked commit journal does not match verified session');
1120
+ }
1121
+ const staging = await (0, layout_js_1.assertSafeFile)(this._stagingPath(id), this.storage);
1122
+ if (!(0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(staging), sourceStagingIdentity)) {
1123
+ throw new commit_journal_js_1.CorruptJournalError('Revoked commit staging does not match journal');
1124
+ }
1125
+ await this._removeAbortedPublication(record, sourceStagingIdentity);
1126
+ await this._discardJournal(id, journalAttemptId);
1127
+ return { status: 'ABORTED', record };
1128
+ }
1129
+ if (isAuthorized) {
1130
+ if (!sessionStore || !(sessionStore.sessions instanceof Map)) {
1131
+ throw storageError('Session store cannot validate commit journal');
1132
+ }
1133
+ const authorizedSession = sessionStore.sessions.get(id);
1134
+ if (authorizedSession && !isAuthorized(authorizedSession.ownerKey)) {
1135
+ await this._markAttemptAborting(id, journalAttemptId);
1136
+ return this.recoverJournal(id, sessionStore, { isAuthorized });
1137
+ }
1138
+ }
1139
+ if (final) {
1140
+ const staging = await this._safeFileOrAbsent(this._stagingPath(id), this.layout.staging);
1141
+ const provenanced = (0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(final), sourceStagingIdentity) ||
1142
+ (staging && (0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(final), (0, commit_journal_js_1.fileIdentity)(staging)));
1143
+ if (!provenanced) {
1144
+ await this._discardJournal(id, journalAttemptId);
1145
+ return { status: 'FILE_EXISTS', record };
1146
+ }
1147
+ if (!(await this._matchesRecord(finalPath, this.layout.root, record))) {
1148
+ throw storageError('Final file does not match commit journal');
1149
+ }
1150
+ if (!sidecar) {
1151
+ const session = await sessionStore.readVerified(b4a_1.default.from(id, 'hex'));
1152
+ if (session.name !== record.name ||
1153
+ session.size !== record.size ||
1154
+ toHex(session.digest) !== record.sha256 ||
1155
+ fingerprint(session.ownerKey) !== record.uploaderFingerprint) {
1156
+ throw new commit_journal_js_1.CorruptJournalError('Commit journal does not match verified session');
1157
+ }
1158
+ await this._writeRecord(record);
1159
+ }
1160
+ if (sidecar && !recordsEqual(sidecar, record)) {
1161
+ throw storageError('Commit sidecar does not match journal');
1162
+ }
1163
+ await this._removeFile(this._sessionPath(id), this.layout.sessions);
1164
+ await this._removeFile(this._stagingPath(id), this.layout.staging);
1165
+ await this._removeFile(this._tarStagingPath(id), this.layout.staging);
1166
+ await this._discardJournal(id, journalAttemptId);
1167
+ return { status: 'COMMITTED', record };
1168
+ }
1169
+ if (sidecar && !recordsEqual(sidecar, record)) {
1170
+ throw storageError('Commit sidecar does not match journal');
1171
+ }
1172
+ if (sidecar)
1173
+ await this._removeFile(this._recordPath(id), this.layout.commits);
1174
+ if (!sessionStore || typeof sessionStore.readVerified !== 'function') {
1175
+ throw storageError('Session store cannot validate recovery state');
1176
+ }
1177
+ const session = await sessionStore.readVerified(b4a_1.default.from(id, 'hex'));
1178
+ if (session.name !== record.name ||
1179
+ session.size !== record.size ||
1180
+ toHex(session.digest) !== record.sha256) {
1181
+ throw storageError('Verified session does not match commit journal');
1182
+ }
1183
+ const stagingPath = this._stagingPath(id);
1184
+ const staging = await this._safeFileOrAbsent(stagingPath, this.layout.staging);
1185
+ if (!staging || !(0, commit_journal_js_1.identitiesEqual)((0, commit_journal_js_1.fileIdentity)(staging), sourceStagingIdentity)) {
1186
+ throw new commit_journal_js_1.CorruptJournalError('Commit journal does not own verified staging');
1187
+ }
1188
+ if (!(await this._matchesRecord(stagingPath, this.layout.staging, record))) {
1189
+ throw storageError('Verified staging does not match commit journal');
1190
+ }
1191
+ await this._discardJournal(id, journalAttemptId);
1192
+ return { status: 'RESUMABLE', record };
1193
+ }
1194
+ async _scanRecords() {
1195
+ const names = await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.commits, this.storage, () => this.storage.readdir(this.layout.commits));
1196
+ const records = [];
1197
+ for (const filename of names.filter(isCommitRecordName).sort()) {
1198
+ const id = filename.slice(0, -'.json'.length);
1199
+ const record = await this._readRecordOrAbsent(this._recordPath(id));
1200
+ if (!record)
1201
+ throw storageError('Commit record disappeared during enumeration');
1202
+ if (record.transferId !== id)
1203
+ throw storageError('Commit record ID does not match path');
1204
+ records.push(record);
1205
+ }
1206
+ return records;
1207
+ }
1208
+ async scrubRecords() {
1209
+ await this._assertLayout();
1210
+ const names = await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.commits, this.storage, () => this.storage.readdir(this.layout.commits));
1211
+ const records = [];
1212
+ let deleted = 0;
1213
+ for (const filename of names.filter(isCommitRecordName).sort()) {
1214
+ const id = filename.slice(0, -'.json'.length);
1215
+ const recordPath = this._recordPath(id);
1216
+ let record = null;
1217
+ let invalid = false;
1218
+ try {
1219
+ const parsed = await (0, atomic_file_js_1.readJson)(recordPath, this.storage, commit_journal_js_1.MAX_COMMIT_METADATA_BYTES);
1220
+ try {
1221
+ record = (0, commit_journal_js_1.assertCommitRecord)(parsed);
1222
+ invalid = record.transferId !== id;
1223
+ }
1224
+ catch {
1225
+ invalid = true;
1226
+ }
1227
+ }
1228
+ catch (err) {
1229
+ if (!(err instanceof atomic_file_js_1.MetadataFormatError))
1230
+ throw err;
1231
+ invalid = true;
1232
+ }
1233
+ if (invalid) {
1234
+ await this._removeFile(recordPath, this.layout.commits);
1235
+ deleted++;
1236
+ continue;
1237
+ }
1238
+ if (!record)
1239
+ throw storageError('Commit record disappeared during scrub');
1240
+ records.push(record);
1241
+ }
1242
+ return { records: await this._resolveRecords(records), deleted };
1243
+ }
1244
+ async _resolveRecords(records) {
1245
+ const counts = new Map();
1246
+ for (const record of records)
1247
+ counts.set(record.name, (counts.get(record.name) ?? 0) + 1);
1248
+ let pending = null;
1249
+ const resolved = [];
1250
+ for (const record of records) {
1251
+ if ((counts.get(record.name) ?? 0) < 2) {
1252
+ resolved.push(record);
1253
+ continue;
1254
+ }
1255
+ if (pending === null)
1256
+ pending = await this._pendingReplacements();
1257
+ const owner = pending.find((journal) => journal.name === record.name &&
1258
+ journal.oldRecord.transferId === record.transferId &&
1259
+ recordsEqual(journal.oldRecord, record));
1260
+ resolved.push(owner ? historyRecord(record, owner.historyName) : record);
1261
+ }
1262
+ const seen = new Set();
1263
+ for (const record of resolved) {
1264
+ if (seen.has(record.name))
1265
+ throw storageError('Duplicate managed commit filename');
1266
+ seen.add(record.name);
1267
+ }
1268
+ return resolved;
1269
+ }
1270
+ /**
1271
+ * A replacement makes the old sidecar the history record after its new
1272
+ * current sidecar is durable, so one name can briefly carry two records.
1273
+ * Only a journal-owned duplicate is tolerated, reported at the history name
1274
+ * that recovery will persist; anything else fails closed.
1275
+ */
1276
+ async list() {
1277
+ await this._assertLayout();
1278
+ return this._resolveRecords(await this._scanRecords());
1279
+ }
1280
+ /**
1281
+ * Locates the durable record a caller is deleting. A pending replacement may
1282
+ * still carry the mutable name on the sidecar the caller knows as history.
1283
+ */
1284
+ async _storedRecordFor(record) {
1285
+ const stored = await this._readRecordOrAbsent(this._recordPath(record.transferId));
1286
+ if (!stored)
1287
+ return null;
1288
+ if (recordsEqual(stored, record))
1289
+ return stored;
1290
+ if (stored.name !== record.name &&
1291
+ recordsEqual(historyRecord(stored, record.name), record) &&
1292
+ (await this._pendingReplacements()).some((journal) => journal.name === stored.name &&
1293
+ journal.historyName === record.name &&
1294
+ recordsEqual(journal.oldRecord, stored))) {
1295
+ return stored;
1296
+ }
1297
+ throw storageError('Commit record changed before deletion');
1298
+ }
1299
+ async delete(record) {
1300
+ await this._assertLayout();
1301
+ (0, commit_journal_js_1.assertCommitRecord)(record);
1302
+ if (!(await this._storedRecordFor(record)))
1303
+ return false;
1304
+ const finalPath = this._finalPath(record.name);
1305
+ const final = await this._safeFileOrAbsent(finalPath, this.layout.root);
1306
+ if (final)
1307
+ await this._removeFile(finalPath, this.layout.root);
1308
+ await this._removeFile(this._recordPath(record.transferId), this.layout.commits);
1309
+ return true;
1310
+ }
1311
+ async purge(record, { preservePath = false } = {}) {
1312
+ await this._assertLayout();
1313
+ (0, commit_journal_js_1.assertCommitRecord)(record);
1314
+ if (!(await this._storedRecordFor(record)))
1315
+ return false;
1316
+ let final;
1317
+ if (preservePath) {
1318
+ let preservedPath = false;
1319
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.root, this.storage, async () => {
1320
+ try {
1321
+ await this.storage.lstat(this._finalPath(record.name));
1322
+ preservedPath = true;
1323
+ }
1324
+ catch (err) {
1325
+ if (!(0, error_code_js_1.isMissing)(err))
1326
+ throw err;
1327
+ }
1328
+ });
1329
+ final = { removed: false, preservedPath };
1330
+ }
1331
+ else {
1332
+ final = await this._removeManagedFinalOrAbsent(record);
1333
+ }
1334
+ await this._removeFile(this._recordPath(record.transferId), this.layout.commits);
1335
+ return { purged: true, preservedPath: final.preservedPath };
1336
+ }
1337
+ }
1338
+ exports.CommitStore = CommitStore;
1339
+ //# sourceMappingURL=commit-store.js.map