diff --git a/Cargo.toml b/Cargo.toml index b0e0d01..a426f6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.4.0-alpha.2" +version = "0.4.0-alpha.4" edition = "2021" license = "AGPL-3.0-only" repository = "https://github.com/dreamlab-ai/solid-pod-rs" diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ddb0d05..0c8f014 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,40 @@ +# v0.4.0-alpha.3 (Phase 4 chain prep — 2026-05-07) + +Adds the `core` feature flag for pure-logic consumers (wasm32 / CF +Workers). All async-IO surfaces — tokio, reqwest, notify, +tokio-tungstenite, futures-util — are now optional dependencies gated +behind the `tokio-runtime` feature. + +The `default` feature set preserves the surface from `0.4.0-alpha.2` +bit-for-bit: `["std", "fs-backend", "memory-backend", "tokio-runtime", +"notifications"]`. Existing consumers do not need to change anything. + +Per ADR-076/078 absorption, `nostr-bbs-pod-worker` (CF Workers) wires +this crate via `default-features = false, features = ["core"]` and +consumes the pure surfaces: `wac`, `webid`, `auth::nip98::verify_at`, +`security::dotfile`, `interop` types, `ldp` parsers (PATCH dialects, +content negotiation, range parsing, server-managed triples), `error`, +`metrics::SecurityMetrics` (dotfile counter only — SSRF counters gate +on `tokio-runtime`). + +Modules behind `tokio-runtime` (unavailable to `core`): + +- `storage` (Storage trait + FsBackend + MemoryBackend) +- `notifications` (WebSocketChannel2023 + WebhookChannel2023) +- `provision` (Storage-driven pod bootstrap) +- `quota` (FsQuotaStore — async-trait QuotaPolicy) +- `oidc` (reqwest-backed JWKS fetcher + DPoP replay cache) +- `security::ssrf` (`tokio::net::lookup_host`-backed DNS guard) +- `wac::StorageAclResolver` (Storage-driven walk-up resolver — the + `AclResolver` trait stays in `core` so consumers can implement KV + backends against the same contract) +- `ldp::LdpContainerOps` (the trait impl that delegates to + `Storage::list`; the LDP parsers themselves remain in `core`) + +236 unit tests pass on default features. `cargo check -p solid-pod-rs +--no-default-features --features core` PASS — confirms the pure +surface compiles without tokio. + # v0.5.0-alpha.2 (Sprint 12 close — 2026-05-06) solid-pod-rs closes the JSS v0.0.60–v0.0.71 feature delta. The workspace diff --git a/crates/solid-pod-rs-activitypub/Cargo.toml b/crates/solid-pod-rs-activitypub/Cargo.toml index 6ed2e0f..cec70ef 100644 --- a/crates/solid-pod-rs-activitypub/Cargo.toml +++ b/crates/solid-pod-rs-activitypub/Cargo.toml @@ -14,7 +14,7 @@ categories = ["web-programming::http-server"] readme = "README.md" [dependencies] -solid-pod-rs = { version = "0.4.0-alpha.2", path = "../solid-pod-rs", default-features = false } +solid-pod-rs = { version = "0.4.0-alpha.4", path = "../solid-pod-rs", default-features = false, features = ["tokio-runtime"] } tokio = { version = "1", features = ["rt", "macros", "sync", "time", "fs"] } async-trait = "0.1" diff --git a/crates/solid-pod-rs-didkey/Cargo.toml b/crates/solid-pod-rs-didkey/Cargo.toml index f8a028b..f8448db 100644 --- a/crates/solid-pod-rs-didkey/Cargo.toml +++ b/crates/solid-pod-rs-didkey/Cargo.toml @@ -19,7 +19,7 @@ path = "src/lib.rs" [dependencies] # Reuse the core SelfSignedVerifier trait + error types. -solid-pod-rs = { version = "0.4.0-alpha.2", path = "../solid-pod-rs", default-features = false, features = ["security-primitives"] } +solid-pod-rs = { version = "0.4.0-alpha.4", path = "../solid-pod-rs", default-features = false, features = ["tokio-runtime", "security-primitives"] } async-trait = "0.1" diff --git a/crates/solid-pod-rs-didkey/tests/upstream_vectors/all_fixtures.rs b/crates/solid-pod-rs-didkey/tests/upstream_vectors/all_fixtures.rs new file mode 100644 index 0000000..1b89ce9 --- /dev/null +++ b/crates/solid-pod-rs-didkey/tests/upstream_vectors/all_fixtures.rs @@ -0,0 +1,76 @@ +// crates/solid-pod-rs-didkey/tests/upstream_vectors/all_fixtures.rs +//! L1 reference-vector tests — solid-pod-rs-didkey substrate. +//! +//! Per ADR-082 D5, the DID-key crate consumes fixtures relevant to identity +//! resolution: did-doc, multibase, bip340 (Schnorr is the underlying signature +//! algo for did:nostr per ADR-074 D1). + +use std::fs; +use std::path::PathBuf; + +fn fixture_root() -> PathBuf { + if let Ok(env_root) = std::env::var("VISIONCLAW_FIXTURE_ROOT") { + return PathBuf::from(env_root); + } + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests"); + p.push("fixtures"); + p +} + +fn try_load_fixture(name: &str) -> Option { + let mut path = fixture_root(); + path.push(name); + let bytes = fs::read(&path).ok()?; + serde_json::from_slice(&bytes).ok() +} + +fn assert_meta_block(fixture: &serde_json::Value, expected_spec_substring: &str) { + let meta = fixture + .get("_meta") + .expect("fixture must have _meta block"); + let spec = meta + .get("spec") + .and_then(|v| v.as_str()) + .expect("_meta.spec required"); + assert!( + spec.contains(expected_spec_substring), + "_meta.spec '{}' did not contain '{}'", + spec, + expected_spec_substring + ); +} + +macro_rules! fixture_test { + ($name:ident, $file:literal, $spec:literal, $min_vectors:expr) => { + #[test] + fn $name() { + let Some(f) = try_load_fixture($file) else { + eprintln!( + "fixture {} not found; skipping (run substrate-side scripts/sync-fixtures.sh first)", + $file + ); + return; + }; + assert_meta_block(&f, $spec); + if let Some(vectors) = f["vectors"].as_array() { + assert!( + vectors.len() >= $min_vectors, + "fixture {} must have >= {} vectors", + $file, + $min_vectors + ); + } + } + }; +} + +fixture_test!(did_doc_load_and_validate, "did-doc-conformance.json", "ADR-074", 7); +fixture_test!(bip340_load_and_validate, "bip340-schnorr.json", "BIP-340", 19); +fixture_test!(multibase_load_and_validate, "multibase.json", "Multibase", 27); + +#[test] +#[ignore = "wires into solid-pod-rs-didkey's DID Document emitter; ADR-074 D2 conformance check — Phase 2"] +fn did_doc_emitter_matches_canonical_shape() { + let _ = try_load_fixture("did-doc-conformance.json"); +} diff --git a/crates/solid-pod-rs-git/Cargo.toml b/crates/solid-pod-rs-git/Cargo.toml index b7ea06e..be5e60c 100644 --- a/crates/solid-pod-rs-git/Cargo.toml +++ b/crates/solid-pod-rs-git/Cargo.toml @@ -17,7 +17,7 @@ readme = "README.md" # Core library: provides the NIP-98 verifier (`auth::nip98::verify_at`) # and associated `PodError`. No explicit feature needed — the auth # module lives in the always-on surface. -solid-pod-rs = { version = "0.4.0-alpha.2", path = "../solid-pod-rs", default-features = false } +solid-pod-rs = { version = "0.4.0-alpha.4", path = "../solid-pod-rs", default-features = false, features = ["tokio-runtime"] } tokio = { version = "1", features = ["process", "rt", "rt-multi-thread", "io-util", "macros", "sync"] } bytes = "1" diff --git a/crates/solid-pod-rs-idp/Cargo.toml b/crates/solid-pod-rs-idp/Cargo.toml index b7fbd09..6e24556 100644 --- a/crates/solid-pod-rs-idp/Cargo.toml +++ b/crates/solid-pod-rs-idp/Cargo.toml @@ -23,7 +23,7 @@ path = "src/lib.rs" # jsonwebtoken + openidconnect already. `rate-limit` gives us the # reference LruRateLimiter. `security-primitives` is implied by # `rate-limit`. -solid-pod-rs = { version = "0.4.0-alpha.2", path = "../solid-pod-rs", default-features = false, features = ["oidc", "dpop-replay-cache", "rate-limit", "security-primitives", "nip98-schnorr"] } +solid-pod-rs = { version = "0.4.0-alpha.4", path = "../solid-pod-rs", default-features = false, features = ["oidc", "dpop-replay-cache", "rate-limit", "security-primitives", "nip98-schnorr"] } tokio = { version = "1", features = ["rt", "macros", "sync", "time"] } serde = { version = "1", features = ["derive"] } diff --git a/crates/solid-pod-rs-nostr/Cargo.toml b/crates/solid-pod-rs-nostr/Cargo.toml index a4f75cf..fa953e4 100644 --- a/crates/solid-pod-rs-nostr/Cargo.toml +++ b/crates/solid-pod-rs-nostr/Cargo.toml @@ -20,7 +20,7 @@ path = "src/lib.rs" [dependencies] # Core library: reuse NIP-98 Schnorr primitives (SigningKey/VerifyingKey # plumbing ship under `nip98-schnorr`) and SSRF policy for outbound fetch. -solid-pod-rs = { version = "0.4.0-alpha.2", path = "../solid-pod-rs", default-features = false, features = ["nip98-schnorr", "did-nostr", "security-primitives"] } +solid-pod-rs = { version = "0.4.0-alpha.4", path = "../solid-pod-rs", default-features = false, features = ["nip98-schnorr", "did-nostr", "security-primitives"] } tokio = { version = "1", features = ["sync", "rt", "macros", "time", "net", "io-util"] } tokio-tungstenite = "0.24" diff --git a/crates/solid-pod-rs-nostr/src/did.rs b/crates/solid-pod-rs-nostr/src/did.rs index 215152c..a53f797 100644 --- a/crates/solid-pod-rs-nostr/src/did.rs +++ b/crates/solid-pod-rs-nostr/src/did.rs @@ -80,22 +80,32 @@ pub struct ServiceEntry { /// Render a minimum-viable (Tier 1) DID document. /// /// Contains: -/// - `@context`: W3C DID Core v1. +/// - `@context`: W3C DID Core v1 + `secp256k1-2019` suite (required so the +/// `SchnorrSecp256k1VerificationKey2019` term resolves under JSON-LD +/// processing). /// - `id`: `did:nostr:`. /// - `alsoKnownAs`: empty array (WebID binding is Tier 3). -/// - `verificationMethod`: single `NostrSchnorrKey2024` entry keyed by -/// the x-only pubkey (`publicKeyMultibase` uses multibase `z` + +/// - `verificationMethod`: single `SchnorrSecp256k1VerificationKey2019` entry +/// keyed by the x-only pubkey (`publicKeyMultibase` uses multibase `z` + /// multicodec `0xe7` for secp256k1 schnorr per emerging convention, /// retaining `publicKeyHex` for JSS parity). +/// +/// Per ADR-074 D1: cross-system DID canonicalisation mandates the suite +/// identifier `SchnorrSecp256k1VerificationKey2019` — the only published W3C +/// secp256k1 Schnorr suite. The previous `NostrSchnorrKey2024` was a forum +/// invention that no W3C verifier resolves. pub fn render_did_document_tier1(pk: &NostrPubkey) -> Value { let did = did_nostr_uri(pk); json!({ - "@context": ["https://www.w3.org/ns/did/v1"], + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1" + ], "id": did, "alsoKnownAs": [], "verificationMethod": [{ "id": format!("{did}#nostr-schnorr"), - "type": "NostrSchnorrKey2024", + "type": "SchnorrSecp256k1VerificationKey2019", "controller": did, "publicKeyHex": pk.to_hex(), "publicKeyMultibase": format_multibase_schnorr(&pk.0), @@ -149,9 +159,11 @@ pub fn render_did_document_tier3( ], "id": did, "alsoKnownAs": also_known_as, + // Per ADR-074 D1: SchnorrSecp256k1VerificationKey2019 is the canonical + // suite identifier across all DreamLab DID emitters. See render_did_document_tier1. "verificationMethod": [{ "id": format!("{did}#nostr-schnorr"), - "type": "NostrSchnorrKey2024", + "type": "SchnorrSecp256k1VerificationKey2019", "controller": did, "publicKeyHex": pk.to_hex(), "publicKeyMultibase": format_multibase_schnorr(&pk.0), @@ -256,11 +268,17 @@ mod tests { let doc = render_did_document_tier1(&pk); assert_eq!(doc["id"], format!("did:nostr:{PK_HEX}")); assert_eq!(doc["@context"][0], "https://www.w3.org/ns/did/v1"); + assert_eq!( + doc["@context"][1], + "https://w3id.org/security/suites/secp256k1-2019/v1", + "Tier-1 must include the secp256k1-2019 suite context so \ + SchnorrSecp256k1VerificationKey2019 resolves under JSON-LD" + ); assert!(doc["alsoKnownAs"].is_array()); assert_eq!(doc["alsoKnownAs"].as_array().unwrap().len(), 0); let vm = &doc["verificationMethod"][0]; - assert_eq!(vm["type"], "NostrSchnorrKey2024"); + assert_eq!(vm["type"], "SchnorrSecp256k1VerificationKey2019"); assert_eq!(vm["publicKeyHex"], PK_HEX); assert!(vm["publicKeyMultibase"] .as_str() @@ -282,7 +300,7 @@ mod tests { assert_eq!(doc["alsoKnownAs"][0], webid); assert_eq!( doc["verificationMethod"][0]["type"], - "NostrSchnorrKey2024" + "SchnorrSecp256k1VerificationKey2019" ); assert_eq!(doc["service"][0]["type"], "SolidWebID"); assert_eq!(doc["service"][0]["serviceEndpoint"], webid); diff --git a/crates/solid-pod-rs-nostr/tests/upstream_vectors/all_fixtures.rs b/crates/solid-pod-rs-nostr/tests/upstream_vectors/all_fixtures.rs new file mode 100644 index 0000000..50f4be6 --- /dev/null +++ b/crates/solid-pod-rs-nostr/tests/upstream_vectors/all_fixtures.rs @@ -0,0 +1,76 @@ +// crates/solid-pod-rs-nostr/tests/upstream_vectors/all_fixtures.rs +//! L1 reference-vector tests — solid-pod-rs-nostr substrate. +//! +//! Per ADR-082 D5, solid-pod-rs consumes fixtures synced from VisionClaw's +//! docs/specs/fixtures/. Fixtures relevant to the nostr/auth crate per the +//! coverage matrix: nip01, nip19, nip98, bip340, rfc8785, did-doc, is-envelope, +//! multibase. + +use std::fs; +use std::path::PathBuf; + +fn fixture_root() -> PathBuf { + if let Ok(env_root) = std::env::var("VISIONCLAW_FIXTURE_ROOT") { + return PathBuf::from(env_root); + } + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests"); + p.push("fixtures"); + p +} + +fn try_load_fixture(name: &str) -> Option { + let mut path = fixture_root(); + path.push(name); + let bytes = fs::read(&path).ok()?; + serde_json::from_slice(&bytes).ok() +} + +fn assert_meta_block(fixture: &serde_json::Value, expected_spec_substring: &str) { + let meta = fixture + .get("_meta") + .expect("fixture must have _meta block"); + let spec = meta + .get("spec") + .and_then(|v| v.as_str()) + .expect("_meta.spec required"); + assert!( + spec.contains(expected_spec_substring), + "_meta.spec '{}' did not contain '{}'", + spec, + expected_spec_substring + ); +} + +macro_rules! fixture_test { + ($name:ident, $file:literal, $spec:literal, $min_vectors:expr) => { + #[test] + fn $name() { + let Some(f) = try_load_fixture($file) else { + eprintln!( + "fixture {} not found; skipping (run substrate-side scripts/sync-fixtures.sh first)", + $file + ); + return; + }; + assert_meta_block(&f, $spec); + if let Some(vectors) = f["vectors"].as_array() { + assert!( + vectors.len() >= $min_vectors, + "fixture {} must have >= {} vectors", + $file, + $min_vectors + ); + } + } + }; +} + +fixture_test!(nip01_events_load_and_validate, "nip01-events.json", "NIP-01", 11); +fixture_test!(nip19_bech32_load_and_validate, "nip19-bech32.json", "NIP-19", 12); +fixture_test!(nip98_tokens_load_and_validate, "nip98-tokens.json", "NIP-98", 6); +fixture_test!(bip340_load_and_validate, "bip340-schnorr.json", "BIP-340", 19); +fixture_test!(rfc8785_load_and_validate, "rfc8785-jcs.json", "RFC 8785", 6); +fixture_test!(multibase_load_and_validate, "multibase.json", "Multibase", 27); +fixture_test!(did_doc_load_and_validate, "did-doc-conformance.json", "ADR-074", 7); +fixture_test!(is_envelope_load_and_validate, "is-envelope-v1.json", "ADR-075", 11); diff --git a/crates/solid-pod-rs-server/Cargo.toml b/crates/solid-pod-rs-server/Cargo.toml index cb18aac..ef25687 100644 --- a/crates/solid-pod-rs-server/Cargo.toml +++ b/crates/solid-pod-rs-server/Cargo.toml @@ -23,11 +23,11 @@ path = "src/main.rs" [dependencies] # Library core — the binary's sole non-transport dependency. -solid-pod-rs = { version = "0.4.0-alpha.2", path = "../solid-pod-rs", features = ["fs-backend", "memory-backend", "config-loader", "legacy-notifications"] } +solid-pod-rs = { version = "0.4.0-alpha.4", path = "../solid-pod-rs", features = ["fs-backend", "memory-backend", "config-loader", "legacy-notifications"] } # IdP crate — Sprint-11 CLI ops (`account delete`, `invite create`) # dispatch into UserStore::delete and the new InviteStore trait. -solid-pod-rs-idp = { version = "0.4.0-alpha.2", path = "../solid-pod-rs-idp" } +solid-pod-rs-idp = { version = "0.4.0-alpha.4", path = "../solid-pod-rs-idp" } # Sprint-11 CLI: `invite create` surfaces invite records with # absolute expiry timestamps. Kept here rather than transitively diff --git a/crates/solid-pod-rs/CHANGELOG.md b/crates/solid-pod-rs/CHANGELOG.md index 99b88ce..5e664b0 100644 --- a/crates/solid-pod-rs/CHANGELOG.md +++ b/crates/solid-pod-rs/CHANGELOG.md @@ -4,6 +4,46 @@ All notable changes to this crate are recorded here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the crate adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.0-alpha.3] - 2026-05-07 (Phase 4 chain prep — `core` feature) + +### Added +- `core` feature flag: pure-logic surface for wasm32 / CF Workers + consumers (no tokio, no reqwest, no DNS resolver, no filesystem). +- `tokio-runtime` feature: gates the async-IO surface. Activated by + `default` so the existing 0.4.0-alpha.2 surface is preserved. +- `notifications` feature: gates the + WebSocketChannel2023 + WebhookChannel2023 stack. Activated by + `default`. + +### Changed +- `tokio`, `tokio-tungstenite`, `futures-util`, `notify`, `reqwest` + are now `optional = true` in `Cargo.toml`. They activate + transitively through `tokio-runtime` (and `notifications` for + reqwest). +- `default = ["std", "fs-backend", "memory-backend", "tokio-runtime", + "notifications"]` (was `["fs-backend", "memory-backend"]`). Net + surface unchanged for downstream consumers. +- `fs-backend`, `memory-backend`, `s3-backend`, `oidc`, + `dpop-replay-cache`, `webhook-signing`, `did-nostr`, `rate-limit`, + `quota`, `legacy-notifications`, `security-primitives` all imply + `tokio-runtime`. +- `From for PodError` is now gated on `fs-backend`. +- `wac::StorageAclResolver`, `ldp::LdpContainerOps`, + `security::ssrf`, all `notifications`/`provision`/`quota`/`storage` + modules are gated on `tokio-runtime`. The pure traits (`AclResolver`, + `RateLimiter`) and parsers stay in `core`. +- `metrics::SecurityMetrics`: SSRF-block helpers gated on + `tokio-runtime`. Dotfile counter remains available under `core`. +- `SecurityMetricsInner`'s SSRF counter fields stay in the struct + unconditionally so `Default`/`Clone` derivations are layout-stable + across feature configurations. + +### Verified +- `cargo check -p solid-pod-rs` (default features) — PASS +- `cargo check -p solid-pod-rs --no-default-features --features core` — PASS +- `cargo check --workspace` (all 7 sibling crates) — PASS +- `cargo test -p solid-pod-rs --lib` — 236 tests PASS + ## [0.5.0-alpha.1] - 2026-04-24 (Sprint 11 — top-10 roadmap closure) Parity vs JSS: **~100 % spec-normative** / **~97 % strict** on the diff --git a/crates/solid-pod-rs/Cargo.toml b/crates/solid-pod-rs/Cargo.toml index 7fab40a..2de4cb6 100644 --- a/crates/solid-pod-rs/Cargo.toml +++ b/crates/solid-pod-rs/Cargo.toml @@ -18,7 +18,10 @@ name = "solid_pod_rs" path = "src/lib.rs" [dependencies] -tokio = { version = "1", features = ["fs", "io-util", "sync", "macros", "rt", "time"] } +# `tokio` is optional so wasm32 / no-runtime consumers can opt out via +# `default-features = false, features = ["core"]`. Default builds still +# pull it in transitively through `tokio-runtime`. +tokio = { version = "1", features = ["fs", "io-util", "sync", "macros", "rt", "time"], optional = true } async-trait = "0.1" serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -31,17 +34,19 @@ hex = "0.4" url = "2" chrono = { version = "0.4", features = ["serde"] } base64 = "0.22" -notify = "6" +notify = { version = "6", optional = true } -# SPARQL-Update parsing for PATCH bodies +# SPARQL-Update parsing for PATCH bodies (pure-logic; always compiled). spargebra = "0.3" -# WebSocketChannel2023 (Solid Notifications 0.2) -tokio-tungstenite = "0.24" -futures-util = "0.3" +# WebSocketChannel2023 (Solid Notifications 0.2). Optional: only needed +# when a tokio runtime is configured for the notifications feature. +tokio-tungstenite = { version = "0.24", optional = true } +futures-util = { version = "0.3", optional = true } -# HTTP client for WebhookChannel2023 delivery -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +# HTTP client for WebhookChannel2023 delivery + did:nostr resolver. +# Optional so `core` consumers don't pull a TLS stack. +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"], optional = true } # Optional backends aws-sdk-s3 = { version = "1", optional = true } @@ -75,12 +80,33 @@ serde_yaml = { version = "0.9", optional = true } toml = { version = "0.8", optional = true } [features] -default = ["fs-backend", "memory-backend"] -fs-backend = [] -memory-backend = [] -s3-backend = ["dep:aws-sdk-s3"] -oidc = ["dep:openidconnect", "dep:jsonwebtoken"] +# `default` preserves the surface from 0.4.0-alpha.2: filesystem + +# in-memory backends, the tokio runtime, the notifications stack +# (WebSocketChannel2023 + WebhookChannel2023 — `reqwest`-backed), plus +# the JSS-parity umbrella. +default = ["std", "fs-backend", "memory-backend", "tokio-runtime", "notifications"] + +# --------------------------------------------------------------------------- +# 0.4.0-alpha.3: feature flags for wasm32 / CF-Workers consumers. +# Per ADR-076/078 absorption, `nostr-bbs-pod-worker` consumes the pure +# logic surfaces (wac, webid, dotfile, nip98 verifier, ldp parsers, +# interop types, security path/URL primitives) without dragging in tokio +# or reqwest. `core` is the entry point for those consumers. +# --------------------------------------------------------------------------- +core = ["std"] +std = [] + +# Activate the tokio runtime + the websocket/futures stack. Pulled in +# transitively by every async-IO feature below. Pure-logic consumers +# leave this off via `default-features = false, features = ["core"]`. +tokio-runtime = ["dep:tokio", "dep:tokio-tungstenite", "dep:futures-util"] + +fs-backend = ["tokio-runtime", "dep:notify"] +memory-backend = ["tokio-runtime"] +s3-backend = ["dep:aws-sdk-s3", "tokio-runtime"] +oidc = ["dep:openidconnect", "dep:jsonwebtoken", "tokio-runtime", "dep:reqwest"] nip98-schnorr = ["dep:k256"] +notifications = ["tokio-runtime", "dep:reqwest"] # --------------------------------------------------------------------------- # Sprint 4 / JSS parity umbrella (ADR-056 / PRD §F). @@ -91,11 +117,12 @@ jss-v04 = [] # F1/F2: SSRF guard + dotfile allowlist (lightweight structs always # compiled; integration gated at call sites). -security-primitives = ["jss-v04"] +security-primitives = ["jss-v04", "tokio-runtime"] # F3: legacy `solid-0.1` WebSocket notifications adapter for SolidOS -# data-browser compat. -legacy-notifications = ["jss-v04"] +# data-browser compat. Implies the tokio runtime + the new +# `notifications` feature it sits on top of. +legacy-notifications = ["jss-v04", "tokio-runtime", "notifications"] # F4: WAC `acl:origin` enforcement per WAC §4.3. When off, the # evaluator ignores the Origin header for backward compat. @@ -103,7 +130,7 @@ acl-origin = ["jss-v04"] # F5: DPoP jti replay cache (Solid-OIDC §5.2 / RFC 9449). # Enables `oidc::replay` and pulls in `lru`. -dpop-replay-cache = ["oidc", "jss-v04", "dep:lru"] +dpop-replay-cache = ["oidc", "jss-v04", "dep:lru", "tokio-runtime"] # F6: JSS-compatible layered config loader. The `config` module is # always compiled (lightweight struct definitions); this feature flag @@ -116,20 +143,20 @@ config-loader = ["jss-v04", "dep:serde_yaml", "dep:toml"] # Sprint 6 C: RFC 9421 HTTP Message Signatures on outgoing webhook # deliveries, Retry-After handling, circuit breaker. Consumers opt in. -webhook-signing = ["jss-v04", "dep:ed25519-dalek", "dep:httpdate", "dep:rand"] +webhook-signing = ["jss-v04", "dep:ed25519-dalek", "dep:httpdate", "dep:rand", "tokio-runtime", "dep:reqwest"] # Sprint 6 D: did:nostr resolver (DID-Doc ↔ WebID bidirectional # alsoKnownAs/owl:sameAs verification). Surfaces under # `solid_pod_rs::interop::did_nostr`. No new crate dependencies: uses # the reqwest + serde + url stack already pulled in by the library, # and std::sync::RwLock for the cache. -did-nostr = ["jss-v04", "security-primitives"] +did-nostr = ["jss-v04", "security-primitives", "tokio-runtime", "dep:reqwest"] # Sprint 7 A: rate-limit primitive. Library exposes a `RateLimiter` # trait plus an LRU-backed reference implementation. CORS support is # gated under `jss-v04` (no new deps). Enabling `rate-limit` pulls in # `lru` + `parking_lot`. -rate-limit = ["jss-v04", "dep:lru", "dep:parking_lot"] +rate-limit = ["jss-v04", "dep:lru", "dep:parking_lot", "tokio-runtime"] # Sprint 7 B: pod-level quota policy. Ships `FsQuotaStore` backed by # `.quota.json` sidecars; integrates with the config loader via @@ -137,7 +164,7 @@ rate-limit = ["jss-v04", "dep:lru", "dep:parking_lot"] # always-on `jss-v04` umbrella; this flag only gates the FS adapter # (which shares serde_json + tokio::fs already pulled in by the crate, # no new deps). -quota = ["jss-v04", "config-loader"] +quota = ["jss-v04", "config-loader", "tokio-runtime"] [dev-dependencies] tokio = { version = "1", features = ["full"] } @@ -148,7 +175,12 @@ criterion = { version = "0.5", features = ["html_reports"] } # Sprint 7 D: integration tests drive the server binary's actix `App` # through `test::init_service`. Dev-dependency only — never ships in # the library's public surface. -solid-pod-rs-server = { version = "0.4.0-alpha.2", path = "../solid-pod-rs-server" } +# Path-only dev-dependency: dropping the version pin avoids a publish +# cycle (solid-pod-rs-server depends on solid-pod-rs at the same +# version, so cargo can't resolve the dev-dep against crates.io until +# the parent itself publishes). cargo strips dev-deps from the +# published metadata, so this is the supported pattern. +solid-pod-rs-server = { path = "../solid-pod-rs-server" } axum = "0.7" tracing-subscriber = { version = "0.3", features = ["env-filter"] } rand = { version = "0.8", features = ["small_rng"] } diff --git a/crates/solid-pod-rs/src/error.rs b/crates/solid-pod-rs/src/error.rs index c8e57a8..37b3671 100644 --- a/crates/solid-pod-rs/src/error.rs +++ b/crates/solid-pod-rs/src/error.rs @@ -94,6 +94,10 @@ pub enum PodError { QuotaExceeded(#[from] crate::quota::QuotaExceeded), } +// `notify` is an optional dep activated by `fs-backend`. Wasm32 / `core` +// consumers compile this crate without it; the `Watch` variant remains +// constructible from a string when needed by other backends. +#[cfg(feature = "fs-backend")] impl From for PodError { fn from(e: notify::Error) -> Self { PodError::Watch(e.to_string()) diff --git a/crates/solid-pod-rs/src/interop.rs b/crates/solid-pod-rs/src/interop.rs index 2b6ed38..c0d7eeb 100644 --- a/crates/solid-pod-rs/src/interop.rs +++ b/crates/solid-pod-rs/src/interop.rs @@ -316,16 +316,25 @@ pub mod did_nostr { /// Build a minimal DID Doc for publication at the well-known URL. /// Tier-1 schema (matches JSS): `id`, `alsoKnownAs`, and a single - /// `verificationMethod` entry of type `NostrSchnorrKey2024` derived - /// from the x-only pubkey. + /// `verificationMethod` entry of type + /// `SchnorrSecp256k1VerificationKey2019` derived from the x-only pubkey. + /// + /// Per ADR-074 D1 (cross-system DID:Nostr canonicalisation): all DreamLab + /// emitters MUST use `SchnorrSecp256k1VerificationKey2019` (the only + /// published W3C secp256k1 Schnorr suite). The legacy `NostrSchnorrKey2024` + /// term was a forum invention that no W3C verifier can resolve. Tier-1 + /// includes the secp256k1-2019 suite context so the term resolves. pub fn did_nostr_document(pubkey: &str, also_known_as: &[String]) -> serde_json::Value { serde_json::json!({ - "@context": ["https://www.w3.org/ns/did/v1"], + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1" + ], "id": format!("did:nostr:{}", pubkey), "alsoKnownAs": also_known_as, "verificationMethod": [{ "id": format!("did:nostr:{}#nostr-schnorr", pubkey), - "type": "NostrSchnorrKey2024", + "type": "SchnorrSecp256k1VerificationKey2019", "controller": format!("did:nostr:{}", pubkey), "publicKeyHex": pubkey, }] diff --git a/crates/solid-pod-rs/src/ldp.rs b/crates/solid-pod-rs/src/ldp.rs index 568af80..affbf0c 100644 --- a/crates/solid-pod-rs/src/ldp.rs +++ b/crates/solid-pod-rs/src/ldp.rs @@ -20,10 +20,15 @@ use std::collections::BTreeSet; use std::fmt::Write as _; +#[cfg(feature = "tokio-runtime")] use async_trait::async_trait; use serde::Serialize; use crate::error::PodError; +// `Storage` lives behind `tokio-runtime` — the LDP parsers themselves +// are pure and compile under `core`, only the storage-driven container +// representation helper at the bottom of this file needs the trait. +#[cfg(feature = "tokio-runtime")] use crate::storage::Storage; /// Well-known IRI constants used in LDP, WAC, and server-managed triples. @@ -1978,6 +1983,7 @@ pub fn apply_patch_to_absent( // LdpContainerOps trait (backwards compatible) // --------------------------------------------------------------------------- +#[cfg(feature = "tokio-runtime")] #[async_trait] pub trait LdpContainerOps: Storage { async fn container_representation( @@ -1989,6 +1995,7 @@ pub trait LdpContainerOps: Storage { } } +#[cfg(feature = "tokio-runtime")] impl LdpContainerOps for T {} // --------------------------------------------------------------------------- diff --git a/crates/solid-pod-rs/src/lib.rs b/crates/solid-pod-rs/src/lib.rs index 0bf8de8..dae973f 100644 --- a/crates/solid-pod-rs/src/lib.rs +++ b/crates/solid-pod-rs/src/lib.rs @@ -13,6 +13,10 @@ //! //! | Flag | Default | Purpose | //! |-------------------------|:-------:|-----------------------------------------------| +//! | `core` | off | Pure-logic surfaces only — wasm32 / CF Workers. | +//! | `std` | on | std lib (always; reserved for future no_std). | +//! | `tokio-runtime` | on | Tokio + tokio-tungstenite + futures-util. | +//! | `notifications` | on | WebSocketChannel2023 + WebhookChannel2023. | //! | `fs-backend` | on | POSIX filesystem storage. | //! | `memory-backend` | on | In-process `HashMap` storage (tests/demos). | //! | `s3-backend` | off | AWS S3 / S3-compatible object stores. | @@ -28,6 +32,11 @@ //! | `rate-limit` | off | Sliding-window LRU rate limiter. | //! | `quota` | off | Per-pod `.quota.json` sidecar (atomic writes). | //! +//! `core` consumers wire the crate via `default-features = false, +//! features = ["core"]` and get only the pure-logic surfaces (no +//! tokio, no reqwest, no DNS resolver, no filesystem). See +//! `RELEASE_NOTES.md` v0.4.0-alpha.3 for the absorbed surfaces map. +//! //! ## Module overview //! //! | Module | Responsibility | @@ -81,6 +90,14 @@ #![deny(unsafe_code)] #![warn(rust_2018_idioms)] +// --------------------------------------------------------------------------- +// Always-compiled (`core`) modules. +// +// Pure-logic surfaces: parsers, validators, type definitions. None of +// these reach for tokio, reqwest, or notify directly. Wasm32 / CF +// Workers consumers wire these via +// `default-features = false, features = ["core"]`. +// --------------------------------------------------------------------------- pub mod auth; pub mod config; pub mod error; @@ -88,13 +105,26 @@ pub mod interop; pub mod ldp; pub mod metrics; pub mod multitenant; +pub mod security; +pub mod wac; +pub mod webid; + +// --------------------------------------------------------------------------- +// `tokio-runtime`-gated modules. +// +// These pull tokio (mpsc, fs, broadcast) or reqwest (HTTP client) and +// are unavailable to `core` consumers. They are wired in by the +// `default` feature set so the existing surface from 0.4.0-alpha.2 is +// preserved bit-for-bit on native builds. +// --------------------------------------------------------------------------- +#[cfg(feature = "notifications")] pub mod notifications; +#[cfg(feature = "tokio-runtime")] pub mod provision; +#[cfg(feature = "tokio-runtime")] pub mod quota; -pub mod security; +#[cfg(feature = "tokio-runtime")] pub mod storage; -pub mod wac; -pub mod webid; #[cfg(feature = "oidc")] pub mod oidc; @@ -106,7 +136,9 @@ pub mod oidc; #[cfg(feature = "legacy-notifications")] pub mod handlers; -// Re-exports for ergonomic consumers. +// --------------------------------------------------------------------------- +// `core` re-exports — always available. +// --------------------------------------------------------------------------- pub use auth::nip98::Nip98Verifier; pub use auth::self_signed::{ CidVerifier, ProofEnvelope, SelfSignedError, SelfSignedVerifier, VerifiedSubject, @@ -114,10 +146,8 @@ pub use auth::self_signed::{ pub use error::PodError; pub use metrics::SecurityMetrics; pub use security::{ - is_path_allowed, is_safe_url, resolve_and_check, DotfileAllowlist, DotfileError, - DotfilePathError, IpClass, SsrfError, SsrfPolicy, + is_path_allowed, DotfileAllowlist, DotfileError, DotfilePathError, }; -pub use storage::{ResourceMeta, Storage, StorageEvent}; pub use wac::{ check_origin, evaluate_access, evaluate_access_with_groups, extract_origin_patterns, method_to_mode, mode_name, parse_turtle_acl, serialize_turtle_acl, wac_allow_header, @@ -138,15 +168,24 @@ pub use interop::{ Nip05Document, SolidWellKnown, WebFingerJrd, WebFingerLink, }; pub use multitenant::{PathResolver, PodResolver, ResolvedPath, SubdomainResolver}; +pub use webid::{ + extract_oidc_issuer, generate_webid_html, generate_webid_html_with_issuer, + validate_webid_html, +}; + +// --------------------------------------------------------------------------- +// `tokio-runtime`-gated re-exports. +// --------------------------------------------------------------------------- +#[cfg(feature = "tokio-runtime")] +pub use security::{is_safe_url, resolve_and_check, IpClass, SsrfError, SsrfPolicy}; +#[cfg(feature = "tokio-runtime")] +pub use storage::{ResourceMeta, Storage, StorageEvent}; +#[cfg(feature = "tokio-runtime")] pub use provision::{ check_admin_override, provision_pod, AdminOverride, ProvisionOutcome, ProvisionPlan, QuotaTracker, }; +#[cfg(feature = "tokio-runtime")] pub use quota::{QuotaExceeded, QuotaPolicy, QuotaUsage}; - #[cfg(feature = "quota")] pub use quota::FsQuotaStore; -pub use webid::{ - extract_oidc_issuer, generate_webid_html, generate_webid_html_with_issuer, - validate_webid_html, -}; diff --git a/crates/solid-pod-rs/src/metrics.rs b/crates/solid-pod-rs/src/metrics.rs index 044e49c..33326dc 100644 --- a/crates/solid-pod-rs/src/metrics.rs +++ b/crates/solid-pod-rs/src/metrics.rs @@ -14,6 +14,11 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +// `IpClass` lives in the SSRF guard which only compiles under +// `tokio-runtime` (DNS resolution requires the tokio reactor). The +// SSRF-block counter helpers below are gated to match. Dotfile +// counters remain available under `core`. +#[cfg(feature = "tokio-runtime")] use crate::security::ssrf::IpClass; /// Atomic counter bundle, cheap to clone. @@ -24,11 +29,20 @@ pub struct SecurityMetrics { #[derive(Debug, Default)] struct SecurityMetricsInner { - // SSRF block counters, labelled by IpClass. + // SSRF block counters, labelled by IpClass. Only read by the + // SSRF-block helpers, which are gated on `tokio-runtime`. The + // fields stay in the struct unconditionally so the layout — and + // therefore `Default`/`Clone` derivations — is identical across + // feature configurations. + #[cfg_attr(not(feature = "tokio-runtime"), allow(dead_code))] ssrf_blocked_private: AtomicU64, + #[cfg_attr(not(feature = "tokio-runtime"), allow(dead_code))] ssrf_blocked_loopback: AtomicU64, + #[cfg_attr(not(feature = "tokio-runtime"), allow(dead_code))] ssrf_blocked_link_local: AtomicU64, + #[cfg_attr(not(feature = "tokio-runtime"), allow(dead_code))] ssrf_blocked_multicast: AtomicU64, + #[cfg_attr(not(feature = "tokio-runtime"), allow(dead_code))] ssrf_blocked_reserved: AtomicU64, // `Public` is never blocked under the default classifier, but // callers that carry a denylist hit count it under `Reserved` @@ -45,6 +59,7 @@ impl SecurityMetrics { } /// Increment the SSRF block counter for `class`. + #[cfg(feature = "tokio-runtime")] pub fn record_ssrf_block(&self, class: IpClass) { let counter = match class { IpClass::Private => &self.inner.ssrf_blocked_private, @@ -57,6 +72,7 @@ impl SecurityMetrics { } /// Read the SSRF block counter for `class`. + #[cfg(feature = "tokio-runtime")] pub fn ssrf_blocked_total(&self, class: IpClass) -> u64 { let counter = match class { IpClass::Private => &self.inner.ssrf_blocked_private, diff --git a/crates/solid-pod-rs/src/security/mod.rs b/crates/solid-pod-rs/src/security/mod.rs index 26fb8b1..ce18803 100644 --- a/crates/solid-pod-rs/src/security/mod.rs +++ b/crates/solid-pod-rs/src/security/mod.rs @@ -47,14 +47,24 @@ //! all private/loopback/link-local space; dotfile allowlist permits //! only `.acl` and `.meta`. +// `dotfile` and `cors` are pure-logic primitives — always compiled. pub mod cors; pub mod dotfile; + +// `rate_limit` is async-trait based but uses no tokio internals; the +// trait + decision types compile under `core`. The reference +// `LruRateLimiter` impl is gated separately. pub mod rate_limit; + +// `ssrf` reaches for `tokio::net::lookup_host` for DNS resolution; +// keep it on the tokio-runtime path only. +#[cfg(feature = "tokio-runtime")] pub mod ssrf; pub use cors::{AllowedOrigins, CorsPolicy}; pub use dotfile::{is_path_allowed, DotfileAllowlist, DotfileError, DotfilePathError}; pub use rate_limit::{RateLimitDecision, RateLimitKey, RateLimitSubject, RateLimiter}; +#[cfg(feature = "tokio-runtime")] pub use ssrf::{is_safe_url, resolve_and_check, IpClass, SsrfError, SsrfPolicy}; #[cfg(feature = "rate-limit")] diff --git a/crates/solid-pod-rs/src/wac/mod.rs b/crates/solid-pod-rs/src/wac/mod.rs index fca8a74..6ae6f33 100644 --- a/crates/solid-pod-rs/src/wac/mod.rs +++ b/crates/solid-pod-rs/src/wac/mod.rs @@ -137,7 +137,9 @@ pub use evaluator::{ pub use issuer::{IssuerConditionBody, IssuerConditionEvaluator}; pub use origin::{check_origin, extract_origin_patterns, Origin, OriginDecision, OriginPattern}; pub use parser::{parse_turtle_acl, parse_turtle_acl_with_limit}; -pub use resolver::{AclResolver, StorageAclResolver}; +pub use resolver::AclResolver; +#[cfg(feature = "tokio-runtime")] +pub use resolver::StorageAclResolver; pub use serializer::serialize_turtle_acl; /// Access modes defined by WAC. diff --git a/crates/solid-pod-rs/src/wac/resolver.rs b/crates/solid-pod-rs/src/wac/resolver.rs index 29fd023..c51a42a 100644 --- a/crates/solid-pod-rs/src/wac/resolver.rs +++ b/crates/solid-pod-rs/src/wac/resolver.rs @@ -7,9 +7,16 @@ use async_trait::async_trait; use crate::error::PodError; +// `Storage` lives behind `tokio-runtime`; the storage-backed resolver +// impl below is gated to match. The `AclResolver` trait is pure and +// remains available under `core` so wasm32 consumers can implement +// their own KV-backed resolver against the same contract. +#[cfg(feature = "tokio-runtime")] use crate::storage::Storage; use crate::wac::document::AclDocument; +#[cfg(feature = "tokio-runtime")] use crate::wac::parse_jsonld_acl; +#[cfg(feature = "tokio-runtime")] use crate::wac::parser::parse_turtle_acl; /// Resolves the effective ACL document for a resource using the WAC walk-up-the-tree algorithm. @@ -28,10 +35,12 @@ pub trait AclResolver: Send + Sync { } /// `AclResolver` backed by a [`Storage`] implementation. +#[cfg(feature = "tokio-runtime")] pub struct StorageAclResolver { storage: std::sync::Arc, } +#[cfg(feature = "tokio-runtime")] impl StorageAclResolver { /// Wrap a shared storage handle in a resolver. pub fn new(storage: std::sync::Arc) -> Self { @@ -39,6 +48,7 @@ impl StorageAclResolver { } } +#[cfg(feature = "tokio-runtime")] #[async_trait] impl AclResolver for StorageAclResolver { /// Walk from `resource_path` toward `/`, returning the first valid `.acl` sidecar found. diff --git a/crates/solid-pod-rs/tests/did_nostr_resolver.rs b/crates/solid-pod-rs/tests/did_nostr_resolver.rs index d33085d..700448a 100644 --- a/crates/solid-pod-rs/tests/did_nostr_resolver.rs +++ b/crates/solid-pod-rs/tests/did_nostr_resolver.rs @@ -8,7 +8,7 @@ //! //! 1. `did_nostr_well_known_url_format` — pure URL composition. //! 2. `did_nostr_document_emits_minimal_schema` — doc shape + -//! `NostrSchnorrKey2024` verification method entry. +//! `SchnorrSecp256k1VerificationKey2019` verification method entry (per ADR-074 D1). //! 3. `did_nostr_resolver_returns_webid_when_backlink_present` — happy //! path: DID Doc with one `alsoKnownAs`, WebID profile carries //! `owl:sameAs` back-link → `Some(web_id)`. @@ -64,10 +64,18 @@ fn did_nostr_document_emits_minimal_schema() { assert_eq!(doc["alsoKnownAs"][0], "https://alice.example/me#i"); let vm = &doc["verificationMethod"][0]; - assert_eq!(vm["type"], "NostrSchnorrKey2024"); + // ADR-074 D1: canonical W3C Schnorr suite identifier. + assert_eq!(vm["type"], "SchnorrSecp256k1VerificationKey2019"); assert_eq!(vm["controller"], format!("did:nostr:{TEST_PUBKEY}")); assert_eq!(vm["publicKeyHex"], TEST_PUBKEY); assert_eq!(vm["id"], format!("did:nostr:{TEST_PUBKEY}#nostr-schnorr")); + + // Tier-1 must also include the secp256k1-2019 suite context. + let contexts = doc["@context"].as_array().expect("@context array"); + assert!( + contexts.iter().any(|c| c == "https://w3id.org/security/suites/secp256k1-2019/v1"), + "DID Doc must include the secp256k1-2019 suite context (ADR-074 D1)", + ); } // --- test-3 --------------------------------------------------------------