solid-pod-rs 0.4.0-alpha.4

Rust-native Solid Pod server library — LDP, WAC, WebID, Solid-OIDC, Solid Notifications, NIP-98. Framework-agnostic.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
# Changelog

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
121-row tracker. 835 workspace tests pass (↑ from 733 at Sprint 10);
clippy `-D warnings --all-targets` clean; 0 security findings on
new crypto surface.

### Added

- **`auth::self_signed::SelfSignedVerifier` trait + `CidVerifier`**
  — row 152. Fan-out dispatcher accepting any of Nip98 / did:key /
  future subject types, wired into `wac::issuer::IssuerCondition`
  dispatch. Net-new — we ship LWS 1.0 SSI-CID ahead of JSS.
- **`notifications::legacy::LegacyWebSocketSession`** — row 91.
  Full solid-0.1 protocol (`sub`/`ack`/`err`/`pub`/`unsub`) with
  per-subscription WAC Read re-check, 100 subs/conn cap, 2 KiB URL
  cap, and ancestor-container fanout on publish. Feature
  `legacy-notifications`.
- **`config::ConfigLoader::from_file`** — rows 120–124. JSON / YAML /
  TOML auto-detected by extension; `with_cli_overlay` at the top of
  the precedence stack; `parse_size` extended with IEC binary
  suffixes (`KiB`/`MiB`/`GiB`/`TiB`); 31 `JSS_*` env vars wired.
  Feature `config-loader`.
- **`multitenant::is_file_like_label`** — rows 125, 162. 15+
  web-asset extensions, case-insensitive, short-circuits the
  subdomain resolver.

### Changed

- PARITY-CHECKLIST.md: 14 rows promoted to `present`; sibling-crate
  reality table updated with `solid-pod-rs-didkey` (858 LOC).
- Row 60 JSS source: `src/auth/identity-normalizer.js` → the actual
  JSS file `src/auth/nostr.js`.

### Fixed

- README code example: `FsStorage``FsBackend` (matches the
  exported type).

### Security

- New crypto code (did:key + self-signed JWT) carries explicit
  algorithm allowlists and `alg=none` hard-rejects. AQE
  `security_scan_comprehensive` reports 0 vulnerabilities for both
  `solid-pod-rs-didkey` and `solid-pod-rs-idp`.

---

## [0.4.0-alpha.1] - 2026-04-24 (Sprint 8 + Sprint 9 consolidation)

Snapshot at commit `2275146`. Parity vs JSS: **85 % spec-normative**
(91/109 rows) / **66 % strict** on the full 121-row tracker (80/121).
567 tests pass across the workspace with the full Sprint 9 feature
matrix.

### Added — Sprint 9 (WAC 2.0 + pod bootstrap + conditions)

- **`wac::validate_for_write(&AclDocument, &ConditionRegistry)`**  returns `UnsupportedCondition` so the binder can emit 422
  `application/problem+json` per WAC 2.0 §5. Bound into the server's
  PUT / PATCH / POST paths for ACL documents.
- **`acl:origin` enforcement (net-new vs JSS).** Feature `acl-origin`
  gates `wac::origin::check_origin_allowed`; a request missing or
  mismatching the `Origin` header against the ACL's origin allowlist
  is denied. This is a strengthening beyond JSS, which does not
  enforce `acl:origin`. Parity row 62a.
- **`oidc::replay::DpopReplayCache`** — per-process LRU of seen `jti`
  claims, clock-aware, bounded, safe under concurrent access.
  Benchmarks in `benches/dpop_replay_bench.rs`. Feature
  `dpop-replay-cache`.
- **`provision::provision_pod`** — idempotent pod bootstrap. Seeds
  base containers (`/profile/`, `/settings/`, `/inbox/`, `/public/`),
  writes a WebID profile (with `solid:oidcIssuer` + CID storage link),
  mounts type indexes (`publicTypeIndex` + `privateTypeIndex` under
  `/settings/`), and installs a public-read root ACL.
- **WAC 2.0 condition framework** (Sprint 6 land-date, Sprint 9
  documentation close): `acl:condition`, `acl:ClientCondition`,
  `acl:IssuerCondition`, `ConditionRegistry`, `RequestContext`,
  `EmptyDispatcher`, `ClientConditionEvaluator`,
  `IssuerConditionEvaluator`. Unknown condition types parse but
  evaluate to `NotApplicable` (fail-closed). Parity rows 53–56.

### Added — Sprint 8 (JSS 0.0.144 – 0.0.154 tracking + LWS 1.0)

- **NIP-98 BIP-340 Schnorr signature verification** under
  `nip98-schnorr`, exercised in the LWS 1.0 Auth Suite rows.
- **CID-bound storage links in WebID**  `webid::generate_webid_html_with_cid` emits a Content-Identifier
  reference alongside the storage endpoint for IPFS/IPLD-backed pods.
- **Cache-Control on RDF resources** — containers revalidate,
  resources expire. `ldp::cache_control_for` helper.
