@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,90 +1,48 @@
1
1
  import type { APIContext } from 'astro';
2
2
  import { bearerToken } from '../../lib/util';
3
3
  import { lazyCleanupExpiredTokens } from '../../lib/token-cleanup';
4
- import { getRuntimeString } from '../../lib/secrets';
5
- import { getAccountByIdentifier, getRefreshToken, markRefreshTokenRotated, storeRefreshToken } from '../../db/account';
6
- import { verifyRefreshToken, issueSessionTokens, computeGraceExpiry } from '../../lib/session-tokens';
4
+ import { attemptRefresh, type RefreshOutcome } from '../../lib/refresh-session';
5
+ import { buildDidDocument } from '../../lib/did-document';
6
+ import { getAccountState } from '../../db/dal';
7
+ import { getAccountByIdentifier } from '../../db/account';
8
+ import { toWireStatus } from '../../lib/account-state';
7
9
 
8
10
  export const prerender = false;
9
11
 
10
12
  export async function POST({ locals, request }: APIContext) {
11
13
  const { env } = locals.runtime;
12
14
  const token = bearerToken(request);
13
- if (!token) {
14
- return new Response(
15
- JSON.stringify({ error: 'AuthRequired', message: 'No authorization token provided' }),
16
- { status: 401, headers: { 'Content-Type': 'application/json' } }
17
- );
18
- }
19
-
20
- const verification = await verifyRefreshToken(env, token).catch(() => null);
21
- if (!verification) {
22
- return new Response(
23
- JSON.stringify({ error: 'InvalidToken', message: 'Invalid or expired refresh token' }),
24
- { status: 401, headers: { 'Content-Type': 'application/json' } }
25
- );
26
- }
27
-
28
15
  const nowSec = Math.floor(Date.now() / 1000);
29
- const { decoded } = verification;
30
16
 
31
- if (!decoded || typeof decoded.jti !== 'string') {
32
- return new Response(
33
- JSON.stringify({ error: 'InvalidToken', message: 'Malformed refresh token' }),
34
- { status: 401, headers: { 'Content-Type': 'application/json' } }
35
- );
36
- }
37
-
38
- if (typeof decoded.exp !== 'number' || decoded.exp <= nowSec) {
39
- return new Response(
40
- JSON.stringify({ error: 'ExpiredToken', message: 'Refresh token expired' }),
41
- { status: 401, headers: { 'Content-Type': 'application/json' } }
42
- );
43
- }
17
+ const outcome: RefreshOutcome = await attemptRefresh({ env, token, nowSec });
44
18
 
45
- const stored = await getRefreshToken(env, decoded.jti);
46
- if (!stored) {
47
- return new Response(
48
- JSON.stringify({ error: 'InvalidToken', message: 'Refresh token has been revoked' }),
49
- { status: 401, headers: { 'Content-Type': 'application/json' } }
50
- );
51
- }
52
-
53
- if (stored.expiresAt <= nowSec) {
54
- return new Response(
55
- JSON.stringify({ error: 'ExpiredToken', message: 'Refresh token expired' }),
56
- { status: 401, headers: { 'Content-Type': 'application/json' } }
57
- );
58
- }
19
+ // Cleanup is best-effort and runs ~1% of the time regardless of outcome.
20
+ lazyCleanupExpiredTokens(env).catch(console.error);
59
21
 
60
- if (stored.did !== decoded.sub) {
22
+ if (outcome.tag === 'failure') {
61
23
  return new Response(
62
- JSON.stringify({ error: 'InvalidToken', message: 'Token subject mismatch' }),
63
- { status: 401, headers: { 'Content-Type': 'application/json' } }
24
+ JSON.stringify({ error: outcome.code, message: outcome.message }),
25
+ { status: outcome.status, headers: { 'Content-Type': 'application/json' } },
64
26
  );
65
27
  }
66
28
 
67
- const account = await getAccountByIdentifier(env, stored.did);
68
- const did = stored.did;
69
- const handle = account?.handle ?? (await getRuntimeString(env, 'PDS_HANDLE', 'user.example'));
70
-
71
- // Rotate: generate new token pair with new JTI
72
- const { accessJwt, refreshJwt, refreshPayload, refreshExpiry } = await issueSessionTokens(env, did, { jti: stored.nextId ?? undefined });
73
-
74
- await storeRefreshToken(env, {
75
- id: refreshPayload.jti,
76
- did,
77
- expiresAt: refreshExpiry,
78
- appPasswordName: stored.appPasswordName ?? null,
79
- });
80
-
81
- const graceExpiry = computeGraceExpiry(stored.expiresAt, nowSec);
82
- await markRefreshTokenRotated(env, decoded.jti, refreshPayload.jti, graceExpiry);
83
-
84
- // Lazy cleanup of expired tokens (runs 1% of the time)
85
- lazyCleanupExpiredTokens(env).catch(console.error);
86
-
87
- return new Response(JSON.stringify({ did, handle, accessJwt, refreshJwt }), {
88
- headers: { 'Content-Type': 'application/json' },
89
- });
29
+ const didDoc = await buildDidDocument(env, outcome.did, outcome.handle);
30
+ const accountState = await getAccountState(env, outcome.did);
31
+ const wire = accountState ? toWireStatus(accountState) : { active: true };
32
+ const account = await getAccountByIdentifier(env, outcome.did);
33
+ const email = account?.email ?? (env.PDS_EMAIL as string | undefined);
34
+
35
+ return new Response(
36
+ JSON.stringify({
37
+ did: outcome.did,
38
+ didDoc,
39
+ handle: outcome.handle,
40
+ accessJwt: outcome.accessJwt,
41
+ refreshJwt: outcome.refreshJwt,
42
+ active: wire.active,
43
+ ...(wire.status ? { status: wire.status } : {}),
44
+ ...(email ? { email, emailConfirmed: true, emailAuthFactor: false } : {}),
45
+ }),
46
+ { headers: { 'Content-Type': 'application/json' } },
47
+ );
90
48
  }
@@ -2,6 +2,10 @@ import type { APIContext } from 'astro';
2
2
  import { getDb } from '../../db/client';
3
3
  import { blob_ref } from '../../db/schema';
4
4
  import { eq } from 'drizzle-orm';
5
+ import { CID } from 'multiformats/cid';
6
+ import { sha256 } from 'multiformats/hashes/sha2';
7
+ import { putBlobRef } from '../../db/dal';
8
+ import { isAccountActive } from '../../db/dal';
5
9
 
6
10
  export const prerender = false;
7
11
 
@@ -19,66 +23,111 @@ export async function GET({ locals, url }: APIContext) {
19
23
  const { env } = locals.runtime;
20
24
 
21
25
  try {
26
+ const configuredDid = typeof env.PDS_DID === 'string' ? env.PDS_DID : undefined;
27
+ const did = url.searchParams.get('did') ?? configuredDid;
22
28
  const cid = url.searchParams.get('cid');
23
- if (!cid) {
29
+ if (!did || !cid) {
24
30
  return new Response(
25
31
  JSON.stringify({
26
32
  error: 'InvalidRequest',
27
- message: 'cid parameter is required'
33
+ message: 'did and cid parameters are required'
28
34
  }),
29
35
  { status: 400, headers: { 'Content-Type': 'application/json' } }
30
36
  );
31
37
  }
32
38
 
39
+ const active = await isAccountActive(env, did);
40
+ if (!active) {
41
+ return new Response(
42
+ JSON.stringify({ error: 'AccountInactive', message: 'Account is not active' }),
43
+ { status: 403, headers: { 'Content-Type': 'application/json' } }
44
+ );
45
+ }
46
+
33
47
  const db = getDb(env);
34
48
 
35
49
  // Look up blob metadata by CID
36
- const blobMeta = await db
50
+ let blobMeta = await db
37
51
  .select()
38
52
  .from(blob_ref)
39
53
  .where(eq(blob_ref.cid, cid))
40
54
  .get();
41
55
 
42
- if (!blobMeta) {
56
+ let key: string | null = blobMeta?.key ?? null;
57
+ let mime: string = blobMeta?.mime ?? 'application/octet-stream';
58
+ let size: number | null = blobMeta?.size ?? null;
59
+
60
+ // Fallback for older uploads: derive R2 key from CID (raw/sha256) if DB row missing
61
+ if (!key) {
62
+ try {
63
+ const link = CID.parse(cid);
64
+ // blob CIDs are raw (0x55) with sha256 multihash
65
+ if (link.multihash.code !== sha256.code) {
66
+ return new Response(
67
+ JSON.stringify({ error: 'InvalidRequest', message: 'Unsupported multihash' }),
68
+ { status: 400, headers: { 'Content-Type': 'application/json' } }
69
+ );
70
+ }
71
+ // Recreate legacy R2 key scheme used by store.put()
72
+ const digest = link.multihash.digest; // Uint8Array
73
+ // base64url encode
74
+ let s = '';
75
+ for (const b of digest) s += String.fromCharCode(b);
76
+ const b64url = btoa(s).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
77
+ key = `blobs/by-cid/${b64url}`;
78
+ } catch {
79
+ key = null;
80
+ }
81
+ }
82
+
83
+ if (!key) {
43
84
  return new Response(
44
- JSON.stringify({
45
- error: 'BlobNotFound',
46
- message: 'Blob not found'
47
- }),
48
- { status: 404, headers: { 'Content-Type': 'application/json' } }
85
+ JSON.stringify({ error: 'InvalidRequest', message: 'Blob not found' }),
86
+ { status: 400, headers: { 'Content-Type': 'application/json' } }
49
87
  );
50
88
  }
51
89
 
52
90
  // Fetch blob from R2
53
- const r2 = env.BLOBS;
54
- const object = await r2.get(blobMeta.key);
91
+ const r2 = env.ALTERAN_BLOBS;
92
+ const object = await r2.get(key);
55
93
 
56
94
  if (!object) {
57
95
  return new Response(
58
- JSON.stringify({
59
- error: 'BlobNotFound',
60
- message: 'Blob not found in storage'
61
- }),
62
- { status: 404, headers: { 'Content-Type': 'application/json' } }
96
+ JSON.stringify({ error: 'InvalidRequest', message: 'Blob not found' }),
97
+ { status: 400, headers: { 'Content-Type': 'application/json' } }
63
98
  );
64
99
  }
65
100
 
66
- // Stream the blob with appropriate content type
67
- return new Response(object.body as any, {
101
+ if (!blobMeta) {
102
+ try {
103
+ size = object.size ?? size ?? 0;
104
+ await putBlobRef(env, did, cid, key, mime, Number(size ?? 0));
105
+ } catch (backfillError) {
106
+ // Backfill is opportunistic; serving the blob is the priority.
107
+ console.warn('getBlob backfill failed:', backfillError);
108
+ }
109
+ }
110
+
111
+ // workers-types' ReadableStream lacks the DOM-types readMany member; the
112
+ // shapes are wire-compatible at runtime, so widen through unknown.
113
+ return new Response(object.body as unknown as ReadableStream<Uint8Array>, {
68
114
  status: 200,
69
115
  headers: {
70
- 'Content-Type': blobMeta.mime,
71
- 'Content-Length': blobMeta.size.toString(),
116
+ 'Content-Type': mime,
117
+ ...(size != null ? { 'Content-Length': String(size) } : {}),
72
118
  'Cache-Control': 'public, max-age=31536000, immutable',
119
+ 'x-content-type-options': 'nosniff',
120
+ 'content-security-policy': "default-src 'none'; sandbox",
73
121
  },
74
122
  });
75
- } catch (error: any) {
123
+ } catch (error) {
124
+ const message = error instanceof Error ? error.message : 'Failed to retrieve blob';
76
125
  return new Response(
77
126
  JSON.stringify({
78
127
  error: 'InternalServerError',
79
- message: error.message || 'Failed to retrieve blob'
128
+ message,
80
129
  }),
81
130
  { status: 500, headers: { 'Content-Type': 'application/json' } }
82
131
  );
83
132
  }
84
- }
133
+ }
@@ -8,7 +8,7 @@ export const prerender = false;
8
8
  export async function GET({ locals, request }: APIContext) {
9
9
  const { env } = locals.runtime;
10
10
  const url = new URL(request.url);
11
- const did = url.searchParams.get('did') ?? (env.PDS_DID ?? 'did:example:single-user');
11
+ const did = url.searchParams.get('did') ?? (env.PDS_DID as string);
12
12
  const head = await getRepoRoot(env);
13
13
  const rows = await dalListRecords(env);
14
14
  const records = rows
@@ -6,7 +6,7 @@ export const prerender = false;
6
6
  export async function GET({ locals, request }: APIContext) {
7
7
  const { env } = locals.runtime;
8
8
  const url = new URL(request.url);
9
- const did = url.searchParams.get('did') ?? (env.PDS_DID ?? 'did:example:single-user');
9
+ const did = url.searchParams.get('did') ?? (env.PDS_DID as string);
10
10
 
11
11
  // Support commit range queries
12
12
  const fromParam = url.searchParams.get('from');
@@ -6,6 +6,11 @@ export const prerender = false;
6
6
  export async function GET({ locals }: APIContext) {
7
7
  const { env } = locals.runtime;
8
8
  const root = await getRepoRoot(env);
9
- if (!root) return new Response(JSON.stringify({ root: null, rev: 0 }), { headers: { 'Content-Type': 'application/json' } });
10
- return new Response(JSON.stringify({ root: root.commitCid, rev: root.rev }), { headers: { 'Content-Type': 'application/json' } });
9
+ if (!root) {
10
+ return new Response(
11
+ JSON.stringify({ error: 'HeadNotFound', message: 'Head not found' }),
12
+ { status: 404, headers: { 'Content-Type': 'application/json' } }
13
+ );
14
+ }
15
+ return new Response(JSON.stringify({ root: root.commitCid }), { headers: { 'Content-Type': 'application/json' } });
11
16
  }
@@ -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 did = url.searchParams.get('did') || env.PDS_DID || 'did:example:single-user';
13
+ const did = url.searchParams.get('did') || (env.PDS_DID as string);
14
14
 
15
15
  try {
16
16
  const root = await getRoot(env);
@@ -1,9 +1,6 @@
1
1
  import type { APIContext } from 'astro';
2
2
  import { RepoManager } from '../../services/repo-manager';
3
- import { encodeRecordBlock } from '../../services/car';
4
- import * as dagCbor from '@ipld/dag-cbor';
5
- import { CID } from 'multiformats/cid';
6
- import { sha256 } from 'multiformats/hashes/sha2';
3
+ import { buildRecordProofCar } from '../../services/car';
7
4
 
8
5
  export const prerender = false;
9
6
 
@@ -14,7 +11,7 @@ export const prerender = false;
14
11
  export async function GET({ locals, url }: APIContext) {
15
12
  const { env } = locals.runtime;
16
13
 
17
- const did = url.searchParams.get('did') || env.PDS_DID || 'did:example:single-user';
14
+ const did = url.searchParams.get('did') || (env.PDS_DID as string);
18
15
  const collection = url.searchParams.get('collection');
19
16
  const rkey = url.searchParams.get('rkey');
20
17
 
@@ -26,31 +23,12 @@ export async function GET({ locals, url }: APIContext) {
26
23
  }
27
24
 
28
25
  try {
29
- const repoManager = new RepoManager(env);
30
- const record = await repoManager.getRecord(collection, rkey);
31
-
32
- if (!record) {
33
- return new Response(
34
- JSON.stringify({ error: 'RecordNotFound' }),
35
- { status: 404, headers: { 'Content-Type': 'application/json' } }
36
- );
37
- }
38
-
39
- // Encode the record as a single-block CAR snapshot (root = record CID)
40
- const { cid, bytes } = await encodeRecordBlock(record);
41
-
42
- // Minimal CAR encoding (header + single block)
43
- const header = dagCbor.encode({ version: 1, roots: [cid] });
44
- const varint = (n: number) => { const a:number[]=[]; while(n>=0x80){a.push((n&0x7f)|0x80); n>>>=7;} a.push(n); return new Uint8Array(a); };
45
- const concat = (parts: Uint8Array[]) => { const len = parts.reduce((n,p)=>n+p.byteLength,0); const out = new Uint8Array(len); let o=0; for(const p of parts){out.set(p,o); o+=p.byteLength;} return out; };
46
- const block = concat([cid.bytes, bytes]);
47
- const carBytes = concat([varint(header.byteLength), header, varint(block.byteLength), block]);
48
-
49
- return new Response(carBytes as any, {
26
+ const { bytes } = await buildRecordProofCar(env as any, did, collection, rkey);
27
+ return new Response(bytes as any, {
50
28
  status: 200,
51
29
  headers: {
52
30
  'Content-Type': 'application/vnd.ipld.car; version=1',
53
- 'Content-Disposition': 'inline; filename="record.car"',
31
+ 'Content-Disposition': 'inline; filename="record-proof.car"',
54
32
  },
55
33
  });
56
34
  } catch (error) {
@@ -8,7 +8,7 @@ export const prerender = false;
8
8
  export async function GET({ locals, request }: APIContext) {
9
9
  const { env } = locals.runtime;
10
10
  const url = new URL(request.url);
11
- const did = url.searchParams.get('did') ?? (env.PDS_DID ?? 'did:example:single-user');
11
+ const did = url.searchParams.get('did') ?? (env.PDS_DID as string);
12
12
  const head = await getRepoRoot(env);
13
13
  const rows = await dalListRecords(env);
14
14
  const records = rows
@@ -1,17 +1,62 @@
1
1
  import type { APIContext } from 'astro';
2
+ import { errorMessage } from '../../lib/errors';
2
3
  import { buildRepoCar } from '../../services/car';
3
4
 
4
5
  export const prerender = false;
5
6
 
7
+ /**
8
+ * com.atproto.sync.getRepo
9
+ * Returns a CAR snapshot of the repo for initial crawl/index.
10
+ */
6
11
  export async function GET({ locals, request }: APIContext) {
7
12
  const { env } = locals.runtime;
8
13
  const url = new URL(request.url);
9
- const did = url.searchParams.get('did') ?? (env.PDS_DID ?? 'did:example:single-user');
10
- const car = await buildRepoCar(env, did);
11
- return new Response(car.bytes as any, {
14
+ const did = url.searchParams.get('did') ?? (env.PDS_DID as string);
15
+ // Phase 1: accept but ignore since param (diff to be implemented later)
16
+ const _since = url.searchParams.get('since');
17
+
18
+ try {
19
+ const { bytes } = await buildRepoCar(env, did);
20
+ const stream = new ReadableStream<Uint8Array>({
21
+ start(controller) {
22
+ controller.enqueue(bytes);
23
+ controller.close();
24
+ },
25
+ });
26
+ return new Response(stream as any, {
27
+ status: 200,
28
+ headers: {
29
+ // Official content type for CAR v1
30
+ 'Content-Type': 'application/vnd.ipld.car',
31
+ 'Cache-Control': 'no-store',
32
+ },
33
+ });
34
+ } catch (error) {
35
+ const msg = String(errorMessage(error) || error);
36
+ // Map to lexicon-specified errors
37
+ const known = ['RepoNotFound', 'RepoTakendown', 'RepoSuspended', 'RepoDeactivated'];
38
+ const name = known.find((n) => msg.includes(n)) || (msg.includes('HeadNotFound') ? 'RepoNotFound' : null);
39
+ if (name) {
40
+ return new Response(JSON.stringify({ error: name, message: msg }), {
41
+ status: 400,
42
+ headers: { 'Content-Type': 'application/json' },
43
+ });
44
+ }
45
+ console.error('getRepo (CAR) error:', error);
46
+ return new Response(JSON.stringify({ error: 'InternalServerError', message: msg }), {
47
+ status: 500,
48
+ headers: { 'Content-Type': 'application/json' },
49
+ });
50
+ }
51
+ }
52
+
53
+ // Avoid heavy CAR construction for HEAD. Respond with headers only.
54
+ export async function HEAD() {
55
+ return new Response(null, {
56
+ status: 200,
12
57
  headers: {
13
- 'content-type': 'application/vnd.ipld.car; version=1',
14
- 'content-disposition': 'inline; filename="repo.car"',
58
+ 'Content-Type': 'application/vnd.ipld.car',
59
+ 'Cache-Control': 'no-store',
15
60
  },
16
61
  });
17
62
  }
@@ -0,0 +1,58 @@
1
+ import type { APIContext } from 'astro';
2
+ import { getRoot as getRepoRoot } from '../../db/repo';
3
+ import { getAccountState } from '../../db/dal';
4
+ import { toWireStatus } from '../../lib/account-state';
5
+
6
+ export const prerender = false;
7
+
8
+ /**
9
+ * com.atproto.sync.getRepoStatus
10
+ * Mirrors upstream PDS: returns did, active, optional status, and rev if active.
11
+ */
12
+ export async function GET({ locals, request }: APIContext) {
13
+ const { env } = locals.runtime;
14
+ const url = new URL(request.url);
15
+ const configuredDid = typeof env.PDS_DID === 'string' ? env.PDS_DID : '';
16
+ const did = url.searchParams.get('did') ?? configuredDid;
17
+
18
+ try {
19
+ // Best-effort FSM lookup: an unmigrated row or a transient DB error both
20
+ // fall through to active=true so reads aren't blocked by an internal hiccup.
21
+ let active = true;
22
+ let status: string | undefined;
23
+ try {
24
+ const state = await getAccountState(env, did);
25
+ if (state) {
26
+ const wire = toWireStatus(state);
27
+ active = wire.active;
28
+ status = wire.status;
29
+ }
30
+ } catch (stateError) {
31
+ console.warn('getAccountState failed:', stateError);
32
+ }
33
+
34
+ let rev: string | undefined;
35
+ if (active) {
36
+ const head = await getRepoRoot(env);
37
+ if (head?.rev) rev = String(head.rev);
38
+ }
39
+
40
+ return new Response(
41
+ JSON.stringify({ did, active, ...(status ? { status } : {}), ...(rev ? { rev } : {}) }),
42
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
43
+ );
44
+ } catch (error) {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ if (message.includes('RepoNotFound')) {
47
+ return new Response(JSON.stringify({ error: 'RepoNotFound', message }), {
48
+ status: 400,
49
+ headers: { 'Content-Type': 'application/json' },
50
+ });
51
+ }
52
+ return new Response(JSON.stringify({ error: 'InternalServerError', message }), {
53
+ status: 500,
54
+ headers: { 'Content-Type': 'application/json' },
55
+ });
56
+ }
57
+ }
58
+
@@ -12,12 +12,12 @@ export const prerender = false;
12
12
  export async function GET({ locals, url }: APIContext) {
13
13
  const { env } = locals.runtime;
14
14
 
15
- const did = url.searchParams.get('did') || env.PDS_DID || 'did:example:single-user';
15
+ const did = url.searchParams.get('did') || (env.PDS_DID as string);
16
16
  const since = url.searchParams.get('since') || '';
17
17
  const limit = parseInt(url.searchParams.get('limit') || '500', 10);
18
18
 
19
19
  try {
20
- const db = drizzle(env.DB);
20
+ const db = drizzle(env.ALTERAN_DB);
21
21
 
22
22
  const blobs = since
23
23
  ? await db
@@ -1,4 +1,5 @@
1
1
  import type { APIContext } from 'astro';
2
+ import { getRoot as getRepoRoot } from '../../db/repo';
2
3
 
3
4
  export const prerender = false;
4
5
 
@@ -11,14 +12,15 @@ export async function GET({ locals, url }: APIContext) {
11
12
 
12
13
  const did = env.PDS_DID || 'did:example:single-user';
13
14
  const handle = env.PDS_HANDLE || 'user.example.com';
15
+ const head = await getRepoRoot(env);
14
16
 
15
17
  return new Response(
16
18
  JSON.stringify({
17
19
  repos: [
18
20
  {
19
21
  did,
20
- head: '', // TODO: Get from repo_root
21
- rev: '', // TODO: Get from repo_root
22
+ head: head?.commitCid ?? null,
23
+ rev: head?.rev ?? null,
22
24
  active: true,
23
25
  },
24
26
  ],
@@ -28,4 +30,4 @@ export async function GET({ locals, url }: APIContext) {
28
30
  headers: { 'Content-Type': 'application/json' },
29
31
  }
30
32
  );
31
- }
33
+ }