Encrypted, ephemeral, private memos on atproto

feat(producer): implement creating items

graham.systems ebcc4349 8195c329

verified
Changed files
+50 -6
packages
producer
+6 -1
deno.lock
··· 16 "npm:@atcute/jetstream@^1.1.2": "1.1.2", 17 "npm:@atcute/lex-cli@^2.3.1": "2.3.1", 18 "npm:@atcute/lexicons@^1.2.2": "1.2.2", 19 "npm:@atproto/lexicon@~0.5.1": "0.5.1" 20 }, 21 "jsr": { ··· 115 "@standard-schema/spec", 116 "esm-env" 117 ] 118 }, 119 "@atproto/common-web@0.4.3": { 120 "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", ··· 236 "dependencies": [ 237 "npm:@atcute/atproto@^3.1.9", 238 "npm:@atcute/client@^4.0.5", 239 - "npm:@atcute/lexicons@^1.2.2" 240 ] 241 }, 242 "packages/shared": {
··· 16 "npm:@atcute/jetstream@^1.1.2": "1.1.2", 17 "npm:@atcute/lex-cli@^2.3.1": "2.3.1", 18 "npm:@atcute/lexicons@^1.2.2": "1.2.2", 19 + "npm:@atcute/tid@^1.0.3": "1.0.3", 20 "npm:@atproto/lexicon@~0.5.1": "0.5.1" 21 }, 22 "jsr": { ··· 116 "@standard-schema/spec", 117 "esm-env" 118 ] 119 + }, 120 + "@atcute/tid@1.0.3": { 121 + "integrity": "sha512-wfMJx1IMdnu0CZgWl0uR4JO2s6PGT1YPhpytD4ZHzEYKKQVuqV6Eb/7vieaVo1eYNMp2FrY67FZObeR7utRl2w==" 122 }, 123 "@atproto/common-web@0.4.3": { 124 "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", ··· 240 "dependencies": [ 241 "npm:@atcute/atproto@^3.1.9", 242 "npm:@atcute/client@^4.0.5", 243 + "npm:@atcute/lexicons@^1.2.2", 244 + "npm:@atcute/tid@^1.0.3" 245 ] 246 }, 247 "packages/shared": {
+2 -1
packages/producer/deno.jsonc
··· 6 "imports": { 7 "@atcute/atproto": "npm:@atcute/atproto@^3.1.9", 8 "@atcute/client": "npm:@atcute/client@^4.0.5", 9 - "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2" 10 } 11 }
··· 6 "imports": { 7 "@atcute/atproto": "npm:@atcute/atproto@^3.1.9", 8 "@atcute/client": "npm:@atcute/client@^4.0.5", 9 + "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2", 10 + "@atcute/tid": "npm:@atcute/tid@^1.0.3" 11 } 12 }
+42 -4
packages/producer/mod.ts
··· 1 import { produceRequirements } from "@cistern/shared"; 2 import type { 3 LocalPublicKey, 4 ProducerOptions, ··· 6 } from "./types.ts"; 7 import { type Did, parse, type ResourceUri } from "@atcute/lexicons"; 8 import type { Client, CredentialManager } from "@atcute/client"; 9 import { 10 AppCisternLexiconPubkey, 11 } from "@cistern/lexicon"; 12 ··· 60 61 /** 62 * Creates an item and saves it as a record in the user's PDS 63 - * @todo Error if there is no selected public key 64 - * @todo Construct valid item 65 - * @todo Save item to PDS 66 */ 67 - async createItem() {} 68 69 /** 70 * Lists public keys registered in the user's PDS
··· 1 import { produceRequirements } from "@cistern/shared"; 2 + import { encryptText } from "@cistern/crypto"; 3 import type { 4 LocalPublicKey, 5 ProducerOptions, ··· 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 ··· 63 64 /** 65 * Creates an item and saves it as a record in the user's PDS 66 */ 67 + async createItem(text: string): Promise<ResourceUri> { 68 + if (!this.publicKey) { 69 + throw new Error( 70 + "no public key set; select a public key before creating an item", 71 + ); 72 + } 73 + 74 + const payload = encryptText( 75 + Uint8Array.fromBase64(this.publicKey.record.content), 76 + text, 77 + ); 78 + const record: AppCisternLexiconItem.Main = { 79 + $type: "app.cistern.lexicon.item", 80 + tid: now(), 81 + algorithm: "x_wing-xchacha20_poly1305-sha3_512", 82 + ciphertext: payload.cipherText, 83 + contentHash: payload.hash, 84 + contentLength: payload.length, 85 + nonce: payload.nonce, 86 + payload: payload.content, 87 + pubkey: this.publicKey.uri, 88 + }; 89 + 90 + const res = await this.rpc.post("com.atproto.repo.createRecord", { 91 + input: { 92 + collection: "app.cistern.lexicon.item", 93 + repo: this.did, 94 + record, 95 + }, 96 + }); 97 + 98 + if (!res.ok) { 99 + throw new Error( 100 + `failed to create new item: ${res.status} ${res.data.error}`, 101 + ); 102 + } 103 + 104 + return res.data.uri; 105 + } 106 107 /** 108 * Lists public keys registered in the user's PDS