solid-pod-rs
Framework-agnostic Rust library for serving Solid Protocol 0.11 pods: LDP resources and containers, Web Access Control, WebID, Solid Notifications 0.2, Solid-OIDC 0.1, and NIP-98 HTTP auth.
Parity vs JSS: ~100 % spec-normative (~98 % strict on the full
132-row tracker — see PARITY-CHECKLIST.md).
702 tests pass across the workspace as of Sprint 12 close (2026-05-06).
The library has no opinions about the HTTP runtime; wire it into
actix-web, axum, hyper, or any other server. For a turnkey binary
use the sibling crate solid-pod-rs-server.
[]
= "0.4.0-alpha.1"
use FsBackend;
use PathBuf;
let storage = new;
// Compose with your framework; see examples/embed_in_actix.rs.
Feature flags
| Flag | Default | Purpose |
|---|---|---|
fs-backend |
on | POSIX filesystem storage. |
memory-backend |
on | In-process HashMap storage (tests/demos). |
s3-backend |
off | AWS S3 / S3-compatible object stores. |
oidc |
off | Solid-OIDC 0.1 + DPoP. |
dpop-replay-cache |
off | DPoP jti replay cache (pulls oidc). |
nip98-schnorr |
off | BIP-340 signature verification for NIP-98. |
acl-origin |
off | WAC acl:origin enforcement. |
security-primitives |
off | SSRF guard + dotfile allowlist. |
legacy-notifications |
off | solid-0.1 WebSocket adapter (SolidOS). |
config-loader |
off | Layered config loader with JSS_* env vars. |
webhook-signing |
off | RFC 9421 Ed25519 webhook signing. |
did-nostr |
off | did:nostr resolver in interop. |
rate-limit |
off | Sliding-window LRU rate limiter. |
quota |
off | Per-pod .quota.json sidecar (atomic writes). |
Modules
| Module | Responsibility |
|---|---|
storage |
Storage trait + FS / Memory / S3 backends. |
ldp |
Resources, containers, content negotiation, PATCH, Prefer. |
wac |
Access control evaluator + WAC 2.0 conditions framework. |
webid |
WebID profile documents (emits solid:oidcIssuer + CID). |
auth |
NIP-98 HTTP authentication. |
oidc |
Solid-OIDC 0.1 + DPoP (verified) + JWKS + replay cache. |
notifications |
WebSocket, Webhook (RFC 9421 signed), legacy adapter. |
security |
SSRF guard + dotfile allowlist + CORS + rate limiter. |
quota |
Per-pod .quota.json sidecar with atomic writes. |
multitenant |
PodResolver trait; path + subdomain modes. |
config |
Layered configuration schema. |
interop |
.well-known/solid, WebFinger, NodeInfo 2.1, did:nostr. |
provision |
Pod bootstrap (WebID + containers + type indexes + ACL). |
Sibling crate ecosystem
Five sibling crates live in the workspace — all functional and shipping as of Sprint 12. Integrators may depend on them today.
| Crate | LOC | Parity rows | JSS source refs |
|---|---|---|---|
solid-pod-rs-activitypub |
4,453 | 102–108, 131, 169–172 | src/ap/{index,routes/inbox,routes/outbox,store}.js |
solid-pod-rs-git |
1,685 | 69, 100 | src/handlers/git.js |
solid-pod-rs-idp |
6,160 | 74–81, 130 | src/idp/{index,provider,passkey,interactions,credentials}.js |
solid-pod-rs-nostr |
2,177 | 89, 90, 101, 132 | src/{did/resolver,nostr/relay,auth/did-nostr}.js |
solid-pod-rs-didkey |
1,167 | 153 | W3C did:key spec + LWS 1.0 SSI |
The did:nostr resolver shipped in Sprint 6 lives inside the core
library (interop::did_nostr under did-nostr) as well as the
solid-pod-rs-nostr crate, so the Tier 1 + Tier 3 DID flow is
available either way.
WAC inheritance model
flowchart TD
REQ["Request for<br/>/pod/notes/2024/entry.ttl"] --> Q1{".acl sidecar<br/>at entry.ttl.acl?"}
Q1 -->|found| EVAL["Evaluate ACL rules<br/>against AuthContext"]
Q1 -->|not found| Q2{".acl at<br/>/pod/notes/2024/.acl?"}
Q2 -->|"found (acl:default)"| EVAL
Q2 -->|not found| Q3{".acl at<br/>/pod/notes/.acl?"}
Q3 -->|"found (acl:default)"| EVAL
Q3 -->|not found| Q4{".acl at<br/>/pod/.acl?"}
Q4 -->|"found (acl:default)"| EVAL
Q4 -->|not found| DENY["DENY<br/>(no ACL = no access)"]
EVAL --> CHK{"Agent match?<br/>acl:agent / agentClass<br/>/ agentGroup"}
CHK -->|"matched + mode ok"| ALLOW["ALLOW<br/>+ WAC-Allow header"]
CHK -->|"no match"| DENY2["DENY<br/>+ WAC-Allow header"]
style REQ fill:#4a90d9,stroke:#2c5f8a,color:#fff
style ALLOW fill:#2ecc71,stroke:#1a9850,color:#fff
style DENY fill:#e74c3c,stroke:#c0392b,color:#fff
style DENY2 fill:#e74c3c,stroke:#c0392b,color:#fff
style EVAL fill:#9b59b6,stroke:#7d3c98,color:#fff
Security posture
- DPoP signature verification against the proof's embedded JWK
(RFC 9449 §4.3), with an algorithm allowlist (
ES256/ES384,RS256/RS384/RS512,PS256/PS384/PS512,EdDSA);alg=noneand HMAC hard-rejected.athaccess-token hash binding enforced.jtireplay cache underdpop-replay-cache. - SSRF guard — RFC 1918, loopback, link-local, and cloud metadata endpoints are rejected on every outbound fetch (JWKS discovery, webhook delivery, did:nostr resolution). DNS-rebinding is closed by pinning the resolved IP on the per-call reqwest client.
- Dotfile allowlist — only
.acl,.meta,.well-known,.quota.json, and.accountare served. All other dotfiles return 404 regardless of storage-layer presence. - RFC 7638 canonical JWK thumbprints — replaces the previous hand-rolled JSON template; verified byte-for-byte against the spec's appendix-A vector.
- WAC parser bounds — 1 MiB Turtle input cap via
parse_turtle_acl_with_limit(JSS_MAX_ACL_BYTES); 32-level JSON-LD depth cap viaparse_jsonld_acl_with_limits. ReturnsPodError::PayloadTooLargeon oversized input (CWE-400, Sprint 12). - Atomic quota writes — temp-file + rename so concurrent writers
cannot observe a torn
.quota.json. - RFC 9421 webhook signing — Ed25519 over
@method,@target-uri,content-type,content-digest(RFC 9530),date,x-solid-notification-id.
See SECURITY.md for disclosure policy and a full
cryptographic verification matrix.
Documentation
- Workspace README:
../../README.md - Diátaxis docs:
docs/ - Agent integration guide:
docs/reference/agent-integration-guide.md - Parity vs JSS:
PARITY-CHECKLIST.md - Gap analysis:
GAP-ANALYSIS.md - Changelog:
CHANGELOG.md
Licence
AGPL-3.0-only — see ../../LICENSE and
NOTICE.