Skip to main content

Crate solid_pod_rs_server

Crate solid_pod_rs_server 

Source
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-configured actix_web::App with 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 — Reads JSS_MAX_REQUEST_BODY from the environment.
  • cli — CLI argument definitions (clap derive).

§Route table

MethodPathHandler
GET/HEAD/{tail:.*}handle_get
GET/{folder}/*Glob merged Turtle
PUT/{tail:.*}handle_put
PUT/{tail:.*}/ + Link: BasicContainerContainer creation
POST/{tail:.*}/handle_post
PATCH/{tail:.*}handle_patch
DELETE/{tail:.*}handle_delete
COPY/{tail:.*} + Source headerhandle_copy
OPTIONS/{tail:.*}handle_options
POST/api/accounts/newPod provisioning
GET/pods/check/{name}Pod existence check
POST/login/passwordCredentials login
POST/account/password/resetPassword reset
POST/account/password/changePassword change
GET/.well-known/solidSolid discovery
GET/.well-known/webfingerWebFinger JRD
GET/.well-known/nodeinfoNodeInfo discovery
GET/.well-known/nodeinfo/2.1NodeInfo 2.1
GET/.well-known/did/nostr/{pubkey}.jsonDID:nostr document
GET/pay/.infoPayment discovery
GET/pay/.balanceWeb-Ledger balance
POST/pay/.depositTXO + MRC20 deposit
GET/pay/.addressTweaked deposit addr
GET/POST/pay/.offers .sell .swap .poolOrder book + AMM
POST/pay/.buy .withdraw .withdraw-satsToken mint/voucher
GET/{pod}/{path}.prov.ttlPROV-O git-mark sidecar
GET/{pod}/_prov/{commit_sha}Resolve a git-mark
POST/{pod}/_prov/anchorUpgrade to Bitcoin anchor
GET/api/exports/allJSON-LD pod export (export-jsonld, acl:Control-gated)
GET/POST/{pod}/info/refs …/git-{upload,receive}-packGit 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)

  1. NormalizePath – collapse // and decode %-encoded segments.
  2. PathTraversalGuard – defence-in-depth .. re-check.
  3. DotfileGuard – rejects .env etc unless on the allowlist.
  4. PayloadConfig – enforces JSS_MAX_REQUEST_BODY body cap.
  5. ErrorLoggingMiddleware – structured 5xx logging.
  6. 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_pubkey returns None on a replayed id → the WAC gate denies with 401). TTL / size via SOLID_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_enabled in 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:origin gate — the request Origin header is threaded into the evaluator (enforce_read_ctx / enforce_write_ctx), so ACLs bearing acl:origin triples gate cross-origin access. Plain ACLs are unaffected; acl:Control bypasses 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.json

Until 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], no actix-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 its PodService-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 flags

See 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.sh

The 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.js

When 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 varCLI flagDefault
JSS_MASHLIB--mashliboff
JSS_MASHLIB_CDN--mashlib-cdn2.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 valueServed as
text/turtleTurtle (N-Triples is a syntactic subset, emitted verbatim)
application/n-triplesN-Triples
application/ld+jsonJSON-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_pubkey returns None) so the WAC gate denies with 401, closing the ±120s replay window the stateless verifier leaves open. Tunable via SOLID_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 fn that 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:origin gate — the request Origin header is threaded into the WAC evaluator, so ACLs declaring acl:origin triples gate cross-origin access. Plain ACLs are unaffected; acl:Control bypasses the origin gate so an owner can always repair a mis-configured ACL. acl:origin is the only WAC 2.0 condition satisfiable end-to-end today — client_id / issuer conditions 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 repo

That 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 env

Identity 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:

GroupTools
Resourceslist_resources, read_resource, write_resource, create_resource, delete_resource, head_resource
Access controlread_acl, write_acl
Skills & docslist_skills, get_skill, get_pod_skill, list_docs, read_docs
Pod & federationpod_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 varCLI flagDefault
JSS_MCP--mcp / --no-mcpoff

§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-run

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

FlagEnv varDefault
--podJSS_PODhttp://localhost:4443
--nostr-privkeyNOSTR_PRIVKEY
--tokenJSS_BEARER_TOKEN
--branchesmain,gh-pages
--dry-runoff

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 install

Without 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:

FeaturePurpose
fs-backendFilesystem storage (JSS default)
memory-backendIn-memory storage (test / dev)
config-loaderF6 layered config loader
legacy-notificationsF3 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)

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-side solid_pod_rs::provenance::BlockAnchorer. Server-side only (builds a reqwest::Client); wasm consumers implement the trait over fetch. 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}.json via the pod’s solid_pod_rs::storage::Storage backend (JSS token.js:189-208). Native-only; holds the issuer secret off the public solid_pod_rs::mrc20::Mrc20Trail type. MRC20 trail persistence — the server-side load/save for a token’s Bitcoin-anchored state chain (ADR-059 Phase 4; JSS token.js:189-208).

Structs§

AppState
Actix-web shared state.
CorsHeaders
Adds the same CORS envelope JSS emits from its global onRequest hook.
CorsHeadersMiddleware
Per-request service instance produced by CorsHeaders.
DotfileGuard
Actix middleware that blocks dotfile paths unless they appear on the allowlist.
DotfileGuardMiddleware
Per-request service instance produced by DotfileGuard.
ErrorLoggingMiddleware
Observes outbound responses and logs 5xx results with the full error chain. Pass-through on 2xx/3xx/4xx. Shaped as an actix Transform so it slots into the middleware stack in build_app.
ErrorLoggingMiddlewareService
Per-request service instance produced by ErrorLoggingMiddleware.
NodeInfoMeta
NodeInfo 2.1 body inputs. Kept here so tests can override them.
PathTraversalGuard
Actix middleware that rejects requests containing .. path-traversal sequences.
PathTraversalGuardMiddleware
Per-request service instance produced by PathTraversalGuard.
PodCreateLimiter
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’s JSS_BODY_LIMIT, #474/#543) and parse via parse_size. The canonical name wins when both are set. On any failure, returns DEFAULT_BODY_CAP.
build_app
Build the complete actix App for the Solid Pod server. Both the binary (main.rs) and the workspace integration tests call this.