- **`.acl` + `.meta` content negotiation** — both discovery resources
  honour `Accept:` and serialise to Turtle, JSON-LD, or N-Triples.
- **did:nostr ↔ WebID `alsoKnownAs` round-trip** (Sprint 6 land,
  Sprint 8 close) — `interop::did_nostr::DidNostrResolver` fetches
  the DID Doc, walks `alsoKnownAs`, fetches each candidate WebID
  profile, and verifies a back-link via `owl:sameAs` or
  `schema:sameAs`.

### Changed

- `PARITY-CHECKLIST.md` recalibrated to 121 rows with explicit
  classification per row (present / partial / missing / net-new /
  deferred / wontfix). Spec-normative denominator excludes wontfix +
  deferred; strict denominator counts every row.
- `oidc::verify_dpop_proof_core` signature extended with an
  `AlgorithmAllowlist` parameter. Existing call sites compile
  unchanged via `AlgorithmAllowlist::default()`.

### Fixed

- **Atomic quota writes (P0, Sprint 8).** `FsQuotaStore::record` and
  `FsQuotaStore::reconcile` now serialise to a temp file under the
  pod root and `fs::rename` into place. Concurrent writers can no
  longer observe a half-written `.quota.json`; the race window closed
  on the Sprint 7 land is confirmed eliminated in
  `tests/quota_fs_atomic.rs`.

### Security

- **DPoP proof signature is now actually verified (P0, CVE-class,
  Sprint 9).** `oidc::verify_dpop_proof_core` previously decoded the
  proof body without verifying the JWT signature against
  `header.jwk` — any forged proof authenticated. The function now
  dispatches on `header.alg` via an allowlist
  (`ES256`/`ES384`, `RS256`/`RS384`/`RS512`, `PS256`/`PS384`/`PS512`,
  `EdDSA`), builds a `DecodingKey::from_jwk`, and rejects `alg=none`
  and the HMAC family unconditionally. RFC 9449 §4.3 conformance
  restored. Parity row 62b. **Deployments that issued DPoP-bound
  access tokens before the upgrade should rotate them.**
- **RFC 9449 §4.3 `ath` binding**`ath` claim presence + hash match
  against the SHA-256 of the bearer token, constant-time compared.
- **SSRF guard on JWKS + OIDC discovery** — every outbound in
  `oidc::jwks::fetch_jwks` runs the SSRF policy on the issuer host,
  pins the TCP connect to the approved IP via `.resolve()` (defeats
  DNS rebinding between SSRF check and connect), re-runs the policy
  on the discovered `jwks_uri`. 900 s cache TTL mirrors JSS.
- **Dotfile allowlist extended** to `.acl`, `.meta`, `.well-known`,
  `.quota.json`. All other dotfiles are 404 regardless of storage
  presence.
