swarm-deploy 0.0.0 → 0.2.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 (148) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/LICENSE.md +201 -0
  3. package/NOTICE.md +13 -0
  4. package/README.md +1283 -0
  5. package/RELEASING.md +71 -0
  6. package/SECURITY.md +40 -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 +27 -0
  17. package/dist/cli.js +751 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/client.d.ts +111 -0
  20. package/dist/client.js +276 -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 +31 -0
  29. package/dist/errors.js +37 -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 +32 -0
  35. package/dist/files.js +85 -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/hooks-module.d.ts +9 -0
  41. package/dist/hooks-module.js +118 -0
  42. package/dist/hooks-module.js.map +1 -0
  43. package/dist/hooks.d.ts +69 -0
  44. package/dist/hooks.js +88 -0
  45. package/dist/hooks.js.map +1 -0
  46. package/dist/identity.d.ts +7 -0
  47. package/dist/identity.js +50 -0
  48. package/dist/identity.js.map +1 -0
  49. package/dist/index.d.ts +12 -0
  50. package/dist/index.js +21 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/os-node.d.ts +2 -0
  53. package/dist/os-node.js +7 -0
  54. package/dist/os-node.js.map +1 -0
  55. package/dist/path-node.d.ts +2 -0
  56. package/dist/path-node.js +7 -0
  57. package/dist/path-node.js.map +1 -0
  58. package/dist/process-node.d.ts +2 -0
  59. package/dist/process-node.js +7 -0
  60. package/dist/process-node.js.map +1 -0
  61. package/dist/release.d.ts +26 -0
  62. package/dist/release.js +419 -0
  63. package/dist/release.js.map +1 -0
  64. package/dist/server.d.ts +239 -0
  65. package/dist/server.js +928 -0
  66. package/dist/server.js.map +1 -0
  67. package/dist/storage/atomic-file.d.ts +15 -0
  68. package/dist/storage/atomic-file.js +172 -0
  69. package/dist/storage/atomic-file.js.map +1 -0
  70. package/dist/storage/commit-journal.d.ts +120 -0
  71. package/dist/storage/commit-journal.js +417 -0
  72. package/dist/storage/commit-journal.js.map +1 -0
  73. package/dist/storage/commit-store.d.ts +307 -0
  74. package/dist/storage/commit-store.js +1691 -0
  75. package/dist/storage/commit-store.js.map +1 -0
  76. package/dist/storage/layout.d.ts +16 -0
  77. package/dist/storage/layout.js +409 -0
  78. package/dist/storage/layout.js.map +1 -0
  79. package/dist/storage/link-store.d.ts +70 -0
  80. package/dist/storage/link-store.js +363 -0
  81. package/dist/storage/link-store.js.map +1 -0
  82. package/dist/storage/recovery.d.ts +100 -0
  83. package/dist/storage/recovery.js +260 -0
  84. package/dist/storage/recovery.js.map +1 -0
  85. package/dist/storage/retention.d.ts +183 -0
  86. package/dist/storage/retention.js +619 -0
  87. package/dist/storage/retention.js.map +1 -0
  88. package/dist/storage/root-coordinator.d.ts +7 -0
  89. package/dist/storage/root-coordinator.js +27 -0
  90. package/dist/storage/root-coordinator.js.map +1 -0
  91. package/dist/storage/session-store.d.ts +2 -0
  92. package/dist/storage/session-store.js +7 -0
  93. package/dist/storage/session-store.js.map +1 -0
  94. package/dist/storage/tar-session-store.d.ts +147 -0
  95. package/dist/storage/tar-session-store.js +1044 -0
  96. package/dist/storage/tar-session-store.js.map +1 -0
  97. package/dist/storage/tree-fs.d.ts +44 -0
  98. package/dist/storage/tree-fs.js +580 -0
  99. package/dist/storage/tree-fs.js.map +1 -0
  100. package/dist/storage/tree-staging.d.ts +9 -0
  101. package/dist/storage/tree-staging.js +64 -0
  102. package/dist/storage/tree-staging.js.map +1 -0
  103. package/dist/storage/types.d.ts +77 -0
  104. package/dist/storage/types.js +3 -0
  105. package/dist/storage/types.js.map +1 -0
  106. package/dist/symlinks.d.ts +36 -0
  107. package/dist/symlinks.js +128 -0
  108. package/dist/symlinks.js.map +1 -0
  109. package/dist/tar-protocol/controls.d.ts +73 -0
  110. package/dist/tar-protocol/controls.js +291 -0
  111. package/dist/tar-protocol/controls.js.map +1 -0
  112. package/dist/tar-protocol/direct-wire.d.ts +43 -0
  113. package/dist/tar-protocol/direct-wire.js +194 -0
  114. package/dist/tar-protocol/direct-wire.js.map +1 -0
  115. package/dist/tar-protocol/extract.d.ts +18 -0
  116. package/dist/tar-protocol/extract.js +124 -0
  117. package/dist/tar-protocol/extract.js.map +1 -0
  118. package/dist/tar-protocol/hash.d.ts +10 -0
  119. package/dist/tar-protocol/hash.js +44 -0
  120. package/dist/tar-protocol/hash.js.map +1 -0
  121. package/dist/tar-protocol/lifecycle.d.ts +13 -0
  122. package/dist/tar-protocol/lifecycle.js +90 -0
  123. package/dist/tar-protocol/lifecycle.js.map +1 -0
  124. package/dist/tar-protocol/manifest.d.ts +58 -0
  125. package/dist/tar-protocol/manifest.js +312 -0
  126. package/dist/tar-protocol/manifest.js.map +1 -0
  127. package/dist/tar-protocol/tree-extract.d.ts +37 -0
  128. package/dist/tar-protocol/tree-extract.js +240 -0
  129. package/dist/tar-protocol/tree-extract.js.map +1 -0
  130. package/dist/tar-protocol/tree-manifest.d.ts +45 -0
  131. package/dist/tar-protocol/tree-manifest.js +277 -0
  132. package/dist/tar-protocol/tree-manifest.js.map +1 -0
  133. package/dist/tar-protocol/tree-path.d.ts +15 -0
  134. package/dist/tar-protocol/tree-path.js +68 -0
  135. package/dist/tar-protocol/tree-path.js.map +1 -0
  136. package/dist/tar-protocol/tree.d.ts +68 -0
  137. package/dist/tar-protocol/tree.js +269 -0
  138. package/dist/tar-protocol/tree.js.map +1 -0
  139. package/dist/tar-protocol/ustar.d.ts +36 -0
  140. package/dist/tar-protocol/ustar.js +167 -0
  141. package/dist/tar-protocol/ustar.js.map +1 -0
  142. package/dist/types.d.ts +61 -0
  143. package/dist/types.js +3 -0
  144. package/dist/types.js.map +1 -0
  145. package/dist/validation.d.ts +4 -0
  146. package/dist/validation.js +26 -0
  147. package/dist/validation.js.map +1 -0
  148. package/package.json +103 -1
