@nostrify/nostrify 0.50.5 → 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.
- package/.turbo/turbo-build.log +36 -32
- package/.turbo/turbo-test.log +114 -95
- package/.turbo/turbo-typecheck.log +8 -6
- package/CHANGELOG.md +8 -0
- package/NRelay1.test.ts +148 -1
- package/NRelay1.ts +78 -3
- package/dist/NRelay1.d.ts +14 -0
- package/dist/NRelay1.d.ts.map +1 -1
- package/dist/NRelay1.js +63 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,32 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
[1mnpm[22m [33mwarn[39m [94mUnknown env config "verify-deps-before-run". This will stop working in the next major version of npm.[39m
|
|
7
|
+
[1mnpm[22m [33mwarn[39m [94mUnknown env config "_jsr-registry". This will stop working in the next major version of npm.[39m
|
|
8
|
+
⠙[1G[0KBuilding with esbuild...
|
|
9
|
+
|
|
10
|
+
[37mdist/[0m[1mNRelay1.js[0m [36m12.8kb[0m
|
|
11
|
+
[37mdist/[0m[1mNSchema.js[0m [36m8.2kb[0m
|
|
12
|
+
[37mdist/[0m[1mNPool.js[0m [36m6.0kb[0m
|
|
13
|
+
[37mdist/test/[0m[1mTestRelayServer.js[0m [36m4.7kb[0m
|
|
14
|
+
[37mdist/[0m[1mNSet.js[0m [36m4.1kb[0m
|
|
15
|
+
[37mdist/[0m[1mNConnectSigner.js[0m [36m3.9kb[0m
|
|
16
|
+
[37mdist/[0m[1mNBrowserSigner.js[0m [36m3.9kb[0m
|
|
17
|
+
[37mdist/ln/[0m[1mLNURL.js[0m [36m3.3kb[0m
|
|
18
|
+
[37mdist/[0m[1mNIP98.js[0m [36m2.7kb[0m
|
|
19
|
+
[37mdist/uploaders/[0m[1mBlossomUploader.js[0m [36m2.3kb[0m
|
|
20
|
+
[37mdist/uploaders/[0m[1mNostrBuildUploader.js[0m [36m1.9kb[0m
|
|
21
|
+
[37mdist/test/[0m[1mMockRelay.js[0m [36m1.4kb[0m
|
|
22
|
+
[37mdist/[0m[1mBunkerURI.js[0m [36m1.4kb[0m
|
|
23
|
+
[37mdist/[0m[1mNKinds.js[0m [36m1.1kb[0m
|
|
24
|
+
[37mdist/utils/[0m[1mgetFilterLimit.js[0m [36m1.1kb[0m
|
|
25
|
+
[37mdist/[0m[1mNIP05.js[0m [36m1.1kb[0m
|
|
26
|
+
[37mdist/[0m[1mNSecSigner.js[0m [36m1.1kb[0m
|
|
27
|
+
[37mdist/utils/[0m[1mMachina.js[0m [36m925b [0m
|
|
28
|
+
[37mdist/[0m[1mNCache.js[0m [36m828b [0m
|
|
29
|
+
[37mdist/[0m[1mmod.js[0m [36m815b [0m
|
|
30
|
+
[37mdist/test/[0m[1mmod.js[0m [36m701b [0m
|
|
31
|
+
[37mdist/[0m[1mNIP98Client.js[0m [36m664b [0m
|
|
32
|
+
[37mdist/test/[0m[1mErrorRelay.js[0m [36m557b [0m
|
|
33
|
+
[37m...and 9 more output files...[0m
|
|
34
|
+
|
|
35
|
+
⚡ [32mDone in 22ms[0m
|
|
36
|
+
Done!
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,111 +1,130 @@
|
|
|
1
1
|
|
|
2
|
-
> @nostrify/nostrify@0.50.
|
|
2
|
+
> @nostrify/nostrify@0.50.5 test /home/alex/Projects/nostrify/packages/nostrify
|
|
3
3
|
> node --test "**/*.test.ts"
|
|
4
4
|
|
|
5
|
-
✔ BunkerURI (4.
|
|
6
|
-
✔ BunkerURI.fromJSON (
|
|
7
|
-
✔ NBrowserSigner - without extension (408.
|
|
8
|
-
✔ NBrowserSigner - with extension polyfill (
|
|
9
|
-
✔ NBrowserSigner.nip44 - with extension polyfill (
|
|
10
|
-
✔ NBrowserSigner.nip04 - with extension polyfill (
|
|
11
|
-
✔ NBrowserSigner.getRelays - with extension polyfill (
|
|
12
|
-
✔ NBrowserSigner - missing nip44 support (1.
|
|
13
|
-
✔ NBrowserSigner - missing nip04 support (1.
|
|
14
|
-
✔ NBrowserSigner - feature detection (1.
|
|
15
|
-
✔ NBrowserSigner - extension appears after delay (
|
|
16
|
-
✔ NBrowserSigner - nip44 works when extension appears after delay (
|
|
17
|
-
✔ NCache (
|
|
18
|
-
✔ NConnectSigner.signEvent with nip04 encryption (
|
|
19
|
-
✔ NConnectSigner.signEvent with nip44 encryption (
|
|
20
|
-
✔ NIP05.lookup (
|
|
21
|
-
✔ NIP05.lookup with invalid values but valid profile pointer (
|
|
22
|
-
✔ NIP05.lookup with invalid document (
|
|
23
|
-
✔ NIP50.parseInput (
|
|
24
|
-
✔ NIP50.parseInput with negated token (
|
|
25
|
-
✔ NIP98.template (
|
|
26
|
-
✔ NIP98.template with payload (
|
|
27
|
-
✔ NIP98.verify (
|
|
28
|
-
✔ NIP98.verify fails with missing header (
|
|
29
|
-
✔ NIP98.verify fails with missing token (2.
|
|
30
|
-
✔ NIP98.verify fails with invalid token (2.
|
|
31
|
-
✔ NIP98.verify fails with invalid event (
|
|
32
|
-
✔ NIP98.verify fails with wrong event kind (
|
|
33
|
-
✔ NIP98.verify fails with wrong request URL (
|
|
34
|
-
✔ NIP98.verify fails with wrong request method (
|
|
35
|
-
✔ NIP98.verify fails with expired event (
|
|
36
|
-
✔ NIP98.verify fails with invalid payload (
|
|
37
|
-
✔ NIP98Client.fetch - basic GET request (
|
|
38
|
-
✔ NIP98Client.fetch - POST request with body (
|
|
39
|
-
✔ NIP98Client.fetch - with Request object input (
|
|
40
|
-
✔ NIP98Client.fetch - with URL object input (
|
|
41
|
-
✔ NIP98Client.fetch - uses default fetch when not provided (
|
|
42
|
-
✔ NIP98Client.fetch - preserves existing headers (
|
|
43
|
-
✔ NIP98Client.fetch - event can be verified with NIP98.verify (
|
|
44
|
-
✔ NIP98Client.fetch - handles different HTTP methods (
|
|
45
|
-
✔ NKinds (2.
|
|
46
|
-
✔ NPool.query (
|
|
47
|
-
✔ NPool.req (
|
|
48
|
-
✔ NPool.event (
|
|
49
|
-
✔ NPool.query with eoseTimeout (
|
|
50
|
-
✔ NPool.query with eoseTimeout disabled (
|
|
51
|
-
✔ NRelay1.query (
|
|
52
|
-
✔ NRelay1.query with NIP-50 search preserves relay order (
|
|
53
|
-
✔ NRelay1.query mismatched filter (
|
|
54
|
-
✔ NRelay1.req (
|
|
55
|
-
✔ NRelay1.event (
|
|
56
|
-
|
|
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
|
|
57
60
|
▶ NRelay1 idleTimeout
|
|
58
|
-
✔ websocket opens (
|
|
59
|
-
✔ websocket closes after idleTimeout (
|
|
60
|
-
✔ websocket wakes up during activity (
|
|
61
|
-
✔ NRelay1 idleTimeout (
|
|
62
|
-
✔ NRelay1.count rejects when the server sends CLOSED (
|
|
63
|
-
✔ NRelay1 closes when it receives a binary message (
|
|
64
|
-
✔
|
|
65
|
-
✔
|
|
66
|
-
✔
|
|
67
|
-
✔
|
|
68
|
-
✔ n.
|
|
69
|
-
✔
|
|
70
|
-
✔
|
|
71
|
-
✔
|
|
72
|
-
✔
|
|
73
|
-
✔
|
|
74
|
-
✔
|
|
75
|
-
✔
|
|
76
|
-
✔
|
|
77
|
-
✔
|
|
78
|
-
✔
|
|
79
|
-
✔
|
|
80
|
-
✔
|
|
81
|
-
✔ LNURL.
|
|
82
|
-
✔
|
|
83
|
-
✔
|
|
84
|
-
✔
|
|
85
|
-
|
|
86
|
-
✔
|
|
87
|
-
✔
|
|
88
|
-
✔
|
|
89
|
-
|
|
90
|
-
✔
|
|
91
|
-
✔ push
|
|
92
|
-
✔
|
|
93
|
-
✔
|
|
94
|
-
✔
|
|
95
|
-
✔
|
|
96
|
-
|
|
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
|
|
97
116
|
ℹ suites 0
|
|
98
|
-
ℹ pass
|
|
117
|
+
ℹ pass 107
|
|
99
118
|
ℹ fail 1
|
|
100
119
|
ℹ cancelled 0
|
|
101
120
|
ℹ skipped 1
|
|
102
121
|
ℹ todo 0
|
|
103
|
-
ℹ duration_ms
|
|
122
|
+
ℹ duration_ms 4739.688853
|
|
104
123
|
|
|
105
124
|
✖ failing tests:
|
|
106
125
|
|
|
107
126
|
test at uploaders/NostrBuildUploader.test.ts:12:7
|
|
108
|
-
✖ NostrBuildUploader.upload (
|
|
127
|
+
✖ NostrBuildUploader.upload (35.675948ms)
|
|
109
128
|
Error: nostr.build with default uploader requires a secret key to be configured
|
|
110
129
|
at TestContext.<anonymous> (file:///home/alex/Projects/nostrify/packages/nostrify/uploaders/NostrBuildUploader.test.ts:25:13)
|
|
111
130
|
at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @nostrify/nostrify@0.51.0 typecheck /home/alex/Projects/nostrify/packages/nostrify
|
|
4
|
+
> npx tsc --noEmit
|
|
5
|
+
|
|
6
|
+
[1mnpm[22m [33mwarn[39m [94mUnknown env config "verify-deps-before-run". This will stop working in the next major version of npm.[39m
|
|
7
|
+
[1mnpm[22m [33mwarn[39m [94mUnknown env config "_jsr-registry". This will stop working in the next major version of npm.[39m
|
|
8
|
+
⠙[1G[0K
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 0.50.5
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/NRelay1.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
5
|
import { ExponentialBackoff, WebsocketEvent } from "websocket-ts";
|
|
@@ -304,3 +304,150 @@ await test("NRelay1 closes when it receives a binary message", async () => {
|
|
|
304
304
|
|
|
305
305
|
await rejects(() => relay.query([{ kinds: [1] }]));
|
|
306
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
|
+
});
|
package/NRelay1.ts
CHANGED
|
@@ -65,6 +65,15 @@ export class NRelay1 implements NRelay {
|
|
|
65
65
|
private opts: NRelay1Opts;
|
|
66
66
|
private relayInfoPromise?: Promise<NostrRelayInfo | undefined>;
|
|
67
67
|
|
|
68
|
+
/** Promise that resolves when the current AUTH flow completes. */
|
|
69
|
+
private authPromise?: Promise<void>;
|
|
70
|
+
/** Set of subscription IDs that have already been retried after auth, to prevent infinite loops. */
|
|
71
|
+
private authRetriedSubs = new Set<string>();
|
|
72
|
+
/** Set of event IDs that have already been retried after auth, to prevent infinite loops. */
|
|
73
|
+
private authRetriedEvents = new Set<string>();
|
|
74
|
+
/** Pending events waiting for AUTH, keyed by event ID. */
|
|
75
|
+
private pendingEvents = new Map<string, NostrEvent>();
|
|
76
|
+
|
|
68
77
|
private ee = new EventTarget();
|
|
69
78
|
|
|
70
79
|
get subscriptions(): readonly NostrClientREQ[] {
|
|
@@ -241,7 +250,12 @@ export class NRelay1 implements NRelay {
|
|
|
241
250
|
);
|
|
242
251
|
break;
|
|
243
252
|
case 'CLOSED':
|
|
253
|
+
if (auth && msg[2].startsWith('auth-required:') && !this.authRetriedSubs.has(msg[1])) {
|
|
254
|
+
this.retrySubAfterAuth(msg[1]);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
244
257
|
this.subs.delete(msg[1]);
|
|
258
|
+
this.authRetriedSubs.delete(msg[1]);
|
|
245
259
|
this.maybeStartIdleTimer();
|
|
246
260
|
this.ee.dispatchEvent(
|
|
247
261
|
new CustomEvent(`sub:${msg[1]}`, { detail: msg }),
|
|
@@ -251,6 +265,12 @@ export class NRelay1 implements NRelay {
|
|
|
251
265
|
);
|
|
252
266
|
break;
|
|
253
267
|
case 'OK':
|
|
268
|
+
if (auth && !msg[2] && msg[3].startsWith('auth-required:') && !this.authRetriedEvents.has(msg[1])) {
|
|
269
|
+
this.retryEventAfterAuth(msg[1]);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
this.pendingEvents.delete(msg[1]);
|
|
273
|
+
this.authRetriedEvents.delete(msg[1]);
|
|
254
274
|
this.ee.dispatchEvent(new CustomEvent(`ok:${msg[1]}`, { detail: msg }));
|
|
255
275
|
break;
|
|
256
276
|
case 'NOTICE':
|
|
@@ -262,9 +282,62 @@ export class NRelay1 implements NRelay {
|
|
|
262
282
|
);
|
|
263
283
|
break;
|
|
264
284
|
case 'AUTH':
|
|
265
|
-
auth
|
|
266
|
-
|
|
267
|
-
|
|
285
|
+
if (auth) {
|
|
286
|
+
this.authPromise = this.doAuth(auth, msg[1]);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Perform NIP-42 authentication and wait for the relay's OK response. */
|
|
292
|
+
private async doAuth(auth: (challenge: string) => Promise<NostrEvent>, challenge: string): Promise<void> {
|
|
293
|
+
try {
|
|
294
|
+
const event = await auth(challenge);
|
|
295
|
+
const result = this.once(`ok:${event.id}`);
|
|
296
|
+
this.send(['AUTH', event]);
|
|
297
|
+
const [, , ok] = await result;
|
|
298
|
+
if (!ok) {
|
|
299
|
+
this.log({ level: 'warn', ns: 'relay.auth', message: 'AUTH failed' });
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
// AUTH failed, nothing to do
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Re-send a subscription after AUTH completes. */
|
|
307
|
+
private async retrySubAfterAuth(subscriptionId: string): Promise<void> {
|
|
308
|
+
const req = this.subs.get(subscriptionId);
|
|
309
|
+
if (!req) return;
|
|
310
|
+
|
|
311
|
+
this.authRetriedSubs.add(subscriptionId);
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
await this.authPromise;
|
|
315
|
+
} catch {
|
|
316
|
+
// AUTH failed — fall through to let the CLOSED propagate
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Re-send the original REQ if the subscription is still active.
|
|
320
|
+
if (this.subs.has(subscriptionId)) {
|
|
321
|
+
this.send(req);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Re-send an event after AUTH completes. */
|
|
326
|
+
private async retryEventAfterAuth(eventId: string): Promise<void> {
|
|
327
|
+
const event = this.pendingEvents.get(eventId);
|
|
328
|
+
if (!event) return;
|
|
329
|
+
|
|
330
|
+
this.authRetriedEvents.add(eventId);
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
await this.authPromise;
|
|
334
|
+
} catch {
|
|
335
|
+
// AUTH failed — fall through to let the failed OK propagate
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Re-send the event if it's still pending.
|
|
339
|
+
if (this.pendingEvents.has(eventId)) {
|
|
340
|
+
this.send(['EVENT', event]);
|
|
268
341
|
}
|
|
269
342
|
}
|
|
270
343
|
|
|
@@ -282,6 +355,8 @@ export class NRelay1 implements NRelay {
|
|
|
282
355
|
this.maybeStartIdleTimer();
|
|
283
356
|
break;
|
|
284
357
|
case 'EVENT':
|
|
358
|
+
this.pendingEvents.set(msg[1].id, msg[1]);
|
|
359
|
+
return this.socket.send(JSON.stringify(msg));
|
|
285
360
|
case 'COUNT':
|
|
286
361
|
return this.socket.send(JSON.stringify(msg));
|
|
287
362
|
}
|
package/dist/NRelay1.d.ts
CHANGED
|
@@ -33,6 +33,14 @@ export declare class NRelay1 implements NRelay {
|
|
|
33
33
|
private url;
|
|
34
34
|
private opts;
|
|
35
35
|
private relayInfoPromise?;
|
|
36
|
+
/** Promise that resolves when the current AUTH flow completes. */
|
|
37
|
+
private authPromise?;
|
|
38
|
+
/** Set of subscription IDs that have already been retried after auth, to prevent infinite loops. */
|
|
39
|
+
private authRetriedSubs;
|
|
40
|
+
/** Set of event IDs that have already been retried after auth, to prevent infinite loops. */
|
|
41
|
+
private authRetriedEvents;
|
|
42
|
+
/** Pending events waiting for AUTH, keyed by event ID. */
|
|
43
|
+
private pendingEvents;
|
|
36
44
|
private ee;
|
|
37
45
|
get subscriptions(): readonly NostrClientREQ[];
|
|
38
46
|
private log;
|
|
@@ -47,6 +55,12 @@ export declare class NRelay1 implements NRelay {
|
|
|
47
55
|
private createSocket;
|
|
48
56
|
/** Handle a NIP-01 relay message. */
|
|
49
57
|
protected receive(msg: NostrRelayMsg): void;
|
|
58
|
+
/** Perform NIP-42 authentication and wait for the relay's OK response. */
|
|
59
|
+
private doAuth;
|
|
60
|
+
/** Re-send a subscription after AUTH completes. */
|
|
61
|
+
private retrySubAfterAuth;
|
|
62
|
+
/** Re-send an event after AUTH completes. */
|
|
63
|
+
private retryEventAfterAuth;
|
|
50
64
|
/** Send a NIP-01 client message to the relay. */
|
|
51
65
|
protected send(msg: NostrClientMsg): void;
|
|
52
66
|
req(filters: NostrFilter[], opts?: {
|
package/dist/NRelay1.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NRelay1.d.ts","sourceRoot":"","sources":["../NRelay1.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,UAAU,EACV,WAAW,EACX,gBAAgB,EAEhB,cAAc,EACd,eAAe,EACf,aAAa,EAGb,cAAc,EACd,MAAM,EACP,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EAAkC,SAAS,EAAoC,MAAM,cAAc,CAAC;AAC3G,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAc1C,2DAA2D;AAC7D,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,IAAI,CAAC,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC9C,8GAA8G;IAC9G,OAAO,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAC1B,8JAA8J;IAC9J,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC7B,wFAAwF;IACxF,WAAW,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC;IACzC,uBAAuB;IACvB,GAAG,CAAC,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAC5B,kGAAkG;IAClG,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,CAAC;IAC5E,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG;QAAE,MAAM,IAAI,SAAS,CAAA;KAAE,GAAG,KAAK,CAAC;CACtE;AAED,8CAA8C;AAC9C,qBAAa,OAAQ,YAAW,MAAM;IACpC,MAAM,EAAE,SAAS,CAAC;IAElB,OAAO,CAAC,IAAI,CAAqC;IACjD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAC,CAAgC;IAClD,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,gBAAgB,CAAC,CAAsC;IAE/D,OAAO,CAAC,EAAE,CAAqB;IAE/B,IAAI,aAAa,IAAI,SAAS,cAAc,EAAE,CAE7C;IAED,OAAO,CAAC,GAAG;gBAIC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB;IAO/C,mDAAmD;YACrC,cAAc;IAsD5B,8FAA8F;IACxF,YAAY,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAOxF,yEAAyE;IACzE,OAAO,CAAC,YAAY;IA8EpB,qCAAqC;IACrC,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"NRelay1.d.ts","sourceRoot":"","sources":["../NRelay1.ts"],"names":[],"mappings":"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"}
|