@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.
Files changed (124) hide show
  1. package/README.md +45 -0
  2. package/docs/automatic-transport.md +37 -0
  3. package/exports/browser/{client-CAC7RVMf.js → client-DKKzPSlb.js} +252 -76
  4. package/exports/browser/{messages-Dw86z0zq.js → messages-DCePyqKC.js} +1 -1
  5. package/exports/browser/{peernet-B2jRhqGI.js → peernet-DjddqiXc.js} +460 -18
  6. package/exports/browser/peernet.js +1 -1
  7. package/exports/peernet.js +445 -3
  8. package/exports/types/archive.d.ts +69 -0
  9. package/exports/types/peernet.d.ts +7 -1
  10. package/package.json +9 -5
  11. package/.esdoc.json +0 -10
  12. package/.gitattributes +0 -2
  13. package/.github/workflows/release.yml +0 -34
  14. package/.prettierrc +0 -7
  15. package/.travis.yml +0 -27
  16. package/BREAKING_CHANGES.md +0 -35
  17. package/deploy.js +0 -8
  18. package/exports/browser/browser-D-r0O9Qn.js +0 -53
  19. package/exports/browser/browser-DvU1xNFS.js +0 -147
  20. package/exports/browser/browser-_hiyXwPp.js +0 -147
  21. package/exports/browser/client-86Tnax66.js +0 -2224
  22. package/exports/browser/client-BFxJts6j.js +0 -2224
  23. package/exports/browser/client-BVhUamQG.js +0 -2200
  24. package/exports/browser/client-BzvWzzLv.js +0 -2224
  25. package/exports/browser/client-C9GBz2Tf.js +0 -2224
  26. package/exports/browser/client-CQzuu6At.js +0 -2224
  27. package/exports/browser/client-CVkFejdx.js +0 -2224
  28. package/exports/browser/client-Cj_UUO8W.js +0 -2224
  29. package/exports/browser/client-CrGaeBjj.js +0 -2224
  30. package/exports/browser/client-DDC8C510.js +0 -2224
  31. package/exports/browser/client-ERocgKET.js +0 -2224
  32. package/exports/browser/client-a8rws3cu.js +0 -2224
  33. package/exports/browser/identity-CYVPDmkf.js +0 -8402
  34. package/exports/browser/identity-ChyyZSve.js +0 -8397
  35. package/exports/browser/index-BD0Anx7_.js +0 -37
  36. package/exports/browser/index-Bxr5Iztg.js +0 -37
  37. package/exports/browser/messages-BFCmg6Vb.js +0 -212
  38. package/exports/browser/messages-BHIC2mQu.js +0 -212
  39. package/exports/browser/messages-CimzJ4wr.js +0 -210
  40. package/exports/browser/messages-Cjoa_LGO.js +0 -210
  41. package/exports/browser/messages-D7JxkkDW.js +0 -212
  42. package/exports/browser/messages-DH0tgNM9.js +0 -207
  43. package/exports/browser/messages-DVHcNvs_.js +0 -212
  44. package/exports/browser/messages-DqL_g2qw.js +0 -210
  45. package/exports/browser/messages-UKnuelZ7.js +0 -207
  46. package/exports/browser/messages-XgZZUMma.js +0 -212
  47. package/exports/browser/messages-kfFyctPL.js +0 -210
  48. package/exports/browser/messages-psROnfRi.js +0 -212
  49. package/exports/browser/peernet-B9VL4Xwe.js +0 -9522
  50. package/exports/browser/peernet-C-HNdrcd.js +0 -9528
  51. package/exports/browser/peernet-C4rOvyyU.js +0 -9661
  52. package/exports/browser/peernet-Ca1iM2tN.js +0 -9336
  53. package/exports/browser/peernet-CbI0-jFn.js +0 -9651
  54. package/exports/browser/peernet-CtHScDyh.js +0 -9524
  55. package/exports/browser/peernet-DfuxUK3u.js +0 -9525
  56. package/exports/browser/peernet-DiEcNDLD.js +0 -9655
  57. package/exports/browser/peernet-DtuRXOzP.js +0 -9653
  58. package/exports/browser/peernet-r_9IyzTe.js +0 -9336
  59. package/exports/browser/peernet-rcD5XL1y.js +0 -9590
  60. package/exports/browser/peernet-rw8HJGOz.js +0 -9597
  61. package/exports/messages-CHKVVTVE.js +0 -204
  62. package/exports/messages-DaUtFfBh.js +0 -207
  63. package/index.html +0 -19
  64. package/rollup.config.js +0 -143
  65. package/src/dht/dht.ts +0 -142
  66. package/src/discovery/peer-discovery.js +0 -75
  67. package/src/errors/errors.js +0 -12
  68. package/src/file-transfer.ts +0 -194
  69. package/src/handlers/data.js +0 -15
  70. package/src/handlers/message.js +0 -34
  71. package/src/identity.ts +0 -172
  72. package/src/messages/chat.ts +0 -13
  73. package/src/messages/data-response.ts +0 -13
  74. package/src/messages/data.ts +0 -17
  75. package/src/messages/dht-response.ts +0 -13
  76. package/src/messages/dht.ts +0 -21
  77. package/src/messages/file-link.ts +0 -17
  78. package/src/messages/file.ts +0 -17
  79. package/src/messages/peer-response.ts +0 -13
  80. package/src/messages/peer.ts +0 -13
  81. package/src/messages/peernet.ts +0 -13
  82. package/src/messages/ps.ts +0 -13
  83. package/src/messages/request.ts +0 -13
  84. package/src/messages/response.ts +0 -13
  85. package/src/messages.js +0 -12
  86. package/src/peer-info.js +0 -9
  87. package/src/peernet.ts +0 -1113
  88. package/src/prompts/password/browser.js +0 -1
  89. package/src/prompts/password/node.js +0 -6
  90. package/src/prompts/password.js +0 -1
  91. package/src/proto/chat-message.proto.js +0 -6
  92. package/src/proto/data-response.proto.js +0 -4
  93. package/src/proto/data.proto.js +0 -4
  94. package/src/proto/dht-response.proto.js +0 -4
  95. package/src/proto/dht.proto.js +0 -4
  96. package/src/proto/file-link.proto.js +0 -5
  97. package/src/proto/file.proto.js +0 -10
  98. package/src/proto/peer-response.proto.js +0 -3
  99. package/src/proto/peer.proto.js +0 -3
  100. package/src/proto/peernet.proto.js +0 -7
  101. package/src/proto/ps.proto.js +0 -4
  102. package/src/proto/request.proto.js +0 -4
  103. package/src/proto/response.proto.js +0 -3
  104. package/src/types.ts +0 -25
  105. package/src/utils/utils.js +0 -77
  106. package/test/all.js +0 -5
  107. package/test/client.js +0 -15
  108. package/test/codec.js +0 -56
  109. package/test/file-transfer.test.js +0 -81
  110. package/test/fresh-empty-store.js +0 -15
  111. package/test/hash.js +0 -14
  112. package/test/index.js +0 -6
  113. package/test/lastBlock.js +0 -7
  114. package/test/messages.js +0 -26
  115. package/test/peer-data-exchange.test.js +0 -52
  116. package/test/peer-file-exchange.test.js +0 -78
  117. package/test/peernet.js +0 -17
  118. package/test/peernet.test.js +0 -477
  119. package/test-output.log +0 -84
  120. package/test.js +0 -63
  121. package/test2.js +0 -13
  122. package/test3.js +0 -16
  123. package/test4.js +0 -7
  124. package/tsconfig.json +0 -13
@@ -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
- peerId;
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
- peerId: string;
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": "2.0.0",
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": ">=19.0.0"
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.9.12",
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
@@ -1,10 +0,0 @@
1
- {
2
- "source": "./src",
3
- "destination": "./docs",
4
- "plugins": [{
5
- "name": "esdoc-standard-plugin"
6
- }, {
7
- "name": "esdoc-ecmascript-proposal-plugin",
8
- "option": {"all": true}
9
- }]
10
- }
package/.gitattributes DELETED
@@ -1,2 +0,0 @@
1
- # Auto detect text files and perform LF normalization
2
- * text=auto