Skip to content

feat(web): shared-folder intra-share move and useFolderNavigation consolidation#509

Merged
FSM1 merged 39 commits into
mainfrom
feat/shared-folder-move-intra-share-and-usefoldernavigation-unwra
Jun 18, 2026
Merged

feat(web): shared-folder intra-share move and useFolderNavigation consolidation#509
FSM1 merged 39 commits into
mainfrom
feat/shared-folder-move-intra-share-and-usefoldernavigation-unwra

Conversation

@FSM1

@FSM1 FSM1 commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 49 lets a write-permission share recipient move a file between subfolders within a single shared folder, re-encrypting the file's FileMetadata from the source subfolder's folderKey to the destination's so it stays decryptable for owner and recipient after the move. It also consolidates the web useFolderNavigation key-unwrap onto the SDK and brings the shared view to batch + drag move parity with the private vault.

Important

Scope grew during review. Getting the move to work for owner-created destination subfolders surfaced a cross-layer gap, and the fix became a deliberate, owner-approved re-architecture of IPNS publishing. This PR now also contains:

  1. Write-key distribution — a read-write folder share now hands the recipient the IPNS signing keys for owner-created descendants (not just read keys).
  2. Decentralized, signature-gated IPNS — the folder_ipns cache is keyed by ipnsName (not per-user) and publishes are authorized by record signature (key possession), not a server-side share check.

These were folded in here (rather than a separate PR) by request, to avoid a rebase. See the two sections below.

What's included (Phase 49)

  • REQ-1/2 — SDK shared-subtree enumeration + move op: enumerateSharedSubtree (lazy DFS, per-node writability) and stateless moveInSharedFolder (publish DEST → re-key FileMetadata via reencryptFileMetadataForFolderChange → publish SOURCE → adopt → emit; temp keys zeroed).
  • REQ-3 — web single-item move: moveItemHandler, a shared subtree MoveDialog, onMove wired into the folder-view context menu.
  • REQ-4 — useFolderNavigation consolidation: replaced the hand-rolled ECIES unwrap + IPNS-resolve + decrypt with client.ensureFolderLoaded, preserving the IPNS-propagation retry and cloning SDK-owned key buffers.
  • REQ-5 — e2e: two-account within-share move + decrypt-survival spec (shared-folder-move.spec.ts), asserting content decrypts via the TextEditor read path for both Bob and Alice after sync.
  • REQ-6 — batch + drag parity: multi-select + SelectionActionBar, batch handler looping moveInSharedFolder, MoveDialog items prop, internal drag-and-drop onto SharedFolderRow.
  • Regression fix (4bb181995): the shared batch action bar was gated on selectedIds.size > 0 and rendered above the list, so a plain single click (the first click of a double-click) popped it in and shifted the rows mid-double-click — silently breaking folder navigation (writable-shares 8.2). Gated on size > 1, mirroring the private vault.

Added: write-key distribution for shared descendants (1be0dca11)

collectChildKeys previously re-wrapped only read keys (folder/file) for a folder share's descendants — never the IPNS signing keys — so a write-share recipient could only write to items they created themselves, and moving a file into an owner-created subfolder was impossible.

  • On a write share it now also re-wraps each descendant's ipnsPrivateKeyfolder-ipns (folders) / file-ipns (files, guarded on the optional field). Invite links stay read-only ('read').
  • CHILD_KEY_TYPES gains folder-ipns so the create-share endpoint accepts it; the API client was regenerated.
  • Security-reviewed: correct ECIES direction, full plaintext-key zeroing on all paths, no over-distribution. LOW risk.

Added: decentralized, signature-gated IPNS (909c50c55)

The server relayed IPNS through a per-user folder_ipns table and authorized non-owner publishes via a write-share lookup — so a recipient publishing an owner-created descendant forked a duplicate record instead of updating the canonical one (the move would not complete end-to-end). Rather than extend the share-based authorization, we moved to the model IPNS itself uses:

  • folder_ipns is keyed by ipnsName alone (one canonical record per name). userId is retained as a denormalized creator marker (listing / TEE enrollment / cleanup).
  • publishRecord verifies the record's Ed25519 SignatureV2 (@cipherbox/crypto.verifyIpnsRecordSignature, backed by the ipns package) before create or update. Possession of the key — proven by the signature — is the authority to update a name, regardless of which user publishes.
  • The API's hand-rolled IPNS protobuf parser was replaced by @cipherbox/crypto.parseIpnsRecord (also ipns-backed); the resolve response contract (cid/sequenceNumber/signatureV2/data/pubKey) is preserved.
  • Anti-rollback: rejects a record whose embedded (signature-covered, tamper-evident) sequence regresses below the stored record's — closing a replay/rollback hole the single-canonical-row model would otherwise widen (an observer could replay an old, still-valid record to roll a folder back to a stale CID).
  • Migration 1749300000000 dedupes to the highest-sequence row, then swaps the unique constraint (user_id, ipns_name)(ipns_name). Idempotent.

Tradeoff (accepted)

Write access is now revocable only by IPNS key rotation + re-share, not a server-side share revoke. This is the intended decentralization property: the server no longer gates who may publish a name — the key does.

Security review

Overall LOW after the anti-rollback fix. The signature-binding core is sound and fail-closed (a record signed by a different key for a target name is rejected; V1-only / expired / malformed records are rejected). The one High finding (cross-user replay/rollback via the optional CAS + server-counter masking) is addressed by the embedded-sequence anti-rollback check. Documented fast-follows: client-side signatureVerified enforcement and un-cached-name resolve verification (defense-in-depth; the server fix already prevents rolled-back records from being stored).

