@bitkyc08/opencodex 2.7.35 → 2.7.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +4 -2
  4. package/README.ru.md +1 -1
  5. package/README.zh-CN.md +1 -1
  6. package/bin/ocx.mjs +52 -0
  7. package/gui/dist/assets/index-BpX-hoSd.css +1 -0
  8. package/gui/dist/assets/index-ZmFopEYw.js +52 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -1
  11. package/src/adapters/cursor/cursor-errors.ts +38 -1
  12. package/src/adapters/cursor/discovery.ts +1 -0
  13. package/src/adapters/cursor/effort-map.ts +1 -0
  14. package/src/adapters/cursor/live-models.ts +22 -5
  15. package/src/adapters/cursor/live-transport.ts +82 -7
  16. package/src/adapters/cursor/transport.ts +2 -0
  17. package/src/adapters/cursor.ts +5 -2
  18. package/src/adapters/openai-responses.ts +64 -1
  19. package/src/cli/doctor.ts +10 -0
  20. package/src/cli/help.ts +10 -0
  21. package/src/cli/index.ts +88 -9
  22. package/src/cli/internal-dispatch.ts +20 -0
  23. package/src/cli/status.ts +15 -4
  24. package/src/cli/tray-proxy.ts +52 -0
  25. package/src/codex/auth-api.ts +46 -5
  26. package/src/codex/autostart-health.ts +149 -0
  27. package/src/codex/catalog/aggregation.ts +268 -0
  28. package/src/codex/catalog/bundled.ts +188 -0
  29. package/src/codex/catalog/effort.ts +263 -0
  30. package/src/codex/catalog/metadata.ts +176 -0
  31. package/src/codex/catalog/parsing.ts +399 -0
  32. package/src/codex/catalog/provider-fetch.ts +609 -0
  33. package/src/codex/catalog/sync.ts +540 -0
  34. package/src/codex/catalog.ts +11 -2426
  35. package/src/codex/inject.ts +165 -3
  36. package/src/codex/shim.ts +141 -8
  37. package/src/codex/sync.ts +17 -2
  38. package/src/config.ts +23 -0
  39. package/src/lib/errors.ts +11 -0
  40. package/src/providers/antigravity-models.ts +33 -0
  41. package/src/providers/kiro-models.ts +2 -0
  42. package/src/providers/registry.ts +2 -2
  43. package/src/responses/state.ts +69 -6
  44. package/src/server/auth-cors.ts +3 -0
  45. package/src/server/management/agent-settings-routes.ts +536 -0
  46. package/src/server/management/combo-routes.ts +210 -0
  47. package/src/server/management/config-routes.ts +302 -0
  48. package/src/server/management/context.ts +21 -0
  49. package/src/server/management/logs-usage-routes.ts +176 -0
  50. package/src/server/management/model-routes.ts +253 -0
  51. package/src/server/management/oauth-account-routes.ts +301 -0
  52. package/src/server/management/provider-routes.ts +408 -0
  53. package/src/server/management/shared.ts +186 -0
  54. package/src/server/management-api.ts +23 -1806
  55. package/src/server/responses/collaboration.ts +300 -0
  56. package/src/server/responses/compact.ts +342 -0
  57. package/src/server/responses/core.ts +1498 -0
  58. package/src/server/responses/encrypted-payload.ts +231 -0
  59. package/src/server/responses/fetch-helpers.ts +157 -0
  60. package/src/server/responses.ts +9 -2172
  61. package/src/server/startup-action-control.ts +41 -0
  62. package/src/server/startup-health-cache.ts +100 -0
  63. package/src/server/windows-tray-control.ts +41 -0
  64. package/src/service.ts +171 -19
  65. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  66. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  67. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  68. package/src/tray/assets/opencodex-tray.png +0 -0
  69. package/src/tray/windows-tray.ps1 +290 -0
  70. package/src/tray/windows.ts +628 -0
  71. package/src/types.ts +5 -0
  72. package/src/update/index.ts +43 -0
  73. package/src/update/job.ts +46 -0
  74. package/src/update/tray-update-plan.d.mts +18 -0
  75. package/src/update/tray-update-plan.mjs +38 -0
  76. package/src/usage/cost.ts +0 -0
  77. package/src/usage/expected-prices.ts +9 -2
  78. package/src/usage/summary.ts +42 -7
  79. package/gui/dist/assets/index-BunUANVE.js +0 -52
  80. package/gui/dist/assets/index-Sg-7L_oZ.css +0 -1
