@ascorbic/pds 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -0
- package/dist/cli.js +528 -0
- package/dist/index.d.ts +332 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3130 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3130 @@
|
|
|
1
|
+
import { DurableObject, env } from "cloudflare:workers";
|
|
2
|
+
import { BlockMap, ReadableBlockstore, Repo, WriteOpAction, blocksToCarFile, readCarWithRoot } from "@atproto/repo";
|
|
3
|
+
import { Secp256k1Keypair, randomStr } from "@atproto/crypto";
|
|
4
|
+
import { CID } from "@atproto/lex-data";
|
|
5
|
+
import { TID } from "@atproto/common-web";
|
|
6
|
+
import { AtUri, ensureValidDid, ensureValidHandle } from "@atproto/syntax";
|
|
7
|
+
import { cidForRawBytes, decode, encode } from "@atproto/lex-cbor";
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import { cors } from "hono/cors";
|
|
10
|
+
import { SignJWT, jwtVerify } from "jose";
|
|
11
|
+
import { compare } from "bcryptjs";
|
|
12
|
+
import { Lexicons } from "@atproto/lexicon";
|
|
13
|
+
|
|
14
|
+
//#region rolldown:runtime
|
|
15
|
+
var __defProp = Object.defineProperty;
|
|
16
|
+
var __exportAll = (all, symbols) => {
|
|
17
|
+
let target = {};
|
|
18
|
+
for (var name in all) {
|
|
19
|
+
__defProp(target, name, {
|
|
20
|
+
get: all[name],
|
|
21
|
+
enumerable: true
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (symbols) {
|
|
25
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
26
|
+
}
|
|
27
|
+
return target;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/storage.ts
|
|
32
|
+
/**
|
|
33
|
+
* SQLite-backed repository storage for Cloudflare Durable Objects.
|
|
34
|
+
*
|
|
35
|
+
* Implements the RepoStorage interface from @atproto/repo, storing blocks
|
|
36
|
+
* in a SQLite database within a Durable Object.
|
|
37
|
+
*/
|
|
38
|
+
var SqliteRepoStorage = class extends ReadableBlockstore {
|
|
39
|
+
constructor(sql) {
|
|
40
|
+
super();
|
|
41
|
+
this.sql = sql;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Initialize the database schema. Should be called once on DO startup.
|
|
45
|
+
*/
|
|
46
|
+
initSchema() {
|
|
47
|
+
this.sql.exec(`
|
|
48
|
+
-- Block storage (MST nodes + record blocks)
|
|
49
|
+
CREATE TABLE IF NOT EXISTS blocks (
|
|
50
|
+
cid TEXT PRIMARY KEY,
|
|
51
|
+
bytes BLOB NOT NULL,
|
|
52
|
+
rev TEXT NOT NULL
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_blocks_rev ON blocks(rev);
|
|
56
|
+
|
|
57
|
+
-- Repo state (single row)
|
|
58
|
+
CREATE TABLE IF NOT EXISTS repo_state (
|
|
59
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
60
|
+
root_cid TEXT,
|
|
61
|
+
rev TEXT,
|
|
62
|
+
seq INTEGER NOT NULL DEFAULT 0
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
-- Initialize with empty state if not exists
|
|
66
|
+
INSERT OR IGNORE INTO repo_state (id, root_cid, rev, seq)
|
|
67
|
+
VALUES (1, NULL, NULL, 0);
|
|
68
|
+
|
|
69
|
+
-- Firehose events (sequenced commit log)
|
|
70
|
+
CREATE TABLE IF NOT EXISTS firehose_events (
|
|
71
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
72
|
+
event_type TEXT NOT NULL,
|
|
73
|
+
payload BLOB NOT NULL,
|
|
74
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_firehose_created_at ON firehose_events(created_at);
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get the current root CID of the repository.
|
|
82
|
+
*/
|
|
83
|
+
async getRoot() {
|
|
84
|
+
const rows = this.sql.exec("SELECT root_cid FROM repo_state WHERE id = 1").toArray();
|
|
85
|
+
if (rows.length === 0 || !rows[0]?.root_cid) return null;
|
|
86
|
+
return CID.parse(rows[0].root_cid);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get the current revision string.
|
|
90
|
+
*/
|
|
91
|
+
async getRev() {
|
|
92
|
+
const rows = this.sql.exec("SELECT rev FROM repo_state WHERE id = 1").toArray();
|
|
93
|
+
return rows.length > 0 ? rows[0].rev ?? null : null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get the current sequence number for firehose events.
|
|
97
|
+
*/
|
|
98
|
+
async getSeq() {
|
|
99
|
+
const rows = this.sql.exec("SELECT seq FROM repo_state WHERE id = 1").toArray();
|
|
100
|
+
return rows.length > 0 ? rows[0].seq ?? 0 : 0;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Increment and return the next sequence number.
|
|
104
|
+
*/
|
|
105
|
+
async nextSeq() {
|
|
106
|
+
this.sql.exec("UPDATE repo_state SET seq = seq + 1 WHERE id = 1");
|
|
107
|
+
return this.getSeq();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get the raw bytes for a block by CID.
|
|
111
|
+
*/
|
|
112
|
+
async getBytes(cid) {
|
|
113
|
+
const rows = this.sql.exec("SELECT bytes FROM blocks WHERE cid = ?", cid.toString()).toArray();
|
|
114
|
+
if (rows.length === 0 || !rows[0]?.bytes) return null;
|
|
115
|
+
return new Uint8Array(rows[0].bytes);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if a block exists.
|
|
119
|
+
*/
|
|
120
|
+
async has(cid) {
|
|
121
|
+
return this.sql.exec("SELECT 1 FROM blocks WHERE cid = ? LIMIT 1", cid.toString()).toArray().length > 0;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get multiple blocks at once.
|
|
125
|
+
*/
|
|
126
|
+
async getBlocks(cids) {
|
|
127
|
+
const blocks = new BlockMap();
|
|
128
|
+
const missing = [];
|
|
129
|
+
for (const cid of cids) {
|
|
130
|
+
const bytes = await this.getBytes(cid);
|
|
131
|
+
if (bytes) blocks.set(cid, bytes);
|
|
132
|
+
else missing.push(cid);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
blocks,
|
|
136
|
+
missing
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Store a single block.
|
|
141
|
+
*/
|
|
142
|
+
async putBlock(cid, block, rev) {
|
|
143
|
+
this.sql.exec("INSERT OR REPLACE INTO blocks (cid, bytes, rev) VALUES (?, ?, ?)", cid.toString(), block, rev);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Store multiple blocks at once.
|
|
147
|
+
*/
|
|
148
|
+
async putMany(blocks, rev) {
|
|
149
|
+
const internalMap = blocks.map;
|
|
150
|
+
if (internalMap) for (const [cidStr, bytes] of internalMap) this.sql.exec("INSERT OR REPLACE INTO blocks (cid, bytes, rev) VALUES (?, ?, ?)", cidStr, bytes, rev);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Update the repository root.
|
|
154
|
+
*/
|
|
155
|
+
async updateRoot(cid, rev) {
|
|
156
|
+
this.sql.exec("UPDATE repo_state SET root_cid = ?, rev = ? WHERE id = 1", cid.toString(), rev);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Apply a commit atomically: add new blocks, remove old blocks, update root.
|
|
160
|
+
*/
|
|
161
|
+
async applyCommit(commit) {
|
|
162
|
+
const internalMap = commit.newBlocks.map;
|
|
163
|
+
if (internalMap) for (const [cidStr, bytes] of internalMap) this.sql.exec("INSERT OR REPLACE INTO blocks (cid, bytes, rev) VALUES (?, ?, ?)", cidStr, bytes, commit.rev);
|
|
164
|
+
const removedSet = commit.removedCids.set;
|
|
165
|
+
if (removedSet) for (const cidStr of removedSet) this.sql.exec("DELETE FROM blocks WHERE cid = ?", cidStr);
|
|
166
|
+
await this.updateRoot(commit.cid, commit.rev);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get total storage size in bytes.
|
|
170
|
+
*/
|
|
171
|
+
async sizeInBytes() {
|
|
172
|
+
const rows = this.sql.exec("SELECT SUM(LENGTH(bytes)) as total FROM blocks").toArray();
|
|
173
|
+
return rows.length > 0 ? rows[0].total ?? 0 : 0;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Clear all data (for testing).
|
|
177
|
+
*/
|
|
178
|
+
async destroy() {
|
|
179
|
+
this.sql.exec("DELETE FROM blocks");
|
|
180
|
+
this.sql.exec("UPDATE repo_state SET root_cid = NULL, rev = NULL WHERE id = 1");
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Count the number of blocks stored.
|
|
184
|
+
*/
|
|
185
|
+
async countBlocks() {
|
|
186
|
+
const rows = this.sql.exec("SELECT COUNT(*) as count FROM blocks").toArray();
|
|
187
|
+
return rows.length > 0 ? rows[0].count ?? 0 : 0;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/sequencer.ts
|
|
193
|
+
/**
|
|
194
|
+
* Sequencer manages the firehose event log.
|
|
195
|
+
*
|
|
196
|
+
* Stores commit events in SQLite and provides methods for:
|
|
197
|
+
* - Sequencing new commits
|
|
198
|
+
* - Backfilling events from a cursor
|
|
199
|
+
* - Getting the latest sequence number
|
|
200
|
+
*/
|
|
201
|
+
var Sequencer = class {
|
|
202
|
+
constructor(sql) {
|
|
203
|
+
this.sql = sql;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Add a commit to the firehose sequence.
|
|
207
|
+
* Returns the complete sequenced event for broadcasting.
|
|
208
|
+
*/
|
|
209
|
+
async sequenceCommit(data) {
|
|
210
|
+
const carBytes = await blocksToCarFile(data.commit, data.newBlocks);
|
|
211
|
+
const time = (/* @__PURE__ */ new Date()).toISOString();
|
|
212
|
+
const eventPayload = {
|
|
213
|
+
repo: data.did,
|
|
214
|
+
commit: data.commit,
|
|
215
|
+
rev: data.rev,
|
|
216
|
+
since: data.since,
|
|
217
|
+
blocks: carBytes,
|
|
218
|
+
ops: data.ops.map((op) => ({
|
|
219
|
+
action: op.action,
|
|
220
|
+
path: `${op.collection}/${op.rkey}`,
|
|
221
|
+
cid: "cid" in op && op.cid ? op.cid : null
|
|
222
|
+
})),
|
|
223
|
+
rebase: false,
|
|
224
|
+
tooBig: carBytes.length > 1e6,
|
|
225
|
+
blobs: [],
|
|
226
|
+
time
|
|
227
|
+
};
|
|
228
|
+
const payload = encode(eventPayload);
|
|
229
|
+
const seq = this.sql.exec(`INSERT INTO firehose_events (event_type, payload)
|
|
230
|
+
VALUES ('commit', ?)
|
|
231
|
+
RETURNING seq`, payload).one().seq;
|
|
232
|
+
return {
|
|
233
|
+
seq,
|
|
234
|
+
type: "commit",
|
|
235
|
+
event: {
|
|
236
|
+
...eventPayload,
|
|
237
|
+
seq
|
|
238
|
+
},
|
|
239
|
+
time
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get events from a cursor position.
|
|
244
|
+
* Returns up to `limit` events after the cursor.
|
|
245
|
+
*/
|
|
246
|
+
async getEventsSince(cursor, limit = 100) {
|
|
247
|
+
return this.sql.exec(`SELECT seq, event_type, payload, created_at
|
|
248
|
+
FROM firehose_events
|
|
249
|
+
WHERE seq > ?
|
|
250
|
+
ORDER BY seq ASC
|
|
251
|
+
LIMIT ?`, cursor, limit).toArray().map((row) => {
|
|
252
|
+
const decoded = decode(new Uint8Array(row.payload));
|
|
253
|
+
return {
|
|
254
|
+
seq: row.seq,
|
|
255
|
+
type: "commit",
|
|
256
|
+
event: {
|
|
257
|
+
...decoded,
|
|
258
|
+
seq: row.seq
|
|
259
|
+
},
|
|
260
|
+
time: row.created_at
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Get the latest sequence number.
|
|
266
|
+
* Returns 0 if no events have been sequenced yet.
|
|
267
|
+
*/
|
|
268
|
+
getLatestSeq() {
|
|
269
|
+
return this.sql.exec("SELECT MAX(seq) as seq FROM firehose_events").one()?.seq ?? 0;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Prune old events to keep the log from growing indefinitely.
|
|
273
|
+
* Keeps the most recent `keepCount` events.
|
|
274
|
+
*/
|
|
275
|
+
async pruneOldEvents(keepCount = 1e4) {
|
|
276
|
+
this.sql.exec(`DELETE FROM firehose_events
|
|
277
|
+
WHERE seq < (SELECT MAX(seq) - ? FROM firehose_events)`, keepCount);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/blobs.ts
|
|
283
|
+
/**
|
|
284
|
+
* BlobStore manages blob storage in R2.
|
|
285
|
+
* Blobs are stored with CID-based keys prefixed by the account's DID.
|
|
286
|
+
*/
|
|
287
|
+
var BlobStore = class {
|
|
288
|
+
constructor(r2, did) {
|
|
289
|
+
this.r2 = r2;
|
|
290
|
+
this.did = did;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Upload a blob to R2 and return a BlobRef.
|
|
294
|
+
*/
|
|
295
|
+
async putBlob(bytes, mimeType) {
|
|
296
|
+
const cid = await cidForRawBytes(bytes);
|
|
297
|
+
const key = `${this.did}/${cid.toString()}`;
|
|
298
|
+
await this.r2.put(key, bytes, { httpMetadata: { contentType: mimeType } });
|
|
299
|
+
return {
|
|
300
|
+
$type: "blob",
|
|
301
|
+
ref: { $link: cid.toString() },
|
|
302
|
+
mimeType,
|
|
303
|
+
size: bytes.length
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Retrieve a blob from R2 by CID.
|
|
308
|
+
*/
|
|
309
|
+
async getBlob(cid) {
|
|
310
|
+
const key = `${this.did}/${cid.toString()}`;
|
|
311
|
+
return this.r2.get(key);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Check if a blob exists in R2.
|
|
315
|
+
*/
|
|
316
|
+
async hasBlob(cid) {
|
|
317
|
+
const key = `${this.did}/${cid.toString()}`;
|
|
318
|
+
return await this.r2.head(key) !== null;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/account-do.ts
|
|
324
|
+
/**
|
|
325
|
+
* Account Durable Object - manages a single user's AT Protocol repository.
|
|
326
|
+
*
|
|
327
|
+
* This DO provides:
|
|
328
|
+
* - SQLite-backed block storage for the repository
|
|
329
|
+
* - AT Protocol Repo instance for repository operations
|
|
330
|
+
* - Firehose WebSocket connections
|
|
331
|
+
* - Sequence number management
|
|
332
|
+
*/
|
|
333
|
+
var AccountDurableObject = class extends DurableObject {
|
|
334
|
+
storage = null;
|
|
335
|
+
repo = null;
|
|
336
|
+
keypair = null;
|
|
337
|
+
sequencer = null;
|
|
338
|
+
blobStore = null;
|
|
339
|
+
storageInitialized = false;
|
|
340
|
+
repoInitialized = false;
|
|
341
|
+
constructor(ctx, env$2) {
|
|
342
|
+
super(ctx, env$2);
|
|
343
|
+
if (!env$2.SIGNING_KEY) throw new Error("Missing required environment variable: SIGNING_KEY");
|
|
344
|
+
if (!env$2.DID) throw new Error("Missing required environment variable: DID");
|
|
345
|
+
if (env$2.BLOBS) this.blobStore = new BlobStore(env$2.BLOBS, env$2.DID);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Initialize the storage adapter. Called lazily on first storage access.
|
|
349
|
+
*/
|
|
350
|
+
async ensureStorageInitialized() {
|
|
351
|
+
if (!this.storageInitialized) await this.ctx.blockConcurrencyWhile(async () => {
|
|
352
|
+
if (this.storageInitialized) return;
|
|
353
|
+
this.storage = new SqliteRepoStorage(this.ctx.storage.sql);
|
|
354
|
+
this.storage.initSchema();
|
|
355
|
+
this.sequencer = new Sequencer(this.ctx.storage.sql);
|
|
356
|
+
this.storageInitialized = true;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Initialize the Repo instance. Called lazily on first repo access.
|
|
361
|
+
*/
|
|
362
|
+
async ensureRepoInitialized() {
|
|
363
|
+
await this.ensureStorageInitialized();
|
|
364
|
+
if (!this.repoInitialized) await this.ctx.blockConcurrencyWhile(async () => {
|
|
365
|
+
if (this.repoInitialized) return;
|
|
366
|
+
this.keypair = await Secp256k1Keypair.import(this.env.SIGNING_KEY);
|
|
367
|
+
const root = await this.storage.getRoot();
|
|
368
|
+
if (root) this.repo = await Repo.load(this.storage, root);
|
|
369
|
+
else this.repo = await Repo.create(this.storage, this.env.DID, this.keypair);
|
|
370
|
+
this.repoInitialized = true;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Get the storage adapter for direct access (used by tests and internal operations).
|
|
375
|
+
*/
|
|
376
|
+
async getStorage() {
|
|
377
|
+
await this.ensureStorageInitialized();
|
|
378
|
+
return this.storage;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Get the Repo instance for repository operations.
|
|
382
|
+
*/
|
|
383
|
+
async getRepo() {
|
|
384
|
+
await this.ensureRepoInitialized();
|
|
385
|
+
return this.repo;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Get the signing keypair for repository operations.
|
|
389
|
+
*/
|
|
390
|
+
async getKeypair() {
|
|
391
|
+
await this.ensureRepoInitialized();
|
|
392
|
+
return this.keypair;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Update the Repo instance after mutations.
|
|
396
|
+
*/
|
|
397
|
+
async setRepo(repo) {
|
|
398
|
+
this.repo = repo;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* RPC method: Get repo metadata for describeRepo
|
|
402
|
+
*/
|
|
403
|
+
async rpcDescribeRepo() {
|
|
404
|
+
const repo = await this.getRepo();
|
|
405
|
+
const collections = [];
|
|
406
|
+
const seenCollections = /* @__PURE__ */ new Set();
|
|
407
|
+
for await (const record of repo.walkRecords()) if (!seenCollections.has(record.collection)) {
|
|
408
|
+
seenCollections.add(record.collection);
|
|
409
|
+
collections.push(record.collection);
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
did: repo.did,
|
|
413
|
+
collections,
|
|
414
|
+
cid: repo.cid.toString()
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* RPC method: Get a single record
|
|
419
|
+
*/
|
|
420
|
+
async rpcGetRecord(collection, rkey) {
|
|
421
|
+
const repo = await this.getRepo();
|
|
422
|
+
const dataKey = `${collection}/${rkey}`;
|
|
423
|
+
const recordCid = await repo.data.get(dataKey);
|
|
424
|
+
if (!recordCid) return null;
|
|
425
|
+
const record = await repo.getRecord(collection, rkey);
|
|
426
|
+
if (!record) return null;
|
|
427
|
+
return {
|
|
428
|
+
cid: recordCid.toString(),
|
|
429
|
+
record
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* RPC method: List records in a collection
|
|
434
|
+
*/
|
|
435
|
+
async rpcListRecords(collection, opts) {
|
|
436
|
+
const repo = await this.getRepo();
|
|
437
|
+
const records = [];
|
|
438
|
+
const startFrom = opts.cursor || `${collection}/`;
|
|
439
|
+
for await (const record of repo.walkRecords(startFrom)) {
|
|
440
|
+
if (record.collection !== collection) {
|
|
441
|
+
if (records.length > 0) break;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
records.push({
|
|
445
|
+
uri: AtUri.make(repo.did, record.collection, record.rkey).toString(),
|
|
446
|
+
cid: record.cid.toString(),
|
|
447
|
+
value: record.record
|
|
448
|
+
});
|
|
449
|
+
if (records.length >= opts.limit + 1) break;
|
|
450
|
+
}
|
|
451
|
+
if (opts.reverse) records.reverse();
|
|
452
|
+
const hasMore = records.length > opts.limit;
|
|
453
|
+
const results = hasMore ? records.slice(0, opts.limit) : records;
|
|
454
|
+
return {
|
|
455
|
+
records: results,
|
|
456
|
+
cursor: hasMore ? `${collection}/${results[results.length - 1]?.uri.split("/").pop() ?? ""}` : void 0
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* RPC method: Create a record
|
|
461
|
+
*/
|
|
462
|
+
async rpcCreateRecord(collection, rkey, record) {
|
|
463
|
+
const repo = await this.getRepo();
|
|
464
|
+
const keypair = await this.getKeypair();
|
|
465
|
+
const actualRkey = rkey || TID.nextStr();
|
|
466
|
+
const createOp = {
|
|
467
|
+
action: WriteOpAction.Create,
|
|
468
|
+
collection,
|
|
469
|
+
rkey: actualRkey,
|
|
470
|
+
record
|
|
471
|
+
};
|
|
472
|
+
const prevRev = repo.commit.rev;
|
|
473
|
+
this.repo = await repo.applyWrites([createOp], keypair);
|
|
474
|
+
const dataKey = `${collection}/${actualRkey}`;
|
|
475
|
+
const recordCid = await this.repo.data.get(dataKey);
|
|
476
|
+
if (!recordCid) throw new Error(`Failed to create record: ${collection}/${actualRkey}`);
|
|
477
|
+
if (this.sequencer) {
|
|
478
|
+
const newBlocks = new BlockMap();
|
|
479
|
+
const rows = this.ctx.storage.sql.exec("SELECT cid, bytes FROM blocks WHERE rev = ?", this.repo.commit.rev).toArray();
|
|
480
|
+
for (const row of rows) {
|
|
481
|
+
const cid = CID.parse(row.cid);
|
|
482
|
+
const bytes = new Uint8Array(row.bytes);
|
|
483
|
+
newBlocks.set(cid, bytes);
|
|
484
|
+
}
|
|
485
|
+
const opWithCid = {
|
|
486
|
+
...createOp,
|
|
487
|
+
cid: recordCid
|
|
488
|
+
};
|
|
489
|
+
const commitData = {
|
|
490
|
+
did: this.repo.did,
|
|
491
|
+
commit: this.repo.cid,
|
|
492
|
+
rev: this.repo.commit.rev,
|
|
493
|
+
since: prevRev,
|
|
494
|
+
newBlocks,
|
|
495
|
+
ops: [opWithCid]
|
|
496
|
+
};
|
|
497
|
+
const event = await this.sequencer.sequenceCommit(commitData);
|
|
498
|
+
await this.broadcastCommit(event);
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
uri: AtUri.make(this.repo.did, collection, actualRkey).toString(),
|
|
502
|
+
cid: recordCid.toString(),
|
|
503
|
+
commit: {
|
|
504
|
+
cid: this.repo.cid.toString(),
|
|
505
|
+
rev: this.repo.commit.rev
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* RPC method: Delete a record
|
|
511
|
+
*/
|
|
512
|
+
async rpcDeleteRecord(collection, rkey) {
|
|
513
|
+
const repo = await this.getRepo();
|
|
514
|
+
const keypair = await this.getKeypair();
|
|
515
|
+
if (!await repo.getRecord(collection, rkey)) return null;
|
|
516
|
+
const deleteOp = {
|
|
517
|
+
action: WriteOpAction.Delete,
|
|
518
|
+
collection,
|
|
519
|
+
rkey
|
|
520
|
+
};
|
|
521
|
+
const prevRev = repo.commit.rev;
|
|
522
|
+
const updatedRepo = await repo.applyWrites([deleteOp], keypair);
|
|
523
|
+
this.repo = updatedRepo;
|
|
524
|
+
if (this.sequencer) {
|
|
525
|
+
const newBlocks = new BlockMap();
|
|
526
|
+
const rows = this.ctx.storage.sql.exec("SELECT cid, bytes FROM blocks WHERE rev = ?", this.repo.commit.rev).toArray();
|
|
527
|
+
for (const row of rows) {
|
|
528
|
+
const cid = CID.parse(row.cid);
|
|
529
|
+
const bytes = new Uint8Array(row.bytes);
|
|
530
|
+
newBlocks.set(cid, bytes);
|
|
531
|
+
}
|
|
532
|
+
const commitData = {
|
|
533
|
+
did: this.repo.did,
|
|
534
|
+
commit: this.repo.cid,
|
|
535
|
+
rev: this.repo.commit.rev,
|
|
536
|
+
since: prevRev,
|
|
537
|
+
newBlocks,
|
|
538
|
+
ops: [deleteOp]
|
|
539
|
+
};
|
|
540
|
+
const event = await this.sequencer.sequenceCommit(commitData);
|
|
541
|
+
await this.broadcastCommit(event);
|
|
542
|
+
}
|
|
543
|
+
return { commit: {
|
|
544
|
+
cid: updatedRepo.cid.toString(),
|
|
545
|
+
rev: updatedRepo.commit.rev
|
|
546
|
+
} };
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* RPC method: Put a record (create or update)
|
|
550
|
+
*/
|
|
551
|
+
async rpcPutRecord(collection, rkey, record) {
|
|
552
|
+
const repo = await this.getRepo();
|
|
553
|
+
const keypair = await this.getKeypair();
|
|
554
|
+
const op = await repo.getRecord(collection, rkey) !== null ? {
|
|
555
|
+
action: WriteOpAction.Update,
|
|
556
|
+
collection,
|
|
557
|
+
rkey,
|
|
558
|
+
record
|
|
559
|
+
} : {
|
|
560
|
+
action: WriteOpAction.Create,
|
|
561
|
+
collection,
|
|
562
|
+
rkey,
|
|
563
|
+
record
|
|
564
|
+
};
|
|
565
|
+
const prevRev = repo.commit.rev;
|
|
566
|
+
this.repo = await repo.applyWrites([op], keypair);
|
|
567
|
+
const dataKey = `${collection}/${rkey}`;
|
|
568
|
+
const recordCid = await this.repo.data.get(dataKey);
|
|
569
|
+
if (!recordCid) throw new Error(`Failed to put record: ${collection}/${rkey}`);
|
|
570
|
+
if (this.sequencer) {
|
|
571
|
+
const newBlocks = new BlockMap();
|
|
572
|
+
const rows = this.ctx.storage.sql.exec("SELECT cid, bytes FROM blocks WHERE rev = ?", this.repo.commit.rev).toArray();
|
|
573
|
+
for (const row of rows) {
|
|
574
|
+
const cid = CID.parse(row.cid);
|
|
575
|
+
const bytes = new Uint8Array(row.bytes);
|
|
576
|
+
newBlocks.set(cid, bytes);
|
|
577
|
+
}
|
|
578
|
+
const opWithCid = {
|
|
579
|
+
...op,
|
|
580
|
+
cid: recordCid
|
|
581
|
+
};
|
|
582
|
+
const commitData = {
|
|
583
|
+
did: this.repo.did,
|
|
584
|
+
commit: this.repo.cid,
|
|
585
|
+
rev: this.repo.commit.rev,
|
|
586
|
+
since: prevRev,
|
|
587
|
+
newBlocks,
|
|
588
|
+
ops: [opWithCid]
|
|
589
|
+
};
|
|
590
|
+
const event = await this.sequencer.sequenceCommit(commitData);
|
|
591
|
+
await this.broadcastCommit(event);
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
uri: AtUri.make(this.repo.did, collection, rkey).toString(),
|
|
595
|
+
cid: recordCid.toString(),
|
|
596
|
+
commit: {
|
|
597
|
+
cid: this.repo.cid.toString(),
|
|
598
|
+
rev: this.repo.commit.rev
|
|
599
|
+
},
|
|
600
|
+
validationStatus: "valid"
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* RPC method: Apply multiple writes (batch create/update/delete)
|
|
605
|
+
*/
|
|
606
|
+
async rpcApplyWrites(writes) {
|
|
607
|
+
const repo = await this.getRepo();
|
|
608
|
+
const keypair = await this.getKeypair();
|
|
609
|
+
const ops = [];
|
|
610
|
+
const results = [];
|
|
611
|
+
for (const write of writes) if (write.$type === "com.atproto.repo.applyWrites#create") {
|
|
612
|
+
const rkey = write.rkey || TID.nextStr();
|
|
613
|
+
const op = {
|
|
614
|
+
action: WriteOpAction.Create,
|
|
615
|
+
collection: write.collection,
|
|
616
|
+
rkey,
|
|
617
|
+
record: write.value
|
|
618
|
+
};
|
|
619
|
+
ops.push(op);
|
|
620
|
+
results.push({
|
|
621
|
+
$type: "com.atproto.repo.applyWrites#createResult",
|
|
622
|
+
collection: write.collection,
|
|
623
|
+
rkey,
|
|
624
|
+
action: WriteOpAction.Create
|
|
625
|
+
});
|
|
626
|
+
} else if (write.$type === "com.atproto.repo.applyWrites#update") {
|
|
627
|
+
if (!write.rkey) throw new Error("Update requires rkey");
|
|
628
|
+
const op = {
|
|
629
|
+
action: WriteOpAction.Update,
|
|
630
|
+
collection: write.collection,
|
|
631
|
+
rkey: write.rkey,
|
|
632
|
+
record: write.value
|
|
633
|
+
};
|
|
634
|
+
ops.push(op);
|
|
635
|
+
results.push({
|
|
636
|
+
$type: "com.atproto.repo.applyWrites#updateResult",
|
|
637
|
+
collection: write.collection,
|
|
638
|
+
rkey: write.rkey,
|
|
639
|
+
action: WriteOpAction.Update
|
|
640
|
+
});
|
|
641
|
+
} else if (write.$type === "com.atproto.repo.applyWrites#delete") {
|
|
642
|
+
if (!write.rkey) throw new Error("Delete requires rkey");
|
|
643
|
+
const op = {
|
|
644
|
+
action: WriteOpAction.Delete,
|
|
645
|
+
collection: write.collection,
|
|
646
|
+
rkey: write.rkey
|
|
647
|
+
};
|
|
648
|
+
ops.push(op);
|
|
649
|
+
results.push({
|
|
650
|
+
$type: "com.atproto.repo.applyWrites#deleteResult",
|
|
651
|
+
collection: write.collection,
|
|
652
|
+
rkey: write.rkey,
|
|
653
|
+
action: WriteOpAction.Delete
|
|
654
|
+
});
|
|
655
|
+
} else throw new Error(`Unknown write type: ${write.$type}`);
|
|
656
|
+
const prevRev = repo.commit.rev;
|
|
657
|
+
this.repo = await repo.applyWrites(ops, keypair);
|
|
658
|
+
const finalResults = [];
|
|
659
|
+
const opsWithCids = [];
|
|
660
|
+
for (let i = 0; i < results.length; i++) {
|
|
661
|
+
const result = results[i];
|
|
662
|
+
const op = ops[i];
|
|
663
|
+
if (result.action === WriteOpAction.Delete) {
|
|
664
|
+
finalResults.push({ $type: result.$type });
|
|
665
|
+
opsWithCids.push(op);
|
|
666
|
+
} else {
|
|
667
|
+
const dataKey = `${result.collection}/${result.rkey}`;
|
|
668
|
+
const recordCid = await this.repo.data.get(dataKey);
|
|
669
|
+
finalResults.push({
|
|
670
|
+
$type: result.$type,
|
|
671
|
+
uri: AtUri.make(this.repo.did, result.collection, result.rkey).toString(),
|
|
672
|
+
cid: recordCid?.toString(),
|
|
673
|
+
validationStatus: "valid"
|
|
674
|
+
});
|
|
675
|
+
opsWithCids.push({
|
|
676
|
+
...op,
|
|
677
|
+
cid: recordCid
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (this.sequencer) {
|
|
682
|
+
const newBlocks = new BlockMap();
|
|
683
|
+
const rows = this.ctx.storage.sql.exec("SELECT cid, bytes FROM blocks WHERE rev = ?", this.repo.commit.rev).toArray();
|
|
684
|
+
for (const row of rows) {
|
|
685
|
+
const cid = CID.parse(row.cid);
|
|
686
|
+
const bytes = new Uint8Array(row.bytes);
|
|
687
|
+
newBlocks.set(cid, bytes);
|
|
688
|
+
}
|
|
689
|
+
const commitData = {
|
|
690
|
+
did: this.repo.did,
|
|
691
|
+
commit: this.repo.cid,
|
|
692
|
+
rev: this.repo.commit.rev,
|
|
693
|
+
since: prevRev,
|
|
694
|
+
newBlocks,
|
|
695
|
+
ops: opsWithCids
|
|
696
|
+
};
|
|
697
|
+
const event = await this.sequencer.sequenceCommit(commitData);
|
|
698
|
+
await this.broadcastCommit(event);
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
commit: {
|
|
702
|
+
cid: this.repo.cid.toString(),
|
|
703
|
+
rev: this.repo.commit.rev
|
|
704
|
+
},
|
|
705
|
+
results: finalResults
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* RPC method: Get repo status
|
|
710
|
+
*/
|
|
711
|
+
async rpcGetRepoStatus() {
|
|
712
|
+
const repo = await this.getRepo();
|
|
713
|
+
return {
|
|
714
|
+
did: repo.did,
|
|
715
|
+
head: repo.cid.toString(),
|
|
716
|
+
rev: repo.commit.rev
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* RPC method: Export repo as CAR
|
|
721
|
+
*/
|
|
722
|
+
async rpcGetRepoCar() {
|
|
723
|
+
const root = await (await this.getStorage()).getRoot();
|
|
724
|
+
if (!root) throw new Error("No repository root found");
|
|
725
|
+
const rows = this.ctx.storage.sql.exec("SELECT cid, bytes FROM blocks").toArray();
|
|
726
|
+
const blocks = new BlockMap();
|
|
727
|
+
for (const row of rows) {
|
|
728
|
+
const cid = CID.parse(row.cid);
|
|
729
|
+
const bytes = new Uint8Array(row.bytes);
|
|
730
|
+
blocks.set(cid, bytes);
|
|
731
|
+
}
|
|
732
|
+
return blocksToCarFile(root, blocks);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* RPC method: Import repo from CAR file
|
|
736
|
+
* This is used for account migration - importing an existing repository
|
|
737
|
+
* from another PDS.
|
|
738
|
+
*/
|
|
739
|
+
async rpcImportRepo(carBytes) {
|
|
740
|
+
await this.ensureStorageInitialized();
|
|
741
|
+
if (await this.storage.getRoot()) throw new Error("Repository already exists. Cannot import over existing repository.");
|
|
742
|
+
const { root: rootCid, blocks } = await readCarWithRoot(carBytes);
|
|
743
|
+
const importRev = TID.nextStr();
|
|
744
|
+
await this.storage.putMany(blocks, importRev);
|
|
745
|
+
this.keypair = await Secp256k1Keypair.import(this.env.SIGNING_KEY);
|
|
746
|
+
this.repo = await Repo.load(this.storage, rootCid);
|
|
747
|
+
if (this.repo.did !== this.env.DID) {
|
|
748
|
+
await this.storage.destroy();
|
|
749
|
+
throw new Error(`DID mismatch: CAR file contains DID ${this.repo.did}, but expected ${this.env.DID}`);
|
|
750
|
+
}
|
|
751
|
+
this.repoInitialized = true;
|
|
752
|
+
return {
|
|
753
|
+
did: this.repo.did,
|
|
754
|
+
rev: this.repo.commit.rev,
|
|
755
|
+
cid: rootCid.toString()
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* RPC method: Upload a blob to R2
|
|
760
|
+
*/
|
|
761
|
+
async rpcUploadBlob(bytes, mimeType) {
|
|
762
|
+
if (!this.blobStore) throw new Error("Blob storage not configured");
|
|
763
|
+
const MAX_BLOB_SIZE = 5 * 1024 * 1024;
|
|
764
|
+
if (bytes.length > MAX_BLOB_SIZE) throw new Error(`Blob too large: ${bytes.length} bytes (max ${MAX_BLOB_SIZE})`);
|
|
765
|
+
return this.blobStore.putBlob(bytes, mimeType);
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* RPC method: Get a blob from R2
|
|
769
|
+
*/
|
|
770
|
+
async rpcGetBlob(cidStr) {
|
|
771
|
+
if (!this.blobStore) throw new Error("Blob storage not configured");
|
|
772
|
+
const cid = CID.parse(cidStr);
|
|
773
|
+
return this.blobStore.getBlob(cid);
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Encode a firehose frame (header + body CBOR).
|
|
777
|
+
*/
|
|
778
|
+
encodeFrame(header, body) {
|
|
779
|
+
const headerBytes = encode(header);
|
|
780
|
+
const bodyBytes = encode(body);
|
|
781
|
+
const frame = new Uint8Array(headerBytes.length + bodyBytes.length);
|
|
782
|
+
frame.set(headerBytes, 0);
|
|
783
|
+
frame.set(bodyBytes, headerBytes.length);
|
|
784
|
+
return frame;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Encode a commit event frame.
|
|
788
|
+
*/
|
|
789
|
+
encodeCommitFrame(event) {
|
|
790
|
+
return this.encodeFrame({
|
|
791
|
+
op: 1,
|
|
792
|
+
t: "#commit"
|
|
793
|
+
}, event.event);
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Encode an error frame.
|
|
797
|
+
*/
|
|
798
|
+
encodeErrorFrame(error, message) {
|
|
799
|
+
const header = { op: -1 };
|
|
800
|
+
const body = {
|
|
801
|
+
error,
|
|
802
|
+
message
|
|
803
|
+
};
|
|
804
|
+
return this.encodeFrame(header, body);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Backfill firehose events from a cursor.
|
|
808
|
+
*/
|
|
809
|
+
async backfillFirehose(ws, cursor) {
|
|
810
|
+
if (!this.sequencer) throw new Error("Sequencer not initialized");
|
|
811
|
+
if (cursor > this.sequencer.getLatestSeq()) {
|
|
812
|
+
const frame = this.encodeErrorFrame("FutureCursor", "Cursor is in the future");
|
|
813
|
+
ws.send(frame);
|
|
814
|
+
ws.close(1008, "FutureCursor");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const events = await this.sequencer.getEventsSince(cursor, 1e3);
|
|
818
|
+
for (const event of events) {
|
|
819
|
+
const frame = this.encodeCommitFrame(event);
|
|
820
|
+
ws.send(frame);
|
|
821
|
+
}
|
|
822
|
+
if (events.length > 0) {
|
|
823
|
+
const lastEvent = events[events.length - 1];
|
|
824
|
+
if (lastEvent) {
|
|
825
|
+
const attachment = ws.deserializeAttachment();
|
|
826
|
+
attachment.cursor = lastEvent.seq;
|
|
827
|
+
ws.serializeAttachment(attachment);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Broadcast a commit event to all connected firehose clients.
|
|
833
|
+
*/
|
|
834
|
+
async broadcastCommit(event) {
|
|
835
|
+
const frame = this.encodeCommitFrame(event);
|
|
836
|
+
for (const ws of this.ctx.getWebSockets()) try {
|
|
837
|
+
ws.send(frame);
|
|
838
|
+
const attachment = ws.deserializeAttachment();
|
|
839
|
+
attachment.cursor = event.seq;
|
|
840
|
+
ws.serializeAttachment(attachment);
|
|
841
|
+
} catch (e) {
|
|
842
|
+
console.error("Error broadcasting to WebSocket:", e);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Handle WebSocket upgrade for firehose (subscribeRepos).
|
|
847
|
+
*/
|
|
848
|
+
async handleFirehoseUpgrade(request) {
|
|
849
|
+
await this.ensureStorageInitialized();
|
|
850
|
+
const cursorParam = new URL(request.url).searchParams.get("cursor");
|
|
851
|
+
const cursor = cursorParam ? parseInt(cursorParam, 10) : null;
|
|
852
|
+
const pair = new WebSocketPair();
|
|
853
|
+
const client = pair[0];
|
|
854
|
+
const server = pair[1];
|
|
855
|
+
this.ctx.acceptWebSocket(server);
|
|
856
|
+
server.serializeAttachment({
|
|
857
|
+
cursor: cursor ?? 0,
|
|
858
|
+
connectedAt: Date.now()
|
|
859
|
+
});
|
|
860
|
+
if (cursor !== null) await this.backfillFirehose(server, cursor);
|
|
861
|
+
return new Response(null, {
|
|
862
|
+
status: 101,
|
|
863
|
+
webSocket: client
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* WebSocket message handler (hibernation API).
|
|
868
|
+
*/
|
|
869
|
+
webSocketMessage(_ws, _message) {}
|
|
870
|
+
/**
|
|
871
|
+
* WebSocket close handler (hibernation API).
|
|
872
|
+
*/
|
|
873
|
+
webSocketClose(_ws, _code, _reason, _wasClean) {}
|
|
874
|
+
/**
|
|
875
|
+
* WebSocket error handler (hibernation API).
|
|
876
|
+
*/
|
|
877
|
+
webSocketError(_ws, error) {
|
|
878
|
+
console.error("WebSocket error:", error);
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Emit an identity event to notify downstream services to refresh identity cache.
|
|
882
|
+
*/
|
|
883
|
+
async rpcEmitIdentityEvent(handle) {
|
|
884
|
+
await this.ensureStorageInitialized();
|
|
885
|
+
const time = (/* @__PURE__ */ new Date()).toISOString();
|
|
886
|
+
const seq = this.ctx.storage.sql.exec(`INSERT INTO firehose_events (event_type, payload)
|
|
887
|
+
VALUES ('identity', ?)
|
|
888
|
+
RETURNING seq`, new Uint8Array(0)).one().seq;
|
|
889
|
+
const header = {
|
|
890
|
+
op: 1,
|
|
891
|
+
t: "#identity"
|
|
892
|
+
};
|
|
893
|
+
const body = {
|
|
894
|
+
seq,
|
|
895
|
+
did: this.env.DID,
|
|
896
|
+
time,
|
|
897
|
+
handle
|
|
898
|
+
};
|
|
899
|
+
const headerBytes = encode(header);
|
|
900
|
+
const bodyBytes = encode(body);
|
|
901
|
+
const frame = new Uint8Array(headerBytes.length + bodyBytes.length);
|
|
902
|
+
frame.set(headerBytes, 0);
|
|
903
|
+
frame.set(bodyBytes, headerBytes.length);
|
|
904
|
+
for (const ws of this.ctx.getWebSockets()) try {
|
|
905
|
+
ws.send(frame);
|
|
906
|
+
} catch (e) {
|
|
907
|
+
console.error("Error broadcasting identity event:", e);
|
|
908
|
+
}
|
|
909
|
+
return { seq };
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* HTTP fetch handler for WebSocket upgrades.
|
|
913
|
+
* This is used instead of RPC to avoid WebSocket serialization errors.
|
|
914
|
+
*/
|
|
915
|
+
async fetch(request) {
|
|
916
|
+
if (new URL(request.url).pathname === "/xrpc/com.atproto.sync.subscribeRepos") return this.handleFirehoseUpgrade(request);
|
|
917
|
+
return new Response("Method not allowed", { status: 405 });
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
//#endregion
|
|
922
|
+
//#region src/session.ts
|
|
923
|
+
const ACCESS_TOKEN_LIFETIME = "2h";
|
|
924
|
+
const REFRESH_TOKEN_LIFETIME = "90d";
|
|
925
|
+
/**
|
|
926
|
+
* Create a secret key from string for HS256 signing
|
|
927
|
+
*/
|
|
928
|
+
function createSecretKey(secret) {
|
|
929
|
+
return new TextEncoder().encode(secret);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Create an access token (short-lived, 2 hours)
|
|
933
|
+
*/
|
|
934
|
+
async function createAccessToken(jwtSecret, userDid, serviceDid) {
|
|
935
|
+
const secret = createSecretKey(jwtSecret);
|
|
936
|
+
return new SignJWT({ scope: "atproto" }).setProtectedHeader({
|
|
937
|
+
alg: "HS256",
|
|
938
|
+
typ: "at+jwt"
|
|
939
|
+
}).setIssuedAt().setIssuer(serviceDid).setAudience(serviceDid).setSubject(userDid).setExpirationTime(ACCESS_TOKEN_LIFETIME).sign(secret);
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Create a refresh token (long-lived, 90 days)
|
|
943
|
+
*/
|
|
944
|
+
async function createRefreshToken(jwtSecret, userDid, serviceDid) {
|
|
945
|
+
const secret = createSecretKey(jwtSecret);
|
|
946
|
+
return new SignJWT({
|
|
947
|
+
scope: "com.atproto.refresh",
|
|
948
|
+
jti: crypto.randomUUID()
|
|
949
|
+
}).setProtectedHeader({
|
|
950
|
+
alg: "HS256",
|
|
951
|
+
typ: "refresh+jwt"
|
|
952
|
+
}).setIssuedAt().setIssuer(serviceDid).setAudience(serviceDid).setSubject(userDid).setExpirationTime(REFRESH_TOKEN_LIFETIME).sign(secret);
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Verify an access token and return the payload
|
|
956
|
+
*/
|
|
957
|
+
async function verifyAccessToken(token, jwtSecret, serviceDid) {
|
|
958
|
+
const { payload, protectedHeader } = await jwtVerify(token, createSecretKey(jwtSecret), {
|
|
959
|
+
issuer: serviceDid,
|
|
960
|
+
audience: serviceDid
|
|
961
|
+
});
|
|
962
|
+
if (protectedHeader.typ !== "at+jwt") throw new Error("Invalid token type");
|
|
963
|
+
if (payload.scope !== "atproto") throw new Error("Invalid scope");
|
|
964
|
+
return payload;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Verify a refresh token and return the payload
|
|
968
|
+
*/
|
|
969
|
+
async function verifyRefreshToken(token, jwtSecret, serviceDid) {
|
|
970
|
+
const { payload, protectedHeader } = await jwtVerify(token, createSecretKey(jwtSecret), {
|
|
971
|
+
issuer: serviceDid,
|
|
972
|
+
audience: serviceDid
|
|
973
|
+
});
|
|
974
|
+
if (protectedHeader.typ !== "refresh+jwt") throw new Error("Invalid token type");
|
|
975
|
+
if (payload.scope !== "com.atproto.refresh") throw new Error("Invalid scope");
|
|
976
|
+
if (!payload.jti) throw new Error("Missing token ID");
|
|
977
|
+
return payload;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Verify a password against a bcrypt hash
|
|
981
|
+
*/
|
|
982
|
+
async function verifyPassword(password, hash) {
|
|
983
|
+
return compare(password, hash);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
//#endregion
|
|
987
|
+
//#region src/middleware/auth.ts
|
|
988
|
+
async function requireAuth(c, next) {
|
|
989
|
+
const auth = c.req.header("Authorization");
|
|
990
|
+
if (!auth?.startsWith("Bearer ")) return c.json({
|
|
991
|
+
error: "AuthMissing",
|
|
992
|
+
message: "Authorization header required"
|
|
993
|
+
}, 401);
|
|
994
|
+
const token = auth.slice(7);
|
|
995
|
+
if (token === c.env.AUTH_TOKEN) return next();
|
|
996
|
+
const serviceDid = `did:web:${c.env.PDS_HOSTNAME}`;
|
|
997
|
+
try {
|
|
998
|
+
const payload = await verifyAccessToken(token, c.env.JWT_SECRET, serviceDid);
|
|
999
|
+
if (payload.sub !== c.env.DID) return c.json({
|
|
1000
|
+
error: "AuthenticationRequired",
|
|
1001
|
+
message: "Invalid access token"
|
|
1002
|
+
}, 401);
|
|
1003
|
+
c.set("auth", {
|
|
1004
|
+
did: payload.sub,
|
|
1005
|
+
scope: payload.scope
|
|
1006
|
+
});
|
|
1007
|
+
return next();
|
|
1008
|
+
} catch {
|
|
1009
|
+
return c.json({
|
|
1010
|
+
error: "AuthenticationRequired",
|
|
1011
|
+
message: "Invalid authentication token"
|
|
1012
|
+
}, 401);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
//#endregion
|
|
1017
|
+
//#region src/service-auth.ts
|
|
1018
|
+
const MINUTE = 60 * 1e3;
|
|
1019
|
+
function jsonToB64Url(json) {
|
|
1020
|
+
return Buffer.from(JSON.stringify(json)).toString("base64url");
|
|
1021
|
+
}
|
|
1022
|
+
function noUndefinedVals(obj) {
|
|
1023
|
+
const result = {};
|
|
1024
|
+
for (const [key, val] of Object.entries(obj)) if (val !== void 0) result[key] = val;
|
|
1025
|
+
return result;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Create a service JWT for proxied requests to AppView.
|
|
1029
|
+
* The JWT asserts that the PDS vouches for the user identified by `iss`.
|
|
1030
|
+
*/
|
|
1031
|
+
async function createServiceJwt(params) {
|
|
1032
|
+
const { iss, aud, keypair } = params;
|
|
1033
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
1034
|
+
const exp = iat + MINUTE / 1e3;
|
|
1035
|
+
const lxm = params.lxm ?? void 0;
|
|
1036
|
+
const jti = randomStr(16, "hex");
|
|
1037
|
+
const header = {
|
|
1038
|
+
typ: "JWT",
|
|
1039
|
+
alg: keypair.jwtAlg
|
|
1040
|
+
};
|
|
1041
|
+
const payload = noUndefinedVals({
|
|
1042
|
+
iat,
|
|
1043
|
+
iss,
|
|
1044
|
+
aud,
|
|
1045
|
+
exp,
|
|
1046
|
+
lxm,
|
|
1047
|
+
jti
|
|
1048
|
+
});
|
|
1049
|
+
const toSignStr = `${jsonToB64Url(header)}.${jsonToB64Url(payload)}`;
|
|
1050
|
+
const toSign = Buffer.from(toSignStr, "utf8");
|
|
1051
|
+
return `${toSignStr}.${Buffer.from(await keypair.sign(toSign)).toString("base64url")}`;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
//#endregion
|
|
1055
|
+
//#region src/xrpc/sync.ts
|
|
1056
|
+
async function getRepo(c, accountDO) {
|
|
1057
|
+
const did = c.req.query("did");
|
|
1058
|
+
if (!did) return c.json({
|
|
1059
|
+
error: "InvalidRequest",
|
|
1060
|
+
message: "Missing required parameter: did"
|
|
1061
|
+
}, 400);
|
|
1062
|
+
try {
|
|
1063
|
+
ensureValidDid(did);
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
return c.json({
|
|
1066
|
+
error: "InvalidRequest",
|
|
1067
|
+
message: `Invalid DID format: ${err instanceof Error ? err.message : String(err)}`
|
|
1068
|
+
}, 400);
|
|
1069
|
+
}
|
|
1070
|
+
if (did !== c.env.DID) return c.json({
|
|
1071
|
+
error: "RepoNotFound",
|
|
1072
|
+
message: `Repository not found for DID: ${did}`
|
|
1073
|
+
}, 404);
|
|
1074
|
+
const carBytes = await accountDO.rpcGetRepoCar();
|
|
1075
|
+
return new Response(carBytes, {
|
|
1076
|
+
status: 200,
|
|
1077
|
+
headers: {
|
|
1078
|
+
"Content-Type": "application/vnd.ipld.car",
|
|
1079
|
+
"Content-Length": carBytes.length.toString()
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
async function getRepoStatus(c, accountDO) {
|
|
1084
|
+
const did = c.req.query("did");
|
|
1085
|
+
if (!did) return c.json({
|
|
1086
|
+
error: "InvalidRequest",
|
|
1087
|
+
message: "Missing required parameter: did"
|
|
1088
|
+
}, 400);
|
|
1089
|
+
try {
|
|
1090
|
+
ensureValidDid(did);
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
return c.json({
|
|
1093
|
+
error: "InvalidRequest",
|
|
1094
|
+
message: `Invalid DID format: ${err instanceof Error ? err.message : String(err)}`
|
|
1095
|
+
}, 400);
|
|
1096
|
+
}
|
|
1097
|
+
if (did !== c.env.DID) return c.json({
|
|
1098
|
+
error: "RepoNotFound",
|
|
1099
|
+
message: `Repository not found for DID: ${did}`
|
|
1100
|
+
}, 404);
|
|
1101
|
+
const data = await accountDO.rpcGetRepoStatus();
|
|
1102
|
+
return c.json({
|
|
1103
|
+
did: data.did,
|
|
1104
|
+
active: true,
|
|
1105
|
+
status: "active",
|
|
1106
|
+
rev: data.rev
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
async function listRepos(c, accountDO) {
|
|
1110
|
+
const data = await accountDO.rpcGetRepoStatus();
|
|
1111
|
+
return c.json({ repos: [{
|
|
1112
|
+
did: data.did,
|
|
1113
|
+
head: data.head,
|
|
1114
|
+
rev: data.rev,
|
|
1115
|
+
active: true
|
|
1116
|
+
}] });
|
|
1117
|
+
}
|
|
1118
|
+
async function listBlobs(c, _accountDO) {
|
|
1119
|
+
const did = c.req.query("did");
|
|
1120
|
+
if (!did) return c.json({
|
|
1121
|
+
error: "InvalidRequest",
|
|
1122
|
+
message: "Missing required parameter: did"
|
|
1123
|
+
}, 400);
|
|
1124
|
+
try {
|
|
1125
|
+
ensureValidDid(did);
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
return c.json({
|
|
1128
|
+
error: "InvalidRequest",
|
|
1129
|
+
message: `Invalid DID format: ${err instanceof Error ? err.message : String(err)}`
|
|
1130
|
+
}, 400);
|
|
1131
|
+
}
|
|
1132
|
+
if (did !== c.env.DID) return c.json({
|
|
1133
|
+
error: "RepoNotFound",
|
|
1134
|
+
message: `Repository not found for DID: ${did}`
|
|
1135
|
+
}, 404);
|
|
1136
|
+
if (!c.env.BLOBS) return c.json({ cids: [] });
|
|
1137
|
+
const prefix = `${did}/`;
|
|
1138
|
+
const cursor = c.req.query("cursor");
|
|
1139
|
+
const limit = Math.min(Number(c.req.query("limit")) || 500, 1e3);
|
|
1140
|
+
const listed = await c.env.BLOBS.list({
|
|
1141
|
+
prefix,
|
|
1142
|
+
limit,
|
|
1143
|
+
cursor: cursor || void 0
|
|
1144
|
+
});
|
|
1145
|
+
const result = { cids: listed.objects.map((obj) => obj.key.slice(prefix.length)) };
|
|
1146
|
+
if (listed.truncated && listed.cursor) result.cursor = listed.cursor;
|
|
1147
|
+
return c.json(result);
|
|
1148
|
+
}
|
|
1149
|
+
async function getBlob(c, _accountDO) {
|
|
1150
|
+
const did = c.req.query("did");
|
|
1151
|
+
const cid = c.req.query("cid");
|
|
1152
|
+
if (!did || !cid) return c.json({
|
|
1153
|
+
error: "InvalidRequest",
|
|
1154
|
+
message: "Missing required parameters: did, cid"
|
|
1155
|
+
}, 400);
|
|
1156
|
+
try {
|
|
1157
|
+
ensureValidDid(did);
|
|
1158
|
+
} catch (err) {
|
|
1159
|
+
return c.json({
|
|
1160
|
+
error: "InvalidRequest",
|
|
1161
|
+
message: `Invalid DID format: ${err instanceof Error ? err.message : String(err)}`
|
|
1162
|
+
}, 400);
|
|
1163
|
+
}
|
|
1164
|
+
if (did !== c.env.DID) return c.json({
|
|
1165
|
+
error: "RepoNotFound",
|
|
1166
|
+
message: `Repository not found for DID: ${did}`
|
|
1167
|
+
}, 404);
|
|
1168
|
+
if (!c.env.BLOBS) return c.json({
|
|
1169
|
+
error: "ServiceUnavailable",
|
|
1170
|
+
message: "Blob storage is not configured"
|
|
1171
|
+
}, 503);
|
|
1172
|
+
const key = `${did}/${cid}`;
|
|
1173
|
+
const blob = await c.env.BLOBS.get(key);
|
|
1174
|
+
if (!blob) return c.json({
|
|
1175
|
+
error: "BlobNotFound",
|
|
1176
|
+
message: `Blob not found: ${cid}`
|
|
1177
|
+
}, 404);
|
|
1178
|
+
return new Response(blob.body, {
|
|
1179
|
+
status: 200,
|
|
1180
|
+
headers: {
|
|
1181
|
+
"Content-Type": blob.httpMetadata?.contentType || "application/octet-stream",
|
|
1182
|
+
"Content-Length": blob.size.toString()
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
//#endregion
|
|
1188
|
+
//#region src/lexicons/app.bsky.actor.profile.json
|
|
1189
|
+
var app_bsky_actor_profile_exports = /* @__PURE__ */ __exportAll({
|
|
1190
|
+
default: () => app_bsky_actor_profile_default,
|
|
1191
|
+
defs: () => defs$15,
|
|
1192
|
+
id: () => id$15,
|
|
1193
|
+
lexicon: () => lexicon$15
|
|
1194
|
+
});
|
|
1195
|
+
var lexicon$15 = 1;
|
|
1196
|
+
var id$15 = "app.bsky.actor.profile";
|
|
1197
|
+
var defs$15 = { "main": {
|
|
1198
|
+
"type": "record",
|
|
1199
|
+
"description": "A declaration of a Bluesky account profile.",
|
|
1200
|
+
"key": "literal:self",
|
|
1201
|
+
"record": {
|
|
1202
|
+
"type": "object",
|
|
1203
|
+
"properties": {
|
|
1204
|
+
"displayName": {
|
|
1205
|
+
"type": "string",
|
|
1206
|
+
"maxGraphemes": 64,
|
|
1207
|
+
"maxLength": 640
|
|
1208
|
+
},
|
|
1209
|
+
"description": {
|
|
1210
|
+
"type": "string",
|
|
1211
|
+
"description": "Free-form profile description text.",
|
|
1212
|
+
"maxGraphemes": 256,
|
|
1213
|
+
"maxLength": 2560
|
|
1214
|
+
},
|
|
1215
|
+
"pronouns": {
|
|
1216
|
+
"type": "string",
|
|
1217
|
+
"description": "Free-form pronouns text.",
|
|
1218
|
+
"maxGraphemes": 20,
|
|
1219
|
+
"maxLength": 200
|
|
1220
|
+
},
|
|
1221
|
+
"website": {
|
|
1222
|
+
"type": "string",
|
|
1223
|
+
"format": "uri"
|
|
1224
|
+
},
|
|
1225
|
+
"avatar": {
|
|
1226
|
+
"type": "blob",
|
|
1227
|
+
"description": "Small image to be displayed next to posts from account. AKA, 'profile picture'",
|
|
1228
|
+
"accept": ["image/png", "image/jpeg"],
|
|
1229
|
+
"maxSize": 1e6
|
|
1230
|
+
},
|
|
1231
|
+
"banner": {
|
|
1232
|
+
"type": "blob",
|
|
1233
|
+
"description": "Larger horizontal image to display behind profile view.",
|
|
1234
|
+
"accept": ["image/png", "image/jpeg"],
|
|
1235
|
+
"maxSize": 1e6
|
|
1236
|
+
},
|
|
1237
|
+
"labels": {
|
|
1238
|
+
"type": "union",
|
|
1239
|
+
"description": "Self-label values, specific to the Bluesky application, on the overall account.",
|
|
1240
|
+
"refs": ["com.atproto.label.defs#selfLabels"]
|
|
1241
|
+
},
|
|
1242
|
+
"joinedViaStarterPack": {
|
|
1243
|
+
"type": "ref",
|
|
1244
|
+
"ref": "com.atproto.repo.strongRef"
|
|
1245
|
+
},
|
|
1246
|
+
"pinnedPost": {
|
|
1247
|
+
"type": "ref",
|
|
1248
|
+
"ref": "com.atproto.repo.strongRef"
|
|
1249
|
+
},
|
|
1250
|
+
"createdAt": {
|
|
1251
|
+
"type": "string",
|
|
1252
|
+
"format": "datetime"
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
} };
|
|
1257
|
+
var app_bsky_actor_profile_default = {
|
|
1258
|
+
lexicon: lexicon$15,
|
|
1259
|
+
id: id$15,
|
|
1260
|
+
defs: defs$15
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
//#endregion
|
|
1264
|
+
//#region src/lexicons/app.bsky.embed.external.json
|
|
1265
|
+
var app_bsky_embed_external_exports = /* @__PURE__ */ __exportAll({
|
|
1266
|
+
default: () => app_bsky_embed_external_default,
|
|
1267
|
+
defs: () => defs$14,
|
|
1268
|
+
id: () => id$14,
|
|
1269
|
+
lexicon: () => lexicon$14
|
|
1270
|
+
});
|
|
1271
|
+
var lexicon$14 = 1;
|
|
1272
|
+
var id$14 = "app.bsky.embed.external";
|
|
1273
|
+
var defs$14 = {
|
|
1274
|
+
"main": {
|
|
1275
|
+
"type": "object",
|
|
1276
|
+
"description": "A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).",
|
|
1277
|
+
"required": ["external"],
|
|
1278
|
+
"properties": { "external": {
|
|
1279
|
+
"type": "ref",
|
|
1280
|
+
"ref": "#external"
|
|
1281
|
+
} }
|
|
1282
|
+
},
|
|
1283
|
+
"external": {
|
|
1284
|
+
"type": "object",
|
|
1285
|
+
"required": [
|
|
1286
|
+
"uri",
|
|
1287
|
+
"title",
|
|
1288
|
+
"description"
|
|
1289
|
+
],
|
|
1290
|
+
"properties": {
|
|
1291
|
+
"uri": {
|
|
1292
|
+
"type": "string",
|
|
1293
|
+
"format": "uri"
|
|
1294
|
+
},
|
|
1295
|
+
"title": { "type": "string" },
|
|
1296
|
+
"description": { "type": "string" },
|
|
1297
|
+
"thumb": {
|
|
1298
|
+
"type": "blob",
|
|
1299
|
+
"accept": ["image/*"],
|
|
1300
|
+
"maxSize": 1e6
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
},
|
|
1304
|
+
"view": {
|
|
1305
|
+
"type": "object",
|
|
1306
|
+
"required": ["external"],
|
|
1307
|
+
"properties": { "external": {
|
|
1308
|
+
"type": "ref",
|
|
1309
|
+
"ref": "#viewExternal"
|
|
1310
|
+
} }
|
|
1311
|
+
},
|
|
1312
|
+
"viewExternal": {
|
|
1313
|
+
"type": "object",
|
|
1314
|
+
"required": [
|
|
1315
|
+
"uri",
|
|
1316
|
+
"title",
|
|
1317
|
+
"description"
|
|
1318
|
+
],
|
|
1319
|
+
"properties": {
|
|
1320
|
+
"uri": {
|
|
1321
|
+
"type": "string",
|
|
1322
|
+
"format": "uri"
|
|
1323
|
+
},
|
|
1324
|
+
"title": { "type": "string" },
|
|
1325
|
+
"description": { "type": "string" },
|
|
1326
|
+
"thumb": {
|
|
1327
|
+
"type": "string",
|
|
1328
|
+
"format": "uri"
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
var app_bsky_embed_external_default = {
|
|
1334
|
+
lexicon: lexicon$14,
|
|
1335
|
+
id: id$14,
|
|
1336
|
+
defs: defs$14
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
//#endregion
|
|
1340
|
+
//#region src/lexicons/app.bsky.embed.images.json
|
|
1341
|
+
var app_bsky_embed_images_exports = /* @__PURE__ */ __exportAll({
|
|
1342
|
+
default: () => app_bsky_embed_images_default,
|
|
1343
|
+
defs: () => defs$13,
|
|
1344
|
+
description: () => description$3,
|
|
1345
|
+
id: () => id$13,
|
|
1346
|
+
lexicon: () => lexicon$13
|
|
1347
|
+
});
|
|
1348
|
+
var lexicon$13 = 1;
|
|
1349
|
+
var id$13 = "app.bsky.embed.images";
|
|
1350
|
+
var description$3 = "A set of images embedded in a Bluesky record (eg, a post).";
|
|
1351
|
+
var defs$13 = {
|
|
1352
|
+
"main": {
|
|
1353
|
+
"type": "object",
|
|
1354
|
+
"required": ["images"],
|
|
1355
|
+
"properties": { "images": {
|
|
1356
|
+
"type": "array",
|
|
1357
|
+
"items": {
|
|
1358
|
+
"type": "ref",
|
|
1359
|
+
"ref": "#image"
|
|
1360
|
+
},
|
|
1361
|
+
"maxLength": 4
|
|
1362
|
+
} }
|
|
1363
|
+
},
|
|
1364
|
+
"image": {
|
|
1365
|
+
"type": "object",
|
|
1366
|
+
"required": ["image", "alt"],
|
|
1367
|
+
"properties": {
|
|
1368
|
+
"image": {
|
|
1369
|
+
"type": "blob",
|
|
1370
|
+
"accept": ["image/*"],
|
|
1371
|
+
"maxSize": 1e6
|
|
1372
|
+
},
|
|
1373
|
+
"alt": {
|
|
1374
|
+
"type": "string",
|
|
1375
|
+
"description": "Alt text description of the image, for accessibility."
|
|
1376
|
+
},
|
|
1377
|
+
"aspectRatio": {
|
|
1378
|
+
"type": "ref",
|
|
1379
|
+
"ref": "app.bsky.embed.defs#aspectRatio"
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
"view": {
|
|
1384
|
+
"type": "object",
|
|
1385
|
+
"required": ["images"],
|
|
1386
|
+
"properties": { "images": {
|
|
1387
|
+
"type": "array",
|
|
1388
|
+
"items": {
|
|
1389
|
+
"type": "ref",
|
|
1390
|
+
"ref": "#viewImage"
|
|
1391
|
+
},
|
|
1392
|
+
"maxLength": 4
|
|
1393
|
+
} }
|
|
1394
|
+
},
|
|
1395
|
+
"viewImage": {
|
|
1396
|
+
"type": "object",
|
|
1397
|
+
"required": [
|
|
1398
|
+
"thumb",
|
|
1399
|
+
"fullsize",
|
|
1400
|
+
"alt"
|
|
1401
|
+
],
|
|
1402
|
+
"properties": {
|
|
1403
|
+
"thumb": {
|
|
1404
|
+
"type": "string",
|
|
1405
|
+
"format": "uri",
|
|
1406
|
+
"description": "Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View."
|
|
1407
|
+
},
|
|
1408
|
+
"fullsize": {
|
|
1409
|
+
"type": "string",
|
|
1410
|
+
"format": "uri",
|
|
1411
|
+
"description": "Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View."
|
|
1412
|
+
},
|
|
1413
|
+
"alt": {
|
|
1414
|
+
"type": "string",
|
|
1415
|
+
"description": "Alt text description of the image, for accessibility."
|
|
1416
|
+
},
|
|
1417
|
+
"aspectRatio": {
|
|
1418
|
+
"type": "ref",
|
|
1419
|
+
"ref": "app.bsky.embed.defs#aspectRatio"
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
var app_bsky_embed_images_default = {
|
|
1425
|
+
lexicon: lexicon$13,
|
|
1426
|
+
id: id$13,
|
|
1427
|
+
description: description$3,
|
|
1428
|
+
defs: defs$13
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
//#endregion
|
|
1432
|
+
//#region src/lexicons/app.bsky.embed.record.json
|
|
1433
|
+
var app_bsky_embed_record_exports = /* @__PURE__ */ __exportAll({
|
|
1434
|
+
default: () => app_bsky_embed_record_default,
|
|
1435
|
+
defs: () => defs$12,
|
|
1436
|
+
description: () => description$2,
|
|
1437
|
+
id: () => id$12,
|
|
1438
|
+
lexicon: () => lexicon$12
|
|
1439
|
+
});
|
|
1440
|
+
var lexicon$12 = 1;
|
|
1441
|
+
var id$12 = "app.bsky.embed.record";
|
|
1442
|
+
var description$2 = "A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.";
|
|
1443
|
+
var defs$12 = {
|
|
1444
|
+
"main": {
|
|
1445
|
+
"type": "object",
|
|
1446
|
+
"required": ["record"],
|
|
1447
|
+
"properties": { "record": {
|
|
1448
|
+
"type": "ref",
|
|
1449
|
+
"ref": "com.atproto.repo.strongRef"
|
|
1450
|
+
} }
|
|
1451
|
+
},
|
|
1452
|
+
"view": {
|
|
1453
|
+
"type": "object",
|
|
1454
|
+
"required": ["record"],
|
|
1455
|
+
"properties": { "record": {
|
|
1456
|
+
"type": "union",
|
|
1457
|
+
"refs": [
|
|
1458
|
+
"#viewRecord",
|
|
1459
|
+
"#viewNotFound",
|
|
1460
|
+
"#viewBlocked",
|
|
1461
|
+
"#viewDetached",
|
|
1462
|
+
"app.bsky.feed.defs#generatorView",
|
|
1463
|
+
"app.bsky.graph.defs#listView",
|
|
1464
|
+
"app.bsky.labeler.defs#labelerView",
|
|
1465
|
+
"app.bsky.graph.defs#starterPackViewBasic"
|
|
1466
|
+
]
|
|
1467
|
+
} }
|
|
1468
|
+
},
|
|
1469
|
+
"viewRecord": {
|
|
1470
|
+
"type": "object",
|
|
1471
|
+
"required": [
|
|
1472
|
+
"uri",
|
|
1473
|
+
"cid",
|
|
1474
|
+
"author",
|
|
1475
|
+
"value",
|
|
1476
|
+
"indexedAt"
|
|
1477
|
+
],
|
|
1478
|
+
"properties": {
|
|
1479
|
+
"uri": {
|
|
1480
|
+
"type": "string",
|
|
1481
|
+
"format": "at-uri"
|
|
1482
|
+
},
|
|
1483
|
+
"cid": {
|
|
1484
|
+
"type": "string",
|
|
1485
|
+
"format": "cid"
|
|
1486
|
+
},
|
|
1487
|
+
"author": {
|
|
1488
|
+
"type": "ref",
|
|
1489
|
+
"ref": "app.bsky.actor.defs#profileViewBasic"
|
|
1490
|
+
},
|
|
1491
|
+
"value": {
|
|
1492
|
+
"type": "unknown",
|
|
1493
|
+
"description": "The record data itself."
|
|
1494
|
+
},
|
|
1495
|
+
"labels": {
|
|
1496
|
+
"type": "array",
|
|
1497
|
+
"items": {
|
|
1498
|
+
"type": "ref",
|
|
1499
|
+
"ref": "com.atproto.label.defs#label"
|
|
1500
|
+
}
|
|
1501
|
+
},
|
|
1502
|
+
"replyCount": { "type": "integer" },
|
|
1503
|
+
"repostCount": { "type": "integer" },
|
|
1504
|
+
"likeCount": { "type": "integer" },
|
|
1505
|
+
"quoteCount": { "type": "integer" },
|
|
1506
|
+
"embeds": {
|
|
1507
|
+
"type": "array",
|
|
1508
|
+
"items": {
|
|
1509
|
+
"type": "union",
|
|
1510
|
+
"refs": [
|
|
1511
|
+
"app.bsky.embed.images#view",
|
|
1512
|
+
"app.bsky.embed.video#view",
|
|
1513
|
+
"app.bsky.embed.external#view",
|
|
1514
|
+
"app.bsky.embed.record#view",
|
|
1515
|
+
"app.bsky.embed.recordWithMedia#view"
|
|
1516
|
+
]
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1519
|
+
"indexedAt": {
|
|
1520
|
+
"type": "string",
|
|
1521
|
+
"format": "datetime"
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
},
|
|
1525
|
+
"viewNotFound": {
|
|
1526
|
+
"type": "object",
|
|
1527
|
+
"required": ["uri", "notFound"],
|
|
1528
|
+
"properties": {
|
|
1529
|
+
"uri": {
|
|
1530
|
+
"type": "string",
|
|
1531
|
+
"format": "at-uri"
|
|
1532
|
+
},
|
|
1533
|
+
"notFound": {
|
|
1534
|
+
"type": "boolean",
|
|
1535
|
+
"const": true
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
},
|
|
1539
|
+
"viewBlocked": {
|
|
1540
|
+
"type": "object",
|
|
1541
|
+
"required": [
|
|
1542
|
+
"uri",
|
|
1543
|
+
"blocked",
|
|
1544
|
+
"author"
|
|
1545
|
+
],
|
|
1546
|
+
"properties": {
|
|
1547
|
+
"uri": {
|
|
1548
|
+
"type": "string",
|
|
1549
|
+
"format": "at-uri"
|
|
1550
|
+
},
|
|
1551
|
+
"blocked": {
|
|
1552
|
+
"type": "boolean",
|
|
1553
|
+
"const": true
|
|
1554
|
+
},
|
|
1555
|
+
"author": {
|
|
1556
|
+
"type": "ref",
|
|
1557
|
+
"ref": "app.bsky.feed.defs#blockedAuthor"
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
},
|
|
1561
|
+
"viewDetached": {
|
|
1562
|
+
"type": "object",
|
|
1563
|
+
"required": ["uri", "detached"],
|
|
1564
|
+
"properties": {
|
|
1565
|
+
"uri": {
|
|
1566
|
+
"type": "string",
|
|
1567
|
+
"format": "at-uri"
|
|
1568
|
+
},
|
|
1569
|
+
"detached": {
|
|
1570
|
+
"type": "boolean",
|
|
1571
|
+
"const": true
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
var app_bsky_embed_record_default = {
|
|
1577
|
+
lexicon: lexicon$12,
|
|
1578
|
+
id: id$12,
|
|
1579
|
+
description: description$2,
|
|
1580
|
+
defs: defs$12
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
//#endregion
|
|
1584
|
+
//#region src/lexicons/app.bsky.embed.recordWithMedia.json
|
|
1585
|
+
var app_bsky_embed_recordWithMedia_exports = /* @__PURE__ */ __exportAll({
|
|
1586
|
+
default: () => app_bsky_embed_recordWithMedia_default,
|
|
1587
|
+
defs: () => defs$11,
|
|
1588
|
+
description: () => description$1,
|
|
1589
|
+
id: () => id$11,
|
|
1590
|
+
lexicon: () => lexicon$11
|
|
1591
|
+
});
|
|
1592
|
+
var lexicon$11 = 1;
|
|
1593
|
+
var id$11 = "app.bsky.embed.recordWithMedia";
|
|
1594
|
+
var description$1 = "A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.";
|
|
1595
|
+
var defs$11 = {
|
|
1596
|
+
"main": {
|
|
1597
|
+
"type": "object",
|
|
1598
|
+
"required": ["record", "media"],
|
|
1599
|
+
"properties": {
|
|
1600
|
+
"record": {
|
|
1601
|
+
"type": "ref",
|
|
1602
|
+
"ref": "app.bsky.embed.record"
|
|
1603
|
+
},
|
|
1604
|
+
"media": {
|
|
1605
|
+
"type": "union",
|
|
1606
|
+
"refs": [
|
|
1607
|
+
"app.bsky.embed.images",
|
|
1608
|
+
"app.bsky.embed.video",
|
|
1609
|
+
"app.bsky.embed.external"
|
|
1610
|
+
]
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
},
|
|
1614
|
+
"view": {
|
|
1615
|
+
"type": "object",
|
|
1616
|
+
"required": ["record", "media"],
|
|
1617
|
+
"properties": {
|
|
1618
|
+
"record": {
|
|
1619
|
+
"type": "ref",
|
|
1620
|
+
"ref": "app.bsky.embed.record#view"
|
|
1621
|
+
},
|
|
1622
|
+
"media": {
|
|
1623
|
+
"type": "union",
|
|
1624
|
+
"refs": [
|
|
1625
|
+
"app.bsky.embed.images#view",
|
|
1626
|
+
"app.bsky.embed.video#view",
|
|
1627
|
+
"app.bsky.embed.external#view"
|
|
1628
|
+
]
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
var app_bsky_embed_recordWithMedia_default = {
|
|
1634
|
+
lexicon: lexicon$11,
|
|
1635
|
+
id: id$11,
|
|
1636
|
+
description: description$1,
|
|
1637
|
+
defs: defs$11
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
//#endregion
|
|
1641
|
+
//#region src/lexicons/app.bsky.feed.like.json
|
|
1642
|
+
var app_bsky_feed_like_exports = /* @__PURE__ */ __exportAll({
|
|
1643
|
+
default: () => app_bsky_feed_like_default,
|
|
1644
|
+
defs: () => defs$10,
|
|
1645
|
+
id: () => id$10,
|
|
1646
|
+
lexicon: () => lexicon$10
|
|
1647
|
+
});
|
|
1648
|
+
var lexicon$10 = 1;
|
|
1649
|
+
var id$10 = "app.bsky.feed.like";
|
|
1650
|
+
var defs$10 = { "main": {
|
|
1651
|
+
"type": "record",
|
|
1652
|
+
"description": "Record declaring a 'like' of a piece of subject content.",
|
|
1653
|
+
"key": "tid",
|
|
1654
|
+
"record": {
|
|
1655
|
+
"type": "object",
|
|
1656
|
+
"required": ["subject", "createdAt"],
|
|
1657
|
+
"properties": {
|
|
1658
|
+
"subject": {
|
|
1659
|
+
"type": "ref",
|
|
1660
|
+
"ref": "com.atproto.repo.strongRef"
|
|
1661
|
+
},
|
|
1662
|
+
"createdAt": {
|
|
1663
|
+
"type": "string",
|
|
1664
|
+
"format": "datetime"
|
|
1665
|
+
},
|
|
1666
|
+
"via": {
|
|
1667
|
+
"type": "ref",
|
|
1668
|
+
"ref": "com.atproto.repo.strongRef"
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
} };
|
|
1673
|
+
var app_bsky_feed_like_default = {
|
|
1674
|
+
lexicon: lexicon$10,
|
|
1675
|
+
id: id$10,
|
|
1676
|
+
defs: defs$10
|
|
1677
|
+
};
|
|
1678
|
+
|
|
1679
|
+
//#endregion
|
|
1680
|
+
//#region src/lexicons/app.bsky.feed.post.json
|
|
1681
|
+
var app_bsky_feed_post_exports = /* @__PURE__ */ __exportAll({
|
|
1682
|
+
default: () => app_bsky_feed_post_default,
|
|
1683
|
+
defs: () => defs$9,
|
|
1684
|
+
id: () => id$9,
|
|
1685
|
+
lexicon: () => lexicon$9
|
|
1686
|
+
});
|
|
1687
|
+
var lexicon$9 = 1;
|
|
1688
|
+
var id$9 = "app.bsky.feed.post";
|
|
1689
|
+
var defs$9 = {
|
|
1690
|
+
"main": {
|
|
1691
|
+
"type": "record",
|
|
1692
|
+
"description": "Record containing a Bluesky post.",
|
|
1693
|
+
"key": "tid",
|
|
1694
|
+
"record": {
|
|
1695
|
+
"type": "object",
|
|
1696
|
+
"required": ["text", "createdAt"],
|
|
1697
|
+
"properties": {
|
|
1698
|
+
"text": {
|
|
1699
|
+
"type": "string",
|
|
1700
|
+
"maxLength": 3e3,
|
|
1701
|
+
"maxGraphemes": 300,
|
|
1702
|
+
"description": "The primary post content. May be an empty string, if there are embeds."
|
|
1703
|
+
},
|
|
1704
|
+
"entities": {
|
|
1705
|
+
"type": "array",
|
|
1706
|
+
"description": "DEPRECATED: replaced by app.bsky.richtext.facet.",
|
|
1707
|
+
"items": {
|
|
1708
|
+
"type": "ref",
|
|
1709
|
+
"ref": "#entity"
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
"facets": {
|
|
1713
|
+
"type": "array",
|
|
1714
|
+
"description": "Annotations of text (mentions, URLs, hashtags, etc)",
|
|
1715
|
+
"items": {
|
|
1716
|
+
"type": "ref",
|
|
1717
|
+
"ref": "app.bsky.richtext.facet"
|
|
1718
|
+
}
|
|
1719
|
+
},
|
|
1720
|
+
"reply": {
|
|
1721
|
+
"type": "ref",
|
|
1722
|
+
"ref": "#replyRef"
|
|
1723
|
+
},
|
|
1724
|
+
"embed": {
|
|
1725
|
+
"type": "union",
|
|
1726
|
+
"refs": [
|
|
1727
|
+
"app.bsky.embed.images",
|
|
1728
|
+
"app.bsky.embed.video",
|
|
1729
|
+
"app.bsky.embed.external",
|
|
1730
|
+
"app.bsky.embed.record",
|
|
1731
|
+
"app.bsky.embed.recordWithMedia"
|
|
1732
|
+
]
|
|
1733
|
+
},
|
|
1734
|
+
"langs": {
|
|
1735
|
+
"type": "array",
|
|
1736
|
+
"description": "Indicates human language of post primary text content.",
|
|
1737
|
+
"maxLength": 3,
|
|
1738
|
+
"items": {
|
|
1739
|
+
"type": "string",
|
|
1740
|
+
"format": "language"
|
|
1741
|
+
}
|
|
1742
|
+
},
|
|
1743
|
+
"labels": {
|
|
1744
|
+
"type": "union",
|
|
1745
|
+
"description": "Self-label values for this post. Effectively content warnings.",
|
|
1746
|
+
"refs": ["com.atproto.label.defs#selfLabels"]
|
|
1747
|
+
},
|
|
1748
|
+
"tags": {
|
|
1749
|
+
"type": "array",
|
|
1750
|
+
"description": "Additional hashtags, in addition to any included in post text and facets.",
|
|
1751
|
+
"maxLength": 8,
|
|
1752
|
+
"items": {
|
|
1753
|
+
"type": "string",
|
|
1754
|
+
"maxLength": 640,
|
|
1755
|
+
"maxGraphemes": 64
|
|
1756
|
+
}
|
|
1757
|
+
},
|
|
1758
|
+
"createdAt": {
|
|
1759
|
+
"type": "string",
|
|
1760
|
+
"format": "datetime",
|
|
1761
|
+
"description": "Client-declared timestamp when this post was originally created."
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
},
|
|
1766
|
+
"replyRef": {
|
|
1767
|
+
"type": "object",
|
|
1768
|
+
"required": ["root", "parent"],
|
|
1769
|
+
"properties": {
|
|
1770
|
+
"root": {
|
|
1771
|
+
"type": "ref",
|
|
1772
|
+
"ref": "com.atproto.repo.strongRef"
|
|
1773
|
+
},
|
|
1774
|
+
"parent": {
|
|
1775
|
+
"type": "ref",
|
|
1776
|
+
"ref": "com.atproto.repo.strongRef"
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
},
|
|
1780
|
+
"entity": {
|
|
1781
|
+
"type": "object",
|
|
1782
|
+
"description": "Deprecated: use facets instead.",
|
|
1783
|
+
"required": [
|
|
1784
|
+
"index",
|
|
1785
|
+
"type",
|
|
1786
|
+
"value"
|
|
1787
|
+
],
|
|
1788
|
+
"properties": {
|
|
1789
|
+
"index": {
|
|
1790
|
+
"type": "ref",
|
|
1791
|
+
"ref": "#textSlice"
|
|
1792
|
+
},
|
|
1793
|
+
"type": {
|
|
1794
|
+
"type": "string",
|
|
1795
|
+
"description": "Expected values are 'mention' and 'link'."
|
|
1796
|
+
},
|
|
1797
|
+
"value": { "type": "string" }
|
|
1798
|
+
}
|
|
1799
|
+
},
|
|
1800
|
+
"textSlice": {
|
|
1801
|
+
"type": "object",
|
|
1802
|
+
"description": "Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings.",
|
|
1803
|
+
"required": ["start", "end"],
|
|
1804
|
+
"properties": {
|
|
1805
|
+
"start": {
|
|
1806
|
+
"type": "integer",
|
|
1807
|
+
"minimum": 0
|
|
1808
|
+
},
|
|
1809
|
+
"end": {
|
|
1810
|
+
"type": "integer",
|
|
1811
|
+
"minimum": 0
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
var app_bsky_feed_post_default = {
|
|
1817
|
+
lexicon: lexicon$9,
|
|
1818
|
+
id: id$9,
|
|
1819
|
+
defs: defs$9
|
|
1820
|
+
};
|
|
1821
|
+
|
|
1822
|
+
//#endregion
|
|
1823
|
+
//#region src/lexicons/app.bsky.feed.repost.json
|
|
1824
|
+
var app_bsky_feed_repost_exports = /* @__PURE__ */ __exportAll({
|
|
1825
|
+
default: () => app_bsky_feed_repost_default,
|
|
1826
|
+
defs: () => defs$8,
|
|
1827
|
+
id: () => id$8,
|
|
1828
|
+
lexicon: () => lexicon$8
|
|
1829
|
+
});
|
|
1830
|
+
var lexicon$8 = 1;
|
|
1831
|
+
var id$8 = "app.bsky.feed.repost";
|
|
1832
|
+
var defs$8 = { "main": {
|
|
1833
|
+
"description": "Record representing a 'repost' of an existing Bluesky post.",
|
|
1834
|
+
"type": "record",
|
|
1835
|
+
"key": "tid",
|
|
1836
|
+
"record": {
|
|
1837
|
+
"type": "object",
|
|
1838
|
+
"required": ["subject", "createdAt"],
|
|
1839
|
+
"properties": {
|
|
1840
|
+
"subject": {
|
|
1841
|
+
"type": "ref",
|
|
1842
|
+
"ref": "com.atproto.repo.strongRef"
|
|
1843
|
+
},
|
|
1844
|
+
"createdAt": {
|
|
1845
|
+
"type": "string",
|
|
1846
|
+
"format": "datetime"
|
|
1847
|
+
},
|
|
1848
|
+
"via": {
|
|
1849
|
+
"type": "ref",
|
|
1850
|
+
"ref": "com.atproto.repo.strongRef"
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
} };
|
|
1855
|
+
var app_bsky_feed_repost_default = {
|
|
1856
|
+
lexicon: lexicon$8,
|
|
1857
|
+
id: id$8,
|
|
1858
|
+
defs: defs$8
|
|
1859
|
+
};
|
|
1860
|
+
|
|
1861
|
+
//#endregion
|
|
1862
|
+
//#region src/lexicons/app.bsky.feed.threadgate.json
|
|
1863
|
+
var app_bsky_feed_threadgate_exports = /* @__PURE__ */ __exportAll({
|
|
1864
|
+
default: () => app_bsky_feed_threadgate_default,
|
|
1865
|
+
defs: () => defs$7,
|
|
1866
|
+
id: () => id$7,
|
|
1867
|
+
lexicon: () => lexicon$7
|
|
1868
|
+
});
|
|
1869
|
+
var lexicon$7 = 1;
|
|
1870
|
+
var id$7 = "app.bsky.feed.threadgate";
|
|
1871
|
+
var defs$7 = {
|
|
1872
|
+
"main": {
|
|
1873
|
+
"type": "record",
|
|
1874
|
+
"key": "tid",
|
|
1875
|
+
"description": "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository.",
|
|
1876
|
+
"record": {
|
|
1877
|
+
"type": "object",
|
|
1878
|
+
"required": ["post", "createdAt"],
|
|
1879
|
+
"properties": {
|
|
1880
|
+
"post": {
|
|
1881
|
+
"type": "string",
|
|
1882
|
+
"format": "at-uri",
|
|
1883
|
+
"description": "Reference (AT-URI) to the post record."
|
|
1884
|
+
},
|
|
1885
|
+
"allow": {
|
|
1886
|
+
"description": "List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply.",
|
|
1887
|
+
"type": "array",
|
|
1888
|
+
"maxLength": 5,
|
|
1889
|
+
"items": {
|
|
1890
|
+
"type": "union",
|
|
1891
|
+
"refs": [
|
|
1892
|
+
"#mentionRule",
|
|
1893
|
+
"#followerRule",
|
|
1894
|
+
"#followingRule",
|
|
1895
|
+
"#listRule"
|
|
1896
|
+
]
|
|
1897
|
+
}
|
|
1898
|
+
},
|
|
1899
|
+
"createdAt": {
|
|
1900
|
+
"type": "string",
|
|
1901
|
+
"format": "datetime"
|
|
1902
|
+
},
|
|
1903
|
+
"hiddenReplies": {
|
|
1904
|
+
"type": "array",
|
|
1905
|
+
"maxLength": 300,
|
|
1906
|
+
"items": {
|
|
1907
|
+
"type": "string",
|
|
1908
|
+
"format": "at-uri"
|
|
1909
|
+
},
|
|
1910
|
+
"description": "List of hidden reply URIs."
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
},
|
|
1915
|
+
"mentionRule": {
|
|
1916
|
+
"type": "object",
|
|
1917
|
+
"description": "Allow replies from actors mentioned in your post.",
|
|
1918
|
+
"properties": {}
|
|
1919
|
+
},
|
|
1920
|
+
"followerRule": {
|
|
1921
|
+
"type": "object",
|
|
1922
|
+
"description": "Allow replies from actors who follow you.",
|
|
1923
|
+
"properties": {}
|
|
1924
|
+
},
|
|
1925
|
+
"followingRule": {
|
|
1926
|
+
"type": "object",
|
|
1927
|
+
"description": "Allow replies from actors you follow.",
|
|
1928
|
+
"properties": {}
|
|
1929
|
+
},
|
|
1930
|
+
"listRule": {
|
|
1931
|
+
"type": "object",
|
|
1932
|
+
"description": "Allow replies from actors on a list.",
|
|
1933
|
+
"required": ["list"],
|
|
1934
|
+
"properties": { "list": {
|
|
1935
|
+
"type": "string",
|
|
1936
|
+
"format": "at-uri"
|
|
1937
|
+
} }
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
var app_bsky_feed_threadgate_default = {
|
|
1941
|
+
lexicon: lexicon$7,
|
|
1942
|
+
id: id$7,
|
|
1943
|
+
defs: defs$7
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1946
|
+
//#endregion
|
|
1947
|
+
//#region src/lexicons/app.bsky.graph.block.json
|
|
1948
|
+
var app_bsky_graph_block_exports = /* @__PURE__ */ __exportAll({
|
|
1949
|
+
default: () => app_bsky_graph_block_default,
|
|
1950
|
+
defs: () => defs$6,
|
|
1951
|
+
id: () => id$6,
|
|
1952
|
+
lexicon: () => lexicon$6
|
|
1953
|
+
});
|
|
1954
|
+
var lexicon$6 = 1;
|
|
1955
|
+
var id$6 = "app.bsky.graph.block";
|
|
1956
|
+
var defs$6 = { "main": {
|
|
1957
|
+
"type": "record",
|
|
1958
|
+
"description": "Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details.",
|
|
1959
|
+
"key": "tid",
|
|
1960
|
+
"record": {
|
|
1961
|
+
"type": "object",
|
|
1962
|
+
"required": ["subject", "createdAt"],
|
|
1963
|
+
"properties": {
|
|
1964
|
+
"subject": {
|
|
1965
|
+
"type": "string",
|
|
1966
|
+
"format": "did",
|
|
1967
|
+
"description": "DID of the account to be blocked."
|
|
1968
|
+
},
|
|
1969
|
+
"createdAt": {
|
|
1970
|
+
"type": "string",
|
|
1971
|
+
"format": "datetime"
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
} };
|
|
1976
|
+
var app_bsky_graph_block_default = {
|
|
1977
|
+
lexicon: lexicon$6,
|
|
1978
|
+
id: id$6,
|
|
1979
|
+
defs: defs$6
|
|
1980
|
+
};
|
|
1981
|
+
|
|
1982
|
+
//#endregion
|
|
1983
|
+
//#region src/lexicons/app.bsky.graph.follow.json
|
|
1984
|
+
var app_bsky_graph_follow_exports = /* @__PURE__ */ __exportAll({
|
|
1985
|
+
default: () => app_bsky_graph_follow_default,
|
|
1986
|
+
defs: () => defs$5,
|
|
1987
|
+
id: () => id$5,
|
|
1988
|
+
lexicon: () => lexicon$5
|
|
1989
|
+
});
|
|
1990
|
+
var lexicon$5 = 1;
|
|
1991
|
+
var id$5 = "app.bsky.graph.follow";
|
|
1992
|
+
var defs$5 = { "main": {
|
|
1993
|
+
"type": "record",
|
|
1994
|
+
"description": "Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView.",
|
|
1995
|
+
"key": "tid",
|
|
1996
|
+
"record": {
|
|
1997
|
+
"type": "object",
|
|
1998
|
+
"required": ["subject", "createdAt"],
|
|
1999
|
+
"properties": {
|
|
2000
|
+
"subject": {
|
|
2001
|
+
"type": "string",
|
|
2002
|
+
"format": "did"
|
|
2003
|
+
},
|
|
2004
|
+
"createdAt": {
|
|
2005
|
+
"type": "string",
|
|
2006
|
+
"format": "datetime"
|
|
2007
|
+
},
|
|
2008
|
+
"via": {
|
|
2009
|
+
"type": "ref",
|
|
2010
|
+
"ref": "com.atproto.repo.strongRef"
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
} };
|
|
2015
|
+
var app_bsky_graph_follow_default = {
|
|
2016
|
+
lexicon: lexicon$5,
|
|
2017
|
+
id: id$5,
|
|
2018
|
+
defs: defs$5
|
|
2019
|
+
};
|
|
2020
|
+
|
|
2021
|
+
//#endregion
|
|
2022
|
+
//#region src/lexicons/app.bsky.graph.list.json
|
|
2023
|
+
var app_bsky_graph_list_exports = /* @__PURE__ */ __exportAll({
|
|
2024
|
+
default: () => app_bsky_graph_list_default,
|
|
2025
|
+
defs: () => defs$4,
|
|
2026
|
+
id: () => id$4,
|
|
2027
|
+
lexicon: () => lexicon$4
|
|
2028
|
+
});
|
|
2029
|
+
var lexicon$4 = 1;
|
|
2030
|
+
var id$4 = "app.bsky.graph.list";
|
|
2031
|
+
var defs$4 = { "main": {
|
|
2032
|
+
"type": "record",
|
|
2033
|
+
"description": "Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists.",
|
|
2034
|
+
"key": "tid",
|
|
2035
|
+
"record": {
|
|
2036
|
+
"type": "object",
|
|
2037
|
+
"required": [
|
|
2038
|
+
"name",
|
|
2039
|
+
"purpose",
|
|
2040
|
+
"createdAt"
|
|
2041
|
+
],
|
|
2042
|
+
"properties": {
|
|
2043
|
+
"purpose": {
|
|
2044
|
+
"type": "ref",
|
|
2045
|
+
"description": "Defines the purpose of the list (aka, moderation-oriented or curration-oriented)",
|
|
2046
|
+
"ref": "app.bsky.graph.defs#listPurpose"
|
|
2047
|
+
},
|
|
2048
|
+
"name": {
|
|
2049
|
+
"type": "string",
|
|
2050
|
+
"maxLength": 64,
|
|
2051
|
+
"minLength": 1,
|
|
2052
|
+
"description": "Display name for list; can not be empty."
|
|
2053
|
+
},
|
|
2054
|
+
"description": {
|
|
2055
|
+
"type": "string",
|
|
2056
|
+
"maxGraphemes": 300,
|
|
2057
|
+
"maxLength": 3e3
|
|
2058
|
+
},
|
|
2059
|
+
"descriptionFacets": {
|
|
2060
|
+
"type": "array",
|
|
2061
|
+
"items": {
|
|
2062
|
+
"type": "ref",
|
|
2063
|
+
"ref": "app.bsky.richtext.facet"
|
|
2064
|
+
}
|
|
2065
|
+
},
|
|
2066
|
+
"avatar": {
|
|
2067
|
+
"type": "blob",
|
|
2068
|
+
"accept": ["image/png", "image/jpeg"],
|
|
2069
|
+
"maxSize": 1e6
|
|
2070
|
+
},
|
|
2071
|
+
"labels": {
|
|
2072
|
+
"type": "union",
|
|
2073
|
+
"refs": ["com.atproto.label.defs#selfLabels"]
|
|
2074
|
+
},
|
|
2075
|
+
"createdAt": {
|
|
2076
|
+
"type": "string",
|
|
2077
|
+
"format": "datetime"
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
} };
|
|
2082
|
+
var app_bsky_graph_list_default = {
|
|
2083
|
+
lexicon: lexicon$4,
|
|
2084
|
+
id: id$4,
|
|
2085
|
+
defs: defs$4
|
|
2086
|
+
};
|
|
2087
|
+
|
|
2088
|
+
//#endregion
|
|
2089
|
+
//#region src/lexicons/app.bsky.graph.listitem.json
|
|
2090
|
+
var app_bsky_graph_listitem_exports = /* @__PURE__ */ __exportAll({
|
|
2091
|
+
default: () => app_bsky_graph_listitem_default,
|
|
2092
|
+
defs: () => defs$3,
|
|
2093
|
+
id: () => id$3,
|
|
2094
|
+
lexicon: () => lexicon$3
|
|
2095
|
+
});
|
|
2096
|
+
var lexicon$3 = 1;
|
|
2097
|
+
var id$3 = "app.bsky.graph.listitem";
|
|
2098
|
+
var defs$3 = { "main": {
|
|
2099
|
+
"type": "record",
|
|
2100
|
+
"description": "Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records.",
|
|
2101
|
+
"key": "tid",
|
|
2102
|
+
"record": {
|
|
2103
|
+
"type": "object",
|
|
2104
|
+
"required": [
|
|
2105
|
+
"subject",
|
|
2106
|
+
"list",
|
|
2107
|
+
"createdAt"
|
|
2108
|
+
],
|
|
2109
|
+
"properties": {
|
|
2110
|
+
"subject": {
|
|
2111
|
+
"type": "string",
|
|
2112
|
+
"format": "did",
|
|
2113
|
+
"description": "The account which is included on the list."
|
|
2114
|
+
},
|
|
2115
|
+
"list": {
|
|
2116
|
+
"type": "string",
|
|
2117
|
+
"format": "at-uri",
|
|
2118
|
+
"description": "Reference (AT-URI) to the list record (app.bsky.graph.list)."
|
|
2119
|
+
},
|
|
2120
|
+
"createdAt": {
|
|
2121
|
+
"type": "string",
|
|
2122
|
+
"format": "datetime"
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
} };
|
|
2127
|
+
var app_bsky_graph_listitem_default = {
|
|
2128
|
+
lexicon: lexicon$3,
|
|
2129
|
+
id: id$3,
|
|
2130
|
+
defs: defs$3
|
|
2131
|
+
};
|
|
2132
|
+
|
|
2133
|
+
//#endregion
|
|
2134
|
+
//#region src/lexicons/app.bsky.richtext.facet.json
|
|
2135
|
+
var app_bsky_richtext_facet_exports = /* @__PURE__ */ __exportAll({
|
|
2136
|
+
default: () => app_bsky_richtext_facet_default,
|
|
2137
|
+
defs: () => defs$2,
|
|
2138
|
+
id: () => id$2,
|
|
2139
|
+
lexicon: () => lexicon$2
|
|
2140
|
+
});
|
|
2141
|
+
var lexicon$2 = 1;
|
|
2142
|
+
var id$2 = "app.bsky.richtext.facet";
|
|
2143
|
+
var defs$2 = {
|
|
2144
|
+
"main": {
|
|
2145
|
+
"type": "object",
|
|
2146
|
+
"description": "Annotation of a sub-string within rich text.",
|
|
2147
|
+
"required": ["index", "features"],
|
|
2148
|
+
"properties": {
|
|
2149
|
+
"index": {
|
|
2150
|
+
"type": "ref",
|
|
2151
|
+
"ref": "#byteSlice"
|
|
2152
|
+
},
|
|
2153
|
+
"features": {
|
|
2154
|
+
"type": "array",
|
|
2155
|
+
"items": {
|
|
2156
|
+
"type": "union",
|
|
2157
|
+
"refs": [
|
|
2158
|
+
"#mention",
|
|
2159
|
+
"#link",
|
|
2160
|
+
"#tag"
|
|
2161
|
+
]
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
},
|
|
2166
|
+
"mention": {
|
|
2167
|
+
"type": "object",
|
|
2168
|
+
"description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.",
|
|
2169
|
+
"required": ["did"],
|
|
2170
|
+
"properties": { "did": {
|
|
2171
|
+
"type": "string",
|
|
2172
|
+
"format": "did"
|
|
2173
|
+
} }
|
|
2174
|
+
},
|
|
2175
|
+
"link": {
|
|
2176
|
+
"type": "object",
|
|
2177
|
+
"description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.",
|
|
2178
|
+
"required": ["uri"],
|
|
2179
|
+
"properties": { "uri": {
|
|
2180
|
+
"type": "string",
|
|
2181
|
+
"format": "uri"
|
|
2182
|
+
} }
|
|
2183
|
+
},
|
|
2184
|
+
"tag": {
|
|
2185
|
+
"type": "object",
|
|
2186
|
+
"description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').",
|
|
2187
|
+
"required": ["tag"],
|
|
2188
|
+
"properties": { "tag": {
|
|
2189
|
+
"type": "string",
|
|
2190
|
+
"maxLength": 640,
|
|
2191
|
+
"maxGraphemes": 64
|
|
2192
|
+
} }
|
|
2193
|
+
},
|
|
2194
|
+
"byteSlice": {
|
|
2195
|
+
"type": "object",
|
|
2196
|
+
"description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.",
|
|
2197
|
+
"required": ["byteStart", "byteEnd"],
|
|
2198
|
+
"properties": {
|
|
2199
|
+
"byteStart": {
|
|
2200
|
+
"type": "integer",
|
|
2201
|
+
"minimum": 0
|
|
2202
|
+
},
|
|
2203
|
+
"byteEnd": {
|
|
2204
|
+
"type": "integer",
|
|
2205
|
+
"minimum": 0
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2210
|
+
var app_bsky_richtext_facet_default = {
|
|
2211
|
+
lexicon: lexicon$2,
|
|
2212
|
+
id: id$2,
|
|
2213
|
+
defs: defs$2
|
|
2214
|
+
};
|
|
2215
|
+
|
|
2216
|
+
//#endregion
|
|
2217
|
+
//#region src/lexicons/com.atproto.label.defs.json
|
|
2218
|
+
var com_atproto_label_defs_exports = /* @__PURE__ */ __exportAll({
|
|
2219
|
+
default: () => com_atproto_label_defs_default,
|
|
2220
|
+
defs: () => defs$1,
|
|
2221
|
+
id: () => id$1,
|
|
2222
|
+
lexicon: () => lexicon$1
|
|
2223
|
+
});
|
|
2224
|
+
var lexicon$1 = 1;
|
|
2225
|
+
var id$1 = "com.atproto.label.defs";
|
|
2226
|
+
var defs$1 = {
|
|
2227
|
+
"label": {
|
|
2228
|
+
"type": "object",
|
|
2229
|
+
"description": "Metadata tag on an atproto resource (eg, repo or record).",
|
|
2230
|
+
"required": [
|
|
2231
|
+
"src",
|
|
2232
|
+
"uri",
|
|
2233
|
+
"val",
|
|
2234
|
+
"cts"
|
|
2235
|
+
],
|
|
2236
|
+
"properties": {
|
|
2237
|
+
"ver": {
|
|
2238
|
+
"type": "integer",
|
|
2239
|
+
"description": "The AT Protocol version of the label object."
|
|
2240
|
+
},
|
|
2241
|
+
"src": {
|
|
2242
|
+
"type": "string",
|
|
2243
|
+
"format": "did",
|
|
2244
|
+
"description": "DID of the actor who created this label."
|
|
2245
|
+
},
|
|
2246
|
+
"uri": {
|
|
2247
|
+
"type": "string",
|
|
2248
|
+
"format": "uri",
|
|
2249
|
+
"description": "AT URI of the record, repository (account), or other resource that this label applies to."
|
|
2250
|
+
},
|
|
2251
|
+
"cid": {
|
|
2252
|
+
"type": "string",
|
|
2253
|
+
"format": "cid",
|
|
2254
|
+
"description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."
|
|
2255
|
+
},
|
|
2256
|
+
"val": {
|
|
2257
|
+
"type": "string",
|
|
2258
|
+
"maxLength": 128,
|
|
2259
|
+
"description": "The short string name of the value or type of this label."
|
|
2260
|
+
},
|
|
2261
|
+
"neg": {
|
|
2262
|
+
"type": "boolean",
|
|
2263
|
+
"description": "If true, this is a negation label, overwriting a previous label."
|
|
2264
|
+
},
|
|
2265
|
+
"cts": {
|
|
2266
|
+
"type": "string",
|
|
2267
|
+
"format": "datetime",
|
|
2268
|
+
"description": "Timestamp when this label was created."
|
|
2269
|
+
},
|
|
2270
|
+
"exp": {
|
|
2271
|
+
"type": "string",
|
|
2272
|
+
"format": "datetime",
|
|
2273
|
+
"description": "Timestamp at which this label expires (no longer applies)."
|
|
2274
|
+
},
|
|
2275
|
+
"sig": {
|
|
2276
|
+
"type": "bytes",
|
|
2277
|
+
"description": "Signature of dag-cbor encoded label."
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
},
|
|
2281
|
+
"selfLabels": {
|
|
2282
|
+
"type": "object",
|
|
2283
|
+
"description": "Metadata tags on an atproto record, published by the author within the record.",
|
|
2284
|
+
"required": ["values"],
|
|
2285
|
+
"properties": { "values": {
|
|
2286
|
+
"type": "array",
|
|
2287
|
+
"items": {
|
|
2288
|
+
"type": "ref",
|
|
2289
|
+
"ref": "#selfLabel"
|
|
2290
|
+
},
|
|
2291
|
+
"maxLength": 10
|
|
2292
|
+
} }
|
|
2293
|
+
},
|
|
2294
|
+
"selfLabel": {
|
|
2295
|
+
"type": "object",
|
|
2296
|
+
"description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.",
|
|
2297
|
+
"required": ["val"],
|
|
2298
|
+
"properties": { "val": {
|
|
2299
|
+
"type": "string",
|
|
2300
|
+
"maxLength": 128,
|
|
2301
|
+
"description": "The short string name of the value or type of this label."
|
|
2302
|
+
} }
|
|
2303
|
+
},
|
|
2304
|
+
"labelValueDefinition": {
|
|
2305
|
+
"type": "object",
|
|
2306
|
+
"description": "Declares a label value and its expected interpretations and behaviors.",
|
|
2307
|
+
"required": [
|
|
2308
|
+
"identifier",
|
|
2309
|
+
"severity",
|
|
2310
|
+
"blurs",
|
|
2311
|
+
"locales"
|
|
2312
|
+
],
|
|
2313
|
+
"properties": {
|
|
2314
|
+
"identifier": {
|
|
2315
|
+
"type": "string",
|
|
2316
|
+
"description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
|
|
2317
|
+
"maxLength": 100,
|
|
2318
|
+
"maxGraphemes": 100
|
|
2319
|
+
},
|
|
2320
|
+
"severity": {
|
|
2321
|
+
"type": "string",
|
|
2322
|
+
"description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
|
|
2323
|
+
"knownValues": [
|
|
2324
|
+
"inform",
|
|
2325
|
+
"alert",
|
|
2326
|
+
"none"
|
|
2327
|
+
]
|
|
2328
|
+
},
|
|
2329
|
+
"blurs": {
|
|
2330
|
+
"type": "string",
|
|
2331
|
+
"description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
|
|
2332
|
+
"knownValues": [
|
|
2333
|
+
"content",
|
|
2334
|
+
"media",
|
|
2335
|
+
"none"
|
|
2336
|
+
]
|
|
2337
|
+
},
|
|
2338
|
+
"defaultSetting": {
|
|
2339
|
+
"type": "string",
|
|
2340
|
+
"description": "The default setting for this label.",
|
|
2341
|
+
"knownValues": [
|
|
2342
|
+
"ignore",
|
|
2343
|
+
"warn",
|
|
2344
|
+
"hide"
|
|
2345
|
+
],
|
|
2346
|
+
"default": "warn"
|
|
2347
|
+
},
|
|
2348
|
+
"adultOnly": {
|
|
2349
|
+
"type": "boolean",
|
|
2350
|
+
"description": "Does the user need to have adult content enabled in order to configure this label?"
|
|
2351
|
+
},
|
|
2352
|
+
"locales": {
|
|
2353
|
+
"type": "array",
|
|
2354
|
+
"items": {
|
|
2355
|
+
"type": "ref",
|
|
2356
|
+
"ref": "#labelValueDefinitionStrings"
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
},
|
|
2361
|
+
"labelValueDefinitionStrings": {
|
|
2362
|
+
"type": "object",
|
|
2363
|
+
"description": "Strings which describe the label in the UI, localized into a specific language.",
|
|
2364
|
+
"required": [
|
|
2365
|
+
"lang",
|
|
2366
|
+
"name",
|
|
2367
|
+
"description"
|
|
2368
|
+
],
|
|
2369
|
+
"properties": {
|
|
2370
|
+
"lang": {
|
|
2371
|
+
"type": "string",
|
|
2372
|
+
"description": "The code of the language these strings are written in.",
|
|
2373
|
+
"format": "language"
|
|
2374
|
+
},
|
|
2375
|
+
"name": {
|
|
2376
|
+
"type": "string",
|
|
2377
|
+
"description": "A short human-readable name for the label.",
|
|
2378
|
+
"maxGraphemes": 64,
|
|
2379
|
+
"maxLength": 640
|
|
2380
|
+
},
|
|
2381
|
+
"description": {
|
|
2382
|
+
"type": "string",
|
|
2383
|
+
"description": "A longer description of what the label means and why it might be applied.",
|
|
2384
|
+
"maxGraphemes": 1e4,
|
|
2385
|
+
"maxLength": 1e5
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
},
|
|
2389
|
+
"labelValue": {
|
|
2390
|
+
"type": "string",
|
|
2391
|
+
"knownValues": [
|
|
2392
|
+
"!hide",
|
|
2393
|
+
"!no-promote",
|
|
2394
|
+
"!warn",
|
|
2395
|
+
"!no-unauthenticated",
|
|
2396
|
+
"dmca-violation",
|
|
2397
|
+
"doxxing",
|
|
2398
|
+
"porn",
|
|
2399
|
+
"sexual",
|
|
2400
|
+
"nudity",
|
|
2401
|
+
"nsfl",
|
|
2402
|
+
"gore"
|
|
2403
|
+
]
|
|
2404
|
+
}
|
|
2405
|
+
};
|
|
2406
|
+
var com_atproto_label_defs_default = {
|
|
2407
|
+
lexicon: lexicon$1,
|
|
2408
|
+
id: id$1,
|
|
2409
|
+
defs: defs$1
|
|
2410
|
+
};
|
|
2411
|
+
|
|
2412
|
+
//#endregion
|
|
2413
|
+
//#region src/lexicons/com.atproto.repo.strongRef.json
|
|
2414
|
+
var com_atproto_repo_strongRef_exports = /* @__PURE__ */ __exportAll({
|
|
2415
|
+
default: () => com_atproto_repo_strongRef_default,
|
|
2416
|
+
defs: () => defs,
|
|
2417
|
+
description: () => description,
|
|
2418
|
+
id: () => id,
|
|
2419
|
+
lexicon: () => lexicon
|
|
2420
|
+
});
|
|
2421
|
+
var lexicon = 1;
|
|
2422
|
+
var id = "com.atproto.repo.strongRef";
|
|
2423
|
+
var description = "A URI with a content-hash fingerprint.";
|
|
2424
|
+
var defs = { "main": {
|
|
2425
|
+
"type": "object",
|
|
2426
|
+
"required": ["uri", "cid"],
|
|
2427
|
+
"properties": {
|
|
2428
|
+
"uri": {
|
|
2429
|
+
"type": "string",
|
|
2430
|
+
"format": "at-uri"
|
|
2431
|
+
},
|
|
2432
|
+
"cid": {
|
|
2433
|
+
"type": "string",
|
|
2434
|
+
"format": "cid"
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
} };
|
|
2438
|
+
var com_atproto_repo_strongRef_default = {
|
|
2439
|
+
lexicon,
|
|
2440
|
+
id,
|
|
2441
|
+
description,
|
|
2442
|
+
defs
|
|
2443
|
+
};
|
|
2444
|
+
|
|
2445
|
+
//#endregion
|
|
2446
|
+
//#region src/validation.ts
|
|
2447
|
+
/**
|
|
2448
|
+
* Record validator for AT Protocol records.
|
|
2449
|
+
*
|
|
2450
|
+
* Validates records against official Bluesky lexicon schemas.
|
|
2451
|
+
* Uses optimistic validation strategy:
|
|
2452
|
+
* - If a lexicon schema is loaded for the collection, validate the record
|
|
2453
|
+
* - If no schema is loaded, allow the record (fail-open)
|
|
2454
|
+
*
|
|
2455
|
+
* This allows the PDS to accept records for new or unknown collection types
|
|
2456
|
+
* while still validating known types when schemas are available.
|
|
2457
|
+
*/
|
|
2458
|
+
var RecordValidator = class {
|
|
2459
|
+
lex;
|
|
2460
|
+
strictMode;
|
|
2461
|
+
constructor(options = {}) {
|
|
2462
|
+
this.lex = options.lexicons ?? new Lexicons();
|
|
2463
|
+
this.strictMode = options.strict ?? false;
|
|
2464
|
+
this.loadBlueskySchemas();
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* Load official Bluesky lexicon schemas from vendored JSON files.
|
|
2468
|
+
* Uses Vite's glob import to automatically load all schema files.
|
|
2469
|
+
*/
|
|
2470
|
+
loadBlueskySchemas() {
|
|
2471
|
+
const schemas = {
|
|
2472
|
+
"./lexicons/app.bsky.actor.profile.json": app_bsky_actor_profile_exports,
|
|
2473
|
+
"./lexicons/app.bsky.embed.external.json": app_bsky_embed_external_exports,
|
|
2474
|
+
"./lexicons/app.bsky.embed.images.json": app_bsky_embed_images_exports,
|
|
2475
|
+
"./lexicons/app.bsky.embed.record.json": app_bsky_embed_record_exports,
|
|
2476
|
+
"./lexicons/app.bsky.embed.recordWithMedia.json": app_bsky_embed_recordWithMedia_exports,
|
|
2477
|
+
"./lexicons/app.bsky.feed.like.json": app_bsky_feed_like_exports,
|
|
2478
|
+
"./lexicons/app.bsky.feed.post.json": app_bsky_feed_post_exports,
|
|
2479
|
+
"./lexicons/app.bsky.feed.repost.json": app_bsky_feed_repost_exports,
|
|
2480
|
+
"./lexicons/app.bsky.feed.threadgate.json": app_bsky_feed_threadgate_exports,
|
|
2481
|
+
"./lexicons/app.bsky.graph.block.json": app_bsky_graph_block_exports,
|
|
2482
|
+
"./lexicons/app.bsky.graph.follow.json": app_bsky_graph_follow_exports,
|
|
2483
|
+
"./lexicons/app.bsky.graph.list.json": app_bsky_graph_list_exports,
|
|
2484
|
+
"./lexicons/app.bsky.graph.listitem.json": app_bsky_graph_listitem_exports,
|
|
2485
|
+
"./lexicons/app.bsky.richtext.facet.json": app_bsky_richtext_facet_exports,
|
|
2486
|
+
"./lexicons/com.atproto.label.defs.json": com_atproto_label_defs_exports,
|
|
2487
|
+
"./lexicons/com.atproto.repo.strongRef.json": com_atproto_repo_strongRef_exports
|
|
2488
|
+
};
|
|
2489
|
+
for (const schema of Object.values(schemas)) this.lex.add(schema.default);
|
|
2490
|
+
}
|
|
2491
|
+
/**
|
|
2492
|
+
* Validate a record against its lexicon schema.
|
|
2493
|
+
*
|
|
2494
|
+
* @param collection - The NSID of the record type (e.g., "app.bsky.feed.post")
|
|
2495
|
+
* @param record - The record object to validate
|
|
2496
|
+
* @throws {Error} If validation fails and schema is loaded
|
|
2497
|
+
*/
|
|
2498
|
+
validateRecord(collection, record) {
|
|
2499
|
+
if (!this.hasSchema(collection)) {
|
|
2500
|
+
if (this.strictMode) throw new Error(`No lexicon schema loaded for collection: ${collection}. Enable optimistic validation or add the schema.`);
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
try {
|
|
2504
|
+
this.lex.assertValidRecord(collection, record);
|
|
2505
|
+
} catch (error) {
|
|
2506
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2507
|
+
throw new Error(`Lexicon validation failed for ${collection}: ${message}`);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Check if a schema is loaded for a collection.
|
|
2512
|
+
*/
|
|
2513
|
+
hasSchema(collection) {
|
|
2514
|
+
try {
|
|
2515
|
+
this.lex.getDefOrThrow(collection);
|
|
2516
|
+
return true;
|
|
2517
|
+
} catch {
|
|
2518
|
+
return false;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Add a lexicon schema to the validator.
|
|
2523
|
+
*
|
|
2524
|
+
* @param doc - The lexicon document to add
|
|
2525
|
+
*
|
|
2526
|
+
* @example
|
|
2527
|
+
* ```ts
|
|
2528
|
+
* validator.addSchema({
|
|
2529
|
+
* lexicon: 1,
|
|
2530
|
+
* id: "com.example.post",
|
|
2531
|
+
* defs: { ... }
|
|
2532
|
+
* })
|
|
2533
|
+
* ```
|
|
2534
|
+
*/
|
|
2535
|
+
addSchema(doc) {
|
|
2536
|
+
this.lex.add(doc);
|
|
2537
|
+
}
|
|
2538
|
+
/**
|
|
2539
|
+
* Get list of all loaded schema NSIDs.
|
|
2540
|
+
*/
|
|
2541
|
+
getLoadedSchemas() {
|
|
2542
|
+
return Array.from(this.lex).map((doc) => doc.id);
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* Get the underlying Lexicons instance for advanced usage.
|
|
2546
|
+
*/
|
|
2547
|
+
getLexicons() {
|
|
2548
|
+
return this.lex;
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
/**
|
|
2552
|
+
* Shared validator instance (singleton pattern).
|
|
2553
|
+
* Uses optimistic validation by default (strict: false).
|
|
2554
|
+
*
|
|
2555
|
+
* Automatically loads all schemas from ./lexicons/*.json
|
|
2556
|
+
*
|
|
2557
|
+
* Additional schemas can be added:
|
|
2558
|
+
* ```ts
|
|
2559
|
+
* import { validator } from './validation'
|
|
2560
|
+
* validator.addSchema(myCustomSchema)
|
|
2561
|
+
* ```
|
|
2562
|
+
*/
|
|
2563
|
+
const validator = new RecordValidator({ strict: false });
|
|
2564
|
+
|
|
2565
|
+
//#endregion
|
|
2566
|
+
//#region src/xrpc/repo.ts
|
|
2567
|
+
function invalidRecordError(c, err, prefix) {
|
|
2568
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2569
|
+
return c.json({
|
|
2570
|
+
error: "InvalidRecord",
|
|
2571
|
+
message: prefix ? `${prefix}: ${message}` : message
|
|
2572
|
+
}, 400);
|
|
2573
|
+
}
|
|
2574
|
+
async function describeRepo(c, accountDO) {
|
|
2575
|
+
const repo = c.req.query("repo");
|
|
2576
|
+
if (!repo) return c.json({
|
|
2577
|
+
error: "InvalidRequest",
|
|
2578
|
+
message: "Missing required parameter: repo"
|
|
2579
|
+
}, 400);
|
|
2580
|
+
try {
|
|
2581
|
+
ensureValidDid(repo);
|
|
2582
|
+
} catch (err) {
|
|
2583
|
+
return c.json({
|
|
2584
|
+
error: "InvalidRequest",
|
|
2585
|
+
message: `Invalid DID format: ${err instanceof Error ? err.message : String(err)}`
|
|
2586
|
+
}, 400);
|
|
2587
|
+
}
|
|
2588
|
+
if (repo !== c.env.DID) return c.json({
|
|
2589
|
+
error: "RepoNotFound",
|
|
2590
|
+
message: `Repository not found: ${repo}`
|
|
2591
|
+
}, 404);
|
|
2592
|
+
const data = await accountDO.rpcDescribeRepo();
|
|
2593
|
+
return c.json({
|
|
2594
|
+
did: c.env.DID,
|
|
2595
|
+
handle: c.env.HANDLE,
|
|
2596
|
+
didDoc: {
|
|
2597
|
+
"@context": ["https://www.w3.org/ns/did/v1"],
|
|
2598
|
+
id: c.env.DID,
|
|
2599
|
+
alsoKnownAs: [`at://${c.env.HANDLE}`],
|
|
2600
|
+
verificationMethod: [{
|
|
2601
|
+
id: `${c.env.DID}#atproto`,
|
|
2602
|
+
type: "Multikey",
|
|
2603
|
+
controller: c.env.DID,
|
|
2604
|
+
publicKeyMultibase: c.env.SIGNING_KEY_PUBLIC
|
|
2605
|
+
}]
|
|
2606
|
+
},
|
|
2607
|
+
collections: data.collections,
|
|
2608
|
+
handleIsCorrect: true
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
async function getRecord(c, accountDO) {
|
|
2612
|
+
const repo = c.req.query("repo");
|
|
2613
|
+
const collection = c.req.query("collection");
|
|
2614
|
+
const rkey = c.req.query("rkey");
|
|
2615
|
+
if (!repo || !collection || !rkey) return c.json({
|
|
2616
|
+
error: "InvalidRequest",
|
|
2617
|
+
message: "Missing required parameters: repo, collection, rkey"
|
|
2618
|
+
}, 400);
|
|
2619
|
+
try {
|
|
2620
|
+
ensureValidDid(repo);
|
|
2621
|
+
} catch (err) {
|
|
2622
|
+
return c.json({
|
|
2623
|
+
error: "InvalidRequest",
|
|
2624
|
+
message: `Invalid DID format: ${err instanceof Error ? err.message : String(err)}`
|
|
2625
|
+
}, 400);
|
|
2626
|
+
}
|
|
2627
|
+
if (repo !== c.env.DID) return c.json({
|
|
2628
|
+
error: "RepoNotFound",
|
|
2629
|
+
message: `Repository not found: ${repo}`
|
|
2630
|
+
}, 404);
|
|
2631
|
+
const result = await accountDO.rpcGetRecord(collection, rkey);
|
|
2632
|
+
if (!result) return c.json({
|
|
2633
|
+
error: "RecordNotFound",
|
|
2634
|
+
message: `Record not found: ${collection}/${rkey}`
|
|
2635
|
+
}, 404);
|
|
2636
|
+
return c.json({
|
|
2637
|
+
uri: AtUri.make(repo, collection, rkey).toString(),
|
|
2638
|
+
cid: result.cid,
|
|
2639
|
+
value: result.record
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
async function listRecords(c, accountDO) {
|
|
2643
|
+
const repo = c.req.query("repo");
|
|
2644
|
+
const collection = c.req.query("collection");
|
|
2645
|
+
const limitStr = c.req.query("limit");
|
|
2646
|
+
const cursor = c.req.query("cursor");
|
|
2647
|
+
const reverseStr = c.req.query("reverse");
|
|
2648
|
+
if (!repo || !collection) return c.json({
|
|
2649
|
+
error: "InvalidRequest",
|
|
2650
|
+
message: "Missing required parameters: repo, collection"
|
|
2651
|
+
}, 400);
|
|
2652
|
+
try {
|
|
2653
|
+
ensureValidDid(repo);
|
|
2654
|
+
} catch (err) {
|
|
2655
|
+
return c.json({
|
|
2656
|
+
error: "InvalidRequest",
|
|
2657
|
+
message: `Invalid DID format: ${err instanceof Error ? err.message : String(err)}`
|
|
2658
|
+
}, 400);
|
|
2659
|
+
}
|
|
2660
|
+
if (repo !== c.env.DID) return c.json({
|
|
2661
|
+
error: "RepoNotFound",
|
|
2662
|
+
message: `Repository not found: ${repo}`
|
|
2663
|
+
}, 404);
|
|
2664
|
+
const limit = Math.min(limitStr ? Number.parseInt(limitStr, 10) : 50, 100);
|
|
2665
|
+
const reverse = reverseStr === "true";
|
|
2666
|
+
const result = await accountDO.rpcListRecords(collection, {
|
|
2667
|
+
limit,
|
|
2668
|
+
cursor,
|
|
2669
|
+
reverse
|
|
2670
|
+
});
|
|
2671
|
+
return c.json(result);
|
|
2672
|
+
}
|
|
2673
|
+
async function createRecord(c, accountDO) {
|
|
2674
|
+
const { repo, collection, rkey, record } = await c.req.json();
|
|
2675
|
+
if (!repo || !collection || !record) return c.json({
|
|
2676
|
+
error: "InvalidRequest",
|
|
2677
|
+
message: "Missing required parameters: repo, collection, record"
|
|
2678
|
+
}, 400);
|
|
2679
|
+
if (repo !== c.env.DID) return c.json({
|
|
2680
|
+
error: "InvalidRepo",
|
|
2681
|
+
message: `Invalid repository: ${repo}`
|
|
2682
|
+
}, 400);
|
|
2683
|
+
try {
|
|
2684
|
+
validator.validateRecord(collection, record);
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
return invalidRecordError(c, err);
|
|
2687
|
+
}
|
|
2688
|
+
const result = await accountDO.rpcCreateRecord(collection, rkey, record);
|
|
2689
|
+
return c.json(result);
|
|
2690
|
+
}
|
|
2691
|
+
async function deleteRecord(c, accountDO) {
|
|
2692
|
+
const { repo, collection, rkey } = await c.req.json();
|
|
2693
|
+
if (!repo || !collection || !rkey) return c.json({
|
|
2694
|
+
error: "InvalidRequest",
|
|
2695
|
+
message: "Missing required parameters: repo, collection, rkey"
|
|
2696
|
+
}, 400);
|
|
2697
|
+
if (repo !== c.env.DID) return c.json({
|
|
2698
|
+
error: "InvalidRepo",
|
|
2699
|
+
message: `Invalid repository: ${repo}`
|
|
2700
|
+
}, 400);
|
|
2701
|
+
const result = await accountDO.rpcDeleteRecord(collection, rkey);
|
|
2702
|
+
if (!result) return c.json({
|
|
2703
|
+
error: "RecordNotFound",
|
|
2704
|
+
message: `Record not found: ${collection}/${rkey}`
|
|
2705
|
+
}, 404);
|
|
2706
|
+
return c.json(result);
|
|
2707
|
+
}
|
|
2708
|
+
async function putRecord(c, accountDO) {
|
|
2709
|
+
const { repo, collection, rkey, record } = await c.req.json();
|
|
2710
|
+
if (!repo || !collection || !rkey || !record) return c.json({
|
|
2711
|
+
error: "InvalidRequest",
|
|
2712
|
+
message: "Missing required parameters: repo, collection, rkey, record"
|
|
2713
|
+
}, 400);
|
|
2714
|
+
if (repo !== c.env.DID) return c.json({
|
|
2715
|
+
error: "InvalidRepo",
|
|
2716
|
+
message: `Invalid repository: ${repo}`
|
|
2717
|
+
}, 400);
|
|
2718
|
+
try {
|
|
2719
|
+
validator.validateRecord(collection, record);
|
|
2720
|
+
} catch (err) {
|
|
2721
|
+
return invalidRecordError(c, err);
|
|
2722
|
+
}
|
|
2723
|
+
try {
|
|
2724
|
+
const result = await accountDO.rpcPutRecord(collection, rkey, record);
|
|
2725
|
+
return c.json(result);
|
|
2726
|
+
} catch (err) {
|
|
2727
|
+
return c.json({
|
|
2728
|
+
error: "InvalidRequest",
|
|
2729
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2730
|
+
}, 400);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
async function applyWrites(c, accountDO) {
|
|
2734
|
+
const { repo, writes } = await c.req.json();
|
|
2735
|
+
if (!repo || !writes || !Array.isArray(writes)) return c.json({
|
|
2736
|
+
error: "InvalidRequest",
|
|
2737
|
+
message: "Missing required parameters: repo, writes"
|
|
2738
|
+
}, 400);
|
|
2739
|
+
if (repo !== c.env.DID) return c.json({
|
|
2740
|
+
error: "InvalidRepo",
|
|
2741
|
+
message: `Invalid repository: ${repo}`
|
|
2742
|
+
}, 400);
|
|
2743
|
+
if (writes.length > 200) return c.json({
|
|
2744
|
+
error: "InvalidRequest",
|
|
2745
|
+
message: "Too many writes. Max: 200"
|
|
2746
|
+
}, 400);
|
|
2747
|
+
for (let i = 0; i < writes.length; i++) {
|
|
2748
|
+
const write = writes[i];
|
|
2749
|
+
if (write.$type === "com.atproto.repo.applyWrites#create" || write.$type === "com.atproto.repo.applyWrites#update") try {
|
|
2750
|
+
validator.validateRecord(write.collection, write.value);
|
|
2751
|
+
} catch (err) {
|
|
2752
|
+
return invalidRecordError(c, err, `Write ${i}`);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
try {
|
|
2756
|
+
const result = await accountDO.rpcApplyWrites(writes);
|
|
2757
|
+
return c.json(result);
|
|
2758
|
+
} catch (err) {
|
|
2759
|
+
return c.json({
|
|
2760
|
+
error: "InvalidRequest",
|
|
2761
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2762
|
+
}, 400);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
async function uploadBlob(c, accountDO) {
|
|
2766
|
+
const contentType = c.req.header("Content-Type") || "application/octet-stream";
|
|
2767
|
+
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
2768
|
+
const MAX_BLOB_SIZE = 5 * 1024 * 1024;
|
|
2769
|
+
if (bytes.length > MAX_BLOB_SIZE) return c.json({
|
|
2770
|
+
error: "BlobTooLarge",
|
|
2771
|
+
message: `Blob size ${bytes.length} exceeds maximum of ${MAX_BLOB_SIZE} bytes`
|
|
2772
|
+
}, 400);
|
|
2773
|
+
try {
|
|
2774
|
+
const blobRef = await accountDO.rpcUploadBlob(bytes, contentType);
|
|
2775
|
+
return c.json({ blob: blobRef });
|
|
2776
|
+
} catch (err) {
|
|
2777
|
+
if (err instanceof Error && err.message.includes("Blob storage not configured")) return c.json({
|
|
2778
|
+
error: "ServiceUnavailable",
|
|
2779
|
+
message: "Blob storage is not configured"
|
|
2780
|
+
}, 503);
|
|
2781
|
+
throw err;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
async function importRepo(c, accountDO) {
|
|
2785
|
+
if (c.req.header("Content-Type") !== "application/vnd.ipld.car") return c.json({
|
|
2786
|
+
error: "InvalidRequest",
|
|
2787
|
+
message: "Content-Type must be application/vnd.ipld.car for repository import"
|
|
2788
|
+
}, 400);
|
|
2789
|
+
const carBytes = new Uint8Array(await c.req.arrayBuffer());
|
|
2790
|
+
if (carBytes.length === 0) return c.json({
|
|
2791
|
+
error: "InvalidRequest",
|
|
2792
|
+
message: "Empty CAR file"
|
|
2793
|
+
}, 400);
|
|
2794
|
+
const MAX_CAR_SIZE = 100 * 1024 * 1024;
|
|
2795
|
+
if (carBytes.length > MAX_CAR_SIZE) return c.json({
|
|
2796
|
+
error: "RepoTooLarge",
|
|
2797
|
+
message: `Repository size ${carBytes.length} exceeds maximum of ${MAX_CAR_SIZE} bytes`
|
|
2798
|
+
}, 400);
|
|
2799
|
+
try {
|
|
2800
|
+
const result = await accountDO.rpcImportRepo(carBytes);
|
|
2801
|
+
return c.json(result);
|
|
2802
|
+
} catch (err) {
|
|
2803
|
+
if (err instanceof Error) {
|
|
2804
|
+
if (err.message.includes("already exists")) return c.json({
|
|
2805
|
+
error: "RepoAlreadyExists",
|
|
2806
|
+
message: "Repository already exists. Cannot import over existing data."
|
|
2807
|
+
}, 409);
|
|
2808
|
+
if (err.message.includes("DID mismatch")) return c.json({
|
|
2809
|
+
error: "InvalidRepo",
|
|
2810
|
+
message: err.message
|
|
2811
|
+
}, 400);
|
|
2812
|
+
if (err.message.includes("no roots") || err.message.includes("no blocks") || err.message.includes("Invalid root")) return c.json({
|
|
2813
|
+
error: "InvalidRepo",
|
|
2814
|
+
message: `Invalid CAR file: ${err.message}`
|
|
2815
|
+
}, 400);
|
|
2816
|
+
}
|
|
2817
|
+
throw err;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
//#endregion
|
|
2822
|
+
//#region src/xrpc/server.ts
|
|
2823
|
+
async function describeServer(c) {
|
|
2824
|
+
return c.json({
|
|
2825
|
+
did: c.env.DID,
|
|
2826
|
+
availableUserDomains: [],
|
|
2827
|
+
inviteCodeRequired: false
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* Create a new session (login)
|
|
2832
|
+
*/
|
|
2833
|
+
async function createSession(c) {
|
|
2834
|
+
const { identifier, password } = await c.req.json();
|
|
2835
|
+
if (!identifier || !password) return c.json({
|
|
2836
|
+
error: "InvalidRequest",
|
|
2837
|
+
message: "Missing identifier or password"
|
|
2838
|
+
}, 400);
|
|
2839
|
+
if (identifier !== c.env.HANDLE && identifier !== c.env.DID) return c.json({
|
|
2840
|
+
error: "AuthenticationRequired",
|
|
2841
|
+
message: "Invalid identifier or password"
|
|
2842
|
+
}, 401);
|
|
2843
|
+
if (!await verifyPassword(password, c.env.PASSWORD_HASH)) return c.json({
|
|
2844
|
+
error: "AuthenticationRequired",
|
|
2845
|
+
message: "Invalid identifier or password"
|
|
2846
|
+
}, 401);
|
|
2847
|
+
const serviceDid = `did:web:${c.env.PDS_HOSTNAME}`;
|
|
2848
|
+
const accessJwt = await createAccessToken(c.env.JWT_SECRET, c.env.DID, serviceDid);
|
|
2849
|
+
const refreshJwt = await createRefreshToken(c.env.JWT_SECRET, c.env.DID, serviceDid);
|
|
2850
|
+
return c.json({
|
|
2851
|
+
accessJwt,
|
|
2852
|
+
refreshJwt,
|
|
2853
|
+
handle: c.env.HANDLE,
|
|
2854
|
+
did: c.env.DID,
|
|
2855
|
+
active: true
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Refresh a session
|
|
2860
|
+
*/
|
|
2861
|
+
async function refreshSession(c) {
|
|
2862
|
+
const authHeader = c.req.header("Authorization");
|
|
2863
|
+
if (!authHeader?.startsWith("Bearer ")) return c.json({
|
|
2864
|
+
error: "AuthenticationRequired",
|
|
2865
|
+
message: "Refresh token required"
|
|
2866
|
+
}, 401);
|
|
2867
|
+
const token = authHeader.slice(7);
|
|
2868
|
+
const serviceDid = `did:web:${c.env.PDS_HOSTNAME}`;
|
|
2869
|
+
try {
|
|
2870
|
+
if ((await verifyRefreshToken(token, c.env.JWT_SECRET, serviceDid)).sub !== c.env.DID) return c.json({
|
|
2871
|
+
error: "AuthenticationRequired",
|
|
2872
|
+
message: "Invalid refresh token"
|
|
2873
|
+
}, 401);
|
|
2874
|
+
const accessJwt = await createAccessToken(c.env.JWT_SECRET, c.env.DID, serviceDid);
|
|
2875
|
+
const refreshJwt = await createRefreshToken(c.env.JWT_SECRET, c.env.DID, serviceDid);
|
|
2876
|
+
return c.json({
|
|
2877
|
+
accessJwt,
|
|
2878
|
+
refreshJwt,
|
|
2879
|
+
handle: c.env.HANDLE,
|
|
2880
|
+
did: c.env.DID,
|
|
2881
|
+
active: true
|
|
2882
|
+
});
|
|
2883
|
+
} catch (err) {
|
|
2884
|
+
return c.json({
|
|
2885
|
+
error: "ExpiredToken",
|
|
2886
|
+
message: err instanceof Error ? err.message : "Invalid refresh token"
|
|
2887
|
+
}, 400);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
/**
|
|
2891
|
+
* Get current session info
|
|
2892
|
+
*/
|
|
2893
|
+
async function getSession(c) {
|
|
2894
|
+
const authHeader = c.req.header("Authorization");
|
|
2895
|
+
if (!authHeader?.startsWith("Bearer ")) return c.json({
|
|
2896
|
+
error: "AuthenticationRequired",
|
|
2897
|
+
message: "Access token required"
|
|
2898
|
+
}, 401);
|
|
2899
|
+
const token = authHeader.slice(7);
|
|
2900
|
+
const serviceDid = `did:web:${c.env.PDS_HOSTNAME}`;
|
|
2901
|
+
if (token === c.env.AUTH_TOKEN) return c.json({
|
|
2902
|
+
handle: c.env.HANDLE,
|
|
2903
|
+
did: c.env.DID,
|
|
2904
|
+
active: true
|
|
2905
|
+
});
|
|
2906
|
+
try {
|
|
2907
|
+
if ((await verifyAccessToken(token, c.env.JWT_SECRET, serviceDid)).sub !== c.env.DID) return c.json({
|
|
2908
|
+
error: "AuthenticationRequired",
|
|
2909
|
+
message: "Invalid access token"
|
|
2910
|
+
}, 401);
|
|
2911
|
+
return c.json({
|
|
2912
|
+
handle: c.env.HANDLE,
|
|
2913
|
+
did: c.env.DID,
|
|
2914
|
+
active: true
|
|
2915
|
+
});
|
|
2916
|
+
} catch (err) {
|
|
2917
|
+
return c.json({
|
|
2918
|
+
error: "InvalidToken",
|
|
2919
|
+
message: err instanceof Error ? err.message : "Invalid access token"
|
|
2920
|
+
}, 401);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
/**
|
|
2924
|
+
* Delete current session (logout)
|
|
2925
|
+
*/
|
|
2926
|
+
async function deleteSession(c) {
|
|
2927
|
+
return c.json({});
|
|
2928
|
+
}
|
|
2929
|
+
/**
|
|
2930
|
+
* Get account status - used for migration checks
|
|
2931
|
+
*/
|
|
2932
|
+
async function getAccountStatus(c, accountDO) {
|
|
2933
|
+
try {
|
|
2934
|
+
const status = await accountDO.rpcGetRepoStatus();
|
|
2935
|
+
return c.json({
|
|
2936
|
+
activated: true,
|
|
2937
|
+
validDid: true,
|
|
2938
|
+
repoRev: status.rev,
|
|
2939
|
+
repoBlocks: null,
|
|
2940
|
+
indexedRecords: null,
|
|
2941
|
+
privateStateValues: null,
|
|
2942
|
+
expectedBlobs: null,
|
|
2943
|
+
importedBlobs: null
|
|
2944
|
+
});
|
|
2945
|
+
} catch (err) {
|
|
2946
|
+
return c.json({
|
|
2947
|
+
activated: false,
|
|
2948
|
+
validDid: true,
|
|
2949
|
+
repoRev: null,
|
|
2950
|
+
repoBlocks: null,
|
|
2951
|
+
indexedRecords: null,
|
|
2952
|
+
privateStateValues: null,
|
|
2953
|
+
expectedBlobs: null,
|
|
2954
|
+
importedBlobs: null
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
//#endregion
|
|
2960
|
+
//#region package.json
|
|
2961
|
+
var version = "0.0.0";
|
|
2962
|
+
|
|
2963
|
+
//#endregion
|
|
2964
|
+
//#region src/index.ts
|
|
2965
|
+
const env$1 = env;
|
|
2966
|
+
for (const key of [
|
|
2967
|
+
"DID",
|
|
2968
|
+
"HANDLE",
|
|
2969
|
+
"PDS_HOSTNAME",
|
|
2970
|
+
"AUTH_TOKEN",
|
|
2971
|
+
"SIGNING_KEY",
|
|
2972
|
+
"SIGNING_KEY_PUBLIC",
|
|
2973
|
+
"JWT_SECRET",
|
|
2974
|
+
"PASSWORD_HASH"
|
|
2975
|
+
]) if (!env$1[key]) throw new Error(`Missing required environment variable: ${key}`);
|
|
2976
|
+
try {
|
|
2977
|
+
ensureValidDid(env$1.DID);
|
|
2978
|
+
ensureValidHandle(env$1.HANDLE);
|
|
2979
|
+
} catch (err) {
|
|
2980
|
+
throw new Error(`Invalid DID or handle: ${err instanceof Error ? err.message : String(err)}`);
|
|
2981
|
+
}
|
|
2982
|
+
const APPVIEW_DID = "did:web:api.bsky.app";
|
|
2983
|
+
const CHAT_DID = "did:web:api.bsky.chat";
|
|
2984
|
+
let keypairPromise = null;
|
|
2985
|
+
function getKeypair() {
|
|
2986
|
+
if (!keypairPromise) keypairPromise = Secp256k1Keypair.import(env$1.SIGNING_KEY);
|
|
2987
|
+
return keypairPromise;
|
|
2988
|
+
}
|
|
2989
|
+
const app = new Hono();
|
|
2990
|
+
app.use("*", cors({
|
|
2991
|
+
origin: "*",
|
|
2992
|
+
allowMethods: [
|
|
2993
|
+
"GET",
|
|
2994
|
+
"POST",
|
|
2995
|
+
"PUT",
|
|
2996
|
+
"DELETE",
|
|
2997
|
+
"OPTIONS"
|
|
2998
|
+
],
|
|
2999
|
+
allowHeaders: ["*"],
|
|
3000
|
+
exposeHeaders: ["Content-Type"],
|
|
3001
|
+
maxAge: 86400
|
|
3002
|
+
}));
|
|
3003
|
+
function getAccountDO(env$2) {
|
|
3004
|
+
const id$16 = env$2.ACCOUNT.idFromName("account");
|
|
3005
|
+
return env$2.ACCOUNT.get(id$16);
|
|
3006
|
+
}
|
|
3007
|
+
app.get("/.well-known/did.json", (c) => {
|
|
3008
|
+
const didDocument = {
|
|
3009
|
+
"@context": [
|
|
3010
|
+
"https://www.w3.org/ns/did/v1",
|
|
3011
|
+
"https://w3id.org/security/multikey/v1",
|
|
3012
|
+
"https://w3id.org/security/suites/secp256k1-2019/v1"
|
|
3013
|
+
],
|
|
3014
|
+
id: c.env.DID,
|
|
3015
|
+
alsoKnownAs: [`at://${c.env.HANDLE}`],
|
|
3016
|
+
verificationMethod: [{
|
|
3017
|
+
id: `${c.env.DID}#atproto`,
|
|
3018
|
+
type: "Multikey",
|
|
3019
|
+
controller: c.env.DID,
|
|
3020
|
+
publicKeyMultibase: c.env.SIGNING_KEY_PUBLIC
|
|
3021
|
+
}],
|
|
3022
|
+
service: [{
|
|
3023
|
+
id: "#atproto_pds",
|
|
3024
|
+
type: "AtprotoPersonalDataServer",
|
|
3025
|
+
serviceEndpoint: `https://${c.env.PDS_HOSTNAME}`
|
|
3026
|
+
}]
|
|
3027
|
+
};
|
|
3028
|
+
return c.json(didDocument);
|
|
3029
|
+
});
|
|
3030
|
+
app.get("/.well-known/atproto-did", (c) => {
|
|
3031
|
+
if (c.env.HANDLE !== c.env.PDS_HOSTNAME) return c.notFound();
|
|
3032
|
+
return new Response(c.env.DID, { headers: { "Content-Type": "text/plain" } });
|
|
3033
|
+
});
|
|
3034
|
+
app.get("/health", (c) => c.json({
|
|
3035
|
+
status: "ok",
|
|
3036
|
+
version
|
|
3037
|
+
}));
|
|
3038
|
+
app.get("/xrpc/com.atproto.sync.getRepo", (c) => getRepo(c, getAccountDO(c.env)));
|
|
3039
|
+
app.get("/xrpc/com.atproto.sync.getRepoStatus", (c) => getRepoStatus(c, getAccountDO(c.env)));
|
|
3040
|
+
app.get("/xrpc/com.atproto.sync.getBlob", (c) => getBlob(c, getAccountDO(c.env)));
|
|
3041
|
+
app.get("/xrpc/com.atproto.sync.listRepos", (c) => listRepos(c, getAccountDO(c.env)));
|
|
3042
|
+
app.get("/xrpc/com.atproto.sync.listBlobs", (c) => listBlobs(c, getAccountDO(c.env)));
|
|
3043
|
+
app.get("/xrpc/com.atproto.sync.subscribeRepos", async (c) => {
|
|
3044
|
+
if (c.req.header("Upgrade") !== "websocket") return c.json({
|
|
3045
|
+
error: "InvalidRequest",
|
|
3046
|
+
message: "Expected WebSocket upgrade"
|
|
3047
|
+
}, 400);
|
|
3048
|
+
return getAccountDO(c.env).fetch(c.req.raw);
|
|
3049
|
+
});
|
|
3050
|
+
app.get("/xrpc/com.atproto.repo.describeRepo", (c) => describeRepo(c, getAccountDO(c.env)));
|
|
3051
|
+
app.get("/xrpc/com.atproto.repo.getRecord", (c) => getRecord(c, getAccountDO(c.env)));
|
|
3052
|
+
app.get("/xrpc/com.atproto.repo.listRecords", (c) => listRecords(c, getAccountDO(c.env)));
|
|
3053
|
+
app.post("/xrpc/com.atproto.repo.createRecord", requireAuth, (c) => createRecord(c, getAccountDO(c.env)));
|
|
3054
|
+
app.post("/xrpc/com.atproto.repo.deleteRecord", requireAuth, (c) => deleteRecord(c, getAccountDO(c.env)));
|
|
3055
|
+
app.post("/xrpc/com.atproto.repo.uploadBlob", requireAuth, (c) => uploadBlob(c, getAccountDO(c.env)));
|
|
3056
|
+
app.post("/xrpc/com.atproto.repo.applyWrites", requireAuth, (c) => applyWrites(c, getAccountDO(c.env)));
|
|
3057
|
+
app.post("/xrpc/com.atproto.repo.putRecord", requireAuth, (c) => putRecord(c, getAccountDO(c.env)));
|
|
3058
|
+
app.post("/xrpc/com.atproto.repo.importRepo", requireAuth, (c) => importRepo(c, getAccountDO(c.env)));
|
|
3059
|
+
app.get("/xrpc/com.atproto.server.describeServer", describeServer);
|
|
3060
|
+
app.use("/xrpc/com.atproto.identity.resolveHandle", async (c, next) => {
|
|
3061
|
+
if (c.req.query("handle") === c.env.HANDLE) return c.json({ did: c.env.DID });
|
|
3062
|
+
await next();
|
|
3063
|
+
});
|
|
3064
|
+
app.post("/xrpc/com.atproto.server.createSession", createSession);
|
|
3065
|
+
app.post("/xrpc/com.atproto.server.refreshSession", refreshSession);
|
|
3066
|
+
app.get("/xrpc/com.atproto.server.getSession", getSession);
|
|
3067
|
+
app.post("/xrpc/com.atproto.server.deleteSession", deleteSession);
|
|
3068
|
+
app.get("/xrpc/com.atproto.server.getAccountStatus", requireAuth, (c) => getAccountStatus(c, getAccountDO(c.env)));
|
|
3069
|
+
app.get("/xrpc/app.bsky.actor.getPreferences", requireAuth, (c) => {
|
|
3070
|
+
return c.json({ preferences: [] });
|
|
3071
|
+
});
|
|
3072
|
+
app.post("/xrpc/app.bsky.actor.putPreferences", requireAuth, async (c) => {
|
|
3073
|
+
return c.json({});
|
|
3074
|
+
});
|
|
3075
|
+
app.get("/xrpc/app.bsky.ageassurance.getState", requireAuth, (c) => {
|
|
3076
|
+
return c.json({
|
|
3077
|
+
state: {
|
|
3078
|
+
status: "assured",
|
|
3079
|
+
access: "full",
|
|
3080
|
+
lastInitiatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3081
|
+
},
|
|
3082
|
+
metadata: { accountCreatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
3083
|
+
});
|
|
3084
|
+
});
|
|
3085
|
+
app.post("/admin/emit-identity", requireAuth, async (c) => {
|
|
3086
|
+
const result = await getAccountDO(c.env).rpcEmitIdentityEvent(c.env.HANDLE);
|
|
3087
|
+
return c.json(result);
|
|
3088
|
+
});
|
|
3089
|
+
app.all("/xrpc/*", async (c) => {
|
|
3090
|
+
const url = new URL(c.req.url);
|
|
3091
|
+
url.protocol = "https:";
|
|
3092
|
+
const lxm = url.pathname.replace("/xrpc/", "");
|
|
3093
|
+
const isChat = lxm.startsWith("chat.bsky.");
|
|
3094
|
+
url.host = isChat ? "api.bsky.chat" : "api.bsky.app";
|
|
3095
|
+
const audienceDid = isChat ? CHAT_DID : APPVIEW_DID;
|
|
3096
|
+
const auth = c.req.header("Authorization");
|
|
3097
|
+
let headers = {};
|
|
3098
|
+
if (auth?.startsWith("Bearer ")) {
|
|
3099
|
+
const token = auth.slice(7);
|
|
3100
|
+
const serviceDid = `did:web:${c.env.PDS_HOSTNAME}`;
|
|
3101
|
+
try {
|
|
3102
|
+
let userDid;
|
|
3103
|
+
if (token === c.env.AUTH_TOKEN) userDid = c.env.DID;
|
|
3104
|
+
else userDid = (await verifyAccessToken(token, c.env.JWT_SECRET, serviceDid)).sub;
|
|
3105
|
+
const keypair = await getKeypair();
|
|
3106
|
+
headers["Authorization"] = `Bearer ${await createServiceJwt({
|
|
3107
|
+
iss: userDid,
|
|
3108
|
+
aud: audienceDid,
|
|
3109
|
+
lxm,
|
|
3110
|
+
keypair
|
|
3111
|
+
})}`;
|
|
3112
|
+
} catch {}
|
|
3113
|
+
}
|
|
3114
|
+
const originalHeaders = Object.fromEntries(c.req.raw.headers);
|
|
3115
|
+
delete originalHeaders["authorization"];
|
|
3116
|
+
const reqInit = {
|
|
3117
|
+
method: c.req.method,
|
|
3118
|
+
headers: {
|
|
3119
|
+
...originalHeaders,
|
|
3120
|
+
...headers
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
3123
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") reqInit.body = c.req.raw.body;
|
|
3124
|
+
return fetch(url.toString(), reqInit);
|
|
3125
|
+
});
|
|
3126
|
+
var src_default = app;
|
|
3127
|
+
|
|
3128
|
+
//#endregion
|
|
3129
|
+
export { AccountDurableObject, src_default as default };
|
|
3130
|
+
//# sourceMappingURL=index.js.map
|