@alteran/astro 0.3.8 → 0.5.2

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 (136) 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/dal.ts +34 -23
  13. package/src/db/repo.ts +35 -35
  14. package/src/db/schema.ts +5 -1
  15. package/src/db/seed.ts +5 -13
  16. package/src/entrypoints/server.ts +2 -22
  17. package/src/handlers/root.ts +4 -4
  18. package/src/lib/account-state.ts +156 -0
  19. package/src/lib/actor.ts +28 -12
  20. package/src/lib/appview/auth-policy.ts +66 -0
  21. package/src/lib/appview/did-resolver.ts +233 -0
  22. package/src/lib/appview/proxy.ts +221 -0
  23. package/src/lib/appview/service-config.ts +61 -0
  24. package/src/lib/appview/service-jwt.ts +93 -0
  25. package/src/lib/appview/types.ts +25 -0
  26. package/src/lib/appview.ts +5 -532
  27. package/src/lib/auth-errors.ts +24 -0
  28. package/src/lib/auth.ts +63 -15
  29. package/src/lib/blockstore-gc.ts +2 -1
  30. package/src/lib/cache.ts +30 -4
  31. package/src/lib/chat.ts +14 -8
  32. package/src/lib/commit.ts +26 -36
  33. package/src/lib/config.ts +26 -15
  34. package/src/lib/did-document.ts +32 -0
  35. package/src/lib/errors.ts +54 -0
  36. package/src/lib/feed.ts +18 -19
  37. package/src/lib/firehose/frames.ts +87 -47
  38. package/src/lib/firehose/validation.ts +3 -3
  39. package/src/lib/jwt.ts +85 -177
  40. package/src/lib/labeler.ts +43 -30
  41. package/src/lib/logger.ts +4 -0
  42. package/src/lib/mst/block-map.ts +172 -0
  43. package/src/lib/mst/blockstore.ts +56 -93
  44. package/src/lib/mst/index.ts +1 -0
  45. package/src/lib/mst/leaf.ts +25 -0
  46. package/src/lib/mst/mst.ts +81 -237
  47. package/src/lib/mst/serialize.ts +97 -0
  48. package/src/lib/mst/types.ts +21 -0
  49. package/src/lib/oauth/clients.ts +67 -0
  50. package/src/lib/oauth/dpop-errors.ts +15 -0
  51. package/src/lib/oauth/dpop.ts +150 -0
  52. package/src/lib/oauth/resource.ts +199 -0
  53. package/src/lib/oauth/store.ts +77 -0
  54. package/src/lib/preferences.ts +9 -34
  55. package/src/lib/refresh-session.ts +161 -0
  56. package/src/lib/relay.ts +10 -8
  57. package/src/lib/secrets.ts +6 -7
  58. package/src/lib/sequencer.ts +12 -3
  59. package/src/lib/service-auth.ts +184 -0
  60. package/src/lib/session-tokens.ts +28 -76
  61. package/src/lib/streaming-car.ts +3 -0
  62. package/src/lib/tracing.ts +4 -3
  63. package/src/lib/util.ts +65 -15
  64. package/src/middleware.ts +1 -1
  65. package/src/pages/.well-known/did.json.ts +27 -30
  66. package/src/pages/.well-known/oauth-authorization-server.ts +31 -0
  67. package/src/pages/.well-known/oauth-protected-resource.ts +22 -0
  68. package/src/pages/debug/record.ts +1 -1
  69. package/src/pages/debug/sequencer.ts +28 -0
  70. package/src/pages/oauth/authorize.ts +78 -0
  71. package/src/pages/oauth/consent.ts +80 -0
  72. package/src/pages/oauth/par.ts +121 -0
  73. package/src/pages/oauth/token.ts +158 -0
  74. package/src/pages/xrpc/[...nsid].ts +61 -0
  75. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +12 -13
  76. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +23 -23
  77. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +9 -2
  78. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +9 -2
  79. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +9 -2
  80. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +43 -41
  81. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +10 -3
  82. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +40 -9
  83. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +41 -29
  84. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +20 -6
  85. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +1 -1
  86. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +101 -11
  87. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +44 -14
  88. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +41 -13
  89. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +2 -2
  90. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +14 -1
  91. package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +14 -6
  92. package/src/pages/xrpc/com.atproto.repo.listRecords.ts +1 -1
  93. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +42 -14
  94. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +76 -15
  95. package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +20 -8
  96. package/src/pages/xrpc/com.atproto.server.createSession.ts +31 -11
  97. package/src/pages/xrpc/com.atproto.server.describeServer.ts +1 -1
  98. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +12 -5
  99. package/src/pages/xrpc/com.atproto.server.getSession.ts +22 -8
  100. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +30 -72
  101. package/src/pages/xrpc/com.atproto.sync.getBlob.ts +71 -22
  102. package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +1 -1
  103. package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +1 -1
  104. package/src/pages/xrpc/com.atproto.sync.getHead.ts +7 -2
  105. package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +1 -1
  106. package/src/pages/xrpc/com.atproto.sync.getRecord.ts +5 -27
  107. package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +1 -1
  108. package/src/pages/xrpc/com.atproto.sync.getRepo.ts +50 -5
  109. package/src/pages/xrpc/com.atproto.sync.getRepoStatus.ts +58 -0
  110. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +1 -1
  111. package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
  112. package/src/services/car.ts +207 -55
  113. package/src/services/r2-blob-store.ts +1 -1
  114. package/src/services/repo/blockstore-ops.ts +29 -0
  115. package/src/services/repo/operations.ts +133 -0
  116. package/src/services/repo-manager.ts +202 -253
  117. package/src/worker/runtime.ts +53 -8
  118. package/src/worker/sequencer/broadcast.ts +91 -0
  119. package/src/worker/sequencer/cid-helpers.ts +39 -0
  120. package/src/worker/sequencer/payload.ts +84 -0
  121. package/src/worker/sequencer/types.ts +36 -0
  122. package/src/worker/sequencer/upgrade.ts +141 -0
  123. package/src/worker/sequencer.ts +288 -412
  124. package/types/env.d.ts +15 -3
  125. package/src/pages/xrpc/app.bsky.actor.getProfile.ts +0 -49
  126. package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +0 -51
  127. package/src/pages/xrpc/app.bsky.feed.getActorFeeds.ts +0 -25
  128. package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +0 -42
  129. package/src/pages/xrpc/app.bsky.feed.getFeedGenerators.ts +0 -25
  130. package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +0 -37
  131. package/src/pages/xrpc/app.bsky.feed.getPosts.ts +0 -26
  132. package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +0 -47
  133. package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +0 -29
  134. package/src/pages/xrpc/app.bsky.graph.getFollows.ts +0 -29
  135. package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +0 -20
  136. package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +0 -27
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,23 +10,23 @@ 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) {
@@ -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
  };
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
  */
