Encrypted, ephemeral, private memos on atproto

init with basic lexicon schemas

Changed files
+233
packages
+15
README.md
··· 1 + # Cistern 2 + 3 + Cistern is an attempt at implementing a private, personal quick-capture system 4 + on AT Protocol. Cistern "items" are encrypted, so that they are only readable 5 + by the holder of the correct secret key—stored off-protocol. These items are 6 + intended to be ephemeral, and to be deleted after they've been read. 7 + 8 + The intention is for Cistern to bridge the gap between where ideas are had and 9 + where they can be stored long-term. For example, let's say you have an idea 10 + while at a restaurant. You create an item using your phone, and once you're 11 + back at your desk and you open Obsidian, that item is automatically pulled from 12 + your PDS, decrypted, and deleted from your PDS. If your notebook was open at 13 + the time you created your item, the Cistern Obsidian plugin would have been 14 + notified of the new item via the Jetstream, and so you would find your memo 15 + waiting for you once you got home.
+5
deno.jsonc
··· 1 + { 2 + "workspace": [ 3 + "packages/*" 4 + ] 5 + }
+57
deno.lock
··· 1 + { 2 + "version": "5", 3 + "specifiers": { 4 + "npm:@atproto/lexicon@~0.5.1": "0.5.1" 5 + }, 6 + "npm": { 7 + "@atproto/common-web@0.4.3": { 8 + "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", 9 + "dependencies": [ 10 + "graphemer", 11 + "multiformats", 12 + "uint8arrays", 13 + "zod" 14 + ] 15 + }, 16 + "@atproto/lexicon@0.5.1": { 17 + "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==", 18 + "dependencies": [ 19 + "@atproto/common-web", 20 + "@atproto/syntax", 21 + "iso-datestring-validator", 22 + "multiformats", 23 + "zod" 24 + ] 25 + }, 26 + "@atproto/syntax@0.4.1": { 27 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==" 28 + }, 29 + "graphemer@1.4.0": { 30 + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" 31 + }, 32 + "iso-datestring-validator@2.2.2": { 33 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" 34 + }, 35 + "multiformats@9.9.0": { 36 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" 37 + }, 38 + "uint8arrays@3.0.0": { 39 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 40 + "dependencies": [ 41 + "multiformats" 42 + ] 43 + }, 44 + "zod@3.25.76": { 45 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" 46 + } 47 + }, 48 + "workspace": { 49 + "members": { 50 + "packages/lexicon": { 51 + "dependencies": [ 52 + "npm:@atproto/lexicon@~0.5.1" 53 + ] 54 + } 55 + } 56 + } 57 + }
+9
packages/lexicon/deno.jsonc
··· 1 + { 2 + "name": "@cistern/lexicon", 3 + "exports": { 4 + ".": "mod.ts" 5 + }, 6 + "imports": { 7 + "@atproto/lexicon": "npm:@atproto/lexicon@^0.5.1" 8 + } 9 + }
+11
packages/lexicon/mod.ts
··· 1 + import item from "./schemas/item.json" with { type: "json" }; 2 + import pubkey from "./schemas/pubkey.json" with { type: "json" }; 3 + 4 + import { Lexicons } from "@atproto/lexicon"; 5 + 6 + const lexicons = new Lexicons(); 7 + 8 + lexicons.add(item); 9 + lexicons.add(pubkey); 10 + 11 + export { item, lexicons, pubkey };
+101
packages/lexicon/schemas/item.json
··· 1 + { 2 + "version": 1, 3 + "id": "app.cistern.lexicon.item", 4 + "description": "An encrypted value meant to be accessed and deleted later.", 5 + "defs": { 6 + "encryptedField": { 7 + "type": "object", 8 + "description": "An inline-encrypted property", 9 + "required": ["value", "nonce"], 10 + "properties": { 11 + "value": { 12 + "type": "string", 13 + "description": "Encrypted field value" 14 + }, 15 + "nonce": { 16 + "type": "string", 17 + "description": "Nonce used to encrypt field value" 18 + } 19 + } 20 + }, 21 + "main": { 22 + "type": "record", 23 + "description": "A manifest representing an encrypted note or binary file", 24 + "record": { 25 + "type": "object", 26 + "required": [ 27 + "tid", 28 + "ciphertext", 29 + "nonce", 30 + "algorithm", 31 + "pubkey", 32 + "chunks", 33 + "metadata" 34 + ], 35 + "properties": { 36 + "tid": { 37 + "type": "string", 38 + "description": "TID representing when this item was created", 39 + "format": "tid" 40 + } 41 + "ciphertext": { 42 + "type": "string", 43 + "description": "Encapsulated shared ciphertext" 44 + }, 45 + "nonce": { 46 + "type": "string", 47 + "description": "Nonce used for content encryption", 48 + "maxLength": 64, 49 + }, 50 + "algorithm": { 51 + "type": "string", 52 + "description": "Algorithm used for encryption, in <kem>-<cipher>-<hash> format.", 53 + "knownValues": ["x_wing-xchacha20_poly1305-sha3_512"] 54 + }, 55 + "pubkey": { 56 + "type": "string", 57 + "description": "URI to the public key used to encrypt this item", 58 + "format": "at-uri" 59 + }, 60 + "chunks": { 61 + "type": "array", 62 + "description": "Encrypted blobs that compose the encrypted contents of the item" 63 + "items": { 64 + "type": "blob", 65 + "accept": "application/octet-stream", 66 + "maxSize": 50000000 67 + }, 68 + "minLength": 1 69 + }, 70 + "metadata": { 71 + "type": "object", 72 + "description": "Information about the encrypted content.", 73 + "required": ["format", "length", "hash"], 74 + "properties": { 75 + "format": { 76 + "type": "string", 77 + "description": "Original contents format", 78 + "knownValues": ["text", "file"] 79 + }, 80 + "length": { 81 + "type": "ref", 82 + "description": "Original content length in bytes", 83 + "ref": "app.cistern.lexicon.item#encryptedField" 84 + }, 85 + "hash": { 86 + "type": "string", 87 + "description": "SHA-256 hash of the original file", 88 + "maxLength": 64, 89 + }, 90 + "mimetype": { 91 + "type": "ref", 92 + "description": "Mimetype of original contents, if a file", 93 + "ref": "app.cistern.lexicon.item#encryptedField" 94 + } 95 + } 96 + } 97 + } 98 + } 99 + } 100 + } 101 + }
+35
packages/lexicon/schemas/pubkey.json
··· 1 + { 2 + "version": 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 + }