@alteran/astro 0.7.6 → 0.8.1

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 (75) 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/observability.ts +53 -12
  24. package/src/lib/oauth/resource.ts +49 -0
  25. package/src/lib/preference-policy.ts +45 -0
  26. package/src/lib/preferences.ts +0 -4
  27. package/src/lib/public-host.ts +127 -0
  28. package/src/lib/ratelimit.ts +37 -12
  29. package/src/lib/relay.ts +7 -27
  30. package/src/lib/repo-write-blob-constraints.ts +141 -0
  31. package/src/lib/repo-write-data.ts +195 -0
  32. package/src/lib/repo-write-error.ts +46 -0
  33. package/src/lib/repo-write-validation.ts +463 -0
  34. package/src/lib/session-tokens.ts +22 -5
  35. package/src/lib/unsupported-routes.ts +32 -0
  36. package/src/lib/util.ts +57 -2
  37. package/src/pages/.well-known/atproto-did.ts +15 -3
  38. package/src/pages/.well-known/did.json.ts +13 -7
  39. package/src/pages/debug/db/bootstrap.ts +4 -3
  40. package/src/pages/debug/gc/blobs.ts +11 -8
  41. package/src/pages/debug/record.ts +11 -0
  42. package/src/pages/oauth/token.ts +78 -33
  43. package/src/pages/xrpc/[...nsid].ts +17 -9
  44. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +9 -3
  45. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +17 -4
  46. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +4 -2
  47. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +4 -2
  48. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +4 -2
  49. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +10 -6
  50. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +4 -3
  51. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +13 -5
  52. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +4 -2
  53. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +4 -2
  54. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +12 -36
  55. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +90 -139
  56. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +74 -47
  57. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +119 -46
  58. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +21 -20
  59. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +6 -1
  60. package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +4 -2
  61. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +84 -47
  62. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +199 -78
  63. package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +4 -2
  64. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +88 -21
  65. package/src/pages/xrpc/com.atproto.server.getSession.ts +3 -13
  66. package/src/pages/xrpc/com.atproto.sync.getBlob.ts +92 -74
  67. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +45 -23
  68. package/src/services/car.ts +13 -0
  69. package/src/services/repo/apply-prepared-writes.ts +185 -0
  70. package/src/services/repo/blob-refs.ts +48 -0
  71. package/src/services/repo/blockstore-ops.ts +59 -17
  72. package/src/services/repo/list-blobs.ts +43 -0
  73. package/src/services/repo-manager.ts +221 -78
  74. package/src/worker/runtime.ts +1 -1
  75. package/src/worker/sequencer/upgrade.ts +4 -1
@@ -1,72 +1,145 @@
1
1
  import type { APIContext } from 'astro';
2
- import { errorCode, errorMessage } from '../../lib/errors';
3
- import { verifyResourceRequestHybrid, dpopResourceUnauthorized, handleResourceAuthError } from '../../lib/oauth/resource';
2
+ import { errorCode } from '../../lib/errors';
3
+ import { verifyResourceRequestHybrid, dpopResourceUnauthorized, handleResourceAuthError, insufficientScopeResponse } from '../../lib/oauth/resource';
4
+ import { canWriteRepo } from '../../lib/auth-scope';
4
5
  import { checkRate } from '../../lib/ratelimit';
5
6
  import { readJsonBounded } from '../../lib/util';
6
- import { RepoManager } from '../../services/repo-manager';
7
- import { bumpRoot } from '../../db/repo';
7
+ import { assertRepoHead, bumpRoot } from '../../db/repo';
8
8
  import { notifySequencer } from '../../lib/sequencer';
9
+ import {
10
+ deleteRecordStatements,
11
+ deleteUnreferencedBlobKeys,
12
+ getRecordBlobKeys,
13
+ isAccountActive,
14
+ setRecordBlobUsageStatements,
15
+ sweepEligibleUnreferencedBlobKeys,
16
+ } from '../../db/dal';
17
+ import {
18
+ assertRepoWriteInput,
19
+ deleteRecordAuthorizations,
20
+ handleRepoWriteError,
21
+ jsonError,
22
+ prepareDeleteRecord,
23
+ RepoWriteError,
24
+ retryNoSwapCommit,
25
+ } from '../../lib/repo-write-validation';
9
26
 
10
27
  export const prerender = false;
11
28
 
