+6
.letta/settings.json
+6
.letta/settings.json
+3
.letta/settings.local.json
+3
.letta/settings.local.json
-241
CLAUDE.md
-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
+98
-12
README.md
···
1
1
# Cistern
2
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.
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`
7
46
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.
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
+1
deno.jsonc
+1502
-13
deno.lock
+1502
-13
deno.lock
···
1
1
{
2
2
"version": "5",
3
3
"specifiers": {
4
+
"jsr:@hono/hono@^4.10.5": "4.10.5",
5
+
"jsr:@logtape/logtape@^1.2.0": "1.2.0",
4
6
"jsr:@noble/ciphers@^2.0.1": "2.0.1",
5
7
"jsr:@noble/curves@2.0": "2.0.1",
6
8
"jsr:@noble/hashes@2": "2.0.1",
7
9
"jsr:@noble/hashes@2.0": "2.0.1",
8
10
"jsr:@noble/hashes@^2.0.1": "2.0.1",
9
11
"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:@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",
12
15
"jsr:@std/expect@^1.0.17": "1.0.17",
13
-
"jsr:@std/internal@^1.0.10": "1.0.10",
16
+
"jsr:@std/internal@^1.0.10": "1.0.12",
17
+
"jsr:@std/internal@^1.0.12": "1.0.12",
14
18
"npm:@atcute/atproto@^3.1.9": "3.1.9",
15
19
"npm:@atcute/client@^4.0.5": "4.0.5",
16
20
"npm:@atcute/jetstream@^1.1.2": "1.1.2",
21
+
"npm:@atcute/lex-cli@*": "2.3.1",
17
22
"npm:@atcute/lex-cli@^2.3.1": "2.3.1",
18
23
"npm:@atcute/lexicons@^1.2.2": "1.2.2",
19
24
"npm:@atcute/tid@^1.0.3": "1.0.3",
20
-
"npm:@atproto/lexicon@~0.5.1": "0.5.1"
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"
21
31
},
22
32
"jsr": {
33
+
"@hono/hono@4.10.5": {
34
+
"integrity": "13dbf2a528feb8189ad13394b213f0cf5f83b0ba4b2fadd0549993426db9ad2d"
35
+
},
36
+
"@logtape/logtape@1.2.0": {
37
+
"integrity": "8e1d3af5c91966cc5689cfb17081a36bccfdff28ff6314769185661f5147e74d"
38
+
},
23
39
"@noble/ciphers@2.0.1": {
24
40
"integrity": "1d28df773a29684c85844d27eefbb7cad3e4ce62849b63dae3024baf66cf769f"
25
41
},
···
39
55
"jsr:@noble/hashes@2.0"
40
56
]
41
57
},
42
-
"@puregarlic/randimal@1.0.1": {
43
-
"integrity": "101da9e89561f4b8038426f47f6cb258484a11887e1f4d8a2b07cd5ebf487a5d"
58
+
"@puregarlic/randimal@1.1.1": {
59
+
"integrity": "4e1fa61982cf2f610e9ad851d0fd0ff7bc3bb7b7a3c6cccae59f5ae2e68a7e47"
44
60
},
45
-
"@std/assert@1.0.14": {
46
-
"integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4",
61
+
"@std/assert@1.0.15": {
62
+
"integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b",
47
63
"dependencies": [
48
-
"jsr:@std/internal"
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"
49
71
]
50
72
},
51
73
"@std/expect@1.0.17": {
52
74
"integrity": "316b47dd65c33e3151344eb3267bf42efba17d1415425f07ed96185d67fc04d9",
53
75
"dependencies": [
54
76
"jsr:@std/assert",
55
-
"jsr:@std/internal"
77
+
"jsr:@std/internal@^1.0.10"
56
78
]
57
79
},
58
80
"@std/internal@1.0.10": {
59
81
"integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7"
82
+
},
83
+
"@std/internal@1.0.12": {
84
+
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
60
85
}
61
86
},
62
87
"npm": {
···
145
170
"@badrap/valita@0.4.6": {
146
171
"integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="
147
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
+
},
148
216
"@mary-ext/event-iterator@1.0.0": {
149
217
"integrity": "sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ==",
150
218
"dependencies": [
···
154
222
"@mary-ext/simple-event-emitter@1.0.0": {
155
223
"integrity": "sha512-meA/zJZKIN1RVBNEYIbjufkUrW7/tRjHH60FjolpG1ixJKo76TB208qefQLNdOVDA7uIG0CGEDuhmMirtHKLAg=="
156
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
+
},
157
310
"@optique/core@0.6.2": {
158
311
"integrity": "sha512-HTxIHJ8xLOSZotiU6Zc5BCJv+SJ8DMYmuiQM+7tjF7RolJn/pdZNe7M78G3+DgXL9lIf82l8aGcilmgVYRQnGQ=="
159
312
},
···
163
316
"@optique/core"
164
317
]
165
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
+
},
166
681
"@standard-schema/spec@1.0.0": {
167
682
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="
168
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
+
},
169
976
"esm-env@1.2.2": {
170
977
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
978
+
},
979
+
"etag@1.8.1": {
980
+
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
171
981
},
172
982
"event-target-polyfill@0.0.4": {
173
983
"integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="
174
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
+
},
175
1095
"graphemer@1.4.0": {
176
1096
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
177
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
+
},
178
1164
"iso-datestring-validator@2.2.2": {
179
1165
"integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="
180
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
+
},
181
1231
"multiformats@9.9.0": {
182
1232
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="
183
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
+
},
184
1267
"partysocket@1.1.6": {
185
1268
"integrity": "sha512-LkEk8N9hMDDsDT0iDK0zuwUDFVrVMUXFXCeN3850Ng8wtjPqPBeJlwdeY6ROlJSEh3tPoTTasXoSBYH76y118w==",
186
1269
"dependencies": [
187
1270
"event-target-polyfill"
188
1271
]
189
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
+
},
190
1285
"picocolors@1.1.1": {
191
1286
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
192
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
+
},
193
1294
"prettier@3.6.2": {
194
1295
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
195
1296
"bin": true
196
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
+
},
197
1584
"type-fest@4.41.0": {
198
1585
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="
199
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
+
},
200
1599
"uint8arrays@3.0.0": {
201
1600
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
202
1601
"dependencies": [
203
1602
"multiformats"
204
1603
]
205
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
+
},
206
1680
"yocto-queue@1.2.1": {
207
1681
"integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="
208
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
+
},
209
1689
"zod@3.25.76": {
210
1690
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
211
1691
}
···
218
1698
"members": {
219
1699
"packages/consumer": {
220
1700
"dependencies": [
221
-
"jsr:@puregarlic/randimal@^1.0.1",
1701
+
"jsr:@puregarlic/randimal@^1.1.1",
222
1702
"jsr:@std/expect@^1.0.17",
223
-
"npm:@atcute/atproto@^3.1.9",
224
1703
"npm:@atcute/client@^4.0.5",
225
1704
"npm:@atcute/jetstream@^1.1.2",
226
1705
"npm:@atcute/lexicons@^1.2.2",
···
242
1721
"npm:@atproto/lexicon@~0.5.1"
243
1722
]
244
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
+
},
245
1734
"packages/producer": {
246
1735
"dependencies": [
247
1736
"jsr:@std/expect@^1.0.17",
248
-
"npm:@atcute/atproto@^3.1.9",
249
1737
"npm:@atcute/client@^4.0.5",
250
1738
"npm:@atcute/lexicons@^1.2.2",
251
1739
"npm:@atcute/tid@^1.0.3"
···
253
1741
},
254
1742
"packages/shared": {
255
1743
"dependencies": [
1744
+
"npm:@atcute/atproto@^3.1.9",
256
1745
"npm:@atcute/client@^4.0.5",
257
1746
"npm:@atcute/lexicons@^1.2.2"
258
1747
]
+52
-52
e2e.test.ts
+52
-52
e2e.test.ts
···
29
29
let consumer: Awaited<ReturnType<typeof createConsumer>>;
30
30
let producer: Awaited<ReturnType<typeof createProducer>>;
31
31
let keypair: Awaited<ReturnType<typeof consumer.generateKeyPair>>;
32
-
let itemUri: string;
32
+
let memoUri: string;
33
33
let testMessage: string;
34
34
35
35
await t.step("Create consumer", async () => {
···
47
47
48
48
expect(keypair.privateKey).toBeInstanceOf(Uint8Array);
49
49
expect(keypair.publicKey).toBeDefined();
50
-
expect(keypair.publicKey).toContain("app.cistern.lexicon.pubkey");
50
+
expect(keypair.publicKey).toContain("app.cistern.pubkey");
51
51
});
52
52
53
53
try {
···
63
63
expect(producer.publicKey?.uri).toEqual(keypair.publicKey);
64
64
});
65
65
66
-
await t.step("Create encrypted item", async () => {
66
+
await t.step("Create encrypted memo", async () => {
67
67
testMessage = `E2E Test - ${new Date().toISOString()}`;
68
-
itemUri = await producer.createItem(testMessage);
68
+
memoUri = await producer.createMemo(testMessage);
69
69
70
-
expect(itemUri).toBeDefined();
71
-
expect(itemUri).toContain("app.cistern.lexicon.item");
70
+
expect(memoUri).toBeDefined();
71
+
expect(memoUri).toContain("app.cistern.memo");
72
72
});
73
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);
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
78
}
79
79
80
-
expect(items.length).toBeGreaterThan(0);
80
+
expect(memos.length).toBeGreaterThan(0);
81
81
82
-
const ourItem = items.find((item) => item.text === testMessage);
83
-
expect(ourItem).toBeDefined();
84
-
expect(ourItem!.text).toEqual(testMessage);
82
+
const ourMemo = memos.find((memo) => memo.text === testMessage);
83
+
expect(ourMemo).toBeDefined();
84
+
expect(ourMemo!.text).toEqual(testMessage);
85
85
});
86
86
87
-
await t.step("Delete item", async () => {
88
-
const itemRkey = itemUri.split("/").pop()!;
89
-
await consumer.deleteItem(itemRkey);
87
+
await t.step("Delete memo", async () => {
88
+
const memoRkey = memoUri.split("/").pop()!;
89
+
await consumer.deleteMemo(memoRkey);
90
90
91
91
// Verify deletion
92
-
const itemsAfterDelete = [];
93
-
for await (const item of consumer.listItems()) {
94
-
itemsAfterDelete.push(item);
92
+
const memosAfterDelete = [];
93
+
for await (const memo of consumer.listMemos()) {
94
+
memosAfterDelete.push(memo);
95
95
}
96
96
97
-
const deletedItem = itemsAfterDelete.find(
98
-
(item) => item.text === testMessage,
97
+
const deletedMemo = memosAfterDelete.find(
98
+
(memo) => memo.text === testMessage,
99
99
);
100
-
expect(deletedItem).toBeUndefined();
100
+
expect(deletedMemo).toBeUndefined();
101
101
});
102
102
103
103
await t.step("List public keys", async () => {
···
118
118
119
119
const res = await consumer.rpc.post("com.atproto.repo.deleteRecord", {
120
120
input: {
121
-
collection: "app.cistern.lexicon.pubkey",
121
+
collection: "app.cistern.pubkey",
122
122
repo: consumer.did,
123
123
rkey: publicKeyRkey,
124
124
},
···
131
131
});
132
132
133
133
Deno.test({
134
-
name: "E2E: Multiple items with same keypair",
134
+
name: "E2E: Multiple memos with same keypair",
135
135
ignore: SKIP_E2E,
136
136
async fn(t) {
137
137
const handle = Deno.env.get("CISTERN_HANDLE") as Handle;
···
141
141
let producer: Awaited<ReturnType<typeof createProducer>>;
142
142
let keypair: Awaited<ReturnType<typeof consumer.generateKeyPair>>;
143
143
let messages: string[];
144
-
let itemUris: string[];
144
+
let memoUris: string[];
145
145
146
146
await t.step("Create consumer and generate keypair", async () => {
147
147
consumer = await createConsumer({
···
167
167
expect(producer.publicKey?.uri).toEqual(keypair.publicKey);
168
168
});
169
169
170
-
await t.step("Create multiple encrypted items", async () => {
170
+
await t.step("Create multiple encrypted memos", async () => {
171
171
messages = [
172
-
`E2E Item 1 - ${new Date().toISOString()}`,
173
-
`E2E Item 2 - ${new Date().toISOString()}`,
174
-
`E2E Item 3 - ${new Date().toISOString()}`,
172
+
`E2E Memo 1 - ${new Date().toISOString()}`,
173
+
`E2E Memo 2 - ${new Date().toISOString()}`,
174
+
`E2E Memo 3 - ${new Date().toISOString()}`,
175
175
];
176
176
177
-
itemUris = [];
177
+
memoUris = [];
178
178
for (const message of messages) {
179
-
const uri = await producer.createItem(message);
180
-
itemUris.push(uri);
179
+
const uri = await producer.createMemo(message);
180
+
memoUris.push(uri);
181
181
}
182
182
183
-
expect(itemUris).toHaveLength(3);
183
+
expect(memoUris).toHaveLength(3);
184
184
});
185
185
186
-
await t.step("Decrypt all items", async () => {
187
-
const items = [];
188
-
for await (const item of consumer.listItems()) {
189
-
items.push(item);
186
+
await t.step("Decrypt all memos", async () => {
187
+
const memos = [];
188
+
for await (const memo of consumer.listMemos()) {
189
+
memos.push(memo);
190
190
}
191
191
192
-
expect(items.length).toBeGreaterThanOrEqual(3);
192
+
expect(memos.length).toBeGreaterThanOrEqual(3);
193
193
194
194
// Verify all test messages are present
195
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);
196
+
const memo = memos.find((m) => m.text === message);
197
+
expect(memo).toBeDefined();
198
+
expect(memo!.text).toEqual(message);
199
199
}
200
200
});
201
201
202
-
await t.step("Cleanup: Delete test items", async () => {
203
-
for (const uri of itemUris) {
202
+
await t.step("Cleanup: Delete test memos", async () => {
203
+
for (const uri of memoUris) {
204
204
const rkey = uri.split("/").pop()!;
205
-
await consumer.deleteItem(rkey);
205
+
await consumer.deleteMemo(rkey);
206
206
}
207
207
208
-
// Verify all items deleted
209
-
const remainingItems = [];
210
-
for await (const item of consumer.listItems()) {
211
-
remainingItems.push(item);
208
+
// Verify all memos deleted
209
+
const remainingMemos = [];
210
+
for await (const memo of consumer.listMemos()) {
211
+
remainingMemos.push(memo);
212
212
}
213
213
214
214
for (const message of messages) {
215
-
const item = remainingItems.find((i) => i.text === message);
216
-
expect(item).toBeUndefined();
215
+
const memo = remainingMemos.find((m) => m.text === message);
216
+
expect(memo).toBeUndefined();
217
217
}
218
218
});
219
219
} finally {
···
222
222
223
223
const res = await consumer.rpc.post("com.atproto.repo.deleteRecord", {
224
224
input: {
225
-
collection: "app.cistern.lexicon.pubkey",
225
+
collection: "app.cistern.pubkey",
226
226
repo: consumer.did,
227
227
rkey: publicKeyRkey,
228
228
},
+52
-5
packages/consumer/README.md
+52
-5
packages/consumer/README.md
···
1
-
# `@cistern/consumer`
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
+
```
2
37
3
-
The Consumer module is responsible for the following:
38
+
### List Memos (Polling)
4
39
5
-
- Generating key pairs
6
-
- Retrieving and decrypting items
7
-
- Subscribing to Jetstream to monitor for items
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
+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
+6
-2
packages/consumer/deno.jsonc
···
1
1
{
2
2
"name": "@cistern/consumer",
3
+
"version": "1.0.3",
4
+
"license": "MIT",
3
5
"exports": {
4
6
".": "./mod.ts"
5
7
},
8
+
"publish": {
9
+
"exclude": ["*.test.ts"]
10
+
},
6
11
"imports": {
7
-
"@atcute/atproto": "npm:@atcute/atproto@^3.1.9",
8
12
"@atcute/client": "npm:@atcute/client@^4.0.5",
9
13
"@atcute/jetstream": "npm:@atcute/jetstream@^1.1.2",
10
14
"@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2",
11
15
"@atcute/tid": "npm:@atcute/tid@^1.0.3",
12
-
"@puregarlic/randimal": "jsr:@puregarlic/randimal@^1.0.1",
16
+
"@puregarlic/randimal": "jsr:@puregarlic/randimal@^1.1.1",
13
17
"@std/expect": "jsr:@std/expect@^1.0.17"
14
18
}
15
19
}
+91
-95
packages/consumer/mod.test.ts
+91
-95
packages/consumer/mod.test.ts
···
5
5
import type { Client, CredentialManager } from "@atcute/client";
6
6
import type { Did, Handle, ResourceUri } from "@atcute/lexicons";
7
7
import { now } from "@atcute/tid";
8
+
import type { AppCisternMemo } from "@cistern/lexicon";
9
+
import type { XRPCProcedures, XRPCQueries } from "@cistern/shared";
8
10
9
11
// Helper to create a mock Consumer instance
10
12
function createMockConsumer(
···
30
32
}
31
33
32
34
// Helper to create a mock RPC client
33
-
function createMockRpcClient(): Client {
35
+
function createMockRpcClient(): Client<XRPCQueries, XRPCProcedures> {
34
36
return {
35
37
get: () => {
36
38
throw new Error("Mock RPC get not implemented");
···
38
40
post: () => {
39
41
throw new Error("Mock RPC post not implemented");
40
42
},
41
-
} as unknown as Client;
43
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
42
44
}
43
45
44
46
Deno.test({
···
49
51
expect(consumer.did).toEqual("did:plc:test123");
50
52
expect(consumer.keypair).toBeUndefined();
51
53
expect(consumer.rpc).toBeDefined();
52
-
expect(consumer.manager).toBeDefined();
53
54
},
54
55
});
55
56
···
58
59
fn() {
59
60
const mockKeypair = {
60
61
privateKey: new Uint8Array(32).toBase64(),
61
-
publicKey:
62
-
"at://did:plc:test/app.cistern.lexicon.pubkey/abc123" as ResourceUri,
62
+
publicKey: "at://did:plc:test/app.cistern.pubkey/abc123" as ResourceUri,
63
63
};
64
64
65
65
const consumer = createMockConsumer({
···
95
95
return Promise.resolve({
96
96
ok: true,
97
97
data: {
98
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/generated123",
98
+
uri: "at://did:plc:test/app.cistern.pubkey/generated123",
99
99
},
100
100
});
101
101
}
102
102
return Promise.resolve({ ok: false, status: 500, data: {} });
103
103
},
104
-
} as unknown as Client;
104
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
105
105
106
106
const consumer = createMockConsumer({ rpc: mockRpc });
107
107
const keypair = await consumer.generateKeyPair();
···
109
109
expect(keypair).toBeDefined();
110
110
expect(keypair.privateKey).toBeInstanceOf(Uint8Array);
111
111
expect(keypair.publicKey).toEqual(
112
-
"at://did:plc:test/app.cistern.lexicon.pubkey/generated123",
112
+
"at://did:plc:test/app.cistern.pubkey/generated123",
113
113
);
114
114
expect(consumer.keypair).toEqual(keypair);
115
115
116
-
expect(capturedCollection).toEqual("app.cistern.lexicon.pubkey");
116
+
expect(capturedCollection).toEqual("app.cistern.pubkey");
117
117
expect(capturedRecord).toMatchObject({
118
-
$type: "app.cistern.lexicon.pubkey",
118
+
$type: "app.cistern.pubkey",
119
119
algorithm: "x_wing",
120
120
});
121
121
},
···
131
131
keypair: {
132
132
privateKey: new Uint8Array(32).toBase64(),
133
133
publicKey:
134
-
"at://did:plc:test/app.cistern.lexicon.pubkey/existing" as ResourceUri,
134
+
"at://did:plc:test/app.cistern.pubkey/existing" as ResourceUri,
135
135
},
136
136
},
137
137
});
···
152
152
status: 500,
153
153
data: { error: "Internal Server Error" },
154
154
}),
155
-
} as unknown as Client;
155
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
156
156
157
157
const consumer = createMockConsumer({ rpc: mockRpc });
158
158
···
163
163
});
164
164
165
165
Deno.test({
166
-
name: "listItems throws when no keypair is set",
166
+
name: "listMemos throws when no keypair is set",
167
167
async fn() {
168
168
const consumer = createMockConsumer();
169
169
170
-
const iterator = consumer.listItems();
170
+
const iterator = consumer.listMemos();
171
171
await expect(iterator.next()).rejects.toThrow(
172
-
"no key pair set; generate a key before listing items",
172
+
"no key pair set; generate a key before listing memos",
173
173
);
174
174
},
175
175
});
176
176
177
177
Deno.test({
178
-
name: "listItems decrypts and yields items",
178
+
name: "listMemos decrypts and yields memos",
179
179
async fn() {
180
180
const keys = generateKeys();
181
-
const testText = "Test item content";
181
+
const testText = "Test memo content";
182
182
const encrypted = encryptText(keys.publicKey, testText);
183
183
const testTid = now();
184
184
···
190
190
data: {
191
191
records: [
192
192
{
193
-
uri: "at://did:plc:test/app.cistern.lexicon.item/item1",
193
+
uri: "at://did:plc:test/app.cistern.memo/memo1",
194
194
value: {
195
-
$type: "app.cistern.lexicon.item",
195
+
$type: "app.cistern.memo",
196
196
tid: testTid,
197
-
ciphertext: encrypted.cipherText,
198
-
nonce: encrypted.nonce,
197
+
ciphertext: { $bytes: encrypted.cipherText },
198
+
nonce: { $bytes: encrypted.nonce },
199
199
algorithm: "x_wing-xchacha20_poly1305-sha3_512",
200
-
pubkey: "at://did:plc:test/app.cistern.lexicon.pubkey/key1",
201
-
payload: encrypted.content,
200
+
pubkey: "at://did:plc:test/app.cistern.pubkey/key1",
201
+
payload: { $bytes: encrypted.content },
202
202
contentLength: encrypted.length,
203
-
contentHash: encrypted.hash,
204
-
},
203
+
contentHash: { $bytes: encrypted.hash },
204
+
} as AppCisternMemo.Main,
205
205
},
206
206
],
207
207
cursor: undefined,
···
210
210
}
211
211
return Promise.resolve({ ok: false, status: 500, data: {} });
212
212
},
213
-
} as unknown as Client;
213
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
214
214
215
215
const consumer = createMockConsumer({
216
216
rpc: mockRpc,
···
219
219
appPassword: "test-password",
220
220
keypair: {
221
221
privateKey: keys.secretKey.toBase64(),
222
-
publicKey:
223
-
"at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
222
+
publicKey: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri,
224
223
},
225
224
},
226
225
});
227
226
228
-
const items = [];
229
-
for await (const item of consumer.listItems()) {
230
-
items.push(item);
227
+
const memos = [];
228
+
for await (const memo of consumer.listMemos()) {
229
+
memos.push(memo);
231
230
}
232
231
233
-
expect(items).toHaveLength(1);
234
-
expect(items[0].text).toEqual(testText);
235
-
expect(items[0].tid).toEqual(testTid);
232
+
expect(memos).toHaveLength(1);
233
+
expect(memos[0].text).toEqual(testText);
234
+
expect(memos[0].tid).toEqual(testTid);
236
235
},
237
236
});
238
237
239
238
Deno.test({
240
-
name: "listItems skips items with mismatched public key",
239
+
name: "listmemos skips memos with mismatched public key",
241
240
async fn() {
242
241
const keys = generateKeys();
243
-
const testText = "Test item content";
242
+
const testText = "Test memo content";
244
243
const encrypted = encryptText(keys.publicKey, testText);
245
244
const testTid = now();
246
245
···
252
251
data: {
253
252
records: [
254
253
{
255
-
uri: "at://did:plc:test/app.cistern.lexicon.item/item1",
254
+
uri: "at://did:plc:test/app.cistern.memo/memo1",
256
255
value: {
257
-
$type: "app.cistern.lexicon.item",
256
+
$type: "app.cistern.memo",
258
257
tid: testTid,
259
-
ciphertext: encrypted.cipherText,
260
-
nonce: encrypted.nonce,
258
+
ciphertext: { $bytes: encrypted.cipherText },
259
+
nonce: { $bytes: encrypted.nonce },
261
260
algorithm: "x_wing-xchacha20_poly1305-sha3_512",
262
261
pubkey:
263
-
"at://did:plc:test/app.cistern.lexicon.pubkey/different-key",
264
-
payload: encrypted.content,
262
+
"at://did:plc:test/app.cistern.pubkey/different-key",
263
+
payload: { $bytes: encrypted.content },
265
264
contentLength: encrypted.length,
266
-
contentHash: encrypted.hash,
267
-
},
265
+
contentHash: { $bytes: encrypted.hash },
266
+
} as AppCisternMemo.Main,
268
267
},
269
268
],
270
269
cursor: undefined,
···
273
272
}
274
273
return Promise.resolve({ ok: false, status: 500, data: {} });
275
274
},
276
-
} as unknown as Client;
275
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
277
276
278
277
const consumer = createMockConsumer({
279
278
rpc: mockRpc,
···
283
282
keypair: {
284
283
privateKey: keys.secretKey.toBase64(),
285
284
publicKey:
286
-
"at://did:plc:test/app.cistern.lexicon.pubkey/my-key" as ResourceUri,
285
+
"at://did:plc:test/app.cistern.pubkey/my-key" as ResourceUri,
287
286
},
288
287
},
289
288
});
290
289
291
-
const items = [];
292
-
for await (const item of consumer.listItems()) {
293
-
items.push(item);
290
+
const memos = [];
291
+
for await (const memo of consumer.listMemos()) {
292
+
memos.push(memo);
294
293
}
295
294
296
-
expect(items).toHaveLength(0);
295
+
expect(memos).toHaveLength(0);
297
296
},
298
297
});
299
298
300
299
Deno.test({
301
-
name: "listItems handles pagination",
300
+
name: "listMemos handles pagination",
302
301
async fn() {
303
302
const keys = generateKeys();
304
-
const text1 = "First item";
305
-
const text2 = "Second item";
303
+
const text1 = "First memo";
304
+
const text2 = "Second memo";
306
305
const encrypted1 = encryptText(keys.publicKey, text1);
307
306
const encrypted2 = encryptText(keys.publicKey, text2);
308
307
const tid1 = now();
···
320
319
data: {
321
320
records: [
322
321
{
323
-
uri: "at://did:plc:test/app.cistern.lexicon.item/item1",
322
+
uri: "at://did:plc:test/app.cistern.memo/memo1",
324
323
value: {
325
-
$type: "app.cistern.lexicon.item",
324
+
$type: "app.cistern.memo",
326
325
tid: tid1,
327
-
ciphertext: encrypted1.cipherText,
328
-
nonce: encrypted1.nonce,
326
+
ciphertext: { $bytes: encrypted1.cipherText },
327
+
nonce: { $bytes: encrypted1.nonce },
329
328
algorithm: "x_wing-xchacha20_poly1305-sha3_512",
330
-
pubkey:
331
-
"at://did:plc:test/app.cistern.lexicon.pubkey/key1",
332
-
payload: encrypted1.content,
329
+
pubkey: "at://did:plc:test/app.cistern.pubkey/key1",
330
+
payload: { $bytes: encrypted1.content },
333
331
contentLength: encrypted1.length,
334
-
contentHash: encrypted1.hash,
335
-
},
332
+
contentHash: { $bytes: encrypted1.hash },
333
+
} as AppCisternMemo.Main,
336
334
},
337
335
],
338
336
cursor: "next-page",
···
344
342
data: {
345
343
records: [
346
344
{
347
-
uri: "at://did:plc:test/app.cistern.lexicon.item/item2",
345
+
uri: "at://did:plc:test/app.cistern.memo/memo2",
348
346
value: {
349
-
$type: "app.cistern.lexicon.item",
347
+
$type: "app.cistern.memo",
350
348
tid: tid2,
351
-
ciphertext: encrypted2.cipherText,
352
-
nonce: encrypted2.nonce,
349
+
ciphertext: { $bytes: encrypted2.cipherText },
350
+
nonce: { $bytes: encrypted2.nonce },
353
351
algorithm: "x_wing-xchacha20_poly1305-sha3_512",
354
-
pubkey:
355
-
"at://did:plc:test/app.cistern.lexicon.pubkey/key1",
356
-
payload: encrypted2.content,
352
+
pubkey: "at://did:plc:test/app.cistern.pubkey/key1",
353
+
payload: { $bytes: encrypted2.content },
357
354
contentLength: encrypted2.length,
358
-
contentHash: encrypted2.hash,
359
-
},
355
+
contentHash: { $bytes: encrypted2.hash },
356
+
} as AppCisternMemo.Main,
360
357
},
361
358
],
362
359
cursor: undefined,
···
366
363
}
367
364
return Promise.resolve({ ok: false, status: 500, data: {} });
368
365
},
369
-
} as unknown as Client;
366
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
370
367
371
368
const consumer = createMockConsumer({
372
369
rpc: mockRpc,
···
375
372
appPassword: "test-password",
376
373
keypair: {
377
374
privateKey: keys.secretKey.toBase64(),
378
-
publicKey:
379
-
"at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
375
+
publicKey: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri,
380
376
},
381
377
},
382
378
});
383
379
384
-
const items = [];
385
-
for await (const item of consumer.listItems()) {
386
-
items.push(item);
380
+
const memos = [];
381
+
for await (const memo of consumer.listMemos()) {
382
+
memos.push(memo);
387
383
}
388
384
389
-
expect(items).toHaveLength(2);
390
-
expect(items[0].text).toEqual(text1);
391
-
expect(items[1].text).toEqual(text2);
385
+
expect(memos).toHaveLength(2);
386
+
expect(memos[0].text).toEqual(text1);
387
+
expect(memos[1].text).toEqual(text2);
392
388
expect(callCount).toEqual(2);
393
389
},
394
390
});
395
391
396
392
Deno.test({
397
-
name: "listItems throws when list request fails",
393
+
name: "listMemos throws when list request fails",
398
394
async fn() {
399
395
const mockRpc = {
400
396
get: () =>
···
403
399
status: 401,
404
400
data: { error: "Unauthorized" },
405
401
}),
406
-
} as unknown as Client;
402
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
407
403
408
404
const consumer = createMockConsumer({
409
405
rpc: mockRpc,
···
412
408
appPassword: "test-password",
413
409
keypair: {
414
410
privateKey: new Uint8Array(32).toBase64(),
415
-
publicKey: "at://did:plc:test/app.cistern.lexicon.pubkey/key1",
411
+
publicKey: "at://did:plc:test/app.cistern.pubkey/key1",
416
412
},
417
413
},
418
414
});
419
415
420
-
const iterator = consumer.listItems();
421
-
await expect(iterator.next()).rejects.toThrow("failed to list items");
416
+
const iterator = consumer.listMemos();
417
+
await expect(iterator.next()).rejects.toThrow("failed to list memos");
422
418
},
423
419
});
424
420
425
421
Deno.test({
426
-
name: "subscribeToItems throws when no keypair is set",
422
+
name: "subscribeToMemos throws when no keypair is set",
427
423
async fn() {
428
424
const consumer = createMockConsumer();
429
425
430
-
const iterator = consumer.subscribeToItems();
426
+
const iterator = consumer.subscribeToMemos();
431
427
await expect(iterator.next()).rejects.toThrow(
432
428
"no key pair set; generate a key before subscribing",
433
429
);
···
435
431
});
436
432
437
433
Deno.test({
438
-
name: "deleteItem successfully deletes an item",
434
+
name: "deleteMemo successfully deletes a memo",
439
435
async fn() {
440
436
let deletedRkey: string | undefined;
441
437
···
452
448
}
453
449
return Promise.resolve({ ok: false, status: 500, data: {} });
454
450
},
455
-
} as unknown as Client;
451
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
456
452
457
453
const consumer = createMockConsumer({ rpc: mockRpc });
458
454
459
-
await consumer.deleteItem("item123");
455
+
await consumer.deleteMemo("memo123");
460
456
461
-
expect(deletedRkey).toEqual("item123");
457
+
expect(deletedRkey).toEqual("memo123");
462
458
},
463
459
});
464
460
465
461
Deno.test({
466
-
name: "deleteItem throws when delete request fails",
462
+
name: "deleteMemo throws when delete request fails",
467
463
async fn() {
468
464
const mockRpc = {
469
465
post: () =>
···
472
468
status: 404,
473
469
data: { error: "Not Found" },
474
470
}),
475
-
} as unknown as Client;
471
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
476
472
477
473
const consumer = createMockConsumer({ rpc: mockRpc });
478
474
479
-
await expect(consumer.deleteItem("item123")).rejects.toThrow(
480
-
"failed to delete item item123",
475
+
await expect(consumer.deleteMemo("memo123")).rejects.toThrow(
476
+
"failed to delete memo memo123",
481
477
);
482
478
},
483
479
});
+2
-210
packages/consumer/mod.ts
+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
+25
-3
packages/consumer/types.ts
···
1
1
import type { BaseClientOptions, ClientRequirements } from "@cistern/shared";
2
-
import type { ResourceUri, Tid } from "@atcute/lexicons";
2
+
import type { RecordKey, ResourceUri, Tid } from "@atcute/lexicons";
3
3
4
+
/**
5
+
* A locally-stored key pair suitable for storage
6
+
*/
4
7
export interface InputLocalKeyPair {
8
+
/** An X-Wing private key, encoded in base64 */
5
9
privateKey: string;
6
-
publicKey: ResourceUri;
10
+
11
+
/** An AT URI to the `app.cistern.pubkey` record derived from this private key */
12
+
publicKey: string;
7
13
}
8
14
15
+
/**
16
+
* InputLocalKeyPair, with `privateKey` decoded to a Uint8Array
17
+
*/
9
18
export interface LocalKeyPair {
19
+
/** An X-Wing private key in raw byte format */
10
20
privateKey: Uint8Array;
21
+
22
+
/** An AT URI to the `app.cistern.pubkey` record derived from this private key */
11
23
publicKey: ResourceUri;
12
24
}
13
25
26
+
/** Credentials and optional keypair for creating a Consumer client */
14
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 */
15
29
keypair?: InputLocalKeyPair;
16
30
}
17
31
32
+
/** Asynchronously-acquired parameters required to construct a Client. `createConsumer` will translate from `ConsumerOptions` to `ConsumerParams` for you */
18
33
export type ConsumerParams = ClientRequirements<ConsumerOptions>;
19
34
20
-
export interface DecryptedItem {
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 */
21
41
tid: Tid;
42
+
43
+
/** The original, decrypted contents of the memo */
22
44
text: string;
23
45
}
+11
packages/crypto/README.md
+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
+5
packages/crypto/deno.jsonc
+10
packages/lexicon/README.md
+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
+5
packages/lexicon/deno.jsonc
···
1
1
{
2
2
"name": "@cistern/lexicon",
3
+
"version": "1.0.0",
4
+
"license": "MIT",
3
5
"exports": {
4
6
".": "./mod.ts"
7
+
},
8
+
"publish": {
9
+
"exclude": ["*.test.ts"]
5
10
},
6
11
"tasks": {
7
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
-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
-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
+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
+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
+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
-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
-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
+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
+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> {}
+182
packages/mcp/README.md
+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
+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
+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
+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
+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
+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
+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
+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
+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
+5
-1
packages/producer/deno.jsonc
···
1
1
{
2
2
"name": "@cistern/producer",
3
+
"version": "1.0.2",
4
+
"license": "MIT",
3
5
"exports": {
4
6
".": "./mod.ts"
5
7
},
8
+
"publish": {
9
+
"exclude": ["*.test.ts"]
10
+
},
6
11
"imports": {
7
-
"@atcute/atproto": "npm:@atcute/atproto@^3.1.9",
8
12
"@atcute/client": "npm:@atcute/client@^4.0.5",
9
13
"@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2",
10
14
"@atcute/tid": "npm:@atcute/tid@^1.0.3",
+44
-44
packages/producer/mod.test.ts
+44
-44
packages/producer/mod.test.ts
···
4
4
import type { ProducerParams, PublicKeyOption } from "./types.ts";
5
5
import type { Client, CredentialManager } from "@atcute/client";
6
6
import type { Did, Handle, ResourceUri } from "@atcute/lexicons";
7
+
import type { AppCisternPubkey } from "@cistern/lexicon";
8
+
import type { XRPCProcedures, XRPCQueries } from "@cistern/shared";
7
9
8
10
// Helper to create a mock Producer instance
9
11
function createMockProducer(
···
29
31
}
30
32
31
33
// Helper to create a mock RPC client
32
-
function createMockRpcClient(): Client {
34
+
function createMockRpcClient(): Client<XRPCQueries, XRPCProcedures> {
33
35
return {
34
36
get: () => {
35
37
throw new Error("Mock RPC get not implemented");
···
37
39
post: () => {
38
40
throw new Error("Mock RPC post not implemented");
39
41
},
40
-
} as unknown as Client;
42
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
41
43
}
42
44
43
45
Deno.test({
···
48
50
expect(producer.did).toEqual("did:plc:test123");
49
51
expect(producer.publicKey).toBeUndefined();
50
52
expect(producer.rpc).toBeDefined();
51
-
expect(producer.manager).toBeDefined();
52
53
},
53
54
});
54
55
···
56
57
name: "Producer constructor initializes with existing public key",
57
58
fn() {
58
59
const mockPublicKey: PublicKeyOption = {
59
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
60
+
uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri,
60
61
name: "Test Key",
61
62
content: new Uint8Array(32).toBase64(),
62
63
};
···
72
73
});
73
74
74
75
Deno.test({
75
-
name: "createItem successfully creates and uploads an encrypted item",
76
+
name: "createMemo successfully creates and uploads an encrypted memo",
76
77
async fn() {
77
78
const keys = generateKeys();
78
79
let capturedRecord: unknown;
···
91
92
return Promise.resolve({
92
93
ok: true,
93
94
data: {
94
-
uri:
95
-
"at://did:plc:test/app.cistern.lexicon.item/item123" as ResourceUri,
95
+
uri: "at://did:plc:test/app.cistern.memo/memo123" as ResourceUri,
96
96
},
97
97
});
98
98
}
99
99
return Promise.resolve({ ok: false, status: 500, data: {} });
100
100
},
101
-
} as unknown as Client;
101
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
102
102
103
103
const producer = createMockProducer({
104
104
rpc: mockRpc,
105
105
publicKey: {
106
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
106
+
uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri,
107
107
name: "Test Key",
108
108
content: keys.publicKey.toBase64(),
109
109
},
110
110
});
111
111
112
-
const uri = await producer.createItem("Test message");
112
+
const uri = await producer.createMemo("Test message");
113
113
114
-
expect(uri).toEqual("at://did:plc:test/app.cistern.lexicon.item/item123");
115
-
expect(capturedCollection).toEqual("app.cistern.lexicon.item");
114
+
expect(uri).toEqual("at://did:plc:test/app.cistern.memo/memo123");
115
+
expect(capturedCollection).toEqual("app.cistern.memo");
116
116
expect(capturedRecord).toMatchObject({
117
-
$type: "app.cistern.lexicon.item",
117
+
$type: "app.cistern.memo",
118
118
algorithm: "x_wing-xchacha20_poly1305-sha3_512",
119
119
});
120
120
},
121
121
});
122
122
123
123
Deno.test({
124
-
name: "createItem throws when no public key is set",
124
+
name: "createMemo throws when no public key is set",
125
125
async fn() {
126
126
const producer = createMockProducer();
127
127
128
-
await expect(producer.createItem("Test message")).rejects.toThrow(
129
-
"no public key set; select a public key before creating an item",
128
+
await expect(producer.createMemo("Test message")).rejects.toThrow(
129
+
"no public key set; select a public key before creating a memo",
130
130
);
131
131
},
132
132
});
133
133
134
134
Deno.test({
135
-
name: "createItem throws when upload fails",
135
+
name: "createMemo throws when upload fails",
136
136
async fn() {
137
137
const keys = generateKeys();
138
138
const mockRpc = {
···
142
142
status: 500,
143
143
data: { error: "Internal Server Error" },
144
144
}),
145
-
} as unknown as Client;
145
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
146
146
147
147
const producer = createMockProducer({
148
148
rpc: mockRpc,
149
149
publicKey: {
150
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
150
+
uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri,
151
151
name: "Test Key",
152
152
content: keys.publicKey.toBase64(),
153
153
},
154
154
});
155
155
156
-
await expect(producer.createItem("Test message")).rejects.toThrow(
157
-
"failed to create new item",
156
+
await expect(producer.createMemo("Test message")).rejects.toThrow(
157
+
"failed to create new memo",
158
158
);
159
159
},
160
160
});
···
170
170
data: {
171
171
records: [
172
172
{
173
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1",
173
+
uri: "at://did:plc:test/app.cistern.pubkey/key1",
174
174
value: {
175
-
$type: "app.cistern.lexicon.pubkey",
175
+
$type: "app.cistern.pubkey",
176
176
name: "Key 1",
177
177
algorithm: "x_wing",
178
-
content: new Uint8Array(32).toBase64(),
178
+
content: { $bytes: new Uint8Array(32).toBase64() },
179
179
createdAt: new Date().toISOString(),
180
-
},
180
+
} as AppCisternPubkey.Main,
181
181
},
182
182
{
183
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key2",
183
+
uri: "at://did:plc:test/app.cistern.pubkey/key2",
184
184
value: {
185
-
$type: "app.cistern.lexicon.pubkey",
185
+
$type: "app.cistern.pubkey",
186
186
name: "Key 2",
187
187
algorithm: "x_wing",
188
-
content: new Uint8Array(32).toBase64(),
188
+
content: { $bytes: new Uint8Array(32).toBase64() },
189
189
createdAt: new Date().toISOString(),
190
-
},
190
+
} as AppCisternPubkey.Main,
191
191
},
192
192
],
193
193
cursor: undefined,
···
196
196
}
197
197
return Promise.resolve({ ok: false, status: 500, data: {} });
198
198
},
199
-
} as unknown as Client;
199
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
200
200
201
201
const producer = createMockProducer({ rpc: mockRpc });
202
202
···
216
216
async fn() {
217
217
let callCount = 0;
218
218
const mockRpc = {
219
-
get: (endpoint: string, params?: { params?: { cursor?: string } }) => {
219
+
get: (endpoint: string, _params?: { params?: { cursor?: string } }) => {
220
220
if (endpoint === "com.atproto.repo.listRecords") {
221
221
callCount++;
222
222
···
226
226
data: {
227
227
records: [
228
228
{
229
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1",
229
+
uri: "at://did:plc:test/app.cistern.pubkey/key1",
230
230
value: {
231
-
$type: "app.cistern.lexicon.pubkey",
231
+
$type: "app.cistern.pubkey",
232
232
name: "Key 1",
233
233
algorithm: "x_wing",
234
-
content: new Uint8Array(32).toBase64(),
234
+
content: { $bytes: new Uint8Array(32).toBase64() },
235
235
createdAt: new Date().toISOString(),
236
-
},
236
+
} as AppCisternPubkey.Main,
237
237
},
238
238
],
239
239
cursor: "next-page",
···
245
245
data: {
246
246
records: [
247
247
{
248
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key2",
248
+
uri: "at://did:plc:test/app.cistern.pubkey/key2",
249
249
value: {
250
-
$type: "app.cistern.lexicon.pubkey",
250
+
$type: "app.cistern.pubkey",
251
251
name: "Key 2",
252
252
algorithm: "x_wing",
253
-
content: new Uint8Array(32).toBase64(),
253
+
content: { $bytes: new Uint8Array(32).toBase64() },
254
254
createdAt: new Date().toISOString(),
255
-
},
255
+
} as AppCisternPubkey.Main,
256
256
},
257
257
],
258
258
cursor: undefined,
···
262
262
}
263
263
return Promise.resolve({ ok: false, status: 500, data: {} });
264
264
},
265
-
} as unknown as Client;
265
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
266
266
267
267
const producer = createMockProducer({ rpc: mockRpc });
268
268
···
288
288
status: 401,
289
289
data: { error: "Unauthorized" },
290
290
}),
291
-
} as unknown as Client;
291
+
} as unknown as Client<XRPCQueries, XRPCProcedures>;
292
292
293
293
const producer = createMockProducer({ rpc: mockRpc });
294
294
···
303
303
const producer = createMockProducer();
304
304
305
305
const mockPublicKey: PublicKeyOption = {
306
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
306
+
uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri,
307
307
name: "Selected Key",
308
308
content: new Uint8Array(32).toBase64(),
309
309
};
···
323
323
fn() {
324
324
const producer = createMockProducer({
325
325
publicKey: {
326
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/old" as ResourceUri,
326
+
uri: "at://did:plc:test/app.cistern.pubkey/old" as ResourceUri,
327
327
name: "Old Key",
328
328
content: new Uint8Array(32).toBase64(),
329
329
},
···
332
332
expect(producer.publicKey?.name).toEqual("Old Key");
333
333
334
334
const newKey: PublicKeyOption = {
335
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/new" as ResourceUri,
335
+
uri: "at://did:plc:test/app.cistern.pubkey/new" as ResourceUri,
336
336
name: "New Key",
337
337
content: new Uint8Array(32).toBase64(),
338
338
};
+2
-157
packages/producer/mod.ts
+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
+12
-2
packages/producer/types.ts
···
1
1
import type { BaseClientOptions, ClientRequirements } from "@cistern/shared";
2
-
import type { RecordKey, ResourceUri } from "@atcute/lexicons";
2
+
import type { ResourceUri } from "@atcute/lexicons";
3
3
4
+
/** Credentials and an optional public key, used for deriving `ProducerParams` */
4
5
export interface ProducerOptions extends BaseClientOptions {
5
-
publicKey?: RecordKey;
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;
6
8
}
7
9
10
+
/** Required parameters for constructing a `Producer`. These are automatically created for you in `createProducer` */
8
11
export type ProducerParams = ClientRequirements<ProducerOptions> & {
12
+
/** Optional public key and its contents */
9
13
publicKey?: PublicKeyOption;
10
14
};
11
15
16
+
/** A simplified public key, suitable for local storage */
12
17
export interface PublicKeyOption {
18
+
/** Full AT-URI of this key */
13
19
uri: ResourceUri;
20
+
21
+
/** Generated friendly name for this public key */
14
22
name: string;
23
+
24
+
/** The contents of this public key, encoded in base64 */
15
25
content: string;
16
26
}