Test status

  • crypto: 150/150 (incl. new verifyIpnsRecordSignature/parseIpnsRecord round-trip vs real records).
  • api: ipns 136/136 (incl. anti-rollback + signature-gated cases), shares/republish/vault 289/289, nest build clean.
  • web: 54/54 unit; tsc -b + eslint clean across web/crypto/api; e2e tsc --noEmit clean.
  • Full two-account move + decrypt-survival e2e (shared-folder-move.spec.ts) validated via CI E2E against this branch.

Notes

  • Commits in this branch are unsigned (the 1Password SSH signer was disabled for the autonomous session). A signed rebase (or admin override) is needed before merge.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

Summary by CodeRabbit

Release Notes

  • New Features

    • Added moving for files and folders within shared folder hierarchies (single-item).
    • Introduced a shared Move dialog with a writable destination picker.
    • Added batch move for multi-selected items.
    • Enabled drag-and-drop move onto shared folders.
  • Improvements

    • Enhanced shared views with multi-select and selection-aware move actions.
    • Improved shared folder loading during navigation, including propagation-delay retry handling.

FSM1 and others added 30 commits June 18, 2026 02:21
…tion consolidation

Entire-Checkpoint: e58bf755e671
Entire-Checkpoint: cc679fefc1f7
Entire-Checkpoint: 244b430c8211
…ivate-vault parity)

Entire-Checkpoint: e7041909f23d
…ion consolidation

5 plans across 3 waves: SDK enumerate+move op (TDD), useFolderNavigation
unwrap consolidation, web move UX, batch+drag parity, and the two-account
decrypt-survival e2e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 5aa79041aceb
…research resolved

Entire-Checkpoint: aa1b86a5cc69
…hod and enumerateSharedSubtree

Entire-Checkpoint: 3022e6584373
…rateSharedSubtree

- Add moveInSharedFolder stateless op to shared-write.ts (dual src/dest context,
  publish DEST first, re-key FileMetadata, publish SOURCE, no key zeroing in op)
- Export moveInSharedFolder from share/index.ts
- Add CipherBoxClient.moveInSharedFolder with write-cap guard (T-49-01),
  fresh dest loadFolderMetadata (A1), adoptSharedFolderResult for SOURCE only,
  finally-zeroing destFolderKey/destIpnsPrivateKey/fileIpnsPrivateKey (T-49-04)
- Add CipherBoxClient.enumerateSharedSubtree with stack-based DFS, visited Set
  guard (cycle prevention), writable flag from folder-ipns key presence

Entire-Checkpoint: b1ede215cbe2
…ion with ensureFolderLoaded

- Remove duplicate web-side unwrapKey + resolveIpnsRecord + fetchAndDecryptMetadata block (lines 241-302)
- Delegate unwrap/resolve/decrypt to client.ensureFolderLoaded (SDK single source of truth)
- Preserve 3x/2s IPNS-propagation retry wrapper with latestNavTarget guard on each iteration
- Clone SDK-owned key buffers into FolderNode (new Uint8Array) to prevent use-after-zero on client.destroy()
- Remove now-dead imports: unwrapKey, hexToBytes, fetchAndDecryptMetadata, resolveIpnsRecord, useAuthStore
- Add FolderState type import from @cipherbox/sdk for mapping

Entire-Checkpoint: 923d8deca666
…lowlist + moveItemHandler

- Add 'moveInSharedFolder' and 'enumerateSharedSubtree' to SharedFolderClient Pick union
- Add moveItemHandler to useSharedWriteOps routing through runWrite -> client.moveInSharedFolder
- Expose moveItem in useSharedNavigation return type

Entire-Checkpoint: 2dffc00ec239
- Add vi.mock stubs for react/useCallback, auth store, sdk-provider, share.service
- Add 3 moveItemHandler cases: routes runWrite->client.moveInSharedFolder, surfaces errors, guards absent keypair
- Tests run in node env (no React render harness) matching existing file style

Entire-Checkpoint: a54d3d8ef26e
…type

- New SharedMoveDialog loading picker nodes via enumerateSharedSubtree
- Props: open/onClose/onConfirm(destFolderId,destIpnsName)/item/currentFolderId/shareId/isLoading
- Disables read-only and current-folder rows; role=button + onKeyDown a11y
- :focus-visible style added to dialogs.css with modern rgb() notation
- SharedMoveDialog readonly badge [RO] for non-writable folders
- Fix vi.fn type argument in test for newer vitest compat

Entire-Checkpoint: 37b6e4209fa6
- Import SharedMoveDialog and destructure moveItem from useSharedNavigation
- Add moveDialogItem state + handleMoveClick callback
- Wire onMove on folder-view ContextMenu for write-permission recipients (files only)
- List-view (synthetic top-level shares) ContextMenu has no onMove (T-49-09)
- Mount SharedMoveDialog with onConfirm calling moveItem; refresh via sharedFolder:updated projection

Entire-Checkpoint: a468fcb78a68
- 49-03-SUMMARY.md: SharedMoveDialog + moveItemHandler + onMove wiring
- STATE.md: advance to plan 4 of 5, add decisions
- ROADMAP.md: mark 49-03 complete

Entire-Checkpoint: dccd7953e33e
…Handler

