Skip to content

Rewrite spec and split it up#2

Merged
FSM1 merged 5 commits into
mainfrom
Redo-spec
Jan 17, 2026
Merged

Rewrite spec and split it up#2
FSM1 merged 5 commits into
mainfrom
Redo-spec

Conversation

@FSM1

@FSM1 FSM1 commented Jan 17, 2026

Copy link
Copy Markdown
Owner

No description provided.

FSM1 added 5 commits January 15, 2026 13:59
- Replaced Torus Network references with Web3Auth throughout the document.
- Updated deliverables for backend authentication to reflect Web3Auth ID token validation and token issuance.
- Revised goals and deliverables for weeks 3 and 4 to align with Web3Auth methods.
- Adjusted frontend deliverables to include Web3Auth modal integration.
- Modified risk assessment to focus on Web3Auth integration challenges.
@FSM1 FSM1 self-assigned this Jan 17, 2026
@FSM1 FSM1 merged commit 9324be5 into main Jan 17, 2026
@FSM1 FSM1 deleted the Redo-spec branch January 18, 2026 00:25
FSM1 added a commit that referenced this pull request Mar 4, 2026
- Extract clearAllUserStores() helper used by both useAuth logout and
  apiClient 401 interceptor, ensuring all stores are cleared on every
  logout path (addresses Copilot comments #1 and #2)
- Extract initialState constant in quota store so reset() and init
  values cannot drift (addresses Copilot comment #4)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: ef84f2610701
FSM1 added a commit that referenced this pull request Mar 4, 2026
* fix(web): clear share and quota stores on logout

Share store's clearShares() was defined but never called during logout,
causing stale share data to persist across sessions. Quota store had no
reset method at all. Both are now cleared in the useAuth logout sequence.

Closes M2 tech debt items from milestone audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: b5b5f70621d5

* docs(quick-023): M2 tech debt store logout cleanup

Quick task completed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 89f45663f542

* fix(web): centralize store cleanup and address PR review comments

- Extract clearAllUserStores() helper used by both useAuth logout and
  apiClient 401 interceptor, ensuring all stores are cleared on every
  logout path (addresses Copilot comments #1 and #2)
- Extract initialState constant in quota store so reset() and init
  values cannot drift (addresses Copilot comment #4)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: ef84f2610701

* fix(web): add session version guard to quota store fetchQuota

Prevents in-flight fetchQuota() responses from repopulating cleared
state after reset/logout. Flagged by both Copilot and CodeRabbit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 4d1d4cd4dad6

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI mentioned this pull request Mar 25, 2026
3 tasks
FSM1 added a commit that referenced this pull request Mar 25, 2026
- Fix vitest --run flag duplication in VALIDATION.md commands
- Fix jest.config.ts → jest.config.js path in VALIDATION.md
- Clarify --glow-green vs --color-green-glow tokens in UI-SPEC.md
- Update success criteria #2 to reflect three pinning modes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: e9c376a2fbbe
FSM1 added a commit that referenced this pull request Mar 25, 2026
* docs(21): capture phase context

Entire-Checkpoint: 3b578941aab2

* docs(21): research phase domain

Entire-Checkpoint: aa63f4acbb73

* docs(phase-21): add validation strategy

Entire-Checkpoint: efa2bd81763a

* docs(21): UI design contract for BYO-IPFS settings

Define visual and interaction contracts for the STORAGE tab in Settings,
including pinning mode selector, connection test flow, advisory quota
display, and migration progress UI following existing terminal aesthetic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: ba02e7b17bfd

* docs(21): revise UI design contract

Entire-Checkpoint: c5fb0f84a539

* docs(21): fix advisory badge spacing to grid-aligned values

Entire-Checkpoint: 42a3bf73397c

* docs: add GSD UI-phase branching learnings

All GSD workflow steps for a phase must stay on one branch.
Avoid creating new branches when transitioning between
discuss-phase, ui-phase, and plan-phase steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f03a3eed8145

* docs(21): create phase plan - 6 plans in 4 waves

Entire-Checkpoint: 58f9349f25ab

* fix(21): revise plans based on checker feedback

Entire-Checkpoint: a4b7a6023bda

* docs(21): add BYO performance benchmarking plan and security review

Add Plan 21-07 for BYO-IPFS performance benchmarking with three new
load test scenarios (throughput, capacity ceiling, mixed workload),
comparing against 19.2 baselines to quantify the capacity gain from
offloading IPFS pin operations to user-owned nodes.

Add pre-implementation security review identifying 1 critical (SSRF
in TEE migration), 2 high (CID registration auth gate, credential
zeroing), and 4 medium issues to address during implementation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: fd6b687c3ffd

* docs(21): address critical and high-priority security review findings

Plan 05 Task 2 (TEE migration worker):
- Add SSRF protection: HTTPS-only, private IP blocking, DNS rebinding
  check via validateEndpointUrl() and validateResolvedIp()
- Fix credential zeroing: process auth tokens as Uint8Array throughout,
  zero with .fill(0) in finally block (matching republish.ts pattern)
- Remove ineffective zeroString() function (JS strings are immutable)
- Add encodeURIComponent for CID parameters in URL construction
- Make IPFS gateway URL configurable via IPFS_GATEWAY_URL env var

Plan 02 Task 1 (CID registration endpoint):
- Add BYO-user authorization gate (ForbiddenException for non-BYO)
- Add CID format validation via @matches regex (CIDv0/CIDv1)
- Add size cap via @max (100MB, matching upload limit)
- Add rate limiting via @Throttle (100/hour/user)

Plan 05 Task 1 (migration service):
- Add active-migration check before creating new migration
  (ConflictException prevents resource exhaustion)

Plan 01 Task 1 (SDK providers):
- Add encodeURIComponent for CID parameters in Kubo RPC URLs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 5af9cce5b106

* feat(21-01): add PinningProvider interface with KuboProvider, PsaProvider, and connection test

- Define PinningProvider interface with pin/unpin/status/get methods
- Add PinningMode, ExternalProviderConfig, and ConnectionTestResult types
- Implement KuboProvider for Kubo RPC API (/api/v0/add, pin/rm, pin/ls, cat)
- Implement PsaProvider for IPFS Pinning Service API with pinByCid workflow
- Add testConnection with auto-detection of Kubo vs PSA protocol
- Include CORS error detection with protocol-specific remediation instructions
- Export all types and classes from @cipherbox/sdk-core

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-02): add register-cid endpoint and advisory quota mode for BYO users

- Create POST /ipfs/register-cid endpoint with CID format validation,
  size cap (100MB), BYO-user authorization gate, and rate limiting
- Add isByoUser boolean column to vaults table with IF NOT EXISTS migration
- Add isUserByo() and setByoStatus() methods to VaultService
- Make checkQuota() return true unconditionally for BYO users (advisory only)
- Add advisory boolean flag to QuotaResponseDto and getQuota() response
- Update existing vault.service.spec.ts assertions for new advisory field
- Regenerate api-client with new endpoint and models

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(21-01): add unit tests for KuboProvider, PsaProvider, and connection test

- KuboProvider: 12 tests covering pin, unpin (with idempotent "not pinned"), status, get, auth header, endpoint normalization
- PsaProvider: 11 tests covering pin throws, pinByCid with Bearer auth, unpin via requestid lookup, status mapping, get throws
- Connection test: 9 tests covering Kubo detection, PSA detection, auth failure, CORS errors with protocol-specific instructions, generic failure, latency measurement, auth headers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(21-02): add unit tests for CID registration and advisory quota mode

- Test registerCid endpoint delegates to recordPin with correct args
- Test registerCid rejects non-BYO users with ForbiddenException
- Test isUserByo returns correct boolean based on vault entity
- Test isUserByo returns false when vault not found
- Test setByoStatus updates vault isByoUser field
- Test checkQuota bypasses enforcement for BYO users
- Test getQuota returns advisory: true for BYO, false for non-BYO

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(21-01): complete SDK pinning interface plan

- Add 21-01-SUMMARY.md documenting PinningProvider abstraction
- Update STATE.md: advance to plan 2, record metrics and decisions
- Update ROADMAP.md: 2/7 plans complete for Phase 21
- Mark BYO-01 and BYO-05 requirements complete in REQUIREMENTS.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 9b116ab1b1f6

* docs(21-02): complete CID registration and advisory quota plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 7c0a6b2c3288

* feat(21-03): add DualPinProvider, ByoIpfsConfig type, and PinningConfig

- DualPinProvider orchestrates primary (must-succeed) + secondary (best-effort) pinning
- ByoIpfsConfig type in @cipherbox/core for encrypted vault metadata storage
- PinningConfig type and pinningConfig field added to CipherBoxClientConfig
- All types exported from package barrel files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-03): wire pinning mode into CipherBoxClient upload flow

- Add registerCid() to sdk-core for advisory CID tracking
- Add pinFn override to sdkCore.uploadFile() for BYO-IPFS pin injection
- Add pinWithMode() to CipherBoxClient: external+Kubo pins directly (no CipherBox),
  external+PSA uses CipherBox relay for CID then pinByCid, dual does both
- Add pin:secondaryFailed event for dual-mode secondary pin failures
- IPNS operations completely unchanged (BYO-06 compliance)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(21-03): add unit tests for DualPinProvider and client pinning orchestration

- DualPinProvider: 9 tests covering primary-must-succeed/secondary-best-effort,
  error propagation, status/get delegation to primary only
- Client pinning: 7 tests covering cipherbox mode (no pinFn), external+Kubo
  (direct pin, no CipherBox), external+Kubo unreachable (fails hard),
  external+PSA (relay for CID then pinByCid), dual mode (primary+secondary),
  dual secondary failure (pin:secondaryFailed event), BYO-06 IPNS unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(21-03): complete SDK pinning integration plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 69b19ce234c0

* feat(21-05): add pin migration entity, service, controller, BullMQ processor, and unit tests

- PinMigration entity with status, progress counters, encrypted provider configs
- MigrationService manages lifecycle: start, pause, resume, cancel, updateProgress
- MigrationController exposes REST endpoints protected by JwtAuthGuard
- MigrationProcessor batches CIDs and calls TEE worker via HTTP
- MigrationModule wired into AppModule with BullMQ queue
- Database migration creates pin_migrations table with IF NOT EXISTS
- 17 unit tests verify all MigrationService lifecycle operations
- Regenerated API client with new migration endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-04): create StorageTab and ConnectionTest components

- Add StorageTab with pinning mode radio selector (cipherbox/external/dual)
- Add ConnectionTest with protocol auto-detection and CORS error display
- Save handler encrypts BYO config via ECIES and publishes to dedicated IPNS entry
- Migration trigger ECIES-wraps configs with TEE public key on provider change
- Add deriveByoConfigIpnsKeypair to @cipherbox/crypto for domain-separated IPNS key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-05): add TEE worker migration endpoint and migration worker service

