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.