@alteran/astro 0.3.9 → 0.5.2

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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -30
  3. package/index.js +34 -28
  4. package/migrations/0007_bored_spitfire.sql +26 -0
  5. package/migrations/0008_furry_ozymandias.sql +2 -0
  6. package/migrations/meta/0007_snapshot.json +534 -0
  7. package/migrations/meta/0008_snapshot.json +548 -0
  8. package/migrations/meta/_journal.json +14 -0
  9. package/package.json +10 -9
  10. package/src/app.ts +8 -4
  11. package/src/db/account.ts +25 -6
  12. package/src/db/dal.ts +34 -23
  13. package/src/db/repo.ts +35 -35
  14. package/src/db/schema.ts +5 -1
  15. package/src/db/seed.ts +5 -13
  16. package/src/entrypoints/server.ts +2 -22
  17. package/src/handlers/root.ts +4 -4
  18. package/src/lib/account-state.ts +156 -0
  19. package/src/lib/actor.ts +28 -12
  20. package/src/lib/appview/auth-policy.ts +66 -0
  21. package/src/lib/appview/did-resolver.ts +233 -0
  22. package/src/lib/appview/proxy.ts +221 -0
  23. package/src/lib/appview/service-config.ts +61 -0
  24. package/src/lib/appview/service-jwt.ts +93 -0
  25. package/src/lib/appview/types.ts +25 -0
  26. package/src/lib/appview.ts +5 -532
  27. package/src/lib/auth-errors.ts +24 -0
  28. package/src/lib/auth.ts +63 -15
  29. package/src/lib/blockstore-gc.ts +2 -1
  30. package/src/lib/cache.ts +30 -4
  31. package/src/lib/chat.ts +14 -8
  32. package/src/lib/commit.ts +26 -36
  33. package/src/lib/config.ts +26 -15
  34. package/src/lib/did-document.ts +32 -0
  35. package/src/lib/errors.ts +54 -0
  36. package/src/lib/feed.ts +18 -19
  37. package/src/lib/firehose/frames.ts +87 -47
  38. package/src/lib/firehose/validation.ts +3 -3
  39. package/src/lib/jwt.ts +85 -177
  40. package/src/lib/labeler.ts +43 -30
  41. package/src/lib/logger.ts +4 -0
  42. package/src/lib/mst/block-map.ts +172 -0
  43. package/src/lib/mst/blockstore.ts +56 -93
  44. package/src/lib/mst/index.ts +1 -0
  45. package/src/lib/mst/leaf.ts +25 -0
  46. package/src/lib/mst/mst.ts +81 -237
  47. package/src/lib/mst/serialize.ts +97 -0
  48. package/src/lib/mst/types.ts +21 -0
  49. package/src/lib/oauth/clients.ts +67 -0
  50. package/src/lib/oauth/dpop-errors.ts +15 -0
  51. package/src/lib/oauth/dpop.ts +150 -0
  52. package/src/lib/oauth/resource.ts +199 -0
  53. package/src/lib/oauth/store.ts +77 -0
  54. package/src/lib/preferences.ts +9 -34
  55. package/src/lib/refresh-session.ts +161 -0
  56. package/src/lib/relay.ts +10 -8
  57. package/src/lib/secrets.ts +6 -7
  58. package/src/lib/sequencer.ts +12 -3
  59. package/src/lib/service-auth.ts +184 -0
  60. package/src/lib/session-tokens.ts +28 -76
  61. package/src/lib/streaming-car.ts +3 -0
  62. package/src/lib/tracing.ts +4 -3
  63. package/src/lib/util.ts +65 -15
  64. package/src/middleware.ts +1 -1
  65. package/src/pages/.well-known/did.json.ts +27 -30
  66. package/src/pages/.well-known/oauth-authorization-server.ts +31 -0
  67. package/src/pages/.well-known/oauth-protected-resource.ts +22 -0
  68. package/src/pages/debug/record.ts +1 -1
  69. package/src/pages/debug/sequencer.ts +28 -0
  70. package/src/pages/oauth/authorize.ts +78 -0
  71. package/src/pages/oauth/consent.ts +80 -0
  72. package/src/pages/oauth/par.ts +121 -0
  73. package/src/pages/oauth/token.ts +158 -0
  74. package/src/pages/xrpc/[...nsid].ts +61 -0
  75. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +12 -13
  76. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +23 -23
  77. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +9 -2
  78. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +9 -2
  79. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +9 -2
  80. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +43 -41
  81. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +10 -3
  82. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +40 -9
  83. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +41 -29
  84. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +20 -6
  85. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +1 -1
  86. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +101 -11
  87. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +44 -14
  88. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +41 -13
  89. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +2 -2
  90. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +14 -1
  91. package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +14 -6
  92. package/src/pages/xrpc/com.atproto.repo.listRecords.ts +1 -1
  93. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +42 -14
  94. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +76 -15
  95. package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +20 -8
  96. package/src/pages/xrpc/com.atproto.server.createSession.ts +31 -11
  97. package/src/pages/xrpc/com.atproto.server.describeServer.ts +1 -1
  98. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +12 -5
  99. package/src/pages/xrpc/com.atproto.server.getSession.ts +22 -8
  100. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +30 -72
  101. package/src/pages/xrpc/com.atproto.sync.getBlob.ts +71 -22
  102. package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +1 -1
  103. package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +1 -1
  104. package/src/pages/xrpc/com.atproto.sync.getHead.ts +7 -2
  105. package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +1 -1
  106. package/src/pages/xrpc/com.atproto.sync.getRecord.ts +5 -27
  107. package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +1 -1
  108. package/src/pages/xrpc/com.atproto.sync.getRepo.ts +50 -5
  109. package/src/pages/xrpc/com.atproto.sync.getRepoStatus.ts +58 -0
  110. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +1 -1
  111. package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
  112. package/src/services/car.ts +207 -55
  113. package/src/services/r2-blob-store.ts +1 -1
  114. package/src/services/repo/blockstore-ops.ts +29 -0
  115. package/src/services/repo/operations.ts +133 -0
  116. package/src/services/repo-manager.ts +202 -253
  117. package/src/worker/runtime.ts +53 -8
  118. package/src/worker/sequencer/broadcast.ts +91 -0
  119. package/src/worker/sequencer/cid-helpers.ts +39 -0
  120. package/src/worker/sequencer/payload.ts +84 -0
  121. package/src/worker/sequencer/types.ts +36 -0
  122. package/src/worker/sequencer/upgrade.ts +141 -0
  123. package/src/worker/sequencer.ts +263 -405
  124. package/types/env.d.ts +15 -3
  125. package/src/pages/xrpc/app.bsky.actor.getProfile.ts +0 -49
  126. package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +0 -51
  127. package/src/pages/xrpc/app.bsky.feed.getActorFeeds.ts +0 -25
  128. package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +0 -42
  129. package/src/pages/xrpc/app.bsky.feed.getFeedGenerators.ts +0 -25
  130. package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +0 -37
  131. package/src/pages/xrpc/app.bsky.feed.getPosts.ts +0 -26
  132. package/src/pages/xrpc/app.bsky.feed.getSuggestedFeeds.ts +0 -23
  133. package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +0 -47
  134. package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +0 -29
  135. package/src/pages/xrpc/app.bsky.graph.getFollows.ts +0 -29
  136. package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +0 -20
  137. package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +0 -27
  138. package/src/pages/xrpc/app.bsky.unspecced.getSuggestedFeeds.ts +0 -23
