@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.
- package/README.md +25 -25
- package/migrations/0010_eminent_klaw.sql +37 -0
- package/migrations/0011_chief_darwin.sql +31 -0
- package/migrations/0012_backfill_blob_usage.sql +39 -0
- package/migrations/meta/0010_snapshot.json +790 -0
- package/migrations/meta/0011_snapshot.json +813 -0
- package/migrations/meta/_journal.json +22 -1
- package/package.json +24 -41
- package/src/db/blob.ts +323 -0
- package/src/db/dal.ts +224 -78
- package/src/db/repo.ts +205 -25
- package/src/db/schema.ts +14 -5
- package/src/handlers/debug.ts +4 -3
- package/src/lib/appview/auth-policy.ts +7 -24
- package/src/lib/appview/proxy.ts +56 -23
- package/src/lib/appview/types.ts +1 -6
- package/src/lib/auth-scope.ts +399 -0
- package/src/lib/auth.ts +40 -39
- package/src/lib/commit.ts +37 -15
- package/src/lib/did-document.ts +4 -5
- package/src/lib/jwt.ts +3 -1
- package/src/lib/mime.ts +9 -0
- package/src/lib/oauth/observability.ts +53 -12
- package/src/lib/oauth/resource.ts +49 -0
- package/src/lib/preference-policy.ts +45 -0
- package/src/lib/preferences.ts +0 -4
- package/src/lib/public-host.ts +127 -0
- package/src/lib/ratelimit.ts +37 -12
- package/src/lib/relay.ts +7 -27
- package/src/lib/repo-write-blob-constraints.ts +141 -0
- package/src/lib/repo-write-data.ts +195 -0
- package/src/lib/repo-write-error.ts +46 -0
- package/src/lib/repo-write-validation.ts +463 -0
- package/src/lib/session-tokens.ts +22 -5
- package/src/lib/unsupported-routes.ts +32 -0
- package/src/lib/util.ts +57 -2
- package/src/pages/.well-known/atproto-did.ts +15 -3
- package/src/pages/.well-known/did.json.ts +13 -7
- package/src/pages/debug/db/bootstrap.ts +4 -3
- package/src/pages/debug/gc/blobs.ts +11 -8
- package/src/pages/debug/record.ts +11 -0
- package/src/pages/oauth/token.ts +78 -33
- package/src/pages/xrpc/[...nsid].ts +17 -9
- package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +9 -3
- package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +17 -4
- package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +4 -2
- package/src/pages/xrpc/chat.bsky.convo.getLog.ts +4 -2
- package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +4 -2
- package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +10 -6
- package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +4 -3
- package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +13 -5
- package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +4 -2
- package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +4 -2
- package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +12 -36
- package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +90 -139
- package/src/pages/xrpc/com.atproto.repo.createRecord.ts +74 -47
- package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +119 -46
- package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +21 -20
- package/src/pages/xrpc/com.atproto.repo.getRecord.ts +6 -1
- package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +4 -2
- package/src/pages/xrpc/com.atproto.repo.putRecord.ts +84 -47
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +199 -78
- package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +4 -2
- package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +88 -21
- package/src/pages/xrpc/com.atproto.server.getSession.ts +3 -13
- package/src/pages/xrpc/com.atproto.sync.getBlob.ts +92 -74
- package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +45 -23
- package/src/services/car.ts +13 -0
- package/src/services/repo/apply-prepared-writes.ts +185 -0
- package/src/services/repo/blob-refs.ts +48 -0
- package/src/services/repo/blockstore-ops.ts +59 -17
- package/src/services/repo/list-blobs.ts +43 -0
- package/src/services/repo-manager.ts +221 -78
- package/src/worker/runtime.ts +1 -1
- package/src/worker/sequencer/upgrade.ts +4 -1
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { readJson } from '../../lib/util';
|
|
3
|
-
|
|
4
2
|
export const prerender = false;
|
|
5
3
|
|
|
6
4
|
/**
|
|
@@ -8,38 +6,16 @@ export const prerender = false;
|
|
|
8
6
|
* Update the handle for the repository
|
|
9
7
|
*/
|
|
10
8
|
export async function POST({ locals, request }: APIContext) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// TODO: Implement handle verification (DNS TXT or HTTP)
|
|
25
|
-
// TODO: Update PDS_HANDLE configuration
|
|
26
|
-
// For single-user PDS, this would require redeployment with new config
|
|
27
|
-
|
|
28
|
-
return new Response(
|
|
29
|
-
JSON.stringify({
|
|
30
|
-
error: 'NotImplemented',
|
|
31
|
-
message: 'Handle updates require PDS reconfiguration for single-user mode',
|
|
32
|
-
}),
|
|
33
|
-
{
|
|
34
|
-
status: 501,
|
|
35
|
-
headers: { 'Content-Type': 'application/json' },
|
|
36
|
-
}
|
|
37
|
-
);
|
|
38
|
-
} catch (error) {
|
|
39
|
-
console.error('updateHandle error:', error);
|
|
40
|
-
return new Response(
|
|
41
|
-
JSON.stringify({ error: 'InternalServerError', message: String(error) }),
|
|
42
|
-
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
43
|
-
);
|
|
44
|
-
}
|
|
9
|
+
void locals;
|
|
10
|
+
void request;
|
|
11
|
+
return new Response(
|
|
12
|
+
JSON.stringify({
|
|
13
|
+
error: 'NotImplemented',
|
|
14
|
+
message: 'Handle updates require PDS reconfiguration for single-user mode',
|
|
15
|
+
}),
|
|
16
|
+
{
|
|
17
|
+
status: 501,
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
},
|
|
20
|
+
);
|
|
45
21
|
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { isAccountActive } from '../../db/dal';
|
|
2
|
+
import { errorCode } from '../../lib/errors';
|
|
3
|
+
import { readJsonBounded } from '../../lib/util';
|
|
4
|
+
import { verifyResourceRequestHybrid, dpopResourceUnauthorized, handleResourceAuthError, insufficientScopeResponse } from '../../lib/oauth/resource';
|
|
5
|
+
import { canWriteRepo } from '../../lib/auth-scope';
|
|
6
|
+
import { deleteUnreferencedBlobKeys, isAccountActive, sweepEligibleUnreferencedBlobKeys } from '../../db/dal';
|
|
7
7
|
import { checkRate } from '../../lib/ratelimit';
|
|
8
8
|
import { notifySequencer } from '../../lib/sequencer';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import {
|
|
10
|
+
applyWritesAuthorizations,
|
|
11
|
+
assertRepoWriteInput,
|
|
12
|
+
handleRepoWriteError,
|
|
13
|
+
jsonError,
|
|
14
|
+
prepareApplyWrites,
|
|
15
|
+
RepoWriteError,
|
|
16
|
+
retryNoSwapCommit,
|
|
17
|
+
} from '../../lib/repo-write-validation';
|
|
12
18
|
|
|
13
19
|
export const prerender = false;
|
|
14
20
|
|
|
@@ -17,155 +23,92 @@ export const prerender = false;
|
|
|
17
23
|
* Apply a batch of repository writes atomically
|
|
18
24
|
*/
|
|
19
25
|
export async function POST({ locals, request }: APIContext) {
|
|
20
|
-
const { env } = locals.runtime;
|
|
26
|
+
const { env, ctx } = locals.runtime;
|
|
27
|
+
let auth: NonNullable<Awaited<ReturnType<typeof verifyResourceRequestHybrid>>>;
|
|
21
28
|
try {
|
|
22
|
-
const
|
|
23
|
-
if (!
|
|
29
|
+
const verified = await verifyResourceRequestHybrid(env, request);
|
|
30
|
+
if (!verified) return dpopResourceUnauthorized(env);
|
|
31
|
+
auth = verified;
|
|
24
32
|
} catch (error) {
|
|
25
33
|
const handled = await handleResourceAuthError(env, error);
|
|
26
34
|
if (handled) return handled;
|
|
27
35
|
throw error;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
);
|
|
38
|
+
let body: unknown;
|
|
39
|
+
try {
|
|
40
|
+
body = await readJsonBounded(env, request);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const rateLimitResponse = await checkRate(env, request, 'writes', { key: auth.did });
|
|
43
|
+
if (rateLimitResponse) return rateLimitResponse;
|
|
44
|
+
if (errorCode(error) === 'PayloadTooLarge') {
|
|
45
|
+
return jsonError('PayloadTooLarge', undefined, 413);
|
|
46
|
+
}
|
|
47
|
+
return jsonError('BadRequest');
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
if (rateLimitResponse) return rateLimitResponse;
|
|
45
|
-
|
|
50
|
+
let writeRateCharged = false;
|
|
46
51
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return
|
|
57
|
-
JSON.stringify({ error: 'InvalidRequest', message: 'writes must be an array' }),
|
|
58
|
-
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
59
|
-
);
|
|
52
|
+
const input = assertRepoWriteInput('com.atproto.repo.applyWrites', body);
|
|
53
|
+
const writes = Array.isArray(input.writes) ? input.writes : [];
|
|
54
|
+
if (writes.length > 200) {
|
|
55
|
+
const rateLimitResponse = await checkRate(env, request, 'writes', { key: auth.did });
|
|
56
|
+
writeRateCharged = true;
|
|
57
|
+
if (rateLimitResponse) return rateLimitResponse;
|
|
58
|
+
return jsonError('InvalidRequest', 'Too many writes. Max: 200');
|
|
59
|
+
}
|
|
60
|
+
for (const write of applyWritesAuthorizations(input)) {
|
|
61
|
+
if (!canWriteRepo(auth.access, write.collection, write.action)) return insufficientScopeResponse();
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const newMstBlocksAll: Array<[import('multiformats/cid').CID, Uint8Array]> = [];
|
|
69
|
-
let firstPrevMst: import('multiformats/cid').CID | null = null;
|
|
70
|
-
let lastMst: import('../../lib/mst').MST | null = null;
|
|
71
|
-
|
|
72
|
-
type WriteOperation = {
|
|
73
|
-
$type?: string;
|
|
74
|
-
collection?: string;
|
|
75
|
-
rkey?: string;
|
|
76
|
-
value?: Record<string, unknown>;
|
|
77
|
-
};
|
|
78
|
-
// Apply all writes atomically
|
|
79
|
-
for (const rawWrite of writes) {
|
|
80
|
-
const write = rawWrite as WriteOperation;
|
|
81
|
-
const { $type, collection, rkey, value } = write;
|
|
82
|
-
if (typeof collection !== 'string' || typeof rkey !== 'string') {
|
|
83
|
-
return new Response(
|
|
84
|
-
JSON.stringify({
|
|
85
|
-
error: 'InvalidRequest',
|
|
86
|
-
message: 'collection and rkey are required strings on every write',
|
|
87
|
-
}),
|
|
88
|
-
{ status: 400, headers: { 'Content-Type': 'application/json' } },
|
|
89
|
-
);
|
|
90
|
-
}
|
|
64
|
+
const rateLimitResponse = await checkRate(env, request, 'writes', {
|
|
65
|
+
key: auth.did,
|
|
66
|
+
cost: writes.length,
|
|
67
|
+
});
|
|
68
|
+
writeRateCharged = true;
|
|
69
|
+
if (rateLimitResponse) return rateLimitResponse;
|
|
91
70
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
cid: recordCid.toString(),
|
|
103
|
-
json: JSON.stringify(value),
|
|
104
|
-
});
|
|
105
|
-
results.push({
|
|
106
|
-
$type: 'com.atproto.repo.applyWrites#createResult',
|
|
107
|
-
uri: `at://${repo}/${collection}/${rkey}`,
|
|
108
|
-
cid: recordCid.toString(),
|
|
109
|
-
validationStatus: 'valid',
|
|
110
|
-
});
|
|
111
|
-
} else if ($type === 'com.atproto.repo.applyWrites#update') {
|
|
112
|
-
const { mst, recordCid, prevMstRoot, newMstBlocks } = await repoManager.updateRecord(collection, rkey, value);
|
|
113
|
-
if (!firstPrevMst) firstPrevMst = prevMstRoot;
|
|
114
|
-
lastMst = mst;
|
|
115
|
-
opsForCommit.push({ action: 'update', path: `${collection}/${rkey}`, cid: recordCid });
|
|
116
|
-
for (const [cid, bytes] of newMstBlocks) newMstBlocksAll.push([cid, bytes]);
|
|
117
|
-
await dalPutRecord(env, {
|
|
118
|
-
uri: `at://${pdsDid}/${collection}/${rkey}`,
|
|
119
|
-
did: pdsDid,
|
|
120
|
-
cid: recordCid.toString(),
|
|
121
|
-
json: JSON.stringify(value),
|
|
122
|
-
});
|
|
123
|
-
results.push({
|
|
124
|
-
$type: 'com.atproto.repo.applyWrites#updateResult',
|
|
125
|
-
uri: `at://${repo}/${collection}/${rkey}`,
|
|
126
|
-
cid: recordCid.toString(),
|
|
127
|
-
validationStatus: 'valid',
|
|
128
|
-
});
|
|
129
|
-
} else if ($type === 'com.atproto.repo.applyWrites#delete') {
|
|
130
|
-
const { mst, prevMstRoot, newMstBlocks } = await repoManager.deleteRecord(collection, rkey);
|
|
131
|
-
if (!firstPrevMst) firstPrevMst = prevMstRoot;
|
|
132
|
-
lastMst = mst;
|
|
133
|
-
opsForCommit.push({ action: 'delete', path: `${collection}/${rkey}`, cid: null });
|
|
134
|
-
for (const [cid, bytes] of newMstBlocks) newMstBlocksAll.push([cid, bytes]);
|
|
135
|
-
results.push({
|
|
136
|
-
$type: 'com.atproto.repo.applyWrites#deleteResult',
|
|
137
|
-
});
|
|
138
|
-
}
|
|
71
|
+
// Check if account is active
|
|
72
|
+
const active = await isAccountActive(env, auth.did);
|
|
73
|
+
if (!active) {
|
|
74
|
+
return new Response(
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
error: 'AccountDeactivated',
|
|
77
|
+
message: 'Account is deactivated. Activate it before making changes.'
|
|
78
|
+
}),
|
|
79
|
+
{ status: 403, headers: { 'Content-Type': 'application/json' } }
|
|
80
|
+
);
|
|
139
81
|
}
|
|
140
82
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
newMstBlocks: newMstBlocksAll,
|
|
83
|
+
const { prepared, applied } = await retryNoSwapCommit(input, async () => {
|
|
84
|
+
const prepared = await prepareApplyWrites(env, auth, input);
|
|
85
|
+
const applied = await prepared.repo.applyPreparedWrites(prepared.writes, prepared.expectedCommitCid);
|
|
86
|
+
return { prepared, applied };
|
|
146
87
|
});
|
|
147
88
|
|
|
148
89
|
// Notify sequencer about the commit for firehose
|
|
149
|
-
|
|
150
|
-
// Prefer commitData/sig/blocks returned by bumpRoot (authoritative)
|
|
90
|
+
if (applied.commit && applied.commitCid && applied.rev && applied.commitData && applied.sig && applied.blocks) {
|
|
151
91
|
await notifySequencer(env, {
|
|
152
|
-
did:
|
|
153
|
-
commitCid,
|
|
154
|
-
rev,
|
|
155
|
-
data: commitData,
|
|
156
|
-
sig,
|
|
157
|
-
ops:
|
|
158
|
-
|
|
92
|
+
did: prepared.did,
|
|
93
|
+
commitCid: applied.commitCid,
|
|
94
|
+
rev: applied.rev,
|
|
95
|
+
data: applied.commitData,
|
|
96
|
+
sig: applied.sig,
|
|
97
|
+
ops: applied.ops,
|
|
98
|
+
blocks: applied.blocks,
|
|
99
|
+
});
|
|
100
|
+
await deleteUnreferencedBlobKeys(env, applied.dereferencedBlobKeys).catch((error) => {
|
|
101
|
+
console.warn('[applyWrites] Failed to clean dereferenced blobs:', error);
|
|
159
102
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
103
|
+
ctx?.waitUntil(sweepEligibleUnreferencedBlobKeys(env).catch((error) => {
|
|
104
|
+
console.warn('[applyWrites] Failed to sweep dereferenced blobs:', error);
|
|
105
|
+
}));
|
|
163
106
|
}
|
|
164
107
|
|
|
165
108
|
return new Response(
|
|
166
109
|
JSON.stringify({
|
|
167
|
-
commit
|
|
168
|
-
results,
|
|
110
|
+
...(applied.commit ? { commit: applied.commit } : {}),
|
|
111
|
+
results: applied.results,
|
|
169
112
|
}),
|
|
170
113
|
{
|
|
171
114
|
status: 200,
|
|
@@ -173,11 +116,19 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
173
116
|
}
|
|
174
117
|
);
|
|
175
118
|
} catch (error) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
119
|
+
if (!writeRateCharged && error instanceof RepoWriteError) {
|
|
120
|
+
const rateLimitResponse = await checkRate(env, request, 'writes', { key: auth.did });
|
|
121
|
+
if (rateLimitResponse) return rateLimitResponse;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
return handleRepoWriteError(error);
|
|
125
|
+
} catch (unhandled) {
|
|
126
|
+
console.error('applyWrites error:', unhandled);
|
|
127
|
+
console.error('Error stack:', unhandled instanceof Error ? unhandled.stack : 'No stack');
|
|
128
|
+
return new Response(
|
|
129
|
+
JSON.stringify({ error: 'InternalServerError', message: String(unhandled) }),
|
|
130
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
182
133
|
}
|
|
183
134
|
}
|
|
@@ -1,74 +1,101 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { errorCode
|
|
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 { isAccountActive } from '../../db/dal';
|
|
9
|
+
import {
|
|
10
|
+
assertRepoWriteInput,
|
|
11
|
+
createRecordAuthorizations,
|
|
12
|
+
handleRepoWriteError,
|
|
13
|
+
jsonError,
|
|
14
|
+
prepareCreateRecord,
|
|
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
22
|
const { env } = locals.runtime;
|
|
23
|
+
let auth: NonNullable<Awaited<ReturnType<typeof verifyResourceRequestHybrid>>>;
|
|
13
24
|
try {
|
|
14
|
-
const
|
|
15
|
-
if (!
|
|
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
|
-
|
|
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 (
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
43
|
+
return jsonError('BadRequest');
|
|
33
44
|
}
|
|
34
|
-
const { collection, rkey } = body ?? {};
|
|
35
|
-
let { record } = body ?? {};
|
|
36
|
-
if (!collection || !record) return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (typeof record.createdAt !== 'string') {
|
|
44
|
-
record.createdAt = new Date().toISOString();
|
|
46
|
+
let writeRateCharged = false;
|
|
47
|
+
try {
|
|
48
|
+
const input = assertRepoWriteInput('com.atproto.repo.createRecord', body);
|
|
49
|
+
for (const write of createRecordAuthorizations(input)) {
|
|
50
|
+
if (!canWriteRepo(auth.access, write.collection, write.action)) return insufficientScopeResponse();
|
|
45
51
|
}
|
|
46
|
-
}
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
did: env.PDS_DID as string,
|
|
52
|
-
commitCid: result.commitCid,
|
|
53
|
-
rev: result.rev,
|
|
54
|
-
data: result.commitData,
|
|
55
|
-
sig: result.sig,
|
|
56
|
-
ops: result.ops,
|
|
57
|
-
blocks: result.blocks
|
|
58
|
-
});
|
|
53
|
+
const rateLimitResponse = await checkRate(env, request, 'writes', { key: auth.did });
|
|
54
|
+
writeRateCharged = true;
|
|
55
|
+
if (rateLimitResponse) return rateLimitResponse;
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
if (!(await isAccountActive(env, auth.did))) {
|
|
58
|
+
return jsonError('AccountDeactivated', 'Account is deactivated. Activate it before making changes.', 403);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { prepared, result } = await retryNoSwapCommit(input, async () => {
|
|
62
|
+
const prepared = await prepareCreateRecord(env, auth, input);
|
|
63
|
+
const { write, repo } = prepared;
|
|
64
|
+
const result = await repo.createRecord(
|
|
65
|
+
write.collection,
|
|
66
|
+
write.record,
|
|
67
|
+
write.rkey,
|
|
68
|
+
write.blobKeys,
|
|
69
|
+
prepared.expectedCommitCid,
|
|
70
|
+
);
|
|
71
|
+
return { prepared, result };
|
|
72
|
+
});
|
|
73
|
+
await notifySequencer(env, {
|
|
74
|
+
did: prepared.did,
|
|
75
|
+
commitCid: result.commitCid,
|
|
66
76
|
rev: result.rev,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
data: result.commitData,
|
|
78
|
+
sig: result.sig,
|
|
79
|
+
ops: result.ops,
|
|
80
|
+
blocks: result.blocks
|
|
81
|
+
});
|
|
70
82
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
return new Response(JSON.stringify({
|
|
84
|
+
uri: result.uri,
|
|
85
|
+
cid: result.cid,
|
|
86
|
+
commit: {
|
|
87
|
+
cid: result.commitCid,
|
|
88
|
+
rev: result.rev,
|
|
89
|
+
},
|
|
90
|
+
validationStatus: prepared.write.validationStatus,
|
|
91
|
+
}), {
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (!writeRateCharged && error instanceof RepoWriteError) {
|
|
96
|
+
const rateLimitResponse = await checkRate(env, request, 'writes', { key: auth.did });
|
|
97
|
+
if (rateLimitResponse) return rateLimitResponse;
|
|
98
|
+
}
|
|
99
|
+
return handleRepoWriteError(error);
|
|
100
|
+
}
|
|
74
101
|
}
|