Skip to content

feat: rotation soundness — content-key, inner-grant, concurrent-add, crash-safe resume#582

Merged
FSM1 merged 45 commits into
mainfrom
feat/rotation-soundness-revocation-guarantees
Jun 29, 2026
Merged

feat: rotation soundness — content-key, inner-grant, concurrent-add, crash-safe resume#582
FSM1 merged 45 commits into
mainfrom
feat/rotation-soundness-revocation-guarantees

Conversation

@FSM1

@FSM1 FSM1 commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Phase 64 — Rotation Soundness: Revocation Guarantees

Makes the Phase-63 rotation engine cryptographically sound by filling its four stubbed seams and hardening the multi-node BFS walk, then proving it against a live local API stack. Read-revocation now closes all three crypto gaps and survives a crash mid-walk (within the achievable mid-milestone scope).

What changed

  • CRIT-1 / ROT-03mintFileKeyOnRotate mints a fresh fileKey' on file rotation so a holder of the old readKey/fileKey cannot decrypt the next published version (lazy contentRekeyPending wiring deferred to Phase 65 — the node/v3 schema is frozen this phase).
  • HIGH-3 / ROT-04reMintGrantsRootedAt re-mints readDescriptorRef (ECIES) for every non-revoked recipient incl. inner grants and deletes the revoked recipient's row, behind a transport-decoupled callback seam (live shares schema is Phase 66).
  • HIGH-4 / ROT-05mergeChildren three-way merge + mergeConcurrentChildren on a CAS-409 re-fetch the parent, merge concurrently-added SealedChildRefs, and re-seal — a concurrent add is never dropped.
  • ROT-06verifySubtreeClean rebuilds the resume frontier from published IPNS truth; the D-07 walk hardening lands (completedNodeIds ordering, resume guard, terminal-status persist, queue-key zeroization, per-node convergence guard).
  • D-01 fail-closed — deleted the placeholder IPNS-publish fallback; the engine throws without a real per-node key (test keys via nodeKeySource).
  • D-02 critical fix — the parent's SealedChildRef[N].readKeySealed is re-sealed under the parent's NEW readKey' out-of-band in the caller and republished (the Phase-63 bug: it was sealed under the child's own key and never written back).
  • D-06 binding stabilityupdateFolderMetadataAndPublish requires stable nodeId/nodeGeneration (threaded through all six client.ts call sites); moveItem re-seals the moved child under the destination parent's readKey.
  • TEST-01 — new tests/sdk-e2e/src/suites/rotation-crash-safety.test.ts abort-and-resume + concurrent-add suite (3/3 green against the live stack).

