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.
- package/CHANGELOG.md +32 -0
- package/LICENSE.md +201 -0
- package/NOTICE.md +13 -0
- package/README.md +652 -0
- package/RELEASING.md +71 -0
- package/SECURITY.md +41 -0
- package/dist/abort.d.ts +16 -0
- package/dist/abort.js +52 -0
- package/dist/abort.js.map +1 -0
- package/dist/allowlist.d.ts +2 -0
- package/dist/allowlist.js +21 -0
- package/dist/allowlist.js.map +1 -0
- package/dist/bin/swarm-deploy.d.ts +2 -0
- package/dist/bin/swarm-deploy.js +41 -0
- package/dist/bin/swarm-deploy.js.map +1 -0
- package/dist/cli.d.ts +25 -0
- package/dist/cli.js +694 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +121 -0
- package/dist/client.js +285 -0
- package/dist/client.js.map +1 -0
- package/dist/direct-dht.d.ts +81 -0
- package/dist/direct-dht.js +254 -0
- package/dist/direct-dht.js.map +1 -0
- package/dist/error-code.d.ts +15 -0
- package/dist/error-code.js +30 -0
- package/dist/error-code.js.map +1 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +33 -0
- package/dist/errors.js.map +1 -0
- package/dist/events-node.d.ts +2 -0
- package/dist/events-node.js +7 -0
- package/dist/events-node.js.map +1 -0
- package/dist/files.d.ts +47 -0
- package/dist/files.js +150 -0
- package/dist/files.js.map +1 -0
- package/dist/fs-node.d.ts +2 -0
- package/dist/fs-node.js +7 -0
- package/dist/fs-node.js.map +1 -0
- package/dist/identity.d.ts +7 -0
- package/dist/identity.js +50 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/os-node.d.ts +2 -0
- package/dist/os-node.js +7 -0
- package/dist/os-node.js.map +1 -0
- package/dist/path-node.d.ts +2 -0
- package/dist/path-node.js +7 -0
- package/dist/path-node.js.map +1 -0
- package/dist/process-node.d.ts +2 -0
- package/dist/process-node.js +7 -0
- package/dist/process-node.js.map +1 -0
- package/dist/server.d.ts +153 -0
- package/dist/server.js +576 -0
- package/dist/server.js.map +1 -0
- package/dist/storage/atomic-file.d.ts +15 -0
- package/dist/storage/atomic-file.js +172 -0
- package/dist/storage/atomic-file.js.map +1 -0
- package/dist/storage/commit-journal.d.ts +83 -0
- package/dist/storage/commit-journal.js +279 -0
- package/dist/storage/commit-journal.js.map +1 -0
- package/dist/storage/commit-store.d.ts +246 -0
- package/dist/storage/commit-store.js +1339 -0
- package/dist/storage/commit-store.js.map +1 -0
- package/dist/storage/layout.d.ts +16 -0
- package/dist/storage/layout.js +403 -0
- package/dist/storage/layout.js.map +1 -0
- package/dist/storage/recovery.d.ts +99 -0
- package/dist/storage/recovery.js +253 -0
- package/dist/storage/recovery.js.map +1 -0
- package/dist/storage/retention.d.ts +163 -0
- package/dist/storage/retention.js +465 -0
- package/dist/storage/retention.js.map +1 -0
- package/dist/storage/root-coordinator.d.ts +7 -0
- package/dist/storage/root-coordinator.js +27 -0
- package/dist/storage/root-coordinator.js.map +1 -0
- package/dist/storage/session-store.d.ts +2 -0
- package/dist/storage/session-store.js +7 -0
- package/dist/storage/session-store.js.map +1 -0
- package/dist/storage/tar-session-store.d.ts +116 -0
- package/dist/storage/tar-session-store.js +816 -0
- package/dist/storage/tar-session-store.js.map +1 -0
- package/dist/storage/types.d.ts +62 -0
- package/dist/storage/types.js +3 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/tar-protocol/controls.d.ts +48 -0
- package/dist/tar-protocol/controls.js +185 -0
- package/dist/tar-protocol/controls.js.map +1 -0
- package/dist/tar-protocol/direct-wire.d.ts +43 -0
- package/dist/tar-protocol/direct-wire.js +194 -0
- package/dist/tar-protocol/direct-wire.js.map +1 -0
- package/dist/tar-protocol/extract.d.ts +18 -0
- package/dist/tar-protocol/extract.js +124 -0
- package/dist/tar-protocol/extract.js.map +1 -0
- package/dist/tar-protocol/hash.d.ts +10 -0
- package/dist/tar-protocol/hash.js +44 -0
- package/dist/tar-protocol/hash.js.map +1 -0
- package/dist/tar-protocol/lifecycle.d.ts +13 -0
- package/dist/tar-protocol/lifecycle.js +90 -0
- package/dist/tar-protocol/lifecycle.js.map +1 -0
- package/dist/tar-protocol/manifest.d.ts +46 -0
- package/dist/tar-protocol/manifest.js +287 -0
- package/dist/tar-protocol/manifest.js.map +1 -0
- package/dist/tar-protocol/ustar.d.ts +22 -0
- package/dist/tar-protocol/ustar.js +87 -0
- package/dist/tar-protocol/ustar.js.map +1 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +4 -0
- package/dist/validation.js +26 -0
- package/dist/validation.js.map +1 -0
- 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&label=version&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&logoColor=white&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&logoColor=white&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&logoColor=white&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.
|