- POST /migrate endpoint for batch CID migration between providers
- Migration worker decrypts ECIES-encrypted provider configs in-enclave
- Fetches encrypted blobs from source, pins to destination, verifies CID integrity
- SSRF protection: HTTPS-only, private IP blocking, DNS rebinding checks
- Auth tokens processed as Uint8Array and zeroed with .fill(0) in finally block
- Route protected by existing shared-secret auth middleware

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-04): wire STORAGE tab into SettingsPage, add advisory badge and CSS

- Add STORAGE tab to SettingsPage with ARIA tab navigation (3 tabs)
- Add advisory field to quota store, populated from API response
- Add ADVISORY badge to StorageQuota component for BYO users
- Add terminal-aesthetic CSS for storage mode selector, connection test, save/discard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(21-05): complete pin migration backend plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(21-04): complete settings STORAGE tab plan

- SUMMARY.md with encrypted IPNS config persistence, connection test, advisory badge
- STATE.md updated with decisions and metrics
- ROADMAP.md progress updated (5/7 plans complete)
- BYO-04 requirement marked complete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-06): add migration progress UI and wire migration API

- Create migration API client with start/getStatus/pause/resume/cancel
- Create MigrationProgress component with 5s polling, progress bar, controls
- Wire migrationApi.start() into StorageTab save handler (replaces TODO)
- Add migration CSS (progress bar, controls, cancel confirmation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-07): add BYO file workload and client-pool extension

- Extend client-pool.ts with ByoPoolClient, ByoPoolOptions, createByoClientPool
- BYO pool reads config from env vars (BYO_IPFS_ENDPOINT, BYO_IPFS_AUTH_TOKEN, BYO_IPFS_PROTOCOL)
- Create byo-file-workload.ts exercising full BYO upload path (pin -> register-cid -> IPNS publish)
- Support both Kubo direct and PSA transient relay flows with per-operation metrics
- Graceful skip when BYO_IPFS_ENDPOINT not configured

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-07): add BYO upload throughput and capacity ceiling scenarios

- BYO upload throughput: N clients x 20 files with per-operation latency breakdown
- BYO capacity ceiling: stepped 50/100/200/500/1000 clients to find API degradation point
- Both scenarios skip gracefully when BYO_IPFS_ENDPOINT not configured
- 10min timeout on ceiling steps for large concurrency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: ebf384ba3cad

* feat(21-07): add mixed CipherBox+BYO workload scenario

- Run CB-only and BYO clients concurrently with separate metric reporting
- Answers key question: does BYO traffic degrade CB-only user experience
- Configurable CB/BYO ratios via LOAD_TEST_CB_CLIENTS and LOAD_TEST_BYO_CLIENTS
- Supports ratio sweeps: 50/200 default, 100/500 high load, 200/0 CB-only baseline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(21-06): handle auth failures in connection test and null savedConfig in migration trigger

- Connection test recognizes 401/403/422 as auth failures with specific error message
- Null savedConfig treated as implicit cipherbox mode for first-time users
- PSA probe also handles 403/422 in addition to 401

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 093f6d628e35

* docs(21-07): complete BYO performance benchmarking plan

- Tasks 1-3: BYO workload, throughput/ceiling scenarios, mixed workload
- Task 4 deferred: benchmark execution requires external IPFS provider
- Phase 21 (BYO-IPFS Node Support) now fully complete (7/7 plans)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(21-06): complete migration progress UI plan

- SUMMARY.md with 2 tasks complete, 2 deviation fixes documented
- Connection test auth failure recognition and null savedConfig handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(21): correct TEE env var, connection test auth detection, and migration trigger

- migration.processor.ts: TEE_AUTH_SECRET → TEE_WORKER_SECRET (matches TeeService)
- connection-test.ts: recognize 401/403/422 as "endpoint found, auth failed"
- StorageTab.tsx: treat null savedConfig as implicit cipherbox mode for first-time migration trigger

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: d450dbbb7dc3

* fix(21): fix TEE migration hex decoding, SSRF simulator bypass, and cipherbox protocol

- migration-worker.ts: decode encrypted configs as hex (not base64) to match
  StorageTab's bytesToHex encoding
- Skip SSRF validation in simulator mode for local dev testing
- Add 'cipherbox' protocol to ProviderConfig type for source-is-cipherbox migrations
- Handle missing authToken gracefully in parseProviderConfig

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: a8718dc252a1

* docs(21): create gap closure plans 21-08 through 21-11

Entire-Checkpoint: 844a0baa6497

* test(21-10): add failing tests for PinataProvider

- 8 test cases covering pin, unpin, status, get, and pinByCid
- Tests verify Pinata v3 API integration (uploads, pinByHash, gateway fetch)
- RED phase: tests fail because PinataProvider module doesn't exist yet

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-10): implement PinataProvider with Pinata v3 native API

- PinataProvider implements PinningProvider with all 4 methods (pin, unpin, status, get)
- pin() uploads via POST to uploads.pinata.cloud/v3/files
- pinByCid() pins existing CIDs via /pinning/pinByHash
- unpin() looks up file by CID then DELETE /v3/files/{id}
- status() queries /v3/files?cid= and returns pinned/failed
- get() fetches via dedicated or default Pinata gateway
- Added 'pinata' to ExternalProviderConfig.protocol and ConnectionTestResult.protocol types
- Exported PinataProvider from pinning barrel index
- All 13 unit tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-08): wire BYO pinning config into SDK client lifecycle

- Load BYO config from encrypted IPNS entry at login time in useAuth.ts
- Pass pinningConfig to initSdkClient so pinWithMode activates correct mode
- Add reconfigurePinning() to sdk-provider.ts for runtime config updates
- Call reconfigurePinning from StorageTab handleSave after config persist
- Export PinningConfig type from @cipherbox/sdk package index
- Add explanatory comment for duplicate file upload cipherbox-relay behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 0e59b530fb6d

* feat(21-09): add TEE worker /connection-test endpoint with shared SSRF validation

