@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.
Files changed (67) hide show
  1. package/README.md +11 -33
  2. package/dist/eventCollection.d.ts +1 -1
  3. package/dist/eventCollection.d.ts.map +1 -1
  4. package/dist/eventCollection.js +2 -7
  5. package/dist/eventCollection.js.map +1 -1
  6. package/dist/eventKeys.d.ts +1 -0
  7. package/dist/eventKeys.d.ts.map +1 -1
  8. package/dist/eventKeys.js +7 -1
  9. package/dist/eventKeys.js.map +1 -1
  10. package/dist/events.d.ts +2 -0
  11. package/dist/events.d.ts.map +1 -1
  12. package/dist/events.js +41 -61
  13. package/dist/events.js.map +1 -1
  14. package/dist/index.d.ts +4 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +10 -6
  17. package/dist/index.js.map +1 -1
  18. package/dist/resolver/nostr.d.ts.map +1 -1
  19. package/dist/resolver/nostr.js +17 -12
  20. package/dist/resolver/nostr.js.map +1 -1
  21. package/dist/snapshot.d.ts +5 -0
  22. package/dist/snapshot.d.ts.map +1 -1
  23. package/dist/snapshot.js +9 -2
  24. package/dist/snapshot.js.map +1 -1
  25. package/dist/treeEventSnapshots.d.ts.map +1 -1
  26. package/dist/treeEventSnapshots.js +6 -11
  27. package/dist/treeEventSnapshots.js.map +1 -1
  28. package/package.json +18 -14
  29. package/src/eventCollection.ts +189 -0
  30. package/src/eventKeys.ts +155 -0
  31. package/src/eventValidation.ts +71 -0
  32. package/src/events.ts +900 -0
  33. package/src/index.ts +122 -0
  34. package/src/replaceablePublish.ts +204 -0
  35. package/src/resolver/index.ts +14 -0
  36. package/src/resolver/nostr.ts +1210 -0
  37. package/src/snapshot.ts +213 -0
  38. package/src/treeEventSnapshotPermalinks.ts +150 -0
  39. package/src/treeEventSnapshots.ts +280 -0
  40. package/dist/webrtc/index.d.ts +0 -6
  41. package/dist/webrtc/index.d.ts.map +0 -1
  42. package/dist/webrtc/index.js +0 -7
  43. package/dist/webrtc/index.js.map +0 -1
  44. package/dist/webrtc/lruCache.d.ts +0 -20
  45. package/dist/webrtc/lruCache.d.ts.map +0 -1
  46. package/dist/webrtc/lruCache.js +0 -59
  47. package/dist/webrtc/lruCache.js.map +0 -1
  48. package/dist/webrtc/peer.d.ts +0 -133
  49. package/dist/webrtc/peer.d.ts.map +0 -1
  50. package/dist/webrtc/peer.js +0 -617
  51. package/dist/webrtc/peer.js.map +0 -1
  52. package/dist/webrtc/peerSelector.d.ts +0 -2
  53. package/dist/webrtc/peerSelector.d.ts.map +0 -1
  54. package/dist/webrtc/peerSelector.js +0 -2
  55. package/dist/webrtc/peerSelector.js.map +0 -1
  56. package/dist/webrtc/protocol.d.ts +0 -3
  57. package/dist/webrtc/protocol.d.ts.map +0 -1
  58. package/dist/webrtc/protocol.js +0 -2
  59. package/dist/webrtc/protocol.js.map +0 -1
  60. package/dist/webrtc/store.d.ts +0 -217
  61. package/dist/webrtc/store.d.ts.map +0 -1
  62. package/dist/webrtc/store.js +0 -1320
  63. package/dist/webrtc/store.js.map +0 -1
  64. package/dist/webrtc/types.d.ts +0 -101
  65. package/dist/webrtc/types.d.ts.map +0 -1
  66. package/dist/webrtc/types.js +0 -5
  67. package/dist/webrtc/types.js.map +0 -1
