Initial: config.saiden.dev worker with KV-backed secrets and files, project/env scoping

This commit is contained in:
marauder-actual
2026-06-10 16:59:18 +02:00
commit 339f68de4b
10 changed files with 3495 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
.wrangler/
.dev.vars
+3156
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
{
"name": "config-saiden-dev",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"test": "vitest run"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250101.0",
"typescript": "^5.8",
"vitest": "^3.0",
"wrangler": "^4.0"
}
}
+26
View File
@@ -0,0 +1,26 @@
import type { Env } from "./types";
export function authenticate(request: Request, env: Env): Response | null {
const header = request.headers.get("Authorization");
if (!header) {
return json(401, { error: "Missing Authorization header" });
}
const [scheme, token] = header.split(" ", 2);
if (scheme !== "Bearer" || !token) {
return json(401, { error: "Invalid Authorization format. Use: Bearer <key>" });
}
if (token !== env.API_KEY) {
return json(403, { error: "Invalid API key" });
}
return null; // authenticated
}
export function json(status: number, body: unknown): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
+74
View File
@@ -0,0 +1,74 @@
import type { Env } from "./types";
import { authenticate, json } from "./auth";
import { getSecret, putSecret, deleteSecret, listSecrets } from "./routes/secrets";
import { getFile, putFile, deleteFile, listFiles } from "./routes/files";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Auth gate
const authError = authenticate(request, env);
if (authError) return authError;
const url = new URL(request.url);
const method = request.method;
const path = url.pathname;
// Parse: /:project/:env/secrets[/:key]
// Parse: /:project/:env/files[/:path+]
const match = path.match(/^\/([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)\/(secrets|files)(?:\/(.+))?$/);
if (!match) {
return json(404, {
error: "Not found",
usage: "/:project/:env/secrets[/:key] or /:project/:env/files[/:path]",
});
}
const [, project, environment, resource, rest] = match;
// --- Secrets ---
if (resource === "secrets") {
const kv = env.CONFIG_SECRETS;
if (!rest) {
if (method === "GET") return listSecrets({ project, env: environment }, request, kv);
return json(405, { error: "Method not allowed" });
}
const params = { project, env: environment, key: rest };
switch (method) {
case "GET":
return getSecret(params, kv);
case "PUT":
return putSecret(params, request, kv);
case "DELETE":
return deleteSecret(params, kv);
default:
return json(405, { error: "Method not allowed" });
}
}
// --- Files ---
if (resource === "files") {
const kv = env.CONFIG_FILES;
if (!rest) {
if (method === "GET") return listFiles({ project, env: environment }, request, kv);
return json(405, { error: "Method not allowed" });
}
const params = { project, env: environment, path: rest };
switch (method) {
case "GET":
return getFile(params, kv);
case "PUT":
return putFile(params, request, kv);
case "DELETE":
return deleteFile(params, kv);
default:
return json(405, { error: "Method not allowed" });
}
}
return json(404, { error: "Not found" });
},
};
+104
View File
@@ -0,0 +1,104 @@
import type { Env, FileParams, FileMetadata, RouteParams } from "../types";
import { json } from "../auth";
const PREFIX = "file";
function kvKey(project: string, env: string, path: string): string {
return `${PREFIX}:${project}:${env}:${path}`;
}
function kvPrefix(project: string, env: string): string {
return `${PREFIX}:${project}:${env}:`;
}
function guessContentType(path: string): string {
const ext = path.split(".").pop()?.toLowerCase();
const map: Record<string, string> = {
json: "application/json",
toml: "application/toml",
yaml: "application/yaml",
yml: "application/yaml",
xml: "application/xml",
ini: "text/plain",
conf: "text/plain",
txt: "text/plain",
env: "text/plain",
sh: "text/x-shellscript",
ts: "text/typescript",
js: "text/javascript",
};
return map[ext ?? ""] ?? "application/octet-stream";
}
export async function getFile(params: FileParams, kv: KVNamespace): Promise<Response> {
const key = kvKey(params.project, params.env, params.path);
const { value, metadata } = await kv.getWithMetadata<FileMetadata>(key);
if (value === null) {
return json(404, { error: "File not found" });
}
return new Response(value, {
status: 200,
headers: {
"Content-Type": metadata?.contentType ?? "application/octet-stream",
"X-Updated-At": metadata?.updatedAt ?? "",
},
});
}
export async function putFile(params: FileParams, request: Request, kv: KVNamespace): Promise<Response> {
const content = await request.text();
if (!content) {
return json(400, { error: "Empty body" });
}
const contentType =
request.headers.get("Content-Type") ?? guessContentType(params.path);
const metadata: FileMetadata = {
contentType,
updatedAt: new Date().toISOString(),
size: content.length,
};
await kv.put(kvKey(params.project, params.env, params.path), content, { metadata });
return json(200, {
path: params.path,
project: params.project,
env: params.env,
contentType,
size: content.length,
stored: true,
});
}
export async function deleteFile(params: FileParams, kv: KVNamespace): Promise<Response> {
await kv.delete(kvKey(params.project, params.env, params.path));
return json(200, { path: params.path, deleted: true });
}
export async function listFiles(params: RouteParams, request: Request, kv: KVNamespace): Promise<Response> {
const url = new URL(request.url);
const cursor = url.searchParams.get("cursor") ?? undefined;
const limit = Math.min(parseInt(url.searchParams.get("limit") ?? "100", 10), 1000);
const prefix = kvPrefix(params.project, params.env);
const result = await kv.list<FileMetadata>({ prefix, cursor, limit });
const files = result.keys.map((k) => ({
path: k.name.slice(prefix.length),
contentType: k.metadata?.contentType,
size: k.metadata?.size,
updatedAt: k.metadata?.updatedAt,
}));
return json(200, {
project: params.project,
env: params.env,
files,
cursor: result.list_complete ? undefined : result.cursor,
complete: result.list_complete,
});
}
+54
View File
@@ -0,0 +1,54 @@
import type { Env, SecretParams, RouteParams } from "../types";
import { json } from "../auth";
const PREFIX = "secret";
function kvKey(project: string, env: string, key: string): string {
return `${PREFIX}:${project}:${env}:${key}`;
}
function kvPrefix(project: string, env: string): string {
return `${PREFIX}:${project}:${env}:`;
}
export async function getSecret(params: SecretParams, kv: KVNamespace): Promise<Response> {
const value = await kv.get(kvKey(params.project, params.env, params.key));
if (value === null) {
return json(404, { error: "Secret not found" });
}
return json(200, { key: params.key, value });
}
export async function putSecret(params: SecretParams, request: Request, kv: KVNamespace): Promise<Response> {
const body = await request.json<{ value: string }>().catch(() => null);
if (!body || typeof body.value !== "string") {
return json(400, { error: "Body must be JSON with a 'value' string field" });
}
await kv.put(kvKey(params.project, params.env, params.key), body.value);
return json(200, { key: params.key, project: params.project, env: params.env, stored: true });
}
export async function deleteSecret(params: SecretParams, kv: KVNamespace): Promise<Response> {
await kv.delete(kvKey(params.project, params.env, params.key));
return json(200, { key: params.key, deleted: true });
}
export async function listSecrets(params: RouteParams, request: Request, kv: KVNamespace): Promise<Response> {
const url = new URL(request.url);
const cursor = url.searchParams.get("cursor") ?? undefined;
const limit = Math.min(parseInt(url.searchParams.get("limit") ?? "100", 10), 1000);
const prefix = kvPrefix(params.project, params.env);
const result = await kv.list({ prefix, cursor, limit });
const keys = result.keys.map((k) => k.name.slice(prefix.length));
return json(200, {
project: params.project,
env: params.env,
keys,
cursor: result.list_complete ? undefined : result.cursor,
complete: result.list_complete,
});
}
+30
View File
@@ -0,0 +1,30 @@
export interface Env {
CONFIG_SECRETS: KVNamespace;
CONFIG_FILES: KVNamespace;
API_KEY: string;
}
export interface RouteParams {
project: string;
env: string;
}
export interface SecretParams extends RouteParams {
key: string;
}
export interface FileParams extends RouteParams {
path: string;
}
export interface ListResult {
keys: string[];
cursor?: string;
complete: boolean;
}
export interface FileMetadata {
contentType: string;
updatedAt: string;
size: number;
}
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"lib": ["ESNext"],
"types": ["@cloudflare/workers-types"],
"isolatedModules": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
+15
View File
@@ -0,0 +1,15 @@
name = "config-saiden-dev"
main = "src/index.ts"
compatibility_date = "2025-01-01"
routes = [
{ pattern = "config.saiden.dev", custom_domain = true }
]
[[kv_namespaces]]
binding = "CONFIG_SECRETS"
id = "425dd5e9d641479e8130d20b83fa0224"
[[kv_namespaces]]
binding = "CONFIG_FILES"
id = "f2c224cbfce04852ba4616d51fa545b7"