export class ConfigurationError extends Error { constructor(field: string, instruction: string) { // Only schema-owned field names/instructions belong here, never input values. super(`Invalid configuration: ${field}: ${instruction}`); this.name = "ConfigurationError"; } } export type Environment = "production" | "development"; export type Bindings = Record; export function record( value: unknown, keys: readonly string[], field: string, ): Bindings { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new ConfigurationError(field, "expected an object"); } if ( Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null ) { throw new ConfigurationError( field, "expected a plain configuration object", ); } if (Object.keys(value).some((key) => !keys.includes(key))) { throw new ConfigurationError( field, `contains unsupported fields; allowed fields: ${keys.join(", ")}`, ); } return value as Bindings; } export function json(value: unknown, field: string): unknown { if (typeof value !== "string") throw new ConfigurationError( field, "set a JSON object string in Worker variables", ); try { return JSON.parse(value); } catch { throw new ConfigurationError( field, "set valid JSON (parser details are withheld to protect credentials)", ); } } export function text(value: unknown, field: string): string { if ( typeof value !== "string" || !value.trim() || value !== value.trim() || value.length > 512 || /[\u0000-\u001f\u007f]/.test(value) ) { throw new ConfigurationError( field, "set a nonempty string of at most 512 characters without surrounding whitespace or control characters", ); } return value; } export function origin( value: unknown, field: string, environment: Environment, ): string { const input = text(value, field); let url: URL; try { url = new URL(input); } catch { throw new ConfigurationError(field, "set an absolute HTTPS origin"); } const local = environment === "development" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname); if ( (url.protocol !== "https:" && !(local && url.protocol === "http:")) || url.origin !== input || url.username || url.password ) { throw new ConfigurationError( field, "set an HTTPS origin without credentials, path, query, fragment or trailing slash; HTTP loopback is allowed only in development", ); } return input; } export function serverSettings( env: Bindings, mode: "customer-runtime" | "control-plane", allowed: readonly string[], ) { if (env.FLAREBOT_MODE !== mode) throw new ConfigurationError( "FLAREBOT_MODE", `set ${mode} for this Worker entry`, ); const environment = env.FLAREBOT_ENV ?? "production"; if (environment !== "production" && environment !== "development") throw new ConfigurationError( "FLAREBOT_ENV", "set production or development", ); if ( Object.keys(env).some( (key) => key.startsWith("FLAREBOT_") && !allowed.includes(key), ) ) { throw new ConfigurationError( "Worker bindings", "unsupported FLAREBOT_ binding for this entry; check customer-runtime versus control-plane configuration", ); } if ( environment === "production" && env.FLAREBOT_DEV_OVERRIDES !== undefined ) { throw new ConfigurationError( "FLAREBOT_DEV_OVERRIDES", "remove development overrides from production", ); } return Object.freeze({ mode, environment }); }