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.
Files changed (50) hide show
  1. package/README.md +112 -8
  2. package/base16/index.js +6 -3
  3. package/base36/index.js +12 -8
  4. package/base62/index.js +15 -11
  5. package/base64/index.js +18 -2
  6. package/base93/index.js +19 -7
  7. package/content-key/event/index.js +40 -12
  8. package/double-dh/index.js +21 -6
  9. package/ecdh/index.js +12 -1
  10. package/error/index.js +32 -0
  11. package/event/helpers/serialize.js +31 -0
  12. package/event/index.js +116 -0
  13. package/i18n/index.js +15 -13
  14. package/idb/index.js +5 -3
  15. package/idb-queue/index.js +21 -20
  16. package/index.js +10 -0
  17. package/key/index.js +31 -18
  18. package/kind/index.js +244 -0
  19. package/nip04/index.js +47 -0
  20. package/nip05/index.js +61 -0
  21. package/nip19/index.js +176 -43
  22. package/nip44/helpers.js +95 -0
  23. package/nip44/index.js +14 -0
  24. package/nip44-v3/index.js +35 -16
  25. package/nip46/helpers/frame.js +6 -6
  26. package/nip46/helpers/url.js +8 -6
  27. package/nip46/services/bunker-signer.js +7 -6
  28. package/nip46/services/client.js +8 -7
  29. package/nip46/services/server-session.js +8 -7
  30. package/nip46/services/transport.js +9 -8
  31. package/nip96/index.js +285 -0
  32. package/nip98/index.js +56 -0
  33. package/nwt/index.js +241 -0
  34. package/package.json +22 -3
  35. package/private-channel/helpers/chunks.js +7 -6
  36. package/private-channel/helpers/event.js +5 -4
  37. package/private-channel/index.js +63 -61
  38. package/private-channel/services/received-chunks.js +4 -3
  39. package/private-message/index.js +32 -31
  40. package/private-messenger/index.js +313 -54
  41. package/private-messenger/recovery/index.js +15 -14
  42. package/private-messenger/services/channel-state.js +28 -7
  43. package/private-messenger/services/storage-maintenance.js +142 -46
  44. package/relay/helpers/hll.js +3 -1
  45. package/relay/services/query.js +3 -3
  46. package/relay/services/relay-connection.js +222 -121
  47. package/relay/services/relay-pool.js +13 -10
  48. package/temporary-storage/index.js +3 -1
  49. package/url/index.js +131 -0
  50. package/web-storage-queue/index.js +8 -6
@@ -1,5 +1,6 @@
1
- import { getEventHash } from 'nostr-tools'
1
+ import { getEventHash } from '../../event/index.js'
2
2
  import { bytesToBase64, base64ToBytes } from '../../base64/index.js'
3
+ import { ValidationError } from '../../error/index.js'
3
4
  import { ASK_KIND, parseRumorContent } from '../../private-message/index.js'
4
5
 
5
6
  export const SEEDER_PRESENCE_CODE = 'seederPresence_8mj8'
@@ -37,7 +38,7 @@ function encodeJsonlRows (...rows) {
37
38
  return bytesToBase64(encoder.encode(rows.map(row => String(row).endsWith('\n') ? row : `${row}\n`).join('')))
38
39
  }
39
40
 
