feat(sdk): self-bootstrap folder tree from root IPNS key#498
Conversation
… root IPNS key Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 489e091e0cc7
The SDK client owned folder state (folderTree) but its config carried only rootIpnsName + rootFolderKey, not the root IPNS signing key. So the client could not resolve/publish root or lazy-load folders on its own, and the web app had to seed folderTree via ensureFolderRegistered before every folderTree-dependent method. That asymmetry is the root cause of the whole "Folder not loaded" class — most visibly, restoring a recycle-bin item after a reload threw 'Target folder not loaded' when the item's subfolder parent was never navigated to that session (pre-existing since #296; recycle-bin.spec.ts misses it because it uploads in-session and never reloads). Changes: - Add optional rootIpnsKeypair to CipherBoxClientConfig; defensively copied in the constructor and zeroed in destroy(), mirroring vaultKeypair/rootFolderKey. - Add CipherBoxClient.ensureFolderLoaded(target): when a root IPNS keypair is configured, walk the folder tree from root (DFS, early exit), unwrapping each subfolder's folderKeyEncrypted/ipnsPrivateKeyEncrypted with the vault keypair and loading metadata until the target is registered. Every folder on the path is cached. A corrupt sibling entry is skipped, not fatal. - Wire ensureFolderLoaded as a self-heal fallback into every folderTree-dependent method (create/rename/move/delete/upload(s)/replace/version ops/share) and the bin deleteToBin/restoreFromBin wrappers. When no rootIpnsKeypair is configured it returns null before any crypto, preserving the prior 'Folder not loaded' behavior. - Web: pass vaultStore.rootIpnsKeypair into the SDK config at initSdkClient (already guaranteed non-null by the existing guard). Security: no new key exposure — the client already holds vaultKeypair.privateKey, rootFolderKey, and every loaded folder's ipnsPrivateKey in memory (zeroed on destroy). Reviewed: no key transposition, cycles bounded by a visited set, errors leak no key bytes. Tests: - SDK unit: ensureFolderLoaded — already-loaded no-op, no-keypair null, root-as-target, deep walk, unreachable target, skip unresolvable/corrupt sibling. - Web E2E: restore a subfolder item after reload without navigating into the subfolder (the scenario recycle-bin.spec.ts misses). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 78b22528e949
/simplify cleanup of the self-bootstrap change. Behavior-preserving: - Collapse the 11 duplicated `folderTree.get(x) ?? ensureFolderLoaded(x); if (!folder) throw 'X not loaded'` blocks into one private requireFolder(ipnsName, label) helper. Mutations and the two bin wrappers now route through it, so the get-or-self-load-or-throw contract lives in one place and a new method can't silently drop the self-heal fallback (the #494 drift class). - Drop the redundant `as FolderEntry` cast — the `type === 'folder'` guard already narrows the discriminated union. - Simplify the root load (single `?? await loadFolder` expression) and the constructor's rootIpnsKeypair assignment (`?? undefined` vs conditional spread). - Drop a brittle unwrapKey call-count assertion from the deep-walk test; the hasFolder assertions already cover path registration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 6c13c23c3e2b
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
Walkthrough
ChangesSDK Client Self-Bootstrap
Sequence Diagram(s)sequenceDiagram
participant useAuth
participant CipherBoxClient
participant ensureFolderLoaded
participant loadFolderMetadata
participant unwrapKey
useAuth->>CipherBoxClient: new CipherBoxClient({ rootIpnsKeypair, ... })
note over CipherBoxClient: stores internalRootIpnsKeypair
CipherBoxClient->>ensureFolderLoaded: restoreFromBin(targetFolderIpnsName)
ensureFolderLoaded->>CipherBoxClient: folderTree.get(target) → miss
ensureFolderLoaded->>loadFolderMetadata: rootIpnsName
loadFolderMetadata-->>ensureFolderLoaded: root FolderMetadata (children list)
loop each subfolder
ensureFolderLoaded->>unwrapKey: encryptedKey, vaultPrivateKey
unwrapKey-->>ensureFolderLoaded: folderKey
ensureFolderLoaded->>loadFolderMetadata: subfolder.ipnsName
loadFolderMetadata-->>ensureFolderLoaded: FolderMetadata
ensureFolderLoaded->>CipherBoxClient: folderTree.register(subfolder)
end
ensureFolderLoaded-->>CipherBoxClient: FolderState (target found)
CipherBoxClient-->>useAuth: restore operation proceeds
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Release Preview
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #498 +/- ##
==========================================
+ Coverage 63.59% 63.80% +0.21%
==========================================
Files 140 140
Lines 10577 10633 +56
Branches 1157 1175 +18
==========================================
+ Hits 6726 6784 +58
+ Misses 3612 3610 -2
Partials 239 239
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Delete ensureFolderRegistered and all 14 call sites across useDropUpload, useFileOperations, useFileVersions, and useFolderMutations. The SDK requireFolder chokepoint (PR #498) now self-bootstraps folders from the root IPNS key on first mutation, making these web-side pre-seed calls redundant. Also removes the unused FolderNode import from sdk-provider.ts. useFolderNavigation.ts requires no change: the key-unwrap there serves the display/metadata-load path, not SDK seeding. REQ-2 (#9). Typecheck and lint green.
Delete ensureFolderRegistered and all 14 call sites across useDropUpload, useFileOperations, useFileVersions, and useFolderMutations. The SDK requireFolder chokepoint (PR #498) now self-bootstraps folders from the root IPNS key on first mutation, making these web-side pre-seed calls redundant. Also removes the unused FolderNode import from sdk-provider.ts. useFolderNavigation.ts requires no change: the key-unwrap there serves the display/metadata-load path, not SDK seeding. REQ-2 (#9). Typecheck and lint green.
…ame at rest (#500) * docs: add phase 48 — SDK self-bootstrap regression fix and shared-folder consolidation Bundles three pending todos onto one branch plus the PR #498 web-e2e regression: - REQ-1 (P0): fix self-bootstrap loadFolder clobbering fresher folderTree state with a stale IPNS snapshot (regressed main web-e2e, run 27587113911) - REQ-2 (#9): remove redundant web ensureFolderRegistered seeding + useFolderNavigation unwrap, gated on REQ-1 - REQ-3 (#8): route shared-folder writes through the SDK client - REQ-4 (#5 / Phase-14 M1): encrypt share itemName at rest via ECIES Defers CRDT-IPNS-inbox research (#2). REQ-1 acceptance: pre-merge web-e2e via 'gh workflow run web-e2e.yml --ref <branch>'. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: f826ffa63025 * docs(48): research phase domain Entire-Checkpoint: ed421ad6df8c * docs(48): add context and validation strategy Locks REQ-3 (sibling sharedFolderTree by shareId) and the two REQ-4 policy decisions: A2=lazy client backfill of legacy plaintext itemName, A3=include the invite flow. Adds Nyquist VALIDATION.md (REQ-1 acceptance = pre-merge web-e2e dispatch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: a1ae21343869 * docs(48): create phase plan (6 plans, 3 waves) Entire-Checkpoint: 9b36cdf380e5 * docs(48): add pattern map Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 66a841737a7a * test(48-01): add RED reconcile guard test for loadFolder - Three cases: keep-fresher (RED), absent-loads (green), older-overwritten (green) - Test A confirms the bug: existing sequenceNumber=5n clobbered by stale snapshot=3n - Uses bigint literals and vitest *.test.ts convention per memory Entire-Checkpoint: ebe50165e85d * feat(48-01): add sequence guard to loadFolder to prevent stale clobber - Read existing folderTree entry after IPNS resolve - If existing.sequenceNumber >= result.sequenceNumber, keep existing and skip set() - Emits folder:loaded with existing children/sequenceNumber on the guard path - Absent folders still resolve and set normally (no #498 regression) - Fixes P0: bin-restore and version-restore regressed in main web-e2e run 27587113911 Entire-Checkpoint: 035cb28ff942 * docs(48-01): SUMMARY and STATE for plan 01 checkpoint Tasks 1-2 complete (RED+GREEN); awaiting Task 3 PRE-MERGE web-e2e gate. Entire-Checkpoint: c531516e1121 * fix(48-01): re-resolve bin before auto-repair to prevent empty clobber loadBin treated a null resolveIpnsRecord as 'bin empty' and published an empty record with no expectedSequenceNumber, so a transient cold-cache null after page.reload (with two concurrent loadBin calls) destroyed the real bin record server-side — the deleted item vanished from the bin after reload (bin-restore-after-reload.spec.ts step 6). Re-resolve before the destructive empty publish, and re-resolve after publishing to adopt any real higher-sequence record. SDK-only; 183 SDK tests pass. Unsigned: 1Password SSH signer wedged mid-run (AFK); needs re-sign or admin-merge before landing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 23d236999200 * fix(48-01): guard folder-nav redirect so a stale subfolder resolve cannot bounce an off-route user A subfolder resolve that fails after the user has navigated to another route force-navigated to /files via the catch block, guarded only by latestNavTarget. A react-router nav to /bin does not update that ref, so the redirect bounced the user off /bin back to /files. Add a route-still-here guard using the live pathname before the redirect fires. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 5ef54e2e74a7 * docs(48): REQ-1 gate blocked handoff - bin-restore-after-reload still red Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 8c2131ef9dab * docs(48): correct REQ-1 root cause from trace - bin IPNS 404 auto-repair clobber, not nav Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 86e784543056 * fix(48-01): stop loadBin from clobbering the bin with an empty auto-repair publish loadBin previously published an empty bin record on a double-null resolve via saveBinMetadata with no expectedSequenceNumber; the API then unconditionally incremented the sequence and overwrote latestCid with the empty-bin CID, wiping the real bin after delete+reload (two concurrent loadBin calls could both clobber). The API /ipns/resolve already DB-cache-falls-back, so a client-side null means the record is genuinely absent at that instant - publishing empty on read is never safe. Changes (all client-side, no API change): - loadBin: remove the destructive empty-bin auto-repair publish. On no record, return an in-memory empty BinState (entries=[], sequenceNumber=0) WITHOUT publishing. The first addToBin lazily creates the real record at seq 0+1=1, matching the API create-path. Added a bounded retry (6x ~500ms) to convert a transient null into a successful load. - deleteToBin: self-heal by lazily loading the bin when binState is null. Bin init is fire-and-forget on login, so a delete soon after login/reload previously threw BinNotLoadedError and the web fell back to a HARD delete - the item vanished instead of moving to the bin. This was the actual cause of recycle-bin failures. - client.loadBin: anti-clobber guard - the in-memory empty fallback never overwrites or broadcasts over an already-loaded non-empty bin. Tests: invert the old bin.test that asserted the destructive empty publish to assert loadBin does NOT publish on null; keep the does-not-clobber test; add deleteToBin self-heal + loadBin anti-clobber tests. Full SDK suite green; recycle-bin.spec E2E green 8/8. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 5869de8fb5a5 * fix(48-01): keep isLoading true during session restore to stop protected-route login bounce After a reload, CoreKit re-initializes (coreKitInitialized flips true) a beat before restoreSession sets isLoggingIn/isAuthenticated, leaving a transient where !isLoading && !isAuthenticated are both true. A protected route mounting in that gap (opening /bin right after a reload) hit BinPage's auth guard and redirected /bin -> / -> /files, so the bin never rendered (bin-restore-after-reload.spec.ts step 6). Including (coreKitLoggedIn && !isAuthenticated) in isLoading keeps it true until restore resolves; a genuinely logged-out user has coreKitLoggedIn false so the login redirect still fires. Verified locally: the spec passes end-to-end under NODE_ENV=test (2x). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 8892055ce642 * refactor: remove web folder-seeding now that SDK self-bootstraps Delete ensureFolderRegistered and all 14 call sites across useDropUpload, useFileOperations, useFileVersions, and useFolderMutations. The SDK requireFolder chokepoint (PR #498) now self-bootstraps folders from the root IPNS key on first mutation, making these web-side pre-seed calls redundant. Also removes the unused FolderNode import from sdk-provider.ts. useFolderNavigation.ts requires no change: the key-unwrap there serves the display/metadata-load path, not SDK seeding. REQ-2 (#9). Typecheck and lint green. * docs(48-02): complete REQ-2 web folder-seeding removal plan Entire-Checkpoint: 3cc6e6139aaf * feat: add shared-folder state contracts to SDK - Add SharedFolderState type carrying share-context fields - Add SharedFolderTree keyed by shareId with key-zeroing on delete/clear - Add sharedFolder:updated event member to SdkEvent union Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test: add shared-folder tree isolation and client shared-write tests - SharedFolderTree per-share isolation + key-zeroing (GREEN) - client.uploadToSharedFolder read/delegate/write-back/emit contract (RED) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: SDK client owns shared-folder state and publish - Add sharedFolderTree field + loadSharedFolder/hasSharedFolder/getSharedFolderState/unloadSharedFolder - Add five shared write methods delegating to share/shared-write.ts via publishWithCas - Each method reads state, builds context, writes back, emits sharedFolder:updated - Export SharedFolderState and SharedFolderTree from index Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: complete 48-03 shared-folder SDK ownership plan Entire-Checkpoint: dc7607abe28a * feat: encrypt share itemName at rest via additive item_name_encrypted column - Add additive nullable item_name_encrypted bytea to shares and share_invites via migration EncryptShareItemName1749200000000 (no data UPDATE; server is zero-knowledge and cannot re-encrypt legacy plaintext rows) - Add itemNameEncrypted entity columns plus hex-validated optional DTO fields on create-share, create-invite, claim-invite and the response DTOs - Persist client-supplied ECIES ciphertext in createShare, createInvite and the invite-claim path; server never encrypts (REQ-4 decision A3) - Extend ECIES round-trip test for UTF-8 itemName and add ciphertext-persist plus no-server-encrypt service assertions - Regenerate @cipherbox/api-client to expose itemNameEncrypted Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: complete 48-05 REQ-4 API itemName encryption plan Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: c873f80a93e3 * feat(web): route shared-folder writes through SDK with event-fed projection - Add shared-folder-projection helpers seedSharedFolder + subscribeSharedFolderProjection - Seed SDK sharedFolderTree on share/subfolder entry and up/breadcrumb depth restore - Subscribe useSharedNavigation refs to sharedFolder:updated as projection-only state - Route all five useSharedWriteOps handlers through getSdkClient shared methods, no write-back - Drop withConflictRetry and buildSharedWriteContext ceremony; SDK owns CAS retry - Add projection unit tests proving per-share isolation and reads-nothing-back Implements REQ-3 web side (phase 48) * docs(48-04): complete shared-folder write projection plan Entire-Checkpoint: fcf515ad5df0 * test(48-06): add failing test for share itemName decrypt and backfill helpers * docs(48-07): plan SDK-owned shared-folder refresh and poller consolidation Entire-Checkpoint: 9f0cd23de3e3 * feat(48-06): ECIES-wrap share itemName on create and decrypt for display - Wrap itemName with recipient pubkey in ShareDialog before createShare; send ciphertext-only (empty plaintext) - Wrap itemName with ephemeral pubkey on invite-create; re-wrap for recipient on claim (decision A3) - Decrypt itemNameEncrypted into the plaintext store projection on received-share load; display sites unchanged - Add decryptItemName + shouldBackfill helpers and lazy-backfill pass for legacy rows (decision A2) * docs(48-06): complete REQ-4 web itemName encryption plan Entire-Checkpoint: 102cf857992f * feat(48-07): add SDK client.refreshSharedFolder for shared-folder re-resolve - refreshSharedFolder(shareId) re-resolves IPNS via sdkCore.loadFolderMetadata - applies the #489 sequence-guard: stale/equal resolve re-emits existing state - adopts a newer sequence into sharedFolderTree and emits sharedFolder:updated - throws Shared folder not loaded on an unloaded shareId - TDD-covered: newer adopt, stale no-clobber, null no-op, unloaded throw * refactor(48-07): route shared poller through SDK refreshSharedFolder - 30s poller calls getSdkClient().refreshSharedFolder(currentShareId) - remove inline refreshFolderContents IPNS resolve + IPFS fetch + decrypt - drop now-unused resolveIpnsRecord/fetchFromIpfs/decryptFolderMetadata imports - projection subscription is now the sole ref writer on write AND poll paths - revocation check unchanged * test(48-07): cover poll-through-SDK refreshSharedFolder projection path - add refreshSharedFolder to the fake SharedFolderClient recording the call - assert the poller calls refreshSharedFolder with the active shareId - assert refs update ONLY via the sharedFolder:updated subscription, not the call - assert a mismatched-shareId refresh event is ignored per-share isolation * docs(48-07): summarize SDK-owned shared-folder refresh and poller consolidation - record Tasks 1-4 commits, verify results, and dist-rebuild confirmation - Task 5 shared-folder sync UAT deferred to end-of-phase web-e2e * fix(web): seed SDK shared-folder state when opening a write-shared file The recipient save path routes through client.updateSharedFile (REQ-3, 48-04), which calls requireSharedFolder and throws 'Shared folder not loaded' when the share is absent from the SDK sharedFolderTree. The folder-open branch seeds the tree, but the standalone file-share branch did not — so a recipient's Ctrl+S on a write-shared file threw, the editor never closed, and writable-shares.spec.ts '10.3 Bob opens the write-shared file and can edit it' timed out at waitFor hidden. Seed the file branch (children: [], sequenceNumber: 0n; updateSharedFile is a file-only publish that reads folderKey + owner/recipient pubkeys only and ignores children/sequence). Only write shares (IPNS key present) seed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: c89f6e96802a * fix(api): allow empty itemName on create-invite to match create-share REQ-4 (48-06) moved the share display name into the ECIES itemNameEncrypted ciphertext and changed the web client to POST itemName: '' on create. The sibling CreateShareDto was relaxed to accept this, but CreateInviteDto kept @minlength(1) on itemName, so the global ValidationPipe (whitelist + forbidNonWhitelisted) rejected invite-link creation with HTTP 400 — failing invite-link-workflow.spec.ts '3.1 Alice creates an invite link for a file' at waitForSuccess. Drop @minlength(1) to mirror CreateShareDto. No OpenAPI/client change: the Nest swagger plugin does not emit minLength, and the generated client source is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: e0689f5cec4f * refactor(48): drop dead shared-write folderKey prop, redundant 0x-strip, stale doc Simplify-pass cleanups (no behavior change): - useSharedWriteOps: remove the unused folderKey param (the SDK owns the folder key via seeded sharedFolderTree state) and its call-site wiring. - shared-folder-projection.parsePublicKey: drop the redundant 0x-strip; hexToBytes already strips an optional 0x prefix. - client.ts getFolderIpnsPrivateKey: fix a doc comment that referenced ensureFolderRegistered, which this branch deleted. Deferred (noted, out of scope for this PR): SharedFolderTree is keyed by shareId so the web re-seeds per nav depth (deeper fix = a shared ensureFolderLoaded analog); serial DFS in ensureFolderLoaded; pre-existing share-key cache bypass in updateSharedFileHandler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 5cdb4df5efb2 * test(48-05): cover invite/claim itemNameEncrypted ciphertext persistence Add 3 behavioral tests to share-invite.service.spec.ts: - createInvite: persists hex ciphertext as Buffer passthrough (no server encryption) - createInvite: persists null for legacy clients that omit itemNameEncrypted - claimInvite: re-wrapped itemNameEncrypted from DTO is stored as Buffer on the Share Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Entire-Checkpoint: a49ce185baba * docs(48): mark phase nyquist-compliant after filling invite ciphertext test gap Update 48-VALIDATION.md: real per-task verification map (REQ-1..4 all green via unit + web-e2e), Wave 0 checked, sign-off approved, audit trail (1 gap found, 1 resolved). The gap — invite/claim itemNameEncrypted persistence — was closed by share-invite.service.spec.ts (commit e8a3a2fe0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 42e4f8b5b5a4 * fix(api): widen itemNameEncrypted MaxLength to 2500 for worst-case names The 1024-char cap was copied from the fixed-size encryptedKey field, but itemNameEncrypted wraps a variable-length display name: a 255-char multibyte name (CJK/emoji) is up to ~765 UTF-8 bytes, whose ECIES ciphertext is ~1724 hex chars — exceeding 1024 and triggering a 400 (same class as the create-invite MinLength regression). Raise the cap to 2500 across create-share, create-invite, and claim-invite. No OpenAPI/client change (the swagger plugin does not emit maxLength). Flagged by CodeRabbit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: aa221dedc616 * fix(web): harden shared itemName decrypt and unload SDK keys on unmount Two review findings on the REQ-3/REQ-4 shared paths: - fetchReceivedShares decrypted itemName inside Promise.all, so one corrupt / wrong-key / truncated ciphertext row rejected the WHOLE received-shares page. Guard per row, degrading just that row to its plaintext fallback (security review, MEDIUM availability). - The sharedFolder:updated projection effect only unsubscribed on unmount; it never unloaded the SDK's cloned shared-folder state, so folderKey + ipnsPrivateKey lingered in SDK memory after leaving a share. Call unloadSharedFolder(activeShareId) in cleanup to zero them (CodeRabbit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 19d891c11b3e * docs(48): add phase 48 security review report Two-agent security review of the REQ-4 itemName at-rest encryption and REQ-3 shared-folder key handling. Overall risk LOW: zero-knowledge invariant holds (server stores ciphertext verbatim, never encrypts/decrypts), crypto correct, key-zeroing + cross-share isolation hold. One MEDIUM (received-shares decrypt guard) fixed; LOW items deferred with rationale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 1a497a44e1ab * chore: sync Cargo.lock to crate versions 0.6.0 for fuse and sdk The cipherbox-fuse and cipherbox-sdk crate Cargo.toml files are at 0.6.0 but the committed lockfile still pinned 0.5.3 / 0.5.0. Regenerate the lock to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 17ebf9d4ebfa * chore(release): set release targets for PR #500 * test(48): cover itemNameEncrypted present/null branches in invite controllers The new itemNameEncrypted hex-or-null ternaries in share-invites.controller and invites.controller added per-file branches whose present-vs-null sides were not both exercised, dropping branch coverage below threshold (63.88% < 65% and 73.33% < 74%). Add explicit tests for the present (Buffer to hex string) and absent (null) branches in createInvite, listInvites, and getInviteData, and set itemNameEncrypted on the base mocks. Both files now clear their branch thresholds (69.44% and 76.66%). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: a5866cb6aa14 * test(48): update sdk-e2e bin suite to the self-healing bin contract Commit 73c9f03 intentionally changed the bin contract: loadBin returns an in-memory empty state at sequenceNumber 0 WITHOUT publishing (publishing an empty record on a null resolve is destructive), and the client bin methods self-heal or lazy-load instead of throwing BinNotLoadedError. The sdk-e2e suite still asserted the old contract. bin-operations.test.ts: - fresh-account loadBin now expects sequenceNumber 0 (was 1). - "BinNotLoadedError when bin not loaded" becomes a self-heal test: deleteToBin lazily loads the bin and self-bootstraps the root, then fails only on the missing child with 'Item not found'. error-cases.test.ts (bin ops without an explicit loadBin): - deleteToBin self-heals and fails with 'Item not found' (not BinNotLoadedError). - restoreFromBin / permanentDelete reach the entry lookup (binState lazily loaded empty by the prior deleteToBin) and fail with 'Bin entry not found'. - emptyBin resolves on the already-loaded empty bin. Verified against the source-of-truth unit tests (packages/sdk/src/__tests__/ bin.test.ts, green) and the client self-heal logic in client.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 7b06e5dedd10 * fix(sdk): guard shared-folder adopt and emit against unload race, zero stale keys Two CodeRabbit findings on the SDK shared-folder state: - adoptSharedFolderResult and the updateSharedFile / refreshSharedFolder emit sites used a pre-await state snapshot, so a write/refresh completing AFTER unloadSharedFolder(shareId) (now triggered on unmount) could resurrect the unloaded entry and emit sharedFolder:updated for it. Re-read live state right before write-back/emit and no-op if absent; callers pass shareId. - SharedFolderTree.set now zeroes the previous entry's folderKey/ipnsPrivateKey before overwriting so stale key material doesn't linger across re-seeds — guarded on reference identity so the adopt path (which spreads the live entry's same buffers) never zeroes the key it is about to clone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 45d6a1f9e98b * fix(web): disable no-op itemName backfill and wipe plaintext name buffers Two CodeRabbit findings on the REQ-4 share itemName paths: - backfillSentShareItemNames re-wrapped every legacy row via ECIES on each owner share-list load but cannot persist (no PATCH endpoint, T-48-18) — wasted CPU with no durable effect. Disable the invocation until the update endpoint exists; the function is kept ready for one-line wiring. - ShareDialog and invite.service encoded the plaintext display name into a Uint8Array and wrapped it without clearing the temporary bytes. Wrap in try/finally and fill(0) the encoded buffer, per the clear-sensitive-data rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: d9b1db6d1ed0 --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Summary
The SDK client owned folder state (
folderTree) but its config carried onlyrootIpnsName+rootFolderKey, not the root IPNS signing key. So the client could not resolve/publish root or lazy-load folders on its own — the web app had to pre-seedfolderTreeviaensureFolderRegisteredbefore every folderTree-dependent method. That asymmetry is the root cause of the whole "Folder not loaded" class.Most visibly: restoring a recycle-bin item after a reload threw
Target folder not loadedwhen the item's subfolder parent was never navigated to that session (only root is re-seeded on reload). Pre-existing since#296;recycle-bin.spec.tsmisses it because it uploads in-session and never reloads.This PR gives the client the root IPNS keypair so it can self-bootstrap: walk the folder tree from root, unwrapping each subfolder's keys with the vault keypair, until the target is loaded.
Changes
CipherBoxClientConfig.rootIpnsKeypair(optional, Ed25519) — defensively copied in the constructor and zeroed indestroy(), mirroringvaultKeypair/rootFolderKey.CipherBoxClient.ensureFolderLoaded(target)— when a root keypair is configured, DFS-walks from root (early exit), ECIES-unwrapping each subfolder'sfolderKeyEncrypted/ipnsPrivateKeyEncryptedand loading metadata until the target is registered. Caches the whole path; a corrupt sibling entry is skipped, not fatal. Returnsnullbefore any crypto when no keypair is configured, preserving the priorFolder not loadedbehavior.requireFolder(ipnsName, label)chokepoint — the 11 folderTree-dependent mutations (create/rename/move/delete/upload(s)/replace/version ops/share) and the 2 bin wrappers (deleteToBin/restoreFromBin) all route through one get-or-self-load-or-throw helper, so a new method can't silently drop the self-heal fallback (the#494drift class).initSdkClientnow passesvaultStore.rootIpnsKeypair(already guaranteed non-null by the existing guard).Security
No new crypto primitives — only ECIES-unwraps existing keys and loads metadata via the unchanged sdk-core AES-256-GCM path. No key logging, no new storage, no server transmission. Reviewed (independent
security-reviewerpass + CodeRabbit, both clean): no key transposition, correct ECIES recipient key, cycles bounded by avisitedset, errors leak no key bytes,destroy()zeroization consistent with the existing pattern. The root IPNS key already lived client-side invaultStore; this only hands a defensive copy to the SDK, which already holds equivalent key material.Testing
ensure-folder-loaded.test.ts(7 cases): already-loaded no-op, no-keypair null, root-as-target, deep walk + path caching, unreachable target, skip-no-IPNS-record, skip-corrupt-sibling. Full SDK suite green (179 passing, 3 live-API skipped under CI).bin-restore-after-reload.spec.ts: restore a subfolder item after reload without navigating into the subfolder (the scenariorecycle-bin.spec.tsmisses). Note: web-e2e runs only on main-push.pnpm typecheckclean (full chain + webtsc -b); ESLint clean.Follow-ups (not in this PR)
ensureFolderRegisteredcalls (~16 sites) are now redundant no-ops (the SDK self-heals) and the webuseFolderNavigationunwrap path duplicates the SDK's — both can be deleted once self-heal proves out.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests