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
WAC (src/wac/) authorizes by WebID: to grant access, the resource owner needs to know the recipient's identity and write it into the ACL. This works poorly for:
Inbox / drop-box sharing — Alice wants a one-shot URL she can hand to a stranger that lets them write one file into her inbox, no WebID required.
Time-bound delegated reads — Alice wants to share a private resource with a colleague for 24 hours without provisioning them in her ACL.
Account recovery handoff — pair a capability URI with an OTP (see sibling issue) so the recovery link is the capability and the OTP proves channel control.
What's missing is a way to grant access by URI possession rather than by WebID. This is the capability-URL pattern (RFC-spirit: a URL is a token if it's unguessable; here we go further and make it cryptographically scoped).
Proposed design
A capability URI is a normal HTTPS URL with an opaque token segment that carries a signed claim. The server verifies the signature, checks scope/expiry/use-count, and treats the request as authorized for exactly the scope the token grants.
Signed with the issuer's Nostr key (same key the WebID's authentication set already declares, see src/auth/nostr-keys.js).
Verification path
Plugs in alongside the existing auth middleware (src/auth/middleware.js):
Detect ?cap= query param or /cap/ path segment.
Parse payload, verify signature against iss's declared keys.
Check nbf <= now < exp, requested method in scope, request URL matches res (exact or container-prefix).
Atomically decrement uses in a server-side cap-token ledger (single-use enforced even under concurrent reads).
Check constraints against the actual request (body size, content type).
If all pass: synthesize a virtual agent for the duration of this request with exactly the scope's permissions. WAC check is bypassed — the capability is the authorization.
Storage
A cap-token ledger per pod (/.cap/ledger.json or similar) tracks jti -> uses_remaining, revoked_at. Tokens that have exhausted uses or been revoked stay in the ledger until exp for replay protection.
DELETE /.well-known/cap/{jti}
Authorization: <pod owner's normal auth>
Use cases
Inbox / drop-box: Alice POSTs { res: "/inbox/", scope: ["POST"], uses: 1, constraints: { max_bytes: 5MB } } → hands the resulting URI to a stranger → they upload one file → cap auto-expires.
Time-bound delegated read: { res: "/private/q3-report.pdf", scope: ["GET"], ttl: 86400, uses: 10 } → share via email; expires after 24h or 10 fetches.
Unguessable: payload includes 32-byte jti, plus the whole token is signature-bound — forging is as hard as forging the issuer's signature.
Scope minimality: token must declare exact resource, method set, byte ceiling, content type. No wildcards in v1.
Single use by default: uses: 1 unless explicitly bumped, atomic decrement on the ledger.
No bearer leakage in logs: server must strip ?cap= from access logs; recommend path-form tokens with a documented log-scrubbing config snippet.
Referer leak: warn callers that path-form tokens can be leaked via Referer if the cap'd resource embeds external links; recommend Referrer-Policy: no-referrer on cap responses.
Revocation list: ledger keeps revoked jtis until original exp to prevent reuse via cached copies.
Issuer-key rotation: if the issuer rotates their key, all outstanding caps signed by the old key are invalidated. Document this.
Macaroons-style attenuated delegation (recipient further restricts a cap).
Group-scoped caps (anyone in {webid set} can use).
Cap chaining (B issues a sub-cap from A's cap).
UI in mashlib for managing/revoking caps.
Open questions
Path-form vs query-form? Path is more share-friendly but harder to strip from logs; query is easier to strip but uglier.
Should the cap require the recipient to authenticate (e.g. via OTP) as a second factor for write capabilities, or is possession alone sufficient?
Where in the pod tree does the cap ledger live? Needs to be writable by the cap-verification layer but not exposed via normal LDP read.
Interaction with WAC: when a cap grants access to a container, does it cascade to children by default, or must each child have its own cap? (Lean: no cascade in v1; explicit container-cap with a separate flag in v2.)
Problem
WAC (
src/wac/) authorizes by WebID: to grant access, the resource owner needs to know the recipient's identity and write it into the ACL. This works poorly for:What's missing is a way to grant access by URI possession rather than by WebID. This is the capability-URL pattern (RFC-spirit: a URL is a token if it's unguessable; here we go further and make it cryptographically scoped).
Proposed design
A capability URI is a normal HTTPS URL with an opaque token segment that carries a signed claim. The server verifies the signature, checks scope/expiry/use-count, and treats the request as authorized for exactly the scope the token grants.
URI shape
Or as a path-style token for shareability:
The token payload (JSON):
{ "v": 1, "iss": "https://alice.example.com/profile/card.jsonld#me", "res": "https://alice.example.com/private/report.pdf", "scope": ["GET"], "exp": 1700000000, "nbf": 1699990000, "uses": 1, "jti": "random-32-bytes", "constraints": { "max_bytes": 1048576, "content_type": ["application/pdf"] } }Signed with the issuer's Nostr key (same key the WebID's
authenticationset already declares, seesrc/auth/nostr-keys.js).Verification path
Plugs in alongside the existing auth middleware (
src/auth/middleware.js):?cap=query param or/cap/path segment.iss's declared keys.nbf <= now < exp, requested method inscope, request URL matchesres(exact or container-prefix).usesin a server-side cap-token ledger (single-use enforced even under concurrent reads).constraintsagainst the actual request (body size, content type).Storage
A cap-token ledger per pod (
/.cap/ledger.jsonor similar) tracksjti -> uses_remaining, revoked_at. Tokens that have exhausted uses or been revoked stay in the ledger untilexpfor replay protection.Issuance API
Revocation
Use cases
{ res: "/inbox/", scope: ["POST"], uses: 1, constraints: { max_bytes: 5MB } }→ hands the resulting URI to a stranger → they upload one file → cap auto-expires.{ res: "/private/q3-report.pdf", scope: ["GET"], ttl: 86400, uses: 10 }→ share via email; expires after 24h or 10 fetches.Security
jti, plus the whole token is signature-bound — forging is as hard as forging the issuer's signature.uses: 1unless explicitly bumped, atomic decrement on the ledger.?cap=from access logs; recommend path-form tokens with a documented log-scrubbing config snippet.Refererif the cap'd resource embeds external links; recommendReferrer-Policy: no-referreron cap responses.jtis until originalexpto prevent reuse via cached copies.resmust be an absolute URL and the server compares full origin, not just path — defends against the path-mode/subdomain-mode mismatch issues we've already hit (In subdomain mode, files at the base-domain root are unreachable (401) #307 lineage).Out of scope (follow-ups)
Open questions
Related