@alteran/astro 0.1.7 → 0.1.8

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.
@@ -0,0 +1,79 @@
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
+ /**
8
+ * com.atproto.server.createAccount
9
+ *
10
+ * Single-user PDS implementation:
11
+ * - Only allows creating account for the configured PDS_DID
12
+ * - Creates account in deactivated state for migration
13
+ * - Optionally validates serviceAuth JWT from old PDS
14
+ */
15
+ export async function POST({ locals, request }: APIContext) {
16
+ const { env } = locals.runtime;
17
+
18
+ // Require authentication for account creation
19
+ if (!(await isAuthorized(request, env))) return unauthorized();
20
+
21
+ try {
22
+ const body = await request.json() as { did?: string; handle?: string; deactivated?: boolean };
23
+ const { did, handle, deactivated } = body;
24
+
25
+ // Validate required fields
26
+ if (!did) {
27
+ return new Response(
28
+ JSON.stringify({ error: 'InvalidRequest', message: 'did is required' }),
29
+ { status: 400, headers: { 'Content-Type': 'application/json' } }
30
+ );
31
+ }
32
+
33
+ // Single-user enforcement: only allow configured DID
34
+ const configuredDid = env.PDS_DID;
35
+ if (did !== configuredDid) {
36
+ return new Response(
37
+ JSON.stringify({
38
+ error: 'InvalidRequest',
39
+ message: `This is a single-user PDS. Only ${configuredDid} is allowed.`
40
+ }),
41
+ { status: 400, headers: { 'Content-Type': 'application/json' } }
42
+ );
43
+ }
44
+
45
+ // Check if account already exists
46
+ const existing = await getAccountState(env, did);
47
+ if (existing) {
48
+ return new Response(
49
+ JSON.stringify({
50
+ error: 'AccountAlreadyExists',
51
+ message: 'Account already exists for this DID'
52
+ }),
53
+ { status: 400, headers: { 'Content-Type': 'application/json' } }
54
+ );
55
+ }
56
+
57
+ // Create account in deactivated state (for migration)
58
+ const active = deactivated === true ? false : true;
59
+ await createAccountState(env, did, active);
60
+
61
+ return new Response(
62
+ JSON.stringify({
63
+ did,
64
+ handle: handle || env.PDS_HANDLE,
65
+ active,
66
+ createdAt: new Date().toISOString()
67
+ }),
68
+ { status: 200, headers: { 'Content-Type': 'application/json' } }
69
+ );
70
+ } catch (error: any) {
71
+ return new Response(
72
+ JSON.stringify({
73
+ error: 'InternalServerError',
74
+ message: error.message || 'Failed to create account'
75
+ }),
76
+ { status: 500, headers: { 'Content-Type': 'application/json' } }
77
+ );
78
+ }
79
+ }
@@ -0,0 +1,53 @@
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
+ }
@@ -0,0 +1,84 @@
1
+ import type { APIContext } from 'astro';
2
+ import { getDb } from '../../db/client';
3
+ import { blob_ref } from '../../db/schema';
4
+ import { eq } from 'drizzle-orm';
5
+
6
+ export const prerender = false;
7
+
8
+ /**
9
+ * com.atproto.sync.getBlob
10
+ *
11
+ * Serves a blob by its CID. Used during migration to transfer blobs
12
+ * from the old PDS to the new PDS.
13
+ *
14
+ * Query params:
15
+ * - did: The DID of the account (optional, defaults to configured PDS_DID)
16
+ * - cid: The CID of the blob to retrieve (required)
17
+ */
18
+ export async function GET({ locals, url }: APIContext) {
19
+ const { env } = locals.runtime;
20
+
21
+ try {
22
+ const cid = url.searchParams.get('cid');
23
+ if (!cid) {
24
+ return new Response(
25
+ JSON.stringify({
26
+ error: 'InvalidRequest',
27
+ message: 'cid parameter is required'
28
+ }),
29
+ { status: 400, headers: { 'Content-Type': 'application/json' } }
30
+ );
31
+ }
32
+
33
+ const db = getDb(env);
34
+
35
+ // Look up blob metadata by CID
36
+ const blobMeta = await db
37
+ .select()
38
+ .from(blob_ref)
39
+ .where(eq(blob_ref.cid, cid))
40
+ .get();
41
+
42
+ if (!blobMeta) {
43
+ return new Response(
44
+ JSON.stringify({
45
+ error: 'BlobNotFound',
46
+ message: 'Blob not found'
47
+ }),
48
+ { status: 404, headers: { 'Content-Type': 'application/json' } }
49
+ );
50
+ }
51
+
52
+ // Fetch blob from R2
53
+ const r2 = env.BLOBS;
54
+ const object = await r2.get(blobMeta.key);
55
+
56
+ if (!object) {
57
+ return new Response(
58
+ JSON.stringify({
59
+ error: 'BlobNotFound',
60
+ message: 'Blob not found in storage'
61
+ }),
62
+ { status: 404, headers: { 'Content-Type': 'application/json' } }
63
+ );
64
+ }
65
+
66
+ // Stream the blob with appropriate content type
67
+ return new Response(object.body as any, {
68
+ status: 200,
69
+ headers: {
70
+ 'Content-Type': blobMeta.mime,
71
+ 'Content-Length': blobMeta.size.toString(),
72
+ 'Cache-Control': 'public, max-age=31536000, immutable',
73
+ },
74
+ });
75
+ } catch (error: any) {
76
+ return new Response(
77
+ JSON.stringify({
78
+ error: 'InternalServerError',
79
+ message: error.message || 'Failed to retrieve blob'
80
+ }),
81
+ { status: 500, headers: { 'Content-Type': 'application/json' } }
82
+ );
83
+ }
84
+ }
@@ -1,5 +1,6 @@
1
1
  import { seed } from '../db/seed';