package/src/index.ts ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @hashtree/nostr - Nostr integration for hashtree
3
+ *
4
+ * Provides Nostr ref resolving and event storage. Blob transport lives in
5
+ * @hashtree/fips-transport; Nostr event distribution is independent of blob routing.
6
+ */
7
+
8
+ export const DEFAULT_RELAYS: string[] = [
9
+ 'wss://relay.damus.io',
10
+ 'wss://relay.primal.net',
11
+ 'wss://relay.nostr.band',
12
+ 'wss://temp.iris.to',
13
+ 'wss://relay.snort.social',
14
+ ];
15
+
16
+ // Ref resolvers
17
+ export {
18
+ createNostrRefResolver,
19
+ // Legacy alias
20
+ createNostrRefResolver as createNostrRootResolver,
21
+ type NostrRefResolverConfig,
22
+ // Legacy alias
23
+ type NostrRefResolverConfig as NostrRootResolverConfig,
24
+ type NostrEvent,
25
+ type NostrFilter,
26
+ type Nip19Like,
27
+ type VisibilityCallbacks,
28
+ type ParsedTreeVisibility,
29
+ } from './resolver/index.js';
30
+
31
+ // Event storage and indexing
32
+ export {
33
+ NOSTR_EVENT_ENVELOPE_VERSION,
34
+ NostrEventStore,
35
+ decodeStoredNostrEventMsgpack,
36
+ encodeStoredNostrEventMsgpack,
37
+ type StoredNostrEvent,
38
+ type NostrEventManifest,
39
+ type ListEventsOptions,
40
+ type NostrEventQuery,
41
+ type NostrEventQueryValue,
42
+ } from './events.js';
43
+
44
+ export {
45
+ COLLECTION_MANIFEST_METADATA_FILE,
46
+ DEFAULT_NOSTR_EVENT_COLLECTION_SOURCE_ID,
47
+ NOSTR_EVENT_ITEM_FORMAT,
48
+ NOSTR_EVENT_PROJECTION_FORMAT,
49
+ collectionManifestToNostrEventManifest,
50
+ createNostrEventCollectionDefinition,
51
+ createNostrEventCollectionSource,
52
+ createNostrEventCollectionWriter,
53
+ nostrEventCollectionManifestMetadata,
54
+ nostrEventCollectionRootMetadata,
55
+ nostrEventManifestToCollectionManifest,
56
+ } from './eventCollection.js';
57
+
58
+ export {
59
+ MANIFEST_BY_AUTHOR_KIND_TIME,
60
+ MANIFEST_BY_AUTHOR_TIME,
61
+ MANIFEST_BY_ID,
62
+ MANIFEST_BY_KIND_TIME,
63
+ MANIFEST_BY_KIND_TIME_AUTHOR,
64
+ MANIFEST_BY_TAG,
65
+ MANIFEST_BY_TIME,
66
+ MANIFEST_PARAMETERIZED_REPLACEABLE,
67
+ MANIFEST_REPLACEABLE,
68
+ parameterizedReplaceableKey,
69
+ replaceableKey,
70
+ kindTimeAuthorKey,
71
+ tagPrefix,
72
+ } from './eventKeys.js';
73
+
74
+ export {
75
+ encodeSignedNostrEventJson,
76
+ decodeSignedNostrEventJson,
77
+ storeSignedNostrEventSnapshot,
78
+ readSignedNostrEventSnapshot,
79
+ parseHashtreeRootEvent,
80
+ isHashtreeRootKind,
81
+ HASHTREE_LABEL,
82
+ HASHTREE_ROOT_KIND,
83
+ HASHTREE_LEGACY_ROOT_KIND,
84
+ HASHTREE_ROOT_KINDS,
85
+ type ParsedHashtreeRootEvent,
86
+ type SnapshotTreeLike,
87
+ type SnapshotTarget,
88
+ } from './snapshot.js';
89
+
90
+ export {
91
+ storeTreeEventSnapshot,
92
+ readTreeEventSnapshot,
93
+ fetchLatestTreeEventSnapshot,
94
+ watchLatestTreeEventSnapshot,
95
+ compareTreeEventSnapshots,
96
+ isNewerTreeEventSnapshot,
97
+ snapshotMatchesRootCid,
98
+ resolveSnapshotRootCid,
99
+ type TreeEventSnapshotInfo,
100
+ type TreeEventSnapshotQuery,
101
+ type FetchLatestTreeEventSnapshotConfig,
102
+ type WatchTreeEventSnapshotsConfig,
103
+ } from './treeEventSnapshots.js';
104
+
105
+ export {
106
+ buildTreeEventSnapshotPermalink,
107
+ parseTreeEventSnapshotPermalink,
108
+ normalizeTreeEventSnapshotLinkKey,
109
+ type TreeEventSnapshotPermalink,
110
+ type BuildTreeEventSnapshotPermalinkOptions,
111
+ } from './treeEventSnapshotPermalinks.js';
112
+
113
+ export {
114
+ createReplaceablePublishQueue,
115
+ replaceableEventCoordinateFromTemplate,
116
+ replaceableEventCoordinateKey,
117
+ type ReplaceableEventCoordinate,
118
+ type ReplaceableEventTemplateLike,
119
+ type ReplaceablePublishQueueConfig,
120
+ type ReplaceablePublishRequest,
121
+ type ReplaceablePublishOutcome,
122
+ } from './replaceablePublish.js';
@@ -0,0 +1,204 @@
1
+ import {
2
+ isParameterizedReplaceableKind,
3
+ isReplaceableKind,
4
+ parameterizedReplaceableKey,
5
+ replaceableKey,
6
+ } from './eventKeys.js';
7
+
8
+ export interface ReplaceableEventCoordinate {
9
+ pubkey: string;
10
+ kind: number;
11
+ dTag?: string;
12
+ }
13
+
14
+ export interface ReplaceableEventTemplateLike {
15
+ kind: number;
16
+ tags: string[][];
17
+ }
18
+
19
+ export interface ReplaceablePublishQueueConfig {
20
+ nowMs?: () => number;
21
+ sleep?: (ms: number) => Promise<void>;
22
+ }
23
+
24
+ export interface ReplaceablePublishRequest<TResult> {
25
+ coordinate: string | ReplaceableEventCoordinate;
26
+ publish: (createdAt: number) => Promise<TResult>;
27
+ }
28
+
29
+ export type ReplaceablePublishOutcome<TResult> =
30
+ | { status: 'published'; createdAt: number; result: TResult }
31
+ | { status: 'superseded' };
32
+
33
+ interface QueueEntry<TResult> {
34
+ publish: (createdAt: number) => Promise<TResult>;
35
+ resolve: (value: ReplaceablePublishOutcome<TResult>) => void;
36
+ reject: (reason?: unknown) => void;
37
+ }
38
+
39
+ interface PublishSlot {
40
+ active: boolean;
41
+ lastCreatedAt: number | null;
42
+ lastTouchedAtMs: number;
43
+ queued: QueueEntry<any> | null;
44
+ }
45
+
46
+ const IDLE_SLOT_RETENTION_MS = 60_000;
47
+
48
+ function sleepFor(ms: number): Promise<void> {
49
+ return new Promise((resolve) => {
50
+ setTimeout(resolve, ms);
51
+ });
52
+ }
53
+
54
+ function normalizeTagValue(value: unknown): string {
55
+ return typeof value === 'string' ? value : '';
56
+ }
57
+
58
+ function getDTag(tags: readonly string[][]): string | null {
59
+ for (const tag of tags) {
60
+ if (tag[0] !== 'd') {
61
+ continue;
62
+ }
63
+ const value = normalizeTagValue(tag[1]).trim();
64
+ if (value) {
65
+ return value;
66
+ }
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ export function replaceableEventCoordinateKey(coordinate: ReplaceableEventCoordinate): string {
73
+ if (isReplaceableKind(coordinate.kind)) {
74
+ return replaceableKey(coordinate.pubkey, coordinate.kind);
75
+ }
76
+
77
+ if (isParameterizedReplaceableKind(coordinate.kind)) {
78
+ const dTag = coordinate.dTag?.trim();
79
+ if (!dTag) {
80
+ throw new Error('Parameterized replaceable coordinates require a non-empty d tag');
81
+ }
82
+ return parameterizedReplaceableKey(coordinate.pubkey, coordinate.kind, dTag);
83
+ }
84
+
85
+ throw new Error(`Kind ${coordinate.kind} is not replaceable`);
86
+ }
87
+
88
+ export function replaceableEventCoordinateFromTemplate(
89
+ pubkey: string,
90
+ event: ReplaceableEventTemplateLike,
91
+ ): ReplaceableEventCoordinate {
92
+ if (isReplaceableKind(event.kind)) {
93
+ return { pubkey, kind: event.kind };
94
+ }
95
+
96
+ if (isParameterizedReplaceableKind(event.kind)) {
97
+ const dTag = getDTag(event.tags);
98
+ if (!dTag) {
99
+ throw new Error('Parameterized replaceable events require a non-empty d tag');
100
+ }
101
+ return { pubkey, kind: event.kind, dTag };
102
+ }
103
+
104
+ throw new Error(`Kind ${event.kind} is not replaceable`);
105
+ }
106
+
107
+ async function waitForSlot(options: {
108
+ nowMs: () => number;
109
+ sleep: (ms: number) => Promise<void>;
110
+ slot: PublishSlot;
111
+ }): Promise<number> {
112
+ const { nowMs, sleep, slot } = options;
113
+ while (slot.lastCreatedAt !== null && Math.floor(nowMs() / 1_000) <= slot.lastCreatedAt) {
114
+ const earliestNextDispatchMs = (slot.lastCreatedAt + 1) * 1_000;
115
+ await sleep(Math.max(1, earliestNextDispatchMs - nowMs()));
116
+ }
117
+
118
+ const dispatchAtMs = nowMs();
119
+ const createdAt = Math.floor(dispatchAtMs / 1_000);
120
+ slot.lastCreatedAt = createdAt;
121
+ return createdAt;
122
+ }
123
+
124
+ export function createReplaceablePublishQueue(config: ReplaceablePublishQueueConfig = {}) {
125
+ const nowMs = config.nowMs ?? (() => Date.now());
126
+ const sleep = config.sleep ?? sleepFor;
127
+ const slots = new Map<string, PublishSlot>();
128
+
129
+ function pruneIdleSlots(): void {
130
+ const now = nowMs();
131
+ for (const [key, slot] of slots) {
132
+ if (slot.active || slot.queued) {
133
+ continue;
134
+ }
135
+ if (now - slot.lastTouchedAtMs > IDLE_SLOT_RETENTION_MS) {
136
+ slots.delete(key);
137
+ }
138
+ }
139
+ }
140
+
141
+ async function drain(key: string, slot: PublishSlot): Promise<void> {
142
+ try {
143
+ while (slot.queued) {
144
+ const entry = slot.queued;
145
+ slot.queued = null;
146
+
147
+ const createdAt = await waitForSlot({ nowMs, sleep, slot });
148
+ slot.lastTouchedAtMs = nowMs();
149
+ try {
150
+ const result = await entry.publish(createdAt);
151
+ entry.resolve({ status: 'published', createdAt, result });
152
+ } catch (error) {
153
+ entry.reject(error);
154
+ }
155
+ }
156
+ } finally {
157
+ slot.active = false;
158
+ if (slot.queued) {
159
+ slot.active = true;
160
+ void drain(key, slot);
161
+ return;
162
+ }
163
+ slot.lastTouchedAtMs = nowMs();
164
+ }
165
+ }
166
+
167
+ return {
168
+ publish<TResult>(request: ReplaceablePublishRequest<TResult>): Promise<ReplaceablePublishOutcome<TResult>> {
169
+ pruneIdleSlots();
170
+ const key = typeof request.coordinate === 'string'
171
+ ? request.coordinate
172
+ : replaceableEventCoordinateKey(request.coordinate);
173
+ let slot = slots.get(key);
174
+ if (!slot) {
175
+ slot = {
176
+ active: false,
177
+ lastCreatedAt: null,
178
+ lastTouchedAtMs: nowMs(),
179
+ queued: null,
180
+ };
181
+ slots.set(key, slot);
182
+ }
183
+
184
+ return new Promise<ReplaceablePublishOutcome<TResult>>((resolve, reject) => {
185
+ const queuedEntry = slot!.queued as QueueEntry<TResult> | null;
186
+ if (queuedEntry) {
187
+ queuedEntry.resolve({ status: 'superseded' });
188
+ }
189
+
190
+ slot!.lastTouchedAtMs = nowMs();
191
+ slot!.queued = {
192
+ publish: request.publish,
193
+ resolve,
194
+ reject,
195
+ };
196
+
197
+ if (!slot!.active) {
198
+ slot!.active = true;
199
+ void drain(key, slot!);
200
+ }
201
+ });
202
+ },
203
+ };
204
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Ref resolvers - map human-readable keys to merkle root hashes (refs)
3
+ */
4
+ export {
5
+ createNostrRefResolver,
6
+ type NostrRefResolverConfig,
7
+ type VisibilityCallbacks,
8
+ type ParsedTreeVisibility,
9
+ // Legacy aliases
10
+ type NostrRefResolverConfig as NostrRootResolverConfig,
11
+ type NostrEvent,
12
+ type NostrFilter,
13
+ type Nip19Like,
14
+ } from './nostr.js';