Encrypted, ephemeral, private memos on atproto

refactor(producer): extract class from mod.ts

graham.systems b5612b0f 8ce06347

verified
Changed files
+169 -167
packages
producer
+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 + }
+2 -167
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 { 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 - } 1 + export * from "./client.ts"; 2 + export * from "./types.ts";