Skip to content

Add missing Backend→Client CID return flows in IPFS metadata upload sequences#12

Merged
FSM1 merged 3 commits into
feat/spec-hardeningfrom
copilot/sub-pr-4-ef61048c-1e79-416f-b1f6-2a5aa856d3ba
Jan 18, 2026
Merged

Add missing Backend→Client CID return flows in IPFS metadata upload sequences#12
FSM1 merged 3 commits into
feat/spec-hardeningfrom
copilot/sub-pr-4-ef61048c-1e79-416f-b1f6-2a5aa856d3ba

Conversation

Copilot AI commented Jan 18, 2026

Copy link
Copy Markdown
Contributor

Sequence diagrams showed Backend→IPFS metadata uploads returning CIDs, but omitted the subsequent Backend→Client response. Clients need this CID to construct IPNS records pointing to the uploaded metadata.

Changes:

  • Added B->>C: Return {cid: metadataCid} after all B->>IPFS: Add metadata, return CID operations
  • Fixed 8 sequence diagrams across file upload, folder operations (create/rename/move), and updates

Before:

C->>B: POST /ipfs/add (encrypted metadata)
B->>IPFS: Add metadata, return CID
C->>C: Sign IPNS record (Ed25519)  # Client has no CID yet
Loading

After:

C->>B: POST /ipfs/add (encrypted metadata)
B->>IPFS: Add metadata, return CID
B->>C: Return {cid: metadataCid}
C->>C: Sign IPNS record (Ed25519)  # Client uses received CID
Loading

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

… after IPFS metadata upload

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>
Copilot AI changed the title [WIP] Update IPFS/IPNS relay endpoint details based on feedback Add missing Backend→Client CID return flows in IPFS metadata upload sequences Jan 18, 2026
Copilot AI requested a review from FSM1 January 18, 2026 01:11
@FSM1 FSM1 marked this pull request as ready for review January 18, 2026 01:13
@FSM1 FSM1 merged commit 863e762 into feat/spec-hardening Jan 18, 2026
@FSM1 FSM1 deleted the copilot/sub-pr-4-ef61048c-1e79-416f-b1f6-2a5aa856d3ba branch January 18, 2026 01:13
FSM1 added a commit that referenced this pull request Jan 18, 2026
* bump version to 1.8.1 and update documentation for IPFS/IPNS relay integration