- Add batchMoveItemsHandler to useSharedWriteOps: loops client.moveInSharedFolder
  per item inside runWrite, stops on first failure, clearSelection on success
- Add batchMoveItems to UseSharedNavigationReturn type
- Add selectedIds Set, selectedItems useMemo, multiSelectActive, clearSelection,
  handleSelect, and handleBatchMoveClick to SharedFileBrowser
- Wire SelectionActionBar into folder view (write shares only, multiSelectActive)
- Mount batch-mode SharedMoveDialog opened from SelectionActionBar
- Pass isSelected/onSelect/onMoveItemTo/selectedItems to SharedFolderRow

Entire-Checkpoint: 4ea2f59e45f2
- Add optional items?: FolderChild[] prop to SharedMoveDialog (mirrors private
  MoveDialog item|items shape from MoveDialog.tsx:20-21)
- isBatchMode flag: items.length > 1 auto-adapts title (Move N items) and label
- SharedFileBrowser opens dialog in batch mode from handleBatchMoveClick,
  passing items={batchMoveItems_} and routing onConfirm to batchMoveItems
- Single-item path (item prop) unchanged

Entire-Checkpoint: a88d61c493fc
- handleDragStart: multi-select-aware payload (application/json {items,parentId})
  includes all selectedItems when dragged item is part of a selection (> 1),
  else single item — mirrors FileListItem :160-177
- handleDrop: folder rows only; parses application/json defensively (T-49-13);
  routes single item -> moveItemHandler, multi -> batchMoveItemsHandler via
  onMoveItemTo callback in SharedFileBrowser; ignores payload parentId (drop
  target row id/ipnsName is the authoritative dest, T-49-12)
- handleDragOver/handleDragLeave: visual affordance (isDragOver -> css class
  file-list-item--drag-over); distinguishes internal moves from external drops
  via dataTransfer.types.includes('application/json')
- onSelect click handler for Ctrl/Cmd+click multi-select selection
- External file upload drop (container level) preserved; SharedFolderRow does
  not handle external file drops (falls through to parent handler)
- a11y: draggable + existing role=row + onKeyDown Enter/Space for navigation

Entire-Checkpoint: b1e16fc43eff
- Mirrors MoveDialogPage shape for the shared-folder picker dialog
- dialog() scoped via .move-dialog-folder-list filter (avoids collision with private MoveDialog)
- getFolderItem/folderItems target .shared-move-dialog-folder-item rows (role=button)
- waitForTreeLoaded polls loading indicator hidden + listbox visible
- move() helper: waitForTreeLoaded -> selectFolder -> clickMove -> waitForClose

Entire-Checkpoint: 245398a29315
- Two-account Alice/Bob setup mirroring writable-shares.spec.ts (createWalletTestAccount, closeWalletTestAccounts, navigateToShared)
- Alice creates parent folder with a subfolder and a text file; shares read-write with Bob
- Bob navigates via SharedFileBrowserPage, right-clicks file -> Move to... -> selects subfolder in SharedMoveDialogPage -> confirm
- Asserts file disappears from source view, appears in subfolder in Bob view
- readContentViaEditor (right-click -> Edit -> waitForContentLoaded -> getContent) asserts content decrypts for Bob (REQ-5 T-49-14)
- alice.page.reload({waitUntil:'networkidle'}) -> owner navigates to subfolder -> readContentViaEditor asserts same decrypted content for Alice
- Decrypt-on-read assertion (TextEditorDialogPage.getContent) not mere list visibility

Entire-Checkpoint: 6ff2b365878a
- 49-05-SUMMARY.md: SharedMoveDialogPage + shared-folder-move.spec.ts decrypt-survival e2e
- STATE.md: completed_plans 150->151 (100%), added P05 metrics row, updated last session
- ROADMAP.md: mark 49-04 and 49-05 complete, mark todos #7 and #8 as closed

Entire-Checkpoint: 5dea4ff22a11
…e e2e deferred to main-push)

Entire-Checkpoint: 9821869af98d
…ernalFileDrag, collapse reset effects)

Entire-Checkpoint: acb4a538dbbd
… on success

- moveInSharedFolder throws when a folder is moved into itself
- enumerateSharedSubtree returns parentId so the picker can disable the moved
  folder's own subtree (move-into-descendant would cycle the tree)
- SharedMoveDialog disables moved folder(s) + descendants; SharedFolderRow drag
  drop skips dropping a folder onto itself
- runWrite returns success; batchMoveItems clears selection only on success

Entire-Checkpoint: d63fc00d5327
…rity review)

Each DFS step unwrapped a subfolder AES folderKey but dropped it without
zeroing, leaving plaintext folder keys live in the heap across the walk.
Wrap each iteration in try/finally with folderKey.fill(0).

Entire-Checkpoint: e696538da67f
… review)

The shared multi-select action bar stubbed onDownload/onDelete to no-ops, so a
Delete button rendered that silently did nothing — misleading users about
deletion of shared data. Make those handlers optional in SelectionActionBar and
omit them in the shared browser so only the wired Move action renders.

Entire-Checkpoint: 4ca756afaef6
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Release Preview

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

