@alteran/astro 0.7.7 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +25 -25
  2. package/migrations/0010_eminent_klaw.sql +37 -0
  3. package/migrations/0011_chief_darwin.sql +31 -0
  4. package/migrations/0012_backfill_blob_usage.sql +39 -0
  5. package/migrations/meta/0010_snapshot.json +790 -0
  6. package/migrations/meta/0011_snapshot.json +813 -0
  7. package/migrations/meta/_journal.json +22 -1
  8. package/package.json +24 -41
  9. package/src/db/blob.ts +323 -0
  10. package/src/db/dal.ts +224 -78
  11. package/src/db/repo.ts +205 -25
  12. package/src/db/schema.ts +14 -5
  13. package/src/handlers/debug.ts +4 -3
  14. package/src/lib/appview/auth-policy.ts +7 -24
  15. package/src/lib/appview/proxy.ts +56 -23
  16. package/src/lib/appview/types.ts +1 -6
  17. package/src/lib/auth-scope.ts +399 -0
  18. package/src/lib/auth.ts +40 -39
  19. package/src/lib/commit.ts +37 -15
  20. package/src/lib/did-document.ts +4 -5
  21. package/src/lib/jwt.ts +3 -1
  22. package/src/lib/mime.ts +9 -0
  23. package/src/lib/oauth/resource.ts +49 -0
  24. package/src/lib/preference-policy.ts +45 -0
  25. package/src/lib/preferences.ts +0 -4
  26. package/src/lib/public-host.ts +127 -0
  27. package/src/lib/ratelimit.ts +37 -12
  28. package/src/lib/relay.ts +7 -27
  29. package/src/lib/repo-write-blob-constraints.ts +141 -0
  30. package/src/lib/repo-write-data.ts +195 -0
  31. package/src/lib/repo-write-error.ts +46 -0
  32. package/src/lib/repo-write-validation.ts +463 -0
  33. package/src/lib/session-tokens.ts +22 -5
  34. package/src/lib/unsupported-routes.ts +32 -0
  35. package/src/lib/util.ts +57 -2
  36. package/src/pages/.well-known/atproto-did.ts +15 -3
  37. package/src/pages/.well-known/did.json.ts +13 -7
  38. package/src/pages/debug/db/bootstrap.ts +4 -3
  39. package/src/pages/debug/gc/blobs.ts +11 -8
  40. package/src/pages/debug/record.ts +11 -0
  41. package/src/pages/xrpc/[...nsid].ts +17 -9
  42. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +9 -3
  43. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +17 -4
  44. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +4 -2
  45. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +4 -2
  46. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +4 -2
  47. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +10 -6
  48. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +4 -3
  49. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +13 -5
  50. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +4 -2
  51. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +4 -2
  52. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +12 -36
  53. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +90 -139
  54. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +74 -47
  55. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +119 -46
  56. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +21 -20
  57. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +6 -1
  58. package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +4 -2
  59. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +84 -47
  60. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +199 -78
  61. package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +4 -2
  62. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +88 -21
  63. package/src/pages/xrpc/com.atproto.server.getSession.ts +3 -13
  64. package/src/pages/xrpc/com.atproto.sync.getBlob.ts +92 -74
  65. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +45 -23
  66. package/src/services/car.ts +13 -0
  67. package/src/services/repo/apply-prepared-writes.ts +185 -0
  68. package/src/services/repo/blob-refs.ts +48 -0
  69. package/src/services/repo/blockstore-ops.ts +59 -17
  70. package/src/services/repo/list-blobs.ts +43 -0
  71. package/src/services/repo-manager.ts +221 -78
  72. package/src/worker/runtime.ts +1 -1
  73. package/src/worker/sequencer/upgrade.ts +4 -1
@@ -2,8 +2,13 @@ import type { APIContext } from 'astro';
2
2
  import { errorMessage } from '../../lib/errors';
3
3
  import { withCache, CACHE_CONFIGS } from '../../lib/cache';
4
4
  import { resolveSecret } from '../../lib/secrets';
