libp2r2p 0.7.0 → 0.9.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 +112 -8
- package/base16/index.js +6 -3
- package/base36/index.js +12 -8
- package/base62/index.js +15 -11
- package/base64/index.js +18 -2
- package/base93/index.js +19 -7
- package/content-key/event/index.js +40 -12
- package/double-dh/index.js +21 -6
- package/ecdh/index.js +12 -1
- package/error/index.js +32 -0
- package/event/helpers/serialize.js +31 -0
- package/event/index.js +116 -0
- package/i18n/index.js +15 -13
- package/idb/index.js +5 -3
- package/idb-queue/index.js +21 -20
- package/index.js +10 -0
- package/key/index.js +31 -18
- package/kind/index.js +244 -0
- package/nip04/index.js +47 -0
- package/nip05/index.js +61 -0
- package/nip19/index.js +176 -43
- package/nip44/helpers.js +95 -0
- package/nip44/index.js +14 -0
- package/nip44-v3/index.js +35 -16
- package/nip46/helpers/frame.js +6 -6
- package/nip46/helpers/url.js +8 -6
- package/nip46/services/bunker-signer.js +7 -6
- package/nip46/services/client.js +8 -7
- package/nip46/services/server-session.js +8 -7
- package/nip46/services/transport.js +9 -8
- package/nip96/index.js +285 -0
- package/nip98/index.js +56 -0
- package/nwt/index.js +241 -0
- package/package.json +22 -3
- package/private-channel/helpers/chunks.js +7 -6
- package/private-channel/helpers/event.js +5 -4
- package/private-channel/index.js +63 -61
- package/private-channel/services/received-chunks.js +4 -3
- package/private-message/index.js +32 -31
- package/private-messenger/index.js +313 -54
- package/private-messenger/recovery/index.js +15 -14
- package/private-messenger/services/channel-state.js +28 -7
- package/private-messenger/services/storage-maintenance.js +142 -46
- package/relay/helpers/hll.js +3 -1
- package/relay/services/query.js +3 -3
- package/relay/services/relay-connection.js +222 -121
- package/relay/services/relay-pool.js +13 -10
- package/temporary-storage/index.js +3 -1
- package/url/index.js +131 -0
- package/web-storage-queue/index.js +8 -6
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// userSigner,
|
|
4
4
|
// contentKeySigner, // optional when userSigner handles content keys internally
|
|
5
5
|
// nymSigner: optionalDefaultNymSigner,
|
|
6
|
+
// staleChannelSeconds: optionalStaleChannelRetention,
|
|
7
|
+
// identityStorageRetentionSeconds: optionalIdentityStorageRetention,
|
|
6
8
|
// channels: [{ signer: privateChannelSigner, relays, mode: 'leecher', seeders: optionalSeederPubkeys, offlineRecoverySeconds: optionalChannelOverride }],
|
|
7
9
|
// onContentKeyChange: event => reviewContentKeyUse(event),
|
|
8
10
|
// onError: err => reportPrivateMessengerError(err)
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
// - Re-watching after reload fetches the gap from lastSeenAt to now.
|
|
31
33
|
// - Browser offline/online events add explicit offline ranges with a small skew.
|
|
32
34
|
// - Recovery defaults to 7 days and can be overridden per channel; zero disables durable recovery.
|
|
33
|
-
// - Channel state not watched
|
|
35
|
+
// - Channel state not actively leased or watched within the configured stale window is pruned.
|
|
34
36
|
// - Seeders announce presence every 10min and are used for the relay-uncovered left edge of a missed range.
|
|
35
37
|
// - Configured seeders are all asked; auto-discovered seeders are capped to the 8 most recently active.
|
|
36
38
|
// - Seeder/watchtower channels store reconstructed router events in a separate IndexedDB queue and auto-reply to recovery asks.
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
|
|
40
42
|
import * as privateMessage from '../private-message/index.js'
|
|
41
43
|
import { bytesToBase64 } from '../base64/index.js'
|
|
44
|
+
import { ValidationError } from '../error/index.js'
|
|
42
45
|
import { getRelaysByPubkey, pickRelaysForPubkeys, subscribeRelayListUpdates } from '../relay/index.js'
|
|
43
46
|
import * as privateChannel from '../private-channel/index.js'
|
|
44
47
|
import { DEFAULT_RECEIVED_CHUNK_TTL_MS } from '../private-channel/services/received-chunks.js'
|
|
@@ -47,9 +50,11 @@ import { createChannelStateStore } from './services/channel-state.js'
|
|
|
47
50
|
import { DEFAULT_STALE_CHANNEL_SECONDS } from './constants/index.js'
|
|
48
51
|
import {
|
|
49
52
|
activatePrivateMessengerStorage,
|
|
53
|
+
DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS,
|
|
50
54
|
maintainPrivateMessengerStorage,
|
|
51
55
|
PRIVATE_MESSENGER_STORAGE_HEARTBEAT_MS,
|
|
52
56
|
PRIVATE_MESSENGER_STORAGE_MAINTENANCE_MS,
|
|
57
|
+
readPrivateMessengerStorage,
|
|
53
58
|
releasePrivateMessengerStorage
|
|
54
59
|
} from './services/storage-maintenance.js'
|
|
55
60
|
import {
|
|
@@ -66,6 +71,7 @@ import {
|
|
|
66
71
|
} from './recovery/index.js'
|
|
67
72
|
|
|
68
73
|
export { DEFAULT_STALE_CHANNEL_SECONDS } from './constants/index.js'
|
|
74
|
+
export { DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS } from './services/storage-maintenance.js'
|
|
69
75
|
export {
|
|
70
76
|
compactSeedNymCarriers,
|
|
71
77
|
compactSeedRouterRows,
|
|
@@ -81,6 +87,7 @@ export {
|
|
|
81
87
|
|
|
82
88
|
const DEFAULT_OFFLINE_RECOVERY_SECONDS = 7 * 24 * 60 * 60
|
|
83
89
|
const MAX_OFFLINE_RECOVERY_SECONDS = Math.floor(Number.MAX_SAFE_INTEGER / 1000)
|
|
90
|
+
const STORAGE_POLICY_BROADCAST_CHANNEL = 'libp2r2p:private-messenger:storage-policy'
|
|
84
91
|
const DEFAULT_OFFLINE_SKEW_SECONDS = 30
|
|
85
92
|
const DEFAULT_RELOAD_GAP_DELAY_MS = 500
|
|
86
93
|
const DEFAULT_SEEDER_PRESENCE_INTERVAL_MS = 10 * 60 * 1000
|
|
@@ -120,7 +127,21 @@ function nowSeconds () {
|
|
|
120
127
|
|
|
121
128
|
function normalizeOfflineRecoverySeconds (value) {
|
|
122
129
|
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_OFFLINE_RECOVERY_SECONDS) {
|
|
123
|
-
throw new
|
|
130
|
+
throw new ValidationError('INVALID_OFFLINE_RECOVERY_SECONDS')
|
|
131
|
+
}
|
|
132
|
+
return value
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeStaleChannelSeconds (value) {
|
|
136
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_OFFLINE_RECOVERY_SECONDS) {
|
|
137
|
+
throw new ValidationError('INVALID_STALE_CHANNEL_SECONDS')
|
|
138
|
+
}
|
|
139
|
+
return value
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeIdentityStorageRetentionSeconds (value) {
|
|
143
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_OFFLINE_RECOVERY_SECONDS) {
|
|
144
|
+
throw new ValidationError('INVALID_IDENTITY_STORAGE_RETENTION_SECONDS')
|
|
124
145
|
}
|
|
125
146
|
return value
|
|
126
147
|
}
|
|
@@ -130,7 +151,7 @@ function uniq (values) {
|
|
|
130
151
|
}
|
|
131
152
|
|
|
132
153
|
function normalizeAutoDeletionCapability (value) {
|
|
133
|
-
if (typeof value !== 'boolean') throw new
|
|
154
|
+
if (typeof value !== 'boolean') throw new ValidationError('AUTO_DELETION_CAPABILITY_BOOLEAN_REQUIRED')
|
|
134
155
|
return value
|
|
135
156
|
}
|
|
136
157
|
|
|
@@ -142,7 +163,11 @@ function parseJson (raw, fallback) {
|
|
|
142
163
|
try { return JSON.parse(raw || '') } catch { return fallback }
|
|
143
164
|
}
|
|
144
165
|
|
|
145
|
-
function
|
|
166
|
+
function areStateValuesEqual (left, right) {
|
|
167
|
+
return JSON.stringify(left) === JSON.stringify(right)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function doesModeStoreRecoverySeeds (mode) {
|
|
146
171
|
return mode === 'seeder' || mode === 'watchtower'
|
|
147
172
|
}
|
|
148
173
|
|
|
@@ -164,6 +189,7 @@ export class PrivateMessenger {
|
|
|
164
189
|
constructor ({
|
|
165
190
|
offlineRecoverySeconds = DEFAULT_OFFLINE_RECOVERY_SECONDS,
|
|
166
191
|
staleChannelSeconds = DEFAULT_STALE_CHANNEL_SECONDS,
|
|
192
|
+
identityStorageRetentionSeconds = DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS,
|
|
167
193
|
offlineSkewSeconds = DEFAULT_OFFLINE_SKEW_SECONDS,
|
|
168
194
|
reloadGapDelayMs = DEFAULT_RELOAD_GAP_DELAY_MS,
|
|
169
195
|
seederPresenceIntervalMs = DEFAULT_SEEDER_PRESENCE_INTERVAL_MS,
|
|
@@ -188,10 +214,12 @@ export class PrivateMessenger {
|
|
|
188
214
|
_setInterval = globalThis.setInterval.bind(globalThis),
|
|
189
215
|
_clearInterval = globalThis.clearInterval.bind(globalThis),
|
|
190
216
|
_storageSetInterval = globalThis.setInterval.bind(globalThis),
|
|
191
|
-
_storageClearInterval = globalThis.clearInterval.bind(globalThis)
|
|
217
|
+
_storageClearInterval = globalThis.clearInterval.bind(globalThis),
|
|
218
|
+
_BroadcastChannel = _indexedDB === globalThis.indexedDB ? globalThis.BroadcastChannel : undefined
|
|
192
219
|
} = {}) {
|
|
193
220
|
this.offlineRecoverySeconds = normalizeOfflineRecoverySeconds(offlineRecoverySeconds)
|
|
194
|
-
this.staleChannelSeconds = staleChannelSeconds
|
|
221
|
+
this.staleChannelSeconds = normalizeStaleChannelSeconds(staleChannelSeconds)
|
|
222
|
+
this.identityStorageRetentionSeconds = normalizeIdentityStorageRetentionSeconds(identityStorageRetentionSeconds)
|
|
195
223
|
this.offlineSkewSeconds = offlineSkewSeconds
|
|
196
224
|
this.reloadGapDelayMs = reloadGapDelayMs
|
|
197
225
|
this.seederPresenceIntervalMs = seederPresenceIntervalMs
|
|
@@ -217,6 +245,7 @@ export class PrivateMessenger {
|
|
|
217
245
|
this._clearInterval = _clearInterval
|
|
218
246
|
this._storageSetInterval = _storageSetInterval
|
|
219
247
|
this._storageClearInterval = _storageClearInterval
|
|
248
|
+
this._BroadcastChannel = _BroadcastChannel
|
|
220
249
|
|
|
221
250
|
this.userSigner = null
|
|
222
251
|
this.contentKeySigner = null
|
|
@@ -245,13 +274,17 @@ export class PrivateMessenger {
|
|
|
245
274
|
this.storageHeartbeatTimer = null
|
|
246
275
|
this.storageMaintenanceTimer = null
|
|
247
276
|
this.storageMaintenancePromise = null
|
|
277
|
+
this.storagePolicyRevision = 0
|
|
278
|
+
this.storagePolicyNeedsApply = false
|
|
279
|
+
this.storagePolicyBroadcast = null
|
|
280
|
+
this.storagePolicyRefreshTail = Promise.resolve()
|
|
248
281
|
this.closePromise = null
|
|
249
282
|
this.initSettledPromise = null
|
|
250
283
|
this.initialized = false
|
|
251
284
|
}
|
|
252
285
|
|
|
253
286
|
async init ({ userSigner, contentKeySigner, nymSigner, channels = [], relays = [], mode = 'leecher' }) {
|
|
254
|
-
if (!userSigner?.getPublicKey) throw new
|
|
287
|
+
if (!userSigner?.getPublicKey) throw new ValidationError('USER_SIGNER_REQUIRED')
|
|
255
288
|
this.assertOpen()
|
|
256
289
|
if (this.initSettledPromise) throw new Error('PRIVATE_MESSENGER_INIT_IN_PROGRESS')
|
|
257
290
|
if (this.initialized) throw new Error('PRIVATE_MESSENGER_ALREADY_INITIALIZED')
|
|
@@ -264,13 +297,20 @@ export class PrivateMessenger {
|
|
|
264
297
|
this.nymSigner = nymSigner || null
|
|
265
298
|
this.userPubkey = await userSigner.getPublicKey()
|
|
266
299
|
this.prefix = `libp2r2p:private-messenger:${this.userPubkey}`
|
|
267
|
-
await activatePrivateMessengerStorage({
|
|
300
|
+
const storageSnapshot = await activatePrivateMessengerStorage({
|
|
268
301
|
userPubkey: this.userPubkey,
|
|
269
302
|
leaseId: this.storageLeaseId,
|
|
303
|
+
activeChannelPubkeys: [],
|
|
304
|
+
storagePolicy: {
|
|
305
|
+
staleChannelSeconds: this.staleChannelSeconds,
|
|
306
|
+
identityStorageRetentionSeconds: this.identityStorageRetentionSeconds
|
|
307
|
+
},
|
|
270
308
|
indexedDB: this._indexedDB
|
|
271
309
|
})
|
|
310
|
+
this.applyStoragePolicySnapshot(storageSnapshot)
|
|
272
311
|
this.storageActive = true
|
|
273
312
|
this.lastStorageTouch = Date.now()
|
|
313
|
+
this.startStoragePolicyBroadcast()
|
|
274
314
|
this.assertOpen()
|
|
275
315
|
await PrivateMessenger.maintainStorage({
|
|
276
316
|
indexedDB: this._indexedDB,
|
|
@@ -302,13 +342,15 @@ export class PrivateMessenger {
|
|
|
302
342
|
this.assertOpen()
|
|
303
343
|
this.startStorageMaintenance()
|
|
304
344
|
this.state = { channels: await this.stateStore.load() }
|
|
305
|
-
await this.cleanupStaleChannels()
|
|
306
345
|
await this.update({ userSigner, contentKeySigner, nymSigner: this.nymSigner, channels, relays, mode })
|
|
307
346
|
await this.pruneStoredSeeds()
|
|
308
347
|
this.initialized = true
|
|
348
|
+
await this.refreshAndApplyStoragePolicy()
|
|
349
|
+
this.broadcastStoragePolicyChange()
|
|
309
350
|
return this
|
|
310
351
|
} catch (err) {
|
|
311
352
|
this.stopStorageMaintenance()
|
|
353
|
+
this.stopStoragePolicyBroadcast()
|
|
312
354
|
try { await this.queueOperationTail } catch {}
|
|
313
355
|
try { await this.stateWriteTail } catch {}
|
|
314
356
|
await Promise.allSettled([
|
|
@@ -338,10 +380,102 @@ export class PrivateMessenger {
|
|
|
338
380
|
if (this.closePromise) throw new Error('PRIVATE_MESSENGER_CLOSED')
|
|
339
381
|
}
|
|
340
382
|
|
|
383
|
+
applyStoragePolicySnapshot (snapshot) {
|
|
384
|
+
if (!snapshot) return false
|
|
385
|
+
const staleChannelSeconds = normalizeStaleChannelSeconds(snapshot.staleChannelSeconds)
|
|
386
|
+
const identityStorageRetentionSeconds = normalizeIdentityStorageRetentionSeconds(
|
|
387
|
+
snapshot.identityStorageRetentionSeconds
|
|
388
|
+
)
|
|
389
|
+
const policyRevision = Math.max(0, Number(snapshot.policyRevision) || 0)
|
|
390
|
+
const changed = this.staleChannelSeconds !== staleChannelSeconds ||
|
|
391
|
+
this.identityStorageRetentionSeconds !== identityStorageRetentionSeconds
|
|
392
|
+
this.staleChannelSeconds = staleChannelSeconds
|
|
393
|
+
this.identityStorageRetentionSeconds = identityStorageRetentionSeconds
|
|
394
|
+
this.storagePolicyRevision = policyRevision
|
|
395
|
+
if (changed && this.initialized) this.storagePolicyNeedsApply = true
|
|
396
|
+
return changed
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
startStoragePolicyBroadcast () {
|
|
400
|
+
if (this.storagePolicyBroadcast || typeof this._BroadcastChannel !== 'function') return
|
|
401
|
+
try {
|
|
402
|
+
const channel = new this._BroadcastChannel(STORAGE_POLICY_BROADCAST_CHANNEL)
|
|
403
|
+
channel.unref?.()
|
|
404
|
+
channel.onmessage = event => {
|
|
405
|
+
const message = event?.data
|
|
406
|
+
if (message?.userPubkey !== this.userPubkey) return
|
|
407
|
+
if (!Number.isSafeInteger(message.policyRevision) || message.policyRevision <= (this.storagePolicyRevision || 0)) return
|
|
408
|
+
this.refreshAndApplyStoragePolicy().catch(err => {
|
|
409
|
+
try { this.onError?.(err) } catch {}
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
this.storagePolicyBroadcast = channel
|
|
413
|
+
} catch {}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
stopStoragePolicyBroadcast () {
|
|
417
|
+
const channel = this.storagePolicyBroadcast
|
|
418
|
+
this.storagePolicyBroadcast = null
|
|
419
|
+
if (!channel) return
|
|
420
|
+
channel.onmessage = null
|
|
421
|
+
channel.close?.()
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
broadcastStoragePolicyChange () {
|
|
425
|
+
try {
|
|
426
|
+
this.storagePolicyBroadcast?.postMessage({
|
|
427
|
+
userPubkey: this.userPubkey,
|
|
428
|
+
policyRevision: this.storagePolicyRevision || 0
|
|
429
|
+
})
|
|
430
|
+
} catch (err) {
|
|
431
|
+
try { this.onError?.(err) } catch {}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async readStoragePolicySnapshot () {
|
|
436
|
+
if (!this.userPubkey) return null
|
|
437
|
+
const snapshot = await readPrivateMessengerStorage({
|
|
438
|
+
userPubkey: this.userPubkey,
|
|
439
|
+
indexedDB: this._indexedDB
|
|
440
|
+
})
|
|
441
|
+
this.applyStoragePolicySnapshot(snapshot)
|
|
442
|
+
return snapshot
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async applyPendingStoragePolicy () {
|
|
446
|
+
if (!this.storagePolicyNeedsApply || !this.initialized || this.closePromise) return false
|
|
447
|
+
this.storagePolicyNeedsApply = false
|
|
448
|
+
try {
|
|
449
|
+
const channels = [...this.channels.values()]
|
|
450
|
+
await this.applyRecoveryPolicies(channels)
|
|
451
|
+
await this.cleanupStaleChannels()
|
|
452
|
+
const pubkeys = [...this.channels.keys()]
|
|
453
|
+
if (pubkeys.length) {
|
|
454
|
+
await this.unwatch(pubkeys)
|
|
455
|
+
await this.watch(pubkeys)
|
|
456
|
+
}
|
|
457
|
+
await this.reconcilePresencePublishers()
|
|
458
|
+
return true
|
|
459
|
+
} catch (err) {
|
|
460
|
+
this.storagePolicyNeedsApply = true
|
|
461
|
+
throw err
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
refreshAndApplyStoragePolicy () {
|
|
466
|
+
const previous = this.storagePolicyRefreshTail || Promise.resolve()
|
|
467
|
+
const refresh = previous.catch(() => {}).then(async () => {
|
|
468
|
+
await this.readStoragePolicySnapshot()
|
|
469
|
+
return this.applyPendingStoragePolicy()
|
|
470
|
+
})
|
|
471
|
+
this.storagePolicyRefreshTail = refresh
|
|
472
|
+
return refresh
|
|
473
|
+
}
|
|
474
|
+
|
|
341
475
|
startStorageMaintenance () {
|
|
342
476
|
if (this.storageHeartbeatTimer || this.storageMaintenanceTimer) return
|
|
343
477
|
this.storageHeartbeatTimer = this._storageSetInterval(() => (
|
|
344
|
-
this.
|
|
478
|
+
this.runStorageHeartbeat().catch(err => {
|
|
345
479
|
try { this.onError?.(err) } catch {}
|
|
346
480
|
})
|
|
347
481
|
), PRIVATE_MESSENGER_STORAGE_HEARTBEAT_MS)
|
|
@@ -362,13 +496,15 @@ export class PrivateMessenger {
|
|
|
362
496
|
|
|
363
497
|
runStorageMaintenance () {
|
|
364
498
|
if (this.storageMaintenancePromise) return this.storageMaintenancePromise
|
|
365
|
-
const maintenance =
|
|
366
|
-
PrivateMessenger.maintainStorage({
|
|
499
|
+
const maintenance = (async () => {
|
|
500
|
+
await PrivateMessenger.maintainStorage({
|
|
367
501
|
indexedDB: this._indexedDB,
|
|
368
502
|
temporaryStorageArea: this.temporaryStorageArea
|
|
369
|
-
})
|
|
370
|
-
this.
|
|
371
|
-
|
|
503
|
+
})
|
|
504
|
+
await this.refreshAndApplyStoragePolicy()
|
|
505
|
+
await this.cleanupStaleChannels()
|
|
506
|
+
await this.pruneStoredSeeds()
|
|
507
|
+
})().catch(err => {
|
|
372
508
|
try { this.onError?.(err) } catch {}
|
|
373
509
|
}).finally(() => {
|
|
374
510
|
if (this.storageMaintenancePromise === maintenance) this.storageMaintenancePromise = null
|
|
@@ -377,6 +513,12 @@ export class PrivateMessenger {
|
|
|
377
513
|
return maintenance
|
|
378
514
|
}
|
|
379
515
|
|
|
516
|
+
async runStorageHeartbeat () {
|
|
517
|
+
await this.stampActiveChannelActivity()
|
|
518
|
+
await this.touchStorageActivity({ force: true })
|
|
519
|
+
await this.applyPendingStoragePolicy()
|
|
520
|
+
}
|
|
521
|
+
|
|
380
522
|
async touchStorageActivity ({ force = false } = {}) {
|
|
381
523
|
if (!this.storageActive || this.closePromise) return false
|
|
382
524
|
if (this.storageTouchPromise) return this.storageTouchPromise
|
|
@@ -387,9 +529,13 @@ export class PrivateMessenger {
|
|
|
387
529
|
const touch = activatePrivateMessengerStorage({
|
|
388
530
|
userPubkey: this.userPubkey,
|
|
389
531
|
leaseId: this.storageLeaseId,
|
|
532
|
+
activeChannelPubkeys: [...this.channels.keys()],
|
|
390
533
|
indexedDB: this._indexedDB,
|
|
391
534
|
now
|
|
392
|
-
}).then(
|
|
535
|
+
}).then(snapshot => {
|
|
536
|
+
this.applyStoragePolicySnapshot(snapshot)
|
|
537
|
+
return true
|
|
538
|
+
}, err => {
|
|
393
539
|
this.lastStorageTouch = previousTouch
|
|
394
540
|
throw err
|
|
395
541
|
}).finally(() => {
|
|
@@ -435,12 +581,28 @@ export class PrivateMessenger {
|
|
|
435
581
|
})
|
|
436
582
|
}
|
|
437
583
|
|
|
438
|
-
async update (
|
|
584
|
+
async update (options = {}) {
|
|
439
585
|
this.assertOpen()
|
|
586
|
+
const {
|
|
587
|
+
userSigner = this.userSigner,
|
|
588
|
+
contentKeySigner = this.contentKeySigner,
|
|
589
|
+
nymSigner = this.nymSigner,
|
|
590
|
+
channels = [...this.channels.values()],
|
|
591
|
+
relays = [],
|
|
592
|
+
mode = 'leecher'
|
|
593
|
+
} = options
|
|
594
|
+
const updatesStalePolicy = Object.hasOwn(options, 'staleChannelSeconds')
|
|
595
|
+
const updatesIdentityPolicy = Object.hasOwn(options, 'identityStorageRetentionSeconds')
|
|
596
|
+
let nextStaleChannelSeconds = updatesStalePolicy
|
|
597
|
+
? normalizeStaleChannelSeconds(options.staleChannelSeconds)
|
|
598
|
+
: this.staleChannelSeconds
|
|
599
|
+
let nextIdentityStorageRetentionSeconds = updatesIdentityPolicy
|
|
600
|
+
? normalizeIdentityStorageRetentionSeconds(options.identityStorageRetentionSeconds)
|
|
601
|
+
: this.identityStorageRetentionSeconds
|
|
440
602
|
if (userSigner) {
|
|
441
603
|
const userPubkey = await userSigner.getPublicKey?.()
|
|
442
|
-
if (!userPubkey) throw new
|
|
443
|
-
if (this.userPubkey && userPubkey !== this.userPubkey) throw new
|
|
604
|
+
if (!userPubkey) throw new ValidationError('USER_SIGNER_REQUIRED')
|
|
605
|
+
if (this.userPubkey && userPubkey !== this.userPubkey) throw new ValidationError('USER_SIGNER_MISMATCH')
|
|
444
606
|
this.userSigner = userSigner
|
|
445
607
|
}
|
|
446
608
|
this.contentKeySigner = contentKeySigner || null
|
|
@@ -449,19 +611,48 @@ export class PrivateMessenger {
|
|
|
449
611
|
const nextChannels = await this.normalizeChannels(channels, { relays, mode })
|
|
450
612
|
this.assertOpen()
|
|
451
613
|
const nextPubkeys = new Set(nextChannels.map(channel => channel.pubkey))
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
614
|
+
const removedPubkeys = [...this.channels.keys()].filter(pubkey => !nextPubkeys.has(pubkey))
|
|
615
|
+
const updatesStoragePolicy = updatesStalePolicy || updatesIdentityPolicy
|
|
616
|
+
|
|
617
|
+
if (updatesStoragePolicy) {
|
|
618
|
+
const currentPolicy = await this.readStoragePolicySnapshot()
|
|
619
|
+
if (!updatesStalePolicy) nextStaleChannelSeconds = currentPolicy?.staleChannelSeconds ?? this.staleChannelSeconds
|
|
620
|
+
if (!updatesIdentityPolicy) {
|
|
621
|
+
nextIdentityStorageRetentionSeconds = currentPolicy?.identityStorageRetentionSeconds ?? this.identityStorageRetentionSeconds
|
|
457
622
|
}
|
|
458
623
|
}
|
|
624
|
+
|
|
625
|
+
if (removedPubkeys.length) {
|
|
626
|
+
await this.stampChannelActivity(removedPubkeys)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const storageSnapshot = await activatePrivateMessengerStorage({
|
|
630
|
+
userPubkey: this.userPubkey,
|
|
631
|
+
leaseId: this.storageLeaseId,
|
|
632
|
+
activeChannelPubkeys: [...nextPubkeys],
|
|
633
|
+
storagePolicy: updatesStoragePolicy
|
|
634
|
+
? {
|
|
635
|
+
staleChannelSeconds: nextStaleChannelSeconds,
|
|
636
|
+
identityStorageRetentionSeconds: nextIdentityStorageRetentionSeconds
|
|
637
|
+
}
|
|
638
|
+
: undefined,
|
|
639
|
+
indexedDB: this._indexedDB
|
|
640
|
+
})
|
|
641
|
+
this.applyStoragePolicySnapshot(storageSnapshot)
|
|
642
|
+
this.lastStorageTouch = Date.now()
|
|
643
|
+
if (updatesStoragePolicy) this.broadcastStoragePolicyChange()
|
|
644
|
+
|
|
645
|
+
await this.unwatch([...this.channels.keys()])
|
|
646
|
+
this.channels.clear()
|
|
459
647
|
for (const channel of nextChannels) this.channels.set(channel.pubkey, channel)
|
|
460
648
|
|
|
461
|
-
await this.cleanupStaleChannels()
|
|
649
|
+
await this.cleanupStaleChannels({ storageSnapshot })
|
|
462
650
|
await this.applyRecoveryPolicies(nextChannels)
|
|
463
651
|
await this.watch()
|
|
464
652
|
await this.reconcilePresencePublishers()
|
|
653
|
+
if (this.storagePolicyRevision === storageSnapshot.policyRevision) {
|
|
654
|
+
this.storagePolicyNeedsApply = false
|
|
655
|
+
}
|
|
465
656
|
return this
|
|
466
657
|
}
|
|
467
658
|
|
|
@@ -476,10 +667,10 @@ export class PrivateMessenger {
|
|
|
476
667
|
const hasChannelSendRelays = Boolean(channel.sendRelays?.length)
|
|
477
668
|
const hasDefaultRelays = Boolean(defaults.relays?.length)
|
|
478
669
|
const pubkey = channel.pubkey || await signer?.getPublicKey?.()
|
|
479
|
-
if (!pubkey) throw new
|
|
480
|
-
if (!signer && !readerSigner) throw new
|
|
670
|
+
if (!pubkey) throw new ValidationError('CHANNEL_PUBKEY_REQUIRED')
|
|
671
|
+
if (!signer && !readerSigner) throw new ValidationError('CHANNEL_SIGNER_REQUIRED')
|
|
481
672
|
const mode = channel.mode || defaults.mode || 'leecher'
|
|
482
|
-
if (!signer &&
|
|
673
|
+
if (!signer && doesModeStoreRecoverySeeds(mode)) throw new ValidationError('PRIVATE_CHANNEL_WRITER_REQUIRED')
|
|
483
674
|
const readerPubkey = channel.readerPubkey || channel.privateChannelReaderPubkey || await readerSigner?.getPublicKey?.() || pubkey
|
|
484
675
|
const autoDeletionCapability = channel.autoDeletionCapability === undefined
|
|
485
676
|
? undefined
|
|
@@ -543,7 +734,7 @@ export class PrivateMessenger {
|
|
|
543
734
|
if (channel.sendRelays.length) return { relays: channel.sendRelays, recoveryRelays }
|
|
544
735
|
if (channel.relays.length) return { relays: channel.relays, recoveryRelays }
|
|
545
736
|
const derived = await this.readRelayToReceivers(receiverPubkeys)
|
|
546
|
-
if (!relayMapRelays(derived).length) throw new
|
|
737
|
+
if (!relayMapRelays(derived).length) throw new ValidationError('NO_RELAYS')
|
|
547
738
|
return { relayToReceivers: derived, recoveryRelays }
|
|
548
739
|
}
|
|
549
740
|
|
|
@@ -551,14 +742,19 @@ export class PrivateMessenger {
|
|
|
551
742
|
return structuredClone(this.state)
|
|
552
743
|
}
|
|
553
744
|
|
|
554
|
-
writeState (state) {
|
|
555
|
-
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
745
|
+
writeState (state, { touchStorage = true } = {}) {
|
|
746
|
+
if (touchStorage) this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
747
|
+
const previous = this.state?.channels || {}
|
|
556
748
|
const next = {
|
|
557
749
|
channels: isPlainObject(state?.channels) ? structuredClone(state.channels) : {}
|
|
558
750
|
}
|
|
751
|
+
const changed = Object.fromEntries(Object.entries(next.channels)
|
|
752
|
+
.filter(([pubkey, value]) => !areStateValuesEqual(previous[pubkey], value)))
|
|
753
|
+
const removed = Object.keys(previous).filter(pubkey => !Object.hasOwn(next.channels, pubkey))
|
|
559
754
|
this.state = next
|
|
560
|
-
|
|
561
|
-
const
|
|
755
|
+
if (!Object.keys(changed).length && !removed.length) return this.stateWriteTail
|
|
756
|
+
const snapshot = structuredClone(changed)
|
|
757
|
+
const write = this.stateWriteTail.then(() => this.stateStore.update(snapshot, removed))
|
|
562
758
|
this.stateWriteTail = write.catch(err => {
|
|
563
759
|
try { this.onError?.(err) } catch {}
|
|
564
760
|
})
|
|
@@ -569,6 +765,29 @@ export class PrivateMessenger {
|
|
|
569
765
|
await this.stateWriteTail
|
|
570
766
|
}
|
|
571
767
|
|
|
768
|
+
async stampChannelActivity (pubkeys) {
|
|
769
|
+
if (!this.stateStore || !this.state) return false
|
|
770
|
+
pubkeys = [...new Set(pubkeys || [])].filter(pubkey => this.state.channels[pubkey])
|
|
771
|
+
if (!pubkeys.length) return false
|
|
772
|
+
const lastWatchedAt = nowSeconds()
|
|
773
|
+
for (const pubkey of pubkeys) {
|
|
774
|
+
this.state.channels[pubkey] = {
|
|
775
|
+
...this.state.channels[pubkey],
|
|
776
|
+
lastWatchedAt
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const write = this.stateWriteTail.then(() => this.stateStore.touch(pubkeys, lastWatchedAt))
|
|
780
|
+
this.stateWriteTail = write.catch(err => {
|
|
781
|
+
try { this.onError?.(err) } catch {}
|
|
782
|
+
})
|
|
783
|
+
await write
|
|
784
|
+
return true
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
stampActiveChannelActivity () {
|
|
788
|
+
return this.stampChannelActivity([...this.channels.keys()])
|
|
789
|
+
}
|
|
790
|
+
|
|
572
791
|
updateChannelState (pubkey, patch) {
|
|
573
792
|
const state = this.readState()
|
|
574
793
|
const current = state.channels[pubkey] || {}
|
|
@@ -578,9 +797,19 @@ export class PrivateMessenger {
|
|
|
578
797
|
}
|
|
579
798
|
|
|
580
799
|
removeChannelState (pubkey) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
800
|
+
this.removeChannelStates([pubkey])
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
removeChannelStates (pubkeys) {
|
|
804
|
+
pubkeys = [...new Set(pubkeys || [])]
|
|
805
|
+
for (const pubkey of pubkeys) delete this.state.channels[pubkey]
|
|
806
|
+
if (!pubkeys.length) return this.stateWriteTail
|
|
807
|
+
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
808
|
+
const write = this.stateWriteTail.then(() => this.stateStore.update({}, pubkeys))
|
|
809
|
+
this.stateWriteTail = write.catch(err => {
|
|
810
|
+
try { this.onError?.(err) } catch {}
|
|
811
|
+
})
|
|
812
|
+
return write
|
|
584
813
|
}
|
|
585
814
|
|
|
586
815
|
markSeen (pubkey, createdAt = nowSeconds()) {
|
|
@@ -765,7 +994,7 @@ export class PrivateMessenger {
|
|
|
765
994
|
const channelPubkeys = uniq(channels)
|
|
766
995
|
for (const pubkey of channelPubkeys) {
|
|
767
996
|
const channel = this.channels.get(pubkey)
|
|
768
|
-
if (!channel) throw new
|
|
997
|
+
if (!channel) throw new ValidationError('UNKNOWN_CHANNEL')
|
|
769
998
|
const watchRelays = await this.resolveWatchRelays(channel)
|
|
770
999
|
this.assertOpen()
|
|
771
1000
|
const stop = await this._privateMessage.watch({
|
|
@@ -879,7 +1108,7 @@ export class PrivateMessenger {
|
|
|
879
1108
|
|
|
880
1109
|
async handleAsk (channelPubkey, message) {
|
|
881
1110
|
this.trackSeederActivity(channelPubkey, message)
|
|
882
|
-
if (
|
|
1111
|
+
if (doesModeStoreRecoverySeeds(this.channels.get(channelPubkey)?.mode) && messageCode(message) === MISSING_MESSAGES_ASK_CODE) {
|
|
883
1112
|
await this.replyWithStoredSeeds(channelPubkey, message)
|
|
884
1113
|
return
|
|
885
1114
|
}
|
|
@@ -1233,10 +1462,10 @@ export class PrivateMessenger {
|
|
|
1233
1462
|
async reconcilePresencePublishers () {
|
|
1234
1463
|
const starts = []
|
|
1235
1464
|
for (const pubkey of [...this.presenceTimers.keys()]) {
|
|
1236
|
-
if (!
|
|
1465
|
+
if (!doesModeStoreRecoverySeeds(this.channels.get(pubkey)?.mode) || !this.offlineRecoverySecondsFor(pubkey)) this.stopPresencePublisher(pubkey)
|
|
1237
1466
|
}
|
|
1238
1467
|
for (const [pubkey, channel] of this.channels) {
|
|
1239
|
-
if (
|
|
1468
|
+
if (doesModeStoreRecoverySeeds(channel.mode) && this.offlineRecoverySecondsFor(channel)) starts.push(this.startPresencePublisher(pubkey))
|
|
1240
1469
|
else this.stopPresencePublisher(pubkey)
|
|
1241
1470
|
}
|
|
1242
1471
|
await Promise.all(starts)
|
|
@@ -1256,13 +1485,13 @@ export class PrivateMessenger {
|
|
|
1256
1485
|
|
|
1257
1486
|
requireChannel (pubkey) {
|
|
1258
1487
|
const channel = this.channels.get(pubkey)
|
|
1259
|
-
if (!channel) throw new
|
|
1488
|
+
if (!channel) throw new ValidationError('UNKNOWN_CHANNEL')
|
|
1260
1489
|
return channel
|
|
1261
1490
|
}
|
|
1262
1491
|
|
|
1263
1492
|
requireWritableChannel (pubkey) {
|
|
1264
1493
|
const channel = this.requireChannel(pubkey)
|
|
1265
|
-
if (!channel.signer) throw new
|
|
1494
|
+
if (!channel.signer) throw new ValidationError('PRIVATE_CHANNEL_WRITER_REQUIRED')
|
|
1266
1495
|
return channel
|
|
1267
1496
|
}
|
|
1268
1497
|
|
|
@@ -1270,7 +1499,7 @@ export class PrivateMessenger {
|
|
|
1270
1499
|
return channel.autoDeletionCapability ?? this.autoDeletionCapability
|
|
1271
1500
|
}
|
|
1272
1501
|
|
|
1273
|
-
|
|
1502
|
+
requestedOfflineRecoverySecondsFor (channelOrPubkey) {
|
|
1274
1503
|
const channel = typeof channelOrPubkey === 'string'
|
|
1275
1504
|
? this.channels.get(channelOrPubkey)
|
|
1276
1505
|
: channelOrPubkey
|
|
@@ -1282,6 +1511,18 @@ export class PrivateMessenger {
|
|
|
1282
1511
|
: normalizeOfflineRecoverySeconds(persisted)
|
|
1283
1512
|
}
|
|
1284
1513
|
|
|
1514
|
+
offlineRecoverySecondsFor (channelOrPubkey) {
|
|
1515
|
+
return Math.min(
|
|
1516
|
+
this.requestedOfflineRecoverySecondsFor(channelOrPubkey),
|
|
1517
|
+
this.staleChannelSeconds,
|
|
1518
|
+
this.identityStorageRetentionSeconds
|
|
1519
|
+
)
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
staleChannelSecondsForCleanup () {
|
|
1523
|
+
return Math.min(this.staleChannelSeconds, this.identityStorageRetentionSeconds)
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1285
1526
|
eventExpirationSecondsFor (channel) {
|
|
1286
1527
|
return this.offlineRecoverySecondsFor(channel) || privateChannel.EXPIRATION_SECONDS
|
|
1287
1528
|
}
|
|
@@ -1297,13 +1538,14 @@ export class PrivateMessenger {
|
|
|
1297
1538
|
const now = nowSeconds()
|
|
1298
1539
|
for (const channel of channels) {
|
|
1299
1540
|
const current = state.channels[channel.pubkey] || {}
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1302
|
-
|
|
1541
|
+
const requestedSeconds = channel.offlineRecoverySeconds
|
|
1542
|
+
const effectiveSeconds = this.offlineRecoverySecondsFor(channel)
|
|
1543
|
+
current.offlineRecoverySeconds = requestedSeconds
|
|
1544
|
+
if (!effectiveSeconds) {
|
|
1303
1545
|
delete current.openOfflineStart
|
|
1304
1546
|
current.offlineRanges = []
|
|
1305
1547
|
} else {
|
|
1306
|
-
const cutoff = now -
|
|
1548
|
+
const cutoff = now - effectiveSeconds
|
|
1307
1549
|
current.offlineRanges = mergeRanges((current.offlineRanges || [])
|
|
1308
1550
|
.filter(range => range.end >= cutoff)
|
|
1309
1551
|
.map(range => ({ ...range, start: Math.max(range.start, cutoff) })))
|
|
@@ -1319,7 +1561,7 @@ export class PrivateMessenger {
|
|
|
1319
1561
|
|
|
1320
1562
|
requireNymSigner (channel, override) {
|
|
1321
1563
|
const signer = override || channel?.nymSigner || this.nymSigner
|
|
1322
|
-
if (!signer?.getPublicKey) throw new
|
|
1564
|
+
if (!signer?.getPublicKey) throw new ValidationError('NYM_SIGNER_REQUIRED')
|
|
1323
1565
|
return signer
|
|
1324
1566
|
}
|
|
1325
1567
|
|
|
@@ -1463,7 +1705,7 @@ export class PrivateMessenger {
|
|
|
1463
1705
|
|
|
1464
1706
|
const routerRecord = record?.recordType === ROUTER_SEED_RECORD_TYPE ? record.router : null
|
|
1465
1707
|
if (!isPrivateChannelRouter(routerRecord)) return null
|
|
1466
|
-
if (!this._privateChannel.unwrapEvent) throw new
|
|
1708
|
+
if (!this._privateChannel.unwrapEvent) throw new ValidationError('PRIVATE_CHANNEL_UNWRAP_UNSUPPORTED')
|
|
1467
1709
|
|
|
1468
1710
|
const channel = this.requireChannel(channelPubkey)
|
|
1469
1711
|
const router = {
|
|
@@ -1568,6 +1810,7 @@ export class PrivateMessenger {
|
|
|
1568
1810
|
await this.flushStateWrites()
|
|
1569
1811
|
await this.queue.removeBy('byChannel', pubkey)
|
|
1570
1812
|
await this.seedQueue.removeBy('byChannel', pubkey)
|
|
1813
|
+
await this.touchStorageActivity({ force: true })
|
|
1571
1814
|
this.ensureRelayListWatcher()
|
|
1572
1815
|
})
|
|
1573
1816
|
}
|
|
@@ -1576,19 +1819,26 @@ export class PrivateMessenger {
|
|
|
1576
1819
|
return this.runQueueOperation(() => this.queue.clear())
|
|
1577
1820
|
}
|
|
1578
1821
|
|
|
1579
|
-
async cleanupStaleChannels () {
|
|
1822
|
+
async cleanupStaleChannels ({ storageSnapshot } = {}) {
|
|
1580
1823
|
if (!this.prefix) return
|
|
1824
|
+
storageSnapshot ||= await this.readStoragePolicySnapshot()
|
|
1825
|
+
if (!storageSnapshot) return
|
|
1826
|
+
const activeChannelPubkeys = new Set(storageSnapshot.activeChannelPubkeys || [])
|
|
1581
1827
|
return this.runQueueOperation(async () => {
|
|
1582
|
-
|
|
1583
|
-
const
|
|
1828
|
+
await this.flushStateWrites()
|
|
1829
|
+
const state = { channels: await this.stateStore.load() }
|
|
1830
|
+
const cutoff = nowSeconds() - this.staleChannelSecondsForCleanup()
|
|
1831
|
+
const stalePubkeys = []
|
|
1584
1832
|
for (const [pubkey, channel] of Object.entries(state.channels)) {
|
|
1833
|
+
if (activeChannelPubkeys.has(pubkey)) continue
|
|
1585
1834
|
if ((channel.lastWatchedAt || 0) >= cutoff) continue
|
|
1586
1835
|
delete state.channels[pubkey]
|
|
1836
|
+
stalePubkeys.push(pubkey)
|
|
1587
1837
|
await this.queue?.removeBy('byChannel', pubkey)
|
|
1588
1838
|
await this.seedQueue?.removeBy('byChannel', pubkey)
|
|
1589
1839
|
}
|
|
1590
|
-
this.
|
|
1591
|
-
await this.
|
|
1840
|
+
this.state = state
|
|
1841
|
+
if (stalePubkeys.length) await this.removeChannelStates(stalePubkeys)
|
|
1592
1842
|
})
|
|
1593
1843
|
}
|
|
1594
1844
|
|
|
@@ -1636,15 +1886,18 @@ export class PrivateMessenger {
|
|
|
1636
1886
|
this.stopOffline = null
|
|
1637
1887
|
this.stopOnline = null
|
|
1638
1888
|
this.stopStorageMaintenance()
|
|
1889
|
+
this.stopStoragePolicyBroadcast()
|
|
1639
1890
|
|
|
1640
1891
|
this.closePromise = (async () => {
|
|
1641
1892
|
let unwatchError
|
|
1642
1893
|
try { await unwatchPromise } catch (err) { unwatchError = err }
|
|
1643
1894
|
await initSettledPromise
|
|
1895
|
+
await this.stampActiveChannelActivity()
|
|
1644
1896
|
await this.queueOperationTail
|
|
1645
1897
|
await this.stateWriteTail
|
|
1646
1898
|
try { await this.storageTouchPromise } catch {}
|
|
1647
1899
|
await this.storageMaintenancePromise
|
|
1900
|
+
try { await this.storagePolicyRefreshTail } catch {}
|
|
1648
1901
|
await Promise.all([
|
|
1649
1902
|
this.queue?.close?.(),
|
|
1650
1903
|
this.seedQueue?.close?.(),
|
|
@@ -1658,6 +1911,12 @@ export class PrivateMessenger {
|
|
|
1658
1911
|
})
|
|
1659
1912
|
}
|
|
1660
1913
|
this.storageActive = false
|
|
1914
|
+
if (this.identityStorageRetentionSeconds === 0) {
|
|
1915
|
+
await PrivateMessenger.maintainStorage({
|
|
1916
|
+
indexedDB: this._indexedDB,
|
|
1917
|
+
temporaryStorageArea: this.temporaryStorageArea
|
|
1918
|
+
})
|
|
1919
|
+
}
|
|
1661
1920
|
if (unwatchError) throw unwatchError
|
|
1662
1921
|
})()
|
|
1663
1922
|
return this.closePromise
|