From d5bbc14c8d061fecb0ecef10e43522ef0f24c144 Mon Sep 17 00:00:00 2001 From: imbytecat Date: Mon, 18 May 2026 23:28:12 +0800 Subject: [PATCH] feat: move base URL, API key, and model to server .env - BASE_URL, API_KEY, MODEL now read from process.env (Bun auto-loads .env) - requireEnv() fails fast at startup if any is missing - request body simplifies to { prompt, size, referenceImages? } - client drops the three fields from form and localStorage - add .env.example as the variable-name source of truth - AGENTS.md notes the 0.0.0.0 bind now exposes the upstream quota to anyone reachable on the network --- .env.example | 3 +++ AGENTS.md | 29 +++++++++++++++++++++++++---- client.ts | 14 +++++--------- index.html | 38 ++++++++++---------------------------- index.ts | 42 +++++++++++++++++++++--------------------- 5 files changed, 64 insertions(+), 62 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5bc89d3 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +BASE_URL=https://api.openai.com/v1 +API_KEY=sk-... +MODEL=gpt-image-2 diff --git a/AGENTS.md b/AGENTS.md index a3b2d63..9634f29 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,26 @@ proxy-read timeout. `dotenv` — Bun loads `.env` automatically. - Bun version baseline: `1.3.13` (per `README.md`). +## Config + +Required env vars (validated at startup via `requireEnv`; the process exits +if any is missing): + +| Var | Example | Purpose | +|---|---|---| +| `BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible base URL | +| `API_KEY` | `sk-…` | Bearer token sent to upstream | +| `MODEL` | `gpt-image-2` | Model name forwarded to upstream | + +`.env.example` is the source of truth for variable names. The real `.env` +is gitignored. Restart the server after changing env vars — they are read +once at module load. + +These secrets stay **server-side**. The browser only sends `prompt`, +`size`, and `referenceImages`. Combined with the `0.0.0.0` bind, this +means anyone reachable on the network can spend your upstream quota — +bind to `127.0.0.1` or put auth in front if that matters. + ## Commands - Install: `bun install` @@ -41,7 +61,9 @@ Three files do everything: connections before the first keepalive can fire. The symptom is an empty EventStream in DevTools and `request timed out after 10 seconds` in the log. - - `POST /api/generate` uses `streamSSE` from `hono/streaming`. Emits: + - `POST /api/generate` uses `streamSSE` from `hono/streaming`. Accepts + `{ prompt, size, referenceImages? }` — `BASE_URL`, `API_KEY`, and + `MODEL` come from env, not the request. Emits: - `event: partial` — `{ image: dataUrl, index }` for each `image_generation.partial_image` / `image_edit.partial_image`. - `event: final` — `{ image: dataUrl }` for `*.completed`. @@ -80,9 +102,8 @@ Three files do everything: `signal`, and the `onopen` / `onmessage` / `onerror` callbacks. - On `done`, the client calls `abort.abort()` to terminate the `fetchEventSource` loop cleanly — otherwise it would retry forever. - - Text fields (`baseURL`, `apiKey`, `model`, `size`, `prompt`) persist - in `localStorage` under the `aip:` prefix. Reference images - stay in-memory only. + - Text fields (`size`, `prompt`) persist in `localStorage` under the + `aip:` prefix. Reference images stay in-memory only. - `index.html` — markup + inline CSS only. No JS lives here. No router, no DB, no auth, no AI SDK. API key is supplied per-request by the diff --git a/client.ts b/client.ts index 099e50a..5f508cf 100644 --- a/client.ts +++ b/client.ts @@ -2,13 +2,12 @@ import { fetchEventSource } from "@microsoft/fetch-event-source"; const byId = (id: string) => document.getElementById(id) as T; -const input = (id: string) => byId(id); const select = (id: string) => byId(id); const textarea = (id: string) => byId(id); -const persistedFields = ["baseURL", "apiKey", "model", "size", "prompt"] as const; +const persistedFields = ["size", "prompt"] as const; for (const f of persistedFields) { - const el = byId(f); + const el = byId(f); const saved = localStorage.getItem("aip:" + f); if (saved) el.value = saved; const save = () => localStorage.setItem("aip:" + f, el.value); @@ -42,7 +41,7 @@ function renderRefPreview() { }); } -input("refImages").addEventListener("change", async (e) => { +byId("refImages").addEventListener("change", async (e) => { const el = e.target as HTMLInputElement; const files = Array.from(el.files ?? []); for (const file of files) { @@ -67,16 +66,13 @@ byId("generate").addEventListener("click", async () => { const result = byId("result"); const body = { - baseURL: input("baseURL").value.trim(), - apiKey: input("apiKey").value.trim(), - model: input("model").value.trim(), size: select("size").value, prompt: textarea("prompt").value.trim(), referenceImages: refImages, }; - if (!body.baseURL || !body.apiKey || !body.model || !body.prompt) { - status.textContent = "Please fill in Base URL, API Key, Model and Prompt."; + if (!body.prompt) { + status.textContent = "Please enter a prompt."; status.className = "status error"; return; } diff --git a/index.html b/index.html index 7262579..fd19eda 100644 --- a/index.html +++ b/index.html @@ -196,31 +196,13 @@

