CLAUDE.md#
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands#
Testing#
# Run all tests across the monorepo
deno test --allow-env
# Run tests for a specific package
deno test packages/crypto/
# Run E2E tests (requires CISTERN_HANDLE and CISTERN_APP_PASSWORD environment variables)
deno test --allow-env --allow-net e2e.test.ts
Lexicon Code Generation#
# Generate TypeScript types from JSON lexicon definitions
cd packages/lexicon
deno task generate
This generates types in packages/lexicon/src/types/ from the JSON schemas in
packages/lexicon/lexicons/. Run this after modifying any .json files in the
lexicons directory.
Type Checking#
# Deno executes TypeScript directly - no build step needed
# Check types explicitly with:
deno check <file.ts>
Architecture Overview#
Cistern is a Deno monorepo implementing a private, encrypted quick-capture system on AT Protocol. Items are end-to-end encrypted using post-quantum cryptography and stored temporarily in the user's PDS (Personal Data Server).
Monorepo Structure#
Five packages with clear separation of concerns:
@cistern/crypto- Core cryptographic primitives (encryption/decryption/keys)@cistern/lexicon- AT Protocol schema definitions (pubkey + item records)@cistern/shared- Authentication utilities and common code@cistern/producer- Creates and encrypts items for storage@cistern/consumer- Retrieves, decrypts, and deletes items
Internal imports use the @cistern/* namespace defined in each package's
deno.jsonc.
Producer/Consumer Pattern#
Producer workflow (packages/producer/mod.ts):
- Select a public key from those registered in the user's PDS
- Encrypt plaintext using the public key
- Create an
app.cistern.lexicon.itemrecord with the encrypted payload - Upload to PDS
Consumer workflow (packages/consumer/mod.ts):
- Generate an X-Wing keypair (post-quantum)
- Upload public key to PDS as
app.cistern.lexicon.pubkeyrecord - Keep private key locally (never uploaded)
- Retrieve items via polling (
listItems()) or streaming (subscribeToItems()) - Decrypt items matching the local keypair
- Delete items after consumption
Encryption Architecture#
Algorithm: x_wing-xchacha20_poly1305-sha3_512
The encryption system uses a hybrid approach combining:
- X-Wing KEM (Key Encapsulation Mechanism) - post-quantum hybrid combining ML-KEM-768 and X25519
- XChaCha20-Poly1305 - authenticated encryption cipher
- SHA3-512 - content integrity verification
Encryption flow (packages/crypto/src/encrypt.ts):
- X-Wing encapsulation generates a shared secret from the public key
- XChaCha20-Poly1305 encrypts the plaintext using the shared secret
- SHA3-512 hash computed for integrity verification
- Returns
EncryptedPayloadcontaining ciphertext, nonce, hash, and metadata
Decryption flow (packages/crypto/src/decrypt.ts):
- X-Wing decapsulation recovers the shared secret using the private key
- XChaCha20-Poly1305 decrypts the content
- Integrity verification: check content length and SHA3-512 hash match
- Returns plaintext or throws error if verification fails
AT Protocol Integration#
Cistern uses two record types in the user's PDS:
app.cistern.lexicon.pubkey
(packages/lexicon/lexicons/app/cistern/lexicon/pubkey.json):
- Stores public keys with human-readable names
- Referenced by items via AT-URI
- Schema:
{name, algorithm, content, createdAt}
app.cistern.lexicon.item
(packages/lexicon/lexicons/app/cistern/lexicon/item.json):
- Stores encrypted items temporarily
- Schema:
{tid, ciphertext, nonce, algorithm, pubkey, payload, contentLength, contentHash} - The
pubkeyfield is an AT-URI reference to the public key record
Real-time Streaming#
The consumer can subscribe to new items via Jetstream (packages/consumer/mod.ts:150-190):
- Connects to Bluesky's Jetstream WebSocket service
- Filters for
app.cistern.lexicon.itemcreates matching user DID - Decrypts items as they arrive in real-time
- Used for instant delivery (e.g., Obsidian plugin waiting for new memos)
Key Management#
Private keys never leave the consumer's device. The security model depends on:
- Private key stored off-protocol (e.g., in an Obsidian vault)
- Public key stored in PDS as a record
- Items encrypted with public key can only be decrypted by matching private key
- Each keypair can have a human-readable name (e.g., "Work Laptop", "Phone")
Dependencies#
Cryptography (JSR packages):
@noble/post-quantum- X-Wing KEM implementation@noble/ciphers- XChaCha20-Poly1305@noble/hashes- SHA3-512
AT Protocol (npm packages):
@atcute/client- RPC client for PDS communication@atcute/jetstream- Real-time event streaming@atcute/lexicons- Schema validation@atcute/tid- Timestamp identifiers
Key Files and Locations#
Cryptographic Operations#
packages/crypto/src/keys.ts- Keypair generation (X-Wing)packages/crypto/src/encrypt.ts- Encryption logicpackages/crypto/src/decrypt.ts- Decryption + integrity verificationpackages/crypto/src/*.test.ts- Crypto unit tests
Producer Implementation#
packages/producer/mod.ts- Main producer class and encryption workflow
Consumer Implementation#
packages/consumer/mod.ts- Keypair management, item retrieval, Jetstream subscription
Authentication#
packages/shared/produce-requirements.ts- DID resolution and session creation- Uses Slingshot service for handle → DID resolution
- Creates authenticated RPC client with app password
Schema Definitions#
packages/lexicon/lexicons/app/cistern/lexicon/*.json- AT Protocol record schemaspackages/lexicon/src/types/- Generated TypeScript types (rundeno task generateto update)packages/lexicon/lex.config.ts- Lexicon generator configuration
Important Patterns#
Error Handling in Decryption#
Decryption can fail for multiple reasons (packages/crypto/src/decrypt.ts):
- Wrong private key (decapsulation fails)
- Corrupted ciphertext (authentication fails)
- Length mismatch (integrity check fails)
- Hash mismatch (integrity check fails)
Always wrap decrypt calls in try-catch and handle gracefully.
Pagination in Consumer#
listItems() returns an async generator that handles pagination automatically.
It yields decrypted items and internally manages cursors. Consumers should
iterate with for await loops.
Resource URIs#
AT Protocol uses AT-URIs to reference records: at://<did>/<collection>/<rkey>
The consumer caches the public key's AT-URI with the local keypair to filter which items it can decrypt.
Testing#
Unit Tests#
Each package contains unit tests following these conventions:
- Test files use
.test.tssuffix - Use
@std/expectfor assertions - Mock external dependencies (RPC clients, credentials)
- Test both success and error paths
Test locations:
packages/crypto/src/*.test.ts- Cryptographic operationspackages/consumer/mod.test.ts- Consumer functionalitypackages/producer/mod.test.ts- Producer functionality
End-to-End Tests#
e2e.test.ts contains integration tests that use real AT Protocol credentials:
- Requires
CISTERN_HANDLEandCISTERN_APP_PASSWORDenvironment variables - Tests full workflow: keypair generation, encryption, decryption, deletion
- Uses Deno test steps to segment each phase
- Automatically skipped if environment variables are not set
- Cleans up all test data after execution