@bzzsocial/ndk 0.0.0-stage → 3.0.3-bzz.7

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 (245) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -2
  3. package/dist/index.d.mts +7392 -0
  4. package/dist/index.d.ts +7392 -0
  5. package/dist/index.js +15372 -0
  6. package/dist/index.mjs +15195 -0
  7. package/dist/test/index.d.mts +4584 -0
  8. package/dist/test/index.d.ts +4584 -0
  9. package/dist/test/index.js +13744 -0
  10. package/dist/test/index.mjs +13712 -0
  11. package/dist/workers/sig-verification.d.mts +2 -0
  12. package/dist/workers/sig-verification.d.ts +2 -0
  13. package/dist/workers/sig-verification.js +38 -0
  14. package/dist/workers/sig-verification.mjs +36 -0
  15. package/package.json +72 -4
  16. package/src/ai-guardrails/event/publishing.test.ts +176 -0
  17. package/src/ai-guardrails/event/signing.test.ts +124 -0
  18. package/src/ai-guardrails/event/signing.ts +225 -0
  19. package/src/ai-guardrails/index.ts +364 -0
  20. package/src/ai-guardrails/ndk/fetch-events.test.ts +257 -0
  21. package/src/ai-guardrails/ndk/fetch-events.ts +166 -0
  22. package/src/ai-guardrails/ndk.test.ts +64 -0
  23. package/src/ai-guardrails/ndk.ts +34 -0
  24. package/src/ai-guardrails/subscription/deprecated-handlers.test.ts +105 -0
  25. package/src/ai-guardrails/subscription/deprecated-handlers.ts +75 -0
  26. package/src/ai-guardrails/types.ts +69 -0
  27. package/src/app-settings/index.ts +71 -0
  28. package/src/cache/filter.test.ts +139 -0
  29. package/src/cache/filter.ts +78 -0
  30. package/src/cache/index.ts +474 -0
  31. package/src/count/index.test.ts +247 -0
  32. package/src/count/index.ts +213 -0
  33. package/src/dvm/schedule.ts +132 -0
  34. package/src/events/content-tagger.test.ts +284 -0
  35. package/src/events/content-tagger.ts +249 -0
  36. package/src/events/dedup.ts +14 -0
  37. package/src/events/encode.test.ts +35 -0
  38. package/src/events/encryption.test.ts +484 -0
  39. package/src/events/encryption.ts +106 -0
  40. package/src/events/fetch-tagged-event.ts +44 -0
  41. package/src/events/gift-wrapping.test.ts +139 -0
  42. package/src/events/gift-wrapping.ts +155 -0
  43. package/src/events/index.test.ts +863 -0
  44. package/src/events/index.ts +1130 -0
  45. package/src/events/kind.ts +20 -0
  46. package/src/events/kinds/article.ts +152 -0
  47. package/src/events/kinds/blossom-list.ts +91 -0
  48. package/src/events/kinds/cashu/fedimint.ts +95 -0
  49. package/src/events/kinds/cashu/mint-recommendation.ts +95 -0
  50. package/src/events/kinds/cashu/mint.ts +93 -0
  51. package/src/events/kinds/cashu/token.ts +155 -0
  52. package/src/events/kinds/cashu/tx.test.ts +44 -0
  53. package/src/events/kinds/cashu/tx.ts +190 -0
  54. package/src/events/kinds/classified.ts +182 -0
  55. package/src/events/kinds/collaborative.test.ts +453 -0
  56. package/src/events/kinds/collaborative.ts +346 -0
  57. package/src/events/kinds/drafts.ts +152 -0
  58. package/src/events/kinds/dvm/NDKTranscriptionDVM.ts +69 -0
  59. package/src/events/kinds/dvm/feedback.ts +51 -0
  60. package/src/events/kinds/dvm/index.ts +6 -0
  61. package/src/events/kinds/dvm/request.ts +131 -0
  62. package/src/events/kinds/dvm/result.ts +93 -0
  63. package/src/events/kinds/follow-pack.test.ts +169 -0
  64. package/src/events/kinds/follow-pack.ts +123 -0
  65. package/src/events/kinds/highlight.ts +116 -0
  66. package/src/events/kinds/image.ts +51 -0
  67. package/src/events/kinds/index.ts +224 -0
  68. package/src/events/kinds/interest-list.test.ts +171 -0
  69. package/src/events/kinds/interest-list.ts +82 -0
  70. package/src/events/kinds/lists/index.test.ts +51 -0
  71. package/src/events/kinds/lists/index.ts +415 -0
  72. package/src/events/kinds/nip89/app-handler.test.ts +106 -0
  73. package/src/events/kinds/nip89/app-handler.ts +83 -0
  74. package/src/events/kinds/nutzap/index.test.ts +192 -0
  75. package/src/events/kinds/nutzap/index.ts +384 -0
  76. package/src/events/kinds/nutzap/mint-list.ts +76 -0
  77. package/src/events/kinds/nutzap/proof.ts +18 -0
  78. package/src/events/kinds/nutzap/validation.test.ts +340 -0
  79. package/src/events/kinds/nutzap/validation.ts +79 -0
  80. package/src/events/kinds/p2p-order.test.ts +359 -0
  81. package/src/events/kinds/p2p-order.ts +469 -0
  82. package/src/events/kinds/project-template.ts +132 -0
  83. package/src/events/kinds/project.ts +114 -0
  84. package/src/events/kinds/relay-feed-list.example.ts +86 -0
  85. package/src/events/kinds/relay-feed-list.test.ts +97 -0
  86. package/src/events/kinds/relay-feed-list.ts +108 -0
  87. package/src/events/kinds/relay-list.ts +118 -0
  88. package/src/events/kinds/repost.ts +67 -0
  89. package/src/events/kinds/simple-group/index.ts +234 -0
  90. package/src/events/kinds/simple-group/member-list.ts +43 -0
  91. package/src/events/kinds/simple-group/metadata.ts +64 -0
  92. package/src/events/kinds/story.test.ts +141 -0
  93. package/src/events/kinds/story.ts +458 -0
  94. package/src/events/kinds/subscriptions/amount.ts +61 -0
  95. package/src/events/kinds/subscriptions/receipt.ts +148 -0
  96. package/src/events/kinds/subscriptions/subscription-start.ts +149 -0
  97. package/src/events/kinds/subscriptions/tier.test.ts +76 -0
  98. package/src/events/kinds/subscriptions/tier.ts +121 -0
  99. package/src/events/kinds/task.ts +42 -0
  100. package/src/events/kinds/thread.ts +45 -0
  101. package/src/events/kinds/video.ts +140 -0
  102. package/src/events/kinds/voice-message.ts +257 -0
  103. package/src/events/kinds/wiki.ts +88 -0
  104. package/src/events/kinds/zap.ts +115 -0
  105. package/src/events/nip10.test.ts +108 -0
  106. package/src/events/nip10.ts +75 -0
  107. package/src/events/nip19.test.ts +49 -0
  108. package/src/events/nip19.ts +36 -0
  109. package/src/events/nip73.ts +13 -0
  110. package/src/events/repost.test.ts +40 -0
  111. package/src/events/repost.ts +44 -0
  112. package/src/events/serializer.test.ts +174 -0
  113. package/src/events/serializer.ts +120 -0
  114. package/src/events/signature.ts +97 -0
  115. package/src/events/signed-event-types.test.ts +149 -0
  116. package/src/events/validation.ts +113 -0
  117. package/src/events/wrap.test.ts +164 -0
  118. package/src/events/wrap.ts +128 -0
  119. package/src/index.ts +103 -0
  120. package/src/light-bolt11-decoder.d.ts +4 -0
  121. package/src/ndk/active-user.test.ts +89 -0
  122. package/src/ndk/active-user.ts +41 -0
  123. package/src/ndk/entity.ts +31 -0
  124. package/src/ndk/fetch-event-from-tag.ts +156 -0
  125. package/src/ndk/fetchEvent-guardrails.test.ts +208 -0
  126. package/src/ndk/fetchEvent-invalid-filter.test.ts +80 -0
  127. package/src/ndk/fetchUser.test.ts +149 -0
  128. package/src/ndk/guardrailOff.test.ts +75 -0
  129. package/src/ndk/index.ts +1249 -0
  130. package/src/ndk/nip19.test.ts +122 -0
  131. package/src/ndk/queue/index.ts +89 -0
  132. package/src/nip19/index.ts +5 -0
  133. package/src/nip49/index.test.ts +100 -0
  134. package/src/nip49/index.ts +5 -0
  135. package/src/outbox/index.ts +128 -0
  136. package/src/outbox/read/with-authors.ts +24 -0
  137. package/src/outbox/relay-ranking.ts +27 -0
  138. package/src/outbox/tracker.test.ts +68 -0
  139. package/src/outbox/tracker.ts +185 -0
  140. package/src/outbox/write.ts +37 -0
  141. package/src/relay/auth-on-demand.test.ts +197 -0
  142. package/src/relay/auth-policies.test.ts +19 -0
  143. package/src/relay/auth-policies.ts +109 -0
  144. package/src/relay/auth-retry.test.ts +241 -0
  145. package/src/relay/connections.test.ts +231 -0
  146. package/src/relay/connectivity.test.ts +370 -0
  147. package/src/relay/connectivity.ts +1106 -0
  148. package/src/relay/index.test.ts +167 -0
  149. package/src/relay/index.ts +446 -0
  150. package/src/relay/keepalive-bun.test.ts +86 -0
  151. package/src/relay/keepalive.test.ts +196 -0
  152. package/src/relay/keepalive.ts +118 -0
  153. package/src/relay/nip11.test.ts +196 -0
  154. package/src/relay/nip11.ts +85 -0
  155. package/src/relay/nip42.test.ts +269 -0
  156. package/src/relay/pool/index.test.ts +183 -0
  157. package/src/relay/pool/index.ts +537 -0
  158. package/src/relay/pool/system-disconnection.test.ts +280 -0
  159. package/src/relay/publisher.ts +114 -0
  160. package/src/relay/reconnection-backoff.test.ts +92 -0
  161. package/src/relay/reconnection-integration.bun.test.ts +112 -0
  162. package/src/relay/score.ts +2 -0
  163. package/src/relay/sets/calculate.test.ts +220 -0
  164. package/src/relay/sets/calculate.ts +203 -0
  165. package/src/relay/sets/index.ts +416 -0
  166. package/src/relay/sets/publish.test.ts +231 -0
  167. package/src/relay/sets/utils.ts +29 -0
  168. package/src/relay/signature-verification-stats.ts +150 -0
  169. package/src/relay/signature-verification.test.ts +192 -0
  170. package/src/relay/simple-verification.test.ts +13 -0
  171. package/src/relay/sub-manager.ts +81 -0
  172. package/src/relay/subscription.test.ts +166 -0
  173. package/src/relay/subscription.ts +542 -0
  174. package/src/relay/validation-ratio.test.ts +37 -0
  175. package/src/signers/deserialization.ts +39 -0
  176. package/src/signers/index.ts +97 -0
  177. package/src/signers/nip07/index.test.ts +32 -0
  178. package/src/signers/nip07/index.ts +302 -0
  179. package/src/signers/nip46/backend/connect.ts +41 -0
  180. package/src/signers/nip46/backend/get-public-key.ts +12 -0
  181. package/src/signers/nip46/backend/index.test.ts +340 -0
  182. package/src/signers/nip46/backend/index.ts +232 -0
  183. package/src/signers/nip46/backend/nip04-decrypt.ts +39 -0
  184. package/src/signers/nip46/backend/nip04-encrypt.ts +39 -0
  185. package/src/signers/nip46/backend/nip44-decrypt.ts +39 -0
  186. package/src/signers/nip46/backend/nip44-encrypt.ts +39 -0
  187. package/src/signers/nip46/backend/ping.ts +25 -0
  188. package/src/signers/nip46/backend/sign-event.ts +48 -0
  189. package/src/signers/nip46/backend/switch-relays.ts +28 -0
  190. package/src/signers/nip46/index.test.ts +560 -0
  191. package/src/signers/nip46/index.ts +640 -0
  192. package/src/signers/nip46/nostrconnect.ts +50 -0
  193. package/src/signers/nip46/restore.test.ts +44 -0
  194. package/src/signers/nip46/rpc.ts +221 -0
  195. package/src/signers/private-key/index.test.ts +151 -0
  196. package/src/signers/private-key/index.ts +241 -0
  197. package/src/signers/registry.ts +14 -0
  198. package/src/signers/serialization.test.ts +212 -0
  199. package/src/signers/types.ts +7 -0
  200. package/src/subscription/eose.test.ts +92 -0
  201. package/src/subscription/event-dedup.test.ts +445 -0
  202. package/src/subscription/exclusive-relay.test.ts +444 -0
  203. package/src/subscription/future-timestamp-grace.test.ts +276 -0
  204. package/src/subscription/grouping.test.ts +71 -0
  205. package/src/subscription/grouping.ts +76 -0
  206. package/src/subscription/index.test.ts +113 -0
  207. package/src/subscription/index.ts +1136 -0
  208. package/src/subscription/manager.test.ts +64 -0
  209. package/src/subscription/manager.ts +121 -0
  210. package/src/subscription/outbox-late-arrival.test.ts +172 -0
  211. package/src/subscription/utils/format-filters.ts +57 -0
  212. package/src/subscription/utils.test.ts +91 -0
  213. package/src/subscription/utils.ts +276 -0
  214. package/src/subscription/verify-before-emit.test.ts +108 -0
  215. package/src/subscription.test.ts +175 -0
  216. package/src/test-relay-autoconnect.ts +133 -0
  217. package/src/thread/index.test.ts +267 -0
  218. package/src/thread/index.ts +242 -0
  219. package/src/types.ts +45 -0
  220. package/src/user/follows.test.ts +58 -0
  221. package/src/user/follows.ts +45 -0
  222. package/src/user/index.test.ts +365 -0
  223. package/src/user/index.ts +465 -0
  224. package/src/user/nip05.test.ts +39 -0
  225. package/src/user/nip05.ts +107 -0
  226. package/src/user/pin.ts +45 -0
  227. package/src/user/profile.ts +112 -0
  228. package/src/utils/filter-validation.test.ts +925 -0
  229. package/src/utils/filter-validation.ts +436 -0
  230. package/src/utils/filter.ts +41 -0
  231. package/src/utils/get-users-relay-list.ts +171 -0
  232. package/src/utils/imeta.test.ts +100 -0
  233. package/src/utils/imeta.ts +74 -0
  234. package/src/utils/index.ts +5 -0
  235. package/src/utils/normalize-url.ts +352 -0
  236. package/src/utils/timeout.ts +15 -0
  237. package/src/utils/validation.ts +86 -0
  238. package/src/workers/sig-verification.ts +54 -0
  239. package/src/zap/invoice.ts +111 -0
  240. package/src/zapper/index.test.ts +151 -0
  241. package/src/zapper/index.ts +701 -0
  242. package/src/zapper/ln.ts +84 -0
  243. package/src/zapper/nip57.test.ts +329 -0
  244. package/src/zapper/nip57.ts +85 -0
  245. package/src/zapper/nip61.ts +57 -0