Cascade Details

  • api minor -> sdk-core patch (direct dependency)
  • crypto minor -> core patch (direct dependency)
  • crypto minor -> desktop patch (direct dependency)
  • crypto minor -> tee-worker patch (direct dependency)
  • api minor -> cipherbox-fuse patch (direct dependency)
  • api minor -> cipherbox-sdk patch (direct dependency)

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.92760% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.23%. Comparing base (2c639de) to head (03d4596).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
packages/crypto/src/ipns/parse-record.ts 77.77% 4 Missing ⚠️
packages/sdk/src/client.ts 97.70% 2 Missing and 1 partial ⚠️
packages/crypto/src/ipns/verify-record.ts 90.00% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main     #509       +/-   ##
===========================================
+ Coverage   64.03%   85.23%   +21.20%     
===========================================
  Files         142      107       -35     
  Lines       10937     6827     -4110     
  Branches     1226     1234        +8     
===========================================
- Hits         7003     5819     -1184     
+ Misses       3689      766     -2923     
+ Partials      245      242        -3     
Flag Coverage Δ
api 85.23% <95.92%> (+0.54%) ⬆️
api-client 85.23% <95.92%> (+0.54%) ⬆️
core 85.23% <95.92%> (+0.54%) ⬆️
crypto 85.23% <95.92%> (+0.54%) ⬆️
desktop ?
sdk 85.23% <95.92%> (+0.54%) ⬆️
sdk-core 85.23% <95.92%> (+0.54%) ⬆️

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 88.40% <100.00%> (+0.31%) ⬆️
apps/api/src/shares/types.ts 100.00% <100.00%> (ø)
packages/sdk/src/share/index.ts 100.00% <ø> (ø)
packages/sdk/src/share/shared-write.ts 98.70% <100.00%> (+0.18%) ⬆️
packages/crypto/src/ipns/verify-record.ts 90.00% <90.00%> (ø)
packages/sdk/src/client.ts 82.43% <97.70%> (+1.36%) ⬆️
packages/crypto/src/ipns/parse-record.ts 77.77% <77.77%> (ø)

... and 36 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.

@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.

🧹 Nitpick comments (4)
tests/web-e2e/page-objects/file-browser/shared-move-dialog.page.ts (2)

45-47: ⚡ Quick win

Use exact-name matching for folder row selection.

Line 46 currently uses substring matching (hasText: name), which can select the wrong row when folder names overlap (e.g., docs vs docs-old).

Proposed fix
   getFolderItem(name: string): Locator {
-    return this.folderList().locator('.shared-move-dialog-folder-item', { hasText: name });
+    return this.folderList().locator('.shared-move-dialog-folder-item').filter({
+      has: this.page.locator('.move-dialog-folder-name', { hasText: new RegExp(`^${name}$`) }),
+    });
   }
🤖 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 `@tests/web-e2e/page-objects/file-browser/shared-move-dialog.page.ts` around
lines 45 - 47, The getFolderItem method uses substring matching with hasText:
name, which can incorrectly select folders when names overlap (e.g., searching
for "docs" matches both "docs" and "docs-old"). To fix this, modify the locator
in getFolderItem to use exact-name matching instead. Replace the hasText option
with a more specific selector or filtering logic that matches only the complete
folder name exactly, ensuring that partial name matches are not selected.

109-113: ⚡ Quick win

Fail fast in waitForTreeLoaded when the dialog enters an error state.

Line 109-Line 113 can report “loaded” even when subtree loading failed; explicitly checking loadError improves diagnostics and reduces flaky downstream failures.

Proposed fix
   async waitForTreeLoaded(options?: { timeout?: number }): Promise<void> {
     await this.folderList().waitFor({ state: 'visible', ...options });
     // Allow an extra tick for the loading indicator to disappear
     await this.loadingIndicator().waitFor({ state: 'hidden', timeout: options?.timeout ?? 15_000 });
+    if (await this.loadError().isVisible()) {
+      throw new Error(`SharedMoveDialog failed to load folder tree: ${await this.loadError().textContent()}`);
+    }
   }
🤖 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 `@tests/web-e2e/page-objects/file-browser/shared-move-dialog.page.ts` around
lines 109 - 113, The waitForTreeLoaded method in the SharedMoveDialogPage class
currently only checks if the folder list is visible and the loading indicator is
hidden, but it does not explicitly verify that the dialog has not entered an
error state during loading. Add an additional wait condition to check that the
loadError element is hidden or not visible before the method resolves, similar
to how the loadingIndicator is checked. This will ensure that if subtree loading
fails and triggers an error state, the method will fail fast rather than
reporting a successful load, reducing flaky downstream test failures.
apps/web/src/components/file-browser/SharedFolderRow.tsx (1)

114-152: 💤 Low value

Type narrowing gap when accessing item.ipnsName.

The handleDrop callback accesses item.ipnsName at line 149, but TypeScript won't narrow the FolderChild union type based on the isFolder guard at line 121 because isFolder is a derived boolean rather than a type predicate. This could cause a TypeScript error depending on strict settings, since ipnsName only exists on FolderEntry, not FilePointer.

🔧 Suggested type-safe access
       // Guard: never drop a folder onto itself (one of the dragged items) — that
       // would move it into itself and cycle the tree (mirrors FileListItem).
       if (parsed.items.some((i) => i.id === item.id)) return;

+      // Type guard: onMoveItemTo only set for folders, but narrow for TS
+      if (item.type !== 'folder') return;
+
       // Route through the parent's handleDropOnFolder-equivalent, forwarding the
       // dragged items so the parent moves exactly what was dragged. Per-item
       // validation (name collision + write-capability) is in the SDK.
       onMoveItemTo(item.id, item.ipnsName, parsed.items);
