Skip to content

feat(05): Folder System - IPNS metadata, folder hierarchy, and operations - #39

Merged
FSM1 merged 26 commits into
mainfrom
feat/phase-5-folder-system
Jan 21, 2026
Merged

feat(05): Folder System - IPNS metadata, folder hierarchy, and operations#39
FSM1 merged 26 commits into
mainfrom
feat/phase-5-folder-system

Conversation

@FSM1

@FSM1 FSM1 commented Jan 21, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add backend IPNS module with delegated routing relay (POST /ipns/publish)
  • Extend @cipherbox/crypto with IPNS record creation and folder metadata encryption
  • Create frontend vault/folder Zustand stores and IPNS publishing service
  • Implement complete folder CRUD operations (create, delete, rename, move)

What's Included

Backend (apps/api)

  • IpnsModule with POST /ipns/publish endpoint
  • FolderIpns entity for tracking IPNS names/CIDs (TEE republishing)
  • Delegated routing client with exponential backoff retry

Crypto Package (packages/crypto)

  • IPNS record creation using ipns npm package with libp2p integration
  • IPNS name derivation from Ed25519 public keys
  • Folder metadata types (FolderMetadata, FolderEntry, FileEntry)
  • AES-256-GCM encryption/decryption for folder metadata
  • 24 new tests for IPNS and folder operations

Frontend (apps/web)

  • useVaultStore - memory-only key management with zeroing
  • useFolderStore - folder tree state management
  • ipns.service.ts - local IPNS signing with backend relay
  • folder.service.ts - complete CRUD operations
  • useFolder hook - React integration with loading/error state

Requirements Completed

  • FOLD-01: User can create folders ✓
  • FOLD-02: User can delete folders (recursive) ✓
  • FOLD-03: User can nest folders up to 20 levels deep ✓
  • FOLD-04: User can rename folders ✓
  • FOLD-05: User can move folders between parent folders ✓
  • FOLD-06: Each folder has its own IPNS keypair ✓
  • FILE-04: User can rename files ✓
  • FILE-05: User can move files between folders ✓
  • API-05: Backend relays pre-signed IPNS records ✓

Test plan

  • Backend: IPNS publish endpoint accepts valid records
  • Crypto: IPNS record creation tests pass (13 tests)
  • Crypto: Folder metadata encryption tests pass (11 tests)
  • Frontend: Folder creation with depth limit enforcement
  • Frontend: Recursive folder deletion with file unpinning
  • Frontend: Move operations prevent circular references

🤖 Generated with Claude Code

FSM1 and others added 25 commits January 21, 2026 04:19
Phase 05: Folder System
- Implementation decisions documented
- Phase boundary established
Phase 05: Folder System
- Standard stack: ipns, @libp2p/crypto, @libp2p/peer-id packages
- Architecture: client-signed IPNS with delegated routing API relay
- Folder metadata encryption patterns documented
- Pitfalls: Ed25519 key format, sequence numbers, TTL vs lifetime
- Code examples for IPNS record creation and publishing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 05-03: Add depth check in createFolder (FOLD-03)
- 05-03: Add getDepth helper to exports
- 05-03: Document VaultStore auth flow integration
- 05-04: Add deleteFile function to Task 1
- 05-04: Add depth validation in handleCreate hook
- 05-04: Update must_haves to include file deletion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add FolderIpns entity with unique (userId, ipnsName) constraint
- Track ipnsName, latestCid, sequenceNumber for each folder
- Store ECIES-wrapped IPNS keys for TEE republishing
- Add PublishIpnsDto with validation decorators
- Add PublishIpnsResponseDto for publish response
- Add ipns, @libp2p/crypto, @libp2p/peer-id, multiformats dependencies
- createIpnsRecord: converts @noble/ed25519 keys to libp2p format
- deriveIpnsName: derives k51... IPNS name from Ed25519 public key
- marshalIpnsRecord/unmarshalIpnsRecord: protobuf serialization wrappers
- publishRecord() validates base64, relays to delegated-ipfs.dev
- Exponential backoff retry on rate limits and network errors
- upsertFolderIpns() tracks folder IPNS names and CIDs
- getFolderIpns() and getAllFolderIpns() for TEE republishing
- Configurable DELEGATED_ROUTING_URL env var
- FolderMetadata, FolderEntry, FileEntry types per RESEARCH.md spec
- EncryptedFolderMetadata for storage format
- encryptFolderMetadata: AES-256-GCM encryption with random IV
- decryptFolderMetadata: restores metadata from encrypted form
…ient