* Fix date inconsistency in TECHNICAL_ARCHITECTURE.md (#7)

* Initial plan

* Fix date inconsistency in TECHNICAL_ARCHITECTURE.md

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Fix date inconsistency in DATA_FLOWS.md frontmatter (#8)

* Initial plan

* Fix date inconsistency in DATA_FLOWS.md frontmatter

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Add missing CID return arrows in IPFS/IPNS sequence diagrams (#6)

* Initial plan

* Add missing return arrows in IPFS/IPNS sequence diagrams

Added `B->>C: Return CID` after all `B->>IPFS: Add metadata, return CID` operations to show that the Backend returns the CID to the Client before the Client signs the IPNS record. The Client needs this CID to create the IPNS record that points to it.

Updated 8 sequence diagrams:
- File Upload Flow (IPNS publish)
- Create Folder (empty folder + update parent)
- Rename File/Folder
- Move File/Folder (destination + source)
- Delete File
- Update File Content

Bumped version to 1.8.2

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Add missing Backend→Client CID return flows in IPFS/IPNS sequence diagrams (#5)

* Initial plan

* Add return arrows from Backend to Client in IPFS/IPNS sequence diagrams

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Add missing Backend→Client CID return arrows in IPFS sequence diagrams (#10)

* Initial plan

* Add missing Backend->Client CID return arrows in sequence diagrams

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Add missing CID return step in IPNS publish sequence diagrams (#16)

* Initial plan

* Add missing return CID step in IPNS publish sequences

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Fix date inconsistency in API_SPECIFICATION.md (#14)

* Initial plan

* Fix date inconsistency in API_SPECIFICATION.md

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Fix missing YAML frontmatter delimiter in API_SPECIFICATION.md (#15)

* Initial plan

* Fix YAML frontmatter: add missing opening delimiter to API_SPECIFICATION.md

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Fix missing Backend→Client CID return in IPFS/IPNS sequence diagrams (#11)

* Initial plan

* Fix sequence diagrams: add missing Backend->Client CID return arrows

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Fix missing return arrow in IPNS publishing sequence diagram (#13)

* Initial plan

* Add missing return arrow in IPNS publishing sequence diagram

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Fix date inconsistency in CLIENT_SPECIFICATION.md (#9)

* Initial plan

* Fix date inconsistency in CLIENT_SPECIFICATION.md

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Add missing Backend→Client CID return flows in IPFS metadata upload sequences (#12)

* Initial plan

* Add missing return arrows showing CID returned from backend to client after IPFS metadata upload

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Fix documentation inconsistencies and clarify IPFS/IPNS relay architecture (#17)

* Initial plan

* Fix all PR review feedback items

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Improve documentation clarity per code review feedback

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Add BASE64 encoding steps in IPNS publish sequences and bump to v1.9.0 (#18)

* Initial plan

* Add BASE64 encoding steps in IPNS sequences and bump version to 1.9.0

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

* Fix inconsistent indentation in mermaid sequence diagrams (#19)

* Initial plan

* Fix mermaid diagram indentation to use consistent 4-space formatting

Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FSM1 <12774278+FSM1@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
FSM1 added a commit that referenced this pull request Jun 15, 2026
Phase 45 (PR #491) implemented all 7 scoped todos but the bookkeeping was never
updated (flagged in 45-VERIFICATION.md). Reconcile:

- Move the 6 fully-delivered todos (#11/#12/#15/#18/#19/#20) pending -> done and tick
  their ROADMAP scope checkboxes.
- Keep #14 (test coverage) in pending: only Tier 1 (durability/replay safety-net tests)
  landed; Tier 2 (read_ops/write_ops handler harness) is still open, so it should be
  picked up on the next run.
- Fold the standalone 2026-06-15 fuse-test-coverage todo into #14 (single source of
  truth): records the fuser ReplySender blocker (vendored fuser doesn't export
  ReplySender, so handler reply objects can't be built in unit tests) and that
  journal_helpers.rs builders are unit-testable now via a make_test_fs helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: a2ff42b91a4a
FSM1 added a commit that referenced this pull request Jun 15, 2026
* docs: add phase 45 - desktop FUSE write-durability cleanup

Entire-Checkpoint: b65081e82eb3

* docs(fuse): research phase 45 - desktop FUSE write-durability cleanup

Entire-Checkpoint: a8fbb9ebd3f7

* docs(45): add validation strategy

Entire-Checkpoint: b3374d7857d7

* docs(45): map code patterns for phase 45

Entire-Checkpoint: 82a8e79b6fbf

* docs(45): create phase plan for desktop FUSE write-durability cleanup

Six plans across five waves covering all 7 scope items (#11/#12/#14/#15/#18/#19/#20).
Tests-first safety net (Plan 01, Wave 1) lands before the no-behavior-change refactors.

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

* docs(45): mark research questions resolved and fill validation commands

Entire-Checkpoint: f7d2f6782991

* docs(45): annotate roadmap waves and record planning completion

Entire-Checkpoint: df1c6b9521ed

* test(45-01): add crash/partial/retry-exhaustion durability tests T-45-01/02/03

- crash_mid_write_entry_survives_reload: entry put+not-removed survives
  fresh WriteQueue on same dir (simulates process kill after fsync)
- partial_journal_write_is_skipped_not_panicked: truncated .json file
  is skipped with warn, good entry still returned (V5/T-43-03)
- retry_exhaustion_keeps_failed_entry_on_disk: 4 record_failure calls
  on max_retries=3 queue parks entry as Failed, entry stays on disk (D-09)
- Rule 1 auto-fix: make_temp_queue() now uses pid+counter instead of
  tid+counter to prevent stale inter-run temp dir collisions that caused
  park_on_max_retries to fail with left:2 right:1 under --test-threads=4

Entire-Checkpoint: c3eb9d1ad133

* test(45-01): add replay-skip-failed/folder-key-cache/merge tests T-45-06/07/08

- replay_for_vault_does_not_touch_failed_entries: puts Failed entry, runs
  replay against non-routable API, asserts entry count stays 1 (skip path)
- resolve_folder_key_cache_resolves_shared_parent_once: calls private
  resolve_folder_key twice with root shortcut (folder==root), asserts
  both return identical key bytes; marked for #15 cache extension
- merge_folder_children_unions_new_and_existing: local [existing+new]
  merged with remote [existing], result len==2 and local version wins
- Also applies cargo fmt fixes to crates/sdk/src/queue.rs (import order,
  line-wrap style) so cargo fmt --check passes clean for both crates

Entire-Checkpoint: 4f93a1bd8f6e

* docs(45-01): complete write-durability test safety net plan

Six characterization tests T-45-01/02/03/06/07/08 land and pass green
against current Phase-43/44 code. No production code changed.

Entire-Checkpoint: 604b53361682

* refactor(45-02): extract default_journal_dir and JOURNAL_MAX_RETRIES into shared helper

- Add pub fn default_journal_dir() -> PathBuf and pub const JOURNAL_MAX_RETRIES: u32 = 5
  to apps/desktop/src-tauri/src/fuse/mod.rs
- Route fuse/mod.rs mount path through the shared helper (removes inline dirs chain + literal 5)
- Route commands/sync.rs through crate::fuse::default_journal_dir() + JOURNAL_MAX_RETRIES
- Route fuse/windows/mod.rs (winfsp mount path) through the same shared helper
- Add unit test default_journal_dir_ends_with_cipherbox_cb_journal pinning path tail
- All three WriteQueue::new call sites now use JOURNAL_MAX_RETRIES; no literal 5 remains

Entire-Checkpoint: b92e4fdf5fd3

* docs(45-02): complete extract-journal-dir-helper plan

Entire-Checkpoint: 392261e2fd50

* test(45-03): add RED tests T-45-04 and T-45-04-compat for Option<String> sentinel

- upload_entry_none_ipns_round_trips: builds UploadFile with file_meta_ipns_name: None,
  serializes and deserializes, asserts field is None (T-45-04)
- legacy_empty_string_ipns_loads_as_none: hand-written JSON with legacy empty-string
  value asserts deserializes as None; real name asserts Some(name) (T-45-04-compat)
- Both tests fail to compile until the field type changes to Option<String>

Entire-Checkpoint: 315e60a9eaf7

* feat(45-03): change file_meta_ipns_name to Option<String> with serde compat shim

- Add deser_opt_string helper that maps legacy empty-string sentinel to None
  so pre-Phase-45 on-disk journal entries still deserialize (T-45-03-INT)
- Change JournalOp::UploadFile.file_meta_ipns_name from String to Option<String>
  annotated with #[serde(default, deserialize_with = deser_opt_string)]
- Update make_upload_entry test helper and all test constructors to Some(...)
- 48 cipherbox-sdk tests pass including T-45-04 and T-45-04-compat

Entire-Checkpoint: 7de769e92365

* feat(45-03): propagate Option<String> through fuser, winfsp write paths and replay

- read_ops.rs: remove unwrap_or_default() sentinel; pass file_meta_ipns_name.clone()
  (Option<String>) directly into JournalOp::UploadFile
- windows/write_ops.rs: same change for winfsp write path
- lib.rs: change replay_upload_entry signature to file_meta_ipns_name: Option<&str>;
  wrap per-file IPNS publish block in if let Some(name) = file_meta_ipns_name guard;
  update call site to pass file_meta_ipns_name.as_deref(); update test fixtures to Some(...)
- All 43 fuse tests and 48 sdk tests pass; winfsp build errors are pre-existing macOS
  platform compilation failures unrelated to this change

Entire-Checkpoint: c0bc2abce2ac

* docs(45-03): complete Option<String> sentinel plan summary and state updates

Entire-Checkpoint: 3f89835fbd75

* test(45-04): add RED test T-45-05 not_found_outcome_drives_first_publish

- Asserts NotFound -> is_first_publish=true, seq=0
- Asserts Found(7) -> is_first_publish=false, seq=8
- Asserts Error(_) -> Err propagated (entry retained)
- Fails to compile because IpnsResolveOutcome not yet defined in error.rs

Entire-Checkpoint: 4e9b12148a73

* feat(45-04): add IpnsResolveOutcome enum and resolve_ipns_for_replay wrapper

- Add pub enum IpnsResolveOutcome { Found(u64), NotFound, Error(String) }
  to crates/fuse/src/error.rs with #[derive(Debug)] only (not thiserror)
- Add async fn resolve_ipns_for_replay in lib.rs wrapping resolve_sequence
  and classifying results: Ok->Found, 404/not-found->NotFound, other Err->Error
- Replace .contains(not found) string match in replay_upload_entry with
  typed match on IpnsResolveOutcome (behavior identical: NotFound->seq-0
  first-publish, Error->retain+propagate)
- Bin publish path (spawn_bin_entry_publish:572) intentionally unchanged per scope
- T-45-05 now passes; full fuse suite: 44 passed

Entire-Checkpoint: 5e548fd02038

* docs(45-04): complete typed IpnsResolveOutcome plan summary and state

- Add 45-04-SUMMARY.md with TDD gate compliance and self-check
- Advance STATE.md to Plan 5 of 6, add decisions and metric row
- Mark 45-04-PLAN.md complete in ROADMAP.md

Entire-Checkpoint: dbc0571c3c74

* refactor(45-05): reuse publish_file_metadata and memoize resolve_folder_key in replay

- (#20) Replace 80-line inline encrypt/IPNS-record/TEE-wrap/publish block in
  replay_upload_entry with a single call to the shared publish_file_metadata.
  ECIES-unwrap (Step 1) and is_first_publish decision (F3) stay local; all
  remaining publish steps delegate to the shared function. Added conditional
  use imports to route fuse -> operations::implementation and winfsp (without
  fuse) -> platform::windows::operations::implementation.

- (#15) Add resolve_folder_key_cached wrapper and a root-seeded
  folder_key_cache local to replay_for_vault. replay_mkdir_entry and
  replay_upload_entry take &mut cache and call the cached wrapper; BFS,
  MAX_RESOLVE_NODES cap, and root-shortcut in resolve_folder_key unchanged.

- Extended T-45-07 (resolve_folder_key_cache_resolves_shared_parent_once)
  to exercise resolve_folder_key_cached with a pre-seeded cache and assert
  single-entry cache state after two lookups of the same name.

- All 44 cipherbox-fuse tests pass; cargo check --workspace exit 0.

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

* docs(45-05): complete publish_file_metadata reuse and resolve_folder_key memoization plan

Entire-Checkpoint: 7af2a2eb32ce

* refactor(fuse): consolidate upload+mkdir journal entry builders into journal_helpers

- Add journal_helpers.rs with build_upload_journal_entry and build_mkdir_journal_entry shared builders
- Wire fuser handle_release and winfsp handle_cleanup to build_upload_journal_entry
- Wire fuser handle_mkdir and winfsp handle_create dir branch to build_mkdir_journal_entry
- Each platform retains its own reply/inode mutations and spawn block
- Preserves WinFsp write_generation increment timing difference (read after bump vs before)
- All 44 crash-recovery tests pass; cargo check --workspace clean; fmt check clean

Entire-Checkpoint: ee0779451bb1

* docs(45-06): complete journal-helpers consolidation plan

Entire-Checkpoint: 546e291bb17d

* docs(45): add code review report with false-positive verdict

Entire-Checkpoint: 266466b97326

* docs(phase-45): complete phase execution

Entire-Checkpoint: 57ccd6146897

* docs(phase-45): add security threat verification

Entire-Checkpoint: 04497394bc91

* docs(phase-45): mark validation nyquist-compliant

Entire-Checkpoint: db4adc7537ce

* refactor(fuse): dedup journal-builder helpers and clarify plaintext invariant

Quality cleanup of the new journal_helpers.rs (no behavior change):
- extract current_unix_ms(), generate_entry_id(), wrap_key_to_hex() to
  remove copy-paste across build_upload_journal_entry / build_mkdir_journal_entry
- drop the dead wrapped_key_hex rename-clone; inline at the struct field
- collapse the duplicate write_generation/parent_ino inode lookups into one
- replace the opaque file_meta_ipns_name.as_ref().and_then(|_| ...) guard with
  an explicit is_none() branch
- correct the module/struct docs: the journal stores ciphertext only, but the
  result struct does carry transient in-memory plaintext for the pending_content
  read-after-write cache (never persisted)

cargo test -p cipherbox-fuse -p cipherbox-sdk green (44 + 48); workspace check clean.

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

* fix(fuse): zeroize file key on encryption and wrap error paths

generate_file_key() returns a plain [u8; 32] with no zeroize-on-drop, so an
early `?` return from encrypt_aes_gcm or ecies::wrap_key dropped the raw file
key without clearing it — contradicting the adjacent comment. Clear the key via
clear_bytes before every error return so it is wiped on all paths.

Addresses CodeRabbit review on PR #491.

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

* docs: capture todos for deferred replay hardening from PR 491 review

Two CodeRabbit findings on PR #491 are pre-existing crash-recovery behaviors
that Phase 45 (#18/#19) deliberately preserved (no-behavior-change), so they are
out of scope for that PR and logged as follow-ups:
- replay IPNS resolve uses cache fallback (transient failure can become Found)
- legacy empty file_meta_ipns_name replay publishes an empty FilePointer

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

* test(fuse): cover the replay IPNS resolve classification predicate

Extract the #19 not-found/404 substring classification out of the async,
network-bound resolve_ipns_for_replay into a pure classify_resolve_outcome(result)
helper, and unit-test it directly (Found / NotFound case-insensitive + 404 /
Error). This pins the brittle substring contract without a live API client and
lifts the one cheaply-coverable untested branch in the replay sequencing path.
Behavior is unchanged — resolve_ipns_for_replay now just wraps the pure helper.

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

* fix(fuse): harden mkdir durability — roll back inode on journal failure, replay child IPNS

Address two outside-of-diff CodeRabbit findings on PR #491:

- mkdir inserts the child inode and mutates the parent before building/fsyncing
  the journal entry. On build/put failure the error was returned to the OS but
  the ghost directory was left in the inode table with no durable replay record.
  Roll back via InodeTable::remove on both the fuser and WinFsp paths.

- replay_mkdir_entry only merged the child FolderEntry into the parent; it never
  re-published the child folder's own seq-0 IPNS record. A crash after the
  MkdirPublish fsync but before the live background publish left the parent
  pointing at an unresolvable child IPNS name. Re-publish the child's initial
  empty FolderMetadata idempotently (skip on Found, retain on resolve Error)
  with TEE first-publish enrollment, mirroring the UploadFile replay path.

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

* docs: log todo for fuse journal_helpers/read_ops/write_ops test coverage

journal_helpers.rs builders are pure and unit-testable now (needs a make_test_fs
helper). read_ops/write_ops handlers are blocked: they consume concrete fuser
Reply objects, and the vendored fuser does not export ReplySender, so a capturing
sender can't be implemented from cipherbox-fuse. Captures options A/B/C for later.

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

* docs: reconcile Phase 45 todo bookkeeping with shipped work

Phase 45 (PR #491) implemented all 7 scoped todos but the bookkeeping was never
updated (flagged in 45-VERIFICATION.md). Reconcile:

- Move the 6 fully-delivered todos (#11/#12/#15/#18/#19/#20) pending -> done and tick
  their ROADMAP scope checkboxes.
- Keep #14 (test coverage) in pending: only Tier 1 (durability/replay safety-net tests)
  landed; Tier 2 (read_ops/write_ops handler harness) is still open, so it should be
  picked up on the next run.
- Fold the standalone 2026-06-15 fuse-test-coverage todo into #14 (single source of
  truth): records the fuser ReplySender blocker (vendored fuser doesn't export
  ReplySender, so handler reply objects can't be built in unit tests) and that
  journal_helpers.rs builders are unit-testable now via a make_test_fs helper.

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

---------

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

2 participants