@alteran/astro 0.1.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.
Files changed (90) hide show
  1. package/README.md +558 -0
  2. package/index.d.ts +12 -0
  3. package/index.js +129 -0
  4. package/package.json +75 -0
  5. package/src/_worker.ts +44 -0
  6. package/src/app.ts +10 -0
  7. package/src/db/client.ts +7 -0
  8. package/src/db/dal.ts +97 -0
  9. package/src/db/repo.ts +135 -0
  10. package/src/db/schema.ts +89 -0
  11. package/src/db/seed.ts +14 -0
  12. package/src/env.d.ts +4 -0
  13. package/src/handlers/debug.ts +34 -0
  14. package/src/handlers/health.ts +6 -0
  15. package/src/handlers/ready.ts +14 -0
  16. package/src/handlers/root.ts +5 -0
  17. package/src/handlers/wellknown.ts +7 -0
  18. package/src/handlers/xrpc.repo.core.ts +57 -0
  19. package/src/handlers/xrpc.server.createSession.ts +25 -0
  20. package/src/handlers/xrpc.server.refreshSession.ts +43 -0
  21. package/src/lib/auth.ts +20 -0
  22. package/src/lib/blockstore-gc.ts +197 -0
  23. package/src/lib/cache.ts +236 -0
  24. package/src/lib/car-reader.ts +157 -0
  25. package/src/lib/commit-log-pruning.ts +76 -0
  26. package/src/lib/commit.ts +162 -0
  27. package/src/lib/config.ts +208 -0
  28. package/src/lib/errors.ts +142 -0
  29. package/src/lib/firehose/frames.ts +229 -0
  30. package/src/lib/firehose/parse.ts +82 -0
  31. package/src/lib/firehose/validation.ts +9 -0
  32. package/src/lib/handle.ts +90 -0
  33. package/src/lib/jwt.ts +150 -0
  34. package/src/lib/logger.ts +73 -0
  35. package/src/lib/metrics.ts +194 -0
  36. package/src/lib/mst/blockstore.ts +105 -0
  37. package/src/lib/mst/index.ts +3 -0
  38. package/src/lib/mst/mst.ts +643 -0
  39. package/src/lib/mst/util.ts +86 -0
  40. package/src/lib/ratelimit.ts +34 -0
  41. package/src/lib/sequencer.ts +10 -0
  42. package/src/lib/streaming-car.ts +137 -0
  43. package/src/lib/token-cleanup.ts +38 -0
  44. package/src/lib/tracing.ts +136 -0
  45. package/src/lib/util.ts +55 -0
  46. package/src/middleware.ts +102 -0
  47. package/src/pages/.well-known/atproto-did.ts +7 -0
  48. package/src/pages/.well-known/did.json.ts +76 -0
  49. package/src/pages/debug/blob/[...key].ts +27 -0
  50. package/src/pages/debug/db/bootstrap.ts +23 -0
  51. package/src/pages/debug/db/commits.ts +20 -0
  52. package/src/pages/debug/gc/blobs.ts +16 -0
  53. package/src/pages/debug/record.ts +33 -0
  54. package/src/pages/health.ts +68 -0
  55. package/src/pages/index.astro +57 -0
  56. package/src/pages/index.ts +2 -0
  57. package/src/pages/ready.ts +16 -0
  58. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +38 -0
  59. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +45 -0
  60. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +73 -0
  61. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +36 -0
  62. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +36 -0
  63. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +51 -0
  64. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +25 -0
  65. package/src/pages/xrpc/com.atproto.repo.listRecords.ts +57 -0
  66. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +36 -0
  67. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +53 -0
  68. package/src/pages/xrpc/com.atproto.server.createSession.ts +92 -0
  69. package/src/pages/xrpc/com.atproto.server.deleteSession.ts +25 -0
  70. package/src/pages/xrpc/com.atproto.server.describeServer.ts +17 -0
  71. package/src/pages/xrpc/com.atproto.server.getSession.ts +46 -0
  72. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +67 -0
  73. package/src/pages/xrpc/com.atproto.sync.getBlocks.json.ts +16 -0
  74. package/src/pages/xrpc/com.atproto.sync.getBlocks.ts +56 -0
  75. package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +20 -0
  76. package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +43 -0
  77. package/src/pages/xrpc/com.atproto.sync.getHead.ts +11 -0
  78. package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +42 -0
  79. package/src/pages/xrpc/com.atproto.sync.getRecord.ts +63 -0
  80. package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +20 -0
  81. package/src/pages/xrpc/com.atproto.sync.getRepo.range.ts +34 -0
  82. package/src/pages/xrpc/com.atproto.sync.getRepo.ts +17 -0
  83. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +53 -0
  84. package/src/pages/xrpc/com.atproto.sync.listRepos.ts +31 -0
  85. package/src/services/car.ts +249 -0
  86. package/src/services/r2-blob-store.ts +87 -0
  87. package/src/services/repo-manager.ts +339 -0
  88. package/src/shims/astro-internal-handler.d.ts +4 -0
  89. package/src/worker/sequencer.ts +563 -0
  90. package/types/env.d.ts +48 -0