- Add IpnsController with POST /ipns/publish endpoint
- Add IpnsModule with proper exports for future VaultModule integration
- Register IpnsModule and FolderIpns entity in app.module.ts
- Update generate-openapi.ts with IpnsController and IpnsService
- Regenerate OpenAPI spec and web API client
Tasks completed: 3/3
- Create FolderIpns entity and DTOs
- Create IpnsService with delegated routing
- Create IpnsController and IpnsModule

SUMMARY: .planning/phases/05-folder-system/05-01-SUMMARY.md
- IPNS record tests: createIpnsRecord, deriveIpnsName, marshal/unmarshal
- Folder metadata tests: encrypt/decrypt round-trip, security tests
- Tests verify compatibility with IPFS network expectations
- All 132 tests passing
Tasks completed: 3/3
- Task 1: Add IPNS dependencies and create record functions
- Task 2: Create folder metadata types and encryption
- Task 3: Add tests for IPNS record and folder metadata

SUMMARY: .planning/phases/05-folder-system/05-02-SUMMARY.md
- Add useVaultStore Zustand store for decrypted vault keys
- Store rootFolderKey (AES-256) for folder encryption
- Store rootIpnsKeypair (Ed25519) for IPNS signing
- Store rootIpnsName derived from keypair
- Implement memory-zeroing on clearVaultKeys (MEDIUM-02 security)
- Document integration flow with auth.store.ts
- Add createAndPublishIpnsRecord for local signing and backend relay
- Sign IPNS records locally with Ed25519 private key
- Marshal and base64-encode records for API transport
- Integrate with @cipherbox/crypto createIpnsRecord and marshalIpnsRecord
- Add resolveIpnsRecord stub (Phase 7 implementation)
- Create services/index.ts barrel export
- Add useFolderStore Zustand store for folder tree state
- Export FolderNode type for folder representation
- Track current folder, breadcrumbs, pending publishes
- Implement memory-zeroing on clearFolders (MEDIUM-02 security)

- Add folder.service.ts with CRUD operations
- getDepth: calculate folder depth from root
- createFolder: generate IPNS keypair, folder key, ECIES wrap
- loadFolder: stub for Phase 05-04 implementation
- updateFolderMetadata: encrypt metadata, upload, publish IPNS
- Enforce FOLD-03 max depth of 20 in createFolder

- Update services/index.ts barrel export
Tasks completed: 3/3
- Create vault store for key management
- Create IPNS service for record publishing
- Create folder store and service

SUMMARY: .planning/phases/05-folder-system/05-03-SUMMARY.md
- Add renameFolder: updates folder name in parent metadata and publishes IPNS
- Add deleteFolder: recursively collects CIDs, removes from parent, unpins
- Add deleteFileFromFolder: removes file from parent metadata and unpins
- Add folder.store removeFolder: clears folder keys and removes from state
- Add folder.store updateFolderName: updates folder name in local state
- Name collision check on rename with descriptive error
- Add moveFolder: add-before-remove pattern, depth limit check, self-move prevention
- Add moveFile: add-before-remove pattern, name collision check
- Add renameFile: updates file name in parent metadata with collision check
- Add calculateSubtreeDepth: computes max depth for move validation (FOLD-03)
- Add isDescendantOf: prevents moving folder into itself or descendants
- Add useFolder hook with createFolder, renameItem, moveItem, deleteItem
- Each operation manages loading/error state
- Depth limit validation (FOLD-03) on folder creation
- Uses add-before-remove pattern for move operations
- Updates local folder store state after operations
- Add hooks barrel export file (hooks/index.ts)
Tasks completed: 3/3
- Task 1: Implement folder rename and delete operations
- Task 2: Implement move operations for files and folders
- Task 3: Create useFolder hook for UI integration

