···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";
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000