@alteran/astro 0.3.9 → 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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -30
  3. package/index.js +34 -28
  4. package/migrations/0007_bored_spitfire.sql +26 -0
  5. package/migrations/0008_furry_ozymandias.sql +2 -0
  6. package/migrations/meta/0007_snapshot.json +534 -0
  7. package/migrations/meta/0008_snapshot.json +548 -0
  8. package/migrations/meta/_journal.json +14 -0
  9. package/package.json +10 -9
  10. package/src/app.ts +8 -4
  11. package/src/db/account.ts +25 -6
  12. package/src/db/dal.ts +34 -23
  13. package/src/db/repo.ts +35 -35
  14. package/src/db/schema.ts +5 -1
  15. package/src/db/seed.ts +5 -13
  16. package/src/entrypoints/server.ts +2 -22
  17. package/src/handlers/root.ts +4 -4
  18. package/src/lib/account-state.ts +156 -0
  19. package/src/lib/actor.ts +28 -12
  20. package/src/lib/appview/auth-policy.ts +66 -0
  21. package/src/lib/appview/did-resolver.ts +233 -0
  22. package/src/lib/appview/proxy.ts +221 -0
  23. package/src/lib/appview/service-config.ts +61 -0
  24. package/src/lib/appview/service-jwt.ts +93 -0
  25. package/src/lib/appview/types.ts +25 -0
  26. package/src/lib/appview.ts +5 -532
  27. package/src/lib/auth-errors.ts +24 -0
  28. package/src/lib/auth.ts +63 -15
  29. package/src/lib/blockstore-gc.ts +2 -1
  30. package/src/lib/cache.ts +30 -4
  31. package/src/lib/chat.ts +14 -8
  32. package/src/lib/commit.ts +26 -36
  33. package/src/lib/config.ts +26 -15
  34. package/src/lib/did-document.ts +32 -0
  35. package/src/lib/errors.ts +54 -0
  36. package/src/lib/feed.ts +18 -19
  37. package/src/lib/firehose/frames.ts +87 -47
  38. package/src/lib/firehose/validation.ts +3 -3
  39. package/src/lib/jwt.ts +85 -177
  40. package/src/lib/labeler.ts +43 -30
  41. package/src/lib/logger.ts +4 -0
  42. package/src/lib/mst/block-map.ts +172 -0
  43. package/src/lib/mst/blockstore.ts +56 -93
  44. package/src/lib/mst/index.ts +1 -0
  45. package/src/lib/mst/leaf.ts +25 -0
  46. package/src/lib/mst/mst.ts +81 -237
  47. package/src/lib/mst/serialize.ts +97 -0
  48. package/src/lib/mst/types.ts +21 -0
  49. package/src/lib/oauth/clients.ts +67 -0
  50. package/src/lib/oauth/dpop-errors.ts +15 -0
  51. package/src/lib/oauth/dpop.ts +150 -0
  52. package/src/lib/oauth/resource.ts +199 -0
  53. package/src/lib/oauth/store.ts +77 -0
  54. package/src/lib/preferences.ts +9 -34
  55. package/src/lib/refresh-session.ts +161 -0
  56. package/src/lib/relay.ts +10 -8
  57. package/src/lib/secrets.ts +6 -7
  58. package/src/lib/sequencer.ts +12 -3
  59. package/src/lib/service-auth.ts +184 -0
  60. package/src/lib/session-tokens.ts +28 -76
  61. package/src/lib/streaming-car.ts +3 -0
  62. package/src/lib/tracing.ts +4 -3
  63. package/src/lib/util.ts +65 -15
  64. package/src/middleware.ts +1 -1
  65. package/src/pages/.well-known/did.json.ts +27 -30
  66. package/src/pages/.well-known/oauth-authorization-server.ts +31 -0
  67. package/src/pages/.well-known/oauth-protected-resource.ts +22 -0
  68. package/src/pages/debug/record.ts +1 -1
  69. package/src/pages/debug/sequencer.ts +28 -0
  70. package/src/pages/oauth/authorize.ts +78 -0
  71. package/src/pages/oauth/consent.ts +80 -0
  72. package/src/pages/oauth/par.ts +121 -0
  73. package/src/pages/oauth/token.ts +158 -0
  74. package/src/pages/xrpc/[...nsid].ts +61 -0
  75. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +12 -13
  76. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +23 -23
  77. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +9 -2
  78. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +9 -2
  79. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +9 -2
  80. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +43 -41
  81. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +10 -3
  82. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +40 -9
  83. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +41 -29
  84. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +20 -6
  85. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +1 -1
  86. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +101 -11
  87. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +44 -14
  88. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +41 -13
  89. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +2 -2
  90. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +14 -1
  91. package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +14 -6
  92. package/src/pages/xrpc/com.atproto.repo.listRecords.ts +1 -1
  93. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +42 -14
  94. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +76 -15
  95. package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +20 -8
  96. package/src/pages/xrpc/com.atproto.server.createSession.ts +31 -11
  97. package/src/pages/xrpc/com.atproto.server.describeServer.ts +1 -1
  98. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +12 -5
  99. package/src/pages/xrpc/com.atproto.server.getSession.ts +22 -8
  100. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +30 -72
  101. package/src/pages/xrpc/com.atproto.sync.getBlob.ts +71 -22
  102. package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +1 -1
  103. package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +1 -1
  104. package/src/pages/xrpc/com.atproto.sync.getHead.ts +7 -2
  105. package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +1 -1
  106. package/src/pages/xrpc/com.atproto.sync.getRecord.ts +5 -27
  107. package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +1 -1
  108. package/src/pages/xrpc/com.atproto.sync.getRepo.ts +50 -5
  109. package/src/pages/xrpc/com.atproto.sync.getRepoStatus.ts +58 -0
  110. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +1 -1
  111. package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
  112. package/src/services/car.ts +207 -55
  113. package/src/services/r2-blob-store.ts +1 -1
  114. package/src/services/repo/blockstore-ops.ts +29 -0
  115. package/src/services/repo/operations.ts +133 -0
  116. package/src/services/repo-manager.ts +202 -253
  117. package/src/worker/runtime.ts +53 -8
  118. package/src/worker/sequencer/broadcast.ts +91 -0
  119. package/src/worker/sequencer/cid-helpers.ts +39 -0
  120. package/src/worker/sequencer/payload.ts +84 -0
  121. package/src/worker/sequencer/types.ts +36 -0
  122. package/src/worker/sequencer/upgrade.ts +141 -0
  123. package/src/worker/sequencer.ts +263 -405
  124. package/types/env.d.ts +15 -3
  125. package/src/pages/xrpc/app.bsky.actor.getProfile.ts +0 -49
  126. package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +0 -51
  127. package/src/pages/xrpc/app.bsky.feed.getActorFeeds.ts +0 -25
  128. package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +0 -42
  129. package/src/pages/xrpc/app.bsky.feed.getFeedGenerators.ts +0 -25
  130. package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +0 -37
  131. package/src/pages/xrpc/app.bsky.feed.getPosts.ts +0 -26
  132. package/src/pages/xrpc/app.bsky.feed.getSuggestedFeeds.ts +0 -23
  133. package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +0 -47
  134. package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +0 -29
  135. package/src/pages/xrpc/app.bsky.graph.getFollows.ts +0 -29
  136. package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +0 -20
  137. package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +0 -27
  138. package/src/pages/xrpc/app.bsky.unspecced.getSuggestedFeeds.ts +0 -23
@@ -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 { proxyAppView } from '../../lib/appview';
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
- if (!(await isAuthorized(request, env))) return unauthorized();
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
- return proxyAppView({
13
- request,
14
- env,
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 { proxyAppView } from '../../lib/appview';
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
- if (!(await isAuthorized(request, env))) return unauthorized();
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
- return proxyAppView({
14
- request,
15
- env,
16
- lxm: 'app.bsky.actor.putPreferences',
17
- fallback: async () => {
18
- let body: any;
19
- try {
20
- body = await readJsonBounded(env, request);
21
- } catch (err: any) {
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
- const preferences = Array.isArray(body?.preferences) ? body.preferences : [];
29
- await setActorPreferences(env, preferences);
30
+ const preferences = Array.isArray(body?.preferences) ? body.preferences : [];
31
+ await setActorPreferences(env, preferences);
30
32
 
31
- return new Response(JSON.stringify({}), {
32
- headers: { 'Content-Type': 'application/json' },
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
- if (!(await isAuthorized(request, env))) return unauthorized();
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
- if (!(await isAuthorized(request, env))) return unauthorized();
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
- if (!(await isAuthorized(request, env))) return unauthorized();
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
- if (!(await isAuthorized(request, env))) return unauthorized();
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
- // Load signing key (Ed25519 PKCS#8 base64)
25
- const signingKeyBase64 = await resolveSecret(env.REPO_SIGNING_KEY);
26
- if (!signingKeyBase64) {
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
- error: 'InvalidRequest',
30
- message: 'Signing key not configured'
31
- }),
32
- { status: 400, headers: { 'Content-Type': 'application/json' } }
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 = (await resolveSecret(env.PDS_DID)) ?? 'did:example:single-user';
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: any) {
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: error.message || 'Failed to get recommended credentials'
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
- if (!(await isAuthorized(request, env))) {
19
- return unauthorized();
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: only resolve if handle matches configured handle
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
- return new Response(
35
- JSON.stringify({ error: 'HandleNotFound' }),
36
- { status: 404, headers: { 'Content-Type': 'application/json' } }
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
- if (!(await isAuthorized(request, env))) return unauthorized();
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
- // Load PLC rotation key (hex-encoded secp256k1 private key).
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: any = await import('@ipld/dag-cbor');
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: any = await import('uint8arrays');
54
+ const u8a = await import('uint8arrays');
51
55
 
52
56
  const signer = await Secp256k1Keypair.import(privHex);
53
57
 
54
- // Fetch last op for prev CID
55
- const lastRes = await fetch(`https://plc.directory/${encodeURIComponent(did)}/log/last`);
56
- if (!lastRes.ok) {
57
- const text = await lastRes.text();
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 lastRes.json();
61
- if ((lastOp as any)?.type === 'plc_tombstone') {
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
- // Fetch current document data as defaults and to verify rotation key
69
- const dataRes = await fetch(`https://plc.directory/${encodeURIComponent(did)}/data`);
70
- if (!dataRes.ok) {
71
- const text = await dataRes.text();
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
- const doc = (await dataRes.json()) as any;
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
- if (!services.atproto_pds || typeof services.atproto_pds !== 'object') {
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 (!services.atproto_pds.type) {
85
- services.atproto_pds.type = 'AtprotoPersonalDataServer';
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 = (u8a.toString as any)(sig, 'base64url');
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: any) {
127
+ } catch (error) {
117
128
  console.error('signPlcOperation error:', error);
118
- return jsonErr(500, 'InternalServerError', error?.message || 'Failed to sign PLC operation');
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 { isAuthorized, unauthorized } from '../../lib/auth';
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
- if (!(await isAuthorized(request, env))) return unauthorized();
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 = (await resolveSecret(env.PDS_DID)) ?? 'did:example:single-user';
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: any) {
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.message || 'Failed to submit PLC operation'
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) {