Expand description
§solid-pod-rs-server
Drop-in Solid Pod server binary wrapping
solid-pod-rs with
actix-web. This crate is both a
library (for integration-test reuse) and a binary.
§Public types
AppState— Shared actix-web application state (storage, dotfile policy, body cap).build_app— Builds the fully-configuredactix_web::Appwith all routes and middleware.NodeInfoMeta— NodeInfo 2.1 metadata inputs.PathTraversalGuard— Middleware that rejects..path-traversal attempts.DotfileGuard— Middleware that enforces the dotfile allowlist.ErrorLoggingMiddleware— Middleware that logs 5xx responses with full error chains.body_cap_from_env— ReadsJSS_MAX_REQUEST_BODYfrom the environment.cli— CLI argument definitions (clap derive).
§Route table
| Method | Path | Handler |
|---|---|---|
| GET/HEAD | /{tail:.*} | handle_get |
| GET | /{folder}/* | Glob merged Turtle |
| PUT | /{tail:.*} | handle_put |
| PUT | /{tail:.*}/ + Link: BasicContainer | Container creation |
| POST | /{tail:.*}/ | handle_post |
| PATCH | /{tail:.*} | handle_patch |
| DELETE | /{tail:.*} | handle_delete |
| COPY | /{tail:.*} + Source header | handle_copy |
| OPTIONS | /{tail:.*} | handle_options |
| POST | /api/accounts/new | Pod provisioning |
| GET | /pods/check/{name} | Pod existence check |
| POST | /login/password | Credentials login |
| POST | /account/password/reset | Password reset |
| POST | /account/password/change | Password change |
| GET | /.well-known/solid | Solid discovery |
| GET | /.well-known/webfinger | WebFinger JRD |
| GET | /.well-known/nodeinfo | NodeInfo discovery |
| GET | /.well-known/nodeinfo/2.1 | NodeInfo 2.1 |
| GET | /.well-known/did/nostr/{pubkey}.json | DID:nostr document |
| GET | /pay/.info | Payment discovery |
| GET | /pay/.balance | Web-Ledger balance |
| POST | /pay/.deposit | TXO + MRC20 deposit |
| GET | /pay/.address | Tweaked deposit addr |
| GET/POST | /pay/.offers .sell .swap .pool | Order book + AMM |
| POST | /pay/.buy .withdraw .withdraw-sats | Token mint/voucher |
| GET | /{pod}/{path}.prov.ttl | PROV-O git-mark sidecar |
| GET | /{pod}/_prov/{commit_sha} | Resolve a git-mark |
| POST | /{pod}/_prov/anchor | Upgrade to Bitcoin anchor |
| GET | /api/exports/all | JSON-LD pod export (export-jsonld, acl:Control-gated) |
| GET/POST | /{pod}/info/refs …/git-{upload,receive}-pack | Git smart-HTTP (WAC-gated) |
| GET/POST | /forge /forge/{tail:.*} | Git forge — browse, issues, push tokens (forge feature, namespace-scoped, not WAC-gated) |
The /pay/* HTTP-402 economy routes (handlers::pay) wire the
solid-pod-rs Web-Ledger / order-book / AMM core onto actix; the _prov
routes (handlers::prov, --features git) expose the git-mark +
block-trail provenance API (ADR-059). Block-trail anchor verification and
broadcast go through the native mempool client (mempool.space testnet4
by default), with trail persistence via trail_store. Every LDP
PUT/POST/PATCH to a git-backed pod additionally fires the always-on
git-mark write hook (git_mark_write, when built with --features git).
The /forge/* git forge (JSS forge plugin port; solid_pod_rs_forge)
is mounted under --features forge, which implies git (the forge
reuses the smart-HTTP CGI). forge-anchoring adds the Blocktrails tier
(solid_pod_rs::mrc20); forge-announce adds NIP-34 discovery
publication over solid-pod-rs-nostr. Forge routes are registered
before the pod-git smart-HTTP catch-all so
/forge/<owner>/<repo>.git/info/refs resolves to the forge’s own CGI
forwarding rather than the pod-git handler. Unlike handle_git, the
forge does not go through the pod’s WAC ACL evaluator: it enforces
its own fail-closed namespace-ownership guard (an agent may only
push/comment into the namespace matching its own did:nostr pubkey or
pod username; see solid_pod_rs_forge::ownership) plus an own-area SSRF
check on pod-hosted issue/PR bodies. Repository browsing (tree/blob/
commits/issues list) is unauthenticated-public by design.
§Middleware stack (applied in order)
NormalizePath– collapse//and decode %-encoded segments.PathTraversalGuard– defence-in-depth..re-check.DotfileGuard– rejects.envetc unless on the allowlist.PayloadConfig– enforcesJSS_MAX_REQUEST_BODYbody cap.ErrorLoggingMiddleware– structured 5xx logging.- WAC-on-write – PUT/POST/PATCH/DELETE require a write/append grant.
§Security posture (closeout, 0.5.0-alpha.4)
- NIP-98 single-use replay guard — every request runs through a
shared process-local
Nip98ReplayCache, so a captured token cannot be replayed within the ±120s NIP-98 tolerance window (extract_pubkeyreturnsNoneon a replayed id → the WAC gate denies with 401). TTL / size viaSOLID_POD_NIP98_REPLAY_TTL_SECS/SOLID_POD_NIP98_REPLAY_MAX_SIZE. The cache is per-process; multi-replica deployments share no state. - Fail-open compile guard — this binary references
solid_pod_rs::auth::nip98::assert_schnorr_verification_enabledin const context, so a build that ever dropped BIP-340 signature verification from the NIP-98 verifier is a compile error, not a silently fail-open server. - WAC
acl:origingate — the requestOriginheader is threaded into the evaluator (enforce_read_ctx/enforce_write_ctx), so ACLs bearingacl:origintriples gate cross-origin access. Plain ACLs are unaffected;acl:Controlbypasses the origin gate by design.
§solid-pod-rs-server
Binary distribution of solid-pod-rs — a drop-in
JSS replacement that runs as a single static-ish Rust binary.
§Install
Once published to crates.io (target: v0.4.0):
cargo install solid-pod-rs-server
solid-pod-rs-server --config config.jsonUntil then, build from source:
cargo build --release -p solid-pod-rs-server
./target/release/solid-pod-rs-server --help§Architecture
This crate is a thin binary shell over solid-pod-rs.
Per ADR-056 §D3 (F7 library-server split):
solid-pod-rs— pure library. No#[tokio::main], noactix-web::HttpServer. Framework-agnostic.solid-pod-rs-server(this crate) — owns the actix-web HTTP server, the tokio runtime, clap CLI, the F6 layered config loader, and signal handling. Depends on the library and wires itsPodService-style primitives into concrete HTTP routes.
§Configuration
Configuration is loaded by solid_pod_rs::config::ConfigLoader
(F6, PRD §F6). Precedence (later overrides earlier):
Defaults < File < EnvVars < CLI flagsSee crates/solid-pod-rs/src/config/sources.rs
for the full JSS_* environment variable table.
When the requested listen port is occupied, the binary retains the first
available listener from that port through the next ten ports and prints the
actual base URL. --port 0 requests one operating-system-assigned port.
§Development compatibility helpers
--live-reload (or JSS_LIVE_RELOAD=true) injects the JSS-compatible
WebSocket reload client into served HTML. It is disabled by default and is
intended only for local development.
The write-path authenticator also accepts JSS’s two-part development bearer
token when TOKEN_SECRET is explicitly set to at least 32 bytes. The token is
base64url payload plus HMAC-SHA256 signature; the payload must contain valid
iat, exp, and HTTP(S) WebID claims. An absent or short secret disables this
path, and expired or tampered tokens are rejected.
§External Solid conformance harness
Start a server with working Solid-OIDC discovery, then run:
CTH_BASE_URL=http://127.0.0.1:4000 scripts/test-cth.shThe script checks discovery before starting the official Docker harness and
mounts scripts/cth/test-subjects.ttl read-only. Override CTH_IMAGE or
CTH_TARGET when pinning a harness image or selecting another report target.
The script supplies the integration path; it does not make an external CTH
pass part of the offline Rust test suite.
§Mashlib / SolidOS data browser
Enable the mashlib data browser to render RDF resources in the browser:
# CDN mode (zero config — loads from unpkg.com)
solid-pod-rs-server --mashlib
# CDN with a specific version
solid-pod-rs-server --mashlib --mashlib-cdn 2.1.0
# ES module mode (LOSOS shell)
solid-pod-rs-server --mashlib-module https://host/path/to/mashlib.jsWhen enabled, browser navigation (Accept: text/html) to RDF
resources returns an HTML wrapper that loads mashlib client-side.
The resource’s JSON-LD is embedded inline as a data island (up to
256 KiB) for a zero-network-roundtrip render. XHR / fetch()
requests (Sec-Fetch-Dest: empty) still receive raw RDF.
| Env var | CLI flag | Default |
|---|---|---|
JSS_MASHLIB | --mashlib | off |
JSS_MASHLIB_CDN | --mashlib-cdn | 2.0.0 |
JSS_MASHLIB_MODULE | --mashlib-module | — |
§RDF content negotiation on GET
GET transcodes a stored RDF resource into the syntax the client names in
Accept. KG resources persist as N-Triples; agents and the elevation
extractor can read the same graph as Turtle or JSON-LD without a separate
conversion step. Recognised concrete media types:
Accept value | Served as |
|---|---|
text/turtle | Turtle (N-Triples is a syntactic subset, emitted verbatim) |
application/n-triples | N-Triples |
application/ld+json | JSON-LD (expanded form) |
Transcoded responses carry Content-Type of the negotiated format plus
Vary: Accept. The body is served verbatim when:
- the client names no concrete RDF type (
*/*,application/*, empty); - the requested format already equals the stored format;
- the stored body does not parse as N-Triples (fails soft, never destroys);
- the target is
application/rdf+xml(no serialiser — declined, served as-is).
Non-RDF resources are always served verbatim. PATCH likewise seeds its
working graph from the existing N-Triples body, so an N3/SPARQL-Update
mutation is applied on top of pre-existing triples rather than replacing
them; a body that is neither empty nor parseable N-Triples is refused with
409 rather than silently overwritten.
§Agent write path (NIP-98)
Write verbs (PUT / POST / PATCH / DELETE / COPY) authenticate the
caller via NIP-98: extract_pubkey reconstructs the signed request URL from
actix connection_info, honouring X-Forwarded-Proto so a did:nostr agent
signing https:// behind TLS or a reverse proxy is not rejected with a
spurious 401 URL mismatch. The authenticated pubkey resolves to a
did:nostr:{pubkey} WebID that WAC evaluates against the effective ACL.
On an unauthenticated write the WWW-Authenticate challenge advertises
Nostr realm="Solid", DPoP realm="Solid", Bearer realm="Solid", so an agent
discovers the NIP-98 scheme the write path actually accepts. WAC denial of an
authenticated caller returns 403; a missing credential returns 401.
Verification is hardened three ways (closeout 0.5.0-alpha.4):
- Single-use replay guard — every accepted NIP-98 token id is recorded in
a shared process-local
Nip98ReplayCache; a re-presented token is treated as unauthenticated (extract_pubkeyreturnsNone) so the WAC gate denies with401, closing the ±120s replay window the stateless verifier leaves open. Tunable viaSOLID_POD_NIP98_REPLAY_TTL_SECS/SOLID_POD_NIP98_REPLAY_MAX_SIZE; per-process (multi-replica deployments share no state). - Fail-open compile guard — the binary references a
const fnthat exists only when BIP-340 Schnorr verification is compiled in, so a build that ever dropped signature verification fails to compile rather than silently accepting any forged pubkey. acl:origingate — the requestOriginheader is threaded into the WAC evaluator, so ACLs declaringacl:origintriples gate cross-origin access. Plain ACLs are unaffected;acl:Controlbypasses the origin gate so an owner can always repair a mis-configured ACL.acl:originis the only WAC 2.0 condition satisfiable end-to-end today —client_id/issuerconditions still evaluate deny (no authenticated OIDC client_id / issuer is surfaced yet).
The optional export-jsonld build adds GET /api/exports/all, an
acl:Control-gated JSON-LD time-chain export of the whole pod (native-only).
§Admin API and Native Pod Mesh (alpha.15+)
§Provision endpoint
POST /_admin/provision/{pubkey} creates a new pod for a Nostr pubkey in one
atomic step: pod directory, owner-only .acl, and a git init that sets
receive.denyCurrentBranch=updateInstead so the pod is immediately pushable
over HTTP via /_git/{pubkey}/.
curl -X POST https://pods.example.com/_admin/provision/<hex-pubkey> \
-H "X-Pod-Admin-Key: $SOLID_ADMIN_KEY"
# → { "podUrl": "https://pods.example.com/<hex-pubkey>/", "ok": true }This endpoint is the CF Workers ↔ agentbox handshake: auth-worker calls it
during WebAuthn registration to atomically provision a Solid pod alongside the
Nostr identity. The PSK (SOLID_ADMIN_KEY / --admin-key) must be set for the
endpoint to be active; it returns 403 unconditionally when unset.
Generate a key with:
openssl rand -hex 32§CORS allowlist for the forum git client
The forum’s Source Control panel (components/git_panel.rs) drives
/_git/{pubkey}/ over HTTP from a cross-origin browser context.
SOLID_ALLOWED_ORIGINS / --allowed-origins is a comma-separated list of
origins that will receive Access-Control-Allow-Origin headers.
# Production — lock to known origins
SOLID_ALLOWED_ORIGINS=https://dreamlab-ai.com,https://pods.dreamlab-ai.com
# Development default — empty = wildcard (*)OPTIONS preflights for /_git/{pubkey}/** are handled automatically
(feature git required, which is on by default in this binary).
§Deployment
For the full agentbox mesh deployment (solid-pod-rs-server alongside
auth-worker, R2, and the forum client) see:
docker-compose.solid-pods.yml # in the dreamlab-ai-website agentbox repoThat compose file wires SOLID_ADMIN_KEY, SOLID_ALLOWED_ORIGINS,
JSS_STORAGE_ROOT, and the CF Worker PROVISION_URL binding together.
§MCP server (Model Context Protocol)
POST /mcp exposes the pod as a Model Context Protocol 2025-03-26 tool
surface over the Streamable HTTP transport. Requests are JSON-RPC 2.0;
responses are single-shot JSON, with an SSE upgrade for the streaming
subscribe tool. The endpoint is off by default:
solid-pod-rs-server --mcp # enable
solid-pod-rs-server --no-mcp # force off (overrides JSS_MCP)
JSS_MCP=1 solid-pod-rs-server # enable via envIdentity reuses the pod’s NIP-98 verifier, so every tool call receives
the same WAC treatment as the equivalent REST request; an unauthenticated
/mcp call runs as the anonymous principal. Sixteen tools are exposed:
| Group | Tools |
|---|---|
| Resources | list_resources, read_resource, write_resource, create_resource, delete_resource, head_resource |
| Access control | read_acl, write_acl |
| Skills & docs | list_skills, get_skill, get_pod_skill, list_docs, read_docs |
| Pod & federation | pod_info, subscribe, call_remote_pod |
call_remote_pod is gated to /private/federation/ for did:nostr
identities with a depth-3 recursion cap, so an agent cannot fan out an
unbounded pod-to-pod call graph. Built-in docs and skills are embedded at
compile time via include_dir.
| Env var | CLI flag | Default |
|---|---|---|
JSS_MCP | --mcp / --no-mcp | off |
§install subcommand — push a Solid app into a pod
solid-pod-rs-server install <app-spec>... clones one or more Solid apps
and pushes them into a pod over the git smart protocol (the same
/_git/{pubkey}/ path the forum git client uses). The app-spec grammar
mirrors JSS src/cli/install.js:
<name> → https://github.com/solid-apps/<name>
<org>/<repo> → https://github.com/<org>/<repo>
<full-git-url> → used verbatim (https / git@ / ssh://)
<spec>#<ref> → clone a specific branch/tag/commit
<spec>=<dest> → rename the destination directory# Install the bundled mashlib browser into the default pod
NOSTR_PRIVKEY=<hex> solid-pod-rs-server install mashlib
# Pin a ref, rename the destination, target an explicit pod
solid-pod-rs-server install \
solid/contacts#v2=address-book \
--pod https://pods.example.com/<hex-pubkey>/ \
--nostr-privkey <hex>
# Preview without pushing
solid-pod-rs-server install mashlib --dry-runEach app is pushed to both HEAD:main and HEAD:gh-pages so apps
that publish from either branch land correctly. Authentication uses a
single NIP-98 token minted over the destination repo URL with a *
method wildcard, injected via git http.extraHeader so it covers the
multi-request smart protocol with one static header. A --token bearer
fallback is accepted when NIP-98 signing is unavailable.
The scratch clone is created under the platform temp directory
(std::env::temp_dir(), which honours $TMPDIR) and removed on both
success and failure, so install works on Linux/macOS/Windows and
sandboxed environments such as Termux without a hardcoded /tmp — the
Rust equivalent of JSS #518.
| Flag | Env var | Default |
|---|---|---|
--pod | JSS_POD | http://localhost:4443 |
--nostr-privkey | NOSTR_PRIVKEY | — |
--token | JSS_BEARER_TOKEN | — |
--branches | — | main,gh-pages |
--dry-run | — | off |
NIP-98 minting requires the install cargo feature (which pulls in
solid-pod-rs/nip98-schnorr):
cargo build --release -p solid-pod-rs-server --features installWithout that feature the subcommand still compiles, but only the
--token bearer path can authenticate; the NIP-98 path returns a clear
runtime error telling the operator to rebuild with --features install.
§Feature flags
This binary enables the following solid-pod-rs features by default:
| Feature | Purpose |
|---|---|
fs-backend | Filesystem storage (JSS default) |
memory-backend | In-memory storage (test / dev) |
config-loader | F6 layered config loader |
legacy-notifications | F3 solid-0.1 WS notifications adapter |
Other feature flags (oidc, dpop-replay-cache, and nip98-schnorr) can be
opted into by the operator via a custom build. No stock object-store backend
ships; unknown storage types are rejected during configuration loading.
§Licence
AGPL-3.0-only. See LICENSE. Operating this binary as a
network service triggers AGPL §13 source-disclosure obligations.
§Sibling crates (all functional)
solid-pod-rs-activitypub— ActivityPub federation (4,453 LOC)solid-pod-rs-git— Git HTTP backend (1,685 LOC)solid-pod-rs-idp— Solid-OIDC identity provider (6,160 LOC)solid-pod-rs-nostr— did:nostr + embedded Nostr relay (2,177 LOC)solid-pod-rs-didkey— did:key (Ed25519/P-256/secp256k1) + JWT (1,167 LOC)
Modules§
- cli
- CLI argument definitions (clap derive structs). Operator CLI subcommands — Sprint 11 rows 138, 163, 168.
- mempool
- Native mempool.space REST client (provenance-upgrade Phase 3). Concrete
solid_pod_rs::mrc20::MempoolLookup+ the verify-sidesolid_pod_rs::provenance::BlockAnchorer. Server-side only (builds areqwest::Client); wasm consumers implement the trait overfetch. Native mempool.space REST client — the read-side of block-trail anchors. - trail_
store - MRC20 trail persistence (provenance-upgrade Phase 4). Loads/saves a
token’s Bitcoin-anchored state chain at
/.well-known/token/{ticker}.jsonvia the pod’ssolid_pod_rs::storage::Storagebackend (JSStoken.js:189-208). Native-only; holds the issuer secret off the publicsolid_pod_rs::mrc20::Mrc20Trailtype. MRC20 trail persistence — the server-side load/save for a token’s Bitcoin-anchored state chain (ADR-059 Phase 4; JSStoken.js:189-208).
Structs§
- AppState
- Actix-web shared state.
- Cors
Headers - Adds the same CORS envelope JSS emits from its global
onRequesthook. - Cors
Headers Middleware - Per-request service instance produced by
CorsHeaders. - Dotfile
Guard - Actix middleware that blocks dotfile paths unless they appear on the allowlist.
- Dotfile
Guard Middleware - Per-request service instance produced by
DotfileGuard. - Error
Logging Middleware - Observes outbound responses and logs 5xx results with the full
error chain. Pass-through on 2xx/3xx/4xx. Shaped as an actix
Transformso it slots into the middleware stack inbuild_app. - Error
Logging Middleware Service - Per-request service instance produced by
ErrorLoggingMiddleware. - Node
Info Meta - NodeInfo 2.1 body inputs. Kept here so tests can override them.
- Path
Traversal Guard - Actix middleware that rejects requests containing
..path-traversal sequences. - Path
Traversal Guard Middleware - Per-request service instance produced by
PathTraversalGuard. - PodCreate
Limiter - In-process sliding-window limiter for JSS-compatible
POST /.pods.
Constants§
- DEFAULT_
BODY_ CAP - Discover the body cap from the environment. Accepts values like
50MB,1.5GB, or a bare integer (bytes). Falls back to 50 MiB. - DEFAULT_
PROXY_ BYTE_ CAP - Default byte cap for proxied responses (50 MiB).
Functions§
- body_
cap_ from_ env - Read
JSS_MAX_REQUEST_BODY(or, as a drop-in-config alias, upstream JSS’sJSS_BODY_LIMIT, #474/#543) and parse viaparse_size. The canonical name wins when both are set. On any failure, returnsDEFAULT_BODY_CAP. - build_
app - Build the complete actix
Appfor the Solid Pod server. Both the binary (main.rs) and the workspace integration tests call this.