@@ -0,0 +1,156 @@
1
+ import type { NDKEvent, NDKTag } from "../events";
2
+ import { getRelaysForSync } from "../outbox/write";
3
+ import { NDKRelaySet } from "../relay/sets";
4
+ import { calculateRelaySetsFromFilters } from "../relay/sets/calculate";
5
+ import type { NDKSubscriptionOptions } from "../subscription";
6
+ import type { NDK } from ".";
7
+
8
+ /**
9
+ * Options on how to handle when a relay hint doesn't respond
10
+ * with the requested event.
11
+ *
12
+ * When a tag includes a relay hint, and the relay hint doesn't come back
13
+ * with the event, the fallback options are used to try to fetch the event
14
+ * from somewhere else.
15
+ */
16
+ export type NDKFetchFallbackOptions = {
17
+ /**
18
+ * Relay set to use as a fallback when the hint relay doesn't respond.
19
+ * If not provided, the normal NDK calculation is used (whether explicit relays or outbox calculation)
20
+ * Default is `undefined`.
21
+ */
22
+ relaySet?: NDKRelaySet;
23
+
24
+ /**
25
+ * Type of fallback to use when the hint relay doesn't respond.
26
+ * - "timeout" will wait for a timeout before falling back
27
+ * - "eose" will wait for the EOSE before falling back
28
+ * - "none" will not fall back
29
+ * Default is "timeout".
30
+ */
31
+ type: "timeout" | "eose" | "none";
32
+
33
+ /**
34
+ * Timeout in milliseconds for the fallback relay.
35
+ * Default is 1500ms.
36
+ */
37
+ timeout?: number;
38
+ };
39
+
40
+ function isValidHint(hint: string | undefined) {
41
+ if (!hint || hint === "") return false;
42
+
43
+ // Check if the hint is a valid URL
44
+ try {
45
+ new URL(hint);
46
+ return true;
47
+ } catch (_e) {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ function isRelayHintConnected(ndk: NDK, hint: string | undefined) {
53
+ if (!isValidHint(hint)) return false;
54
+
55
+ return ndk.pool.isRelayConnected(hint!);
56
+ }
57
+
58
+ /**
59
+ * @ignore
60
+ */
61
+ export async function fetchEventFromTag(
62
+ this: NDK,
63
+ tag: NDKTag,
64
+ originalEvent: NDKEvent,
65
+ subOpts?: NDKSubscriptionOptions,
66
+ fallback: NDKFetchFallbackOptions = {
67
+ type: "timeout",
68
+ },
69
+ ) {
70
+ const d = this.debug.extend("fetch-event-from-tag");
71
+ const [_, id, hint] = tag;
72
+
73
+ // If we are supposed to stick to the cache, just go with that
74
+ // if (subOpts?.cacheUsage === NDKSubscriptionCacheUsage.ONLY_CACHE) {
75
+ // return this.fetchEvent(id, subOpts);
76
+ // }
77
+
78
+ // XXXXX
79
+ subOpts = {};
80
+
81
+ d("fetching event from tag", tag, subOpts, fallback);
82
+
83
+ // If we are connected to the relay hint, try exclusively from that relay
84
+ // if (isRelayHintConnected(this, hint)) {
85
+ // d("fetching event from connected relay hint (%s)", normalizeRelayUrl(hint));
86
+ // let event = await this.fetchEvent(id, subOpts, this.pool.getRelay(hint));
87
+ // if (event) return event;
88
+ // }
89
+
90
+ // Check if we have a relay list for the author of the original event
91
+ // and prefer to use those relays
92
+ const authorRelays = getRelaysForSync(this, originalEvent.pubkey);
93
+ if (authorRelays && authorRelays.size > 0) {
94
+ d("fetching event from author relays %o", Array.from(authorRelays));
95
+ const relaySet = NDKRelaySet.fromRelayUrls(Array.from(authorRelays), this);
96
+ const event = await this.fetchEvent(id, subOpts, relaySet);
97
+ if (event) return event;
98
+ } else {
99
+ d("no author relays found for %s", originalEvent.pubkey, originalEvent);
100
+ }
101
+
102
+ // Attempt without relay hint on whatever NDK calculates
103
+ const relaySet = calculateRelaySetsFromFilters(this, [{ ids: [id] }], this.pool);
104
+ d("fetching event without relay hint", relaySet);
105
+ const event = await this.fetchEvent(id, subOpts);
106
+ if (event) return event;
107
+
108
+ // If we didn't get the event, try to fetch in the relay hint
109
+ if (hint && hint !== "") {
110
+ const event = await this.fetchEvent(id, subOpts, this.pool.getRelay(hint, true, true, [{ ids: [id] }]));
111
+ if (event) return event;
112
+ }
113
+
114
+ let result: NDKEvent | null | undefined;
115
+
116
+ const relay = isValidHint(hint) ? this.pool.getRelay(hint, false, true, [{ ids: [id] }]) : undefined;
117
+
118
+ const fetchMaybeWithRelayHint = new Promise<NDKEvent | null>((resolve) => {
119
+ this.fetchEvent(id, subOpts, relay).then(resolve);
120
+ });
121
+
122
+ // if we don't have a relay hint we don't need to setup a fallback
123
+ if (!isValidHint(hint) || fallback.type === "none") {
124
+ return fetchMaybeWithRelayHint;
125
+ }
126
+
127
+ /**
128
+ * Fallback fetch promise.
129
+ */
130
+ const fallbackFetchPromise = new Promise<NDKEvent | null>(async (resolve) => {
131
+ const fallbackRelaySet = fallback.relaySet;
132
+
133
+ const timeout = fallback.timeout ?? 1500;
134
+ const timeoutPromise = new Promise<void>((resolve) => setTimeout(resolve, timeout));
135
+
136
+ // if this is a timeout fallback, we need to wait for the timeout to resolve
137
+ if (fallback.type === "timeout") await timeoutPromise;
138
+
139
+ if (result) {
140
+ resolve(result);
141
+ } else {
142
+ d("fallback fetch triggered");
143
+ const fallbackEvent = await this.fetchEvent(id, subOpts, fallbackRelaySet);
144
+ resolve(fallbackEvent);
145
+ }
146
+ });
147
+
148
+ switch (fallback.type) {
149
+ case "timeout":
150
+ return Promise.race([fetchMaybeWithRelayHint, fallbackFetchPromise]);
151
+ case "eose":
152
+ result = await fetchMaybeWithRelayHint;
153
+ if (result) return result;
154
+ return fallbackFetchPromise;
155
+ }
156
+ }
@@ -0,0 +1,208 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { NDK } from "./index.js";
3
+
4
+ describe("fetchEvent guardrails", () => {
5
+ it("should warn when fetchEvent is called with a decoded naddr filter", async () => {
6
+ const ndk = new NDK({
7
+ explicitRelayUrls: ["wss://relay.primal.net"],
8
+ aiGuardrails: true,
9
+ });
10
+
11
+ const naddrFilter = {
12
+ kinds: [30023],
13
+ authors: ["fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],
14
+ "#d": ["identifier"],
15
+ };
16
+
17
+ try {
18
+ await ndk.fetchEvent(naddrFilter);
19
+ throw new Error("Should have thrown a guardrails error");
20
+ } catch (error: any) {
21
+ expect(error.message).toContain("For fetching a NIP-33 addressable event");
22
+ expect(error.message).toContain("use fetchEvent() with the naddr directly");
23
+ expect(error.message).toContain("nip19.decode(naddr)");
24
+ }
25
+ });
26
+
27
+ it("should warn when fetchEvent is called with a single ID filter", async () => {
28
+ const ndk = new NDK({
29
+ explicitRelayUrls: ["wss://relay.primal.net"],
30
+ aiGuardrails: true,
31
+ });
32
+
33
+ const idFilter = {
34
+ ids: ["abc123"],
35
+ };
36
+
37
+ try {
38
+ await ndk.fetchEvent(idFilter);
39
+ throw new Error("Should have thrown a guardrails error");
40
+ } catch (error: any) {
41
+ expect(error.message).toContain("For fetching a single event");
42
+ expect(error.message).toContain("use fetchEvent() instead");
43
+ expect(error.message).toContain("fetchEvent(eventId)");
44
+ }
45
+ });
46
+
47
+ it("should NOT warn when fetchEvent is called with a filter that is NOT a single event and ratio threshold not exceeded", async () => {
48
+ const ndk = new NDK({
49
+ explicitRelayUrls: ["wss://relay.primal.net"],
50
+ aiGuardrails: true,
51
+ });
52
+
53
+ // This is a filter for multiple events - should not trigger warnings
54
+ // because the ratio threshold hasn't been exceeded yet
55
+ try {
56
+ await Promise.race([
57
+ ndk.fetchEvent(
58
+ {
59
+ kinds: [1],
60
+ limit: 10,
61
+ },
62
+ { closeOnEose: true },
63
+ ),
64
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 100)),
65
+ ]);
66
+ } catch (error: any) {
67
+ // Should timeout, not throw a guardrails error
68
+ if (error.message !== "timeout") {
69
+ expect(error?.message || "").not.toContain("AI_GUARDRAILS");
70
+ }
71
+ }
72
+ });
73
+
74
+ it("should allow skipping the guardrail check", async () => {
75
+ const ndk = new NDK({
76
+ explicitRelayUrls: ["wss://relay.primal.net"],
77
+ aiGuardrails: { skip: new Set(["fetch-events-usage"]) },
78
+ });
79
+
80
+ const naddrFilter = {
81
+ kinds: [30023],
82
+ authors: ["fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],
83
+ "#d": ["identifier"],
84
+ };
85
+
86
+ // Should not throw because we skipped the check
87
+ // Race against timeout to avoid network connection
88
+ try {
89
+ await Promise.race([
90
+ ndk.fetchEvent(naddrFilter, { closeOnEose: true }),
91
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 100)),
92
+ ]);
93
+ } catch (error: any) {
94
+ if (error.message !== "timeout") {
95
+ // If it errors, it should NOT be a guardrails error
96
+ expect(error?.message || "").not.toContain("AI_GUARDRAILS");
97
+ }
98
+ }
99
+ });
100
+
101
+ it("should disable all guardrails for one call with guardrailOff()", async () => {
102
+ const ndk = new NDK({
103
+ explicitRelayUrls: ["wss://relay.primal.net"],
104
+ aiGuardrails: true,
105
+ });
106
+
107
+ const naddrFilter = {
108
+ kinds: [30023],
109
+ authors: ["fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],
110
+ "#d": ["identifier"],
111
+ };
112
+
113
+ // Test that guardrailOff() suppresses the error
114
+ let errorThrown = false;
115
+ try {
116
+ // This would normally throw immediately due to guardrails
117
+ // We wrap in a promise race to avoid waiting for network
118
+ await Promise.race([
119
+ ndk.guardrailOff().fetchEvent(naddrFilter, { closeOnEose: true }),
120
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 100)),
121
+ ]);
122
+ } catch (error: any) {
123
+ if (error.message !== "timeout") {
124
+ errorThrown = true;
125
+ // If it errors, it should NOT be a guardrails error
126
+ expect(error?.message || "").not.toContain("AI_GUARDRAILS");
127
+ }
128
+ }
129
+
130
+ // Verify that the next call DOES trigger the guardrail
131
+ try {
132
+ await ndk.fetchEvent(naddrFilter);
133
+ throw new Error("Should have thrown a guardrails error");
134
+ } catch (error: any) {
135
+ expect(error.message).toContain("AI_GUARDRAILS");
136
+ }
137
+ });
138
+
139
+ it("should disable specific guardrail for one call with guardrailOff('id')", async () => {
140
+ const ndk = new NDK({
141
+ explicitRelayUrls: ["wss://relay.primal.net"],
142
+ aiGuardrails: true,
143
+ });
144
+
145
+ const naddrFilter = {
146
+ kinds: [30023],
147
+ authors: ["fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],
148
+ "#d": ["identifier"],
149
+ };
150
+
151
+ // Test that guardrailOff('id') suppresses the specific error
152
+ try {
153
+ await Promise.race([
154
+ ndk.guardrailOff("fetch-events-usage").fetchEvent(naddrFilter, { closeOnEose: true }),
155
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 100)),
156
+ ]);
157
+ } catch (error: any) {
158
+ if (error.message !== "timeout") {
159
+ // If it errors, it should NOT be a guardrails error
160
+ expect(error?.message || "").not.toContain("AI_GUARDRAILS");
161
+ }
162
+ }
163
+
164
+ // Verify that the next call DOES trigger the guardrail
165
+ try {
166
+ await ndk.fetchEvent(naddrFilter);
167
+ throw new Error("Should have thrown a guardrails error");
168
+ } catch (error: any) {
169
+ expect(error.message).toContain("AI_GUARDRAILS");
170
+ }
171
+ });
172
+
173
+ it("should disable multiple guardrails for one call with guardrailOff(['id1', 'id2'])", async () => {
174
+ const ndk = new NDK({
175
+ explicitRelayUrls: ["wss://relay.primal.net"],
176
+ aiGuardrails: true,
177
+ });
178
+
179
+ const naddrFilter = {
180
+ kinds: [30023],
181
+ authors: ["fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],
182
+ "#d": ["identifier"],
183
+ };
184
+
185
+ // Test that guardrailOff(['id1', 'id2']) suppresses the errors
186
+ try {
187
+ await Promise.race([
188
+ ndk
189
+ .guardrailOff(["fetch-events-usage", "some-other-check"])
190
+ .fetchEvent(naddrFilter, { closeOnEose: true }),
191
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 100)),
192
+ ]);
193
+ } catch (error: any) {
194
+ if (error.message !== "timeout") {
195
+ // If it errors, it should NOT be a guardrails error
196
+ expect(error?.message || "").not.toContain("AI_GUARDRAILS");
197
+ }
198
+ }
199
+
200
+ // Verify that the next call DOES trigger the guardrail
201
+ try {
202
+ await ndk.fetchEvent(naddrFilter);
203
+ throw new Error("Should have thrown a guardrails error");
204
+ } catch (error: any) {
205
+ expect(error.message).toContain("AI_GUARDRAILS");
206
+ }
207
+ });
208
+ });
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { NDK } from "./index.js";
3
+
4
+ describe("fetchEvent with invalid filter (issue #357)", () => {
5
+ it("should not crash with ReferenceError when subscribe throws", async () => {
6
+ const ndk = new NDK({
7
+ explicitRelayUrls: ["wss://relay.example.com"],
8
+ });
9
+
10
+ // Mock subscribe to throw, simulating the scenario where
11
+ // an invalid filter causes subscribe to fail before assignment.
12
+ // The timeout callback must use optional chaining (s?.stop())
13
+ // to avoid crashing on the unassigned variable.
14
+ const subscribeSpy = vi.spyOn(ndk, "subscribe").mockImplementation(() => {
15
+ throw new Error("Invalid filter caused subscribe to fail");
16
+ });
17
+
18
+ try {
19
+ await ndk.fetchEvent({ authors: ["not-a-valid-hex"] });
20
+ } catch (error: any) {
21
+ // Should get the subscribe error, NOT a ReferenceError about 's'
22
+ expect(error).not.toBeInstanceOf(ReferenceError);
23
+ expect(error.message).not.toContain("Cannot access");
24
+ expect(error.message).toContain("Invalid filter caused subscribe to fail");
25
+ }
26
+
27
+ subscribeSpy.mockRestore();
28
+ });
29
+
30
+ it("should not crash when fetchEvent is called with a malformed hex ID string", async () => {
31
+ const ndk = new NDK({
32
+ explicitRelayUrls: ["wss://relay.example.com"],
33
+ });
34
+
35
+ // fetchEvent with a bad string should not throw a ReferenceError.
36
+ // It may timeout or fail for other reasons, but the 's' variable
37
+ // must be safely handled via optional chaining.
38
+ let caughtError: Error | undefined;
39
+ try {
40
+ await Promise.race([
41
+ ndk.fetchEvent("zzz-invalid-id"),
42
+ new Promise((_, reject) =>
43
+ setTimeout(() => reject(new Error("timeout")), 200),
44
+ ),
45
+ ]);
46
+ } catch (error: any) {
47
+ caughtError = error;
48
+ }
49
+
50
+ // Must not be a ReferenceError about 's'
51
+ if (caughtError) {
52
+ expect(caughtError).not.toBeInstanceOf(ReferenceError);
53
+ expect(caughtError.message).not.toContain("Cannot access");
54
+ }
55
+ });
56
+
57
+ it("should resolve null when subscribe throws and timeout fires", async () => {
58
+ vi.useFakeTimers();
59
+
60
+ const ndk = new NDK({
61
+ explicitRelayUrls: ["wss://relay.example.com"],
62
+ });
63
+
64
+ const subscribeSpy = vi.spyOn(ndk, "subscribe").mockImplementation(() => {
65
+ throw new Error("subscribe failed");
66
+ });
67
+
68
+ const promise = ndk.fetchEvent({ ids: ["abc"] });
69
+
70
+ // The promise should have been rejected immediately by the thrown error
71
+ // (Promise constructor catches synchronous throws)
72
+ await expect(promise).rejects.toThrow("subscribe failed");
73
+
74
+ // Advance past the 10-second timeout to ensure it doesn't crash
75
+ vi.advanceTimersByTime(11000);
76
+
77
+ subscribeSpy.mockRestore();
78
+ vi.useRealTimers();
79
+ });
80
+ });
@@ -0,0 +1,149 @@
1
+ import { nip19 } from "nostr-tools";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { NDKUser } from "../user/index.js";
4
+ import * as nip05Module from "../user/nip05.js";
5
+ import { NDK } from "./index.js";
6
+
7
+ describe("ndk.fetchUser()", () => {
8
+ let ndk: NDK;
9
+ const testPubkey = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
10
+ const testNpub = nip19.npubEncode(testPubkey);
11
+ const testNprofile = nip19.nprofileEncode({
12
+ pubkey: testPubkey,
13
+ relays: ["wss://relay1.example.com", "wss://relay2.example.com"],
14
+ });
15
+
16
+ beforeEach(() => {
17
+ ndk = new NDK();
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ describe("with npub string", () => {
25
+ it("should decode npub and create user with correct pubkey", async () => {
26
+ const user = await ndk.fetchUser(testNpub);
27
+ expect(user).toBeInstanceOf(NDKUser);
28
+ expect(user?.pubkey).toBe(testPubkey);
29
+ expect(user?.ndk).toBe(ndk);
30
+ });
31
+
32
+ it("should throw error for invalid npub", async () => {
33
+ await expect(ndk.fetchUser("npub1invalid")).rejects.toThrow();
34
+ });
35
+ });
36
+
37
+ describe("with nprofile string", () => {
38
+ it("should decode nprofile and create user with pubkey and relay hints", async () => {
39
+ const user = await ndk.fetchUser(testNprofile);
40
+ expect(user).toBeInstanceOf(NDKUser);
41
+ expect(user?.pubkey).toBe(testPubkey);
42
+ expect(user?.relayUrls).toEqual(["wss://relay1.example.com", "wss://relay2.example.com"]);
43
+ expect(user?.ndk).toBe(ndk);
44
+ });
45
+
46
+ it("should handle nprofile without relays", async () => {
47
+ const nprofileNoRelays = nip19.nprofileEncode({
48
+ pubkey: testPubkey,
49
+ relays: [],
50
+ });
51
+ const user = await ndk.fetchUser(nprofileNoRelays);
52
+ expect(user).toBeInstanceOf(NDKUser);
53
+ expect(user?.pubkey).toBe(testPubkey);
54
+ expect(user?.relayUrls).toEqual([]);
55
+ });
56
+ });
57
+
58
+ describe("with hex pubkey string", () => {
59
+ it("should accept hex pubkey directly", async () => {
60
+ const user = await ndk.fetchUser(testPubkey);
61
+ expect(user).toBeInstanceOf(NDKUser);
62
+ expect(user?.pubkey).toBe(testPubkey);
63
+ expect(user?.ndk).toBe(ndk);
64
+ });
65
+
66
+ it("should handle various hex pubkey formats", async () => {
67
+ const shortHex = "deadbeef";
68
+ const user = await ndk.fetchUser(shortHex);
69
+ expect(user).toBeInstanceOf(NDKUser);
70
+ expect(user?.pubkey).toBe(shortHex);
71
+ });
72
+ });
73
+
74
+ describe("with NIP-05 identifier", () => {
75
+ it("should resolve NIP-05 with @ symbol", async () => {
76
+ const mockUser = new NDKUser({ pubkey: testPubkey });
77
+ mockUser.ndk = ndk;
78
+
79
+ vi.spyOn(NDKUser, "fromNip05").mockResolvedValue(mockUser);
80
+
81
+ const user = await ndk.fetchUser("pablo@test.com");
82
+ expect(NDKUser.fromNip05).toHaveBeenCalledWith("pablo@test.com", ndk, false);
83
+ expect(user).toBe(mockUser);
84
+ });
85
+
86
+ it("should resolve NIP-05 without @ symbol (domain only)", async () => {
87
+ const mockUser = new NDKUser({ pubkey: testPubkey });
88
+ mockUser.ndk = ndk;
89
+
90
+ vi.spyOn(NDKUser, "fromNip05").mockResolvedValue(mockUser);
91
+
92
+ const user = await ndk.fetchUser("test.com");
93
+ expect(NDKUser.fromNip05).toHaveBeenCalledWith("test.com", ndk, false);
94
+ expect(user).toBe(mockUser);
95
+ });
96
+
97
+ it("should handle NIP-05 resolution failure", async () => {
98
+ vi.spyOn(NDKUser, "fromNip05").mockResolvedValue(undefined);
99
+
100
+ const user = await ndk.fetchUser("nonexistent@test.com");
101
+ expect(NDKUser.fromNip05).toHaveBeenCalledWith("nonexistent@test.com", ndk, false);
102
+ expect(user).toBeUndefined();
103
+ });
104
+
105
+ it("should pass skipCache parameter for NIP-05", async () => {
106
+ const mockUser = new NDKUser({ pubkey: testPubkey });
107
+ mockUser.ndk = ndk;
108
+
109
+ vi.spyOn(NDKUser, "fromNip05").mockResolvedValue(mockUser);
110
+
111
+ await ndk.fetchUser("pablo@test.com", true);
112
+ expect(NDKUser.fromNip05).toHaveBeenCalledWith("pablo@test.com", ndk, true);
113
+ });
114
+
115
+ it("should distinguish between NIP-05 and nprofile with dots", async () => {
116
+ // nprofile should not be mistaken for NIP-05 even if it contains dots somehow
117
+ const user = await ndk.fetchUser(testNprofile);
118
+ expect(user).toBeInstanceOf(NDKUser);
119
+ expect(user?.pubkey).toBe(testPubkey);
120
+ });
121
+ });
122
+
123
+ describe("edge cases", () => {
124
+ it("should handle empty string", async () => {
125
+ const user = await ndk.fetchUser("");
126
+ expect(user).toBeInstanceOf(NDKUser);
127
+ // Empty string is not a valid pubkey, so accessing pubkey should throw
128
+ expect(() => user?.pubkey).toThrow("npub not set");
129
+ });
130
+
131
+ it("should handle strings starting with 'n' but not valid NIP-19", async () => {
132
+ const invalidString = "notanip19string";
133
+ const user = await ndk.fetchUser(invalidString);
134
+ expect(user).toBeInstanceOf(NDKUser);
135
+ expect(user?.pubkey).toBe(invalidString);
136
+ });
137
+
138
+ it("should handle domain with multiple dots", async () => {
139
+ const mockUser = new NDKUser({ pubkey: testPubkey });
140
+ mockUser.ndk = ndk;
141
+
142
+ vi.spyOn(NDKUser, "fromNip05").mockResolvedValue(mockUser);
143
+
144
+ const user = await ndk.fetchUser("sub.domain.test.com");
145
+ expect(NDKUser.fromNip05).toHaveBeenCalledWith("sub.domain.test.com", ndk, false);
146
+ expect(user).toBe(mockUser);
147
+ });
148
+ });
149
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { NDK } from "./index.js";
3
+
4
+ describe("guardrailOff()", () => {
5
+ it("should disable all guardrails for one call", () => {
6
+ const ndk = new NDK({
7
+ explicitRelayUrls: ["wss://relay.primal.net"],
8
+ aiGuardrails: true,
9
+ });
10
+
11
+ const naddrFilter = {
12
+ kinds: [30023],
13
+ authors: ["fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],
14
+ "#d": ["identifier"],
15
+ };
16
+
17
+ // Set the flag
18
+ ndk.guardrailOff();
19
+
20
+ // Verify the flag was set to 'all'
21
+ expect(ndk.aiGuardrails["_nextCallDisabled"]).toBe("all");
22
+
23
+ // Verify shouldCheck returns false for any ID when 'all' is set
24
+ expect(ndk.aiGuardrails.shouldCheck("fetch-events-usage")).toBe(false);
25
+ expect(ndk.aiGuardrails.shouldCheck("any-other-check")).toBe(false);
26
+ });
27
+
28
+ it("should disable specific guardrail for one call", () => {
29
+ const ndk = new NDK({
30
+ explicitRelayUrls: ["wss://relay.primal.net"],
31
+ aiGuardrails: true,
32
+ });
33
+
34
+ // Set the flag for a specific guardrail
35
+ ndk.guardrailOff("fetch-events-usage");
36
+
37
+ // Verify the flag was set correctly
38
+ expect(ndk.aiGuardrails["_nextCallDisabled"]).toBeInstanceOf(Set);
39
+ expect(ndk.aiGuardrails["_nextCallDisabled"]).toContain("fetch-events-usage");
40
+
41
+ // Verify shouldCheck returns false only for the specified ID
42
+ expect(ndk.aiGuardrails.shouldCheck("fetch-events-usage")).toBe(false);
43
+ expect(ndk.aiGuardrails.shouldCheck("other-check")).toBe(true);
44
+ });
45
+
46
+ it("should disable multiple guardrails for one call", () => {
47
+ const ndk = new NDK({
48
+ explicitRelayUrls: ["wss://relay.primal.net"],
49
+ aiGuardrails: true,
50
+ });
51
+
52
+ // Set the flag for multiple guardrails
53
+ ndk.guardrailOff(["fetch-events-usage", "filter-large-limit"]);
54
+
55
+ // Verify the flag was set correctly
56
+ expect(ndk.aiGuardrails["_nextCallDisabled"]).toBeInstanceOf(Set);
57
+ expect(ndk.aiGuardrails["_nextCallDisabled"]).toContain("fetch-events-usage");
58
+ expect(ndk.aiGuardrails["_nextCallDisabled"]).toContain("filter-large-limit");
59
+
60
+ // Verify shouldCheck returns false for specified IDs
61
+ expect(ndk.aiGuardrails.shouldCheck("fetch-events-usage")).toBe(false);
62
+ expect(ndk.aiGuardrails.shouldCheck("filter-large-limit")).toBe(false);
63
+ expect(ndk.aiGuardrails.shouldCheck("other-check")).toBe(true);
64
+ });
65
+
66
+ it("should return this for method chaining", () => {
67
+ const ndk = new NDK({
68
+ explicitRelayUrls: ["wss://relay.primal.net"],
69
+ aiGuardrails: true,
70
+ });
71
+
72
+ const result = ndk.guardrailOff("fetch-events-usage");
73
+ expect(result).toBe(ndk);
74
+ });
75
+ });