@alteran/astro 0.3.9 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) 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/client.ts +1 -1
  13. package/src/db/dal.ts +34 -23
  14. package/src/db/repo.ts +38 -38
  15. package/src/db/schema.ts +5 -1
  16. package/src/db/seed.ts +5 -13
  17. package/src/entrypoints/server.ts +2 -22
  18. package/src/handlers/debug.ts +1 -1
  19. package/src/handlers/ready.ts +1 -1
  20. package/src/handlers/root.ts +4 -4
  21. package/src/handlers/xrpc.server.refreshSession.ts +6 -6
  22. package/src/lib/account-state.ts +156 -0
  23. package/src/lib/actor.ts +29 -13
  24. package/src/lib/appview/auth-policy.ts +66 -0
  25. package/src/lib/appview/did-resolver.ts +233 -0
  26. package/src/lib/appview/proxy.ts +221 -0
  27. package/src/lib/appview/service-config.ts +61 -0
  28. package/src/lib/appview/service-jwt.ts +93 -0
  29. package/src/lib/appview/types.ts +25 -0
  30. package/src/lib/appview.ts +5 -532
  31. package/src/lib/auth-errors.ts +24 -0
  32. package/src/lib/auth.ts +63 -15
  33. package/src/lib/blockstore-gc.ts +6 -5
  34. package/src/lib/cache.ts +30 -4
  35. package/src/lib/chat.ts +20 -14
  36. package/src/lib/commit-log-pruning.ts +2 -2
  37. package/src/lib/commit.ts +26 -36
  38. package/src/lib/config.ts +26 -15
  39. package/src/lib/did-document.ts +32 -0
  40. package/src/lib/errors.ts +54 -0
  41. package/src/lib/feed.ts +18 -19
  42. package/src/lib/firehose/frames.ts +87 -47
  43. package/src/lib/firehose/validation.ts +3 -3
  44. package/src/lib/jwt.ts +85 -177
  45. package/src/lib/labeler.ts +43 -30
  46. package/src/lib/logger.ts +4 -0
  47. package/src/lib/mst/block-map.ts +172 -0
  48. package/src/lib/mst/blockstore.ts +56 -93
  49. package/src/lib/mst/index.ts +1 -0
  50. package/src/lib/mst/leaf.ts +25 -0
  51. package/src/lib/mst/mst.ts +81 -237
  52. package/src/lib/mst/serialize.ts +97 -0
  53. package/src/lib/mst/types.ts +21 -0
  54. package/src/lib/oauth/clients.ts +67 -0
  55. package/src/lib/oauth/dpop-errors.ts +15 -0
  56. package/src/lib/oauth/dpop.ts +150 -0
  57. package/src/lib/oauth/resource.ts +199 -0
  58. package/src/lib/oauth/store.ts +77 -0
  59. package/src/lib/preferences.ts +12 -37
  60. package/src/lib/ratelimit.ts +4 -4
  61. package/src/lib/refresh-session.ts +161 -0
  62. package/src/lib/relay.ts +10 -8
  63. package/src/lib/secrets.ts +6 -7
  64. package/src/lib/sequencer.ts +14 -5
  65. package/src/lib/service-auth.ts +184 -0
  66. package/src/lib/session-tokens.ts +28 -76
  67. package/src/lib/streaming-car.ts +3 -0
  68. package/src/lib/tracing.ts +4 -3
  69. package/src/lib/util.ts +65 -15
  70. package/src/middleware.ts +1 -1
  71. package/src/pages/.well-known/did.json.ts +27 -30
  72. package/src/pages/.well-known/oauth-authorization-server.ts +31 -0
  73. package/src/pages/.well-known/oauth-protected-resource.ts +22 -0
  74. package/src/pages/debug/blob/[...key].ts +2 -2
  75. package/src/pages/debug/db/bootstrap.ts +1 -1
  76. package/src/pages/debug/db/commits.ts +1 -1
  77. package/src/pages/debug/gc/blobs.ts +1 -1
  78. package/src/pages/debug/record.ts +1 -1
  79. package/src/pages/debug/sequencer.ts +28 -0
  80. package/src/pages/health.ts +4 -4
  81. package/src/pages/oauth/authorize.ts +78 -0
  82. package/src/pages/oauth/consent.ts +80 -0
  83. package/src/pages/oauth/par.ts +121 -0
  84. package/src/pages/oauth/token.ts +158 -0
  85. package/src/pages/ready.ts +2 -2
  86. package/src/pages/xrpc/[...nsid].ts +61 -0
  87. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +12 -13
  88. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +23 -23
  89. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +9 -2
  90. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +9 -2
  91. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +9 -2
  92. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +43 -41
  93. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +10 -3
  94. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +40 -9
  95. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +41 -29
  96. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +20 -6
  97. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +1 -1
  98. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +101 -11
  99. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +44 -14
  100. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +41 -13
  101. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +2 -2
  102. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +14 -1
  103. package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +14 -6
  104. package/src/pages/xrpc/com.atproto.repo.listRecords.ts +1 -1
  105. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +42 -14
  106. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +76 -15
  107. package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +20 -8
  108. package/src/pages/xrpc/com.atproto.server.createSession.ts +32 -12
  109. package/src/pages/xrpc/com.atproto.server.describeServer.ts +1 -1
  110. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +12 -5
  111. package/src/pages/xrpc/com.atproto.server.getSession.ts +22 -8
  112. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +30 -72
  113. package/src/pages/xrpc/com.atproto.sync.getBlob.ts +72 -23
  114. package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +1 -1
  115. package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +1 -1
  116. package/src/pages/xrpc/com.atproto.sync.getHead.ts +7 -2
  117. package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +1 -1
  118. package/src/pages/xrpc/com.atproto.sync.getRecord.ts +5 -27
  119. package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +1 -1
  120. package/src/pages/xrpc/com.atproto.sync.getRepo.ts +50 -5
  121. package/src/pages/xrpc/com.atproto.sync.getRepoStatus.ts +58 -0
  122. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +2 -2
  123. package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
  124. package/src/services/car.ts +209 -57
  125. package/src/services/r2-blob-store.ts +4 -4
  126. package/src/services/repo/blockstore-ops.ts +29 -0
  127. package/src/services/repo/operations.ts +133 -0
  128. package/src/services/repo-manager.ts +203 -254
  129. package/src/worker/runtime.ts +56 -11
  130. package/src/worker/sequencer/broadcast.ts +91 -0
  131. package/src/worker/sequencer/cid-helpers.ts +39 -0
  132. package/src/worker/sequencer/payload.ts +84 -0
  133. package/src/worker/sequencer/types.ts +36 -0
  134. package/src/worker/sequencer/upgrade.ts +141 -0
  135. package/src/worker/sequencer.ts +264 -406
  136. package/types/env.d.ts +18 -6
  137. package/src/pages/xrpc/app.bsky.actor.getProfile.ts +0 -49
  138. package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +0 -51
  139. package/src/pages/xrpc/app.bsky.feed.getActorFeeds.ts +0 -25
  140. package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +0 -42
  141. package/src/pages/xrpc/app.bsky.feed.getFeedGenerators.ts +0 -25
  142. package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +0 -37
  143. package/src/pages/xrpc/app.bsky.feed.getPosts.ts +0 -26
  144. package/src/pages/xrpc/app.bsky.feed.getSuggestedFeeds.ts +0 -23
  145. package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +0 -47
  146. package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +0 -29
  147. package/src/pages/xrpc/app.bsky.graph.getFollows.ts +0 -29
  148. package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +0 -20
  149. package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +0 -27
  150. package/src/pages/xrpc/app.bsky.unspecced.getSuggestedFeeds.ts +0 -23
@@ -1,5 +1,6 @@
1
1
  import type { APIContext } from 'astro';
2
2
  import { getRecord as dalGetRecord } from '../../db/dal';
3
+ import { proxyAppView } from '../../lib/appview';
3
4
 
4
5
  export const prerender = false;
5
6
 
@@ -8,7 +9,7 @@ export async function GET({ locals, request }: APIContext) {
8
9
  const url = new URL(request.url);
9
10
  let uri = url.searchParams.get('uri');
10
11
  if (!uri) {
11
- const repo = url.searchParams.get('repo') ?? (env.PDS_DID ?? 'did:example:single-user');
12
+ const repo = url.searchParams.get('repo') ?? (env.PDS_DID as string);
12
13
  const collection = url.searchParams.get('collection');
13
14
  const rkey = url.searchParams.get('rkey');
14
15
  if (repo && collection && rkey) uri = `at://${repo}/${collection}/${rkey}`;
@@ -16,6 +17,18 @@ export async function GET({ locals, request }: APIContext) {
16
17
 
17
18
  if (!uri) return new Response(JSON.stringify({ error: 'BadRequest', message: 'query param uri required' }), { status: 400 });
18
19
 
20
+ // If the repo is not hosted here, proxy to AppView like upstream PDS does
21
+ const localDid = env.PDS_DID || '';
22
+ const repoParam = url.searchParams.get('repo') || '';
23
+ let repoDid = repoParam;
24
+ if (!repoDid && uri.startsWith('at://')) {
25
+ const m = uri.match(/^at:\/\/([^/]+)\//);
26
+ if (m) repoDid = m[1];
27
+ }
28
+ if (repoDid && localDid && repoDid !== localDid) {
29
+ return proxyAppView({ request, env, lxm: 'com.atproto.repo.getRecord' });
30
+ }
31
+
19
32
  const row = await dalGetRecord(env, uri);
20
33
  if (!row) return new Response(JSON.stringify({ error: 'NotFound' }), { status: 404 });
21
34
 
@@ -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 { getDb } from '../../db/client';
4
5
  import { record, blob_ref } from '../../db/schema';
5
6
  import { eq } from 'drizzle-orm';
@@ -16,10 +17,17 @@ export const prerender = false;
16
17
  export async function GET({ locals, request, url }: APIContext) {
17
18
  const { env } = locals.runtime;
18
19
 
19
- if (!(await isAuthorized(request, env))) return unauthorized();
20
+ try {
21
+ if (!(await isAuthorized(request, env))) return unauthorized();
22
+ } catch (error) {
23
+ if (error instanceof AuthTokenExpiredError) {
24
+ return expiredToken();
25
+ }
26
+ throw error;
27
+ }
20
28
 
21
29
  try {
22
- const did = env.PDS_DID ?? 'did:example:single-user';
30
+ const did = String(env.PDS_DID ?? 'did:example:single-user');
23
31
  const limit = parseInt(url.searchParams.get('limit') || '500');
24
32
  const cursor = url.searchParams.get('cursor') || '';
25
33
 
@@ -76,13 +84,13 @@ export async function GET({ locals, request, url }: APIContext) {
76
84
  }),
77
85
  { status: 200, headers: { 'Content-Type': 'application/json' } }
78
86
  );
79
- } catch (error: any) {
87
+ } catch (error) {
80
88
  return new Response(
81
89
  JSON.stringify({
82
90
  error: 'InternalServerError',
83
- message: error.message || 'Failed to list missing blobs'
91
+ message: errorMessage(error) || 'Failed to list missing blobs'
84
92
  }),
85
93
  { status: 500, headers: { 'Content-Type': 'application/json' } }
86
94
  );
87
95
  }
88
- }
96
+ }
@@ -10,7 +10,7 @@ export const prerender = false;
10
10
  export async function GET({ locals, url }: APIContext) {
11
11
  const { env } = locals.runtime;
12
12
 
13
- const repo = url.searchParams.get('repo') || env.PDS_DID || 'did:example:single-user';
13
+ const repo = url.searchParams.get('repo') || (env.PDS_DID as string);
14
14
  const collection = url.searchParams.get('collection');
15
15
  const limit = parseInt(url.searchParams.get('limit') || '50', 10);
16
16
  const cursor = url.searchParams.get('cursor') || undefined;
@@ -1,5 +1,6 @@
1
1
  import type { APIContext } from 'astro';
2
- import { isAuthorized, unauthorized } from '../../lib/auth';
2
+ import { errorCode, errorMessage } from '../../lib/errors';
3
+ import { verifyResourceRequestHybrid, dpopResourceUnauthorized, handleResourceAuthError } from '../../lib/oauth/resource';
3
4
  import { checkRate } from '../../lib/ratelimit';
4
5
  import { readJsonBounded } from '../../lib/util';
5
6
  import { RepoManager } from '../../services/repo-manager';
@@ -9,7 +10,14 @@ export const prerender = false;
9
10
 
10
11
  export async function POST({ locals, request }: APIContext) {
11
12
  const { env } = locals.runtime;
12
- if (!(await isAuthorized(request, env))) return unauthorized();
13
+ try {
14
+ const auth = await verifyResourceRequestHybrid(env, request);
15
+ if (!auth) return dpopResourceUnauthorized(env);
16
+ } catch (error) {
17
+ const handled = await handleResourceAuthError(env, error);
18
+ if (handled) return handled;
19
+ throw error;
20
+ }
13
21
 
14
22
  const rateLimitResponse = await checkRate(env, request, 'writes');
15
23
  if (rateLimitResponse) return rateLimitResponse;
@@ -17,28 +25,48 @@ export async function POST({ locals, request }: APIContext) {
17
25
  let body: any;
18
26
  try {
19
27
  body = await readJsonBounded(env, request);
20
- } catch (e: any) {
21
- if (e?.code === 'PayloadTooLarge') {
28
+ } catch (e) {
29
+ if (errorCode(e) === 'PayloadTooLarge') {
22
30
  return new Response(JSON.stringify({ error: 'PayloadTooLarge' }), { status: 413 });
23
31
  }
24
32
  return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
25
33
  }
26
- const { collection, rkey, record } = body ?? {};
34
+ const { collection, rkey } = body ?? {};
35
+ let { record } = body ?? {};
27
36
  if (!collection || !rkey || !record) return new Response(JSON.stringify({ error: 'BadRequest' }), { status: 400 });
28
37
 
38
+ if (collection === 'app.bsky.feed.post' && record && typeof record === 'object') {
39
+ if (typeof record.text !== 'string') {
40
+ record.text = '';
41
+ }
42
+ if (typeof record.createdAt !== 'string') {
43
+ record.createdAt = new Date().toISOString();
44
+ }
45
+ }
46
+
29
47
  const repo = new RepoManager(env);
30
- const commit = await repo.putRecord(collection, rkey, record);
48
+ const result = await repo.putRecord(collection, rkey, record);
31
49
  await notifySequencer(env, {
32
- did: env.PDS_DID ?? 'did:example:single-user',
33
- commitCid: commit.commitCid,
34
- rev: commit.rev,
35
- data: commit.commitData,
36
- sig: commit.sig,
37
- ops: commit.ops,
38
- blocks: commit.blocks
50
+ did: env.PDS_DID as string,
51
+ commitCid: result.commitCid,
52
+ rev: result.rev,
53
+ data: result.commitData,
54
+ sig: result.sig,
55
+ ops: result.ops,
56
+ blocks: result.blocks
39
57
  });
40
58
 
41
- return new Response(JSON.stringify(commit), {
59
+ const out = {
60
+ uri: result.uri,
61
+ cid: result.cid,
62
+ commit: {
63
+ cid: result.commitCid,
64
+ rev: result.rev,
65
+ },
66
+ validationStatus: 'unknown' as const,
67
+ };
68
+
69
+ return new Response(JSON.stringify(out), {
42
70
  headers: { 'Content-Type': 'application/json' },
43
71
  });
44
72
  }
@@ -1,16 +1,48 @@
1
1
  import type { APIContext } from 'astro';
2
- import { isAuthorized, unauthorized } from '../../lib/auth';
2
+ import { errorMessage } from '../../lib/errors';
3
+ import { verifyResourceRequestHybrid, dpopResourceUnauthorized, handleResourceAuthError } from '../../lib/oauth/resource';
4
+ import { verifyServiceAuth, isServiceAuthToken } from '../../lib/service-auth';
3
5
  import { checkRate } from '../../lib/ratelimit';
4
- import { isAllowedMime } from '../../lib/util';
6
+ import { isAllowedMime, sniffMime, baseMime } from '../../lib/util';
5
7
  import { R2BlobStore } from '../../services/r2-blob-store';
6
8
  import { putBlobRef, checkBlobQuota, updateBlobQuota, isAccountActive } from '../../db/dal';
7
9
  import { resolveSecret } from '../../lib/secrets';
10
+ import { CID } from 'multiformats/cid';
11
+ import { sha256 } from 'multiformats/hashes/sha2';
8
12
 
9
13
  export const prerender = false;
10
14
 
11
15
  export async function POST({ locals, request }: APIContext) {
12
16
  const { env } = locals.runtime;
13
- if (!(await isAuthorized(request, env))) return unauthorized();
17
+
18
+ // Check if this is a service auth request (from video.bsky.app, etc.)
19
+ const authHeader = request.headers.get('authorization');
20
+ const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7).trim() : null;
21
+
22
+ let isServiceAuth = false;
23
+ if (token && isServiceAuthToken(token)) {
24
+ const serviceAuth = await verifyServiceAuth(env, request);
25
+ if (serviceAuth) {
26
+ isServiceAuth = true;
27
+ } else {
28
+ return new Response(
29
+ JSON.stringify({ error: 'AuthRequired', message: 'Invalid service auth token' }),
30
+ { status: 401, headers: { 'Content-Type': 'application/json' } }
31
+ );
32
+ }
33
+ }
34
+
35
+ // If not service auth, verify as user request
36
+ if (!isServiceAuth) {
37
+ try {
38
+ const auth = await verifyResourceRequestHybrid(env, request);
39
+ if (!auth) return dpopResourceUnauthorized(env);
40
+ } catch (error) {
41
+ const handled = await handleResourceAuthError(env, error);
42
+ if (handled) return handled;
43
+ throw error;
44
+ }
45
+ }
14
46
 
15
47
  // Get DID from environment (single-user PDS)
16
48
  const did = (await resolveSecret(env.PDS_DID)) ?? 'did:example:single-user';
@@ -30,12 +62,29 @@ export async function POST({ locals, request }: APIContext) {
30
62
  const rateLimitResponse = await checkRate(env, request, 'blob');
31
63
  if (rateLimitResponse) return rateLimitResponse;
32
64
 
33
- const buf = await request.arrayBuffer();
34
- const contentType = request.headers.get('content-type') ?? 'application/octet-stream';
65
+ // Decompress if Content-Encoding is present (some clients may compress uploads)
66
+ const enc = (request.headers.get('content-encoding') || '').toLowerCase();
67
+ let buf: ArrayBuffer;
68
+ if (enc && (enc === 'gzip' || enc === 'br' || enc === 'deflate')) {
69
+ try {
70
+ // @ts-ignore: DecompressionStream is available in CF Workers runtime
71
+ const ds = new DecompressionStream(enc);
72
+ const decompressed = request.body?.pipeThrough(ds);
73
+ buf = await new Response(decompressed).arrayBuffer();
74
+ } catch {
75
+ // Fallback to raw body if decompression not supported
76
+ buf = await request.arrayBuffer();
77
+ }
78
+ } else {
79
+ buf = await request.arrayBuffer();
80
+ }
81
+ const headerMime = baseMime(request.headers.get('content-type'));
82
+ const sniffed = sniffMime(buf);
83
+ // Prefer sniffed MIME like upstream PDS; fall back to header
84
+ const contentType = sniffed || headerMime;
35
85
 
36
86
  // Skip MIME type validation during migration - accept all types
37
- // Uncomment the line below to re-enable MIME type restrictions after migration
38
- // if (!isAllowedMime(env, contentType)) return new Response(JSON.stringify({ error: 'UnsupportedMediaType' }), { status: 415 });
87
+ // Uncomment to enforce: if (!isAllowedMime(env, contentType)) return new Response(JSON.stringify({ error: 'UnsupportedMediaType' }), { status: 415 });
39
88
 
40
89
  // Check quota before upload
41
90
  const canUpload = await checkBlobQuota(env, did, buf.byteLength);
@@ -51,19 +100,31 @@ export async function POST({ locals, request }: APIContext) {
51
100
 
52
101
  const store = new R2BlobStore(env);
53
102
  try {
54
- const res = await store.put(buf, { contentType });
103
+ const response = await store.put(buf, { contentType });
104
+
105
+ // Compute a CIDv1 (raw) for the blob so clients receive a valid CID link
106
+ const digest = await sha256.digest(new Uint8Array(buf));
107
+ const cid = CID.createV1(0x55, digest); // 0x55 = raw codec
108
+ const cidStr = cid.toString();
55
109
 
56
110
  // Register blob ref with CID-based key
57
- await putBlobRef(env, did, res.sha256, res.key, contentType, res.size);
111
+ await putBlobRef(env, did, cidStr, response.key, contentType, response.size);
58
112
 
59
113
  // Update quota
60
- await updateBlobQuota(env, did, res.size, 1);
114
+ await updateBlobQuota(env, did, response.size, 1);
115
+
116
+ // Mirror upstream shape exactly; helpful debugging header
117
+ // Conform to lexicon: blob object must include $type: 'blob'
118
+ const body = { blob: { $type: 'blob', ref: { $link: cidStr }, mimeType: contentType, size: response.size } };
119
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
120
+ // Debug-only headers (safe for clients to ignore)
121
+ headers['x-sniffed-mime'] = sniffed || '';
122
+ headers['x-header-mime'] = headerMime;
123
+ if (enc) headers['x-upload-encoding'] = enc;
61
124
 
62
- return new Response(JSON.stringify({ blob: { ref: { $link: res.key }, mimeType: contentType, size: res.size } }), {
63
- headers: { 'Content-Type': 'application/json' },
64
- });
65
- } catch (e: any) {
66
- if (String(e.message || '').startsWith('BlobTooLarge')) return new Response(JSON.stringify({ error: 'PayloadTooLarge' }), { status: 413 });
125
+ return new Response(JSON.stringify(body), { headers });
126
+ } catch (e) {
127
+ if (String(errorMessage(e) || '').startsWith('BlobTooLarge')) return new Response(JSON.stringify({ error: 'PayloadTooLarge' }), { status: 413 });
67
128
  return new Response(JSON.stringify({ error: 'UploadFailed' }), { status: 500 });
68
129
  }
69
130
  }
@@ -1,6 +1,8 @@
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 { getAccountState } from '../../db/dal';
5
+ import { toWireStatus } from '../../lib/account-state';
4
6
  import { getDb } from '../../db/client';
5
7
  import { repo_root, record, blob_ref, commit_log } from '../../db/schema';
6
8
  import { eq, count } from 'drizzle-orm';
@@ -20,15 +22,24 @@ export const prerender = false;
20
22
  export async function GET({ locals, request }: APIContext) {
21
23
  const { env } = locals.runtime;
22
24
 
23
- if (!(await isAuthorized(request, env))) return unauthorized();
25
+ try {
26
+ if (!(await isAuthorized(request, env))) return unauthorized();
27
+ } catch (error) {
28
+ if (error instanceof AuthTokenExpiredError) {
29
+ return expiredToken();
30
+ }
31
+ throw error;
32
+ }
24
33
 
25
34
  try {
26
- const did = env.PDS_DID ?? 'did:example:single-user';
35
+ const did = String(env.PDS_DID ?? 'did:example:single-user');
27
36
  const db = getDb(env);
28
37
 
29
- // Get account state
38
+ // Get account state. No row means an unmigrated account, treated as active
39
+ // for backward compatibility.
30
40
  const accountState = await getAccountState(env, did);
31
- const active = accountState?.active ?? true;
41
+ const wire = accountState ? toWireStatus(accountState) : { active: true };
42
+ const { active, status } = wire;
32
43
 
33
44
  // Get repo head
34
45
  const repoRoot = await db
@@ -65,6 +76,7 @@ export async function GET({ locals, request }: APIContext) {
65
76
  JSON.stringify({
66
77
  did,
67
78
  active,
79
+ ...(status ? { status } : {}),
68
80
  head: repoRoot?.commitCid ?? null,
69
81
  rev: repoRoot?.rev ?? 0,
70
82
  recordCount,
@@ -80,13 +92,13 @@ export async function GET({ locals, request }: APIContext) {
80
92
  }),
81
93
  { status: 200, headers: { 'Content-Type': 'application/json' } }
82
94
  );
83
- } catch (error: any) {
95
+ } catch (error) {
84
96
  return new Response(
85
97
  JSON.stringify({
86
98
  error: 'InternalServerError',
87
- message: error.message || 'Failed to check account status'
99
+ message: errorMessage(error) || 'Failed to check account status'
88
100
  }),
89
101
  { status: 500, headers: { 'Content-Type': 'application/json' } }
90
102
  );
91
103
  }
92
- }
104
+ }
@@ -7,6 +7,7 @@ import { createAccount, getAccountByIdentifier, storeRefreshToken } from '../../
7
7
  import { hashPassword, verifyPassword } from '../../lib/password';
8
8
  import { issueSessionTokens } from '../../lib/session-tokens';
9
9
  import { getRuntimeString } from '../../lib/secrets';
10
+ import { buildDidDocument } from '../../lib/did-document';
10
11
 
11
12
  export const prerender = false;
12
13
 
@@ -17,7 +18,7 @@ export async function POST({ locals, request }: APIContext) {
17
18
  const { env } = locals.runtime;
18
19
  const clientIp = request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for') || 'unknown';
19
20
 
20
- const db = drizzle(env.DB);
21
+ const db = drizzle(env.ALTERAN_DB);
21
22
  const now = Math.floor(Date.now() / 1000);
22
23
 
23
24
  // Check if IP is locked out
@@ -33,23 +34,28 @@ export async function POST({ locals, request }: APIContext) {
33
34
  );
34
35
  }
35
36
 
36
- const body = await readJson(request).catch(() => ({ identifier: '', password: '' }));
37
- const identifier = typeof body.identifier === 'string' && body.identifier ? body.identifier : (await getRuntimeString(env, 'PDS_HANDLE', 'user.example'));
37
+ const rawBody = await readJson(request).catch(() => ({}));
38
+ const body = (rawBody ?? {}) as { identifier?: unknown; password?: unknown };
39
+ const identifier: string =
40
+ typeof body.identifier === 'string' && body.identifier
41
+ ? body.identifier
42
+ : (await getRuntimeString(env, 'PDS_HANDLE', 'user.example')) ?? 'user.example';
38
43
  const password = typeof body.password === 'string' ? body.password : '';
39
44
 
40
- let account = await getAccountByIdentifier(env, identifier ?? '');
45
+ let account = await getAccountByIdentifier(env, identifier);
41
46
  if (!account) {
42
47
  const fallbackPassword = await getRuntimeString(env, 'USER_PASSWORD', '');
43
48
  if (fallbackPassword) {
44
- const fallbackDid = await getRuntimeString(env, 'PDS_DID', 'did:example:single-user');
45
- const fallbackHandle = await getRuntimeString(env, 'PDS_HANDLE', identifier);
49
+ const fallbackDid =
50
+ (await getRuntimeString(env, 'PDS_DID', 'did:example:single-user')) ?? 'did:example:single-user';
51
+ const fallbackHandle = (await getRuntimeString(env, 'PDS_HANDLE', identifier)) ?? identifier;
46
52
  const hashed = await hashPassword(fallbackPassword);
47
53
  await createAccount(env, {
48
54
  did: fallbackDid,
49
55
  handle: fallbackHandle,
50
56
  passwordScrypt: hashed,
51
57
  });
52
- account = await getAccountByIdentifier(env, identifier ?? '');
58
+ account = await getAccountByIdentifier(env, identifier);
53
59
  }
54
60
  }
55
61
  const passwordHash = account?.passwordScrypt ?? null;
@@ -102,8 +108,8 @@ export async function POST({ locals, request }: APIContext) {
102
108
  await db.delete(login_attempts).where(eq(login_attempts.ip, clientIp)).run();
103
109
  }
104
110
 
105
- const did = account?.did ?? (await getRuntimeString(env, 'PDS_DID', 'did:example:single-user'));
106
- const handle = account?.handle ?? (await getRuntimeString(env, 'PDS_HANDLE', identifier ?? 'user.example'));
111
+ const did = (account?.did ?? (await getRuntimeString(env, 'PDS_DID', 'did:example:single-user')) ?? 'did:example:single-user');
112
+ const handle = (account?.handle ?? (await getRuntimeString(env, 'PDS_HANDLE', identifier ?? 'user.example')) ?? (identifier ?? 'user.example'));
107
113
 
108
114
  const { accessJwt, refreshJwt, refreshPayload, refreshExpiry } = await issueSessionTokens(env, did);
109
115
 
@@ -114,7 +120,21 @@ export async function POST({ locals, request }: APIContext) {
114
120
  appPasswordName: null,
115
121
  });
116
122
 
117
- return new Response(JSON.stringify({ did, handle, accessJwt, refreshJwt }), {
118
- headers: { 'Content-Type': 'application/json' },
119
- });
123
+ // Build didDoc for the response (required by official API contract)
124
+ const didDoc = await buildDidDocument(env, did, handle);
125
+
126
+ const email = account?.email ?? (env.PDS_EMAIL as string | undefined);
127
+
128
+ return new Response(
129
+ JSON.stringify({
130
+ did,
131
+ didDoc,
132
+ handle,
133
+ accessJwt,
134
+ refreshJwt,
135
+ active: true,
136
+ ...(email ? { email, emailConfirmed: true, emailAuthFactor: false } : {}),
137
+ }),
138
+ { headers: { 'Content-Type': 'application/json' } },
139
+ );
120
140
  }
@@ -5,7 +5,7 @@ export const prerender = false;
5
5
 
6
6
  export function GET({ locals }: APIContext) {
7
7
  const { env } = locals.runtime;
8
- const did = typeof env.PDS_DID === 'string' ? env.PDS_DID : 'did:example:single-user';
8
+ const did = env.PDS_DID as string;
9
9
  const availableUserDomains: string[] = [];
10
10
 
11
11
  const links = typeof env.PDS_LINK_PRIVACY === 'string' || typeof env.PDS_LINK_TOS === 'string'
@@ -1,13 +1,20 @@
1
1
  import type { APIContext } from 'astro';
2
- import { authenticateRequest, unauthorized } from '../../lib/auth';
2
+ import { verifyResourceRequestHybrid, dpopResourceUnauthorized, handleResourceAuthError } from '../../lib/oauth/resource';
3
3
  import { createServiceAuthToken } from '../../lib/appview';
4
4
 
5
5
  export const prerender = false;
6
6
 
7
7
  export async function GET({ locals, request }: APIContext) {
8
8
  const { env } = locals.runtime;
9
- const auth = await authenticateRequest(request, env);
10
- if (!auth) return unauthorized();
9
+ let auth: { did: string; token: string } | null = null;
10
+ try {
11
+ auth = await verifyResourceRequestHybrid(env, request);
12
+ if (!auth) return dpopResourceUnauthorized(env);
13
+ } catch (error) {
14
+ const handled = await handleResourceAuthError(env, error);
15
+ if (handled) return handled;
16
+ throw error;
17
+ }
11
18
 
12
19
  const url = new URL(request.url);
13
20
  const audienceParam = url.searchParams.get('aud');
@@ -50,11 +57,11 @@ export async function GET({ locals, request }: APIContext) {
50
57
  }
51
58
 
52
59
  try {
53
- const token = await createServiceAuthToken(env, auth.claims.sub, audience, lexiconMethod, expiresIn);
60
+ const token = await createServiceAuthToken(env, auth.did, audience, lexiconMethod, expiresIn);
54
61
  return new Response(JSON.stringify({ token }), {
55
62
  headers: { 'Content-Type': 'application/json' },
56
63
  });
57
- } catch (error: any) {
64
+ } catch (error) {
58
65
  console.error('service auth error:', error);
59
66
  return new Response(JSON.stringify({ error: 'InternalServerError' }), {
60
67
  status: 500,
@@ -1,4 +1,6 @@
1
1
  import type { APIContext } from 'astro';
2
+ import { AuthTokenExpiredError, authenticateRequest, expiredToken, unauthorized } from '../../lib/auth';
3
+ import { getAccountByIdentifier } from '../../db/account';
2
4
 
3
5
  export const prerender = false;
4
6
 
@@ -6,20 +8,32 @@ export const prerender = false;
6
8
  * com.atproto.server.getSession
7
9
  * Get information about the current session
8
10
  */
9
- export async function GET({ locals }: APIContext) {
11
+ export async function GET({ locals, request }: APIContext) {
10
12
  const { env } = locals.runtime;
11
13
 
12
- // TODO: Implement proper session validation from Authorization header
13
- // For now, return basic session info for single-user PDS
14
+ // Validate the access token
15
+ let authContext;
16
+ try {
17
+ authContext = await authenticateRequest(request, env);
18
+ } catch (error) {
19
+ if (error instanceof AuthTokenExpiredError) {
20
+ return expiredToken();
21
+ }
22
+ throw error;
23
+ }
24
+ if (!authContext) {
25
+ return unauthorized();
26
+ }
14
27
 
15
- const did = env.PDS_DID ?? 'did:example:single-user';
16
- const handle = env.PDS_HANDLE ?? 'user.example.com';
28
+ const did = authContext.claims.sub;
29
+ const account = await getAccountByIdentifier(env, did);
30
+ const handle = account?.handle ?? (env.PDS_HANDLE as string) ?? 'user.example.com';
17
31
 
18
32
  return new Response(
19
33
  JSON.stringify({
20
34
  did,
21
35
  handle,
22
- email: 'user@example.com', // Single-user PDS doesn't have email
36
+ email: account?.email ?? (env.PDS_EMAIL as string | undefined) ?? 'user@example.com',
23
37
  emailConfirmed: true,
24
38
  emailAuthFactor: false,
25
39
  didDoc: {
@@ -31,7 +45,7 @@ export async function GET({ locals }: APIContext) {
31
45
  {
32
46
  id: '#atproto_pds',
33
47
  type: 'AtprotoPersonalDataServer',
34
- serviceEndpoint: `https://${handle}`,
48
+ serviceEndpoint: `https://${env.PDS_HOSTNAME ?? handle}`,
35
49
  },
36
50
  ],
37
51
  },
@@ -43,4 +57,4 @@ export async function GET({ locals }: APIContext) {
43
57
  },
44
58
  }
45
59
  );
46
- };
60
+ };