40
- function eventInRange (event, since, until) {
41
+ function isEventInRange (event, since, until) {
41
42
  if (!Number.isFinite(event?.created_at)) return true
42
43
  if (since != null && event.created_at < since) return false
43
44
  if (until != null && event.created_at > until) return false
@@ -159,17 +160,17 @@ function routerWithSingleRow (router, payloadRow, row) {
159
160
  }
160
161
  }
161
162
 
162
- function seedRowInRange (seed, since, until) {
163
+ function isSeedRowInRange (seed, since, until) {
163
164
  const firstSeenAt = seed.firstSeenAt ?? seed.router?.created_at
164
165
  const lastSeenAt = seed.lastSeenAt ?? seed.router?.created_at
165
- if (!Number.isFinite(firstSeenAt) || !Number.isFinite(lastSeenAt)) return eventInRange(seed.router, since, until)
166
+ if (!Number.isFinite(firstSeenAt) || !Number.isFinite(lastSeenAt)) return isEventInRange(seed.router, since, until)
166
167
  if (since != null && lastSeenAt < since) return false
167
168
  if (until != null && firstSeenAt > until) return false
168
169
  return true
169
170
  }
170
171
 
171
172
  function compactRoutersFromSeed (seed, { receiverPubkey, since, until }) {
172
- if (!seed?.payloadRow || !seed?.row || !seed?.router || !seedRowInRange(seed, since, until)) return []
173
+ if (!seed?.payloadRow || !seed?.row || !seed?.router || !isSeedRowInRange(seed, since, until)) return []
173
174
  if (receiverPubkey && seed.receiverPubkey !== receiverPubkey) return []
174
175
  return [{
175
176
  recordType: ROUTER_SEED_RECORD_TYPE,
@@ -185,7 +186,7 @@ function compactNymCarriersFromSeed (seed, { since, until }) {
185
186
  // eslint-disable-next-line camelcase
186
187
  const created_at = nymCarrierRecordTime(seed)
187
188
  // eslint-disable-next-line camelcase
188
- if (!seed?.carriers?.length || !eventInRange({ created_at }, since, until)) return []
189
+ if (!seed?.carriers?.length || !isEventInRange({ created_at }, since, until)) return []
189
190
  return [{
190
191
  recordType: NYM_CARRIER_SEED_RECORD_TYPE,
191
192
  carriers: compactSeedNymCarriers(seed.carriers)
@@ -226,10 +227,10 @@ export function createEventReplyPacker ({
226
227
  recordsFromInput = eventRecordFromInput,
227
228
  sendEmptyReply = false
228
229
  }) {
229
- if (!messenger?.reply) throw new Error('MESSENGER_REQUIRED')
230
- if (!question?.id) throw new Error('QUESTION_REQUIRED')
231
- if (!receiverPubkey) throw new Error('RECEIVER_PUBKEY_REQUIRED')
232
- if (!Number.isSafeInteger(eventsPerChunk) || eventsPerChunk < 1) throw new Error('INVALID_EVENTS_PER_CHUNK')
230
+ if (!messenger?.reply) throw new ValidationError('MESSENGER_REQUIRED')
231
+ if (!question?.id) throw new ValidationError('QUESTION_REQUIRED')
232
+ if (!receiverPubkey) throw new ValidationError('RECEIVER_PUBKEY_REQUIRED')
233
+ if (!Number.isSafeInteger(eventsPerChunk) || eventsPerChunk < 1) throw new ValidationError('INVALID_EVENTS_PER_CHUNK')
233
234
 
234
235
  let chunk = ''
235
236
  let chunkEvents = 0
@@ -298,10 +299,10 @@ export function createMissingMessageReplyPacker ({
298
299
  eventsPerChunk = DEFAULT_EVENTS_PER_CHUNK,
299
300
  sendEmptyReply = false
300
301
  }) {
301
- if (!messenger?.reply) throw new Error('MESSENGER_REQUIRED')
302
- if (!question?.id) throw new Error('QUESTION_REQUIRED')
303
- if (!receiverPubkey) throw new Error('RECEIVER_PUBKEY_REQUIRED')
304
- if (!Number.isSafeInteger(eventsPerChunk) || eventsPerChunk < 1) throw new Error('INVALID_EVENTS_PER_CHUNK')
302
+ if (!messenger?.reply) throw new ValidationError('MESSENGER_REQUIRED')
303
+ if (!question?.id) throw new ValidationError('QUESTION_REQUIRED')
304
+ if (!receiverPubkey) throw new ValidationError('RECEIVER_PUBKEY_REQUIRED')
305
+ if (!Number.isSafeInteger(eventsPerChunk) || eventsPerChunk < 1) throw new ValidationError('INVALID_EVENTS_PER_CHUNK')
305
306
 
306
307
  const range = backfillRequestRange(question, since, until)
307
308
  return createEventReplyPacker({
@@ -1,4 +1,5 @@
1
1
  import { run } from '../../idb/index.js'
2
+ import { ValidationError } from '../../error/index.js'
2
3
 
3
4
  const DATABASE_VERSION = 1
4
5
  const CHANNELS_STORE = 'channels'
@@ -11,12 +12,14 @@ database `${prefix}:state:idb`, version 1
11
12
  channels, keyPath "pubkey"
12
13
  pubkey watched channel public key
13
14
  value evolving channel-recovery state, including last-seen/watched times,
14
- mode, relays, seeders, effective offline-recovery retention, offline
15
+ mode, relays, seeders, requested offline-recovery retention, offline
15
16
  ranges, active offline-range start,
16
17
  per-seeder activity, and sent/received content-key usage
17
18
 
18
- The complete channels snapshot is replaced atomically on each persisted state
19
- change; value remains an extensible internal object rather than a public schema.
19
+ Changed channel records are upserted individually and explicit removals are
20
+ applied in the same transaction. This prevents one active instance from
21
+ clearing state owned by another instance of the same principal identity.
22
+ `value` remains an extensible internal object rather than a public schema.
20
23
  */
21
24
 
22
25
  function deferred () {
@@ -78,7 +81,7 @@ function cloneChannels (channels) {
78
81
  }
79
82
 
80
83
  export async function createChannelStateStore ({ prefix, indexedDB = globalThis.indexedDB } = {}) {
81
- if (!prefix) throw new Error('PRIVATE_MESSENGER_STATE_PREFIX_REQUIRED')
84
+ if (!prefix) throw new ValidationError('PRIVATE_MESSENGER_STATE_PREFIX_REQUIRED')
82
85
  const db = await openDatabase(indexedDB, `${prefix}:state:idb`)
83
86
  let closed = false
84
87
  let activeTransactions = 0
@@ -108,16 +111,34 @@ export async function createChannelStateStore ({ prefix, indexedDB = globalThis.
108
111
  })
109
112
  }
110
113
 
111
- async function replace (channels) {
114
+ async function update (channels, removedPubkeys = []) {
112
115
  const snapshot = cloneChannels(channels)
116
+ const removals = [...new Set(removedPubkeys || [])]
113
117
  await runTransaction('readwrite', async tx => {
114
- await run('clear', [], CHANNELS_STORE, null, { tx })
118
+ for (const pubkey of removals) await run('delete', [pubkey], CHANNELS_STORE, null, { tx })
115
119
  for (const [pubkey, value] of Object.entries(snapshot)) {
116
120
  await run('put', [{ pubkey, value }], CHANNELS_STORE, null, { tx })
117
121
  }
118
122
  })
119
123
  }
120
124
 
125
+ async function touch (pubkeys, lastWatchedAt) {
126
+ const uniquePubkeys = [...new Set(pubkeys || [])]
127
+ if (!uniquePubkeys.length) return
128
+ await runTransaction('readwrite', async tx => {
129
+ for (const pubkey of uniquePubkeys) {
130
+ const current = (await run('get', [pubkey], CHANNELS_STORE, null, { tx })).result
131
+ await run('put', [{
132
+ pubkey,
133
+ value: {
134
+ ...(current?.value && typeof current.value === 'object' ? current.value : {}),
135
+ lastWatchedAt
136
+ }
137
+ }], CHANNELS_STORE, null, { tx })
138
+ }
139
+ })
140
+ }
141
+
121
142
  function close () {
122
143
  if (closePromise) return closePromise
123
144
  closed = true
@@ -128,5 +149,5 @@ export async function createChannelStateStore ({ prefix, indexedDB = globalThis.
128
149
  return closePromise
129
150
  }
130
151
 
131
- return { load, replace, close }
152
+ return { load, update, touch, close }
132
153
  }
@@ -1,14 +1,16 @@
1
1
  import { run } from '../../idb/index.js'
2
2
  import { cleanupReceivedChunkStorage } from '../../private-channel/services/received-chunks.js'
3
3
  import { cleanupTemporaryStorage } from '../../temporary-storage/index.js'
4
+ import { DEFAULT_STALE_CHANNEL_SECONDS } from '../constants/index.js'
4
5
 
5
6
  const REGISTRY_DATABASE = 'libp2r2p:private-messenger:registry:idb'
6
7
  const REGISTRY_VERSION = 1
7
- const BUNDLES_STORE = 'bundles'
8
+ const STORAGE_SETS_STORE = 'storageSets'
8
9
  const LEASES_STORE = 'leases'
9
10
  const LOCK_NAME = 'libp2r2p:private-messenger:storage-maintenance'
10
11
  const LEASE_MS = 2 * 60 * 60 * 1000 // 2 hours
11
- const RETENTION_MS = 60 * 24 * 60 * 60 * 1000 // 60 days
12
+ const DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS = 60 * 24 * 60 * 60
13
+ const MAX_RETENTION_SECONDS = Math.floor(Number.MAX_SAFE_INTEGER / 1000)
12
14
  const MAX_RETRY_MS = 5 * 60 * 1000
13
15
 
14
16
  const localLocks = new WeakMap()
@@ -19,19 +21,23 @@ IndexedDB schema, shared by every PrivateMessenger instance on this origin:
19
21
 
20
22
  database "libp2r2p:private-messenger:registry:idb", version 1
21
23
 
22
- bundles, keyPath "userPubkey"
24
+ storageSets, keyPath "userPubkey"
23
25
  userPubkey primary signer public key and storage-set coordinate
24
26
  lastUsedAt most recent durable activity in milliseconds
25
27
  leaseUntil active-instance lease expiry in milliseconds
26
28
  status "ready" or "delete_pending"
27
29
  attempts consecutive failed storage-set deletions
28
30
  nextAttemptAt earliest retry time in milliseconds
31
+ staleChannelSeconds channel-state retention requested by the last configuring instance
32
+ identityStorageRetentionSeconds identity database-set retention after its last activity
33
+ policyRevision monotonically increasing storage-policy revision
29
34
 
30
35
  leases, keyPath "key"
31
36
  key `${userPubkey}:${leaseId}`
32
37
  userPubkey principal signer public key
33
38
  leaseId per-PrivateMessenger instance identifier
34
39
  leaseUntil instance lease expiry in milliseconds
40
+ activeChannelPubkeys channels currently administered by that instance
35
41
 
36
42
  leases indexes
37
43
  byUserPubkey userPubkey
@@ -53,8 +59,8 @@ function openRegistry (indexedDB) {
53
59
  request.onblocked = () => reject(new Error('IDB_DATABASE_BLOCKED'))
54
60
  request.onupgradeneeded = () => {
55
61
  const db = request.result
56
- if (!db.objectStoreNames.contains(BUNDLES_STORE)) {
57
- db.createObjectStore(BUNDLES_STORE, { keyPath: 'userPubkey' })
62
+ if (!db.objectStoreNames.contains(STORAGE_SETS_STORE)) {
63
+ db.createObjectStore(STORAGE_SETS_STORE, { keyPath: 'userPubkey' })
58
64
  }
59
65
  if (!db.objectStoreNames.contains(LEASES_STORE)) {
60
66
  const leases = db.createObjectStore(LEASES_STORE, { keyPath: 'key' })
@@ -72,7 +78,7 @@ function openRegistry (indexedDB) {
72
78
  async function withRegistryTransaction (indexedDB, mode, work) {
73
79
  const db = await openRegistry(indexedDB)
74
80
  try {
75
- const tx = db.transaction([BUNDLES_STORE, LEASES_STORE], mode)
81
+ const tx = db.transaction([STORAGE_SETS_STORE, LEASES_STORE], mode)
76
82
  const done = transactionDone(tx)
77
83
  try {
78
84
  const result = await work(tx)
@@ -100,9 +106,20 @@ async function readLeases (tx, userPubkey) {
100
106
  return records.filter(record => record?.userPubkey === userPubkey)
101
107
  }
102
108
 
103
- async function refreshLeaseUntil (tx, bundle, now, { removeExpired = false } = {}) {
104
- const leases = await readLeases(tx, bundle.userPubkey)
109
+ function normalizeRetentionSeconds (value, fallback) {
110
+ return Number.isSafeInteger(value) && value >= 0 && value <= MAX_RETENTION_SECONDS
111
+ ? value
112
+ : fallback
113
+ }
114
+
115
+ function normalizeActiveChannelPubkeys (values) {
116
+ return [...new Set((values || []).filter(value => typeof value === 'string' && value))].sort()
117
+ }
118
+
119
+ async function refreshLeaseState (tx, storageSet, now, { removeExpired = false } = {}) {
120
+ const leases = await readLeases(tx, storageSet.userPubkey)
105
121
  let leaseUntil = 0
122
+ const activeChannelPubkeys = new Set()
106
123
  for (const lease of leases) {
107
124
  const expiresAt = Math.max(0, Number(lease.leaseUntil) || 0)
108
125
  if (expiresAt <= now) {
@@ -110,12 +127,18 @@ async function refreshLeaseUntil (tx, bundle, now, { removeExpired = false } = {
110
127
  continue
111
128
  }
112
129
  leaseUntil = Math.max(leaseUntil, expiresAt)
130
+ for (const pubkey of normalizeActiveChannelPubkeys(lease.activeChannelPubkeys)) {
131
+ activeChannelPubkeys.add(pubkey)
132
+ }
133
+ }
134
+ storageSet.leaseUntil = leaseUntil
135
+ return {
136
+ leaseUntil,
137
+ activeChannelPubkeys: [...activeChannelPubkeys].sort()
113
138
  }
114
- bundle.leaseUntil = leaseUntil
115
- return leaseUntil
116
139
  }
117
140
 
118
- function normalizeBundle (record) {
141
+ function normalizeStorageSet (record) {
119
142
  if (!record?.userPubkey) return null
120
143
  return {
121
144
  userPubkey: String(record.userPubkey),
@@ -123,7 +146,24 @@ function normalizeBundle (record) {
123
146
  leaseUntil: Math.max(0, Number(record.leaseUntil) || 0),
124
147
  status: record.status === 'delete_pending' ? 'delete_pending' : 'ready',
125
148
  attempts: Math.max(0, Math.floor(Number(record.attempts) || 0)),
126
- nextAttemptAt: Math.max(0, Number(record.nextAttemptAt) || 0)
149
+ nextAttemptAt: Math.max(0, Number(record.nextAttemptAt) || 0),
150
+ staleChannelSeconds: normalizeRetentionSeconds(
151
+ record.staleChannelSeconds,
152
+ DEFAULT_STALE_CHANNEL_SECONDS
153
+ ),
154
+ identityStorageRetentionSeconds: normalizeRetentionSeconds(
155
+ record.identityStorageRetentionSeconds,
156
+ DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS
157
+ ),
158
+ policyRevision: Math.max(0, Math.floor(Number(record.policyRevision) || 0))
159
+ }
160
+ }
161
+
162
+ function storagePolicy (storageSet) {
163
+ return {
164
+ staleChannelSeconds: storageSet.staleChannelSeconds,
165
+ identityStorageRetentionSeconds: storageSet.identityStorageRetentionSeconds,
166
+ policyRevision: storageSet.policyRevision
127
167
  }
128
168
  }
129
169
 
@@ -178,23 +218,29 @@ function retryDelay (attempts) {
178
218
  return Math.min(MAX_RETRY_MS, 1000 * (2 ** Math.min(9, Math.max(0, attempts - 1))))
179
219
  }
180
220
 
181
- async function markAndListDueBundles (indexedDB, now) {
221
+ async function markAndListDueStorageSets (indexedDB, now) {
182
222
  return withRegistryTransaction(indexedDB, 'readwrite', async tx => {
183
- const records = (await run('getAll', [], BUNDLES_STORE, null, { tx })).result
223
+ const records = (await run('getAll', [], STORAGE_SETS_STORE, null, { tx })).result
184
224
  const due = []
185
225
  for (const raw of records) {
186
- const bundle = normalizeBundle(raw)
187
- if (!bundle) continue
188
- await refreshLeaseUntil(tx, bundle, now, { removeExpired: true })
189
- const leaseActive = bundle.leaseUntil > now
190
- if (bundle.status === 'ready' && !leaseActive && bundle.lastUsedAt + RETENTION_MS <= now) {
191
- bundle.status = 'delete_pending'
192
- bundle.attempts = 0
193
- bundle.nextAttemptAt = now
226
+ const storageSet = normalizeStorageSet(raw)
227
+ if (!storageSet) continue
228
+ await refreshLeaseState(tx, storageSet, now, { removeExpired: true })
229
+ const leaseActive = storageSet.leaseUntil > now
230
+ const retentionMs = storageSet.identityStorageRetentionSeconds * 1000
231
+ const expired = storageSet.lastUsedAt + retentionMs <= now
232
+ if (leaseActive || !expired) {
233
+ storageSet.status = 'ready'
234
+ storageSet.attempts = 0
235
+ storageSet.nextAttemptAt = 0
236
+ } else if (storageSet.status === 'ready') {
237
+ storageSet.status = 'delete_pending'
238
+ storageSet.attempts = 0
239
+ storageSet.nextAttemptAt = now
194
240
  }
195
- await run('put', [bundle], BUNDLES_STORE, null, { tx })
196
- if (bundle.status === 'delete_pending' && !leaseActive && bundle.nextAttemptAt <= now) {
197
- due.push(bundle)
241
+ await run('put', [storageSet], STORAGE_SETS_STORE, null, { tx })
242
+ if (storageSet.status === 'delete_pending' && !leaseActive && storageSet.nextAttemptAt <= now) {
243
+ due.push(storageSet)
198
244
  }
199
245
  }
200
246
  return due
@@ -203,35 +249,43 @@ async function markAndListDueBundles (indexedDB, now) {
203
249
 
204
250
  async function finishDeleteAttempt (indexedDB, attempted, deleted, now) {
205
251
  return withRegistryTransaction(indexedDB, 'readwrite', async tx => {
206
- const current = normalizeBundle((await run('get', [attempted.userPubkey], BUNDLES_STORE, null, { tx })).result)
207
- if (!current || current.status !== 'delete_pending' || current.leaseUntil > now) return false
252
+ const current = normalizeStorageSet((await run('get', [attempted.userPubkey], STORAGE_SETS_STORE, null, { tx })).result)
253
+ if (!current || current.status !== 'delete_pending') return false
254
+ await refreshLeaseState(tx, current, now, { removeExpired: true })
255
+ if (current.leaseUntil > now) {
256
+ current.status = 'ready'
257
+ current.attempts = 0
258
+ current.nextAttemptAt = 0
259
+ await run('put', [current], STORAGE_SETS_STORE, null, { tx })
260
+ return false
261
+ }
208
262
  if (deleted) {
209
263
  for (const lease of await readLeases(tx, attempted.userPubkey)) {
210
264
  await run('delete', [lease.key], LEASES_STORE, null, { tx })
211
265
  }
212
- await run('delete', [attempted.userPubkey], BUNDLES_STORE, null, { tx })
266
+ await run('delete', [attempted.userPubkey], STORAGE_SETS_STORE, null, { tx })
213
267
  return true
214
268
  }
215
269
  current.attempts++
216
270
  current.nextAttemptAt = now + retryDelay(current.attempts)
217
- await run('put', [current], BUNDLES_STORE, null, { tx })
271
+ await run('put', [current], STORAGE_SETS_STORE, null, { tx })
218
272
  return false
219
273
  })
220
274
  }
221
275
 
222
276
  async function maintainRegistry (indexedDB, now) {
223
- const due = await markAndListDueBundles(indexedDB, now)
224
- for (const bundle of due) {
225
- const deleted = await deleteStorageSet(indexedDB, bundle.userPubkey)
226
- await finishDeleteAttempt(indexedDB, bundle, deleted, now)
277
+ const due = await markAndListDueStorageSets(indexedDB, now)
278
+ for (const storageSet of due) {
279
+ const deleted = await deleteStorageSet(indexedDB, storageSet.userPubkey)
280
+ await finishDeleteAttempt(indexedDB, storageSet, deleted, now)
227
281
  }
228
282
  const nextAttemptAt = await withRegistryTransaction(indexedDB, 'readonly', async tx => {
229
- const records = (await run('getAll', [], BUNDLES_STORE, null, { tx })).result
283
+ const records = (await run('getAll', [], STORAGE_SETS_STORE, null, { tx })).result
230
284
  let earliest = Infinity
231
285
  for (const raw of records) {
232
- const bundle = normalizeBundle(raw)
233
- if (bundle?.status !== 'delete_pending' || bundle.leaseUntil > now) continue
234
- earliest = Math.min(earliest, bundle.nextAttemptAt)
286
+ const storageSet = normalizeStorageSet(raw)
287
+ if (storageSet?.status !== 'delete_pending' || storageSet.leaseUntil > now) continue
288
+ earliest = Math.min(earliest, storageSet.nextAttemptAt)
235
289
  }
236
290
  return Number.isFinite(earliest) ? earliest : null
237
291
  })
@@ -268,6 +322,8 @@ export async function maintainPrivateMessengerStorage ({
268
322
  export function activatePrivateMessengerStorage ({
269
323
  userPubkey,
270
324
  leaseId,
325
+ activeChannelPubkeys,
326
+ storagePolicy: nextPolicy,
271
327
  indexedDB = globalThis.indexedDB,
272
328
  now = Date.now()
273
329
  } = {}) {
@@ -276,22 +332,61 @@ export function activatePrivateMessengerStorage ({
276
332
  return withMaintenanceLock(indexedDB, () => withRegistryTransaction(indexedDB, 'readwrite', async tx => {
277
333
  userPubkey = String(userPubkey)
278
334
  leaseId = String(leaseId)
279
- const current = normalizeBundle((await run('get', [userPubkey], BUNDLES_STORE, null, { tx })).result)
335
+ const current = normalizeStorageSet((await run('get', [userPubkey], STORAGE_SETS_STORE, null, { tx })).result)
336
+ const currentLease = (await run('get', [leaseKey(userPubkey, leaseId)], LEASES_STORE, null, { tx })).result
337
+ const channels = activeChannelPubkeys === undefined
338
+ ? normalizeActiveChannelPubkeys(currentLease?.activeChannelPubkeys)
339
+ : normalizeActiveChannelPubkeys(activeChannelPubkeys)
280
340
  await run('put', [{
281
341
  key: leaseKey(userPubkey, leaseId),
282
342
  userPubkey,
283
343
  leaseId,
284
- leaseUntil: now + LEASE_MS
344
+ leaseUntil: now + LEASE_MS,
345
+ activeChannelPubkeys: channels
285
346
  }], LEASES_STORE, null, { tx })
286
- const bundle = {
347
+ const hasPolicy = nextPolicy !== undefined
348
+ const storageSet = {
287
349
  userPubkey,
288
350
  lastUsedAt: now,
289
351
  leaseUntil: Math.max(current?.leaseUntil || 0, now + LEASE_MS),
290
352
  status: 'ready',
291
353
  attempts: 0,
292
- nextAttemptAt: 0
354
+ nextAttemptAt: 0,
355
+ staleChannelSeconds: hasPolicy
356
+ ? nextPolicy.staleChannelSeconds
357
+ : current?.staleChannelSeconds ?? DEFAULT_STALE_CHANNEL_SECONDS,
358
+ identityStorageRetentionSeconds: hasPolicy
359
+ ? nextPolicy.identityStorageRetentionSeconds
360
+ : current?.identityStorageRetentionSeconds ?? DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS,
361
+ policyRevision: hasPolicy
362
+ ? (current?.policyRevision || 0) + 1
363
+ : current?.policyRevision || 0
364
+ }
365
+ await run('put', [storageSet], STORAGE_SETS_STORE, null, { tx })
366
+ const leaseState = await refreshLeaseState(tx, storageSet, now)
367
+ return {
368
+ ...storagePolicy(storageSet),
369
+ activeChannelPubkeys: leaseState.activeChannelPubkeys
370
+ }
371
+ }))
372
+ }
373
+
374
+ export function readPrivateMessengerStorage ({
375
+ userPubkey,
376
+ indexedDB = globalThis.indexedDB,
377
+ now = Date.now()
378
+ } = {}) {
379
+ if (!userPubkey) return Promise.reject(new Error('USER_PUBKEY_REQUIRED'))
380
+ return withMaintenanceLock(indexedDB, () => withRegistryTransaction(indexedDB, 'readwrite', async tx => {
381
+ userPubkey = String(userPubkey)
382
+ const storageSet = normalizeStorageSet((await run('get', [userPubkey], STORAGE_SETS_STORE, null, { tx })).result)
383
+ if (!storageSet) return null
384
+ const leaseState = await refreshLeaseState(tx, storageSet, now, { removeExpired: true })
385
+ await run('put', [storageSet], STORAGE_SETS_STORE, null, { tx })
386
+ return {
387
+ ...storagePolicy(storageSet),
388
+ activeChannelPubkeys: leaseState.activeChannelPubkeys
293
389
  }
294
- await run('put', [bundle], BUNDLES_STORE, null, { tx })
295
390
  }))
296
391
  }
297
392
 
@@ -306,19 +401,20 @@ export function releasePrivateMessengerStorage ({
306
401
  return withMaintenanceLock(indexedDB, () => withRegistryTransaction(indexedDB, 'readwrite', async tx => {
307
402
  userPubkey = String(userPubkey)
308
403
  leaseId = String(leaseId)
309
- const current = normalizeBundle((await run('get', [userPubkey], BUNDLES_STORE, null, { tx })).result)
404
+ const current = normalizeStorageSet((await run('get', [userPubkey], STORAGE_SETS_STORE, null, { tx })).result)
310
405
  if (!current) return false
311
406
  await run('delete', [leaseKey(userPubkey, leaseId)], LEASES_STORE, null, { tx })
312
- await refreshLeaseUntil(tx, current, now, { removeExpired: true })
407
+ await refreshLeaseState(tx, current, now, { removeExpired: true })
313
408
  current.lastUsedAt = now
314
409
  current.status = 'ready'
315
410
  current.attempts = 0
316
411
  current.nextAttemptAt = 0
317
- await run('put', [current], BUNDLES_STORE, null, { tx })
412
+ await run('put', [current], STORAGE_SETS_STORE, null, { tx })
318
413
  return true
319
414
  }))
320
415
  }
321
416
 
322
417
  export const PRIVATE_MESSENGER_STORAGE_HEARTBEAT_MS = 60 * 60 * 1000
323
418
  export const PRIVATE_MESSENGER_STORAGE_MAINTENANCE_MS = 6 * 60 * 60 * 1000
324
- export const PRIVATE_MESSENGER_STORAGE_RETENTION_MS = RETENTION_MS
419
+ export const PRIVATE_MESSENGER_IDENTITY_STORAGE_RETENTION_MS = DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS * 1000
420
+ export { DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS }
@@ -1,9 +1,11 @@
1
+ import { ValidationError } from '../../error/index.js'
2
+
1
3
  const REGISTER_COUNT = 256
2
4
  const HLL_HEX_LENGTH = REGISTER_COUNT * 2
3
5
 
4
6
  function assertRegisters (registers) {
5
7
  if (!(registers instanceof Uint8Array) || registers.length !== REGISTER_COUNT) {
6
- throw new Error('INVALID_HLL_REGISTERS')
8
+ throw new ValidationError('INVALID_HLL_REGISTERS')
7
9
  }
8
10
  }
9
11
 
@@ -52,7 +52,7 @@ function relayListCreatedAt (event) {
52
52
  return Number.isFinite(event?.created_at) ? event.created_at : 0
53
53
  }
54
54
 
55
- function relaySetsEqual (a, b) {
55
+ function areRelaySetsEqual (a, b) {
56
56
  const left = new Set(a || [])
57
57
  const right = new Set(b || [])
58
58
  if (left.size !== right.size) return false
@@ -61,8 +61,8 @@ function relaySetsEqual (a, b) {
61
61
  }
62
62
 
63
63
  function relaySetChanges (previous, next) {
64
- const read = !relaySetsEqual(previous?.read, next?.read)
65
- const write = !relaySetsEqual(previous?.write, next?.write)
64
+ const read = !areRelaySetsEqual(previous?.read, next?.read)
65
+ const write = !areRelaySetsEqual(previous?.write, next?.write)
66
66
  return {
67
67
  read,
68
68
  write,