Skip to content

feat: strict fail-closed IPNS verification cutover across Rust, TS, and API#555

Merged
FSM1 merged 56 commits into
mainfrom
feat/ipns-verification-cross-layer-closeout-desktop-and-api
Jun 24, 2026
Merged

feat: strict fail-closed IPNS verification cutover across Rust, TS, and API#555
FSM1 merged 56 commits into
mainfrom
feat/ipns-verification-cross-layer-closeout-desktop-and-api

Conversation

@FSM1

@FSM1 FSM1 commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 60 (req HARD-11) makes IPNS record verification strict fail-closed across every layer — removing all degraded/legacy acceptance paths, unifying first-publish sequencing, and routing every resolve site through a single verified chokepoint. This is the D-01/D-12 lockstep cutover.

What changed (Waves 1-3, code)

  • Rust verify chokepoint relocated to cipherbox_api_client::ipns and shared by sdk/fuse/desktop; VerifyError::Legacy removed; resolve-side EOL/expiry enforced with a 5-minute skew buffer (D-04, D-07, D-08).
  • First-publish sequencing unified: all 9 producers embed sequence 1 (D-02); the API rejects embedded-0 first publishes (D-03).
  • Strict TS resolve (sdk-core): absent signature fields throw, strict sequence equality, Validity/EOL enforced (D-05, D-07).
  • All resolve paths routed fail-closed: 9 FUSE arms, 2 sdk bypasses, 6 desktop Tauri sites; crates/fuse/src/verify.rs deleted (D-04, D-08, D-09).
  • API strict regime: null signed_record returns 404; legacy enrich removed (D-06); the mandatory publish-side signature verify anchor is unchanged.
  • Publish-path verify cache (D-11): short-TTL, byte-exact keyed, size-bounded; only populated after a successful in-process verify; never read by the resolve/TEE paths. Measured cost in docs/CAPACITY.md 1.6.
  • Cross-language vectors regenerated; legacy-absent and first-publish-skew reclassified invalid (D-10).

Verification

  • cargo test --workspace green; API jest 173; sdk-core vitest 252; SDK E2E 89/89 (real client to API IPNS publish/resolve round-trip); cross-language parity vectors green.
  • Security: 27/27 threat mitigations verified (60-SECURITY.md).
  • Goal verification: 16/17 must-haves (60-VERIFICATION.md); the one remaining is the operational staging cutover below.

Review fixes folded in

The SDK E2E gate caught a real regression unit tests missed: 60-05's removal of the resolve "enrich" assumed the Ed25519 pubKey is embedded in the record, but for Ed25519 it lives in the IPNS name — so resolve returned records without signature fields and the strict client failed closed on every round-trip. Fixed by serving the authoritative DB signed record (with pubKey supplied from the publish-validated publicKey column, which the client re-checks against the name) on equal-or-higher sequence.

CodeRabbit review: 7 in-scope findings fixed (duplicate-Validity-key rejection, RFC3339 strictness, registry/web/codec fail-closed hardening, bounded verify cache plus byte-exact keying test); 4 deferred as todos under .planning/todos/pending/; 4 false positives skipped after adversarial verification.

Operational follow-up (Plan 60-08, not in this diff)

The D-12 cutover requires a staging DB wipe, redeploy, and strict-verify smoke test after this lands and the cross-layer CI gates (Windows winfsp, SDK E2E, Desktop E2E) are green. That is a human-action checkpoint; the phase is operationally complete only after that sign-off. Local-dev-DB-wipe guidance is documented in docs/DEVELOPMENT.md.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added strict, fail-closed IPNS verification for publishing and verified resolution, rejecting tampered, malformed, missing, or expired records.
    • Standardized initial IPNS sequence to 1 across desktop, web, and API.
    • Introduced a short-TTL in-process verification cache for repeated publishes.
  • Bug Fixes
    • Prevented cached/unsigned/legacy or partially verified IPNS data from being treated as valid; tightened DB/network resolution selection and error handling.
    • Desktop vault initialization, FUSE prepopulation, and sync now fail safely on verification failures.
  • Tests / Docs
    • Updated verification vectors and SDK/desktop/API tests for strict semantics; refreshed IPNS capacity and development cutover notes.

FSM1 and others added 30 commits June 24, 2026 01:06
Entire-Checkpoint: 7fbf038a9e1d
Entire-Checkpoint: af2b5ddb1acc
8 plans across 4 waves covering HARD-11 and decisions D-01..D-12:
strict no-legacy verify cutover, embed-1 producer unification,
verified-resolve coverage in sdk/desktop, API hot-path verify caching,
cross-language vector regen, and the D-12 lockstep staging wipe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…04 strict cutover

- Add bind_verified_first_publish_seq_skew_now_invalid: asserts Invalid for embedded=0/resp_seq=1 (D-04)
- Add bind_verified_absent_fields_returns_invalid: asserts Invalid for None verdict (Legacy removed)
- Add bind_verified_valid/cid_swap/seq_mismatch/invalid_sig: core correctness tests
- Update absent_fields_returns_none to absent_fields_returns_some_false (D-04: Ok(None) → Ok(Some(false)))
- Tests reference bind_verified/VerifyError/VerifiedResolve not yet in api-client (fail to compile = RED)

Entire-Checkpoint: e4a096d939fb
…D-04 strict cutover

- Add cipherbox-core + ciborium deps to crates/api-client/Cargo.toml (enables decode_ipns_cbor_data)
- Move VerifyError (no Legacy), VerifiedResolve, bind_verified, resolve_ipns_verified to api-client/src/ipns.rs
- Remove VerifyError::Legacy variant (D-04) — all-absent sig fields now Err(Invalid)
- Remove first-publish skew disjunct (resp_seq==1 && embedded_seq==0) for strict equality (D-04)
- Remove Ok(None) all-absent branch from verify_ipns_resolve_signature (falls through to Some(false))
- Reduce crates/fuse/src/verify.rs to thin re-export of api-client symbols (D-08)
- Fold all 9 fuse Legacy caller arms into Invalid fail-closed handling (D-04 compile-time enforcement)
- All 15 api-client unit tests pass; cargo check -p cipherbox-fuse clean

Entire-Checkpoint: df13e20983da
- bind_verified_expired_record_returns_invalid: 2020 past date must return Invalid with 'expired'
- bind_verified_future_validity_returns_ok: far-future date must pass
- bind_verified_within_skew_buffer_returns_ok: not-expired case passes
- Tests fail (expired test returns Ok instead of Err) until bind_verified checks Validity field

Entire-Checkpoint: c0cc81fc667a
…ew buffer

