@alteran/astro 0.7.7 → 0.8.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 (73) hide show
  1. package/README.md +25 -25
  2. package/migrations/0010_eminent_klaw.sql +37 -0
  3. package/migrations/0011_chief_darwin.sql +31 -0
  4. package/migrations/0012_backfill_blob_usage.sql +39 -0
  5. package/migrations/meta/0010_snapshot.json +790 -0
  6. package/migrations/meta/0011_snapshot.json +813 -0
  7. package/migrations/meta/_journal.json +22 -1
  8. package/package.json +24 -41
  9. package/src/db/blob.ts +323 -0
  10. package/src/db/dal.ts +224 -78
  11. package/src/db/repo.ts +205 -25
  12. package/src/db/schema.ts +14 -5
  13. package/src/handlers/debug.ts +4 -3
  14. package/src/lib/appview/auth-policy.ts +7 -24
  15. package/src/lib/appview/proxy.ts +56 -23
  16. package/src/lib/appview/types.ts +1 -6
  17. package/src/lib/auth-scope.ts +399 -0
  18. package/src/lib/auth.ts +40 -39
  19. package/src/lib/commit.ts +37 -15
  20. package/src/lib/did-document.ts +4 -5
  21. package/src/lib/jwt.ts +3 -1
  22. package/src/lib/mime.ts +9 -0
  23. package/src/lib/oauth/resource.ts +49 -0
  24. package/src/lib/preference-policy.ts +45 -0
  25. package/src/lib/preferences.ts +0 -4
  26. package/src/lib/public-host.ts +127 -0
  27. package/src/lib/ratelimit.ts +37 -12
  28. package/src/lib/relay.ts +7 -27
  29. package/src/lib/repo-write-blob-constraints.ts +141 -0
  30. package/src/lib/repo-write-data.ts +195 -0
  31. package/src/lib/repo-write-error.ts +46 -0
  32. package/src/lib/repo-write-validation.ts +463 -0
  33. package/src/lib/session-tokens.ts +22 -5
  34. package/src/lib/unsupported-routes.ts +32 -0
  35. package/src/lib/util.ts +57 -2
  36. package/src/pages/.well-known/atproto-did.ts +15 -3
  37. package/src/pages/.well-known/did.json.ts +13 -7
  38. package/src/pages/debug/db/bootstrap.ts +4 -3
  39. package/src/pages/debug/gc/blobs.ts +11 -8
  40. package/src/pages/debug/record.ts +11 -0
  41. package/src/pages/xrpc/[...nsid].ts +17 -9
  42. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +9 -3
  43. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +17 -4
  44. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +4 -2
  45. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +4 -2
  46. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +4 -2
  47. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +10 -6
  48. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +4 -3
  49. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +13 -5
  50. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +4 -2
  51. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +4 -2
  52. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +12 -36
  53. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +90 -139
  54. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +74 -47
  55. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +119 -46
  56. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +21 -20
  57. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +6 -1
  58. package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +4 -2
  59. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +84 -47
  60. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +199 -78
  61. package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +4 -2
  62. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +88 -21
  63. package/src/pages/xrpc/com.atproto.server.getSession.ts +3 -13
  64. package/src/pages/xrpc/com.atproto.sync.getBlob.ts +92 -74
  65. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +45 -23
  66. package/src/services/car.ts +13 -0
  67. package/src/services/repo/apply-prepared-writes.ts +185 -0
  68. package/src/services/repo/blob-refs.ts +48 -0
  69. package/src/services/repo/blockstore-ops.ts +59 -17
  70. package/src/services/repo/list-blobs.ts +43 -0
  71. package/src/services/repo-manager.ts +221 -78
  72. package/src/worker/runtime.ts +1 -1
  73. package/src/worker/sequencer/upgrade.ts +4 -1
