@hashtree/core 0.1.3 → 0.1.4
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/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +2 -1
- package/dist/builder.js.map +1 -1
- package/dist/compare.d.ts +2 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +8 -0
- package/dist/compare.js.map +1 -0
- package/dist/encrypted.d.ts.map +1 -1
- package/dist/encrypted.js +28 -18
- package/dist/encrypted.js.map +1 -1
- package/dist/hashtree.js +1 -1
- package/dist/hashtree.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/store/blossom.d.ts.map +1 -1
- package/dist/store/blossom.js +52 -22
- package/dist/store/blossom.js.map +1 -1
- package/dist/store/fallback.d.ts +3 -1
- package/dist/store/fallback.d.ts.map +1 -1
- package/dist/store/fallback.js +60 -24
- package/dist/store/fallback.js.map +1 -1
- package/dist/tree/create.d.ts.map +1 -1
- package/dist/tree/create.js +2 -1
- package/dist/tree/create.js.map +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -3
- package/dist/resolver/index.d.ts +0 -5
- package/dist/resolver/index.d.ts.map +0 -1
- package/dist/resolver/index.js +0 -5
- package/dist/resolver/index.js.map +0 -1
- package/dist/resolver/nostr.d.ts +0 -82
- package/dist/resolver/nostr.d.ts.map +0 -1
- package/dist/resolver/nostr.js +0 -868
- package/dist/resolver/nostr.js.map +0 -1
- package/dist/store/dexie.d.ts +0 -44
- package/dist/store/dexie.d.ts.map +0 -1
- package/dist/store/dexie.js +0 -196
- package/dist/store/dexie.js.map +0 -1
- package/dist/store/opfs.d.ts +0 -56
- package/dist/store/opfs.d.ts.map +0 -1
- package/dist/store/opfs.js +0 -200
- package/dist/store/opfs.js.map +0 -1
- package/dist/webrtc/index.d.ts +0 -4
- package/dist/webrtc/index.d.ts.map +0 -1
- package/dist/webrtc/index.js +0 -4
- package/dist/webrtc/index.js.map +0 -1
- package/dist/webrtc/lruCache.d.ts +0 -20
- package/dist/webrtc/lruCache.d.ts.map +0 -1
- package/dist/webrtc/lruCache.js +0 -59
- package/dist/webrtc/lruCache.js.map +0 -1
- package/dist/webrtc/peer.d.ts +0 -122
- package/dist/webrtc/peer.d.ts.map +0 -1
- package/dist/webrtc/peer.js +0 -583
- package/dist/webrtc/peer.js.map +0 -1
- package/dist/webrtc/protocol.d.ts +0 -76
- package/dist/webrtc/protocol.d.ts.map +0 -1
- package/dist/webrtc/protocol.js +0 -167
- package/dist/webrtc/protocol.js.map +0 -1
- package/dist/webrtc/store.d.ts +0 -190
- package/dist/webrtc/store.d.ts.map +0 -1
- package/dist/webrtc/store.js +0 -1043
- package/dist/webrtc/store.js.map +0 -1
- package/dist/webrtc/types.d.ts +0 -196
- package/dist/webrtc/types.d.ts.map +0 -1
- package/dist/webrtc/types.js +0 -46
- package/dist/webrtc/types.js.map +0 -1
package/dist/resolver/nostr.js
DELETED
|
@@ -1,868 +0,0 @@
|
|
|
1
|
-
import { fromHex, toHex, cid } from '../types.js';
|
|
2
|
-
import { encryptKeyForLink, computeKeyId, generateLinkKey, } from '../visibility.js';
|
|
3
|
-
function hasLabel(event, label) {
|
|
4
|
-
return event.tags.some(tag => tag[0] === 'l' && tag[1] === label);
|
|
5
|
-
}
|
|
6
|
-
function hasAnyLabel(event) {
|
|
7
|
-
return event.tags.some(tag => tag[0] === 'l');
|
|
8
|
-
}
|
|
9
|
-
function parseLegacyContent(event) {
|
|
10
|
-
const content = event.content?.trim();
|
|
11
|
-
if (!content)
|
|
12
|
-
return null;
|
|
13
|
-
try {
|
|
14
|
-
const parsed = JSON.parse(content);
|
|
15
|
-
if (parsed && typeof parsed === 'object') {
|
|
16
|
-
const payload = parsed;
|
|
17
|
-
return {
|
|
18
|
-
hash: typeof payload.hash === 'string' ? payload.hash : undefined,
|
|
19
|
-
key: typeof payload.key === 'string' ? payload.key : undefined,
|
|
20
|
-
visibility: typeof payload.visibility === 'string' ? payload.visibility : undefined,
|
|
21
|
-
encryptedKey: typeof payload.encryptedKey === 'string' ? payload.encryptedKey : undefined,
|
|
22
|
-
keyId: typeof payload.keyId === 'string' ? payload.keyId : undefined,
|
|
23
|
-
selfEncryptedKey: typeof payload.selfEncryptedKey === 'string' ? payload.selfEncryptedKey : undefined,
|
|
24
|
-
selfEncryptedLinkKey: typeof payload.selfEncryptedLinkKey === 'string' ? payload.selfEncryptedLinkKey : undefined,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// Ignore JSON parse errors.
|
|
30
|
-
}
|
|
31
|
-
if (/^[0-9a-fA-F]{64}$/.test(content)) {
|
|
32
|
-
return { hash: content };
|
|
33
|
-
}
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Parse hash and visibility info from a nostr event
|
|
38
|
-
* Supports all visibility levels: public, link-visible, private
|
|
39
|
-
*/
|
|
40
|
-
function parseHashAndVisibility(event) {
|
|
41
|
-
const hashTag = event.tags.find(t => t[0] === 'hash')?.[1];
|
|
42
|
-
const legacyContent = hashTag ? null : parseLegacyContent(event);
|
|
43
|
-
const hash = hashTag ?? legacyContent?.hash;
|
|
44
|
-
if (!hash)
|
|
45
|
-
return null;
|
|
46
|
-
const keyTag = event.tags.find(t => t[0] === 'key')?.[1];
|
|
47
|
-
const encryptedKeyTag = event.tags.find(t => t[0] === 'encryptedKey')?.[1];
|
|
48
|
-
const keyIdTag = event.tags.find(t => t[0] === 'keyId')?.[1];
|
|
49
|
-
const selfEncryptedKeyTag = event.tags.find(t => t[0] === 'selfEncryptedKey')?.[1];
|
|
50
|
-
const selfEncryptedLinkKeyTag = event.tags.find(t => t[0] === 'selfEncryptedLinkKey')?.[1];
|
|
51
|
-
const key = keyTag ?? legacyContent?.key;
|
|
52
|
-
const encryptedKey = encryptedKeyTag ?? legacyContent?.encryptedKey;
|
|
53
|
-
const keyId = keyIdTag ?? legacyContent?.keyId;
|
|
54
|
-
const selfEncryptedKey = selfEncryptedKeyTag ?? legacyContent?.selfEncryptedKey;
|
|
55
|
-
const selfEncryptedLinkKey = selfEncryptedLinkKeyTag ?? legacyContent?.selfEncryptedLinkKey;
|
|
56
|
-
let visibility;
|
|
57
|
-
if (encryptedKey) {
|
|
58
|
-
// encryptedKey means link-visible (shareable via link)
|
|
59
|
-
// May also have selfEncryptedKey for owner access
|
|
60
|
-
visibility = 'link-visible';
|
|
61
|
-
}
|
|
62
|
-
else if (selfEncryptedKey) {
|
|
63
|
-
// Only selfEncryptedKey (no encryptedKey) means private
|
|
64
|
-
visibility = 'private';
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
visibility = legacyContent?.visibility ?? 'public';
|
|
68
|
-
}
|
|
69
|
-
return { hash, visibility, key, encryptedKey, keyId, selfEncryptedKey, selfEncryptedLinkKey };
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Legacy parse function for backwards compatibility
|
|
73
|
-
* @deprecated Use parseHashAndVisibility instead
|
|
74
|
-
*/
|
|
75
|
-
function parseHashAndKey(event) {
|
|
76
|
-
const result = parseHashAndVisibility(event);
|
|
77
|
-
if (!result)
|
|
78
|
-
return null;
|
|
79
|
-
return { hash: result.hash, key: result.key };
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Create a NostrRefResolver instance
|
|
83
|
-
*/
|
|
84
|
-
export function createNostrRefResolver(config) {
|
|
85
|
-
const { subscribe: nostrSubscribe, publish: nostrPublish, getPubkey, nip19, visibility } = config;
|
|
86
|
-
// Active subscriptions by key
|
|
87
|
-
const subscriptions = new Map();
|
|
88
|
-
const listSubscriptions = new Map();
|
|
89
|
-
// Persistent local cache for list entries (survives subscription lifecycle)
|
|
90
|
-
// Key is npub, value is map of tree name -> entry
|
|
91
|
-
const localListCache = new Map();
|
|
92
|
-
/**
|
|
93
|
-
* Parse a pointer key into pubkey and tree name
|
|
94
|
-
* Key format: "npub1.../treename" or "npub1.../path/to/treename"
|
|
95
|
-
*/
|
|
96
|
-
function parseKey(key) {
|
|
97
|
-
const slashIdx = key.indexOf('/');
|
|
98
|
-
if (slashIdx === -1)
|
|
99
|
-
return null;
|
|
100
|
-
const npubStr = key.slice(0, slashIdx);
|
|
101
|
-
const treeName = key.slice(slashIdx + 1);
|
|
102
|
-
if (!treeName)
|
|
103
|
-
return null;
|
|
104
|
-
try {
|
|
105
|
-
const decoded = nip19.decode(npubStr);
|
|
106
|
-
if (decoded.type !== 'npub')
|
|
107
|
-
return null;
|
|
108
|
-
return { pubkey: decoded.data, treeName };
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
/**
|
|
116
|
-
* Resolve a key to its current CID.
|
|
117
|
-
* Waits indefinitely until found - caller should apply timeout if needed.
|
|
118
|
-
*/
|
|
119
|
-
async resolve(key) {
|
|
120
|
-
const parsed = parseKey(key);
|
|
121
|
-
if (!parsed)
|
|
122
|
-
return null;
|
|
123
|
-
const { pubkey, treeName } = parsed;
|
|
124
|
-
return new Promise((resolve) => {
|
|
125
|
-
let latestData = null;
|
|
126
|
-
let latestCreatedAt = 0;
|
|
127
|
-
let resolveTimeout = null;
|
|
128
|
-
const doResolve = () => {
|
|
129
|
-
unsubscribe();
|
|
130
|
-
if (latestData) {
|
|
131
|
-
resolve(cid(fromHex(latestData.hash), latestData.key ? fromHex(latestData.key) : undefined));
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
resolve(null);
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
const unsubscribe = nostrSubscribe({
|
|
138
|
-
kinds: [30078],
|
|
139
|
-
authors: [pubkey],
|
|
140
|
-
'#d': [treeName],
|
|
141
|
-
}, (event) => {
|
|
142
|
-
const dTag = event.tags.find(t => t[0] === 'd')?.[1];
|
|
143
|
-
if (dTag !== treeName)
|
|
144
|
-
return;
|
|
145
|
-
if (hasAnyLabel(event) && !hasLabel(event, 'hashtree'))
|
|
146
|
-
return;
|
|
147
|
-
const hashAndKey = parseHashAndKey(event);
|
|
148
|
-
if (!hashAndKey)
|
|
149
|
-
return;
|
|
150
|
-
if ((event.created_at || 0) > latestCreatedAt) {
|
|
151
|
-
latestCreatedAt = event.created_at || 0;
|
|
152
|
-
latestData = hashAndKey;
|
|
153
|
-
}
|
|
154
|
-
// Debounce: wait 100ms after last event to allow newer events to arrive
|
|
155
|
-
// This handles the case where relay sends multiple events for replaceable events
|
|
156
|
-
if (resolveTimeout) {
|
|
157
|
-
clearTimeout(resolveTimeout);
|
|
158
|
-
}
|
|
159
|
-
resolveTimeout = setTimeout(doResolve, 100);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
},
|
|
163
|
-
/**
|
|
164
|
-
* Subscribe to CID changes for a key.
|
|
165
|
-
* Callback fires on each update (including initial value).
|
|
166
|
-
* This runs outside React render cycle.
|
|
167
|
-
*/
|
|
168
|
-
subscribe(key, callback) {
|
|
169
|
-
const parsed = parseKey(key);
|
|
170
|
-
if (!parsed) {
|
|
171
|
-
callback(null);
|
|
172
|
-
return () => { };
|
|
173
|
-
}
|
|
174
|
-
const { pubkey, treeName } = parsed;
|
|
175
|
-
// Check if we already have a subscription for this key
|
|
176
|
-
let sub = subscriptions.get(key);
|
|
177
|
-
if (sub) {
|
|
178
|
-
// Add callback to existing subscription
|
|
179
|
-
sub.callbacks.add(callback);
|
|
180
|
-
// Fire immediately with current value
|
|
181
|
-
console.log('[Resolver] Reusing subscription for', key, {
|
|
182
|
-
hasCurrentHash: !!sub.currentHash,
|
|
183
|
-
currentVisibility: sub.currentVisibility ? JSON.stringify(sub.currentVisibility) : 'null'
|
|
184
|
-
});
|
|
185
|
-
if (sub.currentHash) {
|
|
186
|
-
const keyBytes = sub.currentKey ? fromHex(sub.currentKey) : undefined;
|
|
187
|
-
callback(cid(fromHex(sub.currentHash), keyBytes), sub.currentVisibility ?? undefined);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
// Helper to notify all callbacks
|
|
192
|
-
const notifyCallbacks = (subEntry) => {
|
|
193
|
-
if (!subEntry.currentHash)
|
|
194
|
-
return;
|
|
195
|
-
const keyBytes = subEntry.currentKey ? fromHex(subEntry.currentKey) : undefined;
|
|
196
|
-
const visibilityInfo = subEntry.currentVisibility ?? undefined;
|
|
197
|
-
for (const cb of subEntry.callbacks) {
|
|
198
|
-
try {
|
|
199
|
-
cb(cid(fromHex(subEntry.currentHash), keyBytes), visibilityInfo);
|
|
200
|
-
}
|
|
201
|
-
catch (e) {
|
|
202
|
-
console.error('Resolver callback error:', e);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
// Create new subscription for live updates
|
|
207
|
-
const unsubscribe = nostrSubscribe({
|
|
208
|
-
kinds: [30078],
|
|
209
|
-
authors: [pubkey],
|
|
210
|
-
'#d': [treeName],
|
|
211
|
-
}, (event) => {
|
|
212
|
-
const dTag = event.tags.find(t => t[0] === 'd')?.[1];
|
|
213
|
-
if (dTag !== treeName)
|
|
214
|
-
return;
|
|
215
|
-
if (hasAnyLabel(event) && !hasLabel(event, 'hashtree'))
|
|
216
|
-
return;
|
|
217
|
-
const subEntry = subscriptions.get(key);
|
|
218
|
-
if (!subEntry)
|
|
219
|
-
return;
|
|
220
|
-
const visibilityData = parseHashAndVisibility(event);
|
|
221
|
-
console.log('[Resolver] Event received for', key, {
|
|
222
|
-
visibilityData: visibilityData ? {
|
|
223
|
-
visibility: visibilityData.visibility,
|
|
224
|
-
hasSelfEncryptedLinkKey: !!visibilityData.selfEncryptedLinkKey,
|
|
225
|
-
hasEncryptedKey: !!visibilityData.encryptedKey
|
|
226
|
-
} : 'null'
|
|
227
|
-
});
|
|
228
|
-
if (!visibilityData)
|
|
229
|
-
return;
|
|
230
|
-
const eventCreatedAt = event.created_at || 0;
|
|
231
|
-
const newHash = visibilityData.hash;
|
|
232
|
-
const newKey = visibilityData.key;
|
|
233
|
-
// Only update if this event is newer (or same timestamp with different hash)
|
|
234
|
-
if (eventCreatedAt >= subEntry.latestCreatedAt && newHash && newHash !== subEntry.currentHash) {
|
|
235
|
-
subEntry.currentHash = newHash;
|
|
236
|
-
subEntry.currentKey = newKey || null;
|
|
237
|
-
subEntry.latestCreatedAt = eventCreatedAt;
|
|
238
|
-
// Build visibility info for callback
|
|
239
|
-
const visibilityInfo = {
|
|
240
|
-
visibility: visibilityData.visibility,
|
|
241
|
-
encryptedKey: visibilityData.encryptedKey,
|
|
242
|
-
keyId: visibilityData.keyId,
|
|
243
|
-
selfEncryptedKey: visibilityData.selfEncryptedKey,
|
|
244
|
-
selfEncryptedLinkKey: visibilityData.selfEncryptedLinkKey,
|
|
245
|
-
};
|
|
246
|
-
subEntry.currentVisibility = visibilityInfo;
|
|
247
|
-
// Update localListCache so other subscriptions can find this data
|
|
248
|
-
const npubStr = key.split('/')[0];
|
|
249
|
-
let npubCache = localListCache.get(npubStr);
|
|
250
|
-
if (!npubCache) {
|
|
251
|
-
npubCache = new Map();
|
|
252
|
-
localListCache.set(npubStr, npubCache);
|
|
253
|
-
}
|
|
254
|
-
npubCache.set(treeName, {
|
|
255
|
-
hash: newHash,
|
|
256
|
-
visibility: visibilityData.visibility,
|
|
257
|
-
key: newKey,
|
|
258
|
-
encryptedKey: visibilityData.encryptedKey,
|
|
259
|
-
keyId: visibilityData.keyId,
|
|
260
|
-
selfEncryptedKey: visibilityData.selfEncryptedKey,
|
|
261
|
-
selfEncryptedLinkKey: visibilityData.selfEncryptedLinkKey,
|
|
262
|
-
created_at: eventCreatedAt,
|
|
263
|
-
});
|
|
264
|
-
notifyCallbacks(subEntry);
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
// Check localListCache for initial values (from recent publish calls)
|
|
268
|
-
const npubStr = key.split('/')[0];
|
|
269
|
-
const cachedEntry = localListCache.get(npubStr)?.get(treeName);
|
|
270
|
-
sub = {
|
|
271
|
-
unsubscribe,
|
|
272
|
-
callbacks: new Set([callback]),
|
|
273
|
-
currentHash: cachedEntry?.hash ?? null,
|
|
274
|
-
currentKey: cachedEntry?.key ?? null,
|
|
275
|
-
currentVisibility: cachedEntry ? {
|
|
276
|
-
visibility: cachedEntry.visibility,
|
|
277
|
-
encryptedKey: cachedEntry.encryptedKey,
|
|
278
|
-
keyId: cachedEntry.keyId,
|
|
279
|
-
selfEncryptedKey: cachedEntry.selfEncryptedKey,
|
|
280
|
-
selfEncryptedLinkKey: cachedEntry.selfEncryptedLinkKey,
|
|
281
|
-
} : null,
|
|
282
|
-
latestCreatedAt: cachedEntry?.created_at ?? 0,
|
|
283
|
-
};
|
|
284
|
-
subscriptions.set(key, sub);
|
|
285
|
-
// Fire callback immediately with cached value if available
|
|
286
|
-
if (cachedEntry?.hash) {
|
|
287
|
-
const keyBytes = cachedEntry.key ? fromHex(cachedEntry.key) : undefined;
|
|
288
|
-
callback(cid(fromHex(cachedEntry.hash), keyBytes), sub.currentVisibility ?? undefined);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
// Return unsubscribe function
|
|
292
|
-
return () => {
|
|
293
|
-
const subEntry = subscriptions.get(key);
|
|
294
|
-
if (!subEntry)
|
|
295
|
-
return;
|
|
296
|
-
subEntry.callbacks.delete(callback);
|
|
297
|
-
// If no more callbacks, close the subscription
|
|
298
|
-
if (subEntry.callbacks.size === 0) {
|
|
299
|
-
subEntry.unsubscribe();
|
|
300
|
-
subscriptions.delete(key);
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
},
|
|
304
|
-
/**
|
|
305
|
-
* Publish/update a pointer
|
|
306
|
-
* Handles all visibility types: public, link-visible, private
|
|
307
|
-
* Updates local cache immediately (optimistic), then publishes to network.
|
|
308
|
-
* @param key - The key to publish to
|
|
309
|
-
* @param rootCid - The CID to publish (with encryption key if encrypted)
|
|
310
|
-
* @param options - Publish options (visibility, linkKey, labels)
|
|
311
|
-
* @returns Result with success status and linkKey (for link-visible)
|
|
312
|
-
*/
|
|
313
|
-
async publish(key, rootCid, options) {
|
|
314
|
-
const parsed = parseKey(key);
|
|
315
|
-
if (!parsed)
|
|
316
|
-
return { success: false };
|
|
317
|
-
const { treeName } = parsed;
|
|
318
|
-
const pubkey = getPubkey();
|
|
319
|
-
if (!pubkey)
|
|
320
|
-
return { success: false };
|
|
321
|
-
const visibilityType = options?.visibility ?? 'public';
|
|
322
|
-
const hashHex = toHex(rootCid.hash);
|
|
323
|
-
const chkKey = rootCid.key; // Raw encryption key (Uint8Array)
|
|
324
|
-
const chkKeyHex = chkKey ? toHex(chkKey) : undefined;
|
|
325
|
-
const now = Math.floor(Date.now() / 1000);
|
|
326
|
-
const npubStr = key.split('/')[0];
|
|
327
|
-
// Build visibility info for caches and tags
|
|
328
|
-
const visibilityInfo = { visibility: visibilityType };
|
|
329
|
-
let resultLinkKey;
|
|
330
|
-
// Build nostr event tags
|
|
331
|
-
const tags = [
|
|
332
|
-
['d', treeName],
|
|
333
|
-
['l', 'hashtree'],
|
|
334
|
-
['hash', hashHex],
|
|
335
|
-
];
|
|
336
|
-
// Add directory prefix labels for discoverability
|
|
337
|
-
const parts = treeName.split('/');
|
|
338
|
-
for (let i = 1; i < parts.length; i++) {
|
|
339
|
-
const prefix = parts.slice(0, i).join('/');
|
|
340
|
-
tags.push(['l', prefix]);
|
|
341
|
-
}
|
|
342
|
-
// Add extra labels if provided
|
|
343
|
-
if (options?.labels) {
|
|
344
|
-
for (const label of options.labels) {
|
|
345
|
-
tags.push(['l', label]);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
// Handle visibility-specific encryption and tags
|
|
349
|
-
if (chkKey) {
|
|
350
|
-
switch (visibilityType) {
|
|
351
|
-
case 'public':
|
|
352
|
-
// Plaintext key - anyone can access
|
|
353
|
-
tags.push(['key', chkKeyHex]);
|
|
354
|
-
visibilityInfo.encryptedKey = undefined;
|
|
355
|
-
visibilityInfo.keyId = undefined;
|
|
356
|
-
visibilityInfo.selfEncryptedKey = undefined;
|
|
357
|
-
break;
|
|
358
|
-
case 'link-visible': {
|
|
359
|
-
// Encrypt key with link key (XOR one-time pad)
|
|
360
|
-
const linkKey = options?.linkKey ?? generateLinkKey();
|
|
361
|
-
resultLinkKey = linkKey;
|
|
362
|
-
const encryptedKey = encryptKeyForLink(chkKey, linkKey);
|
|
363
|
-
const keyId = await computeKeyId(linkKey);
|
|
364
|
-
tags.push(['encryptedKey', toHex(encryptedKey)]);
|
|
365
|
-
tags.push(['keyId', toHex(keyId)]);
|
|
366
|
-
visibilityInfo.encryptedKey = toHex(encryptedKey);
|
|
367
|
-
visibilityInfo.keyId = toHex(keyId);
|
|
368
|
-
if (visibility?.encrypt) {
|
|
369
|
-
// Encrypt contentKey to self (so owner can always access without linkKey)
|
|
370
|
-
try {
|
|
371
|
-
const selfEncrypted = await visibility.encrypt(chkKey, pubkey);
|
|
372
|
-
tags.push(['selfEncryptedKey', selfEncrypted]);
|
|
373
|
-
visibilityInfo.selfEncryptedKey = selfEncrypted;
|
|
374
|
-
}
|
|
375
|
-
catch (e) {
|
|
376
|
-
console.error('Failed to self-encrypt content key:', e);
|
|
377
|
-
}
|
|
378
|
-
// Encrypt linkKey to self for URL recovery (so owner can share URLs easily)
|
|
379
|
-
// Owner can also derive linkKey from XOR(encryptedKey, contentKey)
|
|
380
|
-
try {
|
|
381
|
-
const selfEncryptedLinkKey = await visibility.encrypt(linkKey, pubkey);
|
|
382
|
-
tags.push(['selfEncryptedLinkKey', selfEncryptedLinkKey]);
|
|
383
|
-
visibilityInfo.selfEncryptedLinkKey = selfEncryptedLinkKey;
|
|
384
|
-
}
|
|
385
|
-
catch (e) {
|
|
386
|
-
console.error('Failed to self-encrypt link key:', e);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
case 'private': {
|
|
392
|
-
// Encrypt key to self using NIP-44
|
|
393
|
-
if (!visibility?.encrypt) {
|
|
394
|
-
console.error('Cannot publish private tree: no encrypt callback provided');
|
|
395
|
-
return { success: false };
|
|
396
|
-
}
|
|
397
|
-
try {
|
|
398
|
-
const selfEncrypted = await visibility.encrypt(chkKey, pubkey);
|
|
399
|
-
tags.push(['selfEncryptedKey', selfEncrypted]);
|
|
400
|
-
visibilityInfo.selfEncryptedKey = selfEncrypted;
|
|
401
|
-
}
|
|
402
|
-
catch (e) {
|
|
403
|
-
console.error('Failed to encrypt key:', e);
|
|
404
|
-
return { success: false };
|
|
405
|
-
}
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
// 1. Update local caches FIRST (optimistic update for instant UI)
|
|
411
|
-
// Update local list cache (persists even without active subscription)
|
|
412
|
-
let npubCache = localListCache.get(npubStr);
|
|
413
|
-
if (!npubCache) {
|
|
414
|
-
npubCache = new Map();
|
|
415
|
-
localListCache.set(npubStr, npubCache);
|
|
416
|
-
}
|
|
417
|
-
npubCache.set(treeName, {
|
|
418
|
-
hash: hashHex,
|
|
419
|
-
visibility: visibilityType,
|
|
420
|
-
key: visibilityType === 'public' ? chkKeyHex : undefined,
|
|
421
|
-
encryptedKey: visibilityInfo.encryptedKey,
|
|
422
|
-
keyId: visibilityInfo.keyId,
|
|
423
|
-
selfEncryptedKey: visibilityInfo.selfEncryptedKey,
|
|
424
|
-
selfEncryptedLinkKey: visibilityInfo.selfEncryptedLinkKey,
|
|
425
|
-
created_at: now,
|
|
426
|
-
});
|
|
427
|
-
// Update active subscription state
|
|
428
|
-
const sub = subscriptions.get(key);
|
|
429
|
-
if (sub) {
|
|
430
|
-
sub.currentHash = hashHex;
|
|
431
|
-
sub.currentKey = visibilityType === 'public' ? chkKeyHex || null : null;
|
|
432
|
-
sub.latestCreatedAt = now;
|
|
433
|
-
sub.currentVisibility = visibilityInfo;
|
|
434
|
-
// Notify callbacks with CID
|
|
435
|
-
for (const cb of sub.callbacks) {
|
|
436
|
-
try {
|
|
437
|
-
cb(rootCid, visibilityInfo);
|
|
438
|
-
}
|
|
439
|
-
catch (e) {
|
|
440
|
-
console.error('Resolver callback error:', e);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
// Update active list subscriptions
|
|
445
|
-
const listSub = listSubscriptions.get(npubStr);
|
|
446
|
-
if (listSub) {
|
|
447
|
-
const existingEntry = listSub.entriesByDTag.get(treeName);
|
|
448
|
-
const existingWasDeleted = existingEntry && !existingEntry.hash;
|
|
449
|
-
const timeDiff = existingEntry ? now - existingEntry.created_at : 0;
|
|
450
|
-
// Skip if this would undelete a recently-deleted entry (within 30 seconds)
|
|
451
|
-
if (existingWasDeleted && hashHex && timeDiff < 30) {
|
|
452
|
-
// Blocked stale undelete
|
|
453
|
-
}
|
|
454
|
-
else {
|
|
455
|
-
listSub.entriesByDTag.set(treeName, {
|
|
456
|
-
hash: hashHex,
|
|
457
|
-
visibility: visibilityType,
|
|
458
|
-
key: visibilityType === 'public' ? chkKeyHex : undefined,
|
|
459
|
-
encryptedKey: visibilityInfo.encryptedKey,
|
|
460
|
-
keyId: visibilityInfo.keyId,
|
|
461
|
-
selfEncryptedKey: visibilityInfo.selfEncryptedKey,
|
|
462
|
-
selfEncryptedLinkKey: visibilityInfo.selfEncryptedLinkKey,
|
|
463
|
-
created_at: now,
|
|
464
|
-
});
|
|
465
|
-
// Emit updated state immediately to ALL callbacks
|
|
466
|
-
const result = [];
|
|
467
|
-
for (const [dTag, entry] of listSub.entriesByDTag) {
|
|
468
|
-
if (!entry.hash)
|
|
469
|
-
continue; // Skip deleted trees
|
|
470
|
-
result.push({
|
|
471
|
-
key: `${npubStr}/${dTag}`,
|
|
472
|
-
cid: cid(fromHex(entry.hash), entry.key ? fromHex(entry.key) : undefined),
|
|
473
|
-
visibility: entry.visibility,
|
|
474
|
-
encryptedKey: entry.encryptedKey,
|
|
475
|
-
keyId: entry.keyId,
|
|
476
|
-
selfEncryptedKey: entry.selfEncryptedKey,
|
|
477
|
-
selfEncryptedLinkKey: entry.selfEncryptedLinkKey,
|
|
478
|
-
createdAt: entry.created_at,
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
for (const cb of listSub.callbacks) {
|
|
482
|
-
try {
|
|
483
|
-
cb(result);
|
|
484
|
-
}
|
|
485
|
-
catch (e) {
|
|
486
|
-
console.error('List callback error:', e);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
// 2. Publish to network
|
|
492
|
-
try {
|
|
493
|
-
const success = await nostrPublish({
|
|
494
|
-
kind: 30078,
|
|
495
|
-
content: '',
|
|
496
|
-
tags,
|
|
497
|
-
});
|
|
498
|
-
if (!success) {
|
|
499
|
-
return { success: false, linkKey: resultLinkKey };
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
catch (e) {
|
|
503
|
-
console.error('Failed to publish to nostr:', e);
|
|
504
|
-
return { success: false, linkKey: resultLinkKey };
|
|
505
|
-
}
|
|
506
|
-
return { success: true, linkKey: resultLinkKey };
|
|
507
|
-
},
|
|
508
|
-
/**
|
|
509
|
-
* List all trees for a user.
|
|
510
|
-
* Streams results as they arrive - returns unsubscribe function.
|
|
511
|
-
* Caller decides when to stop listening.
|
|
512
|
-
* Supports multiple subscribers per npub - all callbacks receive updates.
|
|
513
|
-
*/
|
|
514
|
-
list(prefix, callback) {
|
|
515
|
-
const parts = prefix.split('/');
|
|
516
|
-
if (parts.length === 0) {
|
|
517
|
-
callback([]);
|
|
518
|
-
return () => { };
|
|
519
|
-
}
|
|
520
|
-
const npubStr = parts[0];
|
|
521
|
-
let pubkey;
|
|
522
|
-
try {
|
|
523
|
-
const decoded = nip19.decode(npubStr);
|
|
524
|
-
if (decoded.type !== 'npub') {
|
|
525
|
-
callback([]);
|
|
526
|
-
return () => { };
|
|
527
|
-
}
|
|
528
|
-
pubkey = decoded.data;
|
|
529
|
-
}
|
|
530
|
-
catch {
|
|
531
|
-
callback([]);
|
|
532
|
-
return () => { };
|
|
533
|
-
}
|
|
534
|
-
// Check if we already have a subscription for this npub
|
|
535
|
-
const existingSub = listSubscriptions.get(npubStr);
|
|
536
|
-
if (existingSub) {
|
|
537
|
-
// Add callback to existing subscription
|
|
538
|
-
existingSub.callbacks.add(callback);
|
|
539
|
-
// Fire immediately with current state
|
|
540
|
-
const result = [];
|
|
541
|
-
for (const [dTag, entry] of existingSub.entriesByDTag) {
|
|
542
|
-
if (!entry.hash) {
|
|
543
|
-
continue;
|
|
544
|
-
}
|
|
545
|
-
result.push({
|
|
546
|
-
key: `${npubStr}/${dTag}`,
|
|
547
|
-
cid: cid(fromHex(entry.hash), entry.key ? fromHex(entry.key) : undefined),
|
|
548
|
-
visibility: entry.visibility,
|
|
549
|
-
encryptedKey: entry.encryptedKey,
|
|
550
|
-
keyId: entry.keyId,
|
|
551
|
-
selfEncryptedKey: entry.selfEncryptedKey,
|
|
552
|
-
selfEncryptedLinkKey: entry.selfEncryptedLinkKey,
|
|
553
|
-
createdAt: entry.created_at,
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
callback(result);
|
|
557
|
-
// Return unsubscribe that removes this callback
|
|
558
|
-
return () => {
|
|
559
|
-
existingSub.callbacks.delete(callback);
|
|
560
|
-
// If no more callbacks, clean up the subscription
|
|
561
|
-
if (existingSub.callbacks.size === 0) {
|
|
562
|
-
existingSub.unsubscribe();
|
|
563
|
-
listSubscriptions.delete(npubStr);
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
// Create new subscription
|
|
568
|
-
const entriesByDTag = new Map();
|
|
569
|
-
const callbacks = new Set([callback]);
|
|
570
|
-
// Pre-populate from local cache (for instant display of locally-created trees)
|
|
571
|
-
const cachedEntries = localListCache.get(npubStr);
|
|
572
|
-
if (cachedEntries) {
|
|
573
|
-
for (const [treeName, entry] of cachedEntries) {
|
|
574
|
-
entriesByDTag.set(treeName, entry);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
const emitCurrentState = () => {
|
|
578
|
-
const result = [];
|
|
579
|
-
for (const [dTag, entry] of entriesByDTag) {
|
|
580
|
-
// Skip entries with empty/null hash (deleted trees)
|
|
581
|
-
if (!entry.hash)
|
|
582
|
-
continue;
|
|
583
|
-
result.push({
|
|
584
|
-
key: `${npubStr}/${dTag}`,
|
|
585
|
-
cid: cid(fromHex(entry.hash), entry.key ? fromHex(entry.key) : undefined),
|
|
586
|
-
visibility: entry.visibility,
|
|
587
|
-
encryptedKey: entry.encryptedKey,
|
|
588
|
-
keyId: entry.keyId,
|
|
589
|
-
selfEncryptedKey: entry.selfEncryptedKey,
|
|
590
|
-
selfEncryptedLinkKey: entry.selfEncryptedLinkKey,
|
|
591
|
-
createdAt: entry.created_at,
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
// Notify ALL callbacks
|
|
595
|
-
for (const cb of callbacks) {
|
|
596
|
-
try {
|
|
597
|
-
cb(result);
|
|
598
|
-
}
|
|
599
|
-
catch (e) {
|
|
600
|
-
console.error('List callback error:', e);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
};
|
|
604
|
-
// Emit cached entries immediately if any
|
|
605
|
-
if (entriesByDTag.size > 0) {
|
|
606
|
-
emitCurrentState();
|
|
607
|
-
}
|
|
608
|
-
const unsubscribe = nostrSubscribe({
|
|
609
|
-
kinds: [30078],
|
|
610
|
-
authors: [pubkey],
|
|
611
|
-
'#l': ['hashtree'],
|
|
612
|
-
}, (event) => {
|
|
613
|
-
const dTag = event.tags.find(t => t[0] === 'd')?.[1];
|
|
614
|
-
if (!dTag)
|
|
615
|
-
return;
|
|
616
|
-
const parsed = parseHashAndVisibility(event);
|
|
617
|
-
const hasHash = !!parsed?.hash;
|
|
618
|
-
const existing = entriesByDTag.get(dTag);
|
|
619
|
-
const eventTime = event.created_at || 0;
|
|
620
|
-
// Timestamp-based update logic:
|
|
621
|
-
// 1. Accept if no existing entry
|
|
622
|
-
// 2. Accept if event is strictly newer
|
|
623
|
-
// 3. Accept if same timestamp and this is a delete (no hash) overwriting a create (has hash)
|
|
624
|
-
// 4. Reject if same timestamp and this would "undelete" (create overwriting delete)
|
|
625
|
-
// 5. Reject if existing is deleted and new event is within 30s (prevent stale event undelete)
|
|
626
|
-
if (existing) {
|
|
627
|
-
const existingIsDeleted = !existing.hash;
|
|
628
|
-
const timeDiff = eventTime - existing.created_at;
|
|
629
|
-
// Block stale events from "undeleting" within 30 seconds
|
|
630
|
-
if (existingIsDeleted && hasHash && timeDiff < 30) {
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
// Block same-timestamp undelete (create can't overwrite delete at same time)
|
|
634
|
-
if (existingIsDeleted && hasHash && timeDiff === 0) {
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
// Only update if newer, or if same time and delete wins over create
|
|
638
|
-
const shouldUpdate = eventTime > existing.created_at ||
|
|
639
|
-
(eventTime === existing.created_at && !hasHash && existing.hash);
|
|
640
|
-
if (!shouldUpdate)
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
// Update the entry
|
|
644
|
-
const entryData = {
|
|
645
|
-
hash: parsed?.hash ?? '',
|
|
646
|
-
visibility: parsed?.visibility ?? 'public',
|
|
647
|
-
key: parsed?.key,
|
|
648
|
-
encryptedKey: parsed?.encryptedKey,
|
|
649
|
-
keyId: parsed?.keyId,
|
|
650
|
-
selfEncryptedKey: parsed?.selfEncryptedKey,
|
|
651
|
-
selfEncryptedLinkKey: parsed?.selfEncryptedLinkKey,
|
|
652
|
-
created_at: eventTime,
|
|
653
|
-
};
|
|
654
|
-
entriesByDTag.set(dTag, entryData);
|
|
655
|
-
// Also update localListCache so subscribe() can find this data
|
|
656
|
-
// This enables cross-function caching: list() receives data, subscribe() can use it
|
|
657
|
-
let npubCache = localListCache.get(npubStr);
|
|
658
|
-
if (!npubCache) {
|
|
659
|
-
npubCache = new Map();
|
|
660
|
-
localListCache.set(npubStr, npubCache);
|
|
661
|
-
}
|
|
662
|
-
npubCache.set(dTag, entryData);
|
|
663
|
-
emitCurrentState();
|
|
664
|
-
});
|
|
665
|
-
// Register this list subscription
|
|
666
|
-
listSubscriptions.set(npubStr, {
|
|
667
|
-
npub: npubStr,
|
|
668
|
-
entriesByDTag,
|
|
669
|
-
callbacks,
|
|
670
|
-
unsubscribe,
|
|
671
|
-
});
|
|
672
|
-
return () => {
|
|
673
|
-
callbacks.delete(callback);
|
|
674
|
-
if (callbacks.size === 0) {
|
|
675
|
-
unsubscribe();
|
|
676
|
-
listSubscriptions.delete(npubStr);
|
|
677
|
-
}
|
|
678
|
-
};
|
|
679
|
-
},
|
|
680
|
-
/**
|
|
681
|
-
* Stop all subscriptions
|
|
682
|
-
*/
|
|
683
|
-
stop() {
|
|
684
|
-
for (const [, sub] of subscriptions) {
|
|
685
|
-
sub.unsubscribe();
|
|
686
|
-
}
|
|
687
|
-
subscriptions.clear();
|
|
688
|
-
listSubscriptions.clear();
|
|
689
|
-
},
|
|
690
|
-
/**
|
|
691
|
-
* Delete a tree by publishing event without hash tag
|
|
692
|
-
* This nullifies the tree - it will be filtered from list results
|
|
693
|
-
*/
|
|
694
|
-
async delete(key) {
|
|
695
|
-
const parsed = parseKey(key);
|
|
696
|
-
if (!parsed)
|
|
697
|
-
return false;
|
|
698
|
-
const { treeName } = parsed;
|
|
699
|
-
const pubkey = getPubkey();
|
|
700
|
-
if (!pubkey)
|
|
701
|
-
return false;
|
|
702
|
-
const now = Math.floor(Date.now() / 1000);
|
|
703
|
-
const npubStr = key.split('/')[0];
|
|
704
|
-
// Update local list cache with empty hash (marks as deleted)
|
|
705
|
-
let npubCache = localListCache.get(npubStr);
|
|
706
|
-
if (!npubCache) {
|
|
707
|
-
npubCache = new Map();
|
|
708
|
-
localListCache.set(npubStr, npubCache);
|
|
709
|
-
}
|
|
710
|
-
npubCache.set(treeName, {
|
|
711
|
-
hash: '', // Empty hash marks as deleted
|
|
712
|
-
visibility: 'public',
|
|
713
|
-
key: undefined,
|
|
714
|
-
created_at: now,
|
|
715
|
-
});
|
|
716
|
-
// Update active subscription state
|
|
717
|
-
const sub = subscriptions.get(key);
|
|
718
|
-
if (sub) {
|
|
719
|
-
sub.currentHash = null;
|
|
720
|
-
sub.currentKey = null;
|
|
721
|
-
sub.latestCreatedAt = now;
|
|
722
|
-
sub.currentVisibility = null;
|
|
723
|
-
// Notify callbacks with null CID
|
|
724
|
-
for (const cb of sub.callbacks) {
|
|
725
|
-
try {
|
|
726
|
-
cb(null);
|
|
727
|
-
}
|
|
728
|
-
catch (e) {
|
|
729
|
-
console.error('Resolver callback error:', e);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
// Update active list subscriptions - set hash to empty and emit
|
|
734
|
-
const listSub = listSubscriptions.get(npubStr);
|
|
735
|
-
if (listSub) {
|
|
736
|
-
listSub.entriesByDTag.set(treeName, {
|
|
737
|
-
hash: '', // Empty hash marks as deleted
|
|
738
|
-
visibility: 'public',
|
|
739
|
-
key: undefined,
|
|
740
|
-
created_at: now,
|
|
741
|
-
});
|
|
742
|
-
// Emit - filter out empty hashes (deleted trees)
|
|
743
|
-
const result = [];
|
|
744
|
-
for (const [dTag, entry] of listSub.entriesByDTag) {
|
|
745
|
-
if (!entry.hash)
|
|
746
|
-
continue;
|
|
747
|
-
result.push({
|
|
748
|
-
key: `${npubStr}/${dTag}`,
|
|
749
|
-
cid: cid(fromHex(entry.hash), entry.key ? fromHex(entry.key) : undefined),
|
|
750
|
-
visibility: entry.visibility,
|
|
751
|
-
encryptedKey: entry.encryptedKey,
|
|
752
|
-
keyId: entry.keyId,
|
|
753
|
-
selfEncryptedKey: entry.selfEncryptedKey,
|
|
754
|
-
selfEncryptedLinkKey: entry.selfEncryptedLinkKey,
|
|
755
|
-
createdAt: entry.created_at,
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
for (const cb of listSub.callbacks) {
|
|
759
|
-
try {
|
|
760
|
-
cb(result);
|
|
761
|
-
}
|
|
762
|
-
catch (e) {
|
|
763
|
-
console.error('List callback error:', e);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
// 2. Publish to Nostr - event without hash tag
|
|
768
|
-
// Use now + 1 to ensure delete timestamp is strictly higher than any create event
|
|
769
|
-
// This is critical for NIP-33: when timestamps are equal, event ID breaks the tie (random)
|
|
770
|
-
nostrPublish({
|
|
771
|
-
kind: 30078,
|
|
772
|
-
content: '',
|
|
773
|
-
tags: [
|
|
774
|
-
['d', treeName],
|
|
775
|
-
['l', 'hashtree'],
|
|
776
|
-
// No hash tag = deleted
|
|
777
|
-
],
|
|
778
|
-
created_at: now + 1,
|
|
779
|
-
}).catch(e => console.error('Failed to publish delete to nostr:', e));
|
|
780
|
-
return true;
|
|
781
|
-
},
|
|
782
|
-
/**
|
|
783
|
-
* Inject a local list entry (for instant UI updates)
|
|
784
|
-
* This updates the local cache to make trees appear immediately.
|
|
785
|
-
*/
|
|
786
|
-
injectListEntry(entry) {
|
|
787
|
-
const parts = entry.key.split('/');
|
|
788
|
-
if (parts.length !== 2)
|
|
789
|
-
return;
|
|
790
|
-
const [npubStr, treeName] = parts;
|
|
791
|
-
const now = Math.floor(Date.now() / 1000);
|
|
792
|
-
const hasHash = !!toHex(entry.cid.hash);
|
|
793
|
-
// Update the local list cache
|
|
794
|
-
let npubCache = localListCache.get(npubStr);
|
|
795
|
-
if (!npubCache) {
|
|
796
|
-
npubCache = new Map();
|
|
797
|
-
localListCache.set(npubStr, npubCache);
|
|
798
|
-
}
|
|
799
|
-
const existing = npubCache.get(treeName);
|
|
800
|
-
const existingWasDeletedCache = existing && !existing.hash;
|
|
801
|
-
const timeDiffCache = existing ? now - existing.created_at : 0;
|
|
802
|
-
// Skip if this would undelete a recently-deleted entry (within 30 seconds)
|
|
803
|
-
if (existingWasDeletedCache && hasHash && timeDiffCache < 30) {
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
if (!existing || now >= existing.created_at) {
|
|
807
|
-
npubCache.set(treeName, {
|
|
808
|
-
hash: toHex(entry.cid.hash),
|
|
809
|
-
visibility: entry.visibility ?? 'public',
|
|
810
|
-
key: entry.cid.key ? toHex(entry.cid.key) : undefined,
|
|
811
|
-
encryptedKey: entry.encryptedKey,
|
|
812
|
-
keyId: entry.keyId,
|
|
813
|
-
selfEncryptedKey: entry.selfEncryptedKey,
|
|
814
|
-
selfEncryptedLinkKey: entry.selfEncryptedLinkKey,
|
|
815
|
-
created_at: now,
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
// If there's an active list subscription for this npub, update it too
|
|
819
|
-
const listSub = listSubscriptions.get(npubStr);
|
|
820
|
-
if (listSub) {
|
|
821
|
-
const existingSub = listSub.entriesByDTag.get(treeName);
|
|
822
|
-
const existingWasDeleted = existingSub && !existingSub.hash;
|
|
823
|
-
const timeDiff = existingSub ? now - existingSub.created_at : 0;
|
|
824
|
-
// Skip if this would undelete a recently-deleted entry (within 30 seconds)
|
|
825
|
-
if (existingWasDeleted && hasHash && timeDiff < 30) {
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
if (!existingSub || now >= existingSub.created_at) {
|
|
829
|
-
listSub.entriesByDTag.set(treeName, {
|
|
830
|
-
hash: toHex(entry.cid.hash),
|
|
831
|
-
visibility: entry.visibility ?? 'public',
|
|
832
|
-
key: entry.cid.key ? toHex(entry.cid.key) : undefined,
|
|
833
|
-
encryptedKey: entry.encryptedKey,
|
|
834
|
-
keyId: entry.keyId,
|
|
835
|
-
selfEncryptedKey: entry.selfEncryptedKey,
|
|
836
|
-
selfEncryptedLinkKey: entry.selfEncryptedLinkKey,
|
|
837
|
-
created_at: now,
|
|
838
|
-
});
|
|
839
|
-
// Emit updated state to ALL callbacks
|
|
840
|
-
const result = [];
|
|
841
|
-
for (const [dTag, e] of listSub.entriesByDTag) {
|
|
842
|
-
if (!e.hash)
|
|
843
|
-
continue; // Skip deleted trees
|
|
844
|
-
result.push({
|
|
845
|
-
key: `${npubStr}/${dTag}`,
|
|
846
|
-
cid: cid(fromHex(e.hash), e.key ? fromHex(e.key) : undefined),
|
|
847
|
-
visibility: e.visibility,
|
|
848
|
-
encryptedKey: e.encryptedKey,
|
|
849
|
-
keyId: e.keyId,
|
|
850
|
-
selfEncryptedKey: e.selfEncryptedKey,
|
|
851
|
-
selfEncryptedLinkKey: e.selfEncryptedLinkKey,
|
|
852
|
-
createdAt: e.created_at,
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
for (const cb of listSub.callbacks) {
|
|
856
|
-
try {
|
|
857
|
-
cb(result);
|
|
858
|
-
}
|
|
859
|
-
catch (err) {
|
|
860
|
-
console.error('List callback error:', err);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
},
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
//# sourceMappingURL=nostr.js.map
|