Encrypted, ephemeral, private memos on atproto

refactor: rename items to memos, flatten lexicon names

graham.systems 668022c5 804583d7

verified
Changed files
+256 -244
packages
consumer
lexicon
lexicons
app
src
types
app
cistern
producer
+32 -8
README.md
··· 1 1 # Cistern 2 2 3 - Cistern is a private, end-to-end encrypted quick-capture system built on AT Protocol. Items are encrypted using post-quantum cryptography and stored temporarily in your Personal Data Server (PDS), then automatically retrieved and deleted after consumption. 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. 4 7 5 - The system bridges the gap between where ideas are captured and where they are stored long-term. Create an encrypted item on your phone, and it automatically appears in your desktop application, decrypted and ready to use. 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. 6 11 7 12 ## Architecture 8 13 9 14 Cistern is a Deno monorepo consisting of five packages: 10 15 11 16 ### `@cistern/crypto` 12 - Core cryptographic operations using post-quantum algorithms. Implements X-Wing key encapsulation with XChaCha20-Poly1305 authenticated encryption and SHA3-512 integrity verification. Handles keypair generation, encryption, and decryption. 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. 13 21 14 22 ### `@cistern/lexicon` 15 - AT Protocol schema definitions for Cistern record types. Defines `app.cistern.lexicon.pubkey` (public key records) and `app.cistern.lexicon.item` (encrypted item records). Includes code generation from JSON schemas. 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. 16 27 17 28 ### `@cistern/shared` 18 - Common utilities and authentication logic. Handles DID resolution via Slingshot service and creates authenticated RPC clients using app passwords. 29 + 30 + Common utilities and authentication logic. Handles DID resolution via Slingshot 31 + service and creates authenticated RPC clients using app passwords. 19 32 20 33 ### `@cistern/producer` 21 - Creates and encrypts items for storage. Manages public key selection, encrypts plaintext content, and uploads encrypted items to the PDS as AT Protocol records. 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. 22 38 23 39 ### `@cistern/consumer` 24 - Retrieves, decrypts, and deletes items. Generates keypairs, manages private keys locally, retrieves items via polling or real-time streaming (Jetstream), and handles item deletion after consumption. 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. 25 44 26 45 ## Security Model 27 46 28 - Private keys never leave the consumer device. Public keys are stored in the PDS as records, while private keys remain off-protocol. Only the holder of the matching private key can decrypt items encrypted with the corresponding public key. 47 + Private keys never leave the consumer device. Public keys are stored in the PDS 48 + as records, while private keys remain off-protocol. Only the holder of the 49 + matching private key can decrypt memos encrypted with the corresponding public 50 + key. 29 51 30 52 ## Testing 31 53 32 54 Run all unit tests: 55 + 33 56 ```bash 34 57 deno test --allow-env 35 58 ``` 36 59 37 60 Run end-to-end tests (requires AT Protocol credentials): 61 + 38 62 ```bash 39 63 CISTERN_HANDLE="your.bsky.social" \ 40 64 CISTERN_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" \
+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 },
+10 -10
packages/consumer/README.md
··· 1 1 # @cistern/consumer 2 2 3 - Consumer client for retrieving, decrypting, and deleting Cistern items. 3 + Consumer client for retrieving, decrypting, and deleting Cistern memos. 4 4 5 5 ## Usage 6 6 ··· 29 29 handle: "user.bsky.social", 30 30 appPassword: "xxxx-xxxx-xxxx-xxxx", 31 31 keypair: { 32 - publicKey: "at://did:plc:abc123/app.cistern.lexicon.pubkey/3jzfcijpj2z", 32 + publicKey: "at://did:plc:abc123/app.cistern.pubkey/3jzfcijpj2z", 33 33 privateKey: "base64-encoded-private-key", 34 34 }, 35 35 }); 36 36 ``` 37 37 38 - ### List Items (Polling) 38 + ### List Memos (Polling) 39 39 40 40 ```typescript 41 - for await (const item of consumer.listItems()) { 42 - console.log(`[${item.tid}] ${item.text}`); 43 - await consumer.deleteItem(item.tid); 41 + for await (const memo of consumer.listMemos()) { 42 + console.log(`[${memo.tid}] ${memo.text}`); 43 + await consumer.deleteMemo(memo.tid); 44 44 } 45 45 ``` 46 46 47 - ### Subscribe to Items (Real-time) 47 + ### Subscribe to Memos (Real-time) 48 48 49 49 ```typescript 50 - for await (const item of consumer.subscribeToItems()) { 51 - console.log(`[${item.tid}] ${item.text}`); 52 - await consumer.deleteItem(item.tid); 50 + for await (const memo of consumer.subscribeToMemos()) { 51 + console.log(`[${memo.tid}] ${memo.text}`); 52 + await consumer.deleteMemo(memo.tid); 53 53 } 54 54 ```
+64 -69
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 { AppCisternLexiconItem } from "@cistern/lexicon"; 8 + import type { AppCisternMemo } from "@cistern/lexicon"; 9 9 10 10 // Helper to create a mock Consumer instance 11 11 function createMockConsumer( ··· 59 59 fn() { 60 60 const mockKeypair = { 61 61 privateKey: new Uint8Array(32).toBase64(), 62 - publicKey: 63 - "at://did:plc:test/app.cistern.lexicon.pubkey/abc123" as ResourceUri, 62 + publicKey: "at://did:plc:test/app.cistern.pubkey/abc123" as ResourceUri, 64 63 }; 65 64 66 65 const consumer = createMockConsumer({ ··· 96 95 return Promise.resolve({ 97 96 ok: true, 98 97 data: { 99 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/generated123", 98 + uri: "at://did:plc:test/app.cistern.pubkey/generated123", 100 99 }, 101 100 }); 102 101 } ··· 110 109 expect(keypair).toBeDefined(); 111 110 expect(keypair.privateKey).toBeInstanceOf(Uint8Array); 112 111 expect(keypair.publicKey).toEqual( 113 - "at://did:plc:test/app.cistern.lexicon.pubkey/generated123", 112 + "at://did:plc:test/app.cistern.pubkey/generated123", 114 113 ); 115 114 expect(consumer.keypair).toEqual(keypair); 116 115 117 - expect(capturedCollection).toEqual("app.cistern.lexicon.pubkey"); 116 + expect(capturedCollection).toEqual("app.cistern.pubkey"); 118 117 expect(capturedRecord).toMatchObject({ 119 - $type: "app.cistern.lexicon.pubkey", 118 + $type: "app.cistern.pubkey", 120 119 algorithm: "x_wing", 121 120 }); 122 121 }, ··· 132 131 keypair: { 133 132 privateKey: new Uint8Array(32).toBase64(), 134 133 publicKey: 135 - "at://did:plc:test/app.cistern.lexicon.pubkey/existing" as ResourceUri, 134 + "at://did:plc:test/app.cistern.pubkey/existing" as ResourceUri, 136 135 }, 137 136 }, 138 137 }); ··· 164 163 }); 165 164 166 165 Deno.test({ 167 - name: "listItems throws when no keypair is set", 166 + name: "listMemos throws when no keypair is set", 168 167 async fn() { 169 168 const consumer = createMockConsumer(); 170 169 171 - const iterator = consumer.listItems(); 170 + const iterator = consumer.listMemos(); 172 171 await expect(iterator.next()).rejects.toThrow( 173 - "no key pair set; generate a key before listing items", 172 + "no key pair set; generate a key before listing memos", 174 173 ); 175 174 }, 176 175 }); 177 176 178 177 Deno.test({ 179 - name: "listItems decrypts and yields items", 178 + name: "listMemos decrypts and yields memos", 180 179 async fn() { 181 180 const keys = generateKeys(); 182 - const testText = "Test item content"; 181 + const testText = "Test memo content"; 183 182 const encrypted = encryptText(keys.publicKey, testText); 184 183 const testTid = now(); 185 184 ··· 191 190 data: { 192 191 records: [ 193 192 { 194 - uri: "at://did:plc:test/app.cistern.lexicon.item/item1", 193 + uri: "at://did:plc:test/app.cistern.memo/memo1", 195 194 value: { 196 - $type: "app.cistern.lexicon.item", 195 + $type: "app.cistern.memo", 197 196 tid: testTid, 198 197 ciphertext: { $bytes: encrypted.cipherText }, 199 198 nonce: { $bytes: encrypted.nonce }, 200 199 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 201 - pubkey: "at://did:plc:test/app.cistern.lexicon.pubkey/key1", 200 + pubkey: "at://did:plc:test/app.cistern.pubkey/key1", 202 201 payload: { $bytes: encrypted.content }, 203 202 contentLength: encrypted.length, 204 203 contentHash: { $bytes: encrypted.hash }, 205 - } as AppCisternLexiconItem.Main, 204 + } as AppCisternMemo.Main, 206 205 }, 207 206 ], 208 207 cursor: undefined, ··· 220 219 appPassword: "test-password", 221 220 keypair: { 222 221 privateKey: keys.secretKey.toBase64(), 223 - publicKey: 224 - "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri, 222 + publicKey: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri, 225 223 }, 226 224 }, 227 225 }); 228 226 229 - const items = []; 230 - for await (const item of consumer.listItems()) { 231 - items.push(item); 227 + const memos = []; 228 + for await (const memo of consumer.listMemos()) { 229 + memos.push(memo); 232 230 } 233 231 234 - expect(items).toHaveLength(1); 235 - expect(items[0].text).toEqual(testText); 236 - 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); 237 235 }, 238 236 }); 239 237 240 238 Deno.test({ 241 - name: "listItems skips items with mismatched public key", 239 + name: "listmemos skips memos with mismatched public key", 242 240 async fn() { 243 241 const keys = generateKeys(); 244 - const testText = "Test item content"; 242 + const testText = "Test memo content"; 245 243 const encrypted = encryptText(keys.publicKey, testText); 246 244 const testTid = now(); 247 245 ··· 253 251 data: { 254 252 records: [ 255 253 { 256 - uri: "at://did:plc:test/app.cistern.lexicon.item/item1", 254 + uri: "at://did:plc:test/app.cistern.memo/memo1", 257 255 value: { 258 - $type: "app.cistern.lexicon.item", 256 + $type: "app.cistern.memo", 259 257 tid: testTid, 260 258 ciphertext: { $bytes: encrypted.cipherText }, 261 259 nonce: { $bytes: encrypted.nonce }, 262 260 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 263 261 pubkey: 264 - "at://did:plc:test/app.cistern.lexicon.pubkey/different-key", 262 + "at://did:plc:test/app.cistern.pubkey/different-key", 265 263 payload: { $bytes: encrypted.content }, 266 264 contentLength: encrypted.length, 267 265 contentHash: { $bytes: encrypted.hash }, 268 - } as AppCisternLexiconItem.Main, 266 + } as AppCisternMemo.Main, 269 267 }, 270 268 ], 271 269 cursor: undefined, ··· 284 282 keypair: { 285 283 privateKey: keys.secretKey.toBase64(), 286 284 publicKey: 287 - "at://did:plc:test/app.cistern.lexicon.pubkey/my-key" as ResourceUri, 285 + "at://did:plc:test/app.cistern.pubkey/my-key" as ResourceUri, 288 286 }, 289 287 }, 290 288 }); 291 289 292 - const items = []; 293 - for await (const item of consumer.listItems()) { 294 - items.push(item); 290 + const memos = []; 291 + for await (const memo of consumer.listMemos()) { 292 + memos.push(memo); 295 293 } 296 294 297 - expect(items).toHaveLength(0); 295 + expect(memos).toHaveLength(0); 298 296 }, 299 297 }); 300 298 301 299 Deno.test({ 302 - name: "listItems handles pagination", 300 + name: "listMemos handles pagination", 303 301 async fn() { 304 302 const keys = generateKeys(); 305 - const text1 = "First item"; 306 - const text2 = "Second item"; 303 + const text1 = "First memo"; 304 + const text2 = "Second memo"; 307 305 const encrypted1 = encryptText(keys.publicKey, text1); 308 306 const encrypted2 = encryptText(keys.publicKey, text2); 309 307 const tid1 = now(); ··· 321 319 data: { 322 320 records: [ 323 321 { 324 - uri: "at://did:plc:test/app.cistern.lexicon.item/item1", 322 + uri: "at://did:plc:test/app.cistern.memo/memo1", 325 323 value: { 326 - $type: "app.cistern.lexicon.item", 324 + $type: "app.cistern.memo", 327 325 tid: tid1, 328 326 ciphertext: { $bytes: encrypted1.cipherText }, 329 327 nonce: { $bytes: encrypted1.nonce }, 330 328 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 331 - pubkey: 332 - "at://did:plc:test/app.cistern.lexicon.pubkey/key1", 329 + pubkey: "at://did:plc:test/app.cistern.pubkey/key1", 333 330 payload: { $bytes: encrypted1.content }, 334 331 contentLength: encrypted1.length, 335 332 contentHash: { $bytes: encrypted1.hash }, 336 - } as AppCisternLexiconItem.Main, 333 + } as AppCisternMemo.Main, 337 334 }, 338 335 ], 339 336 cursor: "next-page", ··· 345 342 data: { 346 343 records: [ 347 344 { 348 - uri: "at://did:plc:test/app.cistern.lexicon.item/item2", 345 + uri: "at://did:plc:test/app.cistern.memo/memo2", 349 346 value: { 350 - $type: "app.cistern.lexicon.item", 347 + $type: "app.cistern.memo", 351 348 tid: tid2, 352 349 ciphertext: { $bytes: encrypted2.cipherText }, 353 350 nonce: { $bytes: encrypted2.nonce }, 354 351 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 355 - pubkey: 356 - "at://did:plc:test/app.cistern.lexicon.pubkey/key1", 352 + pubkey: "at://did:plc:test/app.cistern.pubkey/key1", 357 353 payload: { $bytes: encrypted2.content }, 358 354 contentLength: encrypted2.length, 359 355 contentHash: { $bytes: encrypted2.hash }, 360 - } as AppCisternLexiconItem.Main, 356 + } as AppCisternMemo.Main, 361 357 }, 362 358 ], 363 359 cursor: undefined, ··· 376 372 appPassword: "test-password", 377 373 keypair: { 378 374 privateKey: keys.secretKey.toBase64(), 379 - publicKey: 380 - "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri, 375 + publicKey: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri, 381 376 }, 382 377 }, 383 378 }); 384 379 385 - const items = []; 386 - for await (const item of consumer.listItems()) { 387 - items.push(item); 380 + const memos = []; 381 + for await (const memo of consumer.listMemos()) { 382 + memos.push(memo); 388 383 } 389 384 390 - expect(items).toHaveLength(2); 391 - expect(items[0].text).toEqual(text1); 392 - 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); 393 388 expect(callCount).toEqual(2); 394 389 }, 395 390 }); 396 391 397 392 Deno.test({ 398 - name: "listItems throws when list request fails", 393 + name: "listMemos throws when list request fails", 399 394 async fn() { 400 395 const mockRpc = { 401 396 get: () => ··· 413 408 appPassword: "test-password", 414 409 keypair: { 415 410 privateKey: new Uint8Array(32).toBase64(), 416 - publicKey: "at://did:plc:test/app.cistern.lexicon.pubkey/key1", 411 + publicKey: "at://did:plc:test/app.cistern.pubkey/key1", 417 412 }, 418 413 }, 419 414 }); 420 415 421 - const iterator = consumer.listItems(); 422 - 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"); 423 418 }, 424 419 }); 425 420 426 421 Deno.test({ 427 - name: "subscribeToItems throws when no keypair is set", 422 + name: "subscribeToMemos throws when no keypair is set", 428 423 async fn() { 429 424 const consumer = createMockConsumer(); 430 425 431 - const iterator = consumer.subscribeToItems(); 426 + const iterator = consumer.subscribeToMemos(); 432 427 await expect(iterator.next()).rejects.toThrow( 433 428 "no key pair set; generate a key before subscribing", 434 429 ); ··· 436 431 }); 437 432 438 433 Deno.test({ 439 - name: "deleteItem successfully deletes an item", 434 + name: "deleteMemo successfully deletes a memo", 440 435 async fn() { 441 436 let deletedRkey: string | undefined; 442 437 ··· 457 452 458 453 const consumer = createMockConsumer({ rpc: mockRpc }); 459 454 460 - await consumer.deleteItem("item123"); 455 + await consumer.deleteMemo("memo123"); 461 456 462 - expect(deletedRkey).toEqual("item123"); 457 + expect(deletedRkey).toEqual("memo123"); 463 458 }, 464 459 }); 465 460 466 461 Deno.test({ 467 - name: "deleteItem throws when delete request fails", 462 + name: "deleteMemo throws when delete request fails", 468 463 async fn() { 469 464 const mockRpc = { 470 465 post: () => ··· 477 472 478 473 const consumer = createMockConsumer({ rpc: mockRpc }); 479 474 480 - await expect(consumer.deleteItem("item123")).rejects.toThrow( 481 - "failed to delete item item123", 475 + await expect(consumer.deleteMemo("memo123")).rejects.toThrow( 476 + "failed to delete memo memo123", 482 477 ); 483 478 }, 484 479 });
+29 -32
packages/consumer/mod.ts
··· 5 5 import { JetstreamSubscription } from "@atcute/jetstream"; 6 6 import type { Did } from "@atcute/lexicons/syntax"; 7 7 import type { Client, CredentialManager } from "@atcute/client"; 8 - import { 9 - AppCisternLexiconItem, 10 - type AppCisternLexiconPubkey, 11 - } from "@cistern/lexicon"; 8 + import { AppCisternMemo, type AppCisternPubkey } from "@cistern/lexicon"; 12 9 import type { 13 10 ConsumerOptions, 14 11 ConsumerParams, 15 - DecryptedItem, 12 + DecryptedMemo, 16 13 LocalKeyPair, 17 14 } from "./types.ts"; 18 15 ··· 27 24 } 28 25 29 26 /** 30 - * Client for generating keys and decoding Cistern items. 27 + * Client for generating keys and decoding Cistern memos. 31 28 */ 32 29 export class Consumer { 33 30 did: Did; ··· 58 55 const keys = generateKeys(); 59 56 const name = await generateRandomName(); 60 57 61 - const record: AppCisternLexiconPubkey.Main = { 62 - $type: "app.cistern.lexicon.pubkey", 58 + const record: AppCisternPubkey.Main = { 59 + $type: "app.cistern.pubkey", 63 60 name, 64 61 algorithm: "x_wing", 65 62 content: { $bytes: keys.publicKey.toBase64() }, ··· 67 64 }; 68 65 const res = await this.rpc.post("com.atproto.repo.createRecord", { 69 66 input: { 70 - collection: "app.cistern.lexicon.pubkey", 67 + collection: "app.cistern.pubkey", 71 68 repo: this.did, 72 69 record, 73 70 }, ··· 90 87 } 91 88 92 89 /** 93 - * Asynchronously iterate through items in the user's PDS 90 + * Asynchronously iterate through memos in the user's PDS 94 91 */ 95 - async *listItems(): AsyncGenerator< 96 - DecryptedItem, 92 + async *listMemos(): AsyncGenerator< 93 + DecryptedMemo, 97 94 void, 98 95 undefined 99 96 > { 100 97 if (!this.keypair) { 101 - throw new Error("no key pair set; generate a key before listing items"); 98 + throw new Error("no key pair set; generate a key before listing memos"); 102 99 } 103 100 104 101 let cursor: string | undefined; ··· 106 103 while (true) { 107 104 const res = await this.rpc.get("com.atproto.repo.listRecords", { 108 105 params: { 109 - collection: "app.cistern.lexicon.item", 106 + collection: "app.cistern.memo", 110 107 repo: this.did, 111 108 cursor, 112 109 }, ··· 114 111 115 112 if (!res.ok) { 116 113 throw new Error( 117 - `failed to list items: ${res.status} ${res.data.error}`, 114 + `failed to list memos: ${res.status} ${res.data.error}`, 118 115 ); 119 116 } 120 117 121 118 cursor = res.data.cursor; 122 119 123 120 for (const record of res.data.records) { 124 - const item = parse(AppCisternLexiconItem.mainSchema, record.value); 121 + const memo = parse(AppCisternMemo.mainSchema, record.value); 125 122 126 - if (item.pubkey !== this.keypair.publicKey) continue; 123 + if (memo.pubkey !== this.keypair.publicKey) continue; 127 124 128 125 const decrypted = decryptText(this.keypair.privateKey, { 129 - nonce: item.nonce.$bytes, 130 - cipherText: item.ciphertext.$bytes, 131 - content: item.payload.$bytes, 132 - hash: item.contentHash.$bytes, 133 - length: item.contentLength, 126 + nonce: memo.nonce.$bytes, 127 + cipherText: memo.ciphertext.$bytes, 128 + content: memo.payload.$bytes, 129 + hash: memo.contentHash.$bytes, 130 + length: memo.contentLength, 134 131 }); 135 132 136 133 yield { 137 - tid: item.tid, 134 + tid: memo.tid, 138 135 text: decrypted, 139 136 }; 140 137 } ··· 144 141 } 145 142 146 143 /** 147 - * Subscribes to the Jetstreams for the user's items. Pass `"stop"` into `subscription.next(...)` to cancel 144 + * Subscribes to the Jetstreams for the user's memos. Pass `"stop"` into `subscription.next(...)` to cancel 148 145 * @todo Allow specifying Jetstream endpoint 149 146 */ 150 - async *subscribeToItems(): AsyncGenerator< 151 - DecryptedItem, 147 + async *subscribeToMemos(): AsyncGenerator< 148 + DecryptedMemo, 152 149 void, 153 150 "stop" | undefined 154 151 > { ··· 158 155 159 156 const subscription = new JetstreamSubscription({ 160 157 url: "wss://jetstream2.us-east.bsky.network", 161 - wantedCollections: ["app.cistern.lexicon.item"], 158 + wantedCollections: ["app.cistern.memo"], 162 159 wantedDids: [this.did], 163 160 }); 164 161 ··· 166 163 if (event.kind === "commit" && event.commit.operation === "create") { 167 164 const record = event.commit.record; 168 165 169 - if (!is(AppCisternLexiconItem.mainSchema, record)) { 166 + if (!is(AppCisternMemo.mainSchema, record)) { 170 167 continue; 171 168 } 172 169 ··· 190 187 } 191 188 192 189 /** 193 - * Deletes an item from the user's PDS by record key. 190 + * Deletes a memo from the user's PDS by record key. 194 191 */ 195 - async deleteItem(key: RecordKey) { 192 + async deleteMemo(key: RecordKey) { 196 193 const res = await this.rpc.post("com.atproto.repo.deleteRecord", { 197 194 input: { 198 - collection: "app.cistern.lexicon.item", 195 + collection: "app.cistern.memo", 199 196 repo: this.did, 200 197 rkey: key, 201 198 }, ··· 203 200 204 201 if (!res.ok) { 205 202 throw new Error( 206 - `failed to delete item ${key}: ${res.status} ${res.data.error}`, 203 + `failed to delete memo ${key}: ${res.status} ${res.data.error}`, 207 204 ); 208 205 } 209 206 }
+1 -1
packages/consumer/types.ts
··· 17 17 18 18 export type ConsumerParams = ClientRequirements<ConsumerOptions>; 19 19 20 - export interface DecryptedItem { 20 + export interface DecryptedMemo { 21 21 tid: Tid; 22 22 text: string; 23 23 }
+4 -4
packages/lexicon/README.md
··· 4 4 5 5 ## Record Types 6 6 7 - | Collection | Description | 8 - |------------|-------------| 9 - | `app.cistern.lexicon.pubkey` | Public key records with human-readable names, referenced by items via AT-URI | 10 - | `app.cistern.lexicon.item` | Encrypted item records containing ciphertext, nonce, algorithm metadata, and public key reference | 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 |
+4 -4
packages/lexicon/lexicons/app/cistern/lexicon/item.json packages/lexicon/lexicons/app/cistern/memo.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "app.cistern.lexicon.item", 3 + "id": "app.cistern.memo", 4 4 "description": "An encrypted memo intended to be accessed and deleted later.", 5 5 "defs": { 6 6 "main": { ··· 21 21 "properties": { 22 22 "tid": { 23 23 "type": "string", 24 - "description": "TID representing when this item was created", 24 + "description": "TID representing when this memo was created", 25 25 "format": "tid" 26 26 }, 27 27 "ciphertext": { ··· 39 39 }, 40 40 "pubkey": { 41 41 "type": "string", 42 - "description": "URI to the public key used to encrypt this item", 42 + "description": "URI to the public key used to encrypt this memo", 43 43 "format": "at-uri" 44 44 }, 45 45 "payload": { 46 46 "type": "bytes", 47 - "description": "Encrypted item contents" 47 + "description": "Encrypted memo contents" 48 48 }, 49 49 "contentLength": { 50 50 "type": "integer",
+3 -3
packages/lexicon/lexicons/app/cistern/lexicon/pubkey.json packages/lexicon/lexicons/app/cistern/pubkey.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "app.cistern.lexicon.pubkey", 4 - "description": "A public key used to encrypt Cistern items", 3 + "id": "app.cistern.pubkey", 4 + "description": "A public key used to encrypt Cistern memos", 5 5 "defs": { 6 6 "main": { 7 7 "type": "record", 8 - "description": "A public key used to encrypt Cistern items", 8 + "description": "A public key used to encrypt Cistern memos", 9 9 "record": { 10 10 "type": "object", 11 11 "required": ["name", "algorithm", "content", "createdAt"],
+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";
+5 -5
packages/lexicon/src/types/app/cistern/lexicon/item.ts packages/lexicon/src/types/app/cistern/memo.ts
··· 5 5 const _mainSchema = /*#__PURE__*/ v.record( 6 6 /*#__PURE__*/ v.string(), 7 7 /*#__PURE__*/ v.object({ 8 - $type: /*#__PURE__*/ v.literal("app.cistern.lexicon.item"), 8 + $type: /*#__PURE__*/ v.literal("app.cistern.memo"), 9 9 /** 10 10 * Algorithm used for encryption, in <kem>-<cipher>-<hash> format. 11 11 */ ··· 29 29 */ 30 30 nonce: /*#__PURE__*/ v.bytes(), 31 31 /** 32 - * Encrypted item contents 32 + * Encrypted memo contents 33 33 */ 34 34 payload: /*#__PURE__*/ v.bytes(), 35 35 /** 36 - * URI to the public key used to encrypt this item 36 + * URI to the public key used to encrypt this memo 37 37 */ 38 38 pubkey: /*#__PURE__*/ v.resourceUriString(), 39 39 /** 40 - * TID representing when this item was created 40 + * TID representing when this memo was created 41 41 */ 42 42 tid: /*#__PURE__*/ v.tidString(), 43 43 }), ··· 53 53 54 54 declare module "@atcute/lexicons/ambient" { 55 55 interface Records { 56 - "app.cistern.lexicon.item": mainSchema; 56 + "app.cistern.memo": mainSchema; 57 57 } 58 58 }
+2 -2
packages/lexicon/src/types/app/cistern/lexicon/pubkey.ts packages/lexicon/src/types/app/cistern/pubkey.ts
··· 5 5 const _mainSchema = /*#__PURE__*/ v.record( 6 6 /*#__PURE__*/ v.string(), 7 7 /*#__PURE__*/ v.object({ 8 - $type: /*#__PURE__*/ v.literal("app.cistern.lexicon.pubkey"), 8 + $type: /*#__PURE__*/ v.literal("app.cistern.pubkey"), 9 9 /** 10 10 * KEM algorithm used to generate this key 11 11 */ ··· 35 35 36 36 declare module "@atcute/lexicons/ambient" { 37 37 interface Records { 38 - "app.cistern.lexicon.pubkey": mainSchema; 38 + "app.cistern.pubkey": mainSchema; 39 39 } 40 40 }
+3 -3
packages/producer/README.md
··· 1 1 # @cistern/producer 2 2 3 - Producer client for creating and encrypting Cistern items. 3 + Producer client for creating and encrypting Cistern memos. 4 4 5 5 ## Usage 6 6 ··· 18 18 19 19 producer.selectPublicKey(pubkey); 20 20 21 - const itemUri = await producer.createItem("Hello, world!"); 21 + const memoUri = await producer.createMemo("Hello, world!"); 22 22 ``` 23 23 24 24 Or, if you already have a public key record ID: ··· 30 30 publicKey: "3jzfcijpj2z", 31 31 }); 32 32 33 - const itemUri = await producer.createItem("Hello, world!"); 33 + const memoUri = await producer.createMemo("Hello, world!"); 34 34 ```
+31 -32
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 { AppCisternLexiconPubkey } from "@cistern/lexicon"; 7 + import type { AppCisternPubkey } from "@cistern/lexicon"; 8 8 9 9 // Helper to create a mock Producer instance 10 10 function createMockProducer( ··· 57 57 name: "Producer constructor initializes with existing public key", 58 58 fn() { 59 59 const mockPublicKey: PublicKeyOption = { 60 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri, 60 + uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri, 61 61 name: "Test Key", 62 62 content: new Uint8Array(32).toBase64(), 63 63 }; ··· 73 73 }); 74 74 75 75 Deno.test({ 76 - name: "createItem successfully creates and uploads an encrypted item", 76 + name: "createMemo successfully creates and uploads an encrypted memo", 77 77 async fn() { 78 78 const keys = generateKeys(); 79 79 let capturedRecord: unknown; ··· 92 92 return Promise.resolve({ 93 93 ok: true, 94 94 data: { 95 - uri: 96 - "at://did:plc:test/app.cistern.lexicon.item/item123" as ResourceUri, 95 + uri: "at://did:plc:test/app.cistern.memo/memo123" as ResourceUri, 97 96 }, 98 97 }); 99 98 } ··· 104 103 const producer = createMockProducer({ 105 104 rpc: mockRpc, 106 105 publicKey: { 107 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri, 106 + uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri, 108 107 name: "Test Key", 109 108 content: keys.publicKey.toBase64(), 110 109 }, 111 110 }); 112 111 113 - const uri = await producer.createItem("Test message"); 112 + const uri = await producer.createMemo("Test message"); 114 113 115 - expect(uri).toEqual("at://did:plc:test/app.cistern.lexicon.item/item123"); 116 - 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"); 117 116 expect(capturedRecord).toMatchObject({ 118 - $type: "app.cistern.lexicon.item", 117 + $type: "app.cistern.memo", 119 118 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 120 119 }); 121 120 }, 122 121 }); 123 122 124 123 Deno.test({ 125 - name: "createItem throws when no public key is set", 124 + name: "createMemo throws when no public key is set", 126 125 async fn() { 127 126 const producer = createMockProducer(); 128 127 129 - await expect(producer.createItem("Test message")).rejects.toThrow( 130 - "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", 131 130 ); 132 131 }, 133 132 }); 134 133 135 134 Deno.test({ 136 - name: "createItem throws when upload fails", 135 + name: "createMemo throws when upload fails", 137 136 async fn() { 138 137 const keys = generateKeys(); 139 138 const mockRpc = { ··· 148 147 const producer = createMockProducer({ 149 148 rpc: mockRpc, 150 149 publicKey: { 151 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri, 150 + uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri, 152 151 name: "Test Key", 153 152 content: keys.publicKey.toBase64(), 154 153 }, 155 154 }); 156 155 157 - await expect(producer.createItem("Test message")).rejects.toThrow( 158 - "failed to create new item", 156 + await expect(producer.createMemo("Test message")).rejects.toThrow( 157 + "failed to create new memo", 159 158 ); 160 159 }, 161 160 }); ··· 171 170 data: { 172 171 records: [ 173 172 { 174 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1", 173 + uri: "at://did:plc:test/app.cistern.pubkey/key1", 175 174 value: { 176 - $type: "app.cistern.lexicon.pubkey", 175 + $type: "app.cistern.pubkey", 177 176 name: "Key 1", 178 177 algorithm: "x_wing", 179 178 content: { $bytes: new Uint8Array(32).toBase64() }, 180 179 createdAt: new Date().toISOString(), 181 - } as AppCisternLexiconPubkey.Main, 180 + } as AppCisternPubkey.Main, 182 181 }, 183 182 { 184 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key2", 183 + uri: "at://did:plc:test/app.cistern.pubkey/key2", 185 184 value: { 186 - $type: "app.cistern.lexicon.pubkey", 185 + $type: "app.cistern.pubkey", 187 186 name: "Key 2", 188 187 algorithm: "x_wing", 189 188 content: { $bytes: new Uint8Array(32).toBase64() }, 190 189 createdAt: new Date().toISOString(), 191 - } as AppCisternLexiconPubkey.Main, 190 + } as AppCisternPubkey.Main, 192 191 }, 193 192 ], 194 193 cursor: undefined, ··· 227 226 data: { 228 227 records: [ 229 228 { 230 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1", 229 + uri: "at://did:plc:test/app.cistern.pubkey/key1", 231 230 value: { 232 - $type: "app.cistern.lexicon.pubkey", 231 + $type: "app.cistern.pubkey", 233 232 name: "Key 1", 234 233 algorithm: "x_wing", 235 234 content: { $bytes: new Uint8Array(32).toBase64() }, 236 235 createdAt: new Date().toISOString(), 237 - } as AppCisternLexiconPubkey.Main, 236 + } as AppCisternPubkey.Main, 238 237 }, 239 238 ], 240 239 cursor: "next-page", ··· 246 245 data: { 247 246 records: [ 248 247 { 249 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key2", 248 + uri: "at://did:plc:test/app.cistern.pubkey/key2", 250 249 value: { 251 - $type: "app.cistern.lexicon.pubkey", 250 + $type: "app.cistern.pubkey", 252 251 name: "Key 2", 253 252 algorithm: "x_wing", 254 253 content: { $bytes: new Uint8Array(32).toBase64() }, 255 254 createdAt: new Date().toISOString(), 256 - } as AppCisternLexiconPubkey.Main, 255 + } as AppCisternPubkey.Main, 257 256 }, 258 257 ], 259 258 cursor: undefined, ··· 304 303 const producer = createMockProducer(); 305 304 306 305 const mockPublicKey: PublicKeyOption = { 307 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri, 306 + uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri, 308 307 name: "Selected Key", 309 308 content: new Uint8Array(32).toBase64(), 310 309 }; ··· 324 323 fn() { 325 324 const producer = createMockProducer({ 326 325 publicKey: { 327 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/old" as ResourceUri, 326 + uri: "at://did:plc:test/app.cistern.pubkey/old" as ResourceUri, 328 327 name: "Old Key", 329 328 content: new Uint8Array(32).toBase64(), 330 329 }, ··· 333 332 expect(producer.publicKey?.name).toEqual("Old Key"); 334 333 335 334 const newKey: PublicKeyOption = { 336 - uri: "at://did:plc:test/app.cistern.lexicon.pubkey/new" as ResourceUri, 335 + uri: "at://did:plc:test/app.cistern.pubkey/new" as ResourceUri, 337 336 name: "New Key", 338 337 content: new Uint8Array(32).toBase64(), 339 338 };
+14 -17
packages/producer/mod.ts
··· 8 8 import { type Did, parse, type ResourceUri } from "@atcute/lexicons"; 9 9 import type { Client, CredentialManager } from "@atcute/client"; 10 10 import { now } from "@atcute/tid"; 11 - import { 12 - type AppCisternLexiconItem, 13 - AppCisternLexiconPubkey, 14 - } from "@cistern/lexicon"; 11 + import { type AppCisternMemo, AppCisternPubkey } from "@cistern/lexicon"; 15 12 16 13 import type {} from "@atcute/atproto"; 17 14 ··· 26 23 params: { 27 24 repo: reqs.miniDoc.did, 28 25 rkey, 29 - collection: "app.cistern.lexicon.pubkey", 26 + collection: "app.cistern.pubkey", 30 27 }, 31 28 }); 32 29 ··· 36 33 ); 37 34 } 38 35 39 - const record = parse(AppCisternLexiconPubkey.mainSchema, res.data.value); 36 + const record = parse(AppCisternPubkey.mainSchema, res.data.value); 40 37 41 38 publicKey = { 42 39 uri: res.data.uri, ··· 65 62 } 66 63 67 64 /** 68 - * Creates an item and saves it as a record in the user's PDS 65 + * Creates a memo and saves it as a record in the user's PDS 69 66 */ 70 - async createItem(text: string): Promise<ResourceUri> { 67 + async createMemo(text: string): Promise<ResourceUri> { 71 68 if (!this.publicKey) { 72 69 throw new Error( 73 - "no public key set; select a public key before creating an item", 70 + "no public key set; select a public key before creating a memo", 74 71 ); 75 72 } 76 73 ··· 78 75 Uint8Array.fromBase64(this.publicKey.content), 79 76 text, 80 77 ); 81 - const record: AppCisternLexiconItem.Main = { 82 - $type: "app.cistern.lexicon.item", 78 + const record: AppCisternMemo.Main = { 79 + $type: "app.cistern.memo", 83 80 tid: now(), 84 81 algorithm: "x_wing-xchacha20_poly1305-sha3_512", 85 82 ciphertext: { $bytes: payload.cipherText }, ··· 92 89 93 90 const res = await this.rpc.post("com.atproto.repo.createRecord", { 94 91 input: { 95 - collection: "app.cistern.lexicon.item", 92 + collection: "app.cistern.memo", 96 93 repo: this.did, 97 94 record, 98 95 }, ··· 100 97 101 98 if (!res.ok) { 102 99 throw new Error( 103 - `failed to create new item: ${res.status} ${res.data.error}`, 100 + `failed to create new memo: ${res.status} ${res.data.error}`, 104 101 ); 105 102 } 106 103 ··· 120 117 while (true) { 121 118 const res = await this.rpc.get("com.atproto.repo.listRecords", { 122 119 params: { 123 - collection: "app.cistern.lexicon.pubkey", 120 + collection: "app.cistern.pubkey", 124 121 repo: this.did, 125 122 cursor, 126 123 }, ··· 135 132 cursor = res.data.cursor; 136 133 137 134 for (const record of res.data.records) { 138 - const item = parse(AppCisternLexiconPubkey.mainSchema, record.value); 135 + const memo = parse(AppCisternPubkey.mainSchema, record.value); 139 136 140 137 yield { 141 138 uri: record.uri, 142 - content: item.content.$bytes, 143 - name: item.name, 139 + content: memo.content.$bytes, 140 + name: memo.name, 144 141 }; 145 142 } 146 143