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,816 @@
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.TarSessionStore = 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 controls_js_1 = require("../tar-protocol/controls.js");
14
+ const extract_js_1 = require("../tar-protocol/extract.js");
15
+ const manifest_js_1 = require("../tar-protocol/manifest.js");
16
+ const atomic_file_js_1 = require("./atomic-file.js");
17
+ const commit_journal_js_1 = require("./commit-journal.js");
18
+ const layout_js_1 = require("./layout.js");
19
+ const validation_js_1 = require("../validation.js");
20
+ const VERSION = 2;
21
+ const RECEIVING = 'receiving';
22
+ const VERIFIED = 'verified';
23
+ const DELETING = 'deleting';
24
+ const READ_BYTES = 64 * 1024;
25
+ function problem(message, cause = null) {
26
+ return new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.PROTOCOL_INVALID, message, cause);
27
+ }
28
+ function hex(bytes) {
29
+ return b4a_1.default.toString(bytes, 'hex');
30
+ }
31
+ function bytes(value, name) {
32
+ if (typeof value !== 'string' || !/^[0-9a-f]{64}$/.test(value))
33
+ throw problem(`Invalid ${name}`);
34
+ return b4a_1.default.from(value, 'hex');
35
+ }
36
+ function uint(value, name) {
37
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
38
+ throw problem(`Invalid ${name}`);
39
+ }
40
+ }
41
+ function key(value, name) {
42
+ if (!b4a_1.default.isBuffer(value) || value.byteLength !== 32)
43
+ throw problem(`Invalid ${name}`);
44
+ }
45
+ function sameMetadata(session, metadata, owner) {
46
+ return (sodium_native_1.default.sodium_memcmp(session.ownerKey, owner) &&
47
+ session.name === metadata.name &&
48
+ session.size === metadata.fileSize &&
49
+ session.tarSize === metadata.tarSize &&
50
+ sodium_native_1.default.sodium_memcmp(session.digest, b4a_1.default.from(metadata.fileSha256, 'hex')) &&
51
+ sodium_native_1.default.sodium_memcmp(session.tarDigest, b4a_1.default.from(metadata.tarSha256, 'hex')));
52
+ }
53
+ async function writeAll(handle, value, position) {
54
+ let offset = 0;
55
+ while (offset < value.byteLength) {
56
+ const result = await handle.write(value, offset, value.byteLength - offset, position + offset);
57
+ const written = typeof result === 'number' ? result : result.bytesWritten;
58
+ if (!Number.isSafeInteger(written) || written <= 0)
59
+ throw problem('Unable to write TAR staging');
60
+ offset += written;
61
+ }
62
+ }
63
+ async function readAll(handle, size, position) {
64
+ const value = b4a_1.default.alloc(size);
65
+ let offset = 0;
66
+ while (offset < size) {
67
+ const result = await handle.read(value, offset, size - offset, position + offset);
68
+ const read = typeof result === 'number' ? result : result.bytesRead;
69
+ if (!Number.isSafeInteger(read) || read <= 0)
70
+ throw problem('Truncated TAR staging');
71
+ offset += read;
72
+ }
73
+ return value;
74
+ }
75
+ async function syncDirectory(directory, storage) {
76
+ const handle = await storage.open(directory, 'r');
77
+ try {
78
+ await handle.sync();
79
+ }
80
+ finally {
81
+ await handle.close();
82
+ }
83
+ }
84
+ /** Durable direct-TAR session authority. */
85
+ class TarSessionStore {
86
+ layout;
87
+ maxStagingBytes;
88
+ minFreeBytes;
89
+ clock;
90
+ resumeTtl;
91
+ isSessionActive;
92
+ storage;
93
+ sessions = new Map();
94
+ reservedBytes = 0;
95
+ purgedSessions = 0;
96
+ /** Sessions dropped from memory whose staging files could not be unlinked. */
97
+ strandedSessions = 0;
98
+ initialized = false;
99
+ closed = false;
100
+ closing = false;
101
+ pending = Promise.resolve();
102
+ closePromise = null;
103
+ verifying = new Set();
104
+ verificationTasks = new Set();
105
+ run(operation) {
106
+ const result = this.pending.then(operation, operation);
107
+ this.pending = result.catch(() => { });
108
+ return result;
109
+ }
110
+ constructor({ layout, maxStagingBytes, minFreeBytes = 0, clock = Date, resumeTtl, isSessionActive = () => false, storage = _fs_1.default.promises }) {
111
+ if (!layout || typeof layout !== 'object')
112
+ throw problem('Invalid storage layout');
113
+ (0, validation_js_1.assertSafeUint)(maxStagingBytes, 'maxStagingBytes');
114
+ (0, validation_js_1.assertSafeUint)(minFreeBytes, 'minFreeBytes');
115
+ if (!clock || typeof clock.now !== 'function')
116
+ throw problem('Invalid clock');
117
+ if (resumeTtl !== undefined) {
118
+ (0, validation_js_1.assertSafeUint)(resumeTtl, 'resumeTtl');
119
+ if (resumeTtl === 0)
120
+ throw problem('Invalid resumeTtl');
121
+ }
122
+ if (typeof isSessionActive !== 'function')
123
+ throw problem('Invalid session activity predicate');
124
+ if (!storage || typeof storage !== 'object')
125
+ throw problem('Invalid storage adapter');
126
+ this.layout = layout;
127
+ this.maxStagingBytes = maxStagingBytes;
128
+ this.minFreeBytes = minFreeBytes;
129
+ this.clock = clock;
130
+ this.resumeTtl = resumeTtl;
131
+ this.isSessionActive = isSessionActive;
132
+ this.storage = storage;
133
+ }
134
+ sessionPath(id) {
135
+ return _path_1.default.join(this.layout.sessions, `${id}.json`);
136
+ }
137
+ tarPath(id) {
138
+ return _path_1.default.join(this.layout.staging, `${id}.tar.part`);
139
+ }
140
+ filePath(id) {
141
+ return _path_1.default.join(this.layout.staging, `${id}.part`);
142
+ }
143
+ journalPath(id) {
144
+ return _path_1.default.join(this.layout.journals, `${id}.json`);
145
+ }
146
+ reserve(session) {
147
+ return session.tarSize + session.size;
148
+ }
149
+ assertReady() {
150
+ if (!this.initialized || this.closing || this.closed) {
151
+ throw problem('Session store is not available');
152
+ }
153
+ }
154
+ metadata(metadata) {
155
+ return (0, controls_js_1.decodeMetadataRecord)((0, controls_js_1.encodeMetadataRecord)(metadata));
156
+ }
157
+ fromDisk(id, record) {
158
+ const persisted = record;
159
+ if (persisted.version !== VERSION || persisted.transferId !== id) {
160
+ throw problem('Invalid TAR session');
161
+ }
162
+ const ownerKey = bytes(persisted.ownerKey, 'owner key');
163
+ const transferId = bytes(persisted.transferId, 'transfer ID');
164
+ uint(persisted.fileSize, 'file size');
165
+ uint(persisted.tarSize, 'TAR size');
166
+ uint(persisted.createdAt, 'creation time');
167
+ uint(persisted.updatedAt, 'update time');
168
+ if (!persisted.partialTar ||
169
+ persisted.partialTar.path !== `${id}.tar.part` ||
170
+ ![RECEIVING, VERIFIED, DELETING].includes(persisted.state)) {
171
+ throw problem('Invalid TAR session metadata');
172
+ }
173
+ uint(persisted.partialTar.size, 'partial TAR size');
174
+ if (persisted.partialTar.size > persisted.tarSize) {
175
+ throw problem('Oversized partial TAR');
176
+ }
177
+ const metadata = this.metadata({
178
+ v: 1,
179
+ name: persisted.name,
180
+ fileSize: persisted.fileSize,
181
+ fileSha256: hex(bytes(persisted.fileSha256, 'file digest')),
182
+ tarSize: persisted.tarSize,
183
+ tarSha256: hex(bytes(persisted.tarSha256, 'TAR digest')),
184
+ transferId: id,
185
+ reset: false
186
+ });
187
+ (0, manifest_js_1.assertMetadataTransferId)(ownerKey, metadata);
188
+ return {
189
+ id,
190
+ transferId,
191
+ ownerKey,
192
+ name: metadata.name,
193
+ size: metadata.fileSize,
194
+ digest: b4a_1.default.from(metadata.fileSha256, 'hex'),
195
+ tarSize: metadata.tarSize,
196
+ tarDigest: b4a_1.default.from(metadata.tarSha256, 'hex'),
197
+ tarPath: this.tarPath(id),
198
+ partialTarSize: persisted.partialTar.size,
199
+ createdAt: persisted.createdAt,
200
+ updatedAt: persisted.updatedAt,
201
+ state: persisted.state
202
+ };
203
+ }
204
+ serialize(session) {
205
+ return {
206
+ version: VERSION,
207
+ transferId: session.id,
208
+ ownerKey: hex(session.ownerKey),
209
+ name: session.name,
210
+ fileSize: session.size,
211
+ fileSha256: hex(session.digest),
212
+ tarSize: session.tarSize,
213
+ tarSha256: hex(session.tarDigest),
214
+ partialTar: { path: `${session.id}.tar.part`, size: session.partialTarSize },
215
+ createdAt: session.createdAt,
216
+ updatedAt: session.updatedAt,
217
+ state: session.state
218
+ };
219
+ }
220
+ async writeSession(session) {
221
+ await (0, atomic_file_js_1.writeAtomic)(this.sessionPath(session.id), b4a_1.default.from(JSON.stringify(this.serialize(session))), this.storage);
222
+ }
223
+ async remove(filePath, directory) {
224
+ await (0, layout_js_1.withSafeDirectoryIdentity)(directory, this.storage, async () => {
225
+ try {
226
+ await (0, layout_js_1.assertSafeFile)(filePath, this.storage);
227
+ }
228
+ catch (error) {
229
+ // `assertSafeFile` also throws for a path that exists but is not a
230
+ // regular file, and those errors carry a null cause; `isMissing`
231
+ // absorbs that rather than dereferencing it.
232
+ if ((0, error_code_js_1.isMissing)(error))
233
+ return;
234
+ throw error;
235
+ }
236
+ await this.storage.unlink(filePath);
237
+ });
238
+ await (0, layout_js_1.withSafeDirectoryIdentity)(directory, this.storage, () => syncDirectory(directory, this.storage));
239
+ }
240
+ async purgeCorruptSession(id) {
241
+ await this.remove(this.tarPath(id), this.layout.staging);
242
+ await this.remove(this.sessionPath(id), this.layout.sessions);
243
+ this.purgedSessions++;
244
+ }
245
+ /** Removes only a regular crash residue and syncs its parent before reporting it purged. */
246
+ async purgeSafeResidue(filePath, directory) {
247
+ await (0, layout_js_1.withSafeDirectoryIdentity)(directory, this.storage, async () => {
248
+ await (0, layout_js_1.assertSafeFile)(filePath, this.storage);
249
+ await this.storage.unlink(filePath);
250
+ });
251
+ await (0, layout_js_1.withSafeDirectoryIdentity)(directory, this.storage, () => syncDirectory(directory, this.storage));
252
+ this.purgedSessions++;
253
+ }
254
+ hashPrefix(session) {
255
+ return (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, async () => {
256
+ const handle = await (0, layout_js_1.openSafeRegularFile)(session.tarPath, 'read', this.storage);
257
+ try {
258
+ const stat = await handle.stat();
259
+ if (!stat.isFile() || stat.size !== session.partialTarSize) {
260
+ throw problem('Partial TAR size changed');
261
+ }
262
+ const state = b4a_1.default.alloc(sodium_native_1.default.crypto_hash_sha256_STATEBYTES);
263
+ sodium_native_1.default.crypto_hash_sha256_init(state);
264
+ for (let position = 0; position < session.partialTarSize;) {
265
+ const chunk = await readAll(handle, Math.min(READ_BYTES, session.partialTarSize - position), position);
266
+ sodium_native_1.default.crypto_hash_sha256_update(state, chunk);
267
+ position += chunk.byteLength;
268
+ }
269
+ const digest = b4a_1.default.alloc(32);
270
+ sodium_native_1.default.crypto_hash_sha256_final(state, digest);
271
+ return digest;
272
+ }
273
+ finally {
274
+ await handle.close();
275
+ }
276
+ });
277
+ }
278
+ ensureTarLength(session) {
279
+ return (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, async () => {
280
+ const handle = await (0, layout_js_1.openSafeRegularFile)(session.tarPath, 'write', this.storage);
281
+ try {
282
+ const stat = await handle.stat();
283
+ if (stat.size < session.partialTarSize)
284
+ return false;
285
+ if (stat.size > session.tarSize) {
286
+ throw problem('Oversized partial TAR');
287
+ }
288
+ if (stat.size > session.partialTarSize) {
289
+ await handle.truncate(session.partialTarSize);
290
+ await handle.sync();
291
+ }
292
+ return true;
293
+ }
294
+ finally {
295
+ await handle.close();
296
+ }
297
+ });
298
+ }
299
+ async diskReserve(incoming) {
300
+ if (this.minFreeBytes === 0)
301
+ return;
302
+ if (typeof this.storage.statfs !== 'function') {
303
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.DISK_RESERVE, 'Free-disk reserve is unavailable');
304
+ }
305
+ const stat = await this.storage.statfs(this.layout.staging);
306
+ const available = BigInt(stat.bavail) * BigInt(stat.bsize);
307
+ if (available < BigInt(this.minFreeBytes) + BigInt(this.reservedBytes) + BigInt(incoming)) {
308
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.DISK_RESERVE, 'Minimum free-disk reserve exceeded');
309
+ }
310
+ }
311
+ init() {
312
+ return this.run(async () => {
313
+ if (this.initialized)
314
+ return;
315
+ for (const directory of [
316
+ this.layout.root,
317
+ this.layout.internal,
318
+ this.layout.staging,
319
+ this.layout.sessions
320
+ ]) {
321
+ await (0, layout_js_1.withSafeDirectoryIdentity)(directory, this.storage, () => undefined);
322
+ }
323
+ const names = await this.storage.readdir(this.layout.sessions);
324
+ for (const name of names.sort()) {
325
+ if (!/^[0-9a-f]{64}\.json$/.test(name)) {
326
+ if (/^\.[0-9a-f]{64}\.json\.[0-9a-f]{32,64}\.tmp$/.test(name)) {
327
+ await this.purgeSafeResidue(_path_1.default.join(this.layout.sessions, name), this.layout.sessions);
328
+ continue;
329
+ }
330
+ throw problem('Invalid session metadata path');
331
+ }
332
+ const id = name.slice(0, -5);
333
+ let record;
334
+ try {
335
+ record = await (0, atomic_file_js_1.readJson)(this.sessionPath(id), this.storage, 16 * 1024);
336
+ }
337
+ catch (error) {
338
+ if (!(error instanceof atomic_file_js_1.MetadataFormatError))
339
+ throw error;
340
+ await this.purgeCorruptSession(id);
341
+ continue;
342
+ }
343
+ if (record.version === 1) {
344
+ await this.remove(this.sessionPath(id), this.layout.sessions);
345
+ await this.remove(this.tarPath(id), this.layout.staging);
346
+ await this.remove(this.filePath(id), this.layout.staging);
347
+ await this.remove(this.journalPath(id), this.layout.journals);
348
+ this.purgedSessions++;
349
+ continue;
350
+ }
351
+ let session;
352
+ try {
353
+ session = this.fromDisk(id, record);
354
+ }
355
+ catch (error) {
356
+ if (!(error instanceof errors_js_1.SwarmDeployError) || error.code !== errors_js_1.ERRORS.PROTOCOL_INVALID) {
357
+ throw error;
358
+ }
359
+ await this.purgeCorruptSession(id);
360
+ continue;
361
+ }
362
+ if (session.state === DELETING) {
363
+ await this.remove(session.tarPath, this.layout.staging);
364
+ await this.remove(this.filePath(id), this.layout.staging);
365
+ await this.remove(this.sessionPath(id), this.layout.sessions);
366
+ continue;
367
+ }
368
+ if (session.state === RECEIVING) {
369
+ if (!(await this.ensureTarLength(session))) {
370
+ session.partialTarSize = 0;
371
+ session.updatedAt = this.clock.now();
372
+ await this.writeSession(session);
373
+ }
374
+ }
375
+ else {
376
+ await (0, layout_js_1.assertSafeFile)(this.filePath(id), this.storage);
377
+ }
378
+ this.sessions.set(id, session);
379
+ this.reservedBytes += this.reserve(session);
380
+ if (this.reservedBytes > this.maxStagingBytes) {
381
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.STAGING_LIMIT, 'Recovered TAR sessions exceed staging limit');
382
+ }
383
+ }
384
+ const expected = new Set();
385
+ for (const session of this.sessions.values()) {
386
+ expected.add(`${session.id}.tar.part`);
387
+ if (session.state === VERIFIED)
388
+ expected.add(`${session.id}.part`);
389
+ }
390
+ for (const name of await this.storage.readdir(this.layout.staging)) {
391
+ if (expected.has(name))
392
+ continue;
393
+ const staged = /^([0-9a-f]{64})\.(?:part|tar\.part)$/.exec(name);
394
+ if (staged) {
395
+ const journal = await (0, commit_journal_js_1.readCommitJournal)(staged[1], this.layout, this.storage);
396
+ if (journal)
397
+ continue;
398
+ await this.purgeSafeResidue(_path_1.default.join(this.layout.staging, name), this.layout.staging);
399
+ continue;
400
+ }
401
+ await this.purgeSafeResidue(_path_1.default.join(this.layout.staging, name), this.layout.staging);
402
+ }
403
+ this.initialized = true;
404
+ });
405
+ }
406
+ admit(ownerKey, input) {
407
+ return this.run(async () => {
408
+ this.assertReady();
409
+ key(ownerKey, 'owner key');
410
+ const metadata = this.metadata(input);
411
+ (0, manifest_js_1.assertMetadataTransferId)(ownerKey, metadata);
412
+ const id = metadata.transferId;
413
+ if (this.resumeTtl !== undefined)
414
+ await this.expireUnlocked(this.resumeTtl);
415
+ let session = this.sessions.get(id);
416
+ if (session) {
417
+ if (!sameMetadata(session, metadata, ownerKey)) {
418
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_BUSY, 'Transfer ID belongs to different immutable metadata');
419
+ }
420
+ if (this.verifying.has(id)) {
421
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_BUSY, 'TAR session is being verified');
422
+ }
423
+ if (session.state === DELETING) {
424
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_BUSY, 'TAR session is quarantined');
425
+ }
426
+ if (session.state === VERIFIED) {
427
+ if (metadata.reset)
428
+ throw problem('Cannot reset verified TAR session');
429
+ return { status: 'VERIFIED' };
430
+ }
431
+ if (metadata.reset && session.state === RECEIVING) {
432
+ await this.resetUnlocked(session);
433
+ return { status: 'ACCEPT', offset: 0 };
434
+ }
435
+ if (session.state !== RECEIVING || session.partialTarSize === 0) {
436
+ return { status: 'ACCEPT', offset: 0 };
437
+ }
438
+ return {
439
+ status: 'RESUME',
440
+ offset: session.partialTarSize,
441
+ prefixSha256: await this.hashPrefix(session)
442
+ };
443
+ }
444
+ for (const candidate of this.sessions.values()) {
445
+ if (candidate.name === metadata.name) {
446
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_BUSY, 'Destination has an active session');
447
+ }
448
+ }
449
+ const reservation = metadata.tarSize + metadata.fileSize;
450
+ if (this.reservedBytes > this.maxStagingBytes - reservation) {
451
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.STAGING_LIMIT, 'Staging capacity exceeded');
452
+ }
453
+ await this.diskReserve(reservation);
454
+ const now = this.clock.now();
455
+ session = {
456
+ id,
457
+ transferId: b4a_1.default.from(id, 'hex'),
458
+ ownerKey: b4a_1.default.from(ownerKey),
459
+ name: metadata.name,
460
+ size: metadata.fileSize,
461
+ digest: b4a_1.default.from(metadata.fileSha256, 'hex'),
462
+ tarSize: metadata.tarSize,
463
+ tarDigest: b4a_1.default.from(metadata.tarSha256, 'hex'),
464
+ tarPath: this.tarPath(id),
465
+ partialTarSize: 0,
466
+ createdAt: now,
467
+ updatedAt: now,
468
+ state: RECEIVING
469
+ };
470
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, async () => {
471
+ const handle = await (0, layout_js_1.openSafeRegularFile)(session.tarPath, 'create', this.storage);
472
+ await handle.sync();
473
+ await handle.close();
474
+ await syncDirectory(this.layout.staging, this.storage);
475
+ });
476
+ await this.writeSession(session);
477
+ this.sessions.set(id, session);
478
+ this.reservedBytes += reservation;
479
+ return { status: 'ACCEPT', offset: 0 };
480
+ });
481
+ }
482
+ append(ownerKey, input, offset, data) {
483
+ return this.run(async () => {
484
+ this.assertReady();
485
+ key(ownerKey, 'owner key');
486
+ const metadata = this.metadata(input);
487
+ const session = this.sessions.get(metadata.transferId);
488
+ if (!session || !sameMetadata(session, metadata, ownerKey)) {
489
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_BUSY, 'Unknown or mismatched TAR session');
490
+ }
491
+ if (this.verifying.has(session.id)) {
492
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_BUSY, 'TAR session is being verified');
493
+ }
494
+ if (session.state === DELETING) {
495
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_BUSY, 'TAR session is quarantined');
496
+ }
497
+ if (session.state !== RECEIVING)
498
+ throw problem('Cannot append to verified TAR session');
499
+ if (!Number.isSafeInteger(offset) ||
500
+ offset !== session.partialTarSize ||
501
+ !b4a_1.default.isBuffer(data)) {
502
+ throw problem('TAR append offset does not match durable progress');
503
+ }
504
+ if (data.byteLength > session.tarSize - offset) {
505
+ throw problem('TAR append exceeds deterministic length');
506
+ }
507
+ if (data.byteLength === 0)
508
+ throw problem('TAR append must make positive progress');
509
+ const previous = session.partialTarSize;
510
+ try {
511
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, async () => {
512
+ const handle = await (0, layout_js_1.openSafeRegularFile)(session.tarPath, 'write', this.storage);
513
+ try {
514
+ await writeAll(handle, data, offset);
515
+ await handle.sync();
516
+ }
517
+ finally {
518
+ await handle.close();
519
+ }
520
+ });
521
+ }
522
+ catch (cause) {
523
+ await this.rollbackOrQuarantine(session, previous, cause);
524
+ throw cause;
525
+ }
526
+ session.partialTarSize += data.byteLength;
527
+ session.updatedAt = this.clock.now();
528
+ try {
529
+ await this.writeSession(session);
530
+ }
531
+ catch (error) {
532
+ if (!(0, atomic_file_js_1.atomicWriteRenamed)(error)) {
533
+ await this.rollbackOrQuarantine(session, previous, error);
534
+ session.partialTarSize = previous;
535
+ }
536
+ throw error;
537
+ }
538
+ return session.partialTarSize;
539
+ });
540
+ }
541
+ async rollbackTar(session, length) {
542
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, async () => {
543
+ const handle = await (0, layout_js_1.openSafeRegularFile)(session.tarPath, 'write', this.storage);
544
+ try {
545
+ await handle.truncate(length);
546
+ await handle.sync();
547
+ }
548
+ finally {
549
+ await handle.close();
550
+ }
551
+ });
552
+ }
553
+ async rollbackOrQuarantine(session, length, cause) {
554
+ try {
555
+ await this.rollbackTar(session, length);
556
+ }
557
+ catch (cleanupCause) {
558
+ session.state = DELETING;
559
+ try {
560
+ await this.writeSession(session);
561
+ }
562
+ catch (quarantineCause) {
563
+ throw Object.assign(problem('TAR append rollback and quarantine failed', cause), {
564
+ cleanupCause,
565
+ quarantineCause
566
+ });
567
+ }
568
+ throw Object.assign(problem('TAR append and rollback failed', cause), { cleanupCause });
569
+ }
570
+ }
571
+ async resetUnlocked(session) {
572
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, async () => {
573
+ const handle = await (0, layout_js_1.openSafeRegularFile)(session.tarPath, 'write', this.storage);
574
+ try {
575
+ await handle.truncate(0);
576
+ await handle.sync();
577
+ }
578
+ finally {
579
+ await handle.close();
580
+ }
581
+ });
582
+ session.partialTarSize = 0;
583
+ session.updatedAt = this.clock.now();
584
+ await this.writeSession(session);
585
+ }
586
+ async quarantineVerificationFailure(session, cause, cleanupCause) {
587
+ const failure = Object.assign(problem('TAR verification cleanup failed', cause), {
588
+ cleanupCause
589
+ });
590
+ try {
591
+ await this.run(async () => {
592
+ session.state = DELETING;
593
+ session.updatedAt = this.clock.now();
594
+ await this.writeSession(session);
595
+ });
596
+ }
597
+ catch (quarantineCause) {
598
+ throw Object.assign(failure, { quarantineCause });
599
+ }
600
+ throw failure;
601
+ }
602
+ verify(ownerKey, input) {
603
+ const task = (async () => {
604
+ const prepared = await this.run(() => {
605
+ this.assertReady();
606
+ key(ownerKey, 'owner key');
607
+ const metadata = this.metadata(input);
608
+ const session = this.sessions.get(metadata.transferId);
609
+ if (!session ||
610
+ session.state !== RECEIVING ||
611
+ !sameMetadata(session, metadata, ownerKey) ||
612
+ this.verifying.has(session.id)) {
613
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_BUSY, 'Unknown or mismatched TAR session');
614
+ }
615
+ if (session.partialTarSize !== session.tarSize)
616
+ throw problem('TAR transfer is incomplete');
617
+ this.verifying.add(session.id);
618
+ return { metadata, session };
619
+ });
620
+ const { metadata, session } = prepared;
621
+ let file = null;
622
+ let fileOffset = 0;
623
+ try {
624
+ file = await (0, layout_js_1.openSafeRegularFile)(this.filePath(session.id), 'create', this.storage);
625
+ const source = this.readTar(session);
626
+ await (0, extract_js_1.validateAndExtractTar)(source, metadata, {
627
+ writeTar: () => { },
628
+ writeFile: async (chunk) => {
629
+ await writeAll(file, chunk, fileOffset);
630
+ fileOffset += chunk.byteLength;
631
+ },
632
+ complete: async () => {
633
+ await file.sync();
634
+ },
635
+ abort: async () => {
636
+ await file?.close().catch(() => { });
637
+ file = null;
638
+ await this.remove(this.filePath(session.id), this.layout.staging).catch(() => { });
639
+ }
640
+ });
641
+ await file.close();
642
+ file = null;
643
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, () => syncDirectory(this.layout.staging, this.storage));
644
+ await this.run(async () => {
645
+ if (this.sessions.get(session.id) !== session ||
646
+ session.state !== RECEIVING ||
647
+ !this.verifying.has(session.id)) {
648
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_BUSY, 'TAR session changed during verification');
649
+ }
650
+ const previousUpdatedAt = session.updatedAt;
651
+ session.state = VERIFIED;
652
+ session.updatedAt = this.clock.now();
653
+ try {
654
+ await this.writeSession(session);
655
+ }
656
+ catch (error) {
657
+ if (!(0, atomic_file_js_1.atomicWriteRenamed)(error)) {
658
+ session.state = RECEIVING;
659
+ session.updatedAt = previousUpdatedAt;
660
+ }
661
+ throw error;
662
+ }
663
+ });
664
+ return session;
665
+ }
666
+ catch (error) {
667
+ if (file)
668
+ await file.close().catch(() => { });
669
+ if (session.state !== VERIFIED) {
670
+ try {
671
+ await this.remove(this.filePath(session.id), this.layout.staging);
672
+ }
673
+ catch (cleanupCause) {
674
+ await this.quarantineVerificationFailure(session, error, cleanupCause);
675
+ }
676
+ }
677
+ throw error;
678
+ }
679
+ finally {
680
+ await this.run(() => {
681
+ this.verifying.delete(session.id);
682
+ });
683
+ }
684
+ })();
685
+ this.verificationTasks.add(task);
686
+ const settled = () => {
687
+ this.verificationTasks.delete(task);
688
+ };
689
+ task.then(settled, settled);
690
+ return task;
691
+ }
692
+ async *readTar(session) {
693
+ const handle = await (0, layout_js_1.openSafeRegularFile)(session.tarPath, 'read', this.storage);
694
+ try {
695
+ for (let position = 0; position < session.tarSize;) {
696
+ const chunk = await readAll(handle, Math.min(READ_BYTES, session.tarSize - position), position);
697
+ position += chunk.byteLength;
698
+ yield chunk;
699
+ }
700
+ }
701
+ finally {
702
+ await handle.close();
703
+ }
704
+ }
705
+ readVerified(transferId) {
706
+ return this.run(async () => {
707
+ this.assertReady();
708
+ key(transferId, 'transfer ID');
709
+ const session = this.sessions.get(hex(transferId));
710
+ if (!session || session.state !== VERIFIED)
711
+ throw problem('Session is not verified');
712
+ await (0, layout_js_1.assertSafeFile)(this.filePath(session.id), this.storage);
713
+ return session;
714
+ });
715
+ }
716
+ /**
717
+ * Drops a session from the in-memory authority exactly once.
718
+ *
719
+ * `reservedBytes` must always equal the sum of `reserve()` over `sessions`,
720
+ * so the bookkeeping runs in a `finally`: a failed unlink would otherwise
721
+ * strand the reservation for the lifetime of the process, and leave the name
722
+ * permanently colliding in `admit()`. Any file that survives the failure is a
723
+ * crash residue, which `init()` purges on the next start.
724
+ */
725
+ async removeSession(session) {
726
+ session.state = DELETING;
727
+ session.updatedAt = this.clock.now();
728
+ try {
729
+ await this.writeSession(session);
730
+ await this.remove(session.tarPath, this.layout.staging);
731
+ await this.remove(this.filePath(session.id), this.layout.staging);
732
+ await this.remove(this.sessionPath(session.id), this.layout.sessions);
733
+ }
734
+ finally {
735
+ if (this.sessions.delete(session.id))
736
+ this.reservedBytes -= this.reserve(session);
737
+ }
738
+ }
739
+ delete(transferId) {
740
+ return this.run(async () => {
741
+ this.assertReady();
742
+ key(transferId, 'transfer ID');
743
+ const id = hex(transferId);
744
+ const session = this.sessions.get(id);
745
+ if (!session)
746
+ return false;
747
+ if (this.verifying.has(id)) {
748
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_BUSY, 'TAR session is being verified');
749
+ }
750
+ if (session.state === DELETING) {
751
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_BUSY, 'TAR session is quarantined');
752
+ }
753
+ await this.removeSession(session);
754
+ return true;
755
+ });
756
+ }
757
+ /**
758
+ * Sweeps expired sessions without letting one failure abort the rest.
759
+ *
760
+ * `admit()` runs this opportunistically on every offer, so a single session
761
+ * whose files cannot be unlinked must not deny service to every later
762
+ * upload. Failures are counted in `strandedSessions` and reported to the
763
+ * explicit `expire()` caller, which asked for the sweep and can act on it.
764
+ */
765
+ async expireUnlocked(ttl, predicate = () => true) {
766
+ let removed = 0;
767
+ let failure = null;
768
+ for (const session of [...this.sessions.values()]) {
769
+ if (this.verifying.has(session.id) ||
770
+ session.state === DELETING ||
771
+ this.clock.now() - session.updatedAt <= ttl ||
772
+ !predicate(session) ||
773
+ this.isSessionActive(session)) {
774
+ continue;
775
+ }
776
+ try {
777
+ await this.removeSession(session);
778
+ removed++;
779
+ }
780
+ catch (error) {
781
+ this.strandedSessions++;
782
+ if (failure === null)
783
+ failure = error;
784
+ }
785
+ }
786
+ return { removed, failure };
787
+ }
788
+ expire(ttl, predicate = () => true) {
789
+ return this.run(async () => {
790
+ this.assertReady();
791
+ const { removed, failure } = await this.expireUnlocked(ttl, predicate);
792
+ if (failure !== null)
793
+ throw failure;
794
+ return removed;
795
+ });
796
+ }
797
+ retireCommitted(transferId) {
798
+ return this.delete(transferId);
799
+ }
800
+ close() {
801
+ if (this.closePromise)
802
+ return this.closePromise;
803
+ this.closePromise = (async () => {
804
+ await this.run(() => {
805
+ this.closing = true;
806
+ });
807
+ await Promise.allSettled([...this.verificationTasks]);
808
+ await this.run(() => {
809
+ this.closed = true;
810
+ });
811
+ })();
812
+ return this.closePromise;
813
+ }
814
+ }
815
+ exports.TarSessionStore = TarSessionStore;
816
+ //# sourceMappingURL=tar-session-store.js.map