@@ -1,248 +1,295 @@
1
1
  import { CID } from 'multiformats/cid';
2
2
  import type { Env } from '../env';
3
- import { MST, D1Blockstore, Leaf } from '../lib/mst';
3
+ import { MST, D1Blockstore, type BlockMap } from '../lib/mst';
4
4
  import { drizzle } from 'drizzle-orm/d1';
5
5
  import { repo_root } from '../db/schema';
6
6
  import { eq, sql } from 'drizzle-orm';
7
7
  import type { RepoOp } from '../lib/firehose/frames';
8
- import * as dagCbor from '@ipld/dag-cbor';
9
- import { cidForCbor } from '../lib/mst/util';
10
8
  import { putRecord as dalPutRecord, deleteRecord as dalDeleteRecord } from '../db/dal';
11
9
  import { bumpRoot } from '../db/repo';
12
10
  import { generateTid } from '../lib/commit';
13
11
  import { resolveSecret } from '../lib/secrets';
12
+ import { storeRecord, storeMstBlocks } from './repo/blockstore-ops';
13
+ import { extractOps as extractOpsImpl } from './repo/operations';
14
+ import { ServerMisconfigured } from '../lib/errors';
15
+
16
+ interface RecordMutation {
17
+ mst: MST;
18
+ recordCid: CID;
19
+ prevMstRoot: CID | null;
20
+ newMstBlocks: BlockMap;
21
+ }
22
+
23
+ interface CommitResult {
24
+ uri: string;
25
+ cid: string;
26
+ commitCid: string;
27
+ rev: string;
28
+ ops: RepoOp[];
29
+ commitData: string;
30
+ sig: string;
31
+ blocks: string;
32
+ }
14
33
 
