@alteran/astro 0.3.9 → 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.
- package/LICENSE +21 -0
- package/README.md +19 -30
- package/index.js +34 -28
- package/migrations/0007_bored_spitfire.sql +26 -0
- package/migrations/0008_furry_ozymandias.sql +2 -0
- package/migrations/meta/0007_snapshot.json +534 -0
- package/migrations/meta/0008_snapshot.json +548 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +10 -9
- package/src/app.ts +8 -4
- package/src/db/account.ts +25 -6
- package/src/db/dal.ts +34 -23
- package/src/db/repo.ts +35 -35
- package/src/db/schema.ts +5 -1
- package/src/db/seed.ts +5 -13
- package/src/entrypoints/server.ts +2 -22
- package/src/handlers/root.ts +4 -4
- package/src/lib/account-state.ts +156 -0
- package/src/lib/actor.ts +28 -12
- package/src/lib/appview/auth-policy.ts +66 -0
- package/src/lib/appview/did-resolver.ts +233 -0
- package/src/lib/appview/proxy.ts +221 -0
- package/src/lib/appview/service-config.ts +61 -0
- package/src/lib/appview/service-jwt.ts +93 -0
- package/src/lib/appview/types.ts +25 -0
- package/src/lib/appview.ts +5 -532
- package/src/lib/auth-errors.ts +24 -0
- package/src/lib/auth.ts +63 -15
- package/src/lib/blockstore-gc.ts +2 -1
- package/src/lib/cache.ts +30 -4
- package/src/lib/chat.ts +14 -8
- package/src/lib/commit.ts +26 -36
- package/src/lib/config.ts +26 -15
- package/src/lib/did-document.ts +32 -0
- package/src/lib/errors.ts +54 -0
- package/src/lib/feed.ts +18 -19
- package/src/lib/firehose/frames.ts +87 -47
- package/src/lib/firehose/validation.ts +3 -3
- package/src/lib/jwt.ts +85 -177
- package/src/lib/labeler.ts +43 -30
- package/src/lib/logger.ts +4 -0
- package/src/lib/mst/block-map.ts +172 -0
- package/src/lib/mst/blockstore.ts +56 -93
- package/src/lib/mst/index.ts +1 -0
- package/src/lib/mst/leaf.ts +25 -0
- package/src/lib/mst/mst.ts +81 -237
- package/src/lib/mst/serialize.ts +97 -0
- package/src/lib/mst/types.ts +21 -0
- package/src/lib/oauth/clients.ts +67 -0
- package/src/lib/oauth/dpop-errors.ts +15 -0
- package/src/lib/oauth/dpop.ts +150 -0
- package/src/lib/oauth/resource.ts +199 -0
- package/src/lib/oauth/store.ts +77 -0
- package/src/lib/preferences.ts +9 -34
- package/src/lib/refresh-session.ts +161 -0
- package/src/lib/relay.ts +10 -8
- package/src/lib/secrets.ts +6 -7
- package/src/lib/sequencer.ts +12 -3
- package/src/lib/service-auth.ts +184 -0
- package/src/lib/session-tokens.ts +28 -76
- package/src/lib/streaming-car.ts +3 -0
- package/src/lib/tracing.ts +4 -3
- package/src/lib/util.ts +65 -15
- package/src/middleware.ts +1 -1
- package/src/pages/.well-known/did.json.ts +27 -30
- package/src/pages/.well-known/oauth-authorization-server.ts +31 -0
- package/src/pages/.well-known/oauth-protected-resource.ts +22 -0
- package/src/pages/debug/record.ts +1 -1
- package/src/pages/debug/sequencer.ts +28 -0
- package/src/pages/oauth/authorize.ts +78 -0
- package/src/pages/oauth/consent.ts +80 -0
- package/src/pages/oauth/par.ts +121 -0
- package/src/pages/oauth/token.ts +158 -0
- package/src/pages/xrpc/[...nsid].ts +61 -0
- package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +12 -13
- package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +23 -23
- package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +9 -2
- package/src/pages/xrpc/chat.bsky.convo.getLog.ts +9 -2
- package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +9 -2
- package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +43 -41
- package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +10 -3
- package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +40 -9
- package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +41 -29
- package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +20 -6
- package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +1 -1
- package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +101 -11
- package/src/pages/xrpc/com.atproto.repo.createRecord.ts +44 -14
- package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +41 -13
- package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +2 -2
- package/src/pages/xrpc/com.atproto.repo.getRecord.ts +14 -1
- package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +14 -6
- package/src/pages/xrpc/com.atproto.repo.listRecords.ts +1 -1
- package/src/pages/xrpc/com.atproto.repo.putRecord.ts +42 -14
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +76 -15
- package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +20 -8
- package/src/pages/xrpc/com.atproto.server.createSession.ts +31 -11
- package/src/pages/xrpc/com.atproto.server.describeServer.ts +1 -1
- package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +12 -5
- package/src/pages/xrpc/com.atproto.server.getSession.ts +22 -8
- package/src/pages/xrpc/com.atproto.server.refreshSession.ts +30 -72
- package/src/pages/xrpc/com.atproto.sync.getBlob.ts +71 -22
- package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getHead.ts +7 -2
- package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getRecord.ts +5 -27
- package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getRepo.ts +50 -5
- package/src/pages/xrpc/com.atproto.sync.getRepoStatus.ts +58 -0
- package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
- package/src/services/car.ts +207 -55
- package/src/services/r2-blob-store.ts +1 -1
- package/src/services/repo/blockstore-ops.ts +29 -0
- package/src/services/repo/operations.ts +133 -0
- package/src/services/repo-manager.ts +202 -253
- package/src/worker/runtime.ts +53 -8
- package/src/worker/sequencer/broadcast.ts +91 -0
- package/src/worker/sequencer/cid-helpers.ts +39 -0
- package/src/worker/sequencer/payload.ts +84 -0
- package/src/worker/sequencer/types.ts +36 -0
- package/src/worker/sequencer/upgrade.ts +141 -0
- package/src/worker/sequencer.ts +263 -405
- package/types/env.d.ts +15 -3
- package/src/pages/xrpc/app.bsky.actor.getProfile.ts +0 -49
- package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +0 -51
- package/src/pages/xrpc/app.bsky.feed.getActorFeeds.ts +0 -25
- package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +0 -42
- package/src/pages/xrpc/app.bsky.feed.getFeedGenerators.ts +0 -25
- package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +0 -37
- package/src/pages/xrpc/app.bsky.feed.getPosts.ts +0 -26
- package/src/pages/xrpc/app.bsky.feed.getSuggestedFeeds.ts +0 -23
- package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +0 -47
- package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +0 -29
- package/src/pages/xrpc/app.bsky.graph.getFollows.ts +0 -29
- package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +0 -20
- package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +0 -27
- package/src/pages/xrpc/app.bsky.unspecced.getSuggestedFeeds.ts +0 -23
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 = (
|
|
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 = (
|
|
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 = (
|
|
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
|
-
|
|
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:
|
|
13
|
+
members: unknown[];
|
|
14
14
|
muted: boolean;
|
|
15
15
|
unreadCount: number;
|
|
16
16
|
status?: string;
|
|
17
|
-
lastMessage?:
|
|
18
|
-
lastReaction?:
|
|
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:
|
|
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:
|
|
29
|
-
reaction:
|
|
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
|
-
|
|
134
|
-
|
|
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:
|
|
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
|
|
51
|
+
* Sign a commit with secp256k1 private key
|
|
50
52
|
*/
|
|
51
53
|
export async function signCommit(
|
|
52
54
|
commit: CommitData,
|
|
53
|
-
|
|
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
|
-
//
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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:
|
|
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
|
-
|
|
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://
|
|
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
|
-
|
|
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
|
|
190
|
-
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:
|
|
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
|
-
|
|
34
|
-
|
|
33
|
+
const writable = record as Record<string, unknown>;
|
|
34
|
+
if (collection && typeof writable.$type !== 'string') {
|
|
35
|
+
writable.$type = collection;
|
|
35
36
|
}
|
|
36
|
-
if (typeof
|
|
37
|
-
|
|
37
|
+
if (typeof writable.createdAt !== 'string') {
|
|
38
|
+
writable.createdAt = new Date().toISOString();
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
const
|
|
42
|
-
|
|
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
|
|
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 (!
|
|
82
|
-
return
|
|
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
|
|
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 (!
|
|
95
|
-
return
|
|
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
|
|
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
|
|
150
|
+
return response?.count ?? 0;
|
|
152
151
|
}
|
|
153
152
|
|
|
154
153
|
export async function getPostByUri(env: Env, uri: string): Promise<ParsedPost | null> {
|
|
155
|
-
const
|
|
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 (!
|
|
162
|
-
return parseRow(
|
|
160
|
+
if (!response) return null;
|
|
161
|
+
return parseRow(response);
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
export type { ParsedPost };
|