-
-
- - -
-
- - -
-
- - -
-
- - -
+
+ +
@@ -243,9 +225,9 @@
- Settings are stored in your browser's localStorage - Base URL, API Key, model and size are saved locally. They are sent to - the local Bun server only when you click Generate. + Size and prompt are saved to your browser's localStorage + Base URL, API key and model live in the server's .env — + they are never sent from the browser.
diff --git a/index.ts b/index.ts index 7750285..6bb1d10 100644 --- a/index.ts +++ b/index.ts @@ -6,14 +6,23 @@ import index from "./index.html"; type Size = `${number}x${number}`; type GenerateRequest = { - baseURL?: string; - apiKey?: string; - model?: string; prompt?: string; size?: Size; referenceImages?: string[]; }; +function requireEnv(name: string): string { + const v = process.env[name]; + if (!v) { + throw new Error(`Missing required env: ${name} (see .env.example).`); + } + return v; +} + +const BASE_URL = requireEnv("BASE_URL"); +const API_KEY = requireEnv("API_KEY"); +const MODEL = requireEnv("MODEL"); + function decodeDataUrl( dataUrl: string, ): { bytes: Uint8Array; mime: string } | null { @@ -27,22 +36,19 @@ function decodeDataUrl( } async function callUpstream(args: { - baseURL: string; - apiKey: string; - model: string; prompt: string; size: Size; referenceImages: string[]; stream: boolean; signal?: AbortSignal; }): Promise { - const { baseURL, apiKey, model, prompt, size, referenceImages, stream, signal } = args; + const { prompt, size, referenceImages, stream, signal } = args; const isEdit = referenceImages.length > 0; - const url = `${baseURL.replace(/\/+$/, "")}/images/${isEdit ? "edits" : "generations"}`; + const url = `${BASE_URL.replace(/\/+$/, "")}/images/${isEdit ? "edits" : "generations"}`; if (isEdit) { const form = new FormData(); - form.append("model", model); + form.append("model", MODEL); form.append("prompt", prompt); form.append("size", size); if (stream) { @@ -64,13 +70,13 @@ async function callUpstream(args: { } return fetch(url, { method: "POST", - headers: { Authorization: `Bearer ${apiKey}` }, + headers: { Authorization: `Bearer ${API_KEY}` }, body: form, signal, }); } - const body: Record = { model, prompt, size }; + const body: Record = { model: MODEL, prompt, size }; if (stream) { body.stream = true; body.partial_images = 2; @@ -78,7 +84,7 @@ async function callUpstream(args: { return fetch(url, { method: "POST", headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), @@ -180,18 +186,12 @@ const app = new Hono(); app.post("/api/generate", async (c) => { const body = (await c.req.json()) as GenerateRequest; - const { baseURL, apiKey, model, prompt, size, referenceImages } = body; - if (!baseURL || !apiKey || !model || !prompt) { - return c.json( - { error: "baseURL, apiKey, model, prompt are required" }, - 400, - ); + const { prompt, size, referenceImages } = body; + if (!prompt) { + return c.json({ error: "prompt is required" }, 400); } const refs = Array.isArray(referenceImages) ? referenceImages : []; const args = { - baseURL, - apiKey, - model, prompt, size: size ?? ("1024x1024" as Size), referenceImages: refs,