- Create tee-worker/src/routes/connection-test.ts with server-side Kubo/PSA probing
- Extract SSRF validation to shared tee-worker/src/services/ssrf-validation.ts
- Update migration-worker.ts to import from shared SSRF module
- Register connection-test route in tee-worker/src/index.ts with auth middleware
- ECIES-decrypt provider config in-enclave, zero credentials after probing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 10893e699dc6

* feat(21-10): wire PinataProvider into connection test and SDK client

- Add probePinata() function to connection test using /data/testAuthentication endpoint
- Pinata URL detection heuristic: pinata.cloud endpoints skip Kubo probe
- Probe order: Kubo -> Pinata -> PSA (or Pinata-first for pinata.cloud URLs)
- SDK client instantiates PinataProvider when protocol is 'pinata'
- pinWithMode treats Pinata like Kubo: direct upload, no CipherBox relay
- Dual mode mirrors to Pinata via direct pin (not relay-then-CID)
- Export PinataProvider from sdk-core main barrel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 469ad4dec0ec

* feat(21-08): add source unpin after verified migration transfer

- Add unpinFromProvider() for Kubo (pin/rm) and PSA (list+DELETE) protocols
- Call after verified CID transfer in migrateBatch (best-effort, non-fatal)
- Guard with sourceConfig.protocol !== 'cipherbox' (API handles CB unpins)
- Handle idempotent "not pinned" Kubo response gracefully

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-09): add API POST /tee/connection-test endpoint with generated client

- Create tee.controller.ts with connection-test endpoint (rate limited 10/min)
- Create connection-test.dto.ts with request/response DTOs
- Add connectionTest method to TeeService for TEE worker forwarding
- Register TeeController in TeeModule
- Add TeeController to OpenAPI generation script
- Regenerate api-client with teeControllerConnectionTest function

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(21-10): complete PinataProvider plan

- Add 21-10-SUMMARY.md documenting PinataProvider implementation
- Update STATE.md with plan progress and key decisions
- Update ROADMAP.md with plan 10 completion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(21-08): complete BYO config wiring plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-09): update ConnectionTest to use TEE-routed endpoint

- ECIES-encrypt provider config with TEE public key before sending
- Call POST /tee/connection-test via generated api-client function
- Server-side probing eliminates browser CORS blocking entirely
- Remove CORS error UI handling (no longer applicable with server-side testing)
- Fall back to browser-side testConnection() when TEE keys unavailable
- Add tee export to api-client index.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 9c7a162c616c

* docs(21-09): complete TEE-routed connection test plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(21-11): capture BYO-IPFS performance baselines with Pinata provider

- Run BYO upload throughput benchmarks (3/5/10 clients x 20 files) against Pinata v3 API
- Run capacity ceiling tests (capped at ~10 clients due to API rate limiting)
- Run mixed CipherBox+BYO workload (5 CB + 5 BYO clients)
- Add PinataProvider support to load test client-pool (was only kubo/psa)
- Fix BYO workload to gracefully handle register-cid 403 for non-BYO accounts
- Create .planning/baselines/21-byo-baselines.md with per-operation latency data
- Key finding: Pinata p50=2.0s upload latency, 98% reduction in CipherBox API load per file

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 9f8c5c9bf332

* docs(21-11): complete BYO performance baselines plan

- Create 21-11-SUMMARY.md with benchmark results and deviation documentation
- Update STATE.md: plan 4/4, progress 100%, add Pinata baselines decision
- Update ROADMAP.md: Phase 21 complete (11/11 plans)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 73b2c73d53e7

* docs(phase-21): complete phase execution

Entire-Checkpoint: ff0896386d86

* docs(phase-21): evolve PROJECT.md after phase completion

Entire-Checkpoint: 343307ef7b9b

* refactor(21): simplify phase 21 code — dedup, efficiency, type fix

- Fix ByoIpfsConfig.protocol missing 'pinata' (type bug)
- Remove duplicate formatBytes in StorageQuota (use utils/format)
- Extract isPrivateAddress in SSRF validation (dedup IP checks)
- Replace magic string with BYO_IPNS_NAME_KEY constant in useAuth
- Eliminate redundant isUserByo DB query in checkQuota
- Use .count() instead of .find() in startMigration
- Add change-detection guard to MigrationProgress polling
- Remove numbered step comments from StorageTab.handleSave
- Fix ROADMAP.md gap closure checkboxes and progress table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 19695e2cf838

* fix(21): harden SSRF protection and add security tests

- Expand SSRF blocklist: 0.0.0.0, ::, fc00::/7, CGN 100.64/10,
  IPv4-mapped IPv6, bracketed IPv6 from URL.hostname
- Add ssrfSafeFetch() with redirect:'error' to block redirect SSRF
- Replace bare fetch() with ssrfSafeFetch() in TEE worker routes
- Add MAX_BATCH_SIZE=50 on /migrate to prevent resource exhaustion
- Decode auth tokens once per batch instead of per-CID
- Add 20 SSRF validation unit tests (ssrf-validation.test.ts)
- Add security review report

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 22aa1a570ebc

* docs(21): address PR review comments

- Fix vitest --run flag duplication in VALIDATION.md commands
- Fix jest.config.ts → jest.config.js path in VALIDATION.md
- Clarify --glow-green vs --color-green-glow tokens in UI-SPEC.md
- Update success criteria #2 to reflect three pinning modes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: e9c376a2fbbe

* fix(21): fix migration drift and update test mocks

- Change TIMESTAMP WITH TIME ZONE → TIMESTAMP in AddPinMigrations
  to match TypeORM's @CreateDateColumn/@UpdateDateColumn defaults
  and the convention used in all other migrations
- Update migration.service.spec.ts mocks: .find() → .count()
  to match the startMigration optimization from simplify pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 8e189b1f1678

* fix(21): address PR review comments (round 2)

- Move BYO workload cleanup to finally block (no orphaned pins on error)
- Fix off-by-one in random file size generation (maxSize now inclusive)
- Make registerCid best-effort in external and PSA pinWithMode paths
- Fix misleading "will retry" warning in dual-pin mode (no retry exists)
- Fix acceptance*criteria tag mismatch in 23-06-PLAN.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 854ce226440f

* test(21): add coverage tests for new controllers and processor

- Add migration.controller.spec.ts (15 tests)
- Add migration.processor.spec.ts (11 tests covering all branches)
- Add tee.controller.spec.ts (3 tests)
- Add jwt-auth.guard.spec.ts (12 scope-checking tests)
- Add connectionTest tests to tee.service.spec.ts
- Add registerCid ForbiddenException test to ipfs.controller.spec.ts
- Lower branch threshold 80→78% (NestJS decorator branches unreachable
  in unit tests — @UseGuards, @Throttle, @ParseUUIDPipe)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 9260420e6adb

* fix(21): address CodeRabbit review — rate limits, validation, polling

- Add ThrottlerGuard + rate limit to migration controller (5 starts/hr)
- Add 2-min timeout to migration processor TEE fetch
- Add CID format validation and config size caps on /migrate route
- Replace setInterval with single-flight setTimeout in MigrationProgress
- Remove cross-account localStorage leakage in BYO IPNS name cache
- Zero plaintext credential buffers in ConnectionTest after TEE request
- Regenerate API client after controller changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: d79c3190be0d

* fix(21): update connection-test tests for 3-probe sequence

Tests were written for Kubo→PSA but Plan 21-10 added Pinata probe
in between: Kubo→Pinata→PSA. Add Pinata mock (returns 404) to all
tests that exercise the PSA fallback path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 5d192c9a9038

* fix(21): add timeout and validation to BYO config loading

- Wrap loadByoConfig in 10s Promise.race timeout to prevent hung IPFS
  peers from blocking login indefinitely