package/dist/server.js ADDED
@@ -0,0 +1,928 @@
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.FINGERPRINT_LENGTH = exports.MAX_ACTIVE_UPLOADS = exports.MAX_CONNECTIONS = exports.DEFAULT_MIN_FREE_BYTES = exports.DEFAULT_MAX_ACTIVE_UPLOADS = exports.DEFAULT_MAX_CONNECTIONS = exports.Server = exports.MAX_OWED_POST_COMMIT_RETENTION = void 0;
7
+ exports.fingerprint = fingerprint;
8
+ const b4a_1 = __importDefault(require("b4a"));
9
+ const _events_1 = __importDefault(require("#events"));
10
+ const _fs_1 = __importDefault(require("#fs"));
11
+ const _path_1 = __importDefault(require("#path"));
12
+ const sodium_native_1 = __importDefault(require("sodium-native"));
13
+ const abort_js_1 = require("./abort.js");
14
+ const direct_dht_js_1 = require("./direct-dht.js");
15
+ const errors_js_1 = require("./errors.js");
16
+ const files_js_1 = require("./files.js");
17
+ const hooks_js_1 = require("./hooks.js");
18
+ const identity_js_1 = require("./identity.js");
19
+ const release_js_1 = require("./release.js");
20
+ const commit_store_js_1 = require("./storage/commit-store.js");
21
+ const link_store_js_1 = require("./storage/link-store.js");
22
+ const layout_js_1 = require("./storage/layout.js");
23
+ const root_coordinator_js_1 = require("./storage/root-coordinator.js");
24
+ const recovery_js_1 = require("./storage/recovery.js");
25
+ const retention_js_1 = require("./storage/retention.js");
26
+ const session_store_js_1 = require("./storage/session-store.js");
27
+ const tree_fs_js_1 = require("./storage/tree-fs.js");
28
+ const controls_js_1 = require("./tar-protocol/controls.js");
29
+ const direct_wire_js_1 = require("./tar-protocol/direct-wire.js");
30
+ const hash_js_1 = require("./tar-protocol/hash.js");
31
+ const manifest_js_1 = require("./tar-protocol/manifest.js");
32
+ const tree_manifest_js_1 = require("./tar-protocol/tree-manifest.js");
33
+ const symlinks_js_1 = require("./symlinks.js");
34
+ const EventEmitter = _events_1.default.EventEmitter;
35
+ const DEFAULT_MAX_CONNECTIONS = 64;
36
+ exports.DEFAULT_MAX_CONNECTIONS = DEFAULT_MAX_CONNECTIONS;
37
+ const DEFAULT_MAX_ACTIVE_UPLOADS = 8;
38
+ exports.DEFAULT_MAX_ACTIVE_UPLOADS = DEFAULT_MAX_ACTIVE_UPLOADS;
39
+ const DEFAULT_MIN_FREE_BYTES = 1024 * 1024 * 1024;
40
+ exports.DEFAULT_MIN_FREE_BYTES = DEFAULT_MIN_FREE_BYTES;
41
+ const MAX_CONNECTIONS = 1024;
42
+ exports.MAX_CONNECTIONS = MAX_CONNECTIONS;
43
+ const MAX_ACTIVE_UPLOADS = 1024;
44
+ exports.MAX_ACTIVE_UPLOADS = MAX_ACTIVE_UPLOADS;
45
+ const FINGERPRINT_LENGTH = 12;
46
+ exports.FINGERPRINT_LENGTH = FINGERPRINT_LENGTH;
47
+ const TAR_DURABILITY_BATCH_BYTES = 1024 * 1024;
48
+ /**
49
+ * How many transfers may owe a deferred post-commit retention pass at once.
50
+ *
51
+ * The set grows only when an `afterCommit` hook fails after a durable commit,
52
+ * and entries are removed as soon as a retry succeeds, so reaching the bound
53
+ * means a persistently failing hook. Capping it at the connection ceiling
54
+ * keeps a broken deployment step from growing server memory without limit.
55
+ */
56
+ exports.MAX_OWED_POST_COMMIT_RETENTION = MAX_CONNECTIONS;
57
+ function fail(code, message, cause = null) {
58
+ return new errors_js_1.SwarmDeployError(code, message, cause);
59
+ }
60
+ function codeOf(error) {
61
+ return error instanceof errors_js_1.SwarmDeployError &&
62
+ Object.values(errors_js_1.ERRORS).includes(error.code)
63
+ ? error.code
64
+ : errors_js_1.ERRORS.PROTOCOL_INVALID;
65
+ }
66
+ function positive(value, name, max = Number.MAX_SAFE_INTEGER) {
67
+ if (!Number.isSafeInteger(value) || value <= 0 || value > max) {
68
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, `Invalid ${name}`);
69
+ }
70
+ return value;
71
+ }
72
+ function key(value, label) {
73
+ if (typeof value === 'string') {
74
+ if (!/^[0-9a-f]{64}$/.test(value))
75
+ throw fail(errors_js_1.ERRORS.INVALID_PUBLIC_KEY, `Invalid ${label}`);
76
+ return b4a_1.default.from(value, 'hex');
77
+ }
78
+ if (!b4a_1.default.isBuffer(value) || value.byteLength !== 32) {
79
+ throw fail(errors_js_1.ERRORS.INVALID_PUBLIC_KEY, `Invalid ${label}`);
80
+ }
81
+ return b4a_1.default.from(value);
82
+ }
83
+ function fingerprint(value) {
84
+ return b4a_1.default.toString((0, hash_js_1.sodiumSha256)(value), 'hex').slice(0, FINGERPRINT_LENGTH);
85
+ }
86
+ function safeLogger(logger) {
87
+ const call = (method, message, details) => {
88
+ try {
89
+ logger?.[method]?.(message, details);
90
+ }
91
+ catch { }
92
+ };
93
+ return {
94
+ info: (m, d) => call('info', m, d),
95
+ warn: (m, d) => call('warn', m, d),
96
+ error: (m, d) => call('error', m, d)
97
+ };
98
+ }
99
+ class Server extends EventEmitter {
100
+ publicKey;
101
+ storageDir;
102
+ maxFileBytes;
103
+ maxStagingBytes;
104
+ maxConnections;
105
+ maxActiveUploads;
106
+ idleTimeout;
107
+ cleanupInterval;
108
+ resumeTtl;
109
+ minFreeBytes;
110
+ maxAge;
111
+ maxStorageBytes;
112
+ artifactPatterns;
113
+ maxCount;
114
+ maxVersions;
115
+ versionGranularity;
116
+ releaseMatcher;
117
+ allowedKeySnapshot;
118
+ logger;
119
+ dht;
120
+ dhtFactory;
121
+ storage;
122
+ scheduler;
123
+ replaceNames;
124
+ hooks;
125
+ symlinks;
126
+ linkNames;
127
+ links = null;
128
+ listening = false;
129
+ closed = false;
130
+ keyPair;
131
+ signal;
132
+ abort = (0, abort_js_1.createAbortController)();
133
+ active = new Map();
134
+ activeUploads = new Set();
135
+ /**
136
+ * Transfer IDs this process committed but whose deferred post-commit
137
+ * retention is still owed because `afterCommit` has not yet succeeded. Only
138
+ * these may start a retention pass from an already-committed retry, so an
139
+ * ordinary duplicate offer cannot be used to force repeated full passes.
140
+ * The set is in-memory only and bounded by `MAX_OWED_POST_COMMIT_RETENTION`;
141
+ * a restart or an eviction loses an entry, and the documented fallback is the
142
+ * next startup, scheduled, or commit-triggered pass.
143
+ */
144
+ pendingAfterCommit = new Set();
145
+ /**
146
+ * Authenticated transfer IDs a connection currently owns. One connection
147
+ * holds at most one entry and releases it in a `finally`, so the set is
148
+ * bounded by `maxConnections`. A second connection offering the same
149
+ * transfer is rejected with `FILE_BUSY` rather than running the lifecycle
150
+ * and its hooks concurrently against one staging and commit identity.
151
+ */
152
+ activeTransfers = new Set();
153
+ receives = new Set();
154
+ layout = null;
155
+ sessions = null;
156
+ commits = null;
157
+ retention = null;
158
+ transport = null;
159
+ releaseLock = null;
160
+ listenPromise = null;
161
+ closePromise = null;
162
+ constructor(options) {
163
+ super();
164
+ if (!options || typeof options !== 'object') {
165
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, 'Invalid server options');
166
+ }
167
+ this.keyPair = (0, identity_js_1.keyPairFromSeed)(options.seed);
168
+ this.publicKey = b4a_1.default.from(this.keyPair.publicKey);
169
+ this.storageDir =
170
+ typeof options.storageDir === 'string' && options.storageDir
171
+ ? options.storageDir
172
+ : (() => {
173
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, 'Invalid storage directory');
174
+ })();
175
+ this.maxFileBytes = positive(options.maxFileBytes, 'maxFileBytes');
176
+ this.maxStagingBytes = positive(options.maxStagingBytes, 'maxStagingBytes');
177
+ this.maxConnections = positive(options.maxConnections ?? DEFAULT_MAX_CONNECTIONS, 'maxConnections', MAX_CONNECTIONS);
178
+ this.maxActiveUploads = positive(options.maxActiveUploads ?? DEFAULT_MAX_ACTIVE_UPLOADS, 'maxActiveUploads', MAX_ACTIVE_UPLOADS);
179
+ if (this.maxActiveUploads > this.maxConnections) {
180
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, 'maxActiveUploads exceeds maxConnections');
181
+ }
182
+ this.idleTimeout = positive(options.idleTimeout ?? 60_000, 'idleTimeout', 0x7fffffff);
183
+ this.cleanupInterval = positive(options.cleanupInterval ?? retention_js_1.DEFAULT_CLEANUP_INTERVAL, 'cleanupInterval', 0x7fffffff);
184
+ this.resumeTtl = positive(options.resumeTtl ?? retention_js_1.DEFAULT_RESUME_TTL, 'resumeTtl');
185
+ this.minFreeBytes = options.minFreeBytes ?? DEFAULT_MIN_FREE_BYTES;
186
+ if (!Number.isSafeInteger(this.minFreeBytes) || this.minFreeBytes < 0) {
187
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, 'Invalid minFreeBytes');
188
+ }
189
+ if (options.maxAge !== undefined) {
190
+ positive(options.maxAge, 'maxAge');
191
+ }
192
+ if (options.maxStorageBytes !== undefined) {
193
+ positive(options.maxStorageBytes, 'maxStorageBytes');
194
+ }
195
+ this.maxAge = options.maxAge;
196
+ this.maxStorageBytes = options.maxStorageBytes;
197
+ const patterns = options.artifactPatterns ?? [];
198
+ if (typeof patterns[Symbol.iterator] !== 'function') {
199
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, 'Invalid artifact patterns');
200
+ }
201
+ try {
202
+ this.artifactPatterns = Object.freeze([...patterns]);
203
+ this.releaseMatcher = new release_js_1.ReleaseMatcher(this.artifactPatterns);
204
+ }
205
+ catch (error) {
206
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, 'Invalid artifact patterns', error);
207
+ }
208
+ if (options.maxCount !== undefined) {
209
+ positive(options.maxCount, 'maxCount');
210
+ if (this.releaseMatcher.size === 0) {
211
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, 'maxCount requires artifact patterns');
212
+ }
213
+ }
214
+ if (options.maxVersions !== undefined) {
215
+ positive(options.maxVersions, 'maxVersions');
216
+ if (!this.releaseMatcher.hasVersionPattern) {
217
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, 'maxVersions requires a {version} artifact pattern');
218
+ }
219
+ if (options.versionGranularity !== 'major' && options.versionGranularity !== 'minor') {
220
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, 'Invalid versionGranularity');
221
+ }
222
+ }
223
+ else if (options.versionGranularity !== undefined) {
224
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, 'versionGranularity requires maxVersions');
225
+ }
226
+ this.maxCount = options.maxCount;
227
+ this.maxVersions = options.maxVersions;
228
+ this.versionGranularity = options.versionGranularity;
229
+ const allow = [];
230
+ if (!options.allowedKeys ||
231
+ typeof options.allowedKeys[Symbol.iterator] !== 'function') {
232
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, 'Invalid allowed keys');
233
+ }
234
+ for (const candidate of options.allowedKeys) {
235
+ const candidateKey = key(candidate, 'allowed key');
236
+ if (allow.some((allowed) => sodium_native_1.default.sodium_memcmp(allowed, candidateKey))) {
237
+ throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, 'Duplicate allowed key');
238
+ }
239
+ allow.push(candidateKey);
240
+ }
241
+ this.allowedKeySnapshot = allow.map((candidate) => b4a_1.default.from(candidate));
242
+ this.dht = options.dht;
243
+ this.dhtFactory = options.dhtFactory;
244
+ this.storage = options.storage || _fs_1.default.promises;
245
+ this.scheduler = options.scheduler || { setTimeout, clearTimeout, setInterval, clearInterval };
246
+ this.replaceNames = (0, files_js_1.validateReplaceNames)(options.replaceNames);
247
+ this.hooks = (0, hooks_js_1.snapshotHooks)(options.hooks);
248
+ this.symlinks = (0, symlinks_js_1.compileSymlinkRules)(options.symlinks);
249
+ this.linkNames = (0, symlinks_js_1.symlinkRuleNames)(this.symlinks);
250
+ if (this.symlinks.length > 0)
251
+ (0, tree_fs_js_1.assertSymlinkCapable)(this.storage);
252
+ this.logger = safeLogger(options.logger);
253
+ this.signal = this.abort.signal;
254
+ }
255
+ artifactKind(metadata) {
256
+ return (0, controls_js_1.isTreeMetadata)(metadata) ? 'directory' : 'file';
257
+ }
258
+ payloadBytes(metadata) {
259
+ return (0, controls_js_1.isTreeMetadata)(metadata) ? metadata.payloadBytes : metadata.fileSize;
260
+ }
261
+ payloadDigest(metadata) {
262
+ return (0, controls_js_1.isTreeMetadata)(metadata) ? metadata.treeSha256 : metadata.fileSha256;
263
+ }
264
+ emitSafe(type, payload) {
265
+ try {
266
+ this.emit(type, payload);
267
+ }
268
+ catch { }
269
+ }
270
+ allowed(owner) {
271
+ if (!owner || !b4a_1.default.isBuffer(owner) || owner.byteLength !== 32)
272
+ return false;
273
+ let accepted = false;
274
+ for (const allowed of this.allowedKeySnapshot) {
275
+ accepted = sodium_native_1.default.sodium_memcmp(owner, allowed) || accepted;
276
+ }
277
+ return accepted;
278
+ }
279
+ transfer(metadata) {
280
+ return {
281
+ transfer: fingerprint(b4a_1.default.from(metadata.transferId, 'hex')),
282
+ name: metadata.name,
283
+ kind: this.artifactKind(metadata),
284
+ size: this.payloadBytes(metadata)
285
+ };
286
+ }
287
+ async rejectOffer(socket, event, owner, reason) {
288
+ await (0, direct_wire_js_1.writeAdmission)(socket, { v: 1, status: 'REJECTED', code: reason }, { signal: this.signal, timeout: this.idleTimeout });
289
+ this.emitSafe('offer', {
290
+ ...event,
291
+ fingerprint: fingerprint(owner),
292
+ status: 'rejected',
293
+ reason
294
+ });
295
+ }
296
+ hookArtifact(metadata, release) {
297
+ return Object.freeze({
298
+ name: metadata.name,
299
+ kind: this.artifactKind(metadata),
300
+ size: this.payloadBytes(metadata),
301
+ sha256: this.payloadDigest(metadata),
302
+ transferId: metadata.transferId,
303
+ ...((0, controls_js_1.isTreeMetadata)(metadata) ? { entryCount: metadata.entryCount } : {}),
304
+ ...(metadata.sourceParent === undefined ? {} : { sourceParent: metadata.sourceParent }),
305
+ ...(release === null
306
+ ? {}
307
+ : {
308
+ release: Object.freeze({
309
+ series: release.series,
310
+ ...(release.version === undefined ? {} : { version: release.version })
311
+ })
312
+ })
313
+ });
314
+ }
315
+ /**
316
+ * Computes and converges the desired links from durable commit records.
317
+ * Called with the root lease already held.
318
+ */
319
+ async reconcileLinksUnlocked(records) {
320
+ if (this.symlinks.length === 0 || !this.links)
321
+ return new Set();
322
+ const desired = (0, symlinks_js_1.selectDesiredLinks)(this.symlinks, records);
323
+ await this.links.reconcile(desired, this.linkNames, {
324
+ managedArtifactNames: new Set(records.map((record) => record.name))
325
+ });
326
+ return new Set(desired.map((link) => link.transferId));
327
+ }
328
+ /** Pins link targets when reconciliation fails but retention must continue. */
329
+ async linkPinsFallbackUnlocked(records) {
330
+ const pins = new Set((0, symlinks_js_1.selectDesiredLinks)(this.symlinks, records).map((link) => link.transferId));
331
+ if (!this.links)
332
+ return pins;
333
+ for (const ledger of await this.links.list()) {
334
+ pins.add(ledger.transferId);
335
+ const target = records.find((record) => record.name === ledger.target);
336
+ if (target)
337
+ pins.add(target.transferId);
338
+ }
339
+ return pins;
340
+ }
341
+ /** Reconciles after a durable commit; this is the only caller that leases the root. */
342
+ async reconcileAfterCommit() {
343
+ if (this.symlinks.length === 0 || !this.links || !this.commits)
344
+ return;
345
+ try {
346
+ await (0, root_coordinator_js_1.withRootLease)(this.layout.root, async () => this.reconcileLinksUnlocked(await this.commits.list()));
347
+ }
348
+ catch (error) {
349
+ if (error instanceof errors_js_1.SwarmDeployError && error.code === errors_js_1.ERRORS.LINK_CONFLICT)
350
+ throw error;
351
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.LINK_FAILED, 'Unable to reconcile managed symlinks', error);
352
+ }
353
+ }
354
+ /** Runs a gating callback; only callback exceptions become HOOK_FAILED. */
355
+ async runHook(phase, hook, context) {
356
+ if (hook === undefined)
357
+ return;
358
+ (0, abort_js_1.throwIfAborted)(this.signal);
359
+ let outcome;
360
+ try {
361
+ outcome = await (0, hooks_js_1.invokeHook)(hook, context, this.signal);
362
+ }
363
+ catch (cause) {
364
+ throw (0, hooks_js_1.hookError)(phase, cause);
365
+ }
366
+ if (outcome === 'aborted')
367
+ throw (0, abort_js_1.abortError)();
368
+ }
369
+ /** Runs onFailure once; its own failure is logged and never replaces the original. */
370
+ async runFailureHook(context, owner, transfer) {
371
+ const hook = this.hooks.onFailure;
372
+ if (hook === undefined)
373
+ return;
374
+ try {
375
+ await (0, hooks_js_1.invokeHook)(hook, context, this.signal);
376
+ }
377
+ catch {
378
+ this.logger.warn('Failure hook failed', {
379
+ fingerprint: owner ? fingerprint(owner) : 'invalid',
380
+ transfer,
381
+ phase: context.phase,
382
+ reason: errors_js_1.ERRORS.HOOK_FAILED
383
+ });
384
+ }
385
+ }
386
+ async receive(socket) {
387
+ const owner = socket.remotePublicKey;
388
+ if (this.closed || !this.allowed(owner) || !this.sessions || !this.commits) {
389
+ try {
390
+ socket.destroy(fail(errors_js_1.ERRORS.AUTH_REJECTED, 'Unauthorised direct connection'));
391
+ }
392
+ catch { }
393
+ return;
394
+ }
395
+ if (this.active.size >= this.maxConnections) {
396
+ try {
397
+ socket.destroy(fail(errors_js_1.ERRORS.FILE_BUSY, 'Connection capacity exceeded'));
398
+ }
399
+ catch { }
400
+ return;
401
+ }
402
+ const current = { owner: b4a_1.default.from(owner), transfer: null };
403
+ this.active.set(socket, current);
404
+ socket.on('error', () => { });
405
+ socket.once('close', () => {
406
+ this.active.delete(socket);
407
+ this.emitSafe('connection-close', {
408
+ fingerprint: fingerprint(owner),
409
+ connections: this.active.size
410
+ });
411
+ });
412
+ this.emitSafe('authentication', { status: 'accepted', fingerprint: fingerprint(owner) });
413
+ this.emitSafe('connection', { fingerprint: fingerprint(owner), connections: this.active.size });
414
+ this.emitSafe('connection-open', {
415
+ fingerprint: fingerprint(owner),
416
+ connections: this.active.size
417
+ });
418
+ const reader = new direct_wire_js_1.DirectWireReader(socket);
419
+ let sentAdmission = false;
420
+ let event = null;
421
+ let verificationStarted = false;
422
+ let verificationSucceeded = false;
423
+ let commitStarted = false;
424
+ let commitSucceeded = false;
425
+ let artifact = null;
426
+ let phase = 'offer';
427
+ let hookPath = null;
428
+ let resumed = false;
429
+ let alreadyCommitted = false;
430
+ let hooksFinished = false;
431
+ let finalStarted = false;
432
+ let failureReported = false;
433
+ let guardedTransfer = null;
434
+ /**
435
+ * Hands the transfer back before this connection's last await. Clearing
436
+ * `guardedTransfer` makes the `finally` a no-op, so a late release can
437
+ * never take ownership away from the connection that acquired it next.
438
+ */
439
+ const releaseTransfer = () => {
440
+ if (guardedTransfer === null)
441
+ return;
442
+ this.activeTransfers.delete(guardedTransfer);
443
+ guardedTransfer = null;
444
+ };
445
+ // Runs onFailure at most once per connection, after the client was answered.
446
+ const reportFailure = async (error) => {
447
+ if (failureReported || !event || !artifact)
448
+ return;
449
+ failureReported = true;
450
+ // The guard serializes the commit lifecycle, and this connection has
451
+ // finished its own. `onFailure` only observes, so holding the transfer
452
+ // across a slow callback would reject the client's legitimate retry.
453
+ releaseTransfer();
454
+ await this.runFailureHook(Object.freeze({ artifact, path: hookPath, phase, resumed, alreadyCommitted, error }), owner, event.transfer);
455
+ };
456
+ const rejectEarly = async (reason, message) => {
457
+ await this.rejectOffer(socket, event, owner, reason);
458
+ sentAdmission = true;
459
+ await reportFailure(fail(reason, message));
460
+ };
461
+ try {
462
+ const metadata = await reader.control(controls_js_1.decodeAnyMetadataRecord, this.signal, this.idleTimeout);
463
+ // The decoded record is shape-validated but not yet authenticated; the
464
+ // artifact context carries only its non-secret descriptive fields.
465
+ event = this.transfer(metadata);
466
+ artifact = this.hookArtifact(metadata, null);
467
+ if ((0, controls_js_1.isTreeMetadata)(metadata))
468
+ (0, tree_manifest_js_1.assertTreeMetadataTransferId)(owner, metadata);
469
+ else
470
+ (0, manifest_js_1.assertMetadataTransferId)(owner, metadata);
471
+ // The transfer ID is authenticated from here on, so it is safe to key
472
+ // the single-owner guard on it.
473
+ if (this.activeTransfers.has(metadata.transferId)) {
474
+ await rejectEarly(errors_js_1.ERRORS.FILE_BUSY, 'Transfer is already in progress');
475
+ return;
476
+ }
477
+ this.activeTransfers.add(metadata.transferId);
478
+ guardedTransfer = metadata.transferId;
479
+ let release = null;
480
+ if (this.releaseMatcher.size > 0) {
481
+ release = this.releaseMatcher.match(metadata.name, metadata.sourceParent);
482
+ if (release === null) {
483
+ await rejectEarly(errors_js_1.ERRORS.INVALID_FILENAME, 'Artifact name does not match a configured pattern');
484
+ return;
485
+ }
486
+ artifact = this.hookArtifact(metadata, release);
487
+ }
488
+ if (this.linkNames.has(metadata.name)) {
489
+ await rejectEarly(errors_js_1.ERRORS.INVALID_FILENAME, 'Artifact name is a configured symlink name');
490
+ return;
491
+ }
492
+ const hookArtifact = artifact;
493
+ const stagingHookPath = (0, controls_js_1.isTreeMetadata)(metadata)
494
+ ? _path_1.default.join(this.layout.staging, `${metadata.transferId}.tree`)
495
+ : _path_1.default.join(this.layout.staging, `${metadata.transferId}.part`);
496
+ const finish = async (verified) => {
497
+ phase = 'beforeCommit';
498
+ hookPath = stagingHookPath;
499
+ await this.runHook('beforeCommit', this.hooks.beforeCommit, Object.freeze({
500
+ artifact: hookArtifact,
501
+ path: hookPath,
502
+ resumed,
503
+ alreadyCommitted: false
504
+ }));
505
+ phase = 'commit';
506
+ commitStarted = true;
507
+ await this.commits.commit(verified, {
508
+ retentionManager: this.retention,
509
+ signal: this.signal,
510
+ replaceNames: this.replaceNames,
511
+ release,
512
+ deferPostCommitRetention: this.hooks.afterCommit !== undefined
513
+ });
514
+ this.emitSafe('commit', {
515
+ ...event,
516
+ fingerprint: fingerprint(owner),
517
+ status: 'succeeded'
518
+ });
519
+ commitSucceeded = true;
520
+ hookPath = _path_1.default.join(this.layout.root, metadata.name);
521
+ await this.retireQuietly(verified.transferId, owner);
522
+ await this.reconcileAfterCommit();
523
+ phase = 'afterCommit';
524
+ if (this.hooks.afterCommit !== undefined) {
525
+ this.owePostCommitRetention(metadata.transferId);
526
+ }
527
+ await this.runHook('afterCommit', this.hooks.afterCommit, Object.freeze({
528
+ artifact: hookArtifact,
529
+ path: hookPath,
530
+ resumed,
531
+ alreadyCommitted: false
532
+ }));
533
+ hooksFinished = true;
534
+ await this.settleDeferredRetention(metadata.transferId, owner);
535
+ finalStarted = true;
536
+ await (0, direct_wire_js_1.writeFinal)(socket, { v: 1, status: 'COMMITTED' }, { signal: this.signal, timeout: this.idleTimeout });
537
+ };
538
+ const payloadBytes = this.payloadBytes(metadata);
539
+ if (payloadBytes > this.maxFileBytes || this.activeUploads.size >= this.maxActiveUploads) {
540
+ if (payloadBytes > this.maxFileBytes) {
541
+ await rejectEarly(errors_js_1.ERRORS.FILE_TOO_LARGE, 'File exceeds the maximum size');
542
+ }
543
+ else {
544
+ await rejectEarly(errors_js_1.ERRORS.ACTIVE_UPLOAD_LIMIT, 'Active upload capacity exceeded');
545
+ }
546
+ return;
547
+ }
548
+ const inspected = await this.commits.inspect(metadata.name, {
549
+ name: metadata.name,
550
+ kind: this.artifactKind(metadata),
551
+ size: payloadBytes,
552
+ digest: b4a_1.default.from(this.payloadDigest(metadata), 'hex'),
553
+ ...((0, controls_js_1.isTreeMetadata)(metadata) ? { entryCount: metadata.entryCount } : {}),
554
+ transferId: b4a_1.default.from(metadata.transferId, 'hex'),
555
+ release
556
+ }, { replaceNames: this.replaceNames });
557
+ if (inspected.status === 'ALREADY_COMMITTED') {
558
+ alreadyCommitted = true;
559
+ hookPath = _path_1.default.join(this.layout.root, metadata.name);
560
+ await this.reconcileAfterCommit();
561
+ phase = 'afterCommit';
562
+ await this.runHook('afterCommit', this.hooks.afterCommit, Object.freeze({
563
+ artifact: hookArtifact,
564
+ path: hookPath,
565
+ resumed: false,
566
+ alreadyCommitted: true
567
+ }));
568
+ hooksFinished = true;
569
+ // Only a transfer this process committed can still owe a deferred
570
+ // pass; an ordinary duplicate offer never starts one.
571
+ if (this.pendingAfterCommit.has(metadata.transferId)) {
572
+ await this.settleDeferredRetention(metadata.transferId, owner);
573
+ }
574
+ await (0, direct_wire_js_1.writeAdmission)(socket, { v: 1, status: 'ALREADY_COMMITTED' }, { signal: this.signal, timeout: this.idleTimeout });
575
+ sentAdmission = true;
576
+ this.emitSafe('offer', {
577
+ ...event,
578
+ fingerprint: fingerprint(owner),
579
+ status: 'already-committed'
580
+ });
581
+ return;
582
+ }
583
+ if (inspected.status === 'FILE_EXISTS') {
584
+ await rejectEarly(errors_js_1.ERRORS.FILE_EXISTS, 'Destination already exists');
585
+ return;
586
+ }
587
+ if (this.activeUploads.size >= this.maxActiveUploads) {
588
+ await rejectEarly(errors_js_1.ERRORS.ACTIVE_UPLOAD_LIMIT, 'Active upload capacity exceeded');
589
+ return;
590
+ }
591
+ this.activeUploads.add(socket);
592
+ const admission = await this.sessions.admit(owner, metadata);
593
+ current.transfer = metadata.transferId;
594
+ resumed = admission.status !== 'ACCEPT';
595
+ if (admission.status === 'VERIFIED') {
596
+ await (0, direct_wire_js_1.writeAdmission)(socket, { v: 1, status: 'VERIFIED' }, { signal: this.signal, timeout: this.idleTimeout });
597
+ sentAdmission = true;
598
+ phase = 'verification';
599
+ hookPath = stagingHookPath;
600
+ this.emitSafe('verification', {
601
+ ...event,
602
+ fingerprint: fingerprint(owner),
603
+ status: 'started'
604
+ });
605
+ verificationStarted = true;
606
+ const verified = await this.sessions.readVerified(b4a_1.default.from(metadata.transferId, 'hex'));
607
+ this.emitSafe('verification', {
608
+ ...event,
609
+ fingerprint: fingerprint(owner),
610
+ status: 'succeeded'
611
+ });
612
+ verificationSucceeded = true;
613
+ await finish(verified);
614
+ return;
615
+ }
616
+ await (0, direct_wire_js_1.writeAdmission)(socket, admission.status === 'ACCEPT'
617
+ ? { v: 1, status: 'ACCEPT', offset: 0 }
618
+ : {
619
+ v: 1,
620
+ status: 'RESUME',
621
+ offset: admission.offset,
622
+ prefixSha256: b4a_1.default.toString(admission.prefixSha256, 'hex')
623
+ }, { signal: this.signal, timeout: this.idleTimeout });
624
+ sentAdmission = true;
625
+ phase = 'transfer';
626
+ hookPath = _path_1.default.join(this.layout.staging, `${metadata.transferId}.tar.part`);
627
+ this.emitSafe('offer', {
628
+ ...event,
629
+ fingerprint: fingerprint(owner),
630
+ status: metadata.reset ? 'reset' : admission.status === 'ACCEPT' ? 'accepted' : 'resumed',
631
+ offset: admission.offset
632
+ });
633
+ let offset = admission.offset;
634
+ const batch = b4a_1.default.allocUnsafe(TAR_DURABILITY_BATCH_BYTES);
635
+ let bufferedBytes = 0;
636
+ const append = async (chunk) => {
637
+ offset = await this.sessions.append(owner, metadata, offset, chunk);
638
+ this.emitSafe('progress', {
639
+ ...event,
640
+ fingerprint: fingerprint(owner),
641
+ bytesReceived: offset,
642
+ totalBytes: metadata.tarSize
643
+ });
644
+ };
645
+ const flush = async () => {
646
+ if (bufferedBytes === 0)
647
+ return;
648
+ const bytes = batch.subarray(0, bufferedBytes);
649
+ await append(bytes);
650
+ bufferedBytes = 0;
651
+ };
652
+ await reader.tar(metadata.tarSize - offset, async (chunk) => {
653
+ let position = 0;
654
+ while (position < chunk.byteLength) {
655
+ const take = Math.min(TAR_DURABILITY_BATCH_BYTES - bufferedBytes, chunk.byteLength - position);
656
+ batch.set(chunk.subarray(position, position + take), bufferedBytes);
657
+ bufferedBytes += take;
658
+ position += take;
659
+ if (bufferedBytes === TAR_DURABILITY_BATCH_BYTES)
660
+ await flush();
661
+ }
662
+ }, this.signal, this.idleTimeout);
663
+ await flush();
664
+ await reader.requireEnd(this.signal, this.idleTimeout);
665
+ phase = 'verification';
666
+ this.emitSafe('verification', {
667
+ ...event,
668
+ fingerprint: fingerprint(owner),
669
+ status: 'started'
670
+ });
671
+ verificationStarted = true;
672
+ const verified = await this.sessions.verify(owner, metadata);
673
+ this.emitSafe('verification', {
674
+ ...event,
675
+ fingerprint: fingerprint(owner),
676
+ status: 'succeeded'
677
+ });
678
+ verificationSucceeded = true;
679
+ await finish(verified);
680
+ }
681
+ catch (error) {
682
+ const reason = codeOf(error);
683
+ this.logger.warn('Direct upload failed', {
684
+ fingerprint: owner ? fingerprint(owner) : 'invalid',
685
+ reason
686
+ });
687
+ this.emitSafe('failure', { fingerprint: owner ? fingerprint(owner) : 'invalid', reason });
688
+ if (event && verificationStarted && !verificationSucceeded) {
689
+ this.emitSafe('verification', {
690
+ ...event,
691
+ fingerprint: fingerprint(owner),
692
+ status: 'failed',
693
+ reason
694
+ });
695
+ }
696
+ if (event && commitStarted && !commitSucceeded) {
697
+ this.emitSafe('commit', {
698
+ ...event,
699
+ fingerprint: fingerprint(owner),
700
+ status: 'failed',
701
+ reason
702
+ });
703
+ }
704
+ try {
705
+ if (!sentAdmission) {
706
+ await (0, direct_wire_js_1.writeAdmission)(socket, { v: 1, status: 'REJECTED', code: reason }, { timeout: this.idleTimeout });
707
+ }
708
+ else if (!finalStarted) {
709
+ await (0, direct_wire_js_1.writeFinal)(socket, { v: 1, status: 'FAILED', code: reason }, { timeout: this.idleTimeout });
710
+ }
711
+ }
712
+ catch { }
713
+ try {
714
+ socket.destroy();
715
+ }
716
+ catch { }
717
+ this.activeUploads.delete(socket);
718
+ if (!hooksFinished)
719
+ await reportFailure((0, hooks_js_1.callbackError)(error));
720
+ }
721
+ finally {
722
+ this.activeUploads.delete(socket);
723
+ releaseTransfer();
724
+ reader.closeReader();
725
+ }
726
+ }
727
+ /**
728
+ * Records that a transfer still owes its deferred post-commit retention
729
+ * pass, keeping at most `MAX_OWED_POST_COMMIT_RETENTION` entries.
730
+ *
731
+ * `Set` preserves insertion order, so the oldest owed transfer is evicted
732
+ * first and eviction is deterministic. An evicted transfer loses nothing
733
+ * durable: its artifact is already committed and its rotation falls back to
734
+ * the next startup, scheduled, or commit-triggered pass.
735
+ */
736
+ owePostCommitRetention(transferId) {
737
+ if (this.pendingAfterCommit.has(transferId))
738
+ return;
739
+ if (this.pendingAfterCommit.size >= exports.MAX_OWED_POST_COMMIT_RETENTION) {
740
+ const oldest = this.pendingAfterCommit.values().next();
741
+ if (!oldest.done)
742
+ this.pendingAfterCommit.delete(oldest.value);
743
+ }
744
+ this.pendingAfterCommit.add(transferId);
745
+ }
746
+ /**
747
+ * Runs the post-commit retention pass that a configured `afterCommit` hook
748
+ * deferred, only after that hook succeeded, and then stops owing it. The
749
+ * retention manager already reports its own failures as non-fatal, so
750
+ * nothing here can fail the upload; the pass is attempted exactly once per
751
+ * successful hook, which is why the transfer is cleared either way.
752
+ */
753
+ async settleDeferredRetention(transferId, owner) {
754
+ if (this.hooks.afterCommit === undefined)
755
+ return;
756
+ try {
757
+ if (this.retention)
758
+ await this.retention.afterCommit();
759
+ }
760
+ catch (error) {
761
+ this.logger.warn('Post-commit retention failed', {
762
+ fingerprint: owner ? fingerprint(owner) : 'invalid',
763
+ reason: codeOf(error)
764
+ });
765
+ }
766
+ finally {
767
+ this.pendingAfterCommit.delete(transferId);
768
+ }
769
+ }
770
+ /**
771
+ * Retires a committed session without letting cleanup undo a durable commit.
772
+ *
773
+ * The artifact is already published at this point, so a failure to unlink the
774
+ * session leaves recoverable residue that `init()` purges on the next start.
775
+ * Propagating it here would abort before the terminal record and tell the
776
+ * client its upload failed after it had in fact succeeded.
777
+ */
778
+ async retireQuietly(transferId, owner) {
779
+ try {
780
+ await this.sessions.retireCommitted(transferId);
781
+ }
782
+ catch (error) {
783
+ this.logger.warn('Committed session cleanup failed', {
784
+ fingerprint: owner ? fingerprint(owner) : 'invalid',
785
+ reason: codeOf(error)
786
+ });
787
+ }
788
+ }
789
+ async start() {
790
+ try {
791
+ this.layout = (0, layout_js_1.initLayout)(this.storageDir);
792
+ this.releaseLock = await (0, layout_js_1.acquireStorageLock)(this.layout, { storage: this.storage });
793
+ this.sessions = new session_store_js_1.SessionStore({
794
+ layout: this.layout,
795
+ maxStagingBytes: this.maxStagingBytes,
796
+ minFreeBytes: this.minFreeBytes,
797
+ resumeTtl: this.resumeTtl,
798
+ storage: this.storage,
799
+ isSessionActive: (session) => [...this.active.values()].some((active) => active.transfer === session.id)
800
+ });
801
+ this.commits = new commit_store_js_1.CommitStore({
802
+ layout: this.layout,
803
+ storage: this.storage,
804
+ logger: this.logger
805
+ });
806
+ if (this.symlinks.length > 0) {
807
+ this.links = new link_store_js_1.LinkStore({
808
+ layout: this.layout,
809
+ storage: this.storage
810
+ });
811
+ }
812
+ this.emitSafe('recovery', { status: 'started' });
813
+ await (0, recovery_js_1.prepareStorageRecovery)({
814
+ layout: this.layout,
815
+ commitStore: this.commits,
816
+ logger: this.logger,
817
+ onEvent: ({ type, ...event }) => this.emitSafe(type, event)
818
+ });
819
+ await this.sessions.init();
820
+ const recovered = await (0, recovery_js_1.recoverStorage)({
821
+ layout: this.layout,
822
+ sessionStore: this.sessions,
823
+ commitStore: this.commits,
824
+ logger: this.logger,
825
+ isAuthorized: (owner) => this.allowed(owner),
826
+ onEvent: ({ type, ...event }) => this.emitSafe(type, event)
827
+ });
828
+ this.emitSafe('recovery', {
829
+ status: 'completed',
830
+ journals: recovered.length,
831
+ purgedSessions: this.sessions.purgedSessions
832
+ });
833
+ this.logger.info('Storage recovery completed', {
834
+ journals: recovered.length,
835
+ purgedSessions: this.sessions.purgedSessions
836
+ });
837
+ this.retention = new retention_js_1.RetentionManager({
838
+ layout: this.layout,
839
+ sessionStore: this.sessions,
840
+ commitStore: this.commits,
841
+ maxAge: this.maxAge,
842
+ maxCount: this.maxCount,
843
+ maxVersions: this.maxVersions,
844
+ versionGranularity: this.versionGranularity,
845
+ maxStorageBytes: this.maxStorageBytes,
846
+ resumeTtl: this.resumeTtl,
847
+ cleanupInterval: this.cleanupInterval,
848
+ storage: this.storage,
849
+ scheduler: this.scheduler,
850
+ isSessionActive: (session) => [...this.active.values()].some((active) => active.transfer === session.id),
851
+ hasActiveUploads: () => this.activeUploads.size > 0,
852
+ isPinned: (record) => this.replaceNames.has(record.name),
853
+ managedLinkNames: () => this.linkNames,
854
+ reconcileLinks: (records) => this.reconcileLinksUnlocked(records),
855
+ linkPinsFallback: (records) => this.linkPinsFallbackUnlocked(records),
856
+ logger: this.logger,
857
+ onEvent: ({ type, ...event }) => this.emitSafe(type, event)
858
+ });
859
+ await this.retention.start();
860
+ (0, abort_js_1.throwIfAborted)(this.signal);
861
+ this.transport = new direct_dht_js_1.DirectDhtServer({
862
+ keyPair: this.keyPair,
863
+ allowedClientPublicKeys: this.allowedKeySnapshot.map((value) => b4a_1.default.from(value)),
864
+ dht: this.dht,
865
+ dhtFactory: this.dhtFactory,
866
+ onConnection: (socket) => {
867
+ const receive = this.receive(socket);
868
+ this.receives.add(receive);
869
+ const settled = () => {
870
+ this.receives.delete(receive);
871
+ };
872
+ receive.then(settled, settled);
873
+ }
874
+ });
875
+ await this.transport.listen();
876
+ this.listening = true;
877
+ this.emitSafe('listening', { fingerprint: fingerprint(this.publicKey) });
878
+ this.logger.info('Direct DHT server listening', { fingerprint: fingerprint(this.publicKey) });
879
+ return this;
880
+ }
881
+ catch (error) {
882
+ await this.dispose();
883
+ throw error;
884
+ }
885
+ }
886
+ listen() {
887
+ if (this.closed)
888
+ return Promise.reject(fail(errors_js_1.ERRORS.ABORTED, 'Server is closed'));
889
+ return (this.listenPromise ||= this.start());
890
+ }
891
+ async dispose() {
892
+ for (const socket of this.active.keys()) {
893
+ try {
894
+ socket.destroy();
895
+ }
896
+ catch { }
897
+ }
898
+ await this.transport?.close().catch(() => { });
899
+ this.transport = null;
900
+ await Promise.allSettled([...this.receives]);
901
+ this.active.clear();
902
+ this.activeTransfers.clear();
903
+ this.pendingAfterCommit.clear();
904
+ await this.retention?.stop().catch(() => { });
905
+ this.retention = null;
906
+ this.links = null;
907
+ await this.sessions?.close().catch(() => { });
908
+ this.sessions = null;
909
+ if (this.releaseLock)
910
+ await this.releaseLock().catch(() => { });
911
+ this.releaseLock = null;
912
+ this.listening = false;
913
+ }
914
+ close() {
915
+ if (this.closePromise)
916
+ return this.closePromise;
917
+ this.closed = true;
918
+ this.abort.abort();
919
+ this.closePromise = (async () => {
920
+ await this.listenPromise?.catch(() => { });
921
+ await this.dispose();
922
+ this.emitSafe('close', { status: 'closed' });
923
+ })();
924
+ return this.closePromise;
925
+ }
926
+ }
927
+ exports.Server = Server;
928
+ //# sourceMappingURL=server.js.map