Verification

  • Goal-backward verification: status passed (5/5 success criteria; 2 fully verified, 3 via accepted overrides).
  • Security audit: SECURED — 18/18 threats mitigated + 2 accepted, 0 blocking. The two deferrals are liveness/durability, not confidentiality bypasses (the revoked reader is cut at the root; merged parent bodies are under readKey').
  • Unit: sdk-core 297 passed / 22 skipped; zero new type errors.
  • SDK E2E (live stack): 4/4 runnable tests green (rotation-crash-safety + read-chain-navigation). The other 89 e2e tests are intentionally describe.skip-quarantined mid-milestone ("SDK runtime stubbed, re-enable at phase 63-65 consumer re-wire").

Accepted deferrals (user-decided, todos captured)

  • 2026-06-29-rotation-concurrent-add-merge-downgrades-rotated-child-readkey — HIGH-4 re-merge uses remote-wins, which downgrades an already-rotated child's readKeySealed on concurrent-add (liveness regression, not a revocation bypass). The minimal "never dropped" property is met; the local-wins fix is deferred.
  • 2026-06-29-rotation-fresh-record-resume-and-sc4-double-bump — true fresh-record crash-resume of an already-rotated root needs the Phase-68 durable client floor; SC#4's "without double-bumping" wording also contradicts design §4.5 (double-rotation is the safe recovery). Both → Phase 68.

Notes

  • Pre-existing test-file type errors in cas.test.ts (12) and share/grant.test.ts (11) predate this phase and are invisible to CI (tsconfig.build.json excludes src/**/*.test.ts); not a Phase-64 regression.

CodeRabbit review (local CLI, pre-merge)

17 findings triaged. 7 low-risk fixed in d65cf5d2b (doc sync, nodeGeneration finite-integer validation, zeroization completeness in moveItem/BFS-queue/mintFileKeyOnRotate-failure-path, e2e key hygiene). 1 false positive skipped (a block-scoped publishCalls const flagged as a duplicate). The remaining ~9 are in the rotation-soundness domain already deferred above — captured in 2026-06-29-rotation-coderabbit-followups-deferred (merge re-enqueue + e2e assertion → RR-01; verifySubtreeClean depth + frontier pendingChildCount + resume fail-closed → RR-02; grantCallbacks threading → Phase 66; client.ts move/FolderState items → the SDK consumer re-wire).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved rotation and recovery behavior for folders and files, including safer retries after interruptions.
    • Added support for preserving moved items and concurrent updates during rotation.
  • Bug Fixes

    • Fixed cases where folder identity and generation could drift during updates.
    • Improved key handling so revoked access is cut off more reliably after rotation.
    • Reduced the risk of duplicate progress or missed changes when resuming interrupted operations.

FSM1 and others added 30 commits June 29, 2026 14:11
Entire-Checkpoint: e93088839ad6
Entire-Checkpoint: 500072385b16
Entire-Checkpoint: 7fdbefa471e2
Entire-Checkpoint: a7f5d10d27cf
- RED: 'throws when nodeId not provided' fails (function resolves, no guard yet)
- RED: 'throws when nodeGeneration not provided' fails (function resolves, no guard yet)
- Behavioral round-trip tests pass to document correct identity-preservation behavior
- Uses real sealNode/unsealNode; mocks only IPFS/IPNS I/O layers

Entire-Checkpoint: 3615bef8dac1
…ataAndPublish

- Remove ?? crypto.randomUUID() and ?? 0 fallbacks (D-06 fix)
- Add required nodeId: string and nodeGeneration: number to params type
- Add runtime guards with descriptive error messages (AAD-stability enforcement)
- Add nodeId/nodeGeneration as required fields on FolderState (sdk/types.ts)
- Update existing folder.test.ts calls to supply the required fields
- GREEN: all 5 registration.test.ts tests pass

Entire-Checkpoint: 84bea983f14c
…RUD call sites

- Add nodeId/nodeGeneration optional params to registerFolder (D-06 bridge)
- Set nodeId/nodeGeneration in loadFolder from result.metadata.id/.generation
- Pass folder.nodeId/folder.nodeGeneration to all six updateFolderMetadataAndPublish
  call sites: renameItem, moveItem (src+dst), deleteItem, uploadFile, uploadFiles
- Fix FolderState test fixtures in 7 test files (Rule 1: caused by required fields)

Entire-Checkpoint: 177ccaf415e2
…l (FLAG-63-U2)

Entire-Checkpoint: 1f1ced14e87c
… moveItem (FLAG-63-U2)

- Import sealChildReadKey, unsealChildReadKey, PublishedNode from @cipherbox/core
- After sdkCore.moveItem() link rewrite, resolve child IPNS to get plaintext id/kind
- Unseal movedRef.readKeySealed under source parent folderKey
- Re-seal under dest parent folderKey using unchanged id/kind/generation
- Zero recovered childReadKey (engine-derived, terminal-owned — D-09)
- Do NOT zero sourceFolder.folderKey or destFolder.folderKey (caller-owned)

Entire-Checkpoint: 8d37fe2fc8c8
- Union by ipnsName: local inserted first, remote overwrites on conflict
- Remote wins on conflict so concurrent adds are never silently dropped
- Intentional delete: base entry absent from both local and remote is pruned
- One-sided delete: entry kept when at least one side still holds it
- Pure structural merge: no crypto, no mutation of sealed bytes
- Return type changed from never to SealedChildRef[]

Entire-Checkpoint: 70a7d11743f2
- RED: mintFileKeyOnRotate assigns fresh 32-byte fileKey distinct from old key
- RED: mintFileKeyOnRotate is no-op for folder nodes (no content field added)
- RED: rotateOne sealNode integration — sealNode receives new fileKey after mint

Entire-Checkpoint: 9b10bf0c5309
- Fill mintFileKeyOnRotate seam (ROT-03/CRIT-1): mint fileKey' = generateRandomBytes(32)
  and assign to node.content.fileKey when content present; folder nodes are no-op
- Import generateRandomBytes from @cipherbox/crypto for cryptographic randomness
- Remove obsolete 'mintFileKeyOnRotate throws phase 64' tests from Phase-63 seam suite
- Green: all 19 engine tests pass; old readKey/fileKey holder cannot decrypt next version

Entire-Checkpoint: 503247cc33ff
Entire-Checkpoint: 9003f3740525
- Assert rotateOne throws when nodeIpnsPrivateKey is absent
- Assert publishWithCas not called with all-zero placeholder key
- Assert BFS threads nodeKeySource key to child publishWithCas
- Assert BFS throws when nodeKeySource returns undefined for a child
- Add nodeKeySource? to RotationParams type (type-only seam, no threading yet)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUSYqMyCLbNGDLcn1cp1eh
Entire-Checkpoint: 068bec968119
…eading

- Add runtime guard in rotateOne: throws when nodeIpnsPrivateKey absent
- Remove PLACEHOLDER_WRITE_KEY fallback from publishWithCas ipnsPrivateKey arg
- Destructure nodeKeySource from RotationParams in rotateReadFromNode
- Thread nodeKeySource keys to child and grandchild BFS queue items
- Update all existing rotateOne tests to supply nodeIpnsPrivateKey
- Update all rotateReadFromNode tests with children to supply rootIpnsPrivateKey
  and nodeKeySource; add unsealChildReadKey mock where missing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUSYqMyCLbNGDLcn1cp1eh
Entire-Checkpoint: 5144d4bc942a
…t publish

- Assert sealChildReadKey called with parent new readKey' for child re-seal
- Assert publishWithCas called 3 times for root->child walk
- Assert sealChildReadKey total call count is 3 for root->child walk

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUSYqMyCLbNGDLcn1cp1eh
Entire-Checkpoint: 412974d990b3
- Add newSequenceNumber to RotateOneDone; capture publishWithCas return in rotateOne
- Add ParentTrackingState and parentTracking Map to rotateReadFromNode BFS walker
- After each child rotates, call sealChildReadKey with parent's NEW readKey' (D-02 fix)
- Update parent's mutable SealedChildRef copy with re-sealed key and new generation
- Call updateFolderMetadataAndPublish once when all children done (D-09 batched publish)
- Add childPubId/childPubKind to BFS queue items for AAD binding in D-02 re-seal
- Import updateFolderMetadataAndPublish from folder/registration
- Fix TypeScript tuple destructuring in sealChildReadKey mock assertion

Entire-Checkpoint: 91655dc8a1c4
Entire-Checkpoint: d29b43cfd1a3
- 4 RED tests for reMintGrantsRootedAt (ROT-04/HIGH-3/D-04)
- Test 1: non-revoked grant → wrapKey + updateGrantFn; deleteGrantFn not called
- Test 2: revoked grant → deleteGrantFn only; wrapKey + updateGrantFn not called
- Test 3: mixed set → exactly one update and one delete with correct shareIds
- Test 4: no callbacks supplied → clean no-op (no throw)
- Extend reMintGrantsRootedAt signature with optional GrantRemintCallbacks param
- Add GrantRemintCallbacks type (D-04 transport seam)
- Add grantCallbacks field to RotateOneParams for call-site threading

Entire-Checkpoint: ef5d06556ecc
- Fill reMintGrantsRootedAt body (ROT-04/HIGH-3/D-04)
- Add wrapKey import from @cipherbox/crypto for ECIES descriptor minting
- Add local bytesToBase64 helper for readDescriptorRef encoding
- Enumerate grants via queryGrantsFn; delete revoked, re-mint non-revoked
- Never zero newReadKey (caller is terminal owner per D-09)
- Add grantCallbacks field to RotateOneParams; thread through call site
- Update engine.test.ts seam test to reflect filled reMintGrantsRootedAt

Entire-Checkpoint: 2f84097cb689
Entire-Checkpoint: cd15a6063aa9
FSM1 and others added 12 commits June 29, 2026 18:00
Entire-Checkpoint: 7e9f7f2c3a63
…ergence guard

- Replace old throws-phase-64 test with 5 new ROT-06 tests (Tests 1-5)
- Test 1/2: verifySubtreeClean { isDirty, frontier } return shape (BFS dirty-edge)
- Test 3: resume guard with dirty child triggers D-09 re-publish not short-circuit
- Test 4: clean resume marks complete and persists (passes in RED too, regression gate)
- Test 5: fresh-job no-double-bump convergence guard skips child already at baseline+1
- Update verifySubtreeClean signature to accept rootIpnsName, rootReadKey, ctx

Entire-Checkpoint: 51f009ac09cf
…nvergence guard

- Implement verifySubtreeClean: BFS read-only pass comparing SealedChildRef.generation
  (parent mirror) vs childPub.generation (plaintext) to rebuild dirty-edge frontier
- Rewrite resume guard: call verifySubtreeClean before marking complete (Pitfall 5);
  dirty resume seeds BFS queue from frontier instead of short-circuiting to complete
- Add enqueuedGeneration field to BFS queue items (parent mirror at enqueue time)
- Add convergence guard before each rotateOne in BFS: skip if current published
  generation exceeds enqueued baseline (ROT-06 no-double-bump); still handle D-09

Entire-Checkpoint: 4749801642b4
… queue-key zeroization

- Test 1: reMintGrantsRootedAt throws → nodeId must NOT be in completedNodeIds (D-07)
- Test 2: terminal status=complete must be persisted via persistCallback (terminal persist)
- Test 3: queue-derived child nodeReadKey must be zeroed after grandchildren enqueued;
  rootReadKey must NOT be zeroed (regression gate — caller is terminal owner per D-09)

Entire-Checkpoint: 6d7dbbd5806f
…tion

- Move completedNodeIds.add(nodeId) to AFTER reMintGrantsRootedAt succeeds (D-07):
  a failed re-mint no longer silently skips the node on resume
- Add persistCallback call at terminal status=complete (Pitfall 5: advisory job
  record must be persisted at completion so the resumable walk gate is accurate)
- Zero item.nodeReadKey after grandchildren enqueued (D-09 queue-key ownership):
  engine-derived BFS readKeys are zeroed once their consumers are enqueued;
  caller-supplied rootReadKey is never zeroed (caller is terminal owner)

Entire-Checkpoint: 1dd05f2fedc4
- Task 1: depth-2 tree (root→subfolder→file) happy-path rotation; all nodes
  advance to gen=1; post-rotation read-chain navigation under new keys proves
  multi-level D-02 re-seal; pre-rotation grant returns behind-retry
- Task 2: crash at final persistCallback (call 4, after all D-09s complete);
  fresh resume with seeded completedNodeIds and readKeyPrime converges with
  isDirty=false, no double-bump, zero getRandomValues calls on resume
- Task 3: concurrent IPNS write to parent between root commit and D-09
  batched republish forces CAS-409; mergeChildren (ROT-05/HIGH-4) absorbs
  the conflict and the concurrently-added child survives in the final parent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUSYqMyCLbNGDLcn1cp1eh
Entire-Checkpoint: c7801eb52d88
…esh-record resume)

