@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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COLLECTION_MANIFEST_METADATA_FILE,
|
|
3
|
+
CollectionSource,
|
|
4
|
+
CollectionWriter,
|
|
5
|
+
collectionManifestMetadataFromManifest,
|
|
6
|
+
deserializeCid,
|
|
7
|
+
serializeCid,
|
|
8
|
+
type CollectionDefinition,
|
|
9
|
+
type CollectionManifest,
|
|
10
|
+
type CollectionManifestIndex,
|
|
11
|
+
type CollectionPublishedSchema,
|
|
12
|
+
} from '@hashtree/collection';
|
|
13
|
+
import type { Store } from '@hashtree/core';
|
|
14
|
+
import {
|
|
15
|
+
authorKindTimeKey,
|
|
16
|
+
authorTimeKey,
|
|
17
|
+
getDTag,
|
|
18
|
+
isParameterizedReplaceableKind,
|
|
19
|
+
isReplaceableKind,
|
|
20
|
+
kindTimeKey,
|
|
21
|
+
MANIFEST_BY_AUTHOR_KIND_TIME,
|
|
22
|
+
MANIFEST_BY_AUTHOR_TIME,
|
|
23
|
+
MANIFEST_BY_ID,
|
|
24
|
+
MANIFEST_BY_KIND_TIME,
|
|
25
|
+
MANIFEST_BY_KIND_TIME_AUTHOR,
|
|
26
|
+
MANIFEST_BY_TAG,
|
|
27
|
+
MANIFEST_BY_TIME,
|
|
28
|
+
MANIFEST_PARAMETERIZED_REPLACEABLE,
|
|
29
|
+
MANIFEST_REPLACEABLE,
|
|
30
|
+
kindTimeAuthorKey,
|
|
31
|
+
parameterizedReplaceableKey,
|
|
32
|
+
replaceableKey,
|
|
33
|
+
tagKeys,
|
|
34
|
+
timeKey,
|
|
35
|
+
} from './eventKeys.js';
|
|
36
|
+
import { validateEventShape } from './eventValidation.js';
|
|
37
|
+
import type { NostrEventManifest, StoredNostrEvent } from './events.js';
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_NOSTR_EVENT_COLLECTION_SOURCE_ID = 'nostr-events';
|
|
40
|
+
export const NOSTR_EVENT_ITEM_FORMAT = 'nostr/event@1';
|
|
41
|
+
export const NOSTR_EVENT_PROJECTION_FORMAT = 'hashtree/nostr-event-index@1';
|
|
42
|
+
export { COLLECTION_MANIFEST_METADATA_FILE };
|
|
43
|
+
|
|
44
|
+
const NOSTR_EVENT_PUBLISHED_SCHEMA: CollectionPublishedSchema = {
|
|
45
|
+
itemFormat: NOSTR_EVENT_ITEM_FORMAT,
|
|
46
|
+
projectionFormat: NOSTR_EVENT_PROJECTION_FORMAT,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function createNostrEventCollectionDefinition(
|
|
50
|
+
sourceId: string = DEFAULT_NOSTR_EVENT_COLLECTION_SOURCE_ID,
|
|
51
|
+
): CollectionDefinition<StoredNostrEvent> {
|
|
52
|
+
return {
|
|
53
|
+
sourceId,
|
|
54
|
+
schema: {
|
|
55
|
+
version: 1,
|
|
56
|
+
validate: (event) => {
|
|
57
|
+
validateEventShape(event);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
publishedSchema: NOSTR_EVENT_PUBLISHED_SCHEMA,
|
|
61
|
+
getId: (event) => event.id,
|
|
62
|
+
keyIndexes: [
|
|
63
|
+
{
|
|
64
|
+
name: MANIFEST_BY_AUTHOR_TIME,
|
|
65
|
+
keys: (event) => [authorTimeKey(event)],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: MANIFEST_BY_AUTHOR_KIND_TIME,
|
|
69
|
+
keys: (event) => [authorKindTimeKey(event)],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: MANIFEST_BY_KIND_TIME,
|
|
73
|
+
keys: (event) => [kindTimeKey(event)],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: MANIFEST_BY_KIND_TIME_AUTHOR,
|
|
77
|
+
keys: (event) => [kindTimeAuthorKey(event)],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: MANIFEST_BY_TIME,
|
|
81
|
+
keys: (event) => [timeKey(event)],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: MANIFEST_BY_TAG,
|
|
85
|
+
keys: (event) => tagKeys(event),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: MANIFEST_REPLACEABLE,
|
|
89
|
+
keys: (event) => (
|
|
90
|
+
isReplaceableKind(event.kind)
|
|
91
|
+
? [replaceableKey(event.pubkey, event.kind)]
|
|
92
|
+
: []
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: MANIFEST_PARAMETERIZED_REPLACEABLE,
|
|
97
|
+
keys: (event) => (
|
|
98
|
+
isParameterizedReplaceableKind(event.kind)
|
|
99
|
+
? [parameterizedReplaceableKey(event.pubkey, event.kind, getDTag(event) ?? '')]
|
|
100
|
+
: []
|
|
101
|
+
),
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function nostrEventManifestToCollectionManifest(
|
|
108
|
+
manifest: NostrEventManifest,
|
|
109
|
+
sourceId: string = DEFAULT_NOSTR_EVENT_COLLECTION_SOURCE_ID,
|
|
110
|
+
): CollectionManifest {
|
|
111
|
+
const indexes: Record<string, CollectionManifestIndex> = {
|
|
112
|
+
[MANIFEST_BY_AUTHOR_TIME]: { kind: 'key', root: serializeCid(manifest.byAuthorTime) },
|
|
113
|
+
[MANIFEST_BY_AUTHOR_KIND_TIME]: { kind: 'key', root: serializeCid(manifest.byAuthorKindTime) },
|
|
114
|
+
[MANIFEST_BY_KIND_TIME]: { kind: 'key', root: serializeCid(manifest.byKindTime) },
|
|
115
|
+
[MANIFEST_BY_KIND_TIME_AUTHOR]: { kind: 'key', root: serializeCid(manifest.byKindTimeAuthor) },
|
|
116
|
+
[MANIFEST_BY_TIME]: { kind: 'key', root: serializeCid(manifest.byTime) },
|
|
117
|
+
[MANIFEST_BY_TAG]: { kind: 'key', root: serializeCid(manifest.byTag) },
|
|
118
|
+
[MANIFEST_REPLACEABLE]: { kind: 'key', root: serializeCid(manifest.replaceable) },
|
|
119
|
+
[MANIFEST_PARAMETERIZED_REPLACEABLE]: {
|
|
120
|
+
kind: 'key',
|
|
121
|
+
root: serializeCid(manifest.parameterizedReplaceable),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
version: 1,
|
|
127
|
+
sourceId,
|
|
128
|
+
schemaVersion: 1,
|
|
129
|
+
updatedAt: 0,
|
|
130
|
+
byIdRoot: serializeCid(manifest.byId),
|
|
131
|
+
indexes,
|
|
132
|
+
publishedSchema: NOSTR_EVENT_PUBLISHED_SCHEMA,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function collectionManifestToNostrEventManifest(manifest: CollectionManifest): NostrEventManifest {
|
|
137
|
+
const byId = deserializeCid(manifest.byIdRoot);
|
|
138
|
+
const keyIndexCid = (name: string) => {
|
|
139
|
+
const index = manifest.indexes[name];
|
|
140
|
+
if (!index?.root) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return deserializeCid(index.root);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
byId,
|
|
148
|
+
byAuthorTime: keyIndexCid(MANIFEST_BY_AUTHOR_TIME),
|
|
149
|
+
byAuthorKindTime: keyIndexCid(MANIFEST_BY_AUTHOR_KIND_TIME),
|
|
150
|
+
byKindTime: keyIndexCid(MANIFEST_BY_KIND_TIME),
|
|
151
|
+
byKindTimeAuthor: keyIndexCid(MANIFEST_BY_KIND_TIME_AUTHOR),
|
|
152
|
+
byTime: keyIndexCid(MANIFEST_BY_TIME),
|
|
153
|
+
byTag: keyIndexCid(MANIFEST_BY_TAG),
|
|
154
|
+
replaceable: keyIndexCid(MANIFEST_REPLACEABLE),
|
|
155
|
+
parameterizedReplaceable: keyIndexCid(MANIFEST_PARAMETERIZED_REPLACEABLE),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function createNostrEventCollectionSource(
|
|
160
|
+
store: Store,
|
|
161
|
+
manifest: NostrEventManifest,
|
|
162
|
+
sourceId: string = DEFAULT_NOSTR_EVENT_COLLECTION_SOURCE_ID,
|
|
163
|
+
): Promise<CollectionSource> {
|
|
164
|
+
const collectionManifest = nostrEventManifestToCollectionManifest(manifest, sourceId);
|
|
165
|
+
return new CollectionSource(store, collectionManifest);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function createNostrEventCollectionWriter(
|
|
169
|
+
store: Store,
|
|
170
|
+
manifest: NostrEventManifest,
|
|
171
|
+
sourceId: string = DEFAULT_NOSTR_EVENT_COLLECTION_SOURCE_ID,
|
|
172
|
+
): CollectionWriter<StoredNostrEvent> {
|
|
173
|
+
return new CollectionWriter(
|
|
174
|
+
store,
|
|
175
|
+
createNostrEventCollectionDefinition(sourceId),
|
|
176
|
+
nostrEventManifestToCollectionManifest(manifest, sourceId),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function nostrEventCollectionManifestMetadata(
|
|
181
|
+
manifest: NostrEventManifest,
|
|
182
|
+
sourceId: string = DEFAULT_NOSTR_EVENT_COLLECTION_SOURCE_ID,
|
|
183
|
+
) {
|
|
184
|
+
return collectionManifestMetadataFromManifest(
|
|
185
|
+
nostrEventManifestToCollectionManifest(manifest, sourceId),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const nostrEventCollectionRootMetadata = nostrEventCollectionManifestMetadata;
|
package/src/eventKeys.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { StoredNostrEvent } from './events.js';
|
|
2
|
+
|
|
3
|
+
const MAX_U64 = (1n << 64n) - 1n;
|
|
4
|
+
|
|
5
|
+
export const MANIFEST_BY_ID = 'by-id';
|
|
6
|
+
export const MANIFEST_BY_AUTHOR_TIME = 'by-author-time';
|
|
7
|
+
export const MANIFEST_BY_AUTHOR_KIND_TIME = 'by-author-kind-time';
|
|
8
|
+
export const MANIFEST_BY_KIND_TIME = 'by-kind-time';
|
|
9
|
+
export const MANIFEST_BY_KIND_TIME_AUTHOR = 'by-kind-time-author';
|
|
10
|
+
export const MANIFEST_BY_TIME = 'by-time';
|
|
11
|
+
export const MANIFEST_BY_TAG = 'by-tag';
|
|
12
|
+
export const MANIFEST_REPLACEABLE = 'replaceable';
|
|
13
|
+
export const MANIFEST_PARAMETERIZED_REPLACEABLE = 'parameterized-replaceable';
|
|
14
|
+
|
|
15
|
+
export function padKind(kind: number): string {
|
|
16
|
+
return kind.toString(16).padStart(8, '0');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function reverseTimestamp(createdAt: number): string {
|
|
20
|
+
return (MAX_U64 - BigInt(createdAt)).toString(16).padStart(16, '0');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isReplaceableKind(kind: number): boolean {
|
|
24
|
+
return kind === 0 || kind === 3 || (kind >= 10_000 && kind < 20_000);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isParameterizedReplaceableKind(kind: number): boolean {
|
|
28
|
+
return kind >= 30_000 && kind < 40_000;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getDTag(event: Pick<StoredNostrEvent, 'tags'>): string | null {
|
|
32
|
+
for (const tag of event.tags) {
|
|
33
|
+
if (tag[0] === 'd' && typeof tag[1] === 'string' && tag[1].length > 0) {
|
|
34
|
+
return tag[1];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function compareEvents(a: StoredNostrEvent, b: StoredNostrEvent): number {
|
|
42
|
+
if (a.created_at !== b.created_at) {
|
|
43
|
+
return a.created_at - b.created_at;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return a.id.localeCompare(b.id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function compareReplaceableEvents(a: StoredNostrEvent, b: StoredNostrEvent): number {
|
|
50
|
+
if (a.created_at !== b.created_at) {
|
|
51
|
+
return a.created_at - b.created_at;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return b.id.localeCompare(a.id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function authorTimeKey(event: StoredNostrEvent): string {
|
|
58
|
+
return `${event.pubkey}:${reverseTimestamp(event.created_at)}:${event.id}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function authorKindTimeKey(event: StoredNostrEvent): string {
|
|
62
|
+
return `${event.pubkey}:${padKind(event.kind)}:${reverseTimestamp(event.created_at)}:${event.id}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function kindTimeKey(event: StoredNostrEvent): string {
|
|
66
|
+
return `${padKind(event.kind)}:${reverseTimestamp(event.created_at)}:${event.id}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function kindTimeAuthorKey(event: StoredNostrEvent): string {
|
|
70
|
+
return `${padKind(event.kind)}:${reverseTimestamp(event.created_at)}:${event.pubkey}:${event.id}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function timeKey(event: StoredNostrEvent): string {
|
|
74
|
+
return `${reverseTimestamp(event.created_at)}:${event.id}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createdAtFromIndexKey(key: string): number {
|
|
78
|
+
const parts = key.split(':');
|
|
79
|
+
if (parts.length < 2) {
|
|
80
|
+
throw new Error(`Invalid Nostr index key: ${key}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const reversed = parts[parts.length - 2];
|
|
84
|
+
const reversedTimestamp = BigInt(`0x${reversed}`);
|
|
85
|
+
const createdAt = MAX_U64 - reversedTimestamp;
|
|
86
|
+
if (createdAt > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
87
|
+
throw new Error(`Created_at exceeds safe integer range in Nostr index key: ${key}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Number(createdAt);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeTagName(tagName: string): string {
|
|
94
|
+
if (tagName.length === 0) {
|
|
95
|
+
throw new Error('tag name must be non-empty');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return tagName.toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function normalizeTagValue(tagName: string, tagValue: string): string {
|
|
102
|
+
return tagName === 't' ? tagValue.toLowerCase() : tagValue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function tagKeys(event: StoredNostrEvent): string[] {
|
|
106
|
+
return event.tags.flatMap((tag) => {
|
|
107
|
+
const [name, value] = tag;
|
|
108
|
+
if (!name || !value) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const normalizedName = name.toLowerCase();
|
|
113
|
+
const normalizedValue = normalizeTagValue(normalizedName, value);
|
|
114
|
+
return [`${normalizedName}:${normalizedValue}:${reverseTimestamp(event.created_at)}:${event.id}`];
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function tagPrefix(tagName: string, tagValue: string): string {
|
|
119
|
+
const normalizedName = normalizeTagName(tagName);
|
|
120
|
+
return `${normalizedName}:${normalizeTagValue(normalizedName, tagValue)}:`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function replaceableKey(pubkey: string, kind: number): string {
|
|
124
|
+
return `${pubkey}:${padKind(kind)}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function parameterizedReplaceableKey(pubkey: string, kind: number, dTag: string): string {
|
|
128
|
+
return `${pubkey}:${padKind(kind)}:${dTag}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function retainLatestReplaceableEvents(events: StoredNostrEvent[]): StoredNostrEvent[] {
|
|
132
|
+
const retained = new Map<string, StoredNostrEvent>();
|
|
133
|
+
const passthrough: StoredNostrEvent[] = [];
|
|
134
|
+
|
|
135
|
+
for (const event of events) {
|
|
136
|
+
let slot: string | null = null;
|
|
137
|
+
if (isReplaceableKind(event.kind)) {
|
|
138
|
+
slot = `replaceable:${replaceableKey(event.pubkey, event.kind)}`;
|
|
139
|
+
} else if (isParameterizedReplaceableKind(event.kind)) {
|
|
140
|
+
slot = `parameterized:${parameterizedReplaceableKey(event.pubkey, event.kind, getDTag(event) ?? '')}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!slot) {
|
|
144
|
+
passthrough.push(event);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const existing = retained.get(slot);
|
|
149
|
+
if (!existing || compareReplaceableEvents(event, existing) > 0) {
|
|
150
|
+
retained.set(slot, event);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return [...passthrough, ...retained.values()];
|
|
155
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { StoredNostrEvent } from './events.js';
|
|
2
|
+
|
|
3
|
+
export const HEX_64 = /^[0-9a-f]{64}$/;
|
|
4
|
+
export const HEX_128 = /^[0-9a-f]{128}$/;
|
|
5
|
+
|
|
6
|
+
export function assertStringArray(tags: unknown): asserts tags is string[][] {
|
|
7
|
+
if (!Array.isArray(tags)) {
|
|
8
|
+
throw new Error('Nostr event tags must be an array');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
for (const tag of tags) {
|
|
12
|
+
if (!Array.isArray(tag) || tag.some((value) => typeof value !== 'string')) {
|
|
13
|
+
throw new Error('Nostr event tags must be an array of string arrays');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validateHex64(value: string, label: string): string {
|
|
19
|
+
if (!HEX_64.test(value)) {
|
|
20
|
+
throw new Error(`${label} must be a lowercase 64-character hex string`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateHex128(value: string, label: string): string {
|
|
27
|
+
if (!HEX_128.test(value)) {
|
|
28
|
+
throw new Error(`${label} must be a lowercase 128-character hex string`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function validateCreatedAt(value: number): number {
|
|
35
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
36
|
+
throw new Error('created_at must be a non-negative integer');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateKind(value: number): number {
|
|
43
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
44
|
+
throw new Error('kind must be a non-negative integer');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function validateContent(value: string): string {
|
|
51
|
+
if (typeof value !== 'string') {
|
|
52
|
+
throw new Error('content must be a string');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateEventShape(event: StoredNostrEvent): StoredNostrEvent {
|
|
59
|
+
const normalized = {
|
|
60
|
+
id: validateHex64(event.id, 'event id'),
|
|
61
|
+
pubkey: validateHex64(event.pubkey, 'pubkey'),
|
|
62
|
+
created_at: validateCreatedAt(event.created_at),
|
|
63
|
+
kind: validateKind(event.kind),
|
|
64
|
+
tags: event.tags,
|
|
65
|
+
content: validateContent(event.content),
|
|
66
|
+
sig: validateHex128(event.sig, 'signature'),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
assertStringArray(normalized.tags);
|
|
70
|
+
return normalized;
|
|
71
|
+
}
|