Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
37 changes: 37 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/solid-pod-rs-activitypub/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion crates/solid-pod-rs-didkey/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
76 changes: 76 additions & 0 deletions crates/solid-pod-rs-didkey/tests/upstream_vectors/all_fixtures.rs
Original file line number Diff line number Diff line change
@@ -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<serde_json::Value> {
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");
}
2 changes: 1 addition & 1 deletion crates/solid-pod-rs-git/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion crates/solid-pod-rs-idp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/solid-pod-rs-nostr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
34 changes: 26 additions & 8 deletions crates/solid-pod-rs-nostr/src/did.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:<hex>`.
/// - `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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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()
Expand All @@ -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);
Expand Down
76 changes: 76 additions & 0 deletions crates/solid-pod-rs-nostr/tests/upstream_vectors/all_fixtures.rs
Original file line number Diff line number Diff line change
@@ -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<serde_json::Value> {
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);
4 changes: 2 additions & 2 deletions crates/solid-pod-rs-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions crates/solid-pod-rs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<notify::Error> 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
Expand Down
Loading
Loading