5
- import { Secp256k1Keypair } from '@atproto/crypto';
6
- import { formatMultikey } from '@atproto/crypto/dist/did';
5
+ import { Secp256k1Keypair, formatMultikey } from '@atproto/crypto';
6
+ import {
7
+ canonicalPdsOrigin,
8
+ configuredDid,
9
+ configuredHandle,
10
+ validAtprotoHandle,
11
+ } from '../../lib/public-host';
7
12
 
8
13
  export const prerender = false;
9
14
 
@@ -13,9 +18,10 @@ export async function GET({ locals, request }: APIContext) {
13
18
  return withCache(
14
19
  request,
15
20
  async () => {
16
- const did = env.PDS_DID ?? 'did:example:single-user';
17
- const handle = env.PDS_HANDLE ?? 'user.example.com';
18
- const hostname = env.PDS_HOSTNAME ?? new URL(request.url).hostname;
21
+ const did = await configuredDid(env);
22
+ const handle = await configuredHandle(env);
23
+ const claimedHandle = validAtprotoHandle(handle);
24
+ const serviceEndpoint = await canonicalPdsOrigin(env);
19
25
 
20
26
  let publicKeyMultibase: string | undefined;
21
27
  let signingKeyError: string | undefined;
@@ -59,13 +65,13 @@ export async function GET({ locals, request }: APIContext) {
59
65
  'https://w3id.org/security/multikey/v1',
60
66
  ],
61
67
  id: did,
62
- alsoKnownAs: [`at://${handle}`],
68
+ alsoKnownAs: claimedHandle ? [`at://${claimedHandle}`] : [],
63
69
  verificationMethod: verificationMethods,
64
70
  service: [
65
71
  {
66
72
  id: `${did}#atproto_pds`,
67
73
  type: 'AtprotoPersonalDataServer',
68
- serviceEndpoint: `https://${hostname}`,
74
+ serviceEndpoint,
69
75
  },
70
76
  ],
71
77
  };
@@ -13,11 +13,12 @@ export async function POST({ locals }: APIContext) {
13
13
  }
14
14
  const db = env.ALTERAN_DB;
15
15
  await db.exec("CREATE TABLE IF NOT EXISTS record (uri TEXT PRIMARY KEY, cid TEXT NOT NULL, json TEXT NOT NULL, created_at INTEGER DEFAULT (strftime('%s','now')));");
16
- await db.exec("CREATE TABLE IF NOT EXISTS blob (cid TEXT PRIMARY KEY, key TEXT NOT NULL, mime TEXT NOT NULL, size INTEGER NOT NULL);");
17
- await db.exec("CREATE TABLE IF NOT EXISTS blob_usage (record_uri TEXT NOT NULL, key TEXT NOT NULL);");
16
+ await db.exec("CREATE TABLE IF NOT EXISTS blob (cid TEXT NOT NULL, did TEXT NOT NULL, key TEXT NOT NULL, mime TEXT NOT NULL, size INTEGER NOT NULL, uploaded_at INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (did, cid));");
17
+ await db.exec("CREATE TABLE IF NOT EXISTS blob_usage (did TEXT NOT NULL, record_uri TEXT NOT NULL, key TEXT NOT NULL, PRIMARY KEY (did, record_uri, key));");
18
18
  await db.exec("CREATE TABLE IF NOT EXISTS repo_root (did TEXT PRIMARY KEY, commit_cid TEXT NOT NULL, rev INTEGER NOT NULL);");
19
19
  // Indexes
20
20
  await db.exec("CREATE INDEX IF NOT EXISTS record_cid_idx ON record(cid);");
21
- await db.exec("CREATE INDEX IF NOT EXISTS blob_usage_record_idx ON blob_usage(record_uri);");
21
+ await db.exec("CREATE INDEX IF NOT EXISTS blob_usage_record_idx ON blob_usage(did, record_uri);");
22
+ await db.exec("CREATE INDEX IF NOT EXISTS blob_usage_did_key_idx ON blob_usage(did, key);");
22
23
  return new Response('ok');
23
24
  }
@@ -1,16 +1,19 @@
1
1
  import type { APIContext } from 'astro';
2
- import { listOrphanBlobKeys, deleteBlobByKey } from '../../../db/dal';
2
+ import type { Env } from '../../../env';
3
+ import { deleteUnreferencedBlobKeys, listOrphanBlobRefs } from '../../../db/dal';
3
4
 
4
5
  export const prerender = false;
5
6
 
7
+ function isLocalDebugAllowed(env: Env): boolean {
8
+ const envName = (env as any).ENVIRONMENT as string | undefined;
9
+ const host = env.PDS_HOSTNAME as string | undefined;
10
+ return envName !== 'production' && (!host || host.includes('localhost') || host.startsWith('127.') || host === '::1');
11
+ }
12
+
6
13
  export async function POST({ locals }: APIContext) {
7
14
  const { env } = locals.runtime;
8
- const keys = await listOrphanBlobKeys(env);
9
- let deleted = 0;
10
- for (const key of keys) {
11
- await env.ALTERAN_BLOBS.delete(key).catch(() => {});
12
- await deleteBlobByKey(env, key);
13
- deleted++;
14
- }
15
+ if (!isLocalDebugAllowed(env)) return new Response('Not Found', { status: 404 });
16
+
17
+ const deleted = await deleteUnreferencedBlobKeys(env, await listOrphanBlobRefs(env));
15
18
  return new Response(JSON.stringify({ deleted }), { headers: { 'Content-Type': 'application/json' } });
16
19
  }
@@ -1,10 +1,19 @@
1
1
  import type { APIContext } from 'astro';
2
+ import type { Env } from '../../env';
2
3
  import { getRecord as dalGetRecord, putRecord as dalPutRecord } from '../../db/dal';
3
4
 
4
5
  export const prerender = false;
5
6
 
7
+ function isLocalDebugAllowed(env: Env): boolean {
8
+ const envName = (env as any).ENVIRONMENT as string | undefined;
9
+ const host = env.PDS_HOSTNAME as string | undefined;
10
+ return envName !== 'production' && (!host || host.includes('localhost') || host.startsWith('127.') || host === '::1');
11
+ }
12
+
6
13
  export async function GET({ locals, request }: APIContext) {
7
14
  const { env } = locals.runtime;
15
+ if (!isLocalDebugAllowed(env)) return new Response('Not Found', { status: 404 });
16
+
8
17
  const url = new URL(request.url);
9
18
  const uri = url.searchParams.get('uri');
10
19
  if (!uri) return new Response('missing uri', { status: 400 });
@@ -17,6 +26,8 @@ export async function GET({ locals, request }: APIContext) {
17
26
 
18
27
  export async function POST({ locals, request }: APIContext) {
19
28
  const { env } = locals.runtime;
29
+ if (!isLocalDebugAllowed(env)) return new Response('Not Found', { status: 404 });
30
+
20
31
  const body = (await request.json()) as { uri?: string; json?: unknown };
21
32
  const uri = body.uri;
22
33
  if (!uri) return new Response('missing uri', { status: 400 });
@@ -1,6 +1,10 @@
1
1
  import type { APIContext } from 'astro';
2
2
  import { proxyAppView } from '../../lib/appview';
3
3
  import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
4
+ import {
5
+ isSingleUserUnsupportedRoute,
6
+ unsupportedSingleUserRouteResponse,
7
+ } from '../../lib/unsupported-routes';
4
8
 
5
9
  export const prerender = false;
6
10
 
@@ -20,6 +24,19 @@ function nsidFromParams(params: Record<string, any>): string {
20
24
 
21
25
  async function handle({ locals, request, params }: APIContext): Promise<Response> {
22
26
  const { env } = locals.runtime;
27
+ const nsid = nsidFromParams(params).trim();
28
+ console.log('xrpc catchall invoked:', { nsid, url: request.url });
29
+ if (!nsid) {
30
+ return new Response(JSON.stringify({ error: 'NotFound' }), {
31
+ status: 404,
32
+ headers: { 'Content-Type': 'application/json' },
33
+ });
34
+ }
35
+
36
+ if (isSingleUserUnsupportedRoute(nsid)) {
37
+ return unsupportedSingleUserRouteResponse(nsid);
38
+ }
39
+
23
40
  let auth;
24
41
  try {
25
42
  auth = await authenticateRequest(request, env);
@@ -30,15 +47,6 @@ async function handle({ locals, request, params }: APIContext): Promise<Response
30
47
  throw error;
31
48
  }
32
49
 
33
- const nsid = nsidFromParams(params).trim();
34
- console.log('xrpc catchall invoked:', { nsid, url: request.url });
35
- if (!nsid) {
36
- return new Response(JSON.stringify({ error: 'NotFound' }), {
37
- status: 404,
38
- headers: { 'Content-Type': 'application/json' },
39
- });
40
- }
41
-
42
50
  if (!shouldProxy(nsid)) {
43
51
  return new Response(JSON.stringify({ error: 'NotImplemented' }), {
44
52
  status: 404,
@@ -1,13 +1,18 @@
1
1
  import type { APIContext } from 'astro';
2
- import { authErrorResponse, isAuthorized, unauthorized } from '../../lib/auth';
2
+ import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
3
+ import { canAccessActorPreferences } from '../../lib/auth-scope';
4
+ import { preferencesForAccess } from '../../lib/preference-policy';
3
5
  import { getActorPreferences } from '../../lib/preferences';
4
6
 
5
7
  export const prerender = false;
6
8
 
7
9
  export async function GET({ locals, request }: APIContext) {
8
10
  const { env } = locals.runtime;
11
+ let auth: NonNullable<Awaited<ReturnType<typeof authenticateRequest>>>;
9
12
  try {
10
- if (!(await isAuthorized(request, env))) return unauthorized();
13
+ const verified = await authenticateRequest(request, env);
14
+ if (!verified || !canAccessActorPreferences(verified.access)) return unauthorized();
15
+ auth = verified;
11
16
  } catch (error) {
12
17
  const handled = await authErrorResponse(env, error);
13
18
  if (handled) return handled;
@@ -15,7 +20,8 @@ export async function GET({ locals, request }: APIContext) {
15
20
  }
16
21
 
17
22
  const { preferences } = await getActorPreferences(env);
18
- return new Response(JSON.stringify({ preferences: Array.isArray(preferences) ? preferences : [] }), {
23
+ const visible = preferencesForAccess(Array.isArray(preferences) ? preferences : [], auth.access);
24
+ return new Response(JSON.stringify({ preferences: visible }), {
19
25
  headers: { 'Content-Type': 'application/json' },
20
26
  });
21
27
  }
@@ -1,15 +1,20 @@
1
1
  import type { APIContext } from 'astro';
2
2
  import { errorCode, errorMessage } from '../../lib/errors';
3
- import { authErrorResponse, isAuthorized, unauthorized } from '../../lib/auth';
3
+ import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
4
+ import { canAccessActorPreferences } from '../../lib/auth-scope';
5
+ import { hasAppPasswordRestrictedPreferences, preferencesForWrite } from '../../lib/preference-policy';
4
6
  import { readJsonBounded } from '../../lib/util';
5
- import { setActorPreferences } from '../../lib/preferences';
7
+ import { getActorPreferences, setActorPreferences } from '../../lib/preferences';
6
8
 
7
9
  export const prerender = false;
8
10
 
9
11
  export async function POST({ locals, request }: APIContext) {
10
12
  const { env } = locals.runtime;
13
+ let auth: NonNullable<Awaited<ReturnType<typeof authenticateRequest>>>;
11
14
  try {
12
- if (!(await isAuthorized(request, env))) return unauthorized();
15
+ const verified = await authenticateRequest(request, env);
16
+ if (!verified || !canAccessActorPreferences(verified.access)) return unauthorized();
17
+ auth = verified;
13
18
  } catch (error) {
14
19
  const handled = await authErrorResponse(env, error);
15
20
  if (handled) return handled;
@@ -27,7 +32,15 @@ export async function POST({ locals, request }: APIContext) {
27
32
  }
28
33
 
29
34
  const preferences = Array.isArray(body?.preferences) ? body.preferences : [];
30
- await setActorPreferences(env, preferences);
35
+ if (hasAppPasswordRestrictedPreferences(preferences, auth.access)) {
36
+ return new Response(
37
+ JSON.stringify({ error: 'Forbidden', message: 'App passwords cannot update restricted preferences' }),
38
+ { status: 403, headers: { 'Content-Type': 'application/json' } },
39
+ );
40
+ }
41
+ const existing = auth.access.isAppPassword ? (await getActorPreferences(env)).preferences : [];
42
+ const writable = preferencesForWrite(existing, preferences, auth.access);
43
+ await setActorPreferences(env, writable);
31
44
 
32
45
  return new Response(JSON.stringify({}), {
33
46
  headers: { 'Content-Type': 'application/json' },
@@ -1,12 +1,14 @@
1
1
  import type { APIContext } from 'astro';
2
- import { authErrorResponse, isAuthorized, unauthorized } from '../../lib/auth';
2
+ import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
3
+ import { canUseAppPasswordLevelAccess } from '../../lib/auth-scope';
3
4
 
4
5
  export const prerender = false;
5
6
 
6
7
  export async function GET({ locals, request }: APIContext) {
7
8
  const { env } = locals.runtime;
8
9
  try {
9
- if (!(await isAuthorized(request, env))) return unauthorized();
10
+ const auth = await authenticateRequest(request, env);
11
+ if (!auth || !canUseAppPasswordLevelAccess(auth.access)) return unauthorized();
10
12
  } catch (error) {
11
13
  const handled = await authErrorResponse(env, error);
12
14
  if (handled) return handled;
@@ -1,5 +1,6 @@
1
1
  import type { APIContext } from 'astro';
2
- import { authErrorResponse, isAuthorized, unauthorized } from '../../lib/auth';
2
+ import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
3
+ import { canAccessChat } from '../../lib/auth-scope';
3
4
  import { getPrimaryActor } from '../../lib/actor';
4
5
  import { listChatConvoLogs } from '../../lib/chat';
5
6
 
@@ -8,7 +9,8 @@ export const prerender = false;
8
9
  export async function GET({ locals, request }: APIContext) {
9
10
  const { env } = locals.runtime;
10
11
  try {
11
- if (!(await isAuthorized(request, env))) return unauthorized();
12
+ const auth = await authenticateRequest(request, env);
13
+ if (!auth || !canAccessChat(auth.access)) return unauthorized();
12
14
  } catch (error) {
13
15
  const handled = await authErrorResponse(env, error);
14
16
  if (handled) return handled;
@@ -1,5 +1,6 @@
1
1
  import type { APIContext } from 'astro';
2
- import { authErrorResponse, isAuthorized, unauthorized } from '../../lib/auth';
2
+ import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
3
+ import { canAccessChat } from '../../lib/auth-scope';
3
4
  import { listChatConvos } from '../../lib/chat';
4
5
  import { getPrimaryActor } from '../../lib/actor';
5
6
 
@@ -8,7 +9,8 @@ export const prerender = false;
8
9
  export async function GET({ locals, request }: APIContext) {
9
10
  const { env } = locals.runtime;
10
11
  try {
11
- if (!(await isAuthorized(request, env))) return unauthorized();
12
+ const auth = await authenticateRequest(request, env);
13
+ if (!auth || !canAccessChat(auth.access)) return unauthorized();
12
14
  } catch (error) {
13
15
  const handled = await authErrorResponse(env, error);
14
16
  if (handled) return handled;
@@ -1,6 +1,8 @@
1
1
  import type { APIContext } from 'astro';
2
- import { authErrorResponse, isAuthorized, unauthorized } from '../../lib/auth';
2
+ import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
3
+ import { canAccessFullAccount } from '../../lib/auth-scope';
3
4
  import { resolveSecret } from '../../lib/secrets';
5
+ import { canonicalPdsOrigin, configuredHandle, validAtprotoHandle } from '../../lib/public-host';
4
6
 
5
7
  export const prerender = false;
6
8
 
@@ -15,7 +17,8 @@ export async function GET({ locals, request }: APIContext) {
15
17
  const { env } = locals.runtime;
16
18
 
17
19
  try {
18
- if (!(await isAuthorized(request, env))) return unauthorized();
20
+ const auth = await authenticateRequest(request, env);
21
+ if (!auth || !canAccessFullAccount(auth.access)) return unauthorized();
19
22
  } catch (error) {
20
23
  const handled = await authErrorResponse(env, error);
21
24
  if (handled) return handled;
@@ -23,8 +26,9 @@ export async function GET({ locals, request }: APIContext) {
23
26
  }
24
27
 
25
28
  try {
26
- const handle = (await resolveSecret(env.PDS_HANDLE)) ?? 'example.com';
27
- const hostname = env.PDS_HOSTNAME ?? handle;
29
+ const handle = await configuredHandle(env);
30
+ const claimedHandle = validAtprotoHandle(handle);
31
+ const serviceEndpoint = await canonicalPdsOrigin(env);
28
32
 
29
33
  // Always ES256K: derive did:key from the secp256k1 signing key
30
34
  let didKey: string | undefined;
@@ -70,12 +74,12 @@ export async function GET({ locals, request }: APIContext) {
70
74
 
71
75
  const credentials = {
72
76
  rotationKeys,
73
- alsoKnownAs: [`at://${handle}`],
77
+ alsoKnownAs: claimedHandle ? [`at://${claimedHandle}`] : [],
74
78
  verificationMethods: { atproto: didKey },
75
79
  services: {
76
80
  atproto_pds: {
77
81
  type: 'AtprotoPersonalDataServer',
78
- endpoint: `https://${hostname}`
82
+ endpoint: serviceEndpoint
79
83
  }
80
84
  }
81
85
  };
@@ -1,5 +1,6 @@
1
1
  import type { APIContext } from 'astro';
2
- import { authErrorResponse, isAuthorized, unauthorized } from '../../lib/auth';
2
+ import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
3
+ import { canAccessFullAccount } from '../../lib/auth-scope';
3
4
 
4
5
  export const prerender = false;
5
6
 
@@ -16,7 +17,8 @@ export async function POST({ locals, request }: APIContext) {
16
17
  const { env } = locals.runtime;
17
18
 
18
19
  try {
19
- if (!(await isAuthorized(request, env))) {
20
+ const auth = await authenticateRequest(request, env);
21
+ if (!auth || !canAccessFullAccount(auth.access)) {
20
22
  return unauthorized();
21
23
  }
22
24
  } catch (error) {
@@ -27,4 +29,3 @@ export async function POST({ locals, request }: APIContext) {
27
29
 
28
30
  return new Response(null, { status: 200 });
29
31
  }
30
-
@@ -1,5 +1,6 @@
1
1
  import type { APIContext } from 'astro';
2
2
  import { getAppViewConfig } from '../../lib/appview';
3
+ import { configuredAtprotoHandle, configuredDid, validAtprotoHandle } from '../../lib/public-host';
3
4
 
4
5
  export const prerender = false;
5
6
 
@@ -11,8 +12,6 @@ export async function GET({ locals, url }: APIContext) {
11
12
  const { env } = locals.runtime;
12
13
 
13
14
  const handle = url.searchParams.get('handle');
14
- const configuredHandle = String(env.PDS_HANDLE || 'user.example.com');
15
- const did = String(env.PDS_DID || 'did:example:single-user');
16
15
 
17
16
  if (!handle) {
18
17
  return new Response(
@@ -21,10 +20,19 @@ export async function GET({ locals, url }: APIContext) {
21
20
  );
22
21
  }
23
22
 
23
+ const normalizedHandle = validAtprotoHandle(handle);
24
+ if (!normalizedHandle) {
25
+ return new Response(
26
+ JSON.stringify({ error: 'InvalidRequest', message: 'Unable to resolve handle' }),
27
+ { status: 400, headers: { 'Content-Type': 'application/json' } },
28
+ );
29
+ }
30
+
24
31
  // Single-user PDS: resolve the local configured handle directly
25
- if (handle.toLowerCase() === configuredHandle.toLowerCase()) {
32
+ const configuredHandle = await configuredAtprotoHandle(env);
33
+ if (configuredHandle && normalizedHandle === configuredHandle) {
26
34
  return new Response(
27
- JSON.stringify({ did }),
35
+ JSON.stringify({ did: await configuredDid(env) }),
28
36
  {
29
37
  status: 200,
30
38
  headers: { 'Content-Type': 'application/json' },
@@ -38,7 +46,7 @@ export async function GET({ locals, url }: APIContext) {
38
46
  const app = getAppViewConfig(env);
39
47
  const base = app?.url || 'https://api.bsky.app';
40
48
  const upstream = new URL('/xrpc/com.atproto.identity.resolveHandle', base);
41
- upstream.searchParams.set('handle', handle);
49
+ upstream.searchParams.set('handle', normalizedHandle);
42
50
 
43
51
  const response = await fetch(upstream.toString(), {
44
52
  headers: { accept: 'application/json' },
@@ -1,5 +1,6 @@
1
1
  import type { APIContext } from 'astro';
2
- import { authErrorResponse, isAuthorized, unauthorized } from '../../lib/auth';
2
+ import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
3
+ import { canAccessFullAccount } from '../../lib/auth-scope';
3
4
  import { resolveSecret } from '../../lib/secrets';
4
5
 
5
6
  export const prerender = false;
@@ -16,7 +17,8 @@ export async function POST({ locals, request }: APIContext) {
16
17
  const { env } = locals.runtime;
17
18
 
18
19
  try {
19
- if (!(await isAuthorized(request, env))) return unauthorized();
20
+ const auth = await authenticateRequest(request, env);
21
+ if (!auth || !canAccessFullAccount(auth.access)) return unauthorized();
20
22
  } catch (error) {
21
23
  const handled = await authErrorResponse(env, error);
22
24
  if (handled) return handled;
@@ -1,6 +1,7 @@
1
1
  import type { APIContext } from 'astro';
2
2
  import { errorMessage } from '../../lib/errors';
3
- import { authErrorResponse, isAuthorized, unauthorized } from '../../lib/auth';
3
+ import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
4
+ import { canAccessFullAccount } from '../../lib/auth-scope';
4
5
  import { resolveSecret } from '../../lib/secrets';
5
6
 
6
7
  export const prerender = false;
@@ -16,7 +17,8 @@ export async function POST({ locals, request }: APIContext) {
16
17
  const { env } = locals.runtime;
17
18
 
18
19
  try {
19
- if (!(await isAuthorized(request, env))) return unauthorized();
20
+ const auth = await authenticateRequest(request, env);
21
+ if (!auth || !canAccessFullAccount(auth.access)) return unauthorized();
20
22
  } catch (error) {
21
23
  const handled = await authErrorResponse(env, error);
22
24
  if (handled) return handled;
@@ -1,6 +1,4 @@
1
1
  import type { APIContext } from 'astro';
2
- import { readJson } from '../../lib/util';
3
-
4
2
  export const prerender = false;
5
3
 
6
4
  /**
@@ -8,38 +6,16 @@ export const prerender = false;
8
6
  * Update the handle for the repository
9
7
  */
10
8
  export async function POST({ locals, request }: APIContext) {
11
- const { env } = locals.runtime;
12
-
13
- try {
14
- const body = (await readJson(request)) as { handle?: string };
15
- const { handle } = body;
16
-
17
- if (!handle) {
18
- return new Response(
19
- JSON.stringify({ error: 'InvalidRequest', message: 'handle required' }),
20
- { status: 400, headers: { 'Content-Type': 'application/json' } }
21
- );
22
- }
23
-
24
- // TODO: Implement handle verification (DNS TXT or HTTP)
25
- // TODO: Update PDS_HANDLE configuration
26
- // For single-user PDS, this would require redeployment with new config
27
-
28
- return new Response(
29
- JSON.stringify({
30
- error: 'NotImplemented',
31
- message: 'Handle updates require PDS reconfiguration for single-user mode',
32
- }),
33
- {
34
- status: 501,
35
- headers: { 'Content-Type': 'application/json' },
36
- }
37
- );
38
- } catch (error) {
39
- console.error('updateHandle error:', error);
40
- return new Response(
41
- JSON.stringify({ error: 'InternalServerError', message: String(error) }),
42
- { status: 500, headers: { 'Content-Type': 'application/json' } }
43
- );
44
- }
9
+ void locals;
10
+ void request;
11
+ return new Response(
12
+ JSON.stringify({
13
+ error: 'NotImplemented',
14
+ message: 'Handle updates require PDS reconfiguration for single-user mode',
15
+ }),
16
+ {
17
+ status: 501,
18
+ headers: { 'Content-Type': 'application/json' },
19
+ },
20
+ );
45
21
  }