🤖 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 `@apps/web/src/components/file-browser/SharedFolderRow.tsx` around lines 114 -
152, The handleDrop callback accesses item.ipnsName without proper type
narrowing. Although there is an isFolder guard that returns early if not a
folder, TypeScript cannot narrow the FolderChild union type based on a simple
boolean variable check. To fix this, add an explicit type guard for the item
parameter before accessing ipnsName. You can either use a type predicate
function to check if item is a FolderEntry with ipnsName property, or add a type
assertion at the point where item.ipnsName is accessed in the onMoveItemTo call.
This ensures TypeScript understands that item has the ipnsName property at that
point in the code.
apps/web/src/hooks/__tests__/useSharedWriteOps.test.ts (1)

382-477: ⚡ Quick win

Consider adding test coverage for batchMoveItems.

The moveItem tests are well-structured and cover the critical paths (success, error surfacing, missing keypair). However, batchMoveItems has distinct behavior worth testing:

  • Empty items array early-return
  • clearSelection called only on full success
  • Partial batch failure (first N items succeed, then one fails)

These edge cases would strengthen confidence in the batch flow.

🤖 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 `@apps/web/src/hooks/__tests__/useSharedWriteOps.test.ts` around lines 382 -
477, Add test cases for the batchMoveItems function to cover its distinct
behavior paths that differ from moveItem. Create tests that verify: (1) when an
empty items array is passed, the function returns early without calling the
underlying moveInSharedFolder; (2) clearSelection is invoked only when all items
in the batch are moved successfully, not on partial failures; (3) when the first
N items succeed but a subsequent item fails, the error is properly surfaced via
setError and clearSelection is not called. These tests should follow the same
pattern as the existing moveItem tests, using vi.fn() for mock callbacks and
makeChild for creating test items.
🤖 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.

Nitpick comments:
In `@apps/web/src/components/file-browser/SharedFolderRow.tsx`:
- Around line 114-152: The handleDrop callback accesses item.ipnsName without
proper type narrowing. Although there is an isFolder guard that returns early if
not a folder, TypeScript cannot narrow the FolderChild union type based on a
simple boolean variable check. To fix this, add an explicit type guard for the
item parameter before accessing ipnsName. You can either use a type predicate
function to check if item is a FolderEntry with ipnsName property, or add a type
assertion at the point where item.ipnsName is accessed in the onMoveItemTo call.
This ensures TypeScript understands that item has the ipnsName property at that
point in the code.

In `@apps/web/src/hooks/__tests__/useSharedWriteOps.test.ts`:
- Around line 382-477: Add test cases for the batchMoveItems function to cover
its distinct behavior paths that differ from moveItem. Create tests that verify:
(1) when an empty items array is passed, the function returns early without
calling the underlying moveInSharedFolder; (2) clearSelection is invoked only
when all items in the batch are moved successfully, not on partial failures; (3)
when the first N items succeed but a subsequent item fails, the error is
properly surfaced via setError and clearSelection is not called. These tests
should follow the same pattern as the existing moveItem tests, using vi.fn() for
mock callbacks and makeChild for creating test items.

In `@tests/web-e2e/page-objects/file-browser/shared-move-dialog.page.ts`:
- Around line 45-47: The getFolderItem method uses substring matching with
hasText: name, which can incorrectly select folders when names overlap (e.g.,
searching for "docs" matches both "docs" and "docs-old"). To fix this, modify
the locator in getFolderItem to use exact-name matching instead. Replace the
hasText option with a more specific selector or filtering logic that matches
only the complete folder name exactly, ensuring that partial name matches are
not selected.
- Around line 109-113: The waitForTreeLoaded method in the SharedMoveDialogPage
class currently only checks if the folder list is visible and the loading
indicator is hidden, but it does not explicitly verify that the dialog has not
entered an error state during loading. Add an additional wait condition to check
that the loadError element is hidden or not visible before the method resolves,
similar to how the loadingIndicator is checked. This will ensure that if subtree
loading fails and triggers an error state, the method will fail fast rather than
reporting a successful load, reducing flaky downstream test failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7c9d7742-18eb-469d-a82e-3687ef43b9d3

📥 Commits

Reviewing files that changed from the base of the PR and between 7664771 and 01ccbc0.

📒 Files selected for processing (36)
  • .planning/ROADMAP.md
  • .planning/STATE.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/.gitkeep
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-01-PLAN.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-01-SUMMARY.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-02-PLAN.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-02-SUMMARY.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-03-PLAN.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-03-SUMMARY.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-04-PLAN.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-04-SUMMARY.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-05-PLAN.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-05-SUMMARY.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-CONTEXT.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-PATTERNS.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-RESEARCH.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-VALIDATION.md
  • .planning/phases/49-shared-folder-move-intra-share-and-usefoldernavigation-unwra/49-VERIFICATION.md
  • .planning/security/REVIEW-phase49-20260618T025359Z.md
  • apps/web/src/components/file-browser/SelectionActionBar.tsx
  • apps/web/src/components/file-browser/SharedFileBrowser.tsx
  • apps/web/src/components/file-browser/SharedFolderRow.tsx
  • apps/web/src/components/file-browser/SharedMoveDialog.tsx
  • apps/web/src/hooks/__tests__/useSharedWriteOps.test.ts
  • apps/web/src/hooks/shared-folder-projection.ts
  • apps/web/src/hooks/useFolderNavigation.ts
  • apps/web/src/hooks/useSharedNavigation.ts
  • apps/web/src/hooks/useSharedWriteOps.ts
  • apps/web/src/styles/dialogs.css
  • packages/sdk/src/__tests__/enumerate-shared-subtree.test.ts
  • packages/sdk/src/__tests__/move-in-shared-folder.test.ts
  • packages/sdk/src/client.ts
  • packages/sdk/src/share/index.ts
  • packages/sdk/src/share/shared-write.ts
  • tests/web-e2e/page-objects/file-browser/shared-move-dialog.page.ts
  • tests/web-e2e/tests/shared-folder-move.spec.ts

