Encrypted, ephemeral, private memos on atproto

Compare changes

Choose any two refs to compare.

+6
.letta/settings.json
···
··· 1 + { 2 + "localSharedBlockIds": { 3 + "project": "block-19ad1e80-37cd-4413-8f73-e1dddbb89119", 4 + "skills": "block-25b184a7-e851-42d4-bf5a-9422337e7803" 5 + } 6 + }
+3
.letta/settings.local.json
···
··· 1 + { 2 + "lastAgent": "agent-2470875d-24da-4bab-8670-293b3cbcf3cf" 3 + }
-241
CLAUDE.md
··· 1 - # CLAUDE.md 2 - 3 - This file provides guidance to Claude Code (claude.ai/code) when working with 4 - code in this repository. 5 - 6 - ## Commands 7 - 8 - ### Testing 9 - 10 - ```bash 11 - # Run all tests across the monorepo 12 - deno test --allow-env 13 - 14 - # Run tests for a specific package 15 - deno test packages/crypto/ 16 - 17 - # Run E2E tests (requires CISTERN_HANDLE and CISTERN_APP_PASSWORD environment variables) 18 - deno test --allow-env --allow-net e2e.test.ts 19 - ``` 20 - 21 - ### Lexicon Code Generation 22 - 23 - ```bash 24 - # Generate TypeScript types from JSON lexicon definitions 25 - cd packages/lexicon 26 - deno task generate 27 - ``` 28 - 29 - This generates types in `packages/lexicon/src/types/` from the JSON schemas in 30 - `packages/lexicon/lexicons/`. Run this after modifying any `.json` files in the 31 - lexicons directory. 32 - 33 - ### Type Checking 34 - 35 - ```bash 36 - # Deno executes TypeScript directly - no build step needed 37 - # Check types explicitly with: 38 - deno check <file.ts> 39 - ``` 40 - 41 - ## Architecture Overview 42 - 43 - Cistern is a **Deno monorepo** implementing a private, encrypted quick-capture 44 - system on AT Protocol. Items are end-to-end encrypted using post-quantum 45 - cryptography and stored temporarily in the user's PDS (Personal Data Server). 46 - 47 - ### Monorepo Structure 48 - 49 - Five packages with clear separation of concerns: 50 - 51 - - **`@cistern/crypto`** - Core cryptographic primitives 52 - (encryption/decryption/keys) 53 - - **`@cistern/lexicon`** - AT Protocol schema definitions (pubkey + item 54 - records) 55 - - **`@cistern/shared`** - Authentication utilities and common code 56 - - **`@cistern/producer`** - Creates and encrypts items for storage 57 - - **`@cistern/consumer`** - Retrieves, decrypts, and deletes items 58 - 59 - Internal imports use the `@cistern/*` namespace defined in each package's 60 - `deno.jsonc`. 61 - 62 - ### Producer/Consumer Pattern 63 - 64 - **Producer** workflow (packages/producer/mod.ts): 65 - 66 - 1. Select a public key from those registered in the user's PDS 67 - 2. Encrypt plaintext using the public key 68 - 3. Create an `app.cistern.lexicon.item` record with the encrypted payload 69 - 4. Upload to PDS 70 - 71 - **Consumer** workflow (packages/consumer/mod.ts): 72 - 73 - 1. Generate an X-Wing keypair (post-quantum) 74 - 2. Upload public key to PDS as `app.cistern.lexicon.pubkey` record 75 - 3. Keep private key locally (never uploaded) 76 - 4. Retrieve items via **polling** (`listItems()`) or **streaming** 77 - (`subscribeToItems()`) 78 - 5. Decrypt items matching the local keypair 79 - 6. Delete items after consumption 80 - 81 - ### Encryption Architecture 82 - 83 - **Algorithm**: `x_wing-xchacha20_poly1305-sha3_512` 84 - 85 - The encryption system uses a hybrid approach combining: 86 - 87 - - **X-Wing KEM** (Key Encapsulation Mechanism) - post-quantum hybrid combining 88 - ML-KEM-768 and X25519 89 - - **XChaCha20-Poly1305** - authenticated encryption cipher 90 - - **SHA3-512** - content integrity verification 91 - 92 - **Encryption flow** (packages/crypto/src/encrypt.ts): 93 - 94 - 1. X-Wing encapsulation generates a shared secret from the public key 95 - 2. XChaCha20-Poly1305 encrypts the plaintext using the shared secret 96 - 3. SHA3-512 hash computed for integrity verification 97 - 4. Returns `EncryptedPayload` containing ciphertext, nonce, hash, and metadata 98 - 99 - **Decryption flow** (packages/crypto/src/decrypt.ts): 100 - 101 - 1. X-Wing decapsulation recovers the shared secret using the private key 102 - 2. XChaCha20-Poly1305 decrypts the content 103 - 3. Integrity verification: check content length and SHA3-512 hash match 104 - 4. Returns plaintext or throws error if verification fails 105 - 106 - ### AT Protocol Integration 107 - 108 - Cistern uses two record types in the user's PDS: 109 - 110 - **`app.cistern.lexicon.pubkey`** 111 - (packages/lexicon/lexicons/app/cistern/lexicon/pubkey.json): 112 - 113 - - Stores public keys with human-readable names 114 - - Referenced by items via AT-URI 115 - - Schema: `{name, algorithm, content, createdAt}` 116 - 117 - **`app.cistern.lexicon.item`** 118 - (packages/lexicon/lexicons/app/cistern/lexicon/item.json): 119 - 120 - - Stores encrypted items temporarily 121 - - Schema: 122 - `{tid, ciphertext, nonce, algorithm, pubkey, payload, contentLength, contentHash}` 123 - - The `pubkey` field is an AT-URI reference to the public key record 124 - 125 - ### Real-time Streaming 126 - 127 - The consumer can subscribe to new items via **Jetstream** 128 - (packages/consumer/mod.ts:150-190): 129 - 130 - - Connects to Bluesky's Jetstream WebSocket service 131 - - Filters for `app.cistern.lexicon.item` creates matching user DID 132 - - Decrypts items as they arrive in real-time 133 - - Used for instant delivery (e.g., Obsidian plugin waiting for new memos) 134 - 135 - ### Key Management 136 - 137 - **Private keys never leave the consumer's device.** The security model depends 138 - on: 139 - 140 - - Private key stored off-protocol (e.g., in an Obsidian vault) 141 - - Public key stored in PDS as a record 142 - - Items encrypted with public key can only be decrypted by matching private key 143 - - Each keypair can have a human-readable name (e.g., "Work Laptop", "Phone") 144 - 145 - ### Dependencies 146 - 147 - **Cryptography** (JSR packages): 148 - 149 - - `@noble/post-quantum` - X-Wing KEM implementation 150 - - `@noble/ciphers` - XChaCha20-Poly1305 151 - - `@noble/hashes` - SHA3-512 152 - 153 - **AT Protocol** (npm packages): 154 - 155 - - `@atcute/client` - RPC client for PDS communication 156 - - `@atcute/jetstream` - Real-time event streaming 157 - - `@atcute/lexicons` - Schema validation 158 - - `@atcute/tid` - Timestamp identifiers 159 - 160 - ## Key Files and Locations 161 - 162 - ### Cryptographic Operations 163 - 164 - - `packages/crypto/src/keys.ts` - Keypair generation (X-Wing) 165 - - `packages/crypto/src/encrypt.ts` - Encryption logic 166 - - `packages/crypto/src/decrypt.ts` - Decryption + integrity verification 167 - - `packages/crypto/src/*.test.ts` - Crypto unit tests 168 - 169 - ### Producer Implementation 170 - 171 - - `packages/producer/mod.ts` - Main producer class and encryption workflow 172 - 173 - ### Consumer Implementation 174 - 175 - - `packages/consumer/mod.ts` - Keypair management, item retrieval, Jetstream 176 - subscription 177 - 178 - ### Authentication 179 - 180 - - `packages/shared/produce-requirements.ts` - DID resolution and session 181 - creation 182 - - Uses Slingshot service for handle โ†’ DID resolution 183 - - Creates authenticated RPC client with app password 184 - 185 - ### Schema Definitions 186 - 187 - - `packages/lexicon/lexicons/app/cistern/lexicon/*.json` - AT Protocol record 188 - schemas 189 - - `packages/lexicon/src/types/` - Generated TypeScript types (run 190 - `deno task generate` to update) 191 - - `packages/lexicon/lex.config.ts` - Lexicon generator configuration 192 - 193 - ## Important Patterns 194 - 195 - ### Error Handling in Decryption 196 - 197 - Decryption can fail for multiple reasons (packages/crypto/src/decrypt.ts): 198 - 199 - - Wrong private key (decapsulation fails) 200 - - Corrupted ciphertext (authentication fails) 201 - - Length mismatch (integrity check fails) 202 - - Hash mismatch (integrity check fails) 203 - 204 - Always wrap decrypt calls in try-catch and handle gracefully. 205 - 206 - ### Pagination in Consumer 207 - 208 - `listItems()` returns an async generator that handles pagination automatically. 209 - It yields decrypted items and internally manages cursors. Consumers should 210 - iterate with `for await` loops. 211 - 212 - ### Resource URIs 213 - 214 - AT Protocol uses AT-URIs to reference records: `at://<did>/<collection>/<rkey>` 215 - 216 - The consumer caches the public key's AT-URI with the local keypair to filter 217 - which items it can decrypt. 218 - 219 - ## Testing 220 - 221 - ### Unit Tests 222 - 223 - Each package contains unit tests following these conventions: 224 - - Test files use `.test.ts` suffix 225 - - Use `@std/expect` for assertions 226 - - Mock external dependencies (RPC clients, credentials) 227 - - Test both success and error paths 228 - 229 - **Test locations:** 230 - - `packages/crypto/src/*.test.ts` - Cryptographic operations 231 - - `packages/consumer/mod.test.ts` - Consumer functionality 232 - - `packages/producer/mod.test.ts` - Producer functionality 233 - 234 - ### End-to-End Tests 235 - 236 - `e2e.test.ts` contains integration tests that use real AT Protocol credentials: 237 - - Requires `CISTERN_HANDLE` and `CISTERN_APP_PASSWORD` environment variables 238 - - Tests full workflow: keypair generation, encryption, decryption, deletion 239 - - Uses Deno test steps to segment each phase 240 - - Automatically skipped if environment variables are not set 241 - - Cleans up all test data after execution
···
+98 -12
README.md
··· 1 # Cistern 2 3 - Cistern is an attempt at implementing a private, personal quick-capture system 4 - on AT Protocol. Cistern "items" are encrypted, so that they are only readable by 5 - the holder of the correct secret keyโ€”stored off-protocol. These items are 6 - intended to be ephemeral, and to be deleted after they've been read. 7 8 - The intention is for Cistern to bridge the gap between where ideas are had and 9 - where they can be stored long-term. For example, let's say you have an idea 10 - while at a restaurant. You create an item using your phone, and once you're back 11 - at your desk and you open Obsidian, that item is automatically pulled from your 12 - PDS, decrypted, and deleted from your PDS. If your notebook was open at the time 13 - you created your item, the Cistern Obsidian plugin would have been notified of 14 - the new item via the Jetstream, and so you would find your memo waiting for you 15 - once you got home.
··· 1 # Cistern 2 3 + Cistern is a private, end-to-end encrypted quick-capture system built on AT 4 + Protocol. Memos are encrypted using post-quantum cryptography and stored 5 + temporarily in your Personal Data Server (PDS), then automatically retrieved and 6 + deleted after consumption. 7 + 8 + The system bridges the gap between where ideas are captured and where they are 9 + stored long-term. Create an encrypted memo on your phone, and it automatically 10 + appears in your desktop application, decrypted and ready to use. 11 + 12 + ## Architecture 13 + 14 + Cistern is a Deno monorepo consisting of six packages: 15 + 16 + ### `@cistern/crypto` 17 + 18 + Core cryptographic operations using post-quantum algorithms. Implements X-Wing 19 + key encapsulation with XChaCha20-Poly1305 authenticated encryption and SHA3-512 20 + integrity verification. Handles keypair generation, encryption, and decryption. 21 + 22 + ### `@cistern/lexicon` 23 + 24 + AT Protocol schema definitions for Cistern record types. Defines 25 + `app.cistern.pubkey` (public key records) and `app.cistern.memo` (encrypted memo 26 + records). Includes code generation from JSON schemas. 27 + 28 + ### `@cistern/shared` 29 + 30 + Common utilities and authentication logic. Handles DID resolution via Slingshot 31 + service and creates authenticated RPC clients using app passwords. 32 + 33 + ### `@cistern/producer` 34 + 35 + Creates and encrypts memos for storage. Manages public key selection, encrypts 36 + plaintext content, and uploads encrypted memos to the PDS as AT Protocol 37 + records. 38 + 39 + ### `@cistern/consumer` 40 + 41 + Retrieves, decrypts, and deletes memos. Generates keypairs, manages private keys 42 + locally, retrieves memos via polling or real-time streaming (Jetstream), and 43 + handles memo deletion after consumption. 44 + 45 + ### `@cistern/mcp` 46 47 + Model Context Protocol server that exposes Cistern as MCP tools for AI 48 + assistants. Supports stdio transport for local integrations (Claude Desktop) and 49 + HTTP transport for remote deployments. Automatically generates and persists 50 + keypairs in Deno KV. 51 + 52 + ## Security Model 53 + 54 + Private keys never leave the consumer device. Public keys are stored in the PDS 55 + as records, while private keys remain off-protocol. Only the holder of the 56 + matching private key can decrypt memos encrypted with the corresponding public 57 + key. 58 + 59 + ## Quick Start 60 + 61 + ### Using the MCP Server with Claude Desktop 62 + 63 + 1. Generate an [app password](https://bsky.app/settings/app-passwords) for your Bluesky account 64 + 2. Add to Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`): 65 + 66 + ```json 67 + { 68 + "mcpServers": { 69 + "cistern": { 70 + "command": "deno", 71 + "args": ["task", "--cwd", "/path/to/cistern/packages/mcp", "stdio"], 72 + "env": { 73 + "CISTERN_MCP_HANDLE": "yourname.bsky.social", 74 + "CISTERN_MCP_APP_PASSWORD": "xxxx-xxxx-xxxx-xxxx" 75 + } 76 + } 77 + } 78 + } 79 + ``` 80 + 81 + 3. Restart Claude Desktop 82 + 4. Create memos using the Cistern Producer from any device 83 + 5. Ask Claude to retrieve and process your memos 84 + 85 + See [`packages/mcp/README.md`](./packages/mcp/README.md) for detailed usage. 86 + 87 + ## Testing 88 + 89 + Run all unit tests: 90 + 91 + ```bash 92 + deno test --allow-env 93 + ``` 94 + 95 + Run end-to-end tests (requires AT Protocol credentials): 96 + 97 + ```bash 98 + CISTERN_HANDLE="your.bsky.social" \ 99 + CISTERN_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" \ 100 + deno test --allow-env --allow-net e2e.test.ts 101 + ```
+1
deno.jsonc
··· 2 "workspace": [ 3 "packages/*" 4 ], 5 "imports": { 6 "@std/expect": "jsr:@std/expect@^1.0.17", 7 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2"
··· 2 "workspace": [ 3 "packages/*" 4 ], 5 + "unstable": ["kv"], 6 "imports": { 7 "@std/expect": "jsr:@std/expect@^1.0.17", 8 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2"
+1502 -13
deno.lock
··· 1 { 2 "version": "5", 3 "specifiers": { 4 "jsr:@noble/ciphers@^2.0.1": "2.0.1", 5 "jsr:@noble/curves@2.0": "2.0.1", 6 "jsr:@noble/hashes@2": "2.0.1", 7 "jsr:@noble/hashes@2.0": "2.0.1", 8 "jsr:@noble/hashes@^2.0.1": "2.0.1", 9 "jsr:@noble/post-quantum@~0.5.2": "0.5.2", 10 - "jsr:@puregarlic/randimal@^1.0.1": "1.0.1", 11 - "jsr:@std/assert@^1.0.14": "1.0.14", 12 "jsr:@std/expect@^1.0.17": "1.0.17", 13 - "jsr:@std/internal@^1.0.10": "1.0.10", 14 "npm:@atcute/atproto@^3.1.9": "3.1.9", 15 "npm:@atcute/client@^4.0.5": "4.0.5", 16 "npm:@atcute/jetstream@^1.1.2": "1.1.2", 17 "npm:@atcute/lex-cli@^2.3.1": "2.3.1", 18 "npm:@atcute/lexicons@^1.2.2": "1.2.2", 19 "npm:@atcute/tid@^1.0.3": "1.0.3", 20 - "npm:@atproto/lexicon@~0.5.1": "0.5.1" 21 }, 22 "jsr": { 23 "@noble/ciphers@2.0.1": { 24 "integrity": "1d28df773a29684c85844d27eefbb7cad3e4ce62849b63dae3024baf66cf769f" 25 }, ··· 39 "jsr:@noble/hashes@2.0" 40 ] 41 }, 42 - "@puregarlic/randimal@1.0.1": { 43 - "integrity": "101da9e89561f4b8038426f47f6cb258484a11887e1f4d8a2b07cd5ebf487a5d" 44 }, 45 - "@std/assert@1.0.14": { 46 - "integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4", 47 "dependencies": [ 48 - "jsr:@std/internal" 49 ] 50 }, 51 "@std/expect@1.0.17": { 52 "integrity": "316b47dd65c33e3151344eb3267bf42efba17d1415425f07ed96185d67fc04d9", 53 "dependencies": [ 54 "jsr:@std/assert", 55 - "jsr:@std/internal" 56 ] 57 }, 58 "@std/internal@1.0.10": { 59 "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" 60 } 61 }, 62 "npm": { ··· 145 "@badrap/valita@0.4.6": { 146 "integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==" 147 }, 148 "@mary-ext/event-iterator@1.0.0": { 149 "integrity": "sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ==", 150 "dependencies": [ ··· 154 "@mary-ext/simple-event-emitter@1.0.0": { 155 "integrity": "sha512-meA/zJZKIN1RVBNEYIbjufkUrW7/tRjHH60FjolpG1ixJKo76TB208qefQLNdOVDA7uIG0CGEDuhmMirtHKLAg==" 156 }, 157 "@optique/core@0.6.2": { 158 "integrity": "sha512-HTxIHJ8xLOSZotiU6Zc5BCJv+SJ8DMYmuiQM+7tjF7RolJn/pdZNe7M78G3+DgXL9lIf82l8aGcilmgVYRQnGQ==" 159 }, ··· 163 "@optique/core" 164 ] 165 }, 166 "@standard-schema/spec@1.0.0": { 167 "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" 168 }, 169 "esm-env@1.2.2": { 170 "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" 171 }, 172 "event-target-polyfill@0.0.4": { 173 "integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==" 174 }, 175 "graphemer@1.4.0": { 176 "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" 177 }, 178 "iso-datestring-validator@2.2.2": { 179 "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" 180 }, 181 "multiformats@9.9.0": { 182 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" 183 }, 184 "partysocket@1.1.6": { 185 "integrity": "sha512-LkEk8N9hMDDsDT0iDK0zuwUDFVrVMUXFXCeN3850Ng8wtjPqPBeJlwdeY6ROlJSEh3tPoTTasXoSBYH76y118w==", 186 "dependencies": [ 187 "event-target-polyfill" 188 ] 189 }, 190 "picocolors@1.1.1": { 191 "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" 192 }, 193 "prettier@3.6.2": { 194 "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", 195 "bin": true 196 }, 197 "type-fest@4.41.0": { 198 "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==" 199 }, 200 "uint8arrays@3.0.0": { 201 "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 202 "dependencies": [ 203 "multiformats" 204 ] 205 }, 206 "yocto-queue@1.2.1": { 207 "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==" 208 }, 209 "zod@3.25.76": { 210 "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" 211 } ··· 218 "members": { 219 "packages/consumer": { 220 "dependencies": [ 221 - "jsr:@puregarlic/randimal@^1.0.1", 222 "jsr:@std/expect@^1.0.17", 223 - "npm:@atcute/atproto@^3.1.9", 224 "npm:@atcute/client@^4.0.5", 225 "npm:@atcute/jetstream@^1.1.2", 226 "npm:@atcute/lexicons@^1.2.2", ··· 242 "npm:@atproto/lexicon@~0.5.1" 243 ] 244 }, 245 "packages/producer": { 246 "dependencies": [ 247 "jsr:@std/expect@^1.0.17", 248 - "npm:@atcute/atproto@^3.1.9", 249 "npm:@atcute/client@^4.0.5", 250 "npm:@atcute/lexicons@^1.2.2", 251 "npm:@atcute/tid@^1.0.3" ··· 253 }, 254 "packages/shared": { 255 "dependencies": [ 256 "npm:@atcute/client@^4.0.5", 257 "npm:@atcute/lexicons@^1.2.2" 258 ]
··· 1 { 2 "version": "5", 3 "specifiers": { 4 + "jsr:@hono/hono@^4.10.5": "4.10.5", 5 + "jsr:@logtape/logtape@^1.2.0": "1.2.0", 6 "jsr:@noble/ciphers@^2.0.1": "2.0.1", 7 "jsr:@noble/curves@2.0": "2.0.1", 8 "jsr:@noble/hashes@2": "2.0.1", 9 "jsr:@noble/hashes@2.0": "2.0.1", 10 "jsr:@noble/hashes@^2.0.1": "2.0.1", 11 "jsr:@noble/post-quantum@~0.5.2": "0.5.2", 12 + "jsr:@puregarlic/randimal@^1.1.1": "1.1.1", 13 + "jsr:@std/assert@^1.0.14": "1.0.15", 14 + "jsr:@std/cli@^1.0.23": "1.0.23", 15 "jsr:@std/expect@^1.0.17": "1.0.17", 16 + "jsr:@std/internal@^1.0.10": "1.0.12", 17 + "jsr:@std/internal@^1.0.12": "1.0.12", 18 "npm:@atcute/atproto@^3.1.9": "3.1.9", 19 "npm:@atcute/client@^4.0.5": "4.0.5", 20 "npm:@atcute/jetstream@^1.1.2": "1.1.2", 21 + "npm:@atcute/lex-cli@*": "2.3.1", 22 "npm:@atcute/lex-cli@^2.3.1": "2.3.1", 23 "npm:@atcute/lexicons@^1.2.2": "1.2.2", 24 "npm:@atcute/tid@^1.0.3": "1.0.3", 25 + "npm:@atproto/lexicon@~0.5.1": "0.5.1", 26 + "npm:@modelcontextprotocol/inspector@*": "0.15.0_@types+node@24.2.0", 27 + "npm:@modelcontextprotocol/sdk@^1.21.1": "1.21.1_ajv@8.17.1_express@5.1.0_zod@3.25.76", 28 + "npm:@types/node@*": "24.2.0", 29 + "npm:fetch-to-node@^2.1.0": "2.1.0", 30 + "npm:zod@^3.25.76": "3.25.76" 31 }, 32 "jsr": { 33 + "@hono/hono@4.10.5": { 34 + "integrity": "13dbf2a528feb8189ad13394b213f0cf5f83b0ba4b2fadd0549993426db9ad2d" 35 + }, 36 + "@logtape/logtape@1.2.0": { 37 + "integrity": "8e1d3af5c91966cc5689cfb17081a36bccfdff28ff6314769185661f5147e74d" 38 + }, 39 "@noble/ciphers@2.0.1": { 40 "integrity": "1d28df773a29684c85844d27eefbb7cad3e4ce62849b63dae3024baf66cf769f" 41 }, ··· 55 "jsr:@noble/hashes@2.0" 56 ] 57 }, 58 + "@puregarlic/randimal@1.1.1": { 59 + "integrity": "4e1fa61982cf2f610e9ad851d0fd0ff7bc3bb7b7a3c6cccae59f5ae2e68a7e47" 60 }, 61 + "@std/assert@1.0.15": { 62 + "integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b", 63 "dependencies": [ 64 + "jsr:@std/internal@^1.0.12" 65 + ] 66 + }, 67 + "@std/cli@1.0.23": { 68 + "integrity": "bf95b7a9425ba2af1ae5a6359daf58c508f2decf711a76ed2993cd352498ccca", 69 + "dependencies": [ 70 + "jsr:@std/internal@^1.0.12" 71 ] 72 }, 73 "@std/expect@1.0.17": { 74 "integrity": "316b47dd65c33e3151344eb3267bf42efba17d1415425f07ed96185d67fc04d9", 75 "dependencies": [ 76 "jsr:@std/assert", 77 + "jsr:@std/internal@^1.0.10" 78 ] 79 }, 80 "@std/internal@1.0.10": { 81 "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" 82 + }, 83 + "@std/internal@1.0.12": { 84 + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 85 } 86 }, 87 "npm": { ··· 170 "@badrap/valita@0.4.6": { 171 "integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==" 172 }, 173 + "@cspotcode/source-map-support@0.8.1": { 174 + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 175 + "dependencies": [ 176 + "@jridgewell/trace-mapping" 177 + ] 178 + }, 179 + "@floating-ui/core@1.7.2": { 180 + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", 181 + "dependencies": [ 182 + "@floating-ui/utils" 183 + ] 184 + }, 185 + "@floating-ui/dom@1.7.2": { 186 + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", 187 + "dependencies": [ 188 + "@floating-ui/core", 189 + "@floating-ui/utils" 190 + ] 191 + }, 192 + "@floating-ui/react-dom@2.1.4_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 193 + "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", 194 + "dependencies": [ 195 + "@floating-ui/dom", 196 + "react", 197 + "react-dom" 198 + ] 199 + }, 200 + "@floating-ui/utils@0.2.10": { 201 + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" 202 + }, 203 + "@jridgewell/resolve-uri@3.1.2": { 204 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" 205 + }, 206 + "@jridgewell/sourcemap-codec@1.5.4": { 207 + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" 208 + }, 209 + "@jridgewell/trace-mapping@0.3.9": { 210 + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 211 + "dependencies": [ 212 + "@jridgewell/resolve-uri", 213 + "@jridgewell/sourcemap-codec" 214 + ] 215 + }, 216 "@mary-ext/event-iterator@1.0.0": { 217 "integrity": "sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ==", 218 "dependencies": [ ··· 222 "@mary-ext/simple-event-emitter@1.0.0": { 223 "integrity": "sha512-meA/zJZKIN1RVBNEYIbjufkUrW7/tRjHH60FjolpG1ixJKo76TB208qefQLNdOVDA7uIG0CGEDuhmMirtHKLAg==" 224 }, 225 + "@modelcontextprotocol/inspector-cli@0.15.0": { 226 + "integrity": "sha512-mZxRqxYub6qFi3oypLI63yCm9TAxlTO8asE9FeAU4+HFlvKxQrujcfpckcWjqGKhZ0uVH1YUE+VwDx70nz+I5w==", 227 + "dependencies": [ 228 + "@modelcontextprotocol/sdk", 229 + "commander", 230 + "spawn-rx" 231 + ], 232 + "bin": true 233 + }, 234 + "@modelcontextprotocol/inspector-client@0.15.0_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 235 + "integrity": "sha512-zIKxvp5HX1yE+kPOhI42/TVNuM9/RYEizdVmlpov7H38Mg9DeN9DptHYrsVLy8ZEJD1XFAu/eLl+ZtS3ceANNg==", 236 + "dependencies": [ 237 + "@modelcontextprotocol/sdk", 238 + "@radix-ui/react-checkbox", 239 + "@radix-ui/react-dialog", 240 + "@radix-ui/react-icons", 241 + "@radix-ui/react-label", 242 + "@radix-ui/react-popover", 243 + "@radix-ui/react-select", 244 + "@radix-ui/react-slot", 245 + "@radix-ui/react-tabs", 246 + "@radix-ui/react-toast", 247 + "@radix-ui/react-tooltip", 248 + "ajv@6.12.6", 249 + "class-variance-authority", 250 + "clsx", 251 + "cmdk", 252 + "lucide-react", 253 + "pkce-challenge@4.1.0", 254 + "prismjs", 255 + "react", 256 + "react-dom", 257 + "react-simple-code-editor", 258 + "serve-handler", 259 + "tailwind-merge", 260 + "tailwindcss-animate", 261 + "zod" 262 + ], 263 + "bin": true 264 + }, 265 + "@modelcontextprotocol/inspector-server@0.15.0": { 266 + "integrity": "sha512-x1qtDEUeSHURtBH1/WN30NX7O/Imb3u2IoY+T2YCf4mGiB24eo4hEudiZmnuKSDGwDs4BAj2keiFeL3/EwkH9w==", 267 + "dependencies": [ 268 + "@modelcontextprotocol/sdk", 269 + "cors", 270 + "express", 271 + "ws", 272 + "zod" 273 + ], 274 + "bin": true 275 + }, 276 + "@modelcontextprotocol/inspector@0.15.0_@types+node@24.2.0": { 277 + "integrity": "sha512-PN1R7InR48Y6wU8s/vHWc0KOYAjlYQkgCpjUQsNFB078ebdv+empkMI6d1Gg+UIRx8mTrwtbBgv0A6ookGG+0w==", 278 + "dependencies": [ 279 + "@modelcontextprotocol/inspector-cli", 280 + "@modelcontextprotocol/inspector-client", 281 + "@modelcontextprotocol/inspector-server", 282 + "@modelcontextprotocol/sdk", 283 + "concurrently", 284 + "open", 285 + "shell-quote", 286 + "spawn-rx", 287 + "ts-node", 288 + "zod" 289 + ], 290 + "bin": true 291 + }, 292 + "@modelcontextprotocol/sdk@1.21.1_ajv@8.17.1_express@5.1.0_zod@3.25.76": { 293 + "integrity": "sha512-UyLFcJLDvUuZbGnaQqXFT32CpPpGj7VS19roLut6gkQVhb439xUzYWbsUvdI3ZPL+2hnFosuugtYWE0Mcs1rmQ==", 294 + "dependencies": [ 295 + "ajv@8.17.1", 296 + "ajv-formats", 297 + "content-type", 298 + "cors", 299 + "cross-spawn", 300 + "eventsource", 301 + "eventsource-parser", 302 + "express", 303 + "express-rate-limit", 304 + "pkce-challenge@5.0.0", 305 + "raw-body", 306 + "zod", 307 + "zod-to-json-schema" 308 + ] 309 + }, 310 "@optique/core@0.6.2": { 311 "integrity": "sha512-HTxIHJ8xLOSZotiU6Zc5BCJv+SJ8DMYmuiQM+7tjF7RolJn/pdZNe7M78G3+DgXL9lIf82l8aGcilmgVYRQnGQ==" 312 }, ··· 316 "@optique/core" 317 ] 318 }, 319 + "@radix-ui/number@1.1.1": { 320 + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==" 321 + }, 322 + "@radix-ui/primitive@1.1.2": { 323 + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==" 324 + }, 325 + "@radix-ui/react-arrow@1.1.7_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 326 + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", 327 + "dependencies": [ 328 + "@radix-ui/react-primitive", 329 + "react", 330 + "react-dom" 331 + ] 332 + }, 333 + "@radix-ui/react-checkbox@1.3.2_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 334 + "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", 335 + "dependencies": [ 336 + "@radix-ui/primitive", 337 + "@radix-ui/react-compose-refs", 338 + "@radix-ui/react-context", 339 + "@radix-ui/react-presence", 340 + "@radix-ui/react-primitive", 341 + "@radix-ui/react-use-controllable-state", 342 + "@radix-ui/react-use-previous", 343 + "@radix-ui/react-use-size", 344 + "react", 345 + "react-dom" 346 + ] 347 + }, 348 + "@radix-ui/react-collection@1.1.7_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 349 + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", 350 + "dependencies": [ 351 + "@radix-ui/react-compose-refs", 352 + "@radix-ui/react-context", 353 + "@radix-ui/react-primitive", 354 + "@radix-ui/react-slot", 355 + "react", 356 + "react-dom" 357 + ] 358 + }, 359 + "@radix-ui/react-compose-refs@1.1.2_react@18.3.1": { 360 + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", 361 + "dependencies": [ 362 + "react" 363 + ] 364 + }, 365 + "@radix-ui/react-context@1.1.2_react@18.3.1": { 366 + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", 367 + "dependencies": [ 368 + "react" 369 + ] 370 + }, 371 + "@radix-ui/react-dialog@1.1.14_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 372 + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", 373 + "dependencies": [ 374 + "@radix-ui/primitive", 375 + "@radix-ui/react-compose-refs", 376 + "@radix-ui/react-context", 377 + "@radix-ui/react-dismissable-layer", 378 + "@radix-ui/react-focus-guards", 379 + "@radix-ui/react-focus-scope", 380 + "@radix-ui/react-id", 381 + "@radix-ui/react-portal", 382 + "@radix-ui/react-presence", 383 + "@radix-ui/react-primitive", 384 + "@radix-ui/react-slot", 385 + "@radix-ui/react-use-controllable-state", 386 + "aria-hidden", 387 + "react", 388 + "react-dom", 389 + "react-remove-scroll" 390 + ] 391 + }, 392 + "@radix-ui/react-direction@1.1.1_react@18.3.1": { 393 + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", 394 + "dependencies": [ 395 + "react" 396 + ] 397 + }, 398 + "@radix-ui/react-dismissable-layer@1.1.10_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 399 + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", 400 + "dependencies": [ 401 + "@radix-ui/primitive", 402 + "@radix-ui/react-compose-refs", 403 + "@radix-ui/react-primitive", 404 + "@radix-ui/react-use-callback-ref", 405 + "@radix-ui/react-use-escape-keydown", 406 + "react", 407 + "react-dom" 408 + ] 409 + }, 410 + "@radix-ui/react-focus-guards@1.1.2_react@18.3.1": { 411 + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", 412 + "dependencies": [ 413 + "react" 414 + ] 415 + }, 416 + "@radix-ui/react-focus-scope@1.1.7_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 417 + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", 418 + "dependencies": [ 419 + "@radix-ui/react-compose-refs", 420 + "@radix-ui/react-primitive", 421 + "@radix-ui/react-use-callback-ref", 422 + "react", 423 + "react-dom" 424 + ] 425 + }, 426 + "@radix-ui/react-icons@1.3.2_react@18.3.1": { 427 + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", 428 + "dependencies": [ 429 + "react" 430 + ] 431 + }, 432 + "@radix-ui/react-id@1.1.1_react@18.3.1": { 433 + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", 434 + "dependencies": [ 435 + "@radix-ui/react-use-layout-effect", 436 + "react" 437 + ] 438 + }, 439 + "@radix-ui/react-label@2.1.7_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 440 + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", 441 + "dependencies": [ 442 + "@radix-ui/react-primitive", 443 + "react", 444 + "react-dom" 445 + ] 446 + }, 447 + "@radix-ui/react-popover@1.1.14_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 448 + "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", 449 + "dependencies": [ 450 + "@radix-ui/primitive", 451 + "@radix-ui/react-compose-refs", 452 + "@radix-ui/react-context", 453 + "@radix-ui/react-dismissable-layer", 454 + "@radix-ui/react-focus-guards", 455 + "@radix-ui/react-focus-scope", 456 + "@radix-ui/react-id", 457 + "@radix-ui/react-popper", 458 + "@radix-ui/react-portal", 459 + "@radix-ui/react-presence", 460 + "@radix-ui/react-primitive", 461 + "@radix-ui/react-slot", 462 + "@radix-ui/react-use-controllable-state", 463 + "aria-hidden", 464 + "react", 465 + "react-dom", 466 + "react-remove-scroll" 467 + ] 468 + }, 469 + "@radix-ui/react-popper@1.2.7_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 470 + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", 471 + "dependencies": [ 472 + "@floating-ui/react-dom", 473 + "@radix-ui/react-arrow", 474 + "@radix-ui/react-compose-refs", 475 + "@radix-ui/react-context", 476 + "@radix-ui/react-primitive", 477 + "@radix-ui/react-use-callback-ref", 478 + "@radix-ui/react-use-layout-effect", 479 + "@radix-ui/react-use-rect", 480 + "@radix-ui/react-use-size", 481 + "@radix-ui/rect", 482 + "react", 483 + "react-dom" 484 + ] 485 + }, 486 + "@radix-ui/react-portal@1.1.9_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 487 + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", 488 + "dependencies": [ 489 + "@radix-ui/react-primitive", 490 + "@radix-ui/react-use-layout-effect", 491 + "react", 492 + "react-dom" 493 + ] 494 + }, 495 + "@radix-ui/react-presence@1.1.4_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 496 + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", 497 + "dependencies": [ 498 + "@radix-ui/react-compose-refs", 499 + "@radix-ui/react-use-layout-effect", 500 + "react", 501 + "react-dom" 502 + ] 503 + }, 504 + "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 505 + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", 506 + "dependencies": [ 507 + "@radix-ui/react-slot", 508 + "react", 509 + "react-dom" 510 + ] 511 + }, 512 + "@radix-ui/react-roving-focus@1.1.10_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 513 + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", 514 + "dependencies": [ 515 + "@radix-ui/primitive", 516 + "@radix-ui/react-collection", 517 + "@radix-ui/react-compose-refs", 518 + "@radix-ui/react-context", 519 + "@radix-ui/react-direction", 520 + "@radix-ui/react-id", 521 + "@radix-ui/react-primitive", 522 + "@radix-ui/react-use-callback-ref", 523 + "@radix-ui/react-use-controllable-state", 524 + "react", 525 + "react-dom" 526 + ] 527 + }, 528 + "@radix-ui/react-select@2.2.5_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 529 + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", 530 + "dependencies": [ 531 + "@radix-ui/number", 532 + "@radix-ui/primitive", 533 + "@radix-ui/react-collection", 534 + "@radix-ui/react-compose-refs", 535 + "@radix-ui/react-context", 536 + "@radix-ui/react-direction", 537 + "@radix-ui/react-dismissable-layer", 538 + "@radix-ui/react-focus-guards", 539 + "@radix-ui/react-focus-scope", 540 + "@radix-ui/react-id", 541 + "@radix-ui/react-popper", 542 + "@radix-ui/react-portal", 543 + "@radix-ui/react-primitive", 544 + "@radix-ui/react-slot", 545 + "@radix-ui/react-use-callback-ref", 546 + "@radix-ui/react-use-controllable-state", 547 + "@radix-ui/react-use-layout-effect", 548 + "@radix-ui/react-use-previous", 549 + "@radix-ui/react-visually-hidden", 550 + "aria-hidden", 551 + "react", 552 + "react-dom", 553 + "react-remove-scroll" 554 + ] 555 + }, 556 + "@radix-ui/react-slot@1.2.3_react@18.3.1": { 557 + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", 558 + "dependencies": [ 559 + "@radix-ui/react-compose-refs", 560 + "react" 561 + ] 562 + }, 563 + "@radix-ui/react-tabs@1.1.12_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 564 + "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", 565 + "dependencies": [ 566 + "@radix-ui/primitive", 567 + "@radix-ui/react-context", 568 + "@radix-ui/react-direction", 569 + "@radix-ui/react-id", 570 + "@radix-ui/react-presence", 571 + "@radix-ui/react-primitive", 572 + "@radix-ui/react-roving-focus", 573 + "@radix-ui/react-use-controllable-state", 574 + "react", 575 + "react-dom" 576 + ] 577 + }, 578 + "@radix-ui/react-toast@1.2.14_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 579 + "integrity": "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==", 580 + "dependencies": [ 581 + "@radix-ui/primitive", 582 + "@radix-ui/react-collection", 583 + "@radix-ui/react-compose-refs", 584 + "@radix-ui/react-context", 585 + "@radix-ui/react-dismissable-layer", 586 + "@radix-ui/react-portal", 587 + "@radix-ui/react-presence", 588 + "@radix-ui/react-primitive", 589 + "@radix-ui/react-use-callback-ref", 590 + "@radix-ui/react-use-controllable-state", 591 + "@radix-ui/react-use-layout-effect", 592 + "@radix-ui/react-visually-hidden", 593 + "react", 594 + "react-dom" 595 + ] 596 + }, 597 + "@radix-ui/react-tooltip@1.2.7_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 598 + "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", 599 + "dependencies": [ 600 + "@radix-ui/primitive", 601 + "@radix-ui/react-compose-refs", 602 + "@radix-ui/react-context", 603 + "@radix-ui/react-dismissable-layer", 604 + "@radix-ui/react-id", 605 + "@radix-ui/react-popper", 606 + "@radix-ui/react-portal", 607 + "@radix-ui/react-presence", 608 + "@radix-ui/react-primitive", 609 + "@radix-ui/react-slot", 610 + "@radix-ui/react-use-controllable-state", 611 + "@radix-ui/react-visually-hidden", 612 + "react", 613 + "react-dom" 614 + ] 615 + }, 616 + "@radix-ui/react-use-callback-ref@1.1.1_react@18.3.1": { 617 + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", 618 + "dependencies": [ 619 + "react" 620 + ] 621 + }, 622 + "@radix-ui/react-use-controllable-state@1.2.2_react@18.3.1": { 623 + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", 624 + "dependencies": [ 625 + "@radix-ui/react-use-effect-event", 626 + "@radix-ui/react-use-layout-effect", 627 + "react" 628 + ] 629 + }, 630 + "@radix-ui/react-use-effect-event@0.0.2_react@18.3.1": { 631 + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", 632 + "dependencies": [ 633 + "@radix-ui/react-use-layout-effect", 634 + "react" 635 + ] 636 + }, 637 + "@radix-ui/react-use-escape-keydown@1.1.1_react@18.3.1": { 638 + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", 639 + "dependencies": [ 640 + "@radix-ui/react-use-callback-ref", 641 + "react" 642 + ] 643 + }, 644 + "@radix-ui/react-use-layout-effect@1.1.1_react@18.3.1": { 645 + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", 646 + "dependencies": [ 647 + "react" 648 + ] 649 + }, 650 + "@radix-ui/react-use-previous@1.1.1_react@18.3.1": { 651 + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", 652 + "dependencies": [ 653 + "react" 654 + ] 655 + }, 656 + "@radix-ui/react-use-rect@1.1.1_react@18.3.1": { 657 + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", 658 + "dependencies": [ 659 + "@radix-ui/rect", 660 + "react" 661 + ] 662 + }, 663 + "@radix-ui/react-use-size@1.1.1_react@18.3.1": { 664 + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", 665 + "dependencies": [ 666 + "@radix-ui/react-use-layout-effect", 667 + "react" 668 + ] 669 + }, 670 + "@radix-ui/react-visually-hidden@1.2.3_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 671 + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", 672 + "dependencies": [ 673 + "@radix-ui/react-primitive", 674 + "react", 675 + "react-dom" 676 + ] 677 + }, 678 + "@radix-ui/rect@1.1.1": { 679 + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" 680 + }, 681 "@standard-schema/spec@1.0.0": { 682 "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" 683 }, 684 + "@tsconfig/node10@1.0.11": { 685 + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" 686 + }, 687 + "@tsconfig/node12@1.0.11": { 688 + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" 689 + }, 690 + "@tsconfig/node14@1.0.3": { 691 + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" 692 + }, 693 + "@tsconfig/node16@1.0.4": { 694 + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" 695 + }, 696 + "@types/node@24.2.0": { 697 + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", 698 + "dependencies": [ 699 + "undici-types" 700 + ] 701 + }, 702 + "accepts@2.0.0": { 703 + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 704 + "dependencies": [ 705 + "mime-types@3.0.1", 706 + "negotiator" 707 + ] 708 + }, 709 + "acorn-walk@8.3.4": { 710 + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", 711 + "dependencies": [ 712 + "acorn" 713 + ] 714 + }, 715 + "acorn@8.15.0": { 716 + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 717 + "bin": true 718 + }, 719 + "ajv-formats@3.0.1_ajv@8.17.1": { 720 + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", 721 + "dependencies": [ 722 + "ajv@8.17.1" 723 + ], 724 + "optionalPeers": [ 725 + "ajv@8.17.1" 726 + ] 727 + }, 728 + "ajv@6.12.6": { 729 + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 730 + "dependencies": [ 731 + "fast-deep-equal", 732 + "fast-json-stable-stringify", 733 + "json-schema-traverse@0.4.1", 734 + "uri-js" 735 + ] 736 + }, 737 + "ajv@8.17.1": { 738 + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", 739 + "dependencies": [ 740 + "fast-deep-equal", 741 + "fast-uri", 742 + "json-schema-traverse@1.0.0", 743 + "require-from-string" 744 + ] 745 + }, 746 + "ansi-regex@5.0.1": { 747 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 748 + }, 749 + "ansi-styles@4.3.0": { 750 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 751 + "dependencies": [ 752 + "color-convert" 753 + ] 754 + }, 755 + "arg@4.1.3": { 756 + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" 757 + }, 758 + "aria-hidden@1.2.6": { 759 + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", 760 + "dependencies": [ 761 + "tslib" 762 + ] 763 + }, 764 + "balanced-match@1.0.2": { 765 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 766 + }, 767 + "body-parser@2.2.0": { 768 + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", 769 + "dependencies": [ 770 + "bytes@3.1.2", 771 + "content-type", 772 + "debug", 773 + "http-errors", 774 + "iconv-lite@0.6.3", 775 + "on-finished", 776 + "qs", 777 + "raw-body", 778 + "type-is" 779 + ] 780 + }, 781 + "brace-expansion@1.1.12": { 782 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 783 + "dependencies": [ 784 + "balanced-match", 785 + "concat-map" 786 + ] 787 + }, 788 + "bundle-name@4.1.0": { 789 + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", 790 + "dependencies": [ 791 + "run-applescript" 792 + ] 793 + }, 794 + "bytes@3.0.0": { 795 + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" 796 + }, 797 + "bytes@3.1.2": { 798 + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" 799 + }, 800 + "call-bind-apply-helpers@1.0.2": { 801 + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 802 + "dependencies": [ 803 + "es-errors", 804 + "function-bind" 805 + ] 806 + }, 807 + "call-bound@1.0.4": { 808 + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 809 + "dependencies": [ 810 + "call-bind-apply-helpers", 811 + "get-intrinsic" 812 + ] 813 + }, 814 + "chalk@4.1.2": { 815 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 816 + "dependencies": [ 817 + "ansi-styles", 818 + "supports-color@7.2.0" 819 + ] 820 + }, 821 + "class-variance-authority@0.7.1": { 822 + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", 823 + "dependencies": [ 824 + "clsx" 825 + ] 826 + }, 827 + "cliui@8.0.1": { 828 + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 829 + "dependencies": [ 830 + "string-width", 831 + "strip-ansi", 832 + "wrap-ansi" 833 + ] 834 + }, 835 + "clsx@2.1.1": { 836 + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" 837 + }, 838 + "cmdk@1.1.1_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 839 + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", 840 + "dependencies": [ 841 + "@radix-ui/react-compose-refs", 842 + "@radix-ui/react-dialog", 843 + "@radix-ui/react-id", 844 + "@radix-ui/react-primitive", 845 + "react", 846 + "react-dom" 847 + ] 848 + }, 849 + "color-convert@2.0.1": { 850 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 851 + "dependencies": [ 852 + "color-name" 853 + ] 854 + }, 855 + "color-name@1.1.4": { 856 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 857 + }, 858 + "commander@13.1.0": { 859 + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==" 860 + }, 861 + "concat-map@0.0.1": { 862 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 863 + }, 864 + "concurrently@9.2.0": { 865 + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", 866 + "dependencies": [ 867 + "chalk", 868 + "lodash", 869 + "rxjs", 870 + "shell-quote", 871 + "supports-color@8.1.1", 872 + "tree-kill", 873 + "yargs" 874 + ], 875 + "bin": true 876 + }, 877 + "content-disposition@0.5.2": { 878 + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==" 879 + }, 880 + "content-disposition@1.0.0": { 881 + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 882 + "dependencies": [ 883 + "safe-buffer" 884 + ] 885 + }, 886 + "content-type@1.0.5": { 887 + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" 888 + }, 889 + "cookie-signature@1.2.2": { 890 + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" 891 + }, 892 + "cookie@0.7.2": { 893 + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" 894 + }, 895 + "cors@2.8.5": { 896 + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 897 + "dependencies": [ 898 + "object-assign", 899 + "vary" 900 + ] 901 + }, 902 + "create-require@1.1.1": { 903 + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" 904 + }, 905 + "cross-spawn@7.0.6": { 906 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 907 + "dependencies": [ 908 + "path-key", 909 + "shebang-command", 910 + "which" 911 + ] 912 + }, 913 + "debug@4.4.3": { 914 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 915 + "dependencies": [ 916 + "ms" 917 + ] 918 + }, 919 + "default-browser-id@5.0.0": { 920 + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==" 921 + }, 922 + "default-browser@5.2.1": { 923 + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", 924 + "dependencies": [ 925 + "bundle-name", 926 + "default-browser-id" 927 + ] 928 + }, 929 + "define-lazy-prop@3.0.0": { 930 + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==" 931 + }, 932 + "depd@2.0.0": { 933 + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 934 + }, 935 + "detect-node-es@1.1.0": { 936 + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" 937 + }, 938 + "diff@4.0.2": { 939 + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" 940 + }, 941 + "dunder-proto@1.0.1": { 942 + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 943 + "dependencies": [ 944 + "call-bind-apply-helpers", 945 + "es-errors", 946 + "gopd" 947 + ] 948 + }, 949 + "ee-first@1.1.1": { 950 + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 951 + }, 952 + "emoji-regex@8.0.0": { 953 + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 954 + }, 955 + "encodeurl@2.0.0": { 956 + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" 957 + }, 958 + "es-define-property@1.0.1": { 959 + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" 960 + }, 961 + "es-errors@1.3.0": { 962 + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" 963 + }, 964 + "es-object-atoms@1.1.1": { 965 + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 966 + "dependencies": [ 967 + "es-errors" 968 + ] 969 + }, 970 + "escalade@3.2.0": { 971 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" 972 + }, 973 + "escape-html@1.0.3": { 974 + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 975 + }, 976 "esm-env@1.2.2": { 977 "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" 978 + }, 979 + "etag@1.8.1": { 980 + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" 981 }, 982 "event-target-polyfill@0.0.4": { 983 "integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==" 984 }, 985 + "eventsource-parser@3.0.6": { 986 + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==" 987 + }, 988 + "eventsource@3.0.7": { 989 + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", 990 + "dependencies": [ 991 + "eventsource-parser" 992 + ] 993 + }, 994 + "express-rate-limit@7.5.1_express@5.1.0": { 995 + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", 996 + "dependencies": [ 997 + "express" 998 + ] 999 + }, 1000 + "express@5.1.0": { 1001 + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", 1002 + "dependencies": [ 1003 + "accepts", 1004 + "body-parser", 1005 + "content-disposition@1.0.0", 1006 + "content-type", 1007 + "cookie", 1008 + "cookie-signature", 1009 + "debug", 1010 + "encodeurl", 1011 + "escape-html", 1012 + "etag", 1013 + "finalhandler", 1014 + "fresh", 1015 + "http-errors", 1016 + "merge-descriptors", 1017 + "mime-types@3.0.1", 1018 + "on-finished", 1019 + "once", 1020 + "parseurl", 1021 + "proxy-addr", 1022 + "qs", 1023 + "range-parser@1.2.1", 1024 + "router", 1025 + "send", 1026 + "serve-static", 1027 + "statuses", 1028 + "type-is", 1029 + "vary" 1030 + ] 1031 + }, 1032 + "fast-deep-equal@3.1.3": { 1033 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 1034 + }, 1035 + "fast-json-stable-stringify@2.1.0": { 1036 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 1037 + }, 1038 + "fast-uri@3.1.0": { 1039 + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==" 1040 + }, 1041 + "fetch-to-node@2.1.0": { 1042 + "integrity": "sha512-Wq05j6LE1GrWpT2t1YbCkyFY6xKRJq3hx/oRJdWEJpZlik3g25MmdJS6RFm49iiMJw6zpZuBOrgihOgy2jGyAA==" 1043 + }, 1044 + "finalhandler@2.1.0": { 1045 + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 1046 + "dependencies": [ 1047 + "debug", 1048 + "encodeurl", 1049 + "escape-html", 1050 + "on-finished", 1051 + "parseurl", 1052 + "statuses" 1053 + ] 1054 + }, 1055 + "forwarded@0.2.0": { 1056 + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" 1057 + }, 1058 + "fresh@2.0.0": { 1059 + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" 1060 + }, 1061 + "function-bind@1.1.2": { 1062 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" 1063 + }, 1064 + "get-caller-file@2.0.5": { 1065 + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" 1066 + }, 1067 + "get-intrinsic@1.3.0": { 1068 + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 1069 + "dependencies": [ 1070 + "call-bind-apply-helpers", 1071 + "es-define-property", 1072 + "es-errors", 1073 + "es-object-atoms", 1074 + "function-bind", 1075 + "get-proto", 1076 + "gopd", 1077 + "has-symbols", 1078 + "hasown", 1079 + "math-intrinsics" 1080 + ] 1081 + }, 1082 + "get-nonce@1.0.1": { 1083 + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" 1084 + }, 1085 + "get-proto@1.0.1": { 1086 + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 1087 + "dependencies": [ 1088 + "dunder-proto", 1089 + "es-object-atoms" 1090 + ] 1091 + }, 1092 + "gopd@1.2.0": { 1093 + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" 1094 + }, 1095 "graphemer@1.4.0": { 1096 "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" 1097 }, 1098 + "has-flag@4.0.0": { 1099 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 1100 + }, 1101 + "has-symbols@1.1.0": { 1102 + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" 1103 + }, 1104 + "hasown@2.0.2": { 1105 + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 1106 + "dependencies": [ 1107 + "function-bind" 1108 + ] 1109 + }, 1110 + "http-errors@2.0.0": { 1111 + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 1112 + "dependencies": [ 1113 + "depd", 1114 + "inherits", 1115 + "setprototypeof", 1116 + "statuses", 1117 + "toidentifier" 1118 + ] 1119 + }, 1120 + "iconv-lite@0.6.3": { 1121 + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 1122 + "dependencies": [ 1123 + "safer-buffer" 1124 + ] 1125 + }, 1126 + "iconv-lite@0.7.0": { 1127 + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", 1128 + "dependencies": [ 1129 + "safer-buffer" 1130 + ] 1131 + }, 1132 + "inherits@2.0.4": { 1133 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1134 + }, 1135 + "ipaddr.js@1.9.1": { 1136 + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 1137 + }, 1138 + "is-docker@3.0.0": { 1139 + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", 1140 + "bin": true 1141 + }, 1142 + "is-fullwidth-code-point@3.0.0": { 1143 + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 1144 + }, 1145 + "is-inside-container@1.0.0": { 1146 + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", 1147 + "dependencies": [ 1148 + "is-docker" 1149 + ], 1150 + "bin": true 1151 + }, 1152 + "is-promise@4.0.0": { 1153 + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" 1154 + }, 1155 + "is-wsl@3.1.0": { 1156 + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", 1157 + "dependencies": [ 1158 + "is-inside-container" 1159 + ] 1160 + }, 1161 + "isexe@2.0.0": { 1162 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 1163 + }, 1164 "iso-datestring-validator@2.2.2": { 1165 "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" 1166 }, 1167 + "js-tokens@4.0.0": { 1168 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 1169 + }, 1170 + "json-schema-traverse@0.4.1": { 1171 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 1172 + }, 1173 + "json-schema-traverse@1.0.0": { 1174 + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" 1175 + }, 1176 + "lodash@4.17.21": { 1177 + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 1178 + }, 1179 + "loose-envify@1.4.0": { 1180 + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 1181 + "dependencies": [ 1182 + "js-tokens" 1183 + ], 1184 + "bin": true 1185 + }, 1186 + "lucide-react@0.523.0_react@18.3.1": { 1187 + "integrity": "sha512-rUjQoy7egZT9XYVXBK1je9ckBnNp7qzRZOhLQx5RcEp2dCGlXo+mv6vf7Am4LimEcFBJIIZzSGfgTqc9QCrPSw==", 1188 + "dependencies": [ 1189 + "react" 1190 + ] 1191 + }, 1192 + "make-error@1.3.6": { 1193 + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" 1194 + }, 1195 + "math-intrinsics@1.1.0": { 1196 + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" 1197 + }, 1198 + "media-typer@1.1.0": { 1199 + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" 1200 + }, 1201 + "merge-descriptors@2.0.0": { 1202 + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" 1203 + }, 1204 + "mime-db@1.33.0": { 1205 + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 1206 + }, 1207 + "mime-db@1.54.0": { 1208 + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" 1209 + }, 1210 + "mime-types@2.1.18": { 1211 + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 1212 + "dependencies": [ 1213 + "mime-db@1.33.0" 1214 + ] 1215 + }, 1216 + "mime-types@3.0.1": { 1217 + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", 1218 + "dependencies": [ 1219 + "mime-db@1.54.0" 1220 + ] 1221 + }, 1222 + "minimatch@3.1.2": { 1223 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1224 + "dependencies": [ 1225 + "brace-expansion" 1226 + ] 1227 + }, 1228 + "ms@2.1.3": { 1229 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1230 + }, 1231 "multiformats@9.9.0": { 1232 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" 1233 }, 1234 + "negotiator@1.0.0": { 1235 + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" 1236 + }, 1237 + "object-assign@4.1.1": { 1238 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" 1239 + }, 1240 + "object-inspect@1.13.4": { 1241 + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" 1242 + }, 1243 + "on-finished@2.4.1": { 1244 + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 1245 + "dependencies": [ 1246 + "ee-first" 1247 + ] 1248 + }, 1249 + "once@1.4.0": { 1250 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1251 + "dependencies": [ 1252 + "wrappy" 1253 + ] 1254 + }, 1255 + "open@10.1.2": { 1256 + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", 1257 + "dependencies": [ 1258 + "default-browser", 1259 + "define-lazy-prop", 1260 + "is-inside-container", 1261 + "is-wsl" 1262 + ] 1263 + }, 1264 + "parseurl@1.3.3": { 1265 + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 1266 + }, 1267 "partysocket@1.1.6": { 1268 "integrity": "sha512-LkEk8N9hMDDsDT0iDK0zuwUDFVrVMUXFXCeN3850Ng8wtjPqPBeJlwdeY6ROlJSEh3tPoTTasXoSBYH76y118w==", 1269 "dependencies": [ 1270 "event-target-polyfill" 1271 ] 1272 }, 1273 + "path-is-inside@1.0.2": { 1274 + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==" 1275 + }, 1276 + "path-key@3.1.1": { 1277 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" 1278 + }, 1279 + "path-to-regexp@3.3.0": { 1280 + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==" 1281 + }, 1282 + "path-to-regexp@8.3.0": { 1283 + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==" 1284 + }, 1285 "picocolors@1.1.1": { 1286 "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" 1287 }, 1288 + "pkce-challenge@4.1.0": { 1289 + "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==" 1290 + }, 1291 + "pkce-challenge@5.0.0": { 1292 + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==" 1293 + }, 1294 "prettier@3.6.2": { 1295 "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", 1296 "bin": true 1297 }, 1298 + "prismjs@1.30.0": { 1299 + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==" 1300 + }, 1301 + "proxy-addr@2.0.7": { 1302 + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1303 + "dependencies": [ 1304 + "forwarded", 1305 + "ipaddr.js" 1306 + ] 1307 + }, 1308 + "punycode@2.3.1": { 1309 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" 1310 + }, 1311 + "qs@6.14.0": { 1312 + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 1313 + "dependencies": [ 1314 + "side-channel" 1315 + ] 1316 + }, 1317 + "range-parser@1.2.0": { 1318 + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==" 1319 + }, 1320 + "range-parser@1.2.1": { 1321 + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 1322 + }, 1323 + "raw-body@3.0.1": { 1324 + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", 1325 + "dependencies": [ 1326 + "bytes@3.1.2", 1327 + "http-errors", 1328 + "iconv-lite@0.7.0", 1329 + "unpipe" 1330 + ] 1331 + }, 1332 + "react-dom@18.3.1_react@18.3.1": { 1333 + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", 1334 + "dependencies": [ 1335 + "loose-envify", 1336 + "react", 1337 + "scheduler" 1338 + ] 1339 + }, 1340 + "react-remove-scroll-bar@2.3.8_react@18.3.1": { 1341 + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", 1342 + "dependencies": [ 1343 + "react", 1344 + "react-style-singleton", 1345 + "tslib" 1346 + ] 1347 + }, 1348 + "react-remove-scroll@2.7.1_react@18.3.1": { 1349 + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", 1350 + "dependencies": [ 1351 + "react", 1352 + "react-remove-scroll-bar", 1353 + "react-style-singleton", 1354 + "tslib", 1355 + "use-callback-ref", 1356 + "use-sidecar" 1357 + ] 1358 + }, 1359 + "react-simple-code-editor@0.14.1_react@18.3.1_react-dom@18.3.1__react@18.3.1": { 1360 + "integrity": "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==", 1361 + "dependencies": [ 1362 + "react", 1363 + "react-dom" 1364 + ] 1365 + }, 1366 + "react-style-singleton@2.2.3_react@18.3.1": { 1367 + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", 1368 + "dependencies": [ 1369 + "get-nonce", 1370 + "react", 1371 + "tslib" 1372 + ] 1373 + }, 1374 + "react@18.3.1": { 1375 + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", 1376 + "dependencies": [ 1377 + "loose-envify" 1378 + ] 1379 + }, 1380 + "require-directory@2.1.1": { 1381 + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" 1382 + }, 1383 + "require-from-string@2.0.2": { 1384 + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" 1385 + }, 1386 + "router@2.2.0": { 1387 + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 1388 + "dependencies": [ 1389 + "debug", 1390 + "depd", 1391 + "is-promise", 1392 + "parseurl", 1393 + "path-to-regexp@8.3.0" 1394 + ] 1395 + }, 1396 + "run-applescript@7.0.0": { 1397 + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==" 1398 + }, 1399 + "rxjs@7.8.2": { 1400 + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", 1401 + "dependencies": [ 1402 + "tslib" 1403 + ] 1404 + }, 1405 + "safe-buffer@5.2.1": { 1406 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 1407 + }, 1408 + "safer-buffer@2.1.2": { 1409 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1410 + }, 1411 + "scheduler@0.23.2": { 1412 + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", 1413 + "dependencies": [ 1414 + "loose-envify" 1415 + ] 1416 + }, 1417 + "send@1.2.0": { 1418 + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", 1419 + "dependencies": [ 1420 + "debug", 1421 + "encodeurl", 1422 + "escape-html", 1423 + "etag", 1424 + "fresh", 1425 + "http-errors", 1426 + "mime-types@3.0.1", 1427 + "ms", 1428 + "on-finished", 1429 + "range-parser@1.2.1", 1430 + "statuses" 1431 + ] 1432 + }, 1433 + "serve-handler@6.1.6": { 1434 + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", 1435 + "dependencies": [ 1436 + "bytes@3.0.0", 1437 + "content-disposition@0.5.2", 1438 + "mime-types@2.1.18", 1439 + "minimatch", 1440 + "path-is-inside", 1441 + "path-to-regexp@3.3.0", 1442 + "range-parser@1.2.0" 1443 + ] 1444 + }, 1445 + "serve-static@2.2.0": { 1446 + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", 1447 + "dependencies": [ 1448 + "encodeurl", 1449 + "escape-html", 1450 + "parseurl", 1451 + "send" 1452 + ] 1453 + }, 1454 + "setprototypeof@1.2.0": { 1455 + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1456 + }, 1457 + "shebang-command@2.0.0": { 1458 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1459 + "dependencies": [ 1460 + "shebang-regex" 1461 + ] 1462 + }, 1463 + "shebang-regex@3.0.0": { 1464 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" 1465 + }, 1466 + "shell-quote@1.8.3": { 1467 + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==" 1468 + }, 1469 + "side-channel-list@1.0.0": { 1470 + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 1471 + "dependencies": [ 1472 + "es-errors", 1473 + "object-inspect" 1474 + ] 1475 + }, 1476 + "side-channel-map@1.0.1": { 1477 + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 1478 + "dependencies": [ 1479 + "call-bound", 1480 + "es-errors", 1481 + "get-intrinsic", 1482 + "object-inspect" 1483 + ] 1484 + }, 1485 + "side-channel-weakmap@1.0.2": { 1486 + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 1487 + "dependencies": [ 1488 + "call-bound", 1489 + "es-errors", 1490 + "get-intrinsic", 1491 + "object-inspect", 1492 + "side-channel-map" 1493 + ] 1494 + }, 1495 + "side-channel@1.1.0": { 1496 + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 1497 + "dependencies": [ 1498 + "es-errors", 1499 + "object-inspect", 1500 + "side-channel-list", 1501 + "side-channel-map", 1502 + "side-channel-weakmap" 1503 + ] 1504 + }, 1505 + "spawn-rx@5.1.2": { 1506 + "integrity": "sha512-/y7tJKALVZ1lPzeZZB9jYnmtrL7d0N2zkorii5a7r7dhHkWIuLTzZpZzMJLK1dmYRgX/NCc4iarTO3F7BS2c/A==", 1507 + "dependencies": [ 1508 + "debug", 1509 + "rxjs" 1510 + ] 1511 + }, 1512 + "statuses@2.0.1": { 1513 + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 1514 + }, 1515 + "string-width@4.2.3": { 1516 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1517 + "dependencies": [ 1518 + "emoji-regex", 1519 + "is-fullwidth-code-point", 1520 + "strip-ansi" 1521 + ] 1522 + }, 1523 + "strip-ansi@6.0.1": { 1524 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1525 + "dependencies": [ 1526 + "ansi-regex" 1527 + ] 1528 + }, 1529 + "supports-color@7.2.0": { 1530 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1531 + "dependencies": [ 1532 + "has-flag" 1533 + ] 1534 + }, 1535 + "supports-color@8.1.1": { 1536 + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1537 + "dependencies": [ 1538 + "has-flag" 1539 + ] 1540 + }, 1541 + "tailwind-merge@2.6.0": { 1542 + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==" 1543 + }, 1544 + "tailwindcss-animate@1.0.7_tailwindcss@4.1.11": { 1545 + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", 1546 + "dependencies": [ 1547 + "tailwindcss" 1548 + ] 1549 + }, 1550 + "tailwindcss@4.1.11": { 1551 + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==" 1552 + }, 1553 + "toidentifier@1.0.1": { 1554 + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 1555 + }, 1556 + "tree-kill@1.2.2": { 1557 + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", 1558 + "bin": true 1559 + }, 1560 + "ts-node@10.9.2_@types+node@24.2.0_typescript@5.8.3": { 1561 + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", 1562 + "dependencies": [ 1563 + "@cspotcode/source-map-support", 1564 + "@tsconfig/node10", 1565 + "@tsconfig/node12", 1566 + "@tsconfig/node14", 1567 + "@tsconfig/node16", 1568 + "@types/node", 1569 + "acorn", 1570 + "acorn-walk", 1571 + "arg", 1572 + "create-require", 1573 + "diff", 1574 + "make-error", 1575 + "typescript", 1576 + "v8-compile-cache-lib", 1577 + "yn" 1578 + ], 1579 + "bin": true 1580 + }, 1581 + "tslib@2.8.1": { 1582 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" 1583 + }, 1584 "type-fest@4.41.0": { 1585 "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==" 1586 }, 1587 + "type-is@2.0.1": { 1588 + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 1589 + "dependencies": [ 1590 + "content-type", 1591 + "media-typer", 1592 + "mime-types@3.0.1" 1593 + ] 1594 + }, 1595 + "typescript@5.8.3": { 1596 + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 1597 + "bin": true 1598 + }, 1599 "uint8arrays@3.0.0": { 1600 "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 1601 "dependencies": [ 1602 "multiformats" 1603 ] 1604 }, 1605 + "undici-types@7.10.0": { 1606 + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" 1607 + }, 1608 + "unpipe@1.0.0": { 1609 + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" 1610 + }, 1611 + "uri-js@4.4.1": { 1612 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1613 + "dependencies": [ 1614 + "punycode" 1615 + ] 1616 + }, 1617 + "use-callback-ref@1.3.3_react@18.3.1": { 1618 + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", 1619 + "dependencies": [ 1620 + "react", 1621 + "tslib" 1622 + ] 1623 + }, 1624 + "use-sidecar@1.1.3_react@18.3.1": { 1625 + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", 1626 + "dependencies": [ 1627 + "detect-node-es", 1628 + "react", 1629 + "tslib" 1630 + ] 1631 + }, 1632 + "v8-compile-cache-lib@3.0.1": { 1633 + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" 1634 + }, 1635 + "vary@1.1.2": { 1636 + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" 1637 + }, 1638 + "which@2.0.2": { 1639 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1640 + "dependencies": [ 1641 + "isexe" 1642 + ], 1643 + "bin": true 1644 + }, 1645 + "wrap-ansi@7.0.0": { 1646 + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1647 + "dependencies": [ 1648 + "ansi-styles", 1649 + "string-width", 1650 + "strip-ansi" 1651 + ] 1652 + }, 1653 + "wrappy@1.0.2": { 1654 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 1655 + }, 1656 + "ws@8.18.3": { 1657 + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==" 1658 + }, 1659 + "y18n@5.0.8": { 1660 + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" 1661 + }, 1662 + "yargs-parser@21.1.1": { 1663 + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" 1664 + }, 1665 + "yargs@17.7.2": { 1666 + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 1667 + "dependencies": [ 1668 + "cliui", 1669 + "escalade", 1670 + "get-caller-file", 1671 + "require-directory", 1672 + "string-width", 1673 + "y18n", 1674 + "yargs-parser" 1675 + ] 1676 + }, 1677 + "yn@3.1.1": { 1678 + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" 1679 + }, 1680 "yocto-queue@1.2.1": { 1681 "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==" 1682 }, 1683 + "zod-to-json-schema@3.24.6_zod@3.25.76": { 1684 + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", 1685 + "dependencies": [ 1686 + "zod" 1687 + ] 1688 + }, 1689 "zod@3.25.76": { 1690 "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" 1691 } ··· 1698 "members": { 1699 "packages/consumer": { 1700 "dependencies": [ 1701 + "jsr:@puregarlic/randimal@^1.1.1", 1702 "jsr:@std/expect@^1.0.17", 1703 "npm:@atcute/client@^4.0.5", 1704 "npm:@atcute/jetstream@^1.1.2", 1705 "npm:@atcute/lexicons@^1.2.2", ··· 1721 "npm:@atproto/lexicon@~0.5.1" 1722 ] 1723 }, 1724 + "packages/mcp": { 1725 + "dependencies": [ 1726 + "jsr:@hono/hono@^4.10.5", 1727 + "jsr:@logtape/logtape@^1.2.0", 1728 + "jsr:@std/cli@^1.0.23", 1729 + "npm:@modelcontextprotocol/sdk@^1.21.1", 1730 + "npm:fetch-to-node@^2.1.0", 1731 + "npm:zod@^3.25.76" 1732 + ] 1733 + }, 1734 "packages/producer": { 1735 "dependencies": [ 1736 "jsr:@std/expect@^1.0.17", 1737 "npm:@atcute/client@^4.0.5", 1738 "npm:@atcute/lexicons@^1.2.2", 1739 "npm:@atcute/tid@^1.0.3" ··· 1741 }, 1742 "packages/shared": { 1743 "dependencies": [ 1744 + "npm:@atcute/atproto@^3.1.9", 1745 "npm:@atcute/client@^4.0.5", 1746 "npm:@atcute/lexicons@^1.2.2" 1747 ]
+52 -52
e2e.test.ts
··· 29 let consumer: Awaited<ReturnType<typeof createConsumer>>; 30 let producer: Awaited<ReturnType<typeof createProducer>>; 31 let keypair: Awaited<ReturnType<typeof consumer.generateKeyPair>>; 32 - let itemUri: string; 33 let testMessage: string; 34 35 await t.step("Create consumer", async () => { ··· 47 48 expect(keypair.privateKey).toBeInstanceOf(Uint8Array); 49 expect(keypair.publicKey).toBeDefined(); 50 - expect(keypair.publicKey).toContain("app.cistern.lexicon.pubkey"); 51 }); 52 53 try { ··· 63 expect(producer.publicKey?.uri).toEqual(keypair.publicKey); 64 }); 65 66 - await t.step("Create encrypted item", async () => { 67 testMessage = `E2E Test - ${new Date().toISOString()}`; 68 - itemUri = await producer.createItem(testMessage); 69 70 - expect(itemUri).toBeDefined(); 71 - expect(itemUri).toContain("app.cistern.lexicon.item"); 72 }); 73 74 - await t.step("List and decrypt items", async () => { 75 - const items = []; 76 - for await (const item of consumer.listItems()) { 77 - items.push(item); 78 } 79 80 - expect(items.length).toBeGreaterThan(0); 81 82 - const ourItem = items.find((item) => item.text === testMessage); 83 - expect(ourItem).toBeDefined(); 84 - expect(ourItem!.text).toEqual(testMessage); 85 }); 86 87 - await t.step("Delete item", async () => { 88 - const itemRkey = itemUri.split("/").pop()!; 89 - await consumer.deleteItem(itemRkey); 90 91 // Verify deletion 92 - const itemsAfterDelete = []; 93 - for await (const item of consumer.listItems()) { 94 - itemsAfterDelete.push(item); 95 } 96 97 - const deletedItem = itemsAfterDelete.find( 98 - (item) => item.text === testMessage, 99 ); 100 - expect(deletedItem).toBeUndefined(); 101 }); 102 103 await t.step("List public keys", async () => { ··· 118 119 const res = await consumer.rpc.post("com.atproto.repo.deleteRecord", { 120 input: { 121 - collection: "app.cistern.lexicon.pubkey", 122 repo: consumer.did, 123 rkey: publicKeyRkey, 124 }, ··· 131 }); 132 133 Deno.test({ 134 - name: "E2E: Multiple items with same keypair", 135 ignore: SKIP_E2E, 136 async fn(t) { 137 const handle = Deno.env.get("CISTERN_HANDLE") as Handle; ··· 141 let producer: Awaited<ReturnType<typeof createProducer>>; 142 let keypair: Awaited<ReturnType<typeof consumer.generateKeyPair>>; 143 let messages: string[]; 144 - let itemUris: string[]; 145 146 await t.step("Create consumer and generate keypair", async () => { 147 consumer = await createConsumer({ ··· 167 expect(producer.publicKey?.uri).toEqual(keypair.publicKey); 168 }); 169 170 - await t.step("Create multiple encrypted items", async () => { 171 messages = [ 172 - `E2E Item 1 - ${new Date().toISOString()}`, 173 - `E2E Item 2 - ${new Date().toISOString()}`, 174 - `E2E Item 3 - ${new Date().toISOString()}`, 175 ]; 176 177 - itemUris = []; 178 for (const message of messages) { 179 - const uri = await producer.createItem(message); 180 - itemUris.push(uri); 181 } 182 183 - expect(itemUris).toHaveLength(3); 184 }); 185 186 - await t.step("Decrypt all items", async () => { 187 - const items = []; 188 - for await (const item of consumer.listItems()) { 189 - items.push(item); 190 } 191 192 - expect(items.length).toBeGreaterThanOrEqual(3); 193 194 // Verify all test messages are present 195 for (const message of messages) { 196 - const item = items.find((i) => i.text === message); 197 - expect(item).toBeDefined(); 198 - expect(item!.text).toEqual(message); 199 } 200 }); 201 202 - await t.step("Cleanup: Delete test items", async () => { 203 - for (const uri of itemUris) { 204 const rkey = uri.split("/").pop()!; 205 - await consumer.deleteItem(rkey); 206 } 207 208 - // Verify all items deleted 209 - const remainingItems = []; 210 - for await (const item of consumer.listItems()) { 211 - remainingItems.push(item); 212 } 213 214 for (const message of messages) { 215 - const item = remainingItems.find((i) => i.text === message); 216 - expect(item).toBeUndefined(); 217 } 218 }); 219 } finally { ··· 222 223 const res = await consumer.rpc.post("com.atproto.repo.deleteRecord", { 224 input: { 225 - collection: "app.cistern.lexicon.pubkey", 226 repo: consumer.did, 227 rkey: publicKeyRkey, 228 },
··· 29 let consumer: Awaited<ReturnType<typeof createConsumer>>; 30 let producer: Awaited<ReturnType<typeof createProducer>>; 31 let keypair: Awaited<ReturnType<typeof consumer.generateKeyPair>>; 32 + let memoUri: string; 33 let testMessage: string; 34 35 await t.step("Create consumer", async () => { ··· 47 48 expect(keypair.privateKey).toBeInstanceOf(Uint8Array); 49 expect(keypair.publicKey).toBeDefined(); 50 + expect(keypair.publicKey).toContain("app.cistern.pubkey"); 51 }); 52 53 try { ··· 63 expect(producer.publicKey?.uri).toEqual(keypair.publicKey); 64 }); 65 66 + await t.step("Create encrypted memo", async () => { 67 testMessage = `E2E Test - ${new Date().toISOString()}`; 68 + memoUri = await producer.createMemo(testMessage); 69 70 + expect(memoUri).toBeDefined(); 71 + expect(memoUri).toContain("app.cistern.memo"); 72 }); 73 74 + await t.step("List and decrypt memos", async () => { 75 + const memos = []; 76 + for await (const memo of consumer.listMemos()) { 77 + memos.push(memo); 78 } 79 80 + expect(memos.length).toBeGreaterThan(0); 81 82 + const ourMemo = memos.find((memo) => memo.text === testMessage); 83 + expect(ourMemo).toBeDefined(); 84 + expect(ourMemo!.text).toEqual(testMessage); 85 }); 86 87 + await t.step("Delete memo", async () => { 88 + const memoRkey = memoUri.split("/").pop()!; 89 + await consumer.deleteMemo(memoRkey); 90 91 // Verify deletion 92 + const memosAfterDelete = []; 93 + for await (const memo of consumer.listMemos()) { 94 + memosAfterDelete.push(memo); 95 } 96 97 + const deletedMemo = memosAfterDelete.find( 98 + (memo) => memo.text === testMessage, 99 ); 100 + expect(deletedMemo).toBeUndefined(); 101 }); 102 103 await t.step("List public keys", async () => { ··· 118 119 const res = await consumer.rpc.post("com.atproto.repo.deleteRecord", { 120 input: { 121 + collection: "app.cistern.pubkey", 122 repo: consumer.did, 123 rkey: publicKeyRkey, 124 }, ··· 131 }); 132 133 Deno.test({ 134 + name: "E2E: Multiple memos with same keypair", 135 ignore: SKIP_E2E, 136 async fn(t) { 137 const handle = Deno.env.get("CISTERN_HANDLE") as Handle; ··· 141 let producer: Awaited<ReturnType<typeof createProducer>>; 142 let keypair: Awaited<ReturnType<typeof consumer.generateKeyPair>>; 143 let messages: string[]; 144 + let memoUris: string[]; 145 146 await t.step("Create consumer and generate keypair", async () => { 147 consumer = await createConsumer({ ··· 167 expect(producer.publicKey?.uri).toEqual(keypair.publicKey); 168 }); 169 170 + await t.step("Create multiple encrypted memos", async () => { 171 messages = [ 172 + `E2E Memo 1 - ${new Date().toISOString()}`, 173 + `E2E Memo 2 - ${new Date().toISOString()}`, 174 + `E2E Memo 3 - ${new Date().toISOString()}`, 175 ]; 176 177 + memoUris = []; 178 for (const message of messages) { 179 + const uri = await producer.createMemo(message); 180 + memoUris.push(uri); 181 } 182 183 + expect(memoUris).toHaveLength(3); 184 }); 185 186 + await t.step("Decrypt all memos", async () => { 187 + const memos = []; 188 + for await (const memo of consumer.listMemos()) { 189 + memos.push(memo); 190 } 191 192 + expect(memos.length).toBeGreaterThanOrEqual(3); 193 194 // Verify all test messages are present 195 for (const message of messages) { 196 + const memo = memos.find((m) => m.text === message); 197 + expect(memo).toBeDefined(); 198 + expect(memo!.text).toEqual(message); 199 } 200 }); 201 202 + await t.step("Cleanup: Delete test memos", async () => { 203 + for (const uri of memoUris) { 204 const rkey = uri.split("/").pop()!; 205 + await consumer.deleteMemo(rkey); 206 } 207 208 + // Verify all memos deleted 209 + const remainingMemos = []; 210 + for await (const memo of consumer.listMemos()) { 211 + remainingMemos.push(memo); 212 } 213 214 for (const message of messages) { 215 + const memo = remainingMemos.find((m) => m.text === message); 216 + expect(memo).toBeUndefined(); 217 } 218 }); 219 } finally { ··· 222 223 const res = await consumer.rpc.post("com.atproto.repo.deleteRecord", { 224 input: { 225 + collection: "app.cistern.pubkey", 226 repo: consumer.did, 227 rkey: publicKeyRkey, 228 },
+52 -5
packages/consumer/README.md
··· 1 - # `@cistern/consumer` 2 3 - The Consumer module is responsible for the following: 4 5 - - Generating key pairs 6 - - Retrieving and decrypting items 7 - - Subscribing to Jetstream to monitor for items
··· 1 + # @cistern/consumer 2 + 3 + Consumer client for retrieving, decrypting, and deleting Cistern memos. 4 + 5 + ## Usage 6 + 7 + ### Generate Keypair 8 + 9 + ```typescript 10 + import { createConsumer, serializeKey } from "@cistern/consumer"; 11 + 12 + const consumer = await createConsumer({ 13 + handle: "user.bsky.social", 14 + appPassword: "xxxx-xxxx-xxxx-xxxx", 15 + }); 16 + 17 + const keypair = await consumer.generateKeyPair(); 18 + 19 + console.log(`Public key URI: ${keypair.publicKey}`); 20 + console.log(`Private key: ${serializeKey(keypair.privateKey)}`); 21 + ``` 22 + 23 + ### Use Existing Keypair 24 + 25 + ```typescript 26 + import { createConsumer } from "@cistern/consumer"; 27 + 28 + const consumer = await createConsumer({ 29 + handle: "user.bsky.social", 30 + appPassword: "xxxx-xxxx-xxxx-xxxx", 31 + keypair: { 32 + publicKey: "at://did:plc:abc123/app.cistern.pubkey/3jzfcijpj2z", 33 + privateKey: "base64-encoded-private-key", 34 + }, 35 + }); 36 + ``` 37 38 + ### List Memos (Polling) 39 40 + ```typescript 41 + for await (const memo of consumer.listMemos()) { 42 + console.log(`[${memo.tid}] ${memo.text}`); 43 + await consumer.deleteMemo(memo.tid); 44 + } 45 + ``` 46 + 47 + ### Subscribe to Memos (Real-time) 48 + 49 + ```typescript 50 + for await (const memo of consumer.subscribeToMemos()) { 51 + console.log(`[${memo.tid}] ${memo.text}`); 52 + await consumer.deleteMemo(memo.tid); 53 + } 54 + ```
+234
packages/consumer/client.ts
···
··· 1 + import { 2 + produceRequirements, 3 + type XRPCProcedures, 4 + type XRPCQueries, 5 + } from "@cistern/shared"; 6 + import { decryptText, generateKeys } from "@cistern/crypto"; 7 + import { generateRandomName } from "@puregarlic/randimal"; 8 + import { 9 + is, 10 + isResourceUri, 11 + parse, 12 + type RecordKey, 13 + type ResourceUri, 14 + } from "@atcute/lexicons"; 15 + import { JetstreamSubscription } from "@atcute/jetstream"; 16 + import type { Did } from "@atcute/lexicons/syntax"; 17 + import type { Client } from "@atcute/client"; 18 + import { AppCisternMemo, type AppCisternPubkey } from "@cistern/lexicon"; 19 + import type { 20 + ConsumerOptions, 21 + ConsumerParams, 22 + DecryptedMemo, 23 + LocalKeyPair, 24 + } from "./types.ts"; 25 + 26 + /** 27 + * Client for generating keys and decoding Cistern memos. 28 + */ 29 + export class Consumer { 30 + /** DID of the user this consumer acts on behalf of */ 31 + did: Did; 32 + 33 + /** `@atcute/client` instance with credential manager */ 34 + rpc: Client<XRPCQueries, XRPCProcedures>; 35 + 36 + /** Private key used for decrypting and the AT URI of its associated public key */ 37 + keypair?: LocalKeyPair; 38 + 39 + constructor(params: ConsumerParams) { 40 + this.did = params.miniDoc.did; 41 + this.keypair = params.options.keypair 42 + ? { 43 + privateKey: Uint8Array.fromBase64(params.options.keypair.privateKey), 44 + publicKey: params.options.keypair.publicKey as ResourceUri, 45 + } 46 + : undefined; 47 + this.rpc = params.rpc; 48 + } 49 + 50 + /** 51 + * Generates a key pair, uploading the public key to PDS and returning the pair. 52 + */ 53 + async generateKeyPair(): Promise<LocalKeyPair> { 54 + if (this.keypair) { 55 + throw new Error("client already has a key pair"); 56 + } 57 + 58 + const keys = generateKeys(); 59 + const name = await generateRandomName(); 60 + 61 + const record: AppCisternPubkey.Main = { 62 + $type: "app.cistern.pubkey", 63 + name, 64 + algorithm: "x_wing", 65 + content: { $bytes: keys.publicKey.toBase64() }, 66 + createdAt: new Date().toISOString(), 67 + }; 68 + const res = await this.rpc.post("com.atproto.repo.createRecord", { 69 + input: { 70 + collection: "app.cistern.pubkey", 71 + repo: this.did, 72 + record, 73 + }, 74 + }); 75 + 76 + if (!res.ok) { 77 + throw new Error( 78 + `failed to save public key: ${res.status} ${res.data.error}`, 79 + ); 80 + } 81 + 82 + const keypair = { 83 + privateKey: keys.secretKey, 84 + publicKey: res.data.uri, 85 + }; 86 + 87 + this.keypair = keypair; 88 + 89 + return keypair; 90 + } 91 + 92 + /** 93 + * Asynchronously iterate through memos in the user's PDS 94 + */ 95 + async *listMemos(): AsyncGenerator< 96 + DecryptedMemo, 97 + void, 98 + undefined 99 + > { 100 + if (!this.keypair) { 101 + throw new Error("no key pair set; generate a key before listing memos"); 102 + } 103 + 104 + let cursor: string | undefined; 105 + 106 + while (true) { 107 + const res = await this.rpc.get("com.atproto.repo.listRecords", { 108 + params: { 109 + collection: "app.cistern.memo", 110 + repo: this.did, 111 + cursor, 112 + }, 113 + }); 114 + 115 + if (!res.ok) { 116 + throw new Error( 117 + `failed to list memos: ${res.status} ${res.data.error}`, 118 + ); 119 + } 120 + 121 + cursor = res.data.cursor; 122 + 123 + for (const record of res.data.records) { 124 + const memo = parse(AppCisternMemo.mainSchema, record.value); 125 + 126 + if (memo.pubkey !== this.keypair.publicKey) continue; 127 + 128 + const decrypted = decryptText(this.keypair.privateKey, { 129 + nonce: memo.nonce.$bytes, 130 + cipherText: memo.ciphertext.$bytes, 131 + content: memo.payload.$bytes, 132 + hash: memo.contentHash.$bytes, 133 + length: memo.contentLength, 134 + }); 135 + 136 + yield { 137 + key: record.uri.split("/").pop() as RecordKey, 138 + tid: memo.tid, 139 + text: decrypted, 140 + }; 141 + } 142 + 143 + if (!cursor) return; 144 + } 145 + } 146 + 147 + /** 148 + * Subscribes to the Jetstreams for the user's memos. Pass `"stop"` into `subscription.next(...)` to cancel 149 + * @todo Allow specifying Jetstream endpoint 150 + */ 151 + async *subscribeToMemos(): AsyncGenerator< 152 + DecryptedMemo, 153 + void, 154 + "stop" | undefined 155 + > { 156 + if (!this.keypair) { 157 + throw new Error("no key pair set; generate a key before subscribing"); 158 + } 159 + 160 + const subscription = new JetstreamSubscription({ 161 + url: "wss://jetstream2.us-east.bsky.network", 162 + wantedCollections: ["app.cistern.memo"], 163 + wantedDids: [this.did], 164 + }); 165 + 166 + for await (const event of subscription) { 167 + if (event.kind === "commit" && event.commit.operation === "create") { 168 + const record = event.commit.record; 169 + 170 + if (!is(AppCisternMemo.mainSchema, record)) { 171 + continue; 172 + } 173 + 174 + if (record.pubkey !== this.keypair.publicKey) { 175 + continue; 176 + } 177 + 178 + const decrypted = decryptText(this.keypair.privateKey, { 179 + nonce: record.nonce.$bytes, 180 + cipherText: record.ciphertext.$bytes, 181 + content: record.payload.$bytes, 182 + hash: record.contentHash.$bytes, 183 + length: record.contentLength, 184 + }); 185 + 186 + const command = yield { 187 + key: event.commit.rkey, 188 + tid: record.tid, 189 + text: decrypted, 190 + }; 191 + 192 + if (command === "stop") return; 193 + } 194 + } 195 + } 196 + 197 + /** 198 + * Deletes a memo from the user's PDS by record key. 199 + */ 200 + async deleteMemo(key: RecordKey) { 201 + const res = await this.rpc.post("com.atproto.repo.deleteRecord", { 202 + input: { 203 + collection: "app.cistern.memo", 204 + repo: this.did, 205 + rkey: key, 206 + }, 207 + }); 208 + 209 + if (!res.ok) { 210 + throw new Error( 211 + `failed to delete memo ${key}: ${res.status} ${res.data.error}`, 212 + ); 213 + } 214 + } 215 + } 216 + 217 + /** 218 + * Creates a `Consumer` instance with all necessary requirements. This is the recommended way to construct a `Consumer`. 219 + * 220 + * @description Resolves the user's DID using Slingshot, instantiates an `@atcute/client` instance, creates an initial session, and then returns a new Consumer. 221 + * @param {ConsumerOptions} options - Information for constructing the underlying XRPC client 222 + * @returns {Promise<Consumer>} A Cistern consumer client with an authorized session 223 + */ 224 + export async function createConsumer( 225 + options: ConsumerOptions, 226 + ): Promise<Consumer> { 227 + const reqs = await produceRequirements(options); 228 + 229 + if (options.keypair && !isResourceUri(options.keypair.publicKey)) { 230 + throw new Error("provided public key is not a valid AT URI"); 231 + } 232 + 233 + return new Consumer(reqs); 234 + }
+6 -2
packages/consumer/deno.jsonc
··· 1 { 2 "name": "@cistern/consumer", 3 "exports": { 4 ".": "./mod.ts" 5 }, 6 "imports": { 7 - "@atcute/atproto": "npm:@atcute/atproto@^3.1.9", 8 "@atcute/client": "npm:@atcute/client@^4.0.5", 9 "@atcute/jetstream": "npm:@atcute/jetstream@^1.1.2", 10 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2", 11 "@atcute/tid": "npm:@atcute/tid@^1.0.3", 12 - "@puregarlic/randimal": "jsr:@puregarlic/randimal@^1.0.1", 13 "@std/expect": "jsr:@std/expect@^1.0.17" 14 } 15 }
··· 1 { 2 "name": "@cistern/consumer", 3 + "version": "1.0.3", 4 + "license": "MIT", 5 "exports": { 6 ".": "./mod.ts" 7 }, 8 + "publish": { 9 + "exclude": ["*.test.ts"] 10 + }, 11 "imports": { 12 "@atcute/client": "npm:@atcute/client@^4.0.5", 13 "@atcute/jetstream": "npm:@atcute/jetstream@^1.1.2", 14 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2", 15 "@atcute/tid": "npm:@atcute/tid@^1.0.3", 16 + "@puregarlic/randimal": "jsr:@puregarlic/randimal@^1.1.1", 17 "@std/expect": "jsr:@std/expect@^1.0.17" 18 } 19 }
+91 -95
packages/consumer/mod.test.ts
··· 5 import type { Client, CredentialManager } from "@atcute/client"; 6 import type { Did, Handle, ResourceUri } from "@atcute/lexicons"; 7 import { now } from "@atcute/tid"; 8 9 // Helper to create a mock Consumer instance 10 function createMockConsumer( ··· 30 } 31 32 // Helper to create a mock RPC client 33 - function createMockRpcClient(): Client { 34 return { 35 get: () => { 36 throw new Error("Mock RPC get not implemented"); ··· 38 post: () => { 39 throw new Error("Mock RPC post not implemented"); 40 }, 41 - } as unknown as Client; 42 } 43 44 Deno.test({ ··· 49 expect(consumer.did).toEqual("did:plc:test123"); 50 expect(consumer.keypair).toBeUndefined(); 51 expect(consumer.rpc).toBeDefined(); 52 - expect(consumer.manager).toBeDefined(); 53 }, 54 }); 55 ··· 58 fn() { 59 const mockKeypair = { 60 privateKey: new Uint8Array(32).toBase64(), 61 - publicKey: 62 - "at://did:plc:test/app.cistern.lexicon.pubkey/abc123" as ResourceUri, 63 }; 64 65 const consumer = createMockConsumer({ ··· 95 return Promise.resolve({ 96 ok: true, 97 data: { 98 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/generated123", 99 }, 100 }); 101 } 102 return Promise.resolve({ ok: false, status: 500, data: {} }); 103 }, 104 - } as unknown as Client; 105 106 const consumer = createMockConsumer({ rpc: mockRpc }); 107 const keypair = await consumer.generateKeyPair(); ··· 109 expect(keypair).toBeDefined(); 110 expect(keypair.privateKey).toBeInstanceOf(Uint8Array); 111 expect(keypair.publicKey).toEqual( 112 - "at://did:plc:test/app.cistern.lexicon.pubkey/generated123", 113 ); 114 expect(consumer.keypair).toEqual(keypair); 115 116 - expect(capturedCollection).toEqual("app.cistern.lexicon.pubkey"); 117 expect(capturedRecord).toMatchObject({ 118 - $type: "app.cistern.lexicon.pubkey", 119 algorithm: "x_wing", 120 }); 121 }, ··· 131 keypair: { 132 privateKey: new Uint8Array(32).toBase64(), 133 publicKey: 134 - "at://did:plc:test/app.cistern.lexicon.pubkey/existing" as ResourceUri, 135 }, 136 }, 137 }); ··· 152 status: 500, 153 data: { error: "Internal Server Error" }, 154 }), 155 - } as unknown as Client; 156 157 const consumer = createMockConsumer({ rpc: mockRpc }); 158 ··· 163 }); 164 165 Deno.test({ 166 - name: "listItems throws when no keypair is set", 167 async fn() { 168 const consumer = createMockConsumer(); 169 170 - const iterator = consumer.listItems(); 171 await expect(iterator.next()).rejects.toThrow( 172 - "no key pair set; generate a key before listing items", 173 ); 174 }, 175 }); 176 177 Deno.test({ 178 - name: "listItems decrypts and yields items", 179 async fn() { 180 const keys = generateKeys(); 181 - const testText = "Test item content"; 182 const encrypted = encryptText(keys.publicKey, testText); 183 const testTid = now(); 184 ··· 190 data: { 191 records: [ 192 { 193 - uri: "at://did:plc:test/app.cistern.lexicon.item/item1", 194 value: { 195 - $type: "app.cistern.lexicon.item", 196 tid: testTid, 197 - ciphertext: encrypted.cipherText, 198 - nonce: encrypted.nonce, 199 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 200 - pubkey: "at://did:plc:test/app.cistern.lexicon.pubkey/key1", 201 - payload: encrypted.content, 202 contentLength: encrypted.length, 203 - contentHash: encrypted.hash, 204 - }, 205 }, 206 ], 207 cursor: undefined, ··· 210 } 211 return Promise.resolve({ ok: false, status: 500, data: {} }); 212 }, 213 - } as unknown as Client; 214 215 const consumer = createMockConsumer({ 216 rpc: mockRpc, ··· 219 appPassword: "test-password", 220 keypair: { 221 privateKey: keys.secretKey.toBase64(), 222 - publicKey: 223 - "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri, 224 }, 225 }, 226 }); 227 228 - const items = []; 229 - for await (const item of consumer.listItems()) { 230 - items.push(item); 231 } 232 233 - expect(items).toHaveLength(1); 234 - expect(items[0].text).toEqual(testText); 235 - expect(items[0].tid).toEqual(testTid); 236 }, 237 }); 238 239 Deno.test({ 240 - name: "listItems skips items with mismatched public key", 241 async fn() { 242 const keys = generateKeys(); 243 - const testText = "Test item content"; 244 const encrypted = encryptText(keys.publicKey, testText); 245 const testTid = now(); 246 ··· 252 data: { 253 records: [ 254 { 255 - uri: "at://did:plc:test/app.cistern.lexicon.item/item1", 256 value: { 257 - $type: "app.cistern.lexicon.item", 258 tid: testTid, 259 - ciphertext: encrypted.cipherText, 260 - nonce: encrypted.nonce, 261 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 262 pubkey: 263 - "at://did:plc:test/app.cistern.lexicon.pubkey/different-key", 264 - payload: encrypted.content, 265 contentLength: encrypted.length, 266 - contentHash: encrypted.hash, 267 - }, 268 }, 269 ], 270 cursor: undefined, ··· 273 } 274 return Promise.resolve({ ok: false, status: 500, data: {} }); 275 }, 276 - } as unknown as Client; 277 278 const consumer = createMockConsumer({ 279 rpc: mockRpc, ··· 283 keypair: { 284 privateKey: keys.secretKey.toBase64(), 285 publicKey: 286 - "at://did:plc:test/app.cistern.lexicon.pubkey/my-key" as ResourceUri, 287 }, 288 }, 289 }); 290 291 - const items = []; 292 - for await (const item of consumer.listItems()) { 293 - items.push(item); 294 } 295 296 - expect(items).toHaveLength(0); 297 }, 298 }); 299 300 Deno.test({ 301 - name: "listItems handles pagination", 302 async fn() { 303 const keys = generateKeys(); 304 - const text1 = "First item"; 305 - const text2 = "Second item"; 306 const encrypted1 = encryptText(keys.publicKey, text1); 307 const encrypted2 = encryptText(keys.publicKey, text2); 308 const tid1 = now(); ··· 320 data: { 321 records: [ 322 { 323 - uri: "at://did:plc:test/app.cistern.lexicon.item/item1", 324 value: { 325 - $type: "app.cistern.lexicon.item", 326 tid: tid1, 327 - ciphertext: encrypted1.cipherText, 328 - nonce: encrypted1.nonce, 329 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 330 - pubkey: 331 - "at://did:plc:test/app.cistern.lexicon.pubkey/key1", 332 - payload: encrypted1.content, 333 contentLength: encrypted1.length, 334 - contentHash: encrypted1.hash, 335 - }, 336 }, 337 ], 338 cursor: "next-page", ··· 344 data: { 345 records: [ 346 { 347 - uri: "at://did:plc:test/app.cistern.lexicon.item/item2", 348 value: { 349 - $type: "app.cistern.lexicon.item", 350 tid: tid2, 351 - ciphertext: encrypted2.cipherText, 352 - nonce: encrypted2.nonce, 353 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 354 - pubkey: 355 - "at://did:plc:test/app.cistern.lexicon.pubkey/key1", 356 - payload: encrypted2.content, 357 contentLength: encrypted2.length, 358 - contentHash: encrypted2.hash, 359 - }, 360 }, 361 ], 362 cursor: undefined, ··· 366 } 367 return Promise.resolve({ ok: false, status: 500, data: {} }); 368 }, 369 - } as unknown as Client; 370 371 const consumer = createMockConsumer({ 372 rpc: mockRpc, ··· 375 appPassword: "test-password", 376 keypair: { 377 privateKey: keys.secretKey.toBase64(), 378 - publicKey: 379 - "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri, 380 }, 381 }, 382 }); 383 384 - const items = []; 385 - for await (const item of consumer.listItems()) { 386 - items.push(item); 387 } 388 389 - expect(items).toHaveLength(2); 390 - expect(items[0].text).toEqual(text1); 391 - expect(items[1].text).toEqual(text2); 392 expect(callCount).toEqual(2); 393 }, 394 }); 395 396 Deno.test({ 397 - name: "listItems throws when list request fails", 398 async fn() { 399 const mockRpc = { 400 get: () => ··· 403 status: 401, 404 data: { error: "Unauthorized" }, 405 }), 406 - } as unknown as Client; 407 408 const consumer = createMockConsumer({ 409 rpc: mockRpc, ··· 412 appPassword: "test-password", 413 keypair: { 414 privateKey: new Uint8Array(32).toBase64(), 415 - publicKey: "at://did:plc:test/app.cistern.lexicon.pubkey/key1", 416 }, 417 }, 418 }); 419 420 - const iterator = consumer.listItems(); 421 - await expect(iterator.next()).rejects.toThrow("failed to list items"); 422 }, 423 }); 424 425 Deno.test({ 426 - name: "subscribeToItems throws when no keypair is set", 427 async fn() { 428 const consumer = createMockConsumer(); 429 430 - const iterator = consumer.subscribeToItems(); 431 await expect(iterator.next()).rejects.toThrow( 432 "no key pair set; generate a key before subscribing", 433 ); ··· 435 }); 436 437 Deno.test({ 438 - name: "deleteItem successfully deletes an item", 439 async fn() { 440 let deletedRkey: string | undefined; 441 ··· 452 } 453 return Promise.resolve({ ok: false, status: 500, data: {} }); 454 }, 455 - } as unknown as Client; 456 457 const consumer = createMockConsumer({ rpc: mockRpc }); 458 459 - await consumer.deleteItem("item123"); 460 461 - expect(deletedRkey).toEqual("item123"); 462 }, 463 }); 464 465 Deno.test({ 466 - name: "deleteItem throws when delete request fails", 467 async fn() { 468 const mockRpc = { 469 post: () => ··· 472 status: 404, 473 data: { error: "Not Found" }, 474 }), 475 - } as unknown as Client; 476 477 const consumer = createMockConsumer({ rpc: mockRpc }); 478 479 - await expect(consumer.deleteItem("item123")).rejects.toThrow( 480 - "failed to delete item item123", 481 ); 482 }, 483 });
··· 5 import type { Client, CredentialManager } from "@atcute/client"; 6 import type { Did, Handle, ResourceUri } from "@atcute/lexicons"; 7 import { now } from "@atcute/tid"; 8 + import type { AppCisternMemo } from "@cistern/lexicon"; 9 + import type { XRPCProcedures, XRPCQueries } from "@cistern/shared"; 10 11 // Helper to create a mock Consumer instance 12 function createMockConsumer( ··· 32 } 33 34 // Helper to create a mock RPC client 35 + function createMockRpcClient(): Client<XRPCQueries, XRPCProcedures> { 36 return { 37 get: () => { 38 throw new Error("Mock RPC get not implemented"); ··· 40 post: () => { 41 throw new Error("Mock RPC post not implemented"); 42 }, 43 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 44 } 45 46 Deno.test({ ··· 51 expect(consumer.did).toEqual("did:plc:test123"); 52 expect(consumer.keypair).toBeUndefined(); 53 expect(consumer.rpc).toBeDefined(); 54 }, 55 }); 56 ··· 59 fn() { 60 const mockKeypair = { 61 privateKey: new Uint8Array(32).toBase64(), 62 + publicKey: "at://did:plc:test/app.cistern.pubkey/abc123" as ResourceUri, 63 }; 64 65 const consumer = createMockConsumer({ ··· 95 return Promise.resolve({ 96 ok: true, 97 data: { 98 + uri: "at://did:plc:test/app.cistern.pubkey/generated123", 99 }, 100 }); 101 } 102 return Promise.resolve({ ok: false, status: 500, data: {} }); 103 }, 104 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 105 106 const consumer = createMockConsumer({ rpc: mockRpc }); 107 const keypair = await consumer.generateKeyPair(); ··· 109 expect(keypair).toBeDefined(); 110 expect(keypair.privateKey).toBeInstanceOf(Uint8Array); 111 expect(keypair.publicKey).toEqual( 112 + "at://did:plc:test/app.cistern.pubkey/generated123", 113 ); 114 expect(consumer.keypair).toEqual(keypair); 115 116 + expect(capturedCollection).toEqual("app.cistern.pubkey"); 117 expect(capturedRecord).toMatchObject({ 118 + $type: "app.cistern.pubkey", 119 algorithm: "x_wing", 120 }); 121 }, ··· 131 keypair: { 132 privateKey: new Uint8Array(32).toBase64(), 133 publicKey: 134 + "at://did:plc:test/app.cistern.pubkey/existing" as ResourceUri, 135 }, 136 }, 137 }); ··· 152 status: 500, 153 data: { error: "Internal Server Error" }, 154 }), 155 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 156 157 const consumer = createMockConsumer({ rpc: mockRpc }); 158 ··· 163 }); 164 165 Deno.test({ 166 + name: "listMemos throws when no keypair is set", 167 async fn() { 168 const consumer = createMockConsumer(); 169 170 + const iterator = consumer.listMemos(); 171 await expect(iterator.next()).rejects.toThrow( 172 + "no key pair set; generate a key before listing memos", 173 ); 174 }, 175 }); 176 177 Deno.test({ 178 + name: "listMemos decrypts and yields memos", 179 async fn() { 180 const keys = generateKeys(); 181 + const testText = "Test memo content"; 182 const encrypted = encryptText(keys.publicKey, testText); 183 const testTid = now(); 184 ··· 190 data: { 191 records: [ 192 { 193 + uri: "at://did:plc:test/app.cistern.memo/memo1", 194 value: { 195 + $type: "app.cistern.memo", 196 tid: testTid, 197 + ciphertext: { $bytes: encrypted.cipherText }, 198 + nonce: { $bytes: encrypted.nonce }, 199 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 200 + pubkey: "at://did:plc:test/app.cistern.pubkey/key1", 201 + payload: { $bytes: encrypted.content }, 202 contentLength: encrypted.length, 203 + contentHash: { $bytes: encrypted.hash }, 204 + } as AppCisternMemo.Main, 205 }, 206 ], 207 cursor: undefined, ··· 210 } 211 return Promise.resolve({ ok: false, status: 500, data: {} }); 212 }, 213 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 214 215 const consumer = createMockConsumer({ 216 rpc: mockRpc, ··· 219 appPassword: "test-password", 220 keypair: { 221 privateKey: keys.secretKey.toBase64(), 222 + publicKey: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri, 223 }, 224 }, 225 }); 226 227 + const memos = []; 228 + for await (const memo of consumer.listMemos()) { 229 + memos.push(memo); 230 } 231 232 + expect(memos).toHaveLength(1); 233 + expect(memos[0].text).toEqual(testText); 234 + expect(memos[0].tid).toEqual(testTid); 235 }, 236 }); 237 238 Deno.test({ 239 + name: "listmemos skips memos with mismatched public key", 240 async fn() { 241 const keys = generateKeys(); 242 + const testText = "Test memo content"; 243 const encrypted = encryptText(keys.publicKey, testText); 244 const testTid = now(); 245 ··· 251 data: { 252 records: [ 253 { 254 + uri: "at://did:plc:test/app.cistern.memo/memo1", 255 value: { 256 + $type: "app.cistern.memo", 257 tid: testTid, 258 + ciphertext: { $bytes: encrypted.cipherText }, 259 + nonce: { $bytes: encrypted.nonce }, 260 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 261 pubkey: 262 + "at://did:plc:test/app.cistern.pubkey/different-key", 263 + payload: { $bytes: encrypted.content }, 264 contentLength: encrypted.length, 265 + contentHash: { $bytes: encrypted.hash }, 266 + } as AppCisternMemo.Main, 267 }, 268 ], 269 cursor: undefined, ··· 272 } 273 return Promise.resolve({ ok: false, status: 500, data: {} }); 274 }, 275 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 276 277 const consumer = createMockConsumer({ 278 rpc: mockRpc, ··· 282 keypair: { 283 privateKey: keys.secretKey.toBase64(), 284 publicKey: 285 + "at://did:plc:test/app.cistern.pubkey/my-key" as ResourceUri, 286 }, 287 }, 288 }); 289 290 + const memos = []; 291 + for await (const memo of consumer.listMemos()) { 292 + memos.push(memo); 293 } 294 295 + expect(memos).toHaveLength(0); 296 }, 297 }); 298 299 Deno.test({ 300 + name: "listMemos handles pagination", 301 async fn() { 302 const keys = generateKeys(); 303 + const text1 = "First memo"; 304 + const text2 = "Second memo"; 305 const encrypted1 = encryptText(keys.publicKey, text1); 306 const encrypted2 = encryptText(keys.publicKey, text2); 307 const tid1 = now(); ··· 319 data: { 320 records: [ 321 { 322 + uri: "at://did:plc:test/app.cistern.memo/memo1", 323 value: { 324 + $type: "app.cistern.memo", 325 tid: tid1, 326 + ciphertext: { $bytes: encrypted1.cipherText }, 327 + nonce: { $bytes: encrypted1.nonce }, 328 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 329 + pubkey: "at://did:plc:test/app.cistern.pubkey/key1", 330 + payload: { $bytes: encrypted1.content }, 331 contentLength: encrypted1.length, 332 + contentHash: { $bytes: encrypted1.hash }, 333 + } as AppCisternMemo.Main, 334 }, 335 ], 336 cursor: "next-page", ··· 342 data: { 343 records: [ 344 { 345 + uri: "at://did:plc:test/app.cistern.memo/memo2", 346 value: { 347 + $type: "app.cistern.memo", 348 tid: tid2, 349 + ciphertext: { $bytes: encrypted2.cipherText }, 350 + nonce: { $bytes: encrypted2.nonce }, 351 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 352 + pubkey: "at://did:plc:test/app.cistern.pubkey/key1", 353 + payload: { $bytes: encrypted2.content }, 354 contentLength: encrypted2.length, 355 + contentHash: { $bytes: encrypted2.hash }, 356 + } as AppCisternMemo.Main, 357 }, 358 ], 359 cursor: undefined, ··· 363 } 364 return Promise.resolve({ ok: false, status: 500, data: {} }); 365 }, 366 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 367 368 const consumer = createMockConsumer({ 369 rpc: mockRpc, ··· 372 appPassword: "test-password", 373 keypair: { 374 privateKey: keys.secretKey.toBase64(), 375 + publicKey: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri, 376 }, 377 }, 378 }); 379 380 + const memos = []; 381 + for await (const memo of consumer.listMemos()) { 382 + memos.push(memo); 383 } 384 385 + expect(memos).toHaveLength(2); 386 + expect(memos[0].text).toEqual(text1); 387 + expect(memos[1].text).toEqual(text2); 388 expect(callCount).toEqual(2); 389 }, 390 }); 391 392 Deno.test({ 393 + name: "listMemos throws when list request fails", 394 async fn() { 395 const mockRpc = { 396 get: () => ··· 399 status: 401, 400 data: { error: "Unauthorized" }, 401 }), 402 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 403 404 const consumer = createMockConsumer({ 405 rpc: mockRpc, ··· 408 appPassword: "test-password", 409 keypair: { 410 privateKey: new Uint8Array(32).toBase64(), 411 + publicKey: "at://did:plc:test/app.cistern.pubkey/key1", 412 }, 413 }, 414 }); 415 416 + const iterator = consumer.listMemos(); 417 + await expect(iterator.next()).rejects.toThrow("failed to list memos"); 418 }, 419 }); 420 421 Deno.test({ 422 + name: "subscribeToMemos throws when no keypair is set", 423 async fn() { 424 const consumer = createMockConsumer(); 425 426 + const iterator = consumer.subscribeToMemos(); 427 await expect(iterator.next()).rejects.toThrow( 428 "no key pair set; generate a key before subscribing", 429 ); ··· 431 }); 432 433 Deno.test({ 434 + name: "deleteMemo successfully deletes a memo", 435 async fn() { 436 let deletedRkey: string | undefined; 437 ··· 448 } 449 return Promise.resolve({ ok: false, status: 500, data: {} }); 450 }, 451 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 452 453 const consumer = createMockConsumer({ rpc: mockRpc }); 454 455 + await consumer.deleteMemo("memo123"); 456 457 + expect(deletedRkey).toEqual("memo123"); 458 }, 459 }); 460 461 Deno.test({ 462 + name: "deleteMemo throws when delete request fails", 463 async fn() { 464 const mockRpc = { 465 post: () => ··· 468 status: 404, 469 data: { error: "Not Found" }, 470 }), 471 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 472 473 const consumer = createMockConsumer({ rpc: mockRpc }); 474 475 + await expect(consumer.deleteMemo("memo123")).rejects.toThrow( 476 + "failed to delete memo memo123", 477 ); 478 }, 479 });
+2 -210
packages/consumer/mod.ts
··· 1 - import { produceRequirements } from "@cistern/shared"; 2 - import { decryptText, generateKeys } from "@cistern/crypto"; 3 - import { generateRandomName } from "@puregarlic/randimal"; 4 - import { is, parse, type RecordKey } from "@atcute/lexicons"; 5 - import { JetstreamSubscription } from "@atcute/jetstream"; 6 - import type { Did } from "@atcute/lexicons/syntax"; 7 - import type { Client, CredentialManager } from "@atcute/client"; 8 - import { 9 - AppCisternLexiconItem, 10 - type AppCisternLexiconPubkey, 11 - } from "@cistern/lexicon"; 12 - import type { 13 - ConsumerOptions, 14 - ConsumerParams, 15 - DecryptedItem, 16 - LocalKeyPair, 17 - } from "./types.ts"; 18 - 19 - import type {} from "@atcute/atproto"; 20 - 21 - export async function createConsumer( 22 - options: ConsumerOptions, 23 - ): Promise<Consumer> { 24 - const reqs = await produceRequirements(options); 25 - 26 - return new Consumer(reqs); 27 - } 28 - 29 - /** 30 - * Client for generating keys and decoding Cistern items. 31 - */ 32 - export class Consumer { 33 - did: Did; 34 - keypair?: LocalKeyPair; 35 - rpc: Client; 36 - manager: CredentialManager; 37 - 38 - constructor(params: ConsumerParams) { 39 - this.did = params.miniDoc.did; 40 - this.keypair = params.options.keypair 41 - ? { 42 - privateKey: Uint8Array.fromBase64(params.options.keypair.privateKey), 43 - publicKey: params.options.keypair.publicKey, 44 - } 45 - : undefined; 46 - this.rpc = params.rpc; 47 - this.manager = params.manager; 48 - } 49 - 50 - /** 51 - * Generates a key pair, uploading the public key to PDS and returning the pair. 52 - */ 53 - async generateKeyPair(): Promise<LocalKeyPair> { 54 - if (this.keypair) { 55 - throw new Error("client already has a key pair"); 56 - } 57 - 58 - const keys = generateKeys(); 59 - const name = await generateRandomName(); 60 - 61 - const record: AppCisternLexiconPubkey.Main = { 62 - $type: "app.cistern.lexicon.pubkey", 63 - name, 64 - algorithm: "x_wing", 65 - content: keys.publicKey.toBase64(), 66 - createdAt: new Date().toISOString(), 67 - }; 68 - const res = await this.rpc.post("com.atproto.repo.createRecord", { 69 - input: { 70 - collection: "app.cistern.lexicon.pubkey", 71 - repo: this.did, 72 - record, 73 - }, 74 - }); 75 - 76 - if (!res.ok) { 77 - throw new Error( 78 - `failed to save public key: ${res.status} ${res.data.error}`, 79 - ); 80 - } 81 - 82 - const keypair = { 83 - privateKey: keys.secretKey, 84 - publicKey: res.data.uri, 85 - }; 86 - 87 - this.keypair = keypair; 88 - 89 - return keypair; 90 - } 91 - 92 - /** 93 - * Asynchronously iterate through items in the user's PDS 94 - */ 95 - async *listItems(): AsyncGenerator< 96 - DecryptedItem, 97 - void, 98 - undefined 99 - > { 100 - if (!this.keypair) { 101 - throw new Error("no key pair set; generate a key before listing items"); 102 - } 103 - 104 - let cursor: string | undefined; 105 - 106 - while (true) { 107 - const res = await this.rpc.get("com.atproto.repo.listRecords", { 108 - params: { 109 - collection: "app.cistern.lexicon.item", 110 - repo: this.did, 111 - cursor, 112 - }, 113 - }); 114 - 115 - if (!res.ok) { 116 - throw new Error( 117 - `failed to list items: ${res.status} ${res.data.error}`, 118 - ); 119 - } 120 - 121 - cursor = res.data.cursor; 122 - 123 - for (const record of res.data.records) { 124 - const item = parse(AppCisternLexiconItem.mainSchema, record.value); 125 - 126 - if (item.pubkey !== this.keypair.publicKey) continue; 127 - 128 - const decrypted = decryptText(this.keypair.privateKey, { 129 - nonce: item.nonce, 130 - cipherText: item.ciphertext, 131 - content: item.payload, 132 - hash: item.contentHash, 133 - length: item.contentLength, 134 - }); 135 - 136 - yield { 137 - tid: item.tid, 138 - text: decrypted, 139 - }; 140 - } 141 - 142 - if (!cursor) return; 143 - } 144 - } 145 - 146 - /** 147 - * Subscribes to the Jetstreams for the user's items. Pass `"stop"` into `subscription.next(...)` to cancel 148 - * @todo Allow specifying Jetstream endpoint 149 - */ 150 - async *subscribeToItems(): AsyncGenerator< 151 - DecryptedItem, 152 - void, 153 - "stop" | undefined 154 - > { 155 - if (!this.keypair) { 156 - throw new Error("no key pair set; generate a key before subscribing"); 157 - } 158 - 159 - const subscription = new JetstreamSubscription({ 160 - url: "wss://jetstream2.us-east.bsky.network", 161 - wantedCollections: ["app.cistern.lexicon.item"], 162 - wantedDids: [this.did], 163 - }); 164 - 165 - for await (const event of subscription) { 166 - if (event.kind === "commit" && event.commit.operation === "create") { 167 - const record = event.commit.record; 168 - 169 - if (!is(AppCisternLexiconItem.mainSchema, record)) { 170 - continue; 171 - } 172 - 173 - if (record.pubkey !== this.keypair.publicKey) { 174 - continue; 175 - } 176 - 177 - const decrypted = decryptText(this.keypair.privateKey, { 178 - nonce: record.nonce, 179 - cipherText: record.ciphertext, 180 - content: record.payload, 181 - hash: record.contentHash, 182 - length: record.contentLength, 183 - }); 184 - 185 - const command = yield { tid: record.tid, text: decrypted }; 186 - 187 - if (command === "stop") return; 188 - } 189 - } 190 - } 191 - 192 - /** 193 - * Deletes an item from the user's PDS by record key. 194 - */ 195 - async deleteItem(key: RecordKey) { 196 - const res = await this.rpc.post("com.atproto.repo.deleteRecord", { 197 - input: { 198 - collection: "app.cistern.lexicon.item", 199 - repo: this.did, 200 - rkey: key, 201 - }, 202 - }); 203 - 204 - if (!res.ok) { 205 - throw new Error( 206 - `failed to delete item ${key}: ${res.status} ${res.data.error}`, 207 - ); 208 - } 209 - } 210 - }
··· 1 + export * from "./client.ts"; 2 + export * from "./types.ts";
+25 -3
packages/consumer/types.ts
··· 1 import type { BaseClientOptions, ClientRequirements } from "@cistern/shared"; 2 - import type { ResourceUri, Tid } from "@atcute/lexicons"; 3 4 export interface InputLocalKeyPair { 5 privateKey: string; 6 - publicKey: ResourceUri; 7 } 8 9 export interface LocalKeyPair { 10 privateKey: Uint8Array; 11 publicKey: ResourceUri; 12 } 13 14 export interface ConsumerOptions extends BaseClientOptions { 15 keypair?: InputLocalKeyPair; 16 } 17 18 export type ConsumerParams = ClientRequirements<ConsumerOptions>; 19 20 - export interface DecryptedItem { 21 tid: Tid; 22 text: string; 23 }
··· 1 import type { BaseClientOptions, ClientRequirements } from "@cistern/shared"; 2 + import type { RecordKey, ResourceUri, Tid } from "@atcute/lexicons"; 3 4 + /** 5 + * A locally-stored key pair suitable for storage 6 + */ 7 export interface InputLocalKeyPair { 8 + /** An X-Wing private key, encoded in base64 */ 9 privateKey: string; 10 + 11 + /** An AT URI to the `app.cistern.pubkey` record derived from this private key */ 12 + publicKey: string; 13 } 14 15 + /** 16 + * InputLocalKeyPair, with `privateKey` decoded to a Uint8Array 17 + */ 18 export interface LocalKeyPair { 19 + /** An X-Wing private key in raw byte format */ 20 privateKey: Uint8Array; 21 + 22 + /** An AT URI to the `app.cistern.pubkey` record derived from this private key */ 23 publicKey: ResourceUri; 24 } 25 26 + /** Credentials and optional keypair for creating a Consumer client */ 27 export interface ConsumerOptions extends BaseClientOptions { 28 + /** Optional input keypair. If you do not provide this here, you will need to generate one after the client is instantiated */ 29 keypair?: InputLocalKeyPair; 30 } 31 32 + /** Asynchronously-acquired parameters required to construct a Client. `createConsumer` will translate from `ConsumerOptions` to `ConsumerParams` for you */ 33 export type ConsumerParams = ClientRequirements<ConsumerOptions>; 34 35 + /** A simplified, encrypted memo */ 36 + export interface DecryptedMemo { 37 + /** Record key of this memo */ 38 + key: RecordKey; 39 + 40 + /** TID for when the memo was created */ 41 tid: Tid; 42 + 43 + /** The original, decrypted contents of the memo */ 44 text: string; 45 }
+11
packages/crypto/README.md
···
··· 1 + # @cistern/crypto 2 + 3 + Post-quantum cryptographic primitives for Cistern. 4 + 5 + ## Algorithm 6 + 7 + **`x_wing-xchacha20_poly1305-sha3_512`** 8 + 9 + - **X-Wing KEM**: Post-quantum hybrid key encapsulation (ML-KEM-768 + X25519) 10 + - **XChaCha20-Poly1305**: Authenticated encryption 11 + - **SHA3-512**: Content integrity verification
+5
packages/crypto/deno.jsonc
··· 1 { 2 "name": "@cistern/crypto", 3 "exports": { 4 ".": "./mod.ts" 5 }, 6 "imports": { 7 "@noble/ciphers": "jsr:@noble/ciphers@^2.0.1",
··· 1 { 2 "name": "@cistern/crypto", 3 + "version": "1.0.0", 4 + "license": "MIT", 5 "exports": { 6 ".": "./mod.ts" 7 + }, 8 + "publish": { 9 + "exclude": ["*.test.ts"] 10 }, 11 "imports": { 12 "@noble/ciphers": "jsr:@noble/ciphers@^2.0.1",
+10
packages/lexicon/README.md
···
··· 1 + # @cistern/lexicon 2 + 3 + AT Protocol lexicon definitions and TypeScript types for Cistern records. 4 + 5 + ## Record Types 6 + 7 + | Collection | Description | 8 + | -------------------- | ------------------------------------------------------------------------------------------------- | 9 + | `app.cistern.pubkey` | Public key records with human-readable names, referenced by memos via AT-URI | 10 + | `app.cistern.memo` | Encrypted memo records containing ciphertext, nonce, algorithm metadata, and public key reference |
+5
packages/lexicon/deno.jsonc
··· 1 { 2 "name": "@cistern/lexicon", 3 "exports": { 4 ".": "./mod.ts" 5 }, 6 "tasks": { 7 "generate": "deno run --allow-env --allow-sys --allow-read --allow-write npm:@atcute/lex-cli generate -c lex.config.ts"
··· 1 { 2 "name": "@cistern/lexicon", 3 + "version": "1.0.0", 4 + "license": "MIT", 5 "exports": { 6 ".": "./mod.ts" 7 + }, 8 + "publish": { 9 + "exclude": ["*.test.ts"] 10 }, 11 "tasks": { 12 "generate": "deno run --allow-env --allow-sys --allow-read --allow-write npm:@atcute/lex-cli generate -c lex.config.ts"
-63
packages/lexicon/lexicons/app/cistern/lexicon/item.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "app.cistern.lexicon.item", 4 - "description": "An encrypted memo intended to be accessed and deleted later.", 5 - "defs": { 6 - "main": { 7 - "type": "record", 8 - "description": "An encrypted memo", 9 - "record": { 10 - "type": "object", 11 - "required": [ 12 - "tid", 13 - "ciphertext", 14 - "nonce", 15 - "algorithm", 16 - "pubkey", 17 - "payload", 18 - "contentLength", 19 - "contentHash" 20 - ], 21 - "properties": { 22 - "tid": { 23 - "type": "string", 24 - "description": "TID representing when this item was created", 25 - "format": "tid" 26 - }, 27 - "ciphertext": { 28 - "type": "string", 29 - "description": "Encapsulated shared ciphertext", 30 - "maxLength": 2000 31 - }, 32 - "nonce": { 33 - "type": "string", 34 - "description": "Base64-encoded nonce used for content encryption", 35 - "maxLength": 32 36 - }, 37 - "algorithm": { 38 - "type": "string", 39 - "description": "Algorithm used for encryption, in <kem>-<cipher>-<hash> format.", 40 - "knownValues": ["x_wing-xchacha20_poly1305-sha3_512"] 41 - }, 42 - "pubkey": { 43 - "type": "string", 44 - "description": "URI to the public key used to encrypt this item", 45 - "format": "at-uri" 46 - }, 47 - "payload": { 48 - "type": "string", 49 - "description": "Base64-encoded encrypted item contents" 50 - }, 51 - "contentLength": { 52 - "type": "integer", 53 - "description": "Original content length in bytes" 54 - }, 55 - "contentHash": { 56 - "type": "string", 57 - "description": "Base64-encoded hash of the decrypted contents. Verify this before accepting the decrypted message. The algorithm is identified under `algorithm`" 58 - } 59 - } 60 - } 61 - } 62 - } 63 - }
···
-35
packages/lexicon/lexicons/app/cistern/lexicon/pubkey.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "app.cistern.lexicon.pubkey", 4 - "description": "A public key used to encrypt Cistern items", 5 - "defs": { 6 - "main": { 7 - "type": "record", 8 - "description": "A public key used to encrypt Cistern items", 9 - "record": { 10 - "type": "object", 11 - "required": ["name", "algorithm", "content", "createdAt"], 12 - "properties": { 13 - "name": { 14 - "type": "string", 15 - "minGraphemes": 1, 16 - "description": "A memorable name for this public key. Avoid using revealing names, such as \"Graham's Macbook\"" 17 - }, 18 - "algorithm": { 19 - "type": "string", 20 - "knownValues": ["x_wing"], 21 - "description": "KEM algorithm used to generate this key" 22 - }, 23 - "content": { 24 - "type": "string", 25 - "description": "Contents of the public key, encoded in base64" 26 - }, 27 - "createdAt": { 28 - "type": "string", 29 - "format": "datetime" 30 - } 31 - } 32 - } 33 - } 34 - } 35 - }
···
+61
packages/lexicon/lexicons/app/cistern/memo.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.cistern.memo", 4 + "description": "An encrypted memo intended to be accessed and deleted later.", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "description": "An encrypted memo", 9 + "record": { 10 + "type": "object", 11 + "required": [ 12 + "tid", 13 + "ciphertext", 14 + "nonce", 15 + "algorithm", 16 + "pubkey", 17 + "payload", 18 + "contentLength", 19 + "contentHash" 20 + ], 21 + "properties": { 22 + "tid": { 23 + "type": "string", 24 + "description": "TID representing when this memo was created", 25 + "format": "tid" 26 + }, 27 + "ciphertext": { 28 + "type": "bytes", 29 + "description": "Encapsulated shared ciphertext" 30 + }, 31 + "nonce": { 32 + "type": "bytes", 33 + "description": "Nonce used for content encryption" 34 + }, 35 + "algorithm": { 36 + "type": "string", 37 + "description": "Algorithm used for encryption, in <kem>-<cipher>-<hash> format.", 38 + "knownValues": ["x_wing-xchacha20_poly1305-sha3_512"] 39 + }, 40 + "pubkey": { 41 + "type": "string", 42 + "description": "URI to the public key used to encrypt this memo", 43 + "format": "at-uri" 44 + }, 45 + "payload": { 46 + "type": "bytes", 47 + "description": "Encrypted memo contents" 48 + }, 49 + "contentLength": { 50 + "type": "integer", 51 + "description": "Original content length in bytes" 52 + }, 53 + "contentHash": { 54 + "type": "bytes", 55 + "description": "Hash of the decrypted contents. Verify this before accepting the decrypted message. The algorithm is identified under `algorithm`" 56 + } 57 + } 58 + } 59 + } 60 + } 61 + }
+35
packages/lexicon/lexicons/app/cistern/pubkey.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.cistern.pubkey", 4 + "description": "A public key used to encrypt Cistern memos", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "description": "A public key used to encrypt Cistern memos", 9 + "record": { 10 + "type": "object", 11 + "required": ["name", "algorithm", "content", "createdAt"], 12 + "properties": { 13 + "name": { 14 + "type": "string", 15 + "minGraphemes": 1, 16 + "description": "A memorable name for this public key. Avoid using revealing names, such as \"Graham's Macbook\"" 17 + }, 18 + "algorithm": { 19 + "type": "string", 20 + "knownValues": ["x_wing"], 21 + "description": "KEM algorithm used to generate this key" 22 + }, 23 + "content": { 24 + "type": "bytes", 25 + "description": "Contents of the public key" 26 + }, 27 + "createdAt": { 28 + "type": "string", 29 + "format": "datetime" 30 + } 31 + } 32 + } 33 + } 34 + } 35 + }
+2 -2
packages/lexicon/src/index.ts
··· 1 - export * as AppCisternLexiconItem from "./types/app/cistern/lexicon/item.ts"; 2 - export * as AppCisternLexiconPubkey from "./types/app/cistern/lexicon/pubkey.ts";
··· 1 + export * as AppCisternMemo from "./types/app/cistern/memo.ts"; 2 + export * as AppCisternPubkey from "./types/app/cistern/pubkey.ts";
-64
packages/lexicon/src/types/app/cistern/lexicon/item.ts
··· 1 - import type {} from "@atcute/lexicons"; 2 - import * as v from "@atcute/lexicons/validations"; 3 - import type {} from "@atcute/lexicons/ambient"; 4 - 5 - const _mainSchema = /*#__PURE__*/ v.record( 6 - /*#__PURE__*/ v.string(), 7 - /*#__PURE__*/ v.object({ 8 - $type: /*#__PURE__*/ v.literal("app.cistern.lexicon.item"), 9 - /** 10 - * Algorithm used for encryption, in <kem>-<cipher>-<hash> format. 11 - */ 12 - algorithm: /*#__PURE__*/ v.string< 13 - "x_wing-xchacha20_poly1305-sha3_512" | (string & {}) 14 - >(), 15 - /** 16 - * Encapsulated shared ciphertext 17 - * @maxLength 2000 18 - */ 19 - ciphertext: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 20 - /*#__PURE__*/ v.stringLength(0, 2000), 21 - ]), 22 - /** 23 - * Base64-encoded hash of the decrypted contents. Verify this before accepting the decrypted message. The algorithm is identified under `algorithm` 24 - */ 25 - contentHash: /*#__PURE__*/ v.string(), 26 - /** 27 - * Original content length in bytes 28 - */ 29 - contentLength: /*#__PURE__*/ v.integer(), 30 - /** 31 - * Base64-encoded nonce used for content encryption 32 - * @maxLength 32 33 - */ 34 - nonce: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 35 - /*#__PURE__*/ v.stringLength(0, 32), 36 - ]), 37 - /** 38 - * Base64-encoded encrypted item contents 39 - */ 40 - payload: /*#__PURE__*/ v.string(), 41 - /** 42 - * URI to the public key used to encrypt this item 43 - */ 44 - pubkey: /*#__PURE__*/ v.resourceUriString(), 45 - /** 46 - * TID representing when this item was created 47 - */ 48 - tid: /*#__PURE__*/ v.tidString(), 49 - }), 50 - ); 51 - 52 - type main$schematype = typeof _mainSchema; 53 - 54 - export interface mainSchema extends main$schematype {} 55 - 56 - export const mainSchema = _mainSchema as mainSchema; 57 - 58 - export interface Main extends v.InferInput<typeof mainSchema> {} 59 - 60 - declare module "@atcute/lexicons/ambient" { 61 - interface Records { 62 - "app.cistern.lexicon.item": mainSchema; 63 - } 64 - }
···
-40
packages/lexicon/src/types/app/cistern/lexicon/pubkey.ts
··· 1 - import type {} from "@atcute/lexicons"; 2 - import * as v from "@atcute/lexicons/validations"; 3 - import type {} from "@atcute/lexicons/ambient"; 4 - 5 - const _mainSchema = /*#__PURE__*/ v.record( 6 - /*#__PURE__*/ v.string(), 7 - /*#__PURE__*/ v.object({ 8 - $type: /*#__PURE__*/ v.literal("app.cistern.lexicon.pubkey"), 9 - /** 10 - * KEM algorithm used to generate this key 11 - */ 12 - algorithm: /*#__PURE__*/ v.string<"x_wing" | (string & {})>(), 13 - /** 14 - * Contents of the public key, encoded in base64 15 - */ 16 - content: /*#__PURE__*/ v.string(), 17 - createdAt: /*#__PURE__*/ v.datetimeString(), 18 - /** 19 - * A memorable name for this public key. Avoid using revealing names, such as "Graham's Macbook" 20 - * @minGraphemes 1 21 - */ 22 - name: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 23 - /*#__PURE__*/ v.stringGraphemes(1), 24 - ]), 25 - }), 26 - ); 27 - 28 - type main$schematype = typeof _mainSchema; 29 - 30 - export interface mainSchema extends main$schematype {} 31 - 32 - export const mainSchema = _mainSchema as mainSchema; 33 - 34 - export interface Main extends v.InferInput<typeof mainSchema> {} 35 - 36 - declare module "@atcute/lexicons/ambient" { 37 - interface Records { 38 - "app.cistern.lexicon.pubkey": mainSchema; 39 - } 40 - }
···
+51
packages/lexicon/src/types/app/cistern/memo.ts
···
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + 4 + const _mainSchema = /*#__PURE__*/ v.record( 5 + /*#__PURE__*/ v.string(), 6 + /*#__PURE__*/ v.object({ 7 + $type: /*#__PURE__*/ v.literal("app.cistern.memo"), 8 + /** 9 + * Algorithm used for encryption, in <kem>-<cipher>-<hash> format. 10 + */ 11 + algorithm: /*#__PURE__*/ v.string< 12 + "x_wing-xchacha20_poly1305-sha3_512" | (string & {}) 13 + >(), 14 + /** 15 + * Encapsulated shared ciphertext 16 + */ 17 + ciphertext: /*#__PURE__*/ v.bytes(), 18 + /** 19 + * Hash of the decrypted contents. Verify this before accepting the decrypted message. The algorithm is identified under `algorithm` 20 + */ 21 + contentHash: /*#__PURE__*/ v.bytes(), 22 + /** 23 + * Original content length in bytes 24 + */ 25 + contentLength: /*#__PURE__*/ v.integer(), 26 + /** 27 + * Nonce used for content encryption 28 + */ 29 + nonce: /*#__PURE__*/ v.bytes(), 30 + /** 31 + * Encrypted memo contents 32 + */ 33 + payload: /*#__PURE__*/ v.bytes(), 34 + /** 35 + * URI to the public key used to encrypt this memo 36 + */ 37 + pubkey: /*#__PURE__*/ v.resourceUriString(), 38 + /** 39 + * TID representing when this memo was created 40 + */ 41 + tid: /*#__PURE__*/ v.tidString(), 42 + }), 43 + ); 44 + 45 + type main$schematype = typeof _mainSchema; 46 + 47 + export interface mainSchema extends main$schematype {} 48 + 49 + export const mainSchema = _mainSchema as mainSchema; 50 + 51 + export interface Main extends v.InferInput<typeof mainSchema> {}
+33
packages/lexicon/src/types/app/cistern/pubkey.ts
···
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + 4 + const _mainSchema = /*#__PURE__*/ v.record( 5 + /*#__PURE__*/ v.string(), 6 + /*#__PURE__*/ v.object({ 7 + $type: /*#__PURE__*/ v.literal("app.cistern.pubkey"), 8 + /** 9 + * KEM algorithm used to generate this key 10 + */ 11 + algorithm: /*#__PURE__*/ v.string<"x_wing" | (string & {})>(), 12 + /** 13 + * Contents of the public key 14 + */ 15 + content: /*#__PURE__*/ v.bytes(), 16 + createdAt: /*#__PURE__*/ v.datetimeString(), 17 + /** 18 + * A memorable name for this public key. Avoid using revealing names, such as "Graham's Macbook" 19 + * @minGraphemes 1 20 + */ 21 + name: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 22 + /*#__PURE__*/ v.stringGraphemes(1), 23 + ]), 24 + }), 25 + ); 26 + 27 + type main$schematype = typeof _mainSchema; 28 + 29 + export interface mainSchema extends main$schematype {} 30 + 31 + export const mainSchema = _mainSchema as mainSchema; 32 + 33 + export interface Main extends v.InferInput<typeof mainSchema> {}
+2
packages/mcp/.gitignore
···
··· 1 + .env 2 + cistern-mcp.db*
+182
packages/mcp/README.md
···
··· 1 + # @cistern/mcp 2 + 3 + Model Context Protocol (MCP) server for Cistern, enabling AI assistants to retrieve and manage encrypted memos. 4 + 5 + ## Features 6 + 7 + - **Dual Transport Support**: stdio for local integrations (Claude Desktop) and HTTP for remote deployments 8 + - **Automatic Keypair Management**: Generates and persists keypairs in Deno KV on first launch 9 + - **Two MCP Tools**: 10 + - `next_memo`: Retrieve the next outstanding memo 11 + - `delete_memo`: Delete a memo after handling it 12 + 13 + ## Installation 14 + 15 + ### Prerequisites 16 + 17 + - Deno 2.0+ 18 + - AT Protocol account with app password 19 + - Bluesky handle (e.g., `yourname.bsky.social`) 20 + 21 + ### Environment Variables 22 + 23 + **Required:** 24 + ```bash 25 + CISTERN_MCP_HANDLE=yourname.bsky.social 26 + CISTERN_MCP_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx 27 + ``` 28 + 29 + **Optional (for existing keypair):** 30 + ```bash 31 + CISTERN_MCP_PRIVATE_KEY=base64-encoded-private-key 32 + CISTERN_MCP_PUBLIC_KEY_URI=at://did:plc:abc.../app.cistern.pubkey/xyz 33 + ``` 34 + 35 + **Required for HTTP mode:** 36 + ```bash 37 + CISTERN_MCP_BEARER_TOKEN=your-secret-bearer-token 38 + ``` 39 + 40 + ## Usage 41 + 42 + ### stdio Mode (Claude Desktop) 43 + 44 + Run the server in stdio mode for local integrations: 45 + 46 + ```bash 47 + cd packages/mcp 48 + deno task stdio 49 + ``` 50 + 51 + Add to Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json`): 52 + 53 + ```json 54 + { 55 + "mcpServers": { 56 + "cistern": { 57 + "command": "deno", 58 + "args": [ 59 + "task", 60 + "--cwd", 61 + "/path/to/cistern/packages/mcp", 62 + "stdio" 63 + ], 64 + "env": { 65 + "CISTERN_MCP_HANDLE": "yourname.bsky.social", 66 + "CISTERN_MCP_APP_PASSWORD": "xxxx-xxxx-xxxx-xxxx" 67 + } 68 + } 69 + } 70 + } 71 + ``` 72 + 73 + ### HTTP Mode (Remote Deployment) 74 + 75 + Run the server in HTTP mode for remote access: 76 + 77 + ```bash 78 + cd packages/mcp 79 + deno task http 80 + ``` 81 + 82 + The server listens on port 8000 by default. Configure your MCP client to connect via HTTP: 83 + 84 + ```json 85 + { 86 + "url": "http://localhost:8000/mcp", 87 + "headers": { 88 + "Authorization": "Bearer your-secret-bearer-token" 89 + } 90 + } 91 + ``` 92 + 93 + ## Keypair Management 94 + 95 + On first launch without `CISTERN_MCP_PRIVATE_KEY` and `CISTERN_MCP_PUBLIC_KEY_URI`, the server will: 96 + 97 + 1. Check Deno KV for a stored keypair (keyed by handle) 98 + 2. If not found, generate a new X-Wing keypair 99 + 3. Upload the public key to your PDS as an `app.cistern.pubkey` record 100 + 4. Store the keypair in Deno KV at `./cistern-mcp.db` 101 + 5. Log the public key URI for reference 102 + 103 + The keypair persists across restarts and is isolated per handle. 104 + 105 + ### Example First Launch Log 106 + 107 + ``` 108 + [cistern:mcp] starting in stdio mode 109 + [cistern:mcp] no keypair found; generating new keypair for yourname.bsky.social 110 + [cistern:mcp] generated new keypair with public key URI: at://did:plc:abc123.../app.cistern.pubkey/xyz789 111 + [cistern:mcp] stored keypair for yourname.bsky.social 112 + ``` 113 + 114 + ### Example Subsequent Launch Log 115 + 116 + ``` 117 + [cistern:mcp] starting in stdio mode 118 + [cistern:mcp] using stored keypair for yourname.bsky.social 119 + ``` 120 + 121 + ## MCP Tools 122 + 123 + ### `next_memo` 124 + 125 + Retrieves the next outstanding memo from your PDS. 126 + 127 + **Output:** 128 + ```json 129 + { 130 + "key": "3kbxyz789abc", 131 + "tid": "3kbxyz789abc", 132 + "text": "Remember to buy milk" 133 + } 134 + ``` 135 + 136 + Returns `"no memos remaining"` when all memos have been retrieved. 137 + 138 + ### `delete_memo` 139 + 140 + Deletes a memo by record key after it has been handled. 141 + 142 + **Input:** 143 + ```json 144 + { 145 + "key": "3kbxyz789abc" 146 + } 147 + ``` 148 + 149 + **Output:** 150 + ```json 151 + { 152 + "success": true 153 + } 154 + ``` 155 + 156 + ## Development 157 + 158 + ### Testing with MCP Inspector 159 + 160 + ```bash 161 + deno task stdio:inspect 162 + ``` 163 + 164 + This launches the MCP Inspector UI for interactive testing of the stdio server. 165 + 166 + ### Logs 167 + 168 + The server uses LogTape for structured logging: 169 + 170 + - **`[cistern:mcp]`**: Server lifecycle, keypair operations 171 + - **`[cistern:http]`**: HTTP request/response logs (HTTP mode only) 172 + 173 + ## Security 174 + 175 + - **Bearer Authentication**: Required for HTTP mode 176 + - **Private Keys**: Never transmitted; stored locally in Deno KV 177 + - **Session Isolation**: Each HTTP session gets its own Consumer instance 178 + - **CORS**: Configured for MCP protocol headers 179 + 180 + ## Limitations 181 + 182 + - **No Keypair Deletion**: The Consumer SDK doesn't currently support deleting public keys from the PDS. If you want to use a different keypair, you can either set `CISTERN_MCP_PRIVATE_KEY` and `CISTERN_MCP_PUBLIC_KEY_URI` environment variables, or delete the `cistern-mcp.db` SQLite files to force regeneration. You'll need to manually delete the old public key record from your PDS using a tool like [pdsls.dev](https://pdsls.dev).
+29
packages/mcp/deno.jsonc
···
··· 1 + { 2 + "name": "@cistern/mcp", 3 + "version": "1.0.0", 4 + "license": "MIT", 5 + "exports": { 6 + ".": "./index.ts" 7 + }, 8 + "tasks": { 9 + "inspector": "npx @modelcontextprotocol/inspector", 10 + "http": "deno -P --allow-net --env-file ./index.ts --http", 11 + "stdio": "deno -P --env-file ./index.ts", 12 + "stdio:inspect": "npx @modelcontextprotocol/inspector deno task stdio" 13 + }, 14 + "permissions": { 15 + "default": { 16 + "env": true, 17 + "read": ["./cistern-mcp.db"], 18 + "write": ["./cistern-mcp.db"] 19 + } 20 + }, 21 + "imports": { 22 + "hono": "jsr:@hono/hono@^4.10.5", 23 + "@logtape/logtape": "jsr:@logtape/logtape@^1.2.0", 24 + "@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.21.1", 25 + "@std/cli": "jsr:@std/cli@^1.0.23", 26 + "fetch-to-node": "npm:fetch-to-node@^2.1.0", 27 + "zod": "npm:zod@^3.25.76" 28 + } 29 + }
+29
packages/mcp/env.ts
···
··· 1 + import { getLogger } from "@logtape/logtape"; 2 + import type { ConsumerOptions } from "@cistern/consumer"; 3 + 4 + export function collectOptions(): ConsumerOptions { 5 + const logger = getLogger(["cistern", "mcp"]); 6 + const handle = Deno.env.get("CISTERN_MCP_HANDLE"); 7 + const appPassword = Deno.env.get("CISTERN_MCP_APP_PASSWORD"); 8 + 9 + if (!handle || !appPassword) { 10 + logger.error( 11 + "CISTERN_MCP_HANDLE or CISTERN_MCP_APP_PASSWORD are not set in the environment", 12 + ); 13 + return Deno.exit(1); 14 + } 15 + 16 + const privateKey = Deno.env.get("CISTERN_MCP_PRIVATE_KEY"); 17 + const publicKeyUri = Deno.env.get("CISTERN_MCP_PUBLIC_KEY_URI"); 18 + 19 + return { 20 + appPassword, 21 + handle, 22 + keypair: privateKey && publicKeyUri 23 + ? { 24 + privateKey, 25 + publicKey: publicKeyUri, 26 + } 27 + : undefined, 28 + }; 29 + }
+135
packages/mcp/hono.ts
···
··· 1 + import { Hono } from "hono"; 2 + import { cors } from "hono/cors"; 3 + import { bearerAuth } from "hono/bearer-auth"; 4 + import { getLogger, withContext } from "@logtape/logtape"; 5 + import { toFetchResponse, toReqRes } from "fetch-to-node"; 6 + import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 7 + import { collectOptions } from "./env.ts"; 8 + import { createServer } from "./server.ts"; 9 + 10 + export function createApp() { 11 + const app = new Hono(); 12 + const logger = getLogger(["cistern", "http"]); 13 + const sessions = new Map<string, StreamableHTTPServerTransport>(); 14 + 15 + const token = Deno.env.get("CISTERN_MCP_BEARER_TOKEN"); 16 + 17 + if (!token) { 18 + logger.error("http mode requires CISTERN_MCP_BEARER_TOKEN to be set"); 19 + return Deno.exit(1); 20 + } 21 + 22 + app.all( 23 + "/mcp", 24 + cors({ 25 + origin: "*", 26 + allowMethods: ["GET", "POST", "DELETE", "OPTIONS"], 27 + allowHeaders: [ 28 + "Content-Type", 29 + "Authorization", 30 + "Mcp-Session-Id", 31 + "Mcp-Protocol-Version", 32 + ], 33 + exposeHeaders: ["Mcp-Session-Id"], 34 + }), 35 + ); 36 + 37 + app.use("/mcp", bearerAuth({ token })); 38 + app.use("*", async (c, next) => { 39 + const requestId = crypto.randomUUID(); 40 + const startTime = Date.now(); 41 + 42 + await withContext({ 43 + requestId, 44 + method: c.req.method, 45 + url: c.req.url, 46 + userAgent: c.req.header("User-Agent"), 47 + ipAddress: c.req.header("CF-Connecting-IP") || 48 + c.req.header("X-Forwarded-For"), 49 + }, async () => { 50 + logger.info("{method} request started", { 51 + method: c.req.method, 52 + url: c.req.url, 53 + requestId, 54 + }); 55 + 56 + await next(); 57 + 58 + const duration = Date.now() - startTime; 59 + 60 + logger.info("{status} request completed in {duration}ms", { 61 + status: c.res.status, 62 + duration, 63 + requestId, 64 + }); 65 + }); 66 + }); 67 + 68 + app.onError((err, c) => { 69 + logger.error("request error {error}", { 70 + error: { 71 + name: err.name, 72 + message: err.message, 73 + stack: err.stack, 74 + }, 75 + method: c.req.method, 76 + url: c.req.url, 77 + }); 78 + 79 + return c.json({ error: "internal server error" }, 500); 80 + }); 81 + 82 + app.post("/mcp", async (ctx) => { 83 + const sessionId = ctx.req.header("mcp-session-id") ?? crypto.randomUUID(); 84 + let session = sessions.get(sessionId); 85 + 86 + if (session) { 87 + logger.info("resuming session {sessionId}", { sessionId }); 88 + } else { 89 + logger.info("creating new session {sessionId}", { sessionId }); 90 + 91 + const options = collectOptions(); 92 + const server = await createServer(options); 93 + 94 + session = new StreamableHTTPServerTransport({ 95 + sessionIdGenerator: () => sessionId, 96 + }); 97 + 98 + session.onclose = () => { 99 + logger.info("closing session {sessionId}", { sessionId }); 100 + }; 101 + 102 + await server.connect(session); 103 + 104 + sessions.set(sessionId, session); 105 + } 106 + 107 + const { req, res } = toReqRes(ctx.req.raw); 108 + 109 + await session.handleRequest(req, res); 110 + 111 + return await toFetchResponse(res); 112 + }); 113 + 114 + app.on(["GET", "DELETE"], "/mcp", async (ctx) => { 115 + const sessionId = ctx.req.header("mcp-session-id") ?? ""; 116 + const session = sessions.get(sessionId); 117 + 118 + if (!session) { 119 + logger.info("{method} invalid session {sessionId}", { 120 + method: ctx.req.method, 121 + sessionId, 122 + }); 123 + 124 + return ctx.json({ error: "invalid or missing session" }, 401); 125 + } 126 + 127 + const { req, res } = toReqRes(ctx.req.raw); 128 + 129 + await session.handleRequest(req, res); 130 + 131 + return await toFetchResponse(res); 132 + }); 133 + 134 + return app; 135 + }
+62
packages/mcp/index.ts
···
··· 1 + import { parseArgs } from "@std/cli"; 2 + import { AsyncLocalStorage } from "node:async_hooks"; 3 + import { configure, getConsoleSink, getLogger } from "@logtape/logtape"; 4 + import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 + 6 + import { createServer } from "./server.ts"; 7 + import { createApp } from "./hono.ts"; 8 + import { collectOptions } from "./env.ts"; 9 + 10 + async function main() { 11 + await configure({ 12 + sinks: { console: getConsoleSink() }, 13 + loggers: [ 14 + { 15 + category: ["cistern", "mcp"], 16 + lowestLevel: "trace", 17 + sinks: ["console"], 18 + }, 19 + { 20 + category: ["cistern", "http"], 21 + lowestLevel: "info", 22 + sinks: ["console"], 23 + }, 24 + ], 25 + contextLocalStorage: new AsyncLocalStorage(), 26 + }); 27 + 28 + const logger = getLogger(["cistern", "mcp"]); 29 + const args = parseArgs(Deno.args, { 30 + boolean: ["http"], 31 + }); 32 + 33 + if (!args.http) { 34 + logger.info("starting in stdio mode"); 35 + 36 + const options = collectOptions(); 37 + const server = await createServer(options); 38 + const transport = new StdioServerTransport(); 39 + 40 + await server.connect(transport); 41 + } else { 42 + logger.info("starting in streamable HTTP mode"); 43 + 44 + // Validate environment before starting the server 45 + collectOptions(); 46 + 47 + const app = createApp(); 48 + 49 + Deno.serve( 50 + { 51 + onListen(addr) { 52 + logger.info("http server listening at {hostname} on port {port}", { 53 + ...addr, 54 + }); 55 + }, 56 + }, 57 + app.fetch, 58 + ); 59 + } 60 + } 61 + 62 + await main();
+45
packages/mcp/kv.ts
···
··· 1 + import type { InputLocalKeyPair } from "@cistern/consumer"; 2 + import { getLogger } from "@logtape/logtape"; 3 + 4 + const KV_PATH = "./cistern-mcp.db"; 5 + 6 + let kv: Deno.Kv | undefined; 7 + 8 + async function getKv(): Promise<Deno.Kv> { 9 + if (!kv) { 10 + kv = await Deno.openKv(KV_PATH); 11 + } 12 + return kv; 13 + } 14 + 15 + export async function getStoredKeypair( 16 + handle: string, 17 + ): Promise<InputLocalKeyPair | null> { 18 + const logger = getLogger(["cistern", "mcp"]); 19 + const db = await getKv(); 20 + const result = await db.get<InputLocalKeyPair>([ 21 + "cistern", 22 + "keypairs", 23 + handle, 24 + ]); 25 + 26 + if (result.value) { 27 + logger.debug("found stored keypair for {handle}", { handle }); 28 + return result.value; 29 + } 30 + 31 + logger.debug("no stored keypair found for {handle}", { handle }); 32 + return null; 33 + } 34 + 35 + export async function storeKeypair( 36 + handle: string, 37 + keypair: InputLocalKeyPair, 38 + ): Promise<void> { 39 + const logger = getLogger(["cistern", "mcp"]); 40 + const db = await getKv(); 41 + 42 + await db.set(["cistern", "keypairs", handle], keypair); 43 + 44 + logger.info("stored keypair for {handle}", { handle }); 45 + }
+126
packages/mcp/server.ts
···
··· 1 + import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 + import { getLogger } from "@logtape/logtape"; 3 + import { z } from "zod"; 4 + import type { 5 + Consumer, 6 + ConsumerOptions, 7 + DecryptedMemo, 8 + } from "@cistern/consumer"; 9 + import { createConsumer } from "@cistern/consumer"; 10 + import { serializeKey } from "@cistern/crypto"; 11 + import { getStoredKeypair, storeKeypair } from "./kv.ts"; 12 + 13 + export async function createServer(options: ConsumerOptions) { 14 + const logger = getLogger(["cistern", "mcp"]); 15 + 16 + if (!options.keypair) { 17 + const storedKeypair = await getStoredKeypair(options.handle); 18 + if (storedKeypair) { 19 + logger.info("using stored keypair for {handle}", { 20 + handle: options.handle, 21 + }); 22 + options.keypair = storedKeypair; 23 + } 24 + } else { 25 + logger.info("using keypair from environment variables"); 26 + } 27 + 28 + const consumer = await createConsumer(options); 29 + 30 + if (!consumer.keypair) { 31 + logger.info("no keypair found; generating new keypair for {handle}", { 32 + handle: options.handle, 33 + }); 34 + 35 + const keypair = await consumer.generateKeyPair(); 36 + 37 + logger.info("generated new keypair with public key URI: {publicKey}", { 38 + publicKey: keypair.publicKey, 39 + }); 40 + 41 + await storeKeypair(options.handle, { 42 + privateKey: serializeKey(keypair.privateKey), 43 + publicKey: keypair.publicKey, 44 + }); 45 + } 46 + 47 + return _createServerWithConsumer(consumer); 48 + } 49 + 50 + function _createServerWithConsumer(consumer: Consumer) { 51 + const logger = getLogger("cistern-mcp"); 52 + const server = new McpServer({ 53 + name: "cistern-mcp", 54 + version: "1.0.0", 55 + }); 56 + 57 + let iterator: 58 + | AsyncGenerator<DecryptedMemo, void, "stop" | undefined> 59 + | undefined; 60 + 61 + server.registerTool( 62 + "next_memo", 63 + { 64 + title: "Next memo", 65 + description: "Retrieve the next outstanding memo", 66 + outputSchema: { key: z.string(), tid: z.string(), text: z.string() }, 67 + }, 68 + async () => { 69 + if (!iterator) { 70 + logger.trace("creating iterator"); 71 + iterator ??= consumer.listMemos(); 72 + } 73 + 74 + const res = await iterator.next(); 75 + 76 + if (res.done) { 77 + logger.trace("iterator done; cleaning up"); 78 + iterator = undefined; 79 + } 80 + 81 + return { 82 + content: [{ 83 + type: "text", 84 + text: res.value?.text 85 + ? `key: ${res.value.key}, text: ${res.value.text}` 86 + : "no memos remaining", 87 + }], 88 + structuredContent: { 89 + key: res.value?.key ?? "", 90 + tid: res.value?.tid ?? "", 91 + text: res.value?.text ?? "no memos remaining", 92 + }, 93 + }; 94 + }, 95 + ); 96 + 97 + server.registerTool( 98 + "delete_memo", 99 + { 100 + title: "Delete memo", 101 + description: 102 + "Delete a memo by record key, after it has been handled as instructed by the user", 103 + inputSchema: { key: z.string() }, 104 + outputSchema: { success: z.boolean() }, 105 + }, 106 + async ({ key }) => { 107 + try { 108 + await consumer.deleteMemo(key); 109 + 110 + return { 111 + content: [{ type: "text", text: "delete successful" }], 112 + structuredContent: { success: true }, 113 + }; 114 + } catch (error) { 115 + logger.error("failed to delete memo: {error}", { error }); 116 + 117 + return { 118 + content: [{ type: "text", text: "delete unsuccessful" }], 119 + structuredContent: { success: false }, 120 + }; 121 + } 122 + }, 123 + ); 124 + 125 + return server; 126 + }
+34
packages/producer/README.md
···
··· 1 + # @cistern/producer 2 + 3 + Producer client for creating and encrypting Cistern memos. 4 + 5 + ## Usage 6 + 7 + ```typescript 8 + import { createProducer } from "@cistern/producer"; 9 + 10 + const producer = await createProducer({ 11 + handle: "user.bsky.social", 12 + appPassword: "xxxx-xxxx-xxxx-xxxx", 13 + }); 14 + 15 + for await (const pubkey of producer.listPublicKeys()) { 16 + console.log(`${pubkey.name}: ${pubkey.uri}`); 17 + } 18 + 19 + producer.selectPublicKey(pubkey); 20 + 21 + const memoUri = await producer.createMemo("Hello, world!"); 22 + ``` 23 + 24 + Or, if you already have a public key record ID: 25 + 26 + ```typescript 27 + const producer = await createProducer({ 28 + handle: "user.bsky.social", 29 + appPassword: "xxxx-xxxx-xxxx-xxxx", 30 + publicKey: "3jzfcijpj2z", 31 + }); 32 + 33 + const memoUri = await producer.createMemo("Hello, world!"); 34 + ```
+167
packages/producer/client.ts
···
··· 1 + import { produceRequirements } from "@cistern/shared"; 2 + import { encryptText } from "@cistern/crypto"; 3 + import type { 4 + ProducerOptions, 5 + ProducerParams, 6 + PublicKeyOption, 7 + } from "./types.ts"; 8 + import { type Did, parse, type ResourceUri } from "@atcute/lexicons"; 9 + import type { Client } from "@atcute/client"; 10 + import { now } from "@atcute/tid"; 11 + import { type AppCisternMemo, AppCisternPubkey } from "@cistern/lexicon"; 12 + 13 + /** 14 + * Creates a `Producer` instance with all necessary requirements. This is the recommended way to construct a `Producer`. 15 + * 16 + * @description Resolves the user's DID using Slingshot, instantiates an `@atcute/client` instance, creates an initial session, and returns a new `Producer`. If a pubkey record key is provided, it will be resolved and set as the active key. 17 + * @param {ProducerOptions} options - Information for constructing the underlying XRPC client 18 + * @returns {Promise<Producer>} A Cistern producer client with an authorized session 19 + */ 20 + export async function createProducer( 21 + { publicKey: rkey, ...opts }: ProducerOptions, 22 + ): Promise<Producer> { 23 + const reqs = await produceRequirements(opts); 24 + 25 + let publicKey: PublicKeyOption | undefined; 26 + if (rkey) { 27 + const res = await reqs.rpc.get("com.atproto.repo.getRecord", { 28 + params: { 29 + repo: reqs.miniDoc.did, 30 + rkey, 31 + collection: "app.cistern.pubkey", 32 + }, 33 + }); 34 + 35 + if (!res.ok) { 36 + throw new Error( 37 + `invalid public key record ID ${publicKey}, got: ${res.status} ${res.data.error}`, 38 + ); 39 + } 40 + 41 + const record = parse(AppCisternPubkey.mainSchema, res.data.value); 42 + 43 + publicKey = { 44 + uri: res.data.uri, 45 + name: record.name, 46 + content: record.content.$bytes, 47 + }; 48 + } 49 + 50 + return new Producer({ 51 + ...reqs, 52 + publicKey, 53 + }); 54 + } 55 + 56 + /** 57 + * A client for encrypting and creating Cistern memos. 58 + */ 59 + export class Producer { 60 + /** DID of the user this producer acts on behalf of */ 61 + did: Did; 62 + 63 + /** `@atcute/client` instance with credential manager */ 64 + rpc: Client; 65 + 66 + /** Partial public key record, used for encrypting items */ 67 + publicKey?: PublicKeyOption; 68 + 69 + constructor(params: ProducerParams) { 70 + this.did = params.miniDoc.did; 71 + this.rpc = params.rpc; 72 + this.publicKey = params.publicKey; 73 + } 74 + 75 + /** 76 + * Creates a memo and saves it as a record in the user's PDS. 77 + * @param {string} text - The contents of the memo you wish to create 78 + */ 79 + async createMemo(text: string): Promise<ResourceUri> { 80 + if (!this.publicKey) { 81 + throw new Error( 82 + "no public key set; select a public key before creating a memo", 83 + ); 84 + } 85 + 86 + const payload = encryptText( 87 + Uint8Array.fromBase64(this.publicKey.content), 88 + text, 89 + ); 90 + const record: AppCisternMemo.Main = { 91 + $type: "app.cistern.memo", 92 + tid: now(), 93 + algorithm: "x_wing-xchacha20_poly1305-sha3_512", 94 + ciphertext: { $bytes: payload.cipherText }, 95 + contentHash: { $bytes: payload.hash }, 96 + contentLength: payload.length, 97 + nonce: { $bytes: payload.nonce }, 98 + payload: { $bytes: payload.content }, 99 + pubkey: this.publicKey.uri, 100 + }; 101 + 102 + const res = await this.rpc.post("com.atproto.repo.createRecord", { 103 + input: { 104 + collection: "app.cistern.memo", 105 + repo: this.did, 106 + record, 107 + }, 108 + }); 109 + 110 + if (!res.ok) { 111 + throw new Error( 112 + `failed to create new memo: ${res.status} ${res.data.error}`, 113 + ); 114 + } 115 + 116 + return res.data.uri; 117 + } 118 + 119 + /** 120 + * Lists public keys registered in the user's PDS 121 + */ 122 + async *listPublicKeys(): AsyncGenerator< 123 + PublicKeyOption, 124 + void, 125 + void 126 + > { 127 + let cursor: string | undefined; 128 + 129 + while (true) { 130 + const res = await this.rpc.get("com.atproto.repo.listRecords", { 131 + params: { 132 + collection: "app.cistern.pubkey", 133 + repo: this.did, 134 + cursor, 135 + }, 136 + }); 137 + 138 + if (!res.ok) { 139 + throw new Error( 140 + `failed to list public keys: ${res.status} ${res.data.error}`, 141 + ); 142 + } 143 + 144 + cursor = res.data.cursor; 145 + 146 + for (const record of res.data.records) { 147 + const memo = parse(AppCisternPubkey.mainSchema, record.value); 148 + 149 + yield { 150 + uri: record.uri, 151 + content: memo.content.$bytes, 152 + name: memo.name, 153 + }; 154 + } 155 + 156 + if (!cursor) return; 157 + } 158 + } 159 + 160 + /** 161 + * Sets a public key as the main encryption key. This is not necessary to use if you instantiated the client with a public key. 162 + * @param {PublicKeyOption} key - The key you want to use for encryption 163 + */ 164 + selectPublicKey(key: PublicKeyOption) { 165 + this.publicKey = key; 166 + } 167 + }
+5 -1
packages/producer/deno.jsonc
··· 1 { 2 "name": "@cistern/producer", 3 "exports": { 4 ".": "./mod.ts" 5 }, 6 "imports": { 7 - "@atcute/atproto": "npm:@atcute/atproto@^3.1.9", 8 "@atcute/client": "npm:@atcute/client@^4.0.5", 9 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2", 10 "@atcute/tid": "npm:@atcute/tid@^1.0.3",
··· 1 { 2 "name": "@cistern/producer", 3 + "version": "1.0.2", 4 + "license": "MIT", 5 "exports": { 6 ".": "./mod.ts" 7 }, 8 + "publish": { 9 + "exclude": ["*.test.ts"] 10 + }, 11 "imports": { 12 "@atcute/client": "npm:@atcute/client@^4.0.5", 13 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2", 14 "@atcute/tid": "npm:@atcute/tid@^1.0.3",
+44 -44
packages/producer/mod.test.ts
··· 4 import type { ProducerParams, PublicKeyOption } from "./types.ts"; 5 import type { Client, CredentialManager } from "@atcute/client"; 6 import type { Did, Handle, ResourceUri } from "@atcute/lexicons"; 7 8 // Helper to create a mock Producer instance 9 function createMockProducer( ··· 29 } 30 31 // Helper to create a mock RPC client 32 - function createMockRpcClient(): Client { 33 return { 34 get: () => { 35 throw new Error("Mock RPC get not implemented"); ··· 37 post: () => { 38 throw new Error("Mock RPC post not implemented"); 39 }, 40 - } as unknown as Client; 41 } 42 43 Deno.test({ ··· 48 expect(producer.did).toEqual("did:plc:test123"); 49 expect(producer.publicKey).toBeUndefined(); 50 expect(producer.rpc).toBeDefined(); 51 - expect(producer.manager).toBeDefined(); 52 }, 53 }); 54 ··· 56 name: "Producer constructor initializes with existing public key", 57 fn() { 58 const mockPublicKey: PublicKeyOption = { 59 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri, 60 name: "Test Key", 61 content: new Uint8Array(32).toBase64(), 62 }; ··· 72 }); 73 74 Deno.test({ 75 - name: "createItem successfully creates and uploads an encrypted item", 76 async fn() { 77 const keys = generateKeys(); 78 let capturedRecord: unknown; ··· 91 return Promise.resolve({ 92 ok: true, 93 data: { 94 - uri: 95 - "at://did:plc:test/app.cistern.lexicon.item/item123" as ResourceUri, 96 }, 97 }); 98 } 99 return Promise.resolve({ ok: false, status: 500, data: {} }); 100 }, 101 - } as unknown as Client; 102 103 const producer = createMockProducer({ 104 rpc: mockRpc, 105 publicKey: { 106 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri, 107 name: "Test Key", 108 content: keys.publicKey.toBase64(), 109 }, 110 }); 111 112 - const uri = await producer.createItem("Test message"); 113 114 - expect(uri).toEqual("at://did:plc:test/app.cistern.lexicon.item/item123"); 115 - expect(capturedCollection).toEqual("app.cistern.lexicon.item"); 116 expect(capturedRecord).toMatchObject({ 117 - $type: "app.cistern.lexicon.item", 118 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 119 }); 120 }, 121 }); 122 123 Deno.test({ 124 - name: "createItem throws when no public key is set", 125 async fn() { 126 const producer = createMockProducer(); 127 128 - await expect(producer.createItem("Test message")).rejects.toThrow( 129 - "no public key set; select a public key before creating an item", 130 ); 131 }, 132 }); 133 134 Deno.test({ 135 - name: "createItem throws when upload fails", 136 async fn() { 137 const keys = generateKeys(); 138 const mockRpc = { ··· 142 status: 500, 143 data: { error: "Internal Server Error" }, 144 }), 145 - } as unknown as Client; 146 147 const producer = createMockProducer({ 148 rpc: mockRpc, 149 publicKey: { 150 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri, 151 name: "Test Key", 152 content: keys.publicKey.toBase64(), 153 }, 154 }); 155 156 - await expect(producer.createItem("Test message")).rejects.toThrow( 157 - "failed to create new item", 158 ); 159 }, 160 }); ··· 170 data: { 171 records: [ 172 { 173 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1", 174 value: { 175 - $type: "app.cistern.lexicon.pubkey", 176 name: "Key 1", 177 algorithm: "x_wing", 178 - content: new Uint8Array(32).toBase64(), 179 createdAt: new Date().toISOString(), 180 - }, 181 }, 182 { 183 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key2", 184 value: { 185 - $type: "app.cistern.lexicon.pubkey", 186 name: "Key 2", 187 algorithm: "x_wing", 188 - content: new Uint8Array(32).toBase64(), 189 createdAt: new Date().toISOString(), 190 - }, 191 }, 192 ], 193 cursor: undefined, ··· 196 } 197 return Promise.resolve({ ok: false, status: 500, data: {} }); 198 }, 199 - } as unknown as Client; 200 201 const producer = createMockProducer({ rpc: mockRpc }); 202 ··· 216 async fn() { 217 let callCount = 0; 218 const mockRpc = { 219 - get: (endpoint: string, params?: { params?: { cursor?: string } }) => { 220 if (endpoint === "com.atproto.repo.listRecords") { 221 callCount++; 222 ··· 226 data: { 227 records: [ 228 { 229 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1", 230 value: { 231 - $type: "app.cistern.lexicon.pubkey", 232 name: "Key 1", 233 algorithm: "x_wing", 234 - content: new Uint8Array(32).toBase64(), 235 createdAt: new Date().toISOString(), 236 - }, 237 }, 238 ], 239 cursor: "next-page", ··· 245 data: { 246 records: [ 247 { 248 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key2", 249 value: { 250 - $type: "app.cistern.lexicon.pubkey", 251 name: "Key 2", 252 algorithm: "x_wing", 253 - content: new Uint8Array(32).toBase64(), 254 createdAt: new Date().toISOString(), 255 - }, 256 }, 257 ], 258 cursor: undefined, ··· 262 } 263 return Promise.resolve({ ok: false, status: 500, data: {} }); 264 }, 265 - } as unknown as Client; 266 267 const producer = createMockProducer({ rpc: mockRpc }); 268 ··· 288 status: 401, 289 data: { error: "Unauthorized" }, 290 }), 291 - } as unknown as Client; 292 293 const producer = createMockProducer({ rpc: mockRpc }); 294 ··· 303 const producer = createMockProducer(); 304 305 const mockPublicKey: PublicKeyOption = { 306 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri, 307 name: "Selected Key", 308 content: new Uint8Array(32).toBase64(), 309 }; ··· 323 fn() { 324 const producer = createMockProducer({ 325 publicKey: { 326 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/old" as ResourceUri, 327 name: "Old Key", 328 content: new Uint8Array(32).toBase64(), 329 }, ··· 332 expect(producer.publicKey?.name).toEqual("Old Key"); 333 334 const newKey: PublicKeyOption = { 335 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/new" as ResourceUri, 336 name: "New Key", 337 content: new Uint8Array(32).toBase64(), 338 };
··· 4 import type { ProducerParams, PublicKeyOption } from "./types.ts"; 5 import type { Client, CredentialManager } from "@atcute/client"; 6 import type { Did, Handle, ResourceUri } from "@atcute/lexicons"; 7 + import type { AppCisternPubkey } from "@cistern/lexicon"; 8 + import type { XRPCProcedures, XRPCQueries } from "@cistern/shared"; 9 10 // Helper to create a mock Producer instance 11 function createMockProducer( ··· 31 } 32 33 // Helper to create a mock RPC client 34 + function createMockRpcClient(): Client<XRPCQueries, XRPCProcedures> { 35 return { 36 get: () => { 37 throw new Error("Mock RPC get not implemented"); ··· 39 post: () => { 40 throw new Error("Mock RPC post not implemented"); 41 }, 42 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 43 } 44 45 Deno.test({ ··· 50 expect(producer.did).toEqual("did:plc:test123"); 51 expect(producer.publicKey).toBeUndefined(); 52 expect(producer.rpc).toBeDefined(); 53 }, 54 }); 55 ··· 57 name: "Producer constructor initializes with existing public key", 58 fn() { 59 const mockPublicKey: PublicKeyOption = { 60 + uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri, 61 name: "Test Key", 62 content: new Uint8Array(32).toBase64(), 63 }; ··· 73 }); 74 75 Deno.test({ 76 + name: "createMemo successfully creates and uploads an encrypted memo", 77 async fn() { 78 const keys = generateKeys(); 79 let capturedRecord: unknown; ··· 92 return Promise.resolve({ 93 ok: true, 94 data: { 95 + uri: "at://did:plc:test/app.cistern.memo/memo123" as ResourceUri, 96 }, 97 }); 98 } 99 return Promise.resolve({ ok: false, status: 500, data: {} }); 100 }, 101 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 102 103 const producer = createMockProducer({ 104 rpc: mockRpc, 105 publicKey: { 106 + uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri, 107 name: "Test Key", 108 content: keys.publicKey.toBase64(), 109 }, 110 }); 111 112 + const uri = await producer.createMemo("Test message"); 113 114 + expect(uri).toEqual("at://did:plc:test/app.cistern.memo/memo123"); 115 + expect(capturedCollection).toEqual("app.cistern.memo"); 116 expect(capturedRecord).toMatchObject({ 117 + $type: "app.cistern.memo", 118 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 119 }); 120 }, 121 }); 122 123 Deno.test({ 124 + name: "createMemo throws when no public key is set", 125 async fn() { 126 const producer = createMockProducer(); 127 128 + await expect(producer.createMemo("Test message")).rejects.toThrow( 129 + "no public key set; select a public key before creating a memo", 130 ); 131 }, 132 }); 133 134 Deno.test({ 135 + name: "createMemo throws when upload fails", 136 async fn() { 137 const keys = generateKeys(); 138 const mockRpc = { ··· 142 status: 500, 143 data: { error: "Internal Server Error" }, 144 }), 145 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 146 147 const producer = createMockProducer({ 148 rpc: mockRpc, 149 publicKey: { 150 + uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri, 151 name: "Test Key", 152 content: keys.publicKey.toBase64(), 153 }, 154 }); 155 156 + await expect(producer.createMemo("Test message")).rejects.toThrow( 157 + "failed to create new memo", 158 ); 159 }, 160 }); ··· 170 data: { 171 records: [ 172 { 173 + uri: "at://did:plc:test/app.cistern.pubkey/key1", 174 value: { 175 + $type: "app.cistern.pubkey", 176 name: "Key 1", 177 algorithm: "x_wing", 178 + content: { $bytes: new Uint8Array(32).toBase64() }, 179 createdAt: new Date().toISOString(), 180 + } as AppCisternPubkey.Main, 181 }, 182 { 183 + uri: "at://did:plc:test/app.cistern.pubkey/key2", 184 value: { 185 + $type: "app.cistern.pubkey", 186 name: "Key 2", 187 algorithm: "x_wing", 188 + content: { $bytes: new Uint8Array(32).toBase64() }, 189 createdAt: new Date().toISOString(), 190 + } as AppCisternPubkey.Main, 191 }, 192 ], 193 cursor: undefined, ··· 196 } 197 return Promise.resolve({ ok: false, status: 500, data: {} }); 198 }, 199 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 200 201 const producer = createMockProducer({ rpc: mockRpc }); 202 ··· 216 async fn() { 217 let callCount = 0; 218 const mockRpc = { 219 + get: (endpoint: string, _params?: { params?: { cursor?: string } }) => { 220 if (endpoint === "com.atproto.repo.listRecords") { 221 callCount++; 222 ··· 226 data: { 227 records: [ 228 { 229 + uri: "at://did:plc:test/app.cistern.pubkey/key1", 230 value: { 231 + $type: "app.cistern.pubkey", 232 name: "Key 1", 233 algorithm: "x_wing", 234 + content: { $bytes: new Uint8Array(32).toBase64() }, 235 createdAt: new Date().toISOString(), 236 + } as AppCisternPubkey.Main, 237 }, 238 ], 239 cursor: "next-page", ··· 245 data: { 246 records: [ 247 { 248 + uri: "at://did:plc:test/app.cistern.pubkey/key2", 249 value: { 250 + $type: "app.cistern.pubkey", 251 name: "Key 2", 252 algorithm: "x_wing", 253 + content: { $bytes: new Uint8Array(32).toBase64() }, 254 createdAt: new Date().toISOString(), 255 + } as AppCisternPubkey.Main, 256 }, 257 ], 258 cursor: undefined, ··· 262 } 263 return Promise.resolve({ ok: false, status: 500, data: {} }); 264 }, 265 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 266 267 const producer = createMockProducer({ rpc: mockRpc }); 268 ··· 288 status: 401, 289 data: { error: "Unauthorized" }, 290 }), 291 + } as unknown as Client<XRPCQueries, XRPCProcedures>; 292 293 const producer = createMockProducer({ rpc: mockRpc }); 294 ··· 303 const producer = createMockProducer(); 304 305 const mockPublicKey: PublicKeyOption = { 306 + uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri, 307 name: "Selected Key", 308 content: new Uint8Array(32).toBase64(), 309 }; ··· 323 fn() { 324 const producer = createMockProducer({ 325 publicKey: { 326 + uri: "at://did:plc:test/app.cistern.pubkey/old" as ResourceUri, 327 name: "Old Key", 328 content: new Uint8Array(32).toBase64(), 329 }, ··· 332 expect(producer.publicKey?.name).toEqual("Old Key"); 333 334 const newKey: PublicKeyOption = { 335 + uri: "at://did:plc:test/app.cistern.pubkey/new" as ResourceUri, 336 name: "New Key", 337 content: new Uint8Array(32).toBase64(), 338 };
+2 -157
packages/producer/mod.ts
··· 1 - import { produceRequirements } from "@cistern/shared"; 2 - import { encryptText } from "@cistern/crypto"; 3 - import type { 4 - ProducerOptions, 5 - ProducerParams, 6 - PublicKeyOption, 7 - } from "./types.ts"; 8 - import { type Did, parse, type ResourceUri } from "@atcute/lexicons"; 9 - import type { Client, CredentialManager } from "@atcute/client"; 10 - import { now } from "@atcute/tid"; 11 - import { 12 - type AppCisternLexiconItem, 13 - AppCisternLexiconPubkey, 14 - } from "@cistern/lexicon"; 15 - 16 - import type {} from "@atcute/atproto"; 17 - 18 - export async function createProducer( 19 - { publicKey: rkey, ...opts }: ProducerOptions, 20 - ): Promise<Producer> { 21 - const reqs = await produceRequirements(opts); 22 - 23 - let publicKey: PublicKeyOption | undefined; 24 - if (rkey) { 25 - const res = await reqs.rpc.get("com.atproto.repo.getRecord", { 26 - params: { 27 - repo: reqs.miniDoc.did, 28 - rkey, 29 - collection: "app.cistern.lexicon.pubkey", 30 - }, 31 - }); 32 - 33 - if (!res.ok) { 34 - throw new Error( 35 - `invalid public key record ID ${publicKey}, got: ${res.status} ${res.data.error}`, 36 - ); 37 - } 38 - 39 - const record = parse(AppCisternLexiconPubkey.mainSchema, res.data.value); 40 - 41 - publicKey = { 42 - uri: res.data.uri, 43 - name: record.name, 44 - content: record.content, 45 - }; 46 - } 47 - 48 - return new Producer({ 49 - ...reqs, 50 - publicKey, 51 - }); 52 - } 53 - 54 - export class Producer { 55 - did: Did; 56 - rpc: Client; 57 - manager: CredentialManager; 58 - publicKey?: PublicKeyOption; 59 - 60 - constructor(params: ProducerParams) { 61 - this.did = params.miniDoc.did; 62 - this.rpc = params.rpc; 63 - this.manager = params.manager; 64 - this.publicKey = params.publicKey; 65 - } 66 - 67 - /** 68 - * Creates an item and saves it as a record in the user's PDS 69 - */ 70 - async createItem(text: string): Promise<ResourceUri> { 71 - if (!this.publicKey) { 72 - throw new Error( 73 - "no public key set; select a public key before creating an item", 74 - ); 75 - } 76 - 77 - const payload = encryptText( 78 - Uint8Array.fromBase64(this.publicKey.content), 79 - text, 80 - ); 81 - const record: AppCisternLexiconItem.Main = { 82 - $type: "app.cistern.lexicon.item", 83 - tid: now(), 84 - algorithm: "x_wing-xchacha20_poly1305-sha3_512", 85 - ciphertext: payload.cipherText, 86 - contentHash: payload.hash, 87 - contentLength: payload.length, 88 - nonce: payload.nonce, 89 - payload: payload.content, 90 - pubkey: this.publicKey.uri, 91 - }; 92 - 93 - const res = await this.rpc.post("com.atproto.repo.createRecord", { 94 - input: { 95 - collection: "app.cistern.lexicon.item", 96 - repo: this.did, 97 - record, 98 - }, 99 - }); 100 - 101 - if (!res.ok) { 102 - throw new Error( 103 - `failed to create new item: ${res.status} ${res.data.error}`, 104 - ); 105 - } 106 - 107 - return res.data.uri; 108 - } 109 - 110 - /** 111 - * Lists public keys registered in the user's PDS 112 - */ 113 - async *listPublicKeys(): AsyncGenerator< 114 - PublicKeyOption, 115 - void, 116 - void 117 - > { 118 - let cursor: string | undefined; 119 - 120 - while (true) { 121 - const res = await this.rpc.get("com.atproto.repo.listRecords", { 122 - params: { 123 - collection: "app.cistern.lexicon.pubkey", 124 - repo: this.did, 125 - cursor, 126 - }, 127 - }); 128 - 129 - if (!res.ok) { 130 - throw new Error( 131 - `failed to list public keys: ${res.status} ${res.data.error}`, 132 - ); 133 - } 134 - 135 - cursor = res.data.cursor; 136 - 137 - for (const record of res.data.records) { 138 - const item = parse(AppCisternLexiconPubkey.mainSchema, record.value); 139 - 140 - yield { 141 - uri: record.uri, 142 - content: item.content, 143 - name: item.name, 144 - }; 145 - } 146 - 147 - if (!cursor) return; 148 - } 149 - } 150 - 151 - /** 152 - * Sets a public key as the main encryption key 153 - */ 154 - selectPublicKey(key: PublicKeyOption) { 155 - this.publicKey = key; 156 - } 157 - }
··· 1 + export * from "./client.ts"; 2 + export * from "./types.ts";
+12 -2
packages/producer/types.ts
··· 1 import type { BaseClientOptions, ClientRequirements } from "@cistern/shared"; 2 - import type { RecordKey, ResourceUri } from "@atcute/lexicons"; 3 4 export interface ProducerOptions extends BaseClientOptions { 5 - publicKey?: RecordKey; 6 } 7 8 export type ProducerParams = ClientRequirements<ProducerOptions> & { 9 publicKey?: PublicKeyOption; 10 }; 11 12 export interface PublicKeyOption { 13 uri: ResourceUri; 14 name: string; 15 content: string; 16 }
··· 1 import type { BaseClientOptions, ClientRequirements } from "@cistern/shared"; 2 + import type { ResourceUri } from "@atcute/lexicons"; 3 4 + /** Credentials and an optional public key, used for deriving `ProducerParams` */ 5 export interface ProducerOptions extends BaseClientOptions { 6 + /** An optional record key to a Cistern public key. Assumed to be within the specified user's PDS, and retrieved before instantiation. You can omit this value if you intend to select a public key later */ 7 + publicKey?: string; 8 } 9 10 + /** Required parameters for constructing a `Producer`. These are automatically created for you in `createProducer` */ 11 export type ProducerParams = ClientRequirements<ProducerOptions> & { 12 + /** Optional public key and its contents */ 13 publicKey?: PublicKeyOption; 14 }; 15 16 + /** A simplified public key, suitable for local storage */ 17 export interface PublicKeyOption { 18 + /** Full AT-URI of this key */ 19 uri: ResourceUri; 20 + 21 + /** Generated friendly name for this public key */ 22 name: string; 23 + 24 + /** The contents of this public key, encoded in base64 */ 25 content: string; 26 }
+6
packages/shared/README.md
···
··· 1 + # @cistern/shared 2 + 3 + Shared authentication utilities for Cistern producer and consumer packages. 4 + 5 + Provides DID resolution via Slingshot and authenticated RPC client creation for 6 + AT Protocol operations.
+6
packages/shared/deno.jsonc
··· 1 { 2 "name": "@cistern/shared", 3 "exports": { 4 ".": "./mod.ts" 5 }, 6 "imports": { 7 "@atcute/client": "npm:@atcute/client@^4.0.5", 8 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2" 9 }
··· 1 { 2 "name": "@cistern/shared", 3 + "version": "1.0.2", 4 + "license": "MIT", 5 "exports": { 6 ".": "./mod.ts" 7 }, 8 + "publish": { 9 + "exclude": ["*.test.ts"] 10 + }, 11 "imports": { 12 + "@atcute/atproto": "npm:@atcute/atproto@^3.1.9", 13 "@atcute/client": "npm:@atcute/client@^4.0.5", 14 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2" 15 }
+5
packages/shared/produce-requirements.ts
··· 1 import { Client, CredentialManager } from "@atcute/client"; 2 import { resolveMiniDoc } from "./resolve-did.ts"; 3 import type { BaseClientOptions, ClientRequirements } from "./types.ts"; 4 5 export async function produceRequirements<Options extends BaseClientOptions>( 6 options: Options, 7 ): Promise<ClientRequirements<Options>> { 8 const miniDoc = await resolveMiniDoc(options.handle); 9 const manager = new CredentialManager({ service: miniDoc.pds }); 10 const rpc = new Client({ handler: manager });
··· 1 import { Client, CredentialManager } from "@atcute/client"; 2 import { resolveMiniDoc } from "./resolve-did.ts"; 3 import type { BaseClientOptions, ClientRequirements } from "./types.ts"; 4 + import { isHandle } from "@atcute/lexicons/syntax"; 5 6 export async function produceRequirements<Options extends BaseClientOptions>( 7 options: Options, 8 ): Promise<ClientRequirements<Options>> { 9 + if (!isHandle(options.handle)) { 10 + throw new Error("provided handle is not valid"); 11 + } 12 + 13 const miniDoc = await resolveMiniDoc(options.handle); 14 const manager = new CredentialManager({ service: miniDoc.pds }); 15 const rpc = new Client({ handler: manager });
+18 -2
packages/shared/types.ts
··· 1 import type { Did, Handle } from "@atcute/lexicons"; 2 import type { Client, CredentialManager } from "@atcute/client"; 3 4 export interface MiniDoc { 5 did: Did; ··· 9 } 10 11 export interface BaseClientOptions { 12 - handle: Handle; 13 appPassword: string; 14 } 15 16 export interface ClientRequirements<Options extends BaseClientOptions> { 17 miniDoc: MiniDoc; 18 manager: CredentialManager; 19 - rpc: Client; 20 options: Options; 21 }
··· 1 import type { Did, Handle } from "@atcute/lexicons"; 2 import type { Client, CredentialManager } from "@atcute/client"; 3 + import type { 4 + ComAtprotoRepoCreateRecord, 5 + ComAtprotoRepoDeleteRecord, 6 + ComAtprotoRepoGetRecord, 7 + ComAtprotoRepoListRecords, 8 + } from "@atcute/atproto"; 9 10 export interface MiniDoc { 11 did: Did; ··· 15 } 16 17 export interface BaseClientOptions { 18 + handle: string; 19 appPassword: string; 20 } 21 22 + export interface XRPCQueries { 23 + "com.atproto.repo.getRecord": ComAtprotoRepoGetRecord.mainSchema; 24 + "com.atproto.repo.listRecords": ComAtprotoRepoListRecords.mainSchema; 25 + } 26 + 27 + export interface XRPCProcedures { 28 + "com.atproto.repo.createRecord": ComAtprotoRepoCreateRecord.mainSchema; 29 + "com.atproto.repo.deleteRecord": ComAtprotoRepoDeleteRecord.mainSchema; 30 + } 31 + 32 export interface ClientRequirements<Options extends BaseClientOptions> { 33 miniDoc: MiniDoc; 34 manager: CredentialManager; 35 + rpc: Client<XRPCQueries, XRPCProcedures>; 36 options: Options; 37 }