Entire-Checkpoint: 7ed06cb1ff18
Entire-Checkpoint: 36441ac5f1c9
@github-actions github-actions Bot added release:sdk-core:feat Minor version bump (new feature) for sdk-core release:sdk:feat Minor version bump (new feature) for sdk release:web:fix Patch version bump (bug fix) for web release:tee-worker:fix Patch version bump (bug fix) for tee-worker labels Jun 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Release Preview

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

Cascade Details

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

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@FSM1, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 24 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 20490110-6fa8-41c3-bb6b-1a2fbd6893d2

📥 Commits

Reviewing files that changed from the base of the PR and between d65cf5d and 958c200.

📒 Files selected for processing (8)
  • .planning/todos/pending/2026-06-29-rotation-coderabbit-followups-deferred.md
  • packages/sdk-core/src/__tests__/rotation/engine.test.ts
  • packages/sdk-core/src/rotation/engine.ts
  • packages/sdk/src/__tests__/client-extended.test.ts
  • packages/sdk/src/__tests__/client-move-reencrypt.test.ts
  • packages/sdk/src/__tests__/client.test.ts
  • packages/sdk/src/__tests__/helpers.ts
  • packages/sdk/src/client.ts

Walkthrough

Phase 64 "Rotation Soundness — Revocation Guarantees" fills four previously stubbed rotation engine seams (mintFileKeyOnRotate, reMintGrantsRootedAt, mergeConcurrentChildren, verifySubtreeClean), hardens the BFS rotation loop with fail-closed key guards, crash-resume convergence, and batched parent re-sealing, makes nodeId/nodeGeneration required on folder publish paths, adds moveItem re-seal under the destination parent, implements a real mergeChildren three-way merge, and adds an E2E crash-safety suite.

Changes

Phase 64 Rotation Soundness