- Add decode_ipns_cbor_validity companion fn to crates/core/src/ipns.rs (surfaces Validity bytes)
- Add parse_rfc3339_to_unix_secs manual RFC3339 parser to api-client/src/ipns.rs (no chrono dep)
- Add expiry check in bind_verified: reject when validity < now - 300s (5-min skew buffer)
- Fail-closed: missing/unparseable Validity treated as expired (D-07)
- All 18 api-client tests pass; cargo check --workspace clean
- Decision: companion fn decode_ipns_cbor_validity chosen over 3-tuple return to avoid call-site churn

Entire-Checkpoint: eca0ef60814b
…t cutover plan

Entire-Checkpoint: a531c0ed8137
- crates/fuse/src/write_ops/implementation/mkdir.rs: new-folder create_ipns_record seq 0->1; coordinator.record_publish 0->1
- crates/fuse/src/platform/windows/write_ops.rs: Windows new-folder create_ipns_record seq 0->1; coordinator.record_publish 0->1 (winfsp CI-gated)
- crates/fuse/src/metadata.rs: bin first-publish make_bin_record(0)->make_bin_record(1); coordinator.record_publish 0->1
- apps/desktop/src-tauri/src/commands/vault.rs: vault-key blob and root-folder create_ipns_record seq 0->1 at both sites

Entire-Checkpoint: 1c743c03ab70
- packages/sdk-core/src/vault/index.ts: vault-key blob sequenceNumber 0n->1n
- apps/web/src/hooks/useAuth.ts: vault-key blob and root-folder metadata sequenceNumber 0n->1n at both sites
- apps/web/src/services/vault-settings.service.ts: first-publish fallback let sequenceNumber 0n->1n; forward-publish BigInt(resolved.sequenceNumber ?? 0)+1n unchanged

Entire-Checkpoint: 8ab7632dedd7
- 60-02-SUMMARY.md: 9 producer sites unified, greps clean, self-check passed
- STATE.md: advance to plan 3 of 8, add D-02 decision, record metrics
- ROADMAP.md: mark 60-02 complete

Entire-Checkpoint: c5a25a4c02fb
…solve

- D-05-a: resolveIpnsRecord throws when all sig fields absent (fail closed)
- D-05-b: throws on first-publish skew seq=0/resp=1 (skew disjunct removed)
- D-07-c: throws when CBOR Validity timestamp is in the past (EOL enforce)
- D-07-d: returns verified result for valid in-window record (positive case)
- Rule 1 fix: update vault test expectation from 0n to 1n (Plan 60-02 changed
  publishVaultKeyBlob to embed sequenceNumber=1n; test was stale)

Entire-Checkpoint: f63fc0de98e2
…expiry

D-05: delete legacy else branch in resolveIpnsRecord — absent signature
fields now throw fail closed instead of returning signatureVerified:false.

D-05: remove skew disjunct (embedded=0 resp=1 accepted) from sequence
binding check; strict equality only — all producers now embed 1 (Plan 60-02).

D-07: parse CBOR Validity field after CBOR decode; throw when expiry is
before now minus 5-minute skew buffer (mirrors Rust Plan 60-01 semantics).

Update existing tests that relied on old behavior: legacy-soft-return,
first-publish-skew acceptance, cross-language vector first-publish-skew
and legacy-absent now expect throws per D-05 reclassification.

Entire-Checkpoint: 51ef4c8511ad
…d delete verify.rs

- Replace all crate::verify::resolve_ipns_verified / VerifyError references in
  events.rs, metadata.rs (×3), publish.rs (×2), fs.rs, replay.rs (×9 sites)
  with cipherbox_api_client::ipns::resolve_ipns_verified / VerifyError
- Remove pub mod verify declaration from crates/fuse/src/lib.rs
- Delete crates/fuse/src/verify.rs (thin re-export shim no longer needed)
- Single verified-resolve chokepoint now lives exclusively in api-client (D-08)
- All 9 FUSE caller arms confirmed fail-closed via Invalid (D-04 — folded in 60-01)

Entire-Checkpoint: 920e869bbb6c
…_verified

- crates/sdk/src/registry.rs: replace raw resolve_ipns with resolve_ipns_verified
  in fetch_and_decrypt_registry; map VerifyError::Invalid to RegistryError (D-08)
- crates/sdk/src/sync.rs: replace raw resolve_ipns with resolve_ipns_verified in
  poll(); log error and fail poll cycle on Invalid (D-08)
- apps/desktop/src-tauri/src/fuse/prepopulate.rs: route all 4 resolve sites
  (root, root-file-pointer, subfolder, subfolder-file-pointer) through
  resolve_ipns_verified; fail-closed per-operation with log::error (D-09)
- apps/desktop/src-tauri/src/commands/vault.rs: route 2 resolve sites
  (load_vault_settings, fetch_and_decrypt_vault) through resolve_ipns_verified;
  fail-closed with log::error on Invalid (D-09)
- Zero raw resolve_ipns calls remain in sdk or desktop resolve paths

Entire-Checkpoint: 406637b3cf1e
…loseout plan

Entire-Checkpoint: 962c268b326f
… D-06 null-signed-record 404

Entire-Checkpoint: 44a54cdc7038
…igned-record to 404

- D-03: reject embedded sequence 0n on first publish (only 1 accepted)
- D-06: parseCachedRecord returns null for null signedRecord rows
- D-06: discard inconsistent cached result on CID mismatch instead of warn+override
- D-06: remove withCachedPublicKey enrich call and equal-seq signatureV2 enrich block
- Update spec tests to reflect new strict behavior (D-03 and D-06)
- Publish-side verifyIpnsRecordSignature anchor at ipns.service.ts:87-89 unchanged

Entire-Checkpoint: 840e3ee9a14e
withCachedPublicKey is no longer imported or called after the D-06
resolve enrich removal in ipns.service.ts.

Entire-Checkpoint: 3824158cb0f4
…id in generator

- legacy-absent: expected_result "legacy" -> "invalid" (D-04 strict fail-closed)
- first-publish-skew: expected_result "valid" -> "invalid" (D-05 strict seq equality)
- update expectedResults sanity-check array to match
- regenerate tests/vectors/ipns/verify.json via npx tsx (only 2 cases changed)

Entire-Checkpoint: c43a0d3b7271
- None => "invalid" (D-04: absent fields fail-closed, no legacy branch)
- strict seq_ok = embedded_seq == resp_seq (D-05: skew disjunct removed)
- update doc comment: remove stale "legacy" result reference
- cargo test -p cipherbox-fuse --test ipns_verify_vectors passes (1/1)

Entire-Checkpoint: f41d34fe11b8
- Add scripts/bench-ipns-verify.ts: measures verifyIpnsRecordSignature
  (Ed25519+proto) vs Map.get() cache-hit; typechecked via tsconfig.scripts.json
- Add bench-ipns-verify.ts to tsconfig.scripts.json include list
- Add docs/CAPACITY.md §1.6: measured per-op verify cost (mean 0.105 ms,
  p50 0.095 ms, p99 0.337 ms); confirms cache is justified (go decision)