@@ -0,0 +1,185 @@
1
+ import type { CID } from 'multiformats/cid';
2
+ import type { Env } from '../../env';
3
+ import { bumpRoot } from '../../db/repo';
4
+ import {
5
+ deleteRecordStatements,
6
+ getRecordBlobKeys,
7
+ putRecordStatements,
8
+ setRecordBlobUsageStatements,
9
+ type BlobKeyRef,
10
+ } from '../../db/dal';
11
+ import type { RepoOp } from '../../lib/firehose/frames';
12
+ import { RepoWriteError } from '../../lib/repo-write-error';
13
+ import type { MST } from '../../lib/mst';
14
+ import type { PreparedWrite, ValidationStatus } from '../../lib/repo-write-validation';
15
+ import {
16
+ collectUnstoredMstBlocks,
17
+ encodeRecordBlock,
18
+ type EncodedBlock,
19
+ } from './blockstore-ops';
20
+ import { assertBlobKeysAvailable } from './blob-refs';
21
+
22
+ export interface BatchCommitResult {
23
+ commit: {
24
+ cid: string;
25
+ rev: string;
26
+ } | null;
27
+ commitCid?: string;
28
+ rev?: string;
29
+ ops: RepoOp[];
30
+ commitData?: string;
31
+ sig?: string;
32
+ blocks?: string;
33
+ results: Array<{
34
+ $type: string;
35
+ uri?: string;
36
+ cid?: string;
37
+ validationStatus?: ValidationStatus;
38
+ }>;
39
+ dereferencedBlobKeys: BlobKeyRef[];
40
+ }
41
+
42
+ type SideEffect =
43
+ | { action: 'put'; uri: string; cid: string; record: unknown; blobKeys: string[] }
44
+ | { action: 'delete'; uri: string };
45
+
46
+ export async function applyPreparedWritesToRepo(
47
+ env: Env,
48
+ did: string,
49
+ root: MST,
50
+ writes: PreparedWrite[],
51
+ expectedCommitCid?: string | null,
52
+ ): Promise<BatchCommitResult> {
53
+ let mst = root;
54
+ const prevMstRoot = await mst.getPointer();
55
+ const ops: RepoOp[] = [];
56
+ const sideEffects: SideEffect[] = [];
57
+ const results: BatchCommitResult['results'] = [];
58
+ const recordBlocks: EncodedBlock[] = [];
59
+
60
+ for (const write of writes) {
61
+ const path = `${write.collection}/${write.rkey}`;
62
+ const uri = `at://${did}/${path}`;
63
+ if (write.action === 'delete') {
64
+ const prev = await mst.get(path);
65
+ if (!prev) throw new RepoWriteError('InvalidRequest', 'record does not exist');
66
+ mst = await mst.delete(path);
67
+ ops.push({ action: 'delete', path, cid: null, prev });
68
+ sideEffects.push({ action: 'delete', uri });
69
+ results.push({ $type: 'com.atproto.repo.applyWrites#deleteResult' });
70
+ continue;
71
+ }
72
+
73
+ const prev = await mst.get(path);
74
+ if (write.action === 'update') {
75
+ if (!prev) throw new RepoWriteError('InvalidRequest', 'record does not exist');
76
+ const recordBlock = await encodeRecordBlock(write.record);
77
+ const [recordCid] = recordBlock;
78
+ recordBlocks.push(recordBlock);
79
+ mst = await mst.update(path, recordCid);
80
+ ops.push({ action: 'update', path, cid: recordCid, prev });
81
+ addPutEffect(sideEffects, results, uri, recordCid, write);
82
+ continue;
83
+ }
84
+
85
+ if (prev) throw new RepoWriteError('InvalidRequest', 'record already exists');
86
+ const recordBlock = await encodeRecordBlock(write.record);
87
+ const [recordCid] = recordBlock;
88
+ recordBlocks.push(recordBlock);
89
+ mst = await mst.add(path, recordCid);
90
+ ops.push({ action: 'create', path, cid: recordCid });
91
+ addPutEffect(sideEffects, results, uri, recordCid, write);
92
+ }
93
+
94
+ const dereferencedBlobKeys = await findDereferencedBlobKeys(env, did, sideEffects);
95
+ const currentRoot = await mst.getPointer();
96
+ const newMstBlocks = await collectUnstoredMstBlocks(mst);
97
+ const requiredBlobKeys = sideEffects.flatMap((effect) =>
98
+ effect.action === 'put' ? effect.blobKeys : [],
99
+ );
100
+ await assertBlobKeysAvailable(env, did, requiredBlobKeys);
101
+
102
+ const { commitCid, rev, commitData, sig, blocks } = await bumpRoot(
103
+ env,
104
+ prevMstRoot ?? undefined,
105
+ currentRoot,
106
+ {
107
+ ops,
108
+ newMstBlocks: Array.from(newMstBlocks),
109
+ newRecordBlocks: recordBlocks,
110
+ expectedCommitCid,
111
+ requiredBlobKeys,
112
+ sideEffectStatements: (guard) => sideEffects.flatMap((effect) => {
113
+ if (effect.action === 'delete') {
114
+ return [
115
+ ...deleteRecordStatements(env, effect.uri, guard),
116
+ ...setRecordBlobUsageStatements(env, did, effect.uri, [], guard),
117
+ ];
118
+ }
119
+ return [
120
+ ...putRecordStatements(env, {
121
+ uri: effect.uri,
122
+ did,
123
+ cid: effect.cid,
124
+ json: JSON.stringify(effect.record),
125
+ }, guard),
126
+ ...setRecordBlobUsageStatements(env, did, effect.uri, effect.blobKeys, guard),
127
+ ];
128
+ }),
129
+ },
130
+ );
131
+
132
+ return {
133
+ commit: { cid: commitCid, rev },
134
+ commitCid,
135
+ rev,
136
+ ops,
137
+ commitData,
138
+ sig,
139
+ blocks,
140
+ results,
141
+ dereferencedBlobKeys: dereferencedBlobKeys.map((key) => ({ did, key })),
142
+ };
143
+ }
144
+
145
+ function addPutEffect(
146
+ sideEffects: SideEffect[],
147
+ results: BatchCommitResult['results'],
148
+ uri: string,
149
+ recordCid: CID,
150
+ write: Exclude<PreparedWrite, { action: 'delete' }>,
151
+ ): void {
152
+ const cid = recordCid.toString();
153
+ sideEffects.push({
154
+ action: 'put',
155
+ uri,
156
+ cid,
157
+ record: write.record,
158
+ blobKeys: write.blobKeys,
159
+ });
160
+ results.push({
161
+ $type: `com.atproto.repo.applyWrites#${write.action}Result`,
162
+ uri,
163
+ cid,
164
+ validationStatus: write.validationStatus,
165
+ });
166
+ }
167
+
168
+ async function findDereferencedBlobKeys(
169
+ env: Env,
170
+ did: string,
171
+ sideEffects: SideEffect[],
172
+ ): Promise<string[]> {
173
+ const previousBlobKeysByUri = new Map<string, string[]>();
174
+ const finalBlobKeysByUri = new Map<string, string[]>();
175
+ for (const effect of sideEffects) {
176
+ if (!previousBlobKeysByUri.has(effect.uri)) {
177
+ previousBlobKeysByUri.set(effect.uri, await getRecordBlobKeys(env, did, effect.uri));
178
+ }
179
+ finalBlobKeysByUri.set(effect.uri, effect.action === 'put' ? effect.blobKeys : []);
180
+ }
181
+ return Array.from(previousBlobKeysByUri).flatMap(([uri, previousKeys]) => {
182
+ const finalKeys = finalBlobKeysByUri.get(uri) ?? [];
183
+ return previousKeys.filter((key) => !finalKeys.includes(key));
184
+ });
185
+ }
@@ -0,0 +1,48 @@
1
+ import type { Env } from '../../env';
2
+ import { RepoWriteError } from '../../lib/repo-write-error';
3
+ import { collectBlobRefs } from '../../lib/repo-write-data';
4
+
5
+ export async function assertBlobKeysAvailable(env: Env, did: string, keys: string[]): Promise<void> {
6
+ for (const key of new Set(keys)) {
7
+ const row = await env.ALTERAN_DB.prepare(
8
+ 'SELECT cid, size FROM blob WHERE did = ? AND key = ? LIMIT 1',
9
+ )
10
+ .bind(did, key)
11
+ .first<{ cid: string; size: number }>();
12
+ if (!row) {
13
+ throw new RepoWriteError('BlobNotFound', 'blob not found');
14
+ }
15
+ const object = await env.ALTERAN_BLOBS.head(key);
16
+ if (!object || object.size !== Number(row.size)) {
17
+ throw new RepoWriteError('BlobNotFound', `blob not found: ${row.cid}`);
18
+ }
19
+ }
20
+ }
21
+
22
+ export async function resolveRecordBlobKeys(env: Env, did: string, record: unknown): Promise<string[]> {
23
+ const refs: Array<{ cid: string; mimeType: string; size: number }> = [];
24
+ collectBlobRefs(record, refs);
25
+ const keys = new Set<string>();
26
+ for (const ref of refs) {
27
+ const row = await env.ALTERAN_DB.prepare(
28
+ 'SELECT key, mime, size FROM blob WHERE did = ? AND cid = ? LIMIT 1',
29
+ )
30
+ .bind(did, ref.cid)
31
+ .first<{ key: string; mime: string; size: number }>();
32
+ if (!row) {
33
+ throw new RepoWriteError('BlobNotFound', `blob not found: ${ref.cid}`);
34
+ }
35
+ if (row.mime !== ref.mimeType) {
36
+ throw new RepoWriteError('InvalidMimeType', `blob mime type mismatch: ${ref.cid}`);
37
+ }
38
+ if (Number(row.size) !== ref.size) {
39
+ throw new RepoWriteError('InvalidSize', `blob size mismatch: ${ref.cid}`);
40
+ }
41
+ const object = await env.ALTERAN_BLOBS.head(row.key);
42
+ if (!object || object.size !== Number(row.size)) {
43
+ throw new RepoWriteError('BlobNotFound', `blob not found: ${ref.cid}`);
44
+ }
45
+ keys.add(row.key);
46
+ }
47
+ return Array.from(keys);
48
+ }
@@ -1,29 +1,71 @@
1
1
  import { CID } from 'multiformats/cid';