- **SSRF primitives** (`security::ssrf`) — RFC 1918, loopback
  (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`),
  and cloud-metadata (`169.254.169.254`) addresses are rejected.
- **WAC parser bounds** — 1 MiB Turtle ACL cap (configurable via
  `JSS_MAX_ACL_BYTES`); 32-level JSON-LD depth cap. Defends against
  O(n²) splitter blowup and stack-overflow bombs.

### Deprecated

- None. 0.4.0-alpha.1 is additive over the pre-Sprint-5 surface;
  the Sprint 5 DPoP signature fix was a behavioural change at an
  additive API.

## Unreleased — 2026-04-20 (Sprint 7 — operator surface + server route table)

### Added — operator-surface primitives

- `security/rate_limit.rs` (`RateLimiter` trait + sliding-window
  `LruRateLimiter` reference impl). Per-route + per-subject buckets
  (`Ip`, `WebId`, `Custom`); LRU cap (default 4 096 keys) bounds
  memory under churn; `RateLimitDecision::Deny` carries
  `retry_after_secs`. Feature `rate-limit` (gates the LRU impl;
  trait is always compiled).
- `security/cors.rs` (`CorsPolicy` + `CorsPolicy::from_env`). Reads
  `CORS_ALLOWED_ORIGINS`, `CORS_ALLOW_CREDENTIALS`, `CORS_MAX_AGE`.
  Default `expose_headers` includes `WAC-Allow`, `Link`, `ETag`,
  `Accept-Patch`, `Accept-Post`, `Updates-Via`. Wildcard +
  credentials degrades to echoing the concrete origin per Fetch
  spec; `Vary: Origin` always set so caches don't leak.
- `quota/mod.rs` (`QuotaPolicy` trait + `FsQuotaStore`). Per-pod
  `.quota.json` sidecar; `record` + `reconcile` mirror JSS
  `quota.js`. `PodError::QuotaExceeded` propagates the breach.
  Feature `quota` (gates the FS impl + the error variant).
- `multitenant.rs` (`PodResolver` trait + `PathResolver` default +
  `SubdomainResolver`). Subdomain mode maps `alice.example.org/foo`
  `(pod=Some("alice"), storage_path="/foo")`. Double-pass `..`
  scrub mirrors JSS `urlToPathWithPod`. Unknown subdomain
  gracefully degrades to path-based (no 400 — JSS parity).
- `config::sources::parse_size` — accepts `50MB`, `1.5GB`, raw
  integer bytes; SI multiplier (1000-based) per the test
  fixtures. Wired so `JSS_DEFAULT_QUOTA` env var decodes via
  `parse_size`.
- `interop::nodeinfo_discovery` + `interop::nodeinfo_2_1` — JSON
  helpers for `/.well-known/nodeinfo` + `/.well-known/nodeinfo/2.1`
  per nodeinfo.diaspora.software §3 + §6. `protocols` advertises
  both `solid` and `activitypub`.

### Added — `solid-pod-rs-server` route table

- Library extracted: `crates/solid-pod-rs-server/src/lib.rs`
  exposes `pub fn build_app(state)` so handlers are testable via
  `actix_web::test::init_service` without a real network port.
  Binary `main.rs` becomes a thin entry point.
- Handlers added:
  - `POST /{tail:.*}/` — Slug-resolved child creation in container
    via `ldp::resolve_slug`; emits `Location:` header.
  - `PATCH /{tail:.*}` — dialect dispatch on `Content-Type`
    (`text/n3`, `application/sparql-update`, `application/json-patch+json`);
    PATCH-creates-resource path returns 201 via
    `ldp::apply_patch_to_absent`.
  - `OPTIONS /{tail:.*}``Allow` / `Accept-Post` (containers) /
    `Accept-Patch` / `Accept-Ranges` from `ldp::options_for`.
  - `GET /.well-known/solid` — discovery doc.
  - `GET /.well-known/webfinger` — JRD.
  - `GET /.well-known/nodeinfo` + `/.well-known/nodeinfo/2.1`.
  - `GET /.well-known/did/nostr/{pubkey}.json` (gated `did-nostr`).
- **WAC enforcement on writes** (PUT/POST/PATCH/DELETE) via
  `wac::evaluate_access_ctx_with_registry` — was GET-only at
  alpha.1. 401 on anonymous, 403 on authenticated denial, both
  carry `WAC-Allow` header.
- **`PathTraversalGuard` middleware** — explicit percent-decode +
  `..` segment check (single AND double-encoded). Belt-and-braces
  on top of `actix_web::middleware::NormalizePath`.
- **`DotfileGuard` middleware** — wraps the existing
  `DotfileAllowlist` primitive into the request pipeline.
- **Explicit body-size cap**`JSS_MAX_REQUEST_BODY` (via
  `parse_size`) registers an `actix_web::web::PayloadConfig`.
  Default 50 MiB. PUTs over the cap return 413 explicitly rather
  than relying on actix defaults.
- **`--mashlib-cdn` CLI flag** — plumbed into `AppState`. Static
  asset wiring deferred to a follow-up.

### Added — optional TLS

- `solid-pod-rs-server` gains feature `tls`; when set and both
  `JSS_SSL_KEY` + `JSS_SSL_CERT` env vars are populated, server
  binds via `actix_web::HttpServer::bind_rustls_0_23`. Falls back
  to plain bind when either var is absent.

### Tests

- ~54 new tests across 8 new files:
  - `rate_limit_lru.rs` (5), `cors_preflight.rs` (6).
  - `quota_fs.rs` (5), `tenancy_subdomain.rs` (6),
    `config_size_parsing.rs` (6).
  - `nodeinfo_jss.rs` (7).
  - `server_routes_jss.rs` (12), `server_security.rs` (7).
- Total tests now well past 480 across the workspace with all
  Sprint 7 features enabled
  (`oidc,dpop-replay-cache,legacy-notifications,jss-v04,acl-origin,security-primitives,config-loader,nip98-schnorr,webhook-signing,did-nostr,rate-limit,quota`).

### New Cargo features

- `rate-limit` — sliding-window LRU rate limiter (`lru` +
  `parking_lot`).
- `quota` — pod-quota filesystem adapter (`config-loader`).
- `tls` (in `solid-pod-rs-server`) — `rustls` + `rustls-pemfile`.

### Known follow-ups (queued for Sprint 8 / GA)

- CORS + rate-limit middleware not yet wired into the server's
  actix middleware stack — primitives are in `state` but not
  `.wrap()`ed. Trivial follow-up once the `Transform` adapter
  lands.
- `--mashlib-cdn` flag plumbed; static-asset routes not mounted.
- xtask CTH harness — invoke `solid/conformance-test-harness`
  against the new server binary.
- Module re-export polish: `lib.rs` re-exports for `quota::*`,
  `multitenant::*`, `security::cors::*`, `security::rate_limit::*`
  exposed via `pub use`.

## Unreleased — 2026-04-20 (Sprint 6 — WAC 2.0, LDP gaps, webhook signing, did:nostr)

### Added — WAC 2.0 conditions framework

- New `acl:condition` predicate plus `acl:ClientCondition` and
  `acl:IssuerCondition` evaluators (https://webacl.org/secure-access-conditions/).
- `wac::Condition` enum with `serde(other)` fail-closed sentinel —
  unknown condition types parse but evaluate to `NotApplicable`,
  which the evaluator treats as deny (WAC 2.0 §5).
- `ConditionRegistry`, `RequestContext`, `EmptyDispatcher`, plus
  `ClientConditionEvaluator` and `IssuerConditionEvaluator` for
  per-request gate decisions.
- `wac::validate_for_write(&AclDocument, &ConditionRegistry)` — handler
  hook that returns `UnsupportedCondition` so the binder can emit a
  422 `application/problem+json` per WAC 2.0.
- `wac_allow_header_with_dispatcher` — request-scoped variant that
  omits gated modes from `WAC-Allow` when the relevant condition
  evaluates to `NotApplicable`.

### Changed — `wac.rs` split into `wac/`

- 908-line `wac.rs` decomposed into nine focused sub-modules
  (`mod`, `document`, `evaluator`, `parser`, `serializer`,
  `conditions`, `client`, `issuer`, `resolver`, plus the existing
  `origin`). Every file is now under the 500-line CLAUDE.md ceiling.
- `evaluate_access` / `evaluate_access_with_groups` retained as
  `pub use` re-exports; new `evaluate_access_ctx` and
  `evaluate_access_ctx_with_registry` accept a `RequestContext` for
  conditions wiring.

### Added — webhook RFC 9421 signing

- New `notifications/signing.rs` module: Ed25519-backed RFC 9421
  HTTP Message Signatures over `@method`, `@target-uri`,
  `content-type`, `content-digest` (RFC 9530), `date`,
  `x-solid-notification-id`. Sign + verify symmetric so receivers
  can re-use the verifier.
- `WebhookChannelManager` extended with `signer`, `max_attempts`,
  `max_backoff`, `circuit_threshold`, plus circuit-breaker state.
  Builder methods for each.
- Delivery semantics overhauled:
  - 2xx → success.
  - 410 Gone → fatal drop.
  - 4xx (other) → retain subscription, retry as transient.
  - 429 / 5xx → exponential back-off with ±20 % clamped jitter,
    `Retry-After` honoured (seconds and HTTP-date forms).
  - Network error → same as 5xx.
  - Circuit opens after `circuit_threshold` consecutive failures;
    sub stays alive but is paused until reset.
- New feature flag `webhook-signing` (implies `jss-v04`); pulls in
  `ed25519-dalek`, `httpdate`, optional `rand` for OS jitter.

### Added — did:nostr resolver

- `interop::did_nostr` (gated `did-nostr`):
  - `did_nostr_well_known_url(origin, pubkey)` mirrors JSS
    `.well-known/did/nostr/<pubkey>.json`.
  - `did_nostr_document(pubkey, also_known_as)` builds the Tier-1
    DID Doc with a `NostrSchnorrKey2024` verification method.
  - `DidNostrResolver` performs bidirectional resolution: fetch DID
    Doc, walk `alsoKnownAs`, fetch each candidate WebID profile,
    verify a back-link via `owl:sameAs` or `schema:sameAs`. SSRF-
    checked at every outbound. 5 min success / 60 s failure TTL
    cache mirroring JSS.
- Closes mesh-rank E.4 without instantiating the empty
  `solid-pod-rs-nostr` crate (Auth + Operator inspector
  recommendation).

### Added — LDP hidden gaps

- `resolve_slug` now returns `Result<String, PodError::BadRequest>`;
  rejects `/`, `..`, `\0`, lengths > 255 bytes, and any character
  outside `[A-Za-z0-9._-]`. Absent slug still falls back to UUID.
- `options_for(path)` branches `Accept-Ranges` on container vs
  resource — containers get `none`, resources get `bytes`. Closes
  PARITY row 23.
- `not_found_headers(path, conneg_enabled)` — JSS-parity helper
  emitting `Allow` (no DELETE), `Accept-Put`, `Accept-Post`
  (containers only), `Link rel=acl`, `Vary`.
- `vary_header(conneg_enabled)` — explicit primitive.
- `apply_patch_to_absent(dialect, body)` + new `PatchCreateOutcome`
  enum so callers can issue 201 vs 204 on PATCH-creates-resource.
- `parse_range_header_v2` returning `RangeOutcome::{Full, Partial,
  NotSatisfiable}` so empty resources + Range requests yield 416
  rather than 412. The original `parse_range_header` retained for
  callers that don't need the new distinction.
- New `PodError::BadRequest(String)` variant (used by slug + ACL
  parser bounds).

### Added — WAC parser bounds

- `MAX_ACL_BYTES = 1 MiB` (configurable via `JSS_MAX_ACL_BYTES`)
  enforced on Turtle ACL parse — defends against O(n²) splitter
  blowup on multi-MB inputs.
- `MAX_ACL_JSON_DEPTH = 32` enforced on JSON-LD ACL parse via a
  pre-parse depth-counted JSON skim — defends against
  stack-overflow recursion bombs (200-level deep crafted input
  rejected within 5 ms).

### Fixed — DPoP iat-skew connective

- `oidc::verify_dpop_proof_core` (`src/oidc/mod.rs:517`) used `&&`
  on two mutually-exclusive `saturating_sub` branches, so the
  iat-skew gate was unreachable — any iat outside tolerance
  authenticated. Switched to `||`. Filed by the Sprint 6 coverage
  agent via an `#[ignore]` test that was promoted to a real test
  alongside the source fix.

### Tests

- ~90 new tests added across 11 new files:
  - `wac2_conditions.rs` (8), `wac_validate_for_write.rs` (3) —
    WAC 2.0 framework.
  - `webhook_signing.rs` (3), `webhook_retry.rs` (4) — RFC 9421 +
    delivery.
  - `did_nostr_resolver.rs` (6) — bidirectional resolver.
  - `ldp_slug_jss.rs` (9), `ldp_headers_jss.rs` (8),
    `ldp_patch_create_jss.rs` (4), `ldp_range_jss.rs` (5),
    `wac_parser_bounds.rs` (5) — LDP hidden gaps + parser DoS caps.
  - `oidc_mod_direct.rs` (13), `notifications_mod_direct.rs` (11),
    `nip98_extended.rs` (4), `oidc_integration.rs` (5) — mesh-and-
    QE-flagged zero-test modules brought to ≥85 % line coverage.
- **Total in-tree test count: 436 passing across 32 suites** with
  `oidc,dpop-replay-cache,legacy-notifications,jss-v04,acl-origin,security-primitives,config-loader,nip98-schnorr,webhook-signing,did-nostr`.

### New Cargo features

- `webhook-signing` — Ed25519 signing via `ed25519-dalek`, RFC 9421
  profile.
- `did-nostr` — bidirectional resolver in `interop`.

## Unreleased — 2026-04-20 (Sprint 5 security remediation)

### Security (P0 fixes — CVE-class)

- **DPoP proof signature is now actually verified.** Previously
  `verify_dpop_proof_core` (`src/oidc/mod.rs`) decoded the proof body
  without verifying the JWT signature against `header.jwk` — any forged
  proof authenticated. The function now dispatches on `header.alg`
  (`ES256`/`RS256`/`EdDSA`), verifies via `DecodingKey::from_jwk`, and
  rejects `alg=none`. RFC 9449 §4.3 conformance restored.
- **OIDC access-token verification now dispatches on `header.alg`
  against a `JwkSet`.** `verify_access_token` no longer hard-codes
  `Algorithm::HS256`; the new signature accepts a `TokenVerifyKey`
  (`Symmetric` for the test/dev path, `Asymmetric(JwkSet)` for
  production OPs). `alg=none` is unconditionally rejected.
- **JWK thumbprint is now RFC 7638 canonical.** `Jwk::thumbprint` was
  built from a hand-rolled `format!()` JSON template; replaced with
  `BTreeMap`-backed canonical serialisation so thumbprints match JSS
  and any RFC-compliant verifier byte-for-byte. Locked by an RFC 7638
  appendix-A test vector.
- **SSRF-guarded JWKS + OIDC discovery fetcher (`src/oidc/jwks.rs`,
  new).** `fetch_jwks(issuer, &SsrfPolicy, &Client)` runs the SSRF
  policy on the issuer host, builds a per-call reqwest client with
  `.resolve()` to pin the TCP connect to the approved IP (defeating
  DNS rebinding between check and connect), re-runs the SSRF policy
  on the discovered `jwks_uri` host (never reuses the issuer
  approval), and caches results with a 900s TTL mirroring JSS.
  Closes the F5 documentation/implementation gap.
- **Legacy `solid-0.1` WebSocket now enforces a WAC read check on
  every subscribe.** `LegacyNotificationChannel::subscribe` previously
  permitted any client to subscribe to any URI. New
  `SubscriptionAuthorizer` trait wires `wac::evaluate_access` for
  `AccessMode::Read`; default authorizer is `DenyAllAuthorizer`
  (fail-closed). Same-origin guard added. Denial frame literal is
  now `forbidden`, matching JSS grammar exactly.

### Fixed (pre-existing test + lint regressions cleared)

- **`wac::path_matches` (`src/wac.rs`)**`acl:accessTo` now matches
  exact resource + direct children of a container target only (WAC
  §4.2); previously it either matched all descendants (over-grant) or
  failed root-rooted rules entirely. `acl:default` continues to apply
  recursively.
- **`wac::turtle_pop_term` (`src/wac.rs`)** — terminator set extended
  from whitespace-only to `whitespace | ',' | ';' | ']' | ')'`. The
  previous tokeniser welded trailing punctuation onto identifier
  tokens (e.g. `acl:Write,`), silently dropping multi-mode rules.
- **`ldp::extract_block` (`src/ldp.rs`)** — now requires a left word
  boundary AND a `{`-following position before matching `inserts` /
  `deletes` keywords. The previous greedy substring match treated
  `solid:InsertDeletePatch` (which contains both keywords) as a
  block delimiter, parsing both clauses from the same `{ … }`.
- **SPARQL DELETE / INSERT data normalisation (`src/ldp.rs`)**  `xsd:string` datatype on plain literals now strips back to `None`,
  matching the `Term::literal` constructor and the N-Triples fast
  path. Without this, `BTreeSet<Triple>` ordering diverged and
  `DELETE DATA` reported zero deletions when the triple was present.
- **NIP-98 test fixture (`src/auth/nip98.rs`)**`valid_event` now
  computes a real BIP-340 event id (and a real Schnorr signature when
  `nip98-schnorr` is enabled). The previous `id: "0".repeat(64)`
  placeholder caused id-mismatch failures whenever the verify path
  computed the actual id.
- **Workspace clippy**`rust_2018_idioms` lint group now declares
  `priority = -1` to coexist with future per-lint overrides; the
  group-vs-lint priority error no longer blocks `-D warnings` CI.

### Tests

- 17 new tests across 5 new files exercise the P0 fixes:
  `oidc_dpop_signature.rs` (3), `oidc_thumbprint_rfc7638.rs` (3),
  `oidc_access_token_alg.rs` (3), `oidc_jwks_ssrf.rs` (4),
  `legacy_wac_check.rs` (4).
- Total in-tree test count: **346 passing** across 18 suites with the
  full feature matrix
  (`oidc,dpop-replay-cache,legacy-notifications,jss-v04,acl-origin,security-primitives,config-loader,nip98-schnorr`).

### Documentation

- Engineering report:
  [`docs/explanation/jss-parity-upgrade-2026-04-20.md`]docs/explanation/jss-parity-upgrade-2026-04-20.md
  (six-inspector mesh against the real JSS, 12 sections, 31
  actionable items, PARITY-CHECKLIST corrections).
- QE addendum:
  [`docs/explanation/jss-parity-upgrade-2026-04-20-QE-ADDENDUM.md`]docs/explanation/jss-parity-upgrade-2026-04-20-QE-ADDENDUM.md
  (Agentic QE Fleet validation, +1 P0 / +7 P1, per-module
  quality-gate matrix, test-first sequencing for the 3-sprint plan
  to v0.4.0 GA).
- `PARITY-CHECKLIST.md`: Sprint 5 corrections table; 5 new rows
  (53–56 for WAC 2.0 conditions, 62b for DPoP signature). Top-line
  parity recalibrated from a claimed 76% to a verified 59% — the
  drop reflects honest accounting, not regression.

### Dev-dependencies

- `p256`, `pkcs8` (for ES256 keypair generation in OIDC tests).
- `hmac` (for the dpop-replay test helper that previously spliced an
  unsigned signature; now computes a real HMAC).

### Known follow-ups (queued for Sprint 6 + Sprint 7)

See the engineering report and QE addendum. Headline items: WAC 2.0
condition framework, `wac.rs` module split, RFC 9421 webhook signing,
body-size cap, percent-decode path-traversal middleware in the
binary crate, ACL JSON depth bomb cap, did:nostr resolver in
`interop`, operator-surface primitives (rate-limit, CORS, quota,
subdomain MT, TLS, NodeInfo 2.1), CTH harness in `xtask`.

## 0.4.0-alpha.1 — 2026-04-20

### Added

- SSRF guard with IP classification plus allow/deny lists
  (`src/security/ssrf.rs`). Gated behind `security-primitives`.
- Dotfile allowlist (default `.acl`, `.meta`) enforced at the storage
  boundary (`src/security/dotfile.rs`).
- Legacy `solid-0.1` WebSocket notifications adapter for SolidOS
  data-browser compatibility (`src/notifications/legacy.rs`). Gated
  behind `legacy-notifications`.
- WAC `acl:origin` enforcement per the Web Access Control spec §4.3
  (`src/wac/origin.rs`). Gated behind `acl-origin`.
- DPoP `jti` replay cache per Solid-OIDC §5.2 and RFC 9449 §11.1
  (`src/oidc/replay.rs`). Gated behind `dpop-replay-cache`.
- Layered configuration loader (defaults → file → environment) with
  `JSS_*` environment variable mapping for drop-in operational
  parity (`src/config/`). Gated behind `config-loader`.
- Workspace split into `solid-pod-rs` (library) and
  `solid-pod-rs-server` (binary). The binary is a thin actix-web
  shell; the library no longer mounts HTTP routes.
- Reserved sibling crates for the v0.5.0 surface:
  `solid-pod-rs-activitypub`, `solid-pod-rs-git`,
  `solid-pod-rs-idp`, `solid-pod-rs-nostr`.
- Fresh gap analysis against the real JavaScriptSolidServer
  (97 rows, 76 % strict parity).
- ~30 integration tests and four criterion benchmarks covering the
  new surface.

### Changed

- The library crate no longer constructs `actix-web::HttpServer`;
  transport lives in `solid-pod-rs-server`.
- `verify_dpop_proof` now accepts an optional replay cache handle.
- `evaluate_access` now accepts an optional request origin.
- `NOTICE` restructured for clarity.

### Fixed

- Deduplicated residual `jss-v04` feature keys in `Cargo.toml`.

### Security

- Closes five of the six audit findings from the previous release
  cycle. The remaining finding — library-server coupling — is also
  resolved by the workspace split.

## 0.3.0-alpha.3 — 2026-04-20

### Licence migration (BREAKING)

- Licence changed from dual `MIT OR Apache-2.0` to `AGPL-3.0-only`.
- Inherited from the JavaScriptSolidServer (JSS) ecosystem covenant; JSS
  is AGPL-3.0, and solid-pod-rs preserves the network-service copyleft
  protection rather than weakening it with a permissive relicence.
- `LICENSE-MIT` and `LICENSE-APACHE` removed; `LICENSE` added with full
  AGPL-3.0 text.
- `Cargo.toml` `license` field updated to `"AGPL-3.0-only"`.
- `NOTICE` rewritten to document AGPL covenant + full provenance chain.
- `deny.toml` allowlist flipped to permit AGPL-3.0 in dependency graph.
- Consumers: if you're operating solid-pod-rs as a network service, AGPL
  §13 requires you to distribute corresponding source to your users.

## [0.3.0-alpha.1] — 2026-04-20

### Added — Sprint 3 parity close (ADR-053 §"JSS parity gate")

Every remaining `partial` or `missing` row in the JSS parity checklist
is resolved in this release. 67/67 rows are now either `present` (62)
or `explicitly-deferred` (5) with ADR-053 rationale. No `partial` and
no `missing` rows remain.

**WAC / ACL.**
- `wac::parse_turtle_acl` — Turtle ACL parser (accepts `@prefix`,
  `a`-shorthand, `;`-separated predicate lists, `,`-separated object
  lists). The `StorageAclResolver` now falls back to Turtle when the
  JSON-LD parse fails. Covers Solid's Turtle-authored `.acl` documents.
- `wac::serialize_turtle_acl` — canonical Turtle output round-trip.
- `AclDocument`, `AclAuthorization`, `IdOrIds`, `IdRef` now derive
  `Clone + Serialize` (required for `ProvisionPlan` and round-tripping).

**LDP.**
- `ldp::evaluate_preconditions` — RFC 7232 If-Match / If-None-Match,
  including wildcard (`*`) and comma-separated ETag lists. Returns a
  typed `ConditionalOutcome::{Proceed, PreconditionFailed, NotModified}`
  so callers can map to 412 / 304 without repeating logic.
- `ldp::parse_range_header` + `ldp::slice_range` — RFC 7233 byte
  ranges for binary resources. Supports `start-end`, open-ended
  (`start-`), and suffix-length (`-n`) forms; multi-range is rejected
  by design.
- `ldp::options_for` + `ACCEPT_PATCH` — OPTIONS response builder with
  correct `Allow` set per container/resource, `Accept-Post`,
  `Accept-Patch` (n3 / sparql-update / json-patch), and
  `Accept-Ranges: bytes`.
- `ldp::apply_json_patch` — RFC 6902 (`add`, `remove`, `replace`,
  `test`, `copy`, `move`) with JSON Pointer `-` append semantics.
- `PatchDialect::JsonPatch` + `patch_dialect_from_mime` now recognises
  `application/json-patch+json`.
- `PreferHeader::parse` now tolerates multi-IRI `include=` lists
  (`PreferMinimalContainer` + `PreferContainedIRIs` in one directive).

**WebID.**
- `webid::generate_webid_html_with_issuer` — emits `solid:oidcIssuer`
  for Solid-OIDC follow-your-nose discovery.
- `webid::extract_oidc_issuer` — pulls the issuer claim back out of a
  WebID HTML document (accepts string + `{@id:…}` object forms).

**Auth.**
- `auth::nip98::verify_schnorr_signature` — BIP-340 Schnorr signature
  verification over the canonical NIP-01 event hash. Gated behind the
  new `nip98-schnorr` feature (adds `k256` dep). Structural checks
  remain active in both configurations; verifier is invoked
  automatically from `verify_at` when the feature is on.
- `auth::nip98::compute_event_id` — canonical event-id hash per
  NIP-01, reused by the Schnorr verifier.

**Interop / discovery.**
- New `interop` module with:
  - `well_known_solid` — Solid Protocol §4.1.2 discovery document.
  - `webfinger_response` — RFC 7033 JRD with `acct:` and `https://`
    subjects; advertises OIDC issuer + WebID + pim:storage links.
  - `verify_nip05` — NIP-05 identifier verification, `_` wildcard
    fallback for root-of-domain names.
  - `dev_session` / `DevSession` — typed dev-mode bypass; the type is
    constructable only through this helper so callers can gate it
    behind their own env checks without exposing a header-based path.

**Provisioning.**
- New `provision` module with:
  - `ProvisionPlan` + `provision_pod` — declarative pod bootstrap:
    seeded containers (idempotent), WebID profile, optional root ACL,
    optional quota.
  - `QuotaTracker` — atomic reserve/release with `PreconditionFailed`
    on overrun; `None` quota means unlimited.
  - `check_admin_override` — constant-time shared-secret comparison
    that upgrades requests to the new `AdminOverride` marker type.

**Tests.**
- `tests/parity_close.rs` — 20 Sprint 3 integration tests exercising
  every newly-landed feature.
- `tests/interop_jss.rs` grew from 23 to 42 tests (+19 covering Turtle
  ACL, conditional requests, ranges, JSON Patch, OPTIONS response,
  WebID-OIDC, `.well-known/solid`, WebFinger, NIP-05, provisioning,
  quota, admin override, multi-include Prefer, dev session, JSON Patch
  dialect detection, and the `.meta` Link-rel invariant).
- `tests/schnorr_nip98.rs` — 2 Schnorr tests (feature-gated behind
  `nip98-schnorr`).

**Crate metadata.**
- Version bumped `0.2.0-alpha.1``0.3.0-alpha.1`.
- New `nip98-schnorr` feature in `Cargo.toml`.

### Explicitly-deferred (with rationale)

These five rows retain the `explicitly-deferred` status with an ADR-053
pointer so the parity checklist is never a moving target:

- **WebID-TLS** — legacy, superseded by Solid-OIDC + DPoP.
- **RDF/XML serialisation** — format negotiated; serialiser is a
  consumer-crate concern (avoids pulling in sophia/oxigraph).
- **S3 backend** — feature flag + `aws-sdk-s3` optional dep retained;
  concrete impl lives in VisionClaw pod-worker (backend boundary).
- **R2 / D1 / KV adapters** — Cloudflare-specific; consumer-crate.
- **RemoteStorage compatibility** — not on Solid Protocol path.

## [0.2.0-alpha.1] — 2026-04-19 (Phase 2 close)

### Added

- Full Solid Notifications Protocol 0.2 (WebSocket + Webhook channel
  managers, discovery document, exponential retry + fatal-drop).
- Solid-OIDC 0.1: DPoP proof verification, dynamic client registration,
  discovery, token introspection, WebID extraction (feature-gated
  under `oidc`).
- LDP PATCH: N3 (`solid:inserts`/`deletes`/`where`) and SPARQL-Update
  (`INSERT DATA` / `DELETE DATA` / `DELETE WHERE`).
- Prefer header parser + server-managed triple enforcement.
- ACL inheritance corpus (31 tests) + JSS interop corpus (23 tests).
- Count rolled from 27/67 to 48/67 present.

## [0.1.0-alpha.1] — 2026-04-19

### Added
- Initial crate scaffold as a VisionClaw workspace member.
- `Storage` trait with associated `ResourceMeta` and `StorageEvent`
  types.
- `MemoryBackend` — in-memory backend for tests, backed by an
  `Arc<RwLock<HashMap<...>>>` with a broadcast channel for change
  events.
- `FsBackend` — filesystem backend rooted at a configurable directory,
  with SHA-256 ETags, `.meta.json` sidecar files for content type and
  Link values, and a `notify`-backed file watcher.
- `wac` module — JSON-LD ACL evaluator supporting `acl:agent`,
  `acl:agentClass`, `acl:mode`, `acl:accessTo`, `acl:default`,
  container inheritance, and the WAC-Allow response header.
- `ldp` module — container/resource distinction, Link header
  generation, slug resolution for POST-to-container.
- `webid` module — WebID profile document generation and validation.
- `auth::nip98` module — structural NIP-98 token verification (kind,
  tags, URL/method/payload matching, timestamp tolerance).
- `error::PodError` — crate-wide error type.
- Conformance test suite (`tests/storage_trait.rs`) covering Memory
  and FS backends.
- WAC smoke tests (`tests/wac_basic.rs`).
- `examples/standalone.rs` — minimal actix-web Solid pod server.

### Notes
- The Phase 1 NIP-98 module implements all structural checks. Schnorr
  signature verification is deferred to Phase 2, behind a feature flag
  that will gate the `k256` dependency.
- Notifications module (`src/notifications.rs`) ships with trait
  signatures and in-memory stubs. Full Solid Notifications Protocol
  (WebSocket, Webhook) is the Phase 2 deliverable.

[0.1.0-alpha.1]: https://github.com/DreamLab-AI/VisionClaw