12
29
  export async function POST({ locals, request }: APIContext) {
13
- const { env } = locals.runtime;
30
+ const { env, ctx } = locals.runtime;
31
+ let auth: NonNullable<Awaited<ReturnType<typeof verifyResourceRequestHybrid>>>;
14
32
  try {
15
- const auth = await verifyResourceRequestHybrid(env, request);
16
- if (!auth) return dpopResourceUnauthorized(env);
33
+ const verified = await verifyResourceRequestHybrid(env, request);
34
+ if (!verified) return dpopResourceUnauthorized(env);
35
+ auth = verified;
17
36
  } catch (error) {
18
37
  const handled = await handleResourceAuthError(env, error);
19
38
  if (handled) return handled;
20
39
  throw error;
21
40
  }
22
41
 
23
- const rateLimitResponse = await checkRate(env, request, 'writes');
24
- if (rateLimitResponse) return rateLimitResponse;
25
-
26
- let body: any;
42
+ let body: unknown;
27
43
  try {
28
44
  body = await readJsonBounded(env, request);
29
- } catch (e) {
30
- if (errorCode(e) === 'PayloadTooLarge') {
31
- return new Response(JSON.stringify({ error: 'PayloadTooLarge' }), { status: 413 });
45
+ } catch (error) {
46
+ const rateLimitResponse = await checkRate(env, request, 'writes', { key: auth.did });
47
+ if (rateLimitResponse) return rateLimitResponse;
48
+ if (errorCode(error) === 'PayloadTooLarge') {
49
+ return jsonError('PayloadTooLarge', undefined, 413);
32
50
  }
33
- return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
51
+ return jsonError('BadRequest');
34
52
  }
35
- const { collection, rkey } = body ?? {};
36
- if (!collection || !rkey) return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
37
53
 
38
- const repo = new RepoManager(env);
39
- // Perform the delete in the MST, gather prev/new roots & new blocks
40
- const { mst, prevMstRoot, uri, newMstBlocks } = await repo.deleteRecord(collection, rkey);
54
+ let writeRateCharged = false;
55
+ try {
56
+ const input = assertRepoWriteInput('com.atproto.repo.deleteRecord', body);
57
+ for (const write of deleteRecordAuthorizations(input)) {
58
+ if (!canWriteRepo(auth.access, write.collection, write.action)) return insufficientScopeResponse();
59
+ }
60
+
61
+ const rateLimitResponse = await checkRate(env, request, 'writes', { key: auth.did });
62
+ writeRateCharged = true;
63
+ if (rateLimitResponse) return rateLimitResponse;
41
64
 
42
- // Build ops & bump the repo root to create a signed commit
43
- const currentRoot = await mst.getPointer();
44
- const opsForCommit = [{ action: 'delete' as const, path: `${collection}/${rkey}`, cid: null }];
45
- const { commitCid, rev, commitData, sig, blocks } = await bumpRoot(env, prevMstRoot ?? undefined, currentRoot, {
46
- ops: opsForCommit,
47
- newMstBlocks: Array.from(newMstBlocks),
48
- });
65
+ if (!(await isAccountActive(env, auth.did))) {
66
+ return jsonError('AccountDeactivated', 'Account is deactivated. Activate it before making changes.', 403);
67
+ }
49
68
 
50
- // Notify sequencer with a complete payload matching handleCommitNotification
51
- await notifySequencer(env, {
52
- did: env.PDS_DID as string,
53
- commitCid,
54
- rev,
55
- data: commitData,
56
- sig,
57
- ops: opsForCommit,
58
- blocks,
59
- });
69
+ const committed = await retryNoSwapCommit(input, async () => {
70
+ const prepared = await prepareDeleteRecord(env, auth, input);
71
+ const { write, repo } = prepared;
72
+ if (!prepared.currentCid) {
73
+ await assertRepoHead(env, prepared.did, prepared.expectedCommitCid);
74
+ return { prepared, result: null };
75
+ }
60
76
 
61
- // Respond with official schema
62
- const out = {
63
- commit: {
64
- cid: commitCid,
77
+ const { mst, prevMstRoot, uri, newMstBlocks, currentCid } = await repo.deleteRecord(
78
+ write.collection,
79
+ write.rkey,
80
+ );
81
+ const previousBlobKeys = await getRecordBlobKeys(env, prepared.did, uri);
82
+ const currentRoot = await mst.getPointer();
83
+ const opsForCommit = [{
84
+ action: 'delete' as const,
85
+ path: `${write.collection}/${write.rkey}`,
86
+ cid: null,
87
+ prev: currentCid,
88
+ }];
89
+ const result = await bumpRoot(env, prevMstRoot ?? undefined, currentRoot, {
90
+ ops: opsForCommit,
91
+ newMstBlocks: Array.from(newMstBlocks),
92
+ expectedCommitCid: prepared.expectedCommitCid,
93
+ sideEffectStatements: (guard) => [
94
+ ...deleteRecordStatements(env, uri, guard),
95
+ ...setRecordBlobUsageStatements(env, prepared.did, uri, [], guard),
96
+ ],
97
+ });
98
+ return {
99
+ prepared,
100
+ result: {
101
+ ...result,
102
+ opsForCommit,
103
+ dereferencedBlobKeys: previousBlobKeys.map((key) => ({ did: prepared.did, key })),
104
+ },
105
+ };
106
+ });
107
+ if (!committed.result) {
108
+ return new Response(JSON.stringify({}), {
109
+ headers: { 'Content-Type': 'application/json' },
110
+ });
111
+ }
112
+ const { prepared, result } = committed;
113
+ const { commitCid, rev, commitData, sig, blocks, opsForCommit } = result;
114
+ await notifySequencer(env, {
115
+ did: prepared.did,
116
+ commitCid,
65
117
  rev,
66
- },
67
- };
118
+ data: commitData,
119
+ sig,
120
+ ops: opsForCommit,
121
+ blocks,
122
+ });
123
+ await deleteUnreferencedBlobKeys(env, result.dereferencedBlobKeys).catch((error) => {
124
+ console.warn('[deleteRecord] Failed to clean dereferenced blobs:', error);
125
+ });
126
+ ctx?.waitUntil(sweepEligibleUnreferencedBlobKeys(env).catch((error) => {
127
+ console.warn('[deleteRecord] Failed to sweep dereferenced blobs:', error);
128
+ }));
68
129
 
69
- return new Response(JSON.stringify(out), {
70
- headers: { 'Content-Type': 'application/json' },
71
- });
130
+ return new Response(JSON.stringify({
131
+ commit: {
132
+ cid: commitCid,
133
+ rev,
134
+ },
135
+ }), {
136
+ headers: { 'Content-Type': 'application/json' },
137
+ });
138
+ } catch (error) {
139
+ if (!writeRateCharged && error instanceof RepoWriteError) {
140
+ const rateLimitResponse = await checkRate(env, request, 'writes', { key: auth.did });
141
+ if (rateLimitResponse) return rateLimitResponse;
142
+ }
143
+ return handleRepoWriteError(error);
144
+ }
72
145
  }
@@ -1,5 +1,11 @@
1
1
  import type { APIContext } from 'astro';
2
- import { getRoot } from '../../db/repo';
2
+ import { buildDidDocument } from '../../lib/did-document';
3
+ import {
4
+ configuredDid,
5
+ configuredHandle,
6
+ didDocClaimsHandle,
7
+ handleResolvesToDid,
8
+ } from '../../lib/public-host';
3
9
 
4
10
  export const prerender = false;
5
11
 
@@ -10,30 +16,25 @@ export const prerender = false;
10
16
  export async function GET({ locals, url }: APIContext) {
11
17
  const { env } = locals.runtime;
12
18
 
13
- const repo = url.searchParams.get('repo') || (env.PDS_DID as string);
14
- const did = env.PDS_DID as string;
15
- const handle = env.PDS_HANDLE || 'user.example.com';
19
+ const repo = url.searchParams.get('repo');
20
+ const did = await configuredDid(env);
21
+ const handle = await configuredHandle(env);
22
+ if (repo && repo !== did && repo.toLowerCase() !== handle.toLowerCase()) {
23
+ return new Response(
24
+ JSON.stringify({ error: 'NotFound', message: 'Repo not found' }),
25
+ { status: 404, headers: { 'Content-Type': 'application/json' } },
26
+ );
27
+ }
16
28
 
17
- // Get repo root to check if repo exists
18
- const root = await getRoot(env);
29
+ const didDoc = await buildDidDocument(env, did, handle);
30
+ const handleIsCorrect = didDocClaimsHandle(didDoc, handle) &&
31
+ await handleResolvesToDid(env, handle, did);
19
32
 
20
33
  return new Response(
21
34
  JSON.stringify({
22
35
  did,
23
36
  handle,
24
- didDoc: {
25
- '@context': ['https://www.w3.org/ns/did/v1'],
26
- id: did,
27
- alsoKnownAs: [`at://${handle}`],
28
- verificationMethod: [],
29
- service: [
30
- {
31
- id: '#atproto_pds',
32
- type: 'AtprotoPersonalDataServer',
33
- serviceEndpoint: `https://${handle}`,
34
- },
35
- ],
36
- },
37
+ didDoc,
37
38
  collections: [
38
39
  'app.bsky.feed.post',
39
40
  'app.bsky.feed.like',
@@ -41,7 +42,7 @@ export async function GET({ locals, url }: APIContext) {
41
42
  'app.bsky.graph.follow',
42
43
  'app.bsky.actor.profile',
43
44
  ],
44
- handleIsCorrect: true,
45
+ handleIsCorrect,
45
46
  }),
46
47
  {
47
48
  status: 200,
@@ -30,7 +30,12 @@ export async function GET({ locals, request }: APIContext) {
30
30
  }
31
31
 
32
32
  const row = await dalGetRecord(env, uri);
33
- if (!row) return new Response(JSON.stringify({ error: 'NotFound' }), { status: 404 });
33
+ if (!row) {
34
+ return new Response(
35
+ JSON.stringify({ error: 'RecordNotFound', message: `Could not locate record: ${uri}` }),
36
+ { status: 400, headers: { 'Content-Type': 'application/json' } },
37
+ );
38
+ }
34
39
 
35
40
  return new Response(JSON.stringify({ uri: row.uri, cid: row.cid, value: JSON.parse(row.json) }), {
36
41
  headers: { 'Content-Type': 'application/json' },
@@ -1,6 +1,7 @@
1
1
  import type { APIContext } from 'astro';
2
2
  import { errorMessage } from '../../lib/errors';
3
- import { authErrorResponse, isAuthorized, unauthorized } from '../../lib/auth';
3
+ import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
4
+ import { canUseAppPasswordLevelAccess } from '../../lib/auth-scope';
4
5
  import { getDb } from '../../db/client';
5
6
  import { record, blob_ref } from '../../db/schema';
6
7
  import { eq } from 'drizzle-orm';
@@ -18,7 +19,8 @@ export async function GET({ locals, request, url }: APIContext) {
18
19
  const { env } = locals.runtime;
19
20
 
20
21
  try {
21
- if (!(await isAuthorized(request, env))) return unauthorized();
22
+ const auth = await authenticateRequest(request, env);
23
+ if (!auth || !canUseAppPasswordLevelAccess(auth.access)) return unauthorized();
22
24
  } catch (error) {
23
25
  const handled = await authErrorResponse(env, error);
24
26
  if (handled) return handled;
@@ -1,72 +1,109 @@
1
1
  import type { APIContext } from 'astro';
2
- import { errorCode, errorMessage } from '../../lib/errors';
3
- import { verifyResourceRequestHybrid, dpopResourceUnauthorized, handleResourceAuthError } from '../../lib/oauth/resource';
2
+ import { errorCode } from '../../lib/errors';
3
+ import { verifyResourceRequestHybrid, dpopResourceUnauthorized, handleResourceAuthError, insufficientScopeResponse } from '../../lib/oauth/resource';
4
+ import { canWriteRepo } from '../../lib/auth-scope';
4
5
  import { checkRate } from '../../lib/ratelimit';
5
6
  import { readJsonBounded } from '../../lib/util';
6
- import { RepoManager } from '../../services/repo-manager';
7
7
  import { notifySequencer } from '../../lib/sequencer';
8
+ import { deleteUnreferencedBlobKeys, isAccountActive, sweepEligibleUnreferencedBlobKeys } from '../../db/dal';
9
+ import {
10
+ assertRepoWriteInput,
11
+ handleRepoWriteError,
12
+ jsonError,
13
+ preparePutRecord,
14
+ putRecordAuthorizations,
15
+ RepoWriteError,
16
+ retryNoSwapCommit,
17
+ } from '../../lib/repo-write-validation';
8
18
 
9
19
  export const prerender = false;
10
20
 
11
21
  export async function POST({ locals, request }: APIContext) {
12
- const { env } = locals.runtime;
22
+ const { env, ctx } = locals.runtime;
23
+ let auth: NonNullable<Awaited<ReturnType<typeof verifyResourceRequestHybrid>>>;
13
24
  try {
14
- const auth = await verifyResourceRequestHybrid(env, request);
15
- if (!auth) return dpopResourceUnauthorized(env);
25
+ const verified = await verifyResourceRequestHybrid(env, request);
26
+ if (!verified) return dpopResourceUnauthorized(env);
27
+ auth = verified;
16
28
  } catch (error) {
17
29
  const handled = await handleResourceAuthError(env, error);
18
30
  if (handled) return handled;
19
31
  throw error;
20
32
  }
21
33
 
22
- const rateLimitResponse = await checkRate(env, request, 'writes');
23
- if (rateLimitResponse) return rateLimitResponse;
24
-
25
- let body: any;
34
+ let body: unknown;
26
35
  try {
27
36
  body = await readJsonBounded(env, request);
28
- } catch (e) {
29
- if (errorCode(e) === 'PayloadTooLarge') {
30
- return new Response(JSON.stringify({ error: 'PayloadTooLarge' }), { status: 413 });
37
+ } catch (error) {
38
+ const rateLimitResponse = await checkRate(env, request, 'writes', { key: auth.did });
39
+ if (rateLimitResponse) return rateLimitResponse;
40
+ if (errorCode(error) === 'PayloadTooLarge') {
41
+ return jsonError('PayloadTooLarge', undefined, 413);
31
42
  }
32
- return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
43
+ return jsonError('BadRequest');
33
44
  }
34
- const { collection, rkey } = body ?? {};
35
- let { record } = body ?? {};
36
- if (!collection || !rkey || !record) return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
37
45
 
38
- if (collection === 'app.bsky.feed.post' && record && typeof record === 'object') {
39
- if (typeof record.text !== 'string') {
40
- record.text = '';
41
- }
42
- if (typeof record.createdAt !== 'string') {
43
- record.createdAt = new Date().toISOString();
46
+ let writeRateCharged = false;
47
+ try {
48
+ const input = assertRepoWriteInput('com.atproto.repo.putRecord', body);
49
+ for (const write of putRecordAuthorizations(input)) {
50
+ if (!canWriteRepo(auth.access, write.collection, write.action)) return insufficientScopeResponse();
44
51
  }
45
- }
46
52
 
47
- const repo = new RepoManager(env);
48
- const result = await repo.putRecord(collection, rkey, record);
49
- await notifySequencer(env, {
50
- did: env.PDS_DID as string,
51
- commitCid: result.commitCid,
52
- rev: result.rev,
53
- data: result.commitData,
54
- sig: result.sig,
55
- ops: result.ops,
56
- blocks: result.blocks
57
- });
53
+ const rateLimitResponse = await checkRate(env, request, 'writes', { key: auth.did });
54
+ writeRateCharged = true;
55
+ if (rateLimitResponse) return rateLimitResponse;
58
56
 
59
- const out = {
60
- uri: result.uri,
61
- cid: result.cid,
62
- commit: {
63
- cid: result.commitCid,
64
- rev: result.rev,
65
- },
66
- validationStatus: 'unknown' as const,
67
- };
57
+ if (!(await isAccountActive(env, auth.did))) {
58
+ return jsonError('AccountDeactivated', 'Account is deactivated. Activate it before making changes.', 403);
59
+ }
68
60
 
69
- return new Response(JSON.stringify(out), {
70
- headers: { 'Content-Type': 'application/json' },
71
- });
61
+ const { prepared, result } = await retryNoSwapCommit(input, async () => {
62
+ const prepared = await preparePutRecord(env, auth, input);
63
+ const { write, repo } = prepared;
64
+ const result = await repo.putRecord(
65
+ write.collection,
66
+ write.rkey,
67
+ write.record,
68
+ write.blobKeys,
69
+ prepared.expectedCommitCid,
70
+ );
71
+ return { prepared, result };
72
+ });
73
+ if (result.commitCid && result.rev && result.commitData && result.sig && result.blocks) {
74
+ await notifySequencer(env, {
75
+ did: prepared.did,
76
+ commitCid: result.commitCid,
77
+ rev: result.rev,
78
+ data: result.commitData,
79
+ sig: result.sig,
80
+ ops: result.ops,
81
+ blocks: result.blocks
82
+ });
83
+ await deleteUnreferencedBlobKeys(env, result.dereferencedBlobKeys).catch((error) => {
84
+ console.warn('[putRecord] Failed to clean dereferenced blobs:', error);
85
+ });
86
+ ctx?.waitUntil(sweepEligibleUnreferencedBlobKeys(env).catch((error) => {
87
+ console.warn('[putRecord] Failed to sweep dereferenced blobs:', error);
88
+ }));
89
+ }
90
+
91
+ return new Response(JSON.stringify({
92
+ uri: result.uri,
93
+ cid: result.cid,
94
+ ...(result.commitCid && result.rev ? { commit: {
95
+ cid: result.commitCid,
96
+ rev: result.rev,
97
+ } } : {}),
98
+ validationStatus: prepared.write.validationStatus,
99
+ }), {
100
+ headers: { 'Content-Type': 'application/json' },
101
+ });
102
+ } catch (error) {
103
+ if (!writeRateCharged && error instanceof RepoWriteError) {
104
+ const rateLimitResponse = await checkRate(env, request, 'writes', { key: auth.did });
105
+ if (rateLimitResponse) return rateLimitResponse;
106
+ }
107
+ return handleRepoWriteError(error);
108
+ }
72
109
  }