Skip to content

feat: SDK-owned read chain and resolved folder listings#589

Merged
FSM1 merged 77 commits into
mainfrom
feat/sdk-owned-read-chain-and-resolved-folder-listings
Jul 6, 2026
Merged

feat: SDK-owned read chain and resolved folder listings#589
FSM1 merged 77 commits into
mainfrom
feat/sdk-owned-read-chain-and-resolved-folder-listings

Conversation

@FSM1

@FSM1 FSM1 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Phase 68.2 — SDK-Owned Read Chain and Resolved Folder Listings

Moves the gated read chain and per-child metadata resolution entirely into packages/sdk, exposes resolved folder listings (ResolvedChild), collapses the web store to a thin projection, and closes the Web/SDK folder-state desync bug class (SC#5).

What changed

  • SDK owns the gated read path (SDK-READ-01). IPNS resolve → ROT-07 durable anti-rollback gate (RotationHighWater.enforceResolved, fail-closed on unverified signature, parent-mirror generation sourcing) → IPFS fetch → node unseal, all inside packages/sdk. Raw resolveIpnsRecord is SDK-internal only. The web's parallel read services (ipns.service.ts, file-metadata.service.ts, kind-cache.ts, useFileSize.ts) are deleted.
  • Resolved folder listings (SDK-READ-02). listFolder / listSharedFolder return ResolvedChild[] (ipnsName, name, kind, size?, modifiedAt, sequence), resolved once per folder load and cached in the SDK; the web file list, shared browser, and details dialogs render directly from it.
  • Folder store is a projection (SDK-READ-03). folder.store.ts is a thin ResolvedChild[] projection of SDK state; belt-and-suspenders freshness (re-resolve on navigation via { forceResolve: true } + poll-driven invalidateOpenFolder for the open folder) closes the desync class. SealedChildRef is reverted to its frozen NODE-03 five-field set.
  • Enforced web/SDK boundary (SDK-READ-04, D-07 full scope). apps/web/src makes zero runtime calls into @cipherbox/sdk-core / @cipherbox/core and no raw IPFS/IPNS access on either read or write path (type-only imports allowed), enforced by an allowlist-free grep gate.

Gap closure

  • Round 1 closed the SC#5 desync itself (gated live-resolve-on-navigation; plans 13–14).
  • Round 2 closed two regressions the fresh-stack triage surfaced (plans 15–16, web-only, no SDK change):
    • A — after the kind-cache removal, isFileRef on a bare SealedChildRef always returned false, breaking download affordances and the download logic. Fixed with an isFileRefResolved classifier against the resolved listing.
    • B — the post-upload refresh only resynced root, so an upload into a subfolder never appeared. Fixed by refreshing the open folder via invalidateOpenFolder().

Verification

  • Verify: PASS, 5/5 must-haves (68.2-VERIFICATION.md).
  • Validate (Nyquist): compliant, 0 gaps (68.2-VALIDATION.md).
  • SDK unit suite: 338 passed / 49 skipped. D-07 grep gate: 0 runtime violations.
  • web-e2e (fresh containers): shared-folder-desync 4/4, batch-download 5/5, writable-shares 29/29.
  • SDK E2E gate: 99/101 pass. The 2 failures are tee-republish.test.ts (tee_key_state is empty — no tee-worker in the local stack); this phase touches no TEE/republish code, so they are infra-gated and covered by CI, not a regression. All read-chain / write-chain / publish-gate / share / file-ops / vault suites pass.
  • Security: SECURED — 17/17 threat mitigations present in source, 0 open (68.2-SECURITY.md). ROT-07 read-path gate, fail-closed-on-unverified, parent-mirror generation, and D-07 boundary all verified in code.

Deferred

  • SharedFolderRow drag-payload kind classification uses the legacy path (functionally harmless — the shared drop handler ignores DragItem.type; routes by ipnsName). Captured as a low-priority todo.

Residual pre-existing 68.1 debt (GAP-1 resolveFileMetadata AEAD media/streaming, GAP-2 cold-reload DFS timeout, recovery) is out of this phase's scope and unchanged.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Folder and file views now display resolved sizes, modified dates, and file/folder status more consistently across both shared and owned browsers.
    • Uploads, downloads, version history, and settings flows are routed through the app’s built-in client paths.
  • Bug Fixes

    • Fixed shared-folder desync so newly uploaded items appear correctly after navigation and polling.
    • Fixed download action gating to correctly recognize files even when identity data is incomplete.
    • Improved folder refresh to prevent stale listings after updates, including immediate refresh of the currently open folder.

FSM1 added 30 commits July 6, 2026 00:55
Entire-Checkpoint: a4cc7cd0630a
Entire-Checkpoint: f768c1ed29a3
Grounds the phase plan in live file:line call-site inventories, the
ungated-internal-read-path finding (ROT-07 gate exists only in the
web layer today), and the exact ba3e022 mirror-revert footprint.

Entire-Checkpoint: c3685d93a469
Entire-Checkpoint: 7a5431485365
Entire-Checkpoint: f7f84466cae9
Entire-Checkpoint: 4c7b28737f74
…ved folder listings)

Entire-Checkpoint: e283107cdb4f
RED run confirms the SDK's internal navigate path (ensureFolderLoaded ->
dfsFindFolder -> resolvePublishedNode) is currently ungated:

- Test 1 (below-floor rejection): FAILS -- resolves fine instead of
  throwing SequenceRegressionError (no enforceResolved call exists yet)
- Test 2 (enforceResolved called once, sourcing generation from the
  parent SealedChildRef mirror): FAILS -- enforceResolved is never
  invoked from the internal read path
- Test 3 (fail-closed on signatureVerified=false, before any floor
  mutation): FAILS -- resolvePublishedNode discards signatureVerified
  entirely today

3 failed | 293 passed | 49 skipped (folder-listing-gate: 0/3 passing)

Entire-Checkpoint: 7acfa7f68c62
….enforceResolved

Closes the RESEARCH.md Pitfall 2 gap: the SDK's own navigate path
(ensureFolderLoaded -> dfsFindFolder -> resolvePublishedNode) previously
resolved every read-side IPNS record with ZERO ROT-07 anti-rollback
gating -- the only working read-path gate lived entirely in
apps/web/src/services/ipns.service.ts, which this phase eventually
deletes. This lands the SDK-internal equivalent first so no shipped
security guarantee regresses mid-cutover.

- resolvePublishedNode now threads signatureVerified from
  sdkCore.resolveIpnsRecord (previously discarded)
- dfsFindFolder's per-hop child loop gates through
  rotationHighWater.enforceResolved before trusting a child resolve,
  sourcing generation from childRef.generation (the PARENT
  SealedChildRef mirror), never the child's own envelope generation
  (Pitfall 3 / T-68.2-03)
- ensureRootFolderState gates the root resolve the same way, sourcing
  generation from the in-memory folderTree entry (mirrors the existing
  write-path gate in reconcileFolderSequence, since root has no parent
  SealedChildRef)
- Both sites fail closed on signatureVerified=false BEFORE calling
  enforceResolved (T-68.2-02, no floor mutation on an unverified
  record) and guard Number.MAX_SAFE_INTEGER overflow before Number()
  conversion
- getWriteBodyParams (write-internal-only resolve) intentionally left
  ungated per D-05

pnpm --filter @cipherbox/sdk test -- --run folder-listing-gate: 296/296 passed
pnpm --filter @cipherbox/sdk test -- --run client-rotation: 296/296 passed

