diff --git a/packages/producer/client.ts b/packages/producer/client.ts new file mode 100644 index 0000000..56cde2f --- /dev/null +++ b/packages/producer/client.ts @@ -0,0 +1,167 @@ +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; + } +} diff --git a/packages/producer/mod.ts b/packages/producer/mod.ts index ab8d6fa..bd12951 100644 --- a/packages/producer/mod.ts +++ b/packages/producer/mod.ts @@ -1,167 +1,2 @@ -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, CredentialManager } 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; - } -} +export * from "./client.ts"; +export * from "./types.ts";