libp2r2p 0.0.1
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/AGENTS.md +20 -0
- package/README.md +129 -0
- package/base16/index.js +14 -0
- package/base64/index.js +29 -0
- package/content-key/event/index.js +85 -0
- package/content-key/index.js +1 -0
- package/content-key/services/iykc-proof.js +129 -0
- package/double-dh/index.js +166 -0
- package/ecdh/index.js +8 -0
- package/idb/index.js +64 -0
- package/idb-queue/index.js +793 -0
- package/index.js +21 -0
- package/key/index.js +139 -0
- package/network/index.js +68 -0
- package/nip44-v3/index.js +175 -0
- package/nip46/constants/index.js +5 -0
- package/nip46/helpers/frame.js +51 -0
- package/nip46/helpers/url.js +96 -0
- package/nip46/index.js +5 -0
- package/nip46/services/bunker-signer.js +49 -0
- package/nip46/services/client.js +252 -0
- package/nip46/services/server-session.js +225 -0
- package/nip46/services/transport.js +265 -0
- package/package.json +49 -0
- package/private-channel/constants/index.js +5 -0
- package/private-channel/helpers/chunk-size.js +61 -0
- package/private-channel/helpers/chunks.js +282 -0
- package/private-channel/helpers/event.js +68 -0
- package/private-channel/index.js +942 -0
- package/private-channel/services/received-chunks.js +398 -0
- package/private-message/index.js +672 -0
- package/private-messenger/constants/index.js +1 -0
- package/private-messenger/index.js +1431 -0
- package/private-messenger/recovery/index.js +316 -0
- package/relay/constants/index.js +18 -0
- package/relay/helpers/hll.js +62 -0
- package/relay/helpers/publish.js +100 -0
- package/relay/helpers/routing.js +36 -0
- package/relay/helpers/timer.js +4 -0
- package/relay/index.js +4 -0
- package/relay/services/query.js +224 -0
- package/relay/services/relay-connection.js +156 -0
- package/relay/services/relay-pool.js +908 -0
- package/temporary-storage/index.js +85 -0
- package/tests/base16.test.js +19 -0
- package/tests/base64.test.js +18 -0
- package/tests/content-key-event.test.js +48 -0
- package/tests/fixtures/nip44v3-vectors.json +1 -0
- package/tests/helpers/test-signer.js +185 -0
- package/tests/idb-queue.test.js +153 -0
- package/tests/idb.test.js +91 -0
- package/tests/nip44-v3.test.js +73 -0
- package/tests/nip46.test.js +668 -0
- package/tests/private-channel.test.js +1899 -0
- package/tests/private-message.test.js +460 -0
- package/tests/private-messenger.test.js +1715 -0
- package/tests/queries.test.js +101 -0
- package/tests/queue-parity.test.js +105 -0
- package/tests/relay-hll.test.js +32 -0
- package/tests/relay-pool.test.js +2067 -0
- package/tests/temporary-storage.test.js +89 -0
- package/tests/web-storage-queue.test.js +480 -0
- package/web-storage-queue/index.js +652 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { base64ToBytes, bytesToBase64 } from '../../base64/index.js'
|
|
2
|
+
import { JSONL_CHUNK_BYTES } from '../helpers/chunk-size.js'
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_RECEIVED_CHUNK_TTL_MS = 60 * 60 * 1000 // 1 hour
|
|
5
|
+
// For illustration purposes: A 280-character rumor would take approximately
|
|
6
|
+
// 23 chunks (~0.65 MiB) to be sent if encrypted to 1000 participants.
|
|
7
|
+
export const DEFAULT_RECEIVED_CHUNK_MAX_BYTES = Math.min(JSONL_CHUNK_BYTES * 64, 3 * 1024 * 1024 /* 3 MiB cap */)
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PREFIX = 'libp2r2p:private-channel:received'
|
|
10
|
+
const encoder = new TextEncoder()
|
|
11
|
+
const decoder = new TextDecoder()
|
|
12
|
+
|
|
13
|
+
// Receive buffers survive reloads briefly, unlike send-side temporary chunks.
|
|
14
|
+
// Stale cleanup handles peers that never publish the remaining chunks.
|
|
15
|
+
|
|
16
|
+
function byteLength (value) {
|
|
17
|
+
return encoder.encode(String(value)).length
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseJson (raw, fallback) {
|
|
21
|
+
try { return JSON.parse(raw || '') } catch { return fallback }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function uniq (values) {
|
|
25
|
+
return [...new Set((values || []).filter(Boolean))]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeGroupKeys (value) {
|
|
29
|
+
return Array.isArray(value) ? uniq(value.filter(key => typeof key === 'string' && key)) : []
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeReceived (received) {
|
|
33
|
+
if (!received || typeof received !== 'object' || Array.isArray(received)) return {}
|
|
34
|
+
return Object.fromEntries(
|
|
35
|
+
Object.entries(received)
|
|
36
|
+
.filter(([index, hasChunk]) => hasChunk && Number.isSafeInteger(Number(index)) && Number(index) >= 0)
|
|
37
|
+
.map(([index]) => [String(Number(index)), true])
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeMeta (meta, groupKey) {
|
|
42
|
+
if (!meta || typeof meta !== 'object') return null
|
|
43
|
+
const total = Number(meta.total)
|
|
44
|
+
const nextIndex = Number(meta.nextIndex)
|
|
45
|
+
const rowIndex = Number(meta.rowIndex)
|
|
46
|
+
if (!Number.isSafeInteger(total) || total < 1) return null
|
|
47
|
+
return {
|
|
48
|
+
groupKey,
|
|
49
|
+
channelPubkey: String(meta.channelPubkey || ''),
|
|
50
|
+
routerPubkey: String(meta.routerPubkey || ''),
|
|
51
|
+
total,
|
|
52
|
+
received: normalizeReceived(meta.received),
|
|
53
|
+
receivedCount: Math.max(0, Number(meta.receivedCount) || 0),
|
|
54
|
+
nextIndex: Number.isSafeInteger(nextIndex) && nextIndex >= 0 ? nextIndex : 0,
|
|
55
|
+
rowIndex: Number.isSafeInteger(rowIndex) && rowIndex >= 0 ? rowIndex : 0,
|
|
56
|
+
carry: typeof meta.carry === 'string' ? meta.carry : '',
|
|
57
|
+
payloadCiphertext: typeof meta.payloadCiphertext === 'string' ? meta.payloadCiphertext : '',
|
|
58
|
+
receiverPubkeys: uniq(meta.receiverPubkeys || []),
|
|
59
|
+
byteSize: Math.max(0, Number(meta.byteSize) || 0),
|
|
60
|
+
createdAt: Number(meta.createdAt) || Date.now(),
|
|
61
|
+
updatedAt: Number(meta.updatedAt) || Date.now()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isQuotaExceeded (err) {
|
|
66
|
+
return err?.name === 'QuotaExceededError' ||
|
|
67
|
+
err?.name === 'NS_ERROR_DOM_QUOTA_REACHED' ||
|
|
68
|
+
err?.code === 22 ||
|
|
69
|
+
err?.code === 1014 ||
|
|
70
|
+
/quota/i.test(err?.message || '')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function joinBase64Chunks (parts) {
|
|
74
|
+
let out = ''
|
|
75
|
+
let carry = new Uint8Array()
|
|
76
|
+
|
|
77
|
+
for (const part of parts) {
|
|
78
|
+
const bytes = base64ToBytes(part)
|
|
79
|
+
const joined = new Uint8Array(carry.length + bytes.length)
|
|
80
|
+
joined.set(carry)
|
|
81
|
+
joined.set(bytes, carry.length)
|
|
82
|
+
|
|
83
|
+
const completeLength = joined.length - (joined.length % 3)
|
|
84
|
+
if (completeLength) out += bytesToBase64(joined.slice(0, completeLength))
|
|
85
|
+
carry = joined.slice(completeLength)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (carry.length) out += bytesToBase64(carry)
|
|
89
|
+
return out
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createReceivedChunkStore ({
|
|
93
|
+
prefix = DEFAULT_PREFIX,
|
|
94
|
+
storageArea = globalThis.localStorage,
|
|
95
|
+
ttlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS,
|
|
96
|
+
maxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES
|
|
97
|
+
} = {}) {
|
|
98
|
+
const groupsKey = `${prefix}:groups`
|
|
99
|
+
const configuredTtlMs = Number.isFinite(ttlMs) && ttlMs >= 0 ? ttlMs : DEFAULT_RECEIVED_CHUNK_TTL_MS
|
|
100
|
+
const configuredMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0 ? maxBytes : Infinity
|
|
101
|
+
|
|
102
|
+
function storage () {
|
|
103
|
+
return storageArea
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function metaKey (groupKey) {
|
|
107
|
+
return `${prefix}:group:${groupKey}:meta`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function chunkKey (groupKey, index) {
|
|
111
|
+
return `${prefix}:group:${groupKey}:chunk:${index}`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function groupKeyFor (channelPubkey, routerPubkey) {
|
|
115
|
+
return `${channelPubkey}:${routerPubkey}`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readGroupKeys () {
|
|
119
|
+
return normalizeGroupKeys(parseJson(storage().getItem(groupsKey), []))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function writeGroupKeys (keys) {
|
|
123
|
+
const normalized = normalizeGroupKeys(keys)
|
|
124
|
+
if (normalized.length) storage().setItem(groupsKey, JSON.stringify(normalized))
|
|
125
|
+
else storage().removeItem(groupsKey)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function addGroupKey (groupKey) {
|
|
129
|
+
const keys = readGroupKeys()
|
|
130
|
+
if (!keys.includes(groupKey)) writeGroupKeys(keys.concat(groupKey))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function removeGroupKey (groupKey) {
|
|
134
|
+
writeGroupKeys(readGroupKeys().filter(key => key !== groupKey))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readMeta (groupKey) {
|
|
138
|
+
return normalizeMeta(parseJson(storage().getItem(metaKey(groupKey)), null), groupKey)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function writeMeta (meta) {
|
|
142
|
+
storage().setItem(metaKey(meta.groupKey), JSON.stringify(meta))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function removeGroup (groupKey) {
|
|
146
|
+
const meta = readMeta(groupKey)
|
|
147
|
+
if (meta) {
|
|
148
|
+
for (const index of Object.keys(meta.received)) {
|
|
149
|
+
storage().removeItem(chunkKey(groupKey, index))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
storage().removeItem(metaKey(groupKey))
|
|
153
|
+
removeGroupKey(groupKey)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function allMetas () {
|
|
157
|
+
const keys = readGroupKeys()
|
|
158
|
+
const metas = []
|
|
159
|
+
const liveKeys = []
|
|
160
|
+
for (const groupKey of keys) {
|
|
161
|
+
const meta = readMeta(groupKey)
|
|
162
|
+
if (!meta) continue
|
|
163
|
+
metas.push(meta)
|
|
164
|
+
liveKeys.push(groupKey)
|
|
165
|
+
}
|
|
166
|
+
if (liveKeys.length !== keys.length) writeGroupKeys(liveKeys)
|
|
167
|
+
return metas
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function totalStoredBytes () {
|
|
171
|
+
return allMetas().reduce((total, meta) => {
|
|
172
|
+
return total + meta.byteSize + byteLength(JSON.stringify(meta))
|
|
173
|
+
}, 0)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function oldestGroupKey ({ except } = {}) {
|
|
177
|
+
return allMetas()
|
|
178
|
+
.filter(meta => meta.groupKey !== except)
|
|
179
|
+
.sort((a, b) => a.updatedAt - b.updatedAt)[0]?.groupKey || ''
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function evictOldestUntilFits (requiredBytes = 0, { except } = {}) {
|
|
183
|
+
if (!Number.isFinite(configuredMaxBytes)) return
|
|
184
|
+
while (totalStoredBytes() + requiredBytes > configuredMaxBytes) {
|
|
185
|
+
const groupKey = oldestGroupKey({ except })
|
|
186
|
+
if (!groupKey) break
|
|
187
|
+
removeGroup(groupKey)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function writeChunk (key, value, requiredBytes, except) {
|
|
192
|
+
evictOldestUntilFits(requiredBytes, { except })
|
|
193
|
+
try {
|
|
194
|
+
storage().setItem(key, value)
|
|
195
|
+
return
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (!isQuotaExceeded(err)) throw err
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let groupKey = oldestGroupKey({ except })
|
|
201
|
+
while (groupKey) {
|
|
202
|
+
removeGroup(groupKey)
|
|
203
|
+
try {
|
|
204
|
+
storage().setItem(key, value)
|
|
205
|
+
return
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (!isQuotaExceeded(err)) throw err
|
|
208
|
+
}
|
|
209
|
+
groupKey = oldestGroupKey({ except })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
storage().setItem(key, value)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function cleanupStale (nowMs = Date.now()) {
|
|
216
|
+
const cutoff = nowMs - configuredTtlMs
|
|
217
|
+
for (const meta of allMetas()) {
|
|
218
|
+
if (meta.updatedAt <= cutoff) removeGroup(meta.groupKey)
|
|
219
|
+
}
|
|
220
|
+
evictOldestUntilFits(0)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function put ({ channelPubkey, routerPubkey, index, total, content }) {
|
|
224
|
+
if (!channelPubkey || !routerPubkey) throw new Error('RECEIVED_CHUNK_GROUP_REQUIRED')
|
|
225
|
+
if (!Number.isSafeInteger(index) || !Number.isSafeInteger(total) || index < 0 || total < 1 || index >= total) {
|
|
226
|
+
throw new Error('INVALID_RECEIVED_CHUNK_INDEX')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
cleanupStale()
|
|
230
|
+
|
|
231
|
+
const groupKey = groupKeyFor(channelPubkey, routerPubkey)
|
|
232
|
+
const now = Date.now()
|
|
233
|
+
const chunk = String(content || '')
|
|
234
|
+
const nextBytes = byteLength(chunk)
|
|
235
|
+
let meta = readMeta(groupKey)
|
|
236
|
+
|
|
237
|
+
if (meta && meta.total !== total) {
|
|
238
|
+
removeGroup(groupKey)
|
|
239
|
+
meta = null
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!meta) {
|
|
243
|
+
meta = normalizeMeta({
|
|
244
|
+
channelPubkey,
|
|
245
|
+
routerPubkey,
|
|
246
|
+
total,
|
|
247
|
+
received: {},
|
|
248
|
+
receivedCount: 0,
|
|
249
|
+
nextIndex: 0,
|
|
250
|
+
rowIndex: 0,
|
|
251
|
+
carry: '',
|
|
252
|
+
payloadCiphertext: '',
|
|
253
|
+
receiverPubkeys: [],
|
|
254
|
+
byteSize: 0,
|
|
255
|
+
createdAt: now,
|
|
256
|
+
updatedAt: now
|
|
257
|
+
}, groupKey)
|
|
258
|
+
addGroupKey(groupKey)
|
|
259
|
+
writeMeta(meta)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!meta.received[String(index)]) {
|
|
263
|
+
if (meta.byteSize + nextBytes > configuredMaxBytes) {
|
|
264
|
+
removeGroup(groupKey)
|
|
265
|
+
throw new Error('RECEIVED_CHUNK_GROUP_TOO_LARGE')
|
|
266
|
+
}
|
|
267
|
+
writeChunk(chunkKey(groupKey, index), chunk, nextBytes, groupKey)
|
|
268
|
+
meta.received[String(index)] = true
|
|
269
|
+
meta.receivedCount++
|
|
270
|
+
meta.byteSize += nextBytes
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
meta.total = total
|
|
274
|
+
meta.updatedAt = now
|
|
275
|
+
writeMeta(meta)
|
|
276
|
+
evictOldestUntilFits(0, { except: groupKey })
|
|
277
|
+
return meta
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function status (metaOrGroupKey) {
|
|
281
|
+
const meta = typeof metaOrGroupKey === 'string' ? readMeta(metaOrGroupKey) : metaOrGroupKey
|
|
282
|
+
if (!meta) return { received: 0, missing: [] }
|
|
283
|
+
|
|
284
|
+
const missing = []
|
|
285
|
+
let received = 0
|
|
286
|
+
for (let index = 0; index < meta.total; index++) {
|
|
287
|
+
if (index < meta.nextIndex || meta.received[String(index)]) received++
|
|
288
|
+
else missing.push(index)
|
|
289
|
+
}
|
|
290
|
+
return { received, missing }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function rememberReceiverPubkey (meta, pubkey) {
|
|
294
|
+
if (!pubkey || meta.receiverPubkeys.includes(pubkey)) return
|
|
295
|
+
meta.receiverPubkeys.push(pubkey)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function rememberPayloadCiphertext (meta, ciphertext) {
|
|
299
|
+
if (!meta.payloadCiphertext) meta.payloadCiphertext = ciphertext
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function drainAvailable (groupKey, { onLine } = {}) {
|
|
303
|
+
const meta = readMeta(groupKey)
|
|
304
|
+
if (!meta) return { complete: false, stopped: false, meta: null }
|
|
305
|
+
|
|
306
|
+
while (meta.nextIndex < meta.total) {
|
|
307
|
+
const index = meta.nextIndex
|
|
308
|
+
const raw = storage().getItem(chunkKey(groupKey, index))
|
|
309
|
+
if (raw == null) break
|
|
310
|
+
|
|
311
|
+
const text = `${meta.carry}${decoder.decode(base64ToBytes(raw))}`
|
|
312
|
+
let start = 0
|
|
313
|
+
let end = text.indexOf('\n', start)
|
|
314
|
+
|
|
315
|
+
while (end !== -1) {
|
|
316
|
+
const line = text.slice(start, end)
|
|
317
|
+
start = end + 1
|
|
318
|
+
if (line) {
|
|
319
|
+
const result = await onLine?.(line, meta.rowIndex, meta, { rememberPayloadCiphertext, rememberReceiverPubkey })
|
|
320
|
+
meta.rowIndex++
|
|
321
|
+
if (result?.stop) {
|
|
322
|
+
meta.updatedAt = Date.now()
|
|
323
|
+
writeMeta(meta)
|
|
324
|
+
return { complete: false, stopped: true, meta }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
end = text.indexOf('\n', start)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
meta.carry = text.slice(start)
|
|
331
|
+
|
|
332
|
+
meta.nextIndex++
|
|
333
|
+
meta.updatedAt = Date.now()
|
|
334
|
+
writeMeta(meta)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (meta.nextIndex >= meta.total) {
|
|
338
|
+
if (meta.carry) {
|
|
339
|
+
const result = await onLine?.(meta.carry, meta.rowIndex, meta, { rememberPayloadCiphertext, rememberReceiverPubkey })
|
|
340
|
+
meta.rowIndex++
|
|
341
|
+
meta.carry = ''
|
|
342
|
+
if (result?.stop) {
|
|
343
|
+
meta.updatedAt = Date.now()
|
|
344
|
+
writeMeta(meta)
|
|
345
|
+
return { complete: false, stopped: true, meta }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
meta.updatedAt = Date.now()
|
|
349
|
+
writeMeta(meta)
|
|
350
|
+
return { complete: true, stopped: false, meta }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { complete: false, stopped: false, meta }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function readEnvelopeBundleContent (groupKey) {
|
|
357
|
+
const meta = readMeta(groupKey)
|
|
358
|
+
if (!meta) return ''
|
|
359
|
+
const parts = []
|
|
360
|
+
for (let index = 0; index < meta.total; index++) {
|
|
361
|
+
const chunk = storage().getItem(chunkKey(groupKey, index))
|
|
362
|
+
if (chunk == null) throw new Error('RECEIVED_CHUNK_MISSING')
|
|
363
|
+
parts.push(chunk)
|
|
364
|
+
}
|
|
365
|
+
return joinBase64Chunks(parts)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function readEnvelopeBundleText (groupKey) {
|
|
369
|
+
const content = readEnvelopeBundleContent(groupKey)
|
|
370
|
+
return decoder.decode(base64ToBytes(content))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function readChunkContents (groupKey) {
|
|
374
|
+
const meta = readMeta(groupKey)
|
|
375
|
+
if (!meta) return []
|
|
376
|
+
const parts = []
|
|
377
|
+
for (let index = 0; index < meta.total; index++) {
|
|
378
|
+
const chunk = storage().getItem(chunkKey(groupKey, index))
|
|
379
|
+
if (chunk == null) throw new Error('RECEIVED_CHUNK_MISSING')
|
|
380
|
+
parts.push(chunk)
|
|
381
|
+
}
|
|
382
|
+
return parts
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
cleanupStale()
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
cleanupStale,
|
|
389
|
+
drainAvailable,
|
|
390
|
+
groupKeyFor,
|
|
391
|
+
put,
|
|
392
|
+
readChunkContents,
|
|
393
|
+
readEnvelopeBundleContent,
|
|
394
|
+
readEnvelopeBundleText,
|
|
395
|
+
removeGroup,
|
|
396
|
+
status
|
|
397
|
+
}
|
|
398
|
+
}
|