- Confirms TEE republish path never calls verifyIpnsRecordSignature

Entire-Checkpoint: 124977a3e9c1
@github-actions github-actions Bot added release:cipherbox-sdk:feat Minor version bump (new feature) for cipherbox-sdk release:sdk:fix Patch version bump (bug fix) for sdk release:tee-worker:fix Patch version bump (bug fix) for tee-worker labels Jun 24, 2026
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Release Preview

Package Bump Label Source
api minor release:api:feat Direct (feat commit)
cipherbox-core minor release:cipherbox-core:feat Direct (feat commit)
cipherbox-fuse minor release:cipherbox-fuse:feat Direct (feat commit)
cipherbox-sdk minor release:cipherbox-sdk:feat Direct (feat commit)
core patch release:core:fix Cascade (crypto patch)
crypto patch release:crypto:fix Direct (fix commit)
desktop minor release:desktop:feat Direct (feat commit)
sdk patch release:sdk:fix Cascade (sdk-core minor)
sdk-core minor release:sdk-core:feat Direct (feat commit)
tee-worker patch release:tee-worker:fix Cascade (sdk-core minor)
web minor release:web:feat Direct (feat commit)

Cascade Details

  • sdk-core minor -> sdk patch (direct dependency)
  • sdk-core minor -> tee-worker patch (direct dependency)
  • crypto patch -> core patch (direct dependency)

@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR completes Phase 60's strict fail-closed IPNS verification across all layers (Rust, TypeScript, API), removing the Legacy variant and all degraded-acceptance paths, routing every resolve site through a single shared resolve_ipns_verified chokepoint, and enforcing Validity/EOL expiry with a 5-minute clock-skew buffer. A regression from Plan 60-05 (Ed25519 pubKey absent from network records) is fixed by serving the DB-authoritative signed record on equal-or-higher sequence, with pubKey recovered from the IPNS name when the DB column is null.

  • Rust chokepoint consolidated (crates/api-client/src/ipns.rs): VerifyError::Legacy removed, bind_verified adds CBOR Validity expiry enforcement via a hand-rolled RFC3339 parser that correctly rejects calendar-rollover dates (e.g. Feb 31) without a chrono dependency. All 9 FUSE call sites, 2 SDK call sites, and 6 desktop Tauri sites updated to the shared path.
  • TS strict resolve (sdk-core): first-publish skew disjunct removed, all-absent signature fields now throw, Validity EOL enforced matching the Rust semantics (with the known new Date() vs. Rust parser precision gap tracked in a pending todo).
  • D-11 verify cache (ipns-verify-cache.ts): short-TTL, byte-exact keyed, size-bounded singleton populated only after successful in-process verification \u2014 never from the resolve/DHT path.

Confidence Score: 5/5

Safe to merge with the D-12 staging DB wipe executed first; the cutover is operationally gated and all E2E/unit test suites are green.

The core verify logic (Rust bind_verified, TS resolveIpnsRecord, API publish path) is thoroughly tested across unit, integration, and E2E layers. All three findings are documentation and operational observations, not logic errors in the new strict paths, and are bounded by the documented D-12 operational gate.

apps/api/src/ipns/ipns-verify-cache.spec.ts (test comments misattribute the cache key to parsed signatureV2 rather than raw record bytes); apps/api/src/ipns/ipns.service.ts resolve path for networkSeq > dbSeq (returns unenriched network result without pubKey for Ed25519 names).

Important Files Changed

Filename Overview
crates/api-client/src/ipns.rs Core of the cutover: relocates the verified-resolve chokepoint here, removes the Legacy variant, adds CBOR validity expiry enforcement (D-07) with a custom RFC3339 parser that correctly rejects calendar-rollover dates. bind_verified, verify_ipns_resolve_signature, and parse_rfc3339_to_unix_secs are all well-tested including boundary and skew-buffer cases.
packages/sdk-core/src/ipns/index.ts TS resolve chokepoint made strict: first-publish skew disjunct removed, all-absent fields now throw, Validity expiry enforced — mirrors Rust semantics. Uses new Date(validityStr) for expiry parsing (less strict than the custom Rust RFC3339 parser regarding calendar-rollover dates); a pending todo already tracks this gap.
apps/api/src/ipns/ipns-verify-cache.ts New short-TTL publish-path verify cache (D-11): byte-exact keyed on raw record bytes, size-bounded at 10 000 entries, never populated from the resolve/DHT path. The sequenceNumber parameter is vestigial (always passed as ''), producing a double-colon key that mismatches the module-level doc (flagged in a prior thread).
apps/api/src/ipns/ipns-verify-cache.spec.ts Comprehensive cache unit + integration tests. Tests 5–8 pass correctly (same raw bytes → same cache key), but the beforeEach comment attributes the cache key to signatureV2 from parseIpnsRecord, which the service never uses for keying — potentially misleading about the security property being tested.
apps/api/src/ipns/ipns.service.ts Publish path gains D-11 cache short-circuit; resolve path now prefers the DB record when dbSeq ≥ networkSeq (fixing the Ed25519 pubKey regression from Plan 60-05). When networkSeq > dbSeq the unenriched network result is returned without pubKey enrichment, causing strict-client failures in the transient out-of-band-publish window.
apps/api/src/ipns/ipns-record.codec.ts D-06: null signedRecord now returns null (→ 404). pubKey recovery waterfall (parsed bytes → DB publicKey column → publicKeyFromIpnsName) ensures Ed25519 records always carry the verifying key for the strict client. withCachedPublicKey helper removed (subsumed by new recovery logic).
crates/sdk/src/registry.rs D-08 applied: registry fetch now routes through resolve_ipns_verified. Crucially, first-device bootstrap now correctly triggers only on IpnsNotFound (404); any other error (verification failure, network error) propagates instead of silently granting first-device access.
crates/fuse/src/publish.rs Legacy arm removed; Invalid now covers all-absent records. resolve_sequence falls back to local cache on Invalid (D-02 soft), resolve_sequence_strict returns Err. Cold-cache behaviour for unsigned legacy rows now fails rather than using the DB sequence — strictly correct post-D-12 wipe but operationally dependent on it.
crates/core/src/ipns.rs Adds decode_ipns_cbor_validity helper for D-07 expiry enforcement, with duplicate-key rejection mirroring decode_ipns_cbor_data. CBOR is decoded twice per verification (once for Value/Sequence, once for Validity) — minor inefficiency but correct.
packages/crypto/src/ipns/derive-name.ts Adds publicKeyFromIpnsName, the deterministic inverse of deriveIpnsName, enabling the API to recover Ed25519 pubKey from the IPNS name for rows whose publicKey column is null. Correctly validates raw key length and wraps all errors as CryptoError.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client as SDK Client (TS)
    participant API as NestJS API
    participant Cache as IpnsVerifyCache
    participant DB as PostgreSQL
    participant DHT as Delegated Routing (DHT)
    participant Rust as Rust (fuse/sdk/desktop)

    Note over Client,Rust: PUBLISH PATH (D-11 cache short-circuit)
    Client->>API: POST /ipns/publish
    API->>Cache: isVerified(ipnsName, base64(recordBytes))?
    alt Cache HIT
        Cache-->>API: true, skip Ed25519 verify
    else Cache MISS
        API->>API: verifyIpnsRecordSignature
        API->>Cache: recordVerified
    end
    API->>DB: upsertFolderIpns
    API->>DHT: publish relay

    Note over Client,Rust: RESOLVE PATH
    Client->>API: GET /ipns/resolve
    API->>DB: findOne signedRecord + publicKey
    API->>DHT: resolve network record
    alt "dbSeq >= networkSeq"
        API-->>Client: DB record with pubKey enriched
    else "networkSeq > dbSeq"
        API-->>Client: network result may lack pubKey
    end
    Client->>Client: verifyEd25519 + CBOR binding + Validity EOL

    Note over Rust: RUST CHOKEPOINT (D-08)
    Rust->>API: resolve_ipns
    API-->>Rust: IpnsResolveResponse
    Rust->>Rust: verify_ipns_resolve_signature
    Rust->>Rust: bind_verified CID+seq+Validity
    alt Ok(VerifiedResolve)
        Rust-->>Rust: use verified CID
    else Err(Invalid)
        Rust-->>Rust: fail-closed
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client as SDK Client (TS)
    participant API as NestJS API
    participant Cache as IpnsVerifyCache
    participant DB as PostgreSQL
    participant DHT as Delegated Routing (DHT)
    participant Rust as Rust (fuse/sdk/desktop)

    Note over Client,Rust: PUBLISH PATH (D-11 cache short-circuit)
    Client->>API: POST /ipns/publish
    API->>Cache: isVerified(ipnsName, base64(recordBytes))?
    alt Cache HIT
        Cache-->>API: true, skip Ed25519 verify
    else Cache MISS
        API->>API: verifyIpnsRecordSignature
        API->>Cache: recordVerified
    end
    API->>DB: upsertFolderIpns
    API->>DHT: publish relay

    Note over Client,Rust: RESOLVE PATH
    Client->>API: GET /ipns/resolve
    API->>DB: findOne signedRecord + publicKey
    API->>DHT: resolve network record
    alt "dbSeq >= networkSeq"
        API-->>Client: DB record with pubKey enriched
    else "networkSeq > dbSeq"
        API-->>Client: network result may lack pubKey
    end
    Client->>Client: verifyEd25519 + CBOR binding + Validity EOL

    Note over Rust: RUST CHOKEPOINT (D-08)
    Rust->>API: resolve_ipns
    API-->>Rust: IpnsResolveResponse
    Rust->>Rust: verify_ipns_resolve_signature
    Rust->>Rust: bind_verified CID+seq+Validity
    alt Ok(VerifiedResolve)
        Rust-->>Rust: use verified CID
    else Err(Invalid)
        Rust-->>Rust: fail-closed
    end
