FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
JavaScriptSolidServer/test/did-nostr.test.js at gh-pages · JavaScriptSolidServer/JavaScriptSolidServer · GitHub
JavaScriptSolidServer
JavaScriptSolidServer
Repository navigation
Code
Issues
164
(164)
Pull requests
14
(14)
Actions
Projects
Security and quality
Insights
Expand file tree
Breadcrumbs
JavaScriptSolidServer
/
test
/
did-nostr.test.js
Copy path
More file actions
More file actions
Latest commit
History
History
History
509 lines (454 loc) · 19.2 KB
Breadcrumbs
JavaScriptSolidServer
/
test
/
did-nostr.test.js
Copy path
File metadata and controls
509 lines (454 loc) · 19.2 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
/**
* Tests for did:nostr to WebID resolution
*/
import
{
describe
,
it
,
before
,
after
,
mock
}
from
'node:test'
;
import
assert
from
'node:assert'
;
import
{
generateSecretKey
,
getPublicKey
,
finalizeEvent
}
from
'../src/nostr/event.js'
;
import
{
startTestServer
,
stopTestServer
,
request
,
createTestPod
,
getBaseUrl
,
assertStatus
}
from
'./helpers.js'
;
// Import the module under test
import
{
resolveDidNostrToWebId
,
clearCache
,
_cacheSizeForTests
,
_CACHE_MAX_FOR_TESTS
,
}
from
'../src/auth/did-nostr.js'
;
describe
(
'DID:nostr Resolution'
,
(
)
=>
{
describe
(
'Unit Tests'
,
(
)
=>
{
before
(
(
)
=>
{
clearCache
(
)
;
}
)
;
it
(
'should return null for invalid pubkey'
,
async
(
)
=>
{
const
result
=
await
resolveDidNostrToWebId
(
'invalid'
)
;
assert
.
strictEqual
(
result
,
null
)
;
}
)
;
it
(
'should return null for empty pubkey'
,
async
(
)
=>
{
const
result
=
await
resolveDidNostrToWebId
(
''
)
;
assert
.
strictEqual
(
result
,
null
)
;
}
)
;
it
(
'should return null for null pubkey'
,
async
(
)
=>
{
const
result
=
await
resolveDidNostrToWebId
(
null
)
;
assert
.
strictEqual
(
result
,
null
)
;
}
)
;
it
(
'should return null for pubkey with wrong length'
,
async
(
)
=>
{
const
result
=
await
resolveDidNostrToWebId
(
'abcd1234'
)
;
assert
.
strictEqual
(
result
,
null
)
;
}
)
;
it
(
'should handle non-existent DID gracefully'
,
async
(
)
=>
{
// Use a random pubkey that won't exist
const
sk
=
generateSecretKey
(
)
;
const
pubkey
=
getPublicKey
(
sk
)
;
// This will hit nostr.social and get 404
const
result
=
await
resolveDidNostrToWebId
(
pubkey
)
;
assert
.
strictEqual
(
result
,
null
)
;
}
)
;
}
)
;
describe
(
'checkSameAsLink Function'
,
(
)
=>
{
// We need to test the internal checkSameAsLink function
// Since it's not exported, we test it indirectly through WebID verification
it
(
'should recognize owl:sameAs string value'
,
async
(
)
=>
{
// This test verifies the format we expect in WebID profiles
const
profile
=
{
'@id'
:
'#me'
,
'owl:sameAs'
:
'did:nostr:abcd1234'
}
;
// The profile should have the correct structure
assert
.
strictEqual
(
profile
[
'owl:sameAs'
]
,
'did:nostr:abcd1234'
)
;
}
)
;
it
(
'should recognize sameAs as @id object'
,
async
(
)
=>
{
const
profile
=
{
'@id'
:
'#me'
,
'owl:sameAs'
:
{
'@id'
:
'did:nostr:abcd1234'
}
}
;
assert
.
strictEqual
(
profile
[
'owl:sameAs'
]
[
'@id'
]
,
'did:nostr:abcd1234'
)
;
}
)
;
}
)
;
describe
(
'Nostr Auth with DID Resolution'
,
(
)
=>
{
before
(
async
(
)
=>
{
await
startTestServer
(
)
;
}
)
;
after
(
async
(
)
=>
{
await
stopTestServer
(
)
;
clearCache
(
)
;
}
)
;
it
(
'should create a pod for DID testing'
,
async
(
)
=>
{
const
result
=
await
createTestPod
(
'nostrtest'
)
;
assert
.
ok
(
result
.
webId
,
'Should have webId'
)
;
assert
.
ok
(
result
.
token
,
'Should have token'
)
;
}
)
;
it
(
'should accept valid NIP-98 auth header'
,
async
(
)
=>
{
// Generate a Nostr keypair
const
sk
=
generateSecretKey
(
)
;
const
pubkey
=
getPublicKey
(
sk
)
;
// Create the pod for this pubkey
const
podName
=
pubkey
.
substring
(
0
,
16
)
;
await
createTestPod
(
podName
)
;
// Create a NIP-98 event
const
baseUrl
=
getBaseUrl
(
)
;
const
event
=
finalizeEvent
(
{
kind
:
27235
,
created_at
:
Math
.
floor
(
Date
.
now
(
)
/
1000
)
,
tags
:
[
[
'u'
,
`
${
baseUrl
}
/
${
podName
}
/public/`
]
,
[
'method'
,
'GET'
]
]
,
content
:
''
}
,
sk
)
;
// Encode as base64
const
token
=
Buffer
.
from
(
JSON
.
stringify
(
event
)
)
.
toString
(
'base64'
)
;
// Make request with Nostr auth
const
res
=
await
fetch
(
`
${
baseUrl
}
/
${
podName
}
/public/`
,
{
headers
:
{
'Authorization'
:
`Nostr
${
token
}
`
}
}
)
;
// Should succeed (200) - the Nostr auth should work
// Even without DID resolution, did:nostr:<pubkey> is accepted
assertStatus
(
res
,
200
)
;
}
)
;
it
(
'should return did:nostr when no WebID linked'
,
async
(
)
=>
{
const
sk
=
generateSecretKey
(
)
;
const
pubkey
=
getPublicKey
(
sk
)
;
// Try to resolve - should return null since no alsoKnownAs
const
result
=
await
resolveDidNostrToWebId
(
pubkey
)
;
assert
.
strictEqual
(
result
,
null
,
'Should return null when no WebID linked'
)
;
}
)
;
}
)
;
describe
(
'Same-origin shortcut removed — backlink always verified'
,
(
)
=>
{
// The previous same-origin shortcut returned a WebID without
// checking that the WebID profile actually claimed the pubkey.
// On a multi-tenant origin where one user controls
// /.well-known/did/nostr/<theirPubkey>.json and another user
// controls /<other>/profile/card, the attacker could publish
// a DID doc with `alsoKnownAs` pointing at the OTHER user's
// WebID and the resolver would accept it.
//
// We can't drive the live resolver here because
// validateExternalUrl unconditionally blocks loopback (the
// only thing a unit test can bind to), so we exercise the
// CID-VM backlink check directly via the exposed test seam
// with in-memory profiles. The full multi-tenant flow is
// observable in production once a public host is involved.
let
attackPubkey
;
let
legitPubkey
;
let
legitX
;
let
legitY
;
before
(
async
(
)
=>
{
const
{
secp256k1
}
=
await
import
(
'@noble/curves/secp256k1'
)
;
// Use real on-curve keys; we need a valid point to match the
// verifier's BIP-340 even-y derivation.
legitPubkey
=
getPublicKey
(
generateSecretKey
(
)
)
;
attackPubkey
=
getPublicKey
(
generateSecretKey
(
)
)
;
const
point
=
secp256k1
.
ProjectivePoint
.
fromHex
(
'02'
+
legitPubkey
)
;
const
yHex
=
point
.
toAffine
(
)
.
y
.
toString
(
16
)
.
padStart
(
64
,
'0'
)
;
const
b64u
=
(
hex
)
=>
Buffer
.
from
(
hex
,
'hex'
)
.
toString
(
'base64'
)
.
replace
(
/
\+
/
g
,
'-'
)
.
replace
(
/
\/
/
g
,
'_'
)
.
replace
(
/
=
+
$
/
,
''
)
;
legitX
=
b64u
(
legitPubkey
)
;
legitY
=
b64u
(
yHex
)
;
clearCache
(
)
;
}
)
;
it
(
'checkCidVmBacklink: accepts a profile with matching VM in authentication'
,
async
(
)
=>
{
const
{
_checkCidVmBacklinkForTests
}
=
await
import
(
'../src/auth/did-nostr.js'
)
;
const
subj
=
`http://example.test/profile/card#me`
;
const
profile
=
{
'@id'
:
subj
,
verificationMethod
:
[
{
id
:
`
${
subj
.
replace
(
'#me'
,
''
)
}
#k`
,
type
:
'JsonWebKey'
,
controller
:
subj
,
publicKeyJwk
:
{
kty
:
'EC'
,
crv
:
'secp256k1'
,
x
:
legitX
,
y
:
legitY
}
,
}
]
,
authentication
:
[
`
${
subj
.
replace
(
'#me'
,
''
)
}
#k`
]
,
}
;
assert
.
strictEqual
(
_checkCidVmBacklinkForTests
(
profile
,
legitPubkey
)
,
true
)
;
}
)
;
it
(
'checkCidVmBacklink: rejects a profile without the VM'
,
async
(
)
=>
{
const
{
_checkCidVmBacklinkForTests
}
=
await
import
(
'../src/auth/did-nostr.js'
)
;
const
profile
=
{
'@id'
:
'http://example.test/profile/card#me'
,
verificationMethod
:
[
]
,
authentication
:
[
]
,
}
;
// attackPubkey (all-a) isn't in the profile — multi-tenant
// attack scenario.
assert
.
strictEqual
(
_checkCidVmBacklinkForTests
(
profile
,
attackPubkey
)
,
false
)
;
}
)
;
it
(
'checkCidVmBacklink: rejects a VM in verificationMethod but NOT in authentication'
,
async
(
)
=>
{
const
{
_checkCidVmBacklinkForTests
}
=
await
import
(
'../src/auth/did-nostr.js'
)
;
const
subj
=
`http://example.test/profile/card#me`
;
const
profile
=
{
'@id'
:
subj
,
verificationMethod
:
[
{
id
:
`
${
subj
.
replace
(
'#me'
,
''
)
}
#k`
,
type
:
'JsonWebKey'
,
controller
:
subj
,
publicKeyJwk
:
{
kty
:
'EC'
,
crv
:
'secp256k1'
,
x
:
legitX
,
y
:
legitY
}
,
}
]
,
authentication
:
[
]
,
// <-- not declared for auth
}
;
assert
.
strictEqual
(
_checkCidVmBacklinkForTests
(
profile
,
legitPubkey
)
,
false
)
;
}
)
;
it
(
'checkCidVmBacklink: handles relative @id when docUrl is supplied'
,
async
(
)
=>
{
const
{
_checkCidVmBacklinkForTests
}
=
await
import
(
'../src/auth/did-nostr.js'
)
;
const
docUrl
=
'http://example.test/profile/card.jsonld'
;
// Relative subject AND absolute VM IDs (a common mixed shape).
// Without the docUrl fallback, base would be empty and the
// authentication-membership check would silently fail.
const
profile
=
{
'@id'
:
'#me'
,
verificationMethod
:
[
{
id
:
`
${
docUrl
}
#k`
,
type
:
'JsonWebKey'
,
controller
:
`
${
docUrl
}
#me`
,
publicKeyJwk
:
{
kty
:
'EC'
,
crv
:
'secp256k1'
,
x
:
legitX
,
y
:
legitY
}
,
}
]
,
authentication
:
[
`
${
docUrl
}
#k`
]
,
}
;
assert
.
strictEqual
(
_checkCidVmBacklinkForTests
(
profile
,
legitPubkey
,
docUrl
)
,
true
)
;
}
)
;
it
(
'checkCidVmBacklink: rejects when VM controller is not in expected set'
,
async
(
)
=>
{
const
{
_checkCidVmBacklinkForTests
}
=
await
import
(
'../src/auth/did-nostr.js'
)
;
const
subj
=
'http://example.test/profile/card#me'
;
// VM controller points at a totally different origin — would
// be a planted-key attack. Resource-side verifier rejects this;
// CID-VM backlink must agree.
const
profile
=
{
'@id'
:
subj
,
// No top-level controller — defaults to subject.
verificationMethod
:
[
{
id
:
`
${
subj
.
replace
(
'#me'
,
''
)
}
#k`
,
type
:
'JsonWebKey'
,
controller
:
'http://attacker.example/me'
,
publicKeyJwk
:
{
kty
:
'EC'
,
crv
:
'secp256k1'
,
x
:
legitX
,
y
:
legitY
}
,
}
]
,
authentication
:
[
`
${
subj
.
replace
(
'#me'
,
''
)
}
#k`
]
,
}
;
assert
.
strictEqual
(
_checkCidVmBacklinkForTests
(
profile
,
legitPubkey
)
,
false
)
;
}
)
;
it
(
'checkCidVmBacklink: rejects a VM with NO explicit controller (matches resource-side strictness)'
,
async
(
)
=>
{
// Earlier passes had a permissive branch that accepted a
// controller-less VM if the VM ID and the profile subject
// shared an origin. That made backlink looser than the
// resource-side verifier, opening a binding-rule mismatch
// (DID resolution would say "yes" for keys the LWS10-CID
// verifier would later reject). Both layers now require
// an explicit controller.
const
{
_checkCidVmBacklinkForTests
}
=
await
import
(
'../src/auth/did-nostr.js'
)
;
const
subj
=
'http://example.test/profile/card#me'
;
const
profile
=
{
'@id'
:
subj
,
verificationMethod
:
[
{
id
:
`
${
subj
.
replace
(
'#me'
,
''
)
}
#k`
,
type
:
'JsonWebKey'
,
// controller intentionally absent
publicKeyJwk
:
{
kty
:
'EC'
,
crv
:
'secp256k1'
,
x
:
legitX
,
y
:
legitY
}
,
}
]
,
authentication
:
[
`
${
subj
.
replace
(
'#me'
,
''
)
}
#k`
]
,
}
;
assert
.
strictEqual
(
_checkCidVmBacklinkForTests
(
profile
,
legitPubkey
)
,
false
)
;
}
)
;
}
)
;
describe
(
'Pubkey input validation'
,
(
)
=>
{
// Pubkey is attacker-controlled (NIP-98 event) and is
// interpolated into the resolver URL path and cache key.
// Length-only validation lets a malicious pubkey containing
// `/` or other chars rewrite the URL path on the resolver
// origin and pollute the cache.
it
(
'rejects non-hex pubkeys without making any request'
,
async
(
)
=>
{
// 64-character pubkey containing a path separator —
// length-only validation would let this through and
// cause an arbitrary-path fetch on the resolver origin.
const
evil
=
'a'
.
repeat
(
31
)
+
'/'
+
'b'
.
repeat
(
32
)
;
assert
.
strictEqual
(
evil
.
length
,
64
)
;
const
out
=
await
resolveDidNostrToWebId
(
evil
,
'http://nonexistent.invalid:1'
)
;
assert
.
strictEqual
(
out
,
null
)
;
}
)
;
it
(
'rejects too-short and non-string pubkeys'
,
async
(
)
=>
{
assert
.
strictEqual
(
await
resolveDidNostrToWebId
(
'abc'
)
,
null
)
;
assert
.
strictEqual
(
await
resolveDidNostrToWebId
(
null
)
,
null
)
;
assert
.
strictEqual
(
await
resolveDidNostrToWebId
(
42
)
,
null
)
;
assert
.
strictEqual
(
await
resolveDidNostrToWebId
(
'a'
.
repeat
(
63
)
)
,
null
)
;
assert
.
strictEqual
(
await
resolveDidNostrToWebId
(
'a'
.
repeat
(
65
)
)
,
null
)
;
}
)
;
}
)
;
describe
(
'Cache key includes resolverUrl'
,
(
)
=>
{
// Cross-resolver leakage: different resolvers can legitimately
// disagree about the same pubkey (one might have a DID doc,
// another not). Keying the cache only on pubkey would let a
// hit from one resolver suppress a real lookup against another.
before
(
(
)
=>
clearCache
(
)
)
;
it
(
'does NOT share cache entries across resolvers'
,
async
(
)
=>
{
// Both calls hit unresolvable hosts → both cache as
// failureTtl. Crucially, they cache under DIFFERENT keys, so
// the cache size grows by 2 (not 1).
const
pk
=
'a'
.
repeat
(
64
)
;
const
sizeBefore
=
_cacheSizeForTests
(
)
;
await
resolveDidNostrToWebId
(
pk
,
'http://nonexistent-a.invalid:1'
)
;
await
resolveDidNostrToWebId
(
pk
,
'http://nonexistent-b.invalid:1'
)
;
const
sizeAfter
=
_cacheSizeForTests
(
)
;
assert
.
strictEqual
(
sizeAfter
-
sizeBefore
,
2
,
'each resolver+pubkey pair should cache independently'
)
;
}
)
;
}
)
;
describe
(
'Cache bounding'
,
(
)
=>
{
// The cache is keyed by attacker-controlled NIP-98 pubkeys.
// Without an LRU cap a stream of unique pubkeys would grow
// memory without limit. Drive the cap directly via the
// SSRF / unknown-resolver path (every lookup gets cached as
// a transient failure) and assert the size never exceeds
// CACHE_MAX_ENTRIES.
before
(
(
)
=>
clearCache
(
)
)
;
it
(
'cache stays at-or-under CACHE_MAX_ENTRIES after a burst of misses'
,
async
(
)
=>
{
// Use an unreachable resolver so every lookup fast-fails
// and gets cached as `failureTtl: true`. The LRU code is
// mechanical (set + while size > cap → delete oldest),
// so a small N is enough to assert the invariant
// `size <= cap`.
assert
.
ok
(
_CACHE_MAX_FOR_TESTS
>=
1
,
'cap must be positive'
)
;
const
N
=
5
;
const
before
=
_cacheSizeForTests
(
)
;
for
(
let
i
=
0
;
i
<
N
;
i
++
)
{
const
pk
=
i
.
toString
(
16
)
.
padStart
(
64
,
'0'
)
;
// Force a network failure → cached as failureTtl
await
resolveDidNostrToWebId
(
pk
,
'http://nonexistent.invalid:1'
)
;
}
const
after
=
_cacheSizeForTests
(
)
;
assert
.
ok
(
after
-
before
<=
N
,
'cache shouldn\'t grow more than N'
)
;
assert
.
ok
(
after
<=
_CACHE_MAX_FOR_TESTS
,
`cache size
${
after
}
> cap
${
_CACHE_MAX_FOR_TESTS
}
`
)
;
}
)
;
}
)
;
describe
(
'fetchWithRedirectGuard SSRF / redirect hardening'
,
(
)
=>
{
// The production resolver wraps fetchWithRedirectGuard with
// validateExternalUrl as a hard SSRF gate, which by design
// rejects loopback (`127.0.0.1`) — the only thing a unit test
// can bind to. So testing the resolver end-to-end against a
// local server makes the redirect/cap logic invisible: every
// request fails on the SSRF guard before fetch is even called.
//
// Solution: import fetchWithRedirectGuard directly and inject
// a permissive `_validateUrl` stub. That isolates the redirect
// hop counter, cross-origin check, and size cap from the SSRF
// gate so we can actually observe each one.
let
http
;
let
server
;
let
port
;
let
hopMode
=
'cross-origin'
;
let
fetchWithRedirectGuard
;
const
allowAll
=
async
(
)
=>
(
{
valid
:
true
}
)
;
before
(
async
(
)
=>
{
http
=
await
import
(
'node:http'
)
;
(
{
fetchWithRedirectGuard
}
=
await
import
(
'../src/auth/did-nostr.js'
)
)
;
server
=
http
.
createServer
(
(
req
,
res
)
=>
{
if
(
hopMode
===
'cross-origin'
)
{
res
.
writeHead
(
302
,
{
Location
:
'http://other.example:1/foo.json'
}
)
;
res
.
end
(
)
;
return
;
}
if
(
hopMode
===
'loop'
)
{
// Each hop appends `/r` to the path; the cap fires before
// we ever return a non-3xx.
res
.
writeHead
(
302
,
{
Location
:
req
.
url
+
'/r'
}
)
;
res
.
end
(
)
;
return
;
}
if
(
hopMode
===
'oversize'
)
{
res
.
writeHead
(
200
,
{
'Content-Type'
:
'application/json'
}
)
;
// Stream a body larger than the 1 KB cap we'll pass.
res
.
end
(
'"'
+
'x'
.
repeat
(
2000
)
+
'"'
)
;
return
;
}
if
(
hopMode
===
'ok'
)
{
res
.
writeHead
(
200
,
{
'Content-Type'
:
'application/json'
}
)
;
res
.
end
(
'{"ok":true}'
)
;
return
;
}
res
.
writeHead
(
404
)
.
end
(
)
;
}
)
;
await
new
Promise
(
(
resolve
)
=>
server
.
listen
(
0
,
'127.0.0.1'
,
resolve
)
)
;
port
=
server
.
address
(
)
.
port
;
}
)
;
after
(
async
(
)
=>
{
await
new
Promise
(
(
resolve
)
=>
server
.
close
(
resolve
)
)
;
}
)
;
it
(
'refuses cross-origin redirects'
,
async
(
)
=>
{
hopMode
=
'cross-origin'
;
await
assert
.
rejects
(
(
)
=>
fetchWithRedirectGuard
(
`http://127.0.0.1:
${
port
}
/foo.json`
,
{
_validateUrl
:
allowAll
}
)
,
/
c
r
o
s
s
-
o
r
i
g
i
n
r
e
d
i
r
e
c
t
r
e
f
u
s
e
d
/
,
)
;
}
)
;
it
(
'refuses redirect chains exceeding the hop cap'
,
async
(
)
=>
{
hopMode
=
'loop'
;
await
assert
.
rejects
(
(
)
=>
fetchWithRedirectGuard
(
`http://127.0.0.1:
${
port
}
/start`
,
{
_validateUrl
:
allowAll
}
)
,
/
t
o
o
m
a
n
y
r
e
d
i
r
e
c
t
s
/
,
)
;
}
)
;
it
(
'refuses oversized response bodies'
,
async
(
)
=>
{
hopMode
=
'oversize'
;
await
assert
.
rejects
(
(
)
=>
fetchWithRedirectGuard
(
`http://127.0.0.1:
${
port
}
/big`
,
{
_validateUrl
:
allowAll
,
maxBytes
:
1000
,
}
)
,
/
r
e
s
p
o
n
s
e
t
o
o
l
a
r
g
e
/
,
)
;
}
)
;
it
(
're-runs SSRF validation on every hop'
,
async
(
)
=>
{
hopMode
=
'loop'
;
let
calls
=
0
;
const
counting
=
async
(
url
)
=>
{
calls
++
;
return
{
valid
:
true
}
;
}
;
await
assert
.
rejects
(
(
)
=>
fetchWithRedirectGuard
(
`http://127.0.0.1:
${
port
}
/start`
,
{
_validateUrl
:
counting
}
)
,
/
t
o
o
m
a
n
y
r
e
d
i
r
e
c
t
s
/
,
)
;
// 1 initial + MAX_REDIRECTS (5) hops = 6 calls if we re-validate
// on every hop. < 6 means the per-hop check is missing.
assert
.
ok
(
calls
>=
6
,
`expected ≥6 validator calls, got
${
calls
}
`
)
;
}
)
;
it
(
'returns the response body on success'
,
async
(
)
=>
{
hopMode
=
'ok'
;
const
r
=
await
fetchWithRedirectGuard
(
`http://127.0.0.1:
${
port
}
/ok`
,
{
_validateUrl
:
allowAll
}
)
;
assert
.
strictEqual
(
r
.
status
,
200
)
;
assert
.
strictEqual
(
r
.
body
,
'{"ok":true}'
)
;
}
)
;
}
)
;
describe
(
'Real DID Document Fetch'
,
(
)
=>
{
before
(
(
)
=>
{
clearCache
(
)
;
}
)
;
it
(
'should fetch DID document from nostr.social'
,
async
(
)
=>
{
// Use a known pubkey that exists on nostr.social
// fiatjaf's pubkey
const
pubkey
=
'3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d'
;
// This should not throw, just return null if no WebID linked
const
result
=
await
resolveDidNostrToWebId
(
pubkey
)
;
// fiatjaf likely doesn't have a WebID linked, so expect null
// But the fetch itself should work without error
assert
.
strictEqual
(
result
,
null
,
'Should return null when no bidirectional link'
)
;
}
)
;
it
(
'should cache DID resolution results'
,
async
(
)
=>
{
const
pubkey
=
'3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d'
;
// First call
const
start1
=
Date
.
now
(
)
;
await
resolveDidNostrToWebId
(
pubkey
)
;
const
time1
=
Date
.
now
(
)
-
start1
;
// Second call should be cached (much faster)
const
start2
=
Date
.
now
(
)
;
await
resolveDidNostrToWebId
(
pubkey
)
;
const
time2
=
Date
.
now
(
)
-
start2
;
// Cached call should be < 5ms typically
assert
.
ok
(
time2
<
time1
||
time2
<
10
,
`Cached call should be fast. First:
${
time1
}
ms, Second:
${
time2
}
ms`
)
;
}
)
;
}
)
;
}
)
;
Back
|
FazBrowse Home
|
New Git URL