2
2
  import { validateConfigOrThrow } from '../lib/config';
3
+ import { resolveEnvSecrets } from '../lib/secrets';
3
4
  import type { Env } from '../env';
4
5
  import type { SSRManifest } from 'astro';
5
6
  import type {
@@ -28,8 +29,12 @@ export interface CreatePdsFetchHandlerOptions {
28
29
  */
29
30
  export function createPdsFetchHandler(options?: CreatePdsFetchHandlerOptions): PdsFetchHandler {
30
31
  return async function fetch(request: WorkersRequest, env: Env, ctx: ExecutionContext) {
32
+ // Resolve any Secret Store bindings to strings so downstream code can
33
+ // treat secrets uniformly regardless of source (Secret or Secret Store).
34
+ const resolvedEnv = await resolveEnvSecrets(env);
35
+
31
36
  try {
32
- validateConfigOrThrow(env);
37
+ validateConfigOrThrow(resolvedEnv);
33
38
  } catch (error) {
34
39
  return new Response(
35
40
  JSON.stringify({
@@ -43,7 +48,7 @@ export function createPdsFetchHandler(options?: CreatePdsFetchHandlerOptions): P
43
48
  ) as unknown as WorkersResponse;
44
49
  }
45
50
 
46
- await seed(env.DB, env.PDS_DID ?? 'did:example:single-user');
51
+ await seed(resolvedEnv.DB, (resolvedEnv.PDS_DID as string | undefined) ?? 'did:example:single-user');
47
52
 
48
53
  const url = new URL(request.url);
49
54
  if (url.pathname === '/xrpc/com.atproto.sync.subscribeRepos') {
@@ -51,17 +56,17 @@ export function createPdsFetchHandler(options?: CreatePdsFetchHandlerOptions): P
51
56
  if (upgrade !== 'websocket') {
52
57
  return new Response('Expected websocket', { status: 426 }) as unknown as WorkersResponse;
53
58
  }
54
- if (!env.SEQUENCER) {
59
+ if (!resolvedEnv.SEQUENCER) {
55
60
  return new Response('Sequencer not configured', { status: 503 }) as unknown as WorkersResponse;
56
61
  }
57
62
 
58
- const id = env.SEQUENCER.idFromName('default');
59
- const stub = env.SEQUENCER.get(id);
63
+ const id = resolvedEnv.SEQUENCER.idFromName('default');
64
+ const stub = resolvedEnv.SEQUENCER.get(id);
60
65
  return (await stub.fetch(request as any)) as unknown as WorkersResponse;
61
66
  }
62
67
 
63
68
  const astroFetch = await getAstroFetch(options);
64
- const response = await astroFetch(request, env as any, ctx);
69
+ const response = await astroFetch(request, resolvedEnv as any, ctx);
65
70
  return response as unknown as WorkersResponse;
66
71
  };
67
72
  }
package/types/env.d.ts CHANGED
@@ -7,24 +7,34 @@ import type {
7
7
  R2Bucket,
8
8
  } from '@cloudflare/workers-types';
9
9
 
10
+ // Minimal Secret Store binding interface. Cloudflare exposes each bound secret
11
+ // as an object with an async `get()` that returns the secret value.
12
+ // If @cloudflare/workers-types defines this already, our local type is compatible.
13
+ export interface SecretsStoreSecret {
14
+ get(): Promise<string>;
15
+ }
16
+
10
17
  declare global {
11
18
  interface Env {
12
19
  DB: D1Database;
13
20
  BLOBS: R2Bucket;
14
21
  SEQUENCER?: DurableObjectNamespace;
15
- PDS_HANDLE?: string;
16
- PDS_DID?: string;
22
+ // Secrets can be provided either as Wrangler Secrets (string)
23
+ // or via Secret Store bindings (SecretsStoreSecret).
24
+ PDS_HANDLE?: string | SecretsStoreSecret;
25
+ PDS_DID?: string | SecretsStoreSecret;
17
26
  PDS_HOSTNAME?: string;
18
- USER_PASSWORD?: string;
27
+ USER_PASSWORD?: string | SecretsStoreSecret;
19
28
  PDS_MAX_BLOB_SIZE?: string;
20
- ACCESS_TOKEN_SECRET?: string;
21
- REFRESH_TOKEN_SECRET?: string;
29
+ ACCESS_TOKEN_SECRET?: string | SecretsStoreSecret;
30
+ REFRESH_TOKEN_SECRET?: string | SecretsStoreSecret;
22
31
  PDS_ACCESS_TTL_SEC?: string;
23
32
  PDS_REFRESH_TTL_SEC?: string;
24
33
  JWT_ALGORITHM?: string;
25
- JWT_ED25519_PRIVATE_KEY?: string;
26
- JWT_ED25519_PUBLIC_KEY?: string;
27
- REPO_SIGNING_KEY?: string;
34
+ JWT_ED25519_PRIVATE_KEY?: string | SecretsStoreSecret;
35
+ JWT_ED25519_PUBLIC_KEY?: string | SecretsStoreSecret;
36
+ REPO_SIGNING_KEY?: string | SecretsStoreSecret;
37
+ REPO_SIGNING_PUBLIC_KEY?: string | SecretsStoreSecret;
28
38
  PDS_RATE_LIMIT_PER_MIN?: string;
29
39
  PDS_MAX_JSON_BYTES?: string;
30
40
  PDS_CORS_ORIGIN?: string;