… load, batch coverage

- shared-move-dialog page object: exact folder-name match (no docs/docs-old
  collision) + waitForTreeLoaded fails fast with a clear message on load error
- add batchMoveItems tests: empty early-return, clearSelection only on full
  success, stop-on-first-failure (locks in the on-success-only selection clear)
- skipped the 'item.type !== folder' narrowing nitpick: redundant (isFolder
  already guards) and tsc -b passes strict without it

Entire-Checkpoint: ca5f6fcd5105
@FSM1

FSM1 commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

Addressed the CodeRabbit nitpicks in f2ddd9a3a:

  • shared-move-dialog.page.ts exact-name match ✅ — getFolderItem now filters by an exact folder-name match (getByText(name, { exact: true }), injection-safe) so overlapping names like docs / docs-old can't collide.
  • waitForTreeLoaded fail-fast on error ✅ — reworked to wait for the loading indicator to clear, then throw a descriptive error if loadError is visible, then wait for the list. (Done this way rather than checking after folderList().waitFor('visible'), because on error the list never renders — the original placement would have hung until timeout.)
  • batchMoveItems test coverage ✅ — added three tests: empty-array early return, clearSelection only on full success, and stop-on-first-failure without clearing the selection (this also locks in the on-success-only selection clear).
  • SharedFolderRow item.type !== 'folder' narrowing ⏭️ skipped — redundant: isFolder already guards the same path, tsc -b passes under strict without it, and the earlier cleanup pass intentionally removed that exact line. item.ipnsName resolves cleanly on the FolderChild union.

The earlier review round (CLI) findings — drag move routing by the actually-dragged items, useFolderNavigation redirect-on-IPNS-timeout, listbox role="option"/aria-selected, and keyboard selection parity — were addressed in 01ccbc0c2.

FSM1 and others added 3 commits June 18, 2026 14:01
The shared-view batch action bar was gated on `selectedIds.size > 0`, so a
plain single click — which is also the first click of a double-click — selected
one item and rendered the SelectionActionBar above the file list. That layout
shift moved the rows down mid-double-click, so the second click missed the row
and the `dblclick` never fired: folder navigation silently failed (e2e
writable-shares 8.2 + shared-folder-move 3.x).

Gate the bar on `size > 1`, matching the private vault (FileBrowser :205). A
single click still highlights the row but no longer pops the bar.

Also strip the decorative trailing "/" from folder names in the shared
file-browser page object's getFolderItemNames so exact `toContain(name)`
assertions work (other specs already used slash-tolerant `.includes`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 6a5174789ead
A read-write folder share now grants the recipient the IPNS signing keys
(folder-ipns / file-ipns) for owner-created descendant subfolders and files,
not just the read keys. Without them a recipient could only write to items
they created themselves, so moving a file into an owner-created subfolder was
impossible.

- collectChildKeys re-wraps each descendant's ipnsPrivateKey for the recipient
  when permission is write (file-ipns guarded on the optional field; folders
  always carry it). Invite links stay read-only.
- CHILD_KEY_TYPES gains folder-ipns so the create-share endpoint accepts it;
  regenerated the API client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 2a7e3a5b1b04
Key the folder_ipns cache by ipnsName alone (one canonical record per name)
and authorize publishes by verifying the record's Ed25519 SignatureV2 (key
possession) instead of per-user ownership or a write-share lookup. Any holder
of a name's key may update it -- the model IPNS itself uses -- so a write-share
recipient can publish owner-created descendant records (enabling intra-share
moves) with no server-side share check.

- verifyIpnsRecordSignature / parseIpnsRecord added to @cipherbox/crypto via the
  ipns package, replacing the API hand-rolled protobuf parser.
- publishRecord verifies the signature before create OR update.
- Anti-rollback: reject a record whose embedded (signature-covered) sequence
  regresses below the stored record's, blocking replay of an old still-valid
  record to roll a folder back to a stale CID.
- Migration dedupes to the highest-sequence row, then swaps the unique
  constraint user_id+ipns_name to ipns_name. userId retained as a denormalized
  creator marker for listing / TEE enrollment / cleanup.

