@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 +10 -10
- package/package.json +3 -2
- package/src/lib/jwt.ts +127 -66
- package/src/lib/secrets.ts +29 -24
- package/src/worker/runtime.ts +12 -0
- package/types/env.d.ts +4 -4
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
|
-
- `
|
|
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
|
-
| `
|
|
183
|
-
| `
|
|
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
|
|
200
|
-
wrangler secret put
|
|
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": "
|
|
216
|
-
{ "binding": "
|
|
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
|
-
- `
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
2
|
-
import { getRuntimeString } from
|
|
3
|
-
import { base58btc } from
|
|
4
|
-
import {
|
|
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:
|
|
16
|
+
t: "access" | "refresh";
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
// JWT
|
|
16
|
-
export async function signJwt(
|
|
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(
|
|
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 ===
|
|
31
|
+
return kind === "access" ? accessJwt : refreshJwt;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
|
-
export async function verifyJwt(
|
|
27
|
-
|
|
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(
|
|
40
|
+
const header = JSON.parse(
|
|
41
|
+
atob(parts[0].replace(/-/g, "+").replace(/_/g, "/")),
|
|
42
|
+
);
|
|
30
43
|
|
|
31
|
-
if (header.typ ===
|
|
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:
|
|
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 ===
|
|
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:
|
|
70
|
+
t: "refresh",
|
|
58
71
|
};
|
|
59
72
|
return { valid: true, payload };
|
|
60
73
|
}
|
|
61
74
|
|
|
62
|
-
const payload = JSON.parse(
|
|
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 ===
|
|
80
|
+
if (header.alg === "HS256" && header.typ === "JWT") {
|
|
66
81
|
const secret = await getRuntimeString(
|
|
67
82
|
env,
|
|
68
|
-
payload.t ===
|
|
69
|
-
payload.t ===
|
|
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] +
|
|
73
|
-
} else if (header.alg ===
|
|
74
|
-
ok = await eddsaJwtVerify(parts[0] +
|
|
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:
|
|
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(
|
|
91
|
-
|
|
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(
|
|
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(
|
|
99
|
-
|
|
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:
|
|
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,
|
|
147
|
+
const keyData = await getRuntimeString(env, "REPO_SIGNING_KEY");
|
|
112
148
|
if (!keyData) {
|
|
113
|
-
throw new Error(
|
|
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
|
-
|
|
155
|
+
"pkcs8",
|
|
120
156
|
keyBytes,
|
|
121
|
-
{ name:
|
|
157
|
+
{ name: "Ed25519" } as any,
|
|
122
158
|
false,
|
|
123
|
-
[
|
|
159
|
+
["sign"],
|
|
124
160
|
);
|
|
125
161
|
|
|
126
|
-
const sig = await crypto.subtle.sign(
|
|
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(
|
|
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,
|
|
175
|
+
const keyData = await getRuntimeString(env, "REPO_SIGNING_KEY_PUBLIC");
|
|
136
176
|
if (!keyData) {
|
|
137
|
-
console.error(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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 ?
|
|
167
|
-
const bin = atob(s.replace(/-/g,
|
|
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(
|
|
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:
|
|
231
|
+
{ name: "Ed25519", namedCurve: "Ed25519" } as any,
|
|
181
232
|
false,
|
|
182
|
-
[
|
|
233
|
+
["verify"],
|
|
183
234
|
);
|
|
184
235
|
} catch (error) {
|
|
185
|
-
console.warn(
|
|
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 =
|
|
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:
|
|
254
|
+
attempts.push({ format: "raw", data: didKeyCandidate });
|
|
201
255
|
}
|
|
202
256
|
|
|
203
|
-
const pemMatch = trimmed.match(
|
|
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:
|
|
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:
|
|
271
|
+
attempts.push({ format: "raw", data: decoded });
|
|
216
272
|
} else {
|
|
217
|
-
attempts.push({ format:
|
|
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,
|
|
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(
|
|
304
|
+
if (!didKey.startsWith("did:key:")) return null;
|
|
249
305
|
try {
|
|
250
|
-
const multibase = didKey.slice(
|
|
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(
|
|
311
|
+
console.warn(
|
|
312
|
+
"EdDSA JWT verification warning: unsupported did:key multicodec prefix",
|
|
313
|
+
);
|
|
256
314
|
} catch (error) {
|
|
257
|
-
console.warn(
|
|
315
|
+
console.warn(
|
|
316
|
+
"EdDSA JWT verification warning: failed to parse did:key",
|
|
317
|
+
error,
|
|
318
|
+
);
|
|
258
319
|
}
|
|
259
320
|
return null;
|
|
260
321
|
}
|
package/src/lib/secrets.ts
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
import { setGetEnv } from
|
|
2
|
-
import type { Env } from
|
|
3
|
-
import type { SecretsStoreSecret } from
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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 ===
|
|
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 ===
|
|
50
|
-
if (typeof 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 ===
|
|
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(
|
|
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 ===
|
|
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 ===
|
|
92
|
+
if (typeof value === "string" && value !== "") {
|
|
88
93
|
return value;
|
|
89
94
|
}
|
|
90
95
|
} catch (error) {
|
package/src/worker/runtime.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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;
|
|
50
|
-
PDS_RELAY_NOTIFY?: string;
|
|
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 {
|