@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/util.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { APIContext } from 'astro';
|
|
2
1
|
import { CID } from 'multiformats/cid';
|
|
3
2
|
import * as dagCbor from '@ipld/dag-cbor';
|
|
4
3
|
import { sha256 } from 'multiformats/hashes/sha2';
|
|
4
|
+
import type { Env } from '../env';
|
|
5
|
+
import { PayloadTooLarge } from './errors';
|
|
5
6
|
|
|
6
7
|
export function tryParse(json: string): unknown {
|
|
7
8
|
try {
|
|
@@ -11,33 +12,30 @@ export function tryParse(json: string): unknown {
|
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
export async function readJson(request: Request): Promise<any> {
|
|
15
|
+
export async function readJson(request: Request): Promise<unknown> {
|
|
16
16
|
const max = 64 * 1024;
|
|
17
17
|
const text = await request.text();
|
|
18
|
-
if (text.length > max) throw new
|
|
18
|
+
if (text.length > max) throw new PayloadTooLarge();
|
|
19
19
|
return JSON.parse(text || '{}');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export async function readJsonBounded(env:
|
|
23
|
-
const raw =
|
|
22
|
+
export async function readJsonBounded(env: Env, request: Request): Promise<unknown> {
|
|
23
|
+
const raw = env.PDS_MAX_JSON_BYTES ?? '65536';
|
|
24
24
|
const max = Number(raw) > 0 ? Number(raw) : 65536;
|
|
25
25
|
const text = await request.text();
|
|
26
|
-
if (text.length > max)
|
|
27
|
-
const err: any = new Error('PayloadTooLarge');
|
|
28
|
-
err.code = 'PayloadTooLarge';
|
|
29
|
-
throw err;
|
|
30
|
-
}
|
|
26
|
+
if (text.length > max) throw new PayloadTooLarge();
|
|
31
27
|
return JSON.parse(text || '{}');
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
export function bearerToken(request: Request): string | null {
|
|
35
31
|
const auth = request.headers.get('authorization');
|
|
36
|
-
if (!auth
|
|
37
|
-
return auth.slice(7);
|
|
32
|
+
if (!auth) return null;
|
|
33
|
+
if (auth.startsWith('Bearer ')) return auth.slice(7);
|
|
34
|
+
if (auth.startsWith('DPoP ')) return auth.slice(5);
|
|
35
|
+
return null;
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
export function isAllowedMime(env:
|
|
38
|
+
export function isAllowedMime(env: Env, mime: string): boolean {
|
|
41
39
|
const def = [
|
|
42
40
|
// Images
|
|
43
41
|
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif',
|
|
@@ -50,7 +48,7 @@ export function isAllowedMime(env: any, mime: string): boolean {
|
|
|
50
48
|
// Generic fallback
|
|
51
49
|
'application/octet-stream'
|
|
52
50
|
];
|
|
53
|
-
const raw =
|
|
51
|
+
const raw = env.PDS_ALLOWED_MIME ?? def.join(',');
|
|
54
52
|
const set = new Set(raw.split(',').map((s) => s.trim()).filter(Boolean));
|
|
55
53
|
|
|
56
54
|
// Extract base MIME type (remove charset and other parameters)
|
|
@@ -59,6 +57,58 @@ export function isAllowedMime(env: any, mime: string): boolean {
|
|
|
59
57
|
return set.has(baseMime);
|
|
60
58
|
}
|
|
61
59
|
|
|
60
|
+
export function baseMime(mime: string | null | undefined): string {
|
|
61
|
+
if (!mime) return 'application/octet-stream';
|
|
62
|
+
return mime.toLowerCase().split(';')[0].trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Best-effort MIME sniffing for common image/video/audio formats.
|
|
66
|
+
// Prefer this over client-provided header when possible, mirroring upstream PDS.
|
|
67
|
+
export function sniffMime(buf: ArrayBuffer): string | null {
|
|
68
|
+
const bytes = new Uint8Array(buf);
|
|
69
|
+
const len = bytes.length;
|
|
70
|
+
const ascii = (start: number, n: number) =>
|
|
71
|
+
String.fromCharCode(...bytes.slice(start, start + n));
|
|
72
|
+
|
|
73
|
+
if (len >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
|
|
74
|
+
return 'image/jpeg';
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
len >= 8 &&
|
|
78
|
+
bytes[0] === 0x89 && ascii(1, 3) === 'PNG' && bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a
|
|
79
|
+
) {
|
|
80
|
+
return 'image/png';
|
|
81
|
+
}
|
|
82
|
+
if (len >= 6) {
|
|
83
|
+
const sig6 = ascii(0, 6);
|
|
84
|
+
if (sig6 === 'GIF87a' || sig6 === 'GIF89a') return 'image/gif';
|
|
85
|
+
}
|
|
86
|
+
if (len >= 12 && ascii(0, 4) === 'RIFF' && ascii(8, 4) === 'WEBP') {
|
|
87
|
+
return 'image/webp';
|
|
88
|
+
}
|
|
89
|
+
// ISO BMFF / MP4 / AVIF / QuickTime: find 'ftyp' within first 256 bytes
|
|
90
|
+
{
|
|
91
|
+
const window = Math.min(len, 256);
|
|
92
|
+
for (let i = 0; i + 8 <= window; i++) {
|
|
93
|
+
if (ascii(i, 4) === 'ftyp') {
|
|
94
|
+
const brand = ascii(i + 4, 4);
|
|
95
|
+
const mp4Brands = new Set(['isom', 'iso2', 'mp41', 'mp42', 'avc1', 'MSNV', '3gp4', 'M4V ']);
|
|
96
|
+
if (brand === 'avif' || brand === 'avis' || brand === 'mif1' || brand === 'msf1') return 'image/avif';
|
|
97
|
+
if (brand === 'qt ') return 'video/quicktime';
|
|
98
|
+
if (mp4Brands.has(brand)) return 'video/mp4';
|
|
99
|
+
// Unknown brand: still likely MP4 container
|
|
100
|
+
return 'video/mp4';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// WebM/Matroska (EBML)
|
|
105
|
+
if (len >= 4 && bytes[0] === 0x1a && bytes[1] === 0x45 && bytes[2] === 0xdf && bytes[3] === 0xa3) {
|
|
106
|
+
// Could be audio/webm or video/webm; default to video/webm
|
|
107
|
+
return 'video/webm';
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
62
112
|
export function randomRkey(): string {
|
|
63
113
|
return crypto.randomUUID().replace(/-/g, '').substring(0, 13);
|
|
64
114
|
}
|
package/src/middleware.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineMiddleware, sequence } from 'astro
|
|
1
|
+
import { defineMiddleware, sequence } from 'astro/middleware';
|
|
2
2
|
|
|
3
3
|
const cors = defineMiddleware(async ({ locals, request }, next) => {
|
|
4
4
|
// Match atproto CORS implementation: use wildcard for public endpoints
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
+
import { errorMessage } from '../../lib/errors';
|
|
2
3
|
import { withCache, CACHE_CONFIGS } from '../../lib/cache';
|
|
3
|
-
import { base58btc } from 'multiformats/bases/base58';
|
|
4
4
|
import { resolveSecret } from '../../lib/secrets';
|
|
5
5
|
import { Secp256k1Keypair } from '@atproto/crypto';
|
|
6
6
|
import { formatMultikey } from '@atproto/crypto/dist/did';
|
|
@@ -18,35 +18,28 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
18
18
|
const hostname = env.PDS_HOSTNAME ?? new URL(request.url).hostname;
|
|
19
19
|
|
|
20
20
|
let publicKeyMultibase: string | undefined;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const raw = new Uint8Array(bin.length);
|
|
38
|
-
for (let i = 0; i < bin.length; i++) raw[i] = bin.charCodeAt(i);
|
|
39
|
-
if (raw.byteLength === 32) {
|
|
40
|
-
const prefixed = new Uint8Array(2 + raw.byteLength);
|
|
41
|
-
prefixed[0] = 0xed;
|
|
42
|
-
prefixed[1] = 0x01;
|
|
43
|
-
prefixed.set(raw, 2);
|
|
44
|
-
publicKeyMultibase = base58btc.encode(prefixed);
|
|
45
|
-
}
|
|
46
|
-
} catch (error) {
|
|
47
|
-
console.warn('Failed to encode repo signing key', error);
|
|
21
|
+
let signingKeyError: string | undefined;
|
|
22
|
+
try {
|
|
23
|
+
const signingKey = await resolveSecret((env as any).REPO_SIGNING_KEY);
|
|
24
|
+
if (!signingKey) {
|
|
25
|
+
signingKeyError = 'REPO_SIGNING_KEY not configured';
|
|
26
|
+
console.warn('did.json: REPO_SIGNING_KEY not configured');
|
|
27
|
+
} else {
|
|
28
|
+
const cleaned = signingKey.trim();
|
|
29
|
+
let kp: Secp256k1Keypair;
|
|
30
|
+
if (/^[0-9a-fA-F]{64}$/.test(cleaned)) {
|
|
31
|
+
kp = await Secp256k1Keypair.import(cleaned);
|
|
32
|
+
} else {
|
|
33
|
+
const bin = atob(cleaned.replace(/\s+/g, ''));
|
|
34
|
+
const bytes = new Uint8Array(bin.length);
|
|
35
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
36
|
+
kp = await Secp256k1Keypair.import(bytes);
|
|
48
37
|
}
|
|
38
|
+
publicKeyMultibase = formatMultikey(kp.jwtAlg, kp.publicKeyBytes());
|
|
49
39
|
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
signingKeyError = `Failed to process REPO_SIGNING_KEY: ${errorMessage(error) || error}`;
|
|
42
|
+
console.error('did.json: Failed to process REPO_SIGNING_KEY:', error);
|
|
50
43
|
}
|
|
51
44
|
|
|
52
45
|
const verificationMethods = publicKeyMultibase
|
|
@@ -60,7 +53,7 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
60
53
|
]
|
|
61
54
|
: [];
|
|
62
55
|
|
|
63
|
-
const didDocument = {
|
|
56
|
+
const didDocument: Record<string, unknown> = {
|
|
64
57
|
'@context': [
|
|
65
58
|
'https://www.w3.org/ns/did/v1',
|
|
66
59
|
'https://w3id.org/security/multikey/v1',
|
|
@@ -77,6 +70,11 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
77
70
|
],
|
|
78
71
|
};
|
|
79
72
|
|
|
73
|
+
// Add debug info if signing key has issues (only visible in dev/debug)
|
|
74
|
+
if (signingKeyError && (env as any).ENVIRONMENT !== 'production') {
|
|
75
|
+
didDocument._debug = { signingKeyError };
|
|
76
|
+
}
|
|
77
|
+
|
|
80
78
|
return new Response(JSON.stringify(didDocument, null, 2), {
|
|
81
79
|
headers: {
|
|
82
80
|
'Content-Type': 'application/json',
|
|
@@ -86,4 +84,3 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
86
84
|
CACHE_CONFIGS.DID_DOCUMENT,
|
|
87
85
|
);
|
|
88
86
|
}
|
|
89
|
-
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { withCache, CACHE_CONFIGS } from '../../lib/cache';
|
|
3
|
+
|
|
4
|
+
export const prerender = false;
|
|
5
|
+
|
|
6
|
+
export async function GET({ locals, request }: APIContext) {
|
|
7
|
+
const { env } = locals.runtime;
|
|
8
|
+
return withCache(
|
|
9
|
+
request,
|
|
10
|
+
async () => {
|
|
11
|
+
const url = new URL(request.url);
|
|
12
|
+
const origin = `${url.protocol}//${url.host}`;
|
|
13
|
+
const json = {
|
|
14
|
+
issuer: origin,
|
|
15
|
+
pushed_authorization_request_endpoint: `${origin}/oauth/par`,
|
|
16
|
+
authorization_endpoint: `${origin}/oauth/authorize`,
|
|
17
|
+
token_endpoint: `${origin}/oauth/token`,
|
|
18
|
+
scopes_supported: 'atproto transition:generic',
|
|
19
|
+
response_types_supported: ['code'],
|
|
20
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
21
|
+
code_challenge_methods_supported: ['S256'],
|
|
22
|
+
token_endpoint_auth_methods_supported: ['none', 'private_key_jwt'],
|
|
23
|
+
dpop_signing_alg_values_supported: ['ES256'],
|
|
24
|
+
};
|
|
25
|
+
return new Response(JSON.stringify(json, null, 2), {
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
CACHE_CONFIGS.WELL_KNOWN,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { withCache, CACHE_CONFIGS } from '../../lib/cache';
|
|
3
|
+
|
|
4
|
+
export const prerender = false;
|
|
5
|
+
|
|
6
|
+
export async function GET({ request }: APIContext) {
|
|
7
|
+
return withCache(
|
|
8
|
+
request,
|
|
9
|
+
async () => {
|
|
10
|
+
const url = new URL(request.url);
|
|
11
|
+
const origin = `${url.protocol}//${url.host}`;
|
|
12
|
+
const json = {
|
|
13
|
+
authorization_servers: [origin],
|
|
14
|
+
};
|
|
15
|
+
return new Response(JSON.stringify(json, null, 2), {
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
CACHE_CONFIGS.WELL_KNOWN,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -7,7 +7,7 @@ export async function GET({ locals, params }: APIContext) {
|
|
|
7
7
|
const key = params.key;
|
|
8
8
|
if (!key) return new Response('missing key', { status: 400 });
|
|
9
9
|
|
|
10
|
-
const obj = await env.
|
|
10
|
+
const obj = await env.ALTERAN_BLOBS.get(key);
|
|
11
11
|
if (!obj) return new Response('not found', { status: 404 });
|
|
12
12
|
|
|
13
13
|
const body = obj.body as unknown as BodyInit | null;
|
|
@@ -22,6 +22,6 @@ export async function PUT({ locals, request, params }: APIContext) {
|
|
|
22
22
|
if (!key) return new Response('missing key', { status: 400 });
|
|
23
23
|
|
|
24
24
|
const body = await request.arrayBuffer();
|
|
25
|
-
await env.
|
|
25
|
+
await env.ALTERAN_BLOBS.put(key, body, { httpMetadata: { contentType: request.headers.get('content-type') ?? 'application/octet-stream' } });
|
|
26
26
|
return new Response('uploaded');
|
|
27
27
|
}
|
|
@@ -11,7 +11,7 @@ export async function POST({ locals }: APIContext) {
|
|
|
11
11
|
if (!isLocal) {
|
|
12
12
|
return new Response('Not Found', { status: 404 });
|
|
13
13
|
}
|
|
14
|
-
const db = env.
|
|
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
16
|
await db.exec("CREATE TABLE IF NOT EXISTS blob (cid TEXT PRIMARY KEY, key TEXT NOT NULL, mime TEXT NOT NULL, size INTEGER NOT NULL);");
|
|
17
17
|
await db.exec("CREATE TABLE IF NOT EXISTS blob_usage (record_uri TEXT NOT NULL, key TEXT NOT NULL);");
|
|
@@ -14,7 +14,7 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
14
14
|
|
|
15
15
|
const url = new URL(request.url);
|
|
16
16
|
const n = Math.min(Number(url.searchParams.get('n') ?? '20') || 20, 200);
|
|
17
|
-
const db = drizzle(env.
|
|
17
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
18
18
|
const rows = await db.select().from(commit_log).orderBy(desc(commit_log.seq)).limit(n).all();
|
|
19
19
|
return new Response(JSON.stringify({ commits: rows }), { headers: { 'content-type': 'application/json' } });
|
|
20
20
|
}
|
|
@@ -8,7 +8,7 @@ export async function POST({ locals }: APIContext) {
|
|
|
8
8
|
const keys = await listOrphanBlobKeys(env);
|
|
9
9
|
let deleted = 0;
|
|
10
10
|
for (const key of keys) {
|
|
11
|
-
await env.
|
|
11
|
+
await env.ALTERAN_BLOBS.delete(key).catch(() => {});
|
|
12
12
|
await deleteBlobByKey(env, key);
|
|
13
13
|
deleted++;
|
|
14
14
|
}
|
|
@@ -21,7 +21,7 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
21
21
|
const uri = body.uri;
|
|
22
22
|
if (!uri) return new Response('missing uri', { status: 400 });
|
|
23
23
|
|
|
24
|
-
const did = env.PDS_DID
|
|
24
|
+
const did = env.PDS_DID as string;
|
|
25
25
|
const row = {
|
|
26
26
|
uri,
|
|
27
27
|
did,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { errorMessage } from '../../lib/errors';
|
|
3
|
+
|
|
4
|
+
export const prerender = false;
|
|
5
|
+
|
|
6
|
+
export async function GET({ locals }: APIContext) {
|
|
7
|
+
const { env } = locals.runtime;
|
|
8
|
+
|
|
9
|
+
if (!env.ALTERAN_SEQUENCER) {
|
|
10
|
+
return new Response(JSON.stringify({ error: 'SequencerNotConfigured' }), {
|
|
11
|
+
status: 503,
|
|
12
|
+
headers: { 'Content-Type': 'application/json' },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const id = env.ALTERAN_SEQUENCER.idFromName('default');
|
|
18
|
+
const stub = env.ALTERAN_SEQUENCER.get(id);
|
|
19
|
+
const response = await stub.fetch(new Request('http://internal/metrics') as any);
|
|
20
|
+
const text = await response.text();
|
|
21
|
+
return new Response(text, { status: response.status, headers: { 'Content-Type': 'application/json' } });
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return new Response(JSON.stringify({ error: 'InternalError', message: String(errorMessage(e) || e) }), {
|
|
24
|
+
status: 500,
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/pages/health.ts
CHANGED
|
@@ -22,8 +22,8 @@ export async function GET({ locals }: APIContext) {
|
|
|
22
22
|
|
|
23
23
|
// Check D1 database connectivity
|
|
24
24
|
try {
|
|
25
|
-
if (env.
|
|
26
|
-
await env.
|
|
25
|
+
if (env.ALTERAN_DB) {
|
|
26
|
+
await env.ALTERAN_DB.prepare('SELECT 1').first();
|
|
27
27
|
checks.database.status = 'ok';
|
|
28
28
|
} else {
|
|
29
29
|
checks.database.status = 'error';
|
|
@@ -38,9 +38,9 @@ export async function GET({ locals }: APIContext) {
|
|
|
38
38
|
|
|
39
39
|
// Check R2 storage connectivity
|
|
40
40
|
try {
|
|
41
|
-
if (env.
|
|
41
|
+
if (env.ALTERAN_BLOBS) {
|
|
42
42
|
// Simple list operation to verify connectivity
|
|
43
|
-
await env.
|
|
43
|
+
await env.ALTERAN_BLOBS.list({ limit: 1 });
|
|
44
44
|
checks.storage.status = 'ok';
|
|
45
45
|
} else {
|
|
46
46
|
checks.storage.status = 'error';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { loadPar, saveCode, deletePar } from '../../lib/oauth/store';
|
|
3
|
+
|
|
4
|
+
export const prerender = false;
|
|
5
|
+
|
|
6
|
+
function parseRequestUri(u: string): string | null {
|
|
7
|
+
const p = 'urn:ietf:params:oauth:request_uri:';
|
|
8
|
+
if (!u || !u.startsWith(p)) return null;
|
|
9
|
+
const id = u.slice(p.length);
|
|
10
|
+
return /^[A-Za-z0-9]+$/.test(id) ? id : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function GET({ locals, request }: APIContext) {
|
|
14
|
+
const { env } = locals.runtime;
|
|
15
|
+
const url = new URL(request.url);
|
|
16
|
+
const request_uri = url.searchParams.get('request_uri') || '';
|
|
17
|
+
const client_id = url.searchParams.get('client_id') || '';
|
|
18
|
+
const deny = url.searchParams.get('deny') === '1';
|
|
19
|
+
const prompt = url.searchParams.get('prompt') || '';
|
|
20
|
+
|
|
21
|
+
const id = parseRequestUri(request_uri);
|
|
22
|
+
if (!id) {
|
|
23
|
+
return new Response('invalid request_uri', { status: 400 });
|
|
24
|
+
}
|
|
25
|
+
const par = await loadPar(env, id);
|
|
26
|
+
if (!par) {
|
|
27
|
+
return new Response('request expired or not found', { status: 400 });
|
|
28
|
+
}
|
|
29
|
+
if (client_id && client_id !== par.client_id) {
|
|
30
|
+
return new Response('client_id mismatch', { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (deny) {
|
|
34
|
+
const redirectDeny = new URL(par.redirect_uri);
|
|
35
|
+
redirectDeny.searchParams.set('state', par.state);
|
|
36
|
+
redirectDeny.searchParams.set('error', 'access_denied');
|
|
37
|
+
return new Response(null, { status: 302, headers: { Location: redirectDeny.toString() } });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const requireConsent = String((env as any).PDS_REQUIRE_CONSENT ?? '1') !== '0' || prompt === 'consent';
|
|
41
|
+
if (requireConsent && prompt !== 'none') {
|
|
42
|
+
const consentUrl = new URL('/oauth/consent', `${url.protocol}//${url.host}`);
|
|
43
|
+
consentUrl.searchParams.set('request_uri', request_uri);
|
|
44
|
+
consentUrl.searchParams.set('client_id', par.client_id);
|
|
45
|
+
return new Response(null, { status: 302, headers: { Location: consentUrl.toString() } });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// TODO: implement user authentication + consent UI.
|
|
49
|
+
// For single-user PDS, auto-approve using configured DID.
|
|
50
|
+
const did = String((env as any).PDS_DID ?? 'did:example:single-user');
|
|
51
|
+
|
|
52
|
+
// Issue a short-lived authorization code
|
|
53
|
+
const code = crypto.randomUUID().replace(/-/g, '');
|
|
54
|
+
const now = Math.floor(Date.now() / 1000);
|
|
55
|
+
await saveCode(env, code, {
|
|
56
|
+
code,
|
|
57
|
+
client_id: par.client_id,
|
|
58
|
+
redirect_uri: par.redirect_uri,
|
|
59
|
+
code_challenge: par.code_challenge,
|
|
60
|
+
scope: par.scope,
|
|
61
|
+
dpopJkt: par.dpopJkt,
|
|
62
|
+
did,
|
|
63
|
+
createdAt: now,
|
|
64
|
+
expiresAt: now + 600, // 10 minutes
|
|
65
|
+
used: false,
|
|
66
|
+
});
|
|
67
|
+
await deletePar(env, id);
|
|
68
|
+
|
|
69
|
+
const redirect = new URL(par.redirect_uri);
|
|
70
|
+
redirect.searchParams.set('state', par.state);
|
|
71
|
+
redirect.searchParams.set('iss', `${url.protocol}//${url.host}`);
|
|
72
|
+
redirect.searchParams.set('code', code);
|
|
73
|
+
|
|
74
|
+
return new Response(null, {
|
|
75
|
+
status: 302,
|
|
76
|
+
headers: { Location: redirect.toString() },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { loadPar } from '../../lib/oauth/store';
|
|
3
|
+
import { fetchClientMetadata } from '../../lib/oauth/clients';
|
|
4
|
+
|
|
5
|
+
export const prerender = false;
|
|
6
|
+
|
|
7
|
+
function esc(s: string): string { return s.replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"','\'':'''} as any)[c]); }
|
|
8
|
+
|
|
9
|
+
export async function GET({ locals, request }: APIContext) {
|
|
10
|
+
const url = new URL(request.url);
|
|
11
|
+
const request_uri = url.searchParams.get('request_uri') || '';
|
|
12
|
+
const client_id = url.searchParams.get('client_id') || '';
|
|
13
|
+
|
|
14
|
+
const id = request_uri.replace('urn:ietf:params:oauth:request_uri:', '');
|
|
15
|
+
if (!id) return new Response('invalid request_uri', { status: 400 });
|
|
16
|
+
const par = await loadPar(locals.runtime.env, id);
|
|
17
|
+
if (!par) return new Response('request expired or not found', { status: 400 });
|
|
18
|
+
if (client_id && par.client_id !== client_id) return new Response('client_id mismatch', { status: 400 });
|
|
19
|
+
|
|
20
|
+
let meta: any = null;
|
|
21
|
+
try {
|
|
22
|
+
meta = await fetchClientMetadata(par.client_id);
|
|
23
|
+
} catch {
|
|
24
|
+
// Client metadata is decorative on this page; the consent form still renders.
|
|
25
|
+
}
|
|
26
|
+
const clientName = esc(meta?.client_name || new URL(par.client_id).host);
|
|
27
|
+
const logo = typeof meta?.logo_uri === 'string' ? meta.logo_uri : '';
|
|
28
|
+
const scopes = par.scope.split(' ').filter(Boolean);
|
|
29
|
+
|
|
30
|
+
const allowUrl = new URL('/oauth/authorize', `${url.protocol}//${url.host}`);
|
|
31
|
+
allowUrl.searchParams.set('request_uri', request_uri);
|
|
32
|
+
allowUrl.searchParams.set('client_id', par.client_id);
|
|
33
|
+
|
|
34
|
+
const denyUrl = new URL(par.redirect_uri);
|
|
35
|
+
denyUrl.searchParams.set('state', par.state);
|
|
36
|
+
denyUrl.searchParams.set('error', 'access_denied');
|
|
37
|
+
|
|
38
|
+
const html = `<!doctype html>
|
|
39
|
+
<html>
|
|
40
|
+
<head>
|
|
41
|
+
<meta charset="utf-8" />
|
|
42
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
43
|
+
<title>Authorize ${clientName}</title>
|
|
44
|
+
<style>
|
|
45
|
+
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; padding: 2rem; color: #222; }
|
|
46
|
+
.card { max-width: 560px; margin: 0 auto; border: 1px solid #ddd; border-radius: 8px; padding: 1.5rem; }
|
|
47
|
+
.client { display: flex; gap: 12px; align-items: center; }
|
|
48
|
+
img.logo { width: 40px; height: 40px; border-radius: 6px; object-fit: cover; }
|
|
49
|
+
ul { padding-left: 1.2rem; }
|
|
50
|
+
.actions { display: flex; gap: 12px; margin-top: 1rem; }
|
|
51
|
+
a.btn { display: inline-block; padding: 8px 14px; border-radius: 6px; text-decoration: none; }
|
|
52
|
+
a.primary { background: #0a66ff; color: #fff; }
|
|
53
|
+
a.secondary { background: #eee; color: #333; }
|
|
54
|
+
.scope { background: #f5f5f7; display: inline-block; padding: 2px 8px; border-radius: 999px; margin-right: 6px; font-size: 12px; }
|
|
55
|
+
</style>
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<div class="card">
|
|
59
|
+
<div class="client">
|
|
60
|
+
${logo ? `<img class="logo" src="${esc(logo)}" alt="" />` : ''}
|
|
61
|
+
<div>
|
|
62
|
+
<div style="font-weight:600;">${clientName}</div>
|
|
63
|
+
<div style="color:#555; font-size: 12px;">${esc(par.client_id)}</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<p style="margin-top:1rem;">This app is requesting:</p>
|
|
67
|
+
<div>
|
|
68
|
+
${scopes.map((s) => `<span class="scope">${esc(s)}</span>`).join(' ')}
|
|
69
|
+
</div>
|
|
70
|
+
<div class="actions">
|
|
71
|
+
<a class="btn primary" href="${allowUrl.toString()}">Allow</a>
|
|
72
|
+
<a class="btn secondary" href="${denyUrl.toString()}">Deny</a>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</body>
|
|
76
|
+
</html>`;
|
|
77
|
+
|
|
78
|
+
return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
|
79
|
+
}
|
|
80
|
+
|