@@ -0,0 +1,249 @@
1
+ import type { Env } from '../env';
2
+ import { listRecords } from '../db/dal';
3
+ import { drizzle } from 'drizzle-orm/d1';
4
+ import { desc, and, gte, lte } from 'drizzle-orm';
5
+ import { commit_log } from '../db/schema';
6
+ import { CID } from 'multiformats/cid';
7
+ import * as dagCbor from '@ipld/dag-cbor';
8
+ import { sha256 } from 'multiformats/hashes/sha2';
9
+ import { MST, Leaf, D1Blockstore } from '../lib/mst';
10
+
11
+ export type CarSnapshot = {
12
+ bytes: Uint8Array;
13
+ root: CID;
14
+ blocks: { cid: CID; bytes: Uint8Array }[];
15
+ };
16
+
17
+ export async function encodeRecordBlock(value: unknown) {
18
+ const bytes = dagCbor.encode(value);
19
+ const hash = await sha256.digest(bytes);
20
+ const cid = CID.createV1(dagCbor.code, hash);
21
+ return { cid, bytes } as const;
22
+ }
23
+
24
+ export async function buildRepoCar(env: Env, did: string): Promise<CarSnapshot> {
25
+ // Prefer the latest signed commit from commit_log (authoritative root)
26
+ const db = drizzle(env.DB);
27
+ const tip = await db.select().from(commit_log).orderBy(desc(commit_log.seq)).limit(1).get();
28
+
29
+ if (tip) {
30
+ try {
31
+ // Reconstruct the exact signed commit object that produced tip.cid
32
+ const parsed = JSON.parse(tip.data);
33
+ const prevStr = parsed.prev ?? null;
34
+ const signedCommit = {
35
+ did: parsed.did as string,
36
+ version: parsed.version as number,
37
+ data: CID.parse(String(parsed.data)),
38
+ rev: String(parsed.rev),
39
+ prev: prevStr ? CID.parse(String(prevStr)) : null,
40
+ sig: (() => {
41
+ const bin = atob(String(tip.sig));
42
+ const u8 = new Uint8Array(bin.length);
43
+ for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
44
+ return u8;
45
+ })(),
46
+ } as const;
47
+
48
+ // Encode to CBOR and verify CID matches tip
49
+ const commitBytes = dagCbor.encode(signedCommit);
50
+ const hash = await sha256.digest(commitBytes);
51
+ const commitCid = CID.createV1(dagCbor.code, hash);
52
+
53
+ if (commitCid.toString() === (tip as any).cid) {
54
+ // Build a full snapshot CAR: commit block + all MST nodes + all record blocks
55
+ const blockstore = new D1Blockstore(env);
56
+ const blocks: { cid: CID; bytes: Uint8Array }[] = [{ cid: commitCid, bytes: commitBytes }];
57
+ const seen = new Set<string>([commitCid.toString()]);
58
+
59
+ const addBlock = async (cid: CID) => {
60
+ const key = cid.toString();
61
+ if (seen.has(key)) return;
62
+ const bytes = await blockstore.get(cid);
63
+ if (bytes) {
64
+ seen.add(key);
65
+ blocks.push({ cid, bytes });
66
+ }
67
+ };
68
+
69
+ const mstRoot = CID.parse(String(parsed.data));
70
+ // 1) Add all MST node blocks
71
+ await addMstBlocks(blockstore, mstRoot, seen, blocks);
72
+
73
+ // 2) Add all record leaf blocks by walking the MST
74
+ try {
75
+ const mst = MST.load(blockstore, mstRoot);
76
+ for await (const leaf of mst.walkLeavesFrom('')) {
77
+ await addBlock(leaf.value);
78
+ }
79
+ } catch (e) {
80
+ console.warn('Snapshot: failed traversing MST leaves:', e);
81
+ }
82
+
83
+ const bytes = encodeCar([commitCid], blocks);
84
+ return { bytes, root: commitCid, blocks };
85
+ }
86
+ } catch (e) {
87
+ // Fall through to deterministic snapshot
88
+ console.warn('Failed to reconstruct signed commit from tip; falling back to snapshot:', e);
89
+ }
90
+ }
91
+
92
+ // Fallback: deterministic snapshot built from current records
93
+ const rows = await listRecords(env);
94
+ const blocks: { cid: CID; bytes: Uint8Array }[] = [];
95
+ for (const r of rows) {
96
+ if (!r.uri.startsWith(`at://${did}/`)) continue;
97
+ const value = JSON.parse(r.json);
98
+ const block = await encodeRecordBlock(value);
99
+ blocks.push(block);
100
+ }
101
+ const commitObj = { type: 'commit', did, records: blocks.map((b) => b.cid.toString()).sort() };
102
+ const commit = await encodeRecordBlock(commitObj);
103
+ const bytes = encodeCar([commit.cid], [...blocks, commit]);
104
+ return { bytes, root: commit.cid, blocks: [...blocks, commit] };
105
+ }
106
+
107
+ export async function buildRepoCarRange(env: Env, fromSeq: number, toSeq: number): Promise<CarSnapshot> {
108
+ const db = drizzle(env.DB);
109
+ const rows = await db.select().from(commit_log).where(and(gte(commit_log.seq, fromSeq), lte(commit_log.seq, toSeq))).all();
110
+ const blocks: { cid: CID; bytes: Uint8Array }[] = [];
111
+ for (const r of rows) {
112
+ const b = await encodeRecordBlock({ type: 'commit', rev: r.rev, head: r.cid, ts: r.ts });
113
+ blocks.push(b);
114
+ }
115
+ const root = blocks[blocks.length - 1]?.cid ?? (await encodeRecordBlock({})).cid;
116
+ const bytes = encodeCar([root], blocks);
117
+ return { bytes, root, blocks };
118
+ }
119
+
120
+ export async function buildBlocksCar(values: unknown[]): Promise<CarSnapshot> {
121
+ const blocks: { cid: CID; bytes: Uint8Array }[] = [];
122
+ for (const v of values) {
123
+ const block = await encodeRecordBlock(v);
124
+ blocks.push(block);
125
+ }
126
+ const root = blocks[0]?.cid ?? (await encodeRecordBlock({})).cid;
127
+ const bytes = encodeCar([root], blocks);
128
+ return { bytes, root, blocks };
129
+ }
130
+
131
+ /**
132
+ * Encode a list of already-encoded blocks into a CAR v1 file.
133
+ */
134
+ export function encodeBlocksToCAR(root: CID, blocks: { cid: CID; bytes: Uint8Array }[]): Uint8Array {
135
+ return encodeCar([root], blocks);
136
+ }
137
+
138
+ export function encodeExistingBlocksToCAR(roots: CID[], blocks: { cid: CID; bytes: Uint8Array }[]): Uint8Array {
139
+ return encodeCar(roots, blocks);
140
+ }
141
+
142
+ function concat(parts: Uint8Array[]): Uint8Array {
143
+ const size = parts.reduce((n, p) => n + p.byteLength, 0);
144
+ const buf = new Uint8Array(size);
145
+ let off = 0;
146
+ for (const p of parts) { buf.set(p, off); off += p.byteLength; }
147
+ return buf;
148
+ }
149
+
150
+ function varint(n: number): Uint8Array {
151
+ const bytes: number[] = [];
152
+ while (n >= 0x80) {
153
+ bytes.push((n & 0x7f) | 0x80);
154
+ n >>>= 7;
155
+ }
156
+ bytes.push(n);
157
+ return new Uint8Array(bytes);
158
+ }
159
+
160
+ function encodeCar(roots: CID[], blocks: { cid: CID; bytes: Uint8Array }[]): Uint8Array {
161
+ const header = dagCbor.encode({ version: 1, roots });
162
+ const chunks: Uint8Array[] = [];
163
+ chunks.push(varint(header.byteLength));
164
+ chunks.push(header);
165
+ for (const { cid, bytes } of blocks) {
166
+ const block = concat([cid.bytes, bytes]);
167
+ chunks.push(varint(block.byteLength));
168
+ chunks.push(block);
169
+ }
170
+ return concat(chunks);
171
+ }
172
+
173
+ /**
174
+ * Encode blocks for firehose commit frame
175
+ * Includes commit block, MST nodes, and record blocks
176
+ */
177
+ export async function encodeBlocksForCommit(
178
+ env: Env,
179
+ commitCid: CID,
180
+ mstRoot: CID,
181
+ ops: Array<{ path: string; cid: CID | null }>,
182
+ ): Promise<Uint8Array> {
183
+ const blockstore = new D1Blockstore(env);
184
+ const blocks: { cid: CID; bytes: Uint8Array }[] = [];
185
+ const seen = new Set<string>();
186
+
187
+ // Helper to add block if not already seen
188
+ const addBlock = async (cid: CID) => {
189
+ const cidStr = cid.toString();
190
+ if (seen.has(cidStr)) return;
191
+ seen.add(cidStr);
192
+
193
+ const bytes = await blockstore.get(cid);
194
+ if (bytes) {
195
+ blocks.push({ cid, bytes });
196
+ }
197
+ };
198
+
199
+ // 1. Add commit block
200
+ await addBlock(commitCid);
201
+
202
+ // 2. Add MST nodes by traversing the tree
203
+ await addMstBlocks(blockstore, mstRoot, seen, blocks);
204
+
205
+ // 3. Add record blocks for all operations
206
+ for (const op of ops) {
207
+ if (op.cid) {
208
+ await addBlock(op.cid);
209
+ }
210
+ }
211
+
212
+ // Encode as CAR with commit as root
213
+ return encodeCar([commitCid], blocks);
214
+ }
215
+
216
+ /**
217
+ * Recursively add all MST node blocks
218
+ */
219
+ async function addMstBlocks(
220
+ blockstore: D1Blockstore,
221
+ rootCid: CID,
222
+ seen: Set<string>,
223
+ blocks: { cid: CID; bytes: Uint8Array }[],
224
+ ): Promise<void> {
225
+ const cidStr = rootCid.toString();
226
+ if (seen.has(cidStr)) return;
227
+ seen.add(cidStr);
228
+
229
+ // Add the MST node block itself
230
+ const bytes = await blockstore.get(rootCid);
231
+ if (!bytes) return;
232
+ blocks.push({ cid: rootCid, bytes });
233
+
234
+ // Load MST and traverse children
235
+ try {
236
+ const mst = MST.load(blockstore, rootCid);
237
+ const entries = await mst.getEntries();
238
+
239
+ for (const entry of entries) {
240
+ if (entry.isTree()) {
241
+ // Recursively add child MST blocks
242
+ const childCid = await entry.getPointer();
243
+ await addMstBlocks(blockstore, childCid, seen, blocks);
244
+ }
245
+ }
246
+ } catch (error) {
247
+ console.error('Error traversing MST:', error);
248
+ }
249
+ }
@@ -0,0 +1,87 @@
1
+ // Types via tsconfig.app.json
2
+ import type { R2ObjectBody } from '@cloudflare/workers-types';
3
+ import type { Env } from '../env';
4
+
5
+ export type PutOptions = {
6
+ contentType?: string;
7
+ maxBytes?: number; // default from env or 5 MiB
8
+ };
9
+
10
+ export type PutResult = {
11
+ key: string;
12
+ size: number;
13
+ sha256: string; // base64url
14
+ };
15
+
16
+ type BlobBody = ArrayBuffer | ArrayBufferView;
17
+
18
+ export class R2BlobStore {
19
+ constructor(private env: Env) {}
20
+
21
+ private maxBytes(defaultMax = 5 * 1024 * 1024): number {
22
+ const raw = (this.env as any).PDS_MAX_BLOB_SIZE as string | undefined;
23
+ const n = raw ? Number(raw) : defaultMax;
24
+ return Number.isFinite(n) && n > 0 ? n : defaultMax;
25
+ }
26
+
27
+ private static asUint8Array(data: BlobBody): Uint8Array {
28
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
29
+ if (ArrayBuffer.isView(data)) {
30
+ const view = data as ArrayBufferView;
31
+ return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
32
+ }
33
+ // Fallback should never happen because BlobBody restricts input, but keep typing satisfied
34
+ return new Uint8Array(data as ArrayBuffer);
35
+ }
36
+
37
+ private static toArrayBuffer(data: Uint8Array): ArrayBuffer {
38
+ const bufferLike = data.buffer;
39
+ if (bufferLike instanceof ArrayBuffer) {
40
+ if (data.byteOffset === 0 && data.byteLength === bufferLike.byteLength) {
41
+ return bufferLike;
42
+ }
43
+ return bufferLike.slice(data.byteOffset, data.byteOffset + data.byteLength);
44
+ }
45
+ const buffer = new ArrayBuffer(data.byteLength);
46
+ new Uint8Array(buffer).set(data);
47
+ return buffer;
48
+ }
49
+
50
+ private static b64url(bytes: ArrayBuffer | Uint8Array): string {
51
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
52
+ let s = '';
53
+ for (const v of view) s += String.fromCharCode(v);
54
+ return btoa(s).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
55
+ }
56
+
57
+ private static hex(bytes: Uint8Array): string {
58
+ return Array.from(bytes).map((v) => v.toString(16).padStart(2, '0')).join('');
59
+ }
60
+
61
+ private static cidKey(shaB64url: string, prefix = 'blobs/by-cid/'): string {
62
+ return `${prefix}${shaB64url}`;
63
+ }
64
+
65
+ async put(body: BlobBody, opts: PutOptions = {}): Promise<PutResult> {
66
+ const view = R2BlobStore.asUint8Array(body);
67
+ const size = view.byteLength;
68
+ const limit = opts.maxBytes ?? this.maxBytes();
69
+ if (size > limit) throw new Error(`BlobTooLarge:${size}>${limit}`);
70
+
71
+ const contentType = opts.contentType ?? 'application/octet-stream';
72
+ const sha = await crypto.subtle.digest('SHA-256', view);
73
+ const shaB64 = R2BlobStore.b64url(sha);
74
+ const key = R2BlobStore.cidKey(shaB64);
75
+ const buffer = R2BlobStore.toArrayBuffer(view);
76
+ await this.env.BLOBS.put(key, buffer, { httpMetadata: { contentType } });
77
+ return { key, size, sha256: shaB64 };
78
+ }
79
+
80
+ async get(key: string): Promise<R2ObjectBody | null> {
81
+ return this.env.BLOBS.get(key);
82
+ }
83
+
84
+ async delete(key: string): Promise<void> {
85
+ await this.env.BLOBS.delete(key);
86
+ }
87
+ }
@@ -0,0 +1,339 @@
1
+ import { CID } from 'multiformats/cid';
2
+ import type { Env } from '../env';
3
+ import { MST, D1Blockstore, Leaf } from '../lib/mst';
4
+ import { drizzle } from 'drizzle-orm/d1';
5
+ import { repo_root } from '../db/schema';
6
+ import { eq } from 'drizzle-orm';
7
+ import type { RepoOp } from '../lib/firehose/frames';
8
+ import * as dagCbor from '@ipld/dag-cbor';
9
+ import { cidForCbor } from '../lib/mst/util';
10
+ import { putRecord as dalPutRecord, deleteRecord as dalDeleteRecord } from '../db/dal';
11
+ import { bumpRoot } from '../db/repo';
12
+ import { generateTid } from '../lib/commit';
13
+
14
+ /**
15
+ * Repository Manager
16
+ * Manages MST-based repository operations
17
+ */
18
+ export class RepoManager {
19
+ private blockstore: D1Blockstore;
20
+ private did: string;
21
+
22
+ constructor(private env: Env) {
23
+ this.blockstore = new D1Blockstore(env);
24
+ this.did = env.PDS_DID ?? 'did:example:single-user';
25
+ }
26
+
27
+ /**
28
+ * Get the current MST root
29
+ */
30
+ async getRoot(): Promise<MST | null> {
31
+ const db = drizzle(this.env.DB);
32
+ const row = await db
33
+ .select()
34
+ .from(repo_root)
35
+ .where(eq(repo_root.did, this.did))
36
+ .get();
37
+
38
+ if (!row || !row.commitCid) return null;
39
+
40
+ try {
41
+ const rootCid = CID.parse(row.commitCid);
42
+ return MST.load(this.blockstore, rootCid);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get or create the MST root
50
+ */
51
+ async getOrCreateRoot(): Promise<MST> {
52
+ const existing = await this.getRoot();
53
+ if (existing) return existing;
54
+
55
+ // Create new empty MST
56
+ const mst = await MST.create(this.blockstore, []);
57
+ return mst;
58
+ }
59
+
60
+ /**
61
+ * Add a record to the repository
62
+ */
63
+ async addRecord(collection: string, rkey: string, record: unknown): Promise<{
64
+ mst: MST;
65
+ recordCid: CID;
66
+ prevMstRoot: CID | null;
67
+ }> {
68
+ const key = `${collection}/${rkey}`;
69
+
70
+ // Get previous MST root for op extraction
71
+ const currentMst = await this.getOrCreateRoot();
72
+ const prevMstRoot = await currentMst.getPointer();
73
+
74
+ // Encode record and store in blockstore
75
+ const recordCid = await this.storeRecord(record);
76
+
77
+ // Add the new record
78
+ const newMst = await currentMst.add(key, recordCid);
79
+
80
+ // Store all new MST blocks
81
+ await this.storeMstBlocks(newMst);
82
+
83
+ return { mst: newMst, recordCid, prevMstRoot };
84
+ }
85
+
86
+ /**
87
+ * High-level helper: create record, persist JSON, bump root, return commit info
88
+ */
89
+ async createRecord(collection: string, record: unknown, rkey?: string): Promise<{
90
+ uri: string;
91
+ cid: string;
92
+ commitCid: string;
93
+ rev: string;
94
+ ops: RepoOp[];
95
+ }> {
96
+ const key = rkey ?? generateTid();
97
+ const { mst, recordCid, prevMstRoot } = await this.addRecord(collection, key, record);
98
+
99
+ // Persist JSON to table for easy reads
100
+ const uri = `at://${this.did}/${collection}/${key}`;
101
+ await dalPutRecord(this.env, { uri, did: this.did, cid: recordCid.toString(), json: JSON.stringify(record) } as any);
102
+
103
+ // Update repo root with signed commit and extract ops
104
+ const { commitCid, rev, ops } = await bumpRoot(this.env, prevMstRoot ?? undefined);
105
+
106
+ return { uri, cid: recordCid.toString(), commitCid, rev, ops };
107
+ }
108
+
109
+ /**
110
+ * Update a record in the repository
111
+ */
112
+ async updateRecord(collection: string, rkey: string, record: unknown): Promise<{
113
+ mst: MST;
114
+ recordCid: CID;
115
+ prevMstRoot: CID | null;
116
+ }> {
117
+ const key = `${collection}/${rkey}`;
118
+
119
+ // Get previous MST root for op extraction
120
+ const currentMst = await this.getOrCreateRoot();
121
+ const prevMstRoot = await currentMst.getPointer();
122
+
123
+ // Encode record and store in blockstore
124
+ const recordCid = await this.storeRecord(record);
125
+
126
+ // Update the record
127
+ const newMst = await currentMst.update(key, recordCid);
128
+
129
+ // Store all new MST blocks
130
+ await this.storeMstBlocks(newMst);
131
+
132
+ return { mst: newMst, recordCid, prevMstRoot };
133
+ }
134
+
135
+ /**
136
+ * High-level helper: put record (update), persist JSON, bump root
137
+ */
138
+ async putRecord(collection: string, rkey: string, record: unknown): Promise<{
139
+ uri: string;
140
+ cid: string;
141
+ commitCid: string;
142
+ rev: string;
143
+ ops: RepoOp[];
144
+ }> {
145
+ const { mst, recordCid, prevMstRoot } = await this.updateRecord(collection, rkey, record);
146
+ const uri = `at://${this.did}/${collection}/${rkey}`;
147
+ await dalPutRecord(this.env, { uri, did: this.did, cid: recordCid.toString(), json: JSON.stringify(record) } as any);
148
+ const { commitCid, rev, ops } = await bumpRoot(this.env, prevMstRoot ?? undefined);
149
+ return { uri, cid: recordCid.toString(), commitCid, rev, ops };
150
+ }
151
+
152
+ /**
153
+ * Delete a record from the repository
154
+ */
155
+ async deleteRecord(collection: string, rkey: string): Promise<{
156
+ uri: string;
157
+ commitCid: string;
158
+ rev: string;
159
+ ops: RepoOp[];
160
+ }> {
161
+ const key = `${collection}/${rkey}`;
162
+
163
+ // Get previous MST root for op extraction
164
+ const currentMst = await this.getOrCreateRoot();
165
+ const prevMstRoot = await currentMst.getPointer();
166
+
167
+ // Delete the record
168
+ const newMst = await currentMst.delete(key);
169
+
170
+ // Store all new MST blocks
171
+ await this.storeMstBlocks(newMst);
172
+
173
+ // Delete from records table
174
+ const uri = `at://${this.did}/${collection}/${rkey}`;
175
+ await dalDeleteRecord(this.env, uri);
176
+
177
+ const { commitCid, rev, ops } = await bumpRoot(this.env, prevMstRoot ?? undefined);
178
+ return { uri, commitCid, rev, ops };
179
+ }
180
+
181
+ /**
182
+ * Get a record from the repository
183
+ */
184
+ async getRecord(collection: string, rkey: string): Promise<unknown | null> {
185
+ const key = `${collection}/${rkey}`;
186
+
187
+ const currentMst = await this.getRoot();
188
+ if (!currentMst) return null;
189
+
190
+ const recordCid = await currentMst.get(key);
191
+ if (!recordCid) return null;
192
+
193
+ return this.blockstore.readObj(recordCid);
194
+ }
195
+
196
+ /**
197
+ * List records in a collection
198
+ */
199
+ async listRecords(collection: string, limit = 50, cursor?: string): Promise<{ key: string; cid: CID }[]> {
200
+ const currentMst = await this.getRoot();
201
+ if (!currentMst) return [];
202
+
203
+ const prefix = `${collection}/`;
204
+ const leaves = await currentMst.listWithPrefix(prefix, limit);
205
+
206
+ return leaves
207
+ .filter(leaf => !cursor || leaf.key > `${collection}/${cursor}`)
208
+ .map(leaf => ({
209
+ key: leaf.key.replace(prefix, ''),
210
+ cid: leaf.value,
211
+ }));
212
+ }
213
+
214
+ /**
215
+ * Update the repo root to point to the new MST
216
+ */
217
+ async updateRoot(mst: MST, rev: number): Promise<void> {
218
+ const db = drizzle(this.env.DB);
219
+ const rootCid = await mst.getPointer();
220
+
221
+ await db
222
+ .insert(repo_root)
223
+ .values({
224
+ did: this.did,
225
+ commitCid: rootCid.toString(),
226
+ rev,
227
+ })
228
+ .onConflictDoUpdate({
229
+ target: repo_root.did,
230
+ set: {
231
+ commitCid: rootCid.toString(),
232
+ rev,
233
+ },
234
+ })
235
+ .run();
236
+ }
237
+
238
+ /**
239
+ * Extract operations from MST diff between two commits
240
+ * Compares old MST root with new MST root to identify create/update/delete operations
241
+ */
242
+ async extractOps(prevRoot: CID | null, newRoot: CID): Promise<RepoOp[]> {
243
+ const ops: RepoOp[] = [];
244
+
245
+ // Load both trees
246
+ const newMst = await MST.load(this.blockstore, newRoot).getEntries();
247
+ const prevMst = prevRoot ? await MST.load(this.blockstore, prevRoot).getEntries() : [];
248
+
249
+ // Build maps for efficient lookup
250
+ const prevMap = new Map<string, CID>();
251
+ const newMap = new Map<string, CID>();
252
+
253
+ // Collect all leaves from previous tree
254
+ await this.collectLeaves(prevMst, prevMap);
255
+
256
+ // Collect all leaves from new tree
257
+ await this.collectLeaves(newMst, newMap);
258
+
259
+ // Find creates and updates
260
+ for (const [path, cid] of Array.from(newMap.entries())) {
261
+ const prevCid = prevMap.get(path);
262
+ if (!prevCid) {
263
+ // New key - create operation
264
+ ops.push({
265
+ action: 'create',
266
+ path,
267
+ cid,
268
+ });
269
+ } else if (!prevCid.equals(cid)) {
270
+ // Key exists but CID changed - update operation
271
+ ops.push({
272
+ action: 'update',
273
+ path,
274
+ cid,
275
+ prev: prevCid,
276
+ });
277
+ }
278
+ }
279
+
280
+ // Find deletes
281
+ for (const [path, prevCid] of Array.from(prevMap.entries())) {
282
+ if (!newMap.has(path)) {
283
+ // Key no longer exists - delete operation
284
+ ops.push({
285
+ action: 'delete',
286
+ path,
287
+ cid: null,
288
+ prev: prevCid,
289
+ });
290
+ }
291
+ }
292
+
293
+ // Sort ops by path for deterministic ordering
294
+ ops.sort((a, b) => a.path.localeCompare(b.path));
295
+
296
+ return ops;
297
+ }
298
+
299
+ /**
300
+ * Recursively collect all leaves from MST entries into a map
301
+ */
302
+ private async collectLeaves(entries: (MST | Leaf)[], map: Map<string, CID>): Promise<void> {
303
+ for (const entry of entries) {
304
+ if (entry.isLeaf()) {
305
+ map.set(entry.key, entry.value);
306
+ } else {
307
+ // Recursively collect from subtree
308
+ const subEntries = await entry.getEntries();
309
+ await this.collectLeaves(subEntries, map);
310
+ }
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Store a record in the blockstore and return its CID
316
+ */
317
+ private async storeRecord(record: unknown): Promise<CID> {
318
+ const bytes = dagCbor.encode(record);
319
+ const cid = await cidForCbor(record);
320
+ await this.blockstore.put(cid, bytes);
321
+ return cid;
322
+ }
323
+
324
+ /**
325
+ * Store all blocks from an MST to the blockstore
326
+ */
327
+ private async storeMstBlocks(mst: MST): Promise<void> {
328
+ const { cid, bytes } = await mst.serialize();
329
+ await this.blockstore.put(cid, bytes);
330
+
331
+ // Recursively store child blocks
332
+ const entries = await mst.getEntries();
333
+ for (const entry of entries) {
334
+ if (entry.isTree()) {
335
+ await this.storeMstBlocks(entry);
336
+ }
337
+ }
338
+ }
339
+ }
@@ -0,0 +1,4 @@
1
+ declare module 'astro/internal/handler' {
2
+ export function handle(...args: any[]): Promise<Response>;
3
+ }
4
+