15
- /**
16
- * Repository Manager
17
- * Manages MST-based repository operations
18
- */
19
34
  export class RepoManager {
20
35
  private blockstore: D1Blockstore;
21
- private did: string;
22
36
 
23
37
  constructor(private env: Env) {
24
38
  this.blockstore = new D1Blockstore(env);
25
- // Note: this.did will be set asynchronously, but it's only used in async methods
26
- this.did = 'did:example:single-user'; // Default, will be resolved in async methods
27
39
  }
28
40
 
29
41
  private async getDid(): Promise<string> {
30
- return (await resolveSecret(this.env.PDS_DID)) ?? 'did:example:single-user';
42
+ const did = await resolveSecret(this.env.PDS_DID);
43
+ if (!did) throw new ServerMisconfigured('PDS_DID is required');
44
+ return did;
31
45
  }
32
46
 
33
- /**
34
- * Get the current MST root
35
- */
36
47
  async getRoot(): Promise<MST | null> {
37
- const db = drizzle(this.env.DB);
38
- const row = await db
39
- .select()
40
- .from(repo_root)
41
- .where(eq(repo_root.did, this.did))
42
- .get();
48
+ try {
49
+ const db = drizzle(this.env.DB);
50
+ const did = await this.getDid();
51
+
52
+ const rows = await db
53
+ .select()
54
+ .from(repo_root)
55
+ .where(eq(repo_root.did, did))
56
+ .limit(1);
57
+
58
+ const row = rows[0];
59
+ if (!row) return null;
60
+
61
+ const commit = await this.env.DB.prepare(
62
+ `SELECT data FROM commit_log WHERE cid = ? LIMIT 1`,
63
+ )
64
+ .bind(row.commitCid)
65
+ .first();
66
+
67
+ if (!commit) {
68
+ console.error(`[RepoManager] No commit found for CID: ${row.commitCid}`);
69
+ return null;
70
+ }
43
71
 
44
- if (!row || !row.commitCid) return null;
72
+ const parsed = JSON.parse(String(commit.data));
73
+ const mstRoot = CID.parse(String(parsed.data));
45
74
 
46
- try {
47
- const rootCid = CID.parse(row.commitCid);
48
- return MST.load(this.blockstore, rootCid);
49
- } catch {
75
+ console.log(
76
+ `[RepoManager] Loading MST root: ${mstRoot.toString()} from commit: ${row.commitCid}`,
77
+ );
78
+
79
+ return MST.load(this.blockstore, mstRoot);
80
+ } catch (error) {
81
+ console.error('[RepoManager] Error in getRoot:', error);
50
82
  return null;
51
83
  }
52
84
  }
53
85
 
54
- /**
55
- * Get or create the MST root
56
- */
57
86
  async getOrCreateRoot(): Promise<MST> {
58
87
  const existing = await this.getRoot();
59
- if (existing) return existing;
88
+ if (existing) {
89
+ const pointer = await existing.getPointer();
90
+ console.log(`[RepoManager] Loaded existing MST root: ${pointer.toString()}`);
91
+ return existing;
92
+ }
60
93
 
61
- // Create new empty MST
94
+ console.log('[RepoManager] Creating new empty MST');
62
95
  const mst = await MST.create(this.blockstore, []);
96
+ await storeMstBlocks(this.blockstore, mst);
97
+ const pointer = await mst.getPointer();
98
+ console.log(`[RepoManager] Created new MST root: ${pointer.toString()}`);
63
99
  return mst;
64
100
  }
65
101
 
66
- /**
67
- * Add a record to the repository
68
- */
69
- async addRecord(collection: string, rkey: string, record: unknown): Promise<{
70
- mst: MST;
71
- recordCid: CID;
72
- prevMstRoot: CID | null;
73
- }> {
102
+ async addRecord(
103
+ collection: string,
104
+ rkey: string,
105
+ record: unknown,
106
+ ): Promise<RecordMutation> {
74
107
  const key = `${collection}/${rkey}`;
75
-
76
- // Get previous MST root for op extraction
77
108
  const currentMst = await this.getOrCreateRoot();
78
109
  const prevMstRoot = await currentMst.getPointer();
79
-
80
- // Encode record and store in blockstore
81
- const recordCid = await this.storeRecord(record);
82
-
83
- // Add the new record
110
+ const recordCid = await storeRecord(this.blockstore, record);
84
111
  const newMst = await currentMst.add(key, recordCid);
85
-
86
- // Store all new MST blocks
87
- await this.storeMstBlocks(newMst);
88
-
89
- return { mst: newMst, recordCid, prevMstRoot };
112
+ const newMstBlocks = await storeMstBlocks(this.blockstore, newMst);
113
+ return { mst: newMst, recordCid, prevMstRoot, newMstBlocks };
90
114
  }
91
115
 
92
- /**
93
- * High-level helper: create record, persist JSON, bump root, return commit info
94
- */
95
- async createRecord(collection: string, record: unknown, rkey?: string): Promise<{
96
- uri: string;
97
- cid: string;
98
- commitCid: string;
99
- rev: string;
100
- ops: RepoOp[];
101
- commitData: string;
102
- sig: string;
103
- blocks: string;
104
- }> {
116
+ async createRecord(
117
+ collection: string,
118
+ record: unknown,
119
+ rkey?: string,
120
+ ): Promise<CommitResult> {
105
121
  const key = rkey ?? generateTid();
106
- const { mst, recordCid, prevMstRoot } = await this.addRecord(collection, key, record);
107
-
108
- // Persist JSON to table for easy reads
109
- const uri = `at://${this.did}/${collection}/${key}`;
110
- await dalPutRecord(this.env, { uri, did: this.did, cid: recordCid.toString(), json: JSON.stringify(record) } as any);
122
+ const { mst, recordCid, prevMstRoot, newMstBlocks } = await this.addRecord(
123
+ collection,
124
+ key,
125
+ record,
126
+ );
111
127
 
112
- // Update repo root with signed commit and extract ops
113
- const { commitCid, rev, ops, commitData, sig, blocks } = await bumpRoot(this.env, prevMstRoot ?? undefined);
128
+ const did = await this.getDid();
129
+ const uri = `at://${did}/${collection}/${key}`;
130
+ await dalPutRecord(this.env, {
131
+ uri,
132
+ did,
133
+ cid: recordCid.toString(),
134
+ json: JSON.stringify(record),
135
+ });
136
+
137
+ const currentRoot = await mst.getPointer();
138
+ const { commitCid, rev, ops, commitData, sig, blocks } = await bumpRoot(
139
+ this.env,
140
+ prevMstRoot ?? undefined,
141
+ currentRoot,
142
+ { newMstBlocks: Array.from(newMstBlocks) },
143
+ );
114
144
 
115
145
  return { uri, cid: recordCid.toString(), commitCid, rev, ops, commitData, sig, blocks };
116
146
  }
117
147
 
118
- /**
119
- * Update a record in the repository
120
- */
121
- async updateRecord(collection: string, rkey: string, record: unknown): Promise<{
122
- mst: MST;
123
- recordCid: CID;
124
- prevMstRoot: CID | null;
125
- }> {
148
+ async updateRecord(
149
+ collection: string,
150
+ rkey: string,
151
+ record: unknown,
152
+ ): Promise<RecordMutation> {
126
153
  const key = `${collection}/${rkey}`;
127
-
128
- // Get previous MST root for op extraction
129
154
  const currentMst = await this.getOrCreateRoot();
130
155
  const prevMstRoot = await currentMst.getPointer();
131
-
132
- // Encode record and store in blockstore
133
- const recordCid = await this.storeRecord(record);
134
-
135
- // Update the record
156
+ const recordCid = await storeRecord(this.blockstore, record);
136
157
  const newMst = await currentMst.update(key, recordCid);
137
-
138
- // Store all new MST blocks
139
- await this.storeMstBlocks(newMst);
140
-
141
- return { mst: newMst, recordCid, prevMstRoot };
158
+ const newMstBlocks = await storeMstBlocks(this.blockstore, newMst);
159
+ return { mst: newMst, recordCid, prevMstRoot, newMstBlocks };
142
160
  }
143
161
 
144
- /**
145
- * High-level helper: put record (update), persist JSON, bump root
146
- */
147
- async putRecord(collection: string, rkey: string, record: unknown): Promise<{
148
- uri: string;
149
- cid: string;
150
- commitCid: string;
151
- rev: string;
152
- ops: RepoOp[];
153
- commitData: string;
154
- sig: string;
155
- blocks: string;
156
- }> {
157
- const { mst, recordCid, prevMstRoot } = await this.updateRecord(collection, rkey, record);
158
- const uri = `at://${this.did}/${collection}/${rkey}`;
159
- await dalPutRecord(this.env, { uri, did: this.did, cid: recordCid.toString(), json: JSON.stringify(record) } as any);
160
- const { commitCid, rev, ops, commitData, sig, blocks } = await bumpRoot(this.env, prevMstRoot ?? undefined);
162
+ async putRecord(collection: string, rkey: string, record: unknown): Promise<CommitResult> {
163
+ const { mst, recordCid, prevMstRoot, newMstBlocks } = await this.updateRecord(
164
+ collection,
165
+ rkey,
166
+ record,
167
+ );
168
+ const did = await this.getDid();
169
+ const uri = `at://${did}/${collection}/${rkey}`;
170
+ await dalPutRecord(this.env, {
171
+ uri,
172
+ did,
173
+ cid: recordCid.toString(),
174
+ json: JSON.stringify(record),
175
+ });
176
+ const currentRoot = await mst.getPointer();
177
+ const { commitCid, rev, ops, commitData, sig, blocks } = await bumpRoot(
178
+ this.env,
179
+ prevMstRoot ?? undefined,
180
+ currentRoot,
181
+ { newMstBlocks: Array.from(newMstBlocks) },
182
+ );
161
183
  return { uri, cid: recordCid.toString(), commitCid, rev, ops, commitData, sig, blocks };
162
184
  }
163
185
 
164
- /**
165
- * Delete a record from the repository
166
- */
167
- async deleteRecord(collection: string, rkey: string): Promise<{
168
- uri: string;
169
- commitCid: string;
170
- rev: string;
171
- ops: RepoOp[];
172
- commitData: string;
173
- sig: string;
174
- blocks: string;
175
- }> {
186
+ async deleteRecord(
187
+ collection: string,
188
+ rkey: string,
189
+ ): Promise<{ mst: MST; prevMstRoot: CID | null; uri: string; newMstBlocks: BlockMap }> {
176
190
  const key = `${collection}/${rkey}`;
177
-
178
- // Get previous MST root for op extraction
179
191
  const currentMst = await this.getOrCreateRoot();
180
192
  const prevMstRoot = await currentMst.getPointer();
181
-
182
- // Delete the record
183
193
  const newMst = await currentMst.delete(key);
194
+ const newMstBlocks = await storeMstBlocks(this.blockstore, newMst);
184
195
 
185
- // Store all new MST blocks
186
- await this.storeMstBlocks(newMst);
187
-
188
- // Delete from records table
189
- const uri = `at://${this.did}/${collection}/${rkey}`;
196
+ const did = await this.getDid();
197
+ const uri = `at://${did}/${collection}/${rkey}`;
190
198
  await dalDeleteRecord(this.env, uri);
191
199
 
192
- const { commitCid, rev, ops, commitData, sig, blocks } = await bumpRoot(this.env, prevMstRoot ?? undefined);
193
- return { uri, commitCid, rev, ops, commitData, sig, blocks };
200
+ return { mst: newMst, prevMstRoot, uri, newMstBlocks };
194
201
  }
195
202
 
196
- /**
197
- * Get a record from the repository
198
- */
199
203
  async getRecord(collection: string, rkey: string): Promise<unknown | null> {
200
204
  const key = `${collection}/${rkey}`;
201
-
202
205
  const currentMst = await this.getRoot();
203
- if (!currentMst) return null;
206
+ if (!currentMst) return this.getRecordFromTable(collection, rkey);
204
207
 
205
208
  const recordCid = await currentMst.get(key);
206
- if (!recordCid) return null;
209
+ if (!recordCid) return this.getRecordFromTable(collection, rkey);
207
210
 
208
211
  return this.blockstore.readObj(recordCid);
209
212
  }
210
213
 
211
- /**
212
- * List records in a collection
213
- */
214
- async listRecords(collection: string, limit = 50, cursor?: string): Promise<{ key: string; cid: CID }[]> {
214
+ private async getRecordFromTable(collection: string, rkey: string): Promise<unknown | null> {
215
+ const did = await this.getDid();
216
+ const uri = `at://${did}/${collection}/${rkey}`;
217
+
218
+ const result = await this.env.DB.prepare(`SELECT json FROM record WHERE uri = ?`)
219
+ .bind(uri)
220
+ .first();
221
+
222
+ if (!result) return null;
223
+ try {
224
+ return JSON.parse(result.json as string);
225
+ } catch (parseError) {
226
+ console.warn('[RepoManager] Failed to parse record JSON:', parseError);
227
+ return null;
228
+ }
229
+ }
230
+
231
+ async listRecords(
232
+ collection: string,
233
+ limit = 50,
234
+ cursor?: string,
235
+ ): Promise<{ key: string; cid: CID }[]> {
215
236
  const currentMst = await this.getRoot();
216
- if (!currentMst) return [];
237
+ if (!currentMst) return this.listRecordsFromTable(collection, limit, cursor);
217
238
 
218
239
  const prefix = `${collection}/`;
219
240
  const leaves = await currentMst.listWithPrefix(prefix, limit);
220
241
 
221
- return leaves
222
- .filter(leaf => !cursor || leaf.key > `${collection}/${cursor}`)
223
- .map(leaf => ({
242
+ const results = leaves
243
+ .filter((leaf) => !cursor || leaf.key > `${collection}/${cursor}`)
244
+ .map((leaf) => ({
224
245
  key: leaf.key.replace(prefix, ''),
225
246
  cid: leaf.value,
226
247
  }));
248
+
249
+ if (results.length === 0) return this.listRecordsFromTable(collection, limit, cursor);
250
+ return results;
251
+ }
252
+
253
+ private async listRecordsFromTable(
254
+ collection: string,
255
+ limit = 50,
256
+ cursor?: string,
257
+ ): Promise<{ key: string; cid: CID }[]> {
258
+ const did = await this.getDid();
259
+ const prefix = `at://${did}/${collection}/`;
260
+
261
+ // D1's LIKE planner is flaky on long prefixes; use a >= / < range instead.
262
+ // URIs sort lexicographically so this scans only the collection's slice.
263
+ const rangeEnd =
264
+ prefix.slice(0, -1) +
265
+ String.fromCharCode(prefix.charCodeAt(prefix.length - 1) + 1);
266
+
267
+ const stmt = cursor
268
+ ? this.env.DB.prepare(
269
+ `SELECT uri, cid FROM record WHERE uri >= ? AND uri < ? AND uri > ? ORDER BY uri LIMIT ?`,
270
+ ).bind(prefix, rangeEnd, prefix + cursor, limit)
271
+ : this.env.DB.prepare(
272
+ `SELECT uri, cid FROM record WHERE uri >= ? AND uri < ? ORDER BY uri LIMIT ?`,
273
+ ).bind(prefix, rangeEnd, limit);
274
+
275
+ const result = await stmt.all();
276
+ const rows = result.results as Array<{ uri: string; cid: string }>;
277
+
278
+ return rows.map((row) => ({
279
+ key: row.uri.replace(prefix, ''),
280
+ cid: CID.parse(row.cid),
281
+ }));
227
282
  }
228
283
 
229
- /**
230
- * Update the repo root to point to the new MST
231
- */
232
284
  async updateRoot(mst: MST, rev: number): Promise<void> {
233
285
  const db = drizzle(this.env.DB);
234
286
  const rootCid = await mst.getPointer();
235
287
  const did = await this.getDid();
236
288
  const revStr = String(rev);
237
289
 
238
- // Use sql.raw with excluded to properly reference INSERT values
239
290
  await db
240
291
  .insert(repo_root)
241
- .values({
242
- did,
243
- commitCid: rootCid.toString(),
244
- rev: revStr,
245
- })
292
+ .values({ did, commitCid: rootCid.toString(), rev: revStr })
246
293
  .onConflictDoUpdate({
247
294
  target: repo_root.did,
248
295
  set: {
@@ -253,105 +300,7 @@ export class RepoManager {
253
300
  .run();
254
301
  }
255
302
 
256
- /**
257
- * Extract operations from MST diff between two commits
258
- * Compares old MST root with new MST root to identify create/update/delete operations
259
- */
260
- async extractOps(prevRoot: CID | null, newRoot: CID): Promise<RepoOp[]> {
261
- const ops: RepoOp[] = [];
262
-
263
- // Load both trees
264
- const newMst = await MST.load(this.blockstore, newRoot).getEntries();
265
- const prevMst = prevRoot ? await MST.load(this.blockstore, prevRoot).getEntries() : [];
266
-
267
- // Build maps for efficient lookup
268
- const prevMap = new Map<string, CID>();
269
- const newMap = new Map<string, CID>();
270
-
271
- // Collect all leaves from previous tree
272
- await this.collectLeaves(prevMst, prevMap);
273
-
274
- // Collect all leaves from new tree
275
- await this.collectLeaves(newMst, newMap);
276
-
277
- // Find creates and updates
278
- for (const [path, cid] of Array.from(newMap.entries())) {
279
- const prevCid = prevMap.get(path);
280
- if (!prevCid) {
281
- // New key - create operation
282
- ops.push({
283
- action: 'create',
284
- path,
285
- cid,
286
- });
287
- } else if (!prevCid.equals(cid)) {
288
- // Key exists but CID changed - update operation
289
- ops.push({
290
- action: 'update',
291
- path,
292
- cid,
293
- prev: prevCid,
294
- });
295
- }
296
- }
297
-
298
- // Find deletes
299
- for (const [path, prevCid] of Array.from(prevMap.entries())) {
300
- if (!newMap.has(path)) {
301
- // Key no longer exists - delete operation
302
- ops.push({
303
- action: 'delete',
304
- path,
305
- cid: null,
306
- prev: prevCid,
307
- });
308
- }
309
- }
310
-
311
- // Sort ops by path for deterministic ordering
312
- ops.sort((a, b) => a.path.localeCompare(b.path));
313
-
314
- return ops;
315
- }
316
-
317
- /**
318
- * Recursively collect all leaves from MST entries into a map
319
- */
320
- private async collectLeaves(entries: (MST | Leaf)[], map: Map<string, CID>): Promise<void> {
321
- for (const entry of entries) {
322
- if (entry.isLeaf()) {
323
- map.set(entry.key, entry.value);
324
- } else {
325
- // Recursively collect from subtree
326
- const subEntries = await entry.getEntries();
327
- await this.collectLeaves(subEntries, map);
328
- }
329
- }
330
- }
331
-
332
- /**
333
- * Store a record in the blockstore and return its CID
334
- */
335
- private async storeRecord(record: unknown): Promise<CID> {
336
- const bytes = dagCbor.encode(record);
337
- const cid = await cidForCbor(record);
338
- await this.blockstore.put(cid, bytes);
339
- return cid;
340
- }
341
-
342
- /**
343
- * Store all blocks from an MST to the blockstore
344
- */
345
- private async storeMstBlocks(mst: MST): Promise<void> {
346
- const { cid, bytes } = await mst.serialize();
347
- await this.blockstore.put(cid, bytes);
348
-
349
- // Recursively store child blocks
350
- const entries = await mst.getEntries();
351
- for (const entry of entries) {
352
- if (entry.isTree()) {
353
- await this.storeMstBlocks(entry);
354
- }
355
- }
303
+ extractOps(prevRoot: CID | null, newRoot: CID): Promise<RepoOp[]> {
304
+ return extractOpsImpl(this.blockstore, prevRoot, newRoot);
356
305
  }
357
306
  }
@@ -75,7 +75,7 @@ export function createPdsFetchHandler(options?: CreatePdsFetchHandlerOptions): P
75
75
  return new Response(null, { status: 204, headers }) as unknown as WorkersResponse;
76
76
  }
77
77
 
78
- await seed(resolvedEnv.DB, (resolvedEnv.PDS_DID as string | undefined) ?? 'did:example:single-user');
78
+ await seed(resolvedEnv.DB, resolvedEnv.PDS_DID as string);
79
79
 
80
80
  // Fire-and-forget: let relays know this PDS exists and is reachable.
81
81
  // Throttled per isolate and safe to call frequently.
@@ -89,15 +89,46 @@ export function createPdsFetchHandler(options?: CreatePdsFetchHandlerOptions): P
89
89
  if (!isRelayPath) {
90
90
  ctx.waitUntil(notifyRelaysIfNeeded(resolvedEnv as any, request.url));
91
91
  }
92
- } catch (err) {
92
+ } catch (error) {
93
93
  // Never block on relay notification
94
94
  }
95
95
 
96
96
  const url = new URL(request.url);
97
+
98
+ // Lightweight debug endpoint for Sequencer metrics
99
+ if (url.pathname === '/debug/sequencer' && request.method === 'GET') {
100
+ try {
101
+ if (!('SEQUENCER' in resolvedEnv) || !resolvedEnv.SEQUENCER) {
102
+ return new Response('Sequencer not configured', { status: 503 }) as unknown as WorkersResponse;
103
+ }
104
+ const id = (resolvedEnv as any).SEQUENCER.idFromName('default');
105
+ const stub = (resolvedEnv as any).SEQUENCER.get(id);
106
+ const proxyRequest = new Request(new URL('/metrics', request.url).toString(), { method: 'GET' });
107
+ const response = await stub.fetch(proxyRequest as any);
108
+ // Pass through JSON
109
+ const headers = new Headers(response.headers);
110
+ headers.set('Content-Type', 'application/json');
111
+ return new Response(await response.text(), { status: response.status, headers }) as unknown as WorkersResponse;
112
+ } catch (error) {
113
+ return new Response(JSON.stringify({ error: 'InternalError', message: 'Failed to fetch sequencer metrics' }), { status: 500, headers: { 'Content-Type': 'application/json' } }) as unknown as WorkersResponse;
114
+ }
115
+ }
97
116
  if (url.pathname === '/xrpc/com.atproto.sync.subscribeRepos') {
98
117
  const upgrade = request.headers.get('upgrade');
99
118
  if (upgrade !== 'websocket') {
100
- return new Response('Expected websocket', { status: 426 }) as unknown as WorkersResponse;
119
+ try {
120
+ console.log(JSON.stringify({
121
+ level: 'warn',
122
+ type: 'ws_expected',
123
+ path: url.pathname,
124
+ method: request.method,
125
+ message: 'subscribeRepos requires WebSocket upgrade',
126
+ timestamp: new Date().toISOString(),
127
+ }));
128
+ } catch {
129
+ // Logging-only path; never block the response on log serialization.
130
+ }
131
+ return new Response('This endpoint requires a WebSocket (wss://) upgrade', { status: 426 }) as unknown as WorkersResponse;
101
132
  }
102
133
  if (!resolvedEnv.SEQUENCER) {
103
134
  return new Response('Sequencer not configured', { status: 503 }) as unknown as WorkersResponse;
@@ -123,9 +154,21 @@ type AstroFetchHandler = (
123
154
  let cachedFetchPromise: Promise<AstroFetchHandler> | undefined;
124
155
 
125
156
  async function loadAstroFetchFromManifest(manifest: SSRManifest): Promise<AstroFetchHandler> {
126
- const { createExports } = await import('@astrojs/cloudflare/entrypoints/server.js');
127
- const exports = createExports(manifest);
128
- return exports.default.fetch as unknown as AstroFetchHandler;
157
+ const { App } = await import('astro/app');
158
+ const { handle } = await import('@astrojs/cloudflare/handler');
159
+ const app = new App(manifest);
160
+ return async (
161
+ request: WorkersRequest,
162
+ env: Env,
163
+ ctx: ExecutionContext,
164
+ ) =>
165
+ (await handle(
166
+ manifest,
167
+ app,
168
+ request as any,
169
+ env as any,
170
+ ctx as any,
171
+ )) as unknown as WorkersResponse;
129
172
  }
130
173
 
131
174
  async function getAstroFetch(options?: CreatePdsFetchHandlerOptions): Promise<AstroFetchHandler> {
@@ -135,8 +178,10 @@ async function getAstroFetch(options?: CreatePdsFetchHandlerOptions): Promise<As
135
178
 
136
179
  if (!cachedFetchPromise) {
137
180
  cachedFetchPromise = (async () => {
138
- const { manifest } = await import('@astrojs-manifest');
139
- return loadAstroFetchFromManifest(manifest as SSRManifest);
181
+ const moduleSpecifier = '@astrojs-manifest';
182
+ const mod = await import(/* @vite-ignore */ moduleSpecifier);
183
+ const manifest = (mod as any).manifest as SSRManifest;
184
+ return loadAstroFetchFromManifest(manifest);
140
185
  })();
141
186
  }
142
187