@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/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