+32
-8
README.md
+32
-8
README.md
···
1
1
# Cistern
2
2
3
-
Cistern is a private, end-to-end encrypted quick-capture system built on AT Protocol. Items are encrypted using post-quantum cryptography and stored temporarily in your Personal Data Server (PDS), then automatically retrieved and deleted after consumption.
3
+
Cistern is a private, end-to-end encrypted quick-capture system built on AT
4
+
Protocol. Memos are encrypted using post-quantum cryptography and stored
5
+
temporarily in your Personal Data Server (PDS), then automatically retrieved and
6
+
deleted after consumption.
4
7
5
-
The system bridges the gap between where ideas are captured and where they are stored long-term. Create an encrypted item on your phone, and it automatically appears in your desktop application, decrypted and ready to use.
8
+
The system bridges the gap between where ideas are captured and where they are
9
+
stored long-term. Create an encrypted memo on your phone, and it automatically
10
+
appears in your desktop application, decrypted and ready to use.
6
11
7
12
## Architecture
8
13
9
14
Cistern is a Deno monorepo consisting of five packages:
10
15
11
16
### `@cistern/crypto`
12
-
Core cryptographic operations using post-quantum algorithms. Implements X-Wing key encapsulation with XChaCha20-Poly1305 authenticated encryption and SHA3-512 integrity verification. Handles keypair generation, encryption, and decryption.
17
+
18
+
Core cryptographic operations using post-quantum algorithms. Implements X-Wing
19
+
key encapsulation with XChaCha20-Poly1305 authenticated encryption and SHA3-512
20
+
integrity verification. Handles keypair generation, encryption, and decryption.
13
21
14
22
### `@cistern/lexicon`
15
-
AT Protocol schema definitions for Cistern record types. Defines `app.cistern.lexicon.pubkey` (public key records) and `app.cistern.lexicon.item` (encrypted item records). Includes code generation from JSON schemas.
23
+
24
+
AT Protocol schema definitions for Cistern record types. Defines
25
+
`app.cistern.pubkey` (public key records) and `app.cistern.memo` (encrypted memo
26
+
records). Includes code generation from JSON schemas.
16
27
17
28
### `@cistern/shared`
18
-
Common utilities and authentication logic. Handles DID resolution via Slingshot service and creates authenticated RPC clients using app passwords.
29
+
30
+
Common utilities and authentication logic. Handles DID resolution via Slingshot
31
+
service and creates authenticated RPC clients using app passwords.
19
32
20
33
### `@cistern/producer`
21
-
Creates and encrypts items for storage. Manages public key selection, encrypts plaintext content, and uploads encrypted items to the PDS as AT Protocol records.
34
+
35
+
Creates and encrypts memos for storage. Manages public key selection, encrypts
36
+
plaintext content, and uploads encrypted memos to the PDS as AT Protocol
37
+
records.
22
38
23
39
### `@cistern/consumer`
24
-
Retrieves, decrypts, and deletes items. Generates keypairs, manages private keys locally, retrieves items via polling or real-time streaming (Jetstream), and handles item deletion after consumption.
40
+
41
+
Retrieves, decrypts, and deletes memos. Generates keypairs, manages private keys
42
+
locally, retrieves memos via polling or real-time streaming (Jetstream), and
43
+
handles memo deletion after consumption.
25
44
26
45
## Security Model
27
46
28
-
Private keys never leave the consumer device. Public keys are stored in the PDS as records, while private keys remain off-protocol. Only the holder of the matching private key can decrypt items encrypted with the corresponding public key.
47
+
Private keys never leave the consumer device. Public keys are stored in the PDS
48
+
as records, while private keys remain off-protocol. Only the holder of the
49
+
matching private key can decrypt memos encrypted with the corresponding public
50
+
key.
29
51
30
52
## Testing
31
53
32
54
Run all unit tests:
55
+
33
56
```bash
34
57
deno test --allow-env
35
58
```
36
59
37
60
Run end-to-end tests (requires AT Protocol credentials):
61
+
38
62
```bash
39
63
CISTERN_HANDLE="your.bsky.social" \
40
64
CISTERN_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" \
+52
-52
e2e.test.ts
+52
-52
e2e.test.ts
···
29
29
let consumer: Awaited<ReturnType<typeof createConsumer>>;
30
30
let producer: Awaited<ReturnType<typeof createProducer>>;
31
31
let keypair: Awaited<ReturnType<typeof consumer.generateKeyPair>>;
32
-
let itemUri: string;
32
+
let memoUri: string;
33
33
let testMessage: string;
34
34
35
35
await t.step("Create consumer", async () => {
···
47
47
48
48
expect(keypair.privateKey).toBeInstanceOf(Uint8Array);
49
49
expect(keypair.publicKey).toBeDefined();
50
-
expect(keypair.publicKey).toContain("app.cistern.lexicon.pubkey");
50
+
expect(keypair.publicKey).toContain("app.cistern.pubkey");
51
51
});
52
52
53
53
try {
···
63
63
expect(producer.publicKey?.uri).toEqual(keypair.publicKey);
64
64
});
65
65
66
-
await t.step("Create encrypted item", async () => {
66
+
await t.step("Create encrypted memo", async () => {
67
67
testMessage = `E2E Test - ${new Date().toISOString()}`;
68
-
itemUri = await producer.createItem(testMessage);
68
+
memoUri = await producer.createMemo(testMessage);
69
69
70
-
expect(itemUri).toBeDefined();
71
-
expect(itemUri).toContain("app.cistern.lexicon.item");
70
+
expect(memoUri).toBeDefined();
71
+
expect(memoUri).toContain("app.cistern.memo");
72
72
});
73
73
74
-
await t.step("List and decrypt items", async () => {
75
-
const items = [];
76
-
for await (const item of consumer.listItems()) {
77
-
items.push(item);
74
+
await t.step("List and decrypt memos", async () => {
75
+
const memos = [];
76
+
for await (const memo of consumer.listMemos()) {
77
+
memos.push(memo);
78
78
}
79
79
80
-
expect(items.length).toBeGreaterThan(0);
80
+
expect(memos.length).toBeGreaterThan(0);
81
81
82
-
const ourItem = items.find((item) => item.text === testMessage);
83
-
expect(ourItem).toBeDefined();
84
-
expect(ourItem!.text).toEqual(testMessage);
82
+
const ourMemo = memos.find((memo) => memo.text === testMessage);
83
+
expect(ourMemo).toBeDefined();
84
+
expect(ourMemo!.text).toEqual(testMessage);
85
85
});
86
86
87
-
await t.step("Delete item", async () => {
88
-
const itemRkey = itemUri.split("/").pop()!;
89
-
await consumer.deleteItem(itemRkey);
87
+
await t.step("Delete memo", async () => {
88
+
const memoRkey = memoUri.split("/").pop()!;
89
+
await consumer.deleteMemo(memoRkey);
90
90
91
91
// Verify deletion
92
-
const itemsAfterDelete = [];
93
-
for await (const item of consumer.listItems()) {
94
-
itemsAfterDelete.push(item);
92
+
const memosAfterDelete = [];
93
+
for await (const memo of consumer.listMemos()) {
94
+
memosAfterDelete.push(memo);
95
95
}
96
96
97
-
const deletedItem = itemsAfterDelete.find(
98
-
(item) => item.text === testMessage,
97
+
const deletedMemo = memosAfterDelete.find(
98
+
(memo) => memo.text === testMessage,
99
99
);
100
-
expect(deletedItem).toBeUndefined();
100
+
expect(deletedMemo).toBeUndefined();
101
101
});
102
102
103
103
await t.step("List public keys", async () => {
···
118
118
119
119
const res = await consumer.rpc.post("com.atproto.repo.deleteRecord", {
120
120
input: {
121
-
collection: "app.cistern.lexicon.pubkey",
121
+
collection: "app.cistern.pubkey",
122
122
repo: consumer.did,
123
123
rkey: publicKeyRkey,
124
124
},
···
131
131
});
132
132
133
133
Deno.test({
134
-
name: "E2E: Multiple items with same keypair",
134
+
name: "E2E: Multiple memos with same keypair",
135
135
ignore: SKIP_E2E,
136
136
async fn(t) {
137
137
const handle = Deno.env.get("CISTERN_HANDLE") as Handle;
···
141
141
let producer: Awaited<ReturnType<typeof createProducer>>;
142
142
let keypair: Awaited<ReturnType<typeof consumer.generateKeyPair>>;
143
143
let messages: string[];
144
-
let itemUris: string[];
144
+
let memoUris: string[];
145
145
146
146
await t.step("Create consumer and generate keypair", async () => {
147
147
consumer = await createConsumer({
···
167
167
expect(producer.publicKey?.uri).toEqual(keypair.publicKey);
168
168
});
169
169
170
-
await t.step("Create multiple encrypted items", async () => {
170
+
await t.step("Create multiple encrypted memos", async () => {
171
171
messages = [
172
-
`E2E Item 1 - ${new Date().toISOString()}`,
173
-
`E2E Item 2 - ${new Date().toISOString()}`,
174
-
`E2E Item 3 - ${new Date().toISOString()}`,
172
+
`E2E Memo 1 - ${new Date().toISOString()}`,
173
+
`E2E Memo 2 - ${new Date().toISOString()}`,
174
+
`E2E Memo 3 - ${new Date().toISOString()}`,
175
175
];
176
176
177
-
itemUris = [];
177
+
memoUris = [];
178
178
for (const message of messages) {
179
-
const uri = await producer.createItem(message);
180
-
itemUris.push(uri);
179
+
const uri = await producer.createMemo(message);
180
+
memoUris.push(uri);
181
181
}
182
182
183
-
expect(itemUris).toHaveLength(3);
183
+
expect(memoUris).toHaveLength(3);
184
184
});
185
185
186
-
await t.step("Decrypt all items", async () => {
187
-
const items = [];
188
-
for await (const item of consumer.listItems()) {
189
-
items.push(item);
186
+
await t.step("Decrypt all memos", async () => {
187
+
const memos = [];
188
+
for await (const memo of consumer.listMemos()) {
189
+
memos.push(memo);
190
190
}
191
191
192
-
expect(items.length).toBeGreaterThanOrEqual(3);
192
+
expect(memos.length).toBeGreaterThanOrEqual(3);
193
193
194
194
// Verify all test messages are present
195
195
for (const message of messages) {
196
-
const item = items.find((i) => i.text === message);
197
-
expect(item).toBeDefined();
198
-
expect(item!.text).toEqual(message);
196
+
const memo = memos.find((m) => m.text === message);
197
+
expect(memo).toBeDefined();
198
+
expect(memo!.text).toEqual(message);
199
199
}
200
200
});
201
201
202
-
await t.step("Cleanup: Delete test items", async () => {
203
-
for (const uri of itemUris) {
202
+
await t.step("Cleanup: Delete test memos", async () => {
203
+
for (const uri of memoUris) {
204
204
const rkey = uri.split("/").pop()!;
205
-
await consumer.deleteItem(rkey);
205
+
await consumer.deleteMemo(rkey);
206
206
}
207
207
208
-
// Verify all items deleted
209
-
const remainingItems = [];
210
-
for await (const item of consumer.listItems()) {
211
-
remainingItems.push(item);
208
+
// Verify all memos deleted
209
+
const remainingMemos = [];
210
+
for await (const memo of consumer.listMemos()) {
211
+
remainingMemos.push(memo);
212
212
}
213
213
214
214
for (const message of messages) {
215
-
const item = remainingItems.find((i) => i.text === message);
216
-
expect(item).toBeUndefined();
215
+
const memo = remainingMemos.find((m) => m.text === message);
216
+
expect(memo).toBeUndefined();
217
217
}
218
218
});
219
219
} finally {
···
222
222
223
223
const res = await consumer.rpc.post("com.atproto.repo.deleteRecord", {
224
224
input: {
225
-
collection: "app.cistern.lexicon.pubkey",
225
+
collection: "app.cistern.pubkey",
226
226
repo: consumer.did,
227
227
rkey: publicKeyRkey,
228
228
},
+10
-10
packages/consumer/README.md
+10
-10
packages/consumer/README.md
···
1
1
# @cistern/consumer
2
2
3
-
Consumer client for retrieving, decrypting, and deleting Cistern items.
3
+
Consumer client for retrieving, decrypting, and deleting Cistern memos.
4
4
5
5
## Usage
6
6
···
29
29
handle: "user.bsky.social",
30
30
appPassword: "xxxx-xxxx-xxxx-xxxx",
31
31
keypair: {
32
-
publicKey: "at://did:plc:abc123/app.cistern.lexicon.pubkey/3jzfcijpj2z",
32
+
publicKey: "at://did:plc:abc123/app.cistern.pubkey/3jzfcijpj2z",
33
33
privateKey: "base64-encoded-private-key",
34
34
},
35
35
});
36
36
```
37
37
38
-
### List Items (Polling)
38
+
### List Memos (Polling)
39
39
40
40
```typescript
41
-
for await (const item of consumer.listItems()) {
42
-
console.log(`[${item.tid}] ${item.text}`);
43
-
await consumer.deleteItem(item.tid);
41
+
for await (const memo of consumer.listMemos()) {
42
+
console.log(`[${memo.tid}] ${memo.text}`);
43
+
await consumer.deleteMemo(memo.tid);
44
44
}
45
45
```
46
46
47
-
### Subscribe to Items (Real-time)
47
+
### Subscribe to Memos (Real-time)
48
48
49
49
```typescript
50
-
for await (const item of consumer.subscribeToItems()) {
51
-
console.log(`[${item.tid}] ${item.text}`);
52
-
await consumer.deleteItem(item.tid);
50
+
for await (const memo of consumer.subscribeToMemos()) {
51
+
console.log(`[${memo.tid}] ${memo.text}`);
52
+
await consumer.deleteMemo(memo.tid);
53
53
}
54
54
```
+64
-69
packages/consumer/mod.test.ts
+64
-69
packages/consumer/mod.test.ts
···
5
5
import type { Client, CredentialManager } from "@atcute/client";
6
6
import type { Did, Handle, ResourceUri } from "@atcute/lexicons";
7
7
import { now } from "@atcute/tid";
8
-
import type { AppCisternLexiconItem } from "@cistern/lexicon";
8
+
import type { AppCisternMemo } from "@cistern/lexicon";
9
9
10
10
// Helper to create a mock Consumer instance
11
11
function createMockConsumer(
···
59
59
fn() {
60
60
const mockKeypair = {
61
61
privateKey: new Uint8Array(32).toBase64(),
62
-
publicKey:
63
-
"at://did:plc:test/app.cistern.lexicon.pubkey/abc123" as ResourceUri,
62
+
publicKey: "at://did:plc:test/app.cistern.pubkey/abc123" as ResourceUri,
64
63
};
65
64
66
65
const consumer = createMockConsumer({
···
96
95
return Promise.resolve({
97
96
ok: true,
98
97
data: {
99
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/generated123",
98
+
uri: "at://did:plc:test/app.cistern.pubkey/generated123",
100
99
},
101
100
});
102
101
}
···
110
109
expect(keypair).toBeDefined();
111
110
expect(keypair.privateKey).toBeInstanceOf(Uint8Array);
112
111
expect(keypair.publicKey).toEqual(
113
-
"at://did:plc:test/app.cistern.lexicon.pubkey/generated123",
112
+
"at://did:plc:test/app.cistern.pubkey/generated123",
114
113
);
115
114
expect(consumer.keypair).toEqual(keypair);
116
115
117
-
expect(capturedCollection).toEqual("app.cistern.lexicon.pubkey");
116
+
expect(capturedCollection).toEqual("app.cistern.pubkey");
118
117
expect(capturedRecord).toMatchObject({
119
-
$type: "app.cistern.lexicon.pubkey",
118
+
$type: "app.cistern.pubkey",
120
119
algorithm: "x_wing",
121
120
});
122
121
},
···
132
131
keypair: {
133
132
privateKey: new Uint8Array(32).toBase64(),
134
133
publicKey:
135
-
"at://did:plc:test/app.cistern.lexicon.pubkey/existing" as ResourceUri,
134
+
"at://did:plc:test/app.cistern.pubkey/existing" as ResourceUri,
136
135
},
137
136
},
138
137
});
···
164
163
});
165
164
166
165
Deno.test({
167
-
name: "listItems throws when no keypair is set",
166
+
name: "listMemos throws when no keypair is set",
168
167
async fn() {
169
168
const consumer = createMockConsumer();
170
169
171
-
const iterator = consumer.listItems();
170
+
const iterator = consumer.listMemos();
172
171
await expect(iterator.next()).rejects.toThrow(
173
-
"no key pair set; generate a key before listing items",
172
+
"no key pair set; generate a key before listing memos",
174
173
);
175
174
},
176
175
});
177
176
178
177
Deno.test({
179
-
name: "listItems decrypts and yields items",
178
+
name: "listMemos decrypts and yields memos",
180
179
async fn() {
181
180
const keys = generateKeys();
182
-
const testText = "Test item content";
181
+
const testText = "Test memo content";
183
182
const encrypted = encryptText(keys.publicKey, testText);
184
183
const testTid = now();
185
184
···
191
190
data: {
192
191
records: [
193
192
{
194
-
uri: "at://did:plc:test/app.cistern.lexicon.item/item1",
193
+
uri: "at://did:plc:test/app.cistern.memo/memo1",
195
194
value: {
196
-
$type: "app.cistern.lexicon.item",
195
+
$type: "app.cistern.memo",
197
196
tid: testTid,
198
197
ciphertext: { $bytes: encrypted.cipherText },
199
198
nonce: { $bytes: encrypted.nonce },
200
199
algorithm: "x_wing-xchacha20_poly1305-sha3_512",
201
-
pubkey: "at://did:plc:test/app.cistern.lexicon.pubkey/key1",
200
+
pubkey: "at://did:plc:test/app.cistern.pubkey/key1",
202
201
payload: { $bytes: encrypted.content },
203
202
contentLength: encrypted.length,
204
203
contentHash: { $bytes: encrypted.hash },
205
-
} as AppCisternLexiconItem.Main,
204
+
} as AppCisternMemo.Main,
206
205
},
207
206
],
208
207
cursor: undefined,
···
220
219
appPassword: "test-password",
221
220
keypair: {
222
221
privateKey: keys.secretKey.toBase64(),
223
-
publicKey:
224
-
"at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
222
+
publicKey: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri,
225
223
},
226
224
},
227
225
});
228
226
229
-
const items = [];
230
-
for await (const item of consumer.listItems()) {
231
-
items.push(item);
227
+
const memos = [];
228
+
for await (const memo of consumer.listMemos()) {
229
+
memos.push(memo);
232
230
}
233
231
234
-
expect(items).toHaveLength(1);
235
-
expect(items[0].text).toEqual(testText);
236
-
expect(items[0].tid).toEqual(testTid);
232
+
expect(memos).toHaveLength(1);
233
+
expect(memos[0].text).toEqual(testText);
234
+
expect(memos[0].tid).toEqual(testTid);
237
235
},
238
236
});
239
237
240
238
Deno.test({
241
-
name: "listItems skips items with mismatched public key",
239
+
name: "listmemos skips memos with mismatched public key",
242
240
async fn() {
243
241
const keys = generateKeys();
244
-
const testText = "Test item content";
242
+
const testText = "Test memo content";
245
243
const encrypted = encryptText(keys.publicKey, testText);
246
244
const testTid = now();
247
245
···
253
251
data: {
254
252
records: [
255
253
{
256
-
uri: "at://did:plc:test/app.cistern.lexicon.item/item1",
254
+
uri: "at://did:plc:test/app.cistern.memo/memo1",
257
255
value: {
258
-
$type: "app.cistern.lexicon.item",
256
+
$type: "app.cistern.memo",
259
257
tid: testTid,
260
258
ciphertext: { $bytes: encrypted.cipherText },
261
259
nonce: { $bytes: encrypted.nonce },
262
260
algorithm: "x_wing-xchacha20_poly1305-sha3_512",
263
261
pubkey:
264
-
"at://did:plc:test/app.cistern.lexicon.pubkey/different-key",
262
+
"at://did:plc:test/app.cistern.pubkey/different-key",
265
263
payload: { $bytes: encrypted.content },
266
264
contentLength: encrypted.length,
267
265
contentHash: { $bytes: encrypted.hash },
268
-
} as AppCisternLexiconItem.Main,
266
+
} as AppCisternMemo.Main,
269
267
},
270
268
],
271
269
cursor: undefined,
···
284
282
keypair: {
285
283
privateKey: keys.secretKey.toBase64(),
286
284
publicKey:
287
-
"at://did:plc:test/app.cistern.lexicon.pubkey/my-key" as ResourceUri,
285
+
"at://did:plc:test/app.cistern.pubkey/my-key" as ResourceUri,
288
286
},
289
287
},
290
288
});
291
289
292
-
const items = [];
293
-
for await (const item of consumer.listItems()) {
294
-
items.push(item);
290
+
const memos = [];
291
+
for await (const memo of consumer.listMemos()) {
292
+
memos.push(memo);
295
293
}
296
294
297
-
expect(items).toHaveLength(0);
295
+
expect(memos).toHaveLength(0);
298
296
},
299
297
});
300
298
301
299
Deno.test({
302
-
name: "listItems handles pagination",
300
+
name: "listMemos handles pagination",
303
301
async fn() {
304
302
const keys = generateKeys();
305
-
const text1 = "First item";
306
-
const text2 = "Second item";
303
+
const text1 = "First memo";
304
+
const text2 = "Second memo";
307
305
const encrypted1 = encryptText(keys.publicKey, text1);
308
306
const encrypted2 = encryptText(keys.publicKey, text2);
309
307
const tid1 = now();
···
321
319
data: {
322
320
records: [
323
321
{
324
-
uri: "at://did:plc:test/app.cistern.lexicon.item/item1",
322
+
uri: "at://did:plc:test/app.cistern.memo/memo1",
325
323
value: {
326
-
$type: "app.cistern.lexicon.item",
324
+
$type: "app.cistern.memo",
327
325
tid: tid1,
328
326
ciphertext: { $bytes: encrypted1.cipherText },
329
327
nonce: { $bytes: encrypted1.nonce },
330
328
algorithm: "x_wing-xchacha20_poly1305-sha3_512",
331
-
pubkey:
332
-
"at://did:plc:test/app.cistern.lexicon.pubkey/key1",
329
+
pubkey: "at://did:plc:test/app.cistern.pubkey/key1",
333
330
payload: { $bytes: encrypted1.content },
334
331
contentLength: encrypted1.length,
335
332
contentHash: { $bytes: encrypted1.hash },
336
-
} as AppCisternLexiconItem.Main,
333
+
} as AppCisternMemo.Main,
337
334
},
338
335
],
339
336
cursor: "next-page",
···
345
342
data: {
346
343
records: [
347
344
{
348
-
uri: "at://did:plc:test/app.cistern.lexicon.item/item2",
345
+
uri: "at://did:plc:test/app.cistern.memo/memo2",
349
346
value: {
350
-
$type: "app.cistern.lexicon.item",
347
+
$type: "app.cistern.memo",
351
348
tid: tid2,
352
349
ciphertext: { $bytes: encrypted2.cipherText },
353
350
nonce: { $bytes: encrypted2.nonce },
354
351
algorithm: "x_wing-xchacha20_poly1305-sha3_512",
355
-
pubkey:
356
-
"at://did:plc:test/app.cistern.lexicon.pubkey/key1",
352
+
pubkey: "at://did:plc:test/app.cistern.pubkey/key1",
357
353
payload: { $bytes: encrypted2.content },
358
354
contentLength: encrypted2.length,
359
355
contentHash: { $bytes: encrypted2.hash },
360
-
} as AppCisternLexiconItem.Main,
356
+
} as AppCisternMemo.Main,
361
357
},
362
358
],
363
359
cursor: undefined,
···
376
372
appPassword: "test-password",
377
373
keypair: {
378
374
privateKey: keys.secretKey.toBase64(),
379
-
publicKey:
380
-
"at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
375
+
publicKey: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri,
381
376
},
382
377
},
383
378
});
384
379
385
-
const items = [];
386
-
for await (const item of consumer.listItems()) {
387
-
items.push(item);
380
+
const memos = [];
381
+
for await (const memo of consumer.listMemos()) {
382
+
memos.push(memo);
388
383
}
389
384
390
-
expect(items).toHaveLength(2);
391
-
expect(items[0].text).toEqual(text1);
392
-
expect(items[1].text).toEqual(text2);
385
+
expect(memos).toHaveLength(2);
386
+
expect(memos[0].text).toEqual(text1);
387
+
expect(memos[1].text).toEqual(text2);
393
388
expect(callCount).toEqual(2);
394
389
},
395
390
});
396
391
397
392
Deno.test({
398
-
name: "listItems throws when list request fails",
393
+
name: "listMemos throws when list request fails",
399
394
async fn() {
400
395
const mockRpc = {
401
396
get: () =>
···
413
408
appPassword: "test-password",
414
409
keypair: {
415
410
privateKey: new Uint8Array(32).toBase64(),
416
-
publicKey: "at://did:plc:test/app.cistern.lexicon.pubkey/key1",
411
+
publicKey: "at://did:plc:test/app.cistern.pubkey/key1",
417
412
},
418
413
},
419
414
});
420
415
421
-
const iterator = consumer.listItems();
422
-
await expect(iterator.next()).rejects.toThrow("failed to list items");
416
+
const iterator = consumer.listMemos();
417
+
await expect(iterator.next()).rejects.toThrow("failed to list memos");
423
418
},
424
419
});
425
420
426
421
Deno.test({
427
-
name: "subscribeToItems throws when no keypair is set",
422
+
name: "subscribeToMemos throws when no keypair is set",
428
423
async fn() {
429
424
const consumer = createMockConsumer();
430
425
431
-
const iterator = consumer.subscribeToItems();
426
+
const iterator = consumer.subscribeToMemos();
432
427
await expect(iterator.next()).rejects.toThrow(
433
428
"no key pair set; generate a key before subscribing",
434
429
);
···
436
431
});
437
432
438
433
Deno.test({
439
-
name: "deleteItem successfully deletes an item",
434
+
name: "deleteMemo successfully deletes a memo",
440
435
async fn() {
441
436
let deletedRkey: string | undefined;
442
437
···
457
452
458
453
const consumer = createMockConsumer({ rpc: mockRpc });
459
454
460
-
await consumer.deleteItem("item123");
455
+
await consumer.deleteMemo("memo123");
461
456
462
-
expect(deletedRkey).toEqual("item123");
457
+
expect(deletedRkey).toEqual("memo123");
463
458
},
464
459
});
465
460
466
461
Deno.test({
467
-
name: "deleteItem throws when delete request fails",
462
+
name: "deleteMemo throws when delete request fails",
468
463
async fn() {
469
464
const mockRpc = {
470
465
post: () =>
···
477
472
478
473
const consumer = createMockConsumer({ rpc: mockRpc });
479
474
480
-
await expect(consumer.deleteItem("item123")).rejects.toThrow(
481
-
"failed to delete item item123",
475
+
await expect(consumer.deleteMemo("memo123")).rejects.toThrow(
476
+
"failed to delete memo memo123",
482
477
);
483
478
},
484
479
});
+29
-32
packages/consumer/mod.ts
+29
-32
packages/consumer/mod.ts
···
5
5
import { JetstreamSubscription } from "@atcute/jetstream";
6
6
import type { Did } from "@atcute/lexicons/syntax";
7
7
import type { Client, CredentialManager } from "@atcute/client";
8
-
import {
9
-
AppCisternLexiconItem,
10
-
type AppCisternLexiconPubkey,
11
-
} from "@cistern/lexicon";
8
+
import { AppCisternMemo, type AppCisternPubkey } from "@cistern/lexicon";
12
9
import type {
13
10
ConsumerOptions,
14
11
ConsumerParams,
15
-
DecryptedItem,
12
+
DecryptedMemo,
16
13
LocalKeyPair,
17
14
} from "./types.ts";
18
15
···
27
24
}
28
25
29
26
/**
30
-
* Client for generating keys and decoding Cistern items.
27
+
* Client for generating keys and decoding Cistern memos.
31
28
*/
32
29
export class Consumer {
33
30
did: Did;
···
58
55
const keys = generateKeys();
59
56
const name = await generateRandomName();
60
57
61
-
const record: AppCisternLexiconPubkey.Main = {
62
-
$type: "app.cistern.lexicon.pubkey",
58
+
const record: AppCisternPubkey.Main = {
59
+
$type: "app.cistern.pubkey",
63
60
name,
64
61
algorithm: "x_wing",
65
62
content: { $bytes: keys.publicKey.toBase64() },
···
67
64
};
68
65
const res = await this.rpc.post("com.atproto.repo.createRecord", {
69
66
input: {
70
-
collection: "app.cistern.lexicon.pubkey",
67
+
collection: "app.cistern.pubkey",
71
68
repo: this.did,
72
69
record,
73
70
},
···
90
87
}
91
88
92
89
/**
93
-
* Asynchronously iterate through items in the user's PDS
90
+
* Asynchronously iterate through memos in the user's PDS
94
91
*/
95
-
async *listItems(): AsyncGenerator<
96
-
DecryptedItem,
92
+
async *listMemos(): AsyncGenerator<
93
+
DecryptedMemo,
97
94
void,
98
95
undefined
99
96
> {
100
97
if (!this.keypair) {
101
-
throw new Error("no key pair set; generate a key before listing items");
98
+
throw new Error("no key pair set; generate a key before listing memos");
102
99
}
103
100
104
101
let cursor: string | undefined;
···
106
103
while (true) {
107
104
const res = await this.rpc.get("com.atproto.repo.listRecords", {
108
105
params: {
109
-
collection: "app.cistern.lexicon.item",
106
+
collection: "app.cistern.memo",
110
107
repo: this.did,
111
108
cursor,
112
109
},
···
114
111
115
112
if (!res.ok) {
116
113
throw new Error(
117
-
`failed to list items: ${res.status} ${res.data.error}`,
114
+
`failed to list memos: ${res.status} ${res.data.error}`,
118
115
);
119
116
}
120
117
121
118
cursor = res.data.cursor;
122
119
123
120
for (const record of res.data.records) {
124
-
const item = parse(AppCisternLexiconItem.mainSchema, record.value);
121
+
const memo = parse(AppCisternMemo.mainSchema, record.value);
125
122
126
-
if (item.pubkey !== this.keypair.publicKey) continue;
123
+
if (memo.pubkey !== this.keypair.publicKey) continue;
127
124
128
125
const decrypted = decryptText(this.keypair.privateKey, {
129
-
nonce: item.nonce.$bytes,
130
-
cipherText: item.ciphertext.$bytes,
131
-
content: item.payload.$bytes,
132
-
hash: item.contentHash.$bytes,
133
-
length: item.contentLength,
126
+
nonce: memo.nonce.$bytes,
127
+
cipherText: memo.ciphertext.$bytes,
128
+
content: memo.payload.$bytes,
129
+
hash: memo.contentHash.$bytes,
130
+
length: memo.contentLength,
134
131
});
135
132
136
133
yield {
137
-
tid: item.tid,
134
+
tid: memo.tid,
138
135
text: decrypted,
139
136
};
140
137
}
···
144
141
}
145
142
146
143
/**
147
-
* Subscribes to the Jetstreams for the user's items. Pass `"stop"` into `subscription.next(...)` to cancel
144
+
* Subscribes to the Jetstreams for the user's memos. Pass `"stop"` into `subscription.next(...)` to cancel
148
145
* @todo Allow specifying Jetstream endpoint
149
146
*/
150
-
async *subscribeToItems(): AsyncGenerator<
151
-
DecryptedItem,
147
+
async *subscribeToMemos(): AsyncGenerator<
148
+
DecryptedMemo,
152
149
void,
153
150
"stop" | undefined
154
151
> {
···
158
155
159
156
const subscription = new JetstreamSubscription({
160
157
url: "wss://jetstream2.us-east.bsky.network",
161
-
wantedCollections: ["app.cistern.lexicon.item"],
158
+
wantedCollections: ["app.cistern.memo"],
162
159
wantedDids: [this.did],
163
160
});
164
161
···
166
163
if (event.kind === "commit" && event.commit.operation === "create") {
167
164
const record = event.commit.record;
168
165
169
-
if (!is(AppCisternLexiconItem.mainSchema, record)) {
166
+
if (!is(AppCisternMemo.mainSchema, record)) {
170
167
continue;
171
168
}
172
169
···
190
187
}
191
188
192
189
/**
193
-
* Deletes an item from the user's PDS by record key.
190
+
* Deletes a memo from the user's PDS by record key.
194
191
*/
195
-
async deleteItem(key: RecordKey) {
192
+
async deleteMemo(key: RecordKey) {
196
193
const res = await this.rpc.post("com.atproto.repo.deleteRecord", {
197
194
input: {
198
-
collection: "app.cistern.lexicon.item",
195
+
collection: "app.cistern.memo",
199
196
repo: this.did,
200
197
rkey: key,
201
198
},
···
203
200
204
201
if (!res.ok) {
205
202
throw new Error(
206
-
`failed to delete item ${key}: ${res.status} ${res.data.error}`,
203
+
`failed to delete memo ${key}: ${res.status} ${res.data.error}`,
207
204
);
208
205
}
209
206
}
+1
-1
packages/consumer/types.ts
+1
-1
packages/consumer/types.ts
+4
-4
packages/lexicon/README.md
+4
-4
packages/lexicon/README.md
···
4
4
5
5
## Record Types
6
6
7
-
| Collection | Description |
8
-
|------------|-------------|
9
-
| `app.cistern.lexicon.pubkey` | Public key records with human-readable names, referenced by items via AT-URI |
10
-
| `app.cistern.lexicon.item` | Encrypted item records containing ciphertext, nonce, algorithm metadata, and public key reference |
7
+
| Collection | Description |
8
+
| -------------------- | ------------------------------------------------------------------------------------------------- |
9
+
| `app.cistern.pubkey` | Public key records with human-readable names, referenced by memos via AT-URI |
10
+
| `app.cistern.memo` | Encrypted memo records containing ciphertext, nonce, algorithm metadata, and public key reference |
+4
-4
packages/lexicon/lexicons/app/cistern/lexicon/item.json
packages/lexicon/lexicons/app/cistern/memo.json
+4
-4
packages/lexicon/lexicons/app/cistern/lexicon/item.json
packages/lexicon/lexicons/app/cistern/memo.json
···
1
1
{
2
2
"lexicon": 1,
3
-
"id": "app.cistern.lexicon.item",
3
+
"id": "app.cistern.memo",
4
4
"description": "An encrypted memo intended to be accessed and deleted later.",
5
5
"defs": {
6
6
"main": {
···
21
21
"properties": {
22
22
"tid": {
23
23
"type": "string",
24
-
"description": "TID representing when this item was created",
24
+
"description": "TID representing when this memo was created",
25
25
"format": "tid"
26
26
},
27
27
"ciphertext": {
···
39
39
},
40
40
"pubkey": {
41
41
"type": "string",
42
-
"description": "URI to the public key used to encrypt this item",
42
+
"description": "URI to the public key used to encrypt this memo",
43
43
"format": "at-uri"
44
44
},
45
45
"payload": {
46
46
"type": "bytes",
47
-
"description": "Encrypted item contents"
47
+
"description": "Encrypted memo contents"
48
48
},
49
49
"contentLength": {
50
50
"type": "integer",
+3
-3
packages/lexicon/lexicons/app/cistern/lexicon/pubkey.json
packages/lexicon/lexicons/app/cistern/pubkey.json
+3
-3
packages/lexicon/lexicons/app/cistern/lexicon/pubkey.json
packages/lexicon/lexicons/app/cistern/pubkey.json
···
1
1
{
2
2
"lexicon": 1,
3
-
"id": "app.cistern.lexicon.pubkey",
4
-
"description": "A public key used to encrypt Cistern items",
3
+
"id": "app.cistern.pubkey",
4
+
"description": "A public key used to encrypt Cistern memos",
5
5
"defs": {
6
6
"main": {
7
7
"type": "record",
8
-
"description": "A public key used to encrypt Cistern items",
8
+
"description": "A public key used to encrypt Cistern memos",
9
9
"record": {
10
10
"type": "object",
11
11
"required": ["name", "algorithm", "content", "createdAt"],
+2
-2
packages/lexicon/src/index.ts
+2
-2
packages/lexicon/src/index.ts
···
1
-
export * as AppCisternLexiconItem from "./types/app/cistern/lexicon/item.ts";
2
-
export * as AppCisternLexiconPubkey from "./types/app/cistern/lexicon/pubkey.ts";
1
+
export * as AppCisternMemo from "./types/app/cistern/memo.ts";
2
+
export * as AppCisternPubkey from "./types/app/cistern/pubkey.ts";
+5
-5
packages/lexicon/src/types/app/cistern/lexicon/item.ts
packages/lexicon/src/types/app/cistern/memo.ts
+5
-5
packages/lexicon/src/types/app/cistern/lexicon/item.ts
packages/lexicon/src/types/app/cistern/memo.ts
···
5
5
const _mainSchema = /*#__PURE__*/ v.record(
6
6
/*#__PURE__*/ v.string(),
7
7
/*#__PURE__*/ v.object({
8
-
$type: /*#__PURE__*/ v.literal("app.cistern.lexicon.item"),
8
+
$type: /*#__PURE__*/ v.literal("app.cistern.memo"),
9
9
/**
10
10
* Algorithm used for encryption, in <kem>-<cipher>-<hash> format.
11
11
*/
···
29
29
*/
30
30
nonce: /*#__PURE__*/ v.bytes(),
31
31
/**
32
-
* Encrypted item contents
32
+
* Encrypted memo contents
33
33
*/
34
34
payload: /*#__PURE__*/ v.bytes(),
35
35
/**
36
-
* URI to the public key used to encrypt this item
36
+
* URI to the public key used to encrypt this memo
37
37
*/
38
38
pubkey: /*#__PURE__*/ v.resourceUriString(),
39
39
/**
40
-
* TID representing when this item was created
40
+
* TID representing when this memo was created
41
41
*/
42
42
tid: /*#__PURE__*/ v.tidString(),
43
43
}),
···
53
53
54
54
declare module "@atcute/lexicons/ambient" {
55
55
interface Records {
56
-
"app.cistern.lexicon.item": mainSchema;
56
+
"app.cistern.memo": mainSchema;
57
57
}
58
58
}
+2
-2
packages/lexicon/src/types/app/cistern/lexicon/pubkey.ts
packages/lexicon/src/types/app/cistern/pubkey.ts
+2
-2
packages/lexicon/src/types/app/cistern/lexicon/pubkey.ts
packages/lexicon/src/types/app/cistern/pubkey.ts
···
5
5
const _mainSchema = /*#__PURE__*/ v.record(
6
6
/*#__PURE__*/ v.string(),
7
7
/*#__PURE__*/ v.object({
8
-
$type: /*#__PURE__*/ v.literal("app.cistern.lexicon.pubkey"),
8
+
$type: /*#__PURE__*/ v.literal("app.cistern.pubkey"),
9
9
/**
10
10
* KEM algorithm used to generate this key
11
11
*/
···
35
35
36
36
declare module "@atcute/lexicons/ambient" {
37
37
interface Records {
38
-
"app.cistern.lexicon.pubkey": mainSchema;
38
+
"app.cistern.pubkey": mainSchema;
39
39
}
40
40
}
+3
-3
packages/producer/README.md
+3
-3
packages/producer/README.md
···
1
1
# @cistern/producer
2
2
3
-
Producer client for creating and encrypting Cistern items.
3
+
Producer client for creating and encrypting Cistern memos.
4
4
5
5
## Usage
6
6
···
18
18
19
19
producer.selectPublicKey(pubkey);
20
20
21
-
const itemUri = await producer.createItem("Hello, world!");
21
+
const memoUri = await producer.createMemo("Hello, world!");
22
22
```
23
23
24
24
Or, if you already have a public key record ID:
···
30
30
publicKey: "3jzfcijpj2z",
31
31
});
32
32
33
-
const itemUri = await producer.createItem("Hello, world!");
33
+
const memoUri = await producer.createMemo("Hello, world!");
34
34
```
+31
-32
packages/producer/mod.test.ts
+31
-32
packages/producer/mod.test.ts
···
4
4
import type { ProducerParams, PublicKeyOption } from "./types.ts";
5
5
import type { Client, CredentialManager } from "@atcute/client";
6
6
import type { Did, Handle, ResourceUri } from "@atcute/lexicons";
7
-
import type { AppCisternLexiconPubkey } from "@cistern/lexicon";
7
+
import type { AppCisternPubkey } from "@cistern/lexicon";
8
8
9
9
// Helper to create a mock Producer instance
10
10
function createMockProducer(
···
57
57
name: "Producer constructor initializes with existing public key",
58
58
fn() {
59
59
const mockPublicKey: PublicKeyOption = {
60
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
60
+
uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri,
61
61
name: "Test Key",
62
62
content: new Uint8Array(32).toBase64(),
63
63
};
···
73
73
});
74
74
75
75
Deno.test({
76
-
name: "createItem successfully creates and uploads an encrypted item",
76
+
name: "createMemo successfully creates and uploads an encrypted memo",
77
77
async fn() {
78
78
const keys = generateKeys();
79
79
let capturedRecord: unknown;
···
92
92
return Promise.resolve({
93
93
ok: true,
94
94
data: {
95
-
uri:
96
-
"at://did:plc:test/app.cistern.lexicon.item/item123" as ResourceUri,
95
+
uri: "at://did:plc:test/app.cistern.memo/memo123" as ResourceUri,
97
96
},
98
97
});
99
98
}
···
104
103
const producer = createMockProducer({
105
104
rpc: mockRpc,
106
105
publicKey: {
107
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
106
+
uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri,
108
107
name: "Test Key",
109
108
content: keys.publicKey.toBase64(),
110
109
},
111
110
});
112
111
113
-
const uri = await producer.createItem("Test message");
112
+
const uri = await producer.createMemo("Test message");
114
113
115
-
expect(uri).toEqual("at://did:plc:test/app.cistern.lexicon.item/item123");
116
-
expect(capturedCollection).toEqual("app.cistern.lexicon.item");
114
+
expect(uri).toEqual("at://did:plc:test/app.cistern.memo/memo123");
115
+
expect(capturedCollection).toEqual("app.cistern.memo");
117
116
expect(capturedRecord).toMatchObject({
118
-
$type: "app.cistern.lexicon.item",
117
+
$type: "app.cistern.memo",
119
118
algorithm: "x_wing-xchacha20_poly1305-sha3_512",
120
119
});
121
120
},
122
121
});
123
122
124
123
Deno.test({
125
-
name: "createItem throws when no public key is set",
124
+
name: "createMemo throws when no public key is set",
126
125
async fn() {
127
126
const producer = createMockProducer();
128
127
129
-
await expect(producer.createItem("Test message")).rejects.toThrow(
130
-
"no public key set; select a public key before creating an item",
128
+
await expect(producer.createMemo("Test message")).rejects.toThrow(
129
+
"no public key set; select a public key before creating a memo",
131
130
);
132
131
},
133
132
});
134
133
135
134
Deno.test({
136
-
name: "createItem throws when upload fails",
135
+
name: "createMemo throws when upload fails",
137
136
async fn() {
138
137
const keys = generateKeys();
139
138
const mockRpc = {
···
148
147
const producer = createMockProducer({
149
148
rpc: mockRpc,
150
149
publicKey: {
151
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
150
+
uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri,
152
151
name: "Test Key",
153
152
content: keys.publicKey.toBase64(),
154
153
},
155
154
});
156
155
157
-
await expect(producer.createItem("Test message")).rejects.toThrow(
158
-
"failed to create new item",
156
+
await expect(producer.createMemo("Test message")).rejects.toThrow(
157
+
"failed to create new memo",
159
158
);
160
159
},
161
160
});
···
171
170
data: {
172
171
records: [
173
172
{
174
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1",
173
+
uri: "at://did:plc:test/app.cistern.pubkey/key1",
175
174
value: {
176
-
$type: "app.cistern.lexicon.pubkey",
175
+
$type: "app.cistern.pubkey",
177
176
name: "Key 1",
178
177
algorithm: "x_wing",
179
178
content: { $bytes: new Uint8Array(32).toBase64() },
180
179
createdAt: new Date().toISOString(),
181
-
} as AppCisternLexiconPubkey.Main,
180
+
} as AppCisternPubkey.Main,
182
181
},
183
182
{
184
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key2",
183
+
uri: "at://did:plc:test/app.cistern.pubkey/key2",
185
184
value: {
186
-
$type: "app.cistern.lexicon.pubkey",
185
+
$type: "app.cistern.pubkey",
187
186
name: "Key 2",
188
187
algorithm: "x_wing",
189
188
content: { $bytes: new Uint8Array(32).toBase64() },
190
189
createdAt: new Date().toISOString(),
191
-
} as AppCisternLexiconPubkey.Main,
190
+
} as AppCisternPubkey.Main,
192
191
},
193
192
],
194
193
cursor: undefined,
···
227
226
data: {
228
227
records: [
229
228
{
230
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1",
229
+
uri: "at://did:plc:test/app.cistern.pubkey/key1",
231
230
value: {
232
-
$type: "app.cistern.lexicon.pubkey",
231
+
$type: "app.cistern.pubkey",
233
232
name: "Key 1",
234
233
algorithm: "x_wing",
235
234
content: { $bytes: new Uint8Array(32).toBase64() },
236
235
createdAt: new Date().toISOString(),
237
-
} as AppCisternLexiconPubkey.Main,
236
+
} as AppCisternPubkey.Main,
238
237
},
239
238
],
240
239
cursor: "next-page",
···
246
245
data: {
247
246
records: [
248
247
{
249
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key2",
248
+
uri: "at://did:plc:test/app.cistern.pubkey/key2",
250
249
value: {
251
-
$type: "app.cistern.lexicon.pubkey",
250
+
$type: "app.cistern.pubkey",
252
251
name: "Key 2",
253
252
algorithm: "x_wing",
254
253
content: { $bytes: new Uint8Array(32).toBase64() },
255
254
createdAt: new Date().toISOString(),
256
-
} as AppCisternLexiconPubkey.Main,
255
+
} as AppCisternPubkey.Main,
257
256
},
258
257
],
259
258
cursor: undefined,
···
304
303
const producer = createMockProducer();
305
304
306
305
const mockPublicKey: PublicKeyOption = {
307
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
306
+
uri: "at://did:plc:test/app.cistern.pubkey/key1" as ResourceUri,
308
307
name: "Selected Key",
309
308
content: new Uint8Array(32).toBase64(),
310
309
};
···
324
323
fn() {
325
324
const producer = createMockProducer({
326
325
publicKey: {
327
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/old" as ResourceUri,
326
+
uri: "at://did:plc:test/app.cistern.pubkey/old" as ResourceUri,
328
327
name: "Old Key",
329
328
content: new Uint8Array(32).toBase64(),
330
329
},
···
333
332
expect(producer.publicKey?.name).toEqual("Old Key");
334
333
335
334
const newKey: PublicKeyOption = {
336
-
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/new" as ResourceUri,
335
+
uri: "at://did:plc:test/app.cistern.pubkey/new" as ResourceUri,
337
336
name: "New Key",
338
337
content: new Uint8Array(32).toBase64(),
339
338
};
+14
-17
packages/producer/mod.ts
+14
-17
packages/producer/mod.ts
···
8
8
import { type Did, parse, type ResourceUri } from "@atcute/lexicons";
9
9
import type { Client, CredentialManager } from "@atcute/client";
10
10
import { now } from "@atcute/tid";
11
-
import {
12
-
type AppCisternLexiconItem,
13
-
AppCisternLexiconPubkey,
14
-
} from "@cistern/lexicon";
11
+
import { type AppCisternMemo, AppCisternPubkey } from "@cistern/lexicon";
15
12
16
13
import type {} from "@atcute/atproto";
17
14
···
26
23
params: {
27
24
repo: reqs.miniDoc.did,
28
25
rkey,
29
-
collection: "app.cistern.lexicon.pubkey",
26
+
collection: "app.cistern.pubkey",
30
27
},
31
28
});
32
29
···
36
33
);
37
34
}
38
35
39
-
const record = parse(AppCisternLexiconPubkey.mainSchema, res.data.value);
36
+
const record = parse(AppCisternPubkey.mainSchema, res.data.value);
40
37
41
38
publicKey = {
42
39
uri: res.data.uri,
···
65
62
}
66
63
67
64
/**
68
-
* Creates an item and saves it as a record in the user's PDS
65
+
* Creates a memo and saves it as a record in the user's PDS
69
66
*/
70
-
async createItem(text: string): Promise<ResourceUri> {
67
+
async createMemo(text: string): Promise<ResourceUri> {
71
68
if (!this.publicKey) {
72
69
throw new Error(
73
-
"no public key set; select a public key before creating an item",
70
+
"no public key set; select a public key before creating a memo",
74
71
);
75
72
}
76
73
···
78
75
Uint8Array.fromBase64(this.publicKey.content),
79
76
text,
80
77
);
81
-
const record: AppCisternLexiconItem.Main = {
82
-
$type: "app.cistern.lexicon.item",
78
+
const record: AppCisternMemo.Main = {
79
+
$type: "app.cistern.memo",
83
80
tid: now(),
84
81
algorithm: "x_wing-xchacha20_poly1305-sha3_512",
85
82
ciphertext: { $bytes: payload.cipherText },
···
92
89
93
90
const res = await this.rpc.post("com.atproto.repo.createRecord", {
94
91
input: {
95
-
collection: "app.cistern.lexicon.item",
92
+
collection: "app.cistern.memo",
96
93
repo: this.did,
97
94
record,
98
95
},
···
100
97
101
98
if (!res.ok) {
102
99
throw new Error(
103
-
`failed to create new item: ${res.status} ${res.data.error}`,
100
+
`failed to create new memo: ${res.status} ${res.data.error}`,
104
101
);
105
102
}
106
103
···
120
117
while (true) {
121
118
const res = await this.rpc.get("com.atproto.repo.listRecords", {
122
119
params: {
123
-
collection: "app.cistern.lexicon.pubkey",
120
+
collection: "app.cistern.pubkey",
124
121
repo: this.did,
125
122
cursor,
126
123
},
···
135
132
cursor = res.data.cursor;
136
133
137
134
for (const record of res.data.records) {
138
-
const item = parse(AppCisternLexiconPubkey.mainSchema, record.value);
135
+
const memo = parse(AppCisternPubkey.mainSchema, record.value);
139
136
140
137
yield {
141
138
uri: record.uri,
142
-
content: item.content.$bytes,
143
-
name: item.name,
139
+
content: memo.content.$bytes,
140
+
name: memo.name,
144
141
};
145
142
}
146
143