@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.
- 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 +288 -412
- 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.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/lib/appview.ts
CHANGED
|
@@ -1,532 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const DEFAULT_APPVIEW_URL = 'https://public.api.bsky.app';
|
|
7
|
-
const DEFAULT_APPVIEW_DID = 'did:web:api.bsky.app';
|
|
8
|
-
|
|
9
|
-
export interface AppViewConfig {
|
|
10
|
-
url: string;
|
|
11
|
-
did: string;
|
|
12
|
-
cdnUrlPattern?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let cachedSigningKey: Promise<Secp256k1Keypair> | null = null;
|
|
16
|
-
|
|
17
|
-
const didDocumentCache = new Map<string, Promise<unknown>>();
|
|
18
|
-
|
|
19
|
-
function encodeBase64Url(bytes: Uint8Array): string {
|
|
20
|
-
let binary = '';
|
|
21
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
22
|
-
binary += String.fromCharCode(bytes[i]);
|
|
23
|
-
}
|
|
24
|
-
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function encodeJson(obj: Record<string, unknown>): string {
|
|
28
|
-
const encoder = new TextEncoder();
|
|
29
|
-
return encodeBase64Url(encoder.encode(JSON.stringify(obj)));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function randomHex(bytes = 16): string {
|
|
33
|
-
const arr = new Uint8Array(bytes);
|
|
34
|
-
crypto.getRandomValues(arr);
|
|
35
|
-
return Array.from(arr, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface ProxyTarget {
|
|
39
|
-
did: string;
|
|
40
|
-
url: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
type AuthScope =
|
|
44
|
-
| 'com.atproto.access'
|
|
45
|
-
| 'com.atproto.appPass'
|
|
46
|
-
| 'com.atproto.appPassPrivileged'
|
|
47
|
-
| 'com.atproto.signupQueued'
|
|
48
|
-
| 'com.atproto.takendown';
|
|
49
|
-
|
|
50
|
-
const DEFAULT_ACCESS_SCOPE: AuthScope = 'com.atproto.access';
|
|
51
|
-
const TAKENDOWN_SCOPE: AuthScope = 'com.atproto.takendown';
|
|
52
|
-
const PRIVILEGED_SCOPES = new Set<AuthScope>([
|
|
53
|
-
'com.atproto.access',
|
|
54
|
-
'com.atproto.appPassPrivileged',
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
|
-
const PRIVILEGED_METHODS = new Set<string>([
|
|
58
|
-
'chat.bsky.actor.deleteAccount',
|
|
59
|
-
'chat.bsky.actor.exportAccountData',
|
|
60
|
-
'chat.bsky.convo.deleteMessageForSelf',
|
|
61
|
-
'chat.bsky.convo.getConvo',
|
|
62
|
-
'chat.bsky.convo.getConvoForMembers',
|
|
63
|
-
'chat.bsky.convo.getLog',
|
|
64
|
-
'chat.bsky.convo.getMessages',
|
|
65
|
-
'chat.bsky.convo.leaveConvo',
|
|
66
|
-
'chat.bsky.convo.listConvos',
|
|
67
|
-
'chat.bsky.convo.muteConvo',
|
|
68
|
-
'chat.bsky.convo.sendMessage',
|
|
69
|
-
'chat.bsky.convo.sendMessageBatch',
|
|
70
|
-
'chat.bsky.convo.unmuteConvo',
|
|
71
|
-
'chat.bsky.convo.updateRead',
|
|
72
|
-
'com.atproto.server.createAccount',
|
|
73
|
-
]);
|
|
74
|
-
|
|
75
|
-
const PROTECTED_METHODS = new Set<string>([
|
|
76
|
-
'com.atproto.admin.sendEmail',
|
|
77
|
-
'com.atproto.identity.requestPlcOperationSignature',
|
|
78
|
-
'com.atproto.identity.signPlcOperation',
|
|
79
|
-
'com.atproto.identity.updateHandle',
|
|
80
|
-
'com.atproto.server.activateAccount',
|
|
81
|
-
'com.atproto.server.confirmEmail',
|
|
82
|
-
'com.atproto.server.createAppPassword',
|
|
83
|
-
'com.atproto.server.deactivateAccount',
|
|
84
|
-
'com.atproto.server.getAccountInviteCodes',
|
|
85
|
-
'com.atproto.server.getSession',
|
|
86
|
-
'com.atproto.server.listAppPasswords',
|
|
87
|
-
'com.atproto.server.requestAccountDelete',
|
|
88
|
-
'com.atproto.server.requestEmailConfirmation',
|
|
89
|
-
'com.atproto.server.requestEmailUpdate',
|
|
90
|
-
'com.atproto.server.revokeAppPassword',
|
|
91
|
-
'com.atproto.server.updateEmail',
|
|
92
|
-
]);
|
|
93
|
-
|
|
94
|
-
class ProxyHeaderError extends Error {}
|
|
95
|
-
|
|
96
|
-
function resolveAuthScope(scope: unknown): AuthScope {
|
|
97
|
-
if (typeof scope !== 'string') {
|
|
98
|
-
return DEFAULT_ACCESS_SCOPE;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
switch (scope) {
|
|
102
|
-
case 'com.atproto.access':
|
|
103
|
-
case 'com.atproto.appPass':
|
|
104
|
-
case 'com.atproto.appPassPrivileged':
|
|
105
|
-
case 'com.atproto.signupQueued':
|
|
106
|
-
case 'com.atproto.takendown':
|
|
107
|
-
return scope;
|
|
108
|
-
default:
|
|
109
|
-
console.warn('Unknown auth scope, treating as access scope', scope);
|
|
110
|
-
return DEFAULT_ACCESS_SCOPE;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function parseProxyHeader(header: string): { did: string; serviceId: string } {
|
|
115
|
-
const value = header.trim();
|
|
116
|
-
const hashIndex = value.indexOf('#');
|
|
117
|
-
|
|
118
|
-
if (hashIndex <= 0 || hashIndex === value.length - 1) {
|
|
119
|
-
throw new ProxyHeaderError('invalid format');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (value.indexOf('#', hashIndex + 1) !== -1) {
|
|
123
|
-
throw new ProxyHeaderError('invalid format');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const did = value.slice(0, hashIndex);
|
|
127
|
-
const serviceId = value.slice(hashIndex);
|
|
128
|
-
|
|
129
|
-
if (!did.startsWith('did:')) {
|
|
130
|
-
throw new ProxyHeaderError('invalid DID');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!serviceId.startsWith('#')) {
|
|
134
|
-
throw new ProxyHeaderError('invalid service id');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (value.includes(' ')) {
|
|
138
|
-
throw new ProxyHeaderError('invalid format');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return { did, serviceId };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function resolveProxyTarget(
|
|
145
|
-
env: Env,
|
|
146
|
-
proxyHeader: string,
|
|
147
|
-
config: AppViewConfig,
|
|
148
|
-
): Promise<ProxyTarget> {
|
|
149
|
-
const { did, serviceId } = parseProxyHeader(proxyHeader);
|
|
150
|
-
|
|
151
|
-
if (did === config.did && serviceId === '#bsky_appview') {
|
|
152
|
-
return { did, url: config.url };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const didDoc = await resolveDidDocument(env, did);
|
|
156
|
-
const endpoint = getServiceEndpointFromDidDoc(didDoc, did, serviceId);
|
|
157
|
-
|
|
158
|
-
if (!endpoint) {
|
|
159
|
-
throw new ProxyHeaderError('service id not found in DID document');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return { did, url: endpoint };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function resolveDidDocument(env: Env, did: string): Promise<any> {
|
|
166
|
-
const existing = didDocumentCache.get(did);
|
|
167
|
-
if (existing) {
|
|
168
|
-
return existing;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const loader = fetchDidDocument(env, did).catch((error) => {
|
|
172
|
-
didDocumentCache.delete(did);
|
|
173
|
-
throw error;
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
didDocumentCache.set(did, loader);
|
|
177
|
-
return loader;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async function fetchDidDocument(_env: Env, did: string): Promise<any> {
|
|
181
|
-
let url: string;
|
|
182
|
-
if (did.startsWith('did:web:')) {
|
|
183
|
-
url = buildDidWebUrl(did);
|
|
184
|
-
} else if (did.startsWith('did:plc:')) {
|
|
185
|
-
url = `https://plc.directory/${did}`;
|
|
186
|
-
} else {
|
|
187
|
-
throw new ProxyHeaderError('unsupported DID method');
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const res = await fetch(url, {
|
|
191
|
-
headers: {
|
|
192
|
-
accept: 'application/did+json, application/json;q=0.9',
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
if (!res.ok) {
|
|
197
|
-
throw new ProxyHeaderError('failed to resolve DID document');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return res.json();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function buildDidWebUrl(did: string): string {
|
|
204
|
-
const suffix = did.slice('did:web:'.length);
|
|
205
|
-
const parts = suffix.split(':').map((segment) => {
|
|
206
|
-
try {
|
|
207
|
-
return decodeURIComponent(segment);
|
|
208
|
-
} catch {
|
|
209
|
-
throw new ProxyHeaderError('invalid did:web encoding');
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
const host = parts.shift();
|
|
214
|
-
if (!host) throw new ProxyHeaderError('invalid did:web value');
|
|
215
|
-
|
|
216
|
-
if (parts.length === 0) {
|
|
217
|
-
return `https://${host}/.well-known/did.json`;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const path = parts.join('/');
|
|
221
|
-
return `https://${host}/${path}/did.json`;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function getServiceEndpointFromDidDoc(didDoc: any, did: string, serviceId: string): string | null {
|
|
225
|
-
if (!didDoc || typeof didDoc !== 'object') return null;
|
|
226
|
-
const services = Array.isArray((didDoc as any).service) ? (didDoc as any).service : [];
|
|
227
|
-
if (!services.length) return null;
|
|
228
|
-
|
|
229
|
-
const targets = new Set<string>([serviceId]);
|
|
230
|
-
const docId = typeof (didDoc as any).id === 'string' ? (didDoc as any).id : undefined;
|
|
231
|
-
if (docId && !serviceId.startsWith(docId)) {
|
|
232
|
-
targets.add(`${docId}${serviceId}`);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
for (const service of services) {
|
|
236
|
-
if (!service || typeof service !== 'object') continue;
|
|
237
|
-
const id = typeof service.id === 'string' ? service.id : undefined;
|
|
238
|
-
if (!id || !targets.has(id)) continue;
|
|
239
|
-
|
|
240
|
-
const endpoint = extractServiceEndpoint(service);
|
|
241
|
-
if (endpoint) return endpoint;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function extractServiceEndpoint(service: any): string | null {
|
|
248
|
-
const endpoint = service?.serviceEndpoint;
|
|
249
|
-
if (typeof endpoint === 'string') return endpoint;
|
|
250
|
-
if (endpoint && typeof endpoint === 'object') {
|
|
251
|
-
if (typeof endpoint.uri === 'string') return endpoint.uri;
|
|
252
|
-
if (Array.isArray(endpoint.urls)) {
|
|
253
|
-
const first = endpoint.urls.find((value: unknown) => typeof value === 'string');
|
|
254
|
-
if (typeof first === 'string') return first;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function getServiceSigningKey(env: Env): Promise<Secp256k1Keypair> {
|
|
261
|
-
if (!cachedSigningKey) {
|
|
262
|
-
cachedSigningKey = (async () => {
|
|
263
|
-
const configured =
|
|
264
|
-
(await resolveSecret(env.PDS_SERVICE_SIGNING_KEY_HEX as any)) ??
|
|
265
|
-
(await resolveSecret(env.PDS_PLC_ROTATION_KEY as any));
|
|
266
|
-
|
|
267
|
-
if (!configured || configured.trim() === '') {
|
|
268
|
-
throw new Error('Service signing key is not configured');
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return Secp256k1Keypair.import(configured.trim());
|
|
272
|
-
})();
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return cachedSigningKey;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function getAppViewConfig(env: Env): AppViewConfig | null {
|
|
279
|
-
const url = (typeof env.PDS_BSKY_APP_VIEW_URL === 'string' && env.PDS_BSKY_APP_VIEW_URL.trim() !== '')
|
|
280
|
-
? env.PDS_BSKY_APP_VIEW_URL.trim()
|
|
281
|
-
: DEFAULT_APPVIEW_URL;
|
|
282
|
-
const did = (typeof env.PDS_BSKY_APP_VIEW_DID === 'string' && env.PDS_BSKY_APP_VIEW_DID.trim() !== '')
|
|
283
|
-
? env.PDS_BSKY_APP_VIEW_DID.trim()
|
|
284
|
-
: DEFAULT_APPVIEW_DID;
|
|
285
|
-
|
|
286
|
-
if (!url || !did) return null;
|
|
287
|
-
|
|
288
|
-
const cdn = typeof env.PDS_BSKY_APP_VIEW_CDN_URL_PATTERN === 'string'
|
|
289
|
-
? env.PDS_BSKY_APP_VIEW_CDN_URL_PATTERN.trim()
|
|
290
|
-
: undefined;
|
|
291
|
-
|
|
292
|
-
return { url, did, cdnUrlPattern: cdn || undefined };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async function createServiceJwt(
|
|
296
|
-
env: Env,
|
|
297
|
-
issuerDid: string,
|
|
298
|
-
audienceDid: string,
|
|
299
|
-
lexiconMethod: string | null,
|
|
300
|
-
expiresInSeconds = 60,
|
|
301
|
-
): Promise<string> {
|
|
302
|
-
const keypair = await getServiceSigningKey(env);
|
|
303
|
-
const now = Math.floor(Date.now() / 1000);
|
|
304
|
-
const exp = now + Math.max(1, expiresInSeconds);
|
|
305
|
-
const header = {
|
|
306
|
-
typ: 'JWT',
|
|
307
|
-
alg: keypair.jwtAlg,
|
|
308
|
-
};
|
|
309
|
-
const payload: Record<string, unknown> = {
|
|
310
|
-
iss: issuerDid,
|
|
311
|
-
aud: audienceDid,
|
|
312
|
-
iat: now,
|
|
313
|
-
exp,
|
|
314
|
-
jti: randomHex(),
|
|
315
|
-
};
|
|
316
|
-
if (lexiconMethod) {
|
|
317
|
-
payload.lxm = lexiconMethod;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const encodedHeader = encodeJson(header);
|
|
321
|
-
const encodedPayload = encodeJson(payload);
|
|
322
|
-
const toSign = `${encodedHeader}.${encodedPayload}`;
|
|
323
|
-
const signature = await keypair.sign(new TextEncoder().encode(toSign));
|
|
324
|
-
const encodedSignature = encodeBase64Url(signature);
|
|
325
|
-
return `${toSign}.${encodedSignature}`;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const FORWARDED_HEADERS = [
|
|
329
|
-
'accept',
|
|
330
|
-
'accept-encoding',
|
|
331
|
-
'accept-language',
|
|
332
|
-
'atproto-accept-labelers',
|
|
333
|
-
'atproto-accept-personalized-feed',
|
|
334
|
-
'cache-control',
|
|
335
|
-
'if-none-match',
|
|
336
|
-
'if-modified-since',
|
|
337
|
-
'pragma',
|
|
338
|
-
'x-bsky-topics',
|
|
339
|
-
'x-bsky-feeds',
|
|
340
|
-
'x-bsky-latest',
|
|
341
|
-
'x-bsky-appview-features',
|
|
342
|
-
'user-agent',
|
|
343
|
-
];
|
|
344
|
-
|
|
345
|
-
export interface ProxyAppViewOptions {
|
|
346
|
-
request: Request;
|
|
347
|
-
env: Env;
|
|
348
|
-
lxm: string;
|
|
349
|
-
fallback?: () => Promise<Response>;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export async function proxyAppView({ request, env, lxm, fallback }: ProxyAppViewOptions): Promise<Response> {
|
|
353
|
-
console.log('proxyAppView called:', { lxm, url: request.url });
|
|
354
|
-
|
|
355
|
-
const config = getAppViewConfig(env);
|
|
356
|
-
if (!config) {
|
|
357
|
-
console.log('proxyAppView: No appview config, using fallback');
|
|
358
|
-
return fallback ? await fallback() : new Response('AppView not configured', { status: 501 });
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
console.log('proxyAppView: AppView config found:', { url: config.url, did: config.did });
|
|
362
|
-
|
|
363
|
-
const auth = await authenticateRequest(request, env);
|
|
364
|
-
if (!auth) {
|
|
365
|
-
console.log('proxyAppView: Authentication failed');
|
|
366
|
-
return unauthorized();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (!auth.claims.sub) {
|
|
370
|
-
console.log('proxyAppView: No subject in auth claims');
|
|
371
|
-
return new Response(JSON.stringify({ error: 'InvalidToken' }), {
|
|
372
|
-
status: 401,
|
|
373
|
-
headers: { 'Content-Type': 'application/json' },
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
console.log('proxyAppView: Authenticated as', auth.claims.sub);
|
|
378
|
-
|
|
379
|
-
if (PROTECTED_METHODS.has(lxm)) {
|
|
380
|
-
console.log('proxyAppView: Method is protected, cannot proxy');
|
|
381
|
-
return new Response(
|
|
382
|
-
JSON.stringify({ error: 'InvalidToken', message: 'method cannot be proxied' }),
|
|
383
|
-
{
|
|
384
|
-
status: 400,
|
|
385
|
-
headers: { 'Content-Type': 'application/json' },
|
|
386
|
-
},
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const scope = resolveAuthScope(auth.claims.scope);
|
|
391
|
-
if (scope === TAKENDOWN_SCOPE) {
|
|
392
|
-
console.log('proxyAppView: Account is takendown');
|
|
393
|
-
return new Response(JSON.stringify({ error: 'AccountTakendown' }), {
|
|
394
|
-
status: 403,
|
|
395
|
-
headers: { 'Content-Type': 'application/json' },
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (!PRIVILEGED_SCOPES.has(scope) && PRIVILEGED_METHODS.has(lxm)) {
|
|
400
|
-
console.log('proxyAppView: Insufficient privileges for method');
|
|
401
|
-
return new Response(JSON.stringify({ error: 'InvalidToken' }), {
|
|
402
|
-
status: 401,
|
|
403
|
-
headers: { 'Content-Type': 'application/json' },
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
let target: ProxyTarget = { did: config.did, url: config.url };
|
|
408
|
-
const proxyHeader = request.headers.get('atproto-proxy');
|
|
409
|
-
if (proxyHeader) {
|
|
410
|
-
console.log('proxyAppView: Resolving proxy header:', proxyHeader);
|
|
411
|
-
try {
|
|
412
|
-
target = await resolveProxyTarget(env, proxyHeader, config);
|
|
413
|
-
} catch (error) {
|
|
414
|
-
console.error('AppView proxy header error:', error);
|
|
415
|
-
const isHeaderError = error instanceof ProxyHeaderError;
|
|
416
|
-
return new Response(
|
|
417
|
-
JSON.stringify({ error: isHeaderError ? 'InvalidProxyHeader' : 'ProxyResolutionFailed' }),
|
|
418
|
-
{
|
|
419
|
-
status: isHeaderError ? 400 : 502,
|
|
420
|
-
headers: { 'Content-Type': 'application/json' },
|
|
421
|
-
},
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const originalUrl = new URL(request.url);
|
|
427
|
-
const upstreamUrl = new URL(target.url);
|
|
428
|
-
upstreamUrl.pathname = originalUrl.pathname;
|
|
429
|
-
upstreamUrl.search = originalUrl.search;
|
|
430
|
-
upstreamUrl.hash = '';
|
|
431
|
-
|
|
432
|
-
console.log('proxyAppView: Proxying to', upstreamUrl.toString());
|
|
433
|
-
|
|
434
|
-
const headers = new Headers();
|
|
435
|
-
for (const header of FORWARDED_HEADERS) {
|
|
436
|
-
const value = request.headers.get(header);
|
|
437
|
-
if (value) headers.set(header, value);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
let serviceJwt: string;
|
|
441
|
-
try {
|
|
442
|
-
console.log('proxyAppView: Creating service JWT for', { iss: auth.claims.sub, aud: target.did, lxm });
|
|
443
|
-
serviceJwt = await createServiceJwt(env, auth.claims.sub, target.did, lxm);
|
|
444
|
-
console.log('proxyAppView: Service JWT created successfully');
|
|
445
|
-
} catch (error) {
|
|
446
|
-
console.error('AppView service token error:', error);
|
|
447
|
-
if (fallback) {
|
|
448
|
-
console.log('proxyAppView: Using fallback due to JWT error');
|
|
449
|
-
return fallback();
|
|
450
|
-
}
|
|
451
|
-
return new Response(JSON.stringify({ error: 'ServiceAuthUnavailable' }), {
|
|
452
|
-
status: 503,
|
|
453
|
-
headers: { 'Content-Type': 'application/json' },
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
headers.set('authorization', `Bearer ${serviceJwt}`);
|
|
458
|
-
|
|
459
|
-
const method = request.method.toUpperCase();
|
|
460
|
-
if (method !== 'GET' && method !== 'HEAD' && method !== 'POST') {
|
|
461
|
-
console.log('proxyAppView: Method not allowed:', method);
|
|
462
|
-
return new Response(JSON.stringify({ error: 'MethodNotAllowed' }), {
|
|
463
|
-
status: 405,
|
|
464
|
-
headers: {
|
|
465
|
-
'Content-Type': 'application/json',
|
|
466
|
-
Allow: 'GET, HEAD, POST',
|
|
467
|
-
},
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (!headers.has('accept-encoding')) {
|
|
472
|
-
headers.set('accept-encoding', 'identity');
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (method === 'POST') {
|
|
476
|
-
const contentType = request.headers.get('content-type');
|
|
477
|
-
if (contentType) headers.set('content-type', contentType);
|
|
478
|
-
const contentEncoding = request.headers.get('content-encoding');
|
|
479
|
-
if (contentEncoding) headers.set('content-encoding', contentEncoding);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
try {
|
|
483
|
-
const init: RequestInit = {
|
|
484
|
-
method,
|
|
485
|
-
headers,
|
|
486
|
-
};
|
|
487
|
-
|
|
488
|
-
if (method === 'POST') {
|
|
489
|
-
init.body = request.body as any;
|
|
490
|
-
(init as any).duplex = 'half';
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
console.log('proxyAppView: Fetching upstream');
|
|
494
|
-
const upstream = await fetch(upstreamUrl.toString(), init);
|
|
495
|
-
console.log('proxyAppView: Upstream response:', { status: upstream.status, statusText: upstream.statusText });
|
|
496
|
-
|
|
497
|
-
const responseHeaders = new Headers(upstream.headers);
|
|
498
|
-
return new Response(upstream.body, {
|
|
499
|
-
status: upstream.status,
|
|
500
|
-
statusText: upstream.statusText,
|
|
501
|
-
headers: responseHeaders,
|
|
502
|
-
});
|
|
503
|
-
} catch (error) {
|
|
504
|
-
console.error('AppView proxy error:', error);
|
|
505
|
-
if (fallback) {
|
|
506
|
-
console.log('proxyAppView: Using fallback due to upstream error');
|
|
507
|
-
return fallback();
|
|
508
|
-
}
|
|
509
|
-
return new Response(JSON.stringify({ error: 'UpstreamUnavailable' }), {
|
|
510
|
-
status: 502,
|
|
511
|
-
headers: { 'Content-Type': 'application/json' },
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
export async function getAppViewServiceToken(env: Env, did: string, aud?: string, lxm?: string | null, expiresInSeconds = 60) {
|
|
517
|
-
const config = getAppViewConfig(env);
|
|
518
|
-
if (!config) {
|
|
519
|
-
throw new Error('AppView not configured');
|
|
520
|
-
}
|
|
521
|
-
return createServiceJwt(env, did, aud ?? config.did, lxm ?? null, expiresInSeconds);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
export async function createServiceAuthToken(
|
|
525
|
-
env: Env,
|
|
526
|
-
issuerDid: string,
|
|
527
|
-
audienceDid: string,
|
|
528
|
-
lexiconMethod: string | null,
|
|
529
|
-
expiresInSeconds = 60,
|
|
530
|
-
): Promise<string> {
|
|
531
|
-
return createServiceJwt(env, issuerDid, audienceDid, lexiconMethod, expiresInSeconds);
|
|
532
|
-
}
|
|
1
|
+
export type { AppViewConfig, ServiceConfig, ServiceId, ProxyTarget, AuthScope } from './appview/types';
|
|
2
|
+
export { getAppViewConfig } from './appview/service-config';
|
|
3
|
+
export { createServiceAuthToken, getAppViewServiceToken } from './appview/service-jwt';
|
|
4
|
+
export { proxyAppView } from './appview/proxy';
|
|
5
|
+
export type { ProxyAppViewOptions } from './appview/proxy';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control-flow marker thrown by the auth pipeline when an access token has
|
|
3
|
+
* expired but is otherwise valid. Callers translate this into the
|
|
4
|
+
* `ExpiredToken` XRPC response so clients know to refresh rather than
|
|
5
|
+
* re-authenticate.
|
|
6
|
+
*/
|
|
7
|
+
export class AuthTokenExpiredError extends Error {
|
|
8
|
+
readonly code = 'ExpiredToken';
|
|
9
|
+
|
|
10
|
+
constructor(message: string = 'Token has expired') {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'AuthTokenExpiredError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function expiredToken(message: string = 'Token has expired'): Response {
|
|
17
|
+
return new Response(
|
|
18
|
+
JSON.stringify({ error: 'ExpiredToken', message }),
|
|
19
|
+
{
|
|
20
|
+
status: 400,
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
}
|
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
|
@@ -65,7 +65,8 @@ async function traverseMst(env: Env, rootCid: string, referenced: 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);
|