66// nip05/ and webfinger/ lean on). Default server is NOT public-read, so
77// the anonymous 200 demonstrates the exemption, not a permissive default.
88// * The pathed per-pod DID at `/<user>/did.json` — which lands INSIDE the
9- // pod's WAC-governed /<user>/ namespace. The root /.acl JSS seeds is
10- // public-read on the container ONLY (no acl:default), so a child did.json
11- // is NOT inherited-public: the test writes an explicit public-read
12- // /<user>/did.json.acl to make did:web pathed resolution work, proving the
13- // finding that the pod owner must grant public Read there. The
14- // contract-safe `<prefix>/<user>/did.json` mount is asserted to be
15- // identical and needs no such grant.
9+ // pod's WAC-governed /<user>/ namespace. Since JSS 0.0.219 the plugin
10+ // reserves the parameterized shape via api.reservePath('/:user/did.json')
11+ // (#602), so the anonymous GET answers with NO ACL grant and NO appPaths
12+ // in the config (this config passes neither). The exact-shape guarantee
13+ // is asserted from both sides: the DID document resolves anonymously,
14+ // while a sibling pod resource, a deeper same-basename path, and a write
15+ // to did.json itself all stay WAC-denied. The contract-safe
16+ // `<prefix>/<user>/did.json` mount is asserted to be identical.
1617
1718import { describe , it , before , after } from 'node:test' ;
1819import assert from 'node:assert' ;
@@ -26,8 +27,6 @@ import { probePort, startJss } from '../helpers.js';
2627const __dirname = path . dirname ( fileURLToPath ( new URL ( import . meta. url ) ) ) ;
2728const PLUGIN = path . join ( __dirname , 'plugin.js' ) ;
2829
29- const ACL_CTX = { acl : 'http://www.w3.org/ns/auth/acl#' , foaf : 'http://xmlns.com/foaf/0.1/' } ;
30-
3130/** Write a pod WebID card, optionally carrying a secp256k1 verificationMethod. */
3231function writeCard ( podDir , webId , secpMultibase ) {
3332 fs . mkdirSync ( path . join ( podDir , 'profile' ) , { recursive : true } ) ;
@@ -43,20 +42,6 @@ function writeCard(podDir, webId, secpMultibase) {
4342 fs . writeFileSync ( path . join ( podDir , 'profile' , 'card.jsonld' ) , JSON . stringify ( card , null , 2 ) ) ;
4443}
4544
46- /** Grant anonymous (foaf:Agent) Read on a single resource URL via its .acl. */
47- function writePublicReadAcl ( aclFilePath , resourceUrl ) {
48- fs . writeFileSync ( aclFilePath , JSON . stringify ( {
49- '@context' : ACL_CTX ,
50- '@graph' : [ {
51- '@id' : '#public' ,
52- '@type' : 'acl:Authorization' ,
53- 'acl:agentClass' : { '@id' : 'foaf:Agent' } ,
54- 'acl:accessTo' : { '@id' : resourceUrl } ,
55- 'acl:mode' : [ { '@id' : 'acl:Read' } ] ,
56- } ] ,
57- } , null , 2 ) ) ;
58- }
59-
6045/** A fresh, valid, on-curve secp256k1 pubkey as an f-form Multikey. */
6146function freshSecpFForm ( ) {
6247 const pub = secp256k1 . getPublicKey ( secp256k1 . utils . randomPrivateKey ( ) , true ) ; // compressed
@@ -76,10 +61,10 @@ describe('didweb plugin', () => {
7661 encHost = new URL ( base ) . host . replace ( / : / g, '%3A' ) ; // 127.0.0.1%3A<port>
7762
7863 const root = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'jss-didweb-' ) ) ;
79- // alice: pod with a provisioned secp256k1 key + a public-read did.json ACL
80- // so the ABSOLUTE pathed did:web location resolves anonymously.
64+ // alice: pod with a provisioned secp256k1 key. NO did.json ACL: the
65+ // ABSOLUTE pathed did:web location must resolve anonymously on the
66+ // strength of the parameterized reservation alone (#602).
8167 writeCard ( path . join ( root , 'alice' ) , `${ base } /alice/profile/card#me` , freshSecpFForm ( ) ) ;
82- writePublicReadAcl ( path . join ( root , 'alice' , 'did.json.acl' ) , `${ base } /alice/did.json` ) ;
8368 // carol: pod with NO key → the plugin mints + persists an Ed25519 VM.
8469 writeCard ( path . join ( root , 'carol' ) , `${ base } /carol/profile/card#me` ) ;
8570 // a dot-guarded tree that must never be treated as a pod.
@@ -94,6 +79,8 @@ describe('didweb plugin', () => {
9479 // No baseUrl: the did:web host + service URLs now come from
9580 // api.serverInfo() (#601). The encHost assertions below (derived from
9681 // the real probed origin) must still hold — that's the retrofit proof.
82+ // No appPaths either (there is nowhere to pass one): the absolute
83+ // pathed mount is WAC-exempted by api.reservePath alone (#602).
9784 config : { podsRoot : root , actorPathTemplate : '/ap/<user>/actor' } ,
9885 } ] ,
9986 } ) ;
@@ -142,16 +129,41 @@ describe('didweb plugin', () => {
142129 assert . strictEqual ( ap . serviceEndpoint , `${ base } /ap/alice/actor` , 'AP actor service from actorPathTemplate' ) ;
143130 } ) ;
144131
145- it ( 'resolves the ABSOLUTE pathed did:web location once the pod grants public Read' , async ( ) => {
132+ it ( 'resolves the ABSOLUTE pathed did:web location anonymously via the parameterized reservation' , async ( ) => {
133+ // No ACL grant, no appPaths: api.reservePath('/:user/did.json') (#602)
134+ // alone makes the spec-pinned URL answer inside the WAC-governed pod
135+ // namespace — the wall README finding 2 documented, closed in JSS 0.0.219.
146136 const res = await fetch ( `${ base } /alice/did.json` ) ;
147- assert . strictEqual ( res . status , 200 , 'anonymous GET works because /alice/did.json.acl grants public Read' ) ;
137+ assert . strictEqual ( res . status , 200 , 'anonymous GET answers with no ACL and no appPaths' ) ;
138+ assert . match ( res . headers . get ( 'content-type' ) || '' , / a p p l i c a t i o n \/ d i d \+ j s o n / ) ;
148139 const doc = await res . json ( ) ;
149140 assert . strictEqual ( doc . id , `did:web:${ encHost } :alice` ) ;
150141 // Identical to the contract-safe prefix mount.
151142 const viaPrefix = await ( await fetch ( `${ base } /didweb/alice/did.json` ) ) . json ( ) ;
152143 assert . deepStrictEqual ( doc , viaPrefix , 'absolute and prefix mounts serve the identical DID document' ) ;
153144 } ) ;
154145
146+ it ( 'does not leak the exemption beyond the exact /<user>/did.json shape' , async ( ) => {
147+ // Sibling resource in the same pod namespace: still WAC-denied anonymously.
148+ const sibling = await fetch ( `${ base } /alice/profile/card.jsonld` ) ;
149+ assert . ok ( [ 401 , 403 ] . includes ( sibling . status ) ,
150+ `sibling pod resource stays WAC-governed (got ${ sibling . status } )` ) ;
151+ // Deeper path with the same basename: /:user/did.json is an EXACT-shape
152+ // match (two segments), not a subtree claim.
153+ const deeper = await fetch ( `${ base } /alice/sub/did.json` ) ;
154+ assert . ok ( [ 401 , 403 ] . includes ( deeper . status ) ,
155+ `deeper did.json path stays WAC-governed (got ${ deeper . status } )` ) ;
156+ // Reservations are read-only by default: a write to the reserved shape
157+ // itself must still hit WAC, not fall through as an anonymous LDP PUT.
158+ const put = await fetch ( `${ base } /alice/did.json` , {
159+ method : 'PUT' ,
160+ headers : { 'content-type' : 'application/json' } ,
161+ body : '{"id":"spoofed"}' ,
162+ } ) ;
163+ assert . ok ( [ 401 , 403 ] . includes ( put . status ) ,
164+ `anonymous PUT on the reserved path is WAC-denied (got ${ put . status } )` ) ;
165+ } ) ;
166+
155167 it ( 'mints + persists an Ed25519 VM for a pod with no key in its card' , async ( ) => {
156168 const res = await fetch ( `${ base } /didweb/carol/did.json` ) ;
157169 assert . strictEqual ( res . status , 200 ) ;
0 commit comments