import { produceRequirements } from "@cistern/shared"; import { encryptText } from "@cistern/crypto"; import type { ProducerOptions, ProducerParams, PublicKeyOption, } from "./types.ts"; import { type Did, parse, type ResourceUri } from "@atcute/lexicons"; import type { Client } from "@atcute/client"; import { now } from "@atcute/tid"; import { type AppCisternMemo, AppCisternPubkey } from "@cistern/lexicon"; /** * Creates a `Producer` instance with all necessary requirements. This is the recommended way to construct a `Producer`. * * @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. * @param {ProducerOptions} options - Information for constructing the underlying XRPC client * @returns {Promise} A Cistern producer client with an authorized session */ export async function createProducer( { publicKey: rkey, ...opts }: ProducerOptions, ): Promise { const reqs = await produceRequirements(opts); let publicKey: PublicKeyOption | undefined; if (rkey) { const res = await reqs.rpc.get("com.atproto.repo.getRecord", { params: { repo: reqs.miniDoc.did, rkey, collection: "app.cistern.pubkey", }, }); if (!res.ok) { throw new Error( `invalid public key record ID ${publicKey}, got: ${res.status} ${res.data.error}`, ); } const record = parse(AppCisternPubkey.mainSchema, res.data.value); publicKey = { uri: res.data.uri, name: record.name, content: record.content.$bytes, }; } return new Producer({ ...reqs, publicKey, }); } /** * A client for encrypting and creating Cistern memos. */ export class Producer { /** DID of the user this producer acts on behalf of */ did: Did; /** `@atcute/client` instance with credential manager */ rpc: Client; /** Partial public key record, used for encrypting items */ publicKey?: PublicKeyOption; constructor(params: ProducerParams) { this.did = params.miniDoc.did; this.rpc = params.rpc; this.publicKey = params.publicKey; } /** * Creates a memo and saves it as a record in the user's PDS. * @param {string} text - The contents of the memo you wish to create */ async createMemo(text: string): Promise { if (!this.publicKey) { throw new Error( "no public key set; select a public key before creating a memo", ); } const payload = encryptText( Uint8Array.fromBase64(this.publicKey.content), text, ); const record: AppCisternMemo.Main = { $type: "app.cistern.memo", tid: now(), algorithm: "x_wing-xchacha20_poly1305-sha3_512", ciphertext: { $bytes: payload.cipherText }, contentHash: { $bytes: payload.hash }, contentLength: payload.length, nonce: { $bytes: payload.nonce }, payload: { $bytes: payload.content }, pubkey: this.publicKey.uri, }; const res = await this.rpc.post("com.atproto.repo.createRecord", { input: { collection: "app.cistern.memo", repo: this.did, record, }, }); if (!res.ok) { throw new Error( `failed to create new memo: ${res.status} ${res.data.error}`, ); } return res.data.uri; } /** * Lists public keys registered in the user's PDS */ async *listPublicKeys(): AsyncGenerator< PublicKeyOption, void, void > { let cursor: string | undefined; while (true) { const res = await this.rpc.get("com.atproto.repo.listRecords", { params: { collection: "app.cistern.pubkey", repo: this.did, cursor, }, }); if (!res.ok) { throw new Error( `failed to list public keys: ${res.status} ${res.data.error}`, ); } cursor = res.data.cursor; for (const record of res.data.records) { const memo = parse(AppCisternPubkey.mainSchema, record.value); yield { uri: record.uri, content: memo.content.$bytes, name: memo.name, }; } if (!cursor) return; } } /** * Sets a public key as the main encryption key. This is not necessary to use if you instantiated the client with a public key. * @param {PublicKeyOption} key - The key you want to use for encryption */ selectPublicKey(key: PublicKeyOption) { this.publicKey = key; } }