Layer / File(s) Summary
FolderState nodeId/nodeGeneration contract and propagation
packages/sdk/src/types.ts, packages/sdk-core/src/folder/registration.ts, packages/sdk/src/client.ts, packages/sdk-core/src/__tests__/folder/registration.test.ts, packages/sdk-core/src/__tests__/folder.test.ts, packages/sdk/src/__tests__/...
FolderState gains nodeId and nodeGeneration. updateFolderMetadataAndPublish changes both from optional to required with runtime guards; UUID/0 fallbacks removed. registerFolder/loadFolder store these values; all six metadata publish call sites forward them. ~28 SDK test fixture files updated with placeholder values.
mergeChildren three-way merge and CAS async callback
packages/sdk-core/src/folder/merge.ts, packages/sdk-core/src/cas.ts, packages/sdk-core/src/__tests__/folder-merge.test.ts
mergeChildren implemented: local-first Map, remote overwrites on ipnsName conflict, base-only absent entries pruned. publishWithCas merge callback widened to sync|Promise via await Promise.resolve(...). Test suite replaces skipped Phase-64 placeholder with six live SealedChildRef[] cases.
moveItem destination-parent re-seal
packages/sdk/src/client.ts, packages/sdk-core/src/__tests__/folder/move-reseal.test.ts
moveItem resolves the moved child's IPNS record, unseals readKeySealed under the source folder key, and re-seals under the destination folder key with zeroing in a finally block. Three new unit tests verify full round-trip, SOURCE-key rejection after re-seal, and generation-bound identity stability.
Rotation engine seam implementations
packages/sdk-core/src/rotation/engine.ts, packages/sdk-core/src/__tests__/rotation/grant-remint.test.ts, packages/sdk-core/src/__tests__/rotation/engine.test.ts
Fills mintFileKeyOnRotate (fresh 32-byte fileKey on file nodes, no-op for folders), reMintGrantsRootedAt (ECIES re-wrap non-revoked grants, delete revoked via injected GrantRemintCallbacks), mergeConcurrentChildren (unseal base/remote under old key, merge, re-seal under readKeyPrime), and verifySubtreeClean (BFS IPNS resolve returning {isDirty, frontier}). Exports GrantRemintCallbacks; adds grantCallbacks/nodeKeySource parameters. grant-remint.test.ts added with four callback-behavior tests; engine seam tests updated.
rotateOne hardening and BFS convergence/resume
packages/sdk-core/src/rotation/engine.ts, packages/sdk-core/src/__tests__/rotation/engine.test.ts
rotateOne adds fail-closed guard on nodeIpnsPrivateKey, invokes mintFileKeyOnRotate, wires CAS-409 merge to mergeConcurrentChildren, calls reMintGrantsRootedAt after success, and zeroizes readKeyPrime/minted fileKey only on failure. rotateReadFromNode BFS adds ParentTrackingState/parentTracking for out-of-band sealChildReadKey and batched parent republish, verifySubtreeClean-based resume guard, per-node convergence guard, post-re-mint completedNodeIds.add, terminal persistCallback, and queue-derived key zeroization. ~1200 lines of new engine tests for D-01/D-02/D-07/D-09, convergence, resume, and zeroization invariants.
E2E rotation crash-safety suite
tests/sdk-e2e/src/suites/rotation-crash-safety.test.ts
Three live-stack tests: happy-path depth-2 rotation with revocation verification; abort-and-resume where persistCallback throws at call N=4 and resumed job completes without double-bumping generations; concurrent-add merge where a child published mid-rotation via CAS conflict survives in the merged parent.
Phase 64 planning artifacts
.planning/REQUIREMENTS.md, .planning/ROADMAP.md, .planning/STATE.md, .planning/phases/64-rotation-soundness-revocation-guarantees/*, .planning/todos/pending/2026-06-29-*
ROT-03–06 and TEST-01 marked complete in requirements/roadmap/state. Eight plan/summary pairs (64-01 through 64-08) added with TDD task descriptions, threat models, and completion evidence. Phase context, discussion log, patterns, research, security, validation, and verification documents added. Three deferred-todo files created for concurrent-add merge downgrade, fresh-record resume, and CodeRabbit followups.

Sequence Diagram

sequenceDiagram
  participant caller as rotateReadFromNode caller
  participant BFS as BFS loop
  participant rotateOne
  participant engine as engine seams
  participant ipns as IPNS / publishWithCas
  participant parent as Parent re-seal + republish

  caller->>BFS: rotateReadFromNode(root, rootReadKey, jobRecord, ...)
  alt root already in completedNodeIds
    BFS->>engine: verifySubtreeClean(rootIpnsName, rootReadKey, ctx)
    engine-->>BFS: {isDirty, frontier}
    alt dirty
      BFS->>BFS: seed queue from frontier
    else clean
      BFS-->>caller: status=complete + persistCallback
    end
  end
  loop each queued node
    alt generation already converged
      BFS->>BFS: skip rotateOne, decrement parent pendingChildren
    else
      BFS->>rotateOne: rotateOne(node, parentReadKey, nodeIpnsPrivateKey, ...)
      rotateOne->>rotateOne: fail-closed guard (throw if no nodeIpnsPrivateKey)
      rotateOne->>engine: mintFileKeyOnRotate(node)
      rotateOne->>ipns: publishWithCas(sealedNode)
      ipns-->>rotateOne: casResult (or 409 conflict)
      opt 409 conflict
        rotateOne->>engine: mergeConcurrentChildren(base, remote, oldKey, local, newKey)
        engine-->>rotateOne: merged PublishedNode
      end
      rotateOne->>engine: reMintGrantsRootedAt(nodeId, newReadKey, gen, callbacks)
      rotateOne->>BFS: completedNodeIds.add(nodeId)
    end
    BFS->>parent: sealChildReadKey under parent newReadKey
    opt all children processed
      BFS->>ipns: batched parent republish
    end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • FSM1/cipher-box#488: Introduced mergeChildren into the CAS/IPNS 409 conflict resolution path in packages/sdk-core/src/folder/merge.ts, which this PR replaces the stub implementation of with a real three-way merge.
  • FSM1/cipher-box#494: Introduced or refactored the publishWithCas helper in packages/sdk-core/src/cas.ts; this PR extends that helper's merge callback signature to support async returns.
  • FSM1/cipher-box#579: Phase 63 scaffolded the rotation engine seam stubs (mintFileKeyOnRotate, reMintGrantsRootedAt, etc.) in engine.ts that this PR fills with real implementations.

Suggested labels

release:sdk-core:feat, release:sdk:feat

🚥 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 clearly matches the PR’s main rotation-soundness work across content keys, grants, concurrent merges, and crash-safe resume.
Docstring Coverage ✅ Passed Docstring coverage is 86.36% which is sufficient. The required threshold is 80.00%.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rotation-soundness-revocation-guarantees

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.

@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements Phase 64's rotation soundness guarantees, filling four previously-stubbed seams: content-key rotation (mintFileKeyOnRotate), grant re-minting (reMintGrantsRootedAt), concurrent-add merge (mergeConcurrentChildren + mergeChildren), and crash-safe resume (verifySubtreeClean). It also fixes the Phase-63 D-02 critical bug where child readKeySealed was sealed under the child's own old key rather than the parent's new readKey', and strengthens the D-01 fail-closed guard to reject all-zero IPNS keys.

  • engine.ts: Implements the full BFS rotation walk with D-02 out-of-band re-seal, D-09 batched parent republish, and a convergence guard that skips rotateOne when a child is already ahead of its parent mirror; adds parent tracking per BFS node and key zeroization in a finally block.
  • folder/merge.ts: Implements three-way mergeChildren (union by ipnsName, remote wins, intentional-delete semantics); registration.ts makes nodeId/nodeGeneration required and adds validation guards for AAD stability (D-06).
  • client.ts: Adds re-seal of moved child's readKeySealed under destination parent key in moveItem, threads nodeId/nodeGeneration through all six updateFolderMetadataAndPublish call sites, and adds nodeId/nodeGeneration to FolderState.

Confidence Score: 5/5

The rotation walk is safe to merge: revocation cuts at the root, merged parent bodies are under readKey prime, and the D-02 re-seal fix closes the Phase-63 read-chain bug.

The four previously-stubbed seams are fully implemented with correct key zeroization, D-01 fail-closed enforcement, and D-09 batched republish that correctly re-seals child readKeys under the parent's new key for all normally-exercised paths. Accepted deferrals do not create confidentiality bypasses. The convergence guard readKeySealed gap requires an unlikely concurrent external rotation during the walk to trigger and is not reachable in the tested crash scenario.

The BFS convergence guard path in engine.ts (lines 914-944) merits a follow-up todo before the Phase-68 dirty-resume wiring lands; the e2e test capturedReadKeys index documentation should be updated to list fileKeyPrime entries for file nodes.

Important Files Changed

Filename Overview
packages/sdk-core/src/rotation/engine.ts Core rotation walk — implements D-01 fail-closed, D-02 out-of-band re-seal, D-09 batched parent republish, ROT-06 convergence guard, dirty-resume via verifySubtreeClean, and queue-key zeroization. Convergence guard updates generation but not readKeySealed for already-rotated children, which would leave the parent's D-09 republish with a stale sealed ref if the guard fires on a concurrently-rotated child.
packages/sdk-core/src/folder/merge.ts Three-way merge of SealedChildRef arrays — union by ipnsName with remote-wins on conflict and intentional-delete semantics. Correct implementation; the prune intentional deletes loop is dead code (flagged in prior review) but harmless.
packages/sdk-core/src/folder/registration.ts Promotes nodeId/nodeGeneration from optional to required with runtime validation guards. The inline comment at the merge callback still says Phase-64 stub — throws on any conflict (stale, flagged in prior review).
packages/sdk-core/src/cas.ts Widens merge callback type to accept Promise, wrapped via await Promise.resolve() for backward compatibility. Clean change.
packages/sdk/src/client.ts Adds FLAG-63-U2 re-seal of moved child readKeySealed under dest parent key in moveItem, threads nodeId/nodeGeneration through six updateFolderMetadataAndPublish call sites, and adds nodeId/nodeGeneration to FolderState with defaults for registerFolder.
tests/sdk-e2e/src/suites/rotation-crash-safety.test.ts New e2e suite — happy-path, abort-and-resume (crash after all D-09s complete, isDirty=false), and concurrent-add merge. The getRandomValues spy captures all 32-byte calls including mintFileKeyOnRotate fileKeyPrime, which offsets indices for file nodes beyond what the comment documents.
packages/sdk/src/types.ts Adds nodeId/nodeGeneration fields to FolderState; well-documented with D-06 contract and monotonicity invariants.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant rotateReadFromNode
    participant rotateOne
    participant reMintGrantsRootedAt
    participant updateFolderMetadataAndPublish
    participant verifySubtreeClean

    Caller->>rotateReadFromNode: rootReadKey, jobRecord, nodeKeySource
    rotateReadFromNode->>rotateOne: root node (D-01 key guard)
    rotateOne-->>rotateReadFromNode: readKeyPrime, children, newSequenceNumber

    alt rootResult.skipped (resume path)
        rotateReadFromNode->>verifySubtreeClean: rootIpnsName, rootReadKey
        verifySubtreeClean-->>rotateReadFromNode: isDirty, frontier
        note over rotateReadFromNode: Seed BFS from dirty frontier
    else normal path
        note over rotateReadFromNode: Set up parentTracking for root
    end

    loop BFS frontier
        rotateReadFromNode->>rotateReadFromNode: convergence guard (generation check)
        alt child already rotated
            rotateReadFromNode->>rotateReadFromNode: update generation only, decrement pendingChildCount
        else
            rotateReadFromNode->>rotateOne: child node (per-node IPNS key)
            rotateOne->>reMintGrantsRootedAt: nodeId, readKeyPrime, grantCallbacks
            rotateOne-->>rotateReadFromNode: childReadKey prime, newGeneration, newSequenceNumber
            note over rotateReadFromNode: D-02 sealChildReadKey under parent readKey prime
            note over rotateReadFromNode: Update parentState.children in-place
        end

        alt "pendingChildCount == 0"
            rotateReadFromNode->>updateFolderMetadataAndPublish: D-09 batched parent republish
        end
    end

    rotateReadFromNode->>Caller: "status = complete, persist"
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 Caller
    participant rotateReadFromNode
    participant rotateOne
    participant reMintGrantsRootedAt
    participant updateFolderMetadataAndPublish
    participant verifySubtreeClean

    Caller->>rotateReadFromNode: rootReadKey, jobRecord, nodeKeySource
    rotateReadFromNode->>rotateOne: root node (D-01 key guard)
    rotateOne-->>rotateReadFromNode: readKeyPrime, children, newSequenceNumber

    alt rootResult.skipped (resume path)
        rotateReadFromNode->>verifySubtreeClean: rootIpnsName, rootReadKey
        verifySubtreeClean-->>rotateReadFromNode: isDirty, frontier
        note over rotateReadFromNode: Seed BFS from dirty frontier
    else normal path
        note over rotateReadFromNode: Set up parentTracking for root
    end

    loop BFS frontier
        rotateReadFromNode->>rotateReadFromNode: convergence guard (generation check)
        alt child already rotated
            rotateReadFromNode->>rotateReadFromNode: update generation only, decrement pendingChildCount
        else
            rotateReadFromNode->>rotateOne: child node (per-node IPNS key)
            rotateOne->>reMintGrantsRootedAt: nodeId, readKeyPrime, grantCallbacks
            rotateOne-->>rotateReadFromNode: childReadKey prime, newGeneration, newSequenceNumber
            note over rotateReadFromNode: D-02 sealChildReadKey under parent readKey prime
            note over rotateReadFromNode: Update parentState.children in-place
        end

        alt "pendingChildCount == 0"
            rotateReadFromNode->>updateFolderMetadataAndPublish: D-09 batched parent republish
        end
    end

    rotateReadFromNode->>Caller: "status = complete, persist"
Loading

Reviews (3): Last reviewed commit: "fix(64): harden rotation D-01/D-09 guard..." | Re-trigger Greptile

Comment thread packages/sdk-core/src/rotation/engine.ts
Comment thread packages/sdk-core/src/folder/merge.ts
Comment thread packages/sdk-core/src/rotation/engine.ts

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

Caution

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

⚠️ Outside diff range comments (1)
packages/sdk-core/src/rotation/engine.ts (1)

208-233: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Make grant re-minting reachable from full-tree rotation.

reMintGrantsRootedAt only runs when rotateOne receives innerGrants, but RotationParams has no grant callbacks/grant signal and rotateReadFromNode never forwards them for root or BFS children. Full-tree rotations can therefore mark nodes complete without updating non-revoked grants or deleting revoked grant rows.

Also applies to: 679-689, 924-934, 599-614

🤖 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-core/src/rotation/engine.ts` around lines 208 - 233, Full-tree
rotation currently cannot reach grant re-minting because RotationParams has no
grant-related inputs and rotateReadFromNode does not pass any grant signal into
rotateOne, so reMintGrantsRootedAt never runs for root or BFS children. Extend
RotationParams with the needed grant callback/signal and thread it through
rotateReadFromNode into rotateOne for every node in the tree. Ensure the full
rotation path can invoke reMintGrantsRootedAt so non-revoked grants are updated
and revoked grant rows are deleted before nodes are marked complete.
🧹 Nitpick comments (4)
tests/sdk-e2e/src/suites/rotation-crash-safety.test.ts (1)

698-717: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the concurrent child is actually readable, not just present.

The test only checks ipnsName membership. It would still pass if the CAS merge preserved a stale readKeySealed that cannot be unsealed with readKeyPrimeRoot3. Add a navigation/unseal assertion for concurrentIpnsName.

As per path instructions, **/*.test.ts: “Focus on test coverage, edge cases, and test quality. Ensure tests are meaningful and not just for coverage metrics.”

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