Loading

Reviews (8): Last reviewed commit: "docs(60): record real root cause of macO..." | Re-trigger Greptile

Comment thread crates/api-client/src/ipns.rs
Comment thread apps/api/src/ipns/ipns-verify-cache.ts
Comment thread apps/api/src/ipns/ipns-verify-cache.ts
Comment thread packages/sdk-core/src/ipns/index.ts
Release #554 shipped crates/fuse 0.9.0 to main (manifest bumped) but left
the release-as: 0.9.0 pin in release-please-config.json. On this PR the
pull_request merge tree combines main's manifest (0.9.0) with the pin
(0.9.0), so check-stale-release-as.js flags it stale and the Lint job
fails — which in turn skips every needs:[changes,lint] test job.

Bump the pin to 0.10.0 (next minor after the shipped 0.9.0, matching this
PR's fuse changes). The preview bot's don't-downgrade guard preserves a
higher existing release-as, so this stays stable across recomputes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 6f4d7a2638e8
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.72727% with 138 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.24%. Comparing base (ff9b356) to head (4491fb5).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
apps/desktop/src-tauri/src/fuse/prepopulate.rs 0.00% 44 Missing ⚠️
apps/desktop/src-tauri/src/commands/vault.rs 0.00% 21 Missing ⚠️
crates/api-client/src/ipns.rs 96.00% 12 Missing ⚠️
crates/fuse/src/metadata.rs 0.00% 11 Missing ⚠️
crates/sdk/src/sync.rs 0.00% 11 Missing ⚠️
crates/sdk/src/registry.rs 0.00% 10 Missing ⚠️
crates/fuse/src/replay.rs 0.00% 6 Missing ⚠️
apps/api/src/ipns/ipns-record.codec.ts 72.22% 5 Missing ⚠️
crates/fuse/src/publish.rs 33.33% 4 Missing ⚠️
apps/api/src/ipns/ipns-verify-cache.ts 85.71% 3 Missing ⚠️
... and 4 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #555      +/-   ##
==========================================
+ Coverage   65.97%   69.24%   +3.27%     
==========================================
  Files         148      177      +29     
  Lines       11476    20830    +9354     
  Branches     1302     1305       +3     
==========================================
+ Hits         7571    14424    +6853     
- Misses       3661     6164    +2503     
+ Partials      244      242       -2     
Flag Coverage Δ
api 85.95% <84.90%> (-0.09%) ⬇️
api-client 85.95% <84.90%> (-0.09%) ⬇️
core 85.95% <84.90%> (-0.09%) ⬇️
crypto 85.95% <84.90%> (-0.09%) ⬇️
desktop 16.45% <0.00%> (-14.99%) ⬇️
rust 67.23% <83.24%> (?)
sdk 85.95% <84.90%> (-0.09%) ⬇️
sdk-core 85.95% <84.90%> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
apps/api/src/ipns/ipns.service.ts 87.30% <100.00%> (-1.41%) ⬇️
crates/fuse/src/lib.rs 98.45% <ø> (ø)
crates/fuse/src/write_ops/implementation/mkdir.rs 56.02% <0.00%> (ø)
apps/api/src/ipns/ipns-verify-cache.ts 85.71% <85.71%> (ø)
crates/core/src/ipns.rs 97.87% <91.66%> (ø)
crates/fuse/src/events.rs 0.00% <0.00%> (ø)
crates/fuse/src/fs.rs 55.22% <0.00%> (ø)
crates/fuse/src/publish.rs 61.26% <33.33%> (ø)
apps/api/src/ipns/ipns-record.codec.ts 85.71% <72.22%> (-9.03%) ⬇️
crates/fuse/src/replay.rs 48.16% <0.00%> (ø)
... and 6 more

... and 72 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

FSM1 and others added 2 commits June 24, 2026 16:18
Greptile flagged that post-D-04 verify_ipns_resolve_signature never returns
Ok(None), so bind_verified's None arm is unreachable from the production
resolve path and the all-absent case surfaces as the generic 'signature
verification failed', hiding a meaningfully different root cause from an
operator debugging a pre-cutover record on a non-wiped environment.

Log the distinct cause at the detection site (the only place that can tell
all-absent from a partial downgrade — bind_verified only sees Some(false)),
and document the None arm as the defensive, test-covered totality arm it
now is. No behavior or type change: all paths still fail closed and the 19
api-client ipns tests pass unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: abf288d2aa84
…mn is null

The Phase 60 D-06 fail-closed branch in parseCachedRecord discarded any
authoritative DB record whose nullable publicKey column was never populated.
Writable-shared-folder rows have a null public_key column, so the strict
resolve returned nothing and the web-e2e 'writable-shares 3.4 Bob can open
and edit the file he uploaded' loaded empty content (expect 'Content
uploaded by Bob', received '').

For CipherBox's Ed25519-only IPNS model the k51... name *encodes* the public
key, so it is the authoritative source and is always recoverable even when
the column cache is absent. Add publicKeyFromIpnsName to @cipherbox/crypto
(the exact inverse of deriveIpnsName via peerIdFromCID) and use it as the
pubKey fallback in parseCachedRecord. Still fail-closed: a malformed or
non-Ed25519 name discards the record, and the strict client re-checks
deriveIpnsName(pubKey) === ipnsName.

This is the complete fix for the bug class behind the earlier SDK-E2E
regression too: pubKey no longer depends on a column that isn't always set.

Verified: crypto round-trip test (recovers the exact raw key), API jest 174
(new null-column regression test), SDK E2E 89/89.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 84b678898693
@github-actions github-actions Bot added release:crypto:fix Patch version bump (bug fix) for crypto release:core:fix Patch version bump (bug fix) for core labels Jun 24, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/fuse/src/publish.rs (1)

107-119: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed on VerifyError::Invalid instead of using cached sequence.

Invalid covers signature/CBOR binding failures, not transient availability. Falling back to cache here lets publish flows proceed from an unverifiable live record; keep cache fallback for Api errors only.

Proposed fix
             Err(cipherbox_api_client::ipns::VerifyError::Invalid(msg)) => {
-                // D-02: verify failure on soft resolve — fall back to cache (never wedge).
-                log::warn!(
-                    "resolve_sequence: IPNS {} verify failed: {} — falling back to cache (D-02)",
-                    ipns_name, msg
-                );
-                match self.get_cached(ipns_name) {
-                    Some(cached) => Ok(cached),
-                    None => Err(format!(
-                        "IPNS verify failed and no cached sequence for {}: {}",
-                        ipns_name, msg
-                    )),
-                }
+                Err(format!("IPNS {} verify failed: {}", ipns_name, msg))
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fuse/src/publish.rs` around lines 107 - 119, The `resolve_sequence`
handling for `cipherbox_api_client::ipns::VerifyError::Invalid` should fail
closed instead of falling back to `get_cached`; update the match arm in
`publish.rs` so `Invalid(msg)` logs the verification failure and returns an
error immediately, while preserving cache fallback only for transient
API-related errors elsewhere in the surrounding resolve logic. Use the existing
`resolve_sequence`, `get_cached`, and `VerifyError::Invalid` symbols to keep the
change scoped correctly.
🧹 Nitpick comments (4)
crates/api-client/src/ipns.rs (1)

8-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the stale FUSE re-export note.

The current stack says crates/fuse/src/verify.rs is deleted, but this module doc says it was reduced to a thin re-export. That points future maintainers at a non-existent compatibility path.

Suggested doc cleanup
-//! share one implementation (D-08). The `crates/fuse/src/verify.rs` file is reduced to
-//! a thin re-export of the public symbols here.
+//! share one implementation (D-08).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/api-client/src/ipns.rs` around lines 8 - 9, The module doc in ipns.rs
still references the removed FUSE compatibility path, so update the top-level
documentation comment to remove the stale note about crates/fuse/src/verify.rs
being a thin re-export. Keep the doc focused on the current api-client/IPNS
implementation and ensure any mention of re-exports or deleted modules is
removed from the file-level comment.
crates/fuse/src/metadata.rs (1)

504-531: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix the bin first-publish sequence comment.

Lines 507-508 still describe publishing directly at seq 0, but Line 531 now correctly publishes seq 1.

Proposed cleanup
-            // that not-found as a fatal error, so the first publish must NOT go through the
-            // CAS helper — it publishes directly at seq 0 with expected_sequence_number: None
+            // that not-found as a fatal error, so the first publish must NOT go through the
+            // CAS helper — it publishes directly at seq 1 with expected_sequence_number: None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fuse/src/metadata.rs` around lines 504 - 531, Update the bin
first-publish comment in metadata.rs so it matches the actual behavior in the
first-publish path: the logic in make_bin_record and is_first_bin_publish now
publishes the initial bin record with sequence 1, not 0. Keep the surrounding
explanation about skipping the CAS helper for the first publish, but revise the
inline comment near is_first_bin_publish to describe seq 1 consistently with the
call that uses make_bin_record(1).
crates/fuse/src/write_ops/implementation/mkdir.rs (1)

183-199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the stale first-publish sequence comment.

Line 198 still says sequence 0 should never conflict, but this path now publishes and records sequence 1.

Proposed cleanup
-                        // Sequence 0 should never conflict -- log and continue
+                        // First-publish sequence 1 should never conflict -- log and continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fuse/src/write_ops/implementation/mkdir.rs` around lines 183 - 199,
The first-publish sequence comment in the new-folder publish path is stale and
conflicts with the current logic in mkdir.rs around publish_ipns and
coordinator.record_publish. Update the inline comment in this branch so it
correctly reflects that the initial IPNS publish uses sequence 1 and that the
conflict path should describe sequence 1, not 0; keep the wording aligned with
the existing request setup and publish result handling.
packages/crypto/src/__tests__/ipns-record.test.ts (1)

68-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a valid-but-non-Ed25519 case to this suite.

The malformed-name assertion only covers CID parse failure. publicKeyFromIpnsName() also has a fail-closed branch for valid IPNS names that do not inline a 32-byte Ed25519 key, and apps/api/src/ipns/ipns-record.codec.ts now depends on that branch when recovering a missing publicKey from cached rows. Without a test for a hashed/non-Ed25519 PeerId, that path can regress while this suite stays green. As per path instructions, **/*.test.ts: Focus on test coverage, edge cases, and test quality. Ensure tests are meaningful and not just for coverage metrics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/crypto/src/__tests__/ipns-record.test.ts` around lines 68 - 92, Add
a valid-but-non-Ed25519 coverage case for publicKeyFromIpnsName in
ipns-record.test.ts so the suite exercises the fail-closed branch, not just CID
parse failure. Create a real IPNS name/PeerId that is syntactically valid but
does not inline a 32-byte Ed25519 public key, then assert publicKeyFromIpnsName
throws CryptoError for that input. Keep the existing round-trip tests for
deriveIpnsName and publicKeyFromIpnsName, and place the new case alongside the
current malformed-name test to protect the recovery logic used by
ipns-record.codec.ts.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
@.planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-05-PLAN.md:
- Around line 42-43: The plan file currently references local absolute paths,
which makes the committed doc non-portable and leaks environment-specific
details. Update the referenced entries to use repo-relative paths or the
existing tool alias style instead, and verify the surrounding section in
60-05-PLAN.md no longer contains any user-specific filesystem paths.

In
@.planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-06-PLAN.md:
- Around line 49-50: Replace the machine-specific absolute paths in the planning
note with repo-relative references; update the references in 60-06-PLAN.md to
point to the existing workflow/template files without including the local
username or full filesystem prefix. This change should be made where the two
linked paths are listed so the plan stays portable and does not leak
identity-specific information.

In
@.planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-08-PLAN.md:
- Line 15: The planning doc contains explicit root SSH host details that should
not be tracked in source control. Update the affected references in the planning
content to remove the direct `root@...` access coordinates and replace them with
a sanitized pointer to the approved runbook or secret-managed access path,
keeping the surrounding phase/task text intact.

In `@apps/desktop/src-tauri/src/commands/vault.rs`:
- Around line 182-186: The root-folder publish path in the vault initialization
flow only logs the PublishResult::Conflict case and then continues, which allows
/vault/init to register root_ipns_name anyway. Update the match in the vault.rs
publish handling to fail fast on root-folder conflicts, similar to the vault-key
publish path: return an error from the initialization command when
cipherbox_api_client::PublishResult::Conflict is encountered so the registration
step is skipped. Use the existing publish_ipns and root-folder publish logic as
the place to enforce this abort behavior.

In `@crates/api-client/Cargo.toml`:
- Around line 17-18: Move the ciborium entry out of the normal dependencies for
cipherbox-api-client and into [dev-dependencies], since it is only referenced
from the #[cfg(test)] code in ipns.rs. Update Cargo.toml so runtime dependencies
stay limited to what the library needs, and keep the existing workspace-based
dependency style consistent when relocating ciborium.

In `@crates/api-client/src/ipns.rs`:
- Around line 766-788: The test bind_verified_within_skew_buffer_returns_ok is
not actually asserting the 5-minute skew boundary because it uses a far-future
validity timestamp instead of a real “now minus 2 minutes” case. Update the skew
check in ipns.rs so the boundary is tested against a controllable current time,
either by factoring out the expiry predicate or injecting now into the
validation path, and make this test assert that now - 120s is accepted while now
- 301s is rejected.

In `@crates/core/src/ipns.rs`:
- Around line 136-162: The decoder in decode_ipns_cbor_validity is accepting a
Validity field without verifying that the accompanying ValidityType is the EOL
type expected by bind_verified, which can cause non-EOL data to be treated as
valid. Update the CBOR parsing logic to require a signed ValidityType of 0
whenever Validity is present, and fail closed if ValidityType is missing,
duplicated, or any other value. Keep the duplicate-key hardening behavior and
make the check alongside the existing Validity extraction in
decode_ipns_cbor_validity so the verifier cannot interpret unsupported record
types as EOL.

In `@crates/fuse/src/replay.rs`:
- Around line 455-456: The replay flow is resolving the parent twice, which can
let metadata be merged from one verified parent CID while publishing with a
newer sequence from a later lookup. In replay.rs, update the parent resolution
path in the replay/publish logic around resolve_ipns_verified so the verified
sequence (and associated CID) from the first parent resolve is carried through
to the publish step instead of re-fetching it later. Use the existing parent
resolve result in the replay function to keep the CAS check consistent and avoid
overwriting intervening updates.

In `@crates/fuse/tests/ipns_verify_vectors.rs`:
- Around line 60-65: The vector classifier in `ipns_verify_vectors.rs` is
missing the new signed `Validity` expiry enforcement, so it can still mark
expired records as valid; update the test helper logic used for classification
to match `bind_verified` by checking the decoded IPNS CBOR `Validity` expiry
after `verify_ipns_resolve_signature` and the CID/sequence comparisons. Use the
same expiry parsing/validation path as `resolve_ipns_verified`, either by
reusing an exported helper from `cipherbox-api-client` or by duplicating the
`decode_ipns_cbor_validity` plus RFC3339 expiry check in the test until that
helper is public.

In `@packages/sdk-core/src/ipns/index.ts`:
- Around line 286-299: The IPNS validity parsing in the `verify` flow currently
relies on `new Date(validityStr)`, which accepts looser formats than Rust and
can lead to mismatched verdicts. Update the `Validity` handling to first enforce
a strict RFC3339/nanosecond UTC format before converting to epoch time, using
the existing `validityBytes`, `validityStr`, and `expiryMs` logic as the place
to add the validation. Keep the fail-closed behavior for missing or unparseable
values, but make sure only the exact timestamp shape accepted by the Rust
verifier is allowed.

In `@scripts/bench-ipns-verify.ts`:
- Around line 194-200: The benchmark cache-key simulation is using the wrong
identity shape compared with the publish path. Update the cache-hit setup in
bench-ipns-verify.ts so it matches the contract used by ipns.service.ts: use the
publish-path key format with base64(recordBytes) and the empty sequence slot,
rather than deriving the key from unmarshalled signatureV2 and TEST_SEQ. Keep
the cache simulation aligned with the same key-building logic as the real cache
to avoid skewing the benchmark.

---

Outside diff comments:
In `@crates/fuse/src/publish.rs`:
- Around line 107-119: The `resolve_sequence` handling for
`cipherbox_api_client::ipns::VerifyError::Invalid` should fail closed instead of
falling back to `get_cached`; update the match arm in `publish.rs` so
`Invalid(msg)` logs the verification failure and returns an error immediately,
while preserving cache fallback only for transient API-related errors elsewhere
in the surrounding resolve logic. Use the existing `resolve_sequence`,
`get_cached`, and `VerifyError::Invalid` symbols to keep the change scoped
correctly.

---

Nitpick comments:
In `@crates/api-client/src/ipns.rs`:
- Around line 8-9: The module doc in ipns.rs still references the removed FUSE
compatibility path, so update the top-level documentation comment to remove the
stale note about crates/fuse/src/verify.rs being a thin re-export. Keep the doc
focused on the current api-client/IPNS implementation and ensure any mention of
re-exports or deleted modules is removed from the file-level comment.

In `@crates/fuse/src/metadata.rs`:
- Around line 504-531: Update the bin first-publish comment in metadata.rs so it
matches the actual behavior in the first-publish path: the logic in
make_bin_record and is_first_bin_publish now publishes the initial bin record
with sequence 1, not 0. Keep the surrounding explanation about skipping the CAS
helper for the first publish, but revise the inline comment near
is_first_bin_publish to describe seq 1 consistently with the call that uses
make_bin_record(1).

In `@crates/fuse/src/write_ops/implementation/mkdir.rs`:
- Around line 183-199: The first-publish sequence comment in the new-folder
publish path is stale and conflicts with the current logic in mkdir.rs around
publish_ipns and coordinator.record_publish. Update the inline comment in this
branch so it correctly reflects that the initial IPNS publish uses sequence 1
and that the conflict path should describe sequence 1, not 0; keep the wording
aligned with the existing request setup and publish result handling.

In `@packages/crypto/src/__tests__/ipns-record.test.ts`:
- Around line 68-92: Add a valid-but-non-Ed25519 coverage case for
publicKeyFromIpnsName in ipns-record.test.ts so the suite exercises the
fail-closed branch, not just CID parse failure. Create a real IPNS name/PeerId
that is syntactically valid but does not inline a 32-byte Ed25519 public key,
then assert publicKeyFromIpnsName throws CryptoError for that input. Keep the
existing round-trip tests for deriveIpnsName and publicKeyFromIpnsName, and
place the new case alongside the current malformed-name test to protect the
recovery logic used by ipns-record.codec.ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bef0e64a-8af9-4717-a564-e55cc298ef22

📥 Commits

Reviewing files that changed from the base of the PR and between fbd220f and 5572627.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (65)
  • .planning/ROADMAP.md
  • .planning/STATE.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-01-PLAN.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-01-SUMMARY.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-02-PLAN.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-02-SUMMARY.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-03-PLAN.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-03-SUMMARY.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-04-PLAN.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-04-SUMMARY.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-05-PLAN.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-05-SUMMARY.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-06-PLAN.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-06-SUMMARY.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-07-PLAN.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-07-SUMMARY.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-08-PLAN.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-CONTEXT.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-PATTERNS.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-RESEARCH.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-SECURITY.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-VALIDATION.md
  • .planning/phases/60-ipns-verification-cross-layer-closeout-desktop-and-api/60-VERIFICATION.md
  • .planning/todos/pending/2026-06-24-desktop-root-folder-publish-conflict-failfast.md
  • .planning/todos/pending/2026-06-24-fuse-metadata-unpin-orphaned-cid-on-verify-fail.md
  • .planning/todos/pending/2026-06-24-replay-reuse-verified-parent-sequence.md
  • .planning/todos/pending/2026-06-24-ts-resolve-strict-rfc3339-validity-parity.md
  • apps/api/src/ipns/ipns-record.codec.ts
  • apps/api/src/ipns/ipns-verify-cache.spec.ts
  • apps/api/src/ipns/ipns-verify-cache.ts
  • apps/api/src/ipns/ipns.service.spec.ts
  • apps/api/src/ipns/ipns.service.ts
  • apps/desktop/src-tauri/src/commands/vault.rs
  • apps/desktop/src-tauri/src/fuse/prepopulate.rs
  • apps/web/src/hooks/useAuth.ts
  • apps/web/src/services/vault-settings.service.ts
  • crates/api-client/Cargo.toml
  • crates/api-client/src/ipns.rs
  • crates/core/src/ipns.rs
  • crates/fuse/src/events.rs
  • crates/fuse/src/fs.rs
  • crates/fuse/src/lib.rs
  • crates/fuse/src/metadata.rs
  • crates/fuse/src/platform/windows/write_ops.rs
  • crates/fuse/src/publish.rs
  • crates/fuse/src/replay.rs
  • crates/fuse/src/verify.rs
  • crates/fuse/src/write_ops/implementation/mkdir.rs
  • crates/fuse/tests/ipns_verify_vectors.rs
  • crates/sdk/src/registry.rs
  • crates/sdk/src/sync.rs
  • docs/CAPACITY.md
  • docs/DEVELOPMENT.md
  • packages/crypto/src/__tests__/ipns-record.test.ts
  • packages/crypto/src/index.ts
  • packages/crypto/src/ipns/derive-name.ts
  • packages/sdk-core/src/__tests__/ipns.test.ts
  • packages/sdk-core/src/__tests__/vault.test.ts
  • packages/sdk-core/src/ipns/index.ts
  • packages/sdk-core/src/vault/index.ts
  • release-please-config.json
  • scripts/bench-ipns-verify.ts
  • scripts/gen-ipns-verify-vectors.ts
  • tests/vectors/ipns/verify.json
  • tsconfig.scripts.json
💤 Files with no reviewable changes (2)
  • crates/fuse/src/verify.rs
  • crates/fuse/src/lib.rs

Comment thread apps/desktop/src-tauri/src/commands/vault.rs
Comment thread crates/api-client/Cargo.toml
Comment thread crates/core/src/ipns.rs
Comment thread crates/fuse/src/replay.rs
Comment thread crates/fuse/tests/ipns_verify_vectors.rs
Comment thread packages/sdk-core/src/ipns/index.ts
Comment thread scripts/bench-ipns-verify.ts Outdated
FSM1 and others added 6 commits June 24, 2026 17:36
The web-e2e suite (workers:1, sequential, abort-on-failure) lets a single
flaky early test (account-creation, login-to-vault timing, media-preview)
skip the whole tail. Pre-existing (predates Phase 60); not a required PR
check. Captured for separate stabilization work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 58634ae9f834
…y tests, bench key

- Move ciborium from [dependencies] to [dev-dependencies] in crates/api-client
  (used only in #[cfg(test)]; verify-path CBOR decoding delegates to cipherbox-core).
- Replace the no-op within-skew test (asserted a 2099 timestamp, would pass even
  without the buffer) with two now-relative boundary tests: now-120s accepted
  (inside the 300s buffer), now-301s rejected as expired. Adds a test-only
  secs_to_rfc3339 helper that round-trips through the production parser.
- Align bench-ipns-verify.ts cache-key simulation with the real D-11 identity
  `${ipnsName}:${''}:${base64(recordBytes)}` (empty seq slot, full record bytes),
  not a signatureV2-based key.

No production behavior change. cargo test -p cipherbox-api-client 20/20; scripts typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: c7010ab27935
CodeRabbit web review on PR #555 surfaced two cross-layer / out-of-scope items
deferred from the strict-cutover PR: (1) enforce ValidityType==0 in the IPNS
verifier + keep cross-language vectors in expiry lockstep (Rust+TS+vectors, must
stay in parity), (2) repo-wide scrub of the staging SSH host from tracked docs
(pre-existing across ~10 docs; key-gated access, no credentials).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 628152972d0b
The full CI E2E dispatch on 1f8f8d8 failed on two pre-existing flakes, not
regressions: web media-preview (browser-closed crash) and desktop macOS
cross-client-sync (FUSE-T SMB-cache vs 120s window; macOS only). Zero
production-code delta since the prior passing desktop run (88f0965) — the
only Rust change is skew boundary TESTS inside mod tests. Documented both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 9924b61bfea1
Proven pre-existing: the identical Test 5 failure occurred on main (541e4c6,
run 28043695361, pre-Phase-60). Failure is a re-resolution STALL (detected but
never drained on macOS FUSE-T), not slowness — a timeout bump would not fix it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 5147b4572eaa
4-agent root-cause: stall is fs.rs:392 drain_refresh_completions cache-only continue
when root is in mutated_folders/publish_queue, suppressing the per-file re-resolution
spawn. NOT Phase 60: fs.rs diff 4ins/13del (resolve re-point only); resolve_ipns_verified
never called for the stuck file; record strict-verifiable; fails ~15% on main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 405bde2adf2c
@FSM1 FSM1 merged commit 03209e3 into main Jun 24, 2026
30 checks passed
@FSM1 FSM1 deleted the feat/ipns-verification-cross-layer-closeout-desktop-and-api branch June 24, 2026 19:47
FSM1 added a commit that referenced this pull request Jun 25, 2026
The three todos folded into Phase 60 (IPNS verification cross-layer
closeout) are fully delivered and merged in #555:

- Stream A: strict-equality first-publish cutover
- Stream B: desktop/SDK verified-resolve coverage
- Stream C: API hot-path verify caching

Move them pending/ -> completed/ to reflect delivered scope. The only
remaining Phase 60 item is the operational staging-wipe smoke test
(60-VERIFICATION.md, human_needed), which is not part of these todos.


Claude-Session: https://claude.ai/code/session_01SDNQVLoAw4DbPrPvtQPobh
Entire-Checkpoint: 4bfabb13c6e1

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
FSM1 added a commit that referenced this pull request Jun 25, 2026
Test 5 of test-cross-client-sync.sh flakes (~15%) on macOS only. Pinned the root
cause definitively via a live local FUSE-T mount under RUST_LOG=debug: the desktop
FUSE layer is correct -- it detects the remote edit and re-resolves the FilePointer
to the new CID within ~5s -- but the macOS SMB client caches the file content above
FUSE-T and never re-calls FUSE `open` within the window, serving the stale read. The
mount is `nonotification` and FUSE-T's SMB backend does NOT honor a FUSE
`inval_inode` reverse-notification (verified experimentally), so there is no reliable
FUSE-side fix; the SMB cache TTL is variable and intermittently exceeds the 120s
budget.

Downgrade the content-sync leg to optional/warn on macOS only -- mirroring the Test 7
folder-rename leg, which already does this for the same SMB cache reason. Linux still
enforces it; the Windows .ps1 still enforces it. This unblocks the CI E2E gate (the
flake is platform-limited, not a sync regression). PR #555/#558's re-resolution work
stays in as legitimate hardening.

Also corrects the related todo's earlier (wrong) "resolved by #558" note.


Claude-Session: https://claude.ai/code/session_01SDNQVLoAw4DbPrPvtQPobh
Entire-Checkpoint: e127607fee30

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
FSM1 added a commit that referenced this pull request Jun 26, 2026
…top vault init (#566)

* fix(fuse): return Err instead of inventing CAS base 0 on legacy sequence parse failure

The non-strict `PublishCoordinator::resolve_sequence` cache-fallback contract
must never fabricate a base sequence: on a resolve failure it returns
`Ok(cached)` only when a cache entry exists, else `Err`. The strict cutover in
#555 already removed the old `Legacy { sequence_number }` parse-from-string path
(the source of the invented base-0 fallback CodeRabbit flagged on #553), so the
source already behaves correctly. This adds the regression coverage the prior
fix lacked:

- resolve_sequence_falls_back_to_cache_on_resolve_failure (cache present -> Ok(cached))
- resolve_sequence_errs_on_resolve_failure_without_cache (no cache -> Err)

Both drive the failure via an unroutable 127.0.0.1:1 API (mirrors the existing
strict_resolve_bypasses_cache test), so they run under --features fuse/winfsp in CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDNQVLoAw4DbPrPvtQPobh

* fix(desktop): fail fast on root-folder publish conflict during vault init

The root-folder publish Conflict arm in `initialize_vault` only logged a warning
and fell through to `/vault/init`, unlike the analogous vault-key conflict arm
which fails fast. An unexpected conflict on the first root-folder publish
(sequence 1) was silently swallowed and initialization continued on inconsistent
state. Mirror the vault-key arm: return Err and abort init.

CodeRabbit finding F10 from the Phase 60 ship review; pre-existing, deferred from
the strict-cutover phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDNQVLoAw4DbPrPvtQPobh

* fix(fuse): unpin orphaned blob when verified merge fails closed

In `spawn_metadata_publish`, the new metadata blob (`new_cid`) is uploaded and
server-pinned before the verified-resolve/merge step. The Success and
persistent-Conflict exits both unpin it, but the `VerifyError::Invalid`
fail-closed branch returned early without unpinning, stranding an orphaned pinned
blob on every retry of a verification failure. Add a best-effort
`unpin_content(&api, &new_cid)` before the early Err return, matching the
error-logged-not-propagated pattern of the other exits.

CodeRabbit finding F11 from the Phase 60 ship review; storage-cleanup leak,
deferred from the strict-cutover phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDNQVLoAw4DbPrPvtQPobh

* fix(fuse): unpin orphaned blob in the verify Api arm too

The VerifyError::Api arm of spawn_metadata_publish returned without
unpinning the pre-uploaded new_cid, unlike the sibling VerifyError::Invalid
arm fixed in this PR. A transient resolve API failure after the conflict
branch leaks the same orphaned blob the Invalid-arm cleanup prevents.
Mirror the Invalid arm: best-effort unpin_content before the early Err.

CodeRabbit outside-diff finding on this PR; one-line mirror of the
existing fail-closed cleanup pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDNQVLoAw4DbPrPvtQPobh

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:api:feat Minor version bump (new feature) for api release:cipherbox-core:feat Minor version bump (new feature) for cipherbox-core release:cipherbox-fuse:feat Minor version bump (new feature) for cipherbox-fuse release:cipherbox-sdk:feat Minor version bump (new feature) for cipherbox-sdk release:core:fix Patch version bump (bug fix) for core release:crypto:fix Patch version bump (bug fix) for crypto release:desktop:feat Minor version bump (new feature) for desktop release:sdk:fix Patch version bump (bug fix) for sdk release:sdk-core:feat Minor version bump (new feature) for sdk-core release:tee-worker:fix Patch version bump (bug fix) for tee-worker release:web:feat Minor version bump (new feature) for web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant