@alteran/astro 0.1.13 → 0.3.0

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 (62) hide show
  1. package/README.md +28 -3
  2. package/index.js +2 -4
  3. package/migrations/0006_adorable_spectrum.sql +11 -0
  4. package/migrations/meta/0006_snapshot.json +429 -0
  5. package/migrations/meta/_journal.json +7 -0
  6. package/package.json +6 -3
  7. package/src/db/account.ts +145 -0
  8. package/src/db/dal.ts +27 -9
  9. package/src/db/repo.ts +9 -8
  10. package/src/db/schema.ts +29 -11
  11. package/src/lib/actor.ts +133 -0
  12. package/src/lib/appview.ts +508 -0
  13. package/src/lib/auth.ts +26 -3
  14. package/src/lib/blob-refs.ts +9 -13
  15. package/src/lib/chat.ts +238 -0
  16. package/src/lib/config.ts +15 -7
  17. package/src/lib/feed.ts +165 -0
  18. package/src/lib/jwt.ts +144 -47
  19. package/src/lib/labeler.ts +91 -0
  20. package/src/lib/mst/blockstore.ts +98 -14
  21. package/src/lib/password.ts +40 -0
  22. package/src/lib/preferences.ts +73 -0
  23. package/src/lib/relay.ts +101 -0
  24. package/src/lib/secrets.ts +4 -1
  25. package/src/lib/session-tokens.ts +202 -0
  26. package/src/lib/token-cleanup.ts +3 -12
  27. package/src/lib/util.ts +17 -2
  28. package/src/middleware.ts +20 -21
  29. package/src/pages/.well-known/did.json.ts +45 -32
  30. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +23 -0
  31. package/src/pages/xrpc/app.bsky.actor.getProfile.ts +34 -0
  32. package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +42 -0
  33. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +36 -0
  34. package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +42 -0
  35. package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +37 -0
  36. package/src/pages/xrpc/app.bsky.feed.getPosts.ts +26 -0
  37. package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +35 -0
  38. package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +29 -0
  39. package/src/pages/xrpc/app.bsky.graph.getFollows.ts +29 -0
  40. package/src/pages/xrpc/app.bsky.labeler.getServices.ts +29 -0
  41. package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +20 -0
  42. package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +27 -0
  43. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +19 -0
  44. package/src/pages/xrpc/app.bsky.unspecced.getConfig.ts +15 -0
  45. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +26 -0
  46. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +37 -0
  47. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +64 -66
  48. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +24 -0
  49. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +127 -0
  50. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +91 -0
  51. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +6 -2
  52. package/src/pages/xrpc/com.atproto.server.createSession.ts +36 -8
  53. package/src/pages/xrpc/com.atproto.server.describeServer.ts +37 -4
  54. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +64 -0
  55. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +55 -32
  56. package/src/services/repo-manager.ts +15 -6
  57. package/src/worker/runtime.ts +9 -0
  58. package/types/env.d.ts +10 -1
  59. package/src/pages/xrpc/com.atproto.repo.importRepo.ts +0 -142
  60. package/src/pages/xrpc/com.atproto.server.activateAccount.ts +0 -53
  61. package/src/pages/xrpc/com.atproto.server.createAccount.ts +0 -99
  62. package/src/pages/xrpc/com.atproto.server.deactivateAccount.ts +0 -53
package/src/lib/util.ts CHANGED
@@ -38,10 +38,25 @@ export function bearerToken(request: Request): string | null {
38
38
  }
39
39
 
40
40
  export function isAllowedMime(env: any, mime: string): boolean {
41
- const def = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif'];
41
+ const def = [
42
+ // Images
43
+ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif',
44
+ // Videos
45
+ 'video/mp4', 'video/mpeg', 'video/webm', 'video/quicktime',
46
+ // Audio
47
+ 'audio/mpeg', 'audio/mp4', 'audio/wav', 'audio/webm',
48
+ // JSON (for some Bluesky data)
49
+ 'application/json',
50
+ // Generic fallback
51
+ 'application/octet-stream'
52
+ ];
42
53
  const raw = (env.PDS_ALLOWED_MIME as string | undefined) ?? def.join(',');
43
54
  const set = new Set(raw.split(',').map((s) => s.trim()).filter(Boolean));
44
- return set.has(mime.toLowerCase());
55
+
56
+ // Extract base MIME type (remove charset and other parameters)
57
+ const baseMime = mime.toLowerCase().split(';')[0].trim();
58
+
59
+ return set.has(baseMime);
45
60
  }