package/src/lib/feed.ts CHANGED
@@ -30,18 +30,17 @@ function parseRow(row: PostRow): ParsedPost | null {
30
30
  const record = JSON.parse(row.json) ?? {};
31
31
  if (record && typeof record === 'object' && !Array.isArray(record)) {
32
32
  const collection = inferCollectionFromUri(row.uri);
33
- if (collection && typeof (record as any).$type !== 'string') {
34
- (record as any).$type = collection;
33
+ const writable = record as Record<string, unknown>;
34
+ if (collection && typeof writable.$type !== 'string') {
35
+ writable.$type = collection;
35
36
  }
36
- if (typeof (record as any).createdAt !== 'string') {
37
- (record as any).createdAt = new Date().toISOString();
37
+ if (typeof writable.createdAt !== 'string') {
38
+ writable.createdAt = new Date().toISOString();
38
39
  }
39
40
  }
40
41
 
41
- const createdAt =
42
- record && typeof record === 'object' && typeof (record as any).createdAt === 'string'
43
- ? (record as any).createdAt
44
- : new Date().toISOString();
42
+ const createdAtField = (record as Record<string, unknown>).createdAt;
43
+ const createdAt = typeof createdAtField === 'string' ? createdAtField : new Date().toISOString();
45
44
 
46
45
  return {
47
46
  uri: row.uri,
@@ -72,27 +71,27 @@ export async function listPosts(env: Env, limit: number, cursor?: string): Promi
72
71
  }
73
72
  params.push(safeLimit);
74
73
 
75
- const res = await env.DB.prepare(
74
+ const response = await env.DB.prepare(
76
75
  `SELECT rowid, uri, cid, json FROM record WHERE ${where} ORDER BY rowid DESC LIMIT ?`
77
76
  )
78
77
  .bind(...params)
79
78
  .all<PostRow>();
80
79
 
81
- if (!res?.results) return [];
82
- return res.results.map(parseRow).filter((row): row is ParsedPost => row !== null);
80
+ if (!response?.results) return [];
81
+ return response.results.map(parseRow).filter((row): row is ParsedPost => row !== null);
83
82
  }
84
83
 
85
84
  export async function getPostsByUris(env: Env, uris: string[]): Promise<ParsedPost[]> {
86
85
  if (!uris.length) return [];
87
86
  const placeholders = uris.map(() => '?').join(',');
88
- const res = await env.DB.prepare(
87
+ const response = await env.DB.prepare(
89
88
  `SELECT rowid, uri, cid, json FROM record WHERE uri IN (${placeholders})`
90
89
  )
91
90
  .bind(...uris)
92
91
  .all<PostRow>();
93
92
 
94
- if (!res?.results) return [];
95
- return res.results.map(parseRow).filter((row): row is ParsedPost => row !== null);
93
+ if (!response?.results) return [];
94
+ return response.results.map(parseRow).filter((row): row is ParsedPost => row !== null);
96
95
  }
97
96
 
98
97
  export async function buildFeedViewPosts(env: Env, posts: ParsedPost[]) {
@@ -143,23 +142,23 @@ export async function countPosts(env: Env): Promise<number> {
143
142
  const actor = await getPrimaryActor(env);
144
143
  const prefix = `at://${actor.did}/${POST_COLLECTION}/`;
145
144
  const upperBound = `${prefix}{`; // '{' sorts after 'z', safely bounding rkeys
146
- const res = await env.DB.prepare(
145
+ const response = await env.DB.prepare(
147
146
  'SELECT COUNT(*) as count FROM record WHERE uri >= ? AND uri < ?'
148
147
  )
149
148
  .bind(prefix, upperBound)
150
149
  .first<{ count: number }>();
151
- return res?.count ?? 0;
150
+ return response?.count ?? 0;
152
151
  }
153
152
 
154
153
  export async function getPostByUri(env: Env, uri: string): Promise<ParsedPost | null> {
155
- const res = await env.DB.prepare(
154
+ const response = await env.DB.prepare(
156
155
  'SELECT rowid, uri, cid, json FROM record WHERE uri = ? LIMIT 1'
157
156
  )
158
157
  .bind(uri)
159
158
  .first<PostRow>();
160
159
 
161
- if (!res) return null;
162
- return parseRow(res);
160
+ if (!response) return null;
161
+ return parseRow(response);
163
162
  }
164
163
 
165
164
  export type { ParsedPost };