@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.
- 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/client.ts +1 -1
- package/src/db/dal.ts +34 -23
- package/src/db/repo.ts +38 -38
- package/src/db/schema.ts +5 -1
- package/src/db/seed.ts +5 -13
- package/src/entrypoints/server.ts +2 -22
- package/src/handlers/debug.ts +1 -1
- package/src/handlers/ready.ts +1 -1
- package/src/handlers/root.ts +4 -4
- package/src/handlers/xrpc.server.refreshSession.ts +6 -6
- package/src/lib/account-state.ts +156 -0
- package/src/lib/actor.ts +29 -13
- 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 +6 -5
- package/src/lib/cache.ts +30 -4
- package/src/lib/chat.ts +20 -14
- package/src/lib/commit-log-pruning.ts +2 -2
- 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 +12 -37
- package/src/lib/ratelimit.ts +4 -4
- 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 +14 -5
- 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/blob/[...key].ts +2 -2
- package/src/pages/debug/db/bootstrap.ts +1 -1
- package/src/pages/debug/db/commits.ts +1 -1
- package/src/pages/debug/gc/blobs.ts +1 -1
- package/src/pages/debug/record.ts +1 -1
- package/src/pages/debug/sequencer.ts +28 -0
- package/src/pages/health.ts +4 -4
- 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/ready.ts +2 -2
- 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 +32 -12
- 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 +72 -23
- 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 +2 -2
- package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
- package/src/services/car.ts +209 -57
- package/src/services/r2-blob-store.ts +4 -4
- package/src/services/repo/blockstore-ops.ts +29 -0
- package/src/services/repo/operations.ts +133 -0
- package/src/services/repo-manager.ts +203 -254
- package/src/worker/runtime.ts +56 -11
- 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 +264 -406
- package/types/env.d.ts +18 -6
- 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/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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
})
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
33
|
-
if (!
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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';
|
package/src/lib/blockstore-gc.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 = (
|
|
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,30 +10,30 @@ 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) {
|
|
33
33
|
if (tablesEnsured) return;
|
|
34
34
|
|
|
35
35
|
// Create chat_convo table
|
|
36
|
-
await env.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
};
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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:
|
|
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
|
*/
|