SUMMARY: .planning/phases/05-folder-system/05-04-SUMMARY.md
Phase 5 verified:
- 4/4 plans executed
- 6/6 success criteria verified
- 9 requirements marked complete (FOLD-01 through FOLD-06, FILE-04, FILE-05, API-05)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
CRITICAL:
- Add ValidationPipe to API main.ts to enable DTO validation

HIGH:
- Clear vault/folder stores on logout to zero crypto keys from memory
- Clear all stores on token refresh failure
- Add @nestjs/throttler rate limiting to IPNS publish endpoint

MEDIUM:
- Clear intermediate private key material after use in create-record.ts
- Add sequence number validation (non-negative) in create-record.ts
- Add runtime type validation for decrypted folder metadata
- Use chunked base64 encoding for large folder metadata
- Strengthen DTO validation (ipnsName, metadataCid, encryptedIpnsPrivateKey)
- Sanitize error messages in ipns.service.ts to avoid leaking internals

Documentation:
- Add security review report (.planning/security/REVIEW-2026-01-21-phase5.md)
- Document LOW severity issues for future work

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ThrottlerModule to OpenAPI generator module (required by IpnsController)
- Add comprehensive unit tests for IpnsService (24 tests, 100% coverage)
- Regenerate OpenAPI spec with proper module dependencies

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

codecov Bot commented Jan 21, 2026

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

@FSM1
FSM1 merged commit 8793004 into main Jan 21, 2026
4 checks passed
@FSM1
FSM1 deleted the feat/phase-5-folder-system branch January 27, 2026 03:26
This was referenced Mar 23, 2026
FSM1 added a commit that referenced this pull request Jul 20, 2026
…ks to trusted reader state

