@alteran/astro 0.7.6 → 0.8.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/README.md +25 -25
- package/migrations/0010_eminent_klaw.sql +37 -0
- package/migrations/0011_chief_darwin.sql +31 -0
- package/migrations/0012_backfill_blob_usage.sql +39 -0
- package/migrations/meta/0010_snapshot.json +790 -0
- package/migrations/meta/0011_snapshot.json +813 -0
- package/migrations/meta/_journal.json +22 -1
- package/package.json +24 -41
- package/src/db/blob.ts +323 -0
- package/src/db/dal.ts +224 -78
- package/src/db/repo.ts +205 -25
- package/src/db/schema.ts +14 -5
- package/src/handlers/debug.ts +4 -3
- package/src/lib/appview/auth-policy.ts +7 -24
- package/src/lib/appview/proxy.ts +56 -23
- package/src/lib/appview/types.ts +1 -6
- package/src/lib/auth-scope.ts +399 -0
- package/src/lib/auth.ts +40 -39
- package/src/lib/commit.ts +37 -15
- package/src/lib/did-document.ts +4 -5
- package/src/lib/jwt.ts +3 -1
- package/src/lib/mime.ts +9 -0
- package/src/lib/oauth/observability.ts +53 -12
- package/src/lib/oauth/resource.ts +49 -0
- package/src/lib/preference-policy.ts +45 -0
- package/src/lib/preferences.ts +0 -4
- package/src/lib/public-host.ts +127 -0
- package/src/lib/ratelimit.ts +37 -12
- package/src/lib/relay.ts +7 -27
- package/src/lib/repo-write-blob-constraints.ts +141 -0
- package/src/lib/repo-write-data.ts +195 -0
- package/src/lib/repo-write-error.ts +46 -0
- package/src/lib/repo-write-validation.ts +463 -0
- package/src/lib/session-tokens.ts +22 -5
- package/src/lib/unsupported-routes.ts +32 -0
- package/src/lib/util.ts +57 -2
- package/src/pages/.well-known/atproto-did.ts +15 -3
- package/src/pages/.well-known/did.json.ts +13 -7
- package/src/pages/debug/db/bootstrap.ts +4 -3
- package/src/pages/debug/gc/blobs.ts +11 -8
- package/src/pages/debug/record.ts +11 -0
- package/src/pages/oauth/token.ts +78 -33
- package/src/pages/xrpc/[...nsid].ts +17 -9
- package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +9 -3
- package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +17 -4
- package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +4 -2
- package/src/pages/xrpc/chat.bsky.convo.getLog.ts +4 -2
- package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +4 -2
- package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +10 -6
- package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +4 -3
- package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +13 -5
- package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +4 -2
- package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +4 -2
- package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +12 -36
- package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +90 -139
- package/src/pages/xrpc/com.atproto.repo.createRecord.ts +74 -47
- package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +119 -46
- package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +21 -20
- package/src/pages/xrpc/com.atproto.repo.getRecord.ts +6 -1
- package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +4 -2
- package/src/pages/xrpc/com.atproto.repo.putRecord.ts +84 -47
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +199 -78
- package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +4 -2
- package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +88 -21
- package/src/pages/xrpc/com.atproto.server.getSession.ts +3 -13
- package/src/pages/xrpc/com.atproto.sync.getBlob.ts +92 -74
- package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +45 -23
- package/src/services/car.ts +13 -0
- package/src/services/repo/apply-prepared-writes.ts +185 -0
- package/src/services/repo/blob-refs.ts +48 -0
- package/src/services/repo/blockstore-ops.ts +59 -17
- package/src/services/repo/list-blobs.ts +43 -0
- package/src/services/repo-manager.ts +221 -78
- package/src/worker/runtime.ts +1 -1
- package/src/worker/sequencer/upgrade.ts +4 -1
|
@@ -2,8 +2,13 @@ import type { APIContext } from 'astro';
|
|
|
2
2
|
import { errorMessage } from '../../lib/errors';
|
|
3
3
|
import { withCache, CACHE_CONFIGS } from '../../lib/cache';
|
|
4
4
|
import { resolveSecret } from '../../lib/secrets';
|
|
5
|
-
import { Secp256k1Keypair } from '@atproto/crypto';
|
|
6
|
-
import {
|
|
5
|
+
import { Secp256k1Keypair, formatMultikey } from '@atproto/crypto';
|
|
6
|
+
import {
|
|
7
|
+
canonicalPdsOrigin,
|
|
8
|
+
configuredDid,
|
|
9
|
+
configuredHandle,
|
|
10
|
+
validAtprotoHandle,
|
|
11
|
+
} from '../../lib/public-host';
|
|
7
12
|
|
|
8
13
|
export const prerender = false;
|
|
9
14
|
|
|
@@ -13,9 +18,10 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
13
18
|
return withCache(
|
|
14
19
|
request,
|
|
15
20
|
async () => {
|
|
16
|
-
const did = env
|
|
17
|
-
const handle = env
|
|
18
|
-
const
|
|
21
|
+
const did = await configuredDid(env);
|
|
22
|
+
const handle = await configuredHandle(env);
|
|
23
|
+
const claimedHandle = validAtprotoHandle(handle);
|
|
24
|
+
const serviceEndpoint = await canonicalPdsOrigin(env);
|
|
19
25
|
|
|
20
26
|
let publicKeyMultibase: string | undefined;
|
|
21
27
|
let signingKeyError: string | undefined;
|
|
@@ -59,13 +65,13 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
59
65
|
'https://w3id.org/security/multikey/v1',
|
|
60
66
|
],
|
|
61
67
|
id: did,
|
|
62
|
-
alsoKnownAs: [`at://${
|
|
68
|
+
alsoKnownAs: claimedHandle ? [`at://${claimedHandle}`] : [],
|
|
63
69
|
verificationMethod: verificationMethods,
|
|
64
70
|
service: [
|
|
65
71
|
{
|
|
66
72
|
id: `${did}#atproto_pds`,
|
|
67
73
|
type: 'AtprotoPersonalDataServer',
|
|
68
|
-
serviceEndpoint
|
|
74
|
+
serviceEndpoint,
|
|
69
75
|
},
|
|
70
76
|
],
|
|
71
77
|
};
|
|
@@ -13,11 +13,12 @@ export async function POST({ locals }: APIContext) {
|
|
|
13
13
|
}
|
|
14
14
|
const db = env.ALTERAN_DB;
|
|
15
15
|
await db.exec("CREATE TABLE IF NOT EXISTS record (uri TEXT PRIMARY KEY, cid TEXT NOT NULL, json TEXT NOT NULL, created_at INTEGER DEFAULT (strftime('%s','now')));");
|
|
16
|
-
await db.exec("CREATE TABLE IF NOT EXISTS blob (cid TEXT
|
|
17
|
-
await db.exec("CREATE TABLE IF NOT EXISTS blob_usage (record_uri TEXT NOT NULL, key TEXT NOT NULL);");
|
|
16
|
+
await db.exec("CREATE TABLE IF NOT EXISTS blob (cid TEXT NOT NULL, did TEXT NOT NULL, key TEXT NOT NULL, mime TEXT NOT NULL, size INTEGER NOT NULL, uploaded_at INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (did, cid));");
|
|
17
|
+
await db.exec("CREATE TABLE IF NOT EXISTS blob_usage (did TEXT NOT NULL, record_uri TEXT NOT NULL, key TEXT NOT NULL, PRIMARY KEY (did, record_uri, key));");
|
|
18
18
|
await db.exec("CREATE TABLE IF NOT EXISTS repo_root (did TEXT PRIMARY KEY, commit_cid TEXT NOT NULL, rev INTEGER NOT NULL);");
|
|
19
19
|
// Indexes
|
|
20
20
|
await db.exec("CREATE INDEX IF NOT EXISTS record_cid_idx ON record(cid);");
|
|
21
|
-
await db.exec("CREATE INDEX IF NOT EXISTS blob_usage_record_idx ON blob_usage(record_uri);");
|
|
21
|
+
await db.exec("CREATE INDEX IF NOT EXISTS blob_usage_record_idx ON blob_usage(did, record_uri);");
|
|
22
|
+
await db.exec("CREATE INDEX IF NOT EXISTS blob_usage_did_key_idx ON blob_usage(did, key);");
|
|
22
23
|
return new Response('ok');
|
|
23
24
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import {
|
|
2
|
+
import type { Env } from '../../../env';
|
|
3
|
+
import { deleteUnreferencedBlobKeys, listOrphanBlobRefs } from '../../../db/dal';
|
|
3
4
|
|
|
4
5
|
export const prerender = false;
|
|
5
6
|
|
|
7
|
+
function isLocalDebugAllowed(env: Env): boolean {
|
|
8
|
+
const envName = (env as any).ENVIRONMENT as string | undefined;
|
|
9
|
+
const host = env.PDS_HOSTNAME as string | undefined;
|
|
10
|
+
return envName !== 'production' && (!host || host.includes('localhost') || host.startsWith('127.') || host === '::1');
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export async function POST({ locals }: APIContext) {
|
|
7
14
|
const { env } = locals.runtime;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
await env.ALTERAN_BLOBS.delete(key).catch(() => {});
|
|
12
|
-
await deleteBlobByKey(env, key);
|
|
13
|
-
deleted++;
|
|
14
|
-
}
|
|
15
|
+
if (!isLocalDebugAllowed(env)) return new Response('Not Found', { status: 404 });
|
|
16
|
+
|
|
17
|
+
const deleted = await deleteUnreferencedBlobKeys(env, await listOrphanBlobRefs(env));
|
|
15
18
|
return new Response(JSON.stringify({ deleted }), { headers: { 'Content-Type': 'application/json' } });
|
|
16
19
|
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
+
import type { Env } from '../../env';
|
|
2
3
|
import { getRecord as dalGetRecord, putRecord as dalPutRecord } from '../../db/dal';
|
|
3
4
|
|
|
4
5
|
export const prerender = false;
|
|
5
6
|
|
|
7
|
+
function isLocalDebugAllowed(env: Env): boolean {
|
|
8
|
+
const envName = (env as any).ENVIRONMENT as string | undefined;
|
|
9
|
+
const host = env.PDS_HOSTNAME as string | undefined;
|
|
10
|
+
return envName !== 'production' && (!host || host.includes('localhost') || host.startsWith('127.') || host === '::1');
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export async function GET({ locals, request }: APIContext) {
|
|
7
14
|
const { env } = locals.runtime;
|
|
15
|
+
if (!isLocalDebugAllowed(env)) return new Response('Not Found', { status: 404 });
|
|
16
|
+
|
|
8
17
|
const url = new URL(request.url);
|
|
9
18
|
const uri = url.searchParams.get('uri');
|
|
10
19
|
if (!uri) return new Response('missing uri', { status: 400 });
|
|
@@ -17,6 +26,8 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
17
26
|
|
|
18
27
|
export async function POST({ locals, request }: APIContext) {
|
|
19
28
|
const { env } = locals.runtime;
|
|
29
|
+
if (!isLocalDebugAllowed(env)) return new Response('Not Found', { status: 404 });
|
|
30
|
+
|
|
20
31
|
const body = (await request.json()) as { uri?: string; json?: unknown };
|
|
21
32
|
const uri = body.uri;
|
|
22
33
|
if (!uri) return new Response('missing uri', { status: 400 });
|
package/src/pages/oauth/token.ts
CHANGED
|
@@ -15,15 +15,46 @@ import {
|
|
|
15
15
|
storeRefreshToken,
|
|
16
16
|
updateOAuthSessionCurrent,
|
|
17
17
|
} from '../../db/account';
|
|
18
|
+
import {
|
|
19
|
+
logOauth,
|
|
20
|
+
summarizeTokenForm,
|
|
21
|
+
type OauthTokenFormSummary,
|
|
22
|
+
type OauthTokenStage,
|
|
23
|
+
} from '../../lib/oauth/observability';
|
|
18
24
|
|
|
19
25
|
export const prerender = false;
|
|
20
26
|
|
|
21
27
|
export async function POST({ locals, request }: APIContext) {
|
|
22
28
|
const { env } = locals.runtime;
|
|
29
|
+
const requestId = (locals as { requestId?: string }).requestId ?? null;
|
|
30
|
+
|
|
31
|
+
let formSummary: OauthTokenFormSummary | null = null;
|
|
32
|
+
let clientId: string | null = null;
|
|
33
|
+
|
|
34
|
+
const log = (
|
|
35
|
+
stage: OauthTokenStage,
|
|
36
|
+
extra: { error?: unknown; outcome?: 'ok' | 'error' } = {},
|
|
37
|
+
) =>
|
|
38
|
+
logOauth(request, {
|
|
39
|
+
endpoint: 'token',
|
|
40
|
+
stage,
|
|
41
|
+
outcome: extra.outcome ?? (extra.error !== undefined ? 'error' : 'ok'),
|
|
42
|
+
requestId,
|
|
43
|
+
error: extra.error,
|
|
44
|
+
clientId,
|
|
45
|
+
form: formSummary as Record<string, unknown> | null,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const fail = (stage: OauthTokenStage, code: string, desc: string): Response => {
|
|
49
|
+
log(stage, { error: new Error(desc) });
|
|
50
|
+
return jsonError(code, desc);
|
|
51
|
+
};
|
|
23
52
|
|
|
24
53
|
try {
|
|
25
54
|
const ver = await verifyDpop(env, request, { consumeJti: false, requireNonce: false });
|
|
26
55
|
const form = new URLSearchParams(await request.text());
|
|
56
|
+
formSummary = summarizeTokenForm(form);
|
|
57
|
+
clientId = form.get('client_id') || null;
|
|
27
58
|
const grant_type = form.get('grant_type') || '';
|
|
28
59
|
const issuer = publicPdsOrigin(env, request);
|
|
29
60
|
|
|
@@ -33,24 +64,28 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
33
64
|
const redirect_uri = form.get('redirect_uri') || '';
|
|
34
65
|
const code_verifier = form.get('code_verifier') || '';
|
|
35
66
|
if (!code || !client_id || !redirect_uri || !code_verifier) {
|
|
36
|
-
return
|
|
67
|
+
return fail('auth_code', 'invalid_request', 'Missing parameters');
|
|
37
68
|
}
|
|
38
69
|
|
|
39
70
|
const rec = await consumeCode(env, code);
|
|
40
|
-
if (!rec) return
|
|
41
|
-
if (rec.client_id !== client_id) return
|
|
42
|
-
if (rec.redirect_uri !== redirect_uri) return
|
|
71
|
+
if (!rec) return fail('auth_code', 'invalid_grant', 'Invalid or used code');
|
|
72
|
+
if (rec.client_id !== client_id) return fail('auth_code', 'invalid_grant', 'client_id mismatch');
|
|
73
|
+
if (rec.redirect_uri !== redirect_uri) return fail('auth_code', 'invalid_grant', 'redirect_uri mismatch');
|
|
43
74
|
|
|
44
75
|
const expected = await sha256b64url(code_verifier);
|
|
45
|
-
if (expected !== rec.code_challenge) return
|
|
46
|
-
if (ver.jkt !== rec.dpopJkt) return
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
})
|
|
76
|
+
if (expected !== rec.code_challenge) return fail('auth_code', 'invalid_grant', 'PKCE verification failed');
|
|
77
|
+
if (ver.jkt !== rec.dpopJkt) return fail('auth_code', 'invalid_dpop', 'DPoP key mismatch');
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await requireStoredClientAuthentication(env, client_id, issuer, form, {
|
|
81
|
+
method: rec.clientAuthMethod,
|
|
82
|
+
keyId: rec.clientAuthKeyId ?? null,
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const wrapped = new Error(`Client authentication failed: ${errorMessage(error) ?? error}`);
|
|
86
|
+
log('client_auth', { error: wrapped });
|
|
87
|
+
throw wrapped;
|
|
88
|
+
}
|
|
54
89
|
await consumeDpopVerificationJti(env, ver);
|
|
55
90
|
|
|
56
91
|
const sessionId = crypto.randomUUID().replace(/-/g, '');
|
|
@@ -91,6 +126,7 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
91
126
|
});
|
|
92
127
|
|
|
93
128
|
const expires_in = accessExpiresIn(accessPayload);
|
|
129
|
+
log('success', { outcome: 'ok' });
|
|
94
130
|
return tokenResponse({
|
|
95
131
|
access_token: accessJwt,
|
|
96
132
|
token_type: 'DPoP',
|
|
@@ -105,41 +141,45 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
105
141
|
const refresh_token = form.get('refresh_token') || '';
|
|
106
142
|
const client_id = form.get('client_id') || '';
|
|
107
143
|
if (!refresh_token || !client_id) {
|
|
108
|
-
return
|
|
144
|
+
return fail('refresh_token', 'invalid_request', 'Missing refresh_token or client_id');
|
|
109
145
|
}
|
|
110
146
|
|
|
111
147
|
const verification = await verifyRefreshToken(env, refresh_token).catch(() => null);
|
|
112
|
-
if (!verification || !verification.decoded) return
|
|
148
|
+
if (!verification || !verification.decoded) return fail('refresh_token', 'invalid_grant', 'Invalid refresh token');
|
|
113
149
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
114
|
-
if (verification.decoded.exp <= nowSec) return
|
|
150
|
+
if (verification.decoded.exp <= nowSec) return fail('refresh_token', 'invalid_grant', 'Expired refresh token');
|
|
115
151
|
|
|
116
152
|
const stored = await getRefreshToken(env, verification.decoded.jti);
|
|
117
153
|
if (!stored || stored.tokenKind !== 'oauth' || !stored.oauthSessionId) {
|
|
118
|
-
return
|
|
154
|
+
return fail('refresh_token', 'invalid_grant', 'Refresh token revoked');
|
|
119
155
|
}
|
|
120
156
|
const session = await getOAuthSession(env, stored.oauthSessionId);
|
|
121
157
|
if (!session || session.revokedAt || session.expiresAt <= nowSec) {
|
|
122
|
-
return
|
|
158
|
+
return fail('refresh_token', 'invalid_grant', 'OAuth session revoked');
|
|
123
159
|
}
|
|
124
160
|
|
|
125
161
|
if (stored.revokedAt || stored.nextId || stored.id !== session.currentRefreshTokenId) {
|
|
126
162
|
await revokeOAuthSession(env, session.id, nowSec);
|
|
127
|
-
return
|
|
163
|
+
return fail('refresh_token', 'invalid_grant', 'Refresh token replayed');
|
|
128
164
|
}
|
|
129
|
-
if (stored.expiresAt <= nowSec) return
|
|
165
|
+
if (stored.expiresAt <= nowSec) return fail('refresh_token', 'invalid_grant', 'Expired refresh token');
|
|
130
166
|
if (stored.did !== verification.decoded.sub || stored.did !== session.did) {
|
|
131
167
|
await revokeOAuthSession(env, session.id, nowSec);
|
|
132
|
-
return
|
|
168
|
+
return fail('refresh_token', 'invalid_grant', 'Subject mismatch');
|
|
169
|
+
}
|
|
170
|
+
if (client_id !== session.clientId) return fail('refresh_token', 'invalid_grant', 'client_id mismatch');
|
|
171
|
+
if (ver.jkt !== session.dpopJkt) return fail('refresh_token', 'invalid_dpop', 'DPoP key mismatch');
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await requireStoredClientAuthentication(env, client_id, issuer, form, {
|
|
175
|
+
method: session.clientAuthMethod,
|
|
176
|
+
keyId: session.clientAuthKeyId,
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const wrapped = new Error(`Client authentication failed: ${errorMessage(error) ?? error}`);
|
|
180
|
+
log('client_auth', { error: wrapped });
|
|
181
|
+
throw wrapped;
|
|
133
182
|
}
|
|
134
|
-
if (client_id !== session.clientId) return jsonError('invalid_grant', 'client_id mismatch');
|
|
135
|
-
if (ver.jkt !== session.dpopJkt) return jsonError('invalid_dpop', 'DPoP key mismatch');
|
|
136
|
-
|
|
137
|
-
await requireStoredClientAuthentication(env, client_id, issuer, form, {
|
|
138
|
-
method: session.clientAuthMethod,
|
|
139
|
-
keyId: session.clientAuthKeyId,
|
|
140
|
-
}).catch((error) => {
|
|
141
|
-
throw new Error(`Client authentication failed: ${errorMessage(error) ?? error}`);
|
|
142
|
-
});
|
|
143
183
|
await consumeDpopVerificationJti(env, ver);
|
|
144
184
|
|
|
145
185
|
const accessJti = crypto.randomUUID().replace(/-/g, '');
|
|
@@ -175,9 +215,10 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
175
215
|
});
|
|
176
216
|
} catch {
|
|
177
217
|
await revokeOAuthSession(env, session.id, nowSec);
|
|
178
|
-
return
|
|
218
|
+
return fail('refresh_token', 'invalid_grant', 'Refresh token replayed');
|
|
179
219
|
}
|
|
180
220
|
|
|
221
|
+
log('success', { outcome: 'ok' });
|
|
181
222
|
return tokenResponse({
|
|
182
223
|
access_token: accessJwt,
|
|
183
224
|
token_type: 'DPoP',
|
|
@@ -188,9 +229,13 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
188
229
|
}, await getAuthzNonce(env));
|
|
189
230
|
}
|
|
190
231
|
|
|
191
|
-
return
|
|
232
|
+
return fail('unsupported_grant', 'unsupported_grant_type', 'grant_type must be authorization_code or refresh_token');
|
|
192
233
|
} catch (e) {
|
|
193
|
-
if (e instanceof DpopNonceError)
|
|
234
|
+
if (e instanceof DpopNonceError) {
|
|
235
|
+
log('dpop', { error: e });
|
|
236
|
+
return dpopErrorResponse(env, e);
|
|
237
|
+
}
|
|
238
|
+
log('outer', { error: e });
|
|
194
239
|
return jsonError('invalid_request', errorMessage(e) ?? 'Unknown error');
|
|
195
240
|
}
|
|
196
241
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
2
|
import { proxyAppView } from '../../lib/appview';
|
|
3
3
|
import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
|
|
4
|
+
import {
|
|
5
|
+
isSingleUserUnsupportedRoute,
|
|
6
|
+
unsupportedSingleUserRouteResponse,
|
|
7
|
+
} from '../../lib/unsupported-routes';
|
|
4
8
|
|
|
5
9
|
export const prerender = false;
|
|
6
10
|
|
|
@@ -20,6 +24,19 @@ function nsidFromParams(params: Record<string, any>): string {
|
|
|
20
24
|
|
|
21
25
|
async function handle({ locals, request, params }: APIContext): Promise<Response> {
|
|
22
26
|
const { env } = locals.runtime;
|
|
27
|
+
const nsid = nsidFromParams(params).trim();
|
|
28
|
+
console.log('xrpc catchall invoked:', { nsid, url: request.url });
|
|
29
|
+
if (!nsid) {
|
|
30
|
+
return new Response(JSON.stringify({ error: 'NotFound' }), {
|
|
31
|
+
status: 404,
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isSingleUserUnsupportedRoute(nsid)) {
|
|
37
|
+
return unsupportedSingleUserRouteResponse(nsid);
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
let auth;
|
|
24
41
|
try {
|
|
25
42
|
auth = await authenticateRequest(request, env);
|
|
@@ -30,15 +47,6 @@ async function handle({ locals, request, params }: APIContext): Promise<Response
|
|
|
30
47
|
throw error;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
|
-
const nsid = nsidFromParams(params).trim();
|
|
34
|
-
console.log('xrpc catchall invoked:', { nsid, url: request.url });
|
|
35
|
-
if (!nsid) {
|
|
36
|
-
return new Response(JSON.stringify({ error: 'NotFound' }), {
|
|
37
|
-
status: 404,
|
|
38
|
-
headers: { 'Content-Type': 'application/json' },
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
50
|
if (!shouldProxy(nsid)) {
|
|
43
51
|
return new Response(JSON.stringify({ error: 'NotImplemented' }), {
|
|
44
52
|
status: 404,
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { authErrorResponse,
|
|
2
|
+
import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { canAccessActorPreferences } from '../../lib/auth-scope';
|
|
4
|
+
import { preferencesForAccess } from '../../lib/preference-policy';
|
|
3
5
|
import { getActorPreferences } from '../../lib/preferences';
|
|
4
6
|
|
|
5
7
|
export const prerender = false;
|
|
6
8
|
|
|
7
9
|
export async function GET({ locals, request }: APIContext) {
|
|
8
10
|
const { env } = locals.runtime;
|
|
11
|
+
let auth: NonNullable<Awaited<ReturnType<typeof authenticateRequest>>>;
|
|
9
12
|
try {
|
|
10
|
-
|
|
13
|
+
const verified = await authenticateRequest(request, env);
|
|
14
|
+
if (!verified || !canAccessActorPreferences(verified.access)) return unauthorized();
|
|
15
|
+
auth = verified;
|
|
11
16
|
} catch (error) {
|
|
12
17
|
const handled = await authErrorResponse(env, error);
|
|
13
18
|
if (handled) return handled;
|
|
@@ -15,7 +20,8 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
const { preferences } = await getActorPreferences(env);
|
|
18
|
-
|
|
23
|
+
const visible = preferencesForAccess(Array.isArray(preferences) ? preferences : [], auth.access);
|
|
24
|
+
return new Response(JSON.stringify({ preferences: visible }), {
|
|
19
25
|
headers: { 'Content-Type': 'application/json' },
|
|
20
26
|
});
|
|
21
27
|
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
2
|
import { errorCode, errorMessage } from '../../lib/errors';
|
|
3
|
-
import { authErrorResponse,
|
|
3
|
+
import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
|
|
4
|
+
import { canAccessActorPreferences } from '../../lib/auth-scope';
|
|
5
|
+
import { hasAppPasswordRestrictedPreferences, preferencesForWrite } from '../../lib/preference-policy';
|
|
4
6
|
import { readJsonBounded } from '../../lib/util';
|
|
5
|
-
import { setActorPreferences } from '../../lib/preferences';
|
|
7
|
+
import { getActorPreferences, setActorPreferences } from '../../lib/preferences';
|
|
6
8
|
|
|
7
9
|
export const prerender = false;
|
|
8
10
|
|
|
9
11
|
export async function POST({ locals, request }: APIContext) {
|
|
10
12
|
const { env } = locals.runtime;
|
|
13
|
+
let auth: NonNullable<Awaited<ReturnType<typeof authenticateRequest>>>;
|
|
11
14
|
try {
|
|
12
|
-
|
|
15
|
+
const verified = await authenticateRequest(request, env);
|
|
16
|
+
if (!verified || !canAccessActorPreferences(verified.access)) return unauthorized();
|
|
17
|
+
auth = verified;
|
|
13
18
|
} catch (error) {
|
|
14
19
|
const handled = await authErrorResponse(env, error);
|
|
15
20
|
if (handled) return handled;
|
|
@@ -27,7 +32,15 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
const preferences = Array.isArray(body?.preferences) ? body.preferences : [];
|
|
30
|
-
|
|
35
|
+
if (hasAppPasswordRestrictedPreferences(preferences, auth.access)) {
|
|
36
|
+
return new Response(
|
|
37
|
+
JSON.stringify({ error: 'Forbidden', message: 'App passwords cannot update restricted preferences' }),
|
|
38
|
+
{ status: 403, headers: { 'Content-Type': 'application/json' } },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const existing = auth.access.isAppPassword ? (await getActorPreferences(env)).preferences : [];
|
|
42
|
+
const writable = preferencesForWrite(existing, preferences, auth.access);
|
|
43
|
+
await setActorPreferences(env, writable);
|
|
31
44
|
|
|
32
45
|
return new Response(JSON.stringify({}), {
|
|
33
46
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { authErrorResponse,
|
|
2
|
+
import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { canUseAppPasswordLevelAccess } from '../../lib/auth-scope';
|
|
3
4
|
|
|
4
5
|
export const prerender = false;
|
|
5
6
|
|
|
6
7
|
export async function GET({ locals, request }: APIContext) {
|
|
7
8
|
const { env } = locals.runtime;
|
|
8
9
|
try {
|
|
9
|
-
|
|
10
|
+
const auth = await authenticateRequest(request, env);
|
|
11
|
+
if (!auth || !canUseAppPasswordLevelAccess(auth.access)) return unauthorized();
|
|
10
12
|
} catch (error) {
|
|
11
13
|
const handled = await authErrorResponse(env, error);
|
|
12
14
|
if (handled) return handled;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { authErrorResponse,
|
|
2
|
+
import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { canAccessChat } from '../../lib/auth-scope';
|
|
3
4
|
import { getPrimaryActor } from '../../lib/actor';
|
|
4
5
|
import { listChatConvoLogs } from '../../lib/chat';
|
|
5
6
|
|
|
@@ -8,7 +9,8 @@ export const prerender = false;
|
|
|
8
9
|
export async function GET({ locals, request }: APIContext) {
|
|
9
10
|
const { env } = locals.runtime;
|
|
10
11
|
try {
|
|
11
|
-
|
|
12
|
+
const auth = await authenticateRequest(request, env);
|
|
13
|
+
if (!auth || !canAccessChat(auth.access)) return unauthorized();
|
|
12
14
|
} catch (error) {
|
|
13
15
|
const handled = await authErrorResponse(env, error);
|
|
14
16
|
if (handled) return handled;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { authErrorResponse,
|
|
2
|
+
import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { canAccessChat } from '../../lib/auth-scope';
|
|
3
4
|
import { listChatConvos } from '../../lib/chat';
|
|
4
5
|
import { getPrimaryActor } from '../../lib/actor';
|
|
5
6
|
|
|
@@ -8,7 +9,8 @@ export const prerender = false;
|
|
|
8
9
|
export async function GET({ locals, request }: APIContext) {
|
|
9
10
|
const { env } = locals.runtime;
|
|
10
11
|
try {
|
|
11
|
-
|
|
12
|
+
const auth = await authenticateRequest(request, env);
|
|
13
|
+
if (!auth || !canAccessChat(auth.access)) return unauthorized();
|
|
12
14
|
} catch (error) {
|
|
13
15
|
const handled = await authErrorResponse(env, error);
|
|
14
16
|
if (handled) return handled;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { authErrorResponse,
|
|
2
|
+
import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { canAccessFullAccount } from '../../lib/auth-scope';
|
|
3
4
|
import { resolveSecret } from '../../lib/secrets';
|
|
5
|
+
import { canonicalPdsOrigin, configuredHandle, validAtprotoHandle } from '../../lib/public-host';
|
|
4
6
|
|
|
5
7
|
export const prerender = false;
|
|
6
8
|
|
|
@@ -15,7 +17,8 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
15
17
|
const { env } = locals.runtime;
|
|
16
18
|
|
|
17
19
|
try {
|
|
18
|
-
|
|
20
|
+
const auth = await authenticateRequest(request, env);
|
|
21
|
+
if (!auth || !canAccessFullAccount(auth.access)) return unauthorized();
|
|
19
22
|
} catch (error) {
|
|
20
23
|
const handled = await authErrorResponse(env, error);
|
|
21
24
|
if (handled) return handled;
|
|
@@ -23,8 +26,9 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
try {
|
|
26
|
-
const handle =
|
|
27
|
-
const
|
|
29
|
+
const handle = await configuredHandle(env);
|
|
30
|
+
const claimedHandle = validAtprotoHandle(handle);
|
|
31
|
+
const serviceEndpoint = await canonicalPdsOrigin(env);
|
|
28
32
|
|
|
29
33
|
// Always ES256K: derive did:key from the secp256k1 signing key
|
|
30
34
|
let didKey: string | undefined;
|
|
@@ -70,12 +74,12 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
70
74
|
|
|
71
75
|
const credentials = {
|
|
72
76
|
rotationKeys,
|
|
73
|
-
alsoKnownAs: [`at://${
|
|
77
|
+
alsoKnownAs: claimedHandle ? [`at://${claimedHandle}`] : [],
|
|
74
78
|
verificationMethods: { atproto: didKey },
|
|
75
79
|
services: {
|
|
76
80
|
atproto_pds: {
|
|
77
81
|
type: 'AtprotoPersonalDataServer',
|
|
78
|
-
endpoint:
|
|
82
|
+
endpoint: serviceEndpoint
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { authErrorResponse,
|
|
2
|
+
import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { canAccessFullAccount } from '../../lib/auth-scope';
|
|
3
4
|
|
|
4
5
|
export const prerender = false;
|
|
5
6
|
|
|
@@ -16,7 +17,8 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
16
17
|
const { env } = locals.runtime;
|
|
17
18
|
|
|
18
19
|
try {
|
|
19
|
-
|
|
20
|
+
const auth = await authenticateRequest(request, env);
|
|
21
|
+
if (!auth || !canAccessFullAccount(auth.access)) {
|
|
20
22
|
return unauthorized();
|
|
21
23
|
}
|
|
22
24
|
} catch (error) {
|
|
@@ -27,4 +29,3 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
27
29
|
|
|
28
30
|
return new Response(null, { status: 200 });
|
|
29
31
|
}
|
|
30
|
-
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
2
|
import { getAppViewConfig } from '../../lib/appview';
|
|
3
|
+
import { configuredAtprotoHandle, configuredDid, validAtprotoHandle } from '../../lib/public-host';
|
|
3
4
|
|
|
4
5
|
export const prerender = false;
|
|
5
6
|
|
|
@@ -11,8 +12,6 @@ export async function GET({ locals, url }: APIContext) {
|
|
|
11
12
|
const { env } = locals.runtime;
|
|
12
13
|
|
|
13
14
|
const handle = url.searchParams.get('handle');
|
|
14
|
-
const configuredHandle = String(env.PDS_HANDLE || 'user.example.com');
|
|
15
|
-
const did = String(env.PDS_DID || 'did:example:single-user');
|
|
16
15
|
|
|
17
16
|
if (!handle) {
|
|
18
17
|
return new Response(
|
|
@@ -21,10 +20,19 @@ export async function GET({ locals, url }: APIContext) {
|
|
|
21
20
|
);
|
|
22
21
|
}
|
|
23
22
|
|
|
23
|
+
const normalizedHandle = validAtprotoHandle(handle);
|
|
24
|
+
if (!normalizedHandle) {
|
|
25
|
+
return new Response(
|
|
26
|
+
JSON.stringify({ error: 'InvalidRequest', message: 'Unable to resolve handle' }),
|
|
27
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } },
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
// Single-user PDS: resolve the local configured handle directly
|
|
25
|
-
|
|
32
|
+
const configuredHandle = await configuredAtprotoHandle(env);
|
|
33
|
+
if (configuredHandle && normalizedHandle === configuredHandle) {
|
|
26
34
|
return new Response(
|
|
27
|
-
JSON.stringify({ did }),
|
|
35
|
+
JSON.stringify({ did: await configuredDid(env) }),
|
|
28
36
|
{
|
|
29
37
|
status: 200,
|
|
30
38
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -38,7 +46,7 @@ export async function GET({ locals, url }: APIContext) {
|
|
|
38
46
|
const app = getAppViewConfig(env);
|
|
39
47
|
const base = app?.url || 'https://api.bsky.app';
|
|
40
48
|
const upstream = new URL('/xrpc/com.atproto.identity.resolveHandle', base);
|
|
41
|
-
upstream.searchParams.set('handle',
|
|
49
|
+
upstream.searchParams.set('handle', normalizedHandle);
|
|
42
50
|
|
|
43
51
|
const response = await fetch(upstream.toString(), {
|
|
44
52
|
headers: { accept: 'application/json' },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { authErrorResponse,
|
|
2
|
+
import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { canAccessFullAccount } from '../../lib/auth-scope';
|
|
3
4
|
import { resolveSecret } from '../../lib/secrets';
|
|
4
5
|
|
|
5
6
|
export const prerender = false;
|
|
@@ -16,7 +17,8 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
16
17
|
const { env } = locals.runtime;
|
|
17
18
|
|
|
18
19
|
try {
|
|
19
|
-
|
|
20
|
+
const auth = await authenticateRequest(request, env);
|
|
21
|
+
if (!auth || !canAccessFullAccount(auth.access)) return unauthorized();
|
|
20
22
|
} catch (error) {
|
|
21
23
|
const handled = await authErrorResponse(env, error);
|
|
22
24
|
if (handled) return handled;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
2
|
import { errorMessage } from '../../lib/errors';
|
|
3
|
-
import { authErrorResponse,
|
|
3
|
+
import { authErrorResponse, authenticateRequest, unauthorized } from '../../lib/auth';
|
|
4
|
+
import { canAccessFullAccount } from '../../lib/auth-scope';
|
|
4
5
|
import { resolveSecret } from '../../lib/secrets';
|
|
5
6
|
|
|
6
7
|
export const prerender = false;
|
|
@@ -16,7 +17,8 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
16
17
|
const { env } = locals.runtime;
|
|
17
18
|
|
|
18
19
|
try {
|
|
19
|
-
|
|
20
|
+
const auth = await authenticateRequest(request, env);
|
|
21
|
+
if (!auth || !canAccessFullAccount(auth.access)) return unauthorized();
|
|
20
22
|
} catch (error) {
|
|
21
23
|
const handled = await authErrorResponse(env, error);
|
|
22
24
|
if (handled) return handled;
|