@leofcoin/peernet 2.0.0 → 3.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/README.md +45 -0
- package/docs/automatic-transport.md +37 -0
- package/exports/browser/{client-CAC7RVMf.js → client-DKKzPSlb.js} +252 -76
- package/exports/browser/{messages-Dw86z0zq.js → messages-DCePyqKC.js} +1 -1
- package/exports/browser/{peernet-B2jRhqGI.js → peernet-DjddqiXc.js} +460 -18
- package/exports/browser/peernet.js +1 -1
- package/exports/peernet.js +445 -3
- package/exports/types/archive.d.ts +69 -0
- package/exports/types/peernet.d.ts +7 -1
- package/package.json +9 -5
- package/.esdoc.json +0 -10
- package/.gitattributes +0 -2
- package/.github/workflows/release.yml +0 -34
- package/.prettierrc +0 -7
- package/.travis.yml +0 -27
- package/BREAKING_CHANGES.md +0 -35
- package/deploy.js +0 -8
- package/exports/browser/browser-D-r0O9Qn.js +0 -53
- package/exports/browser/browser-DvU1xNFS.js +0 -147
- package/exports/browser/browser-_hiyXwPp.js +0 -147
- package/exports/browser/client-86Tnax66.js +0 -2224
- package/exports/browser/client-BFxJts6j.js +0 -2224
- package/exports/browser/client-BVhUamQG.js +0 -2200
- package/exports/browser/client-BzvWzzLv.js +0 -2224
- package/exports/browser/client-C9GBz2Tf.js +0 -2224
- package/exports/browser/client-CQzuu6At.js +0 -2224
- package/exports/browser/client-CVkFejdx.js +0 -2224
- package/exports/browser/client-Cj_UUO8W.js +0 -2224
- package/exports/browser/client-CrGaeBjj.js +0 -2224
- package/exports/browser/client-DDC8C510.js +0 -2224
- package/exports/browser/client-ERocgKET.js +0 -2224
- package/exports/browser/client-a8rws3cu.js +0 -2224
- package/exports/browser/identity-CYVPDmkf.js +0 -8402
- package/exports/browser/identity-ChyyZSve.js +0 -8397
- package/exports/browser/index-BD0Anx7_.js +0 -37
- package/exports/browser/index-Bxr5Iztg.js +0 -37
- package/exports/browser/messages-BFCmg6Vb.js +0 -212
- package/exports/browser/messages-BHIC2mQu.js +0 -212
- package/exports/browser/messages-CimzJ4wr.js +0 -210
- package/exports/browser/messages-Cjoa_LGO.js +0 -210
- package/exports/browser/messages-D7JxkkDW.js +0 -212
- package/exports/browser/messages-DH0tgNM9.js +0 -207
- package/exports/browser/messages-DVHcNvs_.js +0 -212
- package/exports/browser/messages-DqL_g2qw.js +0 -210
- package/exports/browser/messages-UKnuelZ7.js +0 -207
- package/exports/browser/messages-XgZZUMma.js +0 -212
- package/exports/browser/messages-kfFyctPL.js +0 -210
- package/exports/browser/messages-psROnfRi.js +0 -212
- package/exports/browser/peernet-B9VL4Xwe.js +0 -9522
- package/exports/browser/peernet-C-HNdrcd.js +0 -9528
- package/exports/browser/peernet-C4rOvyyU.js +0 -9661
- package/exports/browser/peernet-Ca1iM2tN.js +0 -9336
- package/exports/browser/peernet-CbI0-jFn.js +0 -9651
- package/exports/browser/peernet-CtHScDyh.js +0 -9524
- package/exports/browser/peernet-DfuxUK3u.js +0 -9525
- package/exports/browser/peernet-DiEcNDLD.js +0 -9655
- package/exports/browser/peernet-DtuRXOzP.js +0 -9653
- package/exports/browser/peernet-r_9IyzTe.js +0 -9336
- package/exports/browser/peernet-rcD5XL1y.js +0 -9590
- package/exports/browser/peernet-rw8HJGOz.js +0 -9597
- package/exports/messages-CHKVVTVE.js +0 -204
- package/exports/messages-DaUtFfBh.js +0 -207
- package/index.html +0 -19
- package/rollup.config.js +0 -143
- package/src/dht/dht.ts +0 -142
- package/src/discovery/peer-discovery.js +0 -75
- package/src/errors/errors.js +0 -12
- package/src/file-transfer.ts +0 -194
- package/src/handlers/data.js +0 -15
- package/src/handlers/message.js +0 -34
- package/src/identity.ts +0 -172
- package/src/messages/chat.ts +0 -13
- package/src/messages/data-response.ts +0 -13
- package/src/messages/data.ts +0 -17
- package/src/messages/dht-response.ts +0 -13
- package/src/messages/dht.ts +0 -21
- package/src/messages/file-link.ts +0 -17
- package/src/messages/file.ts +0 -17
- package/src/messages/peer-response.ts +0 -13
- package/src/messages/peer.ts +0 -13
- package/src/messages/peernet.ts +0 -13
- package/src/messages/ps.ts +0 -13
- package/src/messages/request.ts +0 -13
- package/src/messages/response.ts +0 -13
- package/src/messages.js +0 -12
- package/src/peer-info.js +0 -9
- package/src/peernet.ts +0 -1113
- package/src/prompts/password/browser.js +0 -1
- package/src/prompts/password/node.js +0 -6
- package/src/prompts/password.js +0 -1
- package/src/proto/chat-message.proto.js +0 -6
- package/src/proto/data-response.proto.js +0 -4
- package/src/proto/data.proto.js +0 -4
- package/src/proto/dht-response.proto.js +0 -4
- package/src/proto/dht.proto.js +0 -4
- package/src/proto/file-link.proto.js +0 -5
- package/src/proto/file.proto.js +0 -10
- package/src/proto/peer-response.proto.js +0 -3
- package/src/proto/peer.proto.js +0 -3
- package/src/proto/peernet.proto.js +0 -7
- package/src/proto/ps.proto.js +0 -4
- package/src/proto/request.proto.js +0 -4
- package/src/proto/response.proto.js +0 -3
- package/src/types.ts +0 -25
- package/src/utils/utils.js +0 -77
- package/test/all.js +0 -5
- package/test/client.js +0 -15
- package/test/codec.js +0 -56
- package/test/file-transfer.test.js +0 -81
- package/test/fresh-empty-store.js +0 -15
- package/test/hash.js +0 -14
- package/test/index.js +0 -6
- package/test/lastBlock.js +0 -7
- package/test/messages.js +0 -26
- package/test/peer-data-exchange.test.js +0 -52
- package/test/peer-file-exchange.test.js +0 -78
- package/test/peernet.js +0 -17
- package/test/peernet.test.js +0 -477
- package/test-output.log +0 -84
- package/test.js +0 -63
- package/test2.js +0 -13
- package/test3.js +0 -16
- package/test4.js +0 -7
- package/tsconfig.json +0 -13
package/exports/peernet.js
CHANGED
|
@@ -480,6 +480,439 @@ class FileTransfer {
|
|
|
480
480
|
}
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
const VERSION = 1;
|
|
484
|
+
const MAX_PACKET_BYTES = 64_000;
|
|
485
|
+
const MAX_ENVELOPE_BYTES = 32_000;
|
|
486
|
+
const MAX_ARCHIVES = 500;
|
|
487
|
+
const MAX_OUTBOX = 500;
|
|
488
|
+
const MAX_AGE = 30 * 24 * 60 * 60 * 1000;
|
|
489
|
+
const MAX_CLOCK_SKEW = 10 * 60 * 1000;
|
|
490
|
+
const MAX_HOPS = 8;
|
|
491
|
+
const encoder = new TextEncoder();
|
|
492
|
+
const decoder = new TextDecoder();
|
|
493
|
+
function isUnsubscribe(value) {
|
|
494
|
+
return typeof value === 'function';
|
|
495
|
+
}
|
|
496
|
+
function encode(value) {
|
|
497
|
+
return encoder.encode(JSON.stringify(value));
|
|
498
|
+
}
|
|
499
|
+
function decode(payload) {
|
|
500
|
+
if (!(payload instanceof Uint8Array))
|
|
501
|
+
throw new TypeError('Archive packets must be encoded bytes.');
|
|
502
|
+
if (payload.byteLength > MAX_PACKET_BYTES)
|
|
503
|
+
throw new RangeError('Archive packet exceeds the size limit.');
|
|
504
|
+
return JSON.parse(decoder.decode(payload));
|
|
505
|
+
}
|
|
506
|
+
function randomId() {
|
|
507
|
+
if (typeof crypto.randomUUID === 'function')
|
|
508
|
+
return crypto.randomUUID();
|
|
509
|
+
return Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
510
|
+
}
|
|
511
|
+
function keyPrefix(network) {
|
|
512
|
+
return `peernet:archive:v1:${encodeURIComponent(network)}:`;
|
|
513
|
+
}
|
|
514
|
+
function recordKey(network, kind, id) {
|
|
515
|
+
return `${keyPrefix(network)}${kind}:${encodeURIComponent(id)}`;
|
|
516
|
+
}
|
|
517
|
+
function stateKey(network) {
|
|
518
|
+
return `${keyPrefix(network)}state`;
|
|
519
|
+
}
|
|
520
|
+
async function storeGet(store, key) {
|
|
521
|
+
if (store.has && !(await store.has(key)))
|
|
522
|
+
return undefined;
|
|
523
|
+
try {
|
|
524
|
+
return await store.get(key);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
if (error?.code === 'LEVEL_NOT_FOUND' ||
|
|
528
|
+
error?.name === 'NotFoundError')
|
|
529
|
+
return undefined;
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function validateEnvelope(envelope, network) {
|
|
534
|
+
const envelopeNetwork = envelope?.network ||
|
|
535
|
+
envelope?.senderCertificate?.network ||
|
|
536
|
+
envelope?.recipientCertificate?.network;
|
|
537
|
+
if (!envelope ||
|
|
538
|
+
typeof envelope !== 'object' ||
|
|
539
|
+
envelopeNetwork !== network ||
|
|
540
|
+
typeof envelope.id !== 'string' ||
|
|
541
|
+
!envelope.id ||
|
|
542
|
+
typeof envelope.recipientAccount !== 'string' ||
|
|
543
|
+
typeof envelope.recipientDeviceId !== 'string' ||
|
|
544
|
+
typeof envelope.ciphertext !== 'string' ||
|
|
545
|
+
!envelope.ciphertext ||
|
|
546
|
+
typeof envelope.signature !== 'string' ||
|
|
547
|
+
!Number.isFinite(envelope.timestamp) ||
|
|
548
|
+
envelope.timestamp < Date.now() - MAX_AGE ||
|
|
549
|
+
envelope.timestamp > Date.now() + MAX_CLOCK_SKEW)
|
|
550
|
+
throw new Error('Only a valid, network-scoped encrypted message envelope can be archived.');
|
|
551
|
+
if ('text' in envelope || 'plaintext' in envelope || 'message' in envelope)
|
|
552
|
+
throw new Error('Plaintext message fields cannot be archived.');
|
|
553
|
+
if (encode(envelope).byteLength > MAX_ENVELOPE_BYTES)
|
|
554
|
+
throw new RangeError('Encrypted message envelope exceeds the archive size limit.');
|
|
555
|
+
}
|
|
556
|
+
function validatePacket(value, network) {
|
|
557
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
558
|
+
return;
|
|
559
|
+
const packet = value;
|
|
560
|
+
if (packet.version !== VERSION ||
|
|
561
|
+
packet.network !== network ||
|
|
562
|
+
typeof packet.id !== 'string' ||
|
|
563
|
+
!packet.id ||
|
|
564
|
+
typeof packet.origin !== 'string' ||
|
|
565
|
+
!packet.origin ||
|
|
566
|
+
!Number.isFinite(packet.timestamp) ||
|
|
567
|
+
packet.timestamp < Date.now() - MAX_AGE ||
|
|
568
|
+
packet.timestamp > Date.now() + MAX_CLOCK_SKEW ||
|
|
569
|
+
!Number.isInteger(packet.hopsRemaining) ||
|
|
570
|
+
packet.hopsRemaining < 0 ||
|
|
571
|
+
packet.hopsRemaining > MAX_HOPS ||
|
|
572
|
+
!['direct', 'inbox-request', 'archive-delivery', 'delivery-receipt'].includes(packet.type) ||
|
|
573
|
+
!packet.payload ||
|
|
574
|
+
typeof packet.payload !== 'object' ||
|
|
575
|
+
Array.isArray(packet.payload))
|
|
576
|
+
return;
|
|
577
|
+
return packet;
|
|
578
|
+
}
|
|
579
|
+
class PeernetArchiveMode {
|
|
580
|
+
#peernet;
|
|
581
|
+
#store;
|
|
582
|
+
#network;
|
|
583
|
+
#topic;
|
|
584
|
+
#verifyEnvelope;
|
|
585
|
+
#verifyInboxRequest;
|
|
586
|
+
#verifyReceipt;
|
|
587
|
+
#handlers = new Set();
|
|
588
|
+
#seen = new Map();
|
|
589
|
+
#archives = new Set();
|
|
590
|
+
#outbox = new Set();
|
|
591
|
+
#enabled = false;
|
|
592
|
+
#started = false;
|
|
593
|
+
#unsubscribe;
|
|
594
|
+
#retryTimer;
|
|
595
|
+
#account = '';
|
|
596
|
+
#deviceId = '';
|
|
597
|
+
constructor(options) {
|
|
598
|
+
if (!options?.peernet?.network || typeof options.peernet.publish !== 'function' ||
|
|
599
|
+
typeof options.peernet.subscribe !== 'function')
|
|
600
|
+
throw new Error('A Peernet instance is required for archive mode.');
|
|
601
|
+
const store = options.store || globalThis.messageStore;
|
|
602
|
+
if (!store || typeof store.get !== 'function' || typeof store.put !== 'function' ||
|
|
603
|
+
typeof store.delete !== 'function' || typeof store.keys !== 'function')
|
|
604
|
+
throw new Error('A persistent Peernet message store is required for archive mode.');
|
|
605
|
+
if (typeof options.verifyEnvelope !== 'function' ||
|
|
606
|
+
typeof options.verifyInboxRequest !== 'function' ||
|
|
607
|
+
typeof options.verifyReceipt !== 'function')
|
|
608
|
+
throw new Error('Archive mode requires envelope, inbox-request and receipt verification functions.');
|
|
609
|
+
this.#peernet = options.peernet;
|
|
610
|
+
this.#store = store;
|
|
611
|
+
this.#network = options.peernet.network;
|
|
612
|
+
this.#topic = `peernet:archive:v1:${this.#network}`;
|
|
613
|
+
this.#verifyEnvelope = options.verifyEnvelope;
|
|
614
|
+
this.#verifyInboxRequest = options.verifyInboxRequest;
|
|
615
|
+
this.#verifyReceipt = options.verifyReceipt;
|
|
616
|
+
}
|
|
617
|
+
get network() {
|
|
618
|
+
return this.#network;
|
|
619
|
+
}
|
|
620
|
+
get enabled() {
|
|
621
|
+
return this.#enabled;
|
|
622
|
+
}
|
|
623
|
+
get archiveCount() {
|
|
624
|
+
return this.#archives.size;
|
|
625
|
+
}
|
|
626
|
+
get pendingCount() {
|
|
627
|
+
return this.#outbox.size;
|
|
628
|
+
}
|
|
629
|
+
onMessage(handler) {
|
|
630
|
+
if (typeof handler !== 'function')
|
|
631
|
+
throw new TypeError('An archive message handler is required.');
|
|
632
|
+
this.#handlers.add(handler);
|
|
633
|
+
return () => this.#handlers.delete(handler);
|
|
634
|
+
}
|
|
635
|
+
async start({ account, deviceId }) {
|
|
636
|
+
if (this.#started)
|
|
637
|
+
return;
|
|
638
|
+
if (!account || !deviceId)
|
|
639
|
+
throw new Error('A local account and chat device ID are required.');
|
|
640
|
+
this.#account = account;
|
|
641
|
+
this.#deviceId = deviceId;
|
|
642
|
+
await this.#load();
|
|
643
|
+
const receiver = (payload) => {
|
|
644
|
+
this.#receive(payload).catch((error) => console.error('Peernet archive packet failed:', error));
|
|
645
|
+
};
|
|
646
|
+
const unsubscribe = await this.#peernet.subscribe(this.#topic, receiver);
|
|
647
|
+
this.#unsubscribe = isUnsubscribe(unsubscribe)
|
|
648
|
+
? unsubscribe
|
|
649
|
+
: typeof globalThis.globalSub?.unsubscribe === 'function'
|
|
650
|
+
? () => globalThis.globalSub.unsubscribe(this.#topic, receiver)
|
|
651
|
+
: undefined;
|
|
652
|
+
this.#started = true;
|
|
653
|
+
this.#retryTimer = setInterval(() => {
|
|
654
|
+
this.#retryOutbox().catch((error) => console.error('Peernet archive retry failed:', error));
|
|
655
|
+
}, 30_000);
|
|
656
|
+
try {
|
|
657
|
+
for (const id of this.#outbox) {
|
|
658
|
+
const bytes = await storeGet(this.#store, recordKey(this.#network, 'outbox', id));
|
|
659
|
+
if (bytes)
|
|
660
|
+
await this.#publish('direct', { envelope: JSON.parse(decoder.decode(bytes)) });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
this.stop();
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
stop() {
|
|
669
|
+
clearInterval(this.#retryTimer);
|
|
670
|
+
this.#retryTimer = undefined;
|
|
671
|
+
this.#unsubscribe?.();
|
|
672
|
+
this.#unsubscribe = undefined;
|
|
673
|
+
this.#started = false;
|
|
674
|
+
this.#account = '';
|
|
675
|
+
this.#deviceId = '';
|
|
676
|
+
}
|
|
677
|
+
async setEnabled(enabled) {
|
|
678
|
+
if (typeof enabled !== 'boolean')
|
|
679
|
+
throw new TypeError('Archive mode must be enabled or disabled explicitly.');
|
|
680
|
+
if (!this.#started)
|
|
681
|
+
await this.#load();
|
|
682
|
+
this.#enabled = enabled;
|
|
683
|
+
await this.#store.put(stateKey(this.#network), encode({ version: VERSION, enabled }));
|
|
684
|
+
if (!enabled) {
|
|
685
|
+
for (const id of this.#archives)
|
|
686
|
+
await this.#store.delete(recordKey(this.#network, 'archive', id));
|
|
687
|
+
this.#archives.clear();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async send(envelope) {
|
|
691
|
+
validateEnvelope(envelope, this.#network);
|
|
692
|
+
if (!(await this.#verifyEnvelope(envelope)))
|
|
693
|
+
throw new Error('Encrypted message signature verification failed.');
|
|
694
|
+
if (this.#outbox.has(envelope.id))
|
|
695
|
+
throw new Error('This encrypted message is already pending.');
|
|
696
|
+
await this.#store.put(recordKey(this.#network, 'outbox', envelope.id), encode(envelope));
|
|
697
|
+
this.#outbox.add(envelope.id);
|
|
698
|
+
if (this.#outbox.size > MAX_OUTBOX)
|
|
699
|
+
await this.#dropOldest('outbox');
|
|
700
|
+
await this.#publish('direct', { envelope });
|
|
701
|
+
}
|
|
702
|
+
async requestInbox(request) {
|
|
703
|
+
if (request.network !== this.#network ||
|
|
704
|
+
request.account !== this.#account ||
|
|
705
|
+
request.deviceId !== this.#deviceId ||
|
|
706
|
+
!(await this.#validInboxRequest(request)))
|
|
707
|
+
throw new Error('A valid signed inbox request for this Peernet network and device is required.');
|
|
708
|
+
await this.#publish('inbox-request', { request });
|
|
709
|
+
}
|
|
710
|
+
async acknowledge(receipt) {
|
|
711
|
+
if (receipt.network !== this.#network ||
|
|
712
|
+
receipt.recipientAccount !== this.#account ||
|
|
713
|
+
receipt.recipientDeviceId !== this.#deviceId ||
|
|
714
|
+
typeof receipt.signature !== 'string' ||
|
|
715
|
+
!Number.isFinite(receipt.timestamp) ||
|
|
716
|
+
Math.abs(Date.now() - receipt.timestamp) > MAX_CLOCK_SKEW)
|
|
717
|
+
throw new Error('Delivery receipt does not belong to this Peernet network and device.');
|
|
718
|
+
await this.#publish('delivery-receipt', { receipt });
|
|
719
|
+
}
|
|
720
|
+
async #load() {
|
|
721
|
+
const state = await storeGet(this.#store, stateKey(this.#network));
|
|
722
|
+
if (state) {
|
|
723
|
+
const saved = JSON.parse(decoder.decode(state));
|
|
724
|
+
if (saved?.version !== VERSION || typeof saved.enabled !== 'boolean')
|
|
725
|
+
throw new Error('Saved Peernet archive settings are invalid.');
|
|
726
|
+
this.#enabled = saved.enabled;
|
|
727
|
+
}
|
|
728
|
+
const prefix = keyPrefix(this.#network);
|
|
729
|
+
const keys = await this.#store.keys();
|
|
730
|
+
for (const keyInput of keys) {
|
|
731
|
+
const key = keyInput.toString().replace(/^\/+/, '');
|
|
732
|
+
if (!key.startsWith(prefix))
|
|
733
|
+
continue;
|
|
734
|
+
const archiveMarker = `${prefix}archive:`;
|
|
735
|
+
const outboxMarker = `${prefix}outbox:`;
|
|
736
|
+
if (key.startsWith(archiveMarker))
|
|
737
|
+
this.#archives.add(decodeURIComponent(key.slice(archiveMarker.length)));
|
|
738
|
+
if (key.startsWith(outboxMarker))
|
|
739
|
+
this.#outbox.add(decodeURIComponent(key.slice(outboxMarker.length)));
|
|
740
|
+
}
|
|
741
|
+
await this.#prune();
|
|
742
|
+
}
|
|
743
|
+
async #publish(type, payload, hopsRemaining = MAX_HOPS) {
|
|
744
|
+
if (!this.#started)
|
|
745
|
+
throw new Error('Peernet archive mode is not started.');
|
|
746
|
+
const packet = {
|
|
747
|
+
version: VERSION,
|
|
748
|
+
id: randomId(),
|
|
749
|
+
network: this.#network,
|
|
750
|
+
origin: this.#peernet.id,
|
|
751
|
+
timestamp: Date.now(),
|
|
752
|
+
hopsRemaining,
|
|
753
|
+
type,
|
|
754
|
+
payload
|
|
755
|
+
};
|
|
756
|
+
const bytes = encode(packet);
|
|
757
|
+
if (bytes.byteLength > MAX_PACKET_BYTES)
|
|
758
|
+
throw new RangeError('Peernet archive packet exceeds its size limit.');
|
|
759
|
+
this.#seen.set(packet.id, packet.timestamp);
|
|
760
|
+
await this.#peernet.publish(this.#topic, bytes);
|
|
761
|
+
}
|
|
762
|
+
async #receive(payload) {
|
|
763
|
+
const packet = validatePacket(decode(payload), this.#network);
|
|
764
|
+
if (!packet || packet.origin === this.#peernet.id || this.#wasSeen(packet.id))
|
|
765
|
+
return;
|
|
766
|
+
this.#markSeen(packet.id);
|
|
767
|
+
let forward = false;
|
|
768
|
+
switch (packet.type) {
|
|
769
|
+
case 'direct':
|
|
770
|
+
case 'archive-delivery': {
|
|
771
|
+
const envelope = packet.payload.envelope;
|
|
772
|
+
try {
|
|
773
|
+
validateEnvelope(envelope, this.#network);
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
if (!(await this.#verifyEnvelope(envelope)))
|
|
779
|
+
break;
|
|
780
|
+
forward = true;
|
|
781
|
+
if (packet.type === 'direct' && this.#enabled)
|
|
782
|
+
await this.#saveArchive(envelope);
|
|
783
|
+
if (envelope.recipientAccount === this.#account && envelope.recipientDeviceId === this.#deviceId) {
|
|
784
|
+
for (const handler of this.#handlers)
|
|
785
|
+
handler({ type: 'message', envelope });
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
case 'inbox-request': {
|
|
790
|
+
const request = packet.payload.request;
|
|
791
|
+
if (!(await this.#validInboxRequest(request)))
|
|
792
|
+
break;
|
|
793
|
+
forward = true;
|
|
794
|
+
if (this.#enabled && !(request.account === this.#account && request.deviceId === this.#deviceId))
|
|
795
|
+
await this.#deliverTo(request);
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
case 'delivery-receipt': {
|
|
799
|
+
const receipt = packet.payload.receipt;
|
|
800
|
+
forward = await this.#receiveReceipt(receipt);
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (forward && packet.hopsRemaining > 0) {
|
|
805
|
+
await this.#peernet.publish(this.#topic, encode({ ...packet, hopsRemaining: packet.hopsRemaining - 1 }));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
async #validInboxRequest(request) {
|
|
809
|
+
return Boolean(request &&
|
|
810
|
+
request.network === this.#network &&
|
|
811
|
+
typeof request.account === 'string' &&
|
|
812
|
+
typeof request.deviceId === 'string' &&
|
|
813
|
+
Number.isFinite(request.timestamp) &&
|
|
814
|
+
Math.abs(Date.now() - request.timestamp) <= MAX_CLOCK_SKEW &&
|
|
815
|
+
typeof request.signature === 'string' &&
|
|
816
|
+
await this.#verifyInboxRequest(request));
|
|
817
|
+
}
|
|
818
|
+
async #saveArchive(envelope) {
|
|
819
|
+
if (this.#archives.has(envelope.id))
|
|
820
|
+
return;
|
|
821
|
+
await this.#store.put(recordKey(this.#network, 'archive', envelope.id), encode({
|
|
822
|
+
envelope,
|
|
823
|
+
storedAt: Date.now()
|
|
824
|
+
}));
|
|
825
|
+
this.#archives.add(envelope.id);
|
|
826
|
+
if (this.#archives.size > MAX_ARCHIVES)
|
|
827
|
+
await this.#dropOldest('archive');
|
|
828
|
+
}
|
|
829
|
+
async #deliverTo(request) {
|
|
830
|
+
for (const id of this.#archives) {
|
|
831
|
+
const bytes = await storeGet(this.#store, recordKey(this.#network, 'archive', id));
|
|
832
|
+
if (!bytes) {
|
|
833
|
+
this.#archives.delete(id);
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
const record = JSON.parse(decoder.decode(bytes));
|
|
837
|
+
const envelope = record?.envelope;
|
|
838
|
+
if (envelope?.recipientAccount === request.account && envelope.recipientDeviceId === request.deviceId)
|
|
839
|
+
await this.#publish('archive-delivery', { envelope });
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async #receiveReceipt(receipt) {
|
|
843
|
+
if (!receipt || receipt.network !== this.#network ||
|
|
844
|
+
typeof receipt.messageId !== 'string' ||
|
|
845
|
+
!Number.isFinite(receipt.timestamp) ||
|
|
846
|
+
Math.abs(Date.now() - receipt.timestamp) > MAX_CLOCK_SKEW)
|
|
847
|
+
return false;
|
|
848
|
+
const keys = ['archive', 'outbox'];
|
|
849
|
+
let verified = false;
|
|
850
|
+
for (const kind of keys) {
|
|
851
|
+
const key = recordKey(this.#network, kind, receipt.messageId);
|
|
852
|
+
const bytes = await storeGet(this.#store, key);
|
|
853
|
+
if (!bytes)
|
|
854
|
+
continue;
|
|
855
|
+
const stored = JSON.parse(decoder.decode(bytes));
|
|
856
|
+
const envelope = kind === 'archive' ? stored.envelope : stored;
|
|
857
|
+
if (!(await this.#verifyReceipt(receipt, envelope)))
|
|
858
|
+
continue;
|
|
859
|
+
await this.#store.delete(key);
|
|
860
|
+
(kind === 'archive' ? this.#archives : this.#outbox).delete(receipt.messageId);
|
|
861
|
+
verified = true;
|
|
862
|
+
}
|
|
863
|
+
return verified;
|
|
864
|
+
}
|
|
865
|
+
async #retryOutbox() {
|
|
866
|
+
if (!this.#started)
|
|
867
|
+
return;
|
|
868
|
+
await this.#prune();
|
|
869
|
+
for (const id of this.#outbox) {
|
|
870
|
+
const bytes = await storeGet(this.#store, recordKey(this.#network, 'outbox', id));
|
|
871
|
+
if (bytes)
|
|
872
|
+
await this.#publish('direct', { envelope: JSON.parse(decoder.decode(bytes)) });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async #prune() {
|
|
876
|
+
const cutoff = Date.now() - MAX_AGE;
|
|
877
|
+
for (const [kind, ids] of [['archive', this.#archives], ['outbox', this.#outbox]]) {
|
|
878
|
+
for (const id of ids) {
|
|
879
|
+
const key = recordKey(this.#network, kind, id);
|
|
880
|
+
const bytes = await storeGet(this.#store, key);
|
|
881
|
+
if (!bytes) {
|
|
882
|
+
ids.delete(id);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
const saved = JSON.parse(decoder.decode(bytes));
|
|
886
|
+
const timestamp = kind === 'archive' ? saved.storedAt : saved.timestamp;
|
|
887
|
+
if (!Number.isFinite(timestamp) || timestamp < cutoff) {
|
|
888
|
+
await this.#store.delete(key);
|
|
889
|
+
ids.delete(id);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async #dropOldest(kind) {
|
|
895
|
+
const ids = kind === 'archive' ? this.#archives : this.#outbox;
|
|
896
|
+
const id = ids.values().next().value;
|
|
897
|
+
if (!id)
|
|
898
|
+
return;
|
|
899
|
+
await this.#store.delete(recordKey(this.#network, kind, id));
|
|
900
|
+
ids.delete(id);
|
|
901
|
+
}
|
|
902
|
+
#wasSeen(id) {
|
|
903
|
+
const timestamp = this.#seen.get(id);
|
|
904
|
+
return timestamp !== undefined && Date.now() - timestamp < MAX_AGE;
|
|
905
|
+
}
|
|
906
|
+
#markSeen(id) {
|
|
907
|
+
this.#seen.set(id, Date.now());
|
|
908
|
+
if (this.#seen.size > 4_000) {
|
|
909
|
+
const oldestId = this.#seen.keys().next().value;
|
|
910
|
+
if (oldestId !== undefined)
|
|
911
|
+
this.#seen.delete(oldestId);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
483
916
|
const DEFAULT_CHUNK_SIZE = 256 * 1024;
|
|
484
917
|
const DEFAULT_BLOCK_CHUNK_SIZE = 256 * 1024;
|
|
485
918
|
const DEFAULT_BLOCK_CHUNK_THRESHOLD = 1024 * 1024;
|
|
@@ -506,7 +939,7 @@ class Peernet {
|
|
|
506
939
|
root;
|
|
507
940
|
identity;
|
|
508
941
|
stores = [];
|
|
509
|
-
|
|
942
|
+
routingId;
|
|
510
943
|
/**
|
|
511
944
|
* @type {Object}
|
|
512
945
|
* @property {Object} peer Instance of Peer
|
|
@@ -541,6 +974,7 @@ class Peernet {
|
|
|
541
974
|
* @param {String} options.root - path to root directory
|
|
542
975
|
* @param {String} options.version - path to root directory
|
|
543
976
|
* @param {String} options.storePrefix - prefix for datatores (lfc)
|
|
977
|
+
* @param {String} options.routingId - optional transport identity, separate from the wallet identity
|
|
544
978
|
* @param {Object} options.transport - transport selection and fallback options passed to @netpeer/swarm
|
|
545
979
|
*
|
|
546
980
|
* @return {Promise} instance of Peernet
|
|
@@ -553,6 +987,9 @@ class Peernet {
|
|
|
553
987
|
* @property {String} network - current network
|
|
554
988
|
*/
|
|
555
989
|
this.network = options.network || 'leofcoin';
|
|
990
|
+
if (options.routingId !== undefined && (typeof options.routingId !== 'string' || !options.routingId.trim()))
|
|
991
|
+
throw new TypeError('routingId must be a non-empty string');
|
|
992
|
+
this.routingId = options.routingId?.trim();
|
|
556
993
|
this.autoStart = options.autoStart === undefined ? true : options.autoStart;
|
|
557
994
|
this.stars = options.stars;
|
|
558
995
|
this.transport = options.transport;
|
|
@@ -584,6 +1021,9 @@ class Peernet {
|
|
|
584
1021
|
return this._init(options, password);
|
|
585
1022
|
}
|
|
586
1023
|
get id() {
|
|
1024
|
+
return this.routingId || this.identity.id;
|
|
1025
|
+
}
|
|
1026
|
+
get peerId() {
|
|
587
1027
|
return this.identity.id;
|
|
588
1028
|
}
|
|
589
1029
|
get selectedAccount() {
|
|
@@ -605,6 +1045,9 @@ class Peernet {
|
|
|
605
1045
|
addCodec(codec) {
|
|
606
1046
|
return utils.addCodec(codec);
|
|
607
1047
|
}
|
|
1048
|
+
createArchiveMode(options) {
|
|
1049
|
+
return new PeernetArchiveMode({ ...options, peernet: this });
|
|
1050
|
+
}
|
|
608
1051
|
async addStore(name, prefix, root, isPrivate = true) {
|
|
609
1052
|
if (name === 'block' ||
|
|
610
1053
|
name === 'transaction' ||
|
|
@@ -691,7 +1134,6 @@ class Peernet {
|
|
|
691
1134
|
this.identity = new Identity(this.network);
|
|
692
1135
|
await this.identity.load(password, options.freshIdentity === true);
|
|
693
1136
|
this._peerHandler = new PeerDiscovery(this.id);
|
|
694
|
-
this.peerId = this.id;
|
|
695
1137
|
/**
|
|
696
1138
|
* converts data -> message -> proto
|
|
697
1139
|
* @see DataHandler
|
|
@@ -1499,4 +1941,4 @@ class Peernet {
|
|
|
1499
1941
|
}
|
|
1500
1942
|
globalThis.Peernet = Peernet;
|
|
1501
1943
|
|
|
1502
|
-
export { FileTransfer, Peernet as default };
|
|
1944
|
+
export { FileTransfer, PeernetArchiveMode, Peernet as default };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type ArchivedEnvelope = {
|
|
2
|
+
id: string;
|
|
3
|
+
network?: string;
|
|
4
|
+
recipientAccount: string;
|
|
5
|
+
recipientDeviceId: string;
|
|
6
|
+
ciphertext: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
export type InboxRequest = {
|
|
11
|
+
account: string;
|
|
12
|
+
deviceId: string;
|
|
13
|
+
network: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
signature: string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
};
|
|
18
|
+
export type DeliveryReceipt = {
|
|
19
|
+
messageId: string;
|
|
20
|
+
recipientAccount: string;
|
|
21
|
+
recipientDeviceId: string;
|
|
22
|
+
network: string;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
signature: string;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
};
|
|
27
|
+
export type ArchiveStore = {
|
|
28
|
+
has?(key: string): Promise<boolean | unknown[]>;
|
|
29
|
+
get(key: string): Promise<Uint8Array>;
|
|
30
|
+
put(key: string, value: Uint8Array): Promise<unknown>;
|
|
31
|
+
delete(key: string): Promise<unknown>;
|
|
32
|
+
keys(): Promise<Array<string | {
|
|
33
|
+
toString(): string;
|
|
34
|
+
}>>;
|
|
35
|
+
};
|
|
36
|
+
export type ArchivePeernet = {
|
|
37
|
+
network: string;
|
|
38
|
+
id: string;
|
|
39
|
+
publish(topic: string, payload: Uint8Array): Promise<unknown>;
|
|
40
|
+
subscribe(topic: string, callback: (payload: Uint8Array) => void): Promise<unknown>;
|
|
41
|
+
};
|
|
42
|
+
export type ArchiveModeOptions = {
|
|
43
|
+
peernet: ArchivePeernet;
|
|
44
|
+
store?: ArchiveStore;
|
|
45
|
+
verifyEnvelope: (envelope: ArchivedEnvelope) => Promise<boolean>;
|
|
46
|
+
verifyInboxRequest: (request: InboxRequest) => Promise<boolean>;
|
|
47
|
+
verifyReceipt: (receipt: DeliveryReceipt, envelope: ArchivedEnvelope) => Promise<boolean>;
|
|
48
|
+
};
|
|
49
|
+
export declare class PeernetArchiveMode {
|
|
50
|
+
#private;
|
|
51
|
+
constructor(options: ArchiveModeOptions);
|
|
52
|
+
get network(): string;
|
|
53
|
+
get enabled(): boolean;
|
|
54
|
+
get archiveCount(): number;
|
|
55
|
+
get pendingCount(): number;
|
|
56
|
+
onMessage(handler: (event: {
|
|
57
|
+
type: 'message';
|
|
58
|
+
envelope: ArchivedEnvelope;
|
|
59
|
+
}) => void): () => void;
|
|
60
|
+
start({ account, deviceId }: {
|
|
61
|
+
account: string;
|
|
62
|
+
deviceId: string;
|
|
63
|
+
}): Promise<void>;
|
|
64
|
+
stop(): void;
|
|
65
|
+
setEnabled(enabled: boolean): Promise<void>;
|
|
66
|
+
send(envelope: ArchivedEnvelope): Promise<void>;
|
|
67
|
+
requestInbox(request: InboxRequest): Promise<void>;
|
|
68
|
+
acknowledge(receipt: DeliveryReceipt): Promise<void>;
|
|
69
|
+
}
|
|
@@ -6,6 +6,7 @@ import { Storage as LeofcoinStorageClass } from '@leofcoin/storage';
|
|
|
6
6
|
import Identity from './identity.js';
|
|
7
7
|
import swarm from '@netpeer/swarm/client';
|
|
8
8
|
import FileTransfer from './file-transfer.js';
|
|
9
|
+
import { PeernetArchiveMode, type ArchiveModeOptions } from './archive.js';
|
|
9
10
|
declare global {
|
|
10
11
|
var LeofcoinStorage: typeof LeofcoinStorageClass;
|
|
11
12
|
var peernet: Peernet;
|
|
@@ -30,7 +31,7 @@ export default class Peernet {
|
|
|
30
31
|
root: string;
|
|
31
32
|
identity: Identity;
|
|
32
33
|
stores: string[];
|
|
33
|
-
|
|
34
|
+
readonly routingId?: string;
|
|
34
35
|
/**
|
|
35
36
|
* @type {Object}
|
|
36
37
|
* @property {Object} peer Instance of Peer
|
|
@@ -65,6 +66,7 @@ export default class Peernet {
|
|
|
65
66
|
* @param {String} options.root - path to root directory
|
|
66
67
|
* @param {String} options.version - path to root directory
|
|
67
68
|
* @param {String} options.storePrefix - prefix for datatores (lfc)
|
|
69
|
+
* @param {String} options.routingId - optional transport identity, separate from the wallet identity
|
|
68
70
|
* @param {Object} options.transport - transport selection and fallback options passed to @netpeer/swarm
|
|
69
71
|
*
|
|
70
72
|
* @return {Promise} instance of Peernet
|
|
@@ -74,12 +76,14 @@ export default class Peernet {
|
|
|
74
76
|
*/
|
|
75
77
|
constructor(options: any, password: any);
|
|
76
78
|
get id(): string;
|
|
79
|
+
get peerId(): string;
|
|
77
80
|
get selectedAccount(): string;
|
|
78
81
|
get accounts(): Promise<[[name: string, externalAddress: string, internalAddress: string]]>;
|
|
79
82
|
get defaultStores(): string[];
|
|
80
83
|
selectAccount(account: string): Promise<unknown>;
|
|
81
84
|
addProto(name: any, proto: any): void;
|
|
82
85
|
addCodec(codec: any): void;
|
|
86
|
+
createArchiveMode(options: Omit<ArchiveModeOptions, 'peernet'>): PeernetArchiveMode;
|
|
83
87
|
addStore(name: any, prefix: any, root: any, isPrivate?: boolean): Promise<void>;
|
|
84
88
|
/**
|
|
85
89
|
* @see MessageHandler
|
|
@@ -362,3 +366,5 @@ export default class Peernet {
|
|
|
362
366
|
get Buffer(): BufferConstructor;
|
|
363
367
|
}
|
|
364
368
|
export { FileTransfer };
|
|
369
|
+
export { PeernetArchiveMode };
|
|
370
|
+
export type { ArchivedEnvelope, DeliveryReceipt, InboxRequest } from './archive.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/peernet",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -91,12 +91,17 @@
|
|
|
91
91
|
},
|
|
92
92
|
"type": "module",
|
|
93
93
|
"engines": {
|
|
94
|
-
"node": ">=
|
|
94
|
+
"node": ">=24.15.0"
|
|
95
95
|
},
|
|
96
|
+
"files": [
|
|
97
|
+
"exports",
|
|
98
|
+
"docs/automatic-transport.md"
|
|
99
|
+
],
|
|
96
100
|
"scripts": {
|
|
97
101
|
"build": "rollup -c",
|
|
98
102
|
"watch": "rollup -c -w",
|
|
99
|
-
"test": "node --test test/peernet.test.js test/file-transfer.test.js",
|
|
103
|
+
"test": "node --test test/peernet.test.js test/file-transfer.test.js test/archive.test.js",
|
|
104
|
+
"test:package": "node test/standalone-install.js",
|
|
100
105
|
"server": "discovery-swarm-webrtc --port=4000",
|
|
101
106
|
"demo": "jsproject --serve ./ --port 6868"
|
|
102
107
|
},
|
|
@@ -112,7 +117,7 @@
|
|
|
112
117
|
"@leofcoin/multi-wallet": "^3.1.11",
|
|
113
118
|
"@leofcoin/storage": "^3.5.38",
|
|
114
119
|
"@mapbox/node-pre-gyp": "^2.0.3",
|
|
115
|
-
"@netpeer/swarm": "^0.
|
|
120
|
+
"@netpeer/swarm": "^0.10.0",
|
|
116
121
|
"@vandeurenglenn/base58": "^1.1.9",
|
|
117
122
|
"@vandeurenglenn/debug": "^1.4.0",
|
|
118
123
|
"@vandeurenglenn/little-pubsub": "^1.5.2",
|
|
@@ -129,7 +134,6 @@
|
|
|
129
134
|
"@rollup/plugin-wasm": "^6.2.2",
|
|
130
135
|
"@types/node": "^25.9.1",
|
|
131
136
|
"@types/qrcode": "^1.5.6",
|
|
132
|
-
"rimraf": "^5.0.10",
|
|
133
137
|
"rollup": "^4.60.4",
|
|
134
138
|
"tslib": "^2.8.1",
|
|
135
139
|
"typescript": "^6.0.2"
|
package/.esdoc.json
DELETED
package/.gitattributes
DELETED