// src/index.ts
import { createWorkerKeyServer } from "@blindcast/keys"
import { hexToBytes } from "@blindcast/crypto"
import { AwsClient } from "aws4fetch"
interface Env {
MASTER_KEY: string // hex, wrangler secret
SALT: string // hex, wrangler secret
R2_ACCESS_KEY_ID: string // wrangler secret
R2_SECRET_ACCESS_KEY: string // wrangler secret
R2_ACCOUNT_ID: string // wrangler var
R2_BUCKET_NAME: string // wrangler var
CORS_ORIGINS: string // wrangler var (comma-separated)
}
// Key server is created once per isolate (env is stable within a Worker isolate)
let keyServer: ReturnType<typeof createWorkerKeyServer> | undefined
function getKeyServer(env: Env) {
if (!keyServer) {
keyServer = createWorkerKeyServer({
masterKey: hexToBytes(env.MASTER_KEY),
salt: hexToBytes(env.SALT),
corsOrigins: env.CORS_ORIGINS.split(",").map((s) => s.trim()),
})
}
return keyServer
}
function getAllowedOrigin(request: Request, env: Env): string {
const requestOrigin = request.headers.get("Origin") ?? ""
const allowed = env.CORS_ORIGINS.split(",").map((s) => s.trim())
if (allowed.includes(requestOrigin)) return requestOrigin
return allowed[0] ?? "*"
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
const origin = getAllowedOrigin(request, env)
// CORS preflight for presign route
if (request.method === "OPTIONS" && url.pathname === "/presign") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Max-Age": "86400",
},
})
}
// Custom presign route
// ⚠️ No authentication — add an Authorization check for production.
if (url.pathname === "/presign" && request.method === "POST") {
return handlePresign(request, env, origin)
}
// Everything else → key server (/keys/*, /health, CORS)
return getKeyServer(env).fetch(
request,
env as unknown as Record<string, unknown>,
)
},
}
async function handlePresign(
request: Request,
env: Env,
origin: string,
): Promise<Response> {
// contentType is sent by the uploader SDK but intentionally not included
// in the presign signature (browsers add unsigned headers that break it).
let body: { keys: { key: string; contentType: string }[] }
try {
body = await request.json()
} catch {
return json(400, { error: "Invalid JSON body" }, origin)
}
if (!Array.isArray(body.keys) || body.keys.length === 0) {
return json(400, { error: "Missing or empty keys array" }, origin)
}
// Validate key patterns (prevent path traversal)
for (const { key } of body.keys) {
if (!/^[a-zA-Z0-9/_.-]+$/.test(key) || key.includes("..")) {
return json(400, { error: "Invalid key pattern" }, origin)
}
}
const client = new AwsClient({
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
service: "s3",
region: "auto",
})
const r2Endpoint = `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`
const urls = await Promise.all(
body.keys.map(async ({ key }) => {
const signed = await client.sign(
new Request(
`${r2Endpoint}/${env.R2_BUCKET_NAME}/${key}?X-Amz-Expires=300`,
{ method: "PUT" },
),
{ aws: { signQuery: true } },
)
return { key, url: signed.url }
}),
)
return json(200, { urls }, origin)
}
function json(status: number, body: unknown, origin: string): Response {
return new Response(JSON.stringify(body), {
status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
})
}