···11+import { produceRequirements } from "@cistern/shared";
22+import { encryptText } from "@cistern/crypto";
33+import type {
44+ ProducerOptions,
55+ ProducerParams,
66+ PublicKeyOption,
77+} from "./types.ts";
88+import { type Did, parse, type ResourceUri } from "@atcute/lexicons";
99+import type { Client } from "@atcute/client";
1010+import { now } from "@atcute/tid";
1111+import { type AppCisternMemo, AppCisternPubkey } from "@cistern/lexicon";
1212+1313+/**
1414+ * Creates a `Producer` instance with all necessary requirements. This is the recommended way to construct a `Producer`.
1515+ *
1616+ * @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.
1717+ * @param {ProducerOptions} options - Information for constructing the underlying XRPC client
1818+ * @returns {Promise<Producer>} A Cistern producer client with an authorized session
1919+ */
2020+export async function createProducer(
2121+ { publicKey: rkey, ...opts }: ProducerOptions,
2222+): Promise<Producer> {
2323+ const reqs = await produceRequirements(opts);
2424+2525+ let publicKey: PublicKeyOption | undefined;
2626+ if (rkey) {
2727+ const res = await reqs.rpc.get("com.atproto.repo.getRecord", {
2828+ params: {
2929+ repo: reqs.miniDoc.did,
3030+ rkey,
3131+ collection: "app.cistern.pubkey",
3232+ },
3333+ });
3434+3535+ if (!res.ok) {
3636+ throw new Error(
3737+ `invalid public key record ID ${publicKey}, got: ${res.status} ${res.data.error}`,
3838+ );
3939+ }
4040+4141+ const record = parse(AppCisternPubkey.mainSchema, res.data.value);
4242+4343+ publicKey = {
4444+ uri: res.data.uri,
4545+ name: record.name,
4646+ content: record.content.$bytes,
4747+ };
4848+ }
4949+5050+ return new Producer({
5151+ ...reqs,
5252+ publicKey,
5353+ });
5454+}
5555+5656+/**
5757+ * A client for encrypting and creating Cistern memos.
5858+ */
5959+export class Producer {
6060+ /** DID of the user this producer acts on behalf of */
6161+ did: Did;
6262+6363+ /** `@atcute/client` instance with credential manager */
6464+ rpc: Client;
6565+6666+ /** Partial public key record, used for encrypting items */
6767+ publicKey?: PublicKeyOption;
6868+6969+ constructor(params: ProducerParams) {
7070+ this.did = params.miniDoc.did;
7171+ this.rpc = params.rpc;
7272+ this.publicKey = params.publicKey;
7373+ }
7474+7575+ /**
7676+ * Creates a memo and saves it as a record in the user's PDS.
7777+ * @param {string} text - The contents of the memo you wish to create
7878+ */
7979+ async createMemo(text: string): Promise<ResourceUri> {
8080+ if (!this.publicKey) {
8181+ throw new Error(
8282+ "no public key set; select a public key before creating a memo",
8383+ );
8484+ }
8585+8686+ const payload = encryptText(
8787+ Uint8Array.fromBase64(this.publicKey.content),
8888+ text,
8989+ );
9090+ const record: AppCisternMemo.Main = {
9191+ $type: "app.cistern.memo",
9292+ tid: now(),
9393+ algorithm: "x_wing-xchacha20_poly1305-sha3_512",
9494+ ciphertext: { $bytes: payload.cipherText },
9595+ contentHash: { $bytes: payload.hash },
9696+ contentLength: payload.length,
9797+ nonce: { $bytes: payload.nonce },
9898+ payload: { $bytes: payload.content },
9999+ pubkey: this.publicKey.uri,
100100+ };
101101+102102+ const res = await this.rpc.post("com.atproto.repo.createRecord", {
103103+ input: {
104104+ collection: "app.cistern.memo",
105105+ repo: this.did,
106106+ record,
107107+ },
108108+ });
109109+110110+ if (!res.ok) {
111111+ throw new Error(
112112+ `failed to create new memo: ${res.status} ${res.data.error}`,
113113+ );
114114+ }
115115+116116+ return res.data.uri;
117117+ }
118118+119119+ /**
120120+ * Lists public keys registered in the user's PDS
121121+ */
122122+ async *listPublicKeys(): AsyncGenerator<
123123+ PublicKeyOption,
124124+ void,
125125+ void
126126+ > {
127127+ let cursor: string | undefined;
128128+129129+ while (true) {
130130+ const res = await this.rpc.get("com.atproto.repo.listRecords", {
131131+ params: {
132132+ collection: "app.cistern.pubkey",
133133+ repo: this.did,
134134+ cursor,
135135+ },
136136+ });
137137+138138+ if (!res.ok) {
139139+ throw new Error(
140140+ `failed to list public keys: ${res.status} ${res.data.error}`,
141141+ );
142142+ }
143143+144144+ cursor = res.data.cursor;
145145+146146+ for (const record of res.data.records) {
147147+ const memo = parse(AppCisternPubkey.mainSchema, record.value);
148148+149149+ yield {
150150+ uri: record.uri,
151151+ content: memo.content.$bytes,
152152+ name: memo.name,
153153+ };
154154+ }
155155+156156+ if (!cursor) return;
157157+ }
158158+ }
159159+160160+ /**
161161+ * Sets a public key as the main encryption key. This is not necessary to use if you instantiated the client with a public key.
162162+ * @param {PublicKeyOption} key - The key you want to use for encryption
163163+ */
164164+ selectPublicKey(key: PublicKeyOption) {
165165+ this.publicKey = key;
166166+ }
167167+}
+2-167
packages/producer/mod.ts
···11-import { produceRequirements } from "@cistern/shared";
22-import { encryptText } from "@cistern/crypto";
33-import type {
44- ProducerOptions,
55- ProducerParams,
66- PublicKeyOption,
77-} from "./types.ts";
88-import { type Did, parse, type ResourceUri } from "@atcute/lexicons";
99-import type { Client, CredentialManager } from "@atcute/client";
1010-import { now } from "@atcute/tid";
1111-import { type AppCisternMemo, AppCisternPubkey } from "@cistern/lexicon";
1212-1313-/**
1414- * Creates a `Producer` instance with all necessary requirements. This is the recommended way to construct a `Producer`.
1515- *
1616- * @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.
1717- * @param {ProducerOptions} options - Information for constructing the underlying XRPC client
1818- * @returns {Promise<Producer>} A Cistern producer client with an authorized session
1919- */
2020-export async function createProducer(
2121- { publicKey: rkey, ...opts }: ProducerOptions,
2222-): Promise<Producer> {
2323- const reqs = await produceRequirements(opts);
2424-2525- let publicKey: PublicKeyOption | undefined;
2626- if (rkey) {
2727- const res = await reqs.rpc.get("com.atproto.repo.getRecord", {
2828- params: {
2929- repo: reqs.miniDoc.did,
3030- rkey,
3131- collection: "app.cistern.pubkey",
3232- },
3333- });
3434-3535- if (!res.ok) {
3636- throw new Error(
3737- `invalid public key record ID ${publicKey}, got: ${res.status} ${res.data.error}`,
3838- );
3939- }
4040-4141- const record = parse(AppCisternPubkey.mainSchema, res.data.value);
4242-4343- publicKey = {
4444- uri: res.data.uri,
4545- name: record.name,
4646- content: record.content.$bytes,
4747- };
4848- }
4949-5050- return new Producer({
5151- ...reqs,
5252- publicKey,
5353- });
5454-}
5555-5656-/**
5757- * A client for encrypting and creating Cistern memos.
5858- */
5959-export class Producer {
6060- /** DID of the user this producer acts on behalf of */
6161- did: Did;
6262-6363- /** `@atcute/client` instance with credential manager */
6464- rpc: Client;
6565-6666- /** Partial public key record, used for encrypting items */
6767- publicKey?: PublicKeyOption;
6868-6969- constructor(params: ProducerParams) {
7070- this.did = params.miniDoc.did;
7171- this.rpc = params.rpc;
7272- this.publicKey = params.publicKey;
7373- }
7474-7575- /**
7676- * Creates a memo and saves it as a record in the user's PDS.
7777- * @param {string} text - The contents of the memo you wish to create
7878- */
7979- async createMemo(text: string): Promise<ResourceUri> {
8080- if (!this.publicKey) {
8181- throw new Error(
8282- "no public key set; select a public key before creating a memo",
8383- );
8484- }
8585-8686- const payload = encryptText(
8787- Uint8Array.fromBase64(this.publicKey.content),
8888- text,
8989- );
9090- const record: AppCisternMemo.Main = {
9191- $type: "app.cistern.memo",
9292- tid: now(),
9393- algorithm: "x_wing-xchacha20_poly1305-sha3_512",
9494- ciphertext: { $bytes: payload.cipherText },
9595- contentHash: { $bytes: payload.hash },
9696- contentLength: payload.length,
9797- nonce: { $bytes: payload.nonce },
9898- payload: { $bytes: payload.content },
9999- pubkey: this.publicKey.uri,
100100- };
101101-102102- const res = await this.rpc.post("com.atproto.repo.createRecord", {
103103- input: {
104104- collection: "app.cistern.memo",
105105- repo: this.did,
106106- record,
107107- },
108108- });
109109-110110- if (!res.ok) {
111111- throw new Error(
112112- `failed to create new memo: ${res.status} ${res.data.error}`,
113113- );
114114- }
115115-116116- return res.data.uri;
117117- }
118118-119119- /**
120120- * Lists public keys registered in the user's PDS
121121- */
122122- async *listPublicKeys(): AsyncGenerator<
123123- PublicKeyOption,
124124- void,
125125- void
126126- > {
127127- let cursor: string | undefined;
128128-129129- while (true) {
130130- const res = await this.rpc.get("com.atproto.repo.listRecords", {
131131- params: {
132132- collection: "app.cistern.pubkey",
133133- repo: this.did,
134134- cursor,
135135- },
136136- });
137137-138138- if (!res.ok) {
139139- throw new Error(
140140- `failed to list public keys: ${res.status} ${res.data.error}`,
141141- );
142142- }
143143-144144- cursor = res.data.cursor;
145145-146146- for (const record of res.data.records) {
147147- const memo = parse(AppCisternPubkey.mainSchema, record.value);
148148-149149- yield {
150150- uri: record.uri,
151151- content: memo.content.$bytes,
152152- name: memo.name,
153153- };
154154- }
155155-156156- if (!cursor) return;
157157- }
158158- }
159159-160160- /**
161161- * Sets a public key as the main encryption key. This is not necessary to use if you instantiated the client with a public key.
162162- * @param {PublicKeyOption} key - The key you want to use for encryption
163163- */
164164- selectPublicKey(key: PublicKeyOption) {
165165- this.publicKey = key;
166166- }
167167-}
11+export * from "./client.ts";
22+export * from "./types.ts";