@omnicross/daemon 0.2.0 → 0.3.0

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.
package/dist/index.js CHANGED
@@ -11,7 +11,8 @@ import {
11
11
  DEFAULT_ACCOUNT_PROBE,
12
12
  DEFAULT_OUTBOUND_PORT,
13
13
  getOutboundApiServer,
14
- normalizeServerConfig as normalizeServerConfig2
14
+ normalizeServerConfig as normalizeServerConfig2,
15
+ validateSearchServerConfig as validateSearchServerConfig2
15
16
  } from "@omnicross/core/outbound-api";
16
17
  import { setSubscriptionRegistryForOutbound } from "@omnicross/core/outbound-api/subscriptionRegistryPort";
17
18
  import { getSharedAccountHealth as getSharedAccountHealth4 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
@@ -875,14 +876,16 @@ import http from "http";
875
876
  import {
876
877
  createNamedKey,
877
878
  DEFAULT_IMAGES_SERVER_CONFIG,
879
+ DEFAULT_SEARCH_SERVER_CONFIG as DEFAULT_SEARCH_SERVER_CONFIG2,
878
880
  effectiveOutboundPermissions as effectiveOutboundPermissions2,
879
881
  gatewayBindingToEndpointConfig,
880
882
  isKindMappedEndpoint,
881
- loadServerConfig as loadServerConfig2,
883
+ loadServerConfig as loadServerConfig3,
882
884
  mergeServerConfig,
883
885
  normalizeProxyConfig,
884
886
  saveServerConfig,
885
- validateOutboundPermissions
887
+ validateOutboundPermissions,
888
+ validateSearchServerConfig
886
889
  } from "@omnicross/core/outbound-api";
887
890
  import {
888
891
  IMAGE_CAPABILITY_UNAVAILABLE_REASONS
@@ -3783,6 +3786,496 @@ async function handleDashboard(deps) {
3783
3786
  return { status: 200, body: summary };
3784
3787
  }
3785
3788
 
3789
+ // src/admin/searchAdminApi.ts
3790
+ import { DEFAULT_SEARCH_SERVER_CONFIG, loadServerConfig } from "@omnicross/core/outbound-api";
3791
+ import { apiSearchContributions as apiSearchContributions2 } from "@omnicross/core/search/api";
3792
+ import { builtinHttpSearchContributions as builtinHttpSearchContributions3, createSearchHttpTransport as createSearchHttpTransport2 } from "@omnicross/core/search/http";
3793
+ import { createSearchRuntime as createSearchRuntime2 } from "@omnicross/core/search";
3794
+
3795
+ // src/search/searchDoctorProjection.ts
3796
+ import { toSearchErrorShape } from "@omnicross/contracts/search-types";
3797
+ import {
3798
+ JINA_CAPABILITIES,
3799
+ SEARXNG_CAPABILITIES,
3800
+ TAVILY_CAPABILITIES,
3801
+ ZHIPU_CAPABILITIES
3802
+ } from "@omnicross/core/search/api";
3803
+ import { builtinHttpSearchContributions } from "@omnicross/core/search/http";
3804
+ var API_DOCTOR_PROVIDERS = [
3805
+ {
3806
+ id: "tavily",
3807
+ capabilities: TAVILY_CAPABILITIES,
3808
+ configured: (configs) => configs.tavily !== void 0,
3809
+ missingReason: "no API key configured"
3810
+ },
3811
+ {
3812
+ id: "jina",
3813
+ capabilities: JINA_CAPABILITIES,
3814
+ configured: (configs) => configs.jina !== void 0,
3815
+ // Honest about the asymmetry: Jina CAN run keyless, but a provider nobody
3816
+ // asked for is still not enabled.
3817
+ missingReason: "not configured (Jina can run without a key, but must be enabled explicitly)"
3818
+ },
3819
+ {
3820
+ id: "searxng",
3821
+ capabilities: SEARXNG_CAPABILITIES,
3822
+ configured: (configs) => configs.searxng !== void 0,
3823
+ missingReason: "no API host configured"
3824
+ },
3825
+ {
3826
+ id: "zhipu",
3827
+ capabilities: ZHIPU_CAPABILITIES,
3828
+ configured: (configs) => configs.zhipu !== void 0,
3829
+ missingReason: "no API key configured"
3830
+ },
3831
+ {
3832
+ id: "z.ai",
3833
+ capabilities: ZHIPU_CAPABILITIES,
3834
+ configured: (configs) => configs["z.ai"] !== void 0,
3835
+ missingReason: "no API key configured"
3836
+ }
3837
+ ];
3838
+ function buildSearchDoctorSnapshot(contributions = builtinHttpSearchContributions(), apiConfigs) {
3839
+ const rows = contributions.map((contribution) => ({
3840
+ providerId: contribution.id,
3841
+ source: contribution.source,
3842
+ kind: contribution.kind,
3843
+ capabilities: contribution.capabilities
3844
+ }));
3845
+ if (apiConfigs === void 0) return rows;
3846
+ for (const provider of API_DOCTOR_PROVIDERS) {
3847
+ if (provider.configured(apiConfigs)) continue;
3848
+ rows.push({
3849
+ providerId: provider.id,
3850
+ source: "builtin",
3851
+ kind: "api",
3852
+ capabilities: provider.capabilities,
3853
+ status: "unconfigured",
3854
+ reason: provider.missingReason
3855
+ });
3856
+ }
3857
+ return rows;
3858
+ }
3859
+ var SEARCH_DOCTOR_QUERY = "MDN HTTP headers documentation";
3860
+ function classifyLiveSearchOutcome(providerId, outcome, checkedAt) {
3861
+ if (outcome.kind === "results") {
3862
+ if (outcome.count > 0) return { providerId, status: "healthy", checkedAt };
3863
+ return {
3864
+ providerId,
3865
+ status: "degraded",
3866
+ checkedAt,
3867
+ reason: "reachable, but the engine returned no usable results (possible partial drift)"
3868
+ };
3869
+ }
3870
+ const error = toSearchErrorShape(outcome.error);
3871
+ const stage = error.details?.stage;
3872
+ const { status, reason } = classifySearchFailure(stage, error.code);
3873
+ return { providerId, status, checkedAt, reason, error };
3874
+ }
3875
+ function classifySearchFailure(stage, code) {
3876
+ if (stage === "challenge") {
3877
+ return { status: "blocked", reason: "the engine served a bot challenge instead of results" };
3878
+ }
3879
+ if (stage === "trust") {
3880
+ return {
3881
+ status: "blocked",
3882
+ reason: "the engine served a page that failed the anti-decoy trust check"
3883
+ };
3884
+ }
3885
+ if (code === "policy_denied") {
3886
+ return {
3887
+ status: "blocked",
3888
+ reason: "the egress policy refused the request target"
3889
+ };
3890
+ }
3891
+ if (code === "parse_failed") {
3892
+ return {
3893
+ status: "failed",
3894
+ reason: "the response was not recognizable as a search result page (parser drift suspected)"
3895
+ };
3896
+ }
3897
+ if (code === "timeout") {
3898
+ return { status: "failed", reason: "the request exceeded its time budget" };
3899
+ }
3900
+ return { status: "failed", reason: `the request failed (${code})` };
3901
+ }
3902
+
3903
+ // src/search/SearchAssembly.ts
3904
+ import { resolveUpstreamDispatcher } from "@omnicross/core/pipeline/upstreamFetch";
3905
+ import { createSearchRuntime } from "@omnicross/core/search";
3906
+ import { apiSearchContributions } from "@omnicross/core/search/api";
3907
+ import {
3908
+ builtinHttpSearchContributions as builtinHttpSearchContributions2,
3909
+ createSearchHttpTransport
3910
+ } from "@omnicross/core/search/http";
3911
+ function searchEgressPolicyFrom(config) {
3912
+ const hosts = config.egress.allowedPrivateHosts;
3913
+ return hosts.length > 0 ? { allowedPrivateHosts: [...hosts] } : {};
3914
+ }
3915
+ function searchPolicyFrom(config) {
3916
+ const { preferred, allowed, fallbackEnabled, maxAttempts } = config.policy;
3917
+ return {
3918
+ ...preferred !== void 0 ? { preferred } : {},
3919
+ ...allowed !== void 0 ? { allowed: [...allowed] } : {},
3920
+ fallbackEnabled,
3921
+ ...maxAttempts !== void 0 ? { maxAttempts } : {}
3922
+ };
3923
+ }
3924
+ function resolveSearchUpstreamDispatcher(url) {
3925
+ return resolveUpstreamDispatcher({ url });
3926
+ }
3927
+ var searchUpstreamProxyConfig = createUpstreamProxyResolver();
3928
+ function resolveSearchUpstreamProxyConfig(url) {
3929
+ return searchUpstreamProxyConfig({ url });
3930
+ }
3931
+ function searchContributionsFrom(config) {
3932
+ return [
3933
+ ...builtinHttpSearchContributions2(
3934
+ createSearchHttpTransport({
3935
+ resolveProxyDispatcher: resolveSearchUpstreamDispatcher,
3936
+ resolveProxyConfig: resolveSearchUpstreamProxyConfig
3937
+ })
3938
+ ),
3939
+ ...apiSearchContributions(config.providers, {
3940
+ egressPolicy: searchEgressPolicyFrom(config),
3941
+ resolveProxyDispatcher: resolveSearchUpstreamDispatcher
3942
+ })
3943
+ ];
3944
+ }
3945
+ function buildSearchRuntime(config, options = {}) {
3946
+ const logger = options.logger ?? null;
3947
+ return createSearchRuntime({
3948
+ contributions: options.contributions ?? searchContributionsFrom(config),
3949
+ policy: searchPolicyFrom(config),
3950
+ ...logger ? {
3951
+ onEvent: (event) => {
3952
+ logger.debug(`[search] ${formatSearchEvent(event)}`);
3953
+ }
3954
+ } : {}
3955
+ });
3956
+ }
3957
+ function formatSearchEvent(event) {
3958
+ const parts = [
3959
+ `type=${event.type}`,
3960
+ `request=${event.requestId}`,
3961
+ `queryHash=${event.queryHash}`,
3962
+ `durationMs=${event.durationMs}`
3963
+ ];
3964
+ if ("providerId" in event && event.providerId !== void 0) {
3965
+ parts.push(`provider=${event.providerId}`);
3966
+ }
3967
+ if ("outcome" in event && event.outcome !== void 0) parts.push(`outcome=${event.outcome}`);
3968
+ if ("errorCode" in event && event.errorCode !== void 0) parts.push(`error=${event.errorCode}`);
3969
+ if ("resultCount" in event && event.resultCount !== void 0) {
3970
+ parts.push(`results=${event.resultCount}`);
3971
+ }
3972
+ if ("fallbackCount" in event && event.fallbackCount !== void 0) {
3973
+ parts.push(`fallbacks=${event.fallbackCount}`);
3974
+ }
3975
+ return parts.join(" ");
3976
+ }
3977
+
3978
+ // src/admin/searchAdminApi.ts
3979
+ var KEYLESS_HTTP_PROVIDER_IDS = /* @__PURE__ */ new Set(["http-bing", "http-duckduckgo"]);
3980
+ var API_PROVIDER_IDS = /* @__PURE__ */ new Set([
3981
+ "tavily",
3982
+ "jina",
3983
+ "searxng",
3984
+ "zhipu",
3985
+ "z.ai"
3986
+ ]);
3987
+ var SEARCH_QUERY_MAX_CODE_UNITS = 256;
3988
+ var QUERY_CONTROL_CHARS = /[\u0000-\u001f\u007f]/u;
3989
+ var SEARCH_RESULT_FIELD_CAPS = { title: 512, url: 2048, content: 1024 };
3990
+ var SEARCH_QUERY_MAX_RESULTS = 5;
3991
+ function sanitizeResultField(value, cap) {
3992
+ const text = typeof value === "string" ? value : value === null || value === void 0 ? "" : String(value);
3993
+ return text.replace(/[\u0000-\u001f\u007f]/gu, "").slice(0, cap);
3994
+ }
3995
+ function writeJson(res, status, body) {
3996
+ res.writeHead(status, { "Content-Type": "application/json" });
3997
+ res.end(JSON.stringify(body));
3998
+ }
3999
+ function writeErr(res, status, message) {
4000
+ writeJson(res, status, { error: { type: "admin_api_error", message } });
4001
+ }
4002
+ var SEARCH_MAX_BODY_BYTES = 64 * 1024;
4003
+ var SearchBodyTooLargeError = class extends Error {
4004
+ };
4005
+ async function readJsonBody2(req) {
4006
+ const chunks = [];
4007
+ let bytes = 0;
4008
+ for await (const chunk of req) {
4009
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4010
+ bytes += buffer.length;
4011
+ if (bytes > SEARCH_MAX_BODY_BYTES) throw new SearchBodyTooLargeError("request body is too large");
4012
+ chunks.push(buffer);
4013
+ }
4014
+ const raw = Buffer.concat(chunks).toString("utf8");
4015
+ if (!raw.trim()) return {};
4016
+ try {
4017
+ const parsed = JSON.parse(raw);
4018
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4019
+ } catch {
4020
+ return {};
4021
+ }
4022
+ }
4023
+ async function readBodyOrReject(req, res) {
4024
+ try {
4025
+ return await readJsonBody2(req);
4026
+ } catch (error) {
4027
+ if (error instanceof SearchBodyTooLargeError) {
4028
+ writeErr(res, 400, error.message);
4029
+ return void 0;
4030
+ }
4031
+ throw error;
4032
+ }
4033
+ }
4034
+ async function handleSearchAdmin(req, res, method, rest, deps) {
4035
+ if (rest.length === 1 && rest[0] === "diagnostics") {
4036
+ if (!deps.searchStatus) {
4037
+ return writeErr(res, 501, "Search status is not available in this build");
4038
+ }
4039
+ if (method !== "GET") {
4040
+ return writeErr(res, 405, `method ${method} not allowed on search diagnostics`);
4041
+ }
4042
+ return handleSearchDiagnostics(res, deps);
4043
+ }
4044
+ if (rest.length === 1 && rest[0] === "test") {
4045
+ if (!deps.searchStatus) {
4046
+ return writeErr(res, 501, "Search status is not available in this build");
4047
+ }
4048
+ if (method !== "POST") {
4049
+ return writeErr(res, 405, `method ${method} not allowed on search test`);
4050
+ }
4051
+ return handleSearchTest(req, res, deps);
4052
+ }
4053
+ if (rest.length === 1 && rest[0] === "query") {
4054
+ if (!deps.searchStatus) {
4055
+ return writeErr(res, 501, "Search status is not available in this build");
4056
+ }
4057
+ if (method !== "POST") {
4058
+ return writeErr(res, 405, `method ${method} not allowed on search query`);
4059
+ }
4060
+ return handleSearchQuery(req, res, deps);
4061
+ }
4062
+ return writeErr(res, 404, `unknown search route '/${rest.join("/")}'`);
4063
+ }
4064
+ async function handleSearchDiagnostics(res, deps) {
4065
+ const status = deps.searchStatus;
4066
+ const persisted = await loadServerConfig(deps.settingsStore);
4067
+ const search = persisted.search ?? DEFAULT_SEARCH_SERVER_CONFIG;
4068
+ const rows = buildSearchDoctorSnapshot(
4069
+ status.runtime.listProviders(),
4070
+ search.providers
4071
+ );
4072
+ const snapshot = {
4073
+ rows,
4074
+ modes: {
4075
+ // codex is read from the LIVE config per request — an admin PUT has
4076
+ // already applied. responses/anthropic were captured at bootstrap.
4077
+ codex: search.modes.codex,
4078
+ responses: status.modes.responses,
4079
+ anthropic: status.modes.anthropic
4080
+ },
4081
+ applySemantics: { codex: "immediate", rest: "restart" }
4082
+ };
4083
+ return writeJson(res, 200, { diagnostics: snapshot });
4084
+ }
4085
+ function persistedSearchContributions(search, fetchImpl) {
4086
+ if (fetchImpl) {
4087
+ const egressPolicy = searchEgressPolicyFrom(search);
4088
+ return [
4089
+ ...builtinHttpSearchContributions3(
4090
+ createSearchHttpTransport2({ fetch: fetchImpl, egressPolicy })
4091
+ ),
4092
+ ...apiSearchContributions2(search.providers, { egressPolicy, fetchImpl })
4093
+ ];
4094
+ }
4095
+ return searchContributionsFrom(search);
4096
+ }
4097
+ async function handleSearchTest(req, res, deps) {
4098
+ const status = deps.searchStatus;
4099
+ const body = await readBodyOrReject(req, res);
4100
+ if (body === void 0) return;
4101
+ const providerId = body["providerId"];
4102
+ if (typeof providerId !== "string" || providerId.length === 0) {
4103
+ return writeErr(res, 400, "providerId must be a non-empty string");
4104
+ }
4105
+ if (!KEYLESS_HTTP_PROVIDER_IDS.has(providerId) && !API_PROVIDER_IDS.has(providerId)) {
4106
+ return writeErr(res, 404, `unknown search provider '${providerId}'`);
4107
+ }
4108
+ const persisted = await loadServerConfig(deps.settingsStore);
4109
+ const search = persisted.search ?? DEFAULT_SEARCH_SERVER_CONFIG;
4110
+ const providers = search.providers;
4111
+ if (!KEYLESS_HTTP_PROVIDER_IDS.has(providerId) && providers[providerId] === void 0) {
4112
+ return writeErr(res, 400, `search provider '${providerId}' is not configured`);
4113
+ }
4114
+ const fetchImpl = status.testFetch;
4115
+ const contributions = persistedSearchContributions(search, fetchImpl);
4116
+ const contribution = contributions.find((c) => c.id === providerId);
4117
+ if (!contribution) {
4118
+ return writeErr(res, 400, `search provider '${providerId}' is not configured`);
4119
+ }
4120
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
4121
+ try {
4122
+ const results = await contribution.provider.search(SEARCH_DOCTOR_QUERY, { maxResults: 5 });
4123
+ const diagnostic = classifyLiveSearchOutcome(
4124
+ contribution.id,
4125
+ { kind: "results", count: results.length },
4126
+ checkedAt
4127
+ );
4128
+ const response = { diagnostic, resultCount: results.length };
4129
+ return writeJson(res, 200, { result: response });
4130
+ } catch (error) {
4131
+ const diagnostic = classifyLiveSearchOutcome(contribution.id, { kind: "failure", error }, checkedAt);
4132
+ const response = { diagnostic };
4133
+ return writeJson(res, 200, { result: response });
4134
+ }
4135
+ }
4136
+ async function handleSearchQuery(req, res, deps) {
4137
+ const status = deps.searchStatus;
4138
+ const body = await readBodyOrReject(req, res);
4139
+ if (body === void 0) return;
4140
+ const providerId = body["providerId"];
4141
+ if (typeof providerId !== "string" || providerId.length === 0) {
4142
+ return writeErr(res, 400, "providerId must be a non-empty string");
4143
+ }
4144
+ if (!KEYLESS_HTTP_PROVIDER_IDS.has(providerId) && !API_PROVIDER_IDS.has(providerId)) {
4145
+ return writeErr(res, 404, `unknown search provider '${providerId}'`);
4146
+ }
4147
+ const query2 = body["query"];
4148
+ if (typeof query2 !== "string" || query2.trim().length === 0) {
4149
+ return writeErr(res, 400, "query must be a non-empty string");
4150
+ }
4151
+ if (query2.length > SEARCH_QUERY_MAX_CODE_UNITS) {
4152
+ return writeErr(res, 400, `query must be at most ${SEARCH_QUERY_MAX_CODE_UNITS} characters`);
4153
+ }
4154
+ if (QUERY_CONTROL_CHARS.test(query2)) {
4155
+ return writeErr(res, 400, "query must not contain control characters");
4156
+ }
4157
+ const persisted = await loadServerConfig(deps.settingsStore);
4158
+ const search = persisted.search ?? DEFAULT_SEARCH_SERVER_CONFIG;
4159
+ const providers = search.providers;
4160
+ if (!KEYLESS_HTTP_PROVIDER_IDS.has(providerId) && providers[providerId] === void 0) {
4161
+ return writeErr(res, 400, `search provider '${providerId}' is not configured`);
4162
+ }
4163
+ const fetchImpl = status.testFetch;
4164
+ const runtime = createSearchRuntime2({
4165
+ contributions: persistedSearchContributions(search, fetchImpl),
4166
+ policy: {
4167
+ ...searchPolicyFrom(search),
4168
+ // The panel always walks: it answers "does a search WORK for this
4169
+ // operator", not "does this one provider behave" — that is `/test`'s
4170
+ // job. The persisted policy's allowlist still bounds the walk.
4171
+ fallbackEnabled: true,
4172
+ preferred: providerId
4173
+ }
4174
+ });
4175
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
4176
+ try {
4177
+ const orchestrated = await runtime.search({ query: query2, options: { maxResults: 5 } });
4178
+ const results = orchestrated.results;
4179
+ const sanitized = results.slice(0, SEARCH_QUERY_MAX_RESULTS).map((result) => ({
4180
+ title: sanitizeResultField(result.title, SEARCH_RESULT_FIELD_CAPS.title),
4181
+ url: sanitizeResultField(result.url, SEARCH_RESULT_FIELD_CAPS.url),
4182
+ content: sanitizeResultField(result.content, SEARCH_RESULT_FIELD_CAPS.content)
4183
+ }));
4184
+ const diagnostic = sanitized.length === 0 ? { providerId: orchestrated.providerId, status: "healthy", checkedAt } : classifyLiveSearchOutcome(
4185
+ orchestrated.providerId,
4186
+ { kind: "results", count: sanitized.length },
4187
+ checkedAt
4188
+ );
4189
+ const response = {
4190
+ diagnostic,
4191
+ providerUsed: orchestrated.providerId,
4192
+ fallbackCount: orchestrated.fallbackCount,
4193
+ resultCount: sanitized.length,
4194
+ results: sanitized
4195
+ };
4196
+ return writeJson(res, 200, { result: response });
4197
+ } catch (error) {
4198
+ const diagnostic = classifyLiveSearchOutcome(providerId, { kind: "failure", error }, checkedAt);
4199
+ const response = { diagnostic };
4200
+ return writeJson(res, 200, { result: response });
4201
+ }
4202
+ }
4203
+
4204
+ // src/admin/searchAdminView.ts
4205
+ var API_KEY_PROVIDERS = /* @__PURE__ */ new Set(["tavily", "jina", "zhipu", "z.ai"]);
4206
+ var BASIC_AUTH_PROVIDERS = /* @__PURE__ */ new Set(["searxng"]);
4207
+ function isRecord(value) {
4208
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4209
+ }
4210
+ function redactSearchServerConfig(search) {
4211
+ const providers = {};
4212
+ for (const [id, raw] of Object.entries(search.providers)) {
4213
+ const entry = raw;
4214
+ const view = {};
4215
+ if (entry.apiHost !== void 0) view.apiHost = entry.apiHost;
4216
+ if (entry.basicAuthUsername !== void 0) view.basicAuthUsername = entry.basicAuthUsername;
4217
+ if (API_KEY_PROVIDERS.has(id)) {
4218
+ view.apiKeyConfigured = typeof entry.apiKey === "string" && entry.apiKey.length > 0;
4219
+ }
4220
+ if (BASIC_AUTH_PROVIDERS.has(id)) {
4221
+ view.basicAuthPasswordConfigured = typeof entry.basicAuthPassword === "string" && entry.basicAuthPassword.length > 0;
4222
+ }
4223
+ providers[id] = view;
4224
+ }
4225
+ return {
4226
+ modes: search.modes,
4227
+ providers,
4228
+ egress: { allowedPrivateHosts: [...search.egress.allowedPrivateHosts] },
4229
+ policy: { ...search.policy, ...search.policy.allowed ? { allowed: [...search.policy.allowed] } : {} }
4230
+ };
4231
+ }
4232
+ function storedSecrets(current, id) {
4233
+ const entry = current.providers[id];
4234
+ if (!entry) return {};
4235
+ const out = {};
4236
+ if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) out.apiKey = entry.apiKey;
4237
+ if (typeof entry.basicAuthPassword === "string" && entry.basicAuthPassword.length > 0) {
4238
+ out.basicAuthPassword = entry.basicAuthPassword;
4239
+ }
4240
+ return out;
4241
+ }
4242
+ function resolveSecretField(entry, field, stored) {
4243
+ if (!(field in entry)) {
4244
+ if (stored !== void 0) entry[field] = stored;
4245
+ return;
4246
+ }
4247
+ const value = entry[field];
4248
+ if (value === null) {
4249
+ delete entry[field];
4250
+ return;
4251
+ }
4252
+ if (typeof value === "string" && value.trim().length > 0) return;
4253
+ if (stored !== void 0) entry[field] = stored;
4254
+ else delete entry[field];
4255
+ }
4256
+ function preserveSearchSecrets(incoming, current) {
4257
+ if (!isRecord(incoming)) return incoming;
4258
+ const section = { ...incoming };
4259
+ const providersValue = section["providers"];
4260
+ if (!isRecord(providersValue)) return section;
4261
+ const providers = {};
4262
+ for (const [id, entryValue] of Object.entries(providersValue)) {
4263
+ if (!isRecord(entryValue)) {
4264
+ providers[id] = entryValue;
4265
+ continue;
4266
+ }
4267
+ const entry = { ...entryValue };
4268
+ delete entry["apiKeyConfigured"];
4269
+ delete entry["basicAuthPasswordConfigured"];
4270
+ const stored = storedSecrets(current, id);
4271
+ resolveSecretField(entry, "apiKey", stored.apiKey);
4272
+ resolveSecretField(entry, "basicAuthPassword", stored.basicAuthPassword);
4273
+ providers[id] = entry;
4274
+ }
4275
+ section["providers"] = providers;
4276
+ return section;
4277
+ }
4278
+
3786
4279
  // src/admin/keyPolicyBody.ts
3787
4280
  function parseKeyPolicyBody(body) {
3788
4281
  const policy = {};
@@ -3845,7 +4338,7 @@ function parseKeyPolicyBody(body) {
3845
4338
  var ENDPOINTS = /* @__PURE__ */ new Set(["chat", "responses", "messages", "gemini"]);
3846
4339
  var TARGET_KINDS = /* @__PURE__ */ new Set(["account", "account-group", "account-pool", "provider"]);
3847
4340
  var FALLBACKS = /* @__PURE__ */ new Set(["next", "fail", "global"]);
3848
- function isRecord(value) {
4341
+ function isRecord2(value) {
3849
4342
  return !!value && typeof value === "object" && !Array.isArray(value);
3850
4343
  }
3851
4344
  function nonBlank(value) {
@@ -3865,7 +4358,7 @@ function validateGatewayBindingsSegment(patch) {
3865
4358
  const ids = /* @__PURE__ */ new Set();
3866
4359
  raw.forEach((entry, index) => {
3867
4360
  const path2 = `bindings[${index}]`;
3868
- if (!isRecord(entry)) {
4361
+ if (!isRecord2(entry)) {
3869
4362
  errors.push(`${path2} must be an object`);
3870
4363
  return;
3871
4364
  }
@@ -3894,12 +4387,12 @@ function validateGatewayBindingsSegment(patch) {
3894
4387
  } else if (entry.modelMappings.length > 100) {
3895
4388
  errors.push(`${path2}.modelMappings cannot contain more than 100 entries`);
3896
4389
  } else if (entry.modelMappings.some(
3897
- (mapping) => !isRecord(mapping) || !nonBlank(mapping.source) || !nonBlank(mapping.target)
4390
+ (mapping) => !isRecord2(mapping) || !nonBlank(mapping.source) || !nonBlank(mapping.target)
3898
4391
  )) {
3899
4392
  errors.push(`${path2}.modelMappings must contain non-empty source and target strings`);
3900
4393
  }
3901
4394
  }
3902
- if (!isRecord(entry.target) || !TARGET_KINDS.has(String(entry.target.kind))) {
4395
+ if (!isRecord2(entry.target) || !TARGET_KINDS.has(String(entry.target.kind))) {
3903
4396
  errors.push(`${path2}.target is invalid`);
3904
4397
  } else {
3905
4398
  if (!nonBlank(entry.target.providerId)) errors.push(`${path2}.target.providerId is required`);
@@ -3914,7 +4407,7 @@ function validateGatewayBindingsSegment(patch) {
3914
4407
  }
3915
4408
  }
3916
4409
  if (entry.modelMap !== void 0) {
3917
- if (!isRecord(entry.modelMap) || Object.values(entry.modelMap).some((value) => typeof value !== "string")) {
4410
+ if (!isRecord2(entry.modelMap) || Object.values(entry.modelMap).some((value) => typeof value !== "string")) {
3918
4411
  errors.push(`${path2}.modelMap must contain string values`);
3919
4412
  }
3920
4413
  }
@@ -3935,19 +4428,19 @@ function validateGatewayBindingsSegment(patch) {
3935
4428
  import {
3936
4429
  generateVoucherCode,
3937
4430
  hashVoucherCode,
3938
- loadServerConfig,
4431
+ loadServerConfig as loadServerConfig2,
3939
4432
  newVoucherId,
3940
4433
  toVoucherInfo,
3941
4434
  voucherCodePrefix
3942
4435
  } from "@omnicross/core/outbound-api";
3943
- function writeJson(res, status, body) {
4436
+ function writeJson2(res, status, body) {
3944
4437
  res.writeHead(status, { "Content-Type": "application/json" });
3945
4438
  res.end(JSON.stringify(body));
3946
4439
  }
3947
- function writeErr(res, status, message) {
3948
- writeJson(res, status, { error: { type: "voucher_error", message } });
4440
+ function writeErr2(res, status, message) {
4441
+ writeJson2(res, status, { error: { type: "voucher_error", message } });
3949
4442
  }
3950
- function readJsonBody2(req) {
4443
+ function readJsonBody3(req) {
3951
4444
  return new Promise((resolve10, reject) => {
3952
4445
  const chunks = [];
3953
4446
  req.on("data", (c) => chunks.push(c));
@@ -3998,26 +4491,26 @@ function parseVoucherCreateBody(body) {
3998
4491
  return { ok: true, input };
3999
4492
  }
4000
4493
  async function voucherEnabled(deps) {
4001
- const config = await loadServerConfig(deps.settingsStore);
4494
+ const config = await loadServerConfig2(deps.settingsStore);
4002
4495
  return config.voucher?.enabled === true;
4003
4496
  }
4004
4497
  async function handleVoucher(req, res, method, rest, deps) {
4005
4498
  const voucherDb = deps.voucherDb;
4006
- if (!voucherDb) return writeErr(res, 501, "Voucher feature is not available");
4499
+ if (!voucherDb) return writeErr2(res, 501, "Voucher feature is not available");
4007
4500
  if (method === "GET" && rest.length === 0) {
4008
4501
  const rows = await voucherDb.voucherList();
4009
- return writeJson(res, 200, { vouchers: rows.map(toVoucherInfo) });
4502
+ return writeJson2(res, 200, { vouchers: rows.map(toVoucherInfo) });
4010
4503
  }
4011
4504
  if (method === "POST" && rest.length === 0) {
4012
- if (!await voucherEnabled(deps)) return writeErr(res, 403, "Voucher feature is disabled");
4505
+ if (!await voucherEnabled(deps)) return writeErr2(res, 403, "Voucher feature is disabled");
4013
4506
  let body;
4014
4507
  try {
4015
- body = await readJsonBody2(req);
4508
+ body = await readJsonBody3(req);
4016
4509
  } catch {
4017
- return writeErr(res, 400, "Invalid JSON in request body");
4510
+ return writeErr2(res, 400, "Invalid JSON in request body");
4018
4511
  }
4019
4512
  const parsed = parseVoucherCreateBody(body);
4020
- if (!parsed.ok) return writeErr(res, 400, parsed.message);
4513
+ if (!parsed.ok) return writeErr2(res, 400, parsed.message);
4021
4514
  const code = generateVoucherCode();
4022
4515
  const created = await voucherDb.voucherCreate({
4023
4516
  id: newVoucherId(),
@@ -4025,7 +4518,7 @@ async function handleVoucher(req, res, method, rest, deps) {
4025
4518
  codePrefix: voucherCodePrefix(code),
4026
4519
  ...parsed.input
4027
4520
  });
4028
- return writeJson(res, 201, {
4521
+ return writeJson2(res, 201, {
4029
4522
  id: created.id,
4030
4523
  codePrefix: created.codePrefix,
4031
4524
  type: created.type,
@@ -4036,11 +4529,11 @@ async function handleVoucher(req, res, method, rest, deps) {
4036
4529
  }
4037
4530
  const id = rest[0];
4038
4531
  if (method === "POST" && id && rest[1] === "revoke") {
4039
- if (!await voucherEnabled(deps)) return writeErr(res, 403, "Voucher feature is disabled");
4532
+ if (!await voucherEnabled(deps)) return writeErr2(res, 403, "Voucher feature is disabled");
4040
4533
  const ok = await voucherDb.voucherRevokeCas(id, Date.now());
4041
- return writeJson(res, ok ? 200 : 409, { ok });
4534
+ return writeJson2(res, ok ? 200 : 409, { ok });
4042
4535
  }
4043
- return writeErr(res, 405, `method ${method} not allowed on voucher`);
4536
+ return writeErr2(res, 405, `method ${method} not allowed on voucher`);
4044
4537
  }
4045
4538
 
4046
4539
  // src/admin/webhookConfigBody.ts
@@ -4970,12 +5463,12 @@ async function handlePricingResolveConflicts(body, deps) {
4970
5463
  }
4971
5464
 
4972
5465
  // src/admin/accountAllowanceApi.ts
4973
- function writeJson2(res, status, body) {
5466
+ function writeJson3(res, status, body) {
4974
5467
  res.writeHead(status, { "Content-Type": "application/json" });
4975
5468
  res.end(JSON.stringify(body));
4976
5469
  }
4977
5470
  function writeError2(res, status, message) {
4978
- writeJson2(res, status, { error: { type: "account_allowance_error", message } });
5471
+ writeJson3(res, status, { error: { type: "account_allowance_error", message } });
4979
5472
  }
4980
5473
  function readJson2(req) {
4981
5474
  return new Promise((resolve10, reject) => {
@@ -5008,7 +5501,7 @@ async function handleAccountAllowanceApi(req, res, method, rest, service) {
5008
5501
  if (!service.getSchedulingStatus) {
5009
5502
  return writeError2(res, 501, "allowance scheduling diagnostics are not available");
5010
5503
  }
5011
- return writeJson2(res, 200, { scheduling: service.getSchedulingStatus() });
5504
+ return writeJson3(res, 200, { scheduling: service.getSchedulingStatus() });
5012
5505
  }
5013
5506
  if (method === "GET") {
5014
5507
  const params = query(req);
@@ -5017,7 +5510,7 @@ async function handleAccountAllowanceApi(req, res, method, rest, service) {
5017
5510
  if (providerId === null) return writeError2(res, 400, "providerId must be claude or codex");
5018
5511
  const accountId = rest.length >= 2 ? rest[1] : params.get("accountId") ?? void 0;
5019
5512
  const allowances = await service.list({ providerId, accountId });
5020
- return writeJson2(res, 200, { allowances });
5513
+ return writeJson3(res, 200, { allowances });
5021
5514
  }
5022
5515
  if (method === "POST" && rest[0] === "refresh") {
5023
5516
  const body = await readJson2(req);
@@ -5032,7 +5525,7 @@ async function handleAccountAllowanceApi(req, res, method, rest, service) {
5032
5525
  if (accountId && allowances.length === 0) {
5033
5526
  return writeError2(res, 404, `Claude account '${accountId}' not found`);
5034
5527
  }
5035
- return writeJson2(res, 200, { allowances });
5528
+ return writeJson3(res, 200, { allowances });
5036
5529
  }
5037
5530
  return writeError2(res, 405, `method ${method} not allowed on account allowances`);
5038
5531
  }
@@ -5051,7 +5544,7 @@ function readBody(req) {
5051
5544
  req.on("error", reject);
5052
5545
  });
5053
5546
  }
5054
- async function readJsonBody3(req) {
5547
+ async function readJsonBody4(req) {
5055
5548
  const raw = await readBody(req);
5056
5549
  if (!raw.trim()) return {};
5057
5550
  try {
@@ -5061,12 +5554,12 @@ async function readJsonBody3(req) {
5061
5554
  return {};
5062
5555
  }
5063
5556
  }
5064
- function writeJson3(res, status, body) {
5557
+ function writeJson4(res, status, body) {
5065
5558
  res.writeHead(status, { "Content-Type": "application/json" });
5066
5559
  res.end(JSON.stringify(body));
5067
5560
  }
5068
5561
  function writeJsonError(res, status, message) {
5069
- writeJson3(res, status, { error: { type: "admin_api_error", message } });
5562
+ writeJson4(res, status, { error: { type: "admin_api_error", message } });
5070
5563
  }
5071
5564
  function maskProviderApiKey(apiKey) {
5072
5565
  if (!apiKey) return "";
@@ -5173,6 +5666,8 @@ async function handleAdminApi(req, res, path2, deps) {
5173
5666
  return await handleServer(req, res, method, deps);
5174
5667
  case "images":
5175
5668
  return await handleImages(res, method, rest, deps);
5669
+ case "search":
5670
+ return await handleSearchAdmin(req, res, method, rest, deps);
5176
5671
  case "accounts":
5177
5672
  return await handleAccounts(req, res, method, rest, deps);
5178
5673
  case "cli":
@@ -5206,7 +5701,7 @@ function requestQuery(req) {
5206
5701
  return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
5207
5702
  }
5208
5703
  function writeResult(res, result) {
5209
- writeJson3(res, result.status, result.body);
5704
+ writeJson4(res, result.status, result.body);
5210
5705
  }
5211
5706
  async function handleUsage(req, res, method, rest, deps) {
5212
5707
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
@@ -5215,13 +5710,13 @@ async function handleUsage(req, res, method, rest, deps) {
5215
5710
  async function handleDashboardRoute(res, method, deps) {
5216
5711
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
5217
5712
  const result = await handleDashboard(deps);
5218
- return writeJson3(res, result.status, result.body);
5713
+ return writeJson4(res, result.status, result.body);
5219
5714
  }
5220
5715
  async function handlePricing(req, res, method, rest, deps) {
5221
5716
  if (rest.length === 0) {
5222
5717
  if (method === "GET") return writeResult(res, await handlePricingList(deps));
5223
5718
  if (method === "PUT") {
5224
- return writeResult(res, await handlePricingUpsert(await readJsonBody3(req), deps));
5719
+ return writeResult(res, await handlePricingUpsert(await readJsonBody4(req), deps));
5225
5720
  }
5226
5721
  if (method === "DELETE") {
5227
5722
  return writeResult(res, await handlePricingDelete(requestQuery(req), deps));
@@ -5232,7 +5727,7 @@ async function handlePricing(req, res, method, rest, deps) {
5232
5727
  return writeResult(res, await handlePricingFetchLatest(deps));
5233
5728
  }
5234
5729
  if (method === "POST" && rest.length === 1 && rest[0] === "resolve-conflicts") {
5235
- return writeResult(res, await handlePricingResolveConflicts(await readJsonBody3(req), deps));
5730
+ return writeResult(res, await handlePricingResolveConflicts(await readJsonBody4(req), deps));
5236
5731
  }
5237
5732
  return writeJsonError(res, 404, `unknown pricing route '${rest.join("/")}'`);
5238
5733
  }
@@ -5246,15 +5741,15 @@ function migrationDeps(deps) {
5246
5741
  }
5247
5742
  async function handleMigrationExport(req, res, method, deps) {
5248
5743
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
5249
- const body = await readJsonBody3(req);
5744
+ const body = await readJsonBody4(req);
5250
5745
  const result = await handleExport(body, migrationDeps(deps));
5251
- return writeJson3(res, result.status, result.body);
5746
+ return writeJson4(res, result.status, result.body);
5252
5747
  }
5253
5748
  async function handleMigrationImport(req, res, method, deps) {
5254
5749
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
5255
- const body = await readJsonBody3(req);
5750
+ const body = await readJsonBody4(req);
5256
5751
  const result = await handleImport(body, migrationDeps(deps));
5257
- return writeJson3(res, result.status, result.body);
5752
+ return writeJson4(res, result.status, result.body);
5258
5753
  }
5259
5754
  async function handleProviders(req, res, method, rest, deps) {
5260
5755
  const cfg = loadConfig(deps.configPath);
@@ -5285,13 +5780,13 @@ async function handleProviders(req, res, method, rest, deps) {
5285
5780
  if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
5286
5781
  const row = cfg.providers.find((p) => p.id === rest[0]);
5287
5782
  if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
5288
- return writeJson3(res, 200, { apiKey: row.apiKey ?? "" });
5783
+ return writeJson4(res, 200, { apiKey: row.apiKey ?? "" });
5289
5784
  }
5290
5785
  if (method === "GET") {
5291
- return writeJson3(res, 200, { providers: cfg.providers.map(toProviderView) });
5786
+ return writeJson4(res, 200, { providers: cfg.providers.map(toProviderView) });
5292
5787
  }
5293
5788
  if (method === "POST") {
5294
- const body = await readJsonBody3(req);
5789
+ const body = await readJsonBody4(req);
5295
5790
  const provider = parseProviderInput(body, void 0);
5296
5791
  if (!provider) return writeJsonError(res, 400, "invalid provider (id, apiFormat, baseUrl required)");
5297
5792
  if (cfg.providers.some((p) => p.id === provider.id)) {
@@ -5299,25 +5794,25 @@ async function handleProviders(req, res, method, rest, deps) {
5299
5794
  }
5300
5795
  cfg.providers.push(provider);
5301
5796
  persistProviders(cfg, deps);
5302
- return writeJson3(res, 201, { provider: toProviderView(provider) });
5797
+ return writeJson4(res, 201, { provider: toProviderView(provider) });
5303
5798
  }
5304
5799
  const id = rest[0];
5305
5800
  if (!id) return writeJsonError(res, 400, "provider id required in path");
5306
5801
  const idx = cfg.providers.findIndex((p) => p.id === id);
5307
5802
  if (idx < 0) return writeJsonError(res, 404, `provider '${id}' not found`);
5308
5803
  if (method === "PUT") {
5309
- const body = await readJsonBody3(req);
5804
+ const body = await readJsonBody4(req);
5310
5805
  const existing = cfg.providers[idx];
5311
5806
  const updated = parseProviderInput(body, existing);
5312
5807
  if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
5313
5808
  cfg.providers[idx] = updated;
5314
5809
  persistProviders(cfg, deps);
5315
- return writeJson3(res, 200, { provider: toProviderView(updated) });
5810
+ return writeJson4(res, 200, { provider: toProviderView(updated) });
5316
5811
  }
5317
5812
  if (method === "DELETE") {
5318
5813
  cfg.providers.splice(idx, 1);
5319
5814
  persistProviders(cfg, deps);
5320
- return writeJson3(res, 200, { ok: true });
5815
+ return writeJson4(res, 200, { ok: true });
5321
5816
  }
5322
5817
  return writeJsonError(res, 405, `method ${method} not allowed on providers`);
5323
5818
  }
@@ -5326,7 +5821,7 @@ function persistProviders(cfg, deps) {
5326
5821
  deps.llmConfig.reload(cfg);
5327
5822
  }
5328
5823
  async function handleProviderReorder(req, res, cfg, deps) {
5329
- const body = await readJsonBody3(req);
5824
+ const body = await readJsonBody4(req);
5330
5825
  const rawOrder = body["order"];
5331
5826
  if (!Array.isArray(rawOrder)) {
5332
5827
  return writeJsonError(res, 400, "reorder requires { order: string[] }");
@@ -5350,14 +5845,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
5350
5845
  }
5351
5846
  cfg.providers = reordered;
5352
5847
  persistProviders(cfg, deps);
5353
- return writeJson3(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
5848
+ return writeJson4(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
5354
5849
  }
5355
5850
  async function handleDiscoverModels(res, id, cfg) {
5356
5851
  if (!id) return writeJsonError(res, 400, "provider id required in path");
5357
5852
  const row = cfg.providers.find((p) => p.id === id);
5358
5853
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
5359
5854
  if (row.apiFormat !== "openai" && row.apiFormat !== "openai-response") {
5360
- return writeJson3(res, 200, { models: [], unsupportedFormat: true });
5855
+ return writeJson4(res, 200, { models: [], unsupportedFormat: true });
5361
5856
  }
5362
5857
  const resolvedKey = resolveEnvKey(row.apiKey);
5363
5858
  const base = row.baseUrl.replace(/\/+$/, "");
@@ -5374,32 +5869,32 @@ async function handleDiscoverModels(res, id, cfg) {
5374
5869
  message = parsed?.error?.message || parsed?.message || message;
5375
5870
  } catch {
5376
5871
  }
5377
- return writeJson3(res, 200, {
5872
+ return writeJson4(res, 200, {
5378
5873
  models: [],
5379
5874
  error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
5380
5875
  });
5381
5876
  }
5382
5877
  const data = await response.json();
5383
5878
  const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
5384
- return writeJson3(res, 200, { models });
5879
+ return writeJson4(res, 200, { models });
5385
5880
  } catch (err5) {
5386
5881
  const message = err5 instanceof Error ? err5.message : String(err5);
5387
- return writeJson3(res, 200, { models: [], error: `discovery failed: ${message}` });
5882
+ return writeJson4(res, 200, { models: [], error: `discovery failed: ${message}` });
5388
5883
  }
5389
5884
  }
5390
5885
  async function handleTestModel(req, res, id, cfg) {
5391
5886
  if (!id) return writeJsonError(res, 400, "provider id required in path");
5392
5887
  const row = cfg.providers.find((p) => p.id === id);
5393
5888
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
5394
- const body = await readJsonBody3(req);
5889
+ const body = await readJsonBody4(req);
5395
5890
  const model = typeof body["model"] === "string" ? body["model"].trim() : "";
5396
5891
  if (!model) return writeJsonError(res, 400, "test requires a { model } string");
5397
5892
  if (row.apiFormat === "gemini") {
5398
- return writeJson3(res, 200, { ok: false, unsupportedFormat: true });
5893
+ return writeJson4(res, 200, { ok: false, unsupportedFormat: true });
5399
5894
  }
5400
5895
  const resolvedKey = resolveEnvKey(row.apiKey);
5401
5896
  if (!resolvedKey) {
5402
- return writeJson3(res, 200, { ok: false, message: "no API key configured for this provider" });
5897
+ return writeJson4(res, 200, { ok: false, message: "no API key configured for this provider" });
5403
5898
  }
5404
5899
  let url = row.baseUrl.replace(/\/+$/, "");
5405
5900
  const prompt = "Reply with the single word: OK.";
@@ -5438,9 +5933,9 @@ async function handleTestModel(req, res, id, cfg) {
5438
5933
  message = parsed?.error?.message || parsed?.message || message;
5439
5934
  } catch {
5440
5935
  }
5441
- return writeJson3(res, 200, { ok: false, status: response.status, latencyMs, message });
5936
+ return writeJson4(res, 200, { ok: false, status: response.status, latencyMs, message });
5442
5937
  }
5443
- return writeJson3(res, 200, {
5938
+ return writeJson4(res, 200, {
5444
5939
  ok: true,
5445
5940
  status: response.status,
5446
5941
  latencyMs,
@@ -5448,7 +5943,7 @@ async function handleTestModel(req, res, id, cfg) {
5448
5943
  });
5449
5944
  } catch (err5) {
5450
5945
  const message = err5 instanceof Error ? err5.message : String(err5);
5451
- return writeJson3(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
5946
+ return writeJson4(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
5452
5947
  }
5453
5948
  }
5454
5949
  function extractSampleText(text, apiFormat) {
@@ -5489,7 +5984,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
5489
5984
  const row = cfg.providers.find((p) => p.id === id);
5490
5985
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
5491
5986
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
5492
- return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
5987
+ return writeJson4(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
5493
5988
  }
5494
5989
  function parsePoolKeyInput(body, existing) {
5495
5990
  const out = {};
@@ -5508,7 +6003,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
5508
6003
  if (!id) return writeJsonError(res, 400, "provider id required in path");
5509
6004
  const idx = cfg.providers.findIndex((p) => p.id === id);
5510
6005
  if (idx < 0) return writeJsonError(res, 404, `provider '${id}' not found`);
5511
- const body = await readJsonBody3(req);
6006
+ const body = await readJsonBody4(req);
5512
6007
  const parsed = parsePoolKeyInput(body);
5513
6008
  if (!parsed.apiKey) return writeJsonError(res, 400, "add requires a non-empty apiKey");
5514
6009
  const keyId = `key-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -5520,7 +6015,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
5520
6015
  row.apiKeys = [...row.apiKeys ?? [], entry];
5521
6016
  persistProviders(cfg, deps);
5522
6017
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
5523
- return writeJson3(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
6018
+ return writeJson4(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
5524
6019
  }
5525
6020
  async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
5526
6021
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -5530,7 +6025,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
5530
6025
  const row = cfg.providers[idx];
5531
6026
  const keyIdx = (row.apiKeys ?? []).findIndex((k) => k.id === keyId);
5532
6027
  if (keyIdx < 0) return writeJsonError(res, 404, `pool key '${keyId}' not found`);
5533
- const body = await readJsonBody3(req);
6028
+ const body = await readJsonBody4(req);
5534
6029
  const existing = row.apiKeys[keyIdx];
5535
6030
  const parsed = parsePoolKeyInput(body, existing);
5536
6031
  const entry = { id: keyId, apiKey: parsed.apiKey || existing.apiKey };
@@ -5540,7 +6035,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
5540
6035
  row.apiKeys[keyIdx] = entry;
5541
6036
  persistProviders(cfg, deps);
5542
6037
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
5543
- return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6038
+ return writeJson4(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
5544
6039
  }
5545
6040
  async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
5546
6041
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -5554,7 +6049,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
5554
6049
  if (row.apiKeys.length === 0) row.apiKeys = void 0;
5555
6050
  persistProviders(cfg, deps);
5556
6051
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
5557
- return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6052
+ return writeJson4(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
5558
6053
  }
5559
6054
  async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
5560
6055
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -5564,11 +6059,11 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
5564
6059
  const row = cfg.providers[idx];
5565
6060
  const keyIdx = (row.apiKeys ?? []).findIndex((k) => k.id === keyId);
5566
6061
  if (keyIdx < 0) return writeJsonError(res, 404, `pool key '${keyId}' not found`);
5567
- const body = await readJsonBody3(req);
6062
+ const body = await readJsonBody4(req);
5568
6063
  row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
5569
6064
  persistProviders(cfg, deps);
5570
6065
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
5571
- return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6066
+ return writeJson4(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
5572
6067
  }
5573
6068
  function parseApiKeysInput(raw, existing) {
5574
6069
  if (!Array.isArray(raw)) return existing;
@@ -5754,13 +6249,13 @@ function handlePresets(res, method) {
5754
6249
  website: p.website,
5755
6250
  modelsEndpoint: p.modelsEndpoint
5756
6251
  }));
5757
- return writeJson3(res, 200, { presets, excluded });
6252
+ return writeJson4(res, 200, { presets, excluded });
5758
6253
  }
5759
6254
  async function handleKeys(req, res, method, rest, deps) {
5760
6255
  if (method === "GET" && rest.length === 0) {
5761
6256
  const rows = await deps.keyDb.outboundApiKeysList();
5762
6257
  const reader = deps.keySpendReader;
5763
- if (!reader) return writeJson3(res, 200, { keys: rows.map(toKeyInfo) });
6258
+ if (!reader) return writeJson4(res, 200, { keys: rows.map(toKeyInfo) });
5764
6259
  const now = Date.now();
5765
6260
  const keys = await Promise.all(
5766
6261
  rows.map(async (row) => {
@@ -5772,13 +6267,13 @@ async function handleKeys(req, res, method, rest, deps) {
5772
6267
  return info;
5773
6268
  })
5774
6269
  );
5775
- return writeJson3(res, 200, { keys });
6270
+ return writeJson4(res, 200, { keys });
5776
6271
  }
5777
6272
  if (method === "POST" && rest.length === 0) {
5778
- const body = await readJsonBody3(req);
6273
+ const body = await readJsonBody4(req);
5779
6274
  const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
5780
6275
  const created = await createNamedKey(deps.keyDb, name);
5781
- return writeJson3(res, 201, {
6276
+ return writeJson4(res, 201, {
5782
6277
  id: created.id,
5783
6278
  name: created.name,
5784
6279
  keyPrefix: created.keyPrefix,
@@ -5789,7 +6284,7 @@ async function handleKeys(req, res, method, rest, deps) {
5789
6284
  }
5790
6285
  if (method === "GET" && rest.length === 2 && rest[1] === "reveal") {
5791
6286
  const revealed = await deps.keyDb.outboundApiKeysReveal(rest[0]);
5792
- if (revealed !== null) return writeJson3(res, 200, { key: revealed });
6287
+ if (revealed !== null) return writeJson4(res, 200, { key: revealed });
5793
6288
  const exists = (await deps.keyDb.outboundApiKeysList()).some((r) => r.id === rest[0]);
5794
6289
  if (!exists) return writeJsonError(res, 404, `key '${rest[0]}' not found`);
5795
6290
  return writeJsonError(
@@ -5804,26 +6299,26 @@ async function handleKeys(req, res, method, rest, deps) {
5804
6299
  const bound = await integrationKeyRequirement(deps, id);
5805
6300
  if (bound) return writeJsonError(res, 409, bound);
5806
6301
  const ok = await deps.keyDb.outboundApiKeysRevoke(id);
5807
- return writeJson3(res, ok ? 200 : 404, { ok });
6302
+ return writeJson4(res, ok ? 200 : 404, { ok });
5808
6303
  }
5809
6304
  if (method === "DELETE" && id && !action) {
5810
6305
  const bound = await integrationKeyRequirement(deps, id);
5811
6306
  if (bound) return writeJsonError(res, 409, bound);
5812
6307
  const ok = await deps.keyDb.outboundApiKeysDelete(id);
5813
- return writeJson3(res, ok ? 200 : 404, { ok });
6308
+ return writeJson4(res, ok ? 200 : 404, { ok });
5814
6309
  }
5815
6310
  if (method === "POST" && id && action === "enabled") {
5816
- const body = await readJsonBody3(req);
6311
+ const body = await readJsonBody4(req);
5817
6312
  const enabled = body["enabled"] === true;
5818
6313
  if (!enabled) {
5819
6314
  const bound = await integrationKeyRequirement(deps, id);
5820
6315
  if (bound) return writeJsonError(res, 409, bound);
5821
6316
  }
5822
6317
  const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
5823
- return writeJson3(res, ok ? 200 : 404, { ok, enabled });
6318
+ return writeJson4(res, ok ? 200 : 404, { ok, enabled });
5824
6319
  }
5825
6320
  if (method === "POST" && id && action === "permissions") {
5826
- const body = await readJsonBody3(req);
6321
+ const body = await readJsonBody4(req);
5827
6322
  if (Object.keys(body).length !== 1 || !Object.prototype.hasOwnProperty.call(body, "permissions")) {
5828
6323
  return writeJsonError(res, 400, "body must contain only permissions");
5829
6324
  }
@@ -5838,19 +6333,19 @@ async function handleKeys(req, res, method, rest, deps) {
5838
6333
  );
5839
6334
  }
5840
6335
  const before = (await deps.keyDb.outboundApiKeysList()).find((row) => row.id === id);
5841
- if (!before) return writeJson3(res, 404, { ok: false });
5842
- if (before.revokedAt !== null) return writeJson3(res, 409, { ok: false });
6336
+ if (!before) return writeJson4(res, 404, { ok: false });
6337
+ if (before.revokedAt !== null) return writeJson4(res, 409, { ok: false });
5843
6338
  const required = await integrationKeyRequirement(deps, id, permissions);
5844
6339
  if (required) return writeJsonError(res, 409, required);
5845
6340
  const ok = await deps.keyDb.outboundApiKeysSetPermissions(id, permissions);
5846
6341
  if (!ok) {
5847
6342
  const current = (await deps.keyDb.outboundApiKeysList()).find((row) => row.id === id);
5848
- return writeJson3(res, current?.revokedAt !== null ? 409 : 404, { ok: false });
6343
+ return writeJson4(res, current?.revokedAt !== null ? 409 : 404, { ok: false });
5849
6344
  }
5850
- return writeJson3(res, 200, { ok: true, allowedEndpoints: permissions });
6345
+ return writeJson4(res, 200, { ok: true, allowedEndpoints: permissions });
5851
6346
  }
5852
6347
  if (method === "POST" && id && action === "max-concurrency") {
5853
- const body = await readJsonBody3(req);
6348
+ const body = await readJsonBody4(req);
5854
6349
  const raw = body["maxConcurrency"];
5855
6350
  let value;
5856
6351
  if (raw === null) {
@@ -5865,14 +6360,14 @@ async function handleKeys(req, res, method, rest, deps) {
5865
6360
  );
5866
6361
  }
5867
6362
  const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
5868
- return writeJson3(res, ok ? 200 : 404, { ok, maxConcurrency: value });
6363
+ return writeJson4(res, ok ? 200 : 404, { ok, maxConcurrency: value });
5869
6364
  }
5870
6365
  if (method === "POST" && id && action === "policy") {
5871
- const body = await readJsonBody3(req);
6366
+ const body = await readJsonBody4(req);
5872
6367
  const parsed = parseKeyPolicyBody(body);
5873
6368
  if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
5874
6369
  const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
5875
- return writeJson3(res, ok ? 200 : 404, { ok });
6370
+ return writeJson4(res, ok ? 200 : 404, { ok });
5876
6371
  }
5877
6372
  return writeJsonError(res, 405, `method ${method} not allowed on keys`);
5878
6373
  }
@@ -5963,7 +6458,8 @@ function outboundServerConfigInput(config) {
5963
6458
  userMessageQueue: config.userMessageQueue,
5964
6459
  concurrencyQueue: config.concurrencyQueue,
5965
6460
  voucher: config.voucher,
5966
- anthropic: config.anthropic
6461
+ anthropic: config.anthropic,
6462
+ search: config.search
5967
6463
  };
5968
6464
  }
5969
6465
  function projectImagesConfigForAdmin(config) {
@@ -6026,15 +6522,21 @@ function currentImageGenerationId(deps) {
6026
6522
  }
6027
6523
  async function handleServer(req, res, method, deps) {
6028
6524
  if (method === "GET") {
6029
- const config = await loadServerConfig2(deps.settingsStore);
6525
+ const config = await loadServerConfig3(deps.settingsStore);
6030
6526
  let server = config;
6031
6527
  if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
6032
6528
  if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
6033
6529
  if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
6034
- return writeJson3(res, 200, { server: projectImagesConfigForAdmin(server) });
6530
+ if (config.search) {
6531
+ server = {
6532
+ ...server,
6533
+ search: redactSearchServerConfig(config.search)
6534
+ };
6535
+ }
6536
+ return writeJson4(res, 200, { server: projectImagesConfigForAdmin(server) });
6035
6537
  }
6036
6538
  if (method === "PUT") {
6037
- const patch = await readJsonBody3(req);
6539
+ const patch = await readJsonBody4(req);
6038
6540
  const queueErrors = validateQueueSegments(patch);
6039
6541
  if (queueErrors.length > 0) {
6040
6542
  return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
@@ -6063,7 +6565,7 @@ async function handleServer(req, res, method, deps) {
6063
6565
  if (billingErrors.length > 0) {
6064
6566
  return writeJsonError(res, 400, `invalid billing config: ${billingErrors.join("; ")}`);
6065
6567
  }
6066
- const current = await loadServerConfig2(deps.settingsStore);
6568
+ const current = await loadServerConfig3(deps.settingsStore);
6067
6569
  let effectivePatch = patch;
6068
6570
  if (patch.proxy) {
6069
6571
  effectivePatch = { ...effectivePatch, proxy: preserveOutboundProxySecrets(patch.proxy, current.proxy) };
@@ -6084,6 +6586,20 @@ async function handleServer(req, res, method, deps) {
6084
6586
  }
6085
6587
  effectivePatch = { ...effectivePatch, images };
6086
6588
  }
6589
+ if (patch.search !== void 0) {
6590
+ const searchPatch = preserveSearchSecrets(
6591
+ patch.search,
6592
+ current.search ?? DEFAULT_SEARCH_SERVER_CONFIG2
6593
+ );
6594
+ const searchErrors = validateSearchServerConfig(searchPatch);
6595
+ if (searchErrors.length > 0) {
6596
+ return writeJsonError(res, 400, `invalid search config: ${searchErrors.join("; ")}`);
6597
+ }
6598
+ effectivePatch = {
6599
+ ...effectivePatch,
6600
+ search: searchPatch
6601
+ };
6602
+ }
6087
6603
  const merged = mergeServerConfig(current, effectivePatch);
6088
6604
  const priorImageGenerationId = currentImageGenerationId(deps);
6089
6605
  try {
@@ -6136,7 +6652,11 @@ async function handleServer(req, res, method, deps) {
6136
6652
  } catch {
6137
6653
  }
6138
6654
  }
6139
- return writeJson3(res, 200, { server: projectImagesConfigForAdmin(merged) });
6655
+ const mergedForAdmin = merged.search ? {
6656
+ ...merged,
6657
+ search: redactSearchServerConfig(merged.search)
6658
+ } : merged;
6659
+ return writeJson4(res, 200, { server: projectImagesConfigForAdmin(mergedForAdmin) });
6140
6660
  }
6141
6661
  return writeJsonError(res, 405, `method ${method} not allowed on server`);
6142
6662
  }
@@ -6153,7 +6673,7 @@ async function handleAccounts(req, res, method, rest, deps) {
6153
6673
  sessionKey: query2.get("sessionKey") ?? void 0,
6154
6674
  limit: Number.isFinite(parsedLimit) ? parsedLimit : 100
6155
6675
  });
6156
- return writeJson3(res, 200, {
6676
+ return writeJson4(res, 200, {
6157
6677
  available: true,
6158
6678
  records,
6159
6679
  capacity: ACCOUNT_ROUTE_ACTIVITY_LIMIT,
@@ -6169,7 +6689,7 @@ async function handleAccounts(req, res, method, rest, deps) {
6169
6689
  providerId: query2.get("providerId") ?? void 0,
6170
6690
  accountId: query2.get("accountId") ?? void 0
6171
6691
  });
6172
- return writeJson3(res, 200, {
6692
+ return writeJson4(res, 200, {
6173
6693
  available: true,
6174
6694
  entries,
6175
6695
  collectedAt: Date.now()
@@ -6188,10 +6708,10 @@ async function handleAccounts(req, res, method, rest, deps) {
6188
6708
  const accounts = await deps.subscriptionAccounts.listAll();
6189
6709
  const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
6190
6710
  const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
6191
- return writeJson3(res, 200, { accounts, providerAccounts, externalCli });
6711
+ return writeJson4(res, 200, { accounts, providerAccounts, externalCli });
6192
6712
  }
6193
6713
  if (method === "POST" && rest[0] === "batch" && rest.length === 1) {
6194
- const body = await readJsonBody3(req);
6714
+ const body = await readJsonBody4(req);
6195
6715
  const parsed = validateAccountBatchBody(body);
6196
6716
  if (!parsed) return writeJsonError(res, 400, "invalid account batch request");
6197
6717
  const result = await deps.subscriptionTokenWriter.batchManageAccounts(parsed.refs, parsed.mutation);
@@ -6207,15 +6727,15 @@ async function handleAccounts(req, res, method, rest, deps) {
6207
6727
  deps.accountAllowanceService?.removeAccountSnapshot?.(ref.providerId, ref.accountId);
6208
6728
  }
6209
6729
  }
6210
- return writeJson3(res, 200, { ok: true, affected: result.affected });
6730
+ return writeJson4(res, 200, { ok: true, affected: result.affected });
6211
6731
  }
6212
6732
  if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
6213
6733
  const result = handleCodexOAuthStatus(rest[2], deps);
6214
- return writeJson3(res, result.status, result.body);
6734
+ return writeJson4(res, result.status, result.body);
6215
6735
  }
6216
6736
  if (method === "DELETE" && rest[0] === "codex" && rest[1] === "oauth" && rest[2]) {
6217
6737
  const result = handleCodexOAuthCancel(rest[2], deps);
6218
- return writeJson3(res, result.status, result.body);
6738
+ return writeJson4(res, result.status, result.body);
6219
6739
  }
6220
6740
  if (method === "GET" && rest.length === 3 && rest[2] === "diagnostics") {
6221
6741
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -6237,7 +6757,7 @@ async function handleAccounts(req, res, method, rest, deps) {
6237
6757
  resumeAt: entry.resumeAt
6238
6758
  })) ?? [];
6239
6759
  const diagnostics = [...health2, ...allowance].sort((left, right) => right.at - left.at).slice(0, 200);
6240
- return writeJson3(res, 200, { diagnostics });
6760
+ return writeJson4(res, 200, { diagnostics });
6241
6761
  }
6242
6762
  if (method === "GET" && rest.length === 3 && rest[2] === "events") {
6243
6763
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -6249,17 +6769,17 @@ async function handleAccounts(req, res, method, rest, deps) {
6249
6769
  }
6250
6770
  const snapshot = deps.accountProbeService?.getAllHistory().find((entry) => entry.providerId === providerId && entry.accountId === accountId);
6251
6771
  const diagnostics = getSharedAccountHealth().getDiagnostics({ providerId, accountId });
6252
- return writeJson3(res, 200, { events: snapshot?.records ?? [], diagnostics });
6772
+ return writeJson4(res, 200, { events: snapshot?.records ?? [], diagnostics });
6253
6773
  }
6254
6774
  if (method === "PATCH" && rest.length === 2) {
6255
6775
  const providerId = asSubscriptionProviderId(rest[0]);
6256
6776
  if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
6257
- const body = await readJsonBody3(req);
6777
+ const body = await readJsonBody4(req);
6258
6778
  const patch = validateAccountMetadataPatch(body);
6259
6779
  if (!patch) return writeJsonError(res, 400, "invalid account metadata patch");
6260
6780
  const result = await deps.subscriptionTokenWriter.patchAccountMetadata(providerId, rest[1], patch);
6261
6781
  if (!result.ok) return writeJsonError(res, 404, `account '${rest[1]}' not found`);
6262
- return writeJson3(res, 200, { ok: true });
6782
+ return writeJson4(res, 200, { ok: true });
6263
6783
  }
6264
6784
  if (method === "PUT" || method === "POST" || method === "DELETE") {
6265
6785
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -6268,15 +6788,15 @@ async function handleAccounts(req, res, method, rest, deps) {
6268
6788
  }
6269
6789
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
6270
6790
  const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
6271
- return writeJson3(res, result.status, result.body);
6791
+ return writeJson4(res, result.status, result.body);
6272
6792
  }
6273
6793
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
6274
- const body2 = await readJsonBody3(req);
6794
+ const body2 = await readJsonBody4(req);
6275
6795
  const result = await handleOAuthComplete(providerId, body2, deps);
6276
- return writeJson3(res, result.status, result.body);
6796
+ return writeJson4(res, result.status, result.body);
6277
6797
  }
6278
6798
  if (method === "POST" && rest[1] === "accounts") {
6279
- const body2 = await readJsonBody3(req);
6799
+ const body2 = await readJsonBody4(req);
6280
6800
  const block = validateTokenBody(providerId, body2);
6281
6801
  if (!block) {
6282
6802
  return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
@@ -6284,20 +6804,20 @@ async function handleAccounts(req, res, method, rest, deps) {
6284
6804
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
6285
6805
  await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
6286
6806
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
6287
- return writeJson3(res, 200, status2 ? { account: status2 } : { ok: true });
6807
+ return writeJson4(res, 200, status2 ? { account: status2 } : { ok: true });
6288
6808
  }
6289
6809
  if (method === "POST" && rest[1] === "import-external") {
6290
6810
  if (providerId !== "claude" && providerId !== "codex") {
6291
6811
  return writeJsonError(res, 400, `provider '${providerId}' has no external CLI store`);
6292
6812
  }
6293
- const body2 = await readJsonBody3(req);
6813
+ const body2 = await readJsonBody4(req);
6294
6814
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
6295
6815
  const result = await deps.subscriptionTokenWriter.importExternalCliAccount(providerId, label);
6296
6816
  if (!result.ok) {
6297
6817
  return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
6298
6818
  }
6299
6819
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
6300
- return writeJson3(res, 200, {
6820
+ return writeJson4(res, 200, {
6301
6821
  ok: true,
6302
6822
  account: status2 ?? void 0,
6303
6823
  nativeCredentialMode: result.nativeCredentialMode,
@@ -6312,7 +6832,7 @@ async function handleAccounts(req, res, method, rest, deps) {
6312
6832
  const writer2 = deps.subscriptionTokenWriter;
6313
6833
  const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
6314
6834
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
6315
- return writeJson3(res, 200, { ok, account: status2 ?? void 0 });
6835
+ return writeJson4(res, 200, { ok, account: status2 ?? void 0 });
6316
6836
  }
6317
6837
  if (method === "POST" && rest.length === 3 && rest[2] === "test") {
6318
6838
  const accountId = rest[1];
@@ -6322,7 +6842,7 @@ async function handleAccounts(req, res, method, rest, deps) {
6322
6842
  return writeJsonError(res, 404, `account '${accountId}' not found`);
6323
6843
  }
6324
6844
  const result = await deps.accountProbeService.testAccountConnection(providerId, accountId);
6325
- return writeJson3(res, 200, {
6845
+ return writeJson4(res, 200, {
6326
6846
  ok: result.ok,
6327
6847
  marked: result.marked,
6328
6848
  tier: result.tier,
@@ -6331,15 +6851,15 @@ async function handleAccounts(req, res, method, rest, deps) {
6331
6851
  }
6332
6852
  if (method === "POST" && rest[2] === "label") {
6333
6853
  const accountId = rest[1];
6334
- const body2 = await readJsonBody3(req);
6854
+ const body2 = await readJsonBody4(req);
6335
6855
  const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
6336
6856
  const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
6337
6857
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
6338
- return writeJson3(res, 200, { ok: true });
6858
+ return writeJson4(res, 200, { ok: true });
6339
6859
  }
6340
6860
  if (method === "POST" && rest[2] === "priority") {
6341
6861
  const accountId = rest[1];
6342
- const body2 = await readJsonBody3(req);
6862
+ const body2 = await readJsonBody4(req);
6343
6863
  const raw = body2["priority"];
6344
6864
  const priority = typeof raw === "number" ? raw : Number(raw);
6345
6865
  if (!Number.isFinite(priority)) {
@@ -6347,11 +6867,11 @@ async function handleAccounts(req, res, method, rest, deps) {
6347
6867
  }
6348
6868
  const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
6349
6869
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
6350
- return writeJson3(res, 200, { ok: true });
6870
+ return writeJson4(res, 200, { ok: true });
6351
6871
  }
6352
6872
  if (method === "POST" && rest[2] === "proxy") {
6353
6873
  const accountId = rest[1];
6354
- const body2 = await readJsonBody3(req);
6874
+ const body2 = await readJsonBody4(req);
6355
6875
  const rawProxy = body2["proxy"];
6356
6876
  let proxy;
6357
6877
  if (rawProxy !== null && rawProxy !== void 0) {
@@ -6360,63 +6880,63 @@ async function handleAccounts(req, res, method, rest, deps) {
6360
6880
  }
6361
6881
  const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
6362
6882
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
6363
- return writeJson3(res, 200, { ok: true });
6883
+ return writeJson4(res, 200, { ok: true });
6364
6884
  }
6365
6885
  if (method === "POST" && rest[2] === "supported-models") {
6366
6886
  const accountId = rest[1];
6367
- const body2 = await readJsonBody3(req);
6887
+ const body2 = await readJsonBody4(req);
6368
6888
  const parsed = validateSupportedModelsBody(body2["supportedModels"]);
6369
6889
  if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
6370
6890
  const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
6371
6891
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
6372
- return writeJson3(res, 200, { ok: true });
6892
+ return writeJson4(res, 200, { ok: true });
6373
6893
  }
6374
6894
  if (method === "PUT" && rest[1] === "active") {
6375
- const body2 = await readJsonBody3(req);
6895
+ const body2 = await readJsonBody4(req);
6376
6896
  const id = typeof body2["id"] === "string" ? body2["id"] : "";
6377
6897
  if (!id) return writeJsonError(res, 400, "active switch requires { id }");
6378
6898
  const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
6379
6899
  if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
6380
- return writeJson3(res, 200, { ok: true });
6900
+ return writeJson4(res, 200, { ok: true });
6381
6901
  }
6382
6902
  if (method === "DELETE" && rest.length === 2) {
6383
6903
  const accountId = rest[1];
6384
6904
  const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
6385
6905
  if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
6386
6906
  deps.accountAllowanceService?.removeAccountSnapshot?.(providerId, accountId);
6387
- return writeJson3(res, 200, { ok: true });
6907
+ return writeJson4(res, 200, { ok: true });
6388
6908
  }
6389
6909
  if (method === "DELETE" && rest.length === 1) {
6390
6910
  await deps.subscriptionTokenWriter.clearProvider(providerId);
6391
6911
  deps.accountAllowanceService?.removeProviderSnapshots?.(providerId);
6392
- return writeJson3(res, 200, { ok: true });
6912
+ return writeJson4(res, 200, { ok: true });
6393
6913
  }
6394
6914
  if (method === "DELETE") {
6395
6915
  return writeJsonError(res, 405, "method DELETE not allowed on this accounts path");
6396
6916
  }
6397
- const body = await readJsonBody3(req);
6917
+ const body = await readJsonBody4(req);
6398
6918
  const config = validateTokenBody(providerId, body);
6399
6919
  if (!config) {
6400
6920
  return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
6401
6921
  }
6402
6922
  await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
6403
6923
  const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
6404
- return writeJson3(res, 200, status ? { account: status } : { ok: true });
6924
+ return writeJson4(res, 200, status ? { account: status } : { ok: true });
6405
6925
  }
6406
6926
  return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
6407
6927
  }
6408
6928
  async function handleCli(req, res, method, rest, deps) {
6409
6929
  if (method === "GET" && rest.length === 0) {
6410
6930
  const result = handleCliList(process.platform, deps.cliPathProbe);
6411
- return writeJson3(res, result.status, result.body);
6931
+ return writeJson4(res, result.status, result.body);
6412
6932
  }
6413
6933
  if (method === "GET" && rest[0] === "sessions") {
6414
6934
  const result = handleCliSessions();
6415
- return writeJson3(res, result.status, result.body);
6935
+ return writeJson4(res, result.status, result.body);
6416
6936
  }
6417
6937
  if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
6418
6938
  const result = handleCliStop(rest[1]);
6419
- return writeJson3(res, result.status, result.body);
6939
+ return writeJson4(res, result.status, result.body);
6420
6940
  }
6421
6941
  if (method === "POST" && rest[1] === "install") {
6422
6942
  const cli = rest[0];
@@ -6424,14 +6944,14 @@ async function handleCli(req, res, method, rest, deps) {
6424
6944
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
6425
6945
  }
6426
6946
  const result = await handleCliInstall(cli, deps.cliCommandRunner);
6427
- return writeJson3(res, result.status, result.body);
6947
+ return writeJson4(res, result.status, result.body);
6428
6948
  }
6429
6949
  if (method === "POST" && rest[1] === "launch") {
6430
6950
  const cli = rest[0];
6431
6951
  if (!isLaunchCliId(cli)) {
6432
6952
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
6433
6953
  }
6434
- const body = await readJsonBody3(req);
6954
+ const body = await readJsonBody4(req);
6435
6955
  const providers = loadConfig(deps.configPath).providers ?? [];
6436
6956
  const result = await handleCliLaunch(cli, body, {
6437
6957
  llmConfig: deps.llmConfig,
@@ -6440,7 +6960,7 @@ async function handleCli(req, res, method, rest, deps) {
6440
6960
  opener: deps.cliTerminalOpener,
6441
6961
  probe: deps.cliPathProbe
6442
6962
  });
6443
- return writeJson3(res, result.status, result.body);
6963
+ return writeJson4(res, result.status, result.body);
6444
6964
  }
6445
6965
  return writeJsonError(res, 405, `method ${method} not allowed on cli`);
6446
6966
  }
@@ -6450,52 +6970,52 @@ async function handleIntegrations(req, res, method, rest, deps) {
6450
6970
  const manager = factory();
6451
6971
  try {
6452
6972
  if (method === "GET" && rest.length === 0) {
6453
- return writeJson3(res, 200, {
6973
+ return writeJson4(res, 200, {
6454
6974
  integrations: await manager.listStatus(),
6455
6975
  gateway: deps.outboundApiServer.getStatus()
6456
6976
  });
6457
6977
  }
6458
6978
  if (method === "POST" && rest.length === 1 && rest[0] === "rotate") {
6459
6979
  await manager.rotateGatewayKey();
6460
- return writeJson3(res, 200, { ok: true, integrations: await manager.listStatus() });
6980
+ return writeJson4(res, 200, { ok: true, integrations: await manager.listStatus() });
6461
6981
  }
6462
6982
  const client = rest[0];
6463
6983
  if (!isIntegrationClient(client)) {
6464
6984
  return writeJsonError(res, 400, `unknown integration client '${client ?? ""}'`);
6465
6985
  }
6466
6986
  if (method === "POST" && rest[1] === "key") {
6467
- const body = await readJsonBody3(req);
6987
+ const body = await readJsonBody4(req);
6468
6988
  if (Object.keys(body).length !== 1 || typeof body.keyId !== "string" || !body.keyId.trim()) {
6469
6989
  return writeJsonError(res, 400, "body must contain a non-empty keyId string");
6470
6990
  }
6471
6991
  const status = await manager.bindIntegrationKey(client, body.keyId.trim());
6472
- return writeJson3(res, 200, { integration: status });
6992
+ return writeJson4(res, 200, { integration: status });
6473
6993
  }
6474
6994
  if (method === "POST" && rest[1] === "plan") {
6475
- const body = await readJsonBody3(req);
6995
+ const body = await readJsonBody4(req);
6476
6996
  const configPath = body.configPath;
6477
6997
  if (configPath !== void 0 && typeof configPath !== "string") {
6478
6998
  return writeJsonError(res, 400, "configPath must be a string");
6479
6999
  }
6480
7000
  const plan = await manager.plan(client, configPath);
6481
- return writeJson3(res, 200, { plan });
7001
+ return writeJson4(res, 200, { plan });
6482
7002
  }
6483
7003
  if (method === "POST" && (rest[1] === "install" || rest[1] === "apply")) {
6484
- const body = await readJsonBody3(req);
7004
+ const body = await readJsonBody4(req);
6485
7005
  const configPath = body.configPath;
6486
7006
  if (configPath !== void 0 && typeof configPath !== "string") {
6487
7007
  return writeJsonError(res, 400, "configPath must be a string");
6488
7008
  }
6489
7009
  const status = await manager.install(client, configPath);
6490
- return writeJson3(res, 200, { integration: status });
7010
+ return writeJson4(res, 200, { integration: status });
6491
7011
  }
6492
7012
  if (method === "POST" && rest[1] === "repair") {
6493
7013
  const status = await manager.repair(client);
6494
- return writeJson3(res, 200, { integration: status });
7014
+ return writeJson4(res, 200, { integration: status });
6495
7015
  }
6496
7016
  if (method === "DELETE" && rest.length === 1 || method === "POST" && rest[1] === "remove") {
6497
7017
  const status = await manager.remove(client);
6498
- return writeJson3(res, 200, { integration: status });
7018
+ return writeJson4(res, 200, { integration: status });
6499
7019
  }
6500
7020
  return writeJsonError(res, 405, `method ${method} not allowed on integrations`);
6501
7021
  } catch (error) {
@@ -6638,7 +7158,7 @@ async function handleImages(res, method, rest, deps) {
6638
7158
  }
6639
7159
  const reader = deps.imageRuntimeStatus;
6640
7160
  if (!reader) return writeJsonError(res, 501, "Images runtime status is not available");
6641
- const serverConfig = await loadServerConfig2(deps.settingsStore);
7161
+ const serverConfig = await loadServerConfig3(deps.settingsStore);
6642
7162
  const images = serverConfig.images ?? DEFAULT_IMAGES_SERVER_CONFIG;
6643
7163
  const lifecycle = reader.status();
6644
7164
  const capability = await reader.inspectCapability(IMAGE_ADMIN_STATUS_TENANT);
@@ -6658,7 +7178,7 @@ async function handleImages(res, method, rest, deps) {
6658
7178
  httpLeases: safeStatusCount(generation.httpLeases),
6659
7179
  hostedLeases: safeStatusCount(generation.hostedLeases)
6660
7180
  }));
6661
- return writeJson3(res, 200, {
7181
+ return writeJson4(res, 200, {
6662
7182
  configured: {
6663
7183
  enabled: images.enabled,
6664
7184
  provider: images.provider,
@@ -6686,7 +7206,7 @@ async function handleImages(res, method, rest, deps) {
6686
7206
  async function handleStatus(res, method, deps) {
6687
7207
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
6688
7208
  const status = deps.outboundApiServer.getStatus();
6689
- const serverConfig = await loadServerConfig2(deps.settingsStore);
7209
+ const serverConfig = await loadServerConfig3(deps.settingsStore);
6690
7210
  const endpoints = ["chat", "responses", "messages", "gemini"].map((endpoint) => {
6691
7211
  const routes = (serverConfig.bindings ?? []).filter((binding) => binding.enabled && binding.endpoint === endpoint).map((binding) => gatewayBindingToEndpointConfig(binding));
6692
7212
  const useSubscription = routes.some((route) => route.useSubscription);
@@ -6724,14 +7244,14 @@ async function handleStatus(res, method, deps) {
6724
7244
  })() : void 0;
6725
7245
  if (status.running) {
6726
7246
  const queueStatus = deps.outboundApiServer.getQueueStatus();
6727
- return writeJson3(res, 200, {
7247
+ return writeJson4(res, 200, {
6728
7248
  ...status,
6729
7249
  endpoints,
6730
7250
  queueStatus,
6731
7251
  ...imageRuntime ? { imageRuntime } : {}
6732
7252
  });
6733
7253
  }
6734
- return writeJson3(res, 200, {
7254
+ return writeJson4(res, 200, {
6735
7255
  ...status,
6736
7256
  endpoints,
6737
7257
  ...imageRuntime ? { imageRuntime } : {}
@@ -6755,18 +7275,18 @@ function resolvePlaygroundPath(endpoint, body) {
6755
7275
  }
6756
7276
  async function handlePlayground(req, res, method, deps) {
6757
7277
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on playground`);
6758
- const body = await readJsonBody3(req);
7278
+ const body = await readJsonBody4(req);
6759
7279
  const endpoint = typeof body["endpoint"] === "string" ? body["endpoint"] : "";
6760
7280
  const key = typeof body["key"] === "string" ? body["key"] : "";
6761
7281
  const payload = body["body"];
6762
7282
  const status = deps.outboundApiServer.getStatus();
6763
7283
  if (!status.running || !status.port) return writeJsonError(res, 503, "outbound server not running");
6764
- const path2 = resolvePlaygroundPath(endpoint, isRecord2(payload) ? payload : {});
7284
+ const path2 = resolvePlaygroundPath(endpoint, isRecord3(payload) ? payload : {});
6765
7285
  if (!path2) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
6766
7286
  const upstreamBody = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
6767
7287
  await proxyToOutbound(res, status.port, path2, key, upstreamBody);
6768
7288
  }
6769
- function isRecord2(v) {
7289
+ function isRecord3(v) {
6770
7290
  return !!v && typeof v === "object" && !Array.isArray(v);
6771
7291
  }
6772
7292
  function proxyToOutbound(res, outboundPort, path2, key, body) {
@@ -6901,7 +7421,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
6901
7421
  }
6902
7422
 
6903
7423
  // src/admin/version.ts
6904
- var DAEMON_VERSION = true ? "0.2.0" : "0.0.0-dev";
7424
+ var DAEMON_VERSION = true ? "0.3.0" : "0.0.0-dev";
6905
7425
 
6906
7426
  // src/admin/AdminServer.ts
6907
7427
  var LOOPBACK_ADDR = "127.0.0.1";
@@ -17376,6 +17896,14 @@ function buildDaemon(config, paths) {
17376
17896
  onEvent: (row, at) => usageThroughput.record(row, at)
17377
17897
  });
17378
17898
  const initialImagesConfig = normalizeServerConfig2(decryptedConfig.server).images;
17899
+ const initialSearchConfig = normalizeServerConfig2(decryptedConfig.server).search;
17900
+ for (const issue of validateSearchServerConfig2(
17901
+ decryptedConfig.server?.search
17902
+ )) {
17903
+ logger.warn("[search] ignoring invalid config: " + issue);
17904
+ }
17905
+ const searchRuntime = buildSearchRuntime(initialSearchConfig, { logger });
17906
+ const searchFrontendModes = initialSearchConfig.modes;
17379
17907
  const imageObservability = new ImageObservability();
17380
17908
  const imageRuntimeObservability = Object.freeze({
17381
17909
  telemetrySink: imageObservability.telemetrySink,
@@ -17505,7 +18033,9 @@ function buildDaemon(config, paths) {
17505
18033
  apiKeyPool,
17506
18034
  usageRecorder,
17507
18035
  openAIOperationRegistry,
17508
- responsesHostedImageIngress
18036
+ responsesHostedImageIngress,
18037
+ searchRuntime,
18038
+ searchFrontendModes
17509
18039
  });
17510
18040
  if (providerProxy.getDeps().openAIOperationRegistry !== openAIOperationRegistry) {
17511
18041
  throw new Error(
@@ -17570,7 +18100,9 @@ function buildDaemon(config, paths) {
17570
18100
  keySpendTracker,
17571
18101
  // configurable-logging: route the server's OWN lifecycle + relay dispatch-error
17572
18102
  // lines through the injected logger (honors level/format/file sink).
17573
- logger
18103
+ logger,
18104
+ // plan 阶段5: the same instance the managed frontends hold.
18105
+ searchRuntime
17574
18106
  });
17575
18107
  const auditDir = defaultAuditDir(paths.configPath);
17576
18108
  const billingDir = defaultBillingDir(paths.configPath);
@@ -17592,6 +18124,10 @@ function buildDaemon(config, paths) {
17592
18124
  }),
17593
18125
  routeLeaseManager,
17594
18126
  subscriptionAccounts,
18127
+ // search-settings-ui D3: the daemon's ONE search runtime + its
18128
+ // bootstrap-captured modes, for `GET /admin/api/search/diagnostics` and
18129
+ // `POST /admin/api/search/test` (501 when a light embedder omits it).
18130
+ searchStatus: { runtime: searchRuntime, modes: searchFrontendModes },
17595
18131
  accountAllowanceService,
17596
18132
  allowanceRefreshScheduler: claudeAllowanceRefreshScheduler,
17597
18133
  accountProbeService: accountHealthProbeScheduler,
@@ -17728,6 +18264,8 @@ function buildDaemon(config, paths) {
17728
18264
  keyDb,
17729
18265
  settingsStore,
17730
18266
  openAIOperationRegistry,
18267
+ searchRuntime,
18268
+ searchFrontendModes,
17731
18269
  imageRuntimeManager,
17732
18270
  imageObservability,
17733
18271
  imageCleanupService,