@alteran/astro 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,7 +94,7 @@ Auth (JWT)
94
94
  - Use `Authorization: Bearer <accessJwt>` on write routes.
95
95
  - Secrets to set (Wrangler secrets or local bindings):
96
96
  - `USER_PASSWORD` (dev login password)
97
- - `ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET` (HMAC keys)
97
+ - `ACCESS_TOKEN`, `REFRESH_TOKEN` (HMAC keys)
98
98
  - `PDS_DID`, `PDS_HANDLE`
99
99
 
100
100
  Rate limiting & limits
@@ -179,8 +179,8 @@ Set these secrets for each environment using `wrangler secret put <NAME> --env <
179
179
  | `PDS_DID` | Your DID identifier | `did:plc:abc123` or `did:web:example.com` |
180
180
  | `PDS_HANDLE` | Your handle | `user.bsky.social` |
181
181
  | `USER_PASSWORD` | Login password | Strong password |
182
- | `ACCESS_TOKEN_SECRET` | JWT access token secret | Random 32+ char string |
183
- | `REFRESH_TOKEN_SECRET` | JWT refresh token secret | Random 32+ char string |
182
+ | `ACCESS_TOKEN` | JWT access token secret | Random 32+ char string |
183
+ | `REFRESH_TOKEN` | JWT refresh token secret | Random 32+ char string |
184
184
  | `REPO_SIGNING_KEY` | Ed25519 signing key (base64) | From `generate-signing-key.ts` |
185
185
 
186
186
  **Generate secrets:**
@@ -196,8 +196,8 @@ bun run scripts/generate-signing-key.ts
196
196
  wrangler secret put PDS_DID --env production
197
197
  wrangler secret put PDS_HANDLE --env production
198
198
  wrangler secret put USER_PASSWORD --env production
199
- wrangler secret put ACCESS_TOKEN_SECRET --env production
200
- wrangler secret put REFRESH_TOKEN_SECRET --env production
199
+ wrangler secret put ACCESS_TOKEN --env production
200
+ wrangler secret put REFRESH_TOKEN --env production
201
201
  wrangler secret put REPO_SIGNING_KEY --env production
202
202
  # Optional: publish public key for DID document
203
203
  wrangler secret put REPO_SIGNING_KEY_PUBLIC --env production
@@ -212,8 +212,8 @@ Instead of Wrangler Secrets, you may bind secrets from Cloudflare Secret Store.
212
212
  // ...
213
213
  "secrets_store_secrets": [
214
214
  { "binding": "USER_PASSWORD", "secret_name": "user_password", "store_id": "<your-store-id>" },
215
- { "binding": "ACCESS_TOKEN_SECRET", "secret_name": "access_token_secret", "store_id": "<your-store-id>" },
216
- { "binding": "REFRESH_TOKEN_SECRET", "secret_name": "refresh_token_secret", "store_id": "<your-store-id>" },
215
+ { "binding": "ACCESS_TOKEN", "secret_name": "access_token", "store_id": "<your-store-id>" },
216
+ { "binding": "REFRESH_TOKEN", "secret_name": "refresh_token", "store_id": "<your-store-id>" },
217
217
  { "binding": "PDS_DID", "secret_name": "pds_did", "store_id": "<your-store-id>" },
218
218
  { "binding": "PDS_HANDLE", "secret_name": "pds_handle", "store_id": "<your-store-id>" }
219
219
  ]
@@ -312,7 +312,7 @@ Blob storage
312
312
  Secrets & config (Wrangler)
313
313
  - Required:
314
314
  - `PDS_DID`, `PDS_HANDLE`, `USER_PASSWORD`
315
- - `ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`
315
+ - `ACCESS_TOKEN`, `REFRESH_TOKEN`
316
316
  - Optional:
317
317
  - `PDS_ALLOWED_MIME`, `PDS_MAX_BLOB_SIZE`, `PDS_MAX_JSON_BYTES`, `PDS_RATE_LIMIT_PER_MIN`, `PDS_CORS_ORIGIN`
318
318
  - Durable Objects: ensure binding for `Sequencer` exists and migration tag added (see `wrangler.jsonc`).
@@ -370,7 +370,7 @@ wrangler secret put REPO_SIGNING_KEY # From step 1
370
370
  wrangler secret put PDS_DID # Your DID
371
371
  wrangler secret put PDS_HANDLE # Your handle
372
372
  wrangler secret put USER_PASSWORD # Login password
373
- wrangler secret put ACCESS_TOKEN_SECRET
373
+ wrangler secret put REFRESH_TOKEN
374
374
  wrangler secret put REFRESH_TOKEN_SECRET
375
375
  # Optional: publish raw public key for DID document
376
376
  wrangler secret put REPO_SIGNING_KEY_PUBLIC
@@ -384,7 +384,7 @@ REPO_SIGNING_KEY=<base64-key-from-step-1>
384
384
  # Optional: publish raw 32-byte public key in did.json
385
385
  REPO_SIGNING_KEY_PUBLIC=<base64-raw-public-key>
386
386
  USER_PASSWORD=your-password
387
- ACCESS_TOKEN_SECRET=your-access-secret
387
+ REFRESH_TOKEN=your-access-secret
388
388
  REFRESH_TOKEN_SECRET=your-refresh-secret
389
389
  PDS_SEQ_WINDOW=512
390
390
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alteran/astro",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Astro integration for running a Cloudflare-hosted Bluesky PDS with Alteran.",
5
5
  "module": "index.js",
6
6
  "types": "index.d.ts",
@@ -39,7 +39,8 @@
39
39
  "db:apply:local:direct": "wrangler d1 migrations apply pds --local",
40
40
  "db:reset:local": "rm -rf .wrangler/state && rm -rf drizzle && bun run db:generate && bun run db:apply:local",
41
41
  "secrets:setup": "bun run scripts/setup-secrets.ts",
42
- "relay:request-crawl": "bun run scripts/request-crawl.ts"
42
+ "relay:request-crawl": "bun run scripts/request-crawl.ts",
43
+ "pds:test-create-session": "bun run scripts/test-create-session.ts"
43
44
  },
44
45
  "devDependencies": {
45
46
  "@astrojs/cloudflare": "^12.6.9",
package/src/lib/jwt.ts CHANGED
@@ -1,7 +1,11 @@
1
- import type { Env } from '../env';
2
- import { getRuntimeString } from './secrets';
3
- import { base58btc } from 'multiformats/bases/base58';
4
- import { issueSessionTokens, verifyAccessToken, verifyRefreshToken } from './session-tokens';
1
+ import type { Env } from "../env";
2
+ import { getRuntimeString } from "./secrets";
3
+ import { base58btc } from "multiformats/bases/base58";
4
+ import {
5
+ issueSessionTokens,
6
+ verifyAccessToken,
7
+ verifyRefreshToken,
8
+ } from "./session-tokens";
5
9
 
6
10
  export interface JwtClaims {
7
11
  sub: string; // DID
@@ -9,26 +13,35 @@ export interface JwtClaims {
9
13
  scope?: string;
10
14
  aud?: string;
11
15
  jti?: string;
12
- t: 'access' | 'refresh';
16
+ t: "access" | "refresh";
13
17
  }
14
18
 
15
19
  // JWT
16
- export async function signJwt(env: Env, claims: JwtClaims, kind: 'access' | 'refresh'): Promise<string> {
20
+ export async function signJwt(
21
+ env: Env,
22
+ claims: JwtClaims,
23
+ kind: "access" | "refresh",
24
+ ): Promise<string> {
17
25
  if (!claims.sub) {
18
- throw new Error('Cannot sign JWT without subject');
26
+ throw new Error("Cannot sign JWT without subject");
19
27
  }
20
28
  const { accessJwt, refreshJwt } = await issueSessionTokens(env, claims.sub, {
21
29
  jti: claims.jti,
22
30
  });
23
- return kind === 'access' ? accessJwt : refreshJwt;
31
+ return kind === "access" ? accessJwt : refreshJwt;
24
32
  }
25
33
 
26
- export async function verifyJwt(env: Env, token: string): Promise<{ valid: boolean; payload: JwtClaims } | null> {
27
- const parts = token.split('.');
34
+ export async function verifyJwt(
35
+ env: Env,
36
+ token: string,
37
+ ): Promise<{ valid: boolean; payload: JwtClaims } | null> {
38
+ const parts = token.split(".");
28
39
  if (parts.length !== 3) return null;
29
- const header = JSON.parse(atob(parts[0].replace(/-/g, '+').replace(/_/g, '/')));
40
+ const header = JSON.parse(
41
+ atob(parts[0].replace(/-/g, "+").replace(/_/g, "/")),
42
+ );
30
43
 
31
- if (header.typ === 'at+jwt') {
44
+ if (header.typ === "at+jwt") {
32
45
  const payload = await verifyAccessToken(env, token).catch(() => null);
33
46
  if (!payload) return null;
34
47
  if (!payload.sub) return null;
@@ -37,7 +50,7 @@ export async function verifyJwt(env: Env, token: string): Promise<{ valid: boole
37
50
  aud: payload.aud as string | undefined,
38
51
  scope: payload.scope as string | undefined,
39
52
  jti: payload.jti as string | undefined,
40
- t: 'access',
53
+ t: "access",
41
54
  };
42
55
  if (payload.handle) {
43
56
  claims.handle = String(payload.handle);
@@ -45,7 +58,7 @@ export async function verifyJwt(env: Env, token: string): Promise<{ valid: boole
45
58
  return { valid: true, payload: claims };
46
59
  }
47
60
 
48
- if (header.typ === 'refresh+jwt') {
61
+ if (header.typ === "refresh+jwt") {
49
62
  const verified = await verifyRefreshToken(env, token).catch(() => null);
50
63
  if (!verified) return null;
51
64
  if (!verified.payload.sub) return null;
@@ -54,24 +67,26 @@ export async function verifyJwt(env: Env, token: string): Promise<{ valid: boole
54
67
  aud: verified.payload.aud as string | undefined,
55
68
  scope: verified.payload.scope as string | undefined,
56
69
  jti: verified.payload.jti as string | undefined,
57
- t: 'refresh',
70
+ t: "refresh",
58
71
  };
59
72
  return { valid: true, payload };
60
73
  }
61
74
 
62
- const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
75
+ const payload = JSON.parse(
76
+ atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")),
77
+ );
63
78
 
64
79
  let ok = false;
65
- if (header.alg === 'HS256' && header.typ === 'JWT') {
80
+ if (header.alg === "HS256" && header.typ === "JWT") {
66
81
  const secret = await getRuntimeString(
67
82
  env,
68
- payload.t === 'refresh' ? 'REFRESH_TOKEN_SECRET' : 'ACCESS_TOKEN_SECRET',
69
- payload.t === 'refresh' ? 'dev-refresh' : 'dev-access'
83
+ payload.t === "refresh" ? "REFRESH_TOKEN_SECRET" : "REFRESH_TOKEN",
84
+ payload.t === "refresh" ? "dev-refresh" : "dev-access",
70
85
  );
71
86
  if (!secret) return null;
72
- ok = await hmacJwtVerify(parts[0] + '.' + parts[1], parts[2], secret);
73
- } else if (header.alg === 'EdDSA' && header.typ === 'JWT') {
74
- ok = await eddsaJwtVerify(parts[0] + '.' + parts[1], parts[2], env);
87
+ ok = await hmacJwtVerify(parts[0] + "." + parts[1], parts[2], secret);
88
+ } else if (header.alg === "EdDSA" && header.typ === "JWT") {
89
+ ok = await eddsaJwtVerify(parts[0] + "." + parts[1], parts[2], env);
75
90
  } else {
76
91
  return null;
77
92
  }
@@ -83,113 +98,152 @@ export async function verifyJwt(env: Env, token: string): Promise<{ valid: boole
83
98
 
84
99
  async function hmacJwtSign(payload: any, secret: string): Promise<string> {
85
100
  const enc = new TextEncoder();
86
- const header = { alg: 'HS256', typ: 'JWT' };
101
+ const header = { alg: "HS256", typ: "JWT" };
87
102
  const h = b64url(enc.encode(JSON.stringify(header)));
88
103
  const p = b64url(enc.encode(JSON.stringify(payload)));
89
104
  const data = `${h}.${p}`;
90
- const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
91
- const sig = await crypto.subtle.sign('HMAC', key, enc.encode(data));
105
+ const key = await crypto.subtle.importKey(
106
+ "raw",
107
+ enc.encode(secret),
108
+ { name: "HMAC", hash: "SHA-256" },
109
+ false,
110
+ ["sign"],
111
+ );
112
+ const sig = await crypto.subtle.sign("HMAC", key, enc.encode(data));
92
113
  const s = b64url(new Uint8Array(sig));
93
114
  return `${h}.${p}.${s}`;
94
115
  }
95
116
 
96
- async function hmacJwtVerify(data: string, sigB64: string, secret: string): Promise<boolean> {
117
+ async function hmacJwtVerify(
118
+ data: string,
119
+ sigB64: string,
120
+ secret: string,
121
+ ): Promise<boolean> {
97
122
  const enc = new TextEncoder();
98
- const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
99
- const ok = await crypto.subtle.verify('HMAC', key, b64urlDecode(sigB64), enc.encode(data));
123
+ const key = await crypto.subtle.importKey(
124
+ "raw",
125
+ enc.encode(secret),
126
+ { name: "HMAC", hash: "SHA-256" },
127
+ false,
128
+ ["verify"],
129
+ );
130
+ const ok = await crypto.subtle.verify(
131
+ "HMAC",
132
+ key,
133
+ b64urlDecode(sigB64),
134
+ enc.encode(data),
135
+ );
100
136
  return !!ok;
101
137
  }
102
138
 
103
139
  async function eddsaJwtSign(payload: any, env: Env): Promise<string> {
104
140
  const enc = new TextEncoder();
105
- const header = { alg: 'EdDSA', typ: 'JWT' };
141
+ const header = { alg: "EdDSA", typ: "JWT" };
106
142
  const h = b64url(enc.encode(JSON.stringify(header)));
107
143
  const p = b64url(enc.encode(JSON.stringify(payload)));
108
144
  const data = `${h}.${p}`;
109
145
 
110
146
  // Import Ed25519 private key from env
111
- const keyData = await getRuntimeString(env, 'REPO_SIGNING_KEY');
147
+ const keyData = await getRuntimeString(env, "REPO_SIGNING_KEY");
112
148
  if (!keyData) {
113
- throw new Error('REPO_SIGNING_KEY not configured for EdDSA JWTs');
149
+ throw new Error("REPO_SIGNING_KEY not configured for EdDSA JWTs");
114
150
  }
115
151
 
116
152
  // Decode base64 private key
117
153
  const keyBytes = b64urlDecode(keyData);
118
154
  const key = await crypto.subtle.importKey(
119
- 'pkcs8',
155
+ "pkcs8",
120
156
  keyBytes,
121
- { name: 'Ed25519' } as any,
157
+ { name: "Ed25519" } as any,
122
158
  false,
123
- ['sign']
159
+ ["sign"],
124
160
  );
125
161
 
126
- const sig = await crypto.subtle.sign('Ed25519', key, enc.encode(data));
162
+ const sig = await crypto.subtle.sign("Ed25519", key, enc.encode(data));
127
163
  const s = b64url(new Uint8Array(sig));
128
164
  return `${h}.${p}.${s}`;
129
165
  }
130
166
 
131
- async function eddsaJwtVerify(data: string, sigB64: string, env: Env): Promise<boolean> {
167
+ async function eddsaJwtVerify(
168
+ data: string,
169
+ sigB64: string,
170
+ env: Env,
171
+ ): Promise<boolean> {
132
172
  const enc = new TextEncoder();
133
173
 
134
174
  // Import Ed25519 public key from env
135
- const keyData = await getRuntimeString(env, 'REPO_SIGNING_KEY_PUBLIC');
175
+ const keyData = await getRuntimeString(env, "REPO_SIGNING_KEY_PUBLIC");
136
176
  if (!keyData) {
137
- console.error('EdDSA JWT verification failed: REPO_SIGNING_KEY_PUBLIC not configured');
177
+ console.error(
178
+ "EdDSA JWT verification failed: REPO_SIGNING_KEY_PUBLIC not configured",
179
+ );
138
180
  return false;
139
181
  }
140
182
 
141
183
  try {
142
184
  const key = await importEd25519PublicKey(keyData);
143
185
  if (!key) {
144
- console.error('EdDSA JWT verification failed: unsupported public key format for Ed25519');
186
+ console.error(
187
+ "EdDSA JWT verification failed: unsupported public key format for Ed25519",
188
+ );
145
189
  return false;
146
190
  }
147
191
 
148
- const ok = await crypto.subtle.verify('Ed25519', key, b64urlDecode(sigB64), enc.encode(data));
192
+ const ok = await crypto.subtle.verify(
193
+ "Ed25519",
194
+ key,
195
+ b64urlDecode(sigB64),
196
+ enc.encode(data),
197
+ );
149
198
  return !!ok;
150
199
  } catch (error) {
151
- console.error('EdDSA JWT verification error:', error);
200
+ console.error("EdDSA JWT verification error:", error);
152
201
  return false;
153
202
  }
154
203
  }
155
204
 
156
205
  function b64url(bytes: ArrayBuffer | Uint8Array): string {
157
206
  const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
158
- let s = '';
207
+ let s = "";
159
208
  for (let i = 0; i < b.length; i++) {
160
209
  s += String.fromCharCode(b[i]);
161
210
  }
162
- return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
211
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
163
212
  }
164
213
 
165
214
  function b64urlDecode(s: string): Uint8Array {
166
- const pad = s.length % 4 === 2 ? '==' : s.length % 4 === 3 ? '=' : '';
167
- const bin = atob(s.replace(/-/g, '+').replace(/_/g, '/') + pad);
215
+ const pad = s.length % 4 === 2 ? "==" : s.length % 4 === 3 ? "=" : "";
216
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + pad);
168
217
  const out = new Uint8Array(bin.length);
169
218
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
170
219
  return out;
171
220
  }
172
221
 
173
- async function importEd25519PublicKey(value: string): Promise<CryptoKey | null> {
222
+ async function importEd25519PublicKey(
223
+ value: string,
224
+ ): Promise<CryptoKey | null> {
174
225
  const attempts = buildPublicKeyCandidates(value);
175
226
  for (const attempt of attempts) {
176
227
  try {
177
228
  return await crypto.subtle.importKey(
178
229
  attempt.format,
179
230
  attempt.data,
180
- { name: 'Ed25519', namedCurve: 'Ed25519' } as any,
231
+ { name: "Ed25519", namedCurve: "Ed25519" } as any,
181
232
  false,
182
- ['verify']
233
+ ["verify"],
183
234
  );
184
235
  } catch (error) {
185
- console.warn('EdDSA JWT verification warning: failed to import key candidate', error);
236
+ console.warn(
237
+ "EdDSA JWT verification warning: failed to import key candidate",
238
+ error,
239
+ );
186
240
  }
187
241
  }
188
242
  return null;
189
243
  }
190
244
 
191
245
  type KeyImportAttempt = { format: KeyFormat; data: Uint8Array };
192
- type KeyFormat = 'raw' | 'spki';
246
+ type KeyFormat = "raw" | "spki";
193
247
 
194
248
  function buildPublicKeyCandidates(value: string): KeyImportAttempt[] {
195
249
  const trimmed = value.trim();
@@ -197,24 +251,26 @@ function buildPublicKeyCandidates(value: string): KeyImportAttempt[] {
197
251
 
198
252
  const didKeyCandidate = decodeDidKey(trimmed);
199
253
  if (didKeyCandidate) {
200
- attempts.push({ format: 'raw', data: didKeyCandidate });
254
+ attempts.push({ format: "raw", data: didKeyCandidate });
201
255
  }
202
256
 
203
- const pemMatch = trimmed.match(/-----BEGIN PUBLIC KEY-----([\s\S]+?)-----END PUBLIC KEY-----/);
257
+ const pemMatch = trimmed.match(
258
+ /-----BEGIN PUBLIC KEY-----([\s\S]+?)-----END PUBLIC KEY-----/,
259
+ );
204
260
  if (pemMatch) {
205
- const derBytes = decodeBase64(pemMatch[1].replace(/\s+/g, ''));
261
+ const derBytes = decodeBase64(pemMatch[1].replace(/\s+/g, ""));
206
262
  if (derBytes) {
207
- attempts.push({ format: 'spki', data: derBytes });
263
+ attempts.push({ format: "spki", data: derBytes });
208
264
  }
209
265
  }
210
266
 
211
- const compact = trimmed.replace(/\s+/g, '');
267
+ const compact = trimmed.replace(/\s+/g, "");
212
268
  const decoded = decodeBase64(compact);
213
269
  if (decoded) {
214
270
  if (decoded.length === 32) {
215
- attempts.push({ format: 'raw', data: decoded });
271
+ attempts.push({ format: "raw", data: decoded });
216
272
  } else {
217
- attempts.push({ format: 'spki', data: decoded });
273
+ attempts.push({ format: "spki", data: decoded });
218
274
  }
219
275
  }
220
276
 
@@ -222,21 +278,21 @@ function buildPublicKeyCandidates(value: string): KeyImportAttempt[] {
222
278
  }
223
279
 
224
280
  function decodeBase64(value: string): Uint8Array | null {
225
- const cleaned = value.replace(/\s+/g, '');
281
+ const cleaned = value.replace(/\s+/g, "");
226
282
  if (!cleaned) return null;
227
283
  try {
228
284
  return b64urlDecode(cleaned);
229
285
  } catch {
230
- const normalized = cleaned.replace(/-/g, '+').replace(/_/g, '/');
286
+ const normalized = cleaned.replace(/-/g, "+").replace(/_/g, "/");
231
287
  const padLength = normalized.length % 4;
232
288
  const padded =
233
289
  padLength === 0
234
290
  ? normalized
235
291
  : padLength === 2
236
- ? normalized + '=='
292
+ ? normalized + "=="
237
293
  : padLength === 3
238
- ? normalized + '='
239
- : normalized + '===';
294
+ ? normalized + "="
295
+ : normalized + "===";
240
296
  const bin = atob(padded);
241
297
  const out = new Uint8Array(bin.length);
242
298
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
@@ -245,16 +301,21 @@ function decodeBase64(value: string): Uint8Array | null {
245
301
  }
246
302
 
247
303
  function decodeDidKey(didKey: string): Uint8Array | null {
248
- if (!didKey.startsWith('did:key:')) return null;
304
+ if (!didKey.startsWith("did:key:")) return null;
249
305
  try {
250
- const multibase = didKey.slice('did:key:'.length);
306
+ const multibase = didKey.slice("did:key:".length);
251
307
  const bytes = base58btc.decode(multibase);
252
308
  if (bytes.length === 34 && bytes[0] === 0xed && bytes[1] === 0x01) {
253
309
  return bytes.slice(2);
254
310
  }
255
- console.warn('EdDSA JWT verification warning: unsupported did:key multicodec prefix');
311
+ console.warn(
312
+ "EdDSA JWT verification warning: unsupported did:key multicodec prefix",
313
+ );
256
314
  } catch (error) {
257
- console.warn('EdDSA JWT verification warning: failed to parse did:key', error);
315
+ console.warn(
316
+ "EdDSA JWT verification warning: failed to parse did:key",
317
+ error,
318
+ );
258
319
  }
259
320
  return null;
260
321
  }
@@ -1,29 +1,33 @@
1
- import { setGetEnv } from 'astro/env/setup';
2
- import type { Env } from '../env';
3
- import type { SecretsStoreSecret } from '../../types/env';
1
+ import { setGetEnv } from "astro/env/setup";
2
+ import type { Env } from "../env";
3
+ import type { SecretsStoreSecret } from "../../types/env";
4
4
 
5
5
  const SECRET_KEYS = [
6
- 'PDS_DID',
7
- 'PDS_HANDLE',
8
- 'USER_PASSWORD',
9
- 'ACCESS_TOKEN_SECRET',
10
- 'REFRESH_TOKEN_SECRET',
11
- 'SESSION_JWT_SECRET',
12
- 'REPO_SIGNING_KEY',
13
- 'REPO_SIGNING_KEY_PUBLIC',
14
- 'PDS_PLC_ROTATION_KEY',
15
- 'PDS_SERVICE_SIGNING_KEY_HEX',
6
+ "PDS_DID",
7
+ "PDS_HANDLE",
8
+ "USER_PASSWORD",
9
+ "REFRESH_TOKEN",
10
+ "REFRESH_TOKEN_SECRET",
11
+ "SESSION_JWT_SECRET",
12
+ "REPO_SIGNING_KEY",
13
+ "REPO_SIGNING_KEY_PUBLIC",
14
+ "PDS_PLC_ROTATION_KEY",
15
+ "PDS_SERVICE_SIGNING_KEY_HEX",
16
16
  ] as const satisfies readonly (keyof Env)[];
17
17
 
18
18
  function isSecretStoreBinding(value: unknown): value is SecretsStoreSecret {
19
- return !!value && typeof value === 'object' && typeof (value as any).get === 'function';
19
+ return (
20
+ !!value &&
21
+ typeof value === "object" &&
22
+ typeof (value as any).get === "function"
23
+ );
20
24
  }
21
25
 
22
26
  export async function resolveSecret(
23
- value: string | SecretsStoreSecret | undefined
27
+ value: string | SecretsStoreSecret | undefined,
24
28
  ): Promise<string | undefined> {
25
29
  if (value === undefined) return undefined;
26
- if (typeof value === 'string') return value;
30
+ if (typeof value === "string") return value;
27
31
  if (isSecretStoreBinding(value)) return value.get();
28
32
  return undefined;
29
33
  }
@@ -41,15 +45,16 @@ export async function resolveEnvSecrets<E extends Env>(env: E): Promise<E> {
41
45
  if (val !== undefined) {
42
46
  resolved[key as string] = val;
43
47
  }
44
- })
48
+ }),
45
49
  );
46
50
 
47
51
  setGetEnv((key) => {
48
52
  const local = resolved[key];
49
- if (typeof local === 'string') return local;
50
- if (typeof local === 'number' || typeof local === 'boolean') return String(local);
53
+ if (typeof local === "string") return local;
54
+ if (typeof local === "number" || typeof local === "boolean")
55
+ return String(local);
51
56
  const fallback = process.env[key];
52
- return typeof fallback === 'string' ? fallback : undefined;
57
+ return typeof fallback === "string" ? fallback : undefined;
53
58
  });
54
59
 
55
60
  return resolved as E;
@@ -62,7 +67,7 @@ let astroGetSecret: AstroGetSecret | null | undefined;
62
67
  async function loadAstroGetSecret(): Promise<AstroGetSecret | null> {
63
68
  if (astroGetSecret !== undefined) return astroGetSecret;
64
69
  try {
65
- const mod = await import('astro:env/server');
70
+ const mod = await import("astro:env/server");
66
71
  astroGetSecret = mod.getSecret as AstroGetSecret;
67
72
  } catch {
68
73
  astroGetSecret = null;
@@ -73,10 +78,10 @@ async function loadAstroGetSecret(): Promise<AstroGetSecret | null> {
73
78
  export async function getRuntimeString<K extends keyof Env>(
74
79
  env: Env,
75
80
  key: K,
76
- fallback?: string
81
+ fallback?: string,
77
82
  ): Promise<string | undefined> {
78
83
  const current = env[key];
79
- if (typeof current === 'string' && current !== '') {
84
+ if (typeof current === "string" && current !== "") {
80
85
  return current;
81
86
  }
82
87
 
@@ -84,7 +89,7 @@ export async function getRuntimeString<K extends keyof Env>(
84
89
  if (secretFn) {
85
90
  try {
86
91
  const value = secretFn(String(key));
87
- if (typeof value === 'string' && value !== '') {
92
+ if (typeof value === "string" && value !== "") {
88
93
  return value;
89
94
  }
90
95
  } catch (error) {
@@ -63,6 +63,18 @@ export function createPdsFetchHandler(options?: CreatePdsFetchHandlerOptions): P
63
63
  ) as unknown as WorkersResponse;
64
64
  }
65
65
 
66
+ // Short-circuit CORS preflight at the worker entrypoint to avoid
67
+ // adapter/method routing mismatches causing 500s on OPTIONS.
68
+ if (request.method === 'OPTIONS') {
69
+ const headers = new Headers({
70
+ 'Access-Control-Allow-Origin': '*',
71
+ 'Access-Control-Allow-Methods': '*',
72
+ 'Access-Control-Allow-Headers': '*',
73
+ 'Access-Control-Max-Age': '86400',
74
+ });
75
+ return new Response(null, { status: 204, headers }) as unknown as WorkersResponse;
76
+ }
77
+
66
78
  await seed(resolvedEnv.DB, (resolvedEnv.PDS_DID as string | undefined) ?? 'did:example:single-user');
67
79
 
68
80
  // Fire-and-forget: let relays know this PDS exists and is reachable.
package/types/env.d.ts CHANGED
@@ -5,7 +5,7 @@ import type {
5
5
  DurableObjectNamespace,
6
6
  ExecutionContext,
7
7
  R2Bucket,
8
- } from '@cloudflare/workers-types';
8
+ } from "@cloudflare/workers-types";
9
9
 
10
10
  // Minimal Secret Store binding interface. Cloudflare exposes each bound secret
11
11
  // as an object with an async `get()` that returns the secret value.
@@ -27,7 +27,7 @@ declare global {
27
27
  PDS_ALLOWED_MIME?: string;
28
28
  USER_PASSWORD?: string | SecretsStoreSecret;
29
29
  PDS_MAX_BLOB_SIZE?: string;
30
- ACCESS_TOKEN_SECRET?: string | SecretsStoreSecret;
30
+ REFRESH_TOKEN?: string | SecretsStoreSecret;
31
31
  REFRESH_TOKEN_SECRET?: string | SecretsStoreSecret;
32
32
  SESSION_JWT_SECRET?: string | SecretsStoreSecret;
33
33
  PDS_ACCESS_TTL_SEC?: string;
@@ -46,8 +46,8 @@ declare global {
46
46
  PDS_BSKY_APP_VIEW_CDN_URL_PATTERN?: string;
47
47
  PDS_SERVICE_SIGNING_KEY_HEX?: string | SecretsStoreSecret;
48
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
49
+ PDS_RELAY_HOSTS?: string; // CSV of relay hostnames (no scheme). Default: bsky.network
50
+ PDS_RELAY_NOTIFY?: string; // 'false' to disable auto notify
51
51
  }
52
52
 
53
53
  namespace App {