@@ -0,0 +1,301 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import type { CatalogModel } from "../../codex/catalog";
4
+ import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
5
+ import {
6
+ DEFAULT_SUBAGENT_MODELS,
7
+ codexAutoStartEnabled,
8
+ hasOwnProvider,
9
+ isValidProviderName,
10
+ multiAgentGuidanceEnabled,
11
+ providerBaseUrlConfigError,
12
+ providerHeadersConfigError,
13
+ saveConfig,
14
+ } from "../../config";
15
+ import {
16
+ clearLoginState,
17
+ getLoginStatus,
18
+ isPublicOAuthProvider,
19
+ listOAuthProviders,
20
+ startLoginFlow,
21
+ submitManualLoginCode,
22
+ upsertOAuthProvider,
23
+ } from "../../oauth";
24
+ import { removeCredential } from "../../oauth/store";
25
+ import { providerDestinationResolvedError } from "../../lib/destination-policy";
26
+ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
27
+ import { deriveProviderPresets } from "../../providers/derive";
28
+ import { providerCodexAccountMode } from "../../providers/registry";
29
+ import { routedSlug, slugEquals } from "../../providers/slug-codec";
30
+ import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
31
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
32
+ import { clearThreadAccountMap } from "../../codex/routing";
33
+ import { primeCodexPoolQuotas } from "../../codex/auth-api";
34
+ import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
35
+ import { resolveCodexHomeDir } from "../../codex/home";
36
+ import { scanStorage } from "../../storage/scanner";
37
+ import { readUsageEntries } from "../../usage/log";
38
+ import { getUsageDebugLogEntries } from "../../usage/debug";
39
+ import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
40
+ import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
41
+ import { getProviderRegistryEntry } from "../../providers/registry";
42
+ import { getDebugLogEntries } from "../../lib/debug-log-buffer";
43
+ import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
44
+ import {
45
+ clearDebugSettings,
46
+ clearDebugSetting,
47
+ getDebugSettings,
48
+ setDebugSettings,
49
+ type DebugFlag,
50
+ } from "../../lib/debug-settings";
51
+ import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
52
+ import { drainAndShutdown } from "../lifecycle";
53
+ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
54
+ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
55
+ import type { PersistedUsageAttempt } from "../../usage/log";
56
+ import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
57
+ import { applySystemEnvToggle } from "../system-env";
58
+
59
+ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
60
+ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
61
+ import type { ManagementContext } from "./context";
62
+
63
+ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<Response | null> {
64
+ const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
65
+
66
+ // Which providers support real OAuth login (drives the GUI's "Log in with …" buttons).
67
+ if (url.pathname === "/api/oauth/providers" && req.method === "GET") {
68
+ return jsonResponse({ providers: listOAuthProviders() });
69
+ }
70
+
71
+ // API-key "login" providers (open dashboard → paste key). Drives the GUI's key-provider picker.
72
+ if (url.pathname === "/api/key-providers" && req.method === "GET") {
73
+ return jsonResponse({ providers: listKeyLoginProviders() });
74
+ }
75
+
76
+ // OAuth login (xai now; anthropic/kimi in cycle 2). Starts the flow and returns the auth URL;
77
+ // the provider's loopback callback server (inside this process) captures the redirect in the
78
+ // background, then the credential is persisted. The GUI opens the URL and polls /api/oauth/status.
79
+ if (url.pathname === "/api/oauth/login" && req.method === "POST") {
80
+ const body = await req.json().catch(() => ({})) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean };
81
+ const provider = (body.provider ?? "").trim().toLowerCase();
82
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
83
+ const accountId = body.accountId?.trim();
84
+ const reauth = body.reauth === true || Boolean(accountId);
85
+ try {
86
+ if (accountId) {
87
+ const { getAccountSet } = await import("../../oauth/store");
88
+ const set = getAccountSet(provider);
89
+ if (!set?.accounts.some(a => a.id === accountId)) {
90
+ return jsonResponse({ error: "Unknown account for reauth" }, 404);
91
+ }
92
+ }
93
+ // addAccount / reauth forces a fresh browser identity (skips local-CLI token import).
94
+ const { url: authUrl, instructions, deviceCode } = await startLoginFlow(provider, {
95
+ forceLogin: body.addAccount === true || reauth,
96
+ ...(accountId ? { reauthAccountId: accountId } : {}),
97
+ });
98
+ upsertOAuthProvider(config, provider); // mutate LIVE config — routing sees it without restart
99
+ if (authUrl && !deviceCode) {
100
+ // Open the browser server-side (the proxy runs on the user's machine) — the GUI's
101
+ // window.open is popup-blocked because it runs after an await, not a direct click.
102
+ const { openUrl } = await import("../../lib/open-url");
103
+ openUrl(authUrl);
104
+ }
105
+ return jsonResponse({ url: authUrl, instructions, deviceCode });
106
+ } catch (err) {
107
+ return jsonResponse({ error: err instanceof Error ? err.message : String(err) }, 409);
108
+ }
109
+ }
110
+
111
+ // Cancel an in-progress browser/device OAuth login (GUI "Cancel" / modal close). Guarded by
112
+ // the same public predicate as /api/oauth/login — only publicly startable flows are cancellable.
113
+ if (url.pathname === "/api/oauth/login/cancel" && req.method === "POST") {
114
+ const body = await req.json().catch(() => ({})) as { provider?: string };
115
+ const provider = (body.provider ?? "").trim().toLowerCase();
116
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
117
+ const { cancelLoginFlow } = await import("../../oauth");
118
+ const cancelled = cancelLoginFlow(provider);
119
+ return jsonResponse({ ok: true, cancelled });
120
+ }
121
+
122
+ // Manual fallback for browser OAuth: paste the final redirect URL (or authorization code)
123
+ // when the browser cannot reach the loopback callback (remote/SSH/blocked localhost).
124
+ if (url.pathname === "/api/oauth/login/code" && req.method === "POST") {
125
+ const body = await req.json().catch(() => ({})) as { provider?: string; input?: string; code?: string };
126
+ const provider = (body.provider ?? "").trim().toLowerCase();
127
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
128
+ const input = typeof body.input === "string" ? body.input : typeof body.code === "string" ? body.code : "";
129
+ // Authorization responses are measured in hundreds of bytes; never accept the
130
+ // generic management-body allowance here.
131
+ if (input.length > 4096) return jsonResponse({ error: "input too long" }, 400);
132
+ const result = submitManualLoginCode(provider, input);
133
+ if (!result.ok) return jsonResponse({ error: result.error }, 409);
134
+ return jsonResponse({ ok: true });
135
+ }
136
+
137
+ if (url.pathname === "/api/oauth/status" && req.method === "GET") {
138
+ const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
139
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
140
+ return jsonResponse(getLoginStatus(provider));
141
+ }
142
+
143
+ if (url.pathname === "/api/oauth/logout" && req.method === "POST") {
144
+ const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
145
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
146
+ await removeCredential(provider);
147
+ clearLoginState(provider);
148
+ // Drop cached/last-good quota rows tied to the removed credential.
149
+ const { clearProviderQuotaCache } = await import("../../providers/quota");
150
+ clearProviderQuotaCache();
151
+ return jsonResponse({ success: true });
152
+ }
153
+
154
+ // Multiauth account management: list a provider's logged-in accounts, switch the active
155
+ // one, or remove one. Emails are masked; tokens never leave the store.
156
+ if (url.pathname === "/api/oauth/accounts" && req.method === "GET") {
157
+ const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
158
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
159
+ const status = getLoginStatus(provider);
160
+ return jsonResponse({ activeAccountId: status.activeAccountId ?? null, accounts: status.accounts ?? [] });
161
+ }
162
+ if (url.pathname === "/api/oauth/accounts/active" && req.method === "PUT") {
163
+ const body = await req.json().catch(() => ({})) as { provider?: string; accountId?: string };
164
+ const provider = (body.provider ?? "").trim().toLowerCase();
165
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
166
+ if (!body.accountId) return jsonResponse({ error: "missing accountId" }, 400);
167
+ const { setActiveAccount } = await import("../../oauth/store");
168
+ if (!(await setActiveAccount(provider, body.accountId))) return jsonResponse({ error: "account not found" }, 404);
169
+ const { clearProviderQuotaCache } = await import("../../providers/quota");
170
+ clearProviderQuotaCache();
171
+ return jsonResponse({ ok: true, provider, activeAccountId: body.accountId });
172
+ }
173
+ if (url.pathname === "/api/oauth/accounts/alias" && req.method === "PUT") {
174
+ const body = await req.json().catch(() => ({})) as { provider?: unknown; accountId?: unknown; alias?: unknown };
175
+ const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
176
+ const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
177
+ const alias = typeof body.alias === "string" ? body.alias.trim() : "";
178
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
179
+ if (!accountId) return jsonResponse({ error: "missing accountId" }, 400);
180
+ if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
181
+ return jsonResponse({ error: "alias must be at most 80 printable characters" }, 400);
182
+ }
183
+ const { setAccountAlias } = await import("../../oauth/store");
184
+ if (!(await setAccountAlias(provider, accountId, alias || undefined))) return jsonResponse({ error: "account not found" }, 404);
185
+ return jsonResponse({ ok: true, provider, accountId, alias: alias || null });
186
+ }
187
+ if (url.pathname === "/api/oauth/accounts" && req.method === "DELETE") {
188
+ const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
189
+ const id = url.searchParams.get("id") ?? "";
190
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
191
+ if (!id) return jsonResponse({ error: "missing id" }, 400);
192
+ const { removeAccount, getAccountSet } = await import("../../oauth/store");
193
+ if (!(await removeAccount(provider, id))) return jsonResponse({ error: "account not found" }, 404);
194
+ if (!getAccountSet(provider)) clearLoginState(provider);
195
+ const { clearProviderQuotaCache } = await import("../../providers/quota");
196
+ clearProviderQuotaCache();
197
+ return jsonResponse({ ok: true });
198
+ }
199
+
200
+ // Multi-key pool for API-key providers (same GUI dropdown as OAuth multiauth): list masked
201
+ // keys, add one (upserts + activates), switch the active key, or remove one. `apiKey` always
202
+ // mirrors the active entry so routing is untouched.
203
+ if (url.pathname === "/api/providers/keys" && req.method === "GET") {
204
+ const name = (url.searchParams.get("name") ?? "").trim();
205
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
206
+ const { listProviderApiKeys } = await import("../../providers/api-keys");
207
+ return jsonResponse(listProviderApiKeys(config, name));
208
+ }
209
+ if (url.pathname === "/api/providers/keys" && req.method === "POST") {
210
+ const body = await req.json().catch(() => ({})) as { name?: string; key?: string; label?: string };
211
+ const name = (body.name ?? "").trim();
212
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
213
+ if (typeof body.key !== "string" || !body.key.trim()) return jsonResponse({ error: "key is required" }, 400);
214
+ const { addProviderApiKey } = await import("../../providers/api-keys");
215
+ const result = addProviderApiKey(config, name, body.key, body.label);
216
+ if ("error" in result) return jsonResponse({ error: result.error }, 400);
217
+ const { clearModelCache } = await import("../../codex/model-cache");
218
+ clearModelCache(name);
219
+ const { clearProviderQuotaCache } = await import("../../providers/quota");
220
+ clearProviderQuotaCache();
221
+ const { clearKeyCooldowns } = await import("../../providers/key-failover");
222
+ clearKeyCooldowns(name); // manual key management resets 429 cooldown state
223
+ return jsonResponse({ ok: true, id: result.id }, 201);
224
+ }
225
+ if (url.pathname === "/api/providers/keys/active" && req.method === "PUT") {
226
+ const body = await req.json().catch(() => ({})) as { name?: string; id?: string };
227
+ const name = (body.name ?? "").trim();
228
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
229
+ if (!body.id) return jsonResponse({ error: "missing id" }, 400);
230
+ const { setActiveProviderApiKey } = await import("../../providers/api-keys");
231
+ if (!setActiveProviderApiKey(config, name, body.id)) return jsonResponse({ error: "key not found" }, 404);
232
+ const { clearModelCache } = await import("../../codex/model-cache");
233
+ clearModelCache(name);
234
+ const { clearProviderQuotaCache } = await import("../../providers/quota");
235
+ clearProviderQuotaCache();
236
+ const { clearKeyCooldowns } = await import("../../providers/key-failover");
237
+ clearKeyCooldowns(name); // manual key management resets 429 cooldown state
238
+ return jsonResponse({ ok: true, name, activeId: body.id });
239
+ }
240
+ if (url.pathname === "/api/providers/keys/alias" && req.method === "PUT") {
241
+ const body = await req.json().catch(() => ({})) as { name?: unknown; id?: unknown; alias?: unknown };
242
+ const name = typeof body.name === "string" ? body.name.trim() : "";
243
+ const id = typeof body.id === "string" ? body.id.trim() : "";
244
+ const alias = typeof body.alias === "string" ? body.alias.trim() : "";
245
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
246
+ if (!id) return jsonResponse({ error: "missing id" }, 400);
247
+ if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
248
+ return jsonResponse({ error: "alias must be at most 80 printable characters" }, 400);
249
+ }
250
+ const { setProviderApiKeyLabel } = await import("../../providers/api-keys");
251
+ if (!setProviderApiKeyLabel(config, name, id, alias || undefined)) return jsonResponse({ error: "key not found" }, 404);
252
+ return jsonResponse({ ok: true, name, id, alias: alias || null });
253
+ }
254
+ if (url.pathname === "/api/providers/keys" && req.method === "DELETE") {
255
+ const name = (url.searchParams.get("name") ?? "").trim();
256
+ const id = url.searchParams.get("id") ?? "";
257
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
258
+ if (!id) return jsonResponse({ error: "missing id" }, 400);
259
+ const { removeProviderApiKey } = await import("../../providers/api-keys");
260
+ if (!removeProviderApiKey(config, name, id)) return jsonResponse({ error: "key not found" }, 404);
261
+ const { clearModelCache } = await import("../../codex/model-cache");
262
+ clearModelCache(name);
263
+ const { clearProviderQuotaCache } = await import("../../providers/quota");
264
+ clearProviderQuotaCache();
265
+ const { clearKeyCooldowns } = await import("../../providers/key-failover");
266
+ clearKeyCooldowns(name); // manual key management resets 429 cooldown state
267
+ return jsonResponse({ ok: true });
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // API Keys management
272
+ // ---------------------------------------------------------------------------
273
+ if (url.pathname === "/api/keys" && req.method === "GET") {
274
+ const keys = config.apiKeys ?? [];
275
+ return jsonResponse({ keys: keys.map(k => ({ id: k.id, name: k.name, prefix: k.key.slice(0, 8) + "...", createdAt: k.createdAt })), endpoint: `http://${config.hostname ?? "127.0.0.1"}:${config.port ?? 10100}/v1/responses` }, 200, req, config);
276
+ }
277
+
278
+ if (url.pathname === "/api/keys" && req.method === "POST") {
279
+ const body = await req.json() as { name?: string };
280
+ const name = (body.name ?? "").trim() || "default";
281
+ // Generate key from provider keys hash + random salt
282
+ const providerKeys = Object.values(config.providers).map(p => p.apiKey ?? "").filter(Boolean).join("|");
283
+ const salt = crypto.randomUUID();
284
+ const hashInput = `${providerKeys}|${salt}|${Date.now()}`;
285
+ const hashBuf = new Bun.CryptoHasher("sha256").update(hashInput).digest();
286
+ const key = "ocx_" + Buffer.from(hashBuf).toString("hex").slice(0, 40);
287
+ const entry = { id: crypto.randomUUID(), name, key, createdAt: new Date().toISOString() };
288
+ config.apiKeys = [...(config.apiKeys ?? []), entry];
289
+ saveConfig(config);
290
+ return jsonResponse({ id: entry.id, name: entry.name, key: entry.key, createdAt: entry.createdAt }, 201, req, config);
291
+ }
292
+
293
+ if (url.pathname === "/api/keys" && req.method === "DELETE") {
294
+ const body = await req.json() as { id?: string };
295
+ if (!body.id) return jsonResponse({ error: "id required" }, 400, req, config);
296
+ config.apiKeys = (config.apiKeys ?? []).filter(k => k.id !== body.id);
297
+ saveConfig(config);
298
+ return jsonResponse({ success: true }, 200, req, config);
299
+ }
300
+ return null;
301
+ }