46
61
 
47
62
  export function randomRkey(): string {
package/src/middleware.ts CHANGED
@@ -1,36 +1,35 @@
1
1
  import { defineMiddleware, sequence } from 'astro:middleware';
2
2
 
3
3
  const cors = defineMiddleware(async ({ locals, request }, next) => {
4
- const { env } = (locals as any).runtime ?? (locals as any);
5
- const corsOrigins = (env.PDS_CORS_ORIGIN ?? '*').split(',').map((s: string) => s.trim()).filter(Boolean);
6
- const origin = request.headers.get('origin') ?? '';
7
-
8
- // In production, never allow wildcard - require explicit origins
9
- const isProduction = env.PDS_HOSTNAME && !env.PDS_HOSTNAME.includes('localhost');
10
- const allowWildcard = !isProduction && corsOrigins.includes('*');
11
-
12
- // Check if origin is in allowlist
13
- const isAllowed = allowWildcard || corsOrigins.includes(origin);
4
+ // Match atproto CORS implementation: use wildcard for public endpoints
5
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
6
+ // For requests without credentials, "*" can be specified as a wildcard
7
+ // This is safer than reflecting the request origin and matches atproto standard
14
8
 
15
9
  if (request.method === 'OPTIONS') {
16
- if (!isAllowed) {
17
- return new Response('CORS origin not allowed', { status: 403 });
18
- }
19
-
10
+ // CORS preflight - match atproto PDS implementation
20
11
  const headers = new Headers({
21
- 'Access-Control-Allow-Origin': allowWildcard ? '*' : origin,
22
- 'Access-Control-Allow-Methods': 'GET, HEAD, POST, OPTIONS',
23
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
24
- 'Access-Control-Max-Age': '86400', // 24 hours
12
+ 'Access-Control-Allow-Origin': '*',
13
+ // Use wildcard for methods (atproto standard)
14
+ 'Access-Control-Allow-Methods': '*',
15
+ // Use wildcard for headers to allow atproto-accept-labelers and other custom headers
16
+ 'Access-Control-Allow-Headers': '*',
17
+ // Match atproto: 1 day max-age for CORS preflight cache
18
+ 'Access-Control-Max-Age': '86400',
25
19
  });
26
20
  return new Response(null, { status: 204, headers });
27
21
  }
28
22
 
29
23
  const response = await next();
30
24
 
31
- if (isAllowed) {
32
- response.headers.set('Access-Control-Allow-Origin', allowWildcard ? '*' : origin);
33
- response.headers.set('Vary', 'Origin');
25
+ // Set CORS headers on all responses (atproto standard)
26
+ response.headers.set('Access-Control-Allow-Origin', '*');
27
+
28
+ // Expose DPoP-Nonce header for OAuth clients (atproto standard)
29
+ // This allows clients to read the DPoP-Nonce header from responses
30
+ const dpopNonce = response.headers.get('DPoP-Nonce');
31
+ if (dpopNonce) {
32
+ response.headers.set('Access-Control-Expose-Headers', 'DPoP-Nonce');
34
33
  }
35
34
 
36
35
  return response;
@@ -1,18 +1,12 @@
1
1
  import type { APIContext } from 'astro';
2
2
  import { withCache, CACHE_CONFIGS } from '../../lib/cache';
3
3
  import { base58btc } from 'multiformats/bases/base58';
4
+ import { resolveSecret } from '../../lib/secrets';
5
+ import { Secp256k1Keypair } from '@atproto/crypto';
6
+ import { formatMultikey } from '@atproto/crypto/dist/did';
4
7
 
5
8
  export const prerender = false;
6
9
 
7
- /**
8
- * DID Document endpoint for did:web
9
- *
10
- * Returns a DID document with:
11
- * - Service endpoints (PDS, firehose)
12
- * - Verification methods (signing key)
13
- *
14
- * Spec: https://w3c-ccg.github.io/did-method-web/
15
- */
16
10
  export async function GET({ locals, request }: APIContext) {
17
11
  const { env } = locals.runtime;
18
12
 
@@ -23,39 +17,57 @@ export async function GET({ locals, request }: APIContext) {
23
17
  const handle = env.PDS_HANDLE ?? 'user.example.com';
24
18
  const hostname = env.PDS_HOSTNAME ?? new URL(request.url).hostname;
25
19
 
26
- // Public repository signing key (raw 32-byte Ed25519) base64-encoded
27
- const pubKeyB64: string | undefined = (env as any).REPO_SIGNING_PUBLIC_KEY;
28
20
  let publicKeyMultibase: string | undefined;
29
- if (pubKeyB64) {
21
+
22
+ const serviceKeyHex = await resolveSecret(env.PDS_SERVICE_SIGNING_KEY_HEX as any);
23
+ if (serviceKeyHex) {
30
24
  try {
31
- const bin = atob(pubKeyB64.replace(/\s+/g, ''));
32
- const raw = new Uint8Array(bin.length);
33
- for (let i = 0; i < bin.length; i++) raw[i] = bin.charCodeAt(i);
34
- if (raw.byteLength === 32) {
35
- // multicodec: ed25519-pub = 0xED 0x01 prefix
36
- const prefixed = new Uint8Array(2 + raw.byteLength);
37
- prefixed[0] = 0xed; prefixed[1] = 0x01; prefixed.set(raw, 2);
38
- publicKeyMultibase = base58btc.encode(prefixed);
25
+ const keypair = await Secp256k1Keypair.import(serviceKeyHex.trim());
26
+ publicKeyMultibase = formatMultikey(keypair.jwtAlg, keypair.publicKeyBytes());
27
+ } catch (error) {
28
+ console.warn('Failed to encode service signing key', error);
29
+ }
30
+ }
31
+
32
+ if (!publicKeyMultibase) {
33
+ const repoPubKeyB64 = await resolveSecret((env as any).REPO_SIGNING_KEY_PUBLIC);
34
+ if (repoPubKeyB64) {
35
+ try {
36
+ const bin = atob(repoPubKeyB64.replace(/\s+/g, ''));
37
+ const raw = new Uint8Array(bin.length);
38
+ for (let i = 0; i < bin.length; i++) raw[i] = bin.charCodeAt(i);
39
+ if (raw.byteLength === 32) {
40
+ const prefixed = new Uint8Array(2 + raw.byteLength);
41
+ prefixed[0] = 0xed;
42
+ prefixed[1] = 0x01;
43
+ prefixed.set(raw, 2);
44
+ publicKeyMultibase = base58btc.encode(prefixed);
45
+ }
46
+ } catch (error) {
47
+ console.warn('Failed to encode repo signing key', error);
39
48
  }
40
- } catch {}
49
+ }
41
50
  }
42
51
 
43
- // Build DID document
52
+ const verificationMethods = publicKeyMultibase
53
+ ? [
54
+ {
55
+ id: `${did}#atproto`,
56
+ type: 'Multikey',
57
+ controller: did,
58
+ publicKeyMultibase,
59
+ },
60
+ ]
61
+ : [];
62
+
44
63
  const didDocument = {
45
64
  '@context': [
46
65
  'https://www.w3.org/ns/did/v1',
47
- 'https://w3id.org/security/suites/ed25519-2020/v1',
66
+ 'https://w3id.org/security/multikey/v1',
48
67
  ],
49
68
  id: did,
50
69
  alsoKnownAs: [`at://${handle}`],
51
- verificationMethod: publicKeyMultibase ? [
52
- {
53
- id: `${did}#atproto`,
54
- type: 'Ed25519VerificationKey2020',
55
- controller: did,
56
- publicKeyMultibase,
57
- },
58
- ] : [],
70
+ verificationMethod: verificationMethods,
59
71
  service: [
60
72
  {
61
73
  id: `${did}#atproto_pds`,
@@ -71,6 +83,7 @@ export async function GET({ locals, request }: APIContext) {
71
83
  },
72
84
  });
73
85
  },
74
- CACHE_CONFIGS.DID_DOCUMENT
86
+ CACHE_CONFIGS.DID_DOCUMENT,
75
87
  );
76
88
  }
89
+
@@ -0,0 +1,23 @@
1
+ import type { APIContext } from 'astro';
2
+ import { proxyAppView } from '../../lib/appview';
3
+ import { isAuthorized, unauthorized } from '../../lib/auth';
4
+ import { getActorPreferences } from '../../lib/preferences';
5
+
6
+ export const prerender = false;
7
+
8
+ export async function GET({ locals, request }: APIContext) {
9
+ const { env } = locals.runtime;
10
+ if (!(await isAuthorized(request, env))) return unauthorized();
11
+
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
+ },
22
+ });
23
+ }
@@ -0,0 +1,34 @@
1
+ import type { APIContext } from 'astro';
2
+ import { proxyAppView } from '../../lib/appview';
3
+ import { buildProfileViewDetailed, getPrimaryActor, matchesPrimaryActor } from '../../lib/actor';
4
+ import { countPosts } from '../../lib/feed';
5
+ import { isAuthorized, unauthorized } from '../../lib/auth';
6
+
7
+ export const prerender = false;
8
+
9
+ export async function GET({ locals, request }: APIContext) {
10
+ const { env } = locals.runtime;
11
+ if (!(await isAuthorized(request, env))) return unauthorized();
12
+
13
+ return proxyAppView({
14
+ request,
15
+ env,
16
+ lxm: 'app.bsky.actor.getProfile',
17
+ fallback: async () => {
18
+ const url = new URL(request.url);
19
+ const identifier = url.searchParams.get('actor');
20
+ const actor = await getPrimaryActor(env);
21
+ if (!matchesPrimaryActor(identifier, actor)) {
22
+ return new Response(JSON.stringify({ error: 'ProfileNotFound' }), { status: 404 });
23
+ }
24
+ const profile = buildProfileViewDetailed(actor, {
25
+ followers: 0,
26
+ follows: 0,
27
+ posts: await countPosts(env),
28
+ });
29
+ return new Response(JSON.stringify(profile), {
30
+ headers: { 'Content-Type': 'application/json' },
31
+ });
32
+ },
33
+ });
34
+ }
@@ -0,0 +1,42 @@
1
+ import type { APIContext } from 'astro';
2
+ import { proxyAppView } from '../../lib/appview';
3
+ import {
4
+ buildProfileViewDetailed,
5
+ getPrimaryActor,
6
+ matchesPrimaryActor,
7
+ } from '../../lib/actor';
8
+ import { countPosts } from '../../lib/feed';
9
+ import { isAuthorized, unauthorized } from '../../lib/auth';
10
+
11
+ export const prerender = false;
12
+
13
+ export async function GET({ locals, request }: APIContext) {
14
+ const { env } = locals.runtime;
15
+ if (!(await isAuthorized(request, env))) return unauthorized();
16
+
17
+ return proxyAppView({
18
+ request,
19
+ env,
20
+ lxm: 'app.bsky.actor.getProfiles',
21
+ fallback: async () => {
22
+ const url = new URL(request.url);
23
+ const actors = url.searchParams.getAll('actors');
24
+ const actor = await getPrimaryActor(env);
25
+ const posts = await countPosts(env);
26
+
27
+ const profiles = actors
28
+ .filter((identifier) => matchesPrimaryActor(identifier, actor))
29
+ .map(() =>
30
+ buildProfileViewDetailed(actor, {
31
+ followers: 0,
32
+ follows: 0,
33
+ posts,
34
+ }),
35
+ );
36
+
37
+ return new Response(JSON.stringify({ profiles }), {
38
+ headers: { 'Content-Type': 'application/json' },
39
+ });
40
+ },
41
+ });
42
+ }
@@ -0,0 +1,36 @@
1
+ import type { APIContext } from 'astro';
2
+ import { proxyAppView } from '../../lib/appview';
3
+ import { isAuthorized, unauthorized } from '../../lib/auth';
4
+ import { readJsonBounded } from '../../lib/util';
5
+ import { setActorPreferences } from '../../lib/preferences';
6
+
7
+ export const prerender = false;
8
+
9
+ export async function POST({ locals, request }: APIContext) {
10
+ const { env } = locals.runtime;
11
+ if (!(await isAuthorized(request, env))) return unauthorized();
12
+
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
+ }
27
+
28
+ const preferences = Array.isArray(body?.preferences) ? body.preferences : [];
29
+ await setActorPreferences(env, preferences);
30
+
31
+ return new Response(JSON.stringify({}), {
32
+ headers: { 'Content-Type': 'application/json' },
33
+ });
34
+ },
35
+ });
36
+ }
@@ -0,0 +1,42 @@
1
+ import type { APIContext } from 'astro';
2
+ import { proxyAppView } from '../../lib/appview';
3
+ import { matchesPrimaryActor, getPrimaryActor } from '../../lib/actor';
4
+ import { buildFeedViewPosts, listPosts } from '../../lib/feed';
5
+ import { isAuthorized, unauthorized } from '../../lib/auth';
6
+
7
+ export const prerender = false;
8
+
9
+ export async function GET({ locals, request }: APIContext) {
10
+ const { env } = locals.runtime;
11
+ if (!(await isAuthorized(request, env))) return unauthorized();
12
+
13
+ return proxyAppView({
14
+ request,
15
+ env,
16
+ lxm: 'app.bsky.feed.getAuthorFeed',
17
+ fallback: async () => {
18
+ const url = new URL(request.url);
19
+ const identifier = url.searchParams.get('actor');
20
+ const actor = await getPrimaryActor(env);
21
+ if (!matchesPrimaryActor(identifier, actor)) {
22
+ return new Response(JSON.stringify({ error: 'ActorNotFound' }), { status: 404 });
23
+ }
24
+
25
+ const limitParam = Number.parseInt(url.searchParams.get('limit') ?? '', 10);
26
+ const limitInput = Number.isFinite(limitParam) ? limitParam : 50;
27
+ const limit = Math.max(1, Math.min(limitInput, 100));
28
+ const cursor = url.searchParams.get('cursor') ?? undefined;
29
+
30
+ const posts = await listPosts(env, limit, cursor);
31
+ const feed = await buildFeedViewPosts(env, posts);
32
+ const nextCursor = posts.length === limit ? String(posts[posts.length - 1].rowid) : undefined;
33
+
34
+ const payload: Record<string, unknown> = { feed };
35
+ if (nextCursor) payload.cursor = nextCursor;
36
+
37
+ return new Response(JSON.stringify(payload), {
38
+ headers: { 'Content-Type': 'application/json' },
39
+ });
40
+ },
41
+ });
42
+ }
@@ -0,0 +1,37 @@
1
+ import type { APIContext } from 'astro';
2
+ import { proxyAppView } from '../../lib/appview';
3
+ import { buildThreadView, getPostByUri } from '../../lib/feed';
4
+ import { isAuthorized, unauthorized } from '../../lib/auth';
5
+
6
+ export const prerender = false;
7
+
8
+ export async function GET({ locals, request }: APIContext) {
9
+ const { env } = locals.runtime;
10
+ if (!(await isAuthorized(request, env))) return unauthorized();
11
+
12
+ return proxyAppView({
13
+ request,
14
+ env,
15
+ lxm: 'app.bsky.feed.getPostThread',
16
+ fallback: async () => {
17
+ const url = new URL(request.url);
18
+ const uri = url.searchParams.get('uri');
19
+ if (!uri) {
20
+ return new Response(
21
+ JSON.stringify({ error: 'BadRequest', message: 'uri parameter required' }),
22
+ { status: 400 },
23
+ );
24
+ }
25
+
26
+ const post = await getPostByUri(env, uri);
27
+ if (!post) {
28
+ return new Response(JSON.stringify({ error: 'NotFound' }), { status: 404 });
29
+ }
30
+
31
+ const thread = await buildThreadView(env, post);
32
+ return new Response(JSON.stringify({ thread }), {
33
+ headers: { 'Content-Type': 'application/json' },
34
+ });
35
+ },
36
+ });
37
+ }
@@ -0,0 +1,26 @@
1
+ import type { APIContext } from 'astro';
2
+ import { proxyAppView } from '../../lib/appview';
3
+ import { buildPostViews, getPostsByUris } from '../../lib/feed';
4
+ import { isAuthorized, unauthorized } from '../../lib/auth';
5
+
6
+ export const prerender = false;
7
+
8
+ export async function GET({ locals, request }: APIContext) {
9
+ const { env } = locals.runtime;
10
+ if (!(await isAuthorized(request, env))) return unauthorized();
11
+
12
+ return proxyAppView({
13
+ request,
14
+ env,
15
+ lxm: 'app.bsky.feed.getPosts',
16
+ fallback: async () => {
17
+ const url = new URL(request.url);
18
+ const uris = url.searchParams.getAll('uris').filter(Boolean);
19
+ const posts = await getPostsByUris(env, uris.slice(0, 25));
20
+ const views = await buildPostViews(env, posts);
21
+ return new Response(JSON.stringify({ posts: views }), {
22
+ headers: { 'Content-Type': 'application/json' },
23
+ });
24
+ },
25
+ });
26
+ }
@@ -0,0 +1,35 @@
1
+ import type { APIContext } from 'astro';
2
+ import { proxyAppView } from '../../lib/appview';
3
+ import { buildFeedViewPosts, listPosts } from '../../lib/feed';
4
+ import { isAuthorized, unauthorized } from '../../lib/auth';
5
+
6
+ export const prerender = false;
7
+
8
+ export async function GET({ locals, request }: APIContext) {
9
+ const { env } = locals.runtime;
10
+ if (!(await isAuthorized(request, env))) return unauthorized();
11
+
12
+ return proxyAppView({
13
+ request,
14
+ env,
15
+ lxm: 'app.bsky.feed.getTimeline',
16
+ fallback: async () => {
17
+ const url = new URL(request.url);
18
+ const cursor = url.searchParams.get('cursor') ?? undefined;
19
+ const limitParam = Number.parseInt(url.searchParams.get('limit') ?? '', 10);
20
+ const limitInput = Number.isFinite(limitParam) ? limitParam : 50;
21
+ const limit = Math.max(1, Math.min(limitInput, 100));
22
+
23
+ const posts = await listPosts(env, limit, cursor);
24
+ const feed = await buildFeedViewPosts(env, posts);
25
+ const nextCursor = posts.length === limit ? String(posts[posts.length - 1].rowid) : undefined;
26
+
27
+ const payload: Record<string, unknown> = { feed };
28
+ if (nextCursor) payload.cursor = nextCursor;
29
+
30
+ return new Response(JSON.stringify(payload), {
31
+ headers: { 'Content-Type': 'application/json' },
32
+ });
33
+ },
34
+ });
35
+ }
@@ -0,0 +1,29 @@
1
+ import type { APIContext } from 'astro';
2
+ import { proxyAppView } from '../../lib/appview';
3
+ import { buildProfileView, getPrimaryActor, matchesPrimaryActor } from '../../lib/actor';
4
+ import { isAuthorized, unauthorized } from '../../lib/auth';
5
+
6
+ export const prerender = false;
7
+
8
+ export async function GET({ locals, request }: APIContext) {
9
+ const { env } = locals.runtime;
10
+ if (!(await isAuthorized(request, env))) return unauthorized();
11
+
12
+ return proxyAppView({
13
+ request,
14
+ env,
15
+ lxm: 'app.bsky.graph.getFollowers',
16
+ fallback: async () => {
17
+ const url = new URL(request.url);
18
+ const identifier = url.searchParams.get('actor');
19
+ const actor = await getPrimaryActor(env);
20
+ if (!matchesPrimaryActor(identifier, actor)) {
21
+ return new Response(JSON.stringify({ error: 'ActorNotFound' }), { status: 404 });
22
+ }
23
+ return new Response(
24
+ JSON.stringify({ subject: buildProfileView(actor), followers: [] }),
25
+ { headers: { 'Content-Type': 'application/json' } },
26
+ );
27
+ },
28
+ });
29
+ }
@@ -0,0 +1,29 @@
1
+ import type { APIContext } from 'astro';
2
+ import { proxyAppView } from '../../lib/appview';
3
+ import { buildProfileView, getPrimaryActor, matchesPrimaryActor } from '../../lib/actor';
4
+ import { isAuthorized, unauthorized } from '../../lib/auth';
5
+
6
+ export const prerender = false;
7
+
8
+ export async function GET({ locals, request }: APIContext) {
9
+ const { env } = locals.runtime;
10
+ if (!(await isAuthorized(request, env))) return unauthorized();
11
+
12
+ return proxyAppView({
13
+ request,
14
+ env,
15
+ lxm: 'app.bsky.graph.getFollows',
16
+ fallback: async () => {
17
+ const url = new URL(request.url);
18
+ const identifier = url.searchParams.get('actor');
19
+ const actor = await getPrimaryActor(env);
20
+ if (!matchesPrimaryActor(identifier, actor)) {
21
+ return new Response(JSON.stringify({ error: 'ActorNotFound' }), { status: 404 });
22
+ }
23
+ return new Response(
24
+ JSON.stringify({ subject: buildProfileView(actor), follows: [] }),
25
+ { headers: { 'Content-Type': 'application/json' } },
26
+ );
27
+ },
28
+ });
29
+ }
@@ -0,0 +1,29 @@
1
+ import type { APIContext } from 'astro';
2
+ import { getLabelerServiceViews } from '../../lib/labeler';
3
+
4
+ export const prerender = false;
5
+
6
+ export async function GET({ locals, request }: APIContext) {
7
+ const { env } = locals.runtime;
8
+ const url = new URL(request.url);
9
+
10
+ const didParams = url.searchParams.getAll('dids');
11
+ const dids = didParams
12
+ .flatMap((entry) => entry.split(',').map((did) => did.trim()))
13
+ .filter(Boolean);
14
+
15
+ if (dids.length === 0) {
16
+ return new Response(JSON.stringify({ error: 'BadRequest', message: 'dids parameter required' }), {
17
+ status: 400,
18
+ headers: { 'Content-Type': 'application/json' },
19
+ });
20
+ }
21
+
22
+ const detailed = url.searchParams.get('detailed') === 'true';
23
+
24
+ const views = await getLabelerServiceViews(env, dids, { detailed });
25
+
26
+ return new Response(JSON.stringify({ views }), {
27
+ headers: { 'Content-Type': 'application/json' },
28
+ });
29
+ }
@@ -0,0 +1,20 @@
1
+ import type { APIContext } from 'astro';
2
+ import { proxyAppView } from '../../lib/appview';
3
+ import { isAuthorized, unauthorized } from '../../lib/auth';
4
+
5
+ export const prerender = false;
6
+
7
+ export async function GET({ locals, request }: APIContext) {
8
+ const { env } = locals.runtime;
9
+ if (!(await isAuthorized(request, env))) return unauthorized();
10
+
11
+ return proxyAppView({
12
+ request,
13
+ env,
14
+ lxm: 'app.bsky.notification.getUnreadCount',
15
+ fallback: async () =>
16
+ new Response(JSON.stringify({ count: 0 }), {
17
+ headers: { 'Content-Type': 'application/json' },
18
+ }),
19
+ });
20
+ }
@@ -0,0 +1,27 @@
1
+ import type { APIContext } from 'astro';
2
+ import { proxyAppView } from '../../lib/appview';
3
+ import { isAuthorized, unauthorized } from '../../lib/auth';
4
+
5
+ export const prerender = false;
6
+
7
+ export async function GET({ locals, request }: APIContext) {
8
+ const { env } = locals.runtime;
9
+ if (!(await isAuthorized(request, env))) return unauthorized();
10
+
11
+ return proxyAppView({
12
+ request,
13
+ env,
14
+ lxm: 'app.bsky.notification.listNotifications',
15
+ fallback: async () =>
16
+ new Response(
17
+ JSON.stringify({
18
+ notifications: [],
19
+ priority: false,
20
+ seenAt: new Date(0).toISOString(),
21
+ }),
22
+ {
23
+ headers: { 'Content-Type': 'application/json' },
24
+ },
25
+ ),
26
+ });
27
+ }
@@ -0,0 +1,19 @@
1
+ import type { APIContext } from 'astro';
2
+ import { isAuthorized, unauthorized } from '../../lib/auth';
3
+
4
+ export const prerender = false;
5
+
6
+ export async function GET({ locals, request }: APIContext) {
7
+ const { env } = locals.runtime;
8
+ if (!(await isAuthorized(request, env))) return unauthorized();
9
+
10
+ return new Response(
11
+ JSON.stringify({
12
+ status: 'unknown',
13
+ lastInitiatedAt: new Date(0).toISOString(),
14
+ }),
15
+ {
16
+ headers: { 'Content-Type': 'application/json' },
17
+ },
18
+ );
19
+ }
@@ -0,0 +1,15 @@
1
+ import type { APIContext } from 'astro';
2
+
3
+ export const prerender = false;
4
+
5
+ export async function GET() {
6
+ return new Response(
7
+ JSON.stringify({
8
+ checkEmailConfirmed: false,
9
+ liveNow: [],
10
+ }),
11
+ {
12
+ headers: { 'Content-Type': 'application/json' },
13
+ },
14
+ );
15
+ }