Entire-Checkpoint: b77befa2fb09
RED: proves listFolder/listSharedFolder/ResolvedChild do not exist yet,
and folder:loaded still emits raw SealedChildRef[] instead of
ResolvedChild[] with kind pre-resolved.

Entire-Checkpoint: eb0451130766
…lder) to the SDK

GREEN: folder-listing.ts's resolveChildren resolves a folder's sealed
children into ResolvedChild[] through a caller-injected gated resolve
function. client.ts adds gatedResolveChild (per-child ROT-07 gate,
mirroring dfsFindFolder's gate for listing resolves), resolveListingChildren
(in-SDK cache keyed by ipnsName, invalidated by sequenceNumber), and the
listFolder/listSharedFolder facade methods. listSharedFolder hoists the
intermediate-folder walk from useSharedNavigationActions rather than
wrapping navigateReadChain (which forces a file leaf).

Entire-Checkpoint: fc2eed31da2e
Retype folder:loaded/folder:updated/sharedFolder:updated children from
SealedChildRef[] to ResolvedChild[] in events.ts. Every emit site in
client.ts now emits through resolveListingChildren (the same cache/gate
listFolder uses) instead of raw SealedChildRef[].

resolveChildren now soft-skips any per-child resolve/unseal failure
(absent record, ROT-07 rollback rejection, unverified signature, or a
corrupted/unopenable read-body) instead of failing the whole listing --
a listing render must degrade gracefully when one sibling can't be
verified; the already-loaded target folder itself remains gated via
ensureFolderLoaded/dfsFindFolder (Plan 01, D-05). Updated 4 pre-existing
tests whose fixtures use legacy pre-node/v3 SealedChildRef shapes (no
readKeySealed) to assert the correct new behavior: an unresolvable
legacy fixture soft-skips to an empty ResolvedChild[] in the event
payload rather than passing the raw fixture through.

adoptSharedFolderResult is now async (awaits the listing resolve); all
call sites updated. updateSharedFile's file-only-publish emission
explicitly invalidates the parent's listingCache entry before
re-resolving, since a file-content-only republish does not bump the
parent folder's own IPNS sequence (the cache's invalidation clock).

Entire-Checkpoint: 33e8167dd6c6
Missed in the GREEN commit: index.ts must re-export the ResolvedChild
type-only (D-07) so the web can type its listFolder/listSharedFolder
render sites without reaching into the SDK's internal folder-listing
module.

Entire-Checkpoint: 6bfa9bf75366
Documents the ResolvedChild listing API, the event payload retype, and
the widened per-child failure-handling deviation with rationale.

Entire-Checkpoint: ead14078e4dd
- Add uploadBytes/downloadBytes/unpin facade methods on CipherBoxClient
  wrapping sdkCore.addToIpfs/fetchFromIpfs/unpinFromIpfs
- Forward onProgress through both upload and download paths so web
  progress bars keep working
- Unit test asserts onProgress forwarding on upload and download

Entire-Checkpoint: 28089239dd79
…tural utils

- Add getFolderMetadata facade method delegating to the gated
  ensureFolderLoaded path (no gate bypass)
- Re-export getDepth/isDescendantOf/calculateSubtreeDepth/
  selectEncryptionMode from @cipherbox/sdk so the web can stop importing
  these pure utils from sdk-core directly (D-07)

Entire-Checkpoint: 9b761b056b7e
… pending plan 11)

Entire-Checkpoint: 4eeb1b1fc0d3
- Author owner+grantee multi-account Playwright spec proving the owner
  sees a grantee's upload into a shared folder without writing first
- Assert size/modifiedAt render from the resolved listing (ResolvedChild),
  not the em-dash/epoch placeholders from the pre-fix mirror
- Drive the proof via nav-triggered re-resolve (D-03), not the 30s poll
- Expected-red until the Phase 68.2 web cutover (Plans 06-11); green is
  the Plan 12 phase gate

Entire-Checkpoint: 5f412ece0e0d
- CipherBoxClient.bootstrapVaultKeys/serializeVault/deserializeVault mediate
  the useAuth.ts vault-bootstrap crypto (initializeVault/encryptVaultKeys/
  serializeVaultBlobV3/deserializeVaultBlobV3) inside the SDK
- CipherBoxClient.publishEmptyRootNode wraps sdkCore.publishEmptyRootNode
  with this.ctx injected
- CipherBoxClient.deriveRegistryIpnsKeypair/encryptRegistry/decryptRegistry
  mediate device-registry.service.ts's registry crypto
- deserializeVault zeroes the already-unwrapped rootReadKey if the paired
  unwrapKey call fails (T-68.2-09 terminal-owner zeroing)
- Re-export VaultInit/DeviceRegistry types from @cipherbox/sdk
- CipherBoxClient.testConnection wraps sdkCore.testConnection
- CipherBoxClient.resolveConfigBlob/publishConfigBlob wrap
  sdkCore.resolveIpnsRecord/createAndPublishIpnsRecord with this.ctx
  injected -- deliberately NOT routed through the ROT-07
  rotationHighWater.enforceResolved gate (BYO config blob is
  user-configured, not a rotation-governed node)
- fetch/add for the config blob's raw bytes reuse the existing
  downloadBytes/uploadBytes facade methods (no config-blob-specific
  duplicates needed)
- Re-export ConnectionTestResult type from @cipherbox/sdk
FSM1 and others added 13 commits July 6, 2026 09:23
- Adds folder-reresolve.test.ts proving ensureFolderLoaded/listFolder
  never re-resolve an already-loaded folder's IPNS record, closing the
  SDK-READ-03 self-referential cache-clock gap (68.2-VERIFICATION.md).
- Adds forceResolve as an accepted-but-ignored no-op option on
  ensureFolderLoaded/listFolder so RED is a runtime failure, not a
  compile error.

Entire-Checkpoint: 9ca8cc629473
…lders

- ensureFolderLoaded/listFolder gain a forceResolve option: for an
  already-loaded folder, this routes to a new gated in-place re-resolve
  (reresolveFolderInPlace/doReresolveFolderInPlace) instead of the
  verbatim cached short-circuit, closing the SDK-READ-03 self-
  referential cache-clock gap (owner never seeing a grantee's later
  upload without writing first).
- The re-resolve mirrors dfsFindFolder's gate exactly: fail-closed on
  an unverified record before any floor mutation, MAX_SAFE_INTEGER
  overflow guard, then RotationHighWater.enforceResolved sourced from
  existing.nodeGeneration (never the freshly relay-served envelope
  generation). A below-floor resolve still rejects with
  SequenceRegressionError and leaves the stored FolderState untouched.
- FolderState is updated in place (single source of truth, D-09) --
  no second cache or store. Concurrent forceResolve calls for the same
  ipnsName dedupe to one network resolve via reresolveInFlight.
- The no-opts cached fast path used by write chokepoints (requireFolder
  etc.) is unchanged.

Entire-Checkpoint: 7a7f8364ee58
- useFolderNavigation.ts: refreshFolderListing (root + already-loaded legs)
  and the cold-load navigateTo path (ensureFolderLoaded retry loop +
  pre-store listFolder) now pass { forceResolve: true }
- useSyncPolling.ts: invalidateOpenFolder passes { forceResolve: true } to
  both client.listFolder and client.ensureFolderLoaded
- write-mutation chokepoints (useFileBrowserActions.ts, folder-helpers.ts)
  left on the cached fast path, unchanged
- updates useSyncPolling.test.ts mock-call assertions for the new option arg

Entire-Checkpoint: 022a3dd337a4
…eeded — CI web-e2e re-confirm)