In `@tests/sdk-e2e/src/suites/rotation-crash-safety.test.ts` around lines 698 -
717, The assertion in the root3 merge test only verifies that concurrentIpnsName
is listed in children, which can miss a stale readKeySealed that cannot actually
be opened. Update the checks around root3FinalNode in the rotation-crash-safety
test to navigate to the concurrent child and unseal it using readKeyPrimeRoot3,
alongside the existing ipnsName membership assertions. Use the existing helpers
unsealNode, fetchFromIpfs, resolveIpnsRecord, and the child traversal from
root3FinalNode.children to confirm the concurrent child is truly readable after
the CAS-409 merge.

Source: Path instructions

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

78-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a same-sequence backfill regression here.

These fixtures now seed the placeholder nodeId / nodeGeneration, but the suite still doesn't assert that an equal-sequence loadFolder() replaces them with metadata.id / metadata.generation. That is the bridge contract registerFolder() now depends on, so this regression currently slips through. As per path instructions, **/*.test.ts: Focus on test coverage, edge cases, and test quality. Ensure tests are meaningful and not just for coverage metrics.

Also applies to: 142-143

🤖 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-load-reconcile.test.ts` around lines 78 -
79, Add a regression test in client-load-reconcile.test.ts around the
loadFolder()/registerFolder flow that starts with placeholder nodeId and
nodeGeneration, then asserts an equal-sequence loadFolder() backfills
metadata.id and metadata.generation onto the existing record. Use the relevant
loadFolder and registerFolder test setup/fixtures already in the suite, and
verify the placeholder values are replaced rather than left unchanged, since
this is the bridge contract the reconciliation logic depends on.

Source: Path instructions

packages/sdk-core/src/__tests__/folder-merge.test.ts (1)

119-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid asserting referential identity here.

remote wins is a data-semantic guarantee, not an aliasing guarantee. expect(result[0]).toBe(remote) will fail on valid implementations that clone or normalize the chosen entry before returning it. Assert the selected fields instead.

Suggested fix
     expect(result).toHaveLength(1);
     expect(result[0].readKeySealed).toBe('remote-sealed');
-    expect(result[0]).toBe(remote);
+    expect(result[0]).toMatchObject({
+      ipnsName: remote.ipnsName,
+      generation: remote.generation,
+      readKeySealed: remote.readKeySealed,
+    });

As per path instructions, **/*.test.ts: “Focus on test coverage, edge cases, and test quality. Ensure tests are meaningful and not just for coverage metrics.”

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

In `@packages/sdk-core/src/__tests__/folder-merge.test.ts` around lines 119 - 127,
The test in mergeChildren is over-specifying implementation details by asserting
referential identity with expect(result[0]).toBe(remote), which can fail even
when remote wins correctly. Update the assertion to verify the data-semantic
outcome only by checking the selected fields on result[0] in
folder-merge.test.ts, keeping the existing mergeChildren scenario and symbols
like makeChild and mergeChildren intact.

Source: Path instructions

packages/sdk-core/src/__tests__/folder/move-reseal.test.ts (1)

139-202: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

This test only proves generation binding, not full identity binding.

The title and comments say id/kind/generation stay bound, but the only negative case here is generation + 1. A regression that stopped including id or kind in the AAD would still pass this suite. Please add rejects for wrong id and wrong kind as well.

As per path instructions, **/*.test.ts: “Focus on test coverage, edge cases, and test quality. Ensure tests are meaningful and not just for coverage metrics.”

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

In `@packages/sdk-core/src/__tests__/folder/move-reseal.test.ts` around lines 139
- 202, The re-seal test in `move-reseal.test.ts` only verifies the generation
binding, so it would miss regressions where `sealChildReadKey` or
`unsealChildReadKey` stop binding `id` or `kind` into the AAD. Update the
`re-seal preserves child node id, kind, and generation` test to add negative
cases that attempt to unseal the sealed blob with an incorrect child `id` and an
incorrect `kind`, and assert both reject with `CryptoError`, alongside the
existing wrong-generation check.

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 `@packages/sdk-core/src/rotation/engine.ts`:
- Around line 891-917: The skip path in rotation/engine.ts only updates the
child’s generation and then republishes the parent, but it leaves the child’s
sealed read key stale. Update the convergence-skip branch around currentPub,
parentTracking, and updateFolderMetadataAndPublish so that a skipped child is
re-sealed under the parent’s new read key before publishing, and ensure any
descendant rotation state for that child is also propagated instead of being
bypassed.
- Around line 426-439: verifySubtreeClean currently only checks
rootNode.children and misses stale deeper descendants, so make the traversal
recursive through the subtree instead of stopping at immediate children. Update
verifySubtreeClean to descend into each child PublishedNode (using
resolveIpnsRecord and fetchFromIpfs as needed), compare generations at every
edge, and accumulate any dirty nodes into frontier so isDirty reflects stale
grandchild and deeper links too. Keep the existing childRef, childResolved,
childPub, and frontier flow, but extend it so the function walks all descendants
before returning.
- Around line 789-803: The skipped-root dirty-resume path in engine.ts can
continue even when the root IPNS private key is missing, which later causes the
non-null assertion on parentState.parentIpnsPrivateKey to reach
updateFolderMetadataAndPublish unsafely. Add an explicit fail-closed guard in
the dirty-resume flow before seeding parentTracking for rootNodeIpnsName so the
republish path is aborted or surfaced as an error when rootIpnsPrivateKey is
undefined. Make the check in the same area as the parentTracking.set logic and
ensure the later parentState.parentIpnsPrivateKey usage is only reachable when
the key is present.
- Around line 383-392: In mergeConcurrentChildren, remote-only SealedChildRef
entries are being carried forward without re-sealing them under the new parent
read key, and returning node.children prevents the BFS from rotating or D-02
re-sealing those children. Update the merge flow to rebuild or rewrap merged
child refs for the new chain before calling sealNode on the merged node, and
ensure the child traversal path does not just reuse node.children but processes
the merged children so remote-only refs are re-sealed under
readKeyPrime/newReadKey.
- Around line 504-512: The guard in rotateOne only rejects missing IPNS keys, so
placeholder or malformed key material can still pass through. Tighten the check
around nodeIpnsPrivateKey in engine.ts to validate that the provided key is a
real IPNS signing key, not just a non-empty Uint8Array, before publishWithCas is
reached. Use the rotateOne and nodeIpnsPrivateKey symbols to locate the guard
and replace the current undefined-only condition with stricter validation plus
the same fail-closed error path.

In `@packages/sdk/src/__tests__/client-move-reencrypt.test.ts`:
- Around line 102-103: The move fixtures in client-move-reencrypt.test.ts use an
empty nodeId, but moveItem() now passes nodeId through
updateFolderMetadataAndPublish() where falsy values throw, so update the
affected fixture objects to use a valid non-empty nodeId while keeping
nodeGeneration unchanged. Make this change for both of the move cases referenced
in the test so the setup exercises the re-encryption path instead of failing
during fixture validation.

In `@packages/sdk/src/__tests__/client.test.ts`:
- Around line 295-296: The deleteItem test fixture is using an invalid folder
state by seeding a falsy nodeId, which no longer matches the metadata-publish
contract. Update the test data in client.test.ts for the deleteItem case to use
a valid non-empty nodeId so the test exercises the actual delete flow; keep the
rest of the fixture aligned with the delete path and use the deleteItem-related
test setup as the locator.

In `@packages/sdk/src/__tests__/helpers.ts`:
- Around line 58-59: The shared test helper is seeding an invalid folder fixture
because setupFolder() uses an empty nodeId, which now violates the publish
contract. Update the helper in helpers.ts to initialize nodeId with a stable
non-empty placeholder so tests that rely on this fixture can reach the publish
path; use setupFolder() and the folder seed fields as the main symbols to locate
the change.

In `@packages/sdk/src/client.ts`:
- Around line 286-299: The cached-folder path in loadFolder() leaves the
placeholder node identity from registerFolder() unchanged when
existing.sequenceNumber matches the loaded record, so later CRUD calls still see
nodeId missing. Update the equal-sequence branch in loadFolder() to merge
result.metadata.id and result.metadata.generation into the cached folder entry
before returning it, keeping the cache consistent for renameItem, deleteItem,
and upload* flows.
- Around line 577-594: The re-seal lookup in moveItem is using the caller-facing
childId instead of the moved node’s ipnsName, so the ref match and IPNS
resolution can fail after the move. Update the moveItem flow in client.ts to
capture the matched updatedDest entry from the ipnsName search and use that node
identifier for both the resolveIpnsRecord call and the subsequent fetch/re-seal
path, or have sdkCore.moveItem() return the moved ref’s ipnsName and thread that
through consistently.

---

Outside diff comments:
In `@packages/sdk-core/src/rotation/engine.ts`:
- Around line 208-233: Full-tree rotation currently cannot reach grant
re-minting because RotationParams has no grant-related inputs and
rotateReadFromNode does not pass any grant signal into rotateOne, so
reMintGrantsRootedAt never runs for root or BFS children. Extend RotationParams
with the needed grant callback/signal and thread it through rotateReadFromNode
into rotateOne for every node in the tree. Ensure the full rotation path can
invoke reMintGrantsRootedAt so non-revoked grants are updated and revoked grant
rows are deleted before nodes are marked complete.

---

Nitpick comments:
In `@packages/sdk-core/src/__tests__/folder-merge.test.ts`:
- Around line 119-127: The test in mergeChildren is over-specifying
implementation details by asserting referential identity with
expect(result[0]).toBe(remote), which can fail even when remote wins correctly.
Update the assertion to verify the data-semantic outcome only by checking the
selected fields on result[0] in folder-merge.test.ts, keeping the existing
mergeChildren scenario and symbols like makeChild and mergeChildren intact.

In `@packages/sdk-core/src/__tests__/folder/move-reseal.test.ts`:
- Around line 139-202: The re-seal test in `move-reseal.test.ts` only verifies
the generation binding, so it would miss regressions where `sealChildReadKey` or
`unsealChildReadKey` stop binding `id` or `kind` into the AAD. Update the
`re-seal preserves child node id, kind, and generation` test to add negative
cases that attempt to unseal the sealed blob with an incorrect child `id` and an
incorrect `kind`, and assert both reject with `CryptoError`, alongside the
existing wrong-generation check.

In `@packages/sdk/src/__tests__/client-load-reconcile.test.ts`:
- Around line 78-79: Add a regression test in client-load-reconcile.test.ts
around the loadFolder()/registerFolder flow that starts with placeholder nodeId
and nodeGeneration, then asserts an equal-sequence loadFolder() backfills
metadata.id and metadata.generation onto the existing record. Use the relevant
loadFolder and registerFolder test setup/fixtures already in the suite, and
verify the placeholder values are replaced rather than left unchanged, since
this is the bridge contract the reconciliation logic depends on.

In `@tests/sdk-e2e/src/suites/rotation-crash-safety.test.ts`:
- Around line 698-717: The assertion in the root3 merge test only verifies that
concurrentIpnsName is listed in children, which can miss a stale readKeySealed
that cannot actually be opened. Update the checks around root3FinalNode in the
rotation-crash-safety test to navigate to the concurrent child and unseal it
using readKeyPrimeRoot3, alongside the existing ipnsName membership assertions.
Use the existing helpers unsealNode, fetchFromIpfs, resolveIpnsRecord, and the
child traversal from root3FinalNode.children to confirm the concurrent child is
truly readable after the CAS-409 merge.
🪄 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: 6d28dba0-3109-4244-9fcc-a5c6651e23dc

📥 Commits

Reviewing files that changed from the base of the PR and between 7216797 and d65cf5d.

📒 Files selected for processing (49)
  • .planning/REQUIREMENTS.md
  • .planning/ROADMAP.md
  • .planning/STATE.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-01-PLAN.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-01-SUMMARY.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-02-PLAN.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-02-SUMMARY.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-03-PLAN.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-03-SUMMARY.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-04-PLAN.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-04-SUMMARY.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-05-PLAN.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-05-SUMMARY.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-06-PLAN.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-06-SUMMARY.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-07-PLAN.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-07-SUMMARY.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-08-PLAN.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-08-SUMMARY.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-CONTEXT.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-DISCUSSION-LOG.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-PATTERNS.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-RESEARCH.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-SECURITY.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-VALIDATION.md
  • .planning/phases/64-rotation-soundness-revocation-guarantees/64-VERIFICATION.md
  • .planning/todos/pending/2026-06-29-rotation-coderabbit-followups-deferred.md
  • .planning/todos/pending/2026-06-29-rotation-concurrent-add-merge-downgrades-rotated-child-readkey.md
  • .planning/todos/pending/2026-06-29-rotation-fresh-record-resume-and-sc4-double-bump.md
  • packages/sdk-core/src/__tests__/folder-merge.test.ts
  • packages/sdk-core/src/__tests__/folder.test.ts
  • packages/sdk-core/src/__tests__/folder/move-reseal.test.ts
  • packages/sdk-core/src/__tests__/folder/registration.test.ts
  • packages/sdk-core/src/__tests__/rotation/engine.test.ts
  • packages/sdk-core/src/__tests__/rotation/grant-remint.test.ts
  • packages/sdk-core/src/cas.ts
  • packages/sdk-core/src/folder/merge.ts
  • packages/sdk-core/src/folder/registration.ts
  • packages/sdk-core/src/rotation/engine.ts
  • packages/sdk/src/__tests__/bin.test.ts
  • packages/sdk/src/__tests__/client-load-reconcile.test.ts
  • packages/sdk/src/__tests__/client-move-reencrypt.test.ts
  • packages/sdk/src/__tests__/client.test.ts
  • packages/sdk/src/__tests__/collect-subtree-ipns-names.test.ts
  • packages/sdk/src/__tests__/ensure-folder-loaded.test.ts
  • packages/sdk/src/__tests__/helpers.ts
  • packages/sdk/src/client.ts
  • packages/sdk/src/types.ts
  • tests/sdk-e2e/src/suites/rotation-crash-safety.test.ts

Comment thread packages/sdk-core/src/rotation/engine.ts
Comment thread packages/sdk-core/src/rotation/engine.ts
Comment thread packages/sdk-core/src/rotation/engine.ts
Comment thread packages/sdk-core/src/rotation/engine.ts
Comment thread packages/sdk-core/src/rotation/engine.ts
Comment thread packages/sdk/src/__tests__/client-move-reencrypt.test.ts Outdated
Comment thread packages/sdk/src/__tests__/client.test.ts Outdated
Comment thread packages/sdk/src/__tests__/helpers.ts Outdated
Comment thread packages/sdk/src/client.ts
Comment thread packages/sdk/src/client.ts Outdated
The Phase-64 moveItem destination re-seal (FLAG-63-U2) looked up the moved
child in updatedDest by childId (a UUID handle), but SealedChildRef carries
no id (NODE-03) — it is keyed by ipnsName. The lookup always missed and threw
"moved child <id> not found in dest after link rewrite", breaking the active
client-extended.test.ts > moveItem CI test (the Test job).

Use the movedRef that sdkCore.moveItem() already returns, locate the real
entry in updatedDest by movedRef.ipnsName, and resolve the child IPNS by
ipnsName. Strengthen the unit test to mock the IPNS resolve/fetch + seal/unseal
and assert the published dest entry carries the re-sealed key (CodeRabbit #2
fixed, #3 addressed).

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

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.10024% with 75 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.65%. Comparing base (7216797) to head (958c200).

Files with missing lines Patch % Lines
packages/sdk-core/src/rotation/engine.ts 80.92% 62 Missing ⚠️
packages/sdk/src/client.ts 84.90% 8 Missing ⚠️
packages/sdk-core/src/folder/registration.ts 78.26% 5 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main     #582       +/-   ##
===========================================
+ Coverage   63.76%   83.65%   +19.89%     
===========================================
  Files         152      116       -36     
  Lines       11246     7367     -3879     
  Branches     1252     1314       +62     
===========================================
- Hits         7171     6163     -1008     
+ Misses       3832      961     -2871     
  Partials      243      243               
Flag Coverage Δ
api 83.65% <82.10%> (+0.48%) ⬆️
api-client 83.65% <82.10%> (+0.48%) ⬆️
core 83.65% <82.10%> (+0.48%) ⬆️
crypto 83.65% <82.10%> (+0.48%) ⬆️
desktop ?
sdk 83.65% <82.10%> (+0.48%) ⬆️
sdk-core 83.65% <82.10%> (+0.48%) ⬆️

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

Files with missing lines Coverage Δ
packages/sdk-core/src/cas.ts 100.00% <100.00%> (ø)
packages/sdk-core/src/folder/merge.ts 100.00% <100.00%> (+90.00%) ⬆️
packages/sdk-core/src/folder/registration.ts 79.85% <78.26%> (+8.16%) ⬆️
packages/sdk/src/client.ts 82.35% <84.90%> (+0.08%) ⬆️
packages/sdk-core/src/rotation/engine.ts 84.13% <80.92%> (+6.90%) ⬆️

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

Comment thread packages/sdk-core/src/rotation/engine.ts
Apply the low-risk quick-win findings from the CodeRabbit + greptile PR reviews;
defer the heavy rotation-soundness rework (RR-01 merge re-enqueue, recursive
verifySubtreeClean, convergence-skip re-seal) to its existing Phase 66/68 todos.

- D-09: mintFileKeyOnRotate now zeroes the pre-rotation node.content.fileKey
  before overwriting (safe — node is a fresh unsealNode output, engine-owned).
- D-01: rotateOne's fail-closed guard rejects malformed/all-zero/wrong-length
  IPNS keys, not just undefined (keys are the 32-byte Ed25519 seed).
- D-01: dirty-resume root republish fails closed when rootIpnsPrivateKey is
  missing, before seeding parentTracking (no more parentIpnsPrivateKey! leaking
  undefined into updateFolderMetadataAndPublish).
- Test fixtures (helpers.setupFolder, client.test, client-move-reencrypt.test)
  seed stable non-empty nodeId placeholders per the folder publish contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUSYqMyCLbNGDLcn1cp1eh
Entire-Checkpoint: c85e68fa1b12
@FSM1 FSM1 merged commit 4ad615a into main Jun 29, 2026
29 checks passed
@FSM1 FSM1 deleted the feat/rotation-soundness-revocation-guarantees branch June 29, 2026 21:18
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:sdk-core:feat Minor version bump (new feature) for sdk-core release:tee-worker:fix Patch version bump (bug fix) for tee-worker release:web:fix Patch version bump (bug fix) for web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant