@alteran/astro 0.1.13 → 0.3.0
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 +28 -3
- package/index.js +2 -4
- package/migrations/0006_adorable_spectrum.sql +11 -0
- package/migrations/meta/0006_snapshot.json +429 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +6 -3
- package/src/db/account.ts +145 -0
- package/src/db/dal.ts +27 -9
- package/src/db/repo.ts +9 -8
- package/src/db/schema.ts +29 -11
- package/src/lib/actor.ts +133 -0
- package/src/lib/appview.ts +508 -0
- package/src/lib/auth.ts +26 -3
- package/src/lib/blob-refs.ts +9 -13
- package/src/lib/chat.ts +238 -0
- package/src/lib/config.ts +15 -7
- package/src/lib/feed.ts +165 -0
- package/src/lib/jwt.ts +144 -47
- package/src/lib/labeler.ts +91 -0
- package/src/lib/mst/blockstore.ts +98 -14
- package/src/lib/password.ts +40 -0
- package/src/lib/preferences.ts +73 -0
- package/src/lib/relay.ts +101 -0
- package/src/lib/secrets.ts +4 -1
- package/src/lib/session-tokens.ts +202 -0
- package/src/lib/token-cleanup.ts +3 -12
- package/src/lib/util.ts +17 -2
- package/src/middleware.ts +20 -21
- package/src/pages/.well-known/did.json.ts +45 -32
- package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +23 -0
- package/src/pages/xrpc/app.bsky.actor.getProfile.ts +34 -0
- package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +42 -0
- package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +36 -0
- package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +42 -0
- package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +37 -0
- package/src/pages/xrpc/app.bsky.feed.getPosts.ts +26 -0
- package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +35 -0
- package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +29 -0
- package/src/pages/xrpc/app.bsky.graph.getFollows.ts +29 -0
- package/src/pages/xrpc/app.bsky.labeler.getServices.ts +29 -0
- package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +20 -0
- package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +27 -0
- package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +19 -0
- package/src/pages/xrpc/app.bsky.unspecced.getConfig.ts +15 -0
- package/src/pages/xrpc/chat.bsky.convo.getLog.ts +26 -0
- package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +37 -0
- package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +64 -66
- package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +24 -0
- package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +127 -0
- package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +91 -0
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +6 -2
- package/src/pages/xrpc/com.atproto.server.createSession.ts +36 -8
- package/src/pages/xrpc/com.atproto.server.describeServer.ts +37 -4
- package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +64 -0
- package/src/pages/xrpc/com.atproto.server.refreshSession.ts +55 -32
- package/src/services/repo-manager.ts +15 -6
- package/src/worker/runtime.ts +9 -0
- package/types/env.d.ts +10 -1
- package/src/pages/xrpc/com.atproto.repo.importRepo.ts +0 -142
- package/src/pages/xrpc/com.atproto.server.activateAccount.ts +0 -53
- package/src/pages/xrpc/com.atproto.server.createAccount.ts +0 -99
- package/src/pages/xrpc/com.atproto.server.deactivateAccount.ts +0 -53
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { getPrimaryActor } from '../../lib/actor';
|
|
4
|
+
import { listChatConvoLogs } from '../../lib/chat';
|
|
5
|
+
|
|
6
|
+
export const prerender = false;
|
|
7
|
+
|
|
8
|
+
export async function GET({ locals, request }: APIContext) {
|
|
9
|
+
const { env } = locals.runtime;
|
|
10
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
11
|
+
|
|
12
|
+
const url = new URL(request.url);
|
|
13
|
+
const cursorParam = url.searchParams.get('cursor');
|
|
14
|
+
const parsedCursor = Number.parseInt(cursorParam ?? '', 10);
|
|
15
|
+
const cursor = Number.isFinite(parsedCursor) ? parsedCursor : undefined;
|
|
16
|
+
|
|
17
|
+
const actor = await getPrimaryActor(env);
|
|
18
|
+
const { logs, cursor: nextCursor } = await listChatConvoLogs(env, actor.did, cursor);
|
|
19
|
+
|
|
20
|
+
const payload: Record<string, unknown> = { logs };
|
|
21
|
+
if (nextCursor) payload.cursor = nextCursor;
|
|
22
|
+
|
|
23
|
+
return new Response(JSON.stringify(payload), {
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { listChatConvos } from '../../lib/chat';
|
|
4
|
+
import { getPrimaryActor } from '../../lib/actor';
|
|
5
|
+
|
|
6
|
+
export const prerender = false;
|
|
7
|
+
|
|
8
|
+
export async function GET({ locals, request }: APIContext) {
|
|
9
|
+
const { env } = locals.runtime;
|
|
10
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
11
|
+
|
|
12
|
+
const url = new URL(request.url);
|
|
13
|
+
const limitInput = Number.parseInt(url.searchParams.get('limit') ?? '', 10);
|
|
14
|
+
const limit = Math.max(1, Math.min(Number.isFinite(limitInput) ? limitInput : 50, 100));
|
|
15
|
+
const cursorParam = url.searchParams.get('cursor');
|
|
16
|
+
const cursor = cursorParam ? Number.parseInt(cursorParam, 10) : undefined;
|
|
17
|
+
const readStateParam = url.searchParams.get('readState');
|
|
18
|
+
const statusParam = url.searchParams.get('status');
|
|
19
|
+
|
|
20
|
+
const filters = {
|
|
21
|
+
readState: readStateParam === 'unread' ? 'unread' : null,
|
|
22
|
+
status:
|
|
23
|
+
statusParam === 'request' || statusParam === 'accepted' ? statusParam : null,
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
const actor = await getPrimaryActor(env);
|
|
27
|
+
const { convos, cursor: nextCursor } = await listChatConvos(env, actor.did, limit, cursor, filters);
|
|
28
|
+
|
|
29
|
+
const payload: Record<string, unknown> = {
|
|
30
|
+
convos,
|
|
31
|
+
};
|
|
32
|
+
if (nextCursor) payload.cursor = nextCursor;
|
|
33
|
+
|
|
34
|
+
return new Response(JSON.stringify(payload), {
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
2
|
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { resolveSecret } from '../../lib/secrets';
|
|
4
|
+
import * as uint8arrays from 'uint8arrays';
|
|
3
5
|
|
|
4
6
|
export const prerender = false;
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* com.atproto.identity.getRecommendedDidCredentials
|
|
8
10
|
*
|
|
9
|
-
* Returns recommended DID credentials for
|
|
10
|
-
*
|
|
11
|
+
* Returns the recommended DID credentials for the current account.
|
|
12
|
+
* This includes the handle, signing key, and PDS endpoint that should be
|
|
13
|
+
* used when updating the PLC identity.
|
|
11
14
|
*/
|
|
12
15
|
export async function GET({ locals, request }: APIContext) {
|
|
13
16
|
const { env } = locals.runtime;
|
|
@@ -15,85 +18,80 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
15
18
|
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
16
19
|
|
|
17
20
|
try {
|
|
18
|
-
const
|
|
19
|
-
const handle = env.PDS_HANDLE ?? 'example.com';
|
|
21
|
+
const handle = (await resolveSecret(env.PDS_HANDLE)) ?? 'example.com';
|
|
20
22
|
const hostname = env.PDS_HOSTNAME ?? handle;
|
|
21
23
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// Load signing key (Ed25519 PKCS#8 base64)
|
|
25
|
+
const signingKeyBase64 = await resolveSecret(env.REPO_SIGNING_KEY);
|
|
26
|
+
if (!signingKeyBase64) {
|
|
27
|
+
return new Response(
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
error: 'InvalidRequest',
|
|
30
|
+
message: 'Signing key not configured'
|
|
31
|
+
}),
|
|
32
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Import Ed25519 private key from PKCS#8 base64
|
|
37
|
+
const b64 = signingKeyBase64.replace(/\s+/g, '');
|
|
38
|
+
const bin = atob(b64);
|
|
39
|
+
const pkcs8 = new Uint8Array(bin.length);
|
|
40
|
+
for (let i = 0; i < bin.length; i++) pkcs8[i] = bin.charCodeAt(i);
|
|
41
|
+
|
|
42
|
+
// Ed25519 PKCS#8 format: the public key is the last 32 bytes of the private key section
|
|
43
|
+
// PKCS#8 structure for Ed25519:
|
|
44
|
+
// - Header (16 bytes)
|
|
45
|
+
// - Private key (32 bytes)
|
|
46
|
+
// - Public key (32 bytes)
|
|
47
|
+
// Total: 80 bytes for unencrypted PKCS#8
|
|
48
|
+
const publicKeyBytes = pkcs8.slice(-32);
|
|
28
49
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
// Create did:key from public key
|
|
51
|
+
// Ed25519 multicodec prefix is 0xed01
|
|
52
|
+
const multicodecPrefix = new Uint8Array([0xed, 0x01]);
|
|
53
|
+
const multicodecKey = new Uint8Array(multicodecPrefix.length + publicKeyBytes.length);
|
|
54
|
+
multicodecKey.set(multicodecPrefix);
|
|
55
|
+
multicodecKey.set(publicKeyBytes, multicodecPrefix.length);
|
|
34
56
|
|
|
35
|
-
|
|
36
|
-
|
|
57
|
+
const didKey = 'did:key:z' + uint8arrays.toString(multicodecKey, 'base58btc');
|
|
58
|
+
|
|
59
|
+
// Get current PLC data to preserve rotation keys
|
|
60
|
+
const did = (await resolveSecret(env.PDS_DID)) ?? 'did:example:single-user';
|
|
61
|
+
const plcResponse = await fetch(`https://plc.directory/${did}/data`);
|
|
62
|
+
|
|
63
|
+
let rotationKeys: string[] = [];
|
|
64
|
+
if (plcResponse.ok) {
|
|
65
|
+
const plcData = await plcResponse.json() as { rotationKeys?: string[] };
|
|
66
|
+
rotationKeys = plcData.rotationKeys || [];
|
|
37
67
|
}
|
|
38
68
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
services: {
|
|
50
|
-
atproto_pds: {
|
|
51
|
-
type: 'AtprotoPersonalDataServer',
|
|
52
|
-
endpoint: `https://${hostname}`
|
|
53
|
-
}
|
|
69
|
+
const credentials = {
|
|
70
|
+
rotationKeys,
|
|
71
|
+
alsoKnownAs: [`at://${handle}`],
|
|
72
|
+
verificationMethods: {
|
|
73
|
+
atproto: didKey
|
|
74
|
+
},
|
|
75
|
+
services: {
|
|
76
|
+
atproto_pds: {
|
|
77
|
+
type: 'AtprotoPersonalDataServer',
|
|
78
|
+
endpoint: `https://${hostname}`
|
|
54
79
|
}
|
|
55
|
-
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return new Response(
|
|
84
|
+
JSON.stringify(credentials),
|
|
56
85
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
57
86
|
);
|
|
58
87
|
} catch (error: any) {
|
|
88
|
+
console.error('Get recommended credentials error:', error);
|
|
59
89
|
return new Response(
|
|
60
90
|
JSON.stringify({
|
|
61
91
|
error: 'InternalServerError',
|
|
62
|
-
message: error.message || 'Failed to get
|
|
92
|
+
message: error.message || 'Failed to get recommended credentials'
|
|
63
93
|
}),
|
|
64
94
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
65
95
|
);
|
|
66
96
|
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Base58 encode (Bitcoin alphabet)
|
|
71
|
-
*/
|
|
72
|
-
function base58Encode(bytes: Uint8Array): string {
|
|
73
|
-
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
74
|
-
|
|
75
|
-
// Convert bytes to bigint
|
|
76
|
-
let num = 0n;
|
|
77
|
-
for (const byte of bytes) {
|
|
78
|
-
num = num * 256n + BigInt(byte);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Convert to base58
|
|
82
|
-
let result = '';
|
|
83
|
-
while (num > 0n) {
|
|
84
|
-
const remainder = Number(num % 58n);
|
|
85
|
-
result = ALPHABET[remainder] + result;
|
|
86
|
-
num = num / 58n;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Add leading '1's for leading zero bytes
|
|
90
|
-
for (const byte of bytes) {
|
|
91
|
-
if (byte === 0) {
|
|
92
|
-
result = '1' + result;
|
|
93
|
-
} else {
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return result;
|
|
99
97
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
|
+
|
|
4
|
+
export const prerender = false;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* com.atproto.identity.requestPlcOperationSignature
|
|
8
|
+
*
|
|
9
|
+
* Single-user PDS instances typically control the PLC rotation key directly,
|
|
10
|
+
* so the email-based 2FA flow used by the public PDS is unnecessary. Clients
|
|
11
|
+
* (like the Indigo goat CLI) still invoke this endpoint prior to signing a PLC
|
|
12
|
+
* operation, so we acknowledge the request and report success without
|
|
13
|
+
* triggering any side effects.
|
|
14
|
+
*/
|
|
15
|
+
export async function POST({ locals, request }: APIContext) {
|
|
16
|
+
const { env } = locals.runtime;
|
|
17
|
+
|
|
18
|
+
if (!(await isAuthorized(request, env))) {
|
|
19
|
+
return unauthorized();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return new Response(null, { status: 200 });
|
|
23
|
+
}
|
|
24
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { resolveSecret } from '../../lib/secrets';
|
|
4
|
+
|
|
5
|
+
export const prerender = false;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* com.atproto.identity.signPlcOperation
|
|
9
|
+
*
|
|
10
|
+
* Signs a PLC update operation with the server's PLC rotation key and returns it
|
|
11
|
+
* to the caller. This endpoint mirrors the behavior of a PDS that controls the
|
|
12
|
+
* PLC rotation key directly (single-user deployments), so the email challenge
|
|
13
|
+
* token is accepted but not enforced here.
|
|
14
|
+
*/
|
|
15
|
+
export async function POST({ locals, request }: APIContext) {
|
|
16
|
+
const { env } = locals.runtime;
|
|
17
|
+
|
|
18
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const body = await request.json() as {
|
|
22
|
+
token?: string;
|
|
23
|
+
rotationKeys?: string[];
|
|
24
|
+
alsoKnownAs?: string[];
|
|
25
|
+
verificationMethods?: Record<string, string>;
|
|
26
|
+
services?: Record<string, { type: string; endpoint: string }>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// NOTE: For single-user PDS we don't enforce email token checks.
|
|
30
|
+
if (!body || typeof body !== 'object') {
|
|
31
|
+
return jsonErr(400, 'InvalidRequest', 'Malformed JSON body');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const did = (await resolveSecret(env.PDS_DID)) ?? '';
|
|
35
|
+
if (!did || !did.startsWith('did:')) {
|
|
36
|
+
return jsonErr(400, 'InvalidRequest', 'PDS_DID is not configured');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Load PLC rotation key (hex-encoded secp256k1 private key).
|
|
40
|
+
// MUST be the rotation key currently present in the PLC document.
|
|
41
|
+
const privHex = ((await resolveSecret(env.PDS_PLC_ROTATION_KEY as any)) || '').trim();
|
|
42
|
+
if (!privHex) {
|
|
43
|
+
return jsonErr(500, 'ServerMisconfigured', 'PDS_PLC_ROTATION_KEY is not configured');
|
|
44
|
+
}
|
|
45
|
+
// Lazy-load deps compatible with Workers runtime
|
|
46
|
+
const { Secp256k1Keypair } = await import('@atproto/crypto');
|
|
47
|
+
const dagCbor: any = await import('@ipld/dag-cbor');
|
|
48
|
+
const { sha256 } = await import('multiformats/hashes/sha2');
|
|
49
|
+
const { CID } = await import('multiformats/cid');
|
|
50
|
+
const u8a: any = await import('uint8arrays');
|
|
51
|
+
|
|
52
|
+
const signer = await Secp256k1Keypair.import(privHex);
|
|
53
|
+
|
|
54
|
+
// Fetch last op for prev CID
|
|
55
|
+
const lastRes = await fetch(`https://plc.directory/${encodeURIComponent(did)}/log/last`);
|
|
56
|
+
if (!lastRes.ok) {
|
|
57
|
+
const text = await lastRes.text();
|
|
58
|
+
return jsonErr(lastRes.status, 'PlcFetchFailed', `Failed to fetch last op: ${text}`);
|
|
59
|
+
}
|
|
60
|
+
const lastOp = await lastRes.json();
|
|
61
|
+
if ((lastOp as any)?.type === 'plc_tombstone') {
|
|
62
|
+
return jsonErr(400, 'DidTombstoned', 'DID is tombstoned');
|
|
63
|
+
}
|
|
64
|
+
const lastOpCbor = dagCbor.encode(lastOp);
|
|
65
|
+
const mh = await sha256.digest(lastOpCbor);
|
|
66
|
+
const prevCid = CID.createV1(dagCbor.code, mh);
|
|
67
|
+
|
|
68
|
+
// Fetch current document data as defaults and to verify rotation key
|
|
69
|
+
const dataRes = await fetch(`https://plc.directory/${encodeURIComponent(did)}/data`);
|
|
70
|
+
if (!dataRes.ok) {
|
|
71
|
+
const text = await dataRes.text();
|
|
72
|
+
return jsonErr(dataRes.status, 'PlcFetchFailed', `Failed to fetch document data: ${text}`);
|
|
73
|
+
}
|
|
74
|
+
const doc = (await dataRes.json()) as any;
|
|
75
|
+
|
|
76
|
+
const rotationKeys = body.rotationKeys ?? doc.rotationKeys ?? [];
|
|
77
|
+
const alsoKnownAs = body.alsoKnownAs ?? doc.alsoKnownAs ?? [];
|
|
78
|
+
const verificationMethods = body.verificationMethods ?? doc.verificationMethods ?? {};
|
|
79
|
+
const services = body.services ?? doc.services ?? {};
|
|
80
|
+
|
|
81
|
+
if (!services.atproto_pds || typeof services.atproto_pds !== 'object') {
|
|
82
|
+
return jsonErr(400, 'InvalidRequest', 'Missing atproto_pds service in PLC operation');
|
|
83
|
+
}
|
|
84
|
+
if (!services.atproto_pds.type) {
|
|
85
|
+
services.atproto_pds.type = 'AtprotoPersonalDataServer';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const unsignedOp = {
|
|
89
|
+
type: 'plc_operation',
|
|
90
|
+
rotationKeys,
|
|
91
|
+
verificationMethods,
|
|
92
|
+
alsoKnownAs,
|
|
93
|
+
services,
|
|
94
|
+
prev: prevCid.toString(),
|
|
95
|
+
} as Record<string, unknown>;
|
|
96
|
+
|
|
97
|
+
const bytes = dagCbor.encode(unsignedOp);
|
|
98
|
+
const sig = await signer.sign(bytes);
|
|
99
|
+
const sigB64 = (u8a.toString as any)(sig, 'base64url');
|
|
100
|
+
const operation = { ...unsignedOp, sig: sigB64 };
|
|
101
|
+
|
|
102
|
+
// sanity: ensure our configured rotation key is included
|
|
103
|
+
const signerDid = (await (await import('@atproto/crypto')).Secp256k1Keypair.import(privHex)).did();
|
|
104
|
+
if (!rotationKeys.includes(signerDid)) {
|
|
105
|
+
return jsonErr(
|
|
106
|
+
400,
|
|
107
|
+
'RotationKeyMismatch',
|
|
108
|
+
`Configured PDS_PLC_ROTATION_KEY (${signerDid}) is not present in PLC rotationKeys. Update PLC or your configuration.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return new Response(JSON.stringify({ operation }), {
|
|
113
|
+
status: 200,
|
|
114
|
+
headers: { 'Content-Type': 'application/json' },
|
|
115
|
+
});
|
|
116
|
+
} catch (error: any) {
|
|
117
|
+
console.error('signPlcOperation error:', error);
|
|
118
|
+
return jsonErr(500, 'InternalServerError', error?.message || 'Failed to sign PLC operation');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function jsonErr(status: number, error: string, message: string) {
|
|
123
|
+
return new Response(
|
|
124
|
+
JSON.stringify({ error, message }),
|
|
125
|
+
{ status, headers: { 'Content-Type': 'application/json' } }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { resolveSecret } from '../../lib/secrets';
|
|
4
|
+
|
|
5
|
+
export const prerender = false;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* com.atproto.identity.submitPlcOperation
|
|
9
|
+
*
|
|
10
|
+
* Submits a signed PLC operation to the PLC directory.
|
|
11
|
+
* This is a proxy endpoint that validates the operation is for the current account
|
|
12
|
+
* before submitting it to plc.directory.
|
|
13
|
+
*/
|
|
14
|
+
export async function POST({ locals, request }: APIContext) {
|
|
15
|
+
const { env } = locals.runtime;
|
|
16
|
+
|
|
17
|
+
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const body = await request.json() as { operation?: any };
|
|
21
|
+
const { operation } = body;
|
|
22
|
+
|
|
23
|
+
if (!operation) {
|
|
24
|
+
return new Response(
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
error: 'InvalidRequest',
|
|
27
|
+
message: 'Missing operation in request body'
|
|
28
|
+
}),
|
|
29
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const did = (await resolveSecret(env.PDS_DID)) ?? 'did:example:single-user';
|
|
34
|
+
|
|
35
|
+
console.log('Submitting PLC operation:', {
|
|
36
|
+
did,
|
|
37
|
+
operationType: operation.type,
|
|
38
|
+
hasSig: !!operation.sig,
|
|
39
|
+
prev: operation.prev
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Submit to PLC directory
|
|
43
|
+
const plcResponse = await fetch(`https://plc.directory/${did}`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json'
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify(operation)
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const responseHeaders: Record<string, string> = {};
|
|
52
|
+
plcResponse.headers.forEach((value, key) => {
|
|
53
|
+
responseHeaders[key] = value;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log('PLC response:', {
|
|
57
|
+
status: plcResponse.status,
|
|
58
|
+
statusText: plcResponse.statusText,
|
|
59
|
+
headers: responseHeaders
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!plcResponse.ok) {
|
|
63
|
+
const errorText = await plcResponse.text();
|
|
64
|
+
console.error('PLC directory error:', errorText);
|
|
65
|
+
return new Response(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
error: 'PlcOperationFailed',
|
|
68
|
+
message: `PLC directory rejected operation (${plcResponse.status}): ${errorText}`
|
|
69
|
+
}),
|
|
70
|
+
{ status: plcResponse.status, headers: { 'Content-Type': 'application/json' } }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const plcResult = await plcResponse.text();
|
|
75
|
+
console.log('PLC submission successful:', plcResult);
|
|
76
|
+
|
|
77
|
+
return new Response(
|
|
78
|
+
JSON.stringify({ success: true }),
|
|
79
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
80
|
+
);
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
console.error('Submit PLC operation error:', error);
|
|
83
|
+
return new Response(
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
error: 'InternalServerError',
|
|
86
|
+
message: error.message || 'Failed to submit PLC operation'
|
|
87
|
+
}),
|
|
88
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -4,6 +4,7 @@ import { checkRate } from '../../lib/ratelimit';
|
|
|
4
4
|
import { isAllowedMime } from '../../lib/util';
|
|
5
5
|
import { R2BlobStore } from '../../services/r2-blob-store';
|
|
6
6
|
import { putBlobRef, checkBlobQuota, updateBlobQuota, isAccountActive } from '../../db/dal';
|
|
7
|
+
import { resolveSecret } from '../../lib/secrets';
|
|
7
8
|
|
|
8
9
|
export const prerender = false;
|
|
9
10
|
|
|
@@ -12,7 +13,7 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
12
13
|
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
13
14
|
|
|
14
15
|
// Get DID from environment (single-user PDS)
|
|
15
|
-
const did = env.PDS_DID ?? 'did:example:single-user';
|
|
16
|
+
const did = (await resolveSecret(env.PDS_DID)) ?? 'did:example:single-user';
|
|
16
17
|
|
|
17
18
|
// Check if account is active
|
|
18
19
|
const active = await isAccountActive(env, did);
|
|
@@ -31,7 +32,10 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
31
32
|
|
|
32
33
|
const buf = await request.arrayBuffer();
|
|
33
34
|
const contentType = request.headers.get('content-type') ?? 'application/octet-stream';
|
|
34
|
-
|
|
35
|
+
|
|
36
|
+
// Skip MIME type validation during migration - accept all types
|
|
37
|
+
// Uncomment the line below to re-enable MIME type restrictions after migration
|
|
38
|
+
// if (!isAllowedMime(env, contentType)) return new Response(JSON.stringify({ error: 'UnsupportedMediaType' }), { status: 415 });
|
|
35
39
|
|
|
36
40
|
// Check quota before upload
|
|
37
41
|
const canUpload = await checkBlobQuota(env, did, buf.byteLength);
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { signJwt } from '../../lib/jwt';
|
|
3
2
|
import { readJson } from '../../lib/util';
|
|
4
3
|
import { drizzle } from 'drizzle-orm/d1';
|
|
5
4
|
import { login_attempts } from '../../db/schema';
|
|
6
5
|
import { eq } from 'drizzle-orm';
|
|
6
|
+
import { createAccount, getAccountByIdentifier, storeRefreshToken } from '../../db/account';
|
|
7
|
+
import { hashPassword, verifyPassword } from '../../lib/password';
|
|
8
|
+
import { issueSessionTokens } from '../../lib/session-tokens';
|
|
9
|
+
import { getRuntimeString } from '../../lib/secrets';
|
|
7
10
|
|
|
8
11
|
export const prerender = false;
|
|
9
12
|
|
|
@@ -30,8 +33,27 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
30
33
|
);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
const
|
|
34
|
-
const
|
|
36
|
+
const body = await readJson(request).catch(() => ({ identifier: '', password: '' }));
|
|
37
|
+
const identifier = typeof body.identifier === 'string' && body.identifier ? body.identifier : (await getRuntimeString(env, 'PDS_HANDLE', 'user.example'));
|
|
38
|
+
const password = typeof body.password === 'string' ? body.password : '';
|
|
39
|
+
|
|
40
|
+
let account = await getAccountByIdentifier(env, identifier ?? '');
|
|
41
|
+
if (!account) {
|
|
42
|
+
const fallbackPassword = await getRuntimeString(env, 'USER_PASSWORD', '');
|
|
43
|
+
if (fallbackPassword) {
|
|
44
|
+
const fallbackDid = await getRuntimeString(env, 'PDS_DID', 'did:example:single-user');
|
|
45
|
+
const fallbackHandle = await getRuntimeString(env, 'PDS_HANDLE', identifier);
|
|
46
|
+
const hashed = await hashPassword(fallbackPassword);
|
|
47
|
+
await createAccount(env, {
|
|
48
|
+
did: fallbackDid,
|
|
49
|
+
handle: fallbackHandle,
|
|
50
|
+
passwordScrypt: hashed,
|
|
51
|
+
});
|
|
52
|
+
account = await getAccountByIdentifier(env, identifier ?? '');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const passwordHash = account?.passwordScrypt ?? null;
|
|
56
|
+
const ok = !!password && !!account && (await verifyPassword(password, passwordHash));
|
|
35
57
|
|
|
36
58
|
if (!ok) {
|
|
37
59
|
// Track failed attempt
|
|
@@ -80,11 +102,17 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
80
102
|
await db.delete(login_attempts).where(eq(login_attempts.ip, clientIp)).run();
|
|
81
103
|
}
|
|
82
104
|
|
|
83
|
-
const did =
|
|
84
|
-
const handle = env
|
|
85
|
-
|
|
86
|
-
const accessJwt = await
|
|
87
|
-
|
|
105
|
+
const did = account?.did ?? (await getRuntimeString(env, 'PDS_DID', 'did:example:single-user'));
|
|
106
|
+
const handle = account?.handle ?? (await getRuntimeString(env, 'PDS_HANDLE', identifier ?? 'user.example'));
|
|
107
|
+
|
|
108
|
+
const { accessJwt, refreshJwt, refreshPayload, refreshExpiry } = await issueSessionTokens(env, did);
|
|
109
|
+
|
|
110
|
+
await storeRefreshToken(env, {
|
|
111
|
+
id: refreshPayload.jti,
|
|
112
|
+
did,
|
|
113
|
+
expiresAt: refreshExpiry,
|
|
114
|
+
appPasswordName: null,
|
|
115
|
+
});
|
|
88
116
|
|
|
89
117
|
return new Response(JSON.stringify({ did, handle, accessJwt, refreshJwt }), {
|
|
90
118
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1,15 +1,48 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
+
import { getAppViewConfig } from '../../lib/appview';
|
|
2
3
|
|
|
3
4
|
export const prerender = false;
|
|
4
5
|
|
|
5
6
|
export function GET({ locals }: APIContext) {
|
|
6
7
|
const { env } = locals.runtime;
|
|
8
|
+
const did = typeof env.PDS_DID === 'string' ? env.PDS_DID : 'did:example:single-user';
|
|
9
|
+
const availableUserDomains: string[] = [];
|
|
10
|
+
|
|
11
|
+
const links = typeof env.PDS_LINK_PRIVACY === 'string' || typeof env.PDS_LINK_TOS === 'string'
|
|
12
|
+
? {
|
|
13
|
+
$type: 'com.atproto.server.describeServer#links' as const,
|
|
14
|
+
...(typeof env.PDS_LINK_PRIVACY === 'string' ? { privacyPolicy: env.PDS_LINK_PRIVACY } : {}),
|
|
15
|
+
...(typeof env.PDS_LINK_TOS === 'string' ? { termsOfService: env.PDS_LINK_TOS } : {}),
|
|
16
|
+
}
|
|
17
|
+
: undefined;
|
|
18
|
+
|
|
19
|
+
const contact = typeof env.PDS_CONTACT_EMAIL === 'string'
|
|
20
|
+
? {
|
|
21
|
+
$type: 'com.atproto.server.describeServer#contact' as const,
|
|
22
|
+
email: env.PDS_CONTACT_EMAIL,
|
|
23
|
+
}
|
|
24
|
+
: undefined;
|
|
25
|
+
|
|
26
|
+
const appView = getAppViewConfig(env);
|
|
27
|
+
|
|
7
28
|
const body = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
handle: env.PDS_HANDLE ?? null,
|
|
29
|
+
did,
|
|
30
|
+
availableUserDomains,
|
|
11
31
|
inviteCodeRequired: false,
|
|
12
|
-
|
|
32
|
+
phoneVerificationRequired: false,
|
|
33
|
+
...(links ? { links } : {}),
|
|
34
|
+
...(contact ? { contact } : {}),
|
|
35
|
+
...(appView
|
|
36
|
+
? {
|
|
37
|
+
services: {
|
|
38
|
+
appview: {
|
|
39
|
+
$type: 'com.atproto.server.describeServer#service' as const,
|
|
40
|
+
serviceEndpoint: appView.url,
|
|
41
|
+
did: appView.did,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
: {}),
|
|
13
46
|
};
|
|
14
47
|
return new Response(JSON.stringify(body), {
|
|
15
48
|
headers: { 'Content-Type': 'application/json' },
|