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
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getEventHash } from '
|
|
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
|
|
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
|
|
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
|
|
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 || !
|
|
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 || !
|
|
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
|
|
230
|
-
if (!question?.id) throw new
|
|
231
|
-
if (!receiverPubkey) throw new
|
|
232
|
-
if (!Number.isSafeInteger(eventsPerChunk) || eventsPerChunk < 1) throw new
|
|
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
|
|
302
|
-
if (!question?.id) throw new
|
|
303
|
-
if (!receiverPubkey) throw new
|
|
304
|
-
if (!Number.isSafeInteger(eventsPerChunk) || eventsPerChunk < 1) throw new
|
|
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,
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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('
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
57
|
-
db.createObjectStore(
|
|
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([
|
|
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
|
-
|
|
104
|
-
|
|
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
|
|
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
|
|
221
|
+
async function markAndListDueStorageSets (indexedDB, now) {
|
|
182
222
|
return withRegistryTransaction(indexedDB, 'readwrite', async tx => {
|
|
183
|
-
const records = (await run('getAll', [],
|
|
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
|
|
187
|
-
if (!
|
|
188
|
-
await
|
|
189
|
-
const leaseActive =
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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', [
|
|
196
|
-
if (
|
|
197
|
-
due.push(
|
|
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 =
|
|
207
|
-
if (!current || current.status !== 'delete_pending'
|
|
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],
|
|
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],
|
|
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
|
|
224
|
-
for (const
|
|
225
|
-
const deleted = await deleteStorageSet(indexedDB,
|
|
226
|
-
await finishDeleteAttempt(indexedDB,
|
|
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', [],
|
|
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
|
|
233
|
-
if (
|
|
234
|
-
earliest = Math.min(earliest,
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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],
|
|
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
|
|
419
|
+
export const PRIVATE_MESSENGER_IDENTITY_STORAGE_RETENTION_MS = DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS * 1000
|
|
420
|
+
export { DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS }
|
package/relay/helpers/hll.js
CHANGED
|
@@ -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
|
|
8
|
+
throw new ValidationError('INVALID_HLL_REGISTERS')
|
|
7
9
|
}
|
|
8
10
|
}
|
|
9
11
|
|
package/relay/services/query.js
CHANGED
|
@@ -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
|
|
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 = !
|
|
65
|
-
const 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,
|