F2: ascent authority moves from the candidate into ReaderContext.parent_node_seed; the gate derives the expected keypair from reader state and fails closed when the seed is absent (engine.md:405-406).
F1: every seed-bearing structure must match the envelope scope/epoch or it is a cross-epoch replay, rejected at grant-section (engine.md:149-153, #39 D3).
F3: the seed blob's AAD must match the envelope scope/epoch/v and the recovered seed must derive the reader's read key via constant-time compare, rejected at unseal (engine.md:406-408 cross-check discipline).
FSM1 added a commit that referenced this pull request Jul 21, 2026
* feat(engine): adoption gate and floor law with sim harness

Land the six-stage adoption gate as a pure pipeline over core's
verify/unseal functions and the durable floor law, with an N-engine
simulation harness on the fake record store.

- gate/adoption.rs: the six stages (record verify, commitment verify,
  grant-section authentication under committed write pseudonyms,
  strictly-newer sequence, epoch-at-or-above-floor, unseal). Every
  cryptographic verdict is a core TrustViolation surfaced verbatim; the
  only engine-domain verdicts are the two floor comparisons. Fail-closed,
  whole-record rejection.
- gate/floor.rs: the floor law — advance only on AAD-confirmed unseal,
  cold-seed from a re-point object's owner-vouched writeEpoch/minReadEpoch,
  writeEpoch advances on sight, grant-blob epoch advisory only, monotonic
  FloorStore regression fail-closed.
- tests/adoption_gate.rs: the six-stage matrix (accept + every reject
  class with its named trust-violation error), the floor-law scenarios,
  the N-engine adversarial sim (replay/transplant/re-sign on one shared
  record store, virtual time), and table-mirroring anti-vacuity meta-tests.

No crypto, codec, or new error code in the engine; time/entropy never
touched. Covered by the existing "Engine Tests" CI gate.

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

* refactor(engine): fail-safe floor ordering and provenance docs

Address the CodeRabbit CLI pre-pass on the adoption-gate floor law:

- advance_on_unseal and cold_seed now commit the trust-critical
  read-epoch (revocation) floor before the sequence/write-epoch floor,
  so a partial FloorStore seam failure leaves the fail-closed state
  (idempotent monotonic raises re-converge on the caller's retry).
- Document the floor-mutation provenance contract (adopt is the sole
  advance_on_unseal caller; cold_seed only takes an authenticated
  RepointObject) and the single-writer invariant that makes the
  stage-4/5 read-then-advance safe without a CAS pair. The cross-key
  transactional seam is FloorStore territory (frozen), out of scope.

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

* fix(engine): bind adoption-gate ascent, structure, and seed-blob checks to trusted reader state

F2: ascent authority moves from the candidate into ReaderContext.parent_node_seed; the gate derives the expected keypair from reader state and fails closed when the seed is absent (engine.md:405-406).
F1: every seed-bearing structure must match the envelope scope/epoch or it is a cross-epoch replay, rejected at grant-section (engine.md:149-153, #39 D3).
F3: the seed blob's AAD must match the envelope scope/epoch/v and the recovered seed must derive the reader's read key via constant-time compare, rejected at unseal (engine.md:406-408 cross-check discipline).

* fix(engine): bind ascent-link AAD to the envelope in the adoption gate

The reader-supplied ascent-link AAD (scope/id/epoch/v/structTag) is now cross-checked against the current envelope before open_ascent_link, closing a replay of a link minted under the same parent seed for another node/epoch/version (engine.md:405-406). Fail closed at grant-section on any mismatch.

* fix(engine): bind the ascent-link seed and complete the adoption-gate cross-check audit

G3: the ascent link's override seed is now cross-checked instead of dropped — it must belong to the envelope epoch and derive the scope root's read key (constant-time), the ascent-link half of the owner-blob/ascent-link/actual-unseal discipline (engine.md:406-408, CONTEXT.md "Ascent link"/"Override seed").
Audit: every candidate- and network-supplied gate input is now bound to the envelope + cross-checked, trusted reader state, or verified by an earlier stage; the only deferral is structure ciphertext_hash recompute (#687).

* fix(engine): bind envelope scope to reader and complete the adoption-gate binding graph

The scope UUID is not in the read-key KDF, so a foreign-scope envelope still unseals under the reader's key — assert candidate.envelope.scope == reader.scope_id once at unseal, joining envelope.scope, reader.scope_id, and every AAD/structure scope into one equivalence class (engine.md cross-check discipline).
Close the seed-blob AAD's free id and structTag edges: aad.id == envelope.id and aad.structTag == the blob-type tag, so seed-blob id/tag join the same graph as scope/epoch/v.
Binding graph is now one connected component per {scope, id, epoch, v}, with structTag pinned to its per-structure domain-separation constant.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FSM1 added a commit that referenced this pull request Jul 22, 2026
Reflects the #712 recipient-binding change in blueprint/core.md so the
normative mailbox description matches the implemented sender-signature
preimage. Amends the #39 D9 mailbox decision (recorded as an ADR in
cipher-box-next).
FSM1 added a commit that referenced this pull request Jul 22, 2026
…731)

Binds the recipient X25519 public key into the mailbox sender-signature preimage, closing the cross-recipient relay-lift. Amends #39 D9 (ADR in FSM1/cipher-box-next#57).

Reviews: independent trio clean (crypto-privacy, security, simplify) + Greptile clean (sole finding a moot pre-release compat note). CodeRabbit was infra-unavailable (org adaptive rate-limit); merged on the strength of trio + Greptile + full green CI per maintainer authorization.

Closes #712.
FSM1 added a commit that referenced this pull request Aug 8, 2026
* docs: name the owner pseudonym seed edge in the KDF catalog

#39 D2 left the owner's pseudonym-sign input as "her root secret", which
names no field, so OwnerScopeKeys has no production implementor. ADR 0005
settles it as a dedicated owner-pseudonym-seed edge from the login secret,
keeping the owner's structure-signing authority off its encryption subkey.

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

* docs: qualify the cross-repo decision reference on the pseudonym bullet

A bare #27 autolinks to this repository's issue 27, not the decision corpus.

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

* docs: qualify every decision-corpus reference across the blueprint

A bare #NN in this repository autolinks to a cipher-box PR or issue, not to
the wayfinder decision it names, and this repository's counter passed 1000
long ago. Qualifies the 255 corpus references across the blueprint corpus and
leaves the 8 genuine cipher-box references bare.

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

---------

Co-authored-by: Claude Opus 5 <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