OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-deploy commits / f5b9240

Refactor to CommonJS + Holepunch module standards
- ESM → CommonJS (require/module.exports) - Remove "type": "module" from package.json - Add "imports" map for bare/node compatibility - CLI and web UI removed from module (consumers build those) - New test/basic.test.js using module API directly (7/8 pass locally, integration test needs cross-machine setup) - Source: 696 lines (under 1,000 limit) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
f5b9240cc73c0507b5a7dc058ff4f89abc8acb47
by plankatron <fleeky@gmail.com> · 2026-04-09T21:21:41.000Z

Changed files (6) +533 -855

index.js +223 -242
……@@ -6,6 +6,8 @@
66 * Backpressure via Protomux ondrain. Hash verification on received files.
77 *
88 * Usage:
9 * const { DeployServer, DeployClient } = require('hyper-deploy')
10 *
911 * const server = new DeployServer(dir)
1012 * await server.ready()
1113 *
……@@ -14,228 +16,221 @@
1416 * const result = await client.pull()
1517 */
1618
17import Hyperswarm from 'hyperswarm'
18import crypto from 'hypercore-crypto'
19import fs from 'bare-fs'
20import path from 'bare-path'
21import ReadyResource from 'ready-resource'
22import safetyCatch from 'safety-catch'
19const Hyperswarm = require('hyperswarm');
20const crypto = require('hypercore-crypto');
21const fs = require('fs');
22const path = require('path');
23const ReadyResource = require('ready-resource');
24const safetyCatch = require('safety-catch');
2325
24import { buildManifest, diffManifest, readChunk, hashFile } from './lib/scanner.js'
25import { attachProtocol, PROTOCOL_VERSION, CHUNK_SIZE } from './lib/protocol.js'
26const { buildManifest, diffManifest, readChunk, hashFile } = require('./lib/scanner');
27const { attachProtocol, PROTOCOL_VERSION, CHUNK_SIZE } = require('./lib/protocol');
2628
2729// ─── DeployServer ────────────────────────────────────────────
2830
29export class DeployServer extends ReadyResource {
31class DeployServer extends ReadyResource {
3032 constructor (dir, opts) {
31 super()
32 opts = opts || {}
33 super();
34 opts = opts || {};
3335
34 this.dir = path.resolve(dir)
35 this.topic = opts.topic || crypto.randomBytes(32)
36 this.swarm = opts.swarm || null
37 this._ownSwarm = !this.swarm
38 this.manifest = null
39 this._manifestHash = null
40 this._discovery = null
41 this._sessions = new Set()
36 this.dir = path.resolve(dir);
37 this.topic = opts.topic || crypto.randomBytes(32);
38 this.swarm = opts.swarm || null;
39 this._ownSwarm = !this.swarm;
40 this.manifest = null;
41 this._manifestHash = null;
42 this._discovery = null;
43 this._sessions = new Set();
4244
43 this.stats = { totalConnections: 0, filesServed: 0, bytesServed: 0 }
45 this.stats = { totalConnections: 0, filesServed: 0, bytesServed: 0 };
4446
45 this.ready().catch(safetyCatch)
47 this.ready().catch(safetyCatch);
4648 }
4749
4850 async _open () {
4951 if (!fs.existsSync(this.dir)) {
50 throw new Error('Directory not found: ' + this.dir)
52 throw new Error('Directory not found: ' + this.dir);
5153 }
5254
53 await this._scan()
55 await this._scan();
5456
55 if (!this.swarm) this.swarm = new Hyperswarm()
56 this.swarm.on('connection', (conn) => this._onConnection(conn))
57 if (!this.swarm) this.swarm = new Hyperswarm();
58 this.swarm.on('connection', (conn) => this._onConnection(conn));
5759
58 this._discovery = this.swarm.join(this.topic, { server: true, client: false })
59 await this._discovery.flushed()
60 this._discovery = this.swarm.join(this.topic, { server: true, client: false });
61 await this._discovery.flushed();
6062
61 this.emit('ready')
63 this.emit('ready');
6264 }
6365
6466 async _close () {
65 // Cancel active sessions
6667 for (const session of this._sessions) {
67 session.destroyed = true
68 session.destroyed = true;
6869 }
69 this._sessions.clear()
70 this._sessions.clear();
7071
7172 if (this._discovery) {
72 await this._discovery.destroy()
73 this._discovery = null
73 await this._discovery.destroy();
74 this._discovery = null;
7475 }
7576 if (this._ownSwarm && this.swarm) {
76 await this.swarm.destroy()
77 this.swarm = null
77 await this.swarm.destroy();
78 this.swarm = null;
7879 }
7980 }
8081
8182 get topicHex () {
82 return this.topic.toString('hex')
83 return this.topic.toString('hex');
8384 }
8485
8586 get connections () {
86 return this._sessions.size
87 return this._sessions.size;
8788 }
8889
8990 async rescan () {
90 await this._scan()
91 return { files: this.manifest.files.length, totalSize: this.manifest.totalSize }
91 await this._scan();
92 return { files: this.manifest.files.length, totalSize: this.manifest.totalSize };
9293 }
9394
9495 async _scan () {
9596 this.manifest = buildManifest(this.dir, {
9697 onProgress: (i, total) => {
97 if (i % 1000 === 0) this.emit('index-progress', i, total)
98 if (i % 1000 === 0) this.emit('index-progress', i, total);
9899 }
99 })
100 });
100101
101 // Compute manifest hash for integrity verification
102 const manifestJson = JSON.stringify(this.manifest.files.map(f => f.path + ':' + f.hash))
103 this._manifestHash = crypto.data(Buffer.from(manifestJson)).toString('hex')
102 const manifestJson = JSON.stringify(this.manifest.files.map(f => f.path + ':' + f.hash));
103 this._manifestHash = crypto.data(Buffer.from(manifestJson)).toString('hex');
104104
105 this.emit('indexed', this.manifest.files.length, this.manifest.totalSize)
105 this.emit('indexed', this.manifest.files.length, this.manifest.totalSize);
106106 }
107107
108108 _onConnection (conn) {
109109 const peerKey = conn.remotePublicKey
110110 ? conn.remotePublicKey.toString('hex').slice(0, 12)
111 : 'unknown'
111 : 'unknown';
112112
113 this.stats.totalConnections++
113 this.stats.totalConnections++;
114114
115 const proto = attachProtocol(conn)
115 const proto = attachProtocol(conn);
116116 if (!proto) {
117 this.emit('error', new Error('Failed to attach protocol for peer ' + peerKey))
118 return
117 this.emit('error', new Error('Failed to attach protocol for peer ' + peerKey));
118 return;
119119 }
120120
121 const session = { peerKey, destroyed: false, filesServed: 0, bytesServed: 0 }
122 this._sessions.add(session)
123 this.emit('peer', { key: peerKey, state: 'connected' })
121 const session = { peerKey, destroyed: false, filesServed: 0, bytesServed: 0 };
122 this._sessions.add(session);
123 this.emit('peer', { key: peerKey, state: 'connected' });
124124
125125 conn.on('close', () => {
126 this._sessions.delete(session)
127 this.emit('peer', { key: peerKey, state: 'disconnected' })
128 })
126 this._sessions.delete(session);
127 this.emit('peer', { key: peerKey, state: 'disconnected' });
128 });
129129
130130 conn.on('error', (err) => {
131 this._sessions.delete(session)
132 this.emit('error', err)
133 })
131 this._sessions.delete(session);
132 this.emit('error', err);
133 });
134134
135 // Send manifest with version and integrity hash
136135 proto.sendMeta({
137136 type: 'manifest',
138137 version: PROTOCOL_VERSION,
139138 files: this.manifest.files,
140139 manifestHash: this._manifestHash
141 }).catch(safetyCatch)
140 }).catch(safetyCatch);
142141
143 // Handle client messages
144142 proto.metaMsg.onmessage = async (raw) => {
145 if (session.destroyed) return
143 if (session.destroyed) return;
146144
147 let msg
148 try { msg = proto.parseMeta(raw) } catch { return }
145 let msg;
146 try { msg = proto.parseMeta(raw); } catch { return; }
149147
150148 if (msg.type === 'up-to-date') {
151 this.emit('peer', { key: peerKey, state: 'done' })
152 return
149 this.emit('peer', { key: peerKey, state: 'done' });
150 return;
153151 }
154152
155153 if (msg.type === 'requests') {
156 this.emit('peer', { key: peerKey, state: 'transferring' })
154 this.emit('peer', { key: peerKey, state: 'transferring' });
157155
158 const requested = new Set(msg.paths)
156 const requested = new Set(msg.paths);
159157 this.emit('progress', {
160158 peer: peerKey,
161159 requested: requested.size,
162160 skipped: this.manifest.files.length - requested.size
163 })
161 });
164162
165163 for (const f of this.manifest.files) {
166 if (session.destroyed) break
167 if (!requested.has(f.path)) continue
168
169 const fullPath = path.join(this.dir, f.path)
170 let stat
171 try { stat = fs.statSync(fullPath) } catch {
172 await proto.sendMeta({ type: 'error', message: 'File not found', path: f.path })
173 continue
164 if (session.destroyed) break;
165 if (!requested.has(f.path)) continue;
166
167 const fullPath = path.join(this.dir, f.path);
168 let stat;
169 try { stat = fs.statSync(fullPath); } catch {
170 await proto.sendMeta({ type: 'error', message: 'File not found', path: f.path });
171 continue;
174172 }
175173
176 // File start
177 await proto.sendMeta({ type: 'file-start', path: f.path, size: stat.size, hash: f.hash })
174 await proto.sendMeta({ type: 'file-start', path: f.path, size: stat.size, hash: f.hash });
178175
179 // Stream data from disk
180 let fd
176 let fd;
181177 try {
182 fd = fs.openSync(fullPath, 'r')
178 fd = fs.openSync(fullPath, 'r');
183179 for (let off = 0; off < stat.size; off += CHUNK_SIZE) {
184 if (session.destroyed) break
185 const len = Math.min(CHUNK_SIZE, stat.size - off)
186 const buf = Buffer.alloc(len)
187 fs.readSync(fd, buf, 0, len, off)
188 await proto.sendData(buf)
189 session.bytesServed += len
190 this.stats.bytesServed += len
180 if (session.destroyed) break;
181 const len = Math.min(CHUNK_SIZE, stat.size - off);
182 const buf = Buffer.alloc(len);
183 fs.readSync(fd, buf, 0, len, off);
184 await proto.sendData(buf);
185 session.bytesServed += len;
186 this.stats.bytesServed += len;
191187 }
192188 } finally {
193 if (fd !== undefined) try { fs.closeSync(fd) } catch {}
189 if (fd !== undefined) try { fs.closeSync(fd); } catch {}
194190 }
195191
196 // File end
197 await proto.sendMeta({ type: 'file-end', path: f.path })
192 await proto.sendMeta({ type: 'file-end', path: f.path });
198193
199 session.filesServed++
200 this.stats.filesServed++
194 session.filesServed++;
195 this.stats.filesServed++;
201196 this.emit('progress', {
202197 peer: peerKey,
203198 file: f.path,
204199 filesServed: session.filesServed,
205200 bytesServed: session.bytesServed
206 })
201 });
207202 }
208203
209204 await proto.sendMeta({
210205 type: 'transfer-complete',
211206 filesServed: session.filesServed,
212207 bytesServed: session.bytesServed
213 })
208 });
214209
215 this.emit('peer', { key: peerKey, state: 'done' })
210 this.emit('peer', { key: peerKey, state: 'done' });
216211 }
217212
218213 if (msg.type === 'done') {
219 this._sessions.delete(session)
214 this._sessions.delete(session);
220215 }
221 }
216 };
222217 }
223218}
224219
225220// ─── DeployClient ────────────────────────────────────────────
226221
227export class DeployClient extends ReadyResource {
222class DeployClient extends ReadyResource {
228223 constructor (topic, destDir, opts) {
229 super()
230 opts = opts || {}
224 super();
225 opts = opts || {};
231226
232 this.topic = typeof topic === 'string' ? Buffer.from(topic, 'hex') : topic
233 this.destDir = path.resolve(destDir)
234 this.swarm = opts.swarm || null
235 this._ownSwarm = !this.swarm
236 this._timeout = opts.timeout !== undefined ? opts.timeout : 30000
237 this._verify = opts.verify !== false
238 this.connected = false
227 this.topic = typeof topic === 'string' ? Buffer.from(topic, 'hex') : topic;
228 this.destDir = path.resolve(destDir);
229 this.swarm = opts.swarm || null;
230 this._ownSwarm = !this.swarm;
231 this._timeout = opts.timeout !== undefined ? opts.timeout : 30000;
232 this._verify = opts.verify !== false;
233 this.connected = false;
239234
240235 this.stats = {
241236 filesTransferred: 0,
……@@ -244,238 +239,222 @@ export class DeployClient extends ReadyResource {
244239 totalFiles: 0,
245240 elapsed: 0,
246241 errors: []
247 }
248
249 this._pullResolve = null
250 this._pullReject = null
251 this._startTime = null
252 this._currentFd = null
253 this._currentPath = null
254 this._currentHash = null
255 this._connectTimer = null
256
257 this.ready().catch(safetyCatch)
242 };
243
244 this._pullResolve = null;
245 this._pullReject = null;
246 this._startTime = null;
247 this._currentFd = null;
248 this._currentPath = null;
249 this._currentHash = null;
250 this._currentWritten = 0;
251 this._connectTimer = null;
252
253 this.ready().catch(safetyCatch);
258254 }
259255
260256 async _open () {
261 fs.mkdirSync(this.destDir, { recursive: true })
257 fs.mkdirSync(this.destDir, { recursive: true });
262258
263 if (!this.swarm) this.swarm = new Hyperswarm()
264 this.swarm.on('connection', (conn) => this._onConnection(conn))
259 if (!this.swarm) this.swarm = new Hyperswarm();
260 this.swarm.on('connection', (conn) => this._onConnection(conn));
265261
266 this.swarm.join(this.topic, { server: false, client: true })
267 await this.swarm.flush()
262 this.swarm.join(this.topic, { server: false, client: true });
263 await this.swarm.flush();
268264 }
269265
270266 async _close () {
271 // Close any open file descriptor
272267 if (this._currentFd !== null) {
273 try { fs.closeSync(this._currentFd) } catch {}
274 this._currentFd = null
268 try { fs.closeSync(this._currentFd); } catch {}
269 this._currentFd = null;
275270 }
276271
277272 if (this._connectTimer) {
278 clearTimeout(this._connectTimer)
279 this._connectTimer = null
273 clearTimeout(this._connectTimer);
274 this._connectTimer = null;
280275 }
281276
282277 if (this._pullReject) {
283 this._pullReject(new Error('Client closed during pull'))
284 this._pullReject = null
285 this._pullResolve = null
278 this._pullReject(new Error('Client closed during pull'));
279 this._pullReject = null;
280 this._pullResolve = null;
286281 }
287282
288283 if (this._ownSwarm && this.swarm) {
289 await this.swarm.destroy()
290 this.swarm = null
284 await this.swarm.destroy();
285 this.swarm = null;
291286 }
292287 }
293288
294 /**
295 * Pull files from the server. Resolves when complete.
296 * @returns {Promise<{ filesTransferred, filesSkipped, bytesReceived, totalFiles, elapsed, errors }>}
297 */
298289 pull () {
299 if (this._pullResolve) return Promise.reject(new Error('Pull already in progress'))
290 if (this._pullResolve) return Promise.reject(new Error('Pull already in progress'));
300291
301 this._startTime = Date.now()
292 this._startTime = Date.now();
302293
303294 return new Promise((resolve, reject) => {
304 this._pullResolve = resolve
305 this._pullReject = reject
295 this._pullResolve = resolve;
296 this._pullReject = reject;
306297
307 // Connection timeout
308298 if (this._timeout > 0 && !this.connected) {
309299 this._connectTimer = setTimeout(() => {
310300 if (!this.connected && this._pullReject) {
311 this._pullReject(new Error('Connection timeout after ' + this._timeout + 'ms'))
312 this._pullReject = null
313 this._pullResolve = null
301 this._pullReject(new Error('Connection timeout after ' + this._timeout + 'ms'));
302 this._pullReject = null;
303 this._pullResolve = null;
314304 }
315 }, this._timeout)
305 }, this._timeout);
316306 }
317 })
307 });
318308 }
319309
320310 _finish () {
321 this.stats.elapsed = Date.now() - this._startTime
311 this.stats.elapsed = Date.now() - this._startTime;
322312 if (this._pullResolve) {
323 const resolve = this._pullResolve
324 this._pullResolve = null
325 this._pullReject = null
326 resolve({ ...this.stats })
313 const resolve = this._pullResolve;
314 this._pullResolve = null;
315 this._pullReject = null;
316 resolve({ ...this.stats });
327317 }
328 this.emit('complete', { ...this.stats })
318 this.emit('complete', { ...this.stats });
329319 }
330320
331321 _fail (err) {
332 this.stats.elapsed = Date.now() - (this._startTime || Date.now())
333 this.emit('error', err)
322 this.stats.elapsed = Date.now() - (this._startTime || Date.now());
323 this.emit('error', err);
334324 if (this._pullReject) {
335 const reject = this._pullReject
336 this._pullReject = null
337 this._pullResolve = null
338 reject(err)
325 const reject = this._pullReject;
326 this._pullReject = null;
327 this._pullResolve = null;
328 reject(err);
339329 }
340330 }
341331
342332 _onConnection (conn) {
343333 if (this._connectTimer) {
344 clearTimeout(this._connectTimer)
345 this._connectTimer = null
334 clearTimeout(this._connectTimer);
335 this._connectTimer = null;
346336 }
347337
348 this.connected = true
338 this.connected = true;
349339 const peerKey = conn.remotePublicKey
350340 ? conn.remotePublicKey.toString('hex').slice(0, 12)
351 : 'unknown'
352 this.emit('connected', peerKey)
341 : 'unknown';
342 this.emit('connected', peerKey);
353343
354344 conn.on('close', () => {
355 this.connected = false
356 // If we were mid-transfer, that's an error
345 this.connected = false;
357346 if (this._pullReject && this.stats.filesTransferred + this.stats.filesSkipped < this.stats.totalFiles) {
358 this._fail(new Error('Connection lost during transfer'))
347 this._fail(new Error('Connection lost during transfer'));
359348 }
360 })
349 });
361350
362351 conn.on('error', (err) => {
363 this.connected = false
364 this._fail(err)
365 })
352 this.connected = false;
353 this._fail(err);
354 });
366355
367 const proto = attachProtocol(conn)
356 const proto = attachProtocol(conn);
368357 if (!proto) {
369 this._fail(new Error('Failed to attach protocol'))
370 return
358 this._fail(new Error('Failed to attach protocol'));
359 return;
371360 }
372361
373362 proto.metaMsg.onmessage = (raw) => {
374 let msg
375 try { msg = proto.parseMeta(raw) } catch (err) {
376 this._fail(new Error('Invalid protocol message'))
377 return
363 let msg;
364 try { msg = proto.parseMeta(raw); } catch {
365 this._fail(new Error('Invalid protocol message'));
366 return;
378367 }
379368
380 if (msg.type === 'manifest') {
381 this._handleManifest(msg, proto)
382 } else if (msg.type === 'file-start') {
383 this._handleFileStart(msg)
384 } else if (msg.type === 'file-end') {
385 this._handleFileEnd(msg)
386 } else if (msg.type === 'transfer-complete') {
387 this._handleTransferComplete(msg, proto)
388 } else if (msg.type === 'error') {
389 this.stats.errors.push({ path: msg.path, message: msg.message })
390 this.emit('error', new Error(msg.message))
369 if (msg.type === 'manifest') this._handleManifest(msg, proto);
370 else if (msg.type === 'file-start') this._handleFileStart(msg);
371 else if (msg.type === 'file-end') this._handleFileEnd(msg);
372 else if (msg.type === 'transfer-complete') this._handleTransferComplete(msg, proto);
373 else if (msg.type === 'error') {
374 this.stats.errors.push({ path: msg.path, message: msg.message });
375 this.emit('error', new Error(msg.message));
391376 }
392 }
377 };
393378
394379 proto.dataMsg.onmessage = (chunk) => {
395380 if (this._currentFd !== null) {
396381 try {
397 fs.writeSync(this._currentFd, chunk, 0, chunk.length, this._currentWritten)
398 this._currentWritten += chunk.length
399 this.stats.bytesReceived += chunk.length
382 fs.writeSync(this._currentFd, chunk, 0, chunk.length, this._currentWritten);
383 this._currentWritten += chunk.length;
384 this.stats.bytesReceived += chunk.length;
400385 } catch (err) {
401 this._fail(new Error('Write failed: ' + err.message))
386 this._fail(new Error('Write failed: ' + err.message));
402387 }
403388 }
404 }
389 };
405390 }
406391
407392 _handleManifest (msg, proto) {
408 // Version check
409393 if (msg.version && msg.version !== PROTOCOL_VERSION) {
410 this._fail(new Error('Protocol version mismatch: expected ' + PROTOCOL_VERSION + ', got ' + msg.version))
411 return
394 this._fail(new Error('Protocol version mismatch: expected ' + PROTOCOL_VERSION + ', got ' + msg.version));
395 return;
412396 }
413397
414 this.stats.totalFiles = msg.files.length
415 this.emit('manifest', msg.files.length)
398 this.stats.totalFiles = msg.files.length;
399 this.emit('manifest', msg.files.length);
416400
417 // Diff against local files
418 const { needed, skipped } = diffManifest(msg.files, this.destDir)
419 this.stats.filesSkipped = skipped
420 this.emit('diff', needed.length, skipped)
401 const { needed, skipped } = diffManifest(msg.files, this.destDir);
402 this.stats.filesSkipped = skipped;
403 this.emit('diff', needed.length, skipped);
421404
422405 if (needed.length === 0) {
423 proto.sendMeta({ type: 'up-to-date' }).catch(safetyCatch)
424 this.emit('up-to-date')
425 this._finish()
426 return
406 proto.sendMeta({ type: 'up-to-date' }).catch(safetyCatch);
407 this.emit('up-to-date');
408 this._finish();
409 return;
427410 }
428411
429 // Pipeline all requests
430 proto.sendMeta({ type: 'requests', paths: needed }).catch(safetyCatch)
412 proto.sendMeta({ type: 'requests', paths: needed }).catch(safetyCatch);
431413 }
432414
433415 _handleFileStart (msg) {
434 const targetPath = path.resolve(path.join(this.destDir, msg.path))
416 const targetPath = path.resolve(path.join(this.destDir, msg.path));
435417
436 // Reject path traversal (e.g. ../../.bashrc)
437418 if (!targetPath.startsWith(this.destDir + path.sep) && targetPath !== this.destDir) {
438 this.stats.errors.push({ path: msg.path, message: 'Path traversal rejected' })
439 this.emit('error', new Error('Path traversal rejected: ' + msg.path))
440 return
419 this.stats.errors.push({ path: msg.path, message: 'Path traversal rejected' });
420 this.emit('error', new Error('Path traversal rejected: ' + msg.path));
421 return;
441422 }
442423
443 fs.mkdirSync(path.dirname(targetPath), { recursive: true })
424 fs.mkdirSync(path.dirname(targetPath), { recursive: true });
444425
445 this._currentPath = msg.path
446 this._currentHash = msg.hash
447 this._currentFd = fs.openSync(targetPath, 'w')
448 this._currentWritten = 0
449 if (msg.size > 0) fs.ftruncateSync(this._currentFd, msg.size)
426 this._currentPath = msg.path;
427 this._currentHash = msg.hash;
428 this._currentFd = fs.openSync(targetPath, 'w');
429 this._currentWritten = 0;
430 if (msg.size > 0) fs.ftruncateSync(this._currentFd, msg.size);
450431 }
451432
452433 _handleFileEnd (msg) {
453434 if (this._currentFd !== null) {
454 fs.closeSync(this._currentFd)
455 this._currentFd = null
435 fs.closeSync(this._currentFd);
436 this._currentFd = null;
456437 }
457438
458 // Verify hash if enabled
459439 if (this._verify && this._currentHash) {
460 const targetPath = path.join(this.destDir, msg.path)
440 const targetPath = path.join(this.destDir, msg.path);
461441 try {
462 const actual = hashFile(targetPath)
442 const actual = hashFile(targetPath);
463443 if (actual !== this._currentHash) {
464 const err = 'Hash mismatch for ' + msg.path
465 this.stats.errors.push({ path: msg.path, message: err })
466 this.emit('error', new Error(err))
467 // Delete corrupted file
468 try { fs.unlinkSync(targetPath) } catch {}
469 return
444 const err = 'Hash mismatch for ' + msg.path;
445 this.stats.errors.push({ path: msg.path, message: err });
446 this.emit('error', new Error(err));
447 try { fs.unlinkSync(targetPath); } catch {}
448 return;
470449 }
471450 } catch (err) {
472 this.stats.errors.push({ path: msg.path, message: err.message })
451 this.stats.errors.push({ path: msg.path, message: err.message });
473452 }
474453 }
475454
476 this.stats.filesTransferred++
477 this._currentPath = null
478 this._currentHash = null
455 this.stats.filesTransferred++;
456 this._currentPath = null;
457 this._currentHash = null;
479458
480459 this.emit('progress', {
481460 file: msg.path,
……@@ -483,11 +462,13 @@ export class DeployClient extends ReadyResource {
483462 filesSkipped: this.stats.filesSkipped,
484463 bytesReceived: this.stats.bytesReceived,
485464 totalFiles: this.stats.totalFiles
486 })
465 });
487466 }
488467
489468 _handleTransferComplete (msg, proto) {
490 proto.sendMeta({ type: 'done' }).catch(safetyCatch)
491 this._finish()
469 proto.sendMeta({ type: 'done' }).catch(safetyCatch);
470 this._finish();
492471 }
493472}
473
474module.exports = { DeployServer, DeployClient };
lib/protocol.js +24 -59
……@@ -1,60 +1,36 @@
1/**
2 * Deploy protocol — Protomux channel for release distribution
3 *
4 * Two message types on one channel:
5 * meta (JSON): typed messages for control flow
6 * data (raw): file content chunks
7 *
8 * Message types:
9 * manifest — server → client: file list with hashes
10 * requests — client → server: which files to send
11 * up-to-date — client → server: nothing needed
12 * file-start — server → client: begin file transfer
13 * file-end — server → client: file transfer complete
14 * transfer-complete — server → client: all files sent
15 * done — client → server: acknowledged complete
16 * error — either direction: something went wrong
17 */
1const Protomux = require('protomux');
2const c = require('compact-encoding');
183
19import Protomux from 'protomux'
20import c from 'compact-encoding'
4const PROTOCOL_NAME = 'hyper-deploy';
5const PROTOCOL_VERSION = 1;
6const CHUNK_SIZE = 65536;
217
22export const PROTOCOL_NAME = 'hyper-deploy'
23export const PROTOCOL_VERSION = 1
24export const CHUNK_SIZE = 65536
8function attachProtocol (stream) {
9 const mux = Protomux.from(stream);
10 if (!mux) return null;
2511
26/**
27 * Attach the deploy protocol to a Protomux-capable stream.
28 *
29 * @param {Stream} stream — SecretStream or Hyperswarm connection
30 * @returns {{ ch, metaMsg, dataMsg, mux, send, sendMeta, sendData } | null}
31 */
32export function attachProtocol (stream) {
33 const mux = Protomux.from(stream)
34 if (!mux) return null
35
36 let drainResolve = null
12 let drainResolve = null;
3713
3814 const ch = mux.createChannel({
3915 protocol: PROTOCOL_NAME,
4016 ondrain () {
4117 if (drainResolve) {
42 const r = drainResolve
43 drainResolve = null
44 r()
18 const r = drainResolve;
19 drainResolve = null;
20 r();
4521 }
4622 }
47 })
23 });
4824
49 if (!ch) return null
25 if (!ch) return null;
5026
51 const metaMsg = ch.addMessage({ encoding: c.raw })
52 const dataMsg = ch.addMessage({ encoding: c.raw })
27 const metaMsg = ch.addMessage({ encoding: c.raw });
28 const dataMsg = ch.addMessage({ encoding: c.raw });
5329
54 ch.open()
30 ch.open();
5531
5632 async function waitDrain () {
57 await new Promise(r => { drainResolve = r })
33 await new Promise(r => { drainResolve = r; });
5834 }
5935
6036 return {
……@@ -63,33 +39,22 @@ export function attachProtocol (stream) {
6339 dataMsg,
6440 mux,
6541
66 /**
67 * Send a typed JSON message on the meta channel with backpressure.
68 * @param {object} obj — must have a `type` field
69 */
7042 async sendMeta (obj) {
7143 if (!metaMsg.send(Buffer.from(JSON.stringify(obj)))) {
72 await waitDrain()
44 await waitDrain();
7345 }
7446 },
7547
76 /**
77 * Send raw data on the data channel with backpressure.
78 * @param {Buffer} buf
79 */
8048 async sendData (buf) {
8149 if (!dataMsg.send(buf)) {
82 await waitDrain()
50 await waitDrain();
8351 }
8452 },
8553
86 /**
87 * Parse a received meta message.
88 * @param {Buffer} raw
89 * @returns {object}
90 */
9154 parseMeta (raw) {
92 return JSON.parse(raw.toString())
55 return JSON.parse(raw.toString());
9356 }
94 }
57 };
9558}
59
60module.exports = { attachProtocol, PROTOCOL_NAME, PROTOCOL_VERSION, CHUNK_SIZE };
lib/scanner.js +41 -78
……@@ -1,108 +1,71 @@
1/**
2 * Directory scanner — walk, stat, hash files
3 *
4 * Returns a manifest: array of { path, size, hash } for all files
5 * under a directory. Used by the server to build the file list
6 * and by the client to determine which files it already has.
7 */
1const fs = require('fs');
2const path = require('path');
3const crypto = require('hypercore-crypto');
84
9import fs from 'bare-fs'
10import path from 'bare-path'
11import crypto from 'hypercore-crypto'
12
13/**
14 * Walk a directory recursively, return file metadata.
15 * @param {string} dir — absolute path to scan
16 * @param {string} [prefix] — internal, for recursion
17 * @returns {Array<{ path: string, size: number }>}
18 */
19export function walkDir (dir, prefix) {
20 prefix = prefix || ''
21 const results = []
5function walkDir (dir, prefix) {
6 prefix = prefix || '';
7 const results = [];
228 try {
239 for (const entry of fs.readdirSync(dir)) {
24 const full = path.join(dir, entry)
25 const rel = prefix ? prefix + '/' + entry : entry
10 const full = path.join(dir, entry);
11 const rel = prefix ? prefix + '/' + entry : entry;
2612 try {
27 const stat = fs.statSync(full)
28 if (stat.isDirectory()) results.push(...walkDir(full, rel))
29 else if (stat.isFile()) results.push({ path: rel, size: stat.size })
13 const stat = fs.statSync(full);
14 if (stat.isDirectory()) results.push(...walkDir(full, rel));
15 else if (stat.isFile()) results.push({ path: rel, size: stat.size });
3016 } catch {}
3117 }
3218 } catch {}
33 return results
19 return results;
3420}
3521
36/**
37 * Hash a file's contents with blake2b-256.
38 * @param {string} filePath — absolute path
39 * @returns {string} hex hash
40 */
41export function hashFile (filePath) {
42 return crypto.data(fs.readFileSync(filePath)).toString('hex')
22function hashFile (filePath) {
23 return crypto.data(fs.readFileSync(filePath)).toString('hex');
4324}
4425
45/**
46 * Build a full manifest: walk + hash every file.
47 * @param {string} dir — absolute path to scan
48 * @param {{ onProgress?: (i: number, total: number) => void }} [opts]
49 * @returns {{ files: Array<{ path: string, size: number, hash: string }>, totalSize: number }}
50 */
51export function buildManifest (dir, opts) {
52 const onProgress = opts && opts.onProgress
53 const files = walkDir(dir)
54 let totalSize = 0
26function buildManifest (dir, opts) {
27 const onProgress = opts && opts.onProgress;
28 const files = walkDir(dir);
29 let totalSize = 0;
5530
5631 for (let i = 0; i < files.length; i++) {
57 const f = files[i]
58 f.hash = hashFile(path.join(dir, f.path))
59 totalSize += f.size
60 if (onProgress) onProgress(i + 1, files.length)
32 const f = files[i];
33 f.hash = hashFile(path.join(dir, f.path));
34 totalSize += f.size;
35 if (onProgress) onProgress(i + 1, files.length);
6136 }
6237
63 return { files, totalSize }
38 return { files, totalSize };
6439}
6540
66/**
67 * Diff a remote manifest against local files.
68 * Returns which files the client needs to request.
69 * @param {Array<{ path: string, size: number, hash: string }>} manifest — server's file list
70 * @param {string} localDir — local directory to compare against
71 * @returns {{ needed: string[], skipped: number }}
72 */
73export function diffManifest (manifest, localDir) {
74 const needed = []
75 let skipped = 0
41function diffManifest (manifest, localDir) {
42 const needed = [];
43 let skipped = 0;
7644
7745 for (const f of manifest) {
78 const localPath = path.join(localDir, f.path)
46 const localPath = path.join(localDir, f.path);
7947 try {
80 const stat = fs.statSync(localPath)
48 const stat = fs.statSync(localPath);
8149 if (stat.size === f.size) {
82 const localHash = hashFile(localPath)
50 const localHash = hashFile(localPath);
8351 if (localHash === f.hash) {
84 skipped++
85 continue
52 skipped++;
53 continue;
8654 }
8755 }
8856 } catch {}
89 needed.push(f.path)
57 needed.push(f.path);
9058 }
9159
92 return { needed, skipped }
60 return { needed, skipped };
9361}
9462
95/**
96 * Read a chunk from a file without loading the whole thing.
97 * @param {string} filePath
98 * @param {number} offset
99 * @param {number} length
100 * @returns {Buffer}
101 */
102export function readChunk (filePath, offset, length) {
103 const fd = fs.openSync(filePath, 'r')
104 const buf = Buffer.alloc(length)
105 fs.readSync(fd, buf, 0, length, offset)
106 fs.closeSync(fd)
107 return buf
63function readChunk (filePath, offset, length) {
64 const fd = fs.openSync(filePath, 'r');
65 const buf = Buffer.alloc(length);
66 fs.readSync(fd, buf, 0, length, offset);
67 fs.closeSync(fd);
68 return buf;
10869}
70
71module.exports = { walkDir, hashFile, buildManifest, diffManifest, readChunk };
package-lock.json +37 -462
……@@ -1,39 +1,29 @@
11{
22 "name": "hyper-deploy",
3 "version": "0.1.0",
3 "version": "0.2.0",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "hyper-deploy",
9 "version": "0.1.0",
9 "version": "0.2.0",
1010 "dependencies": {
11 "@hyperswarm/secret-stream": "^6.0.0",
12 "bare-fs": "^4.0.0",
13 "bare-http1": "^2.0.0",
14 "bare-node-runtime": "^1.2.0",
15 "bare-os": "^3.0.0",
16 "bare-path": "^3.0.0",
1711 "compact-encoding": "^2.0.0",
1812 "hypercore-crypto": "^3.0.0",
1913 "hyperswarm": "^4.0.0",
2014 "protomux": "^3.0.0",
2115 "ready-resource": "^1.0.0",
22 "safety-catch": "^1.0.3",
23 "udx-native": "^1.20.3"
24 },
25 "bin": {
26 "hyper-deploy": "bin/hyper-deploy.js"
16 "safety-catch": "^1.0.0"
2717 },
2818 "devDependencies": {
19 "@hyperswarm/secret-stream": "^6.0.0",
2920 "brittle": "^3.0.0",
30 "corestore": "^7.0.0",
31 "hyper-overlay": "file:../hyper-overlay"
21 "corestore": "^7.0.0"
3222 }
3323 },
3424 "../hyper-overlay": {
3525 "version": "0.1.0",
36 "dev": true,
26 "extraneous": true,
3727 "license": "AGPL-3.0",
3828 "dependencies": {
3929 "@hyperswarm/testnet": "^3.1.4",
……@@ -154,18 +144,9 @@
154144 "version": "2.0.13",
155145 "resolved": "https://registry.npmjs.org/bare-abort/-/bare-abort-2.0.13.tgz",
156146 "integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==",
147 "dev": true,
157148 "license": "Apache-2.0"
158149 },
159 "node_modules/bare-abort-controller": {
160 "version": "1.1.1",
161 "resolved": "https://registry.npmjs.org/bare-abort-controller/-/bare-abort-controller-1.1.1.tgz",
162 "integrity": "sha512-A/3w+PHaGnjlrRcmBeuCk8v23SP35db1X6a5GOkTVSloVmMEWdVhhnlrJWzIvnLJh79ljhAFS1lvLRev09LCGg==",
163 "license": "Apache-2.0",
164 "peer": true,
165 "dependencies": {
166 "bare-events": "^2.7.0"
167 }
168 },
169150 "node_modules/bare-addon-resolve": {
170151 "version": "1.10.0",
171152 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
……@@ -210,25 +191,11 @@
210191 "bare-inspect": "^3.1.2"
211192 }
212193 },
213 "node_modules/bare-async-hooks": {
214 "version": "0.0.0",
215 "resolved": "https://registry.npmjs.org/bare-async-hooks/-/bare-async-hooks-0.0.0.tgz",
216 "integrity": "sha512-xNfGwUobaomCGMGAqohAekS3uMCj+4tvI4AoOaJnO7NfpN+dvFdkC5xkeQtmZzs2vxf2TR5J6i5FDd1ImCZERw==",
217 "license": "Apache-2.0"
218 },
219 "node_modules/bare-buffer": {
220 "version": "3.6.0",
221 "resolved": "https://registry.npmjs.org/bare-buffer/-/bare-buffer-3.6.0.tgz",
222 "integrity": "sha512-/maRWEQ2eBkVNMbNFVsq1pHXJYVj4Y3AixwruB24eKZDs5Gtu0fixzvjYmBIuTsBMtVH5Yb27pQO9BhFa+IlIQ==",
223 "license": "Apache-2.0",
224 "engines": {
225 "bare": ">=1.20.0"
226 }
227 },
228194 "node_modules/bare-bundle": {
229195 "version": "1.10.0",
230196 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
231197 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
198 "dev": true,
232199 "license": "Apache-2.0",
233200 "peerDependencies": {
234201 "bare-buffer": "*",
……@@ -243,31 +210,6 @@
243210 }
244211 }
245212 },
246 "node_modules/bare-channel": {
247 "version": "5.2.3",
248 "resolved": "https://registry.npmjs.org/bare-channel/-/bare-channel-5.2.3.tgz",
249 "integrity": "sha512-2cRErqS4fzzr3ZUSd5W67ka70dZKJ1VWrCsvxPVC8vr3rWvd4aaCG5iCDas9+MFVaUIWvfw04+NH4+FHQDddMQ==",
250 "license": "Apache-2.0",
251 "dependencies": {
252 "bare-events": "^2.0.0",
253 "bare-stream": "^2.7.0",
254 "bare-structured-clone": "^1.4.0"
255 },
256 "engines": {
257 "bare": ">=1.7.0"
258 }
259 },
260 "node_modules/bare-console": {
261 "version": "6.1.0",
262 "resolved": "https://registry.npmjs.org/bare-console/-/bare-console-6.1.0.tgz",
263 "integrity": "sha512-BziGTEIyVh0zn78842mmpOLGTEFE7/S4A5Tb0d2iIu7bryVdoiF0przraYMetJckHxm0P5ITTZrYfRuYRr9n/Q==",
264 "license": "Apache-2.0",
265 "dependencies": {
266 "bare-hrtime": "^2.0.0",
267 "bare-logger": "^2.0.0",
268 "bare-system-logger": "^1.0.2"
269 }
270 },
271213 "node_modules/bare-cov": {
272214 "version": "1.2.1",
273215 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
……@@ -289,6 +231,7 @@
289231 "version": "1.13.4",
290232 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.4.tgz",
291233 "integrity": "sha512-JiCZ5l2YOG1y8J7yy1BCAKTCZrPnHLb7pDRIdurBTOn5oIwBQDIv8iH5Pl2V85vzjl1NZXRfNY4HZLsE942jJA==",
234 "dev": true,
292235 "license": "Apache-2.0",
293236 "dependencies": {
294237 "bare-assert": "^1.2.0",
……@@ -307,31 +250,17 @@
307250 "version": "2.0.0",
308251 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
309252 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
253 "dev": true,
310254 "license": "Apache-2.0",
311255 "dependencies": {
312256 "bare-os": "^3.0.1"
313257 }
314258 },
315 "node_modules/bare-dgram": {
316 "version": "1.0.1",
317 "resolved": "https://registry.npmjs.org/bare-dgram/-/bare-dgram-1.0.1.tgz",
318 "integrity": "sha512-EdsyRErrkWgN8fENdrDdXFEE9HAuJ/m6ehXz13fVj9JhdCaLWIA+L8o5aYNRLt66x08RlyG2vbrRAZoxGfcdlg==",
319 "license": "Apache-2.0",
320 "dependencies": {
321 "bare-events": "^2.5.0",
322 "udx-native": "^1.11.2"
323 }
324 },
325 "node_modules/bare-diagnostics-channel": {
326 "version": "1.1.0",
327 "resolved": "https://registry.npmjs.org/bare-diagnostics-channel/-/bare-diagnostics-channel-1.1.0.tgz",
328 "integrity": "sha512-Reu+EQo+eLpB+a5p8UykFEdXndFaRaSgKV38uAMh/qhE2eTeJcdwAwo74hKYyeN8GD3DIFqC9ZlM4bnDc03CIg==",
329 "license": "Apache-2.0"
330 },
331259 "node_modules/bare-dns": {
332260 "version": "2.1.4",
333261 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
334262 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
263 "dev": true,
335264 "license": "Apache-2.0",
336265 "engines": {
337266 "bare": ">=1.7.0"
……@@ -341,6 +270,7 @@
341270 "version": "1.0.3",
342271 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
343272 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
273 "dev": true,
344274 "license": "Apache-2.0",
345275 "peerDependencies": {
346276 "bare-buffer": "*"
……@@ -355,6 +285,7 @@
355285 "version": "3.0.0",
356286 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
357287 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
288 "dev": true,
358289 "license": "Apache-2.0",
359290 "dependencies": {
360291 "bare-os": "^3.0.1"
……@@ -374,70 +305,11 @@
374305 }
375306 }
376307 },
377 "node_modules/bare-fetch": {
378 "version": "2.8.2",
379 "resolved": "https://registry.npmjs.org/bare-fetch/-/bare-fetch-2.8.2.tgz",
380 "integrity": "sha512-N0gZH8iuuC36Ygd4Kjfeo90aOKZ19cycbuZWVpm9W+f5v2ks/2qvgVIKp9hRDZSo5imqO097ynJcn4w24F8c/A==",
381 "license": "Apache-2.0",
382 "dependencies": {
383 "bare-form-data": "^1.2.0",
384 "bare-http1": "^4.5.2",
385 "bare-https": "^2.0.0",
386 "bare-stream": "^2.9.1",
387 "bare-url": "^2.4.0",
388 "bare-zlib": "^1.3.0"
389 },
390 "peerDependencies": {
391 "bare-abort-controller": "*",
392 "bare-buffer": "*"
393 },
394 "peerDependenciesMeta": {
395 "bare-abort-controller": {
396 "optional": true
397 },
398 "bare-buffer": {
399 "optional": true
400 }
401 }
402 },
403 "node_modules/bare-fetch/node_modules/bare-http1": {
404 "version": "4.5.5",
405 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.5.tgz",
406 "integrity": "sha512-ADITiRo0huP76JGMbv6Arsh9KehHqjEBoYcmjvAo67IY78+/9mV2MeKLLCkiJSFxA85T/m8cTaE44OwJQCcwdw==",
407 "license": "Apache-2.0",
408 "dependencies": {
409 "bare-events": "^2.6.0",
410 "bare-http-parser": "^1.1.1",
411 "bare-stream": "^2.10.0",
412 "bare-tcp": "^2.2.0"
413 },
414 "peerDependencies": {
415 "bare-buffer": "*",
416 "bare-url": "*"
417 },
418 "peerDependenciesMeta": {
419 "bare-buffer": {
420 "optional": true
421 },
422 "bare-url": {
423 "optional": true
424 }
425 }
426 },
427 "node_modules/bare-form-data": {
428 "version": "1.2.1",
429 "resolved": "https://registry.npmjs.org/bare-form-data/-/bare-form-data-1.2.1.tgz",
430 "integrity": "sha512-DFZhp5Tn0S7TjII/xhgLl2BNU6f7l5QbIW/YH7TjzQVnxLa8SaMSbl77jpy0ZR1oUnAo0a7NHJ9QAOU6PPr0Jg==",
431 "license": "Apache-2.0",
432 "dependencies": {
433 "bare-buffer": "^3.6.0",
434 "bare-stream": "^2.6.5"
435 }
436 },
437308 "node_modules/bare-format": {
438309 "version": "1.0.2",
439310 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz",
440311 "integrity": "sha512-GswdhnOnP9QtwRbrf4wLApw5widkaLMsLe2XOs35fQD2YfEN1ApoGka+cZ7PfvzxMgfYXmMhj/2OGlVn5/Dxgw==",
312 "dev": true,
441313 "license": "Apache-2.0",
442314 "dependencies": {
443315 "bare-inspect": "^3.0.0"
……@@ -447,6 +319,7 @@
447319 "version": "4.6.0",
448320 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.6.0.tgz",
449321 "integrity": "sha512-2YkS7NuiJceSEbyEOdSNLE9tsGd+f4+f7C+Nik/MCk27SYdwIMPT/yRKvg++FZhQXgk0KWJKJyXX9RhVV0RGqA==",
322 "dev": true,
450323 "license": "Apache-2.0",
451324 "dependencies": {
452325 "bare-events": "^2.5.4",
……@@ -471,28 +344,21 @@
471344 "version": "2.1.1",
472345 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
473346 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
347 "dev": true,
474348 "license": "Apache-2.0"
475349 },
476350 "node_modules/bare-http-parser": {
477351 "version": "1.1.3",
478352 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.3.tgz",
479353 "integrity": "sha512-+dhVvQi6brHq14L/XHNRQ+TLuVE76VjRmMt61wVEtS+Od8xUslfMHWJN/ZjIIt3RtTG6vPuA+x9cOh7KrkBJsA==",
354 "dev": true,
480355 "license": "Apache-2.0"
481356 },
482 "node_modules/bare-http1": {
483 "version": "2.0.5",
484 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-2.0.5.tgz",
485 "integrity": "sha512-MCaGha2DceFRGAOjrHV6zNYLa71kFR6KHpoc8K2T6ukeL2j7yjbsdFziQIF2mkMjz7RF75sfu1+rtgHEeIBenw==",
486 "license": "Apache-2.0",
487 "dependencies": {
488 "bare-events": "^2.0.0",
489 "streamx": "^2.13.0"
490 }
491 },
492357 "node_modules/bare-https": {
493358 "version": "2.1.3",
494359 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.3.tgz",
495360 "integrity": "sha512-0TI/mJXQGYXmG7UUyWEG+KCJusayIAQLywUjFAskDoKuxfqVnGF+M/mTMrEV8J64DaIdU+5x761FvgmT7M68tA==",
361 "dev": true,
496362 "license": "Apache-2.0",
497363 "dependencies": {
498364 "bare-http1": "^4.4.0",
……@@ -504,6 +370,7 @@
504370 "version": "4.5.5",
505371 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.5.tgz",
506372 "integrity": "sha512-ADITiRo0huP76JGMbv6Arsh9KehHqjEBoYcmjvAo67IY78+/9mV2MeKLLCkiJSFxA85T/m8cTaE44OwJQCcwdw==",
373 "dev": true,
507374 "license": "Apache-2.0",
508375 "dependencies": {
509376 "bare-events": "^2.6.0",
……@@ -587,19 +454,11 @@
587454 }
588455 }
589456 },
590 "node_modules/bare-logger": {
591 "version": "2.0.3",
592 "resolved": "https://registry.npmjs.org/bare-logger/-/bare-logger-2.0.3.tgz",
593 "integrity": "sha512-U6K7NxHdeAHxEgHFDV8zXvgjgDZKQO6LlCrxQvUvlrZEVTnoezSImMWizi85rbZpVsbeI1ZWcLCRGnoIf5EV8Q==",
594 "license": "Apache-2.0",
595 "dependencies": {
596 "bare-format": "^1.0.0"
597 }
598 },
599457 "node_modules/bare-module": {
600458 "version": "6.1.3",
601459 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
602460 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
461 "dev": true,
603462 "license": "Apache-2.0",
604463 "dependencies": {
605464 "bare-bundle": "^1.3.0",
……@@ -624,6 +483,7 @@
624483 "version": "1.4.7",
625484 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
626485 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
486 "dev": true,
627487 "license": "Apache-2.0",
628488 "dependencies": {
629489 "require-addon": "^1.0.2"
……@@ -654,33 +514,11 @@
654514 }
655515 }
656516 },
657 "node_modules/bare-module-traverse": {
658 "version": "2.0.1",
659 "resolved": "https://registry.npmjs.org/bare-module-traverse/-/bare-module-traverse-2.0.1.tgz",
660 "integrity": "sha512-1au+Og5p97T9b6Y7xmHZ7KtpW8vEYtz2jC2whmm+YJp46EaHfk26j91MmQhufdkR/8sdK1Q5p+P9A/Y5GrJg7Q==",
661 "license": "Apache-2.0",
662 "dependencies": {
663 "bare-addon-resolve": "^1.5.0",
664 "bare-module-lexer": "^1.4.0",
665 "bare-module-resolve": "^1.7.0"
666 },
667 "peerDependencies": {
668 "bare-buffer": "*",
669 "bare-url": "*"
670 },
671 "peerDependenciesMeta": {
672 "bare-buffer": {
673 "optional": true
674 },
675 "bare-url": {
676 "optional": true
677 }
678 }
679 },
680517 "node_modules/bare-net": {
681518 "version": "2.3.1",
682519 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz",
683520 "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==",
521 "dev": true,
684522 "license": "Apache-2.0",
685523 "dependencies": {
686524 "bare-events": "^2.2.2",
……@@ -689,104 +527,11 @@
689527 "bare-tcp": "^2.0.0"
690528 }
691529 },
692 "node_modules/bare-node-runtime": {
693 "version": "1.2.0",
694 "resolved": "https://registry.npmjs.org/bare-node-runtime/-/bare-node-runtime-1.2.0.tgz",
695 "integrity": "sha512-oCpkcKBLDuzhtga0Q/63Ds3Gj03I0jIf+VX4mDhSClpoBfz/wnDtl2d1NKQDAVNCoP4I617GrJkS3vwNO4dd7w==",
696 "license": "Apache-2.0",
697 "dependencies": {
698 "bare-abort-controller": "^1.0.0",
699 "bare-assert": "^1.1.0",
700 "bare-async-hooks": "^0.0.0",
701 "bare-buffer": "^3.3.1",
702 "bare-console": "^6.0.1",
703 "bare-crypto": "^1.11.2",
704 "bare-dgram": "^1.0.1",
705 "bare-diagnostics-channel": "^1.1.0",
706 "bare-dns": "^2.1.4",
707 "bare-events": "^2.7.0",
708 "bare-fetch": "^2.5.0",
709 "bare-fs": "^4.2.3",
710 "bare-http1": "^4.0.4",
711 "bare-https": "^2.0.0",
712 "bare-inspector": "^5.0.0",
713 "bare-module": "^6.1.2",
714 "bare-net": "^2.0.2",
715 "bare-os": "^3.6.2",
716 "bare-path": "^3.0.0",
717 "bare-performance": "^1.1.0",
718 "bare-process": "^4.2.1",
719 "bare-punycode": "^0.0.0",
720 "bare-querystring": "^1.0.0",
721 "bare-readline": "^1.1.0",
722 "bare-repl": "^6.0.1",
723 "bare-stream": "^2.7.0",
724 "bare-string-decoder": "^1.0.0",
725 "bare-subprocess": "^5.1.1",
726 "bare-timers": "^3.0.3",
727 "bare-tls": "^2.1.3",
728 "bare-tty": "^5.0.3",
729 "bare-url": "^2.2.2",
730 "bare-utils": "^1.5.1",
731 "bare-v8": "^1.0.1",
732 "bare-vm": "^1.0.0",
733 "bare-worker": "^4.0.0",
734 "bare-ws": "^2.1.0",
735 "bare-zlib": "^1.3.1"
736 }
737 },
738 "node_modules/bare-node-runtime/node_modules/bare-http1": {
739 "version": "4.5.5",
740 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.5.tgz",
741 "integrity": "sha512-ADITiRo0huP76JGMbv6Arsh9KehHqjEBoYcmjvAo67IY78+/9mV2MeKLLCkiJSFxA85T/m8cTaE44OwJQCcwdw==",
742 "license": "Apache-2.0",
743 "dependencies": {
744 "bare-events": "^2.6.0",
745 "bare-http-parser": "^1.1.1",
746 "bare-stream": "^2.10.0",
747 "bare-tcp": "^2.2.0"
748 },
749 "peerDependencies": {
750 "bare-buffer": "*",
751 "bare-url": "*"
752 },
753 "peerDependenciesMeta": {
754 "bare-buffer": {
755 "optional": true
756 },
757 "bare-url": {
758 "optional": true
759 }
760 }
761 },
762 "node_modules/bare-node-runtime/node_modules/bare-inspector": {
763 "version": "5.0.0",
764 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-5.0.0.tgz",
765 "integrity": "sha512-blObRFPBK6yn0/wZbrRgkGcOuLUQUYVvkKbsEp5p3bWJB+uNrAh+qUqOijQHS6mZjGvyGCE86VERp3EiUR2TyA==",
766 "license": "Apache-2.0",
767 "dependencies": {
768 "bare-events": "^2.1.0",
769 "bare-http1": "^4.0.0",
770 "bare-stream": "^2.0.0",
771 "bare-url": "^2.0.0",
772 "bare-ws": "^2.0.0"
773 },
774 "engines": {
775 "bare": ">=1.2.0"
776 },
777 "peerDependencies": {
778 "bare-tcp": "*"
779 },
780 "peerDependenciesMeta": {
781 "bare-tcp": {
782 "optional": true
783 }
784 }
785 },
786530 "node_modules/bare-os": {
787531 "version": "3.8.7",
788532 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz",
789533 "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==",
534 "devOptional": true,
790535 "license": "Apache-2.0",
791536 "engines": {
792537 "bare": ">=1.14.0"
……@@ -796,21 +541,17 @@
796541 "version": "3.0.0",
797542 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
798543 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
544 "devOptional": true,
799545 "license": "Apache-2.0",
800546 "dependencies": {
801547 "bare-os": "^3.0.1"
802548 }
803549 },
804 "node_modules/bare-performance": {
805 "version": "1.3.0",
806 "resolved": "https://registry.npmjs.org/bare-performance/-/bare-performance-1.3.0.tgz",
807 "integrity": "sha512-ITlHSQwsnJSPjpagTtPo043LRkFgcrl9SgPsVUBXZvTeGWuNLUfMRwmwAS+35i8qnIEinySoLxwhVoJNZAIF2g==",
808 "license": "Apache-2.0"
809 },
810550 "node_modules/bare-pipe": {
811551 "version": "4.1.5",
812552 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz",
813553 "integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==",
554 "dev": true,
814555 "license": "Apache-2.0",
815556 "dependencies": {
816557 "bare-events": "^2.0.0",
……@@ -824,6 +565,7 @@
824565 "version": "4.4.1",
825566 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.4.1.tgz",
826567 "integrity": "sha512-JfcTtymq6akqM2bdyTsP4A4GYJceBzb91k/ECmFps75uFf6uO33SM1bOZsRAqyRXExabsQJLn5S41NBl8HTM4A==",
568 "dev": true,
827569 "license": "Apache-2.0",
828570 "dependencies": {
829571 "bare-abort": "^2.0.13",
……@@ -835,56 +577,6 @@
835577 "bare-stdio": "^1.0.1"
836578 }
837579 },
838 "node_modules/bare-punycode": {
839 "version": "0.0.0",
840 "resolved": "https://registry.npmjs.org/bare-punycode/-/bare-punycode-0.0.0.tgz",
841 "integrity": "sha512-PC2Y6mGLytZPJCB9M7CvO5Zb6uWYVUZ+5t3L6vePFuFM6tdC6SsQ+sIsuf0Sa6LHBoWURX7I9yMMbPXMv7TIdQ==",
842 "license": "Apache-2.0",
843 "dependencies": {
844 "punycode": "^2.3.1"
845 }
846 },
847 "node_modules/bare-querystring": {
848 "version": "1.0.0",
849 "resolved": "https://registry.npmjs.org/bare-querystring/-/bare-querystring-1.0.0.tgz",
850 "integrity": "sha512-Kbqdjd4f1MxMtutXzraEKIXqXRfq9uWLwzUT5bLUh9TCiPznY9l6vHc4BUPNIa13DyH1VAlPy3iOxHzhqV2quQ==",
851 "license": "Apache-2.0"
852 },
853 "node_modules/bare-readline": {
854 "version": "1.3.1",
855 "resolved": "https://registry.npmjs.org/bare-readline/-/bare-readline-1.3.1.tgz",
856 "integrity": "sha512-QtSU4ZfgQcDI6AQssjDFqTiRe4rCiciMn+Yqx6siMJZBIftAIFrNSOK0sa5SBrmNv/a7CD9Dm0onUDCRyn5Fdg==",
857 "license": "Apache-2.0",
858 "dependencies": {
859 "bare-ansi-escapes": "^2.0.0",
860 "bare-stream": "^2.0.0"
861 }
862 },
863 "node_modules/bare-realm": {
864 "version": "2.0.1",
865 "resolved": "https://registry.npmjs.org/bare-realm/-/bare-realm-2.0.1.tgz",
866 "integrity": "sha512-kQbYU6AAQu4XBTQesuz2+PpjBx7MJzf5Qw3nSLzQCzbVglx7Ml85MtWEjUe1AeslXqrd+nFPHMEbL55ouISscA==",
867 "license": "Apache-2.0",
868 "engines": {
869 "bare": ">=1.5.0"
870 }
871 },
872 "node_modules/bare-repl": {
873 "version": "6.0.2",
874 "resolved": "https://registry.npmjs.org/bare-repl/-/bare-repl-6.0.2.tgz",
875 "integrity": "sha512-w2R+5LOrMl9UH/gcFjvdIpsB+SgyLr8cW4uJo5bAEzRbRpVA+ZTm8pMHYdj0uqIUBClP/6fkyW0MnB1XkxakmQ==",
876 "license": "Apache-2.0",
877 "dependencies": {
878 "bare-inspect": "^3.0.0",
879 "bare-module": "^6.0.1",
880 "bare-os": "^3.0.1",
881 "bare-path": "^3.0.0",
882 "bare-pipe": "^4.0.0",
883 "bare-readline": "^1.0.0",
884 "bare-stream": "^2.0.0",
885 "bare-tty": "^5.0.0"
886 }
887 },
888580 "node_modules/bare-semver": {
889581 "version": "1.0.2",
890582 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
……@@ -895,6 +587,7 @@
895587 "version": "4.2.0",
896588 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
897589 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
590 "dev": true,
898591 "license": "Apache-2.0",
899592 "dependencies": {
900593 "bare-events": "^2.5.3",
……@@ -908,6 +601,7 @@
908601 "version": "1.0.2",
909602 "resolved": "https://registry.npmjs.org/bare-stdio/-/bare-stdio-1.0.2.tgz",
910603 "integrity": "sha512-3WJDqtvVGP4f+j68kyEC05umOYNwKJ1xG+YAXL8yZ605WgNqiRhVaFq+mVIhBt2eKNp7pa5vCQdhOt1pNh79SA==",
604 "dev": true,
911605 "license": "Apache-2.0",
912606 "dependencies": {
913607 "bare-fs": "^4.5.2",
……@@ -941,52 +635,11 @@
941635 }
942636 }
943637 },
944 "node_modules/bare-string-decoder": {
945 "version": "1.0.0",
946 "resolved": "https://registry.npmjs.org/bare-string-decoder/-/bare-string-decoder-1.0.0.tgz",
947 "integrity": "sha512-FjFvfHo88U7borNQSj9ijP2JTb1asqY6K28OZrix4dF9iZf5D2wi1+99CgLlHT3HtYqdwxRhDAiKu8rVstt5rQ==",
948 "license": "Apache-2.0",
949 "dependencies": {
950 "text-decoder": "^1.2.3"
951 },
952 "peerDependencies": {
953 "bare-buffer": "*"
954 },
955 "peerDependenciesMeta": {
956 "bare-buffer": {
957 "optional": true
958 }
959 }
960 },
961 "node_modules/bare-structured-clone": {
962 "version": "1.5.3",
963 "resolved": "https://registry.npmjs.org/bare-structured-clone/-/bare-structured-clone-1.5.3.tgz",
964 "integrity": "sha512-vC/YqGsp67ZeFnpyAskwaEIXtNscnCwFVKlSk0Oh2X3AqWT6H+7DC9vVI080FohUJeuUuB9UNx4UFdDcyQgZaw==",
965 "license": "Apache-2.0",
966 "dependencies": {
967 "bare-type": "^1.1.0",
968 "compact-encoding": "^2.15.0"
969 },
970 "engines": {
971 "bare": ">=1.2.0"
972 },
973 "peerDependencies": {
974 "bare-buffer": "*",
975 "bare-url": "*"
976 },
977 "peerDependenciesMeta": {
978 "bare-buffer": {
979 "optional": true
980 },
981 "bare-url": {
982 "optional": true
983 }
984 }
985 },
986638 "node_modules/bare-stylize": {
987639 "version": "0.0.1",
988640 "resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz",
989641 "integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==",
642 "dev": true,
990643 "license": "Apache-2.0",
991644 "dependencies": {
992645 "bare-ansi-escapes": "^2.2.3",
……@@ -997,6 +650,7 @@
997650 "version": "5.2.3",
998651 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
999652 "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
653 "dev": true,
1000654 "license": "Apache-2.0",
1001655 "dependencies": {
1002656 "bare-env": "^3.0.0",
……@@ -1017,19 +671,11 @@
1017671 }
1018672 }
1019673 },
1020 "node_modules/bare-system-logger": {
1021 "version": "1.0.3",
1022 "resolved": "https://registry.npmjs.org/bare-system-logger/-/bare-system-logger-1.0.3.tgz",
1023 "integrity": "sha512-HXTXZyhIIS4ZDYun4J9zKNQZDfLgEkkK8wy9nrTFJE5xq8APsOhDxuEsxP0YcNwC23DW1jaNpqD51zDeXPzIdg==",
1024 "license": "Apache-2.0",
1025 "dependencies": {
1026 "bare-logger": "^2.0.0"
1027 }
1028 },
1029674 "node_modules/bare-tcp": {
1030675 "version": "2.2.7",
1031676 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.7.tgz",
1032677 "integrity": "sha512-rjpqNQ2cOCkNo3NeYA/W4GTK3DRkl8sDHO3uos+AEswUjLC8XXMQF8WrJCSjlIowCbS6NUVxKE92X5RGXjyefg==",
678 "dev": true,
1033679 "license": "Apache-2.0",
1034680 "dependencies": {
1035681 "bare-dns": "^2.0.4",
……@@ -1040,38 +686,11 @@
1040686 "bare": ">=1.16.0"
1041687 }
1042688 },
1043 "node_modules/bare-thread": {
1044 "version": "1.2.0",
1045 "resolved": "https://registry.npmjs.org/bare-thread/-/bare-thread-1.2.0.tgz",
1046 "integrity": "sha512-evYpeDqaTROp2JIdVgWrIBc5rVCV64tFMwZdeL13ahYt/lno2RLvTbYrcf1p6NA2p1SxybbSbGQ2F9/gHudrTg==",
1047 "license": "Apache-2.0",
1048 "dependencies": {
1049 "bare-bundle": "^1.9.0",
1050 "bare-module-resolve": "^1.11.2",
1051 "bare-module-traverse": "^2.0.0"
1052 }
1053 },
1054 "node_modules/bare-timers": {
1055 "version": "3.2.1",
1056 "resolved": "https://registry.npmjs.org/bare-timers/-/bare-timers-3.2.1.tgz",
1057 "integrity": "sha512-O+9e6Jol/BoYsQUzAFXG0gWUW1GWryFMblGcNBfi3smPrD3rJIKQwpw2FJARl2j/1VHne4a8YaHPK1cxDKfiYQ==",
1058 "license": "Apache-2.0",
1059 "engines": {
1060 "bare": ">=1.7.0"
1061 },
1062 "peerDependencies": {
1063 "bare-abort-controller": "*"
1064 },
1065 "peerDependenciesMeta": {
1066 "bare-abort-controller": {
1067 "optional": true
1068 }
1069 }
1070 },
1071689 "node_modules/bare-tls": {
1072690 "version": "2.2.1",
1073691 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.2.1.tgz",
1074692 "integrity": "sha512-hZ+ZqwrUO4dyH7/6WYkYWjgAFNJKjzwEYJiDaMnMs+eRleBDjQ3CvNZawpkw0Ar9jnM9NZK6+f6GqjkZ2FLGmQ==",
693 "dev": true,
1075694 "license": "Apache-2.0",
1076695 "dependencies": {
1077696 "bare-net": "^2.0.1",
……@@ -1085,6 +704,7 @@
1085704 "version": "5.1.0",
1086705 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.1.0.tgz",
1087706 "integrity": "sha512-EZLvW4A+XiJgI3TW+e1pMME9PIJsfEXe/DA/WSKzIkq/v7Yarpv/rvG6Z5pGnpo4V/Bd+qopwnCLSR71hMMBYA==",
707 "dev": true,
1088708 "license": "Apache-2.0",
1089709 "dependencies": {
1090710 "bare-events": "^2.2.0",
……@@ -1108,6 +728,7 @@
1108728 "version": "2.4.0",
1109729 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
1110730 "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
731 "devOptional": true,
1111732 "license": "Apache-2.0",
1112733 "peer": true,
1113734 "dependencies": {
……@@ -1118,6 +739,7 @@
1118739 "version": "1.6.0",
1119740 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz",
1120741 "integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==",
742 "dev": true,
1121743 "license": "Apache-2.0",
1122744 "dependencies": {
1123745 "bare-debug-log": "^2.0.0",
……@@ -1128,12 +750,6 @@
1128750 "bare-type": "^1.0.6"
1129751 }
1130752 },
1131 "node_modules/bare-v8": {
1132 "version": "1.0.1",
1133 "resolved": "https://registry.npmjs.org/bare-v8/-/bare-v8-1.0.1.tgz",
1134 "integrity": "sha512-/cR5ZvFWQRdtTZ4tx0j7TKvTWce8UnnLqm88fwHtJmfM7HODIBVjQGDT7KkDLeD2d/eHP2pzB71Y8/QyiMMKrQ==",
1135 "license": "Apache-2.0"
1136 },
1137753 "node_modules/bare-v8-to-istanbul": {
1138754 "version": "1.0.2",
1139755 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
……@@ -1152,31 +768,11 @@
1152768 "which-runtime": "^1.2.1"
1153769 }
1154770 },
1155 "node_modules/bare-vm": {
1156 "version": "1.0.1",
1157 "resolved": "https://registry.npmjs.org/bare-vm/-/bare-vm-1.0.1.tgz",
1158 "integrity": "sha512-yLnbRvKt3AhRTmtfTIrYfdTHqGEfIJc+Fgb2DcHejE0HJ+p5adGxxPMvd3893Z7iXVYnalxukNARn4oJSZELHQ==",
1159 "license": "Apache-2.0",
1160 "dependencies": {
1161 "bare-realm": "^2.0.0"
1162 }
1163 },
1164 "node_modules/bare-worker": {
1165 "version": "4.1.6",
1166 "resolved": "https://registry.npmjs.org/bare-worker/-/bare-worker-4.1.6.tgz",
1167 "integrity": "sha512-yvsektF7xNqlAyjJV+YYeBx+sGZ8Fhbo5Cjc/lpIdkM8Gd/e3MlaxTFg8NknkyvJ4oDJXqvp38Q+ZOn5Gyr+nQ==",
1168 "license": "Apache-2.0",
1169 "dependencies": {
1170 "bare-channel": "^5.1.5",
1171 "bare-events": "^2.2.1",
1172 "bare-module": "^6.0.1",
1173 "bare-thread": "^1.1.3"
1174 }
1175 },
1176771 "node_modules/bare-ws": {
1177772 "version": "2.1.0",
1178773 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.1.0.tgz",
1179774 "integrity": "sha512-2gEWlPK9iyBchACdIY6oQXgmDz3KLrChdwrPgmU3IVXOFTnxTqSUT27WE/+izd4QojHj/SsqDkQiD2HDvuTdAA==",
775 "dev": true,
1180776 "license": "Apache-2.0",
1181777 "dependencies": {
1182778 "bare-crypto": "^1.2.0",
……@@ -1202,6 +798,7 @@
1202798 "version": "4.5.5",
1203799 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.5.tgz",
1204800 "integrity": "sha512-ADITiRo0huP76JGMbv6Arsh9KehHqjEBoYcmjvAo67IY78+/9mV2MeKLLCkiJSFxA85T/m8cTaE44OwJQCcwdw==",
801 "dev": true,
1205802 "license": "Apache-2.0",
1206803 "dependencies": {
1207804 "bare-events": "^2.6.0",
……@@ -1222,15 +819,6 @@
1222819 }
1223820 }
1224821 },
1225 "node_modules/bare-zlib": {
1226 "version": "1.3.3",
1227 "resolved": "https://registry.npmjs.org/bare-zlib/-/bare-zlib-1.3.3.tgz",
1228 "integrity": "sha512-rXNczo+SQg6cn20olmh/mUiGeJK9maipFH/zI/QwYgwhEmOns1R7fl1GV5apNO+aAp4x2d4uUa7HLhO4mhOnBQ==",
1229 "license": "Apache-2.0",
1230 "dependencies": {
1231 "bare-stream": "^2.0.0"
1232 }
1233 },
1234822 "node_modules/big-sparse-array": {
1235823 "version": "1.0.3",
1236824 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
……@@ -1475,10 +1063,6 @@
14751063 "picomatch": "^4.0.2"
14761064 }
14771065 },
1478 "node_modules/hyper-overlay": {
1479 "resolved": "../hyper-overlay",
1480 "link": true
1481 },
14821066 "node_modules/hypercore": {
14831067 "version": "11.28.1",
14841068 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-11.28.1.tgz",
……@@ -1722,15 +1306,6 @@
17221306 "unslab": "^1.3.0"
17231307 }
17241308 },
1725 "node_modules/punycode": {
1726 "version": "2.3.1",
1727 "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
1728 "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
1729 "license": "MIT",
1730 "engines": {
1731 "node": ">=6"
1732 }
1733 },
17341309 "node_modules/queue-tick": {
17351310 "version": "1.0.1",
17361311 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
package.json +8 -14
……@@ -1,33 +1,27 @@
11{
22 "name": "hyper-deploy",
3 "version": "0.1.0",
3 "version": "0.2.0",
44 "description": "P2P release distribution — serve build artifacts to peers over Hyperswarm",
5 "type": "module",
65 "main": "index.js",
7 "bin": {
8 "hyper-deploy": "./bin/hyper-deploy.js"
6 "imports": {
7 "fs": { "bare": "bare-fs", "default": "fs" },
8 "path": { "bare": "bare-path", "default": "path" },
9 "os": { "bare": "bare-os", "default": "os" }
910 },
1011 "scripts": {
1112 "test": "brittle test/*.test.js"
1213 },
1314 "dependencies": {
14 "@hyperswarm/secret-stream": "^6.0.0",
15 "bare-fs": "^4.0.0",
16 "bare-http1": "^2.0.0",
17 "bare-node-runtime": "^1.2.0",
18 "bare-os": "^3.0.0",
19 "bare-path": "^3.0.0",
2015 "compact-encoding": "^2.0.0",
2116 "hypercore-crypto": "^3.0.0",
2217 "hyperswarm": "^4.0.0",
2318 "protomux": "^3.0.0",
2419 "ready-resource": "^1.0.0",
25 "safety-catch": "^1.0.3",
26 "udx-native": "^1.20.3"
20 "safety-catch": "^1.0.0"
2721 },
2822 "devDependencies": {
23 "@hyperswarm/secret-stream": "^6.0.0",
2924 "brittle": "^3.0.0",
30 "corestore": "^7.0.0",
31 "hyper-overlay": "file:../hyper-overlay"
25 "corestore": "^7.0.0"
3226 }
3327}
test/basic.test.js +200 -0
……@@ -0,0 +1,200 @@
1const test = require('brittle');
2const fs = require('fs');
3const path = require('path');
4const os = require('os');
5const crypto = require('hypercore-crypto');
6const SecretStream = require('@hyperswarm/secret-stream');
7const { DeployServer, DeployClient } = require('../');
8const { buildManifest, diffManifest, hashFile } = require('../lib/scanner');
9
10const tmpdir = os.tmpdir();
11
12function mktemp (prefix) {
13 const dir = path.join(tmpdir, 'hyper-deploy-test-' + prefix + '-' + Date.now());
14 fs.mkdirSync(dir, { recursive: true });
15 return dir;
16}
17
18// ── Scanner ─────────────────────────────────────────────────
19
20test('buildManifest scans directory', async (t) => {
21 const dir = mktemp('scan');
22 fs.writeFileSync(path.join(dir, 'a.txt'), 'hello');
23 fs.writeFileSync(path.join(dir, 'b.txt'), 'world');
24 fs.mkdirSync(path.join(dir, 'sub'));
25 fs.writeFileSync(path.join(dir, 'sub', 'c.txt'), 'nested');
26
27 const manifest = buildManifest(dir);
28 t.is(manifest.files.length, 3, '3 files');
29 t.ok(manifest.totalSize > 0, 'has size');
30 t.ok(manifest.files[0].hash, 'has hash');
31 t.ok(manifest.files[0].path, 'has path');
32
33 fs.rmSync(dir, { recursive: true });
34});
35
36test('diffManifest detects changes', async (t) => {
37 const dir = mktemp('diff');
38 fs.writeFileSync(path.join(dir, 'same.txt'), 'unchanged');
39 fs.writeFileSync(path.join(dir, 'changed.txt'), 'original');
40
41 const manifest = buildManifest(dir);
42
43 // Modify one file
44 fs.writeFileSync(path.join(dir, 'changed.txt'), 'modified');
45
46 const { needed, skipped } = diffManifest(manifest.files, dir);
47 t.is(skipped, 1, '1 unchanged');
48 t.is(needed.length, 1, '1 changed');
49 t.is(needed[0], 'changed.txt');
50
51 fs.rmSync(dir, { recursive: true });
52});
53
54test('diffManifest handles missing files', async (t) => {
55 const manifest = { files: [{ path: 'nope.txt', size: 5, hash: 'abc' }] };
56 const { needed, skipped } = diffManifest(manifest.files, '/nonexistent');
57 t.is(needed.length, 1);
58 t.is(skipped, 0);
59});
60
61// ── DeployServer ────────────────────────────────────────────
62
63test('DeployServer lifecycle', async (t) => {
64 const dir = mktemp('srv');
65 fs.writeFileSync(path.join(dir, 'test.txt'), 'hello');
66
67 const server = new DeployServer(dir);
68 await server.ready();
69
70 t.ok(server.manifest, 'has manifest');
71 t.is(server.manifest.files.length, 1);
72 t.ok(server.topicHex, 'has topic');
73 t.is(server.connections, 0);
74
75 await server.close();
76 fs.rmSync(dir, { recursive: true });
77});
78
79test('DeployServer rescan', async (t) => {
80 const dir = mktemp('rescan');
81 fs.writeFileSync(path.join(dir, 'a.txt'), 'first');
82
83 const server = new DeployServer(dir);
84 await server.ready();
85 t.is(server.manifest.files.length, 1);
86
87 fs.writeFileSync(path.join(dir, 'b.txt'), 'second');
88 await server.rescan();
89 t.is(server.manifest.files.length, 2);
90
91 await server.close();
92 fs.rmSync(dir, { recursive: true });
93});
94
95test('DeployServer rejects missing directory', async (t) => {
96 const server = new DeployServer('/nonexistent/path/xyz');
97 try {
98 await server.ready();
99 t.fail('should have thrown');
100 } catch (err) {
101 t.ok(err.message.includes('not found'), err.message);
102 }
103});
104
105// ── DeployClient ────────────────────────────────────────────
106
107test('DeployClient timeout on no connection', async (t) => {
108 const client = new DeployClient(crypto.randomBytes(32), mktemp('timeout'), {
109 timeout: 500
110 });
111 await client.ready();
112
113 try {
114 await client.pull();
115 t.fail('should have timed out');
116 } catch (err) {
117 t.ok(err.message.includes('timeout'), err.message);
118 }
119
120 await client.close();
121});
122
123// ── Integration ─────────────────────────────────────────────
124
125test('DeployServer + DeployClient full transfer', async (t) => {
126 t.timeout(15000);
127
128 const srcDir = mktemp('int-src');
129 for (let i = 0; i < 5; i++) {
130 fs.writeFileSync(path.join(srcDir, 'file_' + i + '.txt'), 'content ' + i);
131 }
132
133 const dstDir = mktemp('int-dst');
134
135 // Separate swarms — same process can't self-connect on one swarm
136 const Hyperswarm = require('hyperswarm');
137 const topic = crypto.randomBytes(32);
138 const swarmA = new Hyperswarm();
139 const swarmB = new Hyperswarm();
140
141 const server = new DeployServer(srcDir, { topic, swarm: swarmA });
142 const client = new DeployClient(topic, dstDir, { swarm: swarmB, timeout: 10000 });
143
144 await server.ready();
145 await client.ready();
146
147 const result = await client.pull();
148
149 t.is(result.filesTransferred, 5, '5 files transferred');
150 t.is(result.filesSkipped, 0, 'none skipped');
151 t.ok(result.bytesReceived > 0, 'received bytes');
152 t.is(result.errors.length, 0, 'no errors');
153
154 for (let i = 0; i < 5; i++) {
155 const srcHash = hashFile(path.join(srcDir, 'file_' + i + '.txt'));
156 const dstHash = hashFile(path.join(dstDir, 'file_' + i + '.txt'));
157 t.is(srcHash, dstHash, 'file_' + i + '.txt matches');
158 }
159
160 await client.close();
161 await server.close();
162 await swarmA.destroy();
163 await swarmB.destroy();
164 fs.rmSync(srcDir, { recursive: true });
165 fs.rmSync(dstDir, { recursive: true });
166});
167
168test('DeployClient file-level dedup', async (t) => {
169 t.timeout(15000);
170
171 const srcDir = mktemp('dup-src');
172 fs.writeFileSync(path.join(srcDir, 'a.txt'), 'unchanged');
173 fs.writeFileSync(path.join(srcDir, 'b.txt'), 'will change');
174
175 const dstDir = mktemp('dup-dst');
176 fs.writeFileSync(path.join(dstDir, 'a.txt'), 'unchanged');
177
178 const Hyperswarm = require('hyperswarm');
179 const topic = crypto.randomBytes(32);
180 const swarmA = new Hyperswarm();
181 const swarmB = new Hyperswarm();
182
183 const server = new DeployServer(srcDir, { topic, swarm: swarmA });
184 const client = new DeployClient(topic, dstDir, { swarm: swarmB, timeout: 10000 });
185
186 await server.ready();
187 await client.ready();
188
189 const result = await client.pull();
190
191 t.is(result.filesTransferred, 1, '1 transferred');
192 t.is(result.filesSkipped, 1, '1 skipped');
193
194 await client.close();
195 await server.close();
196 await swarmA.destroy();
197 await swarmB.destroy();
198 fs.rmSync(srcDir, { recursive: true });
199 fs.rmSync(dstDir, { recursive: true });
200});