Entire-Checkpoint: 5affdba6e4e4
…sions

Gap-closure round 2 for two web-layer regressions the 68.2 read-chain migration
introduced (VERIFICATION gaps; SDK-READ-01..04 unaffected, no SDK change).

Regression A - after the 68.2-11 kind-cache removal, isFileRef(bareSealedChildRef)
always returns false, so identity-only refs classified as folders. This hid the
selection-bar download button and context-menu Download/Edit/Preview items AND
broke the download logic itself: handleDownload/handleBatchDownload filtered
every selected file out. Fixed by classifying against the SDK-resolved
ResolvedChild listing (new isFileRefResolved helper, keyed by ipnsName) at every
download-gating site across the owned-vault and shared browsers.

Regression B - an upload into a subfolder never appeared: UploadZone targets the
open folder (currentFolderId) but onUploadComplete only called handleSync, which
resyncs root only. Fixed by refreshing the open folder immediately via the
existing invalidateOpenFolder(), then the best-effort root/tree resync. Also
force live resolves on the root resync and post-409 resyncFolder for
deterministic-freshness consistency with the nav/poll precedent.

Verified on a fresh-container stack: batch-download 5/5, writable-shares 29/29,
shared-folder-desync 4/4 (SC#5 guard holds). tsc + eslint clean; D-07 boundary
grep gate green (new imports are type-only from the @cipherbox/sdk facade).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7XhtqxJ7pAYkZTmdmKerE
Entire-Checkpoint: a2fc426e2121
…gressions

Plans + summaries for the two VERIFICATION-gap regressions closed in gap-closure
round 2 (fix 0577a34): Regression A (isFileRef kind classification after the
kind-cache removal, plan 15) and Regression B (post-upload refresh targeting root
instead of the open subfolder, plan 16). UAT records the fresh-container
verification: batch-download 5/5, writable-shares 29/29, shared-folder-desync 4/4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7XhtqxJ7pAYkZTmdmKerE
Entire-Checkpoint: e139d069f3c6
Re-verify after gap-closure round 2: VERIFICATION.md -> passed (5/5 must-haves;
the prior human_needed e2e item is now executed-green on fresh containers).
VALIDATION.md -> compliant, 0 gaps. SECURITY.md -> SECURED, 17/17 threats closed.
REQUIREMENTS traceability table marks SDK-READ-01..04 Complete.

Defers two low-priority todos surfaced during ship: SharedFolderRow drag-payload
kind classification (harmless — type unused by the shared drop handler) and
promoting the D-07 web/SDK boundary grep gate to an ESLint/CI rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7XhtqxJ7pAYkZTmdmKerE
Entire-Checkpoint: 64dab836dec3
Applied the clear-cut, low-risk in-scope findings from the local CodeRabbit
review of the phase branch:

- SDK: destroy() now clears the new listingCache + reresolveInFlight maps so no
  resolved listing (private folder structure) or in-flight re-resolve closure
  survives client teardown.
- SDK: in the descendSharedChild hop loop, adopt the minted childReadKey into
  mintedReadKey BEFORE unsealNode so the outer finally zeroes it even if
  unsealNode throws (leak-on-failure fix; terminal-owner buffer, safe to zero).
- web: route handleSync's forced ensureFolderLoaded through runWithFailureUx so
  a gate rejection surfaces the D-05 toast, matching the listFolder leg.
- web: resyncFolder re-reads the store after its awaits and skips the writeback
  if the folder was navigated away/removed (matches refreshFolderListing).
- web: search-index indexes children by their resolved ResolvedChild.kind
  instead of hardcoding 'file', so subfolders no longer index as files.

Deferred the risky/out-of-scope findings as todos (noted in the PR): gating the
non-listing read facades (resolveNodeIdentity/resolveFileMetadata) with the
ROT-07 floor, and a hardening backlog (owned listingCache invalidation,
toResolvedChildView unresolved state, invalidateOpenFolder monotonicity,
refreshSharedFolder write envelope, seq safe-integer, empty-state a11y, test
hardening).

SDK suite 338 passed/49 skipped; web tsc + eslint clean; D-07 gate green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7XhtqxJ7pAYkZTmdmKerE
Entire-Checkpoint: ceb730025e73
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 674fa57d-1c04-4b61-9d1c-abb561bf9f6c

📥 Commits

Reviewing files that changed from the base of the PR and between 218e693 and 63dc156.

📒 Files selected for processing (8)
  • .planning/STATE.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-LEARNINGS.md
  • .planning/todos/pending/2026-07-06-68.2-coderabbit-hardening-backlog.md
  • .planning/todos/pending/2026-07-06-gate-non-listing-read-facades.md
  • apps/web/src/components/file-browser/DetailsDialog.tsx
  • apps/web/src/components/file-browser/FileListItem.tsx
  • apps/web/src/components/file-browser/SelectionActionBar.tsx
  • packages/sdk/src/client.ts
✅ Files skipped from review due to trivial changes (4)
  • .planning/todos/pending/2026-07-06-gate-non-listing-read-facades.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-LEARNINGS.md
  • .planning/todos/pending/2026-07-06-68.2-coderabbit-hardening-backlog.md
  • .planning/STATE.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/src/components/file-browser/SelectionActionBar.tsx
  • apps/web/src/components/file-browser/FileListItem.tsx
  • apps/web/src/components/file-browser/DetailsDialog.tsx
  • packages/sdk/src/client.ts

Walkthrough

Moves gated IPNS read-chain enforcement and resolved-child projection into the SDK, adds SDK facades for transport and vault/registry flows, retypes folder events to ResolvedChild[], and rewires web navigation, rendering, and sync paths to consume SDK-resolved listings. Planning docs, tests, and e2e coverage are updated alongside the code changes.

Changes

SDK-owned read chain and resolved folder listings

Layer / File(s) Summary
Planning documentation
.planning/**
Adds and updates the Phase 68.2 requirements, roadmap/state tracking, plans/summaries, research, patterns, security, validation, verification, UAT, discussion, and todo docs covering the SDK-owned read-chain migration.
SealedChildRef schema revert
packages/core/src/node/{types,encode,decode}.ts, packages/sdk-core/src/folder/metadata-ops.ts, docs/METADATA_SCHEMAS.md
Removes the optional size/modifiedAt mirrors from SealedChildRef, restores the frozen five-field schema, and documents ResolvedChild as the display projection type.
SDK gated read path and listing resolution
packages/sdk/src/{client,folder-listing,events,index}.ts, packages/sdk/src/share/shared-write.ts
Adds ROT-07 gated resolve enforcement, resolveChildren/ResolvedChild, forceResolve re-resolve-in-place with dedup, listing cache, retyped event payloads, and new IPFS/vault/registry/config-blob facade methods.
SDK unit tests
packages/sdk/src/__tests__/*
Adds tests for gating, resolved listing shape/caching, re-resolve dedup, transport facades, vault/registry, BYO-pinning, shared root/child resolution, and node identity resolution; updates existing tests for resolved event payloads.
Web store and freshness wiring
apps/web/src/stores/folder.store.ts, apps/web/src/hooks/{useFolderNavigation,useSyncPolling,folder-helpers,useAuth,useSharedNavigation*,useSharedWriteOps,shared-folder-projection}.ts, apps/web/src/lib/{sdk-provider,clear-user-stores,crypto/key-wrapping}.ts
Collapses the folder store into a ResolvedChild projection, wires navigation/poll freshness via forceResolve and invalidateOpenFolder, adds a bootstrap SDK client for pre-login flows, and removes web-side caches.
File-browser UI repoint
apps/web/src/components/file-browser/**, apps/web/src/utils/fileTypes.ts
Repoints file-list, details, context-menu, selection, and shared-browser components to render and gate actions from resolved listing data instead of raw refs or kind-cache.
Web services SDK migration
apps/web/src/services/**, apps/web/src/hooks/{useDropUpload,useFileOperations,useFileVersions,useFolderMutations,useStreamingPreview}.ts, apps/web/src/lib/version-transforms.ts, settings components
Replaces web-side IPFS/IPNS helpers with SDK facade calls for upload/download/delete/vault-settings/device-registry/BYO-pinning, and deletes ipfs.ts, ipns.service.ts, kind-cache.ts, and useFileSize.ts.
e2e regression coverage
tests/web-e2e/tests/shared-folder-desync.spec.ts, tests/web-e2e/page-objects/file-browser/file-list.page.ts
Adds a multi-account shared-folder desync regression spec and fixes the placeholder locator to em-dash.

Estimated code review effort: 5 (Critical) | ~150 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WebApp
  participant CipherBoxClient
  participant RotationHighWater
  participant SdkCore

  WebApp->>CipherBoxClient: listFolder(ipnsName, { forceResolve })
  CipherBoxClient->>SdkCore: resolveIpnsRecord(ipnsName)
  SdkCore-->>CipherBoxClient: published, sequenceNumber, signatureVerified
  CipherBoxClient->>RotationHighWater: enforceResolved(generation, seq, versionFloor)
  RotationHighWater-->>CipherBoxClient: ok or SequenceRegressionError
  CipherBoxClient->>CipherBoxClient: resolveChildren(children, parentReadKey)
  CipherBoxClient-->>WebApp: ResolvedChild[]
Loading
sequenceDiagram
  participant UploadZone
  participant useSyncPolling
  participant FolderStore
  participant CipherBoxClient

  UploadZone->>useSyncPolling: invalidateOpenFolder()
  useSyncPolling->>CipherBoxClient: listFolder / ensureFolderLoaded (forceResolve)
  CipherBoxClient-->>useSyncPolling: children, rawChildren, sequenceNumber
  useSyncPolling->>FolderStore: update projection fields
Loading

Possibly related PRs

  • FSM1/cipher-box#498: Both PRs modify CipherBoxClient.ensureFolderLoaded/folder-loading behavior in packages/sdk, including gated re-resolve and forceResolve mechanics.
  • FSM1/cipher-box#587: The main PR's ROT-07 gating via RotationHighWater.enforceResolved directly builds on this PR's durable rotation high-water/fail-closed implementation.
  • FSM1/cipher-box#494: Both PRs refactor apps/web/src/stores/folder.store.ts to make it a pure SDK-event-driven projection.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the PR’s main change: moving the read chain and folder listings into the SDK.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sdk-owned-read-chain-and-resolved-folder-listings

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added release:sdk:feat Minor version bump (new feature) for sdk release:web:feat Minor version bump (new feature) for web labels Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Release Preview

Package Bump Label Source
sdk minor release:sdk:feat Direct (feat commit)
web minor release:web:feat Direct (feat commit)

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.72000% with 108 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.93%. Comparing base (1fb8996) to head (63dc156).

Files with missing lines Patch % Lines
packages/sdk/src/client.ts 81.56% 108 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main     #589       +/-   ##
===========================================
+ Coverage   65.13%   79.93%   +14.80%     
===========================================
  Files         152      117       -35     
  Lines       13977    10333     -3644     
  Branches     1603     1698       +95     
===========================================
- Hits         9104     8260      -844     
+ Misses       4631     1828     -2803     
- Partials      242      245        +3     
Flag Coverage Δ
api 79.93% <82.72%> (+0.23%) ⬆️
api-client 79.93% <82.72%> (+0.23%) ⬆️
core 79.93% <82.72%> (+0.23%) ⬆️
crypto 79.93% <82.72%> (+0.23%) ⬆️
desktop ?
sdk 79.93% <82.72%> (+0.23%) ⬆️
sdk-core 79.93% <82.72%> (+0.23%) ⬆️

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

Files with missing lines Coverage Δ
packages/core/src/node/decode.ts 32.93% <100.00%> (-1.84%) ⬇️
packages/core/src/node/encode.ts 88.75% <100.00%> (-0.41%) ⬇️
packages/sdk-core/src/folder/metadata-ops.ts 100.00% <ø> (+4.00%) ⬆️
packages/sdk/src/events.ts 100.00% <ø> (ø)
packages/sdk/src/folder-listing.ts 100.00% <100.00%> (ø)
packages/sdk/src/share/shared-write.ts 84.36% <ø> (-0.10%) ⬇️
packages/sdk/src/client.ts 70.10% <81.56%> (+2.60%) ⬆️

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

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR moves the gated IPNS read chain (ROT-07 anti-rollback, fail-closed on unverified signatures) entirely into packages/sdk, exposes ResolvedChild[] folder listings that carry pre-resolved kind/size/modifiedAt, and collapses the web's folder store to a thin projection of SDK state. Parallel web read services (ipns.service, file-metadata.service, kind-cache) are deleted, and a D-07 grep gate enforces that the web makes zero runtime calls into @cipherbox/sdk-core or raw IPFS/IPNS.

  • SDK read chain (SDK-READ-01/02): listFolder, listSharedFolder, resolveChildIdentity, downloadSharedFile, and resolveShareRoot added to CipherBoxClient; all funnelled through gatedResolveChild (ROT-07 + signatureVerified fail-close, Number.MAX_SAFE_INTEGER overflow guard, parent-mirror generation sourcing). Partial-listing cache-set prevention (post-previous-review fix) is in place.
  • Folder store projection (SDK-READ-03): FolderNode.children is now ResolvedChild[]; rawChildren?: SealedChildRef[] carries the write-path identity mirror and is populated by the nav (refreshFolderListing), poll (invalidateOpenFolder), and upload (onUploadComplete → invalidateOpenFolder) legs — but not by the folder:updated event subscription.
  • Freshness design (D-03): nav re-resolve and 30 s poll update both children and rawChildren; the upload path is also explicitly refreshed via invalidateOpenFolder.

Confidence Score: 4/5

Safe to merge with one real-state gap in the private-vault file browser: mutations other than upload leave the FileList item source stale for up to 30 s.

The SDK read chain, ROT-07 gating, key lifecycle, shared-folder listing, and D-07 boundary enforcement all look correct. The gap is in the folder:updated event subscription in folder.store.ts: it updates the resolved display projection (children) but never calls updateFolderRawChildren, so FileBrowser.tsx's rawChildren-based FileList.items and hasChildren gate go stale after delete, create, rename, or move — showing ghost rows for deleted items and hiding newly created ones until the next sync tick. Upload is handled correctly via explicit invalidateOpenFolder; the mutation path is the gap.

apps/web/src/stores/folder.store.ts (folder:updated subscription does not call updateFolderRawChildren) and apps/web/src/hooks/useFolderMutations.ts / useFileBrowserActions.ts (no post-mutation rawChildren refresh).

Important Files Changed

Filename Overview
packages/sdk/src/folder-listing.ts New module: resolves SealedChildRef[] into ResolvedChild[] via a caller-supplied gated resolve fn; correct per-child error isolation, key zeroing, and generation-source rule.
packages/sdk/src/client.ts Adds listingCache, reresolveInFlight, listFolder, listSharedFolder, resolveChildIdentity, downloadSharedFile, resolveShareRoot, and gatedResolveChild — all with correct ROT-07 gating and key lifecycle. Partial-listing cache-set prevention (post-previous-review fix) is in place.
packages/sdk/src/events.ts Event children type changed from SealedChildRef[] to ResolvedChild[]; all emission sites in client.ts correctly call resolveListingChildren.
apps/web/src/stores/folder.store.ts Adds rawChildren field and updateFolderRawChildren action; folder:updated subscription updates only children (resolved) — not rawChildren — leaving the FileList items source stale after delete/create/move until the next 30 s poll.
apps/web/src/components/file-browser/FileBrowser.tsx Uses rawChildren as FileList.items and for the hasChildren gate; upload path is refreshed correctly via invalidateOpenFolder, but other mutations leave rawChildren stale for up to 30 s.
apps/web/src/hooks/useFolderNavigation.ts Adds refreshFolderListing (nav re-resolve) and setCurrentFolder calls on root/already-loaded paths; correctly populates both children and rawChildren on nav events.
apps/web/src/hooks/useSyncPolling.ts Exports invalidateOpenFolder as the D-03 poll leg; correctly updates both resolved children and rawChildren for the open folder on every 30 s tick.
apps/web/src/hooks/useSharedNavigationActions.ts Removes web-side read-chain walk; delegates to client.resolveShareRoot / client.descendSharedChild; key lifecycle (committed flag, fill(0)) is correct for both file and folder share roots.
apps/web/src/hooks/useSharedNavigation.ts Adds resolvedChildren via listSharedFolder(shareId, []) effect; path=[] always reads the current sharedFolderTree depth which is correctly updated before the effect fires by seedActiveSharedFolder.
apps/web/src/utils/fileTypes.ts Adds isFileRefResolved for kind classification against the resolved listing; isFileRef falls back gracefully when no ResolvedChild is available.
apps/web/src/hooks/folder-helpers.ts resyncFolder now routes through SDK's gated read path; correctly calls both listFolder and ensureFolderLoaded with forceResolve and updates rawChildren.
apps/web/src/hooks/shared-folder-projection.ts subscribeSharedFolderProjection now reads raw children from getSharedFolderState instead of event.children (which is now ResolvedChild[]); correctly supplies raw SealedChildRef[] to apply.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Web as Web (FileBrowser)
    participant Store as folder.store
    participant SDK as CipherBoxClient
    participant Net as IPFS/IPNS

    Note over Web,Net: Folder load (nav re-resolve)
    Web->>SDK: listFolder(ipnsName, forceResolve)
    SDK->>Net: resolveIpnsRecord + fetchFromIpfs
    SDK->>SDK: gatedResolveChild (ROT-07)
    SDK->>SDK: "resolveListingChildren -> ResolvedChild[]"
    SDK-->>Web: ResolvedChild[]
    Web->>SDK: ensureFolderLoaded(ipnsName, forceResolve)
    SDK-->>Web: FolderState (SealedChildRef[])
    Web->>Store: updateFolderChildren(resolved)
    Web->>Store: updateFolderRawChildren(raw)

    Note over Web,Net: Folder mutation (delete/create/move)
    Web->>SDK: deleteToBin / createFolder / renameItem
    SDK->>Net: publish new folder state
    SDK->>SDK: "resolveListingChildren -> ResolvedChild[]"
    SDK-->>Store: folder:updated (ResolvedChild[])
    Store->>Store: updateFolderChildren only
    Note over Store: rawChildren NOT updated until next 30 s poll

    Note over Web,Net: 30 s poll leg (D-03)
    Web->>SDK: invalidateOpenFolder()
    SDK-->>Web: ResolvedChild[] + FolderState
    Web->>Store: updateFolderChildren + updateFolderRawChildren
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 Web as Web (FileBrowser)
    participant Store as folder.store
    participant SDK as CipherBoxClient
    participant Net as IPFS/IPNS

    Note over Web,Net: Folder load (nav re-resolve)
    Web->>SDK: listFolder(ipnsName, forceResolve)
    SDK->>Net: resolveIpnsRecord + fetchFromIpfs
    SDK->>SDK: gatedResolveChild (ROT-07)
    SDK->>SDK: "resolveListingChildren -> ResolvedChild[]"
    SDK-->>Web: ResolvedChild[]
    Web->>SDK: ensureFolderLoaded(ipnsName, forceResolve)
    SDK-->>Web: FolderState (SealedChildRef[])
    Web->>Store: updateFolderChildren(resolved)
    Web->>Store: updateFolderRawChildren(raw)

    Note over Web,Net: Folder mutation (delete/create/move)
    Web->>SDK: deleteToBin / createFolder / renameItem
    SDK->>Net: publish new folder state
    SDK->>SDK: "resolveListingChildren -> ResolvedChild[]"
    SDK-->>Store: folder:updated (ResolvedChild[])
    Store->>Store: updateFolderChildren only
    Note over Store: rawChildren NOT updated until next 30 s poll

    Note over Web,Net: 30 s poll leg (D-03)
    Web->>SDK: invalidateOpenFolder()
    SDK-->>Web: ResolvedChild[] + FolderState
    Web->>Store: updateFolderChildren + updateFolderRawChildren
Loading

Reviews (3): Last reviewed commit: "fix(68.2): address second-round CodeRabb..." | Re-trigger Greptile

Comment thread packages/sdk/src/client.ts
Comment thread apps/web/src/components/file-browser/SelectionActionBar.tsx
FSM1 and others added 2 commits July 6, 2026 17:13
- SDK (P1): do not cache a PARTIAL folder listing. resolveListingChildren cached
  the result of resolveChildren even when a child was dropped (transient network
  / ROT-07 / AEAD failure), pinning the incomplete listing at that sequenceNumber
  until a remote write or reload -- even the forceResolve poll/nav paths reuse a
  same-sequence entry. Skip the cache-set when resolved.length < children.length
  so a later resolve reattempts the dropped child and self-heals.
- web (P2): memoize SelectionActionBar's resolvedByIpnsName map so it is not
  reallocated on every render (resolvedChildren is a fresh array reference each
  parent render).

SDK suite 338 passed/49 skipped; web tsc + eslint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7XhtqxJ7pAYkZTmdmKerE
Entire-Checkpoint: a1094715b3eb
Decisions (single gated read entrypoint, D-07 full boundary, ResolvedChild,
gate-then-delete ordering, revert-mirror-last), lessons (post-mutation refresh
targets the open folder, isFileRef vs bare refs, don't cache partial listings,
tee-republish infra gate, large-PR review skips, terminal-owner zeroization),
patterns, and surprises from the phase.

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

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

Caution

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

⚠️ Outside diff range comments (2)
tests/web-e2e/page-objects/file-browser/file-list.page.ts (1)

155-164: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the em-dash placeholder in getItemType
getItemType still checks for '-', but FileListItem.tsx renders folders with , so the helper returns 'file' for folders. Update the comparison and docstring to match the current UI.

🤖 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/file-list.page.ts` around lines 155 -
164, Update the FileBrowser page object helper so getItemType matches the
current FileListItem UI: it should treat the em-dash placeholder as a folder,
not the hyphen, and the comment above the method should describe that same
behavior. Use the getItemType method and the .file-list-item-size locator to
locate the logic, and change the comparison to align with the folder rendering
used by FileListItem.tsx.
packages/sdk/src/client.ts (1)

3583-3627: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

maybeRepublishFolderForFileMigration emits stale listing data after a file-only publish — missing the cache invalidation applied to the equivalent shared-file path.

This is the emission path for replaceFile/restoreFileVersion/deleteFileVersion. When migratedIpnsPrivateKeyEncrypted is absent (the common case), the folder is not republished, so live.sequenceNumber is unchanged. resolveListingChildren(live.children, live.folderKey, folderIpnsName, live.sequenceNumber) then hits listingCache at the SAME (ipnsName, sequenceNumber) key that was cached before the file's content/size/modifiedAt changed — so the emitted folder:updated event serves stale ResolvedChild data for the just-updated file.

Compare with the shared-file counterpart, updateSharedFile (Lines 5024–5047), which explicitly guards against this exact scenario:

"the just-updated FILE's own PublishedNode (content/modifiedAt) is now stale in listingCache if it was resolved before this update. Invalidate the parent's cache entry..."

and calls this.listingCache.delete(live.ipnsName) before resolving. The owned-file path never applies the same fix, so replacing a file, restoring a version, or deleting a version silently leaves stale size/modifiedAt in the web's resolved listing until an unrelated folder-level mutation happens to bump the sequence.

🩹 Proposed fix
     const live = this.folderTree.get(folderIpnsName) ?? folder;
+    // A file-only publish (replaceFile/restoreFileVersion/deleteFileVersion)
+    // changes the FILE's own PublishedNode without bumping the PARENT
+    // folder's sequenceNumber -- invalidate the cache entry so the
+    // resolve below re-derives every child instead of serving a stale
+    // cached size/modifiedAt for the just-updated file (mirrors the
+    // updateSharedFile fix at 68.2-02 Rule 1).
+    this.listingCache.delete(folderIpnsName);
     this.emitter.emit({
       type: 'folder:updated',
       folderId: folderIpnsName,
       ipnsName: folderIpnsName,
       children: await this.resolveListingChildren(
         live.children,
         live.folderKey,
         folderIpnsName,
         live.sequenceNumber
       ),
       sequenceNumber: live.sequenceNumber,
     });
🤖 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/sdk/src/client.ts` around lines 3583 - 3627, The folder update
emission in maybeRepublishFolderForFileMigration can return stale listing data
when no republish happens because resolveListingChildren reuses listingCache at
the same ipnsName/sequenceNumber key. Mirror the cache invalidation used in
updateSharedFile by clearing the relevant listing cache entry before resolving
and emitting folder:updated for the common file-only path, then re-resolve
children from the fresh state using live folder data.
🧹 Nitpick comments (4)
apps/web/src/components/file-browser/ContextMenu.tsx (1)

99-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate ipnsName → ResolvedChild map construction across components.

The same new Map(resolvedChildren.map((r) => [r.ipnsName, r])) pattern also appears in FileList.tsx (and likely other file-browser components in this cohort). Consider extracting a small shared helper (e.g., alongside isFileRefResolved in fileTypes.ts) to avoid re-implementing this lookup in each component.

♻️ Suggested shared helper
+// apps/web/src/utils/fileTypes.ts
+export function buildResolvedByIpnsName(resolvedChildren: ResolvedChild[]): Map<string, ResolvedChild> {
+  return new Map(resolvedChildren.map((r) => [r.ipnsName, r]));
+}
-  const resolvedByIpnsName = useMemo(
-    () => new Map((resolvedChildren ?? []).map((r) => [r.ipnsName, r])),
-    [resolvedChildren]
-  );
+  const resolvedByIpnsName = useMemo(
+    () => buildResolvedByIpnsName(resolvedChildren ?? []),
+    [resolvedChildren]
+  );
🤖 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/ContextMenu.tsx` around lines 99 - 101,
Duplicate ipnsName to ResolvedChild map construction is repeated in ContextMenu
and other file-browser components. Extract the lookup logic into a shared helper
near isFileRefResolved in fileTypes.ts, then update ContextMenu and FileList to
use that helper instead of rebuilding the Map inline. Keep the helper focused on
taking resolvedChildren and returning the ipnsName keyed lookup so all
components share the same implementation.
apps/web/src/components/file-browser/useFileBrowserActions.ts (1)

152-173: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

handleSync resolves the same folder twice. listFolder(rootIpnsName, { forceResolve: true }) already advances the cached FolderState; the follow-up ensureFolderLoaded(..., { forceResolve: true }) does another network re-resolve and can make children and rawChildren come from different snapshots if a publish lands between awaits. Resolve once, then reuse the already-loaded folder state for the second read.

🤖 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/useFileBrowserActions.ts` around lines
152 - 173, handleSync is resolving the same folder twice, which can produce
mismatched snapshots between children and rawChildren. Update the logic in
useFileBrowserActions.handleSync so listFolder(rootIpnsName, { forceResolve:
true }) performs the single resolve, then reuse that same FolderState result
instead of calling ensureFolderLoaded(..., { forceResolve: true }) again. Keep
the updates to updateFolderChildren, updateFolderRawChildren, and
updateFolderSequence sourced from the one resolved state.
packages/sdk/src/__tests__/client-shared-write.test.ts (1)

132-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test coverage regression: sharedFolder:updated.children assertions reduced to trivial [] checks.

These three tests now assert updated.children equals [] because the legacy PUBLISHED_CHILDREN/REFRESHED_CHILDREN fixtures lack readKeySealed and are soft-skipped by the resolve path — the tests no longer verify that sharedFolder:updated actually carries resolved ResolvedChild[] content for real children. folder-listing.test.ts's buildChildFixture (using sealNode/sealChildReadKey) already shows the pattern for building fixtures the resolver can open; consider reusing it here so these assertions test real resolution instead of the empty-array fallback path.

Also applies to: 192-202, 225-233

🤖 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/sdk/src/__tests__/client-shared-write.test.ts` around lines 132 -
143, The sharedFolder:updated tests are now only checking the empty-array
fallback instead of real resolved children, so update the fixtures used in
client-shared-write.test to produce resolver-openable children with
readKeySealed. Reuse the fixture-building pattern from folder-listing.test and
buildChildFixture (including sealNode/sealChildReadKey) so
sharedFolder:updated.children is asserted as actual ResolvedChild[] content
rather than [] in the affected test cases.

Source: Path instructions

packages/sdk/src/__tests__/client.test.ts (1)

252-261: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same test-quality gap as client-shared-write.test.ts: folder:loaded.children assertion reduced to [].

The mockChildren fixture lacks readKeySealed, so it's soft-skipped and the assertion only proves the skip path works, not that folder:loaded carries correctly resolved ResolvedChild[] data. Consider reusing the buildChildFixture pattern from folder-listing.test.ts to assert meaningful resolved content here too.

🤖 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/sdk/src/__tests__/client.test.ts` around lines 252 - 261, The
`folder:loaded.children` check in `client.test.ts` is too weak because
`mockChildren` is being soft-skipped, so it only verifies the empty-skip path
instead of real resolved payloads. Update the `folder:loaded` assertion in the
relevant test to use a properly resolved fixture, following the
`buildChildFixture` pattern from `folder-listing.test.ts`, and assert the actual
`ResolvedChild[]` content via `loadedEvent.children` rather than expecting an
empty array.

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/STATE.md:
- Around line 29-34: The Phase 68.2 status snapshot is stale and no longer
matches the roadmap progress. Update the state block in STATE.md and the
corresponding roadmap entry in ROADMAP.md so they reflect 14 of 14 plans
complete and Completed 68.2-14-PLAN.md, keeping the Phase 68.2 summary, plan
count, and completion text consistent across both documents.

In `@apps/web/src/components/file-browser/DetailsDialog.tsx`:
- Around line 52-78: The dialog classification in DetailsDialog is using only
folderStore membership via isFolderHeuristic, which can misidentify resolved
folders as files. Update the logic around resolvedItem in DetailsDialog so the
SDK-authoritative resolvedChildren entry drives the kind when present, and use
the folderStore heuristic only as a fallback when no resolved child exists. Make
sure the downstream decisions that depend on isFolderHeuristic, including the
title, effect behavior, and FileDetails/FolderDetails selection, reference the
resolved kind first and keep the existing fallback behavior for missing resolved
items.

In `@apps/web/src/components/file-browser/FileList.tsx`:
- Around line 23-36: `toResolvedChildView` is using `modifiedAt: 0` as a
fallback sentinel, but `FileListItem` treats that as a valid timestamp and shows
a bogus 1970 date instead of the placeholder. Update the display logic in
`FileListItem` (the `dateDisplay`/`sizeDisplay` handling) to treat missing
unresolved entries consistently with `size: undefined`, and render the “—”
placeholder when `modifiedAt` is absent or represents the fallback sentinel
rather than a real timestamp.

In `@apps/web/src/hooks/useFolderNavigation.ts`:
- Around line 59-82: The refreshFolderListing function is doing two forced
re-resolves during cold navigation by calling both client.listFolder(...) and
client.ensureFolderLoaded(..., { forceResolve: true }), which duplicates the
IPNS work. Update refreshFolderListing to avoid the second forced resolve by
either removing forceResolve from the ensureFolderLoaded call or reusing the
state returned from the first refresh path, while preserving the existing
store.updateFolderChildren, store.updateFolderRawChildren, and
store.updateFolderSequence updates.

In `@apps/web/src/hooks/useSharedNavigation.ts`:
- Around line 355-375: The resolved-listing projection in useSharedNavigation is
running before the shared folder seed is complete, so
listSharedFolder(currentShareId, []) can see an uninitialized sharedFolderTree
and leave resolvedChildren empty or stale. Update the flow so
seedActiveSharedFolder completes before this effect can project, or gate the
effect on seed completion using the existing useEffect logic around currentView,
currentShareId, hasSdkClient(), and listSharedFolder. Keep the projection tied
to the seeded state so it only runs once the shared tree is ready.

In `@packages/sdk/src/client.ts`:
- Around line 3873-3925: The resolveFileMetadata path is bypassing the ROT-07
anti-rollback gate by resolving fileRef.ipnsName directly. Update
resolveFileMetadata to go through gatedResolveChild(fileRef) before unsealing
and calling sdkCore.resolveFileMetadata, matching the same enforcement used by
resolveChildIdentity and descendSharedChild. Keep the existing readKey
recovery/zeroing flow, but ensure the gated child resolution is the source of
the validated node metadata.

In `@packages/sdk/src/folder-listing.ts`:
- Around line 87-127: `resolveChildren` is doing per-child resolution
sequentially, which creates avoidable N-round-trip blocking on large folders.
Refactor the child loop in `resolveChildren` to run `gatedResolve`,
`unsealChildReadKey`, and `unsealNode` through a bounded-concurrency limiter
(reuse the existing `p-limit` pattern used elsewhere, and thread the limiter in
from `resolveListingChildren`). Keep the current skip-on-failure semantics and
`childReadKey` cleanup, but make sure the new implementation preserves result
ordering by `children` input while avoiding serial awaits.

---

Outside diff comments:
In `@packages/sdk/src/client.ts`:
- Around line 3583-3627: The folder update emission in
maybeRepublishFolderForFileMigration can return stale listing data when no
republish happens because resolveListingChildren reuses listingCache at the same
ipnsName/sequenceNumber key. Mirror the cache invalidation used in
updateSharedFile by clearing the relevant listing cache entry before resolving
and emitting folder:updated for the common file-only path, then re-resolve
children from the fresh state using live folder data.

In `@tests/web-e2e/page-objects/file-browser/file-list.page.ts`:
- Around line 155-164: Update the FileBrowser page object helper so getItemType
matches the current FileListItem UI: it should treat the em-dash placeholder as
a folder, not the hyphen, and the comment above the method should describe that
same behavior. Use the getItemType method and the .file-list-item-size locator
to locate the logic, and change the comparison to align with the folder
rendering used by FileListItem.tsx.

---

Nitpick comments:
In `@apps/web/src/components/file-browser/ContextMenu.tsx`:
- Around line 99-101: Duplicate ipnsName to ResolvedChild map construction is
repeated in ContextMenu and other file-browser components. Extract the lookup
logic into a shared helper near isFileRefResolved in fileTypes.ts, then update
ContextMenu and FileList to use that helper instead of rebuilding the Map
inline. Keep the helper focused on taking resolvedChildren and returning the
ipnsName keyed lookup so all components share the same implementation.

In `@apps/web/src/components/file-browser/useFileBrowserActions.ts`:
- Around line 152-173: handleSync is resolving the same folder twice, which can
produce mismatched snapshots between children and rawChildren. Update the logic
in useFileBrowserActions.handleSync so listFolder(rootIpnsName, { forceResolve:
true }) performs the single resolve, then reuse that same FolderState result
instead of calling ensureFolderLoaded(..., { forceResolve: true }) again. Keep
the updates to updateFolderChildren, updateFolderRawChildren, and
updateFolderSequence sourced from the one resolved state.

In `@packages/sdk/src/__tests__/client-shared-write.test.ts`:
- Around line 132-143: The sharedFolder:updated tests are now only checking the
empty-array fallback instead of real resolved children, so update the fixtures
used in client-shared-write.test to produce resolver-openable children with
readKeySealed. Reuse the fixture-building pattern from folder-listing.test and
buildChildFixture (including sealNode/sealChildReadKey) so
sharedFolder:updated.children is asserted as actual ResolvedChild[] content
rather than [] in the affected test cases.

In `@packages/sdk/src/__tests__/client.test.ts`:
- Around line 252-261: The `folder:loaded.children` check in `client.test.ts` is
too weak because `mockChildren` is being soft-skipped, so it only verifies the
empty-skip path instead of real resolved payloads. Update the `folder:loaded`
assertion in the relevant test to use a properly resolved fixture, following the
`buildChildFixture` pattern from `folder-listing.test.ts`, and assert the actual
`ResolvedChild[]` content via `loadedEvent.children` rather than expecting an
empty array.
🪄 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: ffcd8adf-a864-4b53-821f-d74b058c7203

📥 Commits

Reviewing files that changed from the base of the PR and between 1fb8996 and 218e693.

📒 Files selected for processing (131)
  • .planning/REQUIREMENTS.md
  • .planning/ROADMAP.md
  • .planning/STATE.md
  • .planning/codebase/INTEGRATIONS.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-01-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-01-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-02-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-02-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-03-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-03-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-04-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-04-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-05-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-05-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-06-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-06-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-07-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-07-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-08-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-08-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-09-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-09-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-10-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-10-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-11-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-11-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-12-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-12-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-13-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-13-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-14-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-14-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-15-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-15-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-16-PLAN.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-16-SUMMARY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-CONTEXT.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-DISCUSSION-LOG.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-PATTERNS.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-RESEARCH.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-SECURITY.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-UAT.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-VALIDATION.md
  • .planning/phases/68.2-sdk-owned-read-chain-and-resolved-folder-listings/68.2-VERIFICATION.md
  • .planning/todos/completed/2026-07-06-d03-poll-invalidation-lacks-automated-coverage.md
  • .planning/todos/pending/2026-07-06-68.2-coderabbit-hardening-backlog.md
  • .planning/todos/pending/2026-07-06-d07-boundary-eslint-rule.md
  • .planning/todos/pending/2026-07-06-gate-non-listing-read-facades.md
  • .planning/todos/pending/2026-07-06-sharedfolderrow-drag-kind-classification.md
  • apps/web/src/components/file-browser/ContextMenu.tsx
  • apps/web/src/components/file-browser/DetailsDialog.tsx
  • apps/web/src/components/file-browser/FileBrowser.tsx
  • apps/web/src/components/file-browser/FileList.tsx
  • apps/web/src/components/file-browser/FileListItem.tsx
  • apps/web/src/components/file-browser/MoveDialog.tsx
  • apps/web/src/components/file-browser/ReplaceFileDialog.tsx
  • 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/TextEditorDialog.tsx
  • apps/web/src/components/file-browser/details/FileDetails.tsx
  • apps/web/src/components/file-browser/details/FolderDetails.tsx
  • apps/web/src/components/file-browser/details/VersionHistory.tsx
  • apps/web/src/components/file-browser/useFileBrowserActions.ts
  • apps/web/src/components/settings/ConnectionTest.tsx
  • apps/web/src/components/settings/StorageTab.tsx
  • apps/web/src/components/settings/VaultTab.tsx
  • apps/web/src/hooks/__tests__/useSharedWriteOps.test.ts
  • apps/web/src/hooks/__tests__/useSyncPolling.test.ts
  • apps/web/src/hooks/folder-helpers.ts
  • apps/web/src/hooks/shared-folder-projection.ts
  • apps/web/src/hooks/useAuth.ts
  • apps/web/src/hooks/useDropUpload.ts
  • apps/web/src/hooks/useFileOperations.ts
  • apps/web/src/hooks/useFileSize.ts
  • apps/web/src/hooks/useFileVersions.ts
  • apps/web/src/hooks/useFolderMutations.ts
  • apps/web/src/hooks/useFolderNavigation.ts
  • apps/web/src/hooks/useSharedNavigation.ts
  • apps/web/src/hooks/useSharedNavigationActions.ts
  • apps/web/src/hooks/useSharedWriteOps.ts
  • apps/web/src/hooks/useStreamingPreview.ts
  • apps/web/src/hooks/useSyncPolling.ts
  • apps/web/src/lib/api/ipfs.ts
  • apps/web/src/lib/clear-user-stores.ts
  • apps/web/src/lib/crypto/key-wrapping.ts
  • apps/web/src/lib/kind-cache.ts
  • apps/web/src/lib/sdk-provider.ts
  • apps/web/src/lib/version-transforms.ts
  • apps/web/src/services/__tests__/ipns.service.test.ts
  • apps/web/src/services/delete.service.test.ts
  • apps/web/src/services/delete.service.ts
  • apps/web/src/services/device-registry.service.ts
  • apps/web/src/services/download.service.ts
  • apps/web/src/services/index.ts
  • apps/web/src/services/ipns.service.ts
  • apps/web/src/services/search-index.service.ts
  • apps/web/src/services/streaming-crypto.service.ts
  • apps/web/src/services/upload.service.ts
  • apps/web/src/services/vault-settings.service.ts
  • apps/web/src/stores/__tests__/folder.store.test.ts
  • apps/web/src/stores/folder.store.ts
  • apps/web/src/stores/vault-settings.store.ts
  • apps/web/src/utils/fileTypes.ts
  • docs/METADATA_SCHEMAS.md
  • packages/core/src/__tests__/node-codec.test.ts
  • packages/core/src/node/decode.ts
  • packages/core/src/node/encode.ts
  • packages/core/src/node/types.ts
  • packages/sdk-core/src/folder/metadata-ops.ts
  • packages/sdk/src/__tests__/byo-pinning-facade.test.ts
  • packages/sdk/src/__tests__/client-shared-write.test.ts
  • packages/sdk/src/__tests__/client.test.ts
  • packages/sdk/src/__tests__/descend-shared-child.test.ts
  • packages/sdk/src/__tests__/download-shared-file.test.ts
  • packages/sdk/src/__tests__/folder-listing-gate.test.ts
  • packages/sdk/src/__tests__/folder-listing.test.ts
  • packages/sdk/src/__tests__/folder-metadata-facade.test.ts
  • packages/sdk/src/__tests__/folder-reresolve.test.ts
  • packages/sdk/src/__tests__/ipfs-transport-facade.test.ts
  • packages/sdk/src/__tests__/resolve-child-identity.test.ts
  • packages/sdk/src/__tests__/resolve-node-identity.test.ts
  • packages/sdk/src/__tests__/resolve-share-root.test.ts
  • packages/sdk/src/__tests__/vault-registry-facade.test.ts
  • packages/sdk/src/client.ts
  • packages/sdk/src/events.ts
  • packages/sdk/src/folder-listing.ts
  • packages/sdk/src/index.ts
  • packages/sdk/src/share/shared-write.ts
  • tests/web-e2e/page-objects/file-browser/file-list.page.ts
  • tests/web-e2e/tests/shared-folder-desync.spec.ts
💤 Files with no reviewable changes (10)
  • apps/web/src/lib/api/ipfs.ts
  • apps/web/src/services/index.ts
  • apps/web/src/hooks/useFileSize.ts
  • apps/web/src/lib/clear-user-stores.ts
  • apps/web/src/services/ipns.service.ts
  • apps/web/src/services/tests/ipns.service.test.ts
  • packages/sdk/src/share/shared-write.ts
  • packages/core/src/tests/node-codec.test.ts
  • packages/sdk-core/src/folder/metadata-ops.ts
  • apps/web/src/lib/kind-cache.ts

Comment thread .planning/STATE.md Outdated
Comment thread apps/web/src/components/file-browser/DetailsDialog.tsx Outdated
Comment thread apps/web/src/components/file-browser/FileList.tsx
Comment thread apps/web/src/hooks/useFolderNavigation.ts
Comment thread apps/web/src/hooks/useSharedNavigation.ts
Comment thread packages/sdk/src/client.ts
Comment thread packages/sdk/src/folder-listing.ts
Fix three in-scope findings from CodeRabbit's re-review of the fix commits;
defer the rest as typed todos (freshness/perf changes too risky to land late
in a verified-green phase).

- DetailsDialog: derive the file/folder kind from the SDK-resolved parent
  listing (authoritative for un-visited subfolders) and fall back to
  folderStore membership only on a listing miss -- the prior folderStore-only
  heuristic misclassified an un-navigated folder as a file, rendering
  FileDetails and skipping folder-metadata resolution.
- FileListItem: treat the `modifiedAt: 0` unresolved sentinel as the "—"
  placeholder instead of rendering the 1970 epoch.
- STATE.md: sync the Phase 68.2 snapshot to 14 of 14 plans (matches ROADMAP).

Deferred to todos:
- resolveFileMetadata/downloadFromIpns ROT-07 gating -> gate-non-listing-read-facades
- double forced re-resolve, shared-seed ordering, resolveChildren parallelism
  -> 68.2 CodeRabbit hardening backlog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7XhtqxJ7pAYkZTmdmKerE
Entire-Checkpoint: fa4a41c813c7
@FSM1 FSM1 merged commit 6534c64 into main Jul 6, 2026
31 checks passed
@FSM1 FSM1 deleted the feat/sdk-owned-read-chain-and-resolved-folder-listings branch July 6, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:sdk:feat Minor version bump (new feature) for sdk 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