@nostrify/nostrify 0.50.4 → 0.51.0

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.
@@ -1,32 +1,36 @@
1
-
2
- > @nostrify/nostrify@0.50.4 build /home/alex/Projects/nostrify/packages/nostrify
3
- > npx tsc -p tsconfig.json && node ../../esbuild.config.js --package ./
4
-
5
- npm warn Unknown env config "verify-deps-before-run". This will stop working in the next major version of npm.
6
- npm warn Unknown env config "_jsr-registry". This will stop working in the next major version of npm.
7
- Building with esbuild...
8
-
9
- dist/NRelay1.js 10.7kb
10
- dist/NSchema.js 8.2kb
11
- dist/NPool.js 6.0kb
12
- dist/test/TestRelayServer.js 4.4kb
13
- dist/NSet.js 4.1kb
14
- dist/NConnectSigner.js 3.9kb
15
- dist/NBrowserSigner.js 3.9kb
16
- dist/ln/LNURL.js 3.3kb
17
- dist/NIP98.js 2.7kb
18
- dist/uploaders/BlossomUploader.js 1.9kb
19
- dist/uploaders/NostrBuildUploader.js 1.9kb
20
- dist/test/MockRelay.js 1.4kb
21
- dist/BunkerURI.js 1.4kb
22
- dist/NKinds.js 1.1kb
23
- dist/NIP05.js 1.1kb
24
- dist/NSecSigner.js 1.1kb
25
- dist/utils/Machina.js 925b
26
- dist/NCache.js 828b
27
- dist/mod.js 815b
28
- dist/test/mod.js 701b
29
- ...and 11 more output files...
30
-
31
- Done in 21ms
32
- Done!
1
+
2
+
3
+ > @nostrify/nostrify@0.51.0 build /home/alex/Projects/nostrify/packages/nostrify
4
+ > npx tsc -p tsconfig.json && node ../../esbuild.config.js --package ./
5
+
6
+ npm warn Unknown env config "verify-deps-before-run". This will stop working in the next major version of npm.
7
+ npm warn Unknown env config "_jsr-registry". This will stop working in the next major version of npm.
8
+ ⠙Building with esbuild...
9
+
10
+ dist/NRelay1.js 12.8kb
11
+ dist/NSchema.js 8.2kb
12
+ dist/NPool.js 6.0kb
13
+ dist/test/TestRelayServer.js 4.7kb
14
+ dist/NSet.js 4.1kb
15
+ dist/NConnectSigner.js 3.9kb
16
+ dist/NBrowserSigner.js 3.9kb
17
+ dist/ln/LNURL.js 3.3kb
18
+ dist/NIP98.js 2.7kb
19
+ dist/uploaders/BlossomUploader.js 2.3kb
20
+ dist/uploaders/NostrBuildUploader.js 1.9kb
21
+ dist/test/MockRelay.js 1.4kb
22
+ dist/BunkerURI.js 1.4kb
23
+ dist/NKinds.js 1.1kb
24
+ dist/utils/getFilterLimit.js 1.1kb
25
+ dist/NIP05.js 1.1kb
26
+ dist/NSecSigner.js 1.1kb
27
+ dist/utils/Machina.js 925b 
28
+ dist/NCache.js 828b 
29
+ dist/mod.js 815b 
30
+ dist/test/mod.js 701b 
31
+ dist/NIP98Client.js 664b 
32
+ dist/test/ErrorRelay.js 557b 
33
+ ...and 9 more output files...
34
+
35
+ ⚡ Done in 22ms
36
+ Done!
@@ -1,114 +1,134 @@
1
-
2
-
3
- > @nostrify/nostrify@0.50.0 test /home/alex/Projects/nostrify/packages/nostrify
4
- > node --test "**/*.test.ts"
5
-
6
- ✔ BunkerURI (2.746358ms)
7
- BunkerURI.fromJSON (0.592066ms)
8
- ✔ NBrowserSigner - without extension (2.480267ms)
9
- ✔ NBrowserSigner - with extension polyfill (166.202262ms)
10
- ✔ NBrowserSigner.nip44 - with extension polyfill (48.982883ms)
11
- ✔ NBrowserSigner.nip04 - with extension polyfill (24.263617ms)
12
- ✔ NBrowserSigner.getRelays - with extension polyfill (1.466605ms)
13
- ✔ NBrowserSigner - missing nip44 support (1.647116ms)
14
- ✔ NBrowserSigner - missing nip04 support (0.388212ms)
15
- ✔ NBrowserSigner - feature detection (1.960176ms)
16
- NCache (11.489503ms)
17
- NConnectSigner.signEvent with nip04 encryption (236.48745ms)
18
- ✔ NConnectSigner.signEvent with nip44 encryption (72.89594ms)
19
- NIP05.lookup (83.374316ms)
20
- ✔ NIP05.lookup with invalid values but valid profile pointer (12.756762ms)
21
- ✔ NIP05.lookup with invalid document (20.462881ms)
22
- NIP50.parseInput (7.233418ms)
23
- ✔ NIP50.parseInput with negated token (0.507317ms)
24
- NIP98.template (4.503421ms)
25
- ✔ NIP98.template with payload (22.610571ms)
26
- ✔ NIP98.verify (166.641341ms)
27
- ✔ NIP98.verify fails with missing header (3.102239ms)
28
- ✔ NIP98.verify fails with missing token (1.647737ms)
29
- ✔ NIP98.verify fails with invalid token (2.754143ms)
30
- ✔ NIP98.verify fails with invalid event (20.081863ms)
31
- ✔ NIP98.verify fails with wrong event kind (25.410189ms)
32
- ✔ NIP98.verify fails with wrong request URL (19.265203ms)
33
- ✔ NIP98.verify fails with wrong request method (25.121986ms)
34
- ✔ NIP98.verify fails with expired event (14.228117ms)
35
- ✔ NIP98.verify fails with invalid payload (45.486742ms)
36
- NIP98Client.fetch - basic GET request (162.816349ms)
37
- ✔ NIP98Client.fetch - POST request with body (45.826032ms)
38
- ✔ NIP98Client.fetch - with Request object input (38.387778ms)
39
- ✔ NIP98Client.fetch - with URL object input (25.195494ms)
40
- ✔ NIP98Client.fetch - uses default fetch when not provided (11.27616ms)
41
- ✔ NIP98Client.fetch - preserves existing headers (13.853149ms)
42
- ✔ NIP98Client.fetch - event can be verified with NIP98.verify (36.586932ms)
43
- ✔ NIP98Client.fetch - handles different HTTP methods (65.246167ms)
44
- NKinds (3.899813ms)
45
- NPool.query (480.159465ms)
46
- ✔ NPool.req (162.806249ms)
47
- ✔ NPool.event (85.226228ms)
48
- ✔ NPool.query with eoseTimeout (625.249102ms)
49
- ✔ NPool.query with eoseTimeout disabled (538.136077ms)
50
- NRelay1.query (316.888891ms)
51
- ✔ NRelay1.query with NIP-50 search preserves relay order (101.77541ms)
52
- ✔ NRelay1.query mismatched filter (35.660044ms)
53
- ✔ NRelay1.req (107.715539ms)
54
- ✔ NRelay1.event (27.486574ms)
55
- ﹣ NRelay1 backoff (0.35573ms) # SKIP
56
- NRelay1 idleTimeout
57
- websocket opens (4.890911ms)
58
- websocket closes after idleTimeout (150.939876ms)
59
- ✔ websocket wakes up during activity (31.703544ms)
60
- ✔ NRelay1 idleTimeout (193.2402ms)
61
- NRelay1.count rejects when the server sends CLOSED (21.977147ms)
62
- NRelay1 closes when it receives a binary message (9.325142ms)
63
- n.id (19.213084ms)
64
- n.bech32 (3.798672ms)
65
- n.filter (47.81926ms)
66
- n.event (34.382045ms)
67
- n.metadata (68.049272ms)
68
- NSecSigner (110.350227ms)
69
- NSecSigner.nip44 (32.295571ms)
70
- NSet (42.044732ms)
71
- NSet.add (replaceable) (0.57253ms)
72
- NSet.add (parameterized) (1.000306ms)
73
- NSet.add (deletion) (0.450089ms)
74
- Construct a RelayError from the reason message (2.143422ms)
75
- Throw a new RelayError if the OK message is false (0.52506ms)
76
- LNURL.fromString (7.847636ms)
77
- LNURL.fromLightningAddress (1.686861ms)
78
- LNURL.toString (1.142133ms)
79
- LNURL.getDetails (86.164146ms)
80
- LNURL.getInvoice (13.898675ms)
81
- ErrorRelay (94.163638ms)
82
- MockRelay (3.052615ms)
83
- BlossomUploader.upload (733.428376ms)
84
- ✖ NostrBuildUploader.upload (33.449327ms)
85
- CircularSet (2.438347ms)
86
- push, iterate, & close (107.313622ms)
87
- close & reopen (0.74796ms)
88
- aborts with signal (52.688019ms)
89
- already aborted signal in constructor (0.717774ms)
90
- push after abort (3.810805ms)
91
- multiple messages in queue (0.582538ms)
92
- ✔ N64 (17.530693ms)
93
- N64.encodeEvent (0.432495ms)
94
- N64.decodeEvent (4.268328ms)
95
- ℹ tests 88
96
- ℹ suites 0
97
- ℹ pass 86
98
- ℹ fail 1
99
- ℹ cancelled 0
100
- ℹ skipped 1
101
- ℹ todo 0
102
- ℹ duration_ms 3673.242963
103
-
104
- ✖ failing tests:
105
-
106
- test at uploaders/NostrBuildUploader.test.ts:12:7
107
- ✖ NostrBuildUploader.upload (33.449327ms)
108
- Error: nostr.build with default uploader requires a secret key to be configured
109
- at TestContext.<anonymous> (file:///home/alex/Projects/nostrify/packages/nostrify/uploaders/NostrBuildUploader.test.ts:25:13)
110
-  at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
111
-  at async Test.run (node:internal/test_runner/test:1102:7)
112
-  at async startSubtestAfterBootstrap (node:internal/test_runner/harness:358:3)
113
- at async file:///home/alex/Projects/nostrify/packages/nostrify/uploaders/NostrBuildUploader.test.ts:12:1
114
-  ELIFECYCLE  Test failed. See above for more details.
1
+
2
+ > @nostrify/nostrify@0.50.5 test /home/alex/Projects/nostrify/packages/nostrify
3
+ > node --test "**/*.test.ts"
4
+
5
+ ✔ BunkerURI (4.028694ms)
6
+ ✔ BunkerURI.fromJSON (1.194504ms)
7
+ NBrowserSigner - without extension (408.101823ms)
8
+ ✔ NBrowserSigner - with extension polyfill (177.58652ms)
9
+ ✔ NBrowserSigner.nip44 - with extension polyfill (53.432633ms)
10
+ ✔ NBrowserSigner.nip04 - with extension polyfill (31.318082ms)
11
+ ✔ NBrowserSigner.getRelays - with extension polyfill (3.337439ms)
12
+ ✔ NBrowserSigner - missing nip44 support (1.117529ms)
13
+ ✔ NBrowserSigner - missing nip04 support (1.643893ms)
14
+ ✔ NBrowserSigner - feature detection (1.047697ms)
15
+ ✔ NBrowserSigner - extension appears after delay (306.657599ms)
16
+ NBrowserSigner - nip44 works when extension appears after delay (336.700014ms)
17
+ NCache (32.945945ms)
18
+ ✔ NConnectSigner.signEvent with nip04 encryption (509.267873ms)
19
+ NConnectSigner.signEvent with nip44 encryption (111.33369ms)
20
+ ✔ NIP05.lookup (154.653466ms)
21
+ ✔ NIP05.lookup with invalid values but valid profile pointer (12.873408ms)
22
+ NIP05.lookup with invalid document (16.375048ms)
23
+ ✔ NIP50.parseInput (12.96974ms)
24
+ NIP50.parseInput with negated token (0.650367ms)
25
+ ✔ NIP98.template (17.834683ms)
26
+ ✔ NIP98.template with payload (52.701182ms)
27
+ ✔ NIP98.verify (228.10839ms)
28
+ ✔ NIP98.verify fails with missing header (6.332332ms)
29
+ ✔ NIP98.verify fails with missing token (2.281917ms)
30
+ ✔ NIP98.verify fails with invalid token (2.654441ms)
31
+ ✔ NIP98.verify fails with invalid event (31.769946ms)
32
+ ✔ NIP98.verify fails with wrong event kind (76.829174ms)
33
+ ✔ NIP98.verify fails with wrong request URL (28.079039ms)
34
+ ✔ NIP98.verify fails with wrong request method (26.629332ms)
35
+ ✔ NIP98.verify fails with expired event (23.384448ms)
36
+ NIP98.verify fails with invalid payload (43.967678ms)
37
+ ✔ NIP98Client.fetch - basic GET request (355.876029ms)
38
+ ✔ NIP98Client.fetch - POST request with body (162.747693ms)
39
+ ✔ NIP98Client.fetch - with Request object input (78.965075ms)
40
+ ✔ NIP98Client.fetch - with URL object input (46.142403ms)
41
+ ✔ NIP98Client.fetch - uses default fetch when not provided (56.968837ms)
42
+ ✔ NIP98Client.fetch - preserves existing headers (38.948264ms)
43
+ ✔ NIP98Client.fetch - event can be verified with NIP98.verify (44.201489ms)
44
+ NIP98Client.fetch - handles different HTTP methods (150.07877ms)
45
+ NKinds (2.788644ms)
46
+ ✔ NPool.query (692.063225ms)
47
+ ✔ NPool.req (214.981082ms)
48
+ ✔ NPool.event (126.440494ms)
49
+ ✔ NPool.query with eoseTimeout (659.85331ms)
50
+ NPool.query with eoseTimeout disabled (565.131734ms)
51
+ ✔ NRelay1.query (485.876171ms)
52
+ ✔ NRelay1.query with NIP-50 search preserves relay order (120.209463ms)
53
+ ✔ NRelay1.query mismatched filter (63.99025ms)
54
+ ✔ NRelay1.req (191.837028ms)
55
+ NRelay1.event sends while connection is open (41.721819ms)
56
+ NRelay1.event sends before connection is open (63.494985ms)
57
+ NRelay1.event throws when OK is false (29.705869ms)
58
+ NRelay1.event sends after reconnect from CLOSING state (88.273164ms)
59
+ NRelay1 backoff (0.48232ms) # SKIP
60
+ NRelay1 idleTimeout
61
+ websocket opens (6.360034ms)
62
+ websocket closes after idleTimeout (149.970737ms)
63
+ websocket wakes up during activity (27.214357ms)
64
+ NRelay1 idleTimeout (191.021158ms)
65
+ NRelay1.count rejects when the server sends CLOSED (20.042641ms)
66
+ NRelay1 closes when it receives a binary message (17.620458ms)
67
+ NRelay1 NIP-42 auth-required REQ retry (118.486511ms)
68
+ NRelay1 NIP-42 auth-required EVENT retry (62.036903ms)
69
+ NRelay1 NIP-42 auth-required does not retry without auth callback (18.852174ms)
70
+ NRelay1 NIP-42 auth-required does not retry infinitely (68.63132ms)
71
+ n.id (24.32918ms)
72
+ n.bech32 (7.411329ms)
73
+ n.filter (135.407499ms)
74
+ n.event (90.556593ms)
75
+ n.metadata (164.43043ms)
76
+ NSecSigner (371.990996ms)
77
+ NSecSigner.nip44 (105.236803ms)
78
+ NSet (49.973754ms)
79
+ NSet.add (replaceable) (0.581187ms)
80
+ NSet.add (parameterized) (5.114544ms)
81
+ NSet.add (deletion) (0.606013ms)
82
+ Construct a RelayError from the reason message (2.640955ms)
83
+ Throw a new RelayError if the OK message is false (0.810269ms)
84
+ LNURL.fromString (13.598537ms)
85
+ LNURL.fromLightningAddress (2.106787ms)
86
+ LNURL.toString (0.99578ms)
87
+ LNURL.getDetails (113.054838ms)
88
+ LNURL.getInvoice (40.071474ms)
89
+ ErrorRelay (216.352701ms)
90
+ MockRelay (5.899475ms)
91
+ BlossomUploader.upload (1023.233031ms)
92
+ NostrBuildUploader.upload (35.675948ms)
93
+ CircularSet (3.09927ms)
94
+ push, iterate, & close (107.709499ms)
95
+ close & reopen (0.89543ms)
96
+ aborts with signal (55.223022ms)
97
+ already aborted signal in constructor (0.925958ms)
98
+ push after abort (3.545873ms)
99
+ multiple messages in queue (0.634127ms)
100
+ N64 (20.852057ms)
101
+ N64.encodeEvent (0.470058ms)
102
+ N64.decodeEvent (13.615198ms)
103
+ ✔ getFilterLimit returns Infinity for single replaceable kind + single author (2.743969ms)
104
+ getFilterLimit returns Infinity for single addressable kind + single author + single #d (0.812543ms)
105
+ ✔ getFilterLimit allows since/until on coordinate filters (0.598139ms)
106
+ getFilterLimit respects explicit limit on coordinate filters (0.530141ms)
107
+ getFilterLimit enforces limit for multiple authors with replaceable kind (0.868239ms)
108
+ getFilterLimit enforces limit for multiple kinds with replaceable kinds (0.580065ms)
109
+ getFilterLimit returns Infinity for addressable kind without #d (not a coordinate filter, but nostr-tools also returns Infinity) (0.439419ms)
110
+ getFilterLimit enforces limit for addressable kind with multiple #d values (0.465198ms)
111
+ getFilterLimit enforces limit when extra filter properties are present (2.643249ms)
112
+ getFilterLimit returns 0 for empty arrays (0.566329ms)
113
+ getFilterLimit works normally for regular kinds (0.710531ms)
114
+ getFilterLimit works normally for ids filter (0.295778ms)
115
+ ℹ tests 109
116
+ ℹ suites 0
117
+ ℹ pass 107
118
+ ℹ fail 1
119
+ ℹ cancelled 0
120
+ ℹ skipped 1
121
+ ℹ todo 0
122
+ ℹ duration_ms 4739.688853
123
+
124
+ ✖ failing tests:
125
+
126
+ test at uploaders/NostrBuildUploader.test.ts:12:7
127
+ ✖ NostrBuildUploader.upload (35.675948ms)
128
+ Error: nostr.build with default uploader requires a secret key to be configured
129
+ at TestContext.<anonymous> (file:///home/alex/Projects/nostrify/packages/nostrify/uploaders/NostrBuildUploader.test.ts:25:13)
130
+ at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
131
+ at async Test.run (node:internal/test_runner/test:1110:7)
132
+ at async startSubtestAfterBootstrap (node:internal/test_runner/harness:358:3)
133
+ at async file:///home/alex/Projects/nostrify/packages/nostrify/uploaders/NostrBuildUploader.test.ts:12:1
134
+  ELIFECYCLE  Test failed. See above for more details.
@@ -1,6 +1,8 @@
1
-
2
- > @nostrify/nostrify@0.50.4 typecheck /home/alex/Projects/nostrify/packages/nostrify
3
- > npx tsc --noEmit
4
-
5
- npm warn Unknown env config "verify-deps-before-run". This will stop working in the next major version of npm.
6
- npm warn Unknown env config "_jsr-registry". This will stop working in the next major version of npm.
1
+
2
+
3
+ > @nostrify/nostrify@0.51.0 typecheck /home/alex/Projects/nostrify/packages/nostrify
4
+ > npx tsc --noEmit
5
+
6
+ npm warn Unknown env config "verify-deps-before-run". This will stop working in the next major version of npm.
7
+ npm warn Unknown env config "_jsr-registry". This will stop working in the next major version of npm.
8
+ ⠙
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.51.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a9dc610: NRelay1: automatically retry REQ and EVENT after NIP-42 AUTH
8
+
9
+ When a relay responds with `auth-required:` in a CLOSED or OK message, NRelay1 now waits for the AUTH flow to complete and re-sends the original request. This implements the full NIP-42 client-side protocol flow. Retries are limited to one attempt per subscription/event to prevent infinite loops.
10
+
11
+ ## 0.50.5
12
+
13
+ ### Patch Changes
14
+
15
+ - Fix NRelay1 and NPool to not enforce an inferred limit on coordinate filters. Filters targeting a single replaceable kind + single author, or a single addressable kind + single author + single `#d` tag, now allow relays to send back historical events instead of closing the subscription after receiving just one event.
16
+
3
17
  ## 0.50.4
4
18
 
5
19
  ### Patch Changes
package/NIP05.ts CHANGED
@@ -10,7 +10,7 @@ interface LookupOpts {
10
10
  export class NIP05 {
11
11
  /** NIP-05 value regex. */
12
12
  static regex(): RegExp {
13
- return /^(?:([\w.+-]+)@)?([\w.-]+)$/;
13
+ return /^(?:([\w.+-]+)@)?((?:[\w-]+\.)+[\w-]+)$/;
14
14
  }
15
15
 
16
16
  /** Nostr pubkey with relays object. */
package/NPool.ts CHANGED
@@ -6,7 +6,7 @@ import type {
6
6
  NostrRelayEVENT,
7
7
  NRelay,
8
8
  } from '@nostrify/types';
9
- import { getFilterLimit } from 'nostr-tools';
9
+ import { getFilterLimit } from './utils/getFilterLimit.ts';
10
10
 
11
11
  import { CircularSet } from './utils/CircularSet.ts';
12
12
  import { Machina } from './utils/Machina.ts';
package/NRelay1.test.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { it, test } from "node:test";
2
- import type { NostrEvent } from "@nostrify/types";
2
+ import type { NostrClientMsg, NostrEvent } from "@nostrify/types";
3
3
  import { deepStrictEqual, ok, rejects } from "node:assert";
4
4
  import { finalizeEvent, generateSecretKey } from "nostr-tools";
5
- import { WebsocketEvent } from "websocket-ts";
5
+ import { ExponentialBackoff, WebsocketEvent } from "websocket-ts";
6
6
 
7
7
  import { genEvent } from "./test/mod.ts";
8
8
  import { TestRelayServer } from "./test/TestRelayServer.ts";
@@ -114,7 +114,7 @@ await test("NRelay1.req", async () => {
114
114
  clearTimeout(tid);
115
115
  });
116
116
 
117
- await test("NRelay1.event", async () => {
117
+ await test("NRelay1.event sends while connection is open", async () => {
118
118
  await using server = await TestRelayServer.create();
119
119
  await using relay = new NRelay1(server.url);
120
120
 
@@ -129,6 +129,71 @@ await test("NRelay1.event", async () => {
129
129
  await relay.event(event);
130
130
  });
131
131
 
132
+ await test("NRelay1.event sends before connection is open", async () => {
133
+ await using server = await TestRelayServer.create();
134
+ await using relay = new NRelay1(server.url);
135
+
136
+ const event: NostrEvent = finalizeEvent({
137
+ kind: 1,
138
+ content: "Sent before connection was established",
139
+ tags: [],
140
+ created_at: Math.floor(Date.now() / 1000),
141
+ }, generateSecretKey());
142
+
143
+ // Send immediately without waiting for the connection to open.
144
+ // The ArrayQueue buffer should hold the message and deliver it
145
+ // once the socket opens.
146
+ await relay.event(event);
147
+ });
148
+
149
+ await test("NRelay1.event throws when OK is false", async () => {
150
+ await using server = await TestRelayServer.create({
151
+ handleMessage(socket, msg) {
152
+ if (msg[0] === "EVENT") {
153
+ server.send(socket, ["OK", msg[1].id, false, "blocked: not allowed"]);
154
+ }
155
+ },
156
+ });
157
+
158
+ await using relay = new NRelay1(server.url);
159
+
160
+ const event: NostrEvent = finalizeEvent({
161
+ kind: 1,
162
+ content: "This event should be rejected",
163
+ tags: [],
164
+ created_at: Math.floor(Date.now() / 1000),
165
+ }, generateSecretKey());
166
+
167
+ await rejects(() => relay.event(event), /blocked: not allowed/);
168
+ });
169
+
170
+ await test("NRelay1.event sends after reconnect from CLOSING state", async () => {
171
+ await using server = await TestRelayServer.create();
172
+ await using relay = new NRelay1(server.url, {
173
+ backoff: new ExponentialBackoff(100),
174
+ });
175
+
176
+ await new Promise<void>((resolve) =>
177
+ relay.socket.addEventListener(WebsocketEvent.open, () => resolve(), {
178
+ once: true,
179
+ })
180
+ );
181
+
182
+ server.dropConnections();
183
+
184
+ // Wait briefly for the socket to register as closed.
185
+ await new Promise((resolve) => setTimeout(resolve, 50));
186
+
187
+ const event: NostrEvent = finalizeEvent({
188
+ kind: 1,
189
+ content: "Sent while connection was closing, delivered after reconnect",
190
+ tags: [],
191
+ created_at: Math.floor(Date.now() / 1000),
192
+ }, generateSecretKey());
193
+
194
+ await relay.event(event);
195
+ });
196
+
132
197
  test.skip("NRelay1 backoff", async () => {
133
198
  await using server = await TestRelayServer.create();
134
199
  await using relay = new NRelay1(server.url);
@@ -239,3 +304,150 @@ await test("NRelay1 closes when it receives a binary message", async () => {
239
304
 
240
305
  await rejects(() => relay.query([{ kinds: [1] }]));
241
306
  });
307
+
308
+ await test("NRelay1 NIP-42 auth-required REQ retry", async () => {
309
+ const sk = generateSecretKey();
310
+ const testEvent = genEvent({ kind: 4, content: "secret DM" }, sk);
311
+
312
+ let reqCount = 0;
313
+
314
+ await using server = await TestRelayServer.create({
315
+ handleMessage(socket, msg: NostrClientMsg) {
316
+ if (msg[0] === "REQ") {
317
+ const [, subId] = msg;
318
+ reqCount++;
319
+ if (reqCount === 1) {
320
+ // First REQ: send AUTH challenge, then reject with auth-required
321
+ socket.send(JSON.stringify(["AUTH", "challenge123"]));
322
+ socket.send(JSON.stringify(["CLOSED", subId, "auth-required: authentication required"]));
323
+ } else {
324
+ // Second REQ (after auth): serve the events
325
+ socket.send(JSON.stringify(["EVENT", subId, testEvent]));
326
+ socket.send(JSON.stringify(["EOSE", subId]));
327
+ }
328
+ }
329
+ if (msg[0] === "AUTH") {
330
+ // Accept the AUTH event
331
+ socket.send(JSON.stringify(["OK", msg[1].id, true, ""]));
332
+ }
333
+ },
334
+ });
335
+
336
+ await using relay = new NRelay1(server.url, {
337
+ auth: async (challenge) => {
338
+ return genEvent({
339
+ kind: 22242,
340
+ tags: [["relay", server.url], ["challenge", challenge]],
341
+ }, sk);
342
+ },
343
+ });
344
+
345
+ const result = await relay.query([{ kinds: [4] }]);
346
+
347
+ deepStrictEqual(result.length, 1);
348
+ deepStrictEqual(result[0].id, testEvent.id);
349
+ deepStrictEqual(reqCount, 2);
350
+ });
351
+
352
+ await test("NRelay1 NIP-42 auth-required EVENT retry", async () => {
353
+ const sk = generateSecretKey();
354
+ const eventToPublish: NostrEvent = finalizeEvent({
355
+ kind: 1,
356
+ content: "Hello from authenticated user",
357
+ tags: [],
358
+ created_at: Math.floor(Date.now() / 1000),
359
+ }, sk);
360
+
361
+ let eventCount = 0;
362
+
363
+ await using server = await TestRelayServer.create({
364
+ handleMessage(socket, msg: NostrClientMsg) {
365
+ if (msg[0] === "EVENT") {
366
+ eventCount++;
367
+ if (eventCount === 1) {
368
+ // First EVENT: send AUTH challenge, then reject with auth-required
369
+ socket.send(JSON.stringify(["AUTH", "challenge456"]));
370
+ socket.send(JSON.stringify(["OK", msg[1].id, false, "auth-required: authentication required"]));
371
+ } else {
372
+ // Second EVENT (after auth): accept
373
+ socket.send(JSON.stringify(["OK", msg[1].id, true, ""]));
374
+ }
375
+ }
376
+ if (msg[0] === "AUTH") {
377
+ // Accept the AUTH event
378
+ socket.send(JSON.stringify(["OK", msg[1].id, true, ""]));
379
+ }
380
+ },
381
+ });
382
+
383
+ await using relay = new NRelay1(server.url, {
384
+ auth: async (challenge) => {
385
+ return genEvent({
386
+ kind: 22242,
387
+ tags: [["relay", server.url], ["challenge", challenge]],
388
+ }, sk);
389
+ },
390
+ });
391
+
392
+ // Should succeed after automatic auth + retry
393
+ await relay.event(eventToPublish);
394
+ deepStrictEqual(eventCount, 2);
395
+ });
396
+
397
+ await test("NRelay1 NIP-42 auth-required does not retry without auth callback", async () => {
398
+ let reqCount = 0;
399
+
400
+ await using server = await TestRelayServer.create({
401
+ handleMessage(socket, msg: NostrClientMsg) {
402
+ if (msg[0] === "REQ") {
403
+ reqCount++;
404
+ const [, subId] = msg;
405
+ socket.send(JSON.stringify(["CLOSED", subId, "auth-required: authentication required"]));
406
+ }
407
+ },
408
+ });
409
+
410
+ // No auth callback provided — CLOSED should pass through without retry
411
+ await using relay = new NRelay1(server.url);
412
+
413
+ const result = await relay.query([{ kinds: [4] }]);
414
+ deepStrictEqual(result, []);
415
+ deepStrictEqual(reqCount, 1);
416
+ });
417
+
418
+ await test("NRelay1 NIP-42 auth-required does not retry infinitely", async () => {
419
+ const sk = generateSecretKey();
420
+ let reqCount = 0;
421
+
422
+ await using server = await TestRelayServer.create({
423
+ handleMessage(socket, msg: NostrClientMsg) {
424
+ if (msg[0] === "REQ") {
425
+ const [, subId] = msg;
426
+ reqCount++;
427
+ // Send AUTH challenge on first REQ only
428
+ if (reqCount === 1) {
429
+ socket.send(JSON.stringify(["AUTH", "challenge789"]));
430
+ }
431
+ // Always reject with auth-required, even after auth
432
+ socket.send(JSON.stringify(["CLOSED", subId, "auth-required: still not allowed"]));
433
+ }
434
+ if (msg[0] === "AUTH") {
435
+ socket.send(JSON.stringify(["OK", msg[1].id, true, ""]));
436
+ }
437
+ },
438
+ });
439
+
440
+ await using relay = new NRelay1(server.url, {
441
+ auth: async (challenge) => {
442
+ return genEvent({
443
+ kind: 22242,
444
+ tags: [["relay", server.url], ["challenge", challenge]],
445
+ }, sk);
446
+ },
447
+ });
448
+
449
+ // Should stop after one retry (reqCount=2), not loop forever
450
+ const result = await relay.query([{ kinds: [4] }]);
451
+ deepStrictEqual(result, []);
452
+ deepStrictEqual(reqCount, 2);
453
+ });