Skip to content

ACL generators emit absolute URIs, host-locking pods (supersedes #303) #427

@melvincarvalho

Description

@melvincarvalho

Problem

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.

Phase tracker

Reproduction

jss start --single-user --no-idp --port 4444
# (or: npx fonstr 4444)
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:4444/   # 200
curl -s -o /dev/null -w "%{http_code}\n" http://0.0.0.0:4444/     # 401
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:4444/   # 401
curl -s -o /dev/null -w "%{http_code}\n" http://<lan-ip>:4444/    # 401

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"):

  1. src/server.js:556 explicitly remaps 0.0.0.0localhost when computing the pod's baseUrl, freezing one host into the pod identity.
  2. 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:

Design

What can be relative

  • 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.

Phase 3 — Fold in #303 (landing page seeding)

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.

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 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.
  • Phase 3: default landing page seeded for multi-user deployments, portable by construction. PR feat: default landing page + ACL at server root (#276) #303 closed.
  • Phase 4: separate issue filed with design proposal.
  • No regression for existing absolute-URI ACL files on disk.

Out of scope

  • Hostless (DID-based) WebIDs — interesting but a much bigger conversation.
  • Auto-detecting and rewriting old absolute-URI ACLs in existing pods (operators can leave them; the parser handles both).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions