@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
package/src/lib/auth.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { APIContext } from 'astro';
2
2
  import type { Env } from '../env';
3
+ import { AuthTokenExpiredError } from './auth-errors';
3
4
  import { verifyJwt, type JwtClaims } from './jwt';
5
+ import { bearerToken } from './util';
4
6
 
5
7
  export interface AuthContext {
6
8
  token: string;
@@ -9,18 +11,57 @@ export interface AuthContext {
9
11
 
10
12
  export async function isAuthorized(request: Request, env: Env): Promise<boolean> {
11
13
  const auth = request.headers.get('authorization');
12
- if (!auth || !auth.startsWith('Bearer ')) return false;
13
- const token = auth.slice(7);
14
+
15
+ console.error('=== AUTH DEBUG START ===');
16
+ console.error('URL:', request.url);
17
+ console.error('Has Auth Header:', !!auth);
18
+ console.error('Auth Prefix:', auth?.substring(0, 30));
19
+ console.error('=== AUTH DEBUG END ===');
20
+
21
+ const token = bearerToken(request);
22
+ if (!token) {
23
+ console.error('RESULT: No Bearer or DPoP token found');
24
+ return false;
25
+ }
26
+
27
+ console.error('Token Length:', token.length);
28
+ console.error('Token Prefix:', token.substring(0, 30));
29
+
14
30
  // Prefer JWT
15
- const ver = await verifyJwt(env, token).catch((err) => {
16
- console.error('JWT verification error:', err);
17
- return null;
18
- });
19
- if (ver && ver.valid && ver.payload.t === 'access') return true;
31
+ let ver;
32
+ try {
33
+ ver = await verifyJwt(env, token);
34
+ } catch (error) {
35
+ if (error instanceof AuthTokenExpiredError) {
36
+ throw error;
37
+ }
38
+ console.error('JWT VERIFICATION ERROR:', error instanceof Error ? error.message : String(error));
39
+ return false;
40
+ }
41
+
42
+ console.error('JWT Valid:', ver?.valid);
43
+ console.error('JWT Type:', ver?.payload?.t);
44
+ console.error('JWT Sub:', ver?.payload?.sub);
45
+
46
+ if (ver && ver.valid && ver.payload.t === 'access') {
47
+ console.error('RESULT: JWT Success');
48
+ return true;
49
+ }
50
+
20
51
  // Back-compat local escape hatch if explicitly enabled
21
52
  const allowDev = (env as any).PDS_ALLOW_DEV_TOKEN === '1';
22
- if (allowDev && token === 'dev-access-token') return true;
23
- if (allowDev && env.USER_PASSWORD && token === env.USER_PASSWORD) return true;
53
+ console.error('Allow Dev Token:', allowDev);
54
+
55
+ if (allowDev && token === 'dev-access-token') {
56
+ console.error('RESULT: Dev token accepted');
57
+ return true;
58
+ }
59
+ if (allowDev && env.USER_PASSWORD && token === env.USER_PASSWORD) {
60
+ console.error('RESULT: User password accepted');
61
+ return true;
62
+ }
63
+
64
+ console.error('RESULT: Unauthorized');
24
65
  return false;
25
66
  }
26
67
 
@@ -29,15 +70,22 @@ export function unauthorized() {
29
70
  }
30
71
 
31
72
  export async function authenticateRequest(request: Request, env: Env): Promise<AuthContext | null> {
32
- const auth = request.headers.get('authorization');
33
- if (!auth || !auth.startsWith('Bearer ')) return null;
34
- const token = auth.slice(7);
35
- const ver = await verifyJwt(env, token).catch((err) => {
36
- console.error('JWT verification error:', err);
73
+ const token = bearerToken(request);
74
+ if (!token) return null;
75
+ let ver;
76
+ try {
77
+ ver = await verifyJwt(env, token);
78
+ } catch (error) {
79
+ if (error instanceof AuthTokenExpiredError) {
80
+ throw error;
81
+ }
82
+ console.error('JWT verification error:', error);
37
83
  return null;
38
- });
84
+ }
39
85
  if (!ver || !ver.valid) return null;
40
86
  const claims = ver.payload as JwtClaims;
41
87
  if (claims.t !== 'access') return null;
42
88
  return { token, claims };
43
89
  }
90
+
91
+ export { AuthTokenExpiredError, expiredToken } from './auth-errors';
@@ -11,7 +11,7 @@ import * as dagCbor from '@ipld/dag-cbor';
11
11
  * This traverses the MST structure to find all blocks that are still in use
12
12
  */
13
13
  async function collectReferencedCids(env: Env, keepCommits: number = 10000): Promise<Set<string>> {
14
- const db = drizzle(env.DB);
14
+ const db = drizzle(env.ALTERAN_DB);
15
15
  const referenced = new Set<string>();
16
16
 
17
17
  // Get recent commits
@@ -60,12 +60,13 @@ async function collectReferencedCids(env: Env, keepCommits: number = 10000): Pro
60
60
  * Recursively traverse MST nodes to collect all CIDs
61
61
  */
62
62
  async function traverseMst(env: Env, rootCid: string, referenced: Set<string>): Promise<void> {
63
- const db = drizzle(env.DB);
63
+ const db = drizzle(env.ALTERAN_DB);
64
64
  const visited = new Set<string>();
65
65
  const queue = [rootCid];
66
66
 
67
67
  while (queue.length > 0) {
68
- const cidStr = queue.shift()!;
68
+ const cidStr = queue.shift();
69
+ if (cidStr === undefined) break;
69
70
 
70
71
  if (visited.has(cidStr)) continue;
71
72
  visited.add(cidStr);
@@ -127,7 +128,7 @@ async function traverseMst(env: Env, rootCid: string, referenced: Set<string>):
127
128
  * @returns Number of blocks removed
128
129
  */
129
130
  export async function pruneOrphanedBlocks(env: Env, keepCommits: number = 10000): Promise<number> {
130
- const db = drizzle(env.DB);
131
+ const db = drizzle(env.ALTERAN_DB);
131
132
 
132
133
  // Collect all CIDs referenced by recent commits
133
134
  const referenced = await collectReferencedCids(env, keepCommits);
@@ -180,7 +181,7 @@ export async function getBlockstoreStats(env: Env): Promise<{
180
181
  total: number;
181
182
  totalSize: number;
182
183
  }> {
183
- const db = drizzle(env.DB);
184
+ const db = drizzle(env.ALTERAN_DB);
184
185
  const blocks = await db.select().from(blockstore).all();
185
186
 
186
187
  const totalSize = blocks.reduce((sum, block) => {
package/src/lib/cache.ts CHANGED
@@ -97,12 +97,26 @@ export function getCacheKey(request: Request, prefix?: string): string {
97
97
  /**
98
98
  * Get cached response from Cache API
99
99
  */
100
+ function resolveDefaultCache(): Cache | null {
101
+ if (typeof caches === 'undefined') {
102
+ return null;
103
+ }
104
+ try {
105
+ return ((caches as any).default ?? null) as Cache | null;
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
100
111
  export async function getCachedResponse(
101
112
  request: Request,
102
113
  options?: { prefix?: string }
103
114
  ): Promise<Response | null> {
104
115
  try {
105
- const cache = (caches as any).default as Cache;
116
+ const cache = resolveDefaultCache();
117
+ if (!cache) {
118
+ return null;
119
+ }
106
120
  const cacheKey = getCacheKey(request, options?.prefix);
107
121
  const cacheUrl = new URL(cacheKey, request.url);
108
122
  const cacheRequest = new Request(cacheUrl, request);
@@ -123,7 +137,10 @@ export async function putCachedResponse(
123
137
  options: CacheOptions
124
138
  ): Promise<void> {
125
139
  try {
126
- const cache = (caches as any).default as Cache;
140
+ const cache = resolveDefaultCache();
141
+ if (!cache) {
142
+ return;
143
+ }
127
144
  const cacheKey = getCacheKey(request, options.prefix);
128
145
  const cacheUrl = new URL(cacheKey, request.url);
129
146
  const cacheRequest = new Request(cacheUrl, request);
@@ -155,7 +172,10 @@ export async function invalidateCache(
155
172
  options?: { prefix?: string }
156
173
  ): Promise<boolean> {
157
174
  try {
158
- const cache = (caches as any).default as Cache;
175
+ const cache = resolveDefaultCache();
176
+ if (!cache) {
177
+ return false;
178
+ }
159
179
  const cacheKey = getCacheKey(request, options?.prefix);
160
180
  const cacheUrl = new URL(cacheKey, request.url);
161
181
  const cacheRequest = new Request(cacheUrl, request);
@@ -183,7 +203,13 @@ export async function withCache(
183
203
  // Check cache first
184
204
  const cached = await getCachedResponse(request, { prefix: options.prefix });
185
205
  if (cached) {
186
- return cached;
206
+ // Clone the cached response to avoid immutable headers issue
207
+ // Cache API responses have immutable headers which Astro may try to modify
208
+ return new Response(cached.body, {
209
+ status: cached.status,
210
+ statusText: cached.statusText,
211
+ headers: new Headers(cached.headers),
212
+ });
187
213
  }
188
214
 
189
215
  // Generate response
package/src/lib/chat.ts CHANGED
@@ -10,30 +10,30 @@ export interface ListConvosFilters {
10
10
  export interface ConvoView {
11
11
  id: string;
12
12
  rev: string;
13
- members: any[];
13
+ members: unknown[];
14
14
  muted: boolean;
15
15
  unreadCount: number;
16
16
  status?: string;
17
- lastMessage?: any;
18
- lastReaction?: any;
17
+ lastMessage?: unknown;
18
+ lastReaction?: unknown;
19
19
  }
20
20
 
21
21
  export type ConvoLogEntry =
22
22
  | { $type: 'chat.bsky.convo.defs#logBeginConvo'; rev: string; convoId: string }
23
- | { $type: 'chat.bsky.convo.defs#logCreateMessage'; rev: string; convoId: string; message: any }
23
+ | { $type: 'chat.bsky.convo.defs#logCreateMessage'; rev: string; convoId: string; message: unknown }
24
24
  | {
25
25
  $type: 'chat.bsky.convo.defs#logAddReaction';
26
26
  rev: string;
27
27
  convoId: string;
28
- message: any;
29
- reaction: any;
28
+ message: unknown;
29
+ reaction: unknown;
30
30
  };
31
31
 
32
32
  export async function ensureChatTables(env: Env) {
33
33
  if (tablesEnsured) return;
34
34
 
35
35
  // Create chat_convo table
36
- await env.DB.prepare(
36
+ await env.ALTERAN_DB.prepare(
37
37
  'CREATE TABLE IF NOT EXISTS chat_convo (' +
38
38
  'id TEXT PRIMARY KEY, ' +
39
39
  'rev TEXT NOT NULL, ' +
@@ -48,7 +48,7 @@ export async function ensureChatTables(env: Env) {
48
48
  ).run();
49
49
 
50
50
  // Create chat_convo_member table
51
- await env.DB.prepare(
51
+ await env.ALTERAN_DB.prepare(
52
52
  'CREATE TABLE IF NOT EXISTS chat_convo_member (' +
53
53
  'convo_id TEXT NOT NULL, ' +
54
54
  'did TEXT NOT NULL, ' +
@@ -61,7 +61,7 @@ export async function ensureChatTables(env: Env) {
61
61
  ).run();
62
62
 
63
63
  // Create index
64
- await env.DB.prepare(
64
+ await env.ALTERAN_DB.prepare(
65
65
  'CREATE INDEX IF NOT EXISTS chat_convo_member_did_idx ON chat_convo_member (did)'
66
66
  ).run();
67
67
 
@@ -103,7 +103,7 @@ export async function listChatConvos(
103
103
  query += ' ORDER BY rowid DESC LIMIT ?';
104
104
  params.push(limit);
105
105
 
106
- const result = await env.DB.prepare(query).bind(...params).all<{
106
+ const result = await env.ALTERAN_DB.prepare(query).bind(...params).all<{
107
107
  rowid: number;
108
108
  id: string;
109
109
  rev: string;
@@ -119,7 +119,7 @@ export async function listChatConvos(
119
119
 
120
120
  if (result.results) {
121
121
  for (const row of result.results) {
122
- const members = await env.DB.prepare(
122
+ const members = await env.ALTERAN_DB.prepare(
123
123
  `SELECT did, handle, display_name, avatar FROM chat_convo_member WHERE convo_id = ? ORDER BY position ASC`
124
124
  )
125
125
  .bind(row.id)
@@ -130,8 +130,14 @@ export async function listChatConvos(
130
130
  avatar: string | null;
131
131
  }>();
132
132
 
133
- const memberViews = (members.results ?? []).map((member) => {
134
- const view: any = {
133
+ type MemberView = {
134
+ did: string;
135
+ handle: string;
136
+ displayName?: string;
137
+ avatar?: string;
138
+ };
139
+ const memberViews: MemberView[] = (members.results ?? []).map((member) => {
140
+ const view: MemberView = {
135
141
  did: member.did,
136
142
  handle: member.handle,
137
143
  };
@@ -180,7 +186,7 @@ export async function listChatConvoLogs(env: Env, did: string, cursor?: number,
180
186
  query += ' ORDER BY rowid DESC LIMIT ?';
181
187
  params.push(limit);
182
188
 
183
- const result = await env.DB.prepare(query).bind(...params).all<{
189
+ const result = await env.ALTERAN_DB.prepare(query).bind(...params).all<{
184
190
  rowid: number;
185
191
  id: string;
186
192
  rev: string;
@@ -18,7 +18,7 @@ import { logger } from './logger';
18
18
  * @returns Number of commits pruned
19
19
  */
20
20
  export async function pruneOldCommits(env: Env, keepCount: number = 10000): Promise<number> {
21
- const db = drizzle(env.DB);
21
+ const db = drizzle(env.ALTERAN_DB);
22
22
 
23
23
  // Get the sequence number of the Nth most recent commit
24
24
  const threshold = await db
@@ -60,7 +60,7 @@ export async function getCommitLogStats(env: Env): Promise<{
60
60
  oldest: number | null;
61
61
  newest: number | null;
62
62
  }> {
63
- const db = drizzle(env.DB);
63
+ const db = drizzle(env.ALTERAN_DB);
64
64
 
65
65
  const [oldest, newest, count] = await Promise.all([
66
66
  db.select({ seq: commit_log.seq }).from(commit_log).orderBy(commit_log.seq).limit(1).get(),
package/src/lib/commit.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { CID } from 'multiformats/cid';
2
2
  import * as dagCbor from '@ipld/dag-cbor';
3
3
  import { sha256 } from 'multiformats/hashes/sha2';
4
+ import { Secp256k1Keypair, verifySignature } from '@atproto/crypto';
5
+ import { ServerMisconfigured } from './errors';
4
6
 
5
7
  /**
6
8
  * AT Protocol Commit Structure
@@ -12,7 +14,7 @@ import { sha256 } from 'multiformats/hashes/sha2';
12
14
  * - data: CID of the MST root
13
15
  * - rev: Revision number (TID format)
14
16
  * - prev: CID of the previous commit (null for first commit)
15
- * - sig: Ed25519 signature over the commit data
17
+ * - sig: secp256k1 signature over the commit data (64-byte compact)
16
18
  */
17
19
 
18
20
  export interface CommitData {
@@ -46,34 +48,37 @@ export function createCommit(
46
48
  }
47
49
 
48
50
  /**
49
- * Sign a commit with Ed25519 private key
51
+ * Sign a commit with secp256k1 private key
50
52
  */
51
53
  export async function signCommit(
52
54
  commit: CommitData,
53
- privateKeyBase64: string,
55
+ privateKey: string,
54
56
  ): Promise<SignedCommit> {
55
57
  // Encode commit to CBOR for signing
56
58
  const commitBytes = dagCbor.encode(commit);
57
59
 
58
- // Import private key (PKCS#8 base64)
59
- const b64 = privateKeyBase64.replace(/\s+/g, '');
60
- const bin = atob(b64);
61
- const pkcs8 = new Uint8Array(bin.length);
62
- for (let i = 0; i < bin.length; i++) pkcs8[i] = bin.charCodeAt(i);
63
- const privateKey = await crypto.subtle.importKey(
64
- 'pkcs8',
65
- pkcs8,
66
- { name: 'Ed25519', namedCurve: 'Ed25519' } as any,
67
- false,
68
- ['sign']
69
- );
70
-
71
- // Sign the commit bytes
72
- const signature = await crypto.subtle.sign('Ed25519', privateKey, new Uint8Array(commitBytes as unknown as Uint8Array));
60
+ // Accept hex (preferred) or base64 input for the 32-byte secp256k1 private key
61
+ const cleaned = privateKey.trim();
62
+ let keypair: Secp256k1Keypair;
63
+ if (/^[0-9a-fA-F]{64}$/.test(cleaned)) {
64
+ keypair = await Secp256k1Keypair.import(cleaned);
65
+ } else {
66
+ // try base64
67
+ try {
68
+ const bin = atob(cleaned.replace(/\s+/g, ''));
69
+ const priv = new Uint8Array(bin.length);
70
+ for (let i = 0; i < bin.length; i++) priv[i] = bin.charCodeAt(i);
71
+ keypair = await Secp256k1Keypair.import(priv);
72
+ } catch {
73
+ throw new ServerMisconfigured('Invalid REPO_SIGNING_KEY format: expected 32-byte hex or base64');
74
+ }
75
+ }
76
+
77
+ const signature = await keypair.sign(new Uint8Array(commitBytes as unknown as Uint8Array));
73
78
 
74
79
  return {
75
80
  ...commit,
76
- sig: new Uint8Array(signature),
81
+ sig: signature,
77
82
  };
78
83
  }
79
84
 
@@ -82,28 +87,13 @@ export async function signCommit(
82
87
  */
83
88
  export async function verifyCommit(
84
89
  signedCommit: SignedCommit,
85
- publicKeyBase64: string,
90
+ didKey: string,
86
91
  ): Promise<boolean> {
87
92
  try {
88
93
  // Extract commit data (without signature)
89
94
  const { sig, ...commit } = signedCommit;
90
95
  const commitBytes = dagCbor.encode(commit);
91
-
92
- // Import public key (SPKI base64)
93
- const b64 = publicKeyBase64.replace(/\s+/g, '');
94
- const bin = atob(b64);
95
- const spki = new Uint8Array(bin.length);
96
- for (let i = 0; i < bin.length; i++) spki[i] = bin.charCodeAt(i);
97
- const publicKey = await crypto.subtle.importKey(
98
- 'spki',
99
- spki,
100
- { name: 'Ed25519', namedCurve: 'Ed25519' } as any,
101
- false,
102
- ['verify']
103
- );
104
-
105
- // Verify signature
106
- return await crypto.subtle.verify('Ed25519', publicKey, sig as any, new Uint8Array(commitBytes as unknown as Uint8Array));
96
+ return verifySignature(didKey, new Uint8Array(commitBytes as unknown as Uint8Array), sig);
107
97
  } catch (error) {
108
98
  console.error('Commit verification failed:', error);
109
99
  return false;
package/src/lib/config.ts CHANGED
@@ -20,21 +20,26 @@ const OPTIONAL_VARS = {
20
20
  PDS_CORS_ORIGIN: '*',
21
21
  PDS_SEQ_WINDOW: '512',
22
22
  ENVIRONMENT: 'development',
23
- PDS_BSKY_APP_VIEW_URL: 'https://public.api.bsky.app',
23
+ PDS_BSKY_APP_VIEW_URL: 'https://api.bsky.app',
24
24
  PDS_BSKY_APP_VIEW_DID: 'did:web:api.bsky.app',
25
25
  PDS_BSKY_APP_VIEW_CDN_URL_PATTERN: '',
26
+ // Additional proxied services
27
+ PDS_BSKY_CHAT_URL: 'https://api.bsky.chat',
28
+ PDS_BSKY_CHAT_DID: 'did:web:api.bsky.chat',
29
+ PDS_OZONE_URL: 'https://mod.bsky.app',
30
+ PDS_OZONE_DID: 'did:plc:ar7c4by46qjdydhdevvrndac',
26
31
  } as const;
27
32
 
28
33
  /**
29
34
  * Configuration validation result
30
35
  */
31
36
  export interface ConfigValidationResult {
32
- valid: boolean;
33
- missing: string[];
34
- warnings: string[];
35
- config: {
36
- required: Record<string, string>;
37
- optional: Record<string, string>;
37
+ readonly valid: boolean;
38
+ readonly missing: readonly string[];
39
+ readonly warnings: readonly string[];
40
+ readonly config: {
41
+ readonly required: Readonly<Record<string, string>>;
42
+ readonly optional: Readonly<Record<string, string>>;
38
43
  };
39
44
  }
40
45
 
@@ -76,13 +81,13 @@ export function validateConfig(env: Env): ConfigValidationResult {
76
81
  }
77
82
 
78
83
  // DID format validation
79
- const did = env.PDS_DID;
84
+ const did = typeof env.PDS_DID === 'string' ? env.PDS_DID : undefined;
80
85
  if (did && !did.startsWith('did:')) {
81
86
  warnings.push(`PDS_DID should start with 'did:' (got: ${did})`);
82
87
  }
83
88
 
84
89
  // Handle format validation
85
- const handle = env.PDS_HANDLE;
90
+ const handle = typeof env.PDS_HANDLE === 'string' ? env.PDS_HANDLE : undefined;
86
91
  if (handle && handle.includes('://')) {
87
92
  warnings.push(`PDS_HANDLE should not include protocol (got: ${handle})`);
88
93
  }
@@ -108,9 +113,7 @@ export function validateConfig(env: Env): ConfigValidationResult {
108
113
  warnings.push('REPO_SIGNING_KEY is not set - repository commits will not be signed');
109
114
  }
110
115
 
111
- if (!env.PDS_SERVICE_SIGNING_KEY_HEX) {
112
- warnings.push('PDS_SERVICE_SIGNING_KEY_HEX is not set - service-to-service authentication will be disabled');
113
- }
116
+ // Service-auth uses REPO_SIGNING_KEY (secp256k1). No separate service key required.
114
117
 
115
118
  const valid = missing.length === 0;
116
119
 
@@ -184,10 +187,18 @@ export function validateConfigOrThrow(env: Env): void {
184
187
  export function getConfig(env: Env) {
185
188
  const result = validateConfig(env);
186
189
 
190
+ const did = env.PDS_DID;
191
+ const handle = env.PDS_HANDLE;
192
+ if (typeof did !== 'string' || did === '' || typeof handle !== 'string' || handle === '') {
193
+ throw new Error(
194
+ `getConfig called with invalid configuration. Missing: ${result.missing.join(', ')}`,
195
+ );
196
+ }
197
+
187
198
  return {
188
199
  // Required
189
- did: env.PDS_DID!,
190
- handle: env.PDS_HANDLE!,
200
+ did,
201
+ handle,
191
202
 
192
203
  // Optional with defaults
193
204
  allowedMime: result.config.optional.PDS_ALLOWED_MIME.split(','),
@@ -209,7 +220,7 @@ export function getConfig(env: Env) {
209
220
  hostname: env.PDS_HOSTNAME,
210
221
  accessTtlSec: env.PDS_ACCESS_TTL_SEC ? parseInt(env.PDS_ACCESS_TTL_SEC) : 3600,
211
222
  refreshTtlSec: env.PDS_REFRESH_TTL_SEC ? parseInt(env.PDS_REFRESH_TTL_SEC) : 2592000,
212
- serviceSigningKeyHex: env.PDS_SERVICE_SIGNING_KEY_HEX,
223
+ serviceSigningKeyHex: undefined,
213
224
  };
214
225
  }
215
226
 
@@ -0,0 +1,32 @@
1
+ import type { Env } from '../env';
2
+ import { getRuntimeString } from './secrets';
3
+
4
+ export interface DidDocument {
5
+ '@context': string[];
6
+ id: string;
7
+ alsoKnownAs: string[];
8
+ verificationMethod: any[];
9
+ service: Array<{
10
+ id: string;
11
+ type: string;
12
+ serviceEndpoint: string;
13
+ }>;
14
+ }
15
+
16
+ export async function buildDidDocument(env: Env, did: string, handle: string): Promise<DidDocument> {
17
+ const hostname = await getRuntimeString(env, 'PDS_HOSTNAME', handle);
18
+
19
+ return {
20
+ '@context': ['https://www.w3.org/ns/did/v1'],
21
+ id: did,
22
+ alsoKnownAs: [`at://${handle}`],
23
+ verificationMethod: [],
24
+ service: [
25
+ {
26
+ id: '#atproto_pds',
27
+ type: 'AtprotoPersonalDataServer',
28
+ serviceEndpoint: `https://${hostname}`,
29
+ },
30
+ ],
31
+ };
32
+ }
package/src/lib/errors.ts CHANGED
@@ -93,6 +93,38 @@ export class InternalServerError extends XRPCError {
93
93
  }
94
94
  }
95
95
 
96
+ // 400 - Invalid atproto-proxy header
97
+ export class InvalidProxyHeader extends XRPCError {
98
+ constructor(message: string = 'Invalid atproto-proxy header', details?: Record<string, unknown>) {
99
+ super('InvalidProxyHeader', message, 400, details);
100
+ this.name = 'InvalidProxyHeader';
101
+ }
102
+ }
103
+
104
+ // 502 - Upstream proxy or DID resolution failure
105
+ export class UpstreamProxyFailure extends XRPCError {
106
+ constructor(message: string = 'Upstream proxy failure', details?: Record<string, unknown>) {
107
+ super('UpstreamProxyFailure', message, 502, details);
108
+ this.name = 'UpstreamProxyFailure';
109
+ }
110
+ }
111
+
112
+ // 500 - Server misconfiguration (missing secrets, invalid signing key, etc)
113
+ export class ServerMisconfigured extends XRPCError {
114
+ constructor(message: string = 'Server misconfigured', details?: Record<string, unknown>) {
115
+ super('ServerMisconfigured', message, 500, details);
116
+ this.name = 'ServerMisconfigured';
117
+ }
118
+ }
119
+
120
+ // 413 - Payload too large (rejected before parsing)
121
+ export class PayloadTooLarge extends XRPCError {
122
+ constructor(message: string = 'Payload too large', details?: Record<string, unknown>) {
123
+ super('PayloadTooLarge', message, 413, details);
124
+ this.name = 'PayloadTooLarge';
125
+ }
126
+ }
127
+
96
128
  /**
97
129
  * User-friendly error messages
98
130
  * Maps technical errors to actionable guidance
@@ -121,6 +153,28 @@ export function categorizeError(status: number): 'client' | 'server' {
121
153
  return status >= 400 && status < 500 ? 'client' : 'server';
122
154
  }
123
155
 
156
+ /**
157
+ * Narrow an unknown thrown value to extract its `code` and `message` fields
158
+ * without resorting to `any`. Useful in catch blocks for libraries that
159
+ * decorate Errors with custom code strings (jose, ResourceAuthError, etc.).
160
+ */
161
+ export function errorCode(error: unknown): string | undefined {
162
+ if (error && typeof error === 'object' && 'code' in error) {
163
+ const value = (error as { code: unknown }).code;
164
+ return typeof value === 'string' ? value : undefined;
165
+ }
166
+ return undefined;
167
+ }
168
+
169
+ export function errorMessage(error: unknown): string {
170
+ if (error instanceof Error) return error.message;
171
+ if (error && typeof error === 'object' && 'message' in error) {
172
+ const value = (error as { message: unknown }).message;
173
+ if (typeof value === 'string') return value;
174
+ }
175
+ return String(error);
176
+ }
177
+
124
178
  /**
125
179
  * Convert any error to XRPCError
126
180
  */