Tradeoff: write access is now revocable only by IPNS key rotation, not a
server-side share revoke (the decentralization goal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 355a7f89d28a
@github-actions github-actions Bot added release:api:feat Minor version bump (new feature) for api release:crypto:feat Minor version bump (new feature) for crypto release:sdk-core:fix Patch version bump (bug fix) for sdk-core release:core:fix Patch version bump (bug fix) for core release:desktop:fix Patch version bump (bug fix) for desktop release:tee-worker:fix Patch version bump (bug fix) for tee-worker release:cipherbox-fuse:fix Patch version bump (bug fix) for cipherbox-fuse release:cipherbox-sdk:fix Patch version bump (bug fix) for cipherbox-sdk labels Jun 18, 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: 1

🤖 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 `@apps/web/src/lib/crypto/key-wrapping.ts`:
- Around line 107-122: The write permission branch unconditionally accesses
folder.ipnsPrivateKeyEncrypted without checking if it exists, which will cause a
runtime crash if legacy folders lack IPNS keys. Add a null guard condition to
check if folder.ipnsPrivateKeyEncrypted exists before proceeding with the
reWrapEncryptedKey call, similar to the file branch pattern that checks
fp.ipnsPrivateKeyEncrypted. Only execute the reWrapEncryptedKey logic and push
to childKeys if the encrypted key is present and not null.
🪄 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: a63d597f-d751-4ecc-9770-f43b236d7917

📥 Commits

Reviewing files that changed from the base of the PR and between 4bb1819 and b13e993.

⛔ Files ignored due to path filters (131)
  • packages/api-client/openapi.json is excluded by !packages/api-client/**
  • packages/api-client/src/generated/auth/auth.ts is excluded by !**/generated/**, !**/generated/**, !packages/api-client/**
  • packages/api-client/src/generated/device-approval/device-approval.ts is excluded by !**/generated/**, !**/generated/**, !packages/api-client/**
  • packages/api-client/src/generated/health/health.ts is excluded by !**/generated/**, !**/generated/**, !packages/api-client/**
  • packages/api-client/src/generated/identity/identity.ts is excluded by !**/generated/**, !**/generated/**, !packages/api-client/**
  • packages/api-client/src/generated/invites/invites.ts is excluded by !**/generated/**, !**/generated/**, !packages/api-client/**
  • packages/api-client/src/generated/ipfs/ipfs.ts is excluded by !**/generated/**, !**/generated/**, !packages/api-client/**
  • packages/api-client/src/generated/ipns/ipns.ts is excluded by !**/generated/**, !**/generated/**, !packages/api-client/**
  • packages/api-client/src/generated/root/root.ts is excluded by !**/generated/**, !**/generated/**, !packages/api-client/**
  • packages/api-client/src/generated/share-invites/share-invites.ts is excluded by !**/generated/**, !**/generated/**, !packages/api-client/**
  • packages/api-client/src/generated/shares/shares.ts is excluded by !**/generated/**, !**/generated/**, !packages/api-client/**
  • packages/api-client/src/generated/tee/tee.ts is excluded by !**/generated/**, !**/generated/**, !packages/api-client/**
  • packages/api-client/src/generated/vault/vault.ts is excluded by !**/generated/**, !**/generated/**, !packages/api-client/**
  • packages/api-client/src/models/addShareKeysDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/authMethodResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/authMethodResponseDtoType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/batchPublishIpnsDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/batchPublishIpnsResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/batchUnenrollIpnsDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/batchUnenrollIpnsResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/childKeyDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/childKeyDtoKeyType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/claimChildKeyDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/claimChildKeyDtoKeyType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/claimInviteDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/claimInviteResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/connectionTestRequestDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/connectionTestResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/connectionTestResponseDtoProtocol.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/createApprovalDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/createInviteDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/createInviteDtoItemType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/createShareDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/createShareDtoItemType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/createShareDtoPermission.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/createShareResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/createShareResponseDtoItemType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/createShareResponseDtoPermission.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/deleteAccountDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/deleteAccountResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/desktopRefreshDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/deviceApprovalControllerCreateRequest201.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/deviceApprovalControllerGetPending200Item.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/deviceApprovalControllerGetStatus200.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/deviceApprovalControllerGetStatus200Status.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/googleLoginDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/googleLoginDtoIntent.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/healthControllerCheck200.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/healthControllerCheck200Details.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/healthControllerCheck200DetailsDatabase.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/healthControllerCheck200Error.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/healthControllerCheck200Info.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/healthControllerCheck200InfoDatabase.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/identityControllerGetJwks200.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/identityControllerGetJwks200KeysItem.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/identityControllerGetWalletNonce200.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/identityTokenResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/index.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/initVaultDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/inviteChildKeyDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/inviteChildKeyDtoKeyType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/inviteDataResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/inviteDataResponseDtoItemType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/inviteDataResponseDtoStatus.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/inviteResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/inviteResponseDtoItemType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/inviteResponseDtoStatus.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/inviteStatusResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/inviteStatusResponseDtoStatus.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/ipfsControllerUploadBody.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/ipnsControllerResolveRecordParams.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/linkMethodDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/linkMethodDtoLoginType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/loginDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/loginDtoLoginType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/loginResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/logoutResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/lookupUserResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/paginatedReceivedSharesDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/paginatedSentSharesDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/pendingRotationResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/pendingRotationResponseDtoItemType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/publishIpnsDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/publishIpnsEntryDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/publishIpnsEntryDtoRecordType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/publishIpnsResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/quotaResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/receivedShareResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/receivedShareResponseDtoItemType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/receivedShareResponseDtoPermission.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/registerCidDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/registerCidResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/resolveIpnsResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/respondApprovalDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/respondApprovalDtoAction.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/sendOtpDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/sendOtpResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/sentShareResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/sentShareResponseDtoItemType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/sentShareResponseDtoPermission.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/setByoStatusDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/setByoStatusResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/shareInvitesControllerListInvitesParams.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/shareKeyEntryDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/shareKeyEntryDtoKeyType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/shareKeyResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/shareKeyResponseDtoKeyType.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/sharesControllerGetReceivedSharesParams.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/sharesControllerGetSentSharesParams.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/sharesControllerLookupUserParams.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/teeKeysDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/testLoginDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/testLoginResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/tokenResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/unlinkMethodDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/unlinkMethodResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/unpinDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/unpinResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/updateEncryptedKeyDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/updateItemNameDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/updatePermissionDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/updatePermissionDtoPermission.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/uploadResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/vaultConfigResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/vaultExportDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/vaultResponseDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/vaultResponseDtoTeeKeys.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/verifyOtpDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/verifyOtpDtoIntent.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/walletVerifyDto.ts is excluded by !packages/api-client/**
  • packages/api-client/src/models/walletVerifyDtoIntent.ts is excluded by !packages/api-client/**
📒 Files selected for processing (16)
  • apps/api/src/ipns/entities/folder-ipns.entity.ts
  • apps/api/src/ipns/ipns-record-parser.spec.ts
  • apps/api/src/ipns/ipns-record-parser.ts
  • apps/api/src/ipns/ipns.module.ts
  • apps/api/src/ipns/ipns.service.spec.ts
  • apps/api/src/ipns/ipns.service.ts
  • apps/api/src/migrations/1749300000000-IpnsCacheKeyedByName.ts
  • apps/api/src/shares/types.ts
  • apps/web/src/components/file-browser/ShareDialog.tsx
  • apps/web/src/lib/crypto/key-wrapping.ts
  • apps/web/src/services/invite.service.ts
  • packages/crypto/src/__tests__/ipns-record.test.ts
  • packages/crypto/src/index.ts
  • packages/crypto/src/ipns/parse-record.ts
  • packages/crypto/src/ipns/verify-record.ts
  • release-please-config.json
💤 Files with no reviewable changes (3)
  • apps/api/src/ipns/ipns.module.ts
  • apps/api/src/ipns/ipns-record-parser.spec.ts
  • apps/api/src/ipns/ipns-record-parser.ts
✅ Files skipped from review due to trivial changes (1)
  • release-please-config.json

Comment thread apps/web/src/lib/crypto/key-wrapping.ts
FSM1 and others added 3 commits June 18, 2026 16:21
The conflict-detection helper faked a competing-device publish by POSTing a
dummy unsigned record to /ipns/publish to advance the server sequence. Under the
new signature-gated IPNS model the server rejects unsigned records (400), so the
bump failed -- surfacing only on Windows, whose .ps1 throws on a failed bump
(the .sh swallowed it as a warning and passed falsely).

Replace the dummy bump with bump-ipns-sequence.mjs, which derives the
deterministic vault IPNS keypair and republishes the current root metadata
UNCHANGED at sequence+1 via the SDK CAS path -- a real, validly-signed,
non-destructive bump, exactly what a legitimate second device does. Restores
genuine conflict exercise on every platform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: c4a5195bab1e
The .mjs E2E helper scripts import dist bundles and are skipped by tsc/eslint/
vitest/jest, so they drift from SDK contract changes and only break deep in slow
single-OS E2E runs (e.g. the conflict-detection bump just broke on Windows
only). Capture a pending todo to type them so drift fails fast at CI build time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: f930d519570b
The write-share branch re-wrapped folder.ipnsPrivateKeyEncrypted
unconditionally. The field is typed required on FolderEntry (so no crash for
type-conforming data), but the file branch already guards its optional
equivalent — mirror that here so malformed/legacy folder metadata decrypted from
IPFS degrades to read-only instead of throwing. Addresses CodeRabbit review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 3c5bc9c42ea9
@FSM1 FSM1 merged commit c36ac6d into main Jun 18, 2026
27 checks passed
@FSM1 FSM1 deleted the feat/shared-folder-move-intra-share-and-usefoldernavigation-unwra branch June 18, 2026 14:39
FSM1 added a commit that referenced this pull request Jun 18, 2026
packages/sdk had release-as pinned to 0.36.0 in release-please-config.json,
equal to both the manifest version and the latest tag. After #509 landed a
releasable feat touching packages/sdk source, release-please attributed it
to sdk (by path) but the pin forced version 0.36.0 again, producing a
self-comparing compare/@cipherbox/sdk-v0.36.0...@cipherbox/sdk-v0.36.0
changelog entry re-appended on every push to main (#510, #514).

The pin went stale because the preview bot's release-target commit for #509
(b13e993) was orphaned by a branch rebase/force-push and is not an ancestor
of main; cancel-in-progress also aborts the bot's re-run, so sdk's target was
never advanced even though sdk source changed.

- Bump packages/sdk release-as to 0.37.0 so the pending feat releases as a
  real minor bump, advancing the version and ending the loop.
- Remove the two duplicate self-comparing 0.36.0 blocks from
  packages/sdk/CHANGELOG.md, leaving the legitimate 0.35.0...0.36.0 entry.
  release-please regenerates a clean 0.37.0 section (incl. #509) next run.


Entire-Checkpoint: 8c1d13c810a7

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-fuse:fix Patch version bump (bug fix) for cipherbox-fuse release:cipherbox-sdk:fix Patch version bump (bug fix) for cipherbox-sdk release:core:fix Patch version bump (bug fix) for core release:crypto:feat Minor version bump (new feature) for crypto release:desktop:fix Patch version bump (bug fix) for desktop release:sdk:feat Minor version bump (new feature) for sdk release:sdk-core:fix Patch version bump (bug fix) 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