57 lines · 2.6 KB · TypeScript
RawDownload
// Shared, runtime-agnostic access-control logic for the frontend.//// The frontend serves an in-process coding agent with shell/filesystem tools,// so its API surface is privileged: reaching it is equivalent to code execution// on the host. Access is therefore gated by an opt-in shared secret. The default// posture is safe-by-default — open for local development and the embedded// desktop app, and locked when running as a production web server without an// explicit token or opt-out. This mirrors the controller's bind/auth policy.//// This module imports nothing Node-specific so it can run in both the Edge// middleware and Node route handlers; the constant-time comparison here is a// pure-JS fallback. Node route guards use node:crypto timingSafeEqual instead. export const STUDIO_TOKEN_HEADER = "x-local-studio-token";export const STUDIO_TOKEN_COOKIE = "local_studio_token"; export type AccessDecision =  | { kind: "allow"; reason: "desktop" | "development" | "no-token" }  | { kind: "require-token"; token: string }; function trimmedEnv(name: string): string {  const value = process.env[name];  return typeof value === "string" ? value.trim() : "";} // Resolve the access posture from environment. Pure function of env so the edge// middleware and the node route guards always agree.//// Gating is OPT-IN: the app is open by default (so a fresh local/desktop setup// is unchanged), and only requires a token once LOCAL_STUDIO_FRONTEND_TOKEN is// set. Set the token when serving the frontend on an untrusted network.export function resolveAccessPosture(): AccessDecision {  // The desktop app embeds a loopback-only Next server — always open, even if a  // token is set elsewhere in the environment.  if (trimmedEnv("LOCAL_STUDIO_DATA_DIR")) return { kind: "allow", reason: "desktop" };  // Local development (`next dev`) is loopback and single-user.  if (process.env.NODE_ENV !== "production") return { kind: "allow", reason: "development" };  const token = trimmedEnv("LOCAL_STUDIO_FRONTEND_TOKEN");  if (token) return { kind: "require-token", token };  // No token configured: open. Setting the token is the opt-in to gating.  return { kind: "allow", reason: "no-token" };} // Constant-time string comparison usable in both Edge and Node runtimes.export function timingSafeStringEqual(a: string, b: string): boolean {  if (a.length !== b.length) return false;  let diff = 0;  for (let i = 0; i < a.length; i += 1) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);  return diff === 0;} export function presentedToken(  headerToken: string | null,  cookieToken: string | null | undefined,): string {  return (headerToken ?? cookieToken ?? "").trim();}