libp2r2p 0.10.28 → 0.11.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/README.md +23 -0
- package/nip27/index.js +1 -0
- package/nip94/README.md +13 -0
- package/nip94/index.js +56 -2
- package/package.json +4 -1
- package/private-channel/helpers/chunk-size.js +1 -1
- package/private-channel/helpers/chunks.js +1 -1
- package/private-channel/helpers/event.js +14 -3
- package/private-channel/index.js +25 -7
- package/private-channel/services/received-chunks.js +6 -3
- package/private-message/index.js +21 -7
- package/private-messenger/event-store/README.md +110 -0
- package/private-messenger/event-store/helpers/codec.js +136 -0
- package/private-messenger/event-store/index.js +176 -0
- package/private-messenger/file/README.md +152 -0
- package/private-messenger/file/helpers/cache.js +159 -0
- package/private-messenger/file/helpers/catalog.js +87 -0
- package/private-messenger/file/helpers/stream.js +31 -0
- package/private-messenger/file/index.js +433 -0
- package/private-messenger/index.js +45 -10
- package/private-messenger/recovery/index.js +1 -1
- package/private-messenger/services/storage-maintenance.js +3 -0
- package/private-messenger/session/README.md +39 -0
- package/private-messenger/session/helpers/publication.js +40 -0
- package/private-messenger/session/helpers/signer.js +13 -0
- package/private-messenger/session/helpers/work-storage.js +32 -0
- package/private-messenger/session/index.js +307 -0
package/README.md
CHANGED
|
@@ -890,3 +890,26 @@ complete per-relay report without delaying the first-success return. Connection
|
|
|
890
890
|
establishment retains its separate 3-second deadline. PrivateMessenger inherits
|
|
891
891
|
this publication policy, including seeder/recovery messages. A timeout indicates
|
|
892
892
|
missing acknowledgement and cannot prove that the recipient did not receive it.
|
|
893
|
+
|
|
894
|
+
## Private file channels (0.11)
|
|
895
|
+
|
|
896
|
+
`libp2r2p/private-messenger/file` provides `createPrivateFileTransfer` for
|
|
897
|
+
separate per-root media channels, bounded indexed recovery and persisted-byte
|
|
898
|
+
progress. [The file API and protocol](private-messenger/file/README.md) document
|
|
899
|
+
signer/storage adapters and lifecycle. All original and thumbnail chunks belong
|
|
900
|
+
to `dm:media:<root>`; small authenticated recovery requests stay on `dm`.
|
|
901
|
+
NIP-94 supports `thumbnail` metadata with unordered `mark`/`size` root extensions.
|
|
902
|
+
|
|
903
|
+
This release changes the encrypted router recipient tag from `r` to `p` without
|
|
904
|
+
legacy fallback. Upgrade communicating consumers together. Inner ask/reply/tell
|
|
905
|
+
`r` tags, file-reference `r` tags, and carrier 26400 are unchanged. Seeders now
|
|
906
|
+
capture outgoing ordinary-message ciphertext before publishing, even without relay
|
|
907
|
+
echo. For files, seeders instead keep expiring recipient authorizations and serve
|
|
908
|
+
verified local chunks as `irfsChunk_v1`; watchtowers retain ciphertext seeds.
|
|
909
|
+
No group derivation or multi-device seed replication is introduced.
|
|
910
|
+
|
|
911
|
+
Private-message consumers can now substitute recovery persistence through
|
|
912
|
+
[`private-messenger/event-store`](private-messenger/event-store/README.md), or use
|
|
913
|
+
local defaults, including a bounded streaming file cache. The optional
|
|
914
|
+
[`private-messenger/session`](private-messenger/session/README.md) coordinator
|
|
915
|
+
provides reusable DM inbox/outbox and attachment lifecycle without UI coupling.
|
package/nip27/index.js
CHANGED
package/nip94/README.md
CHANGED
|
@@ -38,3 +38,16 @@ A bare/empty/duplicate/invalid value throws `INVALID_MEDIA_METADATA_DOWNLOAD`;
|
|
|
38
38
|
`extractMedia()` carries the string flag for ordinary URLs and nfile URLs,
|
|
39
39
|
including long nfile entities and `?localOnly=1`. Metadata fragments do not
|
|
40
40
|
change the bytes or root of a file; clients strip them from download routes.
|
|
41
|
+
|
|
42
|
+
## Thumbnails
|
|
43
|
+
|
|
44
|
+
`thumbnail: { url, root?, size?, sha256? }` emits a `thumb` tag and, when a root
|
|
45
|
+
is supplied, `['r', root, 'mark thumb', 'size <bytes>']`. `sha256` is the optional
|
|
46
|
+
third thumb-tag value. Original `r` stays unmarked. Decode validates nfile/root
|
|
47
|
+
agreement separately for original and thumbnail.
|
|
48
|
+
|
|
49
|
+
Only `r[1]` has a fixed position. `parseRootReference(tag)` parses the variadic
|
|
50
|
+
fields by their names; `mark` and `size` may appear in either order. Unknown
|
|
51
|
+
extensions are ignored. Duplicate recognized fields, ambiguous roots and unsafe
|
|
52
|
+
sizes are rejected. ThumbHash remains unchanged Base64, including through
|
|
53
|
+
percent-encoded NIP-27 `thumbhash` fragments; legacy `blurhash` still parses.
|
package/nip94/index.js
CHANGED
|
@@ -19,10 +19,36 @@ export function decodeFileMetadata (event) {
|
|
|
19
19
|
try { url = new URL(result.url) } catch { throw new ValidationError('INVALID_FILE_METADATA_URL') }
|
|
20
20
|
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password) throw new ValidationError('INVALID_FILE_METADATA_URL')
|
|
21
21
|
if (typeof result.mime !== 'string' || !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(result.mime)) throw new ValidationError('INVALID_FILE_METADATA_MIME')
|
|
22
|
-
for (const [tag, key] of [['
|
|
22
|
+
for (const [tag, key] of [['service', 'service'], ['thumbhash', 'thumbhash'], ['x', 'sha256'], ['ox', 'originalSha256'], ['alt', 'alt']]) {
|
|
23
23
|
const value = field(tag)
|
|
24
24
|
if (value !== undefined) result[key] = value
|
|
25
25
|
}
|
|
26
|
+
const roots = event.tags.filter(tag => Array.isArray(tag) && tag[0] === 'r').map(parseRootReference)
|
|
27
|
+
const originals = roots.filter(ref => ref.mark === undefined)
|
|
28
|
+
const thumbnails = roots.filter(ref => ref.mark === 'thumb')
|
|
29
|
+
if (originals.length > 1 || thumbnails.length > 1) throw new ValidationError('INVALID_FILE_METADATA_ROOT_REFERENCE')
|
|
30
|
+
if (originals.length) result.root = originals[0].root
|
|
31
|
+
const thumbUrl = field('thumb')
|
|
32
|
+
if (thumbUrl !== undefined) {
|
|
33
|
+
const thumbnail = { url: thumbUrl }
|
|
34
|
+
let parsed
|
|
35
|
+
try { parsed = new URL(thumbUrl) } catch { throw new ValidationError('INVALID_FILE_METADATA_THUMBNAIL') }
|
|
36
|
+
if (!['https:', 'http:'].includes(parsed.protocol) || parsed.username || parsed.password) throw new ValidationError('INVALID_FILE_METADATA_THUMBNAIL')
|
|
37
|
+
if (thumbnails.length) {
|
|
38
|
+
thumbnail.root = thumbnails[0].root
|
|
39
|
+
if (thumbnails[0].size !== undefined) thumbnail.size = thumbnails[0].size
|
|
40
|
+
}
|
|
41
|
+
const hash = event.tags.find(tag => tag[0] === 'thumb')?.[2]
|
|
42
|
+
if (hash !== undefined) {
|
|
43
|
+
if (!/^[0-9a-f]{64}$/.test(hash)) throw new ValidationError('INVALID_FILE_METADATA_HASH')
|
|
44
|
+
thumbnail.sha256 = hash
|
|
45
|
+
}
|
|
46
|
+
if (parsed.origin === 'https://nostr.alt') {
|
|
47
|
+
const file = nfileDecode(parsed.pathname.slice(1))
|
|
48
|
+
if (thumbnail.root && thumbnail.root !== file.root) throw new ValidationError('FILE_METADATA_NFILE_MISMATCH')
|
|
49
|
+
}
|
|
50
|
+
result.thumbnail = thumbnail
|
|
51
|
+
} else if (thumbnails.length) throw new ValidationError('INVALID_FILE_METADATA_THUMBNAIL')
|
|
26
52
|
for (const key of ['root', 'sha256', 'originalSha256']) if (result[key] !== undefined && !/^[0-9a-f]{64}$/.test(result[key])) throw new ValidationError('INVALID_FILE_METADATA_HASH')
|
|
27
53
|
const size = field('size')
|
|
28
54
|
if (size !== undefined) {
|
|
@@ -49,7 +75,35 @@ export function createFileMetadata ({ caption = '', created_at: createdAt = Math
|
|
|
49
75
|
if (metadata.download !== undefined && !['0', '1'].includes(metadata.download)) throw new ValidationError('INVALID_FILE_METADATA_DOWNLOAD')
|
|
50
76
|
const fields = [['download', metadata.download], ['url', metadata.url], ['m', metadata.mime], ['r', metadata.root], ['size', metadata.size], ['service', metadata.service], ['thumbhash', metadata.thumbhash], ['x', metadata.sha256], ['ox', metadata.originalSha256], ['alt', metadata.alt]]
|
|
51
77
|
if (metadata.width !== undefined || metadata.height !== undefined) fields.push(['dim', `${metadata.width}x${metadata.height}`])
|
|
52
|
-
|
|
78
|
+
if (metadata.thumbnail !== undefined) {
|
|
79
|
+
const thumb = metadata.thumbnail
|
|
80
|
+
if (!thumb || typeof thumb !== 'object') throw new ValidationError('INVALID_FILE_METADATA_THUMBNAIL')
|
|
81
|
+
fields.push(thumb.sha256 === undefined ? ['thumb', thumb.url] : ['thumb', thumb.url, thumb.sha256])
|
|
82
|
+
if (thumb.root !== undefined) fields.push(['r', thumb.root, 'mark thumb', ...(thumb.size === undefined ? [] : [`size ${thumb.size}`])])
|
|
83
|
+
}
|
|
84
|
+
const event = { kind: FILE_METADATA, created_at: createdAt, content: caption, tags: [...fields.filter(([, value]) => value !== undefined).map(([key, ...values]) => [key, ...values.map(String)]), ...tags.map(tag => [...tag])] }
|
|
53
85
|
decodeFileMetadata(event)
|
|
54
86
|
return event
|
|
55
87
|
}
|
|
88
|
+
|
|
89
|
+
// Only the root has a fixed position; extensions are unordered key/value fields.
|
|
90
|
+
export function parseRootReference (tag) {
|
|
91
|
+
if (!Array.isArray(tag) || tag[0] !== 'r' || typeof tag[1] !== 'string' || !/^[0-9a-f]{64}$/.test(tag[1])) throw new ValidationError('INVALID_FILE_METADATA_ROOT_REFERENCE')
|
|
92
|
+
const result = { root: tag[1] }
|
|
93
|
+
for (const field of tag.slice(2)) {
|
|
94
|
+
if (typeof field !== 'string') throw new ValidationError('INVALID_FILE_METADATA_ROOT_REFERENCE')
|
|
95
|
+
const separator = field.indexOf(' ')
|
|
96
|
+
const key = separator < 0 ? field : field.slice(0, separator)
|
|
97
|
+
if (!['mark', 'size'].includes(key)) continue
|
|
98
|
+
if (Object.hasOwn(result, key) || separator < 0) throw new ValidationError('INVALID_FILE_METADATA_ROOT_REFERENCE')
|
|
99
|
+
const value = field.slice(separator + 1)
|
|
100
|
+
if (key === 'mark') {
|
|
101
|
+
if (!value) throw new ValidationError('INVALID_FILE_METADATA_ROOT_REFERENCE')
|
|
102
|
+
result.mark = value
|
|
103
|
+
} else {
|
|
104
|
+
if (!/^(0|[1-9][0-9]*)$/.test(value) || !Number.isSafeInteger(Number(value))) throw new ValidationError('INVALID_FILE_METADATA_SIZE')
|
|
105
|
+
result.size = Number(value)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libp2r2p",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Peer-to-relay-to-peer",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"p2r2p",
|
|
@@ -86,7 +86,10 @@
|
|
|
86
86
|
"./private-channel": "./private-channel/index.js",
|
|
87
87
|
"./private-message": "./private-message/index.js",
|
|
88
88
|
"./private-messenger": "./private-messenger/index.js",
|
|
89
|
+
"./private-messenger/event-store": "./private-messenger/event-store/index.js",
|
|
90
|
+
"./private-messenger/file": "./private-messenger/file/index.js",
|
|
89
91
|
"./private-messenger/recovery": "./private-messenger/recovery/index.js",
|
|
92
|
+
"./private-messenger/session": "./private-messenger/session/index.js",
|
|
90
93
|
"./relay": "./relay/index.js",
|
|
91
94
|
"./temporary-storage": "./temporary-storage/index.js",
|
|
92
95
|
"./url": "./url/index.js",
|
|
@@ -16,7 +16,7 @@ function routerPlaintextByteLengthForChunk (jsonlByteLength) {
|
|
|
16
16
|
kind: ROUTER_KIND,
|
|
17
17
|
pubkey: SAMPLE_PUBKEY,
|
|
18
18
|
created_at: MAX_TIME_SECONDS,
|
|
19
|
-
tags: [['f', SAMPLE_PUBKEY], ['imkc', SAMPLE_PUBKEY, `${MAX_TIME_SECONDS}:${SAMPLE_SIGNATURE}`], ['c', MAX_CHUNK_TAG_VALUE, MAX_CHUNK_TAG_VALUE], ['
|
|
19
|
+
tags: [['f', SAMPLE_PUBKEY], ['imkc', SAMPLE_PUBKEY, `${MAX_TIME_SECONDS}:${SAMPLE_SIGNATURE}`], ['c', MAX_CHUNK_TAG_VALUE, MAX_CHUNK_TAG_VALUE], ['p', SAMPLE_PUBKEY], ['i', String(Number.MAX_SAFE_INTEGER)]],
|
|
20
20
|
content: 'A'.repeat(base64EncodedByteLength(jsonlByteLength)),
|
|
21
21
|
id: SAMPLE_PUBKEY,
|
|
22
22
|
sig: SAMPLE_SIGNATURE
|
|
@@ -156,7 +156,7 @@ function setPreparedRow (id, index, row, temporaryStorage) {
|
|
|
156
156
|
storageFor(temporaryStorage).setItem(rowTempKey(id, index), row)
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
function readPreparedRow (preparedRows, index) {
|
|
159
|
+
export function readPreparedRow (preparedRows, index) {
|
|
160
160
|
const row = storageFor(preparedRows.temporaryStorage).getItem(rowTempKey(preparedRows.id, index))
|
|
161
161
|
if (typeof row !== 'string') throw new ValidationError('MISSING_PREPARED_ROW')
|
|
162
162
|
return row
|
|
@@ -12,7 +12,7 @@ export function eventByteLength (event) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function readReceiverTag (event) {
|
|
15
|
-
return event.tags?.find(t => t[0] === '
|
|
15
|
+
return event.tags?.find(t => t[0] === 'p')?.[1] || ''
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function readSenderTag (event) {
|
|
@@ -47,14 +47,18 @@ export function readChunkTag (event) {
|
|
|
47
47
|
return { index, total }
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
export function makeRouterEvent ({ pubkey, senderPubkey, imkcPubkey, imkcProof, receiverPubkey, chunkIndex, chunkTotal, content }) {
|
|
50
|
+
export function makeRouterEvent ({ pubkey, senderPubkey, imkcPubkey, imkcProof, receiverPubkey, chunkIndex, chunkTotal, fileChunkIndex, content }) {
|
|
51
51
|
const tags = [['f', senderPubkey]]
|
|
52
52
|
if (imkcPubkey) {
|
|
53
53
|
if (!imkcProof) throw new ValidationError('INVALID_IMKC_PROOF')
|
|
54
54
|
tags.push(['imkc', imkcPubkey, imkcProof])
|
|
55
55
|
}
|
|
56
|
+
if (fileChunkIndex !== undefined) {
|
|
57
|
+
if (!Number.isSafeInteger(fileChunkIndex) || fileChunkIndex < 0) throw new ValidationError('INVALID_FILE_CHUNK_INDEX')
|
|
58
|
+
tags.push(['i', String(fileChunkIndex)])
|
|
59
|
+
}
|
|
56
60
|
tags.push(['c', String(chunkIndex), String(chunkTotal)])
|
|
57
|
-
if (receiverPubkey) tags.push(['
|
|
61
|
+
if (receiverPubkey) tags.push(['p', receiverPubkey])
|
|
58
62
|
return { kind: ROUTER_KIND, pubkey, created_at: nowSeconds(), tags, content }
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -67,3 +71,10 @@ export function makeNymCarrierEvent ({ innerId, chunkIndex, chunkTotal, content,
|
|
|
67
71
|
content
|
|
68
72
|
}
|
|
69
73
|
}
|
|
74
|
+
|
|
75
|
+
export function readFileChunkIndex (event) {
|
|
76
|
+
const tags = event.tags?.filter(tag => tag[0] === 'i') || []
|
|
77
|
+
if (!tags.length) return undefined
|
|
78
|
+
if (tags.length !== 1 || tags[0].length !== 2 || !/^(0|[1-9][0-9]*)$/.test(tags[0][1]) || !Number.isSafeInteger(Number(tags[0][1]))) throw new ValidationError('INVALID_FILE_CHUNK_INDEX')
|
|
79
|
+
return Number(tags[0][1])
|
|
80
|
+
}
|
package/private-channel/index.js
CHANGED
|
@@ -18,12 +18,13 @@ import {
|
|
|
18
18
|
prepareEnvelopeRows,
|
|
19
19
|
preparedRowIndexesForReceivers,
|
|
20
20
|
readChunkContent,
|
|
21
|
+
readPreparedRow,
|
|
21
22
|
receiverPubkeys,
|
|
22
23
|
receiverPubkeysWithoutContentKeys,
|
|
23
24
|
writeChunksFromPreparedRows
|
|
24
25
|
} from './helpers/chunks.js'
|
|
25
26
|
import { EXPIRATION_SECONDS, MAX_EVENT_BYTES, NYM_CARRIER_KIND, PRIVATE_BROADCAST_KIND, ROUTER_KIND } from './constants/index.js'
|
|
26
|
-
import { eventByteLength, hasImkcTag, makeNymCarrierEvent, makeRouterEvent, nowSeconds, readChunkTag, readIdTag, readImkcProof, readImkcTag, readReceiverTag, readSenderTag } from './helpers/event.js'
|
|
27
|
+
import { eventByteLength, hasImkcTag, makeNymCarrierEvent, makeRouterEvent, nowSeconds, readChunkTag, readFileChunkIndex, readIdTag, readImkcProof, readImkcTag, readReceiverTag, readSenderTag } from './helpers/event.js'
|
|
27
28
|
import { createReceivedChunkStore, DEFAULT_RECEIVED_CHUNK_MAX_BYTES, DEFAULT_RECEIVED_CHUNK_TTL_MS } from './services/received-chunks.js'
|
|
28
29
|
|
|
29
30
|
export { EXPIRATION_SECONDS, MAX_EVENT_BYTES, NYM_CARRIER_KIND, PRIVATE_BROADCAST_KIND, ROUTER_KIND } from './constants/index.js'
|
|
@@ -131,7 +132,7 @@ async function prepareRoutedMessage ({ senderSigner, imkcSigner, privateChannelS
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
async function * wrapPreparedEvents ({ privateChannelSigner, receivers, receiverTag, deletionPubkey, expirationSeconds = EXPIRATION_SECONDS, context }) {
|
|
135
|
+
async function * wrapPreparedEvents ({ privateChannelSigner, receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey, expirationSeconds = EXPIRATION_SECONDS, context }) {
|
|
135
136
|
const routerSeckey = generateSecretKey()
|
|
136
137
|
const routerPubkey = getPublicKey(routerSeckey)
|
|
137
138
|
const receiverPubkeyList = receiverPubkeys(receivers)
|
|
@@ -144,6 +145,13 @@ async function * wrapPreparedEvents ({ privateChannelSigner, receivers, receiver
|
|
|
144
145
|
const temporaryStorage = context.preparedRows.temporaryStorage
|
|
145
146
|
|
|
146
147
|
try {
|
|
148
|
+
if (onPreparedSeed) {
|
|
149
|
+
const router = makeRouterEvent({ pubkey: routerPubkey, senderPubkey: context.senderPubkey, imkcPubkey: context.imkcPubkey, imkcProof: context.imkcProof, receiverPubkey: routerReceiverTag, chunkIndex: 0, chunkTotal: 1, fileChunkIndex, content: '' })
|
|
150
|
+
const payloadRow = readPreparedRow(context.preparedRows, 0)
|
|
151
|
+
for (const rowIndex of rowIndexes) {
|
|
152
|
+
await onPreparedSeed({ channelPubkey: context.channelPubkey, router, jsonl: `${payloadRow}\n${readPreparedRow(context.preparedRows, rowIndex)}\n` })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
147
155
|
for (let index = 0; index < total; index++) {
|
|
148
156
|
const content = readChunkContent(id, index, temporaryStorage)
|
|
149
157
|
const router = finalizeEvent(makeRouterEvent({
|
|
@@ -152,6 +160,7 @@ async function * wrapPreparedEvents ({ privateChannelSigner, receivers, receiver
|
|
|
152
160
|
imkcPubkey: context.imkcPubkey,
|
|
153
161
|
imkcProof: context.imkcProof,
|
|
154
162
|
receiverPubkey: routerReceiverTag,
|
|
163
|
+
fileChunkIndex,
|
|
155
164
|
chunkIndex: index,
|
|
156
165
|
chunkTotal: total,
|
|
157
166
|
content
|
|
@@ -172,7 +181,7 @@ async function * wrapPreparedEvents ({ privateChannelSigner, receivers, receiver
|
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
// Streaming version of wrapEvent
|
|
175
|
-
export async function * wrapEvents ({ senderSigner, imkcSigner, privateChannelSigner = senderSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey, event, expirationSeconds = EXPIRATION_SECONDS, temporaryStorageArea, _getIykcProofs = getIykcProofs }) {
|
|
184
|
+
export async function * wrapEvents ({ senderSigner, imkcSigner, privateChannelSigner = senderSigner, privateChannelReaderPubkey, receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey, event, expirationSeconds = EXPIRATION_SECONDS, temporaryStorageArea, _getIykcProofs = getIykcProofs }) {
|
|
176
185
|
const normalizedDeletionPubkey = normalizeDeletionPubkey(deletionPubkey)
|
|
177
186
|
const context = await prepareRoutedMessage({
|
|
178
187
|
senderSigner,
|
|
@@ -185,7 +194,7 @@ export async function * wrapEvents ({ senderSigner, imkcSigner, privateChannelSi
|
|
|
185
194
|
_getIykcProofs
|
|
186
195
|
})
|
|
187
196
|
try {
|
|
188
|
-
yield * wrapPreparedEvents({ privateChannelSigner, receivers, receiverTag, deletionPubkey: normalizedDeletionPubkey, expirationSeconds, context })
|
|
197
|
+
yield * wrapPreparedEvents({ privateChannelSigner, receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey: normalizedDeletionPubkey, expirationSeconds, context })
|
|
189
198
|
} finally {
|
|
190
199
|
cleanupEnvelopeRows(context.preparedRows)
|
|
191
200
|
}
|
|
@@ -415,6 +424,12 @@ export async function unwrapEvent ({ receiverSigner, privateChannelSigner = rece
|
|
|
415
424
|
channelReaderSigner,
|
|
416
425
|
channelReaderPubkey: privateChannelReaderPubkey
|
|
417
426
|
})
|
|
427
|
+
return unwrapRouterEvent({ router, receiverSigner, receiverPubkey, channelPubkey })
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Recovery records contain an encrypted recipient row, not the original outer
|
|
431
|
+
// signature. Authenticate the control response before invoking this decoder.
|
|
432
|
+
export async function unwrapRouterEvent ({ router, receiverSigner, receiverPubkey, channelPubkey }) {
|
|
418
433
|
if (router.kind !== ROUTER_KIND) throw new ValidationError('INVALID_ROUTER_KIND')
|
|
419
434
|
if (receiverPubkey && readReceiverTag(router) && readReceiverTag(router) !== receiverPubkey) return null
|
|
420
435
|
|
|
@@ -506,7 +521,7 @@ function withRecoveryRelays (relays, recoveryRelays) {
|
|
|
506
521
|
return uniq([...(relays || []), ...(recoveryRelays || [])])
|
|
507
522
|
}
|
|
508
523
|
|
|
509
|
-
export async function publish ({ senderSigner, imkcSigner, privateChannelSigner = senderSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs = getIykcProofs, _publish = sendToRelays }) {
|
|
524
|
+
export async function publish ({ senderSigner, imkcSigner, privateChannelSigner = senderSigner, privateChannelReaderPubkey, receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs = getIykcProofs, _publish = sendToRelays }) {
|
|
510
525
|
const normalizedDeletionPubkey = normalizeDeletionPubkey(deletionPubkey)
|
|
511
526
|
const results = []
|
|
512
527
|
// Relays are grouped only when they have the exact same recipient pubkey set
|
|
@@ -524,7 +539,7 @@ export async function publish ({ senderSigner, imkcSigner, privateChannelSigner
|
|
|
524
539
|
})
|
|
525
540
|
try {
|
|
526
541
|
for (const group of groups) {
|
|
527
|
-
for await (const wrappedEvent of wrapPreparedEvents({ privateChannelSigner, receivers: group.receivers, receiverTag, deletionPubkey: normalizedDeletionPubkey, expirationSeconds, context })) {
|
|
542
|
+
for await (const wrappedEvent of wrapPreparedEvents({ privateChannelSigner, receivers: group.receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey: normalizedDeletionPubkey, expirationSeconds, context })) {
|
|
528
543
|
results.push(await _publish(wrappedEvent, withRecoveryRelays(group.relays, recoveryRelays)))
|
|
529
544
|
}
|
|
530
545
|
}
|
|
@@ -534,7 +549,7 @@ export async function publish ({ senderSigner, imkcSigner, privateChannelSigner
|
|
|
534
549
|
return results
|
|
535
550
|
}
|
|
536
551
|
|
|
537
|
-
for await (const wrappedEvent of wrapEvents({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey: normalizedDeletionPubkey, event, expirationSeconds, temporaryStorageArea, _getIykcProofs })) {
|
|
552
|
+
for await (const wrappedEvent of wrapEvents({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey: normalizedDeletionPubkey, event, expirationSeconds, temporaryStorageArea, _getIykcProofs })) {
|
|
538
553
|
results.push(await _publish(wrappedEvent, withRecoveryRelays(relays, recoveryRelays)))
|
|
539
554
|
}
|
|
540
555
|
return results
|
|
@@ -765,6 +780,7 @@ function createProcessor ({
|
|
|
765
780
|
const senderPubkey = readSenderTag(router)
|
|
766
781
|
if (receiverPubkey && readReceiverTag(router) && readReceiverTag(router) !== receiverPubkey && senderPubkey !== receiverPubkey) return
|
|
767
782
|
const { index, total } = readChunkTag(router)
|
|
783
|
+
readFileChunkIndex(router)
|
|
768
784
|
groupKey = receivedChunks.groupKeyFor(channelPubkey, router.pubkey)
|
|
769
785
|
if (ignoredGroups.has(groupKey)) return
|
|
770
786
|
|
|
@@ -773,6 +789,7 @@ function createProcessor ({
|
|
|
773
789
|
const meta = await receivedChunks.put({
|
|
774
790
|
channelPubkey,
|
|
775
791
|
routerPubkey: router.pubkey,
|
|
792
|
+
descriptor: JSON.stringify(router.tags.filter(tag => tag[0] !== 'c')),
|
|
776
793
|
index,
|
|
777
794
|
total,
|
|
778
795
|
contentBytes: base64ToBytes(router.content),
|
|
@@ -976,6 +993,7 @@ export function subscribe ({ receiverSigner, iykcSigner, privateChannelSigner =
|
|
|
976
993
|
const consumePromise = consumeEvents()
|
|
977
994
|
|
|
978
995
|
return {
|
|
996
|
+
ready: events.ready || Promise.resolve(),
|
|
979
997
|
close () {
|
|
980
998
|
controller.abort()
|
|
981
999
|
return consumePromise
|
|
@@ -135,6 +135,7 @@ function normalizeMeta (meta, fallbackTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS) {
|
|
|
135
135
|
return {
|
|
136
136
|
groupKey: String(meta.groupKey),
|
|
137
137
|
channelPubkey: String(meta.channelPubkey || ''),
|
|
138
|
+
descriptor: meta.descriptor,
|
|
138
139
|
routerPubkey: String(meta.routerPubkey || ''),
|
|
139
140
|
total,
|
|
140
141
|
received: normalizeReceived(meta.received),
|
|
@@ -274,7 +275,7 @@ export function createReceivedChunkStore ({
|
|
|
274
275
|
return cleanupStaleRaw(nowMs)
|
|
275
276
|
}
|
|
276
277
|
|
|
277
|
-
async function putOnce ({ channelPubkey, routerPubkey, index, total, contentBytes, ttlMs }) {
|
|
278
|
+
async function putOnce ({ channelPubkey, routerPubkey, index, total, contentBytes, ttlMs, descriptor }) {
|
|
278
279
|
const groupKey = groupKeyFor(channelPubkey, routerPubkey)
|
|
279
280
|
const bytes = normalizeBytes(contentBytes)
|
|
280
281
|
const now = Date.now()
|
|
@@ -287,6 +288,7 @@ export function createReceivedChunkStore ({
|
|
|
287
288
|
await deleteGroupInTransaction(tx, groupKey, usage, meta)
|
|
288
289
|
meta = null
|
|
289
290
|
}
|
|
291
|
+
if (meta && meta.descriptor !== descriptor) throw new ValidationError('INCONSISTENT_ROUTER_DESCRIPTOR')
|
|
290
292
|
if (meta && meta.total !== total) {
|
|
291
293
|
await deleteGroupInTransaction(tx, groupKey, usage, meta)
|
|
292
294
|
meta = null
|
|
@@ -296,6 +298,7 @@ export function createReceivedChunkStore ({
|
|
|
296
298
|
groupKey,
|
|
297
299
|
channelPubkey,
|
|
298
300
|
routerPubkey,
|
|
301
|
+
descriptor,
|
|
299
302
|
total,
|
|
300
303
|
received: {},
|
|
301
304
|
receivedCount: 0,
|
|
@@ -364,7 +367,7 @@ export function createReceivedChunkStore ({
|
|
|
364
367
|
})
|
|
365
368
|
}
|
|
366
369
|
|
|
367
|
-
async function put ({ channelPubkey, routerPubkey, index, total, contentBytes, ttlMs }) {
|
|
370
|
+
async function put ({ channelPubkey, routerPubkey, index, total, contentBytes, ttlMs, descriptor }) {
|
|
368
371
|
if (!channelPubkey || !routerPubkey) throw new ValidationError('RECEIVED_CHUNK_GROUP_REQUIRED')
|
|
369
372
|
if (!Number.isSafeInteger(index) || !Number.isSafeInteger(total) || index < 0 || total < 1 || index >= total) {
|
|
370
373
|
throw new ValidationError('INVALID_RECEIVED_CHUNK_INDEX')
|
|
@@ -373,7 +376,7 @@ export function createReceivedChunkStore ({
|
|
|
373
376
|
await ready()
|
|
374
377
|
while (true) {
|
|
375
378
|
try {
|
|
376
|
-
const result = await putOnce({ channelPubkey, routerPubkey, index, total, contentBytes: bytes, ttlMs })
|
|
379
|
+
const result = await putOnce({ channelPubkey, routerPubkey, index, total, contentBytes: bytes, ttlMs, descriptor })
|
|
377
380
|
if (result.tooLarge) throw new Error('RECEIVED_CHUNK_GROUP_TOO_LARGE')
|
|
378
381
|
return result.meta
|
|
379
382
|
} catch (err) {
|
package/private-message/index.js
CHANGED
|
@@ -517,10 +517,12 @@ async function sendPrivateMessage ({
|
|
|
517
517
|
temporaryStorageArea,
|
|
518
518
|
deletionPubkey,
|
|
519
519
|
_getIykcProofs,
|
|
520
|
+
fileChunkIndex,
|
|
521
|
+
onPreparedSeed,
|
|
520
522
|
_publish = privateChannel.publish
|
|
521
523
|
}) {
|
|
522
524
|
if (!privateChannelSigner?.getPublicKey) throw new ValidationError('PRIVATE_CHANNEL_WRITER_REQUIRED')
|
|
523
|
-
return _publish({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs })
|
|
525
|
+
return _publish({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed, _getIykcProofs })
|
|
524
526
|
}
|
|
525
527
|
|
|
526
528
|
async function sendNymMessage ({
|
|
@@ -560,6 +562,8 @@ export async function ask ({
|
|
|
560
562
|
deletionPubkey,
|
|
561
563
|
deletionSeckey,
|
|
562
564
|
autoDeletionCapability = true,
|
|
565
|
+
fileChunkIndex,
|
|
566
|
+
onPreparedSeed,
|
|
563
567
|
_publish = privateChannel.publish,
|
|
564
568
|
_assertWatching
|
|
565
569
|
}) {
|
|
@@ -583,7 +587,7 @@ export async function ask ({
|
|
|
583
587
|
})
|
|
584
588
|
})
|
|
585
589
|
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
|
|
586
|
-
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers: [receiverPubkey], receiverTag: receiverPubkey, deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
|
|
590
|
+
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers: [receiverPubkey], receiverTag: receiverPubkey, deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed, _getIykcProofs, _publish })
|
|
587
591
|
|
|
588
592
|
return withDelivery({ question }, reports, deletion.deletionSeckey)
|
|
589
593
|
}
|
|
@@ -609,6 +613,8 @@ export async function reply ({
|
|
|
609
613
|
deletionPubkey,
|
|
610
614
|
deletionSeckey,
|
|
611
615
|
autoDeletionCapability = true,
|
|
616
|
+
fileChunkIndex,
|
|
617
|
+
onPreparedSeed,
|
|
612
618
|
_publish = privateChannel.publish
|
|
613
619
|
}) {
|
|
614
620
|
if (!question?.id) throw new ValidationError('QUESTION_REQUIRED')
|
|
@@ -622,7 +628,7 @@ export async function reply ({
|
|
|
622
628
|
})
|
|
623
629
|
})
|
|
624
630
|
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
|
|
625
|
-
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers: [receiverPubkey], receiverTag: receiverPubkey, deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
|
|
631
|
+
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers: [receiverPubkey], receiverTag: receiverPubkey, deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed, _getIykcProofs, _publish })
|
|
626
632
|
return withDelivery({ reply: event }, reports, deletion.deletionSeckey)
|
|
627
633
|
}
|
|
628
634
|
|
|
@@ -646,6 +652,8 @@ export async function tell ({
|
|
|
646
652
|
deletionPubkey,
|
|
647
653
|
deletionSeckey,
|
|
648
654
|
autoDeletionCapability = true,
|
|
655
|
+
fileChunkIndex,
|
|
656
|
+
onPreparedSeed,
|
|
649
657
|
_publish = privateChannel.publish
|
|
650
658
|
}) {
|
|
651
659
|
if (!receiverPubkey) throw new ValidationError('RECEIVER_PUBKEY_REQUIRED')
|
|
@@ -658,7 +666,7 @@ export async function tell ({
|
|
|
658
666
|
})
|
|
659
667
|
})
|
|
660
668
|
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
|
|
661
|
-
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers: [receiverPubkey], receiverTag: receiverPubkey, deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
|
|
669
|
+
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers: [receiverPubkey], receiverTag: receiverPubkey, deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed, _getIykcProofs, _publish })
|
|
662
670
|
return withDelivery({ tell: event }, reports, deletion.deletionSeckey)
|
|
663
671
|
}
|
|
664
672
|
|
|
@@ -682,6 +690,8 @@ export async function yell ({
|
|
|
682
690
|
deletionPubkey,
|
|
683
691
|
deletionSeckey,
|
|
684
692
|
autoDeletionCapability = true,
|
|
693
|
+
fileChunkIndex,
|
|
694
|
+
onPreparedSeed,
|
|
685
695
|
_publish = privateChannel.publish
|
|
686
696
|
}) {
|
|
687
697
|
const receivers = uniq(receiverPubkeys)
|
|
@@ -695,7 +705,7 @@ export async function yell ({
|
|
|
695
705
|
})
|
|
696
706
|
})
|
|
697
707
|
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
|
|
698
|
-
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
|
|
708
|
+
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed, _getIykcProofs, _publish })
|
|
699
709
|
return withDelivery({ yell: event }, reports, deletion.deletionSeckey)
|
|
700
710
|
}
|
|
701
711
|
|
|
@@ -715,13 +725,15 @@ export async function broadcastRumor ({
|
|
|
715
725
|
deletionPubkey,
|
|
716
726
|
deletionSeckey,
|
|
717
727
|
autoDeletionCapability = true,
|
|
728
|
+
fileChunkIndex,
|
|
729
|
+
onPreparedSeed,
|
|
718
730
|
_publish = privateChannel.publish
|
|
719
731
|
}) {
|
|
720
732
|
const receivers = uniq(receiverPubkeys)
|
|
721
733
|
if (!receivers.length) throw new ValidationError('NO_RECEIVERS')
|
|
722
734
|
const { event, wireEvent } = await makeOutgoingRumor({ senderSigner, rumor })
|
|
723
735
|
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
|
|
724
|
-
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
|
|
736
|
+
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed, _getIykcProofs, _publish })
|
|
725
737
|
return withDelivery({ rumor: event }, reports, deletion.deletionSeckey)
|
|
726
738
|
}
|
|
727
739
|
|
|
@@ -741,13 +753,15 @@ export async function broadcastEvent ({
|
|
|
741
753
|
deletionPubkey,
|
|
742
754
|
deletionSeckey,
|
|
743
755
|
autoDeletionCapability = true,
|
|
756
|
+
fileChunkIndex,
|
|
757
|
+
onPreparedSeed,
|
|
744
758
|
_publish = privateChannel.publish
|
|
745
759
|
}) {
|
|
746
760
|
const receivers = uniq(receiverPubkeys)
|
|
747
761
|
if (!receivers.length) throw new ValidationError('NO_RECEIVERS')
|
|
748
762
|
const wireEvent = assertValidSignedEvent({ ...event, tags: cloneTags(event?.tags) })
|
|
749
763
|
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
|
|
750
|
-
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
|
|
764
|
+
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed, _getIykcProofs, _publish })
|
|
751
765
|
return withDelivery({ event: wireEvent }, reports, deletion.deletionSeckey)
|
|
752
766
|
}
|
|
753
767
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Event-store adapters
|
|
2
|
+
|
|
3
|
+
`createPersonalCopyRecoveryStorage({ eventStore, signer })` returns `seeds`,
|
|
4
|
+
`authorizations`, and `close()`. Pass these to `PrivateMessenger` (`seedStorage`)
|
|
5
|
+
and `createPrivateFileTransfer` (`seedStorage`, `authorizationStorage`). The owner
|
|
6
|
+
signer and event store must address the same identity. There is no `window` access
|
|
7
|
+
and no launcher-specific private database API.
|
|
8
|
+
|
|
9
|
+
The signer exposes `getPublicKey`, `obfuscate(value, kind, scope)`, and byte-based
|
|
10
|
+
`nip44v3.decrypt(peer, kind, scope, ciphertext)`. The store exposes the existing
|
|
11
|
+
`query`, `addPersonalCopy`, and `remove` API. Queries require `&tags`, `!ids`,
|
|
12
|
+
`search:sort:desc`, and the personal-copy mirrors used by 44billion. Returned
|
|
13
|
+
wrappers remain encrypted until the adapter decrypts them. The adapter bounds memoized obfuscations (256) and decrypted grants (128); every
|
|
14
|
+
grant use still queries the store, so removal is immediately effective. Permission, lock,
|
|
15
|
+
read-only, quota and operational failures propagate; they are not empty results.
|
|
16
|
+
Expiry cleanup uses the signed wrapper expiration preserved by personal copies,
|
|
17
|
+
without decrypting the archive; the store may also purge expired wrappers itself.
|
|
18
|
+
|
|
19
|
+
## Contracts and ownership
|
|
20
|
+
|
|
21
|
+
- Seeds: `put(record)`, `iterate(query)`, `has(record)`, `removeLocal(query)`,
|
|
22
|
+
`prune({ now })`, and `revoke(records)`.
|
|
23
|
+
- Authorizations: the same operations plus `find(query)`, choosing the newest
|
|
24
|
+
valid share, then the shorter expiry and lexicographically smaller record ID.
|
|
25
|
+
- `iterate` is an async generator. Queries can name `recordId`, `channelPubkey`,
|
|
26
|
+
`receiverPubkey`, `since`/`until`, or the file tuple `controlChannelPubkey`,
|
|
27
|
+
`fileChannelPubkey`, `receiverPubkey`, and `indices`. `signal` cancels reads.
|
|
28
|
+
- The messenger/coordinator never closes caller-owned adapters. Close their
|
|
29
|
+
owner factory after the consumers. No ciphertext is duplicated into a local
|
|
30
|
+
library queue. Limits and storage admission belong to the event store.
|
|
31
|
+
- `createEventStoreChunkStorage({ eventStore })` implements `read`/`save` for
|
|
32
|
+
IRFS chunks. Reconstruction saves an owner template, not a forged announcer
|
|
33
|
+
identity. NostrDB keeps the usual deduplicated payloads and root references.
|
|
34
|
+
- `createEventStoreMessageStorage({ eventStore })` implements
|
|
35
|
+
`save(event, { peerPubkey, hearsay })`, preserving the DM context/provenance.
|
|
36
|
+
|
|
37
|
+
Local defaults remain available when the corresponding adapter is omitted.
|
|
38
|
+
Registration, cache reads and recovery requests never grant file access. A grant
|
|
39
|
+
neither pins chunks nor contains signer material. Synced timestamps never renew
|
|
40
|
+
retention. Missing channel configuration is not a distributed revocation.
|
|
41
|
+
|
|
42
|
+
## Immutable inner events
|
|
43
|
+
|
|
44
|
+
Recovery data uses self-authored kind 30078 templates, `content: ''`, personal
|
|
45
|
+
copy context `''`, and no hearsay. Coordinates are
|
|
46
|
+
`libp2r2p:recovery:seed:<id>` / `libp2r2p:recovery:grant:<id>` (no schema version
|
|
47
|
+
in `d`). Data changes create another snapshot. Imports preserve authored times;
|
|
48
|
+
identical writes query their coordinate and do not re-encrypt/revise it.
|
|
49
|
+
|
|
50
|
+
`id` is lowercase SHA-256 of UTF-8 `JSON.stringify([type, bodyTags])`, where
|
|
51
|
+
`type` is `seed` or `grant` and body tags follow the fixed encoder order below.
|
|
52
|
+
`d`, `t`, `s`, `D`, and CRDT decorations are derived, not hash inputs. The body
|
|
53
|
+
includes expiration. Original strings starting with `%` or `~` escape that first
|
|
54
|
+
character as `%25` or `%7E`; decoding reverses exactly one prefix. This protects
|
|
55
|
+
preserved strings from CRDT metadata interpretation. Numbers are canonical
|
|
56
|
+
nonnegative decimal strings. The decoder rebuilds and verifies all tags and the
|
|
57
|
+
coordinate, tolerating only the store's reserved CRDT decorations.
|
|
58
|
+
|
|
59
|
+
Grant body, in order: `c` (control channel), `channel` (data channel), `peer`,
|
|
60
|
+
`p` (recipient), `root`, optional `size`, `shared`, `expiration`. There is no `r`
|
|
61
|
+
root reference: a grant must not retain file bytes. Creation time is `shared`.
|
|
62
|
+
|
|
63
|
+
Router seed body: `c` (data/ordinary channel), `type=routerEnvelopeRow_v1`, `p`,
|
|
64
|
+
`router` (pubkey, original timestamp), numbered `router-tag` entries (original
|
|
65
|
+
transport `c` removed), `payload`, `key`, optional `iykc` (remaining envelope
|
|
66
|
+
fields), optional `event` (known inner ID), `range` (first/last), optional `file`
|
|
67
|
+
(control channel, peer, root) plus `received`, and `expiration`. Kind 26300 is
|
|
68
|
+
implied by the record type. The original router's `i` supplies the file index.
|
|
69
|
+
The wire reply still reconstructs the existing JSONL rows; no network codec was
|
|
70
|
+
replaced. File seed creation time is `received`; ordinary seed time is range end.
|
|
71
|
+
|
|
72
|
+
Carrier seed body: `c`, `type=nymCarrier_v1`, numbered `carrier` entries containing
|
|
73
|
+
kind/pubkey/timestamp/ID/signature/content, their numbered `carrier-tag` entries
|
|
74
|
+
with `carrierIndex:tagIndex` identities, `range`, `expiration`. Signatures and
|
|
75
|
+
original tag order are verified/preserved. Range/creation time uses the maximum
|
|
76
|
+
carrier timestamp, as the existing recovery codec does.
|
|
77
|
+
|
|
78
|
+
All snapshots have `t=libp2r2p:recovery:<type>` and selector `s`. Selector hashes
|
|
79
|
+
use the same UTF-8 JSON/SHA-256 operation on:
|
|
80
|
+
|
|
81
|
+
- `['grant', control, data, recipient]`
|
|
82
|
+
- `['seed', channel, recipientOrEmpty]`
|
|
83
|
+
- `['file', control, data, recipient, chunkIndex]`
|
|
84
|
+
|
|
85
|
+
Ordinary seeds also carry every UTC Unix day in their interval (`D`, 86400 seconds
|
|
86
|
+
per day). More than 32 days uses the literal `D='*'`. Queries include their days
|
|
87
|
+
and `*`, then verify exact interval overlap. Wide/unbounded queries skip the day
|
|
88
|
+
prefilter. They never filter wrapper `created_at` with the recovery interval.
|
|
89
|
+
Snapshots do not invent an interval between independent observations. The local
|
|
90
|
+
IDB implementation retains its existing logical min/max compaction.
|
|
91
|
+
|
|
92
|
+
## Deletion and synchronization
|
|
93
|
+
|
|
94
|
+
`revoke(records)` writes owner-authored kind-5 personal copies in context `''`,
|
|
95
|
+
with `a` targets for the **inner** 30078 coordinates and advisory `k=30078`.
|
|
96
|
+
Batches contain at most 100 targets. Their timestamp covers stored versions and
|
|
97
|
+
expiration is the latest target expiry. The normal NostrDB private-deletion path
|
|
98
|
+
applies local removal and synced tombstones. There is no custom `state` tag.
|
|
99
|
+
Local eviction/removal and expiry do not publish a deletion. Once a tombstone
|
|
100
|
+
expires, its original targets are expired too. Do not reauthor synced snapshots.
|
|
101
|
+
|
|
102
|
+
Sync transports must keep **their own recovery seeds local**. Persisting seeds
|
|
103
|
+
of the transport that synchronizes seeds into that same event store creates a
|
|
104
|
+
recursive stream of new synchronization data. Consumer chat sessions may use
|
|
105
|
+
these adapters while the vault's synchronization messenger keeps its default.
|
|
106
|
+
|
|
107
|
+
No legacy seed migration or automatic local-to-event-store copying is performed.
|
|
108
|
+
A newly configured adapter records new captures/shares. Paired devices still
|
|
109
|
+
need the same channel configuration, identity keys, mode and locally available
|
|
110
|
+
chunks to serve a synced grant. Watchtowers need only their preserved envelopes.
|