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
package/README.md ADDED
@@ -0,0 +1,652 @@
1
+ <h1 align="center">Swarm Deploy</h1>
2
+
3
+ <p align="center">
4
+ Authenticated, encrypted, resumable artifact uploads over direct HyperDHT.
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://github.com/tetherto/swarm-deploy/actions/workflows/ci.yml"><img alt="CI status" src="https://github.com/tetherto/swarm-deploy/actions/workflows/ci.yml/badge.svg?branch=main"></a>
9
+ <a href="https://github.com/tetherto/swarm-deploy/blob/main/package.json"><img alt="Package version" src="https://img.shields.io/github/package-json/v/tetherto/swarm-deploy?filename=package.json&amp;label=version&amp;style=flat-square"></a>
10
+ <a href="https://nodejs.org/"><img alt="Node.js 22 and 24" src="https://img.shields.io/badge/node-22%20%7C%2024-339933?logo=nodedotjs&amp;logoColor=white&amp;style=flat-square"></a>
11
+ <a href="https://github.com/holepunchto/bare"><img alt="Bare supported" src="https://img.shields.io/badge/Bare-supported-171717?logo=javascript&amp;logoColor=white&amp;style=flat-square"></a>
12
+ <a href="https://www.typescriptlang.org/"><img alt="Strict TypeScript" src="https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&amp;logoColor=white&amp;style=flat-square"></a>
13
+ <a href="https://github.com/tetherto/swarm-deploy/blob/main/LICENSE.md"><img alt="Apache-2.0 license" src="https://img.shields.io/github/license/tetherto/swarm-deploy?style=flat-square"></a>
14
+ </p>
15
+
16
+ Swarm Deploy moves build artifacts from authorized clients to one receiving
17
+ server. Each file uses a fresh direct HyperDHT connection authenticated and
18
+ encrypted with Noise. The client pins the server's public key, while the server
19
+ accepts only client public keys in its immutable startup allowlist.
20
+
21
+ The receiver is upload-only. It does not execute, serve, or provide a download
22
+ protocol for stored artifacts.
23
+
24
+ ## Requirements
25
+
26
+ - Node.js 22 or 24, or the current stable Bare runtime.
27
+ - A persistent server seed and at least one independently generated client seed.
28
+ - A dedicated server storage directory.
29
+ - Network access suitable for HyperDHT.
30
+
31
+ ## Install
32
+
33
+ Install the runtime API in an application:
34
+
35
+ ```sh
36
+ npm install swarm-deploy
37
+ ```
38
+
39
+ Install the CLI globally for a receiving server or CI uploader:
40
+
41
+ ```sh
42
+ npm install --global swarm-deploy
43
+ swarm-deploy --help
44
+ ```
45
+
46
+ ## Security and identity model
47
+
48
+ Three values have different roles:
49
+
50
+ - **Seed:** private 32-byte identity material. Prefer a protected seed file or
51
+ environment secret. The CLI also accepts `--seed <64-lower-hex>`, but command
52
+ arguments can be exposed through shell history, process listings, and CI
53
+ tracing. Never put a seed in an allowlist or application log.
54
+ - **Client public key:** derived from a client seed and installed in the
55
+ server's allowlist.
56
+ - **Server public key:** derived from the server seed and pinned by every
57
+ client. It is both the direct HyperDHT destination and the server identity
58
+ commitment.
59
+
60
+ HyperDHT Noise authenticates both peers and encrypts the transport. Application
61
+ SHA-256 checks verify the deterministic TAR and extracted file bytes.
62
+
63
+ There is no topic, swarm discovery, Protomux channel, dynamic allowlist reload,
64
+ or reconnect budget. Changing an allowed client key requires a controlled
65
+ server restart.
66
+
67
+ Keep seeds stable to preserve identity. Copying one client seed to several
68
+ machines intentionally gives all of them the same uploader identity; use
69
+ separate client seeds when independent authorization is required.
70
+
71
+ ## Provision identities
72
+
73
+ Generate separate server and client seed files:
74
+
75
+ ```sh
76
+ swarm-deploy keygen --out server.seed
77
+ swarm-deploy keygen --out client.seed
78
+ ```
79
+
80
+ `keygen` creates an owner-only file, refuses to overwrite an existing path, and
81
+ prints the corresponding public key—not the seed.
82
+
83
+ Recover public keys later:
84
+
85
+ ```sh
86
+ SERVER_KEY=$(swarm-deploy public-key --seed-file server.seed)
87
+ CLIENT_KEY=$(swarm-deploy public-key --seed-file client.seed)
88
+ ```
89
+
90
+ Public keys are lowercase 64-character hexadecimal strings and are safe to use
91
+ as configuration values.
92
+
93
+ ## Quick start
94
+
95
+ Start the receiver:
96
+
97
+ ```sh
98
+ swarm-deploy server --seed-file server.seed --storage /srv/artifacts \
99
+ --allow-key "$CLIENT_KEY" --max-file-bytes 1073741824 --max-staging-bytes 2147483648
100
+ ```
101
+
102
+ The server prints its full public key and then `ready` after storage recovery,
103
+ scrub, retention initialization, and HyperDHT listening complete:
104
+
105
+ ```text
106
+ 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
107
+ ready
108
+ ```
109
+
110
+ Upload a file:
111
+
112
+ ```sh
113
+ swarm-deploy upload --seed-file client.seed --server-key "$SERVER_KEY" \
114
+ --idle-timeout 60000 ./artifact.bin
115
+ ```
116
+
117
+ Successful output is:
118
+
119
+ ```text
120
+ artifact.bin COMMITTED
121
+ ```
122
+
123
+ Uploading the same managed content again returns `ALREADY_COMMITTED` without
124
+ retransmitting its TAR bytes.
125
+
126
+ ## CLI reference
127
+
128
+ ### `keygen`
129
+
130
+ ```sh
131
+ swarm-deploy keygen --out <seed-file>
132
+ ```
133
+
134
+ Creates a new seed file with owner-only permissions and prints its public key.
135
+ The destination must not already exist.
136
+
137
+ ### `public-key`
138
+
139
+ ```sh
140
+ swarm-deploy public-key --seed-file <seed-file>
141
+ swarm-deploy public-key --seed <64-lower-hex>
142
+ ```
143
+
144
+ Reads a seed file or canonical seed string and prints its public key.
145
+
146
+ ### `server`
147
+
148
+ ```sh
149
+ swarm-deploy server \
150
+ --seed-file <seed-file> \
151
+ --storage <directory> \
152
+ --allow-key <64-lower-hex> \
153
+ --max-file-bytes <bytes> \
154
+ --max-staging-bytes <bytes> \
155
+ [--allow-key <64-lower-hex>]... \
156
+ [--max-storage-bytes <bytes>] \
157
+ [--max-age-days <days>] \
158
+ [--replace-name <safe-basename>]...
159
+ ```
160
+
161
+ Replace `--seed-file <seed-file>` with `--seed <64-lower-hex>` to provide the
162
+ seed inline.
163
+
164
+ Required options:
165
+
166
+ - Exactly one seed source: `--seed-file`, `--seed`, or
167
+ `SWARM_DEPLOY_SERVER_SEED`.
168
+ - `--storage`: dedicated artifact and internal-state root.
169
+ - `--allow-key`: authorized client public key. Repeat for multiple identities;
170
+ duplicates and malformed keys are rejected.
171
+ - `--max-file-bytes`: maximum extracted artifact size.
172
+ - `--max-staging-bytes`: aggregate persistent staging reservation. An admitted
173
+ transfer reserves its deterministic TAR size plus extracted file size, so
174
+ this commonly needs to be at least twice the largest simultaneously staged
175
+ payload.
176
+
177
+ Optional options:
178
+
179
+ - `--max-storage-bytes`: maximum total managed committed storage. Oldest
180
+ eligible artifacts are removed first.
181
+ - `--max-age-days`: remove eligible committed artifacts at or beyond this age.
182
+ - `--replace-name`: permit replacement of this exact basename. Repeat for
183
+ multiple mutable names.
184
+
185
+ The CLI requires at least one `--allow-key`. Its snapshot is immutable for the
186
+ life of the process.
187
+
188
+ ### `upload`
189
+
190
+ ```sh
191
+ swarm-deploy upload \
192
+ --seed-file <seed-file> \
193
+ --server-key <64-lower-hex> \
194
+ [--idle-timeout <milliseconds>] \
195
+ <file-or-directory>
196
+ ```
197
+
198
+ Replace `--seed-file <seed-file>` with `--seed <64-lower-hex>` to provide the
199
+ seed inline.
200
+
201
+ - `--server-key` is the full pinned server public key.
202
+ - `--idle-timeout` defaults to 60 seconds and bounds inactive protocol reads and
203
+ backpressured writes.
204
+ - The input may be any regular binary file; Swarm Deploy creates the canonical
205
+ one-entry USTAR stream automatically. Pre-tarring is not required.
206
+ - A direct file retains its basename.
207
+ - A directory processes immediate regular-file children once, in lexical
208
+ order, with one independent connection and result per file.
209
+ - Subdirectories, symlinks, non-regular files, unsafe names, and names in the
210
+ reserved `history-` namespace are skipped and reported.
211
+ - An unreadable directory entry is reported as a failure while later entries
212
+ continue.
213
+
214
+ Accepted names start with an ASCII letter or digit, contain only letters,
215
+ digits, `.`, `_`, and `-`, and occupy at most 100 UTF-8 bytes. `history-` is
216
+ reserved for server-managed replacement history.
217
+
218
+ If a basename is exactly 64 lowercase hexadecimal characters, pass it with a
219
+ directory component such as `./<name>` so the CLI does not treat it as an
220
+ accidentally pasted seed.
221
+
222
+ ### Seed sources
223
+
224
+ Server and upload commands accept exactly one of:
225
+
226
+ - `--seed-file <seed-file>`
227
+ - `--seed <64-lower-hex>`
228
+ - the role-specific `SWARM_DEPLOY_SERVER_SEED` or
229
+ `SWARM_DEPLOY_CLIENT_SEED` environment variable
230
+
231
+ String values must contain exactly 64 lowercase hexadecimal characters.
232
+ Combining seed sources is an error.
233
+
234
+ The `public-key` command accepts `--seed-file` or `--seed`; it does not consume a
235
+ role-specific environment variable.
236
+
237
+ Prefer seed files or protected environment variables in production. Use
238
+ `--seed` only when exposure through command history, process inspection, and
239
+ tooling logs is acceptable.
240
+
241
+ ### CLI exit codes
242
+
243
+ - `0`: every selected file was committed or already committed.
244
+ - `1`: upload, network, protocol, storage, cleanup, or runtime failure.
245
+ - `2`: usage or configuration error.
246
+
247
+ For a directory, the CLI prints one line for every selected or skipped entry and
248
+ returns `1` if any selected upload failed.
249
+
250
+ ## CI uploader example
251
+
252
+ Store the client seed as a protected CI secret and the server public key as a
253
+ nonsecret variable:
254
+
255
+ ```yaml
256
+ - name: Install uploader
257
+ run: npm install --global swarm-deploy
258
+
259
+ - name: Upload artifact
260
+ env:
261
+ SWARM_DEPLOY_CLIENT_SEED: ${{ secrets.SWARM_DEPLOY_CLIENT_SEED }}
262
+ SWARM_DEPLOY_SERVER_KEY: ${{ vars.SWARM_DEPLOY_SERVER_KEY }}
263
+ run: |
264
+ swarm-deploy upload \
265
+ --server-key "$SWARM_DEPLOY_SERVER_KEY" \
266
+ --idle-timeout 60000 \
267
+ ./dist/artifact-linux-x64.tar.gz
268
+ ```
269
+
270
+ Do not enable shell tracing around commands that read seed environment
271
+ variables or pass `--seed`.
272
+
273
+ ## Transfer and resume behavior
274
+
275
+ Each file follows this lifecycle:
276
+
277
+ 1. The client connects directly to the pinned server public key with its seeded
278
+ HyperDHT identity.
279
+ 2. The server firewall and connection handler verify the authenticated client
280
+ key against the startup allowlist.
281
+ 3. The client sends bounded metadata containing the name, file size and digest,
282
+ deterministic TAR size and digest, and transfer ID.
283
+ 4. The server responds with `ACCEPT`, `RESUME`, `VERIFIED`,
284
+ `ALREADY_COMMITTED`, or a stable rejection.
285
+ 5. The client sends exactly the required deterministic one-entry USTAR bytes.
286
+ 6. The server validates the canonical archive, extracted size, and both
287
+ SHA-256 values before durable commit.
288
+ 7. Success requires an explicit terminal `COMMITTED` result. EOF or socket
289
+ closure is never success.
290
+
291
+ Incomplete uploads retain only durable TAR progress. On reconnect, the server
292
+ returns a TAR-byte offset and SHA-256 of that prefix. The client regenerates and
293
+ compares the prefix before sending the suffix. A mismatch closes that connection
294
+ and retries once from zero with explicit reset intent.
295
+
296
+ The server coalesces network fragments into 1 MiB durability batches plus the
297
+ final remainder. A disconnect can require retransmitting only the uncheckpointed
298
+ in-memory tail; it never advertises bytes that were not durably published.
299
+
300
+ Inactive sessions expire after seven days by default. Active receives and
301
+ verification are protected from expiry.
302
+
303
+ ## Storage, replacement, and retention
304
+
305
+ The storage root contains visible current and historical artifacts plus the
306
+ reserved `.swarm-deploy/` internal directory. Do not modify that directory
307
+ while the server is running.
308
+
309
+ Names are create-only by default:
310
+
311
+ - New content for an unused name is committed atomically.
312
+ - Identical managed content returns `ALREADY_COMMITTED`.
313
+ - Different content for an occupied create-only name returns `FILE_EXISTS`.
314
+ - Unmanaged files and paths are never overwritten or deleted.
315
+
316
+ Names configured with `--replace-name` or `ServerOptions.replaceNames` are
317
+ mutable:
318
+
319
+ - Different verified content atomically becomes current.
320
+ - The prior managed inode and record are preserved as
321
+ `history-<full-old-transfer-id>`.
322
+ - The current mutable name is pinned against age and quota retention.
323
+ - Historical versions remain eligible for retention.
324
+
325
+ Commit journals and inode checks recover interrupted create and replacement
326
+ operations. Recovery rolls back mutations before the durable new current
327
+ sidecar and rolls forward operations after that linearization point.
328
+
329
+ Optional retention applies to managed artifacts only:
330
+
331
+ - `maxAge`/`--max-age-days` removes eligible artifacts by age.
332
+ - `maxStorageBytes`/`--max-storage-bytes` removes the oldest eligible artifacts
333
+ until under quota.
334
+ - Startup recovery re-hashes managed files. Scheduled cleanup validates managed
335
+ metadata and file sizes, removes invalid managed records safely, reports
336
+ unknown paths without deleting them, and applies age and quota retention.
337
+
338
+ Runtime defaults:
339
+
340
+ - 64 authenticated connections.
341
+ - 8 active uploads.
342
+ - 60-second upload inactivity timeout.
343
+ - 1 GiB minimum free-disk reserve.
344
+ - 15-minute cleanup interval.
345
+ - 7-day resumable-session lifetime.
346
+
347
+ The advanced runtime API can override these values; the server CLI intentionally
348
+ exposes only its required limits, committed retention, and replacement policy.
349
+
350
+ ## Runtime API
351
+
352
+ The package is strict TypeScript and exposes the same root API to ESM and
353
+ CommonJS consumers.
354
+
355
+ ### Identity helpers
356
+
357
+ ```ts
358
+ import {
359
+ generateSeed,
360
+ keyPairFromSeed,
361
+ parsePublicKey,
362
+ parseSeed,
363
+ publicKeyFromSeed
364
+ } from 'swarm-deploy'
365
+
366
+ const seed = generateSeed() // 32 random bytes
367
+ const restored = parseSeed(process.env.SEED!) // strict lowercase hex
368
+ const keyPair = keyPairFromSeed(restored)
369
+ const publicKey = publicKeyFromSeed(restored)
370
+ const peer = parsePublicKey(process.env.PEER_PUBLIC_KEY!)
371
+ ```
372
+
373
+ `parseSeed` and `parsePublicKey` require exactly 64 lowercase hexadecimal
374
+ characters. `keyPairFromSeed` is deterministic.
375
+
376
+ `parseAllowlist(text)` parses lowercase client public keys separated by
377
+ newlines. Blank lines and lines beginning with `#` are ignored; malformed or
378
+ duplicate keys throw. It returns a `Set<string>` suitable for
379
+ `ServerOptions.allowedKeys`.
380
+
381
+ ### Server
382
+
383
+ ```ts
384
+ import { Server, parsePublicKey } from 'swarm-deploy'
385
+
386
+ const server = new Server({
387
+ seed: process.env.SWARM_DEPLOY_SERVER_SEED!,
388
+ storageDir: '/srv/artifacts',
389
+ allowedKeys: [
390
+ parsePublicKey(process.env.CLIENT_A_PUBLIC_KEY!),
391
+ parsePublicKey(process.env.CLIENT_B_PUBLIC_KEY!)
392
+ ],
393
+ maxFileBytes: 1024 ** 3,
394
+ maxStagingBytes: 4 * 1024 ** 3,
395
+ maxConnections: 64,
396
+ maxActiveUploads: 8,
397
+ idleTimeout: 60_000,
398
+ cleanupInterval: 15 * 60_000,
399
+ resumeTtl: 7 * 24 * 60 * 60_000,
400
+ minFreeBytes: 1024 ** 3,
401
+ maxAge: 15 * 24 * 60 * 60_000,
402
+ maxStorageBytes: 100 * 1024 ** 3,
403
+ replaceNames: ['release.tar.gz', 'latest.json']
404
+ })
405
+
406
+ await server.listen()
407
+ console.log(server.publicKey.toString('hex'))
408
+
409
+ // Later, after draining or on process shutdown:
410
+ await server.close()
411
+ ```
412
+
413
+ Required `ServerOptions`:
414
+
415
+ - `seed: Buffer | string`
416
+ - `storageDir: string`
417
+ - `allowedKeys: Iterable<Buffer | string>`
418
+ - `maxFileBytes: number`
419
+ - `maxStagingBytes: number`
420
+
421
+ Optional operational limits and policies:
422
+
423
+ - `maxConnections`, `maxActiveUploads`, `idleTimeout`
424
+ - `cleanupInterval`, `resumeTtl`, `minFreeBytes`
425
+ - `maxAge`, `maxStorageBytes`, `replaceNames`
426
+
427
+ Advanced integration and test seams:
428
+
429
+ - `dht` or `dhtFactory` for an injected HyperDHT node.
430
+ - `storage` for a compatible filesystem adapter.
431
+ - `scheduler` for timeout and interval control.
432
+ - `logger` with optional `info`, `warn`, and `error` methods.
433
+
434
+ ### Client
435
+
436
+ ```ts
437
+ import { Client, parsePublicKey } from 'swarm-deploy'
438
+
439
+ const client = new Client({
440
+ seed: process.env.SWARM_DEPLOY_CLIENT_SEED!,
441
+ serverPublicKey: parsePublicKey(process.env.SWARM_DEPLOY_SERVER_KEY!),
442
+ connectTimeout: 30_000,
443
+ idleTimeout: 60_000
444
+ })
445
+
446
+ const result = await client.upload('./dist/release.tar.gz')
447
+ console.log(result.status, result.name, result.size)
448
+
449
+ await client.close()
450
+ ```
451
+
452
+ `ClientOptions.seed` accepts a Buffer or canonical lowercase 64-character hex
453
+ string. `serverPublicKey` is a Buffer produced by `parsePublicKey`. Optional
454
+ values are `connectTimeout`, `idleTimeout`, `dht`, `dhtFactory`, and `logger`.
455
+
456
+ `connectTimeout` defaults to 30 seconds. `idleTimeout` defaults to 60 seconds.
457
+ Calling `close()` aborts pending work, closes active sockets, and is idempotent.
458
+
459
+ ### Upload results
460
+
461
+ A direct file resolves to:
462
+
463
+ ```ts
464
+ interface UploadResult {
465
+ status: 'COMMITTED' | 'ALREADY_COMMITTED'
466
+ name: string
467
+ size: number
468
+ digest: Buffer
469
+ transferId: Buffer
470
+ }
471
+ ```
472
+
473
+ A directory resolves to:
474
+
475
+ ```ts
476
+ interface BatchUploadResult {
477
+ status: 'COMMITTED' | 'FAILED'
478
+ results: Array<
479
+ | UploadResult
480
+ | {
481
+ name: string
482
+ status: ErrorCode
483
+ reason?: string
484
+ }
485
+ >
486
+ skipped: Array<{
487
+ name: string
488
+ path: string
489
+ reason: SkippedUploadReason
490
+ }>
491
+ }
492
+ ```
493
+
494
+ Directory members are processed sequentially. A failed member does not prevent
495
+ later members from being attempted.
496
+
497
+ ### Events
498
+
499
+ `Server` and `Client` are event emitters. Listener and logger exceptions are
500
+ contained and cannot change protocol correctness.
501
+
502
+ Public peer and transfer correlation fields use 12-character SHA-256
503
+ fingerprints. Events never expose seeds, secret keys, full remote public keys,
504
+ TAR contents, or resumable session material. Internal storage warnings may
505
+ include a full SHA-256 uploader fingerprint, but never the uploader key itself.
506
+
507
+ Server events:
508
+
509
+ - `authentication`: accepted client fingerprint.
510
+ - `connection`, `connection-open`, `connection-close`: authenticated connection
511
+ lifecycle and current count.
512
+ - `offer`: accepted, resumed, reset, rejected, or already-committed state.
513
+ - `progress`: durable TAR bytes received and total TAR bytes.
514
+ - `verification`: started, succeeded, or failed.
515
+ - `commit`: succeeded or failed.
516
+ - `recovery`: startup, per-journal, corruption, resumable, and completion
517
+ outcomes.
518
+ - `retention`: startup, scheduled, manual, commit, or post-commit outcomes.
519
+ - `failure`: stable failure code and peer fingerprint.
520
+ - `listening`: local server-key fingerprint.
521
+ - `close`: closed or failed outcome.
522
+
523
+ Client events:
524
+
525
+ - `connection`, `connection-open`, `connection-close`
526
+ - `offer`: offered, accepted, resumed, reset, rejected, or already committed.
527
+ - `progress`: cumulative TAR `bytesSent` and full `totalBytes`, including a
528
+ durable resume offset.
529
+ - `verification`, `commit`
530
+ - `result`: direct result or per-file/aggregate directory result.
531
+ - `skipped`: skipped directory member and stable reason.
532
+ - `failure`, `close`
533
+
534
+ Use the exported `ServerEventMap`, `ClientEventMap`, `ServerEventName`, and
535
+ `ClientEventName` types for event-name-specific payload narrowing:
536
+
537
+ ```ts
538
+ client.on('progress', ({ name, bytesSent, totalBytes }) => {
539
+ console.log(name, `${bytesSent}/${totalBytes}`)
540
+ })
541
+
542
+ server.on('recovery', (event) => {
543
+ if (event.status === 'failed') console.error(event.phase, event.reason)
544
+ })
545
+ ```
546
+
547
+ ### Logging
548
+
549
+ Both constructors accept:
550
+
551
+ ```ts
552
+ interface Logger {
553
+ info?(message: string, details?: Record<string, unknown>): void
554
+ warn?(message: string, details?: Record<string, unknown>): void
555
+ error?(message: string, details?: Record<string, unknown>): void
556
+ }
557
+ ```
558
+
559
+ Logger exceptions are ignored. Logger detail objects are diagnostic rather than
560
+ a stable ingestion schema; use typed event payloads or `SwarmDeployError.code`
561
+ for automation. Do not use fingerprints as credentials.
562
+
563
+ ### Errors
564
+
565
+ Configuration, authentication, protocol, transfer, and managed-storage failures
566
+ generally reject with `SwarmDeployError`. Raw operating-system or adapter errors
567
+ may propagate while selecting or opening a local input, initializing or locking
568
+ the server storage root, or performing filesystem operations:
569
+
570
+ ```ts
571
+ import { ERRORS, SwarmDeployError } from 'swarm-deploy'
572
+
573
+ try {
574
+ await client.upload('./artifact.tgz')
575
+ } catch (error) {
576
+ if (error instanceof SwarmDeployError) {
577
+ console.error(error.code, error.message)
578
+ if (error.code === ERRORS.CONNECT_TIMEOUT) {
579
+ // Server could not be reached and authenticated before the deadline.
580
+ }
581
+ } else {
582
+ console.error(error)
583
+ }
584
+ }
585
+ ```
586
+
587
+ Stable codes include authentication and server-key rejection, invalid
588
+ configuration and protocol records, file and staging limits, disk reserve,
589
+ filename and replacement conflicts, checksum failures, connection or upload
590
+ timeouts, aborts, commit failures, and cleanup failures. Import `ERRORS` rather
591
+ than matching exception messages.
592
+
593
+ ## Production operations
594
+
595
+ - Run the server under a dedicated non-root account.
596
+ - Restrict the server seed and storage root to that account.
597
+ - Supervise the process and wait for the final `ready` line before marking it
598
+ healthy.
599
+ - Restart with the same seed, allowlist, limits, replacement names, and storage
600
+ root so interrupted sessions and commit journals can recover.
601
+ - Alert on nonzero CLI exits and failed authentication, recovery, verification,
602
+ commit, retention, and cleanup events.
603
+ - Never edit `.swarm-deploy/` while the server is running.
604
+ - Stop the server cleanly before backing up or restoring the complete storage
605
+ root.
606
+ - Publish stored artifacts through a separately configured artifact service or
607
+ web server.
608
+ - Roll out an exact package version to a canary before wider deployment.
609
+
610
+ See [SECURITY.md](SECURITY.md) for vulnerability reporting and operator
611
+ precautions.
612
+
613
+ ## Contributor development
614
+
615
+ Production and tests are strict TypeScript. Build output is generated under
616
+ untracked `dist/` and `.test-dist/`. Tests generate binary payloads in temporary
617
+ directories and use an isolated local HyperDHT testnet, not the public DHT.
618
+
619
+ ```sh
620
+ npm ci
621
+ npm run build
622
+ npm run build:test
623
+ npm run test:types
624
+ npm run format:check
625
+ npm run lint
626
+ npm run test:node
627
+ npm run test:bare
628
+ npm run test:property
629
+ npm run test:package
630
+ npm run test:release-tag
631
+ ```
632
+
633
+ Exercise the built CLI directly:
634
+
635
+ ```sh
636
+ node dist/bin/swarm-deploy.js --help
637
+ bare dist/bin/swarm-deploy.js --help
638
+ ```
639
+
640
+ ## Release
641
+
642
+ Version tags use `v<package-version>`. The tag workflow validates the version,
643
+ builds the untracked distribution, checks package contents and types, and
644
+ publishes through the configured npm environment with provenance.
645
+
646
+ See [RELEASING.md](RELEASING.md) for the release and rollback procedure.
647
+
648
+ ## Protocol specification
649
+
650
+ See [docs/spec/swarm-deploy.md](docs/spec/swarm-deploy.md) for the complete
651
+ transport, deterministic TAR, resumability, storage, replacement, recovery,
652
+ retention, threat-model, and package requirements.