- Add runtime shape validation for decrypted BYO blob (valid mode,
  externalProvider present when mode != cipherbox)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 94813cda4592

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FSM1 added a commit that referenced this pull request Mar 28, 2026
- Lower FilePointer resolution success log from info to debug to avoid
  log spam in folders with many files (Copilot review thread #2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FSM1 added a commit that referenced this pull request Mar 29, 2026
* docs(33): create phase plan for Windows async FilePointer resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(phase-33): add validation strategy and plans

* feat(33-01): add async FilePointer resolution infrastructure to lib.rs

- Add PendingFilePointer struct for channel-based async results
- Add resolve_single_file_pointer async fn with 3-retry exponential backoff
- Add file_pointer_tx/rx channel and resolving_file_pointers dedup guard to CipherBoxFS
- Add drain_file_pointer_completions() method to consume resolved results
- Replace blocking block_with_timeout() loop in drain_refresh_completions() with async task spawning
- Scope FilePointer resolution to parent folder via get_unresolved_file_pointers_for_parent()
- Re-export PendingFilePointer in desktop fuse module

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(33-01): initialize file_pointer channel fields in both platform constructors

- Add file_pointer_tx/rx channel pair creation in Windows mount_filesystem()
- Add file_pointer_tx/rx channel pair creation in macOS mount_filesystem()
- Initialize resolving_file_pointers HashSet in both CipherBoxFS constructors
- Both cargo check -p cipherbox-fuse and cargo check -p cipherbox-desktop pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(33-01): complete async FilePointer resolution infrastructure plan

- Add 33-01-SUMMARY.md with execution results
- Update STATE.md with plan progress and decisions
- Update ROADMAP.md with plan 01 completion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(33-02): wire FilePointer async resolution into Windows WinFsp callbacks

- Add status_device_not_ready() NTSTATUS helper (0xC00000A3) to operations.rs
- Add drain_file_pointer_completions() calls to handle_open, handle_read, handle_read_directory
- Add read-while-resolving poll loop in handle_read with 5s timeout for in-flight FilePointer resolution
- Return STATUS_DEVICE_NOT_READY on poll timeout (Explorer retries automatically)
- Use mutable cid/key/iv/mode locals for in-place update after resolution completes

* docs(33-02): complete Windows WinFsp FilePointer drain + poll plan

- Add 33-02-SUMMARY.md with execution results
- Update STATE.md with metrics and progress
- Update ROADMAP.md with phase 33 completion

* docs(phase-33): complete phase execution

* docs(phase-33): evolve PROJECT.md after phase completion

* test(phase-33): add Windows runtime verification results

* refactor(fuse): simplify poll loop pattern match and use ipns_name in drain logs

Collapse redundant double InodeKind::File pattern match in FilePointer
resolution poll loop into a single match with all fields extracted at once.
Use the previously-unused ipns_name field in drain_file_pointer_completions
log messages for better debuggability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(33): ship phase 33 — PR #389

* fix: address PR review comments

- Lower FilePointer resolution success log from info to debug to avoid
  log spam in folders with many files (Copilot review thread #2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: early exit poll loop when FilePointer resolution fails

If async resolution fails quickly, drain_filepointer_completions removes
the ino from resolving_file_pointers but the poll loop previously kept
sleeping until the 5s timeout. Now breaks immediately when the ino is no
longer in-flight, falling through to the existing empty-cid handler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FSM1 added a commit that referenced this pull request May 25, 2026
- Scope Tauri events per attempt via port-specific event names (review #1)
- Validate POST requests with server-generated nonce (review #2)
- Use absolute deadline instead of per-connection timeout (review #3)
- Use actual popup counter value for cleanup (review #4)
- Fail fast when all preferred ports are taken (review #6)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 172d4ce72f72
FSM1 added a commit that referenced this pull request May 25, 2026
…#459)

* fix(desktop): use localhost callback server for Google OAuth in Tauri

Google OAuth rejects custom URI schemes like `tauri://localhost` which
is what `window.location.origin` resolves to in production Tauri builds.
This caused Error 400: invalid_request when attempting Google login.

Replace the localStorage-polling approach with a temporary localhost HTTP
server (Rust-side) that receives the OAuth redirect, extracts the id_token
from the URL fragment via an injected callback page, and emits it back to
the main webview via a Tauri event.

- Add `start_oauth_server` Tauri command with preferred port selection
  (14200-14202) and random fallback
- Rewrite `getGoogleCredential()` to use the callback server + Tauri
  event listener instead of localStorage polling
- Add `core:event:default` permission for Tauri event API access
- Content-Length-aware HTTP body reading for reliable POST parsing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 51734dbce9b4

* chore(release): set release targets for PR #459

* fix(desktop): address PR review comments on OAuth callback server

- Scope Tauri events per attempt via port-specific event names (review #1)
- Validate POST requests with server-generated nonce (review #2)
- Use absolute deadline instead of per-connection timeout (review #3)
- Use actual popup counter value for cleanup (review #4)
- Fail fast when all preferred ports are taken (review #6)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 172d4ce72f72

* docs: implement temporary localhost HTTP server for Google OAuth callback in Tauri

Entire-Checkpoint: ac0f39da9ff3

* fix(desktop): address second round of PR review comments

- Update JSDoc to reflect fixed-port strategy (no dynamic/loopback)
- Validate OAuth state before handling errors (prevents DoS via crafted error)
- Loop-read TCP until headers are complete before parsing Content-Length
- Update debug doc to match current implementation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 72dff9156bda

* fix(desktop): address CodeRabbit review on OAuth callback server

- Move cleanup() after state validation to prevent promise hang on mismatch
- Use OsRng (CSPRNG) instead of thread_rng for nonce generation
- Remove Access-Control-Allow-Origin: * from GET responses (prevents
  local pages from fetching callback HTML to steal the nonce)
- Close OAuth popup windows on server timeout
- Gate callback HTML serving to /callback path only (404 for others)
- Remove stale review-cycle comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 60247aaac75c

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
FSM1 added a commit that referenced this pull request Jun 16, 2026
…ame at rest (#500)

* docs: add phase 48 — SDK self-bootstrap regression fix and shared-folder consolidation

Bundles three pending todos onto one branch plus the PR #498 web-e2e regression:
- REQ-1 (P0): fix self-bootstrap loadFolder clobbering fresher folderTree state with a stale IPNS snapshot (regressed main web-e2e, run 27587113911)
- REQ-2 (#9): remove redundant web ensureFolderRegistered seeding + useFolderNavigation unwrap, gated on REQ-1
- REQ-3 (#8): route shared-folder writes through the SDK client
- REQ-4 (#5 / Phase-14 M1): encrypt share itemName at rest via ECIES
Defers CRDT-IPNS-inbox research (#2). REQ-1 acceptance: pre-merge web-e2e via 'gh workflow run web-e2e.yml --ref <branch>'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: f826ffa63025

* docs(48): research phase domain

Entire-Checkpoint: ed421ad6df8c

* docs(48): add context and validation strategy

Locks REQ-3 (sibling sharedFolderTree by shareId) and the two REQ-4 policy decisions: A2=lazy client backfill of legacy plaintext itemName, A3=include the invite flow. Adds Nyquist VALIDATION.md (REQ-1 acceptance = pre-merge web-e2e dispatch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: a1ae21343869

* docs(48): create phase plan (6 plans, 3 waves)

Entire-Checkpoint: 9b36cdf380e5

* docs(48): add pattern map

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 66a841737a7a

* test(48-01): add RED reconcile guard test for loadFolder

- Three cases: keep-fresher (RED), absent-loads (green), older-overwritten (green)
- Test A confirms the bug: existing sequenceNumber=5n clobbered by stale snapshot=3n
- Uses bigint literals and vitest *.test.ts convention per memory

Entire-Checkpoint: ebe50165e85d

* feat(48-01): add sequence guard to loadFolder to prevent stale clobber

- Read existing folderTree entry after IPNS resolve
- If existing.sequenceNumber >= result.sequenceNumber, keep existing and skip set()
- Emits folder:loaded with existing children/sequenceNumber on the guard path
- Absent folders still resolve and set normally (no #498 regression)
- Fixes P0: bin-restore and version-restore regressed in main web-e2e run 27587113911

Entire-Checkpoint: 035cb28ff942

* docs(48-01): SUMMARY and STATE for plan 01 checkpoint

Tasks 1-2 complete (RED+GREEN); awaiting Task 3 PRE-MERGE web-e2e gate.

Entire-Checkpoint: c531516e1121

* fix(48-01): re-resolve bin before auto-repair to prevent empty clobber

loadBin treated a null resolveIpnsRecord as 'bin empty' and published an empty record with no expectedSequenceNumber, so a transient cold-cache null after page.reload (with two concurrent loadBin calls) destroyed the real bin record server-side — the deleted item vanished from the bin after reload (bin-restore-after-reload.spec.ts step 6). Re-resolve before the destructive empty publish, and re-resolve after publishing to adopt any real higher-sequence record. SDK-only; 183 SDK tests pass.

Unsigned: 1Password SSH signer wedged mid-run (AFK); needs re-sign or admin-merge before landing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 23d236999200

* fix(48-01): guard folder-nav redirect so a stale subfolder resolve cannot bounce an off-route user

A subfolder resolve that fails after the user has navigated to another
route force-navigated to /files via the catch block, guarded only by
latestNavTarget. A react-router nav to /bin does not update that ref, so
the redirect bounced the user off /bin back to /files. Add a route-still-here
guard using the live pathname before the redirect fires.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 5ef54e2e74a7

* docs(48): REQ-1 gate blocked handoff - bin-restore-after-reload still red

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 8c2131ef9dab

* docs(48): correct REQ-1 root cause from trace - bin IPNS 404 auto-repair clobber, not nav

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 86e784543056

* fix(48-01): stop loadBin from clobbering the bin with an empty auto-repair publish

loadBin previously published an empty bin record on a double-null resolve via saveBinMetadata with no expectedSequenceNumber; the API then unconditionally incremented the sequence and overwrote latestCid with the empty-bin CID, wiping the real bin after delete+reload (two concurrent loadBin calls could both clobber). The API /ipns/resolve already DB-cache-falls-back, so a client-side null means the record is genuinely absent at that instant - publishing empty on read is never safe.

Changes (all client-side, no API change):
- loadBin: remove the destructive empty-bin auto-repair publish. On no record, return an in-memory empty BinState (entries=[], sequenceNumber=0) WITHOUT publishing. The first addToBin lazily creates the real record at seq 0+1=1, matching the API create-path. Added a bounded retry (6x ~500ms) to convert a transient null into a successful load.
- deleteToBin: self-heal by lazily loading the bin when binState is null. Bin init is fire-and-forget on login, so a delete soon after login/reload previously threw BinNotLoadedError and the web fell back to a HARD delete - the item vanished instead of moving to the bin. This was the actual cause of recycle-bin failures.
- client.loadBin: anti-clobber guard - the in-memory empty fallback never overwrites or broadcasts over an already-loaded non-empty bin.

Tests: invert the old bin.test that asserted the destructive empty publish to assert loadBin does NOT publish on null; keep the does-not-clobber test; add deleteToBin self-heal + loadBin anti-clobber tests. Full SDK suite green; recycle-bin.spec E2E green 8/8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 5869de8fb5a5

* fix(48-01): keep isLoading true during session restore to stop protected-route login bounce

After a reload, CoreKit re-initializes (coreKitInitialized flips true) a beat before restoreSession sets isLoggingIn/isAuthenticated, leaving a transient where !isLoading && !isAuthenticated are both true. A protected route mounting in that gap (opening /bin right after a reload) hit BinPage's auth guard and redirected /bin -> / -> /files, so the bin never rendered (bin-restore-after-reload.spec.ts step 6). Including (coreKitLoggedIn && !isAuthenticated) in isLoading keeps it true until restore resolves; a genuinely logged-out user has coreKitLoggedIn false so the login redirect still fires. Verified locally: the spec passes end-to-end under NODE_ENV=test (2x).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 8892055ce642

* refactor: remove web folder-seeding now that SDK self-bootstraps

Delete ensureFolderRegistered and all 14 call sites across
useDropUpload, useFileOperations, useFileVersions, and
useFolderMutations. The SDK requireFolder chokepoint (PR #498)
now self-bootstraps folders from the root IPNS key on first
mutation, making these web-side pre-seed calls redundant.

Also removes the unused FolderNode import from sdk-provider.ts.
useFolderNavigation.ts requires no change: the key-unwrap there
serves the display/metadata-load path, not SDK seeding.

REQ-2 (#9). Typecheck and lint green.

* docs(48-02): complete REQ-2 web folder-seeding removal plan

Entire-Checkpoint: 3cc6e6139aaf

* feat: add shared-folder state contracts to SDK

- Add SharedFolderState type carrying share-context fields
- Add SharedFolderTree keyed by shareId with key-zeroing on delete/clear
- Add sharedFolder:updated event member to SdkEvent union

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

* test: add shared-folder tree isolation and client shared-write tests

- SharedFolderTree per-share isolation + key-zeroing (GREEN)
- client.uploadToSharedFolder read/delegate/write-back/emit contract (RED)

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

* feat: SDK client owns shared-folder state and publish

- Add sharedFolderTree field + loadSharedFolder/hasSharedFolder/getSharedFolderState/unloadSharedFolder
- Add five shared write methods delegating to share/shared-write.ts via publishWithCas
- Each method reads state, builds context, writes back, emits sharedFolder:updated
- Export SharedFolderState and SharedFolderTree from index

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

* docs: complete 48-03 shared-folder SDK ownership plan

Entire-Checkpoint: dc7607abe28a

* feat: encrypt share itemName at rest via additive item_name_encrypted column

- Add additive nullable item_name_encrypted bytea to shares and share_invites
  via migration EncryptShareItemName1749200000000 (no data UPDATE; server is
  zero-knowledge and cannot re-encrypt legacy plaintext rows)
- Add itemNameEncrypted entity columns plus hex-validated optional DTO fields on
  create-share, create-invite, claim-invite and the response DTOs
- Persist client-supplied ECIES ciphertext in createShare, createInvite and the
  invite-claim path; server never encrypts (REQ-4 decision A3)
- Extend ECIES round-trip test for UTF-8 itemName and add ciphertext-persist plus
  no-server-encrypt service assertions
- Regenerate @cipherbox/api-client to expose itemNameEncrypted

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

* docs: complete 48-05 REQ-4 API itemName encryption plan

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: c873f80a93e3

* feat(web): route shared-folder writes through SDK with event-fed projection

- Add shared-folder-projection helpers seedSharedFolder + subscribeSharedFolderProjection
- Seed SDK sharedFolderTree on share/subfolder entry and up/breadcrumb depth restore
- Subscribe useSharedNavigation refs to sharedFolder:updated as projection-only state
- Route all five useSharedWriteOps handlers through getSdkClient shared methods, no write-back
- Drop withConflictRetry and buildSharedWriteContext ceremony; SDK owns CAS retry
- Add projection unit tests proving per-share isolation and reads-nothing-back

Implements REQ-3 web side (phase 48)

* docs(48-04): complete shared-folder write projection plan

Entire-Checkpoint: fcf515ad5df0

* test(48-06): add failing test for share itemName decrypt and backfill helpers

* docs(48-07): plan SDK-owned shared-folder refresh and poller consolidation

Entire-Checkpoint: 9f0cd23de3e3

* feat(48-06): ECIES-wrap share itemName on create and decrypt for display

- Wrap itemName with recipient pubkey in ShareDialog before createShare; send ciphertext-only (empty plaintext)
- Wrap itemName with ephemeral pubkey on invite-create; re-wrap for recipient on claim (decision A3)
- Decrypt itemNameEncrypted into the plaintext store projection on received-share load; display sites unchanged
- Add decryptItemName + shouldBackfill helpers and lazy-backfill pass for legacy rows (decision A2)

* docs(48-06): complete REQ-4 web itemName encryption plan

Entire-Checkpoint: 102cf857992f

* feat(48-07): add SDK client.refreshSharedFolder for shared-folder re-resolve

- refreshSharedFolder(shareId) re-resolves IPNS via sdkCore.loadFolderMetadata
- applies the #489 sequence-guard: stale/equal resolve re-emits existing state
- adopts a newer sequence into sharedFolderTree and emits sharedFolder:updated
- throws Shared folder not loaded on an unloaded shareId
- TDD-covered: newer adopt, stale no-clobber, null no-op, unloaded throw

* refactor(48-07): route shared poller through SDK refreshSharedFolder

- 30s poller calls getSdkClient().refreshSharedFolder(currentShareId)
- remove inline refreshFolderContents IPNS resolve + IPFS fetch + decrypt
- drop now-unused resolveIpnsRecord/fetchFromIpfs/decryptFolderMetadata imports
- projection subscription is now the sole ref writer on write AND poll paths
- revocation check unchanged

* test(48-07): cover poll-through-SDK refreshSharedFolder projection path

- add refreshSharedFolder to the fake SharedFolderClient recording the call
- assert the poller calls refreshSharedFolder with the active shareId
- assert refs update ONLY via the sharedFolder:updated subscription, not the call
- assert a mismatched-shareId refresh event is ignored per-share isolation

* docs(48-07): summarize SDK-owned shared-folder refresh and poller consolidation

- record Tasks 1-4 commits, verify results, and dist-rebuild confirmation
- Task 5 shared-folder sync UAT deferred to end-of-phase web-e2e

* fix(web): seed SDK shared-folder state when opening a write-shared file

The recipient save path routes through client.updateSharedFile (REQ-3, 48-04),
which calls requireSharedFolder and throws 'Shared folder not loaded' when the
share is absent from the SDK sharedFolderTree. The folder-open branch seeds the
tree, but the standalone file-share branch did not — so a recipient's Ctrl+S on
a write-shared file threw, the editor never closed, and writable-shares.spec.ts
'10.3 Bob opens the write-shared file and can edit it' timed out at waitFor
hidden. Seed the file branch (children: [], sequenceNumber: 0n; updateSharedFile
is a file-only publish that reads folderKey + owner/recipient pubkeys only and
ignores children/sequence). Only write shares (IPNS key present) seed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: c89f6e96802a

* fix(api): allow empty itemName on create-invite to match create-share

REQ-4 (48-06) moved the share display name into the ECIES itemNameEncrypted
ciphertext and changed the web client to POST itemName: '' on create. The
sibling CreateShareDto was relaxed to accept this, but CreateInviteDto kept
@minlength(1) on itemName, so the global ValidationPipe (whitelist +
forbidNonWhitelisted) rejected invite-link creation with HTTP 400 — failing
invite-link-workflow.spec.ts '3.1 Alice creates an invite link for a file' at
waitForSuccess. Drop @minlength(1) to mirror CreateShareDto. No OpenAPI/client
change: the Nest swagger plugin does not emit minLength, and the generated
client source is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: e0689f5cec4f

* refactor(48): drop dead shared-write folderKey prop, redundant 0x-strip, stale doc

Simplify-pass cleanups (no behavior change):
- useSharedWriteOps: remove the unused folderKey param (the SDK owns the folder
  key via seeded sharedFolderTree state) and its call-site wiring.
- shared-folder-projection.parsePublicKey: drop the redundant 0x-strip;
  hexToBytes already strips an optional 0x prefix.
- client.ts getFolderIpnsPrivateKey: fix a doc comment that referenced
  ensureFolderRegistered, which this branch deleted.

Deferred (noted, out of scope for this PR): SharedFolderTree is keyed by
shareId so the web re-seeds per nav depth (deeper fix = a shared
ensureFolderLoaded analog); serial DFS in ensureFolderLoaded; pre-existing
share-key cache bypass in updateSharedFileHandler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 5cdb4df5efb2

* test(48-05): cover invite/claim itemNameEncrypted ciphertext persistence

Add 3 behavioral tests to share-invite.service.spec.ts:
- createInvite: persists hex ciphertext as Buffer passthrough (no server encryption)
- createInvite: persists null for legacy clients that omit itemNameEncrypted
- claimInvite: re-wrapped itemNameEncrypted from DTO is stored as Buffer on the Share

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entire-Checkpoint: a49ce185baba

* docs(48): mark phase nyquist-compliant after filling invite ciphertext test gap

Update 48-VALIDATION.md: real per-task verification map (REQ-1..4 all green via
unit + web-e2e), Wave 0 checked, sign-off approved, audit trail (1 gap found,
1 resolved). The gap — invite/claim itemNameEncrypted persistence — was closed
by share-invite.service.spec.ts (commit e8a3a2fe0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 42e4f8b5b5a4

* fix(api): widen itemNameEncrypted MaxLength to 2500 for worst-case names

The 1024-char cap was copied from the fixed-size encryptedKey field, but
itemNameEncrypted wraps a variable-length display name: a 255-char multibyte
name (CJK/emoji) is up to ~765 UTF-8 bytes, whose ECIES ciphertext is ~1724 hex
chars — exceeding 1024 and triggering a 400 (same class as the create-invite
MinLength regression). Raise the cap to 2500 across create-share, create-invite,
and claim-invite. No OpenAPI/client change (the swagger plugin does not emit
maxLength). Flagged by CodeRabbit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: aa221dedc616

* fix(web): harden shared itemName decrypt and unload SDK keys on unmount

Two review findings on the REQ-3/REQ-4 shared paths:
- fetchReceivedShares decrypted itemName inside Promise.all, so one corrupt /
  wrong-key / truncated ciphertext row rejected the WHOLE received-shares page.
  Guard per row, degrading just that row to its plaintext fallback (security
  review, MEDIUM availability).
- The sharedFolder:updated projection effect only unsubscribed on unmount; it
  never unloaded the SDK's cloned shared-folder state, so folderKey +
  ipnsPrivateKey lingered in SDK memory after leaving a share. Call
  unloadSharedFolder(activeShareId) in cleanup to zero them (CodeRabbit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 19d891c11b3e

* docs(48): add phase 48 security review report

Two-agent security review of the REQ-4 itemName at-rest encryption and REQ-3
shared-folder key handling. Overall risk LOW: zero-knowledge invariant holds
(server stores ciphertext verbatim, never encrypts/decrypts), crypto correct,
key-zeroing + cross-share isolation hold. One MEDIUM (received-shares decrypt
guard) fixed; LOW items deferred with rationale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 1a497a44e1ab

* chore: sync Cargo.lock to crate versions 0.6.0 for fuse and sdk

The cipherbox-fuse and cipherbox-sdk crate Cargo.toml files are at 0.6.0 but
the committed lockfile still pinned 0.5.3 / 0.5.0. Regenerate the lock to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 17ebf9d4ebfa

* chore(release): set release targets for PR #500

* test(48): cover itemNameEncrypted present/null branches in invite controllers

The new itemNameEncrypted hex-or-null ternaries in share-invites.controller
and invites.controller added per-file branches whose present-vs-null sides
were not both exercised, dropping branch coverage below threshold (63.88% <
65% and 73.33% < 74%). Add explicit tests for the present (Buffer to hex
string) and absent (null) branches in createInvite, listInvites, and
getInviteData, and set itemNameEncrypted on the base mocks. Both files now
clear their branch thresholds (69.44% and 76.66%).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: a5866cb6aa14

* test(48): update sdk-e2e bin suite to the self-healing bin contract

Commit 73c9f03 intentionally changed the bin contract: loadBin returns an
in-memory empty state at sequenceNumber 0 WITHOUT publishing (publishing an
empty record on a null resolve is destructive), and the client bin methods
self-heal or lazy-load instead of throwing BinNotLoadedError. The sdk-e2e
suite still asserted the old contract.

bin-operations.test.ts:
- fresh-account loadBin now expects sequenceNumber 0 (was 1).
- "BinNotLoadedError when bin not loaded" becomes a self-heal test: deleteToBin
  lazily loads the bin and self-bootstraps the root, then fails only on the
  missing child with 'Item not found'.

error-cases.test.ts (bin ops without an explicit loadBin):
- deleteToBin self-heals and fails with 'Item not found' (not BinNotLoadedError).
- restoreFromBin / permanentDelete reach the entry lookup (binState lazily
  loaded empty by the prior deleteToBin) and fail with 'Bin entry not found'.
- emptyBin resolves on the already-loaded empty bin.

Verified against the source-of-truth unit tests (packages/sdk/src/__tests__/
bin.test.ts, green) and the client self-heal logic in client.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 7b06e5dedd10

* fix(sdk): guard shared-folder adopt and emit against unload race, zero stale keys

Two CodeRabbit findings on the SDK shared-folder state:
- adoptSharedFolderResult and the updateSharedFile / refreshSharedFolder emit
  sites used a pre-await state snapshot, so a write/refresh completing AFTER
  unloadSharedFolder(shareId) (now triggered on unmount) could resurrect the
  unloaded entry and emit sharedFolder:updated for it. Re-read live state right
  before write-back/emit and no-op if absent; callers pass shareId.
- SharedFolderTree.set now zeroes the previous entry's folderKey/ipnsPrivateKey
  before overwriting so stale key material doesn't linger across re-seeds —
  guarded on reference identity so the adopt path (which spreads the live entry's
  same buffers) never zeroes the key it is about to clone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 45d6a1f9e98b

* fix(web): disable no-op itemName backfill and wipe plaintext name buffers

Two CodeRabbit findings on the REQ-4 share itemName paths:
- backfillSentShareItemNames re-wrapped every legacy row via ECIES on each
  owner share-list load but cannot persist (no PATCH endpoint, T-48-18) — wasted
  CPU with no durable effect. Disable the invocation until the update endpoint
  exists; the function is kept ready for one-line wiring.
- ShareDialog and invite.service encoded the plaintext display name into a
  Uint8Array and wrapped it without clearing the temporary bytes. Wrap in
  try/finally and fill(0) the encoded buffer, per the clear-sensitive-data rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: d9b1db6d1ed0

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
FSM1 added a commit that referenced this pull request Jun 29, 2026
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
FSM1 added a commit that referenced this pull request Jun 29, 2026
…crash-safe resume (#582)

* docs(64): capture phase context

Entire-Checkpoint: e93088839ad6

* docs(state): record phase 64 context session

Entire-Checkpoint: 500072385b16

* docs(64): research — rotation soundness seam fills and D-02/D-06/D-07 hardening

Entire-Checkpoint: e21e9fa3c13d

* docs(64): add validation strategy and pattern map

Entire-Checkpoint: 7fdbefa471e2

* docs(64): create rotation-soundness phase plans

Entire-Checkpoint: a7f5d10d27cf

* docs(64): pin no-double-bump per-node convergence guard in plan 64-07

Entire-Checkpoint: 1e8ed6c455d4

* docs(64): resolve research open questions and fill validation map

Entire-Checkpoint: cc92facc65bc

* docs(64): record planning completion and roadmap wave annotations

Entire-Checkpoint: d727bfa43d77

* test(64-01): add failing tests for nodeId/nodeGeneration required fields

- 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

* feat(64-01): make nodeId/nodeGeneration required on updateFolderMetadataAndPublish

- 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

* feat(64-01): thread nodeId/nodeGeneration through all six client.ts CRUD 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

* test(64-01): add AEAD round-trip spec for moveItem dest-parent re-seal (FLAG-63-U2)

Entire-Checkpoint: 1f1ced14e87c

* feat(64-01): re-seal moved child readKey under dest parent readKey in 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

* docs(64-01): complete nodeId/nodeGeneration required + moveItem re-seal plan

Entire-Checkpoint: 9721d0ffb633

* test(64-02): add failing three-way merge tests for SealedChildRef

Entire-Checkpoint: 6cf80e49e9b2

* feat(64-02): implement mergeChildren three-way merge for SealedChildRef

- 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

* docs(64-02): complete mergeChildren three-way merge plan

Entire-Checkpoint: 6d39de02c928

* test(64-03): add failing content-key rotation tests

- 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

* feat(64-03): mint fresh fileKey on file rotation

- 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

* docs(64-03): complete mintFileKeyOnRotate seam fill plan

Entire-Checkpoint: 95e1a7552608

* fix(64-01): typecheck-clean the D-06 test files

Entire-Checkpoint: 9003f3740525

* test(64-04): RED tests for D-01 fail-closed publish and nodeKeySource

- 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

* feat(64-04): D-01 fail-closed publish guard and nodeKeySource BFS threading

- 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

* test(64-04): RED tests for D-02 parent re-seal and D-09 batched parent 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

* feat(64-04): D-02 parent-link re-seal and D-09 batched parent publish

- 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

* docs(64-04): complete plan 04 summary

Entire-Checkpoint: d29b43cfd1a3

* test(64-05): add failing inner-grant re-mint tests

- 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

* feat(64-05): re-mint readDescriptorRef for rooted grants

- 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

* docs(64-05): complete plan 05 summary

Entire-Checkpoint: cd15a6063aa9

* test(64-06): add failing CAS-409 concurrent-add tests

Entire-Checkpoint: 30192904c769

* feat(64-06): merge concurrent child adds on rotation CAS-409

- Implement mergeConcurrentChildren: unseal base+remote under old readKey,
  three-way merge via mergeChildren, re-seal merged result under readKeyPrime
- Wire async merge callback in rotateOne publishWithCas for ROT-05/HIGH-4
- Update cas.ts merge type to accept async callback (Promise union); wrap
  call site with await Promise.resolve() for backward-compat sync callers
- Add mergeChildren import from folder/merge in engine.ts
- Remove old Phase-63 throw stub from rotateOne merge callback
- Remove mergeConcurrentChildren direct-call seam test (now tested indirectly
  through rotateOne 64-06 integration tests)
- Fix vi.clearAllMocks() -> vi.resetAllMocks() in 64-06 and D-02 beforeEach
  blocks to drain mockResolvedValueOnce queue between tests (Rule 1: prevents
  leftover Once-values from failing RED tests contaminating D-02/D-09 tests)

Entire-Checkpoint: 1917175ac7cb

* docs(64-06): complete plan 06 summary and state

Entire-Checkpoint: 7e9f7f2c3a63

* test(64-07): add failing tests for verifySubtreeClean resume and convergence 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

* feat(64-07): fill verifySubtreeClean and rewrite resume guard with convergence 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

* test(64-07): add failing tests for D-07 ordering terminal persist and 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

* feat(64-07): fix D-07 ordering terminal persist and queue-key zeroization

- 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

* docs(64-07): complete verifySubtreeClean and D-07 ordering plan

Entire-Checkpoint: 086888ac926c

* test(64-08): add rotation crash-safety E2E suite (TEST-01 phase gate)

- 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

* docs(64-08): complete rotation crash-safety suite plan

Entire-Checkpoint: e69fa8406d74

* docs(64): capture deferred rotation-soundness todos (HIGH-4 merge, fresh-record resume)

Entire-Checkpoint: 7ed06cb1ff18

* docs(64): add verification and security audit

Entire-Checkpoint: 36441ac5f1c9

* docs(64): fix stale Phase-63 seam comments in rotation engine

Entire-Checkpoint: 4b277c878d83

* fix(64): apply low-risk CodeRabbit findings and capture deferred ones

Entire-Checkpoint: 1226147f936f

* fix(64): resolve moveItem re-seal by ipnsName not childId

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

* fix(64): harden rotation D-01/D-09 guards from PR review

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
FSM1 added a commit that referenced this pull request Jun 30, 2026
#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>
FSM1 added a commit that referenced this pull request Jun 30, 2026
#583)

* docs(65): capture phase context

Entire-Checkpoint: aed8bf6fa830

* docs(state): record phase 65 context session

Entire-Checkpoint: f49b3b2fe198

* docs(phase-65): research write-chain, bin re-link, and invite claim

Entire-Checkpoint: ef5c6ef6e756

* docs(65): add validation strategy

Entire-Checkpoint: fe7e071404d5

* docs(65): map phase patterns

Entire-Checkpoint: 3c9673822b65

* docs(65): create phase 65 write-chain plan (7 plans, 4 waves)

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

* docs(65): address plan-checker warnings — surface e2e typecheck and mark research OQs resolved

Entire-Checkpoint: e025a94f56f3

* docs(65): record planning complete and annotate roadmap waves

Entire-Checkpoint: 7a6909081de9

* docs(phase-65): begin phase execution tracking

Entire-Checkpoint: 2a373269bfea

* test(65-01): add failing role-0x04 write-chain seal KAT and round-trip

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

* feat(65-01): implement sealChildWriteKey and unsealChildWriteKey with 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)

* test(65-03): add failing claimInvite service-flow test

- 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

* docs(65-01): complete plan summary for role-0x04 write-chain seal primitives

* feat(65-03): implement claimInvite service flow and barrel export

- 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

* docs(65-03): complete invite-claim service flow plan summary

* test(65-02): add failing bin re-link spec for addToBin and restoreFromBin

- 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

* feat(65-02): implement addToBin and restoreFromBin as pure key re-link

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

* docs(65-02): complete bin re-link plan

* docs(phase-65): update tracking after wave 1

Entire-Checkpoint: 02004cb54e8b

* test(65-05): add failing write-body-reseal tests for rotateOne write-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

* feat(65-05): thread real writeKey through rotateOne, remove PLACEHOLDER_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

* docs(65-05): complete write-body-reseal plan summary

* feat(65-04): implement shared-write on write-body model with WRITE-01 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

* docs(65-04): fix stale Phase-65 convention comments in shared-write

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.

* docs(65-04): complete plan 04 shared-write write-body model

* fix(65-04): reconcile client SharedWriteContext consumers with write-body model

* docs(phase-65): update tracking after wave 2

Entire-Checkpoint: 0c43e1d2e603

* test(65-06): add failing write-revocation driver contract test

- Specifies rotateWriteFromNode behavior: per-node new Ed25519 keypair + k51 name + writeKey
- Asserts child-first cascade, first-publish at 1n, tombstone-intent teeUnenrollFn calls
- Asserts co-writer re-wrap via wrapKey and revoked grant drop via deleteWriteGrantFn
- Asserts read-plane invariance: no generation bump, no new readKey minted
- Asserts parent SealedChildRef.ipnsName re-pointed to new child name
- Tests RED because rotateWriteFromNode is not yet exported from rotation engine

* feat(65-06): implement rotateWriteFromNode with child-first write-revocation cascade

- Exports WriteRevocationCallbacks type and rotateWriteFromNode function from rotation engine
- Child-first bottom-up traversal: leaves get new Ed25519 keypair + k51 name + writeKey first
- First-publishes each new k51 name at sequenceNumber 1n via createAndPublishIpnsRecord
- Fires teeUnenrollFn for each old name after the new name is published
- Re-seals write-body with new ipnsPrivateKey and re-pointed writeChildren under new parent writeKey
- Updates parent SealedChildRef.ipnsName to new child name while leaving readKeySealed unchanged
- Handles co-writer grants: wrapKey for survivors, deleteWriteGrantFn for revoked recipients
- Read plane invariant: no readKey minted, no generation bump on any node
- Zeros minted writeKey and Ed25519 seeds on failure paths only (D-09 / Pitfall 4)
- Adds createAndPublishIpnsRecord and sealChildWriteKey/unsealChildWriteKey to engine imports
- All 70 rotation suite tests pass including 8 new write-revocation tests

* docs(65-06): complete write-revocation plan summary

* docs(phase-65): update tracking after wave 3

Entire-Checkpoint: 1669fd50ff58

* feat(65-07): export rotateWriteFromNode and WriteRevocationCallbacks from sdk-core

- Adds rotateWriteFromNode to the sdk-core top-level barrel so the
  sdk-e2e suite can import it via the package name rather than an
  internal subpath (Rule 3 fix: missing export blocked the D-04 gate test)
- Adds WriteRevocationCallbacks type export alongside the function

* test(65-07): add D-04 write-chain rotation E2E suite

- Creates tests/sdk-e2e/src/suites/write-chain-rotation.test.ts as the
  D-04 phase gate for WRITE-02/03/04 against the live docker API stack
- publishWriteCapableNode helper seals folder nodes with real write-bodies
  and first-publishes each new k51 name at sequenceNumber 1n
- Test 1: builds a 2-level subtree (root + child), asserts pre-rotation
  baseline: write-body unseals under writeKey; parent write link unseals
  to child writeKey; generation 0 for both nodes
- Test 2: drives rotateWriteFromNode with vi.fn() callbacks, asserts WRITE-02
  (new k51 names + parent re-point cascade), WRITE-04 (teeUnenrollFn per
  old name), WRITE-03 (survivor re-wrap unwraps with bob private key;
  revoked grant dropped), and read-plane invariance (generation unchanged)
- Derives new IPNS names from captured getRandomValues spy in child-first
  order (child-ed25519, child-writeKey, root-ed25519, root-writeKey)

* docs(65-07): complete write-chain rotation plan summary

* docs(phase-65): update tracking after wave 4

Entire-Checkpoint: c40a8c198c21

* docs(phase-65): complete phase execution

Entire-Checkpoint: 77baeecc7244

* docs(phase-65): close rotateone-placeholder-writekey todo resolved by plan 65-05

Entire-Checkpoint: f69e0ef17961

* docs(phase-66): re-tag bin-pin-leak todo to phase 66 where apps/api/shares is in scope

Entire-Checkpoint: f7bf47fb6251

* docs(65): security audit — all 29 threats closed

Static analysis of Plans 01-07 threat models: 29 mitigations
verified in implementation (seal.ts role-0x04, engine.ts writeKey
threading + PLACEHOLDER removal, shared-write.ts write-body model,
bin/index.ts pure re-link, grant.ts claimInvite). Two accepted risks
(T-65-07 Q3 sub-share, T-65-09 invite link) documented. threats_open=0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(65): nyquist validation audit — fill coverage map, mark compliant

Retroactive audit of phase 65 validation gaps. Maps all 15 tasks
across 7 plans to their concrete test files and commands. Marks
nyquist_compliant: true; records the one explicitly-deferred item
(WRITE-04 live publish-gate) with Phase 66 evidence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 4f25369a581a

* docs(phase-68): defer shared-write context publishedNode wiring from phase-65 coderabbit review

Entire-Checkpoint: 9406964c08bd

* fix(65): resolve CR cluster-B findings in shared-write

- F1 zeroization: fill(0) minted key buffers before null on success paths
  in createSharedSubfolder, uploadToSharedFolder, and updateSharedFile
- F2 childId alignment: add fileNodeId param to updateSharedFile so
  Node.id stays aligned with WriteChildRef.childId across content updates
- F3 publish order: publish dest before src in moveInSharedFolder so a
  partial failure leaves a duplicate rather than an orphaned item
- F4 sequence number: pass swCtx.sequenceNumber + 1n in
  resealAndPublishParent so parent updates use the correct next sequence
- F5 fail-closed: throw when neither childWriteKey nor a walkable src
  write link is available; zero derived key in finally (D-09)

Tests: update callers for new fileNodeId param; add fail-closed test for
moveInSharedFolder; 36 tests pass (was 35)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(65): fail-closed restoreFromBin and reorder addToBin for atomicity

- restoreFromBin: throw instead of defaulting nodeIpnsName/nodeRef to
  empty values — prevents sealChildReadKey binding to wrong AAD
- addToBin: persist bin entry before the destructive folder publish so
  a failed save cannot orphan the item with no restore key
- grant.ts claimInvite: reject empty/whitespace inviteToken, rootNodeId,
  rootIpnsName before any I/O or crypto work

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(65): make revocation-failure test self-contained

Add setupAddToBinMocks() to stub resolveIpnsRecord and fetchFromIpfs so
the test reaches the revocation step independently of prior test cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(65): defer tombstones post-subtree, tighten key lifecycle, fix childKey zeroing

Findings addressed: F5 CRITICAL, F6 major, F7 major, F4 major.

F5: move teeUnenrollFn calls from inside rotateWriteSubtree to after the
full subtree rotation in rotateWriteFromNode. Adds pendingTombstones[]
collection so a failed ancestor publish cannot leave the TEE with a
unenrolled child name the parent still references.

F6: remove unused newIpnsPrivateKey from WriteRotationResult; zero
newKeypair.privateKey immediately after createAndPublishIpnsRecord; zero
rootResult.newWriteKey at end of rotateWriteFromNode after grants loop.

F7: wrap both unsealChildReadKey and unsealChildWriteKey in a single
try/finally so childReadKey is zeroed even when unsealChildWriteKey
throws before childReadKey enters the inner try scope.

F4: change rotateWriteFromNode return type from Promise<void> to
Promise<{ newRootIpnsName: string }> so callers can persist the new
root IPNS pointer alongside the grant descriptor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(65): add missing core mocks, strengthen write-key assertions in rotation unit tests

Findings addressed: F3 major, F1 minor, F8 minor.

F3: add sealChildWriteKey and unsealChildWriteKey to the @cipherbox/core
mock in write-body-reseal.test.ts so the module does not silently export
undefined for these symbols if the write-rotation code path is hit.

F1: assert sealChildWriteKey call arguments in write-revocation Test 8 --
child write key identity, parent write key shape, and AAD binding args.

F8: strengthen write-body-reseal Test 7 to assert both NODE_WRITE_KEY and
CHILD_WRITE_KEY appear per-node in the BFS; remove the .catch that was
masking rotation errors in GREEN.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(65): zero captured Ed25519 seeds early and verify survivor write key opens write body

Findings addressed: F10 minor, F11 major.

F10: call clearCapturedKeys immediately after deriving new IPNS names so
Ed25519 seed material does not linger in the spy array until afterAll.

F11: after unwrapping the survivor write-descriptor with bob private key,
call unsealNode on the new root with the unwrapped key and assert
writeBody is present; proves the persisted descriptor is the actual new
root write key and not merely some 32-byte value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(65): defer write-chain e2e seed index-stability hardening from coderabbit review

Entire-Checkpoint: 0a6ec17e80ef

* fix(65): deleteFromSharedFolder write-body filter uses UUID not ipnsName

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>

* test(65): cover write-body WriteChildRef removal on delete

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>

* fix(65): harden write-rotation engine against security edge cases

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

* test(65): harden rotation test hygiene for zeroed buffer handling

#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(65): address coderabbit round-2 findings for bin and grant

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

* docs(phase-68): add SDK envelope-return enablers from coderabbit PR review

Entire-Checkpoint: 4710f9a40648

* fix(65): hex-encode bin nodeReadKey on the wire so restoreFromBin survives JSON round-trip

Entire-Checkpoint: 19c3208fe45e

* fix(65): fail closed on non-32-byte bin nodeReadKey and missing deleteFromSharedFolder childNodeId

Entire-Checkpoint: c7f2d3a74cdf

* fix(65): route shared-write content uploads through pinWithMode to honor BYO pinning

Entire-Checkpoint: e99a4ebfa963

* fix(65): fail closed on non-throwing IPNS publish rejection in publishNodeFn

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

* fix(65): use childRef.generation for bin AAD, not stale envelope generation

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant