@alteran/astro 0.3.9 → 0.6.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/LICENSE +21 -0
- package/README.md +19 -30
- package/index.js +34 -28
- package/migrations/0007_bored_spitfire.sql +26 -0
- package/migrations/0008_furry_ozymandias.sql +2 -0
- package/migrations/meta/0007_snapshot.json +534 -0
- package/migrations/meta/0008_snapshot.json +548 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +10 -9
- package/src/app.ts +8 -4
- package/src/db/account.ts +25 -6
- package/src/db/client.ts +1 -1
- package/src/db/dal.ts +34 -23
- package/src/db/repo.ts +38 -38
- package/src/db/schema.ts +5 -1
- package/src/db/seed.ts +5 -13
- package/src/entrypoints/server.ts +2 -22
- package/src/handlers/debug.ts +1 -1
- package/src/handlers/ready.ts +1 -1
- package/src/handlers/root.ts +4 -4
- package/src/handlers/xrpc.server.refreshSession.ts +6 -6
- package/src/lib/account-state.ts +156 -0
- package/src/lib/actor.ts +29 -13
- package/src/lib/appview/auth-policy.ts +66 -0
- package/src/lib/appview/did-resolver.ts +233 -0
- package/src/lib/appview/proxy.ts +221 -0
- package/src/lib/appview/service-config.ts +61 -0
- package/src/lib/appview/service-jwt.ts +93 -0
- package/src/lib/appview/types.ts +25 -0
- package/src/lib/appview.ts +5 -532
- package/src/lib/auth-errors.ts +24 -0
- package/src/lib/auth.ts +63 -15
- package/src/lib/blockstore-gc.ts +6 -5
- package/src/lib/cache.ts +30 -4
- package/src/lib/chat.ts +20 -14
- package/src/lib/commit-log-pruning.ts +2 -2
- package/src/lib/commit.ts +26 -36
- package/src/lib/config.ts +26 -15
- package/src/lib/did-document.ts +32 -0
- package/src/lib/errors.ts +54 -0
- package/src/lib/feed.ts +18 -19
- package/src/lib/firehose/frames.ts +87 -47
- package/src/lib/firehose/validation.ts +3 -3
- package/src/lib/jwt.ts +85 -177
- package/src/lib/labeler.ts +43 -30
- package/src/lib/logger.ts +4 -0
- package/src/lib/mst/block-map.ts +172 -0
- package/src/lib/mst/blockstore.ts +56 -93
- package/src/lib/mst/index.ts +1 -0
- package/src/lib/mst/leaf.ts +25 -0
- package/src/lib/mst/mst.ts +81 -237
- package/src/lib/mst/serialize.ts +97 -0
- package/src/lib/mst/types.ts +21 -0
- package/src/lib/oauth/clients.ts +67 -0
- package/src/lib/oauth/dpop-errors.ts +15 -0
- package/src/lib/oauth/dpop.ts +150 -0
- package/src/lib/oauth/resource.ts +199 -0
- package/src/lib/oauth/store.ts +77 -0
- package/src/lib/preferences.ts +12 -37
- package/src/lib/ratelimit.ts +4 -4
- package/src/lib/refresh-session.ts +161 -0
- package/src/lib/relay.ts +10 -8
- package/src/lib/secrets.ts +6 -7
- package/src/lib/sequencer.ts +14 -5
- package/src/lib/service-auth.ts +184 -0
- package/src/lib/session-tokens.ts +28 -76
- package/src/lib/streaming-car.ts +3 -0
- package/src/lib/tracing.ts +4 -3
- package/src/lib/util.ts +65 -15
- package/src/middleware.ts +1 -1
- package/src/pages/.well-known/did.json.ts +27 -30
- package/src/pages/.well-known/oauth-authorization-server.ts +31 -0
- package/src/pages/.well-known/oauth-protected-resource.ts +22 -0
- package/src/pages/debug/blob/[...key].ts +2 -2
- package/src/pages/debug/db/bootstrap.ts +1 -1
- package/src/pages/debug/db/commits.ts +1 -1
- package/src/pages/debug/gc/blobs.ts +1 -1
- package/src/pages/debug/record.ts +1 -1
- package/src/pages/debug/sequencer.ts +28 -0
- package/src/pages/health.ts +4 -4
- package/src/pages/oauth/authorize.ts +78 -0
- package/src/pages/oauth/consent.ts +80 -0
- package/src/pages/oauth/par.ts +121 -0
- package/src/pages/oauth/token.ts +158 -0
- package/src/pages/ready.ts +2 -2
- package/src/pages/xrpc/[...nsid].ts +61 -0
- package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +12 -13
- package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +23 -23
- package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +9 -2
- package/src/pages/xrpc/chat.bsky.convo.getLog.ts +9 -2
- package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +9 -2
- package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +43 -41
- package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +10 -3
- package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +40 -9
- package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +41 -29
- package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +20 -6
- package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +1 -1
- package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +101 -11
- package/src/pages/xrpc/com.atproto.repo.createRecord.ts +44 -14
- package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +41 -13
- package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +2 -2
- package/src/pages/xrpc/com.atproto.repo.getRecord.ts +14 -1
- package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +14 -6
- package/src/pages/xrpc/com.atproto.repo.listRecords.ts +1 -1
- package/src/pages/xrpc/com.atproto.repo.putRecord.ts +42 -14
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +76 -15
- package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +20 -8
- package/src/pages/xrpc/com.atproto.server.createSession.ts +32 -12
- package/src/pages/xrpc/com.atproto.server.describeServer.ts +1 -1
- package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +12 -5
- package/src/pages/xrpc/com.atproto.server.getSession.ts +22 -8
- package/src/pages/xrpc/com.atproto.server.refreshSession.ts +30 -72
- package/src/pages/xrpc/com.atproto.sync.getBlob.ts +72 -23
- package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getHead.ts +7 -2
- package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getRecord.ts +5 -27
- package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getRepo.ts +50 -5
- package/src/pages/xrpc/com.atproto.sync.getRepoStatus.ts +58 -0
- package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +2 -2
- package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
- package/src/services/car.ts +209 -57
- package/src/services/r2-blob-store.ts +4 -4
- package/src/services/repo/blockstore-ops.ts +29 -0
- package/src/services/repo/operations.ts +133 -0
- package/src/services/repo-manager.ts +203 -254
- package/src/worker/runtime.ts +56 -11
- package/src/worker/sequencer/broadcast.ts +91 -0
- package/src/worker/sequencer/cid-helpers.ts +39 -0
- package/src/worker/sequencer/payload.ts +84 -0
- package/src/worker/sequencer/types.ts +36 -0
- package/src/worker/sequencer/upgrade.ts +141 -0
- package/src/worker/sequencer.ts +264 -406
- package/types/env.d.ts +18 -6
- package/src/pages/xrpc/app.bsky.actor.getProfile.ts +0 -49
- package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +0 -51
- package/src/pages/xrpc/app.bsky.feed.getActorFeeds.ts +0 -25
- package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +0 -42
- package/src/pages/xrpc/app.bsky.feed.getFeedGenerators.ts +0 -25
- package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +0 -37
- package/src/pages/xrpc/app.bsky.feed.getPosts.ts +0 -26
- package/src/pages/xrpc/app.bsky.feed.getSuggestedFeeds.ts +0 -23
- package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +0 -47
- package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +0 -29
- package/src/pages/xrpc/app.bsky.graph.getFollows.ts +0 -29
- package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +0 -20
- package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +0 -27
- package/src/pages/xrpc/app.bsky.unspecced.getSuggestedFeeds.ts +0 -23
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
2
|
+
import { AuthTokenExpiredError, expiredToken, isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
3
|
|
|
4
4
|
export const prerender = false;
|
|
5
5
|
|
|
@@ -15,8 +15,15 @@ export const prerender = false;
|
|
|
15
15
|
export async function POST({ locals, request }: APIContext) {
|
|
16
16
|
const { env } = locals.runtime;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
try {
|
|
19
|
+
if (!(await isAuthorized(request, env))) {
|
|
20
|
+
return unauthorized();
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error instanceof AuthTokenExpiredError) {
|
|
24
|
+
return expiredToken();
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
return new Response(null, { status: 200 });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
+
import { getAppViewConfig } from '../../lib/appview';
|
|
2
3
|
|
|
3
4
|
export const prerender = false;
|
|
4
5
|
|
|
@@ -10,8 +11,8 @@ export async function GET({ locals, url }: APIContext) {
|
|
|
10
11
|
const { env } = locals.runtime;
|
|
11
12
|
|
|
12
13
|
const handle = url.searchParams.get('handle');
|
|
13
|
-
const configuredHandle = env.PDS_HANDLE || 'user.example.com';
|
|
14
|
-
const did = env.PDS_DID || 'did:example:single-user';
|
|
14
|
+
const configuredHandle = String(env.PDS_HANDLE || 'user.example.com');
|
|
15
|
+
const did = String(env.PDS_DID || 'did:example:single-user');
|
|
15
16
|
|
|
16
17
|
if (!handle) {
|
|
17
18
|
return new Response(
|
|
@@ -20,8 +21,8 @@ export async function GET({ locals, url }: APIContext) {
|
|
|
20
21
|
);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
// Single-user PDS:
|
|
24
|
-
if (handle === configuredHandle) {
|
|
24
|
+
// Single-user PDS: resolve the local configured handle directly
|
|
25
|
+
if (handle.toLowerCase() === configuredHandle.toLowerCase()) {
|
|
25
26
|
return new Response(
|
|
26
27
|
JSON.stringify({ did }),
|
|
27
28
|
{
|
|
@@ -31,8 +32,38 @@ export async function GET({ locals, url }: APIContext) {
|
|
|
31
32
|
);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
// For non-local handles, mirror upstream PDS behavior:
|
|
36
|
+
// proxy the resolution to the configured AppView (or api.bsky.app by default).
|
|
37
|
+
try {
|
|
38
|
+
const app = getAppViewConfig(env);
|
|
39
|
+
const base = app?.url || 'https://api.bsky.app';
|
|
40
|
+
const upstream = new URL('/xrpc/com.atproto.identity.resolveHandle', base);
|
|
41
|
+
upstream.searchParams.set('handle', handle);
|
|
42
|
+
|
|
43
|
+
const response = await fetch(upstream.toString(), {
|
|
44
|
+
headers: { accept: 'application/json' },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (response.ok) {
|
|
48
|
+
// Pass through upstream JSON (e.g. { did })
|
|
49
|
+
return new Response(await response.text(), {
|
|
50
|
+
status: 200,
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Map upstream failures to the standard InvalidRequest shape used by PDS
|
|
56
|
+
const text = await response.text().catch(() => '');
|
|
57
|
+
const body = text ? (() => { try { return JSON.parse(text); } catch { return null; } })() : null;
|
|
58
|
+
const message = body?.message || 'Unable to resolve handle';
|
|
59
|
+
return new Response(
|
|
60
|
+
JSON.stringify({ error: 'InvalidRequest', message }),
|
|
61
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
62
|
+
);
|
|
63
|
+
} catch {
|
|
64
|
+
return new Response(
|
|
65
|
+
JSON.stringify({ error: 'InvalidRequest', message: 'Unable to resolve handle' }),
|
|
66
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
2
|
+
import { AuthTokenExpiredError, expiredToken, isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
3
|
import { resolveSecret } from '../../lib/secrets';
|
|
4
4
|
|
|
5
5
|
export const prerender = false;
|
|
@@ -15,7 +15,14 @@ export const prerender = false;
|
|
|
15
15
|
export async function POST({ locals, request }: APIContext) {
|
|
16
16
|
const { env } = locals.runtime;
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
try {
|
|
19
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error instanceof AuthTokenExpiredError) {
|
|
22
|
+
return expiredToken();
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
19
26
|
|
|
20
27
|
try {
|
|
21
28
|
const body = await request.json() as {
|
|
@@ -36,53 +43,58 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
36
43
|
return jsonErr(400, 'InvalidRequest', 'PDS_DID is not configured');
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
// MUST be the rotation key currently present in the PLC document.
|
|
41
|
-
const privHex = ((await resolveSecret(env.PDS_PLC_ROTATION_KEY as any)) || '').trim();
|
|
46
|
+
const privHex = ((await resolveSecret(env.PDS_PLC_ROTATION_KEY)) || '').trim();
|
|
42
47
|
if (!privHex) {
|
|
43
48
|
return jsonErr(500, 'ServerMisconfigured', 'PDS_PLC_ROTATION_KEY is not configured');
|
|
44
49
|
}
|
|
45
|
-
// Lazy-load deps compatible with Workers runtime
|
|
46
50
|
const { Secp256k1Keypair } = await import('@atproto/crypto');
|
|
47
|
-
const dagCbor
|
|
51
|
+
const dagCbor = await import('@ipld/dag-cbor');
|
|
48
52
|
const { sha256 } = await import('multiformats/hashes/sha2');
|
|
49
53
|
const { CID } = await import('multiformats/cid');
|
|
50
|
-
const u8a
|
|
54
|
+
const u8a = await import('uint8arrays');
|
|
51
55
|
|
|
52
56
|
const signer = await Secp256k1Keypair.import(privHex);
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return jsonErr(lastRes.status, 'PlcFetchFailed', `Failed to fetch last op: ${text}`);
|
|
58
|
+
const lastResponse = await fetch(`https://plc.directory/${encodeURIComponent(did)}/log/last`);
|
|
59
|
+
if (!lastResponse.ok) {
|
|
60
|
+
const text = await lastResponse.text();
|
|
61
|
+
return jsonErr(lastResponse.status, 'PlcFetchFailed', `Failed to fetch last op: ${text}`);
|
|
59
62
|
}
|
|
60
|
-
const lastOp = await
|
|
61
|
-
if (
|
|
63
|
+
const lastOp = (await lastResponse.json()) as { type?: string };
|
|
64
|
+
if (lastOp?.type === 'plc_tombstone') {
|
|
62
65
|
return jsonErr(400, 'DidTombstoned', 'DID is tombstoned');
|
|
63
66
|
}
|
|
64
67
|
const lastOpCbor = dagCbor.encode(lastOp);
|
|
65
68
|
const mh = await sha256.digest(lastOpCbor);
|
|
66
69
|
const prevCid = CID.createV1(dagCbor.code, mh);
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return jsonErr(dataRes.status, 'PlcFetchFailed', `Failed to fetch document data: ${text}`);
|
|
71
|
+
const dataResponse = await fetch(`https://plc.directory/${encodeURIComponent(did)}/data`);
|
|
72
|
+
if (!dataResponse.ok) {
|
|
73
|
+
const text = await dataResponse.text();
|
|
74
|
+
return jsonErr(dataResponse.status, 'PlcFetchFailed', `Failed to fetch document data: ${text}`);
|
|
73
75
|
}
|
|
74
|
-
|
|
76
|
+
type PlcDoc = {
|
|
77
|
+
rotationKeys?: string[];
|
|
78
|
+
alsoKnownAs?: string[];
|
|
79
|
+
verificationMethods?: Record<string, string>;
|
|
80
|
+
services?: Record<string, { type?: string; endpoint?: string }>;
|
|
81
|
+
};
|
|
82
|
+
const doc = (await dataResponse.json()) as PlcDoc;
|
|
75
83
|
|
|
76
84
|
const rotationKeys = body.rotationKeys ?? doc.rotationKeys ?? [];
|
|
77
85
|
const alsoKnownAs = body.alsoKnownAs ?? doc.alsoKnownAs ?? [];
|
|
78
86
|
const verificationMethods = body.verificationMethods ?? doc.verificationMethods ?? {};
|
|
79
|
-
const services = body.services ?? doc.services ?? {}
|
|
87
|
+
const services = (body.services ?? doc.services ?? {}) as Record<
|
|
88
|
+
string,
|
|
89
|
+
{ type?: string; endpoint?: string }
|
|
90
|
+
>;
|
|
80
91
|
|
|
81
|
-
|
|
92
|
+
const pdsService = services.atproto_pds;
|
|
93
|
+
if (!pdsService || typeof pdsService !== 'object') {
|
|
82
94
|
return jsonErr(400, 'InvalidRequest', 'Missing atproto_pds service in PLC operation');
|
|
83
95
|
}
|
|
84
|
-
if (!
|
|
85
|
-
|
|
96
|
+
if (!pdsService.type) {
|
|
97
|
+
pdsService.type = 'AtprotoPersonalDataServer';
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
const unsignedOp = {
|
|
@@ -96,10 +108,9 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
96
108
|
|
|
97
109
|
const bytes = dagCbor.encode(unsignedOp);
|
|
98
110
|
const sig = await signer.sign(bytes);
|
|
99
|
-
const sigB64 =
|
|
111
|
+
const sigB64 = u8a.toString(sig, 'base64url');
|
|
100
112
|
const operation = { ...unsignedOp, sig: sigB64 };
|
|
101
113
|
|
|
102
|
-
// sanity: ensure our configured rotation key is included
|
|
103
114
|
const signerDid = (await (await import('@atproto/crypto')).Secp256k1Keypair.import(privHex)).did();
|
|
104
115
|
if (!rotationKeys.includes(signerDid)) {
|
|
105
116
|
return jsonErr(
|
|
@@ -113,9 +124,10 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
113
124
|
status: 200,
|
|
114
125
|
headers: { 'Content-Type': 'application/json' },
|
|
115
126
|
});
|
|
116
|
-
} catch (error
|
|
127
|
+
} catch (error) {
|
|
117
128
|
console.error('signPlcOperation error:', error);
|
|
118
|
-
|
|
129
|
+
const message = error instanceof Error ? error.message : 'Failed to sign PLC operation';
|
|
130
|
+
return jsonErr(500, 'InternalServerError', message);
|
|
119
131
|
}
|
|
120
132
|
}
|
|
121
133
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import {
|
|
2
|
+
import { errorMessage } from '../../lib/errors';
|
|
3
|
+
import { AuthTokenExpiredError, expiredToken, isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
4
|
import { resolveSecret } from '../../lib/secrets';
|
|
4
5
|
|
|
5
6
|
export const prerender = false;
|
|
@@ -14,7 +15,14 @@ export const prerender = false;
|
|
|
14
15
|
export async function POST({ locals, request }: APIContext) {
|
|
15
16
|
const { env } = locals.runtime;
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
try {
|
|
19
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error instanceof AuthTokenExpiredError) {
|
|
22
|
+
return expiredToken();
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
18
26
|
|
|
19
27
|
try {
|
|
20
28
|
const body = await request.json() as { operation?: any };
|
|
@@ -30,7 +38,13 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
30
38
|
);
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
const did =
|
|
41
|
+
const did = await resolveSecret(env.PDS_DID);
|
|
42
|
+
if (!did) {
|
|
43
|
+
return new Response(
|
|
44
|
+
JSON.stringify({ error: 'InvalidRequest', message: 'PDS_DID is not configured' }),
|
|
45
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
34
48
|
|
|
35
49
|
console.log('Submitting PLC operation:', {
|
|
36
50
|
did,
|
|
@@ -78,14 +92,14 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
78
92
|
JSON.stringify({ success: true }),
|
|
79
93
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
80
94
|
);
|
|
81
|
-
} catch (error
|
|
95
|
+
} catch (error) {
|
|
82
96
|
console.error('Submit PLC operation error:', error);
|
|
83
97
|
return new Response(
|
|
84
98
|
JSON.stringify({
|
|
85
99
|
error: 'InternalServerError',
|
|
86
|
-
message: error
|
|
100
|
+
message: errorMessage(error) || 'Failed to submit PLC operation'
|
|
87
101
|
}),
|
|
88
102
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
89
103
|
);
|
|
90
104
|
}
|
|
91
|
-
}
|
|
105
|
+
}
|
|
@@ -11,7 +11,7 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
11
11
|
const { env } = locals.runtime;
|
|
12
12
|
|
|
13
13
|
try {
|
|
14
|
-
const body = await readJson(request);
|
|
14
|
+
const body = (await readJson(request)) as { handle?: string };
|
|
15
15
|
const { handle } = body;
|
|
16
16
|
|
|
17
17
|
if (!handle) {
|
|
@@ -2,9 +2,13 @@ import type { APIContext } from 'astro';
|
|
|
2
2
|
import { RepoManager } from '../../services/repo-manager';
|
|
3
3
|
import { readJson } from '../../lib/util';
|
|
4
4
|
import { bumpRoot } from '../../db/repo';
|
|
5
|
-
import {
|
|
5
|
+
import { verifyResourceRequestHybrid, dpopResourceUnauthorized, handleResourceAuthError } from '../../lib/oauth/resource';
|
|
6
6
|
import { isAccountActive } from '../../db/dal';
|
|
7
7
|
import { checkRate } from '../../lib/ratelimit';
|
|
8
|
+
import { notifySequencer } from '../../lib/sequencer';
|
|
9
|
+
import { encodeBlocksForCommit } from '../../services/car';
|
|
10
|
+
import { CID } from 'multiformats/cid';
|
|
11
|
+
import { putRecord as dalPutRecord } from '../../db/dal';
|
|
8
12
|
|
|
9
13
|
export const prerender = false;
|
|
10
14
|
|
|
@@ -14,10 +18,17 @@ export const prerender = false;
|
|
|
14
18
|
*/
|
|
15
19
|
export async function POST({ locals, request }: APIContext) {
|
|
16
20
|
const { env } = locals.runtime;
|
|
17
|
-
|
|
21
|
+
try {
|
|
22
|
+
const auth = await verifyResourceRequestHybrid(env, request);
|
|
23
|
+
if (!auth) return dpopResourceUnauthorized(env);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const handled = await handleResourceAuthError(env, error);
|
|
26
|
+
if (handled) return handled;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
18
29
|
|
|
19
30
|
// Check if account is active
|
|
20
|
-
const did = env.PDS_DID
|
|
31
|
+
const did = env.PDS_DID as string;
|
|
21
32
|
const active = await isAccountActive(env, did);
|
|
22
33
|
if (!active) {
|
|
23
34
|
return new Response(
|
|
@@ -33,7 +44,12 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
33
44
|
if (rateLimitResponse) return rateLimitResponse;
|
|
34
45
|
|
|
35
46
|
try {
|
|
36
|
-
const body = await readJson(request)
|
|
47
|
+
const body = (await readJson(request)) as {
|
|
48
|
+
repo?: string;
|
|
49
|
+
writes?: unknown[];
|
|
50
|
+
validate?: boolean;
|
|
51
|
+
swapCommit?: string;
|
|
52
|
+
};
|
|
37
53
|
const { repo, writes, validate = true, swapCommit } = body;
|
|
38
54
|
|
|
39
55
|
if (!writes || !Array.isArray(writes)) {
|
|
@@ -44,34 +60,107 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
const repoManager = new RepoManager(env);
|
|
47
|
-
const
|
|
63
|
+
const pdsDid = typeof env.PDS_DID === 'string' ? env.PDS_DID : '';
|
|
64
|
+
type WriteResult = { $type: string; uri?: string; cid?: string; validationStatus?: string };
|
|
65
|
+
const results: WriteResult[] = [];
|
|
66
|
+
// Accumulate ops and new MST blocks for this batch
|
|
67
|
+
const opsForCommit: { action: 'create'|'update'|'delete'; path: string; cid: import('multiformats/cid').CID | null }[] = [];
|
|
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;
|
|
48
71
|
|
|
72
|
+
type WriteOperation = {
|
|
73
|
+
$type?: string;
|
|
74
|
+
collection?: string;
|
|
75
|
+
rkey?: string;
|
|
76
|
+
value?: Record<string, unknown>;
|
|
77
|
+
};
|
|
49
78
|
// Apply all writes atomically
|
|
50
|
-
for (const
|
|
79
|
+
for (const rawWrite of writes) {
|
|
80
|
+
const write = rawWrite as WriteOperation;
|
|
51
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
|
+
}
|
|
52
91
|
|
|
53
92
|
if ($type === 'com.atproto.repo.applyWrites#create') {
|
|
54
|
-
const { mst, recordCid } = await repoManager.addRecord(collection, rkey, value);
|
|
93
|
+
const { mst, recordCid, prevMstRoot, newMstBlocks } = await repoManager.addRecord(collection, rkey, value);
|
|
94
|
+
if (!firstPrevMst) firstPrevMst = prevMstRoot;
|
|
95
|
+
lastMst = mst;
|
|
96
|
+
opsForCommit.push({ action: 'create', path: `${collection}/${rkey}`, cid: recordCid });
|
|
97
|
+
for (const [cid, bytes] of newMstBlocks) newMstBlocksAll.push([cid, bytes]);
|
|
98
|
+
// Persist JSON for local reads
|
|
99
|
+
await dalPutRecord(env, {
|
|
100
|
+
uri: `at://${pdsDid}/${collection}/${rkey}`,
|
|
101
|
+
did: pdsDid,
|
|
102
|
+
cid: recordCid.toString(),
|
|
103
|
+
json: JSON.stringify(value),
|
|
104
|
+
});
|
|
55
105
|
results.push({
|
|
106
|
+
$type: 'com.atproto.repo.applyWrites#createResult',
|
|
56
107
|
uri: `at://${repo}/${collection}/${rkey}`,
|
|
57
108
|
cid: recordCid.toString(),
|
|
109
|
+
validationStatus: 'valid',
|
|
58
110
|
});
|
|
59
111
|
} else if ($type === 'com.atproto.repo.applyWrites#update') {
|
|
60
|
-
const { mst, recordCid } = await repoManager.updateRecord(collection, rkey, value);
|
|
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
|
+
});
|
|
61
123
|
results.push({
|
|
124
|
+
$type: 'com.atproto.repo.applyWrites#updateResult',
|
|
62
125
|
uri: `at://${repo}/${collection}/${rkey}`,
|
|
63
126
|
cid: recordCid.toString(),
|
|
127
|
+
validationStatus: 'valid',
|
|
64
128
|
});
|
|
65
129
|
} else if ($type === 'com.atproto.repo.applyWrites#delete') {
|
|
66
|
-
await repoManager.deleteRecord(collection, rkey);
|
|
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]);
|
|
67
135
|
results.push({
|
|
68
|
-
|
|
136
|
+
$type: 'com.atproto.repo.applyWrites#deleteResult',
|
|
69
137
|
});
|
|
70
138
|
}
|
|
71
139
|
}
|
|
72
140
|
|
|
73
141
|
// Bump repo root to create new commit
|
|
74
|
-
const
|
|
142
|
+
const currentRoot = lastMst ? await lastMst.getPointer() : undefined;
|
|
143
|
+
const { commitCid, rev, commitData, sig, blocks } = await bumpRoot(env, firstPrevMst ?? undefined, currentRoot, {
|
|
144
|
+
ops: opsForCommit,
|
|
145
|
+
newMstBlocks: newMstBlocksAll,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Notify sequencer about the commit for firehose
|
|
149
|
+
try {
|
|
150
|
+
// Prefer commitData/sig/blocks returned by bumpRoot (authoritative)
|
|
151
|
+
await notifySequencer(env, {
|
|
152
|
+
did: pdsDid,
|
|
153
|
+
commitCid,
|
|
154
|
+
rev,
|
|
155
|
+
data: commitData,
|
|
156
|
+
sig,
|
|
157
|
+
ops: opsForCommit,
|
|
158
|
+
...(blocks ? { blocks } : {}),
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('Failed to notify sequencer:', error);
|
|
162
|
+
// Don't fail the request if sequencer notification fails
|
|
163
|
+
}
|
|
75
164
|
|
|
76
165
|
return new Response(
|
|
77
166
|
JSON.stringify({
|
|
@@ -85,6 +174,7 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
85
174
|
);
|
|
86
175
|
} catch (error) {
|
|
87
176
|
console.error('applyWrites error:', error);
|
|
177
|
+
console.error('Error stack:', error instanceof Error ? error.stack : 'No stack');
|
|
88
178
|
return new Response(
|
|
89
179
|
JSON.stringify({ error: 'InternalServerError', message: String(error) }),
|
|
90
180
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import {
|
|
2
|
+
import { errorCode, errorMessage } from '../../lib/errors';
|
|
3
|
+
import { verifyResourceRequestHybrid, dpopResourceUnauthorized, handleResourceAuthError } from '../../lib/oauth/resource';
|
|
3
4
|
import { checkRate } from '../../lib/ratelimit';
|
|
4
5
|
import { readJsonBounded } from '../../lib/util';
|
|
5
6
|
import { RepoManager } from '../../services/repo-manager';
|
|
@@ -9,7 +10,14 @@ export const prerender = false;
|
|
|
9
10
|
|
|
10
11
|
export async function POST({ locals, request }: APIContext) {
|
|
11
12
|
const { env } = locals.runtime;
|
|
12
|
-
|
|
13
|
+
try {
|
|
14
|
+
const auth = await verifyResourceRequestHybrid(env, request);
|
|
15
|
+
if (!auth) return dpopResourceUnauthorized(env);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
const handled = await handleResourceAuthError(env, error);
|
|
18
|
+
if (handled) return handled;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
const rateLimitResponse = await checkRate(env, request, 'writes');
|
|
15
23
|
if (rateLimitResponse) return rateLimitResponse;
|
|
@@ -17,28 +25,50 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
17
25
|
let body: any;
|
|
18
26
|
try {
|
|
19
27
|
body = await readJsonBounded(env, request);
|
|
20
|
-
} catch (e
|
|
21
|
-
if (e
|
|
28
|
+
} catch (e) {
|
|
29
|
+
if (errorCode(e) === 'PayloadTooLarge') {
|
|
22
30
|
return new Response(JSON.stringify({ error: 'PayloadTooLarge' }), { status: 413 });
|
|
23
31
|
}
|
|
24
32
|
return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
|
|
25
33
|
}
|
|
26
|
-
const { collection, rkey
|
|
34
|
+
const { collection, rkey } = body ?? {};
|
|
35
|
+
let { record } = body ?? {};
|
|
27
36
|
if (!collection || !record) return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
|
|
28
37
|
|
|
38
|
+
// Minimal schema alignment for app.bsky.feed.post: ensure required fields
|
|
39
|
+
if (collection === 'app.bsky.feed.post' && record && typeof record === 'object') {
|
|
40
|
+
if (typeof record.text !== 'string') {
|
|
41
|
+
record.text = '';
|
|
42
|
+
}
|
|
43
|
+
if (typeof record.createdAt !== 'string') {
|
|
44
|
+
record.createdAt = new Date().toISOString();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
const repo = new RepoManager(env);
|
|
30
|
-
const
|
|
49
|
+
const result = await repo.createRecord(collection, record, rkey);
|
|
31
50
|
await notifySequencer(env, {
|
|
32
|
-
did: env.PDS_DID
|
|
33
|
-
commitCid:
|
|
34
|
-
rev:
|
|
35
|
-
data:
|
|
36
|
-
sig:
|
|
37
|
-
ops:
|
|
38
|
-
blocks:
|
|
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
|
|
39
58
|
});
|
|
40
59
|
|
|
41
|
-
|
|
60
|
+
// Conform to official PDS response schema
|
|
61
|
+
const out = {
|
|
62
|
+
uri: result.uri,
|
|
63
|
+
cid: result.cid,
|
|
64
|
+
commit: {
|
|
65
|
+
cid: result.commitCid,
|
|
66
|
+
rev: result.rev,
|
|
67
|
+
},
|
|
68
|
+
validationStatus: 'unknown' as const,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return new Response(JSON.stringify(out), {
|
|
42
72
|
headers: { 'Content-Type': 'application/json' },
|
|
43
73
|
});
|
|
44
74
|
}
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import {
|
|
2
|
+
import { errorCode, errorMessage } from '../../lib/errors';
|
|
3
|
+
import { verifyResourceRequestHybrid, dpopResourceUnauthorized, handleResourceAuthError } from '../../lib/oauth/resource';
|
|
3
4
|
import { checkRate } from '../../lib/ratelimit';
|
|
4
5
|
import { readJsonBounded } from '../../lib/util';
|
|
5
6
|
import { RepoManager } from '../../services/repo-manager';
|
|
7
|
+
import { bumpRoot } from '../../db/repo';
|
|
6
8
|
import { notifySequencer } from '../../lib/sequencer';
|
|
7
9
|
|
|
8
10
|
export const prerender = false;
|
|
9
11
|
|
|
10
12
|
export async function POST({ locals, request }: APIContext) {
|
|
11
13
|
const { env } = locals.runtime;
|
|
12
|
-
|
|
14
|
+
try {
|
|
15
|
+
const auth = await verifyResourceRequestHybrid(env, request);
|
|
16
|
+
if (!auth) return dpopResourceUnauthorized(env);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const handled = await handleResourceAuthError(env, error);
|
|
19
|
+
if (handled) return handled;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
13
22
|
|
|
14
23
|
const rateLimitResponse = await checkRate(env, request, 'writes');
|
|
15
24
|
if (rateLimitResponse) return rateLimitResponse;
|
|
@@ -17,8 +26,8 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
17
26
|
let body: any;
|
|
18
27
|
try {
|
|
19
28
|
body = await readJsonBounded(env, request);
|
|
20
|
-
} catch (e
|
|
21
|
-
if (e
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (errorCode(e) === 'PayloadTooLarge') {
|
|
22
31
|
return new Response(JSON.stringify({ error: 'PayloadTooLarge' }), { status: 413 });
|
|
23
32
|
}
|
|
24
33
|
return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
|
|
@@ -27,18 +36,37 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
27
36
|
if (!collection || !rkey) return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
|
|
28
37
|
|
|
29
38
|
const repo = new RepoManager(env);
|
|
30
|
-
|
|
39
|
+
// Perform the delete in the MST, gather prev/new roots & new blocks
|
|
40
|
+
const { mst, prevMstRoot, uri, newMstBlocks } = await repo.deleteRecord(collection, rkey);
|
|
41
|
+
|
|
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
|
+
});
|
|
49
|
+
|
|
50
|
+
// Notify sequencer with a complete payload matching handleCommitNotification
|
|
31
51
|
await notifySequencer(env, {
|
|
32
|
-
did: env.PDS_DID
|
|
33
|
-
commitCid
|
|
34
|
-
rev
|
|
35
|
-
data:
|
|
36
|
-
sig
|
|
37
|
-
ops:
|
|
38
|
-
blocks
|
|
52
|
+
did: env.PDS_DID as string,
|
|
53
|
+
commitCid,
|
|
54
|
+
rev,
|
|
55
|
+
data: commitData,
|
|
56
|
+
sig,
|
|
57
|
+
ops: opsForCommit,
|
|
58
|
+
blocks,
|
|
39
59
|
});
|
|
40
60
|
|
|
41
|
-
|
|
61
|
+
// Respond with official schema
|
|
62
|
+
const out = {
|
|
63
|
+
commit: {
|
|
64
|
+
cid: commitCid,
|
|
65
|
+
rev,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return new Response(JSON.stringify(out), {
|
|
42
70
|
headers: { 'Content-Type': 'application/json' },
|
|
43
71
|
});
|
|
44
72
|
}
|
|
@@ -10,8 +10,8 @@ export const prerender = false;
|
|
|
10
10
|
export async function GET({ locals, url }: APIContext) {
|
|
11
11
|
const { env } = locals.runtime;
|
|
12
12
|
|
|
13
|
-
const repo = url.searchParams.get('repo') || env.PDS_DID
|
|
14
|
-
const did = env.PDS_DID
|
|
13
|
+
const repo = url.searchParams.get('repo') || (env.PDS_DID as string);
|
|
14
|
+
const did = env.PDS_DID as string;
|
|
15
15
|
const handle = env.PDS_HANDLE || 'user.example.com';
|
|
16
16
|
|
|
17
17
|
// Get repo root to check if repo exists
|