a control panel for my server
1import { Database } from "bun:sqlite";
2
3const DB_PATH = process.env.DATABASE_PATH || "./data/control.db";
4const FLAGS_CONFIG_PATH = process.env.FLAGS_CONFIG || "./flags.json";
5
6// Load flags config from file (path from env or default)
7const flagsConfig = await Bun.file(FLAGS_CONFIG_PATH).json().catch(() => {
8 // Fallback to local flags.json if FLAGS_CONFIG path doesn't exist
9 return import("../flags.json").then(m => m.default);
10});
11
12// Initialize database
13const db = new Database(DB_PATH, { create: true });
14db.exec(`
15 CREATE TABLE IF NOT EXISTS flags (
16 id TEXT PRIMARY KEY,
17 enabled INTEGER NOT NULL DEFAULT 0,
18 updated_at TEXT NOT NULL DEFAULT (datetime('now'))
19 )
20`);
21
22// Prepared statements for performance
23const getFlag = db.prepare<{ enabled: number }, [string]>(
24 "SELECT enabled FROM flags WHERE id = ?"
25);
26const setFlagStmt = db.prepare(
27 `INSERT INTO flags (id, enabled, updated_at) VALUES (?, ?, datetime('now'))
28 ON CONFLICT(id) DO UPDATE SET enabled = excluded.enabled, updated_at = datetime('now')`
29);
30const getAllFlags = db.prepare<{ id: string; enabled: number }, []>(
31 "SELECT id, enabled FROM flags"
32);
33
34export interface FlagDefinition {
35 name: string;
36 description: string;
37 paths: string[]; // The paths this flag blocks
38 redact?: Record<string, string[]>; // path -> fields to strip from JSON
39}
40
41export interface ServiceDefinition {
42 name: string;
43 flags: Record<string, FlagDefinition>;
44}
45
46export interface FlagsConfig {
47 services: Record<string, ServiceDefinition>;
48}
49
50export interface FlagStatus {
51 id: string;
52 name: string;
53 description: string;
54 enabled: boolean;
55 service: string;
56}
57
58export function getConfig(): FlagsConfig {
59 return flagsConfig as FlagsConfig;
60}
61
62export function getAllFlagIds(): string[] {
63 const config = getConfig();
64 const ids: string[] = [];
65 for (const service of Object.values(config.services)) {
66 for (const flagId of Object.keys(service.flags)) {
67 ids.push(flagId);
68 }
69 }
70 return ids;
71}
72
73export function getFlagDefinition(
74 flagId: string
75): { flag: FlagDefinition; serviceId: string; service: ServiceDefinition } | null {
76 const config = getConfig();
77 for (const [serviceId, service] of Object.entries(config.services)) {
78 if (flagId in service.flags) {
79 return { flag: service.flags[flagId], serviceId, service };
80 }
81 }
82 return null;
83}
84
85export function getFlagStatus(flagId: string): boolean {
86 const row = getFlag.get(flagId);
87 return row?.enabled === 1;
88}
89
90export function setFlag(flagId: string, enabled: boolean): void {
91 if (!getFlagDefinition(flagId)) {
92 throw new Error(`Unknown flag: ${flagId}`);
93 }
94 setFlagStmt.run(flagId, enabled ? 1 : 0);
95}
96
97export function getAllFlagsStatus(): Record<string, FlagStatus[]> {
98 const config = getConfig();
99 const result: Record<string, FlagStatus[]> = {};
100
101 // Get all current flag states from DB
102 const dbFlags = new Map<string, boolean>();
103 for (const row of getAllFlags.all()) {
104 dbFlags.set(row.id, row.enabled === 1);
105 }
106
107 for (const [serviceId, service] of Object.entries(config.services)) {
108 const flags: FlagStatus[] = [];
109 for (const [flagId, flag] of Object.entries(service.flags)) {
110 flags.push({
111 id: flagId,
112 name: flag.name,
113 description: flag.description,
114 enabled: dbFlags.get(flagId) ?? false,
115 service: serviceId,
116 });
117 }
118 result[serviceId] = flags;
119 }
120
121 return result;
122}
123
124// Match a path pattern against a request path (supports * wildcard for single segment)
125function matchPath(pattern: string, path: string): boolean {
126 if (!pattern.includes("*")) {
127 return path === pattern || path.startsWith(pattern + "/") || path.startsWith(pattern + "?");
128 }
129
130 // Convert pattern to regex: * matches any single path segment
131 const regexPattern = pattern
132 .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape regex special chars except *
133 .replace(/\*/g, "[^/]+"); // * matches one path segment
134
135 const regex = new RegExp(`^${regexPattern}($|\\?|/)`);
136 return regex.test(path);
137}
138
139// Check if a request should be blocked based on host and path
140export function shouldBlock(host: string, path: string): boolean {
141 const config = getConfig();
142
143 for (const [serviceId, service] of Object.entries(config.services)) {
144 // Check if this request matches a service
145 if (!host.includes(serviceId) && !serviceId.includes(host)) {
146 continue;
147 }
148
149 for (const [flagId, flag] of Object.entries(service.flags)) {
150 // Check if flag is enabled (blocking)
151 if (!getFlagStatus(flagId)) {
152 continue;
153 }
154
155 // Check if any of the flag's paths match (supports * wildcard)
156 for (const flagPath of flag.paths) {
157 if (matchPath(flagPath, path)) {
158 return true;
159 }
160 }
161 }
162 }
163
164 return false;
165}
166
167// Get fields to redact from a JSON response based on host and path
168export function getRedactions(host: string, path: string): string[] {
169 const config = getConfig();
170 const fields: string[] = [];
171
172 for (const [serviceId, service] of Object.entries(config.services)) {
173 if (!host.includes(serviceId) && !serviceId.includes(host)) {
174 continue;
175 }
176
177 for (const [flagId, flag] of Object.entries(service.flags)) {
178 if (!getFlagStatus(flagId)) {
179 continue;
180 }
181
182 if (flag.redact) {
183 for (const [redactPath, redactFields] of Object.entries(flag.redact)) {
184 if (path === redactPath || path.startsWith(redactPath + "?")) {
185 fields.push(...redactFields);
186 }
187 }
188 }
189 }
190 }
191
192 return fields;
193}