feat: SDK write-chain, write-revocation, bin re-link, and invite claim#583
Conversation
Entire-Checkpoint: aed8bf6fa830
Entire-Checkpoint: f49b3b2fe198
Entire-Checkpoint: ef5c6ef6e756
Entire-Checkpoint: fe7e071404d5
Entire-Checkpoint: 3c9673822b65
WRITE-01 write-body Ed25519 under writeKey (role 0x04), WRITE-02 full Ed25519 write-revocation (child-first cascade), WRITE-03 co-writer re-wrap + offline error, WRITE-04 tombstone-intent; bin re-link + invite re-wrap (Success Criterion 4). Mock-seam discipline (D-02); sdk-e2e round-trip gate (D-04). OQ-1 (BinEntry.nodeReadKey) and OQ-2 (child-first cascade) resolved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUSYqMyCLbNGDLcn1cp1eh Entire-Checkpoint: 9638c8677236
…ark research OQs resolved Entire-Checkpoint: e025a94f56f3
Entire-Checkpoint: 7a6909081de9
Entire-Checkpoint: 2a373269bfea
- round-trip: sealChildWriteKey then unsealChildWriteKey recovers 32-byte key - role isolation: role-0x02 blob rejected by role-0x04 unseal and vice versa - AAD binding: childId / childKind / childGeneration / wrong key all throw - terminal-owner: input buffers unchanged after both calls (D-09 / T-65-04)
… role 0x04 - verbatim copy of sealChildReadKey / unsealChildReadKey with role byte 0x02 → 0x04 - AAD: buildNodeAad(childId, kindByte, childGeneration, 0x04) — child-writekey role frozen - no new imports: reuses sealAesGcmAad / unsealAesGcmAad / buildNodeAad from @cipherbox/crypto - D-09 terminal-owner: neither function zeros caller-supplied buffers - exported from packages/core/src/node/index.ts and packages/core/src/index.ts - all 9 role-0x04 tests green (round-trip, cross-role rejection, AAD-mismatch, terminal-owner)
- RED: claimInvite import fails — function does not exist yet - Asserts getInviteDataFn called once per claim - Asserts insertShareFn called once with no encryptedChildKeys - Asserts two independent claims of same invite yield two separate grants
- Add claimInvite to packages/sdk-core/src/share/grant.ts composing claimInviteReadKey with injected getInviteDataFn and insertShareFn callbacks - Re-export claimInvite from packages/sdk-core/src/share/index.ts - One re-wrapped root readKey per claimer, no per-child key fan-out - Zero sdk-layer consumption of the fan-out path confirmed by grep gate
…mBin - add nodeReadKey and nodeIpnsName fields to BinEntry in @cipherbox/core - un-skip addToBin and restoreFromBin describe blocks - remove legacy originalFolderKeyEncrypted and FolderChild references - add 4 addToBin tests covering happy-path, revoke ordering, abort-on-revoke-fail, and not-loaded guard - add 5 restoreFromBin tests including AEAD asymmetry proof via real sealChildReadKey - switch vi.mock to importOriginal spread keeping real AES-GCM primitives for asymmetry test
addToBin: - resolve child IPNS record to extract plaintext PublishedNode id and kind - unseal nodeReadKey from source parent folderKey via unsealChildReadKey - revoke shares before destructive folder mutation (fail-closed ordering) - capture nodeReadKey and nodeIpnsName on BinEntry for restore restoreFromBin: - re-seal entry.nodeReadKey under destination folderKey via sealChildReadKey (role 0x02) - build SealedChildRef with re-linked readKeySealed (no content re-encryption) - add restored ref to target folder and publish; remove entry from bin also fix test fixture: use valid UUID for nodeRef.id to satisfy uuidToBytes validator; replace vi.restoreAllMocks with targeted sealSpy.mockRestore to prevent resetting module-level vi.fn mocks (deriveBinIpnsKeypair) in subsequent test cases
Entire-Checkpoint: 02004cb54e8b
…plane preservation - Tests 1-4: assert unsealNode called with nodeWriteKey, sealNode receives real writeKey not all-zeros placeholder, writeBody.ipnsPrivateKey preserved post-rotation - Tests 5-6: fail-closed guard -- writeSealed present without valid writeKey throws - Test 7: nodeKeySource.writeKey threads through BFS to sealNode - All 6 write-plane assertions fail RED against placeholder engine
…ER_WRITE_KEY - Add nodeWriteKey to RotateOneParams and nodeKeySource return type - unsealNode now called with nodeWriteKey to recover write-body on rotation - Fail-closed guard: throws when published.writeSealed present but nodeWriteKey is absent or all-zeros, mirroring the IPNS-key guard pattern - writeKeyForReseal = node.writeBody ? nodeWriteKey : empty; used at all three sealNode sites: main reseal and both CAS-409 merge paths - PLACEHOLDER_WRITE_KEY deleted from all three sites - rotateReadFromNode BFS threads nodeKeySource.writeKey at root, child, grandchild and dirty-resume enqueue sites - Existing IPNS fail-closed guard retained unchanged - Folds FLAG-63-U1 / rotateone-placeholder-writekey-phase65
… security and WRITE-03 offline error - Reshape SharedWriteContext: writeKey + readKey + publishedNode replaces raw folderKey and ipnsPrivateKey fields - Add publishNodeFn and addToIpfsFn transport seams for mock-testable operations - Add buildChildWriteLink helper sealing child writeKey under parent writeKey via role 0x04 - Add walkChildWriteKey helper walking the write chain via unsealChildWriteKey - Prove WRITE-01 security: unsealNode without writeKey returns no writeBody; read-only holder cannot reach ipnsPrivateKey - Prove NODE-03: SealedChildRef carries no write field; write link lives only in parent writeBody.writeChildren - Implement createSharedSubfolder: mint child keypair, build write-body, seal, publish, add SealedChildRef + WriteChildRef to parent - Implement uploadToSharedFolder: same pattern for file node with AES-256-GCM content encryption via addToIpfsFn - Implement renameInSharedFolder: mutate read-body child name, preserve writeChildren, re-publish parent - Implement deleteFromSharedFolder: remove from children and writeChildren by IPNS name, re-publish parent - Implement updateSharedFile: caller-supplied fileReadKey + fileWriteKey + fileIpnsPrivateKey, re-seal + re-publish - Implement moveInSharedFolder: re-seal child readKey under dest readKey via sealChildReadKey, walk write chain for writeKey re-link - Add CannotWriteUntilRefetchError with stable code CANNOT_WRITE_UNTIL_REFETCH for tombstoned publish targets - addShareKeysFn never invoked across all six operations - All 29 shared-write tests pass including WRITE-01 security, write-link round-trip, and WRITE-03 tombstone tests
WriteChildRef.childId is a UUID from crypto.randomUUID matching Node.id, not the IPNS name. Clarify deleteFromSharedFolder itemId semantics and remove incorrect module-header annotation.
Release Preview
Cascade Details
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #583 +/- ##
===========================================
+ Coverage 64.64% 83.63% +18.99%
===========================================
Files 152 116 -36
Lines 11585 8153 -3432
Branches 1314 1389 +75
===========================================
- Hits 7489 6819 -670
+ Misses 3853 1088 -2765
- Partials 243 246 +3
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Greptile SummaryThis PR implements the Phase 65 SDK write plane: a structured write chain with Ed25519 signing material sealed under a separate
Confidence Score: 5/5Safe to merge — all findings from the previous review round are addressed and no new blocking issues were found in the write-chain, rotation, bin, or invite-claim paths. The write-rotation and shared-write implementations follow the documented security invariants carefully: key zeroing on all failure paths, fail-closed guards on missing/zero write keys, correct AAD generation matching (uses childRef.generation throughout), deferred tombstone ordering, and the publishNodeFn success-flag guard. The two remaining observations (versionFloor hardcoded to 0n on restore, and the misleading addToBin parameter name) are non-blocking style and correctness nits that do not affect runtime behaviour in the current system. packages/sdk/src/bin/index.ts — restoreFromBin versionFloor and addToBin parameter naming Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant C as rotateWriteFromNode
participant S as rotateWriteSubtree (recursive)
participant IPFS
participant IPNS
participant CB as WriteRevocationCallbacks
C->>S: rotate(rootIpnsName, readKey, writeKey, pendingTombstones[])
S->>IPNS: resolveIpnsRecord(oldIpnsName)
S->>IPFS: fetchFromIpfs(cid)
S->>S: unsealNode(readKey, writeKey) — fail-closed if no writeBody
loop for each WriteChildRef (child-first)
S->>IPNS: resolveIpnsRecord(candidateChildRef)
S->>IPFS: fetchFromIpfs(childCid)
S->>S: unsealChildReadKey + unsealChildWriteKey
S->>S: rotateWriteSubtree(child) [recursive]
S->>S: zero childReadKey + childWriteKey (D-09)
end
S->>S: generateEd25519Keypair + generateRandomBytes(32)
S->>S: rebuild write-body (newIPNSPrivKey, re-sealed child writeKeys)
S->>S: sealNode(updatedNode, readKey, newWriteKey)
S->>IPFS: addToIpfs(sealedNode)
S->>IPNS: "createAndPublishIpnsRecord(newIpnsName, seq=1n)"
S->>S: zero newKeypair.privateKey (D-09)
S->>S: pendingTombstones.push(oldIpnsName)
S->>C: WriteRotationResult
C->>C: "verify rootResult.nodeId == rootNodeId"
loop deferred tombstones
C->>CB: teeUnenrollFn(oldIpnsName)
end
C->>CB: queryWriteGrantsFn(rootNodeId)
loop for each grant
alt grant.isRevoked
C->>CB: deleteWriteGrantFn(shareId)
else survivor
C->>C: wrapKey(newWriteKey, recipientPublicKey)
C->>CB: writeDescriptorRefPersistFn(shareId, wrapped)
end
end
C->>C: zero rootResult.newWriteKey (finally / D-09)
%%{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 C as rotateWriteFromNode
participant S as rotateWriteSubtree (recursive)
participant IPFS
participant IPNS
participant CB as WriteRevocationCallbacks
C->>S: rotate(rootIpnsName, readKey, writeKey, pendingTombstones[])
S->>IPNS: resolveIpnsRecord(oldIpnsName)
S->>IPFS: fetchFromIpfs(cid)
S->>S: unsealNode(readKey, writeKey) — fail-closed if no writeBody
loop for each WriteChildRef (child-first)
S->>IPNS: resolveIpnsRecord(candidateChildRef)
S->>IPFS: fetchFromIpfs(childCid)
S->>S: unsealChildReadKey + unsealChildWriteKey
S->>S: rotateWriteSubtree(child) [recursive]
S->>S: zero childReadKey + childWriteKey (D-09)
end
S->>S: generateEd25519Keypair + generateRandomBytes(32)
S->>S: rebuild write-body (newIPNSPrivKey, re-sealed child writeKeys)
S->>S: sealNode(updatedNode, readKey, newWriteKey)
S->>IPFS: addToIpfs(sealedNode)
S->>IPNS: "createAndPublishIpnsRecord(newIpnsName, seq=1n)"
S->>S: zero newKeypair.privateKey (D-09)
S->>S: pendingTombstones.push(oldIpnsName)
S->>C: WriteRotationResult
C->>C: "verify rootResult.nodeId == rootNodeId"
loop deferred tombstones
C->>CB: teeUnenrollFn(oldIpnsName)
end
C->>CB: queryWriteGrantsFn(rootNodeId)
loop for each grant
alt grant.isRevoked
C->>CB: deleteWriteGrantFn(shareId)
else survivor
C->>C: wrapKey(newWriteKey, recipientPublicKey)
C->>CB: writeDescriptorRefPersistFn(shareId, wrapped)
end
end
C->>C: zero rootResult.newWriteKey (finally / D-09)
Reviews (6): Last reviewed commit: "fix(65): use childRef.generation for bin..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 17
🧹 Nitpick comments (4)
packages/core/src/bin/types.ts (1)
53-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNarrow
nodeRefto the fields the bin actually persists.
BinEntry.nodeRefis typed as a fullNode, butpackages/sdk/src/bin/index.tsonly storesid,kind, andgeneration, padding the rest with placeholder values. That makes it easy for a later caller to treat this as a real node and read bogus metadata. A dedicatedBinNodeRefshape would keep the persisted contract honest.🤖 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/core/src/bin/types.ts` around lines 53 - 59, The persisted bin reference is typed too broadly: BinEntry.nodeRef currently uses the full Node type even though the bin only saves id, kind, and generation. Narrow this field to a dedicated BinNodeRef shape and update the related bin persistence code in packages/sdk/src/bin/index.ts to use that exact contract so callers cannot mistake placeholder metadata for a real node. Keep the new type aligned with the fields actually written and consumed by BinEntry and the restore path.packages/core/src/__tests__/seal-write-chain.test.ts (1)
30-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse fresh key fixtures per test.
These shared
Uint8Arrayconstants make the suite order-dependent if a crypto helper ever mutates an input buffer. Factory helpers or per-test copies would keep the D-09 assertions isolated and easier to trust. As per path instructions,**/*.test.ts: Focus on test coverage, edge cases, and test quality.🤖 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/core/src/__tests__/seal-write-chain.test.ts` around lines 30 - 37, Use fresh key fixtures in the seal-write-chain tests instead of shared Uint8Array constants, since a helper mutating an input would make the suite order-dependent. Refactor the test setup in seal-write-chain.test.ts to create new childWriteKey, parentWriteKey, and wrong parent key instances per test (or via small factory helpers), keeping the D-09 assertions isolated and avoiding shared mutable state across tests.Source: Path instructions
packages/sdk/src/__tests__/bin.test.ts (1)
231-265: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a regression assertion for the folder-side result.
These cases verify bin state and ordering, but they never assert the folder snapshot that
client.tsconsumes afteraddToBin()/restoreFromBin(). A small contract test around returnedpublishedChildren/ sequence (or explicitfolderTreemutation) would catch the stale-cache break here. As per path instructions,**/*.test.ts: Focus on test coverage, edge cases, and test quality.Also applies to: 427-456
🤖 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__/bin.test.ts` around lines 231 - 265, Add a regression assertion in the relevant bin tests for the folder-side contract that `client.ts` relies on, not just `updatedBinState` and item ordering. After `addToBin()` and/or `restoreFromBin()`, verify the returned folder snapshot behavior through `publishedChildren` and sequence updates, or by asserting the expected `folderTree` mutation, using the existing `addToBin` / `restoreFromBin` test setup and symbols like `FolderTree`, `BinState`, and `client.ts`-consumed results to catch stale-cache regressions.Source: Path instructions
packages/sdk-core/src/__tests__/share/grant.test.ts (1)
276-449: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a failure-path assertion for the “never persist on crypto failure” contract.
This suite covers the happy path well, but it never proves that
insertShareFnstays untouched whengetInviteDataFnrejects orreWrapKeythrows. That’s the main guardrail inclaimInvite, so it’s worth pinning down with an explicit test. 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__/share/grant.test.ts` around lines 276 - 449, Add a failure-path test in claimInvite to assert that persistence never happens when crypto or invite lookup fails. In the claimInvite describe block, add cases where getInviteDataFn rejects and where mockFns.reWrapKey throws, then verify insertShareFn is never called and the error propagates. Use the existing claimInvite, getInviteDataFn, insertShareFn, and mockFns.reWrapKey symbols to keep the new assertions aligned with the current helper setup.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/__tests__/rotation/write-body-reseal.test.ts`:
- Around line 202-215: The green-path tests around rotateOne are swallowing
failures with .catch(() => {}), which can mask regressions now that the mocked
unsealNode/sealNode behavior is in place. Update the affected test cases in
write-body-reseal.test.ts to let rotateOne reject naturally instead of catching
it, while still keeping the existing assertions on the captured mock arguments
for the successful path.
In `@packages/sdk-core/src/__tests__/rotation/write-revocation.test.ts`:
- Around line 256-267: The crypto mocks in the write revocation test are reusing
shared fixture arrays, which lets `rotateWriteFromNode` mutate `NEW_*` constants
and leak state into later tests. Update the mock setup around
`generateEd25519Keypair` and `generateRandomBytes` to return fresh key buffers
on each call in this test, and keep the `deriveIpnsName` expectations tied to
those new per-call values so the test remains isolated.
In `@packages/sdk-core/src/rotation/engine.ts`:
- Around line 1451-1480: The root write key in rotateWriteFromNode is only
cleared on the success path, so any failure in teeUnenrollFn,
queryWriteGrantsFn, wrapKey, or writeDescriptorRefPersistFn can leave sensitive
material in memory. Move rootResult.newWriteKey.fill(0) into a finally block
that always runs after the tombstone/grant processing, and preserve the existing
success/error behavior for the callbacks and grant handling. Keep the zeroing
scoped to the rotateWriteFromNode flow so the key is wiped exactly once after
all uses, even when an exception is thrown.
- Around line 1442-1460: The grant mutation flow is using rootNodeId without
verifying it matches the unsealed rotated root, which could target the wrong
node. In the rotation engine around rotateWriteSubtree, bind the authoritative
root from the unsealed result and compare its node.id to rootNodeId before
calling queryWriteGrantsFn, deleteWriteGrantFn, or persisting the descriptor. If
they differ, stop the rotation path and surface an error so grant rewrites only
proceed for the actual rotated root.
- Around line 1364-1371: The rotation flow in engine.ts is ignoring the result
of createAndPublishIpnsRecord, so a rejected first k51 publish can still proceed
to tombstone creation and grant mutation. Update the publish step to capture the
returned { success, sequenceNumber } from createAndPublishIpnsRecord and verify
success before continuing. If the publish is not successful, stop the rotation
path early and avoid the TEE unenroll and grant rewrap work in the surrounding
rotation logic.
- Around line 1320-1329: The child rotation flow in the parent sealing path
leaves each child’s `newWriteKey` alive after `sealChildWriteKey` finishes, even
though the parent is the terminal owner. Update the child-processing block
around `sealChildWriteKey` and the `idToChildResult`/`ipnsToChildResult`
mappings so the child write keys are cleared immediately after they are used to
seal the parent link. Wrap the cleanup in a `finally` so sensitive keys are
removed even if parent sealing or publish fails after some children have already
rotated.
- Around line 1218-1349: rotateWriteSubtree currently continues after unsealNode
returns a node with no writeBody and later reconstructs a new writeBody, which
can incorrectly mint a writable node from a read-only or unrecoverable
write-plane state. Add a fail-closed guard immediately after unsealNode in
rotateWriteSubtree so the function throws unless the recovered node has a valid
writeBody (and expected writeChildren before proceeding), and ensure any
crypto-related recovery path does not fallback to creating a fresh write body.
Reference the unsealNode call and the subsequent write-body rebuild logic in
rotateWriteSubtree when applying the check.
In `@packages/sdk-core/src/share/grant.ts`:
- Around line 300-328: The grant persistence in claimInvite currently validates
trimmed root identifiers but still passes the original untrimmed
params.rootNodeId and params.rootIpnsName into insertShareFn, so trim those
values once and persist the normalized versions instead. Update claimInvite in
grant.ts to use the trimmed rootNodeId and rootIpnsName variables consistently
for the emptiness checks and the insertShareFn payload, keeping the rest of the
flow unchanged.
- Around line 281-319: The claimInvite flow validates caller-owned key buffers
and then awaits getInviteDataFn before using them, so the original Uint8Array
values can be mutated or zeroized before claimInviteReadKey runs. In
claimInvite, snapshot ephemeralPrivateKey and claimerPublicKey into owned copies
before the await, use those copies for the rest of the async flow and the call
to claimInviteReadKey, and ensure the private-key copy is cleared in a finally
block after use.
In `@packages/sdk/src/__tests__/shared-write.test.ts`:
- Around line 412-459: The deleteFromSharedFolder tests are missing coverage for
write-body cleanup, so they can pass even if the republished parent still
retains the removed WriteChildRef. Update the delete tests around
deleteFromSharedFolder, buildSealedParent, and makeSWCtx to include a real
writeChildren entry for the child being deleted, then assert the republished
parent no longer contains that write-body reference in addition to checking
publishedChildren and the sequence number. Keep the existing addShareKeysFn
assertion, but strengthen the test setup so it validates both visible-child
removal and underlying write-body cleanup.
In `@packages/sdk/src/bin/index.ts`:
- Around line 333-336: The folder delete flow in `packages/sdk/src/bin/index.ts`
is only revoking shares for the single `childId`, but the contract requires
revoking every collected subtree IPNS name before the destructive publish.
Update the revoke step in the folder delete path to pass the full collected
subtree/item list from the delete traversal, not just the root child, so
`revokeSharesForItemsFn` is called for all descendants before mutation.
- Around line 467-488: Make the restore flow idempotent in the restore path
around `sdkCore.updateFolderMetadataAndPublish` so a retry after
`saveBinMetadata()` failure does not duplicate the restored child. Before
building `children: [...targetFolder.children, restoredItem]`, check whether the
target folder already contains the restored item (by `ipnsName` or equivalent
unique ref) and reuse the existing entry instead of appending again. Keep the
existing bin cleanup logic (`remainingEntries`, `saveBinMetadata`) unchanged so
a retried restore can finish removing the bin entry.
- Around line 378-399: The publish result from updateFolderMetadataAndPublish()
is being discarded in the bin op flow, leaving the in-memory folder state stale
after delete/restore. Update the bin operation path in index.ts (the code around
the publish call that builds the returned removedItem/updatedBinState) so it
either mutates folderTree with the publishedChildren/newSequenceNumber or
returns that folder delta alongside the bin state. Make the corresponding bin
ops that call updateFolderMetadataAndPublish() propagate the published folder
snapshot so client.ts can read the fresh state when emitting folder:updated.
In `@packages/sdk/src/share/shared-write.ts`:
- Around line 693-718: The shared-file rebuild in the file-node update path is
resetting immutable/history metadata, so preserve the original createdAt and
versions instead of overwriting them with now and an empty array. Update the
logic around nodeContent and fileNode to source these fields from the existing
file state (or extend the relevant params/data passed into shared-write.ts so
they are available here). Keep the caller-supplied fileNodeId and generation
handling unchanged, but ensure the rebuilt Node still carries forward the prior
creation time and version history.
- Around line 613-617: The delete logic in shared-write uses one itemId for two
different identifiers, but the read-body children are matched by ipnsName while
WriteChildRef.childId comes from the UUID created in
createSharedSubfolder()/uploadToSharedFolder(). Update the deletion flow around
shared-write deletion to accept and use separate identifiers for read-body and
write-body removal, and adjust the filters on swCtx.children and
parentWriteChildren accordingly so each side deletes by its own key.
- Around line 247-272: The updated parent envelope from resealAndPublishParent
is currently discarded, which lets later shared writes rebuild from a stale
parent and lose previously added write links. Update resealAndPublishParent to
return the newly sealed parent envelope alongside the published children and
sequence number, and thread that value through the SharedFolderState/shared
write flow so subsequent writes use the latest publishedNode instead of
re-unsealing the pre-write parent. Locate the change around
resealAndPublishParent and the code that reads SharedFolderState.publishedNode
when rebuilding later writes.
In `@tests/sdk-e2e/src/suites/write-chain-rotation.test.ts`:
- Around line 134-154: The helper in publishWriteCapableNode leaves the minted
IPNS private key on the returned keypair even though callers only need ipnsName.
Keep the plaintext seed scoped to publishWriteCapableNode, ensure
createAndPublishIpnsRecord still uses the keypair during publishing, and then
clear or zero the privateKey in a finally block before returning so sensitive
data is not retained in memory.
---
Nitpick comments:
In `@packages/core/src/__tests__/seal-write-chain.test.ts`:
- Around line 30-37: Use fresh key fixtures in the seal-write-chain tests
instead of shared Uint8Array constants, since a helper mutating an input would
make the suite order-dependent. Refactor the test setup in
seal-write-chain.test.ts to create new childWriteKey, parentWriteKey, and wrong
parent key instances per test (or via small factory helpers), keeping the D-09
assertions isolated and avoiding shared mutable state across tests.
In `@packages/core/src/bin/types.ts`:
- Around line 53-59: The persisted bin reference is typed too broadly:
BinEntry.nodeRef currently uses the full Node type even though the bin only
saves id, kind, and generation. Narrow this field to a dedicated BinNodeRef
shape and update the related bin persistence code in
packages/sdk/src/bin/index.ts to use that exact contract so callers cannot
mistake placeholder metadata for a real node. Keep the new type aligned with the
fields actually written and consumed by BinEntry and the restore path.
In `@packages/sdk-core/src/__tests__/share/grant.test.ts`:
- Around line 276-449: Add a failure-path test in claimInvite to assert that
persistence never happens when crypto or invite lookup fails. In the claimInvite
describe block, add cases where getInviteDataFn rejects and where
mockFns.reWrapKey throws, then verify insertShareFn is never called and the
error propagates. Use the existing claimInvite, getInviteDataFn, insertShareFn,
and mockFns.reWrapKey symbols to keep the new assertions aligned with the
current helper setup.
In `@packages/sdk/src/__tests__/bin.test.ts`:
- Around line 231-265: Add a regression assertion in the relevant bin tests for
the folder-side contract that `client.ts` relies on, not just `updatedBinState`
and item ordering. After `addToBin()` and/or `restoreFromBin()`, verify the
returned folder snapshot behavior through `publishedChildren` and sequence
updates, or by asserting the expected `folderTree` mutation, using the existing
`addToBin` / `restoreFromBin` test setup and symbols like `FolderTree`,
`BinState`, and `client.ts`-consumed results to catch stale-cache regressions.
🪄 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: c0e515f5-cdf6-4ea3-b3c2-456168297312
📒 Files selected for processing (52)
.planning/REQUIREMENTS.md.planning/ROADMAP.md.planning/STATE.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-01-PLAN.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-01-SUMMARY.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-02-PLAN.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-02-SUMMARY.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-03-PLAN.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-03-SUMMARY.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-04-PLAN.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-04-SUMMARY.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-05-PLAN.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-05-SUMMARY.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-06-PLAN.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-06-SUMMARY.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-07-PLAN.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-07-SUMMARY.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-CONTEXT.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-DISCUSSION-LOG.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-PATTERNS.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-RESEARCH.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-SECURITY.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-VALIDATION.md.planning/phases/65-sdk-write-chain-bin-re-link-and-invite-claim/65-VERIFICATION.md.planning/todos/completed/2026-06-29-rotateone-placeholder-writekey-phase65.md.planning/todos/pending/2026-06-23-bin-delete-and-empty-bin-leak-content-and-version-cid-pins.md.planning/todos/pending/2026-06-30-phase68-shared-write-context-publishednode-wiring.md.planning/todos/pending/2026-06-30-write-chain-e2e-seed-index-stability.mdapps/web/src/hooks/shared-folder-projection.tspackages/core/src/__tests__/seal-write-chain.test.tspackages/core/src/bin/types.tspackages/core/src/index.tspackages/core/src/node/index.tspackages/core/src/node/seal.tspackages/sdk-core/src/__tests__/rotation/write-body-reseal.test.tspackages/sdk-core/src/__tests__/rotation/write-revocation.test.tspackages/sdk-core/src/__tests__/share/grant.test.tspackages/sdk-core/src/index.tspackages/sdk-core/src/rotation/engine.tspackages/sdk-core/src/rotation/index.tspackages/sdk-core/src/share/grant.tspackages/sdk-core/src/share/index.tspackages/sdk/src/__tests__/bin.test.tspackages/sdk/src/__tests__/client-shared-write.test.tspackages/sdk/src/__tests__/context.test.tspackages/sdk/src/__tests__/shared-write.test.tspackages/sdk/src/bin/index.tspackages/sdk/src/client.tspackages/sdk/src/share/context.tspackages/sdk/src/share/shared-write.tspackages/sdk/src/types.tstests/sdk-e2e/src/suites/write-chain-rotation.test.ts
WriteChildRef.childId is a UUID minted at creation time; filtering by params.itemId (an IPNS name) never matched, leaving a stale WriteChildRef that later crashed rotateWriteFromNode. Add childNodeId param to deleteFromSharedFolder (mirroring moveInSharedFolder) so read-body is filtered by ipnsName and write-body by UUID. Also extend updateSharedFile with optional originalCreatedAt / originalVersions so callers can preserve immutable metadata; Phase-68 will supply prior values. Update client.deleteFromSharedFolder signature to thread childNodeId through. Remove the misleading "childId === ipnsName" module-level comment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a test that seeds the parent write-body with a WriteChildRef, calls deleteFromSharedFolder with both itemId and childNodeId, then unseals the republished parent envelope and asserts writeBody.writeChildren is empty. Extend buildSealedParent to accept optional writeChildren so tests can seed real write-body state. Update existing delete tests to supply the new required childNodeId param. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#6 fail closed when unsealed node has no recoverable write body, preventing a read-only node from being promoted to write-capable without key proof. #2+#7 zero each child newWriteKey immediately after sealChildWriteKey (inline in the map callback) and add a post-Promise.all defensive sweep for any unclaimed child results — D-09 terminal-owner rule. #8 check createAndPublishIpnsRecord return value success flag before firing tombstones or grant mutations so a non-throwing rejection cannot leave the write plane inconsistent. #9 assert rootResult.nodeId matches caller-supplied rootNodeId before grant mutations to prevent rewrapping grants for a different node. #10 wrap tombstone and grant callbacks in try/finally so the root write key is zeroed even when a callback throws — D-09 terminal-owner rule. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#4 remove swallowed .catch handlers in write-body-reseal green-path tests — RED-phase workaround is no longer needed; let failures surface. #5 return fresh Uint8Array copies from crypto mocks in write-revocation tests so engine fill(0) zeroisation does not mutate shared fixture constants and pollute subsequent tests. #5 same treatment for generateRandomBytes write key mocks. Test 8 in write-revocation now captures fresh copies of sealChildWriteKey arguments via a custom mock implementation, since the engine zeroes the child write key after sealing. #20 zero the minted IPNS private key in publishWriteCapableNode e2e helper after publish in a finally block, narrowing the return type to only ipnsName. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- FIX #16 restoreFromBin: check target folder for existing child by ipnsName before appending restoredItem so a retry after saveBinMetadata failure cannot duplicate the restored child ref. - FIX #11 claimInvite: snapshot ephemeralPrivateKey and claimerPublicKey buffers before the first await so a caller zeroing the inputs mid-await cannot corrupt the subsequent re-wrap; zero the owned copy in finally. - FIX #12 claimInvite: trim rootNodeId/rootIpnsName before using and persisting them so whitespace-padded inputs are not stored as-is. - test(65): add idempotency test for restoreFromBin, trim-input test and snapshot-before-await test for claimInvite; update toBe → toStrictEqual for recipientPublicKey (now a snapshotted copy). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eview Entire-Checkpoint: 4710f9a40648
…vives JSON round-trip Entire-Checkpoint: 19c3208fe45e
|
Good catch on the bin restore path — confirmed and fixed in
Fix:
Verified: core bin 44/44, full |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/sdk/src/client.ts (2)
1735-1756: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPersist the newly published envelope.
state.publishedNodeis the source for the next write-body unseal, but this path publishes a newpublishedenvelope and only adopts children/sequence later. Store the successful envelope as well, or the next shared write can operate from stalewriteChildren.Minimal local fix
const pubResult = await sdkCore.createAndPublishIpnsRecord({ ipnsPrivateKey, ipnsName, metadataCid: ipfsResult.cid, sequenceNumber, ctx: this.ctx, }); + state.publishedNode = published; return { tombstoned: false, newSequenceNumber: pubResult.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 1735 - 1756, The publish path in the client state update is only creating the IPFS/IPNS record and returning the new sequence, but it never persists the newly published envelope, so subsequent shared writes can reuse stale writeChildren from state.publishedNode. Update the publish flow in the client state handler around publishNodeFn to store the successful published envelope into state.publishedNode (and keep it aligned with the adopted children/sequence) immediately after a successful publish, so later unseal/write operations use the latest envelope state.
1742-1748: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winHonor BYO pinning for shared-write uploads.
addToIpfsFnandpublishNodeFncallsdkCore.addToIpfsdirectly, bypassingpinWithMode; inexternalmode this routes shared-write encrypted bytes through CipherBox despite the client’s external-pinning contract.Proposed fix
addToIpfsFn: async (data) => { - const result = await sdkCore.addToIpfs(this.ctx, data); + const result = await this.pinWithMode(data, this.ctx); return { cid: result.cid }; }, publishNodeFn: async ({ published, ipnsName, ipnsPrivateKey, sequenceNumber }) => { const bytes = new TextEncoder().encode(JSON.stringify(published)); - const ipfsResult = await sdkCore.addToIpfs(this.ctx, bytes); + const ipfsResult = await this.pinWithMode(bytes, this.ctx);🤖 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 1742 - 1748, The shared-write upload path in addToIpfsFn and publishNodeFn is bypassing BYO pinning by calling sdkCore.addToIpfs directly, which can route external-mode uploads through CipherBox instead of the client’s pinning contract. Update these code paths to use the existing pinWithMode flow (or the shared helper it delegates to) so the current pinning mode is honored before returning the CID / publishing IPNS state. Keep the fix localized around addToIpfsFn, publishNodeFn, and pinWithMode in packages/sdk/src/client.ts.
🤖 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/core/src/bin/encrypt.ts`:
- Around line 47-53: The fromBinWireForm() rehydration path currently accepts
any hex string and converts it with hexToBytes without verifying the resulting
nodeReadKey length. Update the entry mapping inside fromBinWireForm() to
validate that the decoded Uint8Array is exactly 32 bytes, and reject malformed
input by throwing a clear error before returning metadata. Keep the fix
localized to fromBinWireForm() and the nodeReadKey handling so invalid key
material cannot reach restore/key-sealing code.
In `@packages/sdk/src/client.ts`:
- Around line 1851-1854: `deleteFromSharedFolder` currently accepts
`childNodeId` without enforcing it, so JS callers can omit it and leave the
write-body `WriteChildRef` stale. Add an explicit validation at the start of
`deleteFromSharedFolder` in `Client` to fail fast when `args.childNodeId` is
missing, and prevent the delegate call from proceeding unless both `itemId` and
`childNodeId` are present.
---
Outside diff comments:
In `@packages/sdk/src/client.ts`:
- Around line 1735-1756: The publish path in the client state update is only
creating the IPFS/IPNS record and returning the new sequence, but it never
persists the newly published envelope, so subsequent shared writes can reuse
stale writeChildren from state.publishedNode. Update the publish flow in the
client state handler around publishNodeFn to store the successful published
envelope into state.publishedNode (and keep it aligned with the adopted
children/sequence) immediately after a successful publish, so later unseal/write
operations use the latest envelope state.
- Around line 1742-1748: The shared-write upload path in addToIpfsFn and
publishNodeFn is bypassing BYO pinning by calling sdkCore.addToIpfs directly,
which can route external-mode uploads through CipherBox instead of the client’s
pinning contract. Update these code paths to use the existing pinWithMode flow
(or the shared helper it delegates to) so the current pinning mode is honored
before returning the CID / publishing IPNS state. Keep the fix localized around
addToIpfsFn, publishNodeFn, and pinWithMode in packages/sdk/src/client.ts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a5c05002-6cf7-4a5b-8b5b-179f81541f9c
📒 Files selected for processing (14)
.planning/todos/pending/2026-06-30-phase68-shared-write-context-publishednode-wiring.mdpackages/core/src/__tests__/bin.test.tspackages/core/src/bin/encrypt.tspackages/sdk-core/src/__tests__/rotation/write-body-reseal.test.tspackages/sdk-core/src/__tests__/rotation/write-revocation.test.tspackages/sdk-core/src/__tests__/share/grant.test.tspackages/sdk-core/src/rotation/engine.tspackages/sdk-core/src/share/grant.tspackages/sdk/src/__tests__/bin.test.tspackages/sdk/src/__tests__/shared-write.test.tspackages/sdk/src/bin/index.tspackages/sdk/src/client.tspackages/sdk/src/share/shared-write.tstests/sdk-e2e/src/suites/write-chain-rotation.test.ts
✅ Files skipped from review due to trivial changes (1)
- .planning/todos/pending/2026-06-30-phase68-shared-write-context-publishednode-wiring.md
🚧 Files skipped from review as they are similar to previous changes (9)
- packages/sdk-core/src/tests/share/grant.test.ts
- packages/sdk-core/src/tests/rotation/write-body-reseal.test.ts
- tests/sdk-e2e/src/suites/write-chain-rotation.test.ts
- packages/sdk-core/src/tests/rotation/write-revocation.test.ts
- packages/sdk-core/src/share/grant.ts
- packages/sdk-core/src/rotation/engine.ts
- packages/sdk/src/tests/shared-write.test.ts
- packages/sdk/src/bin/index.ts
- packages/sdk/src/share/shared-write.ts
…eFromSharedFolder childNodeId Entire-Checkpoint: c7f2d3a74cdf
…nor BYO pinning Entire-Checkpoint: e99a4ebfa963
|
Addressing the 2 outside-diff-range comments on 1. I deliberately left 2. |
…hNodeFn The shared-write publishNodeFn closure ignored pubResult.success, so a 2xx relay response carrying success:false would proceed as if the node were committed — leaving the parent's SealedChildRef pointing at an IPNS name that was never published. Guard on success and throw, mirroring rotateWriteSubtree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 9e04bdf55b05
…ration addToBin reconstructed the child-readkey AAD from publishedNode.generation, sourced from an independent IPNS resolve of the child. A stale-CID serve makes that diverge from childRef.generation (the parent mirror the readKeySealed blob was sealed under), so unsealChildReadKey fails GCM auth closed even when the parent folder state is current. Pass childRef.generation per the §2.6 generation-source rule, matching moveItem and navigate.ts. Also capture nodeRef.generation from childRef so restoreFromBin re-seals under the same generation as the unsealed key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 1af3242d5c34
Summary
Phase 65 builds the SDK write plane for the v2.0 metadata/sharing refactor: a write-body that carries Ed25519 signing material sealed under a separate
writeKey, full (c) Ed25519 write-revocation per ADR-0001, bin restore as a pure re-link, and invite claim as a single-key re-wrap. Delivers WRITE-01..04. 7 plans across 4 waves, verified 4/4 must-haves, with the heaviest operation proven end-to-end against the live API.What shipped
sealChildWriteKey/unsealChildWriteKey(role0x04, AAD-bound) added to@cipherbox/core;packages/sdk/src/share/shared-write.tsrewritten onto the Phase-62 write-body model (six former stubs implemented). A read-only holder with only thereadKeycan never reach signing material —unsealNode(published, readKey)returns nowriteBody(asserted inshared-write.test.ts).rotateWriteFromNode(packages/sdk-core/src/rotation/engine.ts): full Ed25519 rotation of the write plane for a subtree, child-first cascade, new k51 name per node (first-publish seq1n), parent re-point to the share root, tombstone-intent viateeUnenrollFn, surviving co-writer re-wrap viawrapKey, revoked recipient dropped. Read plane stays invariant.CannotWriteUntilRefetchErrorthrown by every shared-write op when the injected publish seam reports a tombstoned/rotated-out target.rotateOnenow threads the realwriteKeythrough all three re-seal sites; the all-zerosPLACEHOLDER_WRITE_KEYis removed (folds carried-forward FLAG-63-U1).sealChildReadKeyof the node's ownreadKeyunder the destination parent — no content re-encryption);BinEntry.nodeReadKey/nodeIpnsNameadded; legacyoriginalFolderKeyEncryptedpath deleted.readKeyvia the existingclaimInviteReadKeyprimitive and persists one standard grant — noencryptedChildKeysfan-out.tests/sdk-e2e/src/suites/write-chain-rotation.test.tsround-trip against the live docker API.Gates
pnpm typecheck(full dependency-ordered build+typecheck chain): green.write-chain-rotation2/2 against the real API (asserts new k51 per node, parent re-point cascade, tombstone-intent, co-writer re-wrap, read-plane invariance).65-VERIFICATION.md).Notable deviation
Plan 65-04's
SharedWriteContextreshape under-scoped its consumers; the post-merge gate caught it andfix(65-04)reconciled the surface:packages/sdk/src/share/context.ts,client.ts:buildSharedWriteContextFromState,SharedFolderState, plus a clearly Phase-68-marked placeholder bridge inapps/web/src/hooks/shared-folder-projection.ts(optionalwriteKey?/publishedNode?defaults; the web shared-write path was already non-functional pre-phase-65). Full typecheck + SDK + web-hook tests green after the fix.Deferred / out of scope (tracked)
packages/sdk/src/client.tssubtree IPNS collectstub is unimplemented — no phase-65 plan ownsclient.ts; carry-forward.tests/sdk-e2etype debt in other suites (batch-upload,ipns-consistency,file-operations,folder-crud,bin-operations) references types removed in the earlier metadata refactor — not touched by phase 65; tracked by existingremigrate-filepointer-era-e2e-helper-scripts+upload-batch-test-mock-type-drifttodos. sdk-e2e is not in CI.resolves_phase: 65→66(its fix lives inapps/api/src/shares/*, which phase 65 is prohibited from touching).Review hardening (CodeRabbit triage)
CodeRabbit raised 23 findings; triaged via parallel verify-then-fix worktree agents (18 fixed, 3 skipped with reason, 2 deferred to Phase 68, 1 deferred test-hardening todo):
rotateWriteFromNodereturns the new root IPNS name; tighter key zeroization (post-publish, andchildReadKeyzeroed on the write-key-unseal error path); strengthened rotation/e2e assertions (the e2e now proves the persisted survivor descriptor unseals to the rotated root write key).updateSharedFilekeepsWriteChildRef.childIdaligned with the file Node id (no fresh UUID);moveInSharedFolderpublishes the destination before removing the source; parent re-publish uses the next sequence number; fail-closed when no write key is resolvable; success-path key buffers zeroed before release.restoreFromBinfails closed on missingnodeRef/nodeIpnsName(no wrong-AAD binding);addToBinpersists the bin entry before republishing the source (no orphan on failure).claimInviterejects empty/whitespaceinviteToken/rootNodeId/rootIpnsNamebefore any I/O.publishedNodewriteback + real placeholder values (Phase 68 — see todo); the e2e fixed-index seed identification (test-hardening todo).All gates re-run green after the fixes: full
pnpm typecheck, sdk-core 318, sdk 208, live D-04 e2e 2/2.Test plan
pnpm typecheckgreenwrite-chain-rotatione2e green against local stack65-VERIFICATION.md)🤖 Generated with Claude Code
Summary by CodeRabbit