@alteran/astro 0.3.8 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/dal.ts +34 -23
- package/src/db/repo.ts +35 -35
- package/src/db/schema.ts +5 -1
- package/src/db/seed.ts +5 -13
- package/src/entrypoints/server.ts +2 -22
- package/src/handlers/root.ts +4 -4
- package/src/lib/account-state.ts +156 -0
- package/src/lib/actor.ts +28 -12
- 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 +2 -1
- package/src/lib/cache.ts +30 -4
- package/src/lib/chat.ts +14 -8
- 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 +9 -34
- 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 +12 -3
- 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/record.ts +1 -1
- package/src/pages/debug/sequencer.ts +28 -0
- 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/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 +31 -11
- 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 +71 -22
- 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 +1 -1
- package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
- package/src/services/car.ts +207 -55
- package/src/services/r2-blob-store.ts +1 -1
- package/src/services/repo/blockstore-ops.ts +29 -0
- package/src/services/repo/operations.ts +133 -0
- package/src/services/repo-manager.ts +202 -253
- package/src/worker/runtime.ts +53 -8
- 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 +288 -412
- package/types/env.d.ts +15 -3
- 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.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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { proxyAppView } from '../../lib/appview';
|
|
3
|
+
import { AuthTokenExpiredError, expiredToken, isAuthorized, unauthorized } from '../../lib/auth';
|
|
4
|
+
|
|
5
|
+
export const prerender = false;
|
|
6
|
+
|
|
7
|
+
function shouldProxy(nsid: string): boolean {
|
|
8
|
+
return (
|
|
9
|
+
nsid.startsWith('app.bsky.') ||
|
|
10
|
+
nsid.startsWith('chat.bsky.') ||
|
|
11
|
+
nsid.startsWith('tools.ozone.')
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function nsidFromParams(params: Record<string, any>): string {
|
|
16
|
+
const p = (params as any).nsid;
|
|
17
|
+
if (Array.isArray(p)) return p.join('');
|
|
18
|
+
return typeof p === 'string' ? p : '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function handle({ locals, request, params }: APIContext): Promise<Response> {
|
|
22
|
+
const { env } = locals.runtime;
|
|
23
|
+
try {
|
|
24
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof AuthTokenExpiredError) {
|
|
27
|
+
return expiredToken();
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const nsid = nsidFromParams(params).trim();
|
|
33
|
+
console.log('xrpc catchall invoked:', { nsid, url: request.url });
|
|
34
|
+
if (!nsid) {
|
|
35
|
+
return new Response(JSON.stringify({ error: 'NotFound' }), {
|
|
36
|
+
status: 404,
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!shouldProxy(nsid)) {
|
|
42
|
+
return new Response(JSON.stringify({ error: 'NotImplemented' }), {
|
|
43
|
+
status: 404,
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return proxyAppView({ request, env, lxm: nsid });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function GET(ctx: APIContext) {
|
|
52
|
+
return handle(ctx);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function HEAD(ctx: APIContext) {
|
|
56
|
+
return handle(ctx);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function POST(ctx: APIContext) {
|
|
60
|
+
return handle(ctx);
|
|
61
|
+
}
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import {
|
|
3
|
-
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
2
|
+
import { AuthTokenExpiredError, expiredToken, isAuthorized, unauthorized } from '../../lib/auth';
|
|
4
3
|
import { getActorPreferences } from '../../lib/preferences';
|
|
5
4
|
|
|
6
5
|
export const prerender = false;
|
|
7
6
|
|
|
8
7
|
export async function GET({ locals, request }: APIContext) {
|
|
9
8
|
const { env } = locals.runtime;
|
|
10
|
-
|
|
9
|
+
try {
|
|
10
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error instanceof AuthTokenExpiredError) {
|
|
13
|
+
return expiredToken();
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
lxm: 'app.bsky.actor.getPreferences',
|
|
16
|
-
fallback: async () => {
|
|
17
|
-
const { preferences } = await getActorPreferences(env);
|
|
18
|
-
return new Response(JSON.stringify({ preferences }), {
|
|
19
|
-
headers: { 'Content-Type': 'application/json' },
|
|
20
|
-
});
|
|
21
|
-
},
|
|
18
|
+
const { preferences } = await getActorPreferences(env);
|
|
19
|
+
return new Response(JSON.stringify({ preferences: Array.isArray(preferences) ? preferences : [] }), {
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
21
|
});
|
|
23
22
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import {
|
|
3
|
-
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
2
|
+
import { errorCode, errorMessage } from '../../lib/errors';
|
|
3
|
+
import { AuthTokenExpiredError, expiredToken, isAuthorized, unauthorized } from '../../lib/auth';
|
|
4
4
|
import { readJsonBounded } from '../../lib/util';
|
|
5
5
|
import { setActorPreferences } from '../../lib/preferences';
|
|
6
6
|
|
|
@@ -8,29 +8,29 @@ export const prerender = false;
|
|
|
8
8
|
|
|
9
9
|
export async function POST({ locals, request }: APIContext) {
|
|
10
10
|
const { env } = locals.runtime;
|
|
11
|
-
|
|
11
|
+
try {
|
|
12
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error instanceof AuthTokenExpiredError) {
|
|
15
|
+
return expiredToken();
|
|
16
|
+
}
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
env,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (err?.code === 'PayloadTooLarge') {
|
|
23
|
-
return new Response(JSON.stringify({ error: 'PayloadTooLarge' }), { status: 413 });
|
|
24
|
-
}
|
|
25
|
-
return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
|
|
26
|
-
}
|
|
20
|
+
let body: any;
|
|
21
|
+
try {
|
|
22
|
+
body = await readJsonBounded(env, request);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (errorCode(error) === 'PayloadTooLarge') {
|
|
25
|
+
return new Response(JSON.stringify({ error: 'PayloadTooLarge' }), { status: 413 });
|
|
26
|
+
}
|
|
27
|
+
return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
|
|
28
|
+
}
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
const preferences = Array.isArray(body?.preferences) ? body.preferences : [];
|
|
31
|
+
await setActorPreferences(env, preferences);
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
});
|
|
34
|
-
},
|
|
33
|
+
return new Response(JSON.stringify({}), {
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
35
|
});
|
|
36
36
|
}
|
|
@@ -1,11 +1,18 @@
|
|
|
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
|
|
|
6
6
|
export async function GET({ locals, request }: APIContext) {
|
|
7
7
|
const { env } = locals.runtime;
|
|
8
|
-
|
|
8
|
+
try {
|
|
9
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (error instanceof AuthTokenExpiredError) {
|
|
12
|
+
return expiredToken();
|
|
13
|
+
}
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
9
16
|
|
|
10
17
|
return new Response(
|
|
11
18
|
JSON.stringify({
|
|
@@ -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 { getPrimaryActor } from '../../lib/actor';
|
|
4
4
|
import { listChatConvoLogs } from '../../lib/chat';
|
|
5
5
|
|
|
@@ -7,7 +7,14 @@ export const prerender = false;
|
|
|
7
7
|
|
|
8
8
|
export async function GET({ locals, request }: APIContext) {
|
|
9
9
|
const { env } = locals.runtime;
|
|
10
|
-
|
|
10
|
+
try {
|
|
11
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error instanceof AuthTokenExpiredError) {
|
|
14
|
+
return expiredToken();
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
11
18
|
|
|
12
19
|
const url = new URL(request.url);
|
|
13
20
|
const cursorParam = url.searchParams.get('cursor');
|
|
@@ -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 { listChatConvos } from '../../lib/chat';
|
|
4
4
|
import { getPrimaryActor } from '../../lib/actor';
|
|
5
5
|
|
|
@@ -7,7 +7,14 @@ export const prerender = false;
|
|
|
7
7
|
|
|
8
8
|
export async function GET({ locals, request }: APIContext) {
|
|
9
9
|
const { env } = locals.runtime;
|
|
10
|
-
|
|
10
|
+
try {
|
|
11
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error instanceof AuthTokenExpiredError) {
|
|
14
|
+
return expiredToken();
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
11
18
|
|
|
12
19
|
const url = new URL(request.url);
|
|
13
20
|
const limitInput = Number.parseInt(url.searchParams.get('limit') ?? '', 10);
|
|
@@ -1,7 +1,6 @@
|
|
|
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
|
-
import * as uint8arrays from 'uint8arrays';
|
|
5
4
|
|
|
6
5
|
export const prerender = false;
|
|
7
6
|
|
|
@@ -15,49 +14,53 @@ export const prerender = false;
|
|
|
15
14
|
export async function GET({ locals, request }: APIContext) {
|
|
16
15
|
const { env } = locals.runtime;
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
try {
|
|
18
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (error instanceof AuthTokenExpiredError) {
|
|
21
|
+
return expiredToken();
|
|
22
|
+
}
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
19
25
|
|
|
20
26
|
try {
|
|
21
27
|
const handle = (await resolveSecret(env.PDS_HANDLE)) ?? 'example.com';
|
|
22
28
|
const hostname = env.PDS_HOSTNAME ?? handle;
|
|
23
29
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
// Always ES256K: derive did:key from the secp256k1 signing key
|
|
31
|
+
let didKey: string | undefined;
|
|
32
|
+
const priv = (await resolveSecret(env.REPO_SIGNING_KEY))?.trim();
|
|
33
|
+
if (!priv) {
|
|
27
34
|
return new Response(
|
|
28
|
-
JSON.stringify({
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
JSON.stringify({ error: 'InvalidRequest', message: 'REPO_SIGNING_KEY not configured for ES256K' }),
|
|
36
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const { Secp256k1Keypair } = await import('@atproto/crypto');
|
|
41
|
+
let keypair: { did: () => string };
|
|
42
|
+
if (/^[0-9a-fA-F]{64}$/.test(priv)) {
|
|
43
|
+
keypair = await Secp256k1Keypair.import(priv);
|
|
44
|
+
} else {
|
|
45
|
+
const bin = atob(priv.replace(/\s+/g, ''));
|
|
46
|
+
const bytes = new Uint8Array(bin.length);
|
|
47
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
48
|
+
keypair = await Secp256k1Keypair.import(bytes);
|
|
49
|
+
}
|
|
50
|
+
didKey = keypair.did();
|
|
51
|
+
} catch (keypairError) {
|
|
52
|
+
console.error('REPO_SIGNING_KEY did:key derivation failed:', keypairError);
|
|
53
|
+
return new Response(
|
|
54
|
+
JSON.stringify({ error: 'InvalidRequest', message: 'Failed to derive secp256k1 did:key from REPO_SIGNING_KEY' }),
|
|
55
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } },
|
|
33
56
|
);
|
|
34
57
|
}
|
|
35
|
-
|
|
36
|
-
// Import Ed25519 private key from PKCS#8 base64
|
|
37
|
-
const b64 = signingKeyBase64.replace(/\s+/g, '');
|
|
38
|
-
const bin = atob(b64);
|
|
39
|
-
const pkcs8 = new Uint8Array(bin.length);
|
|
40
|
-
for (let i = 0; i < bin.length; i++) pkcs8[i] = bin.charCodeAt(i);
|
|
41
|
-
|
|
42
|
-
// Ed25519 PKCS#8 format: the public key is the last 32 bytes of the private key section
|
|
43
|
-
// PKCS#8 structure for Ed25519:
|
|
44
|
-
// - Header (16 bytes)
|
|
45
|
-
// - Private key (32 bytes)
|
|
46
|
-
// - Public key (32 bytes)
|
|
47
|
-
// Total: 80 bytes for unencrypted PKCS#8
|
|
48
|
-
const publicKeyBytes = pkcs8.slice(-32);
|
|
49
|
-
|
|
50
|
-
// Create did:key from public key
|
|
51
|
-
// Ed25519 multicodec prefix is 0xed01
|
|
52
|
-
const multicodecPrefix = new Uint8Array([0xed, 0x01]);
|
|
53
|
-
const multicodecKey = new Uint8Array(multicodecPrefix.length + publicKeyBytes.length);
|
|
54
|
-
multicodecKey.set(multicodecPrefix);
|
|
55
|
-
multicodecKey.set(publicKeyBytes, multicodecPrefix.length);
|
|
56
|
-
|
|
57
|
-
const didKey = 'did:key:z' + uint8arrays.toString(multicodecKey, 'base58btc');
|
|
58
58
|
|
|
59
59
|
// Get current PLC data to preserve rotation keys
|
|
60
|
-
const did =
|
|
60
|
+
const did = await resolveSecret(env.PDS_DID);
|
|
61
|
+
if (!did) {
|
|
62
|
+
return new Response(JSON.stringify({ error: 'InvalidRequest', message: 'PDS_DID is not configured' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
63
|
+
}
|
|
61
64
|
const plcResponse = await fetch(`https://plc.directory/${did}/data`);
|
|
62
65
|
|
|
63
66
|
let rotationKeys: string[] = [];
|
|
@@ -69,9 +72,7 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
69
72
|
const credentials = {
|
|
70
73
|
rotationKeys,
|
|
71
74
|
alsoKnownAs: [`at://${handle}`],
|
|
72
|
-
verificationMethods: {
|
|
73
|
-
atproto: didKey
|
|
74
|
-
},
|
|
75
|
+
verificationMethods: { atproto: didKey },
|
|
75
76
|
services: {
|
|
76
77
|
atproto_pds: {
|
|
77
78
|
type: 'AtprotoPersonalDataServer',
|
|
@@ -84,14 +85,15 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
84
85
|
JSON.stringify(credentials),
|
|
85
86
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
86
87
|
);
|
|
87
|
-
} catch (error
|
|
88
|
+
} catch (error) {
|
|
88
89
|
console.error('Get recommended credentials error:', error);
|
|
90
|
+
const message = error instanceof Error ? error.message : 'Failed to get recommended credentials';
|
|
89
91
|
return new Response(
|
|
90
92
|
JSON.stringify({
|
|
91
93
|
error: 'InternalServerError',
|
|
92
|
-
message
|
|
94
|
+
message,
|
|
93
95
|
}),
|
|
94
96
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
95
97
|
);
|
|
96
98
|
}
|
|
97
|
-
}
|
|
99
|
+
}
|
|
@@ -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) {
|