@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,64 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { authenticateRequest, unauthorized } from '../../lib/auth';
|
|
3
|
+
import { createServiceAuthToken } from '../../lib/appview';
|
|
4
|
+
|
|
5
|
+
export const prerender = false;
|
|
6
|
+
|
|
7
|
+
export async function GET({ locals, request }: APIContext) {
|
|
8
|
+
const { env } = locals.runtime;
|
|
9
|
+
const auth = await authenticateRequest(request, env);
|
|
10
|
+
if (!auth) return unauthorized();
|
|
11
|
+
|
|
12
|
+
const url = new URL(request.url);
|
|
13
|
+
const audienceParam = url.searchParams.get('aud');
|
|
14
|
+
const lexParam = url.searchParams.get('lxm');
|
|
15
|
+
const expParam = url.searchParams.get('exp');
|
|
16
|
+
|
|
17
|
+
const audience = audienceParam?.trim();
|
|
18
|
+
if (!audience) {
|
|
19
|
+
return new Response(JSON.stringify({ error: 'MissingAudience' }), {
|
|
20
|
+
status: 400,
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const lexiconMethod = lexParam && lexParam.trim() !== '' ? lexParam.trim() : null;
|
|
26
|
+
|
|
27
|
+
let expiresIn = 60;
|
|
28
|
+
const now = Math.floor(Date.now() / 1000);
|
|
29
|
+
if (expParam !== null) {
|
|
30
|
+
if (!/^-?\d+$/.test(expParam)) {
|
|
31
|
+
return new Response(JSON.stringify({ error: 'BadExpiration', message: 'expiration must be an integer timestamp' }), {
|
|
32
|
+
status: 400,
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const exp = Number(expParam);
|
|
37
|
+
if (exp <= now) {
|
|
38
|
+
return new Response(JSON.stringify({ error: 'BadExpiration', message: 'expiration is in the past' }), {
|
|
39
|
+
status: 400,
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (exp - now > 3600) {
|
|
44
|
+
return new Response(JSON.stringify({ error: 'BadExpiration', message: 'expiration too far in future' }), {
|
|
45
|
+
status: 400,
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
expiresIn = Math.max(1, exp - now);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const token = await createServiceAuthToken(env, auth.claims.sub, audience, lexiconMethod, expiresIn);
|
|
54
|
+
return new Response(JSON.stringify({ token }), {
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
});
|
|
57
|
+
} catch (error: any) {
|
|
58
|
+
console.error('service auth error:', error);
|
|
59
|
+
return new Response(JSON.stringify({ error: 'InternalServerError' }), {
|
|
60
|
+
status: 500,
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { APIContext } from 'astro';
|
|
2
|
-
import { signJwt, verifyJwt } from '../../lib/jwt';
|
|
3
2
|
import { bearerToken } from '../../lib/util';
|
|
4
3
|
import { lazyCleanupExpiredTokens } from '../../lib/token-cleanup';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { getRuntimeString } from '../../lib/secrets';
|
|
5
|
+
import { getAccountByIdentifier, getRefreshToken, markRefreshTokenRotated, storeRefreshToken } from '../../db/account';
|
|
6
|
+
import { verifyRefreshToken, issueSessionTokens, computeGraceExpiry } from '../../lib/session-tokens';
|
|
8
7
|
|
|
9
8
|
export const prerender = false;
|
|
10
9
|
|
|
@@ -18,46 +17,70 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
18
17
|
);
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
20
|
+
const verification = await verifyRefreshToken(env, token).catch(() => null);
|
|
21
|
+
if (!verification) {
|
|
23
22
|
return new Response(
|
|
24
23
|
JSON.stringify({ error: 'InvalidToken', message: 'Invalid or expired refresh token' }),
|
|
25
24
|
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
26
25
|
);
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
38
|
-
);
|
|
39
|
-
}
|
|
28
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
29
|
+
const { decoded } = verification;
|
|
30
|
+
|
|
31
|
+
if (!decoded || typeof decoded.jti !== 'string') {
|
|
32
|
+
return new Response(
|
|
33
|
+
JSON.stringify({ error: 'InvalidToken', message: 'Malformed refresh token' }),
|
|
34
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
35
|
+
);
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
if (typeof decoded.exp !== 'number' || decoded.exp <= nowSec) {
|
|
39
|
+
return new Response(
|
|
40
|
+
JSON.stringify({ error: 'ExpiredToken', message: 'Refresh token expired' }),
|
|
41
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const stored = await getRefreshToken(env, decoded.jti);
|
|
46
|
+
if (!stored) {
|
|
47
|
+
return new Response(
|
|
48
|
+
JSON.stringify({ error: 'InvalidToken', message: 'Refresh token has been revoked' }),
|
|
49
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
if (stored.expiresAt <= nowSec) {
|
|
54
|
+
return new Response(
|
|
55
|
+
JSON.stringify({ error: 'ExpiredToken', message: 'Refresh token expired' }),
|
|
56
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (stored.did !== decoded.sub) {
|
|
61
|
+
return new Response(
|
|
62
|
+
JSON.stringify({ error: 'InvalidToken', message: 'Token subject mismatch' }),
|
|
63
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
64
|
+
);
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
const account = await getAccountByIdentifier(env, stored.did);
|
|
68
|
+
const did = stored.did;
|
|
69
|
+
const handle = account?.handle ?? (await getRuntimeString(env, 'PDS_HANDLE', 'user.example'));
|
|
70
|
+
|
|
71
|
+
// Rotate: generate new token pair with new JTI
|
|
72
|
+
const { accessJwt, refreshJwt, refreshPayload, refreshExpiry } = await issueSessionTokens(env, did, { jti: stored.nextId ?? undefined });
|
|
73
|
+
|
|
74
|
+
await storeRefreshToken(env, {
|
|
75
|
+
id: refreshPayload.jti,
|
|
76
|
+
did,
|
|
77
|
+
expiresAt: refreshExpiry,
|
|
78
|
+
appPasswordName: stored.appPasswordName ?? null,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const graceExpiry = computeGraceExpiry(stored.expiresAt, nowSec);
|
|
82
|
+
await markRefreshTokenRotated(env, decoded.jti, refreshPayload.jti, graceExpiry);
|
|
83
|
+
|
|
61
84
|
// Lazy cleanup of expired tokens (runs 1% of the time)
|
|
62
85
|
lazyCleanupExpiredTokens(env).catch(console.error);
|
|
63
86
|
|
|
@@ -3,13 +3,14 @@ import type { Env } from '../env';
|
|
|
3
3
|
import { MST, D1Blockstore, Leaf } from '../lib/mst';
|
|
4
4
|
import { drizzle } from 'drizzle-orm/d1';
|
|
5
5
|
import { repo_root } from '../db/schema';
|
|
6
|
-
import { eq } from 'drizzle-orm';
|
|
6
|
+
import { eq, sql } from 'drizzle-orm';
|
|
7
7
|
import type { RepoOp } from '../lib/firehose/frames';
|
|
8
8
|
import * as dagCbor from '@ipld/dag-cbor';
|
|
9
9
|
import { cidForCbor } from '../lib/mst/util';
|
|
10
10
|
import { putRecord as dalPutRecord, deleteRecord as dalDeleteRecord } from '../db/dal';
|
|
11
11
|
import { bumpRoot } from '../db/repo';
|
|
12
12
|
import { generateTid } from '../lib/commit';
|
|
13
|
+
import { resolveSecret } from '../lib/secrets';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Repository Manager
|
|
@@ -21,7 +22,12 @@ export class RepoManager {
|
|
|
21
22
|
|
|
22
23
|
constructor(private env: Env) {
|
|
23
24
|
this.blockstore = new D1Blockstore(env);
|
|
24
|
-
this.did
|
|
25
|
+
// Note: this.did will be set asynchronously, but it's only used in async methods
|
|
26
|
+
this.did = 'did:example:single-user'; // Default, will be resolved in async methods
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private async getDid(): Promise<string> {
|
|
30
|
+
return (await resolveSecret(this.env.PDS_DID)) ?? 'did:example:single-user';
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/**
|
|
@@ -217,19 +223,22 @@ export class RepoManager {
|
|
|
217
223
|
async updateRoot(mst: MST, rev: number): Promise<void> {
|
|
218
224
|
const db = drizzle(this.env.DB);
|
|
219
225
|
const rootCid = await mst.getPointer();
|
|
226
|
+
const did = await this.getDid();
|
|
227
|
+
const revStr = String(rev);
|
|
220
228
|
|
|
229
|
+
// Use sql.raw with excluded to properly reference INSERT values
|
|
221
230
|
await db
|
|
222
231
|
.insert(repo_root)
|
|
223
232
|
.values({
|
|
224
|
-
did
|
|
233
|
+
did,
|
|
225
234
|
commitCid: rootCid.toString(),
|
|
226
|
-
rev,
|
|
235
|
+
rev: revStr,
|
|
227
236
|
})
|
|
228
237
|
.onConflictDoUpdate({
|
|
229
238
|
target: repo_root.did,
|
|
230
239
|
set: {
|
|
231
|
-
commitCid:
|
|
232
|
-
rev,
|
|
240
|
+
commitCid: sql.raw('excluded.commit_cid'),
|
|
241
|
+
rev: sql.raw('excluded.rev'),
|
|
233
242
|
},
|
|
234
243
|
})
|
|
235
244
|
.run();
|
package/src/worker/runtime.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { seed } from '../db/seed';
|
|
2
2
|
import { validateConfigOrThrow } from '../lib/config';
|
|
3
3
|
import { resolveEnvSecrets } from '../lib/secrets';
|
|
4
|
+
import { notifyRelaysIfNeeded } from '../lib/relay';
|
|
4
5
|
import type { Env } from '../env';
|
|
5
6
|
import type { SSRManifest } from 'astro';
|
|
6
7
|
import type {
|
|
@@ -64,6 +65,14 @@ export function createPdsFetchHandler(options?: CreatePdsFetchHandlerOptions): P
|
|
|
64
65
|
|
|
65
66
|
await seed(resolvedEnv.DB, (resolvedEnv.PDS_DID as string | undefined) ?? 'did:example:single-user');
|
|
66
67
|
|
|
68
|
+
// Fire-and-forget: let relays know this PDS exists and is reachable.
|
|
69
|
+
// Throttled per isolate and safe to call frequently.
|
|
70
|
+
try {
|
|
71
|
+
ctx.waitUntil(notifyRelaysIfNeeded(resolvedEnv as any, request.url));
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// Never block on relay notification
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
const url = new URL(request.url);
|
|
68
77
|
if (url.pathname === '/xrpc/com.atproto.sync.subscribeRepos') {
|
|
69
78
|
const upgrade = request.headers.get('upgrade');
|
package/types/env.d.ts
CHANGED
|
@@ -29,16 +29,25 @@ declare global {
|
|
|
29
29
|
PDS_MAX_BLOB_SIZE?: string;
|
|
30
30
|
ACCESS_TOKEN_SECRET?: string | SecretsStoreSecret;
|
|
31
31
|
REFRESH_TOKEN_SECRET?: string | SecretsStoreSecret;
|
|
32
|
+
SESSION_JWT_SECRET?: string | SecretsStoreSecret;
|
|
32
33
|
PDS_ACCESS_TTL_SEC?: string;
|
|
33
34
|
PDS_REFRESH_TTL_SEC?: string;
|
|
34
35
|
JWT_ALGORITHM?: string;
|
|
35
36
|
REPO_SIGNING_KEY?: string | SecretsStoreSecret;
|
|
36
|
-
|
|
37
|
+
REPO_SIGNING_KEY_PUBLIC?: string | SecretsStoreSecret;
|
|
38
|
+
PDS_PLC_ROTATION_KEY?: string | SecretsStoreSecret;
|
|
37
39
|
PDS_RATE_LIMIT_PER_MIN?: string;
|
|
38
40
|
PDS_MAX_JSON_BYTES?: string;
|
|
39
41
|
PDS_CORS_ORIGIN?: string;
|
|
40
42
|
PDS_SEQ_WINDOW?: string;
|
|
41
43
|
ENVIRONMENT?: string;
|
|
44
|
+
PDS_BSKY_APP_VIEW_URL?: string;
|
|
45
|
+
PDS_BSKY_APP_VIEW_DID?: string;
|
|
46
|
+
PDS_BSKY_APP_VIEW_CDN_URL_PATTERN?: string;
|
|
47
|
+
PDS_SERVICE_SIGNING_KEY_HEX?: string | SecretsStoreSecret;
|
|
48
|
+
// Relay crawl configuration
|
|
49
|
+
PDS_RELAY_HOSTS?: string; // CSV of relay hostnames (no scheme). Default: bsky.network
|
|
50
|
+
PDS_RELAY_NOTIFY?: string; // 'false' to disable auto notify
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
namespace App {
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import type { APIContext } from 'astro';
|
|
2
|
-
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
|
-
import { parseCarFile } from '../../lib/car-reader';
|
|
4
|
-
import { D1Blockstore } from '../../lib/mst';
|
|
5
|
-
import { getDb } from '../../db/client';
|
|
6
|
-
import { repo_root, commit_log } from '../../db/schema';
|
|
7
|
-
import { putRecord } from '../../db/dal';
|
|
8
|
-
import * as dagCbor from '@ipld/dag-cbor';
|
|
9
|
-
import { CID } from 'multiformats/cid';
|
|
10
|
-
|
|
11
|
-
export const prerender = false;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* com.atproto.repo.importRepo
|
|
15
|
-
*
|
|
16
|
-
* Imports a repository from a CAR (Content Addressable aRchive) file.
|
|
17
|
-
* This is used during account migration to transfer the complete repo history.
|
|
18
|
-
*/
|
|
19
|
-
export async function POST({ locals, request }: APIContext) {
|
|
20
|
-
const { env } = locals.runtime;
|
|
21
|
-
|
|
22
|
-
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
const contentType = request.headers.get('content-type');
|
|
26
|
-
if (contentType !== 'application/vnd.ipld.car') {
|
|
27
|
-
return new Response(
|
|
28
|
-
JSON.stringify({
|
|
29
|
-
error: 'InvalidRequest',
|
|
30
|
-
message: 'Content-Type must be application/vnd.ipld.car'
|
|
31
|
-
}),
|
|
32
|
-
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const did = env.PDS_DID ?? 'did:example:single-user';
|
|
37
|
-
const carBytes = new Uint8Array(await request.arrayBuffer());
|
|
38
|
-
|
|
39
|
-
// Parse CAR file
|
|
40
|
-
const { header, blocks } = parseCarFile(carBytes);
|
|
41
|
-
|
|
42
|
-
if (blocks.length === 0) {
|
|
43
|
-
return new Response(
|
|
44
|
-
JSON.stringify({
|
|
45
|
-
error: 'InvalidRequest',
|
|
46
|
-
message: 'CAR file contains no blocks'
|
|
47
|
-
}),
|
|
48
|
-
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Store all blocks in blockstore
|
|
53
|
-
const blockstore = new D1Blockstore(env);
|
|
54
|
-
for (const block of blocks) {
|
|
55
|
-
await blockstore.put(block.cid, block.bytes);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Find the commit block (root of the CAR)
|
|
59
|
-
const rootCid = header.roots[0];
|
|
60
|
-
if (!rootCid) {
|
|
61
|
-
return new Response(
|
|
62
|
-
JSON.stringify({
|
|
63
|
-
error: 'InvalidRequest',
|
|
64
|
-
message: 'CAR file has no root CID'
|
|
65
|
-
}),
|
|
66
|
-
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Decode the commit to get repo details
|
|
71
|
-
const commitBlock = blocks.find(b => b.cid.equals(rootCid));
|
|
72
|
-
if (!commitBlock) {
|
|
73
|
-
return new Response(
|
|
74
|
-
JSON.stringify({
|
|
75
|
-
error: 'InvalidRequest',
|
|
76
|
-
message: 'Root commit block not found in CAR'
|
|
77
|
-
}),
|
|
78
|
-
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const commit = dagCbor.decode(commitBlock.bytes) as any;
|
|
83
|
-
const rev = commit.rev || commit.version || 1;
|
|
84
|
-
|
|
85
|
-
// Update repo root
|
|
86
|
-
const db = getDb(env);
|
|
87
|
-
await db
|
|
88
|
-
.insert(repo_root)
|
|
89
|
-
.values({
|
|
90
|
-
did,
|
|
91
|
-
commitCid: rootCid.toString(),
|
|
92
|
-
rev: typeof rev === 'string' ? parseInt(rev) : rev,
|
|
93
|
-
})
|
|
94
|
-
.onConflictDoUpdate({
|
|
95
|
-
target: repo_root.did,
|
|
96
|
-
set: {
|
|
97
|
-
commitCid: rootCid.toString(),
|
|
98
|
-
rev: typeof rev === 'string' ? parseInt(rev) : rev,
|
|
99
|
-
},
|
|
100
|
-
})
|
|
101
|
-
.run();
|
|
102
|
-
|
|
103
|
-
// Index records from MST
|
|
104
|
-
// Note: This is a simplified implementation
|
|
105
|
-
// A full implementation would walk the MST tree and index all records
|
|
106
|
-
let recordCount = 0;
|
|
107
|
-
for (const block of blocks) {
|
|
108
|
-
try {
|
|
109
|
-
const obj = dagCbor.decode(block.bytes) as any;
|
|
110
|
-
|
|
111
|
-
// Check if this looks like a record (has $type)
|
|
112
|
-
if (obj && typeof obj === 'object' && obj.$type) {
|
|
113
|
-
// This is a record, we should index it
|
|
114
|
-
// For now, we'll skip detailed indexing and let it be done lazily
|
|
115
|
-
recordCount++;
|
|
116
|
-
}
|
|
117
|
-
} catch {
|
|
118
|
-
// Not a valid CBOR object or not a record, skip
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return new Response(
|
|
123
|
-
JSON.stringify({
|
|
124
|
-
did,
|
|
125
|
-
commitCid: rootCid.toString(),
|
|
126
|
-
rev,
|
|
127
|
-
blocksImported: blocks.length,
|
|
128
|
-
recordsFound: recordCount,
|
|
129
|
-
message: 'Repository imported successfully'
|
|
130
|
-
}),
|
|
131
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
132
|
-
);
|
|
133
|
-
} catch (error: any) {
|
|
134
|
-
return new Response(
|
|
135
|
-
JSON.stringify({
|
|
136
|
-
error: 'InternalServerError',
|
|
137
|
-
message: error.message || 'Failed to import repository'
|
|
138
|
-
}),
|
|
139
|
-
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { APIContext } from 'astro';
|
|
2
|
-
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
|
-
import { setAccountActive, getAccountState } from '../../db/dal';
|
|
4
|
-
|
|
5
|
-
export const prerender = false;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* com.atproto.server.activateAccount
|
|
9
|
-
*
|
|
10
|
-
* Activates a deactivated account after successful migration.
|
|
11
|
-
* This enables write operations on the PDS.
|
|
12
|
-
*/
|
|
13
|
-
export async function POST({ locals, request }: APIContext) {
|
|
14
|
-
const { env } = locals.runtime;
|
|
15
|
-
|
|
16
|
-
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const did = env.PDS_DID ?? 'did:example:single-user';
|
|
20
|
-
|
|
21
|
-
// Check if account exists
|
|
22
|
-
const accountState = await getAccountState(env, did);
|
|
23
|
-
if (!accountState) {
|
|
24
|
-
return new Response(
|
|
25
|
-
JSON.stringify({
|
|
26
|
-
error: 'AccountNotFound',
|
|
27
|
-
message: 'Account does not exist'
|
|
28
|
-
}),
|
|
29
|
-
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Activate the account
|
|
34
|
-
await setAccountActive(env, did, true);
|
|
35
|
-
|
|
36
|
-
return new Response(
|
|
37
|
-
JSON.stringify({
|
|
38
|
-
did,
|
|
39
|
-
active: true,
|
|
40
|
-
message: 'Account activated successfully'
|
|
41
|
-
}),
|
|
42
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
43
|
-
);
|
|
44
|
-
} catch (error: any) {
|
|
45
|
-
return new Response(
|
|
46
|
-
JSON.stringify({
|
|
47
|
-
error: 'InternalServerError',
|
|
48
|
-
message: error.message || 'Failed to activate account'
|
|
49
|
-
}),
|
|
50
|
-
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import type { APIContext } from 'astro';
|
|
2
|
-
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
|
-
import { createAccountState, getAccountState } from '../../db/dal';
|
|
4
|
-
|
|
5
|
-
export const prerender = false;
|
|
6
|
-
|
|
7
|
-
function methodNotAllowedResponse(): Response {
|
|
8
|
-
return new Response(null, {
|
|
9
|
-
status: 405,
|
|
10
|
-
headers: {
|
|
11
|
-
Allow: 'POST',
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* com.atproto.server.createAccount
|
|
18
|
-
*
|
|
19
|
-
* Single-user PDS implementation:
|
|
20
|
-
* - Only allows creating account for the configured PDS_DID
|
|
21
|
-
* - Creates account in deactivated state for migration
|
|
22
|
-
* - Optionally validates serviceAuth JWT from old PDS
|
|
23
|
-
*/
|
|
24
|
-
export async function POST({ locals, request }: APIContext) {
|
|
25
|
-
const { env } = locals.runtime;
|
|
26
|
-
|
|
27
|
-
// Require authentication for account creation
|
|
28
|
-
if (!(await isAuthorized(request, env))) {
|
|
29
|
-
console.log(JSON.stringify({ level: 'warn', type: 'createAccount', message: 'Unauthorized', method: request.method, url: request.url }));
|
|
30
|
-
return unauthorized();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const body = await request.json() as { did?: string; handle?: string; deactivated?: boolean };
|
|
35
|
-
const { did, handle, deactivated } = body;
|
|
36
|
-
|
|
37
|
-
// Validate required fields
|
|
38
|
-
if (!did) {
|
|
39
|
-
return new Response(
|
|
40
|
-
JSON.stringify({ error: 'InvalidRequest', message: 'did is required' }),
|
|
41
|
-
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Single-user enforcement: only allow configured DID
|
|
46
|
-
const configuredDid = env.PDS_DID;
|
|
47
|
-
if (did !== configuredDid) {
|
|
48
|
-
return new Response(
|
|
49
|
-
JSON.stringify({
|
|
50
|
-
error: 'InvalidRequest',
|
|
51
|
-
message: `This is a single-user PDS. Only ${configuredDid} is allowed.`
|
|
52
|
-
}),
|
|
53
|
-
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Check if account already exists
|
|
58
|
-
const existing = await getAccountState(env, did);
|
|
59
|
-
if (existing) {
|
|
60
|
-
return new Response(
|
|
61
|
-
JSON.stringify({
|
|
62
|
-
error: 'AccountAlreadyExists',
|
|
63
|
-
message: 'Account already exists for this DID'
|
|
64
|
-
}),
|
|
65
|
-
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Create account in deactivated state (for migration)
|
|
70
|
-
const active = deactivated === true ? false : true;
|
|
71
|
-
await createAccountState(env, did, active);
|
|
72
|
-
|
|
73
|
-
return new Response(
|
|
74
|
-
JSON.stringify({
|
|
75
|
-
did,
|
|
76
|
-
handle: handle || env.PDS_HANDLE,
|
|
77
|
-
active,
|
|
78
|
-
createdAt: new Date().toISOString()
|
|
79
|
-
}),
|
|
80
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
81
|
-
);
|
|
82
|
-
} catch (error: any) {
|
|
83
|
-
return new Response(
|
|
84
|
-
JSON.stringify({
|
|
85
|
-
error: 'InternalServerError',
|
|
86
|
-
message: error.message || 'Failed to create account'
|
|
87
|
-
}),
|
|
88
|
-
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function HEAD(): Promise<Response> {
|
|
94
|
-
return methodNotAllowedResponse();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export async function GET(): Promise<Response> {
|
|
98
|
-
return methodNotAllowedResponse();
|
|
99
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { APIContext } from 'astro';
|
|
2
|
-
import { isAuthorized, unauthorized } from '../../lib/auth';
|
|
3
|
-
import { setAccountActive, getAccountState } from '../../db/dal';
|
|
4
|
-
|
|
5
|
-
export const prerender = false;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* com.atproto.server.deactivateAccount
|
|
9
|
-
*
|
|
10
|
-
* Deactivates an account, preventing write operations.
|
|
11
|
-
* Used when migrating away from this PDS.
|
|
12
|
-
*/
|
|
13
|
-
export async function POST({ locals, request }: APIContext) {
|
|
14
|
-
const { env } = locals.runtime;
|
|
15
|
-
|
|
16
|
-
if (!(await isAuthorized(request, env))) return unauthorized();
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const did = env.PDS_DID ?? 'did:example:single-user';
|
|
20
|
-
|
|
21
|
-
// Check if account exists
|
|
22
|
-
const accountState = await getAccountState(env, did);
|
|
23
|
-
if (!accountState) {
|
|
24
|
-
return new Response(
|
|
25
|
-
JSON.stringify({
|
|
26
|
-
error: 'AccountNotFound',
|
|
27
|
-
message: 'Account does not exist'
|
|
28
|
-
}),
|
|
29
|
-
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Deactivate the account
|
|
34
|
-
await setAccountActive(env, did, false);
|
|
35
|
-
|
|
36
|
-
return new Response(
|
|
37
|
-
JSON.stringify({
|
|
38
|
-
did,
|
|
39
|
-
active: false,
|
|
40
|
-
message: 'Account deactivated successfully'
|
|
41
|
-
}),
|
|
42
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
43
|
-
);
|
|
44
|
-
} catch (error: any) {
|
|
45
|
-
return new Response(
|
|
46
|
-
JSON.stringify({
|
|
47
|
-
error: 'InternalServerError',
|
|
48
|
-
message: error.message || 'Failed to deactivate account'
|
|
49
|
-
}),
|
|
50
|
-
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
}
|