@leofcoin/peernet 3.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/exports/browser/{client-C_pBoi1g.js → client-DKKzPSlb.js} +1 -1
- package/exports/browser/{messages-BGHaihSi.js → messages-DCePyqKC.js} +1 -1
- package/exports/browser/{peernet-Db43ag2D.js → peernet-DjddqiXc.js} +460 -18
- package/exports/browser/peernet.js +1 -1
- package/exports/peernet.js +445 -3
- package/exports/store.js +3 -0
- package/exports/types/archive.d.ts +69 -0
- package/exports/types/peernet.d.ts +7 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ import Peernet from '@leofcoin/peernet'
|
|
|
35
35
|
const config = {
|
|
36
36
|
network: 'leofcoin:peach',
|
|
37
37
|
networkVersion: 'peach',
|
|
38
|
+
// Optional per-installation transport identity; defaults to the wallet identity.
|
|
39
|
+
routingId: 'lfc-peer-a1b2c3d4',
|
|
38
40
|
stars: ['wss://peach.leofcoin.org']
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -48,6 +50,49 @@ await node.start()
|
|
|
48
50
|
console.log(globalThis.peernet)
|
|
49
51
|
```
|
|
50
52
|
|
|
53
|
+
`node.id` is the routing identity used for peer discovery and swarm connections.
|
|
54
|
+
When `routingId` is omitted it remains the wallet identity for backward
|
|
55
|
+
compatibility. `node.peerId` always identifies the loaded wallet and should be
|
|
56
|
+
used by applications that need the wallet identity, such as chain transaction
|
|
57
|
+
signing. Applications with multiple installations of the same wallet can
|
|
58
|
+
provide a distinct, persisted `routingId` for each installation.
|
|
59
|
+
|
|
60
|
+
### Encrypted chat archive mode
|
|
61
|
+
|
|
62
|
+
Applications that already create and verify encrypted chat envelopes can opt in
|
|
63
|
+
to Peernet's bounded ciphertext archive. Archive settings and records are keyed
|
|
64
|
+
by the complete Peernet network string (including its version, such as
|
|
65
|
+
`leofcoin:peach`) in the message store. Archive mode is disabled by default.
|
|
66
|
+
Records expire after 30 days and are limited to 500 messages per network.
|
|
67
|
+
Plaintext message fields are rejected; the application supplies verification
|
|
68
|
+
functions for the signed envelope, inbox request and delivery receipt.
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
const archive = node.createArchiveMode({
|
|
72
|
+
verifyEnvelope,
|
|
73
|
+
verifyInboxRequest,
|
|
74
|
+
verifyReceipt
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
await archive.setEnabled(true) // Explicit opt-in for this node and network.
|
|
78
|
+
archive.onMessage(({ envelope }) => {
|
|
79
|
+
// Decrypt the envelope in the application, then acknowledge with a signed receipt.
|
|
80
|
+
})
|
|
81
|
+
await archive.start({ account, deviceId })
|
|
82
|
+
await archive.requestInbox(signedInboxRequest)
|
|
83
|
+
|
|
84
|
+
// The envelope must contain ciphertext, recipient account/device, timestamp,
|
|
85
|
+
// signature, and either `network` or a network-bound device certificate.
|
|
86
|
+
await archive.send(encryptedEnvelope)
|
|
87
|
+
await archive.acknowledge(signedDeliveryReceipt)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Inbox requests and receipts must be signed and verified by the application.
|
|
91
|
+
Archives forward only ciphertext and never decrypt messages. Disabling archive
|
|
92
|
+
mode deletes this network's locally archived copies; it does not revoke or
|
|
93
|
+
erase copies on other nodes. Delivery and best-effort deletion depend on
|
|
94
|
+
reachable peers.
|
|
95
|
+
|
|
51
96
|
## API
|
|
52
97
|
|
|
53
98
|
### addRequestHandler
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { L as LittlePubSub, d as deflate_1, i as inflate_1, c as createDebugger } from './peernet-
|
|
1
|
+
import { L as LittlePubSub, d as deflate_1, i as inflate_1, c as createDebugger } from './peernet-DjddqiXc.js';
|
|
2
2
|
import './identity-Cf8KKnho.js';
|
|
3
3
|
import 'crypto';
|
|
4
4
|
import './value-C3vAp-wb.js';
|
|
@@ -7090,23 +7090,23 @@ const getTokens = (proto) => {
|
|
|
7090
7090
|
return tokens;
|
|
7091
7091
|
};
|
|
7092
7092
|
|
|
7093
|
-
const encoder = new TextEncoder();
|
|
7093
|
+
const encoder$1 = new TextEncoder();
|
|
7094
7094
|
const toType = (data) => {
|
|
7095
7095
|
if (data instanceof Uint8Array)
|
|
7096
7096
|
return data;
|
|
7097
7097
|
if (data instanceof ArrayBuffer)
|
|
7098
7098
|
return new Uint8Array(data);
|
|
7099
7099
|
if (typeof data === 'bigint')
|
|
7100
|
-
return encoder.encode(data.toString());
|
|
7100
|
+
return encoder$1.encode(data.toString());
|
|
7101
7101
|
if (typeof data === 'string')
|
|
7102
|
-
return encoder.encode(data);
|
|
7102
|
+
return encoder$1.encode(data);
|
|
7103
7103
|
if (typeof data === 'object')
|
|
7104
|
-
return encoder.encode(JSON.stringify(data, jsonStringifyBigInt$1));
|
|
7104
|
+
return encoder$1.encode(JSON.stringify(data, jsonStringifyBigInt$1));
|
|
7105
7105
|
if (typeof data === 'number' || typeof data === 'boolean')
|
|
7106
|
-
return encoder.encode(data.toString());
|
|
7106
|
+
return encoder$1.encode(data.toString());
|
|
7107
7107
|
throw new Error(`unsuported type ${typeof data || data}`);
|
|
7108
7108
|
};
|
|
7109
|
-
const encode = (proto, input, compress) => {
|
|
7109
|
+
const encode$1 = (proto, input, compress) => {
|
|
7110
7110
|
const tokens = getTokens(proto);
|
|
7111
7111
|
const set = [];
|
|
7112
7112
|
for (let i = 0; i < tokens.length; i++) {
|
|
@@ -7163,8 +7163,8 @@ var index$1 = {
|
|
|
7163
7163
|
toString: toString$1};
|
|
7164
7164
|
|
|
7165
7165
|
const { toString } = index$1;
|
|
7166
|
-
const decoder = new TextDecoder();
|
|
7167
|
-
const decode = (proto, uint8Array, compressed) => {
|
|
7166
|
+
const decoder$1 = new TextDecoder();
|
|
7167
|
+
const decode$1 = (proto, uint8Array, compressed) => {
|
|
7168
7168
|
if (compressed)
|
|
7169
7169
|
uint8Array = pako.inflate(uint8Array);
|
|
7170
7170
|
let deconcated = index$4(uint8Array);
|
|
@@ -7179,13 +7179,13 @@ const decode = (proto, uint8Array, compressed) => {
|
|
|
7179
7179
|
else if (token.type === 'string')
|
|
7180
7180
|
output[token.key] = toString(deconcated[i]);
|
|
7181
7181
|
else if (token.type === 'boolean')
|
|
7182
|
-
output[token.key] = decoder.decode(deconcated[i]) === 'true';
|
|
7182
|
+
output[token.key] = decoder$1.decode(deconcated[i]) === 'true';
|
|
7183
7183
|
else if (token.type === 'number')
|
|
7184
|
-
output[token.key] = Number(decoder.decode(deconcated[i]));
|
|
7184
|
+
output[token.key] = Number(decoder$1.decode(deconcated[i]));
|
|
7185
7185
|
else if (token.type === 'bigint')
|
|
7186
|
-
output[token.key] = BigInt(decoder.decode(deconcated[i]));
|
|
7186
|
+
output[token.key] = BigInt(decoder$1.decode(deconcated[i]));
|
|
7187
7187
|
else if (token.type === 'object' || token.type === 'array')
|
|
7188
|
-
output[token.key] = JSON.parse(decoder.decode(deconcated[i]), jsonParseBigInt$1);
|
|
7188
|
+
output[token.key] = JSON.parse(decoder$1.decode(deconcated[i]), jsonParseBigInt$1);
|
|
7189
7189
|
if (token.optional) {
|
|
7190
7190
|
if (!output[token.key] || output[token.key].length === 0)
|
|
7191
7191
|
delete output[token.key];
|
|
@@ -7196,7 +7196,7 @@ const decode = (proto, uint8Array, compressed) => {
|
|
|
7196
7196
|
return output;
|
|
7197
7197
|
};
|
|
7198
7198
|
|
|
7199
|
-
var index = { encode, decode };
|
|
7199
|
+
var index = { encode: encode$1, decode: decode$1 };
|
|
7200
7200
|
|
|
7201
7201
|
const jsonStringifyBigInt = (key, value) => {
|
|
7202
7202
|
if (typeof value === 'bigint')
|
|
@@ -8662,6 +8662,439 @@ class FileTransfer {
|
|
|
8662
8662
|
}
|
|
8663
8663
|
}
|
|
8664
8664
|
|
|
8665
|
+
const VERSION = 1;
|
|
8666
|
+
const MAX_PACKET_BYTES = 64_000;
|
|
8667
|
+
const MAX_ENVELOPE_BYTES = 32_000;
|
|
8668
|
+
const MAX_ARCHIVES = 500;
|
|
8669
|
+
const MAX_OUTBOX = 500;
|
|
8670
|
+
const MAX_AGE = 30 * 24 * 60 * 60 * 1000;
|
|
8671
|
+
const MAX_CLOCK_SKEW = 10 * 60 * 1000;
|
|
8672
|
+
const MAX_HOPS = 8;
|
|
8673
|
+
const encoder = new TextEncoder();
|
|
8674
|
+
const decoder = new TextDecoder();
|
|
8675
|
+
function isUnsubscribe(value) {
|
|
8676
|
+
return typeof value === 'function';
|
|
8677
|
+
}
|
|
8678
|
+
function encode(value) {
|
|
8679
|
+
return encoder.encode(JSON.stringify(value));
|
|
8680
|
+
}
|
|
8681
|
+
function decode(payload) {
|
|
8682
|
+
if (!(payload instanceof Uint8Array))
|
|
8683
|
+
throw new TypeError('Archive packets must be encoded bytes.');
|
|
8684
|
+
if (payload.byteLength > MAX_PACKET_BYTES)
|
|
8685
|
+
throw new RangeError('Archive packet exceeds the size limit.');
|
|
8686
|
+
return JSON.parse(decoder.decode(payload));
|
|
8687
|
+
}
|
|
8688
|
+
function randomId() {
|
|
8689
|
+
if (typeof crypto.randomUUID === 'function')
|
|
8690
|
+
return crypto.randomUUID();
|
|
8691
|
+
return Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
8692
|
+
}
|
|
8693
|
+
function keyPrefix(network) {
|
|
8694
|
+
return `peernet:archive:v1:${encodeURIComponent(network)}:`;
|
|
8695
|
+
}
|
|
8696
|
+
function recordKey(network, kind, id) {
|
|
8697
|
+
return `${keyPrefix(network)}${kind}:${encodeURIComponent(id)}`;
|
|
8698
|
+
}
|
|
8699
|
+
function stateKey(network) {
|
|
8700
|
+
return `${keyPrefix(network)}state`;
|
|
8701
|
+
}
|
|
8702
|
+
async function storeGet(store, key) {
|
|
8703
|
+
if (store.has && !(await store.has(key)))
|
|
8704
|
+
return undefined;
|
|
8705
|
+
try {
|
|
8706
|
+
return await store.get(key);
|
|
8707
|
+
}
|
|
8708
|
+
catch (error) {
|
|
8709
|
+
if (error?.code === 'LEVEL_NOT_FOUND' ||
|
|
8710
|
+
error?.name === 'NotFoundError')
|
|
8711
|
+
return undefined;
|
|
8712
|
+
throw error;
|
|
8713
|
+
}
|
|
8714
|
+
}
|
|
8715
|
+
function validateEnvelope(envelope, network) {
|
|
8716
|
+
const envelopeNetwork = envelope?.network ||
|
|
8717
|
+
envelope?.senderCertificate?.network ||
|
|
8718
|
+
envelope?.recipientCertificate?.network;
|
|
8719
|
+
if (!envelope ||
|
|
8720
|
+
typeof envelope !== 'object' ||
|
|
8721
|
+
envelopeNetwork !== network ||
|
|
8722
|
+
typeof envelope.id !== 'string' ||
|
|
8723
|
+
!envelope.id ||
|
|
8724
|
+
typeof envelope.recipientAccount !== 'string' ||
|
|
8725
|
+
typeof envelope.recipientDeviceId !== 'string' ||
|
|
8726
|
+
typeof envelope.ciphertext !== 'string' ||
|
|
8727
|
+
!envelope.ciphertext ||
|
|
8728
|
+
typeof envelope.signature !== 'string' ||
|
|
8729
|
+
!Number.isFinite(envelope.timestamp) ||
|
|
8730
|
+
envelope.timestamp < Date.now() - MAX_AGE ||
|
|
8731
|
+
envelope.timestamp > Date.now() + MAX_CLOCK_SKEW)
|
|
8732
|
+
throw new Error('Only a valid, network-scoped encrypted message envelope can be archived.');
|
|
8733
|
+
if ('text' in envelope || 'plaintext' in envelope || 'message' in envelope)
|
|
8734
|
+
throw new Error('Plaintext message fields cannot be archived.');
|
|
8735
|
+
if (encode(envelope).byteLength > MAX_ENVELOPE_BYTES)
|
|
8736
|
+
throw new RangeError('Encrypted message envelope exceeds the archive size limit.');
|
|
8737
|
+
}
|
|
8738
|
+
function validatePacket(value, network) {
|
|
8739
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
8740
|
+
return;
|
|
8741
|
+
const packet = value;
|
|
8742
|
+
if (packet.version !== VERSION ||
|
|
8743
|
+
packet.network !== network ||
|
|
8744
|
+
typeof packet.id !== 'string' ||
|
|
8745
|
+
!packet.id ||
|
|
8746
|
+
typeof packet.origin !== 'string' ||
|
|
8747
|
+
!packet.origin ||
|
|
8748
|
+
!Number.isFinite(packet.timestamp) ||
|
|
8749
|
+
packet.timestamp < Date.now() - MAX_AGE ||
|
|
8750
|
+
packet.timestamp > Date.now() + MAX_CLOCK_SKEW ||
|
|
8751
|
+
!Number.isInteger(packet.hopsRemaining) ||
|
|
8752
|
+
packet.hopsRemaining < 0 ||
|
|
8753
|
+
packet.hopsRemaining > MAX_HOPS ||
|
|
8754
|
+
!['direct', 'inbox-request', 'archive-delivery', 'delivery-receipt'].includes(packet.type) ||
|
|
8755
|
+
!packet.payload ||
|
|
8756
|
+
typeof packet.payload !== 'object' ||
|
|
8757
|
+
Array.isArray(packet.payload))
|
|
8758
|
+
return;
|
|
8759
|
+
return packet;
|
|
8760
|
+
}
|
|
8761
|
+
class PeernetArchiveMode {
|
|
8762
|
+
#peernet;
|
|
8763
|
+
#store;
|
|
8764
|
+
#network;
|
|
8765
|
+
#topic;
|
|
8766
|
+
#verifyEnvelope;
|
|
8767
|
+
#verifyInboxRequest;
|
|
8768
|
+
#verifyReceipt;
|
|
8769
|
+
#handlers = new Set();
|
|
8770
|
+
#seen = new Map();
|
|
8771
|
+
#archives = new Set();
|
|
8772
|
+
#outbox = new Set();
|
|
8773
|
+
#enabled = false;
|
|
8774
|
+
#started = false;
|
|
8775
|
+
#unsubscribe;
|
|
8776
|
+
#retryTimer;
|
|
8777
|
+
#account = '';
|
|
8778
|
+
#deviceId = '';
|
|
8779
|
+
constructor(options) {
|
|
8780
|
+
if (!options?.peernet?.network || typeof options.peernet.publish !== 'function' ||
|
|
8781
|
+
typeof options.peernet.subscribe !== 'function')
|
|
8782
|
+
throw new Error('A Peernet instance is required for archive mode.');
|
|
8783
|
+
const store = options.store || globalThis.messageStore;
|
|
8784
|
+
if (!store || typeof store.get !== 'function' || typeof store.put !== 'function' ||
|
|
8785
|
+
typeof store.delete !== 'function' || typeof store.keys !== 'function')
|
|
8786
|
+
throw new Error('A persistent Peernet message store is required for archive mode.');
|
|
8787
|
+
if (typeof options.verifyEnvelope !== 'function' ||
|
|
8788
|
+
typeof options.verifyInboxRequest !== 'function' ||
|
|
8789
|
+
typeof options.verifyReceipt !== 'function')
|
|
8790
|
+
throw new Error('Archive mode requires envelope, inbox-request and receipt verification functions.');
|
|
8791
|
+
this.#peernet = options.peernet;
|
|
8792
|
+
this.#store = store;
|
|
8793
|
+
this.#network = options.peernet.network;
|
|
8794
|
+
this.#topic = `peernet:archive:v1:${this.#network}`;
|
|
8795
|
+
this.#verifyEnvelope = options.verifyEnvelope;
|
|
8796
|
+
this.#verifyInboxRequest = options.verifyInboxRequest;
|
|
8797
|
+
this.#verifyReceipt = options.verifyReceipt;
|
|
8798
|
+
}
|
|
8799
|
+
get network() {
|
|
8800
|
+
return this.#network;
|
|
8801
|
+
}
|
|
8802
|
+
get enabled() {
|
|
8803
|
+
return this.#enabled;
|
|
8804
|
+
}
|
|
8805
|
+
get archiveCount() {
|
|
8806
|
+
return this.#archives.size;
|
|
8807
|
+
}
|
|
8808
|
+
get pendingCount() {
|
|
8809
|
+
return this.#outbox.size;
|
|
8810
|
+
}
|
|
8811
|
+
onMessage(handler) {
|
|
8812
|
+
if (typeof handler !== 'function')
|
|
8813
|
+
throw new TypeError('An archive message handler is required.');
|
|
8814
|
+
this.#handlers.add(handler);
|
|
8815
|
+
return () => this.#handlers.delete(handler);
|
|
8816
|
+
}
|
|
8817
|
+
async start({ account, deviceId }) {
|
|
8818
|
+
if (this.#started)
|
|
8819
|
+
return;
|
|
8820
|
+
if (!account || !deviceId)
|
|
8821
|
+
throw new Error('A local account and chat device ID are required.');
|
|
8822
|
+
this.#account = account;
|
|
8823
|
+
this.#deviceId = deviceId;
|
|
8824
|
+
await this.#load();
|
|
8825
|
+
const receiver = (payload) => {
|
|
8826
|
+
this.#receive(payload).catch((error) => console.error('Peernet archive packet failed:', error));
|
|
8827
|
+
};
|
|
8828
|
+
const unsubscribe = await this.#peernet.subscribe(this.#topic, receiver);
|
|
8829
|
+
this.#unsubscribe = isUnsubscribe(unsubscribe)
|
|
8830
|
+
? unsubscribe
|
|
8831
|
+
: typeof globalThis.globalSub?.unsubscribe === 'function'
|
|
8832
|
+
? () => globalThis.globalSub.unsubscribe(this.#topic, receiver)
|
|
8833
|
+
: undefined;
|
|
8834
|
+
this.#started = true;
|
|
8835
|
+
this.#retryTimer = setInterval(() => {
|
|
8836
|
+
this.#retryOutbox().catch((error) => console.error('Peernet archive retry failed:', error));
|
|
8837
|
+
}, 30_000);
|
|
8838
|
+
try {
|
|
8839
|
+
for (const id of this.#outbox) {
|
|
8840
|
+
const bytes = await storeGet(this.#store, recordKey(this.#network, 'outbox', id));
|
|
8841
|
+
if (bytes)
|
|
8842
|
+
await this.#publish('direct', { envelope: JSON.parse(decoder.decode(bytes)) });
|
|
8843
|
+
}
|
|
8844
|
+
}
|
|
8845
|
+
catch (error) {
|
|
8846
|
+
this.stop();
|
|
8847
|
+
throw error;
|
|
8848
|
+
}
|
|
8849
|
+
}
|
|
8850
|
+
stop() {
|
|
8851
|
+
clearInterval(this.#retryTimer);
|
|
8852
|
+
this.#retryTimer = undefined;
|
|
8853
|
+
this.#unsubscribe?.();
|
|
8854
|
+
this.#unsubscribe = undefined;
|
|
8855
|
+
this.#started = false;
|
|
8856
|
+
this.#account = '';
|
|
8857
|
+
this.#deviceId = '';
|
|
8858
|
+
}
|
|
8859
|
+
async setEnabled(enabled) {
|
|
8860
|
+
if (typeof enabled !== 'boolean')
|
|
8861
|
+
throw new TypeError('Archive mode must be enabled or disabled explicitly.');
|
|
8862
|
+
if (!this.#started)
|
|
8863
|
+
await this.#load();
|
|
8864
|
+
this.#enabled = enabled;
|
|
8865
|
+
await this.#store.put(stateKey(this.#network), encode({ version: VERSION, enabled }));
|
|
8866
|
+
if (!enabled) {
|
|
8867
|
+
for (const id of this.#archives)
|
|
8868
|
+
await this.#store.delete(recordKey(this.#network, 'archive', id));
|
|
8869
|
+
this.#archives.clear();
|
|
8870
|
+
}
|
|
8871
|
+
}
|
|
8872
|
+
async send(envelope) {
|
|
8873
|
+
validateEnvelope(envelope, this.#network);
|
|
8874
|
+
if (!(await this.#verifyEnvelope(envelope)))
|
|
8875
|
+
throw new Error('Encrypted message signature verification failed.');
|
|
8876
|
+
if (this.#outbox.has(envelope.id))
|
|
8877
|
+
throw new Error('This encrypted message is already pending.');
|
|
8878
|
+
await this.#store.put(recordKey(this.#network, 'outbox', envelope.id), encode(envelope));
|
|
8879
|
+
this.#outbox.add(envelope.id);
|
|
8880
|
+
if (this.#outbox.size > MAX_OUTBOX)
|
|
8881
|
+
await this.#dropOldest('outbox');
|
|
8882
|
+
await this.#publish('direct', { envelope });
|
|
8883
|
+
}
|
|
8884
|
+
async requestInbox(request) {
|
|
8885
|
+
if (request.network !== this.#network ||
|
|
8886
|
+
request.account !== this.#account ||
|
|
8887
|
+
request.deviceId !== this.#deviceId ||
|
|
8888
|
+
!(await this.#validInboxRequest(request)))
|
|
8889
|
+
throw new Error('A valid signed inbox request for this Peernet network and device is required.');
|
|
8890
|
+
await this.#publish('inbox-request', { request });
|
|
8891
|
+
}
|
|
8892
|
+
async acknowledge(receipt) {
|
|
8893
|
+
if (receipt.network !== this.#network ||
|
|
8894
|
+
receipt.recipientAccount !== this.#account ||
|
|
8895
|
+
receipt.recipientDeviceId !== this.#deviceId ||
|
|
8896
|
+
typeof receipt.signature !== 'string' ||
|
|
8897
|
+
!Number.isFinite(receipt.timestamp) ||
|
|
8898
|
+
Math.abs(Date.now() - receipt.timestamp) > MAX_CLOCK_SKEW)
|
|
8899
|
+
throw new Error('Delivery receipt does not belong to this Peernet network and device.');
|
|
8900
|
+
await this.#publish('delivery-receipt', { receipt });
|
|
8901
|
+
}
|
|
8902
|
+
async #load() {
|
|
8903
|
+
const state = await storeGet(this.#store, stateKey(this.#network));
|
|
8904
|
+
if (state) {
|
|
8905
|
+
const saved = JSON.parse(decoder.decode(state));
|
|
8906
|
+
if (saved?.version !== VERSION || typeof saved.enabled !== 'boolean')
|
|
8907
|
+
throw new Error('Saved Peernet archive settings are invalid.');
|
|
8908
|
+
this.#enabled = saved.enabled;
|
|
8909
|
+
}
|
|
8910
|
+
const prefix = keyPrefix(this.#network);
|
|
8911
|
+
const keys = await this.#store.keys();
|
|
8912
|
+
for (const keyInput of keys) {
|
|
8913
|
+
const key = keyInput.toString().replace(/^\/+/, '');
|
|
8914
|
+
if (!key.startsWith(prefix))
|
|
8915
|
+
continue;
|
|
8916
|
+
const archiveMarker = `${prefix}archive:`;
|
|
8917
|
+
const outboxMarker = `${prefix}outbox:`;
|
|
8918
|
+
if (key.startsWith(archiveMarker))
|
|
8919
|
+
this.#archives.add(decodeURIComponent(key.slice(archiveMarker.length)));
|
|
8920
|
+
if (key.startsWith(outboxMarker))
|
|
8921
|
+
this.#outbox.add(decodeURIComponent(key.slice(outboxMarker.length)));
|
|
8922
|
+
}
|
|
8923
|
+
await this.#prune();
|
|
8924
|
+
}
|
|
8925
|
+
async #publish(type, payload, hopsRemaining = MAX_HOPS) {
|
|
8926
|
+
if (!this.#started)
|
|
8927
|
+
throw new Error('Peernet archive mode is not started.');
|
|
8928
|
+
const packet = {
|
|
8929
|
+
version: VERSION,
|
|
8930
|
+
id: randomId(),
|
|
8931
|
+
network: this.#network,
|
|
8932
|
+
origin: this.#peernet.id,
|
|
8933
|
+
timestamp: Date.now(),
|
|
8934
|
+
hopsRemaining,
|
|
8935
|
+
type,
|
|
8936
|
+
payload
|
|
8937
|
+
};
|
|
8938
|
+
const bytes = encode(packet);
|
|
8939
|
+
if (bytes.byteLength > MAX_PACKET_BYTES)
|
|
8940
|
+
throw new RangeError('Peernet archive packet exceeds its size limit.');
|
|
8941
|
+
this.#seen.set(packet.id, packet.timestamp);
|
|
8942
|
+
await this.#peernet.publish(this.#topic, bytes);
|
|
8943
|
+
}
|
|
8944
|
+
async #receive(payload) {
|
|
8945
|
+
const packet = validatePacket(decode(payload), this.#network);
|
|
8946
|
+
if (!packet || packet.origin === this.#peernet.id || this.#wasSeen(packet.id))
|
|
8947
|
+
return;
|
|
8948
|
+
this.#markSeen(packet.id);
|
|
8949
|
+
let forward = false;
|
|
8950
|
+
switch (packet.type) {
|
|
8951
|
+
case 'direct':
|
|
8952
|
+
case 'archive-delivery': {
|
|
8953
|
+
const envelope = packet.payload.envelope;
|
|
8954
|
+
try {
|
|
8955
|
+
validateEnvelope(envelope, this.#network);
|
|
8956
|
+
}
|
|
8957
|
+
catch {
|
|
8958
|
+
break;
|
|
8959
|
+
}
|
|
8960
|
+
if (!(await this.#verifyEnvelope(envelope)))
|
|
8961
|
+
break;
|
|
8962
|
+
forward = true;
|
|
8963
|
+
if (packet.type === 'direct' && this.#enabled)
|
|
8964
|
+
await this.#saveArchive(envelope);
|
|
8965
|
+
if (envelope.recipientAccount === this.#account && envelope.recipientDeviceId === this.#deviceId) {
|
|
8966
|
+
for (const handler of this.#handlers)
|
|
8967
|
+
handler({ type: 'message', envelope });
|
|
8968
|
+
}
|
|
8969
|
+
break;
|
|
8970
|
+
}
|
|
8971
|
+
case 'inbox-request': {
|
|
8972
|
+
const request = packet.payload.request;
|
|
8973
|
+
if (!(await this.#validInboxRequest(request)))
|
|
8974
|
+
break;
|
|
8975
|
+
forward = true;
|
|
8976
|
+
if (this.#enabled && !(request.account === this.#account && request.deviceId === this.#deviceId))
|
|
8977
|
+
await this.#deliverTo(request);
|
|
8978
|
+
break;
|
|
8979
|
+
}
|
|
8980
|
+
case 'delivery-receipt': {
|
|
8981
|
+
const receipt = packet.payload.receipt;
|
|
8982
|
+
forward = await this.#receiveReceipt(receipt);
|
|
8983
|
+
break;
|
|
8984
|
+
}
|
|
8985
|
+
}
|
|
8986
|
+
if (forward && packet.hopsRemaining > 0) {
|
|
8987
|
+
await this.#peernet.publish(this.#topic, encode({ ...packet, hopsRemaining: packet.hopsRemaining - 1 }));
|
|
8988
|
+
}
|
|
8989
|
+
}
|
|
8990
|
+
async #validInboxRequest(request) {
|
|
8991
|
+
return Boolean(request &&
|
|
8992
|
+
request.network === this.#network &&
|
|
8993
|
+
typeof request.account === 'string' &&
|
|
8994
|
+
typeof request.deviceId === 'string' &&
|
|
8995
|
+
Number.isFinite(request.timestamp) &&
|
|
8996
|
+
Math.abs(Date.now() - request.timestamp) <= MAX_CLOCK_SKEW &&
|
|
8997
|
+
typeof request.signature === 'string' &&
|
|
8998
|
+
await this.#verifyInboxRequest(request));
|
|
8999
|
+
}
|
|
9000
|
+
async #saveArchive(envelope) {
|
|
9001
|
+
if (this.#archives.has(envelope.id))
|
|
9002
|
+
return;
|
|
9003
|
+
await this.#store.put(recordKey(this.#network, 'archive', envelope.id), encode({
|
|
9004
|
+
envelope,
|
|
9005
|
+
storedAt: Date.now()
|
|
9006
|
+
}));
|
|
9007
|
+
this.#archives.add(envelope.id);
|
|
9008
|
+
if (this.#archives.size > MAX_ARCHIVES)
|
|
9009
|
+
await this.#dropOldest('archive');
|
|
9010
|
+
}
|
|
9011
|
+
async #deliverTo(request) {
|
|
9012
|
+
for (const id of this.#archives) {
|
|
9013
|
+
const bytes = await storeGet(this.#store, recordKey(this.#network, 'archive', id));
|
|
9014
|
+
if (!bytes) {
|
|
9015
|
+
this.#archives.delete(id);
|
|
9016
|
+
continue;
|
|
9017
|
+
}
|
|
9018
|
+
const record = JSON.parse(decoder.decode(bytes));
|
|
9019
|
+
const envelope = record?.envelope;
|
|
9020
|
+
if (envelope?.recipientAccount === request.account && envelope.recipientDeviceId === request.deviceId)
|
|
9021
|
+
await this.#publish('archive-delivery', { envelope });
|
|
9022
|
+
}
|
|
9023
|
+
}
|
|
9024
|
+
async #receiveReceipt(receipt) {
|
|
9025
|
+
if (!receipt || receipt.network !== this.#network ||
|
|
9026
|
+
typeof receipt.messageId !== 'string' ||
|
|
9027
|
+
!Number.isFinite(receipt.timestamp) ||
|
|
9028
|
+
Math.abs(Date.now() - receipt.timestamp) > MAX_CLOCK_SKEW)
|
|
9029
|
+
return false;
|
|
9030
|
+
const keys = ['archive', 'outbox'];
|
|
9031
|
+
let verified = false;
|
|
9032
|
+
for (const kind of keys) {
|
|
9033
|
+
const key = recordKey(this.#network, kind, receipt.messageId);
|
|
9034
|
+
const bytes = await storeGet(this.#store, key);
|
|
9035
|
+
if (!bytes)
|
|
9036
|
+
continue;
|
|
9037
|
+
const stored = JSON.parse(decoder.decode(bytes));
|
|
9038
|
+
const envelope = kind === 'archive' ? stored.envelope : stored;
|
|
9039
|
+
if (!(await this.#verifyReceipt(receipt, envelope)))
|
|
9040
|
+
continue;
|
|
9041
|
+
await this.#store.delete(key);
|
|
9042
|
+
(kind === 'archive' ? this.#archives : this.#outbox).delete(receipt.messageId);
|
|
9043
|
+
verified = true;
|
|
9044
|
+
}
|
|
9045
|
+
return verified;
|
|
9046
|
+
}
|
|
9047
|
+
async #retryOutbox() {
|
|
9048
|
+
if (!this.#started)
|
|
9049
|
+
return;
|
|
9050
|
+
await this.#prune();
|
|
9051
|
+
for (const id of this.#outbox) {
|
|
9052
|
+
const bytes = await storeGet(this.#store, recordKey(this.#network, 'outbox', id));
|
|
9053
|
+
if (bytes)
|
|
9054
|
+
await this.#publish('direct', { envelope: JSON.parse(decoder.decode(bytes)) });
|
|
9055
|
+
}
|
|
9056
|
+
}
|
|
9057
|
+
async #prune() {
|
|
9058
|
+
const cutoff = Date.now() - MAX_AGE;
|
|
9059
|
+
for (const [kind, ids] of [['archive', this.#archives], ['outbox', this.#outbox]]) {
|
|
9060
|
+
for (const id of ids) {
|
|
9061
|
+
const key = recordKey(this.#network, kind, id);
|
|
9062
|
+
const bytes = await storeGet(this.#store, key);
|
|
9063
|
+
if (!bytes) {
|
|
9064
|
+
ids.delete(id);
|
|
9065
|
+
continue;
|
|
9066
|
+
}
|
|
9067
|
+
const saved = JSON.parse(decoder.decode(bytes));
|
|
9068
|
+
const timestamp = kind === 'archive' ? saved.storedAt : saved.timestamp;
|
|
9069
|
+
if (!Number.isFinite(timestamp) || timestamp < cutoff) {
|
|
9070
|
+
await this.#store.delete(key);
|
|
9071
|
+
ids.delete(id);
|
|
9072
|
+
}
|
|
9073
|
+
}
|
|
9074
|
+
}
|
|
9075
|
+
}
|
|
9076
|
+
async #dropOldest(kind) {
|
|
9077
|
+
const ids = kind === 'archive' ? this.#archives : this.#outbox;
|
|
9078
|
+
const id = ids.values().next().value;
|
|
9079
|
+
if (!id)
|
|
9080
|
+
return;
|
|
9081
|
+
await this.#store.delete(recordKey(this.#network, kind, id));
|
|
9082
|
+
ids.delete(id);
|
|
9083
|
+
}
|
|
9084
|
+
#wasSeen(id) {
|
|
9085
|
+
const timestamp = this.#seen.get(id);
|
|
9086
|
+
return timestamp !== undefined && Date.now() - timestamp < MAX_AGE;
|
|
9087
|
+
}
|
|
9088
|
+
#markSeen(id) {
|
|
9089
|
+
this.#seen.set(id, Date.now());
|
|
9090
|
+
if (this.#seen.size > 4_000) {
|
|
9091
|
+
const oldestId = this.#seen.keys().next().value;
|
|
9092
|
+
if (oldestId !== undefined)
|
|
9093
|
+
this.#seen.delete(oldestId);
|
|
9094
|
+
}
|
|
9095
|
+
}
|
|
9096
|
+
}
|
|
9097
|
+
|
|
8665
9098
|
const DEFAULT_CHUNK_SIZE = 256 * 1024;
|
|
8666
9099
|
const DEFAULT_BLOCK_CHUNK_SIZE = 256 * 1024;
|
|
8667
9100
|
const DEFAULT_BLOCK_CHUNK_THRESHOLD = 1024 * 1024;
|
|
@@ -8688,7 +9121,7 @@ class Peernet {
|
|
|
8688
9121
|
root;
|
|
8689
9122
|
identity;
|
|
8690
9123
|
stores = [];
|
|
8691
|
-
|
|
9124
|
+
routingId;
|
|
8692
9125
|
/**
|
|
8693
9126
|
* @type {Object}
|
|
8694
9127
|
* @property {Object} peer Instance of Peer
|
|
@@ -8723,6 +9156,7 @@ class Peernet {
|
|
|
8723
9156
|
* @param {String} options.root - path to root directory
|
|
8724
9157
|
* @param {String} options.version - path to root directory
|
|
8725
9158
|
* @param {String} options.storePrefix - prefix for datatores (lfc)
|
|
9159
|
+
* @param {String} options.routingId - optional transport identity, separate from the wallet identity
|
|
8726
9160
|
* @param {Object} options.transport - transport selection and fallback options passed to @netpeer/swarm
|
|
8727
9161
|
*
|
|
8728
9162
|
* @return {Promise} instance of Peernet
|
|
@@ -8735,6 +9169,9 @@ class Peernet {
|
|
|
8735
9169
|
* @property {String} network - current network
|
|
8736
9170
|
*/
|
|
8737
9171
|
this.network = options.network || 'leofcoin';
|
|
9172
|
+
if (options.routingId !== undefined && (typeof options.routingId !== 'string' || !options.routingId.trim()))
|
|
9173
|
+
throw new TypeError('routingId must be a non-empty string');
|
|
9174
|
+
this.routingId = options.routingId?.trim();
|
|
8738
9175
|
this.autoStart = options.autoStart === undefined ? true : options.autoStart;
|
|
8739
9176
|
this.stars = options.stars;
|
|
8740
9177
|
this.transport = options.transport;
|
|
@@ -8766,6 +9203,9 @@ class Peernet {
|
|
|
8766
9203
|
return this._init(options, password);
|
|
8767
9204
|
}
|
|
8768
9205
|
get id() {
|
|
9206
|
+
return this.routingId || this.identity.id;
|
|
9207
|
+
}
|
|
9208
|
+
get peerId() {
|
|
8769
9209
|
return this.identity.id;
|
|
8770
9210
|
}
|
|
8771
9211
|
get selectedAccount() {
|
|
@@ -8787,6 +9227,9 @@ class Peernet {
|
|
|
8787
9227
|
addCodec(codec) {
|
|
8788
9228
|
return utils.addCodec(codec);
|
|
8789
9229
|
}
|
|
9230
|
+
createArchiveMode(options) {
|
|
9231
|
+
return new PeernetArchiveMode({ ...options, peernet: this });
|
|
9232
|
+
}
|
|
8790
9233
|
async addStore(name, prefix, root, isPrivate = true) {
|
|
8791
9234
|
if (name === 'block' ||
|
|
8792
9235
|
name === 'transaction' ||
|
|
@@ -8840,7 +9283,7 @@ class Peernet {
|
|
|
8840
9283
|
await getAddress();
|
|
8841
9284
|
this.storePrefix = options.storePrefix;
|
|
8842
9285
|
this.root = options.root;
|
|
8843
|
-
const { RequestMessage, ResponseMessage, PeerMessage, PeerMessageResponse, PeernetMessage, DHTMessage, DHTMessageResponse, DataMessage, DataMessageResponse, PsMessage, ChatMessage, PeernetFile } = await import(/* webpackChunkName: "messages" */ './messages-
|
|
9286
|
+
const { RequestMessage, ResponseMessage, PeerMessage, PeerMessageResponse, PeernetMessage, DHTMessage, DHTMessageResponse, DataMessage, DataMessageResponse, PsMessage, ChatMessage, PeernetFile } = await import(/* webpackChunkName: "messages" */ './messages-DCePyqKC.js');
|
|
8844
9287
|
/**
|
|
8845
9288
|
* proto Object containing protos
|
|
8846
9289
|
* @type {Object}
|
|
@@ -8873,7 +9316,6 @@ class Peernet {
|
|
|
8873
9316
|
this.identity = new Identity(this.network);
|
|
8874
9317
|
await this.identity.load(password, options.freshIdentity === true);
|
|
8875
9318
|
this._peerHandler = new PeerDiscovery(this.id);
|
|
8876
|
-
this.peerId = this.id;
|
|
8877
9319
|
/**
|
|
8878
9320
|
* converts data -> message -> proto
|
|
8879
9321
|
* @see DataHandler
|
|
@@ -8887,7 +9329,7 @@ class Peernet {
|
|
|
8887
9329
|
if (this.#starting || this.#started)
|
|
8888
9330
|
return;
|
|
8889
9331
|
this.#starting = true;
|
|
8890
|
-
const importee = await import('./client-
|
|
9332
|
+
const importee = await import('./client-DKKzPSlb.js');
|
|
8891
9333
|
/**
|
|
8892
9334
|
* @access public
|
|
8893
9335
|
* @type {PeernetClient}
|
|
@@ -9681,4 +10123,4 @@ class Peernet {
|
|
|
9681
10123
|
}
|
|
9682
10124
|
globalThis.Peernet = Peernet;
|
|
9683
10125
|
|
|
9684
|
-
export { FormatInterface as F, LittlePubSub as L,
|
|
10126
|
+
export { FormatInterface as F, LittlePubSub as L, PeernetArchiveMode as P, FileTransfer as a, Peernet as b, createDebugger as c, deflate_1 as d, inflate_1 as i };
|
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 };
|
package/exports/store.js
CHANGED
|
@@ -102,6 +102,9 @@ class Store {
|
|
|
102
102
|
this.root = await init(root, homedir);
|
|
103
103
|
this.version = version;
|
|
104
104
|
this.db = new ClassicLevel(join(this.root, this.name), { valueEncoding: 'view' });
|
|
105
|
+
if (typeof this.db.open === 'function') {
|
|
106
|
+
await this.db.open();
|
|
107
|
+
}
|
|
105
108
|
}
|
|
106
109
|
toKeyPath(key) {
|
|
107
110
|
if (!key.isKeyPath())
|
|
@@ -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.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"scripts": {
|
|
101
101
|
"build": "rollup -c",
|
|
102
102
|
"watch": "rollup -c -w",
|
|
103
|
-
"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
104
|
"test:package": "node test/standalone-install.js",
|
|
105
105
|
"server": "discovery-swarm-webrtc --port=4000",
|
|
106
106
|
"demo": "jsproject --serve ./ --port 6868"
|