You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
createRootPodStructure (and createPodStructure) bake the request-time host into every .acl file via absolute acl:accessTo / acl:default / acl:agent URIs. The pod is then locked to the hostname captured at pod-creation time — accessing the same server via any other host returns 401/403, even for foaf:Agent public-read ACLs.
The read side has supported relative URIs since a736338 (Jan 2026). The write side never got the symmetric treatment. This issue tracks bringing the two sides into alignment, in phases, and supersedes #303.
The seeded /.acl contains acl:accessTo: { '@id': 'http://localhost:4444/' }. The WAC checker does string equality on the resolved accessTo vs. the request URL (src/wac/checker.js:203), so any other host fails to match — even though the rule is foaf:Agent and identity isn't involved.
Root cause
Two pieces, introduced together in v0.0.79 (PR #77 / commit 8adacaa, "feat: Add single-user mode"):
src/server.js:556 explicitly remaps 0.0.0.0 → localhost when computing the pod's baseUrl, freezing one host into the pod identity.
src/handlers/container.js:188 and src/server.js's createRootPodStructure call generateOwnerAcl(podUri, …) (and siblings) with absolute podUri, which src/wac/parser.js:226 writes verbatim into JSON-LD acl:accessTo / acl:default.
Both ends could be relative. The parser at src/wac/parser.js:141 already calls resolveUri(uri, baseUrl) against the ACL's own request URL, so a relative ./ would always match the requesting host.
Prior art
This is a recurring class of bug. All previously closed:
acl:accessTo and acl:default — pure URL matching, no identity. Resolves against ACL URL ⇒ portable across hosts.
acl:agent — also resolves against ACL URL (PR Fix relative URI resolution for acl:agent #65). Makes the on-disk pod portable; pod can be moved to another domain without rewriting ACLs.
What stays absolute
The WebID exposed in the user's profile document (/profile/card.jsonld#me) — this is the global identifier dereferenced by other servers, OIDC webid claim, etc.
The solid:oidcIssuer predicate in the profile.
What this doesn't fix
Owner write across different hosts (auth on host A, request on host B). The token's webid claim is frozen at auth time; relative acl:agent resolves against the request host. They'll mismatch. Needs a separate normalization pass in the auth layer (e.g. --canonical-host flag, or treat localhost ≡ 127.0.0.1 ≡ 0.0.0.0 ≡ configured-host as equivalent identities). Tracked as Phase 4 below; will likely spin off into its own design issue.
Phases
Each phase is independently shippable.
Phase 1 — Relative accessTo / default in generators
Scope. Update generateOwnerAcl, generatePrivateAcl, generateInboxAcl, generatePublicFolderAcl, generatePublicReadAcl in src/wac/parser.js so acl:accessTo and acl:default are emitted as relative URIs (./, ./private/, etc.) rather than absolute. Update callers in src/handlers/container.js and src/server.js (createRootPodStructure) to pass the relative path string instead of ${podUri}….
Unblocks. Public read across hosts. The fonstr-style welcome-page case works on localhost / 0.0.0.0 / 127.0.0.1 / LAN IP / public domain without further change.
Tests.
Unit: each generator emits the relative form.
Integration: --single-user --no-idp server, GET / from each of 4+ hosts → 200.
Regression: existing absolute-URI ACLs on disk still parse and authorize correctly (parser handles both).
Risk. Low. Read side already resolves both forms; this is a write-side change only.
Phase 2 — Relative acl:agent in generators
Scope. Same generators emit acl:agent: { '@id': './profile/card.jsonld#me' } (or whatever the relative form is) when the WebID is hosted under the same pod. PR #65 already proved the parser handles this.
Unblocks. Owner read/write when authed and hitting via the same host (the common single-machine case). Pod becomes portable on disk — operators can mv a pod to a new domain without rewriting ACLs.
Tests.
Unit: each generator emits relative agent for in-pod WebIDs.
Integration: owner authenticates via canonical host, performs reads/writes — same ACL file, no host baked in.
Move test: serialize a pod, point a new server at it on a different host, owner login still works.
Risk. Low. Doesn't change the absolute WebID in the profile document.
Scope. Bring seedServerRoot from PR #303 onto the new generators. Now portable by construction. Multi-user deployments get a default landing page at / instead of a raw container listing. Operator-provided /index.html preserved (skip-if-exists). Close #303 once this lands.
Scope. Reconcile token webid (from auth time) against ACL acl:agent (resolved at request time) when they refer to the same identity but different hosts. Likely shape: a --canonical-host flag (or auto-derived from the OIDC issuer) that the WAC checker uses to normalize both sides before comparison. Touches OIDC, Nostr auth, Schnorr SSO.
Unblocks. Owner write when authed on host A and hitting host B (mobile/LAN/tunnel scenarios — the "fonstr just works everywhere" promise).
Risk. Medium-high. Worth its own design issue before implementation. Will reference back here.
Acceptance criteria
Phase 1: public read works on every interface the server binds, on a fresh single-user pod.
Phase 2: owner read/write works from the canonical host; pod directory can be moved between hostnames without ACL rewrites.
Problem
createRootPodStructure(andcreatePodStructure) bake the request-time host into every.aclfile via absoluteacl:accessTo/acl:default/acl:agentURIs. The pod is then locked to the hostname captured at pod-creation time — accessing the same server via any other host returns 401/403, even forfoaf:Agentpublic-read ACLs.The read side has supported relative URIs since
a736338(Jan 2026). The write side never got the symmetric treatment. This issue tracks bringing the two sides into alignment, in phases, and supersedes #303.Phase tracker
acl:accessTo/acl:defaultin generators — Phase 1: emit relative acl:accessTo / acl:default in ACL generators (#427) #428 (PR feat: emit relative acl:accessTo / acl:default in pod creation (#428) #429, merged)acl:agentin generators — Phase 2: emit relative acl:agent in ACL generators (#427) #430 (PR feat: emit relative acl:agent in pod creation (#430) #431, merged)Reproduction
The seeded
/.aclcontainsacl:accessTo: { '@id': 'http://localhost:4444/' }. The WAC checker does string equality on the resolvedaccessTovs. the request URL (src/wac/checker.js:203), so any other host fails to match — even though the rule isfoaf:Agentand identity isn't involved.Root cause
Two pieces, introduced together in v0.0.79 (PR #77 / commit
8adacaa, "feat: Add single-user mode"):src/server.js:556explicitly remaps0.0.0.0→localhostwhen computing the pod'sbaseUrl, freezing one host into the pod identity.src/handlers/container.js:188andsrc/server.js'screateRootPodStructurecallgenerateOwnerAcl(podUri, …)(and siblings) with absolutepodUri, whichsrc/wac/parser.js:226writes verbatim into JSON-LDacl:accessTo/acl:default.Both ends could be relative. The parser at
src/wac/parser.js:141already callsresolveUri(uri, baseUrl)against the ACL's own request URL, so a relative./would always match the requesting host.Prior art
This is a recurring class of bug. All previously closed:
./reads work sincea736338)acl:agentalso resolved against ACL URL sincebdbbbb7)seedServerRootthat does emit relativeaccessTo: '/', but only for the multi-user landing-page path, and was never merged. This issue supersedes feat: default landing page + ACL at server root (#276) #303.Design
What can be relative
acl:accessToandacl:default— pure URL matching, no identity. Resolves against ACL URL ⇒ portable across hosts.acl:agent— also resolves against ACL URL (PR Fix relative URI resolution for acl:agent #65). Makes the on-disk pod portable; pod can be moved to another domain without rewriting ACLs.What stays absolute
/profile/card.jsonld#me) — this is the global identifier dereferenced by other servers, OIDCwebidclaim, etc.solid:oidcIssuerpredicate in the profile.What this doesn't fix
webidclaim is frozen at auth time; relativeacl:agentresolves against the request host. They'll mismatch. Needs a separate normalization pass in the auth layer (e.g.--canonical-hostflag, or treat localhost ≡ 127.0.0.1 ≡ 0.0.0.0 ≡ configured-host as equivalent identities). Tracked as Phase 4 below; will likely spin off into its own design issue.Phases
Each phase is independently shippable.
Phase 1 — Relative
accessTo/defaultin generatorsScope. Update
generateOwnerAcl,generatePrivateAcl,generateInboxAcl,generatePublicFolderAcl,generatePublicReadAclinsrc/wac/parser.jssoacl:accessToandacl:defaultare emitted as relative URIs (./,./private/, etc.) rather than absolute. Update callers insrc/handlers/container.jsandsrc/server.js(createRootPodStructure) to pass the relative path string instead of${podUri}….Unblocks. Public read across hosts. The fonstr-style welcome-page case works on localhost / 0.0.0.0 / 127.0.0.1 / LAN IP / public domain without further change.
Tests.
--single-user --no-idpserver,GET /from each of 4+ hosts → 200.Risk. Low. Read side already resolves both forms; this is a write-side change only.
Phase 2 — Relative
acl:agentin generatorsScope. Same generators emit
acl:agent: { '@id': './profile/card.jsonld#me' }(or whatever the relative form is) when the WebID is hosted under the same pod. PR #65 already proved the parser handles this.Unblocks. Owner read/write when authed and hitting via the same host (the common single-machine case). Pod becomes portable on disk — operators can
mva pod to a new domain without rewriting ACLs.Tests.
Risk. Low. Doesn't change the absolute WebID in the profile document.
Phase 3 — Fold in #303 (landing page seeding)
Scope. Bring
seedServerRootfrom PR #303 onto the new generators. Now portable by construction. Multi-user deployments get a default landing page at/instead of a raw container listing. Operator-provided/index.htmlpreserved (skip-if-exists). Close #303 once this lands.Tests. Carry forward the four tests from PR #303.
Risk. Low — UX-only at this point. The ACL machinery is already proven by Phases 1–2.
Phase 4 — Cross-host auth normalization (separate sub-issue)
Scope. Reconcile token
webid(from auth time) against ACLacl:agent(resolved at request time) when they refer to the same identity but different hosts. Likely shape: a--canonical-hostflag (or auto-derived from the OIDC issuer) that the WAC checker uses to normalize both sides before comparison. Touches OIDC, Nostr auth, Schnorr SSO.Unblocks. Owner write when authed on host A and hitting host B (mobile/LAN/tunnel scenarios — the "fonstr just works everywhere" promise).
Risk. Medium-high. Worth its own design issue before implementation. Will reference back here.
Acceptance criteria
Out of scope