@hashtree/nostr 0.1.13 → 0.2.3
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 +11 -33
- package/dist/eventCollection.d.ts +1 -1
- package/dist/eventCollection.d.ts.map +1 -1
- package/dist/eventCollection.js +2 -7
- package/dist/eventCollection.js.map +1 -1
- package/dist/eventKeys.d.ts +1 -0
- package/dist/eventKeys.d.ts.map +1 -1
- package/dist/eventKeys.js +7 -1
- package/dist/eventKeys.js.map +1 -1
- package/dist/events.d.ts +2 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +41 -61
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/resolver/nostr.d.ts.map +1 -1
- package/dist/resolver/nostr.js +17 -12
- package/dist/resolver/nostr.js.map +1 -1
- package/dist/snapshot.d.ts +5 -0
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +9 -2
- package/dist/snapshot.js.map +1 -1
- package/dist/treeEventSnapshots.d.ts.map +1 -1
- package/dist/treeEventSnapshots.js +6 -11
- package/dist/treeEventSnapshots.js.map +1 -1
- package/package.json +18 -14
- package/src/eventCollection.ts +189 -0
- package/src/eventKeys.ts +155 -0
- package/src/eventValidation.ts +71 -0
- package/src/events.ts +900 -0
- package/src/index.ts +122 -0
- package/src/replaceablePublish.ts +204 -0
- package/src/resolver/index.ts +14 -0
- package/src/resolver/nostr.ts +1210 -0
- package/src/snapshot.ts +213 -0
- package/src/treeEventSnapshotPermalinks.ts +150 -0
- package/src/treeEventSnapshots.ts +280 -0
- package/dist/webrtc/index.d.ts +0 -6
- package/dist/webrtc/index.d.ts.map +0 -1
- package/dist/webrtc/index.js +0 -7
- 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 -133
- package/dist/webrtc/peer.d.ts.map +0 -1
- package/dist/webrtc/peer.js +0 -617
- package/dist/webrtc/peer.js.map +0 -1
- package/dist/webrtc/peerSelector.d.ts +0 -2
- package/dist/webrtc/peerSelector.d.ts.map +0 -1
- package/dist/webrtc/peerSelector.js +0 -2
- package/dist/webrtc/peerSelector.js.map +0 -1
- package/dist/webrtc/protocol.d.ts +0 -3
- package/dist/webrtc/protocol.d.ts.map +0 -1
- package/dist/webrtc/protocol.js +0 -2
- package/dist/webrtc/protocol.js.map +0 -1
- package/dist/webrtc/store.d.ts +0 -217
- package/dist/webrtc/store.d.ts.map +0 -1
- package/dist/webrtc/store.js +0 -1320
- package/dist/webrtc/store.js.map +0 -1
- package/dist/webrtc/types.d.ts +0 -101
- package/dist/webrtc/types.d.ts.map +0 -1
- package/dist/webrtc/types.js +0 -5
- package/dist/webrtc/types.js.map +0 -1
package/src/events.ts
ADDED
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
import { decode, encode } from '@msgpack/msgpack';
|
|
2
|
+
import {
|
|
3
|
+
CollectionSource,
|
|
4
|
+
serializeCollectionManifestMetadata,
|
|
5
|
+
type CollectionManifest,
|
|
6
|
+
} from '@hashtree/collection';
|
|
7
|
+
import { HashTree, LinkType, type CID, type Store, toHex, sha256 } from '@hashtree/core';
|
|
8
|
+
import {
|
|
9
|
+
COLLECTION_MANIFEST_METADATA_FILE,
|
|
10
|
+
collectionManifestToNostrEventManifest,
|
|
11
|
+
createNostrEventCollectionWriter,
|
|
12
|
+
DEFAULT_NOSTR_EVENT_COLLECTION_SOURCE_ID,
|
|
13
|
+
nostrEventCollectionManifestMetadata,
|
|
14
|
+
nostrEventManifestToCollectionManifest,
|
|
15
|
+
} from './eventCollection.js';
|
|
16
|
+
import {
|
|
17
|
+
compareEvents,
|
|
18
|
+
compareReplaceableEvents,
|
|
19
|
+
createdAtFromIndexKey,
|
|
20
|
+
getDTag,
|
|
21
|
+
isParameterizedReplaceableKind,
|
|
22
|
+
isReplaceableKind,
|
|
23
|
+
MANIFEST_BY_AUTHOR_KIND_TIME,
|
|
24
|
+
MANIFEST_BY_AUTHOR_TIME,
|
|
25
|
+
MANIFEST_BY_ID,
|
|
26
|
+
MANIFEST_BY_KIND_TIME,
|
|
27
|
+
MANIFEST_BY_KIND_TIME_AUTHOR,
|
|
28
|
+
MANIFEST_BY_TAG,
|
|
29
|
+
MANIFEST_BY_TIME,
|
|
30
|
+
MANIFEST_PARAMETERIZED_REPLACEABLE,
|
|
31
|
+
MANIFEST_REPLACEABLE,
|
|
32
|
+
normalizeTagName,
|
|
33
|
+
normalizeTagValue,
|
|
34
|
+
padKind,
|
|
35
|
+
parameterizedReplaceableKey,
|
|
36
|
+
replaceableKey,
|
|
37
|
+
retainLatestReplaceableEvents,
|
|
38
|
+
tagPrefix,
|
|
39
|
+
} from './eventKeys.js';
|
|
40
|
+
import { assertStringArray, validateEventShape, validateHex64, validateKind } from './eventValidation.js';
|
|
41
|
+
|
|
42
|
+
export const NOSTR_EVENT_ENVELOPE_VERSION = 1;
|
|
43
|
+
|
|
44
|
+
export interface StoredNostrEvent {
|
|
45
|
+
id: string;
|
|
46
|
+
pubkey: string;
|
|
47
|
+
created_at: number;
|
|
48
|
+
kind: number;
|
|
49
|
+
tags: string[][];
|
|
50
|
+
content: string;
|
|
51
|
+
sig: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface NostrEventManifest {
|
|
55
|
+
byId: CID | null;
|
|
56
|
+
byAuthorTime: CID | null;
|
|
57
|
+
byAuthorKindTime: CID | null;
|
|
58
|
+
byKindTime: CID | null;
|
|
59
|
+
byKindTimeAuthor: CID | null;
|
|
60
|
+
byTime: CID | null;
|
|
61
|
+
byTag: CID | null;
|
|
62
|
+
replaceable: CID | null;
|
|
63
|
+
parameterizedReplaceable: CID | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ListEventsOptions {
|
|
67
|
+
limit?: number;
|
|
68
|
+
since?: number;
|
|
69
|
+
until?: number;
|
|
70
|
+
/** Throw when a selected event blob is missing or unreadable instead of skipping it. */
|
|
71
|
+
strict?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type NostrEventQueryValue<T> = T | readonly T[];
|
|
75
|
+
|
|
76
|
+
export interface NostrEventQuery {
|
|
77
|
+
ids?: NostrEventQueryValue<string>;
|
|
78
|
+
authors?: NostrEventQueryValue<string>;
|
|
79
|
+
kinds?: NostrEventQueryValue<number>;
|
|
80
|
+
tags?: Record<string, NostrEventQueryValue<string>>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface NormalizedNostrEventQuery {
|
|
84
|
+
ids: string[];
|
|
85
|
+
authors: string[];
|
|
86
|
+
kinds: number[];
|
|
87
|
+
tags: Map<string, string[]>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface NostrEventQueryPlan {
|
|
91
|
+
type: 'ids' | 'index';
|
|
92
|
+
indexName?: string;
|
|
93
|
+
prefixes: string[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const EMPTY_NOSTR_EVENT_QUERY: NormalizedNostrEventQuery = {
|
|
97
|
+
ids: [],
|
|
98
|
+
authors: [],
|
|
99
|
+
kinds: [],
|
|
100
|
+
tags: new Map(),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function normalizeQueryValues<T>(value: NostrEventQueryValue<T> | undefined): T[] {
|
|
104
|
+
if (value === undefined) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
return [...value];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const scalarValue = value as T;
|
|
113
|
+
return [scalarValue];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function dedupeValues<T>(values: T[]): T[] {
|
|
117
|
+
return [...new Set(values)];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeNostrEventQuery(query: NostrEventQuery = {}): NormalizedNostrEventQuery {
|
|
121
|
+
const ids = dedupeValues(normalizeQueryValues(query.ids).map((value) => validateHex64(value, 'event id')));
|
|
122
|
+
const authors = dedupeValues(normalizeQueryValues(query.authors).map((value) => validateHex64(value, 'pubkey')));
|
|
123
|
+
const kinds = dedupeValues(normalizeQueryValues(query.kinds).map((value) => validateKind(value)));
|
|
124
|
+
const tags = new Map<string, string[]>();
|
|
125
|
+
|
|
126
|
+
for (const [rawTagName, rawTagValues] of Object.entries(query.tags ?? {})) {
|
|
127
|
+
const tagName = normalizeTagName(rawTagName);
|
|
128
|
+
const tagValues = dedupeValues(
|
|
129
|
+
normalizeQueryValues(rawTagValues).map((value) => normalizeTagValue(tagName, value)),
|
|
130
|
+
);
|
|
131
|
+
if (tagValues.length > 0) {
|
|
132
|
+
tags.set(tagName, tagValues);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { ids, authors, kinds, tags };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function matchesNostrEventQuery(event: StoredNostrEvent, query: NormalizedNostrEventQuery): boolean {
|
|
140
|
+
if (query.ids.length > 0 && !query.ids.includes(event.id)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (query.authors.length > 0 && !query.authors.includes(event.pubkey)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (query.kinds.length > 0 && !query.kinds.includes(event.kind)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const [tagName, tagValues] of query.tags) {
|
|
151
|
+
const matchesTag = event.tags.some(([candidateTagName, candidateTagValue]) => {
|
|
152
|
+
if (typeof candidateTagName !== 'string' || typeof candidateTagValue !== 'string') {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const normalizedCandidateName = candidateTagName.toLowerCase();
|
|
157
|
+
if (normalizedCandidateName !== tagName) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return tagValues.includes(normalizeTagValue(tagName, candidateTagValue));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (!matchesTag) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function selectNostrEventQueryPlan(
|
|
173
|
+
query: NormalizedNostrEventQuery,
|
|
174
|
+
manifest: NostrEventManifest | null,
|
|
175
|
+
bounded: boolean,
|
|
176
|
+
): NostrEventQueryPlan {
|
|
177
|
+
if (query.ids.length > 0) {
|
|
178
|
+
return { type: 'ids', prefixes: [] };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (query.tags.size === 1 && manifest?.byTag) {
|
|
182
|
+
const firstTag = query.tags.entries().next().value;
|
|
183
|
+
if (firstTag) {
|
|
184
|
+
const [tagName, tagValues] = firstTag;
|
|
185
|
+
return {
|
|
186
|
+
type: 'index',
|
|
187
|
+
indexName: MANIFEST_BY_TAG,
|
|
188
|
+
prefixes: tagValues.map((tagValue) => tagPrefix(tagName, tagValue)),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// A bounded page across many authors should stop at the global page limit.
|
|
194
|
+
// The author-bearing index can reject unrelated events before reading blobs.
|
|
195
|
+
if (bounded && manifest?.byKindTimeAuthor && query.authors.length >= 12 && query.kinds.length > 0) {
|
|
196
|
+
return {
|
|
197
|
+
type: 'index',
|
|
198
|
+
indexName: MANIFEST_BY_KIND_TIME_AUTHOR,
|
|
199
|
+
prefixes: query.kinds.map((kind) => `${padKind(kind)}:`),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (query.authors.length > 0) {
|
|
204
|
+
if (query.kinds.length === 1) {
|
|
205
|
+
const kindPrefix = `${padKind(query.kinds[0]!)}:`;
|
|
206
|
+
return {
|
|
207
|
+
type: 'index',
|
|
208
|
+
indexName: MANIFEST_BY_AUTHOR_KIND_TIME,
|
|
209
|
+
prefixes: query.authors.map((author) => `${author}:${kindPrefix}`),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
type: 'index',
|
|
215
|
+
indexName: MANIFEST_BY_AUTHOR_TIME,
|
|
216
|
+
prefixes: query.authors.map((author) => `${author}:`),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (query.kinds.length > 0) {
|
|
221
|
+
return {
|
|
222
|
+
type: 'index',
|
|
223
|
+
indexName: MANIFEST_BY_KIND_TIME,
|
|
224
|
+
prefixes: query.kinds.map((kind) => `${padKind(kind)}:`),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
type: 'index',
|
|
230
|
+
indexName: MANIFEST_BY_TIME,
|
|
231
|
+
prefixes: [''],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function mergeQueriedEvents(eventGroups: StoredNostrEvent[][], limit?: number): StoredNostrEvent[] {
|
|
236
|
+
const merged = new Map<string, StoredNostrEvent>();
|
|
237
|
+
|
|
238
|
+
for (const group of eventGroups) {
|
|
239
|
+
for (const event of group) {
|
|
240
|
+
const current = merged.get(event.id);
|
|
241
|
+
if (!current || compareEvents(event, current) > 0) {
|
|
242
|
+
merged.set(event.id, event);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const ordered = [...merged.values()].sort((left, right) => compareEvents(right, left));
|
|
248
|
+
return limit === undefined ? ordered : ordered.slice(0, limit);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function canonicalEventIdPayload(event: Omit<StoredNostrEvent, 'id' | 'sig'>): string {
|
|
252
|
+
return JSON.stringify([0, event.pubkey, event.created_at, event.kind, event.tags, event.content]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function computeCanonicalEventId(event: Omit<StoredNostrEvent, 'sig'>): Promise<string> {
|
|
256
|
+
const payload = canonicalEventIdPayload(event);
|
|
257
|
+
return toHex(await sha256(new TextEncoder().encode(payload)));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function encodeStoredNostrEventMsgpack(event: StoredNostrEvent): Uint8Array {
|
|
261
|
+
const normalized = validateEventShape(event);
|
|
262
|
+
return encode([
|
|
263
|
+
NOSTR_EVENT_ENVELOPE_VERSION,
|
|
264
|
+
normalized.id,
|
|
265
|
+
normalized.pubkey,
|
|
266
|
+
normalized.created_at,
|
|
267
|
+
normalized.kind,
|
|
268
|
+
normalized.tags,
|
|
269
|
+
normalized.content,
|
|
270
|
+
normalized.sig,
|
|
271
|
+
]);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function decodeStoredNostrEventMsgpack(data: Uint8Array): StoredNostrEvent {
|
|
275
|
+
const decoded = decode(data);
|
|
276
|
+
if (!Array.isArray(decoded) || decoded.length !== 8) {
|
|
277
|
+
throw new Error('Invalid Nostr event envelope');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const [
|
|
281
|
+
version,
|
|
282
|
+
id,
|
|
283
|
+
pubkey,
|
|
284
|
+
createdAt,
|
|
285
|
+
kind,
|
|
286
|
+
tags,
|
|
287
|
+
content,
|
|
288
|
+
sig,
|
|
289
|
+
] = decoded;
|
|
290
|
+
|
|
291
|
+
if (version !== NOSTR_EVENT_ENVELOPE_VERSION) {
|
|
292
|
+
throw new Error(`Unsupported Nostr event envelope version: ${String(version)}`);
|
|
293
|
+
}
|
|
294
|
+
if (typeof id !== 'string' || typeof pubkey !== 'string' || typeof content !== 'string' || typeof sig !== 'string') {
|
|
295
|
+
throw new Error('Invalid Nostr event envelope fields');
|
|
296
|
+
}
|
|
297
|
+
if (typeof createdAt !== 'number' || !Number.isInteger(createdAt) || createdAt < 0) {
|
|
298
|
+
throw new Error('Invalid Nostr event created_at');
|
|
299
|
+
}
|
|
300
|
+
if (typeof kind !== 'number' || !Number.isInteger(kind) || kind < 0) {
|
|
301
|
+
throw new Error('Invalid Nostr event kind');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
assertStringArray(tags);
|
|
305
|
+
|
|
306
|
+
return validateEventShape({
|
|
307
|
+
id,
|
|
308
|
+
pubkey,
|
|
309
|
+
created_at: createdAt,
|
|
310
|
+
kind,
|
|
311
|
+
tags,
|
|
312
|
+
content,
|
|
313
|
+
sig,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export class NostrEventStore {
|
|
318
|
+
private readonly store: Store;
|
|
319
|
+
private readonly tree: HashTree;
|
|
320
|
+
|
|
321
|
+
constructor(store: Store) {
|
|
322
|
+
this.store = store;
|
|
323
|
+
this.tree = new HashTree({ store });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
encodeEvent(event: StoredNostrEvent): Uint8Array {
|
|
327
|
+
return encodeStoredNostrEventMsgpack(this.validateEventShape(event));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
decodeEvent(data: Uint8Array): StoredNostrEvent {
|
|
331
|
+
return this.validateEventShape(decodeStoredNostrEventMsgpack(data));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async add(root: CID | null, event: StoredNostrEvent): Promise<CID> {
|
|
335
|
+
const normalized = await this.validateEvent(event);
|
|
336
|
+
const manifest = await this.getManifest(root);
|
|
337
|
+
const decision = await this.resolveReplaceableDecision(manifest, normalized);
|
|
338
|
+
if (!decision.accept) {
|
|
339
|
+
if (!root) {
|
|
340
|
+
throw new Error('Rejecting replaceable event without an existing manifest root');
|
|
341
|
+
}
|
|
342
|
+
return root;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const eventBytes = this.encodeEvent(normalized);
|
|
346
|
+
const { cid: eventCid } = await this.tree.putFile(eventBytes);
|
|
347
|
+
const writer = this.collectionWriterFromManifest(manifest);
|
|
348
|
+
if (decision.replaced?.event) {
|
|
349
|
+
await writer.replace(normalized, eventCid, decision.replaced.event);
|
|
350
|
+
} else {
|
|
351
|
+
await writer.put(normalized, eventCid);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const nextManifest = collectionManifestToNostrEventManifest(writer.manifest());
|
|
355
|
+
const manifestRoot = await this.writeManifest(nextManifest);
|
|
356
|
+
if (!manifestRoot) {
|
|
357
|
+
throw new Error('Failed to create Nostr event manifest');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (decision.replaced) {
|
|
361
|
+
await this.store.delete(decision.replaced.cid.hash);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return manifestRoot;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async build(root: CID | null, events: StoredNostrEvent[]): Promise<CID | null> {
|
|
368
|
+
const normalized = retainLatestReplaceableEvents(
|
|
369
|
+
await Promise.all(events.map((event) => this.validateEvent(event))),
|
|
370
|
+
);
|
|
371
|
+
normalized.sort(compareEvents);
|
|
372
|
+
|
|
373
|
+
if (normalized.length === 0) {
|
|
374
|
+
return root;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (root) {
|
|
378
|
+
let current = root;
|
|
379
|
+
for (const event of normalized) {
|
|
380
|
+
current = await this.add(current, event);
|
|
381
|
+
}
|
|
382
|
+
return current;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const writer = this.collectionWriterFromManifest(this.emptyManifest());
|
|
386
|
+
await writer.rebuild(await Promise.all(normalized.map(async (event) => {
|
|
387
|
+
const { cid } = await this.tree.putFile(this.encodeEvent(event));
|
|
388
|
+
return { item: event, cid };
|
|
389
|
+
})));
|
|
390
|
+
|
|
391
|
+
return await this.writeManifest(collectionManifestToNostrEventManifest(writer.manifest()));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async getById(root: CID | null, eventId: string): Promise<StoredNostrEvent | null> {
|
|
395
|
+
const source = this.collectionSourceFromManifest(await this.getManifest(root));
|
|
396
|
+
const eventCid = await source.get(validateHex64(eventId, 'event id'));
|
|
397
|
+
if (!eventCid) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return this.readStoredEvent(eventCid);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async listByAuthor(root: CID | null, pubkey: string, options: ListEventsOptions = {}): Promise<StoredNostrEvent[]> {
|
|
405
|
+
return this.collectEvents(
|
|
406
|
+
this.collectionSourceFromManifest(await this.getManifest(root)),
|
|
407
|
+
MANIFEST_BY_AUTHOR_TIME,
|
|
408
|
+
`${validateHex64(pubkey, 'pubkey')}:`,
|
|
409
|
+
options,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async listByAuthorAndKind(
|
|
414
|
+
root: CID | null,
|
|
415
|
+
pubkey: string,
|
|
416
|
+
kind: number,
|
|
417
|
+
options: ListEventsOptions = {}
|
|
418
|
+
): Promise<StoredNostrEvent[]> {
|
|
419
|
+
return this.collectEvents(
|
|
420
|
+
this.collectionSourceFromManifest(await this.getManifest(root)),
|
|
421
|
+
MANIFEST_BY_AUTHOR_KIND_TIME,
|
|
422
|
+
`${validateHex64(pubkey, 'pubkey')}:${validateKind(kind).toString(16).padStart(8, '0')}:`,
|
|
423
|
+
options,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async *streamByAuthor(
|
|
428
|
+
root: CID | null,
|
|
429
|
+
pubkey: string,
|
|
430
|
+
options: ListEventsOptions = {},
|
|
431
|
+
): AsyncGenerator<StoredNostrEvent> {
|
|
432
|
+
yield* this.streamEvents(
|
|
433
|
+
this.collectionSourceFromManifest(await this.getManifest(root)),
|
|
434
|
+
MANIFEST_BY_AUTHOR_TIME,
|
|
435
|
+
`${validateHex64(pubkey, 'pubkey')}:`,
|
|
436
|
+
options,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async *streamByAuthorAndKind(
|
|
441
|
+
root: CID | null,
|
|
442
|
+
pubkey: string,
|
|
443
|
+
kind: number,
|
|
444
|
+
options: ListEventsOptions = {},
|
|
445
|
+
): AsyncGenerator<StoredNostrEvent> {
|
|
446
|
+
yield* this.streamEvents(
|
|
447
|
+
this.collectionSourceFromManifest(await this.getManifest(root)),
|
|
448
|
+
MANIFEST_BY_AUTHOR_KIND_TIME,
|
|
449
|
+
`${validateHex64(pubkey, 'pubkey')}:${validateKind(kind).toString(16).padStart(8, '0')}:`,
|
|
450
|
+
options,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async query(
|
|
455
|
+
root: CID | null,
|
|
456
|
+
query: NostrEventQuery = {},
|
|
457
|
+
options: ListEventsOptions = {},
|
|
458
|
+
): Promise<StoredNostrEvent[]> {
|
|
459
|
+
const normalizedQuery = normalizeNostrEventQuery(query);
|
|
460
|
+
const manifest = normalizedQuery.ids.length ? null : await this.getManifest(root);
|
|
461
|
+
const plan = selectNostrEventQueryPlan(normalizedQuery, manifest, options.limit !== undefined);
|
|
462
|
+
|
|
463
|
+
if (plan.type === 'ids') {
|
|
464
|
+
return this.queryByIds(root, normalizedQuery, options);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const source = this.collectionSourceFromManifest(manifest!);
|
|
468
|
+
const groups = await Promise.all(
|
|
469
|
+
plan.prefixes.map((prefix) => this.collectEvents(
|
|
470
|
+
source,
|
|
471
|
+
plan.indexName!,
|
|
472
|
+
prefix,
|
|
473
|
+
options,
|
|
474
|
+
normalizedQuery,
|
|
475
|
+
)),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
return mergeQueriedEvents(groups, options.limit);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async *streamQuery(
|
|
482
|
+
root: CID | null,
|
|
483
|
+
query: NostrEventQuery = {},
|
|
484
|
+
options: ListEventsOptions = {},
|
|
485
|
+
): AsyncGenerator<StoredNostrEvent> {
|
|
486
|
+
const normalizedQuery = normalizeNostrEventQuery(query);
|
|
487
|
+
const manifest = normalizedQuery.ids.length ? null : await this.getManifest(root);
|
|
488
|
+
const plan = selectNostrEventQueryPlan(normalizedQuery, manifest, options.limit !== undefined);
|
|
489
|
+
|
|
490
|
+
if (plan.type === 'ids' || plan.prefixes.length !== 1) {
|
|
491
|
+
for (const event of await this.query(root, query, options)) {
|
|
492
|
+
yield event;
|
|
493
|
+
}
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
yield* this.streamEvents(
|
|
498
|
+
this.collectionSourceFromManifest(manifest!),
|
|
499
|
+
plan.indexName!,
|
|
500
|
+
plan.prefixes[0] ?? '',
|
|
501
|
+
options,
|
|
502
|
+
normalizedQuery,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async getReplaceable(root: CID | null, pubkey: string, kind: number): Promise<StoredNostrEvent | null> {
|
|
507
|
+
const source = this.collectionSourceFromManifest(await this.getManifest(root));
|
|
508
|
+
const eventCid = await source.getIndexLink(
|
|
509
|
+
MANIFEST_REPLACEABLE,
|
|
510
|
+
replaceableKey(validateHex64(pubkey, 'pubkey'), validateKind(kind)),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
return eventCid ? this.readStoredEvent(eventCid) : null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async listRecent(root: CID | null, options: ListEventsOptions = {}): Promise<StoredNostrEvent[]> {
|
|
517
|
+
return this.collectEvents(
|
|
518
|
+
this.collectionSourceFromManifest(await this.getManifest(root)),
|
|
519
|
+
MANIFEST_BY_TIME,
|
|
520
|
+
'',
|
|
521
|
+
options,
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async *streamRecent(
|
|
526
|
+
root: CID | null,
|
|
527
|
+
options: ListEventsOptions = {},
|
|
528
|
+
): AsyncGenerator<StoredNostrEvent> {
|
|
529
|
+
yield* this.streamEvents(
|
|
530
|
+
this.collectionSourceFromManifest(await this.getManifest(root)),
|
|
531
|
+
MANIFEST_BY_TIME,
|
|
532
|
+
'',
|
|
533
|
+
options,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async listByTag(
|
|
538
|
+
root: CID | null,
|
|
539
|
+
tagName: string,
|
|
540
|
+
tagValue: string,
|
|
541
|
+
options: ListEventsOptions = {}
|
|
542
|
+
): Promise<StoredNostrEvent[]> {
|
|
543
|
+
return this.collectEvents(
|
|
544
|
+
this.collectionSourceFromManifest(await this.getManifest(root)),
|
|
545
|
+
MANIFEST_BY_TAG,
|
|
546
|
+
tagPrefix(tagName, tagValue),
|
|
547
|
+
options,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async *streamByTag(
|
|
552
|
+
root: CID | null,
|
|
553
|
+
tagName: string,
|
|
554
|
+
tagValue: string,
|
|
555
|
+
options: ListEventsOptions = {},
|
|
556
|
+
): AsyncGenerator<StoredNostrEvent> {
|
|
557
|
+
yield* this.streamEvents(
|
|
558
|
+
this.collectionSourceFromManifest(await this.getManifest(root)),
|
|
559
|
+
MANIFEST_BY_TAG,
|
|
560
|
+
tagPrefix(tagName, tagValue),
|
|
561
|
+
options,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async getParameterizedReplaceable(
|
|
566
|
+
root: CID | null,
|
|
567
|
+
pubkey: string,
|
|
568
|
+
kind: number,
|
|
569
|
+
dTag: string
|
|
570
|
+
): Promise<StoredNostrEvent | null> {
|
|
571
|
+
const source = this.collectionSourceFromManifest(await this.getManifest(root));
|
|
572
|
+
const eventCid = await source.getIndexLink(
|
|
573
|
+
MANIFEST_PARAMETERIZED_REPLACEABLE,
|
|
574
|
+
parameterizedReplaceableKey(
|
|
575
|
+
validateHex64(pubkey, 'pubkey'),
|
|
576
|
+
validateKind(kind),
|
|
577
|
+
dTag,
|
|
578
|
+
),
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
return eventCid ? this.readStoredEvent(eventCid) : null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async getManifest(root: CID | null): Promise<NostrEventManifest> {
|
|
585
|
+
if (!root) {
|
|
586
|
+
return {
|
|
587
|
+
byId: null,
|
|
588
|
+
byAuthorTime: null,
|
|
589
|
+
byAuthorKindTime: null,
|
|
590
|
+
byKindTime: null,
|
|
591
|
+
byKindTimeAuthor: null,
|
|
592
|
+
byTime: null,
|
|
593
|
+
byTag: null,
|
|
594
|
+
replaceable: null,
|
|
595
|
+
parameterizedReplaceable: null,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const entries = await this.tree.listDirectory(root);
|
|
600
|
+
const getCid = (name: string): CID | null => entries.find(entry => entry.name === name)?.cid ?? null;
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
byId: getCid(MANIFEST_BY_ID),
|
|
604
|
+
byAuthorTime: getCid(MANIFEST_BY_AUTHOR_TIME),
|
|
605
|
+
byAuthorKindTime: getCid(MANIFEST_BY_AUTHOR_KIND_TIME),
|
|
606
|
+
byKindTime: getCid(MANIFEST_BY_KIND_TIME),
|
|
607
|
+
byKindTimeAuthor: getCid(MANIFEST_BY_KIND_TIME_AUTHOR),
|
|
608
|
+
byTime: getCid(MANIFEST_BY_TIME),
|
|
609
|
+
byTag: getCid(MANIFEST_BY_TAG),
|
|
610
|
+
replaceable: getCid(MANIFEST_REPLACEABLE),
|
|
611
|
+
parameterizedReplaceable: getCid(MANIFEST_PARAMETERIZED_REPLACEABLE),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async getCollectionManifest(
|
|
616
|
+
root: CID | null,
|
|
617
|
+
sourceId: string = DEFAULT_NOSTR_EVENT_COLLECTION_SOURCE_ID,
|
|
618
|
+
): Promise<CollectionManifest> {
|
|
619
|
+
return nostrEventManifestToCollectionManifest(await this.getManifest(root), sourceId);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async getCollectionSource(
|
|
623
|
+
root: CID | null,
|
|
624
|
+
sourceId: string = DEFAULT_NOSTR_EVENT_COLLECTION_SOURCE_ID,
|
|
625
|
+
): Promise<CollectionSource> {
|
|
626
|
+
return new CollectionSource(this.store, await this.getCollectionManifest(root, sourceId));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async listByKind(
|
|
630
|
+
root: CID | null,
|
|
631
|
+
kind: number,
|
|
632
|
+
options: ListEventsOptions = {}
|
|
633
|
+
): Promise<StoredNostrEvent[]> {
|
|
634
|
+
return this.collectEvents(
|
|
635
|
+
this.collectionSourceFromManifest(await this.getManifest(root)),
|
|
636
|
+
MANIFEST_BY_KIND_TIME,
|
|
637
|
+
`${validateKind(kind).toString(16).padStart(8, '0')}:`,
|
|
638
|
+
options,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async *streamByKind(
|
|
643
|
+
root: CID | null,
|
|
644
|
+
kind: number,
|
|
645
|
+
options: ListEventsOptions = {},
|
|
646
|
+
): AsyncGenerator<StoredNostrEvent> {
|
|
647
|
+
yield* this.streamEvents(
|
|
648
|
+
this.collectionSourceFromManifest(await this.getManifest(root)),
|
|
649
|
+
MANIFEST_BY_KIND_TIME,
|
|
650
|
+
`${validateKind(kind).toString(16).padStart(8, '0')}:`,
|
|
651
|
+
options,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
private async collectEvents(
|
|
656
|
+
source: CollectionSource,
|
|
657
|
+
indexName: string,
|
|
658
|
+
prefix: string,
|
|
659
|
+
options: ListEventsOptions = {},
|
|
660
|
+
query: NormalizedNostrEventQuery = EMPTY_NOSTR_EVENT_QUERY,
|
|
661
|
+
): Promise<StoredNostrEvent[]> {
|
|
662
|
+
const events: StoredNostrEvent[] = [];
|
|
663
|
+
for await (const event of this.streamEvents(source, indexName, prefix, options, query)) {
|
|
664
|
+
events.push(event);
|
|
665
|
+
}
|
|
666
|
+
return events;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
private async *streamEvents(
|
|
670
|
+
source: CollectionSource,
|
|
671
|
+
indexName: string,
|
|
672
|
+
prefix: string,
|
|
673
|
+
options: ListEventsOptions = {},
|
|
674
|
+
query: NormalizedNostrEventQuery = EMPTY_NOSTR_EVENT_QUERY,
|
|
675
|
+
): AsyncGenerator<StoredNostrEvent> {
|
|
676
|
+
const iterator = indexName === MANIFEST_BY_ID
|
|
677
|
+
? source.streamQueryById({ prefix })
|
|
678
|
+
: source.streamQueryIndex(indexName, { prefix });
|
|
679
|
+
let emitted = 0;
|
|
680
|
+
|
|
681
|
+
for await (const { key, cid: eventCid } of iterator) {
|
|
682
|
+
const parts = indexName === MANIFEST_BY_KIND_TIME_AUTHOR ? key.split(':') : null;
|
|
683
|
+
const createdAt = createdAtFromIndexKey(parts ? `${parts[1]}:${parts[3]}` : key);
|
|
684
|
+
if (options.until !== undefined && createdAt > options.until) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (options.since !== undefined && createdAt < options.since) {
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (parts && query.authors.length > 0 && !query.authors.includes(parts[2]!)) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const event = await this.tryReadStoredEvent(eventCid, options.strict);
|
|
695
|
+
if (!event) {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (!matchesNostrEventQuery(event, query)) {
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
yield event;
|
|
703
|
+
emitted += 1;
|
|
704
|
+
if (options.limit !== undefined && emitted >= options.limit) {
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
private async queryByIds(
|
|
711
|
+
root: CID | null,
|
|
712
|
+
query: NormalizedNostrEventQuery,
|
|
713
|
+
options: ListEventsOptions = {},
|
|
714
|
+
): Promise<StoredNostrEvent[]> {
|
|
715
|
+
const events: StoredNostrEvent[] = [];
|
|
716
|
+
for (const eventId of query.ids) {
|
|
717
|
+
const event = await this.getById(root, eventId);
|
|
718
|
+
if (!event) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (options.until !== undefined && event.created_at > options.until) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (options.since !== undefined && event.created_at < options.since) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (!matchesNostrEventQuery(event, query)) {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
events.push(event);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
events.sort((left, right) => compareEvents(right, left));
|
|
734
|
+
return options.limit === undefined ? events : events.slice(0, options.limit);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private async readStoredEvent(eventCid: CID): Promise<StoredNostrEvent> {
|
|
738
|
+
const data = await this.tree.readFile(eventCid);
|
|
739
|
+
if (!data) {
|
|
740
|
+
throw new Error('Stored Nostr event blob is missing');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return this.decodeEvent(data);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
private async tryReadStoredEvent(eventCid: CID, strict = false): Promise<StoredNostrEvent | null> {
|
|
747
|
+
try {
|
|
748
|
+
return await this.readStoredEvent(eventCid);
|
|
749
|
+
} catch (error) {
|
|
750
|
+
if (strict) {
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
753
|
+
if (error instanceof Error && error.message === 'Stored Nostr event blob is missing') {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
console.warn('Skipping unreadable stored Nostr event', error);
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private emptyManifest(): NostrEventManifest {
|
|
762
|
+
return {
|
|
763
|
+
byId: null,
|
|
764
|
+
byAuthorTime: null,
|
|
765
|
+
byAuthorKindTime: null,
|
|
766
|
+
byKindTime: null,
|
|
767
|
+
byKindTimeAuthor: null,
|
|
768
|
+
byTime: null,
|
|
769
|
+
byTag: null,
|
|
770
|
+
replaceable: null,
|
|
771
|
+
parameterizedReplaceable: null,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
private collectionSourceFromManifest(manifest: NostrEventManifest): CollectionSource {
|
|
776
|
+
return new CollectionSource(
|
|
777
|
+
this.store,
|
|
778
|
+
nostrEventManifestToCollectionManifest(manifest),
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private collectionWriterFromManifest(manifest: NostrEventManifest) {
|
|
783
|
+
return createNostrEventCollectionWriter(this.store, manifest);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
private async resolveReplaceableDecision(
|
|
787
|
+
manifest: NostrEventManifest,
|
|
788
|
+
event: StoredNostrEvent,
|
|
789
|
+
): Promise<{ accept: boolean; replaced?: { event: StoredNostrEvent; cid: CID } }> {
|
|
790
|
+
const slot = isReplaceableKind(event.kind)
|
|
791
|
+
? {
|
|
792
|
+
indexName: MANIFEST_REPLACEABLE,
|
|
793
|
+
key: replaceableKey(event.pubkey, event.kind),
|
|
794
|
+
}
|
|
795
|
+
: isParameterizedReplaceableKind(event.kind)
|
|
796
|
+
? {
|
|
797
|
+
indexName: MANIFEST_PARAMETERIZED_REPLACEABLE,
|
|
798
|
+
key: parameterizedReplaceableKey(event.pubkey, event.kind, getDTag(event) ?? ''),
|
|
799
|
+
}
|
|
800
|
+
: null;
|
|
801
|
+
|
|
802
|
+
if (!slot) {
|
|
803
|
+
return { accept: true };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const source = this.collectionSourceFromManifest(manifest);
|
|
807
|
+
const existingCid = await source.getIndexLink(slot.indexName, slot.key);
|
|
808
|
+
if (!existingCid) {
|
|
809
|
+
return { accept: true };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
try {
|
|
813
|
+
const existingEvent = await this.readStoredEvent(existingCid);
|
|
814
|
+
if (compareReplaceableEvents(event, existingEvent) > 0) {
|
|
815
|
+
return {
|
|
816
|
+
accept: true,
|
|
817
|
+
replaced: {
|
|
818
|
+
event: existingEvent,
|
|
819
|
+
cid: existingCid,
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
return { accept: false };
|
|
824
|
+
} catch (error) {
|
|
825
|
+
if (error instanceof Error && error.message === 'Stored Nostr event blob is missing') {
|
|
826
|
+
return { accept: true };
|
|
827
|
+
}
|
|
828
|
+
throw error;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
private async writeManifest(manifest: NostrEventManifest): Promise<CID | null> {
|
|
833
|
+
const entries = [];
|
|
834
|
+
const rootMetadata = nostrEventCollectionManifestMetadata(manifest);
|
|
835
|
+
|
|
836
|
+
if (manifest.byId) {
|
|
837
|
+
entries.push({ name: MANIFEST_BY_ID, cid: manifest.byId, size: 0, type: LinkType.Dir });
|
|
838
|
+
}
|
|
839
|
+
if (manifest.byAuthorTime) {
|
|
840
|
+
entries.push({ name: MANIFEST_BY_AUTHOR_TIME, cid: manifest.byAuthorTime, size: 0, type: LinkType.Dir });
|
|
841
|
+
}
|
|
842
|
+
if (manifest.byAuthorKindTime) {
|
|
843
|
+
entries.push({ name: MANIFEST_BY_AUTHOR_KIND_TIME, cid: manifest.byAuthorKindTime, size: 0, type: LinkType.Dir });
|
|
844
|
+
}
|
|
845
|
+
if (manifest.byKindTime) {
|
|
846
|
+
entries.push({ name: MANIFEST_BY_KIND_TIME, cid: manifest.byKindTime, size: 0, type: LinkType.Dir });
|
|
847
|
+
}
|
|
848
|
+
if (manifest.byKindTimeAuthor) {
|
|
849
|
+
entries.push({ name: MANIFEST_BY_KIND_TIME_AUTHOR, cid: manifest.byKindTimeAuthor, size: 0, type: LinkType.Dir });
|
|
850
|
+
}
|
|
851
|
+
if (manifest.byTime) {
|
|
852
|
+
entries.push({ name: MANIFEST_BY_TIME, cid: manifest.byTime, size: 0, type: LinkType.Dir });
|
|
853
|
+
}
|
|
854
|
+
if (manifest.byTag) {
|
|
855
|
+
entries.push({ name: MANIFEST_BY_TAG, cid: manifest.byTag, size: 0, type: LinkType.Dir });
|
|
856
|
+
}
|
|
857
|
+
if (manifest.replaceable) {
|
|
858
|
+
entries.push({ name: MANIFEST_REPLACEABLE, cid: manifest.replaceable, size: 0, type: LinkType.Dir });
|
|
859
|
+
}
|
|
860
|
+
if (manifest.parameterizedReplaceable) {
|
|
861
|
+
entries.push({
|
|
862
|
+
name: MANIFEST_PARAMETERIZED_REPLACEABLE,
|
|
863
|
+
cid: manifest.parameterizedReplaceable,
|
|
864
|
+
size: 0,
|
|
865
|
+
type: LinkType.Dir,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
if (rootMetadata) {
|
|
869
|
+
const metadataBytes = serializeCollectionManifestMetadata(rootMetadata);
|
|
870
|
+
const { cid, size } = await this.tree.putFile(metadataBytes);
|
|
871
|
+
entries.push({
|
|
872
|
+
name: COLLECTION_MANIFEST_METADATA_FILE,
|
|
873
|
+
cid,
|
|
874
|
+
size,
|
|
875
|
+
type: LinkType.File,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (entries.length === 0) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const { cid } = await this.tree.putDirectory(entries);
|
|
884
|
+
return cid;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
private validateEventShape(event: StoredNostrEvent): StoredNostrEvent {
|
|
888
|
+
return validateEventShape(event);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private async validateEvent(event: StoredNostrEvent): Promise<StoredNostrEvent> {
|
|
892
|
+
const normalized = this.validateEventShape(event);
|
|
893
|
+
const computedId = await computeCanonicalEventId(normalized);
|
|
894
|
+
if (computedId !== normalized.id) {
|
|
895
|
+
throw new Error(`Event id mismatch: expected ${computedId}, got ${normalized.id}`);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return normalized;
|
|
899
|
+
}
|
|
900
|
+
}
|