2
2
  import * as dagCbor from '@ipld/dag-cbor';
3
+ import { fromString as bytesFromString } from 'uint8arrays/from-string';
3
4
  import { cidForCbor } from '../../lib/mst/util';
4
- import type { D1Blockstore, MST, BlockMap } from '../../lib/mst';
5
+ import type { MST, BlockMap } from '../../lib/mst';
6
+ import { RepoWriteError } from '../../lib/repo-write-error';
5
7
 
6
- export async function storeRecord(
7
- blockstore: D1Blockstore,
8
+ export type EncodedBlock = readonly [CID, Uint8Array];
9
+
10
+ export function recordToIpld(record: unknown, depth = 0): unknown {
11
+ if (depth > 100) {
12
+ throw new RepoWriteError('InvalidRequest', 'record is too deeply nested');
13
+ }
14
+ if (Array.isArray(record)) {
15
+ return record.map((item) => recordToIpld(item, depth + 1));
16
+ }
17
+ if (!record || typeof record !== 'object') {
18
+ return record;
19
+ }
20
+ if (record instanceof Uint8Array || CID.asCID(record)) {
21
+ return record;
22
+ }
23
+
24
+ const obj = record as Record<string, unknown>;
25
+ const keys = Object.keys(obj);
26
+ if (keys.length === 1 && typeof obj.$link === 'string') {
27
+ return CID.parse(obj.$link);
28
+ }
29
+ if (keys.length === 1 && typeof obj.$bytes === 'string') {
30
+ return bytesFromString(obj.$bytes, 'base64');
31
+ }
32
+
33
+ const converted: Record<string, unknown> = Object.create(null);
34
+ for (const [key, value] of Object.entries(obj)) {
35
+ if (key === '__proto__') {
36
+ throw new RepoWriteError('InvalidRequest', 'record contains a forbidden object key');
37
+ }
38
+ converted[key] = recordToIpld(value, depth + 1);
39
+ }
40
+ return converted;
41
+ }
42
+
43
+ export async function cidForRecord(record: unknown): Promise<CID> {
44
+ try {
45
+ return cidForCbor(recordToIpld(record));
46
+ } catch (error) {
47
+ if (error instanceof RepoWriteError) throw error;
48
+ throw new RepoWriteError('InvalidRequest', 'record is not dag-cbor encodable');
49
+ }
50
+ }
51
+
52
+ export async function encodeRecordBlock(
8
53
  record: unknown,
9
- ): Promise<CID> {
10
- const bytes = dagCbor.encode(record);
11
- const cid = await cidForCbor(record);
12
- await blockstore.put(cid, bytes);
13
- return cid;
54
+ ): Promise<EncodedBlock> {
55
+ try {
56
+ const ipldRecord = recordToIpld(record);
57
+ const bytes = dagCbor.encode(ipldRecord);
58
+ const cid = await cidForCbor(ipldRecord);
59
+ return [cid, bytes] as const;
60
+ } catch (error) {
61
+ if (error instanceof RepoWriteError) throw error;
62
+ throw new RepoWriteError('InvalidRequest', 'record is not dag-cbor encodable');
63
+ }
14
64
  }
15
65
 
16
- export async function storeMstBlocks(
17
- blockstore: D1Blockstore,
66
+ export async function collectUnstoredMstBlocks(
18
67
  mst: MST,
19
68
  ): Promise<BlockMap> {
20
69
  const diff = await mst.getUnstoredBlocks();
21
- for (const [cid, bytes] of diff.blocks) {
22
- console.log(
23
- `[RepoManager] Storing new MST block: ${cid.toString()}, size: ${bytes.length}`,
24
- );
25
- await blockstore.put(cid, bytes);
26
- }
27
- console.log(`[RepoManager] Stored ${diff.blocks.size} new MST blocks`);
28
70
  return diff.blocks;
29
71
  }
@@ -0,0 +1,43 @@
1
+ import type { Env } from '../../env';
2
+
3
+ type ListBlobCidsParams = {
4
+ did: string;
5
+ since?: string;
6
+ cursor?: string;
7
+ limit: number;
8
+ };
9
+
10
+ export async function listBlobCids(
11
+ env: Env,
12
+ { did, since, cursor, limit }: ListBlobCidsParams,
13
+ ): Promise<{ cids: string[]; cursor?: string }> {
14
+ const pageSize = limit + 1;
15
+ const clauses = ['did = ?'];
16
+ const values: Array<string | number> = [did];
17
+ if (since) {
18
+ clauses.push('repo_rev > ?');
19
+ values.push(since);
20
+ }
21
+ if (cursor) {
22
+ clauses.push('cid > ?');
23
+ values.push(cursor);
24
+ }
25
+ values.push(pageSize);
26
+
27
+ const result = await env.ALTERAN_DB.prepare(
28
+ `SELECT DISTINCT cid
29
+ FROM blob_usage
30
+ WHERE ${clauses.join(' AND ')}
31
+ ORDER BY cid
32
+ LIMIT ?`,
33
+ )
34
+ .bind(...values)
35
+ .all<{ cid: string }>();
36
+
37
+ const rows = result.results ?? [];
38
+ const page = rows.slice(0, limit).map((row) => row.cid);
39
+ return {
40
+ cids: page,
41
+ ...(page.length > 0 ? { cursor: page[page.length - 1] } : {}),
42
+ };
43
+ }