@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,72 +1,145 @@
|
|
|
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 {
|
|
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
|
|
16
|
-
if (!
|
|
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
|
-
|
|
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 (
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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 {
|
|
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')
|
|
14
|
-
const did = env
|
|
15
|
-
const handle = env
|
|
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
|
-
|
|
18
|
-
const
|
|
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
|
|
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)
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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 || !rkey || !record) return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
}
|