# JSS ↔ solid-pod-rs Parity Checklist
Exhaustive row-per-feature tracker against JavaScriptSolidServer (JSS).
Two comparators: the JSS core tree at `8d68ceb` / package `0.0.221`
(2026-10-18; §1–§24, with §22 audited from `f9f7a4d` / `0.0.220` on 2026-08-19
and §23 from `e7e0525`), and the out-of-tree `plugins` repository at `63a16cb`
(2026-10-19; §25, forge only). Canonical JSS
surface: [`docs/reference/jss-feature-inventory.md`](./docs/reference/jss-feature-inventory.md).
Prose companion (categorical summary, port tickets, architecture
discussion): [`GAP-ANALYSIS.md`](./GAP-ANALYSIS.md). This document is the
row-level tracker only — a machine-readable table of every JSS surface
and our status against it.
> **Verification method.** The `Rust file:line` entries and the
> `crates/solid-pod-rs/tests/*_jss.rs` suites are self-contained Rust
> assertions transcribing JSS's observed behaviour. They are **not** live
> differential runs against a running JSS instance; there is no executable
> JSS oracle in CI. Read the `_jss` suffix as "JSS-behaviour regression
> test", not "diffed against JSS at runtime".
> **Referenced ADRs.** Rows below cite ADR-053 (backend boundary / RDF
> format coverage / WebID-TLS deprecation), ADR-056 §D3 (library-server
> split) and, in §20, NRF ADR-089. ADR-053 and ADR-056 are backlog record
> numbers **not** authored in this crate's `docs/adr/` set — which holds
> ADR-057, ADR-058, ADR-059 only. The decisions they name are implemented
> (the split ships as `solid-pod-rs-server`; unsupported S3 configuration is
> rejected; WebID-TLS is a recorded won't-fix). ADR-089 lives in the
> nostr-rust-forum repo. Treat these citations as backlog pointers, not
> extant authoritative documents.
---
## Current state (2026-11-01 — JSS `7c9b0de`, package 0.0.224; plugins `63a16cb`)
**300 rows tracked** across 28 functional sections. §26 records the `0.0.221` →
`0.0.224` core delta: ACLs read by expanded JSON-LD IRI and refusal of ACLs that
grant nothing (#631/#632), PATCH applied as quads with Append for insert-only and
per-resource serialisation (#633), and NIP-11 at the relay URL (#635). §27 records
pre-existing core gaps the 2026-11-01 audit surfaced by checking the server's route
table and runtime behaviour, not library presence; it includes a P0 (row 287:
pods created by `POST /.pods` get no ACL of their own) and runtime-reproduced P1s in
PATCH, conditional requests and ACL validation. A second P0 is listed under §27's
defects: every N3 PATCH corrupts non-ASCII literals in the stored document. §28 maps the JSS plugin system and the
non-forge `plugins` directories (ADR-2011). Rows 31, 39, 40, 41, 91, 92, 93, 95, 101, 103
and 104 were reclassified in place. Method and evidence:
[`docs/reference/jss-parity-audit-2026-11-01.md`](docs/reference/jss-parity-audit-2026-11-01.md).
### Parity percentages
| Metric | Value |
|---|---|
| Strict (present + net-new + semantic-difference, excluding deferred/wontfix/not-applicable) | **≤ 86.4%** — 242/280 classified in-scope rows (`scripts/parity-check.sh`; below its 95% threshold). An upper bound: see below |
| Half-credit (partial-parity counted 0.5) | **≤ 89.5%** |
| Spec-normative surface | **partial** — N3 Patch parsing (row 269), `where` binding (270), conditional writes (286) and ACL IRI resolution (288) are open |
| Protocol-visible surface | **partial** — `/.notifications` advertised but not served (279) |
| JSS-specific extras (AP / Git / IdP / Nostr relay / did:key) | **library-complete, partly unwired** — relay and AP inbox/outbox are not mounted by the server (278, 281) |
> **Authoritative figure.** **86.4% strict** over 300 rows, and an **upper
> bound**. The fall from 99.2% (264 rows, 2026-10-19) is mostly a correction,
> not a regression in the code: the earlier figure counted library presence as
> parity. The 2026-11-01 audit checked server-reached behaviour only for the rows
> in its scope (§26–§28 and the eleven rows it reclassified). The remaining
> `present` rows have not been re-audited for server wiring, and some may be
> library-only in the same way. The `0.0.224` delta adds 14 rows (§26); §27
> records 12 older gaps.
### By status
| Status | Count |
|---|---|
| present (includes present-by-absence) | 216 |
| net-new (ours; not in JSS) | 19 |
| semantic-difference | 7 |
| partial-parity | 17 |
| missing | 21 |
| explicitly-deferred | 5 |
| wontfix-in-crate | 3 |
| other/unclassified (not applicable, architectural, test-meta) | 12 |
| **Total** | **300** |
Counts are the output of `scripts/parity-check.sh` at the 2026-11-01 audit. Core
comparator: upstream `JavaScriptSolidServer` `gh-pages` at `7c9b0de`, package
`0.0.224`, 2026-10-22. Plugins comparator: `plugins` at `63a16cb`, 2026-10-19.
Row-total note: the 132-rows headline (Sprint 12 close) counted the
unique feature rows across sections 1–17. Sprint 13 adds 3 scheduled
rows (196–198, §19) tracking the JSS v0.0.190 Phase 1 port. Sprint 14
adds 2 git-auto-init rows (199–200, §20). Sections
14–16 add 16 test/conformance meta rows that are counted separately
for sprint-pace arithmetic but excluded from the parity denominator
(they track the test suites themselves, not JSS features).
---
## Status key
| Status | Meaning |
|---|---|
| **present** | Feature exists in both with reconciled behaviour; tests on both sides. |
| **partial-parity** | Some sub-features present in solid-pod-rs; remainder documented. |
| **semantic-difference** | Both sides implement it, but observable behaviour differs. |
| **missing** | JSS has it; solid-pod-rs does not. Includes port ticket. |
| **net-new** | solid-pod-rs has it; JSS does not. Kept (ecosystem value) or gated. |
| **explicitly-deferred** | Out of scope with ADR rationale (e.g. legacy formats). |
| **wontfix-in-crate** | Belongs in a consumer crate (admin UI, operator tooling). |
| **shared-gap** | Neither side implements. Port to both recommended but low priority. |
| **present-by-absence** | JSS ships a fix for a bug we never had. Nothing to port. |
---
## Workspace crate reality (2026-08-19)
The core library has **seven** sibling crates. All compile and pass the
workspace test command; deployment readiness remains feature-specific:
| Crate | LOC | Status |
|---|---|---|
| `solid-pod-rs-server` | 10,117 | Canonical Actix server, CLI, MCP, pay/provenance routes. |
| `solid-pod-rs-forge` | 5,335 | Phases 0–3 shipped; later fork/PR/anchor/NIP-34 phases remain scaffolded. |
| `solid-pod-rs-git` | 3,150 | Rows 69, 100 present. CGI bridge + NIP-98 Basic-nostr auth. |
| `solid-pod-rs-nostr` | 2,542 | Rows 89, 90, 101, 132 present. BIP-340, NIP-01/11/16 relay, did:nostr↔WebID resolver. |
| `solid-pod-rs-activitypub` | 3,498 | Rows 102–108, 131 present. HTTP signatures, sqlx store, retry delivery. |
| `solid-pod-rs-idp` | 6,003 | Rows 74–81, 130 implemented; the optional Axum binder has an open authentication-boundary audit finding. |
| `solid-pod-rs-didkey` | 864 | Row 153 present. Ed25519/P-256/secp256k1 did:key and JWT verification. |
**Current receipt (2026-08-19):** `cargo test --workspace --all-targets
--all-features` and `cargo clippy --workspace --all-targets --all-features --
-D warnings` both exit 0. Cargo emits no workspace-wide aggregate test count,
so this tracker does not freeze a number that immediately becomes stale.
---
## 1. LDP (Linked Data Platform)
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 1 | LDP Resource GET | `src/handlers/resource.js` | `Storage::get`, `ldp::link_headers` | present | `src/storage/mod.rs:73`, `src/ldp.rs:95` | Link `rel=type` emitted. |
| 2 | LDP Resource HEAD | `src/handlers/resource.js` | `Storage::head`-equivalent via `ResourceMeta` | present | `src/storage/mod.rs:45` | Consumer binder issues HEAD. |
| 3 | LDP Resource PUT (create-or-replace) | `src/handlers/resource.js` + PUT hook (`src/server.js:455`) | `Storage::put` | present | `src/storage/mod.rs:73` | Returns strong SHA-256 ETag. |
| 4 | LDP Resource DELETE | `src/handlers/resource.js` + DELETE hook | `Storage::delete` | present | `src/storage/mod.rs:73` | |
| 5 | LDP Basic Container GET with `ldp:contains` | `src/ldp/container.js` | `ldp::render_container_jsonld`, `render_container_turtle` | present | `src/ldp.rs:647,709` | Native Turtle + JSON-LD; matches JSS JSON-LD output. |
| 6 | LDP Container POST + Slug fallback | `src/handlers/container.js` | `ldp::resolve_slug` (UUID fallback) | semantic-difference | `src/ldp.rs:119` | JSS uses numeric `-1/-2/…` suffixes. Clients must consume `Location:`. |
| 7 | PUT-to-container rejection (405) | `src/handlers/container.js` | binder returns 405 | present | example server | |
| 8 | Server-managed triples (`dateModified`, `size`, `contains`) | `src/ldp/container.js` | `ldp::server_managed_triples`, `find_illegal_server_managed` | present | `src/ldp.rs:566,620` | LDP §5.2.3.1 enforcement on write. |
| 9 | `contains` direct children only | `src/ldp/container.js` | `Storage::list` collapses nested | present | `src/storage/mod.rs:73` | |
| 10 | LDP Direct Containers | not implemented | not implemented | present (both absent) | — | Solid Protocol mandates Basic only. |
| 11 | LDP Indirect Containers | not implemented | not implemented | present (both absent) | — | Same as 10. |
| 12 | `Prefer` header dispatch (minimal / contained IRIs) | **not implemented** | `ldp::PreferHeader::parse` with multi-include | net-new | `src/ldp.rs:155,164` | We implement LDP §4.2.2 + RFC 7240 multi-include. |
| 13 | Live-reload script injection | `src/handlers/resource.js:23-35` | HTML responses receive the JSS-compatible reload WebSocket script when `--live-reload` / `JSS_LIVE_RELOAD=true` is set | present | `solid-pod-rs-server/src/lib.rs::inject_live_reload` | Disabled by default; unit-tested for enabled and disabled responses. |
| 14 | Pod root bootstrap (profile card, Settings/Preferences.ttl, publicTypeIndex, privateTypeIndex, per-container `.acl`) | `src/server.js:504-548`, `src/handlers/container.js::createPodStructure` | `provision::provision_pod` seeds WebID + containers + ACL + `/settings/publicTypeIndex.jsonld` (`solid:TypeIndex` + `solid:ListedDocument`) + `/settings/privateTypeIndex.jsonld` (`solid:TypeIndex` + `solid:UnlistedDocument`) + `/settings/publicTypeIndex.jsonld.acl` public-read carve-out | present | `src/provision.rs:55, 227-236, 98-` | Closes bundled rows 164 + 166 in the same change. |
## 2. HTTP headers, content negotiation, conditional/range
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 15 | `Link: <http://www.w3.org/ns/ldp#Resource>; rel=type` | `src/ldp/headers.js:15` | `ldp::link_headers` | present | `src/ldp.rs:95` | |
| 16 | `Link: <http://www.w3.org/ns/ldp#Container>; rel=type` + `BasicContainer` on containers | `src/ldp/headers.js:15-29` | `link_headers` | present | `src/ldp.rs:95` | |
| 17 | `Link: <.acl>; rel=acl` | `src/ldp/headers.js:15-29` | `link_headers` | present | `src/ldp.rs:95` | |
| 18 | `Link: <.meta>; rel=describedby` | not explicit | `link_headers` emits on every non-meta, non-acl | net-new | `src/ldp.rs:95` | JSS doesn't emit describedby; we do. |
| 19 | `Link: rel=http://www.w3.org/ns/pim/space#storage` at pod root | emitted | `link_headers` at root path | present | `src/ldp.rs:95` | |
| 20 | `Accept-Patch: text/n3, application/sparql-update` | `src/ldp/headers.js:58` | `ldp::ACCEPT_PATCH` constant + `options_for` | present | `src/ldp.rs:1336`, `ACCEPT_PATCH` const | Also advertises `application/json-patch+json` (net-new). |
| 21 | `Accept-Post` from conneg (ld+json, turtle when conneg on) | `src/rdf/conneg.js:201-216` | `ldp::ACCEPT_POST` constant | present | `src/ldp.rs` `ACCEPT_POST` | We emit all three media types unconditionally. |
| 22 | `Accept-Put` from conneg | `src/rdf/conneg.js:201-216` | advertised in `options_for` | present | `src/ldp.rs:1336` | |
| 23 | `Accept-Ranges: bytes` on resources, `none` on containers | `src/ldp/headers.js:59` | emitted via `options_for` | semantic-difference | `src/ldp.rs:1336` | `options_for()` hard-codes `"bytes"` even on containers. Benign. |
| 24 | `Allow: GET, HEAD, PUT, DELETE, PATCH, OPTIONS` (+POST on containers) | `src/ldp/headers.js:60` | `options_for` → `OptionsResponse` | present | `src/ldp.rs:1336` | |
| 25 | `Vary: Authorization, Origin` (adds `Accept` when conneg on) | `src/ldp/headers.js:61` | `ldp::vary_header(conneg_enabled)` centralised | present | `src/ldp.rs:1516` | Single source of truth, matches JSS `getVaryHeader` post-#315. |
| 26 | `WAC-Allow: user="…", public="…"` | `src/wac/checker.js:279-282` | `wac::wac_allow_header` | present (semantic-difference on token order) | `src/wac/mod.rs` | JSS = source order; ours = alphabetical. Both spec-legal. |
| 27 | `Updates-Via: ws(s)://host/.notifications` | `src/server.js:344-347` | `solid-pod-rs-server::set_updates_via` emits `ws://` / `wss://` discovery on GET and OPTIONS responses | present | `crates/solid-pod-rs-server/src/lib.rs::set_updates_via`, `handle_get`, `handle_options`; `crates/solid-pod-rs/tests/server_routes_jss.rs` | Fixed to append `/.notifications`; regression asserts `wss://pod.example/.notifications`. |
| 28 | CORS: `Access-Control-Allow-Origin` echoed/`*` | `src/ldp/headers.js:96` | `solid-pod-rs-server::CorsHeaders` echoes request Origin or emits `*` | present | `crates/solid-pod-rs-server/src/lib.rs::CorsHeaders`; `crates/solid-pod-rs-server/tests/middleware_guards.rs` | Mirrors JSS global CORS envelope. |
| 29 | CORS `Access-Control-Expose-Headers` (full list) | `src/ldp/headers.js:99` | `solid-pod-rs-server::CorsHeaders` emits JSS expose list including `Updates-Via`, `WAC-Allow`, payment headers | present | `crates/solid-pod-rs-server/src/lib.rs::add_cors_headers`; `crates/solid-pod-rs-server/tests/middleware_guards.rs` | |
| 30 | ETag header on read/write | `src/storage/filesystem.js:32` = md5(mtime+size) | `ResourceMeta::etag` = hex SHA-256 | semantic-difference | `src/storage/mod.rs:45` | Both spec-legal. See GAP §D.6. |
| 31 | If-Match / If-None-Match (conditional) | `src/utils/conditional.js` + `src/handlers/resource.js:124-130` | `ldp::evaluate_preconditions` → `ConditionalOutcome` | partial-parity | `src/ldp.rs:1143` | 304/412 outcomes. 2026-11-01: library-only; the server never calls `evaluate_preconditions` on PUT or PATCH (row 286, runtime). |
| 32 | Range requests (start-end, start-, -suffix) | `src/handlers/resource.js:56-106` | `ldp::parse_range_header`, `slice_range`, `ByteRange::content_range` | present | `src/ldp.rs:1240,1308,1226` | Multi-range rejected on both sides (correct). |
| 33 | OPTIONS method | `src/server.js:452` | `ldp::options_for` → `OptionsResponse` | present | `src/ldp.rs:1336` | |
| 34 | Content-type negotiation (JSON-LD native, Turtle+N3 under `--conneg`) | `src/rdf/conneg.js:33-61` | `ldp::negotiate_format` + `RdfFormat` enum | present | `src/ldp.rs:218,252` | We natively support both always; no flag needed. |
| 35 | N3 input support | `src/rdf/conneg.js` | `rdf_input` parses `text/turtle` and `text/n3` request bodies with oxttl into the in-crate graph; N3 formulas, variables and rules are refused with 415 | present | `src/rdf_input.rs`; `crates/solid-pod-rs-server/src/lib.rs` `validate_rdf_body`; `tests/fixtures/rdf_input/` (23-document differential corpus generated by the JSS n3.js parser) | N3 PATCH was already present. `text/n3` needs `conneg_enabled`, as in JSS. One oxrdf in the lock file. |
| 36 | RDF/XML input/output | recognised but not implemented (`src/rdf/conneg.js:13-25`) | `RdfFormat::RdfXml` negotiated, serialisation deferred | explicitly-deferred | — | ADR-053 §"RDF format coverage". |
| 37 | N-Triples round-trip | not first-class | `Graph::to_ntriples`, `Graph::parse_ntriples` | net-new | `src/ldp.rs:451,465` | Used by test corpora. |
| 38 | Turtle ⇄ JSON-LD round-trip (RDF library choice) | `n3.js` (non-deterministic per-path) | internal `Graph` model | net-new deterministic | `src/ldp.rs:393` | Single IO contract across serialisers. |
## 3. PATCH dialects
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 39 | N3 Patch (Solid Protocol §8.2) with `solid:inserts` / `solid:deletes` / simplified `where` | `src/patch/n3-patch.js:22-120` | `ldp::apply_n3_patch` | partial-parity | `src/ldp.rs:789` | 2026-11-01: the server reads each block as line-wise N-Triples, so prefixed N3 gets 400; see row 269. Evidence moved to `src/ldp.rs:1149-1194`. |
| 40 | N3 Patch `where` precondition failure | `n3-patch.js` never invokes `validatePatch`, silently drops missing deletes | `evaluate_preconditions` → 412 | semantic-difference | `src/ldp.rs:1143` | JSS is silently non-conformant; we return 412. 2026-11-01: reversed by JSS `a6834e8`: JSS now binds `where` and answers 409; we answer 412 and do not bind variables (row 270). |
| 41 | SPARQL-Update (INSERT DATA, DELETE DATA, DELETE+INSERT+WHERE, DELETE WHERE, standalone INSERT WHERE) | `src/patch/sparql-update.js:22-82` (regex) | `ldp::apply_sparql_patch` via `spargebra` | partial-parity | `src/ldp.rs:885` | We accept full SPARQL 1.1 algebra. 2026-11-01: `apply_sparql_patch` is at `src/ldp.rs:1276`; patterns with variables are a 204 no-op (§27 defects), so "full algebra" is overstated. |
| 42 | JSON Patch (RFC 6902) | **not implemented** | `ldp::apply_json_patch` (add/remove/replace/test/copy/move) | net-new | `src/ldp.rs:1363` | Non-normative Solid extension. |
| 43 | PATCH dispatch on `Content-Type` | inline in `src/handlers/resource.js` | `ldp::patch_dialect_from_mime` → `PatchDialect::{N3,Sparql,JsonPatch}` | present | `src/ldp.rs:1552,1558` | |
## 4. Web Access Control (WAC)
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 44 | Default-deny evaluator stance | `src/wac/checker.js:31-34` | `wac::evaluate_access` returns deny on no-ACL | present | `src/wac/mod.rs` | |
| 45 | ACL hierarchy resolution (walk up parent containers) | `src/wac/checker.js:59-113` | `wac::StorageAclResolver` resolves upward | present | `src/wac/mod.rs` | |
| 46 | `acl:default` container inheritance filtering | `src/wac/checker.js:59-113` | resolver respects `acl:default` on parent containers | present | `src/wac/mod.rs` | 15+ scenarios in `tests/wac_inheritance.rs`. |
| 47 | `acl:agent` (specific WebID) | `src/wac/checker.js:129` | `wac::evaluate_access` | present | `src/wac/mod.rs` | |
| 48 | `acl:agentClass foaf:Agent` (public / anonymous) | `src/wac/checker.js:139` | `wac::evaluate_access` | present | `src/wac/mod.rs` | |
| 49 | `acl:agentClass acl:AuthenticatedAgent` | `src/wac/checker.js:147` | `wac::evaluate_access` | present | `src/wac/mod.rs` | |
| 50 | `acl:agentGroup` enforcement (vcard:Group member resolution) | **parsed but not enforced** (`checker.js:193` TODO) | `wac::evaluate_access_with_groups` + `GroupMembership` trait + `StaticGroupMembership` default | net-new behaviour | `src/wac/mod.rs` | We enforce WAC §3.1.4; JSS does not. |
| 51 | `acl:origin` (request Origin gate) | **not implemented** | `wac::origin::OriginPolicy` + `Pattern` enforce Origin header when authorisation carries `acl:origin`; missing Origin when restriction present denies. Integrated into `evaluate_access`. | net-new (strictly more conformant than JSS) | `src/wac/origin.rs`, `src/wac/evaluator.rs`, `tests/acl_origin_test.rs` | Shared-gap closed by our side shipping first. |
| 52 | Modes (Read/Write/Append/Control) | `src/wac/parser.js:13-18` | `wac::AccessMode` enum | present | `src/wac/mod.rs` | |
| 53 | `acl:condition` framework (WAC 2.0 parser + evaluator) | `src/wac/parser.js:162`, `src/wac/checker.js:130-197` | `wac::parser::parse_authorization_body` recognises `acl:condition`; `wac::conditions::Condition::{Client,Issuer,Unknown}` + `ConditionRegistry`; evaluator fails CLOSED on `Unknown` (JSS fails OPEN) | present (strictly more conformant than JSS) | `src/wac/parser.rs`, `src/wac/conditions.rs`, `src/wac/evaluator.rs` | Fail-closed is the conformance advantage. |
| 54 | `acl:client*` / `acl:ClientCondition` (WAC 2.0) | `src/wac/parser.js:162`, `src/wac/checker.js:130-197` | `wac::client::ClientConditionEvaluator` dispatches on `Condition::Client(ClientConditionBody)` with client-id / audience match | present | `src/wac/client.rs`, `src/wac/conditions.rs` | Built-in registered by default via `ConditionRegistry::default_with_client_and_issuer`. |
| 55 | `acl:issuer*` / `acl:IssuerCondition` (WAC 2.0) | `src/wac/parser.js:162`, `src/wac/checker.js:130-197` | `wac::issuer::IssuerConditionEvaluator` dispatches on `Condition::Issuer(IssuerConditionBody)` checking token `iss` | present | `src/wac/issuer.rs`, `src/wac/conditions.rs` | Hook for LWS10 SSI-CID (row 152) once shared verifier lands. |
| 56 | 422 on PUT `.acl` with unknown condition type | n/a (JSS lacks unknown-condition concept) | `wac::conditions::validate_for_write` returns `UnsupportedCondition{iri}` when document carries a `Condition::Unknown`; handler surfaces 422 | present (net-new stricter than JSS) | `src/wac/conditions.rs`, `tests/wac2_conditions.rs` | WAC 2.0 §5 normative; JSS has no equivalent. |
| 57 | Turtle ACL parser | `src/wac/parser.js:13-384` (n3) | `wac::parse_turtle_acl` | present | `src/wac/mod.rs` | |
| 58 | Turtle ACL serialisation | not implemented | `wac::serialize_turtle_acl` | net-new | `src/wac/mod.rs` | |
| 59 | JSON-LD ACL parser | accepted | `serde_json::from_slice` + `AclDocument` | present | `src/wac/mod.rs` | |
| 60 | Cross-identity matching (did:nostr ↔ WebID) | `src/auth/nostr.js` | `cross_identity::CrossIdentityResolver` makes a Nostr caller with a verified bidirectional link present as both the WebID and the did:nostr agent in the WAC gates | present | `crates/solid-pod-rs-server/src/cross_identity.rs`; `crates/solid-pod-rs-server/src/lib.rs` `grant_for_identities`; `src/interop.rs` `did_nostr::backlink_present` | Superset: remote origins are opt-in (`JSS_DID_NOSTR_ORIGINS`); the back-link needs a real sameAs statement, redirects are refused, responses capped. Negative TTL is a flat 60 s (JSS: 5 min verified absence, 1 min transient). |
## 5. Authentication
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 61 | Simple Bearer (HMAC-signed 2-part dev token) | `src/auth/token.js:45-117` | validates two-part base64url HMAC-SHA256 tokens with `iat`, `exp`, and HTTP(S) WebID claims | present | `solid-pod-rs-server/src/lib.rs::verify_dev_bearer` | Requires an explicit `TOKEN_SECRET` of at least 32 bytes; tampered and expired tokens are rejected. |
| 62 | Solid-OIDC DPoP verification | `src/auth/solid-oidc.js:85-251` | `oidc::verify_dpop_proof`, `DpopClaims`, `AccessTokenVerified` | present | `src/oidc/mod.rs` | Feature `oidc`. |
| 62b | DPoP proof signature verification | `src/auth/solid-oidc.js:171-249` (jose `jwtVerify`) | `oidc::verify_dpop_proof_core` dispatches on header alg across ES256/ES384/RS256/RS384/RS512/PS256/PS384/PS512/EdDSA; HS256 only for `kty=oct` (test/dev); constant-time `ath` binding (RFC 9449 §4.3) | present (P0 CVE-class cleared) | `src/oidc/mod.rs` | Covered by `tests/oidc_dpop_signature.rs`, `tests/oidc_access_token_alg.rs`, `tests/oidc_thumbprint_rfc7638.rs`. |
| 63 | DPoP `cnf.jkt` binding enforcement | `src/auth/solid-oidc.js` | `oidc::verify_access_token` | present | `src/oidc/mod.rs` | |
| 64 | DPoP jti replay cache | `src/auth/solid-oidc.js` | `oidc::replay::JtiReplayCache` — LRU 10 000-entry ceiling, 5-min TTL matching iat-skew window; `verify_dpop_proof` takes `Option<&JtiReplayCache>` and rejects `DpopReplayDetected` on seen jti within TTL | present | `src/oidc/replay.rs`, `tests/dpop_replay_test.rs` | Consumer binder still owns lifetime; library provides the primitive. |
| 65 | SSRF validation on JWKS fetch | `src/utils/ssrf.js:15-50` | covered by `security::resolve_and_check` primitive (row 114) | present (primitive shipped, consumer wires in) | `src/security/ssrf.rs` | Binder must call `resolve_and_check` before JWKS fetch. |
| 66 | NIP-98 HTTP auth (kind 27235, `u`/`method`/`payload` tags) | `src/auth/nostr.js:26-267` | `auth::nip98::verify_at`, `Nip98Event`, `Nip98Verified` | present | `src/auth/nip98.rs:65,28,39` | |
| 67 | NIP-98 Schnorr signature verification | via `nostr-tools` (unconditional) | `auth::nip98::verify_schnorr_signature` via `k256` (feature `nip98-schnorr`) | present | `src/auth/nip98.rs:172` | |
| 68 | NIP-98 60s clock skew tolerance | `src/auth/nostr.js` | `verify_at` with `now` param | present | `src/auth/nip98.rs:65` | |
| 69 | NIP-98 `Basic nostr:<token>` for git clients | `src/auth/nostr.js:39-46,178-200` | `solid_pod_rs_git::BasicNostrExtractor` delegates to core `auth::nip98` | present | `crates/solid-pod-rs-git/src/auth.rs` | Sprint 10. |
| 70 | WebID-TLS | `src/auth/webid-tls.js:187-257` | Not implemented; JSS ships WebID-TLS off by default and no client certificate can reach the pod through the TLS-terminating nginx used by agentbox and VisionClaw | not-applicable | none | Deployment-inert, not merely deferred: the nginx configs have no `ssl_verify_client`. |
| 71 | IdP-issued JWT verification | `src/auth/token.js:126-161` | `oidc::verify_access_token` | present | `src/oidc/mod.rs` | |
| 72 | Auth dispatch precedence (DPoP → Nostr → Bearer → WebID-TLS) | `src/auth/token.js:215-269` | consumer-binder responsibility | semantic-difference | example server | Library exposes primitives; binder composes. |
| 73 | `WWW-Authenticate: DPoP realm=…, Bearer realm=…` on 401 | `src/auth/middleware.js:173` | `solid-pod-rs-server::enforce_write` emits `DPoP realm="Solid", Bearer realm="Solid"` on unauthenticated WAC denials | present | `crates/solid-pod-rs-server/src/lib.rs::enforce_write`; `crates/solid-pod-rs/tests/server_security.rs` | |
## 6. IdP (identity provider — JSS runs its own; solid-pod-rs is a relying party)
Rows 74–82 live in `solid-pod-rs-idp` (Sprint 10). Passkeys and Schnorr
SSO ship as `partial-parity` trait hooks — the webauthn-rs / full NIP-07
handshake wiring is a follow-up; the trait shapes are stable. HTML
interaction pages stay `wontfix-in-crate`.
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 74 | `oidc-provider`-based IdP with auth/token/me/reg/session endpoints | `src/idp/index.js:144-168` | `Provider` in `solid-pod-rs-idp` (discovery, JWKS, /auth, /token, /userinfo) | present | `crates/solid-pod-rs-idp/src/provider.rs` | Sprint 10. |
| 75 | Solid-OIDC Dynamic Client Registration | `src/idp/provider.js:147-156` (`registration.enabled=true`, public) | `register_client` (both opaque and Client Identifier Document URL) | present | `crates/solid-pod-rs-idp/src/registration.rs` | Sprint 10. |
| 76 | OIDC discovery document | `src/idp/index.js:171-205` | `discovery::build_discovery` | present | `crates/solid-pod-rs-idp/src/discovery.rs` | Sprint 10. |
| 77 | JWKS endpoint | `src/idp/index.js:208` | `jwks::Jwks` with rotation | present | `crates/solid-pod-rs-idp/src/jwks.rs` | Sprint 10. |
| 78 | Client Identifier Document support (fetch+cache URL client_ids) | `src/idp/provider.js:22-85,429-452` | fetches + caches via SSRF-guarded `reqwest` | present | `crates/solid-pod-rs-idp/src/registration.rs` | Sprint 10. |
| 79 | Credentials endpoint (email+password → Bearer, 10/min rate-limit) | `src/idp/index.js:218-233` | argon2 login + core `RateLimiter` | present | `crates/solid-pod-rs-idp/src/credentials.rs` | Sprint 10. |
| 80 | Passkeys (WebAuthn) via `@simplewebauthn/server` | `src/idp/passkey.js` + wiring `src/idp/index.js:319-380` | `WebauthnPasskey` backed by `webauthn-rs 0.5`, per-user `DashMap` state, base64url credential lookup | present | `crates/solid-pod-rs-idp/src/passkey.rs` | Sprint 11. `passkey` feature-gated. |
| 81 | Schnorr SSO (NIP-07 handshake) | `src/idp/interactions.js` | `Nip07SchnorrSso` full handshake: 32-byte CSPRNG tokens, 5-min TTL, one-shot, BIP-340 Schnorr verify | present | `crates/solid-pod-rs-idp/src/schnorr.rs` | Sprint 11. `schnorr-sso` feature-gated. |
| 82 | HTML login/register/consent/interaction pages | `src/idp/index.js:239-315` | not implemented | wontfix-in-crate | — | Consumer concern. |
| 83 | Invite-only flag + `bin/jss.js invite` | `bin/jss.js invite {create,list,revoke}` | `FileInviteStore` persists invites at `<root>/.server/invites.json` (0600, atomic, tokens hashed at rest); `invite create`, `list` and `revoke` work; the provisioning gate redeems an `X-Invite-Token` when registration is closed, enforcing `max_uses` and expiry | present | `crates/solid-pod-rs-server/src/cli/invite_store.rs`; `crates/solid-pod-rs-server/src/cli/mod.rs` `run_invite_command`; `tests/cli_invite_persist.rs` | Redemption is on the two provisioning routes (`/.pods`, `/api/accounts/new`), where JSS validates the code on the IdP register form. The use is recorded before the pod exists, so a failing request still spends it. |
## 7. WebID
Rows 89–90 live in `solid-pod-rs-nostr` (Sprint 10).
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 84 | WebID profile document generation (HTML + JSON-LD) | `src/webid/profile.js` | `webid::generate_webid_html` | present | `src/webid.rs:7` | |
| 85 | WebID profile validation | inline | `webid::validate_webid_html` | present | `src/webid.rs:99` | |
| 86 | WebID-OIDC discovery (`solid:oidcIssuer` triples) | inline | `webid::generate_webid_html_with_issuer`, `extract_oidc_issuer` | present | `src/webid.rs:13,61` | Follow-your-nose. |
| 87 | WebID discovery (multi-user `/:podName/profile/card#me`) | README §"Pod Structure" | `provision::provision_pod` lays out same paths | present | `src/provision.rs:55` | |
| 88 | WebID discovery (single-user root pod `/profile/card#me`) | `src/server.js:480` | `provision::provision_pod` with `pod_base="/"` | present | `src/provision.rs:55` | |
| 89 | did:nostr DID Document publication at `/.well-known/did/nostr/:pubkey.json` (Tier 1/3) | `src/did/resolver.js:69` (analog: `src/auth/did-nostr.js`) | `render_did_document_tier1`, `render_did_document_tier3` with `publicKeyMultibase` (secp256k1 multicodec 0xe7) | present | `crates/solid-pod-rs-nostr/src/did.rs` | Sprint 10. |
| 90 | did:nostr ↔ WebID resolver via `alsoKnownAs` | `src/auth/did-nostr.js:41-80` | `NostrWebIdResolver` bidirectional, SSRF-guarded, JSON-LD + Turtle fallback | present | `crates/solid-pod-rs-nostr/src/resolver.rs` | Sprint 10. |
## 8. Notifications
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 91 | Solid WebSocket `solid-0.1` legacy (SolidOS) | `src/notifications/websocket.js:1-102,110-147` | `LegacyWebSocketSession` — full sub/ack/err/pub/unsub protocol, per-sub WAC Read re-check, 100 subs/conn cap, 2 KiB URL cap, ancestor-container fanout | partial-parity | `src/notifications/legacy.rs` | Sprint 11. 11 integration tests. 2026-11-01: library-only; the server has no `/.notifications` route although it advertises one (row 279). |
| 92 | WebSocketChannel2023 (Solid Notifications 0.2) | **not implemented** | `notifications::WebSocketChannelManager` (broadcast + 30s heartbeat) | partial-parity | `src/notifications/mod.rs` | 2026-11-01: library-only; the server mounts no notifications endpoint (row 279). |
| 93 | WebhookChannel2023 (Solid Notifications 0.2) | **not implemented** | `notifications::WebhookChannelManager` (AS2.0 POST, 3× retry) | partial-parity | `src/notifications/mod.rs` | 2026-11-01: library-only; the server mounts no notifications endpoint (row 279). |
| 94 | Server-Sent Events | not implemented | not implemented | present (both absent) | — | Not in spec. |
| 95 | Subscription discovery document (`.well-known/solid/notifications`) | status JSON only (`src/notifications/index.js:43`) | `notifications::discovery_document` (full Notifications 0.2 descriptor) | partial-parity | `src/notifications/mod.rs` | 2026-11-01: library-only; the server mounts no notifications endpoint (row 279). |
| 96 | Subscription trait + in-memory registry | inline | `notifications::InMemoryNotifications` | present | `src/notifications/mod.rs` | |
| 97 | Retry + dead-letter on webhook failure | not implemented | `WebhookChannelManager` exponential backoff, drop-on-4xx | net-new | `src/notifications/mod.rs` | |
| 98 | Change notification mapping (storage event → AS2.0 Create/Update/Delete) | inline | `ChangeNotification::from_storage_event` | present | `src/notifications/mod.rs` | |
| 99 | Filesystem watcher → notification pump | `src/notifications/events.js` | `notify`-backed watcher in `Storage::fs` + `pump_from_storage` | present | `src/storage/fs.rs`, `src/notifications/mod.rs` | |
## 9. JSS-specific extras
Rows 100–108 land across `solid-pod-rs-git`, `solid-pod-rs-nostr`,
`solid-pod-rs-activitypub` (Sprint 10 — all three crates now functional).
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 100 | Git HTTP backend (`handleGit` CGI, path-traversal hardening, `receive.denyCurrentBranch=updateInstead`) | `src/handlers/git.js:11-268` + WAC hook `src/server.js:286-314` | `GitHttpService` spawns `git http-backend` CGI with full env plumbing, path-traversal guard, config mutator | present | `crates/solid-pod-rs-git/src/service.rs`, `guard.rs`, `config.rs` | Sprint 10. 29 unit + 8 integration tests. |
| 101 | Nostr relay NIP-01/11/16 | `src/nostr/relay.js:95-286` | `Relay` with `EventStore` + `tokio::broadcast` dispatcher, BIP-340 Schnorr verify, NIP-11 info, NIP-16 replaceable/parameterised classifiers | partial-parity | `crates/solid-pod-rs-nostr/src/relay.rs`, `ws.rs` | Sprint 10. 2026-11-01: library-only; `solid-pod-rs-server` mounts no relay and serves no NIP-11 (rows 278, 280). |
| 102 | ActivityPub Actor on `/profile/card` (Accept-negotiated) | `src/server.js:238-259` | `Actor` + `render_actor` with RSA-2048 publicKey, Mastodon-ordered JSON-LD | present | `crates/solid-pod-rs-activitypub/src/actor.rs` | Sprint 10. |
| 103 | ActivityPub inbox with HTTP Signature verification | `src/ap/routes/inbox.js:57-248` | `handle_inbox` dispatcher + draft-cavage v12 `verify_request_signature` (Mastodon + RFC 9530 digest forms) | partial-parity | `crates/solid-pod-rs-activitypub/src/inbox.rs`, `http_sig.rs` | Sprint 10. 2026-11-01: library-only; the server routes only the actor document (row 281). |
| 104 | ActivityPub outbox + delivery | `src/ap/routes/outbox.js:17-147` | `handle_outbox` + `DeliveryWorker` exp-backoff retry (30s→24h, 6 attempts; drops on 4xx) | partial-parity | `crates/solid-pod-rs-activitypub/src/outbox.rs`, `delivery.rs` | Sprint 10. 2026-11-01: library-only; no outbox route (row 281). |
| 105 | WebFinger (`/.well-known/webfinger`) | `src/ap/index.js:80` | `interop::webfinger_response` (core) + re-export in `activitypub::discovery` | present | `src/interop.rs:81`, `crates/solid-pod-rs-activitypub/src/discovery.rs` | |
| 106 | NodeInfo 2.1 (`/.well-known/nodeinfo[/2.1]`) | `src/ap/index.js:116,130` | `nodeinfo_2_1` + `nodeinfo_wellknown` | present | `crates/solid-pod-rs-activitypub/src/discovery.rs` | Sprint 10. |
| 107 | Follower/Following stored in SQLite (`sql.js`) | `src/ap/store.js` | `Store` via `sqlx::SqlitePool` (followers, following, inbox, outbox, delivery_queue, actors cache) | present | `crates/solid-pod-rs-activitypub/src/store.rs` | Sprint 10. Schema mirrors JSS line-for-line. |
| 108 | SAND stack (AP Actor + did:nostr via `alsoKnownAs`) | `README.md:494-502` | `Actor::with_also_known_as` binds did:nostr into the Actor doc | present | `crates/solid-pod-rs-activitypub/src/actor.rs` | Sprint 10. |
| 109 | Mashlib (SolidOS data-browser) HTML wrapper | `src/mashlib/index.js` | `mashlib::should_serve` + `mashlib::generate_html` — CDN/Local/Module modes, data-island embed, round-trip opt | present | `src/mashlib.rs` | Sprint 13. Static asset serving (Local mode) remains consumer concern. |
| 110 | SolidOS UI static serving | `src/server.js:411` | not implemented | wontfix-in-crate (E.9) | — | Consumer crate. |
| 111 | Pod-create endpoint `POST /.pods` with 1/day/IP rate limit | `src/server.js:692-703`, `src/handlers/container.js:322-454` | `solid-pod-rs-server::handle_create_pod` validates pod name, seeds path-mode pod containers/profile, returns JSS response shape, and enforces one create per IP per day | present | `crates/solid-pod-rs-server/src/lib.rs::handle_create_pod`, `PodCreateLimiter`; `crates/solid-pod-rs/tests/server_routes_jss.rs` | Server-local limiter avoids changing core feature defaults. |
| 112 | Per-write rate limit | `src/server.js:455-458` | `security::rate_limit::RateLimiter` + `RateLimitKey` / `RateLimitSubject` canonical forms | present (library primitive) | `src/security/rate_limit.rs` | Binder wires to rate-limit backend. |
| 113 | Per-pod byte quota with reconcile | `src/storage/quota.js` + `bin/jss.js quota reconcile` | `provision::QuotaTracker` (reserve/release atomic primitive) + `FsQuotaStore::write_sidecar` atomic tempfile+rename + `sweep_quota_orphans` | present | `src/quota/mod.rs:136-172, 174-225, 308`; `tests/quota_race.rs` | CLI absent; core primitive solid. |
| 114 | SSRF guard (blocks RFC1918, link-local, AWS metadata, etc.) | `src/utils/ssrf.js:15-157` | `security::is_safe_url` + `security::resolve_and_check` + `SsrfPolicy::classify` cover RFC 1918 / RFC 4193 ULA / loopback / link-local / cloud-metadata literals (incl. `169.254.169.254`, `fd00:ec2::254`) + `metadata.google.internal` short-circuit | present (P0 cleared) | `src/security/ssrf.rs` | |
| 115 | Dotfile allowlist (permit `.acl`, `.meta`, `.well-known`, block rest) | `src/server.js:265-281` | `security::is_path_allowed` + `security::DotfileAllowlist` — static allowlist (`.acl`, `.meta`, `.well-known`, `.quota.json`); `..` traversal always rejected; env-driven tuning for operators | present (P0 cleared) | `src/security/dotfile.rs` | |
## 10. Storage, config, multi-tenancy
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 116 | Filesystem storage backend | `src/storage/filesystem.js` | `storage::fs::FileSystemStorage` | present | `src/storage/fs.rs` | `.meta.json` sidecars. |
| 117 | In-memory storage backend | provided for tests | `storage::memory::MemoryStorage` with broadcast watcher | present | `src/storage/memory.rs` | |
| 118 | S3/R2/object-store storage | not provided | not provided; unsupported `storage.type` values fail configuration validation | present (both absent) | `src/config/schema.rs` | The former dependency-only `s3-backend` flag was removed because it advertised no usable backend. |
| 119 | SPARQL/memory-only/external-HTTP backends | `sql.js` used only for AP state, not LDP | not provided | explicitly-deferred | — | Not a Solid-spec concern. |
| 120 | Config file (JSON) + env overlay + CLI overlay with precedence | `src/config.js:17-239` | `ConfigLoader::from_file` + `with_env_overlay` + `with_cli_overlay`; auto-detects JSON/YAML/TOML by extension | present | `src/config/loader.rs`, `src/config/sources.rs` | Sprint 11. `config-loader` feature. |
| 121 | `JSS_PORT`/`JSS_HOST`/`JSS_ROOT`/30+ more env vars | `src/config.js:96-132` | 31 `JSS_*` env vars wired in `sources::env` | present | `src/config/sources.rs` | Sprint 11. |
| 122 | `TOKEN_SECRET` mandatory-in-production | `src/auth/token.js:17-34` | surfaces via `ExtrasConfig::admin_key` with skip-serialise for telemetry | present | `src/config/schema.rs` | Sprint 11. |
| 123 | `CORS_ALLOWED_ORIGINS` | `src/ldp/headers.js:98-102` | `ExtrasConfig::cors_allowed_origins` CSV/JSON-array | present | `src/config/schema.rs` | Sprint 11. |
| 124 | Size parsing (`50MB`, `1GB`, `50MiB`) | `src/config.js:137-145` | `parse_size` supports SI + IEC (`KB`/`MB`/`GB`/`TB` + `KiB`/`MiB`/`GiB`/`TiB`) | present | `src/config/sources.rs` | Sprint 11. |
| 125 | Subdomain multi-tenancy (`--subdomains --base-domain example.com`) | `src/server.js:159-170` + `src/utils/url.js` | `SubdomainResolver::resolve` + `is_file_like_label` short-circuits file-ext labels | present | `src/multitenant.rs` | Sprint 11 (rows 125 + 162 bundled). |
| 126 | Path-based multi-tenancy (default) | `src/server.js` path dispatch | supported through `Storage` trait + prefix routing | present | — | |
## 11. Discovery
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 127 | `.well-known/solid` Solid Protocol discovery doc | **not implemented** | `interop::well_known_solid` → `SolidWellKnown` | net-new | `src/interop.rs:27,42` | We ship it per Solid Protocol §4.1.2. |
| 128 | NIP-05 verification (`/.well-known/nostr.json`) — builder primitives | JSS Phase 1 (v0.0.190): pod-resident `/.well-known/nostr.json?name=<local>` backed by `profile/card` `nostr:pubkey` triple | `interop::verify_nip05`, `nip05_document` → `Nip05Document` (builders); server route ships in alpha.11 via row 197 | net-new | `src/interop.rs:139,147,165`; route handler in `solid-pod-rs-server` under feature `nip05-endpoint` | Builder primitives ship as net-new; server-route wiring landed in alpha.11 under feature `nip05-endpoint` — see row 197. |
| 129 | `.well-known/openid-configuration` | `src/idp/index.js:171` (JSS as IdP) | `oidc::discovery_for` (as RP or standalone) | present | `src/oidc/mod.rs` | |
| 130 | `.well-known/jwks.json` | `src/idp/index.js:208` | `Jwks` with rotation, re-exported through `solid-pod-rs-idp` | present | `crates/solid-pod-rs-idp/src/jwks.rs` | Sprint 10. |
| 131 | `.well-known/nodeinfo` + `/2.1` | `src/ap/index.js:116,130` | `nodeinfo_wellknown`, `nodeinfo_2_1` | present | `crates/solid-pod-rs-activitypub/src/discovery.rs` | Sprint 10. |
| 132 | `.well-known/did/nostr/:pubkey.json` | `src/did/resolver.js:69` (analog: `src/auth/did-nostr.js`) | `well_known_path` + tier 1/3 DID Document renderers | present | `crates/solid-pod-rs-nostr/src/did.rs` | Sprint 10. |
| 133 | `.well-known/solid/notifications` discovery | status JSON at `src/notifications/index.js:43` | `notifications::discovery_document` | net-new (richer) | `src/notifications/mod.rs` | |
## 12. Interop / provisioning / admin
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 134 | Pod provisioning (seed containers, WebID, ACL) | `src/server.js:504-548` + `src/handlers/container.js::createPodStructure` | `provision::provision_pod` → `ProvisionOutcome` | present | `src/provision.rs:55,42` | |
| 135 | Account scaffolding | `src/idp/` | `provision::ProvisionPlan` carries pubkey/display_name/pod_base | partial-parity | `src/provision.rs:20` | `solid-pod-rs-idp` ships Argon2 user store, passkey (webauthn-rs), credentials, password change and sessions, but state is in memory. Gaps: no persistent account store (`.idp/accounts` equivalent), `find_by_webid`, `update_last_login`, passkey persistence, account export, and `account delete` runs against a throwaway store. |
| 136 | Admin override (secret-compare) | not provided (operator edits config) | `provision::check_admin_override` constant-time compare | net-new | `src/provision.rs:204` | |
| 137 | Dev-mode session (admin flag, test helper) | not provided | `interop::dev_session` → `DevSession` | net-new | `src/interop.rs:167,176` | Typed constructor only; never from headers. |
| 138 | Quota reconcile (disk scan → DB update) | `bin/jss.js quota reconcile` | `QuotaPolicy::reconcile` re-walks the pod's tree | present | `src/quota/mod.rs:247-259, 308` | CLI subcommand still absent; primitive ships. |
| 139 | CLI binary (`bin/jss.js` with `start`/`init`/`invite`/`quota`) | — | `solid-pod-rs-server` binary crate (ADR-056 §D3) | present | `crates/solid-pod-rs-server/src/main.rs` | Drop-in binary with config loader. `invite`/`quota` subcommands remain P3. |
> **JSS v0.0.190 Phase 1 port (issue #437, alpha.11):** rows 196, 197,
> 198 in §19 below cover key provisioning, the NIP-05 pod endpoint,
> and the JSON-LD time-chain export. Bodies landed and tested in
> 0.4.0-alpha.11; all three feature flags remain default-off.
## 13. Framework / architectural
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 140 | Fastify 4.29.x tightly coupled | `src/server.js:45-562` | framework-agnostic library + separate `solid-pod-rs-server` binary crate | present (architectural) | `src/lib.rs:1`, `crates/solid-pod-rs-server/` | Library-server split (ADR-056 §D3). |
| 141 | `@fastify/rate-limit` | `package.json:32` | `security::rate_limit` primitive (row 112) | present (library primitive) | `src/security/rate_limit.rs` | |
| 142 | `@fastify/websocket` | `package.json:32` | `tokio-tungstenite` | present (different binding) | `Cargo.toml:40` | |
| 143 | `@fastify/middie` (Koa-style mounting for oidc-provider) | `package.json:32` | N/A — we don't embed oidc-provider | — | — | |
| 144 | 10 runtime deps | `package.json` | 13 required + 4 optional (feature-gated) | parity-adjacent | `Cargo.toml` | Feature gates keep default minimal. |
## 14. Tests + conformance
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 145 | Runner | `node --test --test-concurrency=1` (`package.json:21`) | `cargo test` | parity | — | |
| 146 | Test count | 21 top-level `test/*.test.js`, 6,527 lines, "223 tests inc. 27 conformance" (README:944) | 567 tests across integration + inline module tests | parity-plus | `tests/` | |
| 147 | Conformance suite | `test/conformance.test.js` (349 lines) + `test/interop/*.js` | `tests/interop_jss.rs` (42 tests), `tests/parity_close.rs` (20), `tests/wac_inheritance.rs` (31) | parity-plus | `tests/*.rs` | JSS-fixture-driven. |
| 148 | CTH (Conformance Test Harness) compatibility | `scripts/test-cth-compat.js`, `npm run test:cth` | Docker-based external runner with discovery preflight and a checked-in test subject | present | `scripts/test-cth.sh`; `scripts/cth/test-subjects.ttl` | Harness wiring is present; a deployed server and IdP configuration are required to execute the external suite. |
| 149 | Benchmarks (`autocannon`) | `npm run benchmark` → `benchmark.js` (182 lines) | `cargo bench` with criterion (4 benches) | parity | `benches/` | |
## 15. LWS 1.0 Authentication Suite (JSS #319)
The W3C Linked Web Storage WG published four FPWDs on 2026-04-23
(OpenID Connect, SAML 2.0, SSI-CID, SSI-did:key). JSS #319 sequences
implementation; JSS has one landing (CID service profile, #320).
solid-pod-rs is at parity on the OIDC profile baseline, zero landings
on the CID signal, and on an equal-missing footing for did:key and
SAML.
| # | LWS 1.0 / JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 150 | LWS10 **OpenID Connect** profile conformance (FPWD 2026-04-23) | `src/auth/solid-oidc.js` (Solid-OIDC baseline) | Delta audit complete: ADR-057 documents 5 back-compat fields, 7 port tickets, 5 semantic differences | present | `docs/archive/adr/ADR-057-lws10-oidc-delta.md` | Sprint 11. Action items prioritised XS→M; implementation tracked under separate rows. |
| 151 | LWS10 **SAML 2.0** suite (FPWD 2026-04-23) | **not implemented** | **not implemented** | explicitly-deferred (both) | — | JSS #319 box 2 scoped-out. |
| 152 | LWS10 **SSI-CID** (Controlled Identifiers) verifier | **not implemented** | `CidVerifier` fan-out dispatcher + `Nip98Verifier` + `DidKeyVerifier`; wired into `wac::issuer::IssuerCondition` dispatch | **present** (we ship first; JSS hasn't) | `src/auth/self_signed.rs`, `crates/solid-pod-rs-didkey/src/verifier.rs` | Sprint 11. Net-new advantage. |
| 153 | LWS10 **SSI-did:key** auth | **not implemented** (tracked in JSS #86) | New crate `solid-pod-rs-didkey` — Ed25519/P-256/secp256k1 encoding per W3C did:key, hand-rolled JWT verify with alg-confusion gates, `DidKeyVerifier` impl of `SelfSignedVerifier` | **present** (we ship first; JSS hasn't) | `crates/solid-pod-rs-didkey/` | Sprint 11. 29 tests. Net-new advantage. |
| 154 | **CID service entry** in WebID JSON-LD (`service[].@type = lws:OpenIdProvider`, #320, 0.0.154) | `src/webid/profile.js:44-72` (cccd081, 2026-04-23) | `webid::generate_webid_html_with_issuer` emits `service[{ @id: "#oidc", @type: "lws:OpenIdProvider", serviceEndpoint: issuer }]`; round-trippable via `extract_oidc_issuer` (LWS-typed) | present | `src/webid.rs:65-71, 144-177` | First JSS-originated LWS 1.0 conformance surface. |
| 155 | **`cid:` + `lws:` JSON-LD context terms** in generated profiles | `src/webid/profile.js:35-41` | `@context` maps `cid:` → `w3.org/ns/cid/v1#`, `lws:` → `w3.org/ns/lws#`, plus `service`/`serviceEndpoint`/`isPrimaryTopicOf`/`mainEntityOfPage` aliases | present | `src/webid.rs:38-47` | Prerequisite for #320 portable JSON-LD. |
## 16. JSS 0.0.144 → 0.0.154 delta (non-LWS)
Thirteen patch releases in four weeks captured here. Bias is toward
small protocol conformance fixes and operator hardening.
| # | JSS feature | JSS commit | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 156 | Unified Vary header across all RDF variants (single `getVaryHeader`, #315) | 76fc5c6 (0.0.152) | `ldp::vary_header(conneg_enabled)` centralised | present | `src/ldp.rs:1516` | Duplicate of row 25 after centralisation. |
| 157 | `Cache-Control: private, no-cache, must-revalidate` on RDF variants (#315) | 76fc5c6 (0.0.152) | `CACHE_CONTROL_RDF` constant + `cache_control_for(content_type)` helper; wired into `OptionsResponse` and `not_found_headers` when conneg enabled | present | `src/ldp.rs:1647 (constant), 1679 (cache_control_for), 1619 (not_found wiring), 1585 (options wiring)` | Security-adjacent: prevents cross-auth cache bleed. |
| 158 | Top-level Fastify error handler, full stack on 5xx (#312) | 5b34d72 (0.0.151) | `ErrorLoggingMiddleware` actix service emits structured `tracing::error!` with method/path/status/chain/backtrace on 5xx | present | `crates/solid-pod-rs-server/src/lib.rs` | Sprint 11. |
| 159 | Atomic quota writes (tempfile + rename + fsync-adjacent, #309) | 9d9fc5e (0.0.149) | `FsQuotaStore::write_sidecar` writes `.quota.json.tmp-<pid>-<nanos>` then POSIX-renames to `.quota.json`; `sweep_quota_orphans` wired as first step of `reconcile`; 16-way concurrent-write regression test | present (P0 cleared) | `src/quota/mod.rs:136-172, 174-225, 308; tests/quota_race.rs` | |
| 160 | Orphan temp-file cleanup + numeric-string `used` coercion in `sanitizeQuota` (#310) | 133662f, ad511ab (0.0.150) | `sweep_quota_orphans` cleans tempfiles; `QuotaUsage` is structurally typed via serde | present | `src/quota/mod.rs:116-127, 174-225` | Bundled with row 159 fix. |
| 161 | Disk reconcile on corrupt/empty quota file (0cdd8b6) | 0cdd8b6 (0.0.150) | `QuotaPolicy::reconcile` re-walks the pod's tree | present | `src/quota/mod.rs:247-259` | |
| 162 | Subdomain mode: don't rewrite file-like paths (`/foo.ttl`) as pod subdomains (#307) | 6d43e66 (0.0.149) | `is_file_like_label` — 15+ extensions, case-insensitive, short-circuits subdomain resolver | present | `src/multitenant.rs` | Sprint 11. |
| 163 | `jss invite create -u/--uses` stores `maxUses` as null (#304) | 6578ab9 (0.0.148) | `solid-pod-rs-server invite create` CLI + `InviteStore` + `mint_token` + `parse_duration` | present | `crates/solid-pod-rs-server/src/cli/mod.rs`, `crates/solid-pod-rs-idp/src/invites.rs` | Sprint 11. |
| 164 | Type indexes typed `solid:TypeIndex` + `solid:ListedDocument` / `solid:UnlistedDocument` (#301) | 54e4433 (0.0.147) | `provision::provision_pod` writes `/settings/publicTypeIndex.jsonld` typed `solid:TypeIndex` + `solid:ListedDocument`, and `/settings/privateTypeIndex.jsonld` typed `solid:TypeIndex` + `solid:UnlistedDocument` | present | `src/provision.rs:227-236, 83-95` | Covered by `provision::tests::provisions_type_indexes_with_correct_visibility`. |
| 165 | `foaf:isPrimaryTopicOf` + `schema:mainEntityOfPage` in WebID profile (#299) | 01e12b0 (0.0.146) | `webid::generate_webid_html_with_issuer` emits both predicates; covered by `webid::tests::emits_primary_topic_of_and_main_entity_of_page` | present | `src/webid.rs:54-55 (graph), 41-42 (context), 321-344 (test)` | |
| 166 | `publicTypeIndex.jsonld.acl` seeded public-read at pod provision (#297) | 564d501 (0.0.145) | `provision::provision_pod` writes `/settings/publicTypeIndex.jsonld.acl` granting `acl:Read` to `foaf:Agent` + `acl:Control` to the pod owner | present | `src/provision.rs:98-` | Covered by `provision::tests::public_type_index_acl_grants_anonymous_read`. |
| 167 | `.acl` / `.meta` dotfiles recognised as `application/ld+json` for conneg (#294) | de02f15 (0.0.145) | Reusable `ldp::infer_dotfile_content_type(&str) -> Option<&'static str>` maps basenames `/.acl`, `/.meta`, `*.acl`, `*.meta` → `application/ld+json`; `FsBackend::read_meta` consults it when the sidecar is absent | present | `src/ldp.rs:347, 371-425; src/storage/fs.rs:98` | |
| 168 | `jss account delete` CLI + accounts refactor (#292) | d9e56d8 (0.0.144) | `solid-pod-rs-server account delete` CLI + `UserStore::delete` trait extension; stdin-confirm without `--yes` | present | `crates/solid-pod-rs-server/src/cli/mod.rs`, `crates/solid-pod-rs-idp/src/user_store.rs` | Sprint 11. |
---
## Priority legend (for missing rows)
| Priority | Meaning |
|---|---|
| **P0** | Ship-blocker. No outstanding P0 items at Sprint 9 close. |
| **P1** | Must land for JSS feature parity on the protocol-visible surface. |
| **P2** | Operator completeness; target v0.5.0. |
| **P3** | Long-term or consumer-crate concern; unlikely to block anything. |
---
## Top-10 remaining items by port priority (Sprint 10 close)
Sprint 10 closed all 4 sibling crates + their 20 missing rows. The
remaining roadmap is dominated by LWS 1.0 Auth Suite work plus
operator-surface polish.
1. **LWS 1.0 OpenID Connect delta audit** (row 150) — **P1 verify**, v0.5.x — document deltas between current Solid-OIDC impl and the LWS10 OIDC profile. Produce `ADR-057-lws10-oidc-delta.md`.
2. **LWS 1.0 SSI-did:key auth** (row 153) — **P1**, v0.5.x — new `solid-pod-rs-didkey` crate; Ed25519 primary, P-256 + secp256k1 feature-gated. Gated by JSS #86 fixtures.
3. **LWS 1.0 SSI-CID verifier** (row 152) — **P1**, v0.5.x — shared self-signed verifier abstracted over did:nostr + did:key + CID. Hook ready: `acl:issuer*` (row 55) dispatches.
4. **`solid-0.1` legacy notifications adapter** (row 91) — **P1**, v0.4.x — sub/ack/err/pub/unsub, per-sub WAC read check, ancestor-container fanout. SolidOS ecosystem compat.
5. **Passkeys full wiring** (row 80) — **P2**, v0.5.x — webauthn-rs integration behind the `passkey` feature. Trait shape is stable.
6. **Schnorr SSO full handshake** (row 81) — **P2**, v0.5.x — NIP-07 challenge/response against core `auth::nip98` Schnorr verifier.
7. **Config file loader + size parsing + env overlay** (rows 120–124) — **P2**, v0.5.x.
8. **Subdomain multi-tenancy hardening** (rows 125, 162) — **P2**, v0.5.x — file-extension heuristic for path-vs-subdomain dispatch.
9. **Top-level 5xx logging middleware** (row 158) — **P2**, v0.5.x — actix/axum binder concern.
10. **JSS `bin/jss.js` operator tooling surface** (rows 138, 163, 168) — **P3** — `quota reconcile`, `invite create --uses`, `account delete` equivalents in `solid-pod-rs-server` CLI. Operator concern; not blocking.
---
## Net-new advantages (our 6 rows)
1. **`Prefer` header dispatch** (row 12) — LDP §4.2.2 + RFC 7240 multi-include. JSS doesn't implement.
2. **JSON Patch (RFC 6902) PATCH dialect** (row 42) — non-normative Solid extension.
3. **`acl:agentGroup` enforcement** (row 50) — we implement WAC §3.1.4 where JSS only parses.
4. **`acl:origin` enforcement** (row 51) — shared-gap closed by our side shipping first; Sprint 9.
5. **WAC 2.0 fail-closed on unknown conditions + 422 on PUT `.acl`** (rows 53, 56) — WAC 2.0 §5 normative; JSS fails open.
6. **Turtle ACL serialisation** (row 58) — outbound path JSS doesn't provide.
Additional conformance-wins where behaviour differs in our favour
(counted in parity percentages, not listed as separate net-new rows):
strong SHA-256 ETags (row 30), `.meta` describedby Link emission
(row 18), N3 Patch `where` enforcement via 412 where JSS silently
drops (row 40), 422-on-malformed-ACL write rejection (row 59),
richer `.well-known/solid/notifications` discovery (row 95).
---
## 17. JSS v0.0.60–v0.0.71 delta (non-LWS, Sprint 12)
Twelve releases (v0.0.60–v0.0.71) captured here. Bias is toward
security hardening and ActivityPub federation improvements.
ADR-058 has the full drift analysis; PRD at `docs/sprint-12-prd.md`.
| # | JSS feature | JSS commit | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 169 | Size-capped ACL parsing (`safeJsonParse` equivalent, DoS protection) | `204fdfb` (v0.0.71) | `parse_turtle_acl_with_limit` + `parse_jsonld_acl_with_limits`; `MAX_ACL_BYTES` 1 MiB default; `PodError::PayloadTooLarge` | present | `src/wac/parser.rs:33`, `src/wac/mod.rs` | Sprint 12. P0. |
| 170 | Iterative `..` sanitization for podName in subdomain mode | `2569811` (v0.0.71) | `scrub_dotdot` already loops until stable; 2 new regression tests | present | `src/multitenant.rs` | Sprint 12. Already iterative pre-Sprint 12; tests added. |
| 171 | DNS resolution failure → request block in SSRF guard | `4dbf039` (v0.0.71) | `SsrfError::DnsFailure` variant; `resolve_and_check` propagates; 2 new tests | present | `src/security/ssrf.rs` | Sprint 12. P1. |
| 172 | `.account` in dotfile allowlist (IdP login endpoint) | `32c0db2` (v0.0.69) | `DEFAULT_ALLOWED` + `STATIC_ALLOWED_DOTFILES` + `default_dotfile_allowlist()` all include `.account`; 3 new tests | present | `src/security/dotfile.rs`, `src/config/schema.rs` | Sprint 12. P1. |
| 173 | Minimum password length validation (8 chars) | `1feead2` (v0.0.71) | `MIN_PASSWORD_LENGTH = 8`; `validate_password_length()`; enforced at registration + `insert_user`; `PasswordTooShort` error; 8 new tests | present | `crates/solid-pod-rs-idp/src/credentials.rs`, `user_store.rs` | Sprint 12. P1. |
| 174 | AP outbox POST (Note → Create wrapping + delivery to followers) | `25fa813` (v0.0.67) | `handle_outbox_post` wraps raw Notes in Create activities, delivers to follower inboxes via `enqueue_delivery`; 5 new tests | present | `crates/solid-pod-rs-activitypub/src/outbox.rs` | Sprint 12. P2. |
| 175 | User-Agent header on AP federation fetches | `8247293` (v0.0.65) | `solid-pod-rs-activitypub/0.4.0` on `DeliveryWorker` + `HttpActorKeyResolver` | present | `crates/solid-pod-rs-activitypub/src/delivery.rs`, `http_sig.rs` | Sprint 12. P2. |
| 176 | AP actor Accept-negotiation (AP JSON-LD vs LDP profile) | `bfc37db` | `negotiate_actor_format` + `ActorFormat` enum; 8 new tests | present | `crates/solid-pod-rs-activitypub/src/actor.rs` | Sprint 12. P2. |
| 177 | Cloudflare `cf-visitor` protocol detection | `d5a82a8` | `scheme::effective_scheme` reads `X-Forwarded-Proto`, then `cf-visitor`, only when `--trust-proxy` / `JSS_TRUST_PROXY` is set (default off) | present | `crates/solid-pod-rs-server/src/scheme.rs`; `crates/solid-pod-rs-server/src/lib.rs` NIP-98 URL rebuild and base-URI sites | Behaviour change: behind a TLS-terminating proxy the flag must now be set or the NIP-98 URL is rebuilt as http. JSS applies it to ActivityPub only; here it covers every base-URI site. |
| 178 | AP CLI/env config knobs (`--ap-username`, `--ap-display-name`, etc.) | `0837ee2` (v0.0.62) | Feature `activitypub` mounts the actor at `GET /profile/card.jsonld` with `--ap-username`, `--ap-display-name`, `--ap-summary`, `--ap-nostr-pubkey` (env `JSS_AP_*`) | present | `crates/solid-pod-rs-server/src/activitypub.rs`; `crates/solid-pod-rs-server/src/main.rs`; `crates/solid-pod-rs-server/src/config/schema.rs` | Actor route only; inbox, outbox and collections are not mounted. Summary is HTML-escaped (JSS interpolates it raw). RSA key persists at `<data_root>/.ap/keys.json` (0600). |
| 179 | HTML error page helper (401/403 browser pages) | `ae796d0` (v0.0.68) | not implemented | wontfix-in-crate | — | Consumer binder concern. |
## 18. Payments, Tokens & Web Ledgers
Reference: [Melvin Carvalho's "A Practical Guide to Solid"](https://melvin.me/public/solid/)
(10-part series, April 2026). See also
[`docs/reference/melvin-practical-guide.md`](./docs/reference/melvin-practical-guide.md).
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 180 | HTTP 402 PaymentCondition in WAC ACLs | `src/wac/checker.js:130-197`, `src/handlers/pay.js` | `wac::conditions::PaymentCondition` + `payments::payment_required_body` | present | `src/wac/conditions.rs`, `src/payments.rs:267` | Melvin guide parts 3-4. |
| 181 | Pay-per-read auto-deduction | `src/handlers/pay.js` | `payments::debit()` | present | `src/payments.rs:158` | Melvin guide part 4. |
| 182 | X-Cost / X-Balance response headers | `src/handlers/pay.js` | `payments::payment_response_headers()` | present | `src/payments.rs:319` | Melvin guide part 4. |
| 183 | /pay/.balance endpoint | `src/handlers/pay.js` | `payments::balance_response()` | present | `src/payments.rs:328` | Melvin guide part 5. |
| 184 | WebLedger JSON storage | `src/pay/webledger.js` | `payments::WebLedger` struct | present | `src/payments.rs:120` | Melvin guide part 5. |
| 185 | TXO URI parsing (txo:chain:txid:vout) | `src/pay/txo.js` | `payments::parse_txo_uri()` | present | `src/payments.rs:372` | Melvin guide part 5. |
| 186 | Multi-chain deposits (btc, tbtc3, tbtc4, signet) | `--pay-chains` flag | `payments::ChainConfig` with 4 presets | present | `src/payments.rs:215` | Melvin guide part 8. |
| 187 | MRC20 token buy/withdraw (/pay/.buy, /pay/.withdraw) | `src/handlers/pay.js` | `mrc20::Mrc20State`, `Mrc20Op` | present | `src/mrc20.rs:77,98` | Melvin guide part 7. |
| 188 | BIP-341 key chaining (blocktrails) | `blocktrails` npm | `mrc20::bt_derive_chained_pubkey`, `bt_derive_chained_privkey` | present | `src/mrc20.rs:266,293` | Feature `bip341-taproot`. Melvin guide part 6. |
| 189 | MRC20 anchor verification against mempool | `blocktrails` npm | `mrc20::verify_mrc20_anchor` | present | `src/mrc20.rs:412` | Feature `bip341-taproot`. Melvin guide part 6. |
| 190 | Token rate config (--pay-token, --pay-rate) | JSS CLI flags | `PayConfig` token name + rate | present | `src/payments.rs` | Melvin guide part 7. |
| 191 | Payment discovery endpoint (/pay info) | `src/handlers/pay.js` | `payments::pay_info()` | present | `src/payments.rs:280` | Melvin guide part 3. |
| 192 | Peer-to-peer trading (/pay/.sell, /pay/.swap) | `src/handlers/pay.js` (order book) | `trading::OrderBook::create_order` + `execute_swap` | present | `src/trading.rs` | Melvin guide part 8. |
| 193 | /pay/.offers sell order listing | `src/handlers/pay.js` | `trading::OrderBook::list_offers` (all or filtered by pair) | present | `src/trading.rs` | Melvin guide part 8. |
| 194 | AMM constant-product pool (/pay/.pool) | `src/handlers/pay.js` (50 lines) | `trading::AmmPool` (x*y=k, add/remove liquidity, swap, 0.3% fee) | present | `src/trading.rs` | Melvin guide part 9. |
| 195 | Programmable state anchoring (NFTs, contracts, git) | `blocktrails` conceptual | `mrc20::Mrc20Trail` (token-specific) | present | `src/mrc20.rs:110` | JSS ships MRC20 and blocktrails token code only; Rust has `mrc20`, `blocktrail`, `solid-pod-rs-git` marks and the forge trails, so it is a superset. |
## 19. JSS v0.0.190 Phase 1 port (alpha.11)
Tracks the three Phase 1 features shipped by JSS v0.0.190 (May 2026,
issue #437). All three landed in **solid-pod-rs 0.4.0-alpha.11**:
type signatures, module bodies, feature flags, `pub use` lines, and
integration tests are in place. Default-off feature flags so downstream
consumers opt in explicitly.
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 196 | `--provision-keys` flag (single-user) + `POST /.pods {provisionKeys: true}` (multi-user) — generates Schnorr secp256k1 keypair, writes NIP-19 bech32-encoded `privkey.jsonld` to `/private/`, WAC-locks to WebID owner, seeds `nostr:pubkey` triple in WebID profile | JSS v0.0.190 (issue #437): single-user CLI flag + multi-user endpoint body field; keys stored at `pods/<webid>/private/privkey.jsonld`; npub seeded into WebID `profile/card` | `idp::key_provisioning::provision_pod_keys` → `KeyProvisioningOutcome` + `ProvisionPlan::provision_keys` field | present (Phase 1 port) | `crates/solid-pod-rs-idp/src/key_provisioning.rs`; `crates/solid-pod-rs/src/provision.rs` (`provision_keys` field, cfg-gated) | Feature `provision-keys` (default-off; in `solid-pod-rs-idp`, depends on `solid-pod-rs/did-nostr` + core's `nip98-schnorr` + `dep:k256`). BIP-340 Schnorr keypair generation; hand-rolled NIP-19 bech32 (no new deps). 3 integration tests in `tests/key_provisioning_smoke.rs`. |
| 197 | Pod-resident NIP-05 endpoint (`/.well-known/nostr.json?name=<local>`) — server route backed by WebID `nostr:pubkey` triple | JSS v0.0.190: pod-resident NIP-05 lookup endpoint backed by `profile/card` triple | `handle_well_known_nip05` route in `solid-pod-rs-server`; assembles response via existing `interop::nip05_document` builder + `webid::extract_nostr_pubkey` | present (Phase 1 port) | `crates/solid-pod-rs-server/src/lib.rs::handle_well_known_nip05`; `crates/solid-pod-rs/src/webid.rs::extract_nostr_pubkey` | Feature `nip05-endpoint` (default-off; depends on `did-nostr`). Emits `Access-Control-Allow-Origin: *`. 5 integration tests in `tests/nip05_endpoint_integration.rs`. |
| 198 | Pod data export — `GET /api/exports/all` JSON-LD time-chain bundle, `dct:created` ascending, excludes `/private/*` by default, opt-in `?include_private=true` requires owner WAC credential | JSS v0.0.190: agent-friendly pod export endpoint | `export::export_pod_jsonld` → `PodExportBundle` | present (Phase 1 port) | `crates/solid-pod-rs/src/export.rs::export_pod_jsonld` | Feature `export-jsonld` (default-off; depends on `tokio-runtime`). Works with the filesystem and memory backends. Backends without separate creation timestamps mirror `modified` into `created`. Three integration tests cover the library surface; server-route wiring remains deferred. |
---
## 20. Git pod auto-init (alpha.12, JSS #466/#469/#471)
Tracks the `tryAutoInitRepo` feature shipped by JSS in issues #466,
#469, and #471: pods become git repositories at creation time,
enabling HTTP smart-protocol push/pull and two-layer provenance
(Nostr signature + git tree-SHA). Both rows landed in
**solid-pod-rs 0.4.0-alpha.12**.
CF Workers constraint: `git init` requires a subprocess. The hook
trait is wasm32-safe; only the concrete `GitAutoInit` (in
`solid-pod-rs-git`) spawns a subprocess. NRF pod-worker (CF Workers)
must not enable the `git-auto-init` feature — see NRF ADR-089.
Agentbox and native solid-pod-rs-server deployments can enable it
freely.
| # | JSS feature | JSS path | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 199 | `tryAutoInitRepo` hook — `git init -b main <pod_path>` runs once at pod creation; `receive.denyCurrentBranch=updateInstead` config applied | JSS #466/#469/#471: `src/handlers/git.js` `tryAutoInitRepo`; runs on `POST /.pods` provisioning | `provision::GitInitHook` async trait (wasm32-safe; no subprocess in core). `provision_pod_ext<S,H>(storage, plan, git_hook: Option<(&H, &Path)>)` — calls `provision_pod` then fires the hook. Feature `git-auto-init` on `solid-pod-rs`. | present (alpha.12) | `crates/solid-pod-rs/src/provision.rs::GitInitHook`, `provision_pod_ext` | Trait only in core — subprocess-free, wasm32-compatible. CF Workers consumers must not enable this feature. |
| 200 | `GitAutoInit` concrete implementation — spawns `git init -b <branch>` via `tokio::process::Command`, applies `receive.denyCurrentBranch=updateInstead`; errors logged and swallowed so pod creation is never blocked by a missing `git` binary | JSS #466/#469/#471: same `tryAutoInitRepo` body | `solid_pod_rs_git::init::GitAutoInit` implements `GitInitHook`; `with_branch(b)` constructor for non-`main` deployments | present (alpha.12) | `crates/solid-pod-rs-git/src/init.rs` | 4 unit tests: `creates_dot_git`, `sets_deny_current_branch`, `custom_branch`, `idempotent`. Feature `git-auto-init` on `solid-pod-rs-git` (pulls `solid-pod-rs/git-auto-init`). |
## 21. JSS v0.0.197 → v0.0.204 delta (alpha.15)
| # | JSS feature | JSS commit | solid-pod-rs | Status | Rust file:line | Notes |
|---|---|---|---|---|---|---|
| 202 | MCP server — `POST /mcp` Streamable HTTP transport (MCP 2025-03-26), JSON-RPC 2.0, 16 tools (resource CRUD, ACL read/write, skills, docs, `pod_info`, streaming `subscribe`, `call_remote_pod` federation) | JSS #490 — `src/mcp/{index,protocol,tools,skills}.js` | `mcp::handle_mcp` + `mcp::tools` (16 handlers, SSE `subscribe`) | present (alpha.15) | `crates/solid-pod-rs-server/src/mcp/{mod,tools,skills}.rs` | Off by default; `--mcp` / `JSS_MCP`, `--no-mcp` wins. Identity reuses NIP-98 → same WAC as REST. Docs embedded via `include_dir`. `call_remote_pod` gated to `/private/federation/` for `did:nostr`, depth-3 cap. |
| 203 | `install` CLI — clone a Solid app + dual-push (`HEAD:main`, `HEAD:gh-pages`) into a pod over git, NIP-98-authenticated via `http.extraHeader` | JSS `src/cli/install.js` (`nip98Token(dest,'*',privkey)`) | `cli::install::run_install` + `parse_app_spec` | present (alpha.15) | `crates/solid-pod-rs-server/src/cli/install.rs` | App-spec grammar parity (bare/org-repo/URL, `#ref`, `=rename`). NIP-98 minting via `auth::nip98::mint` under server feature `install`; `--token` bearer fallback. 6 spec-parse unit tests. |
| 204 | NIP-98 token minting + git push leniency — single static token covers the multi-request git smart protocol (method `*`, repo-URL prefix) | JSS git.js lenient verify + `nip98Token` | `auth::nip98::mint` / `mint_with_payload`; `MatchPolicy::{Strict,GitLenient}` + `verify_at_with_policy`; git bridge opts into `GitLenient` | present (alpha.15) | `crates/solid-pod-rs/src/auth/nip98.rs`; `crates/solid-pod-rs-git/src/auth.rs` | `Nip98Event` now `Serialize`. Deterministic BIP-340 signature (zero aux-rand) matching `verify_raw`. Schnorr fully verified under both policies. Feature `nip98-schnorr`. 4 new unit tests (mint round-trip, body-binding, lenient accept, foreign-repo reject). |
| 205 | `getContentType` — extension→MIME for sidecar-absent resources (git-extracted app files render inline) | JSS #533 `src/utils/url.js` | `ldp::guess_content_type`; `FsBackend::read_meta` fallback routes through it | present (alpha.15) | `crates/solid-pod-rs/src/ldp.rs`; `crates/solid-pod-rs/src/storage/fs.rs` | Dotfile rule → Solid RDF/playlist overrides → `mime_guess` DB → `application/octet-stream`. New `mime_guess` dep. 4 unit tests. |
| 206 | Symlinked-directory container listing — symlink-to-dir lists as `ldp:BasicContainer` | JSS #531 | `FsBackend` list reclassifies symlinks from dereferenced stat | present (alpha.15) | `crates/solid-pod-rs/src/storage/fs.rs` | `file_type()` lstat reports symlinks as non-dirs; deref restores container semantics. Dangling symlinks remain non-containers. |
| 207 | Mashlib audio pane — serve mashlib for `audio/*` resources | JSS #533 `shouldServeMashlib` | `mashlib::should_serve` matches the `audio/*` family | present (alpha.15) | `crates/solid-pod-rs/src/mashlib.rs` | Whole-family match (mpeg/ogg/wave/flac + `audio/mpegurl`/`audio/x-scpls` playlists) instead of enumerated spellings. Video/image deliberately excluded (no panes). |
## 22. JSS v0.0.204 → v0.0.220 delta (alpha.7 audit)
Comparator: upstream `gh-pages` `f9f7a4d`, package `0.0.220`, fetched
2026-08-19. The range contains 47 commits and 5,267 insertions across 53 files;
many commits are version bumps or tests for the behaviours below.
| # | JSS feature | JSS commit / issue | solid-pod-rs | Status | Rust evidence | Notes |
|---|---|---|---|---|---|---|
| 208 | Preserve blank-node subjects through Turtle ↔ JSON-LD | `b48135c`, #536/#537 | RDF parser and serializer retain `_:` subject identifiers | present | `ldp::parse_ntriples`; blank-node round-trip regression tests | The n3.js defect is structurally absent and pinned by a Rust regression. |
| 209 | RFC 9207 `iss` equals the discovery issuer byte-for-byte | `80a42aa`, #524/#551 | `ProviderConfig::new` slash-normalises once for discovery, codes, and tokens | present | `solid-pod-rs-idp/src/provider.rs::ProviderConfig::new` | Fixed in alpha.7. |
| 210 | HEAD mirrors GET's negotiated content type | `f40978b`, #552/#553 | Actix routes HEAD through `handle_get`; shared negotiation path | present | `solid-pod-rs-server/src/lib.rs::handle_get`; content-negotiation tests | No separate drifting HEAD implementation. |
| 211 | Preserve IdP login errors through `oidc-provider` redirects | `a38a9e1`, #514/#554 | No `oidc-provider` interaction redirect state machine | present-by-absence | typed `ProviderError` results | Node IdP UI transport concern. |
| 212 | Passkey login degrades in WebView/insecure browser contexts | `8af19c4`, #556/#558 | Rust crate exposes protocol operations, not inline browser JavaScript | present-by-absence | `solid-pod-rs-idp/src/passkey.rs` | Client UI owns browser capability detection. |
| 213 | Configurable request body limit and `JSS_BODY_LIMIT` | `12e3e8d`, `e94a18e`, `84eae4b`, #543/#562/#566 | Body cap is configurable; `JSS_BODY_LIMIT` aliases `JSS_MAX_REQUEST_BODY` | present | `config/schema.rs`; server `PayloadConfig` | Rust default remains 50 MiB versus JSS 20 MB: deliberate semantic difference. |
| 214 | Tunnel credential passthrough | `7d1c9f3`, #530/#555 | No embedded Cloudflare tunnel runtime | not applicable | deployment-layer concern | Deployments configure tunnel forwarding outside the compiled pod server. |
| 215 | NIP-98 payload hash covers raw request bytes | `e31293b`, #565/#573 | Hash is verified over the received byte buffer | present | `auth/nip98.rs`; server extraction tests | Avoids JSON reserialisation drift. |
| 216 | Shift automatically from a busy CLI port and print openable URL | `345dc4f`, #557/#574 | retains a bound listener while probing the requested port and the next ten ports; reports the actual base URL | present | `solid-pod-rs-server/src/main.rs::bind_available` | Port `0` requests one OS-assigned ephemeral port. The retained listener avoids a probe-to-bind race. |
| 217 | MCP `write_acl` uses the resource URL in `acl:accessTo` | `3660b96`, #575/#576 | Tool writes a resource-scoped ACL | present | `solid-pod-rs-server/src/mcp/tools.rs` | Prevents the owner-lockout caused by `./`. |
| 218 | Reconcile JWK key path across both secp256k1 Y parities | `9c8b9ec`, #571/#577 | Rust uses typed curve keys and direct BIP-340 verification | present-by-absence | `auth/nip98.rs`; `solid-pod-rs-nostr` typestate verifier | No JavaScript VM JWK import path. |
| 219 | Skip landing-page seeding in JSS `--public` mode | `ec8547b`, #578/#579 | No WAC-disabled public mode | present-by-absence | deny-by-default server invariant | A public-mode exception would weaken the Rust security model. |
| 220 | POST-created `.acl`/`.meta` sidecars require Control | `b9b38ed`, #580 | Destination mode is elevated to Control and lockout-checked | present | server POST enforcement and sidecar-injection tests | Landed in the alpha.4 closeout. |
| 221 | WAC-exempt `appPaths` mount points | `b6a0169`, #582/#585 | Extensions are compile-time routes with explicit handlers | not applicable | server route table | No generic runtime app mount. |
| 222 | Public request → WebID `getAgent` accessor | `327bb01`, #584/#586 | Request authentication exposes the verified agent to route gates | present | `extract_pubkey`, `agent_uri`, `AuthContext` | Framework shape differs; capability is present. |
| 223 | `createServer({ plugins })` runtime loader | `c516bb6`, #589 | Cargo feature graph | not applicable | workspace feature declarations | Recompile-free third-party code loading is intentionally absent. |
| 224 | Repeatable `--plugin module[@prefix]` CLI flag | `3db7130`, #594/#595 | Cargo feature graph | not applicable | server feature declarations | Same architectural decision as row 223. |
| 225 | `api.mountApp` for wrapped Node-style applications | `58797b3`, #583/#590 | Typed Actix route composition at build time | not applicable | `build_app` | No Node middleware ABI. |
| 226 | Plugin `api.serverInfo` origin accessor | `538aa07`, #601/#605 | Server origin lives in typed state/config | not applicable | `AppState::nodeinfo` | No runtime plugin API consumer. |
| 227 | Plugin `api.reservePath` protocol path claims | `9dadbc9`, #602/#607 | Protocol routes are statically reserved before catch-alls | not applicable | `build_app` route order | Static route ownership is stronger than runtime claims. |
| 228 | Plugin roster via `api.plugins` | `18ee785`, #610/#612 | Cargo metadata/features describe compiled extensions | not applicable | Cargo manifests | No loaded-plugin runtime roster. |
| 229 | Derive plugin id from parent directory for generic basenames | `cdfeda8`, #596/#613 | No runtime plugin identifiers | not applicable | — | Node module-loader fix. |
| 230 | DID document context begins with DID Core `did/v1` | `f13526f`, #618; release `f9f7a4d` | Canonical three-context CG 0.1.1 renderer | present | `did_nostr_types::render_did_document` | Rust and JSS `0.0.220` now agree. |
## 23. JSS 0.0.220 → `e7e0525` delta (unreleased head, audited 2026-09-24)
Comparator: upstream `JavaScriptSolidServer/JavaScriptSolidServer` `gh-pages` at
`e7e0525` (package still `0.0.220`), fetched 2026-09-24. One commit, #619, both
halves of which were found while hardening this port and landed upstream from it.
Upstream has no sidestr code at this head, so the estate's sidestr wallet
lives downstream in nostr-rust-forum (its ADR-2015), not here.
| # | JSS feature | JSS commit / issue | solid-pod-rs | Status | Rust evidence | Notes |
|---|---|---|---|---|---|---|
| 231 | Atomic quota reservation: check-and-commit under a per-pod lock on PUT/POST, released on a failed write | `e7e0525`, #619 (`reserveQuota`, `withPodLock`) | `QuotaPolicy::reserve` is the write-path primitive; `FsQuotaStore` serialises it | present | `src/quota/mod.rs:73`, `:272`; `tests/quota_race.rs` | JSS adopted the Rust shape; the commit cites `QuotaPolicy::reserve`. |
| 232 | Idempotent deposit credit: the ledger and the "already credited" key commit together | `e7e0525`, #619 (`creditOnce`, `credited`) | Ledger and replay set live in one `state.json`, committed with one observer-atomic `Storage::put`; `GET /pay/.balance` only reads | present | `solid-pod-rs-server/src/handlers/pay.rs:87-130` (`PaymentState`), `:363-381` (`handle_balance`) | The double-credit window JSS closed never existed here: balance reads never credit, and a deposit's credit and replay key share one write. |
## 24. JSS `0.0.220` → `0.0.221` delta (core, audited 2026-10-19)
Comparator: upstream `JavaScriptSolidServer/JavaScriptSolidServer` `main` at
`8d68ceb` (package `0.0.221`, published 2026-10-18), six commits after `e7e0525`.
Three portable behaviours; the rest is a version bump and merge commits.
| # | JSS feature | JSS commit / issue | solid-pod-rs | Status | Rust evidence | Notes |
|---|---|---|---|---|---|---|
| 233 | CORS `Access-Control-Expose-Headers` adds `Accept-Query`, `Last-Modified`, `WWW-Authenticate` so cross-origin clients can read the auth challenge and cache validators | `7b38261`, PR #629 (`getCorsHeaders`) | `CORS_EXPOSE_HEADERS` is the JSS 0.0.221 list verbatim; the library default `DEFAULT_EXPOSE_HEADERS` gains the same three names | present | `crates/solid-pod-rs-server/src/lib.rs:3398, 3446`; `src/security/cors.rs:45`; `crates/solid-pod-rs-server/tests/middleware_guards.rs`; `tests/cors_preflight.rs` | `Accept-Query` is exposed for parity but this server emits no such header (no QUERY method), so listing it is inert. |
| 234 | Core `--git` lane defers to plugin-owned prefixes: a git-looking URL under a mounted app path (the forge) falls through to the plugin | `372678d`, #616 (`appPaths` check in the git hook) | Structural, not a runtime check: the `/forge` routes are registered before the pod-git smart-HTTP routes, so the forge owns its prefix; pinned by a test because the pod git lane refuses every push (`push_not_served`, ER-28) | present | `crates/solid-pod-rs-server/src/lib.rs:4924`; `crates/solid-pod-rs-server/tests/forge_prefix_precedence.rs` | Does not reopen push: the pod lane still refuses it, and the forge authorises its own pushes with forge push tokens. |
| 235 | `@fastify/websocket` registered once at the server root; a child plugin skips a second registration | `ca88517`, #545 / #620 | Fastify decorator scoping and `upgrade` listener bookkeeping; the Rust server has one WebSocket upgrade path per route, no shared plugin registry | present-by-absence | none | Node/Fastify construct with no Rust counterpart. |
---
## 25. JSS `plugins` repository, `forge/` delta (audited 2026-10-19)
Comparator: `JavaScriptSolidServer/plugins` at `63a16cb` (2026-10-19). Unlike §1–§24
this is **not** the JSS core tree: the forge, and the gitmark console it feeds, live
in the out-of-tree plugins repository. This section audits only the forge commits
`5069759` (repo-held trails, `git mark`, anchor tags), `a730b0b` (`did:nostr` display),
`40efb47`, `bd55eda` and `80a83d9` (mirror on kind-30618). The earlier forge history
(web-edit tiers, `sparseMarks`, the Anchors UI of July 2026, the forge-held trail model)
is **not** row-tracked here: `solid-pod-rs-forge` ships Phases 0–3 of that plugin and
the remainder stays scaffolded, so this section must not be read as full forge parity.
| # | JSS feature | JSS commit / issue | solid-pod-rs | Status | Rust evidence | Notes |
|---|---|---|---|---|---|---|
| 236 | Repo-held git-mark trail: read `blocktrails.json` (profile `gitmark`) at the default-branch tip and re-derive every mark's address with gitmark's state bytes, `sha256` of the commit hash as text | `5069759` (`repoTrail`, `gitmarkStateHash`) | `gitmark::parse_repo_trail` / `read_repo_trail`; the chained walk is `mrc20::bt_trail_outputs` / `bt_trail_addresses` (linear, where JSS re-walks per mark) | present | `crates/solid-pod-rs-forge/src/gitmark.rs:319, 406`; `src/mrc20.rs:797` | Address derivation needs feature `anchoring`; without it the trail is read and tagged but not re-derived. Expected addresses are differential vectors generated by running the JSS `trailAddress` / `trailProgram`. Adds a `gitmark` chain (`gm1p…`) JSS does not have; `regtest` (`bcrt`) is not derived. |
| 237 | `blocktrails.json` served with `Access-Control-Allow-Origin: *` for the hosted verifier; 404 in the same shape with CORS when there is no trail | `5069759` (`blocktrailsResp`) | `Route::BlocktrailsJson` / `h_blocktrails_json` | present | `crates/solid-pod-rs-forge/src/router.rs`; `crates/solid-pod-rs-forge/src/lib.rs:580` | Repo-held trails only: the forge-held trail document is unported (row 240). |
| 238 | Explorer links per chain: mempool.space, `txbt4` on mempool.guide, `xbt` on Kilombino | `5069759` (`EXPLORER_WEB`) | `gitmark::explorer_web`, `explorer_tx_url` | present | `crates/solid-pod-rs-forge/src/gitmark.rs:84, 112` | Helpers only; they feed the Anchors page, row 240. |
| 239 | NIP-34 events 30617 and 30618 name the latest on-chain mark with `chain`, `txo`, `commit` tags beside `anchor` and `r` | `5069759` (`anchorTags`) | `gitmark::anchor_tags`, wired into the NIP-34 builders: events carry `r`, `anchor`, `chain`, `txo`, `commit` for repo-held and forge-held trails, never falling back across models | present | `crates/solid-pod-rs-forge/src/nip34.rs` `gather_facts`; `crates/solid-pod-rs-forge/src/gitmark.rs` `anchor_tags` | Differential: ten repositories rebuilt byte for byte against events produced by the JSS plugin (`tests/jss-vectors/`). |
| 240 | Anchors tab, and the six-line flow (`npm init agent`, `npx fund-agent`, `git mark init`, commit, `git mark`, `git mark announce`) | `5069759` (`marksPage`) | Anchors tab and page: forge-held and repo-held views, enable form, txo form, chain badge, explorer links, behind `anchoring_ui` | present | `crates/solid-pod-rs-forge/src/marks_ui.rs`; `crates/solid-pod-rs-forge/src/trail.rs` `decorate_repo_page` | Plain forms that post to the API and redirect (no client script). The six-line CLI flow text is on the pitch page. |
| 241 | `did:nostr` agents are displayed as `did:nostr:<8 hex>…`, never as an npub; hex stays canonical in paths and ids | `a730b0b` (`didShort`) | `gitmark::did_short`; `html::agent_label` applied to owner crumbs, headings, repo list and author names | present | `crates/solid-pod-rs-forge/src/gitmark.rs:61`; `crates/solid-pod-rs-forge/src/html/mod.rs:63` | Display only. The JSON API field `npub` is not emitted by the Rust forge, so there is nothing to keep. |
| 242 | Mirror repos on their agent's kind-30618 state (opt-in `config.mirror`): one REQ per relay, one `git fetch` per real change into `refs/mirror/*`, a branch moves only to a fetched commit whose trail lists the marked commit | `40efb47` (`lib/mirror.js`, mirror driver) | `mirror::parse_mirror_config`, `verify_event`, `plan_mirror`, `branch_verdict`; `Mirror::apply` / `start` over `MirrorGit` / `GitCli` | present | `crates/solid-pod-rs-forge/src/mirror.rs:248, 359, 570, 670, 1392, 1536` | Behind the default-off feature `mirror` (relay egress plus a remote-influenced `git fetch`); see ADR-2009 for the security posture. Stricter than JSS on ref names, clone schemes and redirects, and on three points an independent security review found upstream also lacks: the committed trail must carry the followed agent's key (`pubkeyBase`), the marked commit must be in the history of the commit a branch moves to, and a state older than one already seen is dropped as a replay; a partial or garbled mark makes a state unusable instead of markless. Residual risks are listed in ADR-2009. The engine is a library: `solid-pod-rs-server` does not start it at boot (there is no `forge-mirror` server feature or config key), so an embedder calls `Mirror::start`. |
| 243 | Mirror record served before the repo-exists guard, so an unfetchable mirror still shows why | `bd55eda` (`apiMirror`) | `Route::ApiMirror` / `h_api_mirror`, served without `resolve_repo` | present | `crates/solid-pod-rs-forge/src/lib.rs:558`; `crates/solid-pod-rs-forge/src/router.rs` | |
| 244 | Mirror backoff resets when a relay answers, not on open; `CLOSED` and `NOTICE` are logged | `80a83d9` | `mirror::backoff_delay`, `classify_relay_frame` | present | `crates/solid-pod-rs-forge/src/mirror.rs:719, 750` | |
| 245 | Percent-encoded traversal refusal (`%2e`, `%2f`, `%5c`, `%00`) on every path | `plugin.js` handler 4977-4984 (`okSegment`/`okPath`) | `guard::percent_guard` applied in `ForgeService::handle` | present | `crates/solid-pod-rs-forge/src/guard.rs` | |
| 246 | JSON API reads: repo list, meta, tree, blob, commits, commit with structured diff | `apiRepoList` 3502 .. `apiCommit` 3596; `parsePatch` 342 | `api` and `diff` modules | present | `crates/solid-pod-rs-forge/src/api.rs`; `crates/solid-pod-rs-forge/src/diff.rs` | `repo.parent` and `forks` come from the fork module. |
| 247 | Issue state, retitle, list, comment; author-or-owner rules; open-count badge | `apiIssueState` 3790, `apiIssueRetitle` 3805, `apiIssuesHandler` 4002 | `api_issues` module over the typed spine | partial-parity | `crates/solid-pod-rs-forge/src/api_issues.rs` | Pod-body delete and edit are client-side buttons in JSS; the server side (hosted PATCH/DELETE) is row 252. |
| 248 | Repo PATCH (description), owner only | `apiRepoPatch` 3823 | `api::api_repo_patch`; `OPTIONS` allows PATCH | present | `crates/solid-pod-rs-forge/src/api.rs` | |
| 249 | Labels: default set, CRUD, set on issue and PR, filter, rename/delete cascade, luminance chips | `labelDefs` 1789, `apiLabel*` 3839-3990, `labelChips` | `labels` module; `PUT pulls/<n>/labels` | present | `crates/solid-pod-rs-forge/src/labels.rs`; `crates/solid-pod-rs-forge/src/pulls.rs` | |
| 250 | Search: bounded `git grep` of the default branch; repo-list `?q=` | `searchData` 1808, `apiSearch` 3991 | `search` module | present | `crates/solid-pod-rs-forge/src/search.rs` | argv only; the query always follows `-e`. |
| 251 | Releases from tags; `.tar.gz` and `.zip` archives | `listReleases` 1846, `archiveResp` 3247 | `releases` module | present | `crates/solid-pod-rs-forge/src/releases.rs` | Archives are buffered (256 MiB cap, 413 beyond); JSS streams them. |
| 252 | Hosted bodies: author-only edit and delete; vendored `xlogin.js` served byte-identical; page CSP | `apiHosted` 4827; `buildCsp` 590 | `hosted_edit` and `xlogin` modules | present | `crates/solid-pod-rs-forge/src/hosted_edit.rs`; `crates/solid-pod-rs-forge/src/xlogin.rs` | The page script tag for the composer is not ported: the Rust views carry no client JavaScript. |
| 253 | `privateRepos`: every read (git, HTML, JSON) owner-only | handler 4909-4916, 4946-4962, 5030-5041 | `privacy` module, enforced on path segments before dispatch | present | `crates/solid-pod-rs-forge/src/privacy.rs` | Fail-closed for routes added later. |
| 254 | Config: `FORGE_CONFIG` file, `baseUrl`, `loopbackUrl`, `cspConnect` | activate 617-632, 745-768 | `config_file` module; server `FORGE_*` environment | present | `crates/solid-pod-rs-forge/src/config_file.rs`; `crates/solid-pod-rs-server/src/lib.rs` `forge_config` | `loopback_url` is stored but nothing consumes it. |
| 255 | Per-repository serialisation of spine writes | `serializeOn` 1145 | `locks::ForgeLocks` | present | `crates/solid-pod-rs-forge/src/locks.rs` | Lock order: issue before pull. |
| 256 | Forks with lineage and counts; `refs/forge/*` hidden from upload-pack | `apiForkCreate` 4037, `readForkParent` 944, `ensureForgeRefsHidden` 978 | `forks` module | present | `crates/solid-pod-rs-forge/src/forks.rs` | Also sets `transfer.hideRefs`. |
| 257 | Compare: cross-fork head fetch, ahead/behind, diff | `compareData` 1026, `apiCompare` 4242 | `compare` module | present | `crates/solid-pod-rs-forge/src/compare.rs` | An anonymous `owner:ref` compare fetches that owner's same-named repo into the base repo; JSS does too. Needs edge rate limiting. |
| 258 | Pull requests: create, list, detail, commits, files, comment, close, reopen, labels | `apiPull*` 4259-4531, pages 3011-3192 | `pulls` module | present | `crates/solid-pod-rs-forge/src/pulls.rs` | Client-side script not ported (no client JS in the Rust views). |
| 259 | Real merges: `merge-tree --write-tree`, fast-forward when possible, two-parent commit, 409 on conflict, compare-and-swap, git >= 2.38 gate; records the anchor tip | `apiPullMerge` 4451-4531, `mergeTreeOf` 1051 | `merge` module | present | `crates/solid-pod-rs-forge/src/merge.rs` | Owner only, as in JSS. |
| 260 | Forge-held Blocktrails trail: enable, `recordTip` on push and merge, pending marks, txo recording, caps, `sparseMarks`, chain/mainnet guard, trail key custody | `apiMarksEnable` 4565, `apiMarkTxo` 4601, `recordTip` 1264, `blocktrailsResp` 4739 | `trail` module; hooked after push and merge | present | `crates/solid-pod-rs-forge/src/trail.rs` | The forge never broadcasts or spends. Key 0600, never logged or returned. A mirrored repo is not advanced by the mirror write path. |
| 261 | NIP-34 kinds 30617 and 30618: builders, forge-instance signer, `GET /nostr`, relay publisher, owner-only `POST /announce` | `buildRepoEvent` 1428, `buildStateEvent` 1497, `signAnnounceEvent` 1410, `publishToRelay` 1530, `announceRepo` 1758 | `nip34` and `publish` modules (feature `announce`) | present | `crates/solid-pod-rs-forge/src/nip34.rs`; `crates/solid-pod-rs-forge/src/publish.rs` | Branch order is byte order where JSS uses ICU `localeCompare`; only tag order differs. Operator relays admitted through `OperatorRelays`. |
| 262 | Mirror hardening beyond JSS: trail bound to the followed key, marked commit reachable, replayed older states dropped, garbled marks unusable, `refs/mirror` hidden, DNS pinning, size cap, bounded queue, backoff, pushes refused | `lib/mirror.js` (none of these) | `mirror` module; upstream PR JavaScriptSolidServer/plugins#15 carries the first five | net-new | `crates/solid-pod-rs-forge/src/mirror.rs` | Server starts it at boot from `FORGE_MIRROR_RELAYS` and `FORGE_MIRROR_FOLLOW` (feature `forge-mirror`). |
| 263 | Web-edit: commit one file from the browser by git plumbing with compare-and-swap; 1 MiB cap; path rules; owner-only; `openEdit` demo mode | `apiRepoEdit` 4151, `cleanEditPath` 4082, `withCors` 3495 | `web_edit` module; `POST api/repos/<o>/<n>/edit` | present | `crates/solid-pod-rs-forge/src/web_edit.rs`; `crates/solid-pod-rs-forge/tests/web_edit.rs` | Stricter than JSS: case-insensitive `.git`, all-dot segments, `check-ref-format` branches, symlink leaf/parent refused (409). `open_edit` reproduces JSS exactly (any caller may edit any repo) and is off by default; no shipped default turns it on. |
| 264 | Ephemeral preview: signed kind-21617 event, no commit | `apiRepoPreview` 4102, `EPHEMERAL_PREVIEW_KIND` 123 | `preview` module; `POST api/repos/<o>/<n>/preview` (feature `announce`) | present | `crates/solid-pod-rs-forge/src/preview.rs` | |
---
## 26. JSS `0.0.221` → `0.0.224` delta (core, audited 2026-11-01)
Comparator: upstream `JavaScriptSolidServer/JavaScriptSolidServer` `gh-pages` at
`7c9b0de` (package `0.0.224`, 2026-10-22), ten commits after `8d68ceb`. Three
portable behaviours: ACLs read by expanded JSON-LD IRI and ACL writes that grant
nothing refused (#631, PR #632: `c189682`, `ae3be87`); PATCH applied as quads with
Append for insert-only, per-resource serialisation and a cross-process lock (PR #633:
`a6834e8`, `2fd3454`); NIP-11 at the relay URL (PR #635: `c42cc13`). The rest is three
version bumps and two merges with no hunks of their own. Audited by an Opus mesh with an
adversarial verifier per lane; rows marked *runtime* were reproduced against the built
`solid-pod-rs-server`, the rest are code-read. Every row states whether the server
reaches the behaviour, not just whether the library has it.
| # | JSS feature | JSS commit / issue | solid-pod-rs | Status | Rust evidence | Notes |
|---|---|---|---|---|---|---|
| 265 | JSON-LD ACLs read by expanded IRI: `@context` (object or array), any prefix, `@vocab`, expanded form, a single node with no `@graph`, `@type` as an array; agent classes normalised to full IRIs | `c189682`, #631/#632 (`src/wac/parser.js`: `parseAcl`, `expandNode`, `aclProp`) | `parse_jsonld_acl` is serde over literal `acl:*` keys and ignores `@context`; a `@graph` object is required | missing | `src/wac/document.rs:45-84`; `src/wac/mod.rs:96-109`; `src/wac/evaluator.rs:128-133` | P2. Reads fail closed, so there is no escalation, but JSON-LD ACLs JSS writes in these forms lock their owners out after migration, and our PUT refuses them with 409 where JSS stores them (201). The Turtle reader already expands prefixes (`src/wac/parser.rs:181`). |
| 266 | A `.acl` body that does not parse is refused | `c189682` (`src/handlers/resource.js:986-991`): `{not json` 400, `[]` 400, `text/plain` 415 | The lockout guard returns `true` on input it cannot parse, and JSON-LD bodies are never validated: the body is stored (201) and resolves as invalid, so everyone, including the owner's repair PUT, then gets 403 | missing | `crates/solid-pod-rs-server/src/lib.rs:906-909`; `src/rdf_input.rs:84-88`; `src/wac/resolver.rs:283-315` | **P1, runtime.** Self-inflicted lockout of a whole subtree; a bad `/.acl` bricks the pod over HTTP, repairable only on the storage backend. Fix: fail closed in the guard, as MCP `write_acl` already does (`mcp/tools.rs:853-856`), and answer 400. The same gap exists through POST to a sidecar and COPY. |
| 267 | A `.acl` write that grants no mode to anyone is refused (PUT 400, PATCH 409, JSON `{error,message}`) | `c189682` + `ae3be87` (`grantsAccess`) | The guard's rule is a superset (the writer must keep Control) and refuses with 409 and a plain-text body | semantic-difference | `crates/solid-pod-rs-server/src/lib.rs:879-944`, `:1756-1763`, `:2005-2017` | P3. The superset rule is deliberate (July P0-2) and also refuses some ACLs JSS stores, such as a public-read-only ACL. Ahead of JSS: our guard also runs on POST to a sidecar and on COPY. The library has no guard, so non-server consumers are unprotected. |
| 268 | Tests: the #631 fixtures (6 parser, 6 integration) | `test/wac.test.js:792-1011` | No HTTP test asserts any guard refusal; `tests/git_marks.rs:202` tolerates either outcome | missing | none | P2. The audit's runtime probes (`docs/reference/jss-parity-audit-2026-11-01/wac-probe-*.rs`, run as an integration test of `solid-pod-rs-server` with `--features dev-bearer`) are the starting point. |
| 269 | N3 Patch parsed as full N3: `@prefix`, prefixed names, `a`, `;` and `,`, long and typed literals, relative IRIs resolved against the resource | `a6834e8`, PR #633 (`src/patch/quads.js:38-62`) | `apply_n3_patch` reads each block as line-wise N-Triples; prefixed names give 400 | partial-parity | `src/ldp.rs:1149-1194`, `:817-862`; `crates/solid-pod-rs-server/src/lib.rs:1961-1963` | **P0, runtime.** JSS's own test bodies get 400. Real Solid clients send prefixed N3. Fix with the `oxttl` parser `rdf_input::parse_rdf_body` already wraps (`src/rdf_input.rs:40`), base set to the resource URL. Supersedes the "present" on row 39. |
| 270 | `solid:where` variables bound against the document; must bind exactly once, else 409 | `a6834e8` (`quads.js:105-153`) | Ground containment only; a miss gives 412; a `?var` gives 400 | missing | `src/ldp.rs:1170-1180` | P1, runtime (412 vs 409). Row 40's "strictly more conformant" is now the wrong way round. |
| 271 | Deleting an absent triple gives 409 (N3 and SPARQL `DELETE DATA`) | `a6834e8` (`quads.js:158-162`) | Silently ignored, 204 | missing | `src/ldp.rs:1186`, `:1432-1438` | P2, runtime. |
| 272 | Blank nodes in a patch minted fresh | `a6834e8` (`quads.js:133-137`) | Labels kept verbatim, can merge with the document's own | missing | `src/ldp.rs:849-853` | P2, code-read. |
| 273 | Insert-only PATCH (no deletes, no `where`) authorised as `acl:Append` | `a6834e8` (`src/server.js:808-823`, `quads.js:91`) | Every PATCH needs `acl:Write`, checked before the body is parsed | partial-parity | `crates/solid-pod-rs-server/src/lib.rs:1921-1928` | P1 usability, runtime (Append-only caller: 401). Never weaker than JSS for any patch shape, so not a security gap; `.meta` is stricter here than in JSS. Fix: parse first, ask for Append when deletes and `where` are empty, Write when the body will not parse. Write implies Append (`src/wac/mod.rs:171-173`). |
| 274 | PATCH serialised per resource in-process: no lost updates under concurrent inserts | `a6834e8` (`src/handlers/resource.js:1191-1215`) | None: read at `:1948`, write at `:2019`, last writer wins | missing | `crates/solid-pod-rs-server/src/lib.rs:1948`, `:2019` | **P1, runtime: 20 parallel inserts all returned 204, 3–4 survived** (four rounds). Temp-file-and-rename stops torn files (ahead of JSS) but not lost updates. PUT and MCP `write` race the same way. |
| 275 | Cross-process `.<name>.lock` (O_EXCL, stale after 15 s, 30 s wait then 503 with `Retry-After`) | `2fd3454` (`src/storage/filesystem.js:135-179`) | None | missing | `src/storage/fs.rs:111-145` | P2. Matters only for several processes on one data root. If added, hide the lock file in listings (`fs.rs:306`). |
| 276 | PATCH reads JSON-LD (`@graph`), HTML data islands and Turtle, and writes back in the stored form | `a6834e8` (`resource.js:1274-1378`) | Only an N-Triples-stored body is patchable; anything else gives 409 | missing | `crates/solid-pod-rs-server/src/lib.rs:2182-2197` | P1, runtime: PATCH after a prefixed-Turtle or JSON-LD PUT gives 409. |
| 277 | `jsonLdToQuads` reads every node of `@graph` (GET-as-Turtle of a `@graph` document no longer empty) | `a6834e8` (`src/rdf/turtle.js`) | Conneg transcodes only N-Triples-stored bodies; stored JSON-LD is served as JSON-LD even to `Accept: text/turtle`; JSON-LD output is a bare array, not `@graph` | partial-parity | `crates/solid-pod-rs-server/src/lib.rs:2139-2172`; `src/ldp.rs:813` | P2. Same root cause as row 276: no general JSON-LD input parser (`src/rdf_input.rs:60-65`). |
| 278 | NIP-11 relay information at the relay URL when `Accept` contains `application/nostr+json`: 200, `application/nostr+json`, `Access-Control-Allow-Origin: *`; a plain GET stays 404 | `c42cc13`, PR #635 (`src/nostr/relay.js:251-265`) | `RelayInfo::jss_compatible` builds the same seven fields and `supported_nips` `[1,11,16]`; no HTTP handler, and the server mounts no relay | missing | `crates/solid-pod-rs-nostr/src/relay.rs:308-329`; `crates/solid-pod-rs-server/Cargo.toml` (no direct `solid-pod-rs-nostr` dependency) | P2: JSS ships the relay default-off (`config.js:64`). The nostr crate is already linked transitively under `forge-announce`, as a client only. WebSocket and NIP-11 share one route in JSS, so an actix mount needs one handler that upgrades or serves the document. Mount request: row 280. |
---
### Local repair progress — candidate ACL admission, 2026-11-01
Rows 266–268 now have a scoped, unreleased server repair. Candidate policies
must parse and grant effective Control on the destination through the same
shared evaluator and linked identities as request admission. PUT, POST,
JSON PATCH, RDF PATCH call sites and COPY use this guard; COPY's accompanying
ACL is retained only when it preserves destination Control. Ordinary `.meta`
bodies still require Control but are not parsed as ACLs. Nine route regression
tests cover malformed bodies, wrong targets, unsatisfied conditions, linked
identity preservation, JSON PATCH, COPY and metadata. RDF PATCH's existing
parse/serialisation limitations remain open, so its call-site inclusion is not
an interoperability claim. The rows above retain the audited baseline and
upstream comparator; this note does not promote every row to parity. Publication,
consumer adoption and deployment remain separate.
The guard uses the existing bounded policy reader and RDF syntax parser. It
does not fix policy information lost by the legacy Turtle reader, JSON-LD IRI
expansion, absolute target resolution (288), provisioning (287), or the
stored container ACL copies (290). Discovery for new interactions now uses
the resolver's canonical sibling location; the ninth regression follows that
HTTP link and verifies the resulting child-access change. Existing inner ACLs
need the explicit review in [the migration guide](docs/how-to/migrate-from-jss.md).
## 27. Pre-existing core gaps surfaced by the 2026-11-01 audit
These are not part of the `0.0.224` delta: JSS core already had them at the prior
comparator `8d68ceb`. They are recorded because the audit checked server routes rather
than library presence, the gap the July closeout named. Rows 91, 101, 103 and 104 are
reclassified in place to match.
| # | JSS feature | JSS commit / issue | solid-pod-rs | Status | Rust evidence | Notes |
|---|---|---|---|---|---|---|
| 279 | `/.notifications` served whenever it is advertised; `Updates-Via` sent only when notifications are on | `src/server.js:537-540`; `src/notifications/index.js:36` | The `solid-0.1` session is library-only. No `/.notifications` route, no WebSocket dependency in the server, yet `Updates-Via` is sent unconditionally on 7 GET/HEAD/OPTIONS paths, `/.well-known/solid` names it as the notification gateway, and the live-reload script dials it | missing | `crates/solid-pod-rs-server/src/lib.rs:1517`, `:2328`, `:428`; `src/interop.rs:65`; `src/handlers/legacy_notifications.rs:99` | **P1.** Clients are pointed at an endpoint that does not exist. Fix: mount the session on an actix WebSocket, or stop advertising it. Rows 91, 92, 93 and 95 are library-only for the same reason. |
| 280 | Relay mount knobs `--nostr`, `--nostr-path` (default `/relay`), `--nostr-max-events`, `JSS_NOSTR*`; `<relay>/info` alias | `bin/jss.js:117-120`; `config.js:63-66`; `relay.js:292-298` | None | missing | none | P2, with row 278. Any WAC exemption for the relay path must match on a segment boundary (exact path, `path/` or `path?`), never a bare prefix. |
| 281 | ActivityPub inbox and outbox routed, with signed delivery | `src/ap/routes/inbox.js`, `outbox.js` | `handle_inbox`, `handle_outbox`, `DeliveryWorker` exist; only the actor document is served | missing | `crates/solid-pod-rs-activitypub/src/inbox.rs:50`, `outbox.rs:33`; `crates/solid-pod-rs-server/src/lib.rs:1456` | P1. July closeout P0-1 remains half-open. Acceptance: the plugins repo `federation-demo/` two-server flow. |
| 282 | WebRTC signalling (`--webrtc`) | `src/webrtc/index.js` (March 2026) | Not provided | missing | none | P2. If ported, use the plugins repo's attach order (`webrtc/plugin.js`), which fixes an auth race core has. Pairs with the plugins `turn/` credentials. |
| 283 | remoteStorage draft-22 at `/storage/:user/*`, on by default | `src/remotestorage.js`; `src/server.js:500` | Not provided | missing | none | P2. `docs/reference/jss-feature-inventory.md` wrongly says JSS lacks it. |
| 284 | Browser terminal over WebSocket (`--terminal`) | `src/terminal/index.js` | Not provided | explicitly-deferred | none | A remote shell. If ever ported, take the plugins repo's model: mandatory access control, curated environment, confined working directory. |
| 285 | `/.tunnel` reverse HTTP tunnel over WebSocket (`--tunnel`) | `src/tunnel/index.js` | Not provided | explicitly-deferred | none | Distinct from row 214 (Cloudflare credential passthrough). |
| 286 | `If-Match` / `If-None-Match` honoured on PUT and PATCH | `src/handlers/resource.js:1246-1255` | `evaluate_preconditions` is library-only; the server never calls it | missing | `src/ldp.rs:1545`; no call in `crates/solid-pod-rs-server/src` | **P1, runtime:** a stale `If-Match` is ignored (PATCH 204, PUT 201) and `If-None-Match: *` overwrites. With row 274 open, clients have no optimistic-concurrency fallback. |
| 287 | Pod created by self-service registration is usable by its owner | `src/server.js:1296-1303` (owner ACL written with the pod, `relativizeOwnerWebId`) | `POST /.pods` (`provision_named_pod`) writes containers and the profile card but no ACL: the owner gets 403 on `GET /alice/` and on PUT into their own pod, and anonymous gets 401 on the profile card | missing | `crates/solid-pod-rs-server/src/lib.rs:2806-2855` | **P0, runtime.** The server seeds no root ACL, so a new pod inherits whatever root ACL the operator configured: with none, the owner is refused (the probe); with a root ACL that has `acl:default`, the pod gets the root's grants, not an owner grant, and anything that root grants publicly applies to the new pod too. `tests/registration_security.rs` covers only the gate. A fix must write path targets at the sidecar our resolver reads (`/alice.acl`). |
| 288 | `acl:accessTo` / `acl:default` given as absolute or relative IRIs match the resource | `src/wac/parser.js:224-227` | The evaluator compares raw server-root paths; absolute IRIs fail even on the server's own origin, and so do `./doc.ttl` and `doc.ttl`; only `</path>` matches. PUT accepts such an ACL (201), then the owner is locked out | missing | `src/wac/evaluator.rs:63-106`, `:286-299`; `crates/solid-pod-rs-server/src/lib.rs:996` | **P1, runtime.** Fails closed. Server provisioning writes paths, so the server's own pods are unaffected, but the library `provision_pod` writes an absolute public-type-index ACL (`src/provision.rs:285-298`): anonymous 401, owner 403. Its test (`src/provision.rs:611-631`) masks this. Fix: resolve targets against the ACL's URL, then compare. |
| 289 | Relative `acl:agent` WebIDs resolved against the ACL | `src/server.js:1296-1303`; `src/wac/parser.js:228` | Agent strings compared raw | missing | `src/wac/evaluator.rs` (`agent_matches_with_groups`) | P2, code-read. A migrated JSS pod's owner rules never match. Fails closed. |
| 290 | Container ACL at `<container>/.acl` | JSS layout `/c/.acl` | Resolver reads `/c.acl`; a JSS-layout `/c/.acl` is ignored and the parent's grant applies | missing | `src/wac/resolver.rs` | **P1**, runtime. Widens access after a migration: the parent's broader grant applies in place of the narrower container ACL. `migrate-from-jss.md` must state the move. |
### Defects found in passing (not parity rows)
These are bugs in our PATCH path, reproduced at runtime, with no JSS counterpart to track.
They are listed for the fix queue, not counted in the parity figures.
- **P0, non-ASCII corruption on every PATCH.** Literals are decoded byte by byte
(`src/ldp.rs:889-890`), so `é` is stored as `c3 83 c2 a9`; stored non-ASCII text is
re-mangled by any later PATCH to the resource, even one that does not touch it, and
`"é"` becomes `"u00e9"`.
- **P1, silent triple loss.** A second triple on one stored line is dropped by the next
PATCH (`src/ldp.rs:832-840`); Turtle using `;` passes the 409 guard, the PATCH returns
204, and the triple after the `;` is lost.
- **P1, SPARQL Update with variables is a no-op that returns 204** (`src/ldp.rs:1441-1460`).
Row 41's "full SPARQL 1.1 algebra" is overstated.
The first two disappear if row 269 moves to a real parser.
---
## 28. JSS plugin system and the `plugins` repository (non-forge, audited 2026-11-01)
Comparators: the JSS core plugin loader (`src/plugins.js`, #206, `createServer({ plugins })`,
`activate(api)`) at `7c9b0de`, unchanged since `8d68ceb`; and `JavaScriptSolidServer/plugins`
at `63a16cb`, unchanged since §25. Rows 221 to 229, 234 and 235 already cover the loader,
`--plugin`, `mountApp`, `serverInfo`, `reservePath`, `getAgent`, the git-lane deferral and the
root WebSocket; they are cross-referenced, not restated. The plugin api itself is not a
parity target (ADR-2011): parity means matching what plugins *achieve*, the core-side
exemptions JSS grants app prefixes, and per-plugin bounded comparators as ADR-2009 set up for
the forge.
| # | JSS feature | JSS commit / issue | solid-pod-rs | Status | Rust evidence | Notes |
|---|---|---|---|---|---|---|
| 291 | Dotfile guard defers to app-plugin prefixes | `0976f3e` (#206); `src/server.js:598-604` | `DotfileGuard` runs before routing and carves out only `/.well-known/`, `/.pods`, `/pay/`; a dot segment under `/forge` gets 403 | partial-parity | `crates/solid-pod-rs-server/src/lib.rs:3844-3846`, `:5198` | Low, runtime. Upstream's forge refuses dot segments itself (`plugins/forge/lib/helpers.js:11`), so JSS answers 404 and ours 403; the residue is the status and no carve-out for a dot-prefixed mount. `0976f3e` predates `8d68ceb`, so this was untracked, not drift. |
| 292 | `deactivate()` on server close | `c516bb6`; `src/plugins.js:452-460` | No shutdown hook: the forge and mirror sit in a process-static `OnceLock`; `MirrorHandle::stop()` exists and nothing calls it | missing | `crates/solid-pod-rs-server/src/lib.rs:4999-5000`; `crates/solid-pod-rs-forge/src/mirror.rs:2206` | P2. ADR-2010 ships forge and mirror on, so this hits shipped builds: relay sessions are cut mid-await at runtime drop instead of closed. |
| 293 | Fail-loud boot on a bad plugin | `c516bb6`; `src/plugins.js:223-254`, `:42-44` | Composition errors are compile errors; a forge construction error at boot is only logged, and every `/forge` request then returns 500 | partial-parity | `crates/solid-pod-rs-server/src/lib.rs:5044-5051`; `crates/solid-pod-rs-server/src/main.rs:459-460` | P3. Make `start_forge` failure fatal when `forge` is compiled in. |
| 294 | `api.storage.pluginDir()`: a private, never-served data directory | `c516bb6`; `src/plugins.js:263-270` | `<data_root>/.forge`, kept off HTTP by `DotfileGuard` | present | `crates/solid-pod-rs-server/src/lib.rs:4833-4840` | Per extension, not generic. |
| 295 | `api.config` and `api.log` | `c516bb6`; `src/plugins.js:58-64`, `:260-261` | Typed `ForgeConfig` from `FORGE_CONFIG` and env; `tracing` | present | `crates/solid-pod-rs-server/src/lib.rs:4919-4957` | |
| 296 | `api.ws.route`: plugin WebSocket endpoints on the host's upgrade stack | `c516bb6` (#588/#589); `src/plugins.js:418-443` | No compiled extension serves a WebSocket | not applicable | — | Becomes applicable with row 279 or 278. |
| 297 | A WAC-exempt prefix must not let unrouted verbs reach the LDP write handlers | `9dadbc9` (#602) guards reservations only; `src/server.js:802`, `:975` | `/forge` is routed for every method ahead of the catch-all | net-new | `crates/solid-pod-rs-server/src/lib.rs:5306-5307`; runtime: PUT under `/forge` returns 404, nothing written | Invariant for every future compiled extension: route all methods under its prefix, or none. Upstream's own comment (`src/plugins.js:324-330`) names the trap. |
| 298 | `api.events.onResourceChange` | #603 (open upstream) | `Storage::watch` → `StorageEvent` | net-new | `src/storage/mod.rs:58-65` | Forge git writes do not emit pod events. |
| 299 | `api.authorize(request, path, mode)` | #604 (open upstream) | Public WAC evaluator | net-new | `src/wac/evaluator.rs:159-234` | Library-level; the forge uses namespace ownership instead. |
| 300 | Plugin-to-plugin hook isolation | `plugins/NOTES.md:213-225` | Single compiled extension; no shared hook registry | present-by-absence | none | Upstream measures zero isolation. |
**Plugins-only directories at `63a16cb` (candidates, not tracked).** ADR-2009 bounds the
plugins comparator to the forge, so none of these is counted. Port-ready candidates, in
order: `didweb/` (`did:web` documents beside did:nostr and did:key), `metrics/` (`/healthz`
and Prometheus over the existing `SecurityMetrics`, `src/metrics.rs:26`), `sparql/` (SELECT
over pod data, WAC-filtered; `spargebra` is already a dependency), `backup/` (`.tar.gz`
beside the JSON-LD export, `src/export.rs:179`), `turn/` (with row 282) and `capability/`
(after upstream #506 settles, and on an established token format rather than a bespoke one;
note the forge's macaroon-lite push token, `crates/solid-pod-rs-forge/src/token.rs`, is
already a bespoke construction scoped to the forge). `pay/` documents that 402 cannot be a
plugin, which matches keeping pay in the core server. Out of scope: admin, bluesky, caldav,
carddav, dashboard, gallery, gitscratch (conflicts with ER-28), globs, jmap, markets,
mastodon, matrix, micropub, oembed, otp, recordweb, ripple, rss, s3, search, shortlink,
webdav. Of the eleven core-feature ports, nip05 (rows 128, 197), webfinger (105), corsproxy
and pay are already tracked elsewhere; relay, notifications, activitypub, webrtc,
remotestorage, terminal and tunnel are rows 278 to 285. The forge is §25. Full classification of all 41
directories: `docs/reference/jss-parity-audit-2026-11-01.md`.
---
## Sprint history
Compressed appendix — one line per sprint. Consult git history for per-row corrections.
- **Sprint 3 close (2026-04-19)**: 97 rows tracked, 74% strict parity (72/97).
- **Sprint 5 (2026-04-20)**: Sprint 5 mesh-and-QE-fleet review corrected multiple over-stated rows, added 5 WAC 2.0 rows (53–56 + 62b) and the DPoP signature surface. Net: 102 rows, 59% strict (verified, not claimed).
- **Sprint 7 (2026-04-21)**: operator surface rows added — rate-limit primitive, CORS helpers, quota CLI, multi-tenancy, server route table (112 rows).
- **Sprint 8 (2026-04-24)**: JSS 0.0.144 → 0.0.154 delta (13 patch releases) + LWS 1.0 Auth Suite rows 150–155 added; 6 rows landed (atomic quota writes, CID service entry, Cache-Control on RDF, dotfile conneg, WebID linkback predicates). 121 rows, 56% strict (~78% spec-normative).
- **Sprint 9 close (2026-04-24)**: 11 rows promoted to `present` (P0 DPoP signature + SSRF + dotfile primitives; WAC 2.0 condition framework; pod bootstrap with type indexes + public-read ACL; DPoP jti replay cache) and row 51 `acl:origin` promoted from shared-gap to net-new. 121 rows, 66% strict / 85% spec-normative. No outstanding P0.
- **Sprint 10 close (2026-04-24)**: Four sibling crates filled out end-to-end (`solid-pod-rs-git`, `solid-pod-rs-nostr`, `solid-pod-rs-activitypub`, `solid-pod-rs-idp`). 20 rows flipped missing → present; 2 rows (80 Passkeys, 81 Schnorr SSO) shipped as partial-parity trait hooks. 733 tests, 83% strict / ~97% spec-normative.
- **Sprint 11 close (2026-04-24)**: Top-10 roadmap closed. 14 rows promoted to `present` (LWS 1.0 Auth Suite rows 150/152/153 — NEW `solid-pod-rs-didkey` crate + shared `SelfSignedVerifier` trait + `CidVerifier` dispatcher + ADR-057 delta audit; solid-0.1 legacy notifications; Passkeys + Schnorr full wiring; config loader + 31 env vars; subdomain file-label heuristic; 5xx logging middleware; `quota reconcile` / `account delete` / `invite create` CLI). 2 rows (152 CID, 153 did:key) are net-new — we ship LWS 1.0 Auth Suite before JSS. Workspace: **835 tests pass / 0 fail**, clippy `-D warnings` clean. 121 rows, **~97% strict / ~100% spec-normative**.
- **Sprint 12 close (2026-05-06)**: JSS v0.0.60–v0.0.71 drift closed. 11 new rows (169–179); 8 landed as `present` (security hardening: size-capped ACL parsing, iterative podName sanitization, DNS failure blocking, `.account` dotfile; IdP: min password length; AP: outbox POST + Note wrapping, User-Agent, Accept-negotiation, follower fan-out, actor cache datetime). 3 deferred (cf-visitor, AP CLI, error pages). 922 insertions across 23 files, ~35 new tests. 132 rows, **~98% strict / ~100% spec-normative**.
- **Sprint 13 close (2026-05-16, alpha.11)**: JSS v0.0.190 Phase 1 absorption complete. 3 new rows (196–198): key provisioning (`provision-keys`), pod-resident NIP-05 endpoint (`nip05-endpoint`), JSON-LD export (`export-jsonld`). All three shipped with bodies + tests. 135 rows, **~98% strict / ~100% spec-normative**.
- **Sprint 14 close (2026-05-16, alpha.12)**: Git pod auto-init (JSS #466/#469/#471). 2 new rows (199–200): `GitInitHook` wasm32-safe trait in core + `GitAutoInit` subprocess impl in `solid-pod-rs-git`. 4 unit tests. `provision_pod_ext` call-site API. 137 rows, **~99% strict / ~100% spec-normative**.
- **Sprint 15 (2026-10-20, alpha.17)**: JSS `0.0.220` → `0.0.221` core delta (rows 233–235: CORS expose list, plugin-prefix precedence, root WebSocket) and the JSS `plugins` forge delta (rows 236–244: repo-held git-mark trails, `blocktrails.json`, explorer links, `did:nostr` display, kind-30618 mirror). 244 rows, 97.3% strict; one new partial row (239) and one deferred row (240).
- **Sprint 16 (2026-10-20, alpha.17)**: the rest of the JSS plugins forge (rows 245–264: JSON API, labels, search, releases, private repos, forks, compare, pull requests, merges, forge-held anchoring, NIP-34 publisher, web-edit, preview), the mirror hardening and review fixes, core rows 35, 60, 83, 177, 178 closed and 70, 195 reclassified. 264 rows, 99.2% strict; open: 135, 247.