@hydraharness/harness-web-search-http 0.1.1-rc.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @hydraharness/harness-web-search-http
2
+
3
+ Serper and Other JSON HTTP search providers for [`ctx.web`](../web/README.md). They share transport and mapping mechanics but retain separate configuration and credentials. Mounting the plugin performs no network calls.
4
+
5
+ ## Configuration
6
+
7
+ `web-search-serper` owns the result limit (8), request bounds, and the `SERPER_API_KEY` credential reference. Requests use POST `https://google.serper.dev/search`, `X-API-KEY`, and `{q,num}` plus `gl`/`hl` only when the agent supplies country/language hints. Hydra applies no saved locale defaults. Organic title/link/snippet/position fields become normalized sources.
8
+
9
+ `web-search-custom` owns the endpoint, display name, GET/POST method, authentication, request fields, and response paths. Empty endpoint is unconfigured. Authentication supports none, Bearer, API key header, and custom header; all values resolve through the existing credential service, defaulting to `CUSTOM_SEARCH_API_KEY`. GET encodes fields as query parameters; POST sends JSON. Nested GET values are JSON strings.
10
+
11
+ Request mapping includes query and limit plus optional country, language, type, and date fields. Response mapping includes results, title, URL, snippet, published date, score, and position. Dot paths permit simple identifiers and reject prototype traversal and expressions. The results path must name an array; each result needs an HTTP(S) URL. Incorrect configured mappings fail the connection test and search as `INVALID_RESPONSE`.
12
+
13
+ Advanced `headerRefs` JSON maps header names to credential references, never literals. `staticBody` accepts non-secret JSON fields; credential-like names are rejected. Endpoints cannot contain userinfo or credential-like query parameters. Redirects are refused before forwarding credentials. Responses are limited to `maxResponseBytes` (5,000,000 by default) before JSON parsing. Provider timeouts default to 60000 ms and combine with caller cancellation. Keys are resolved for every operation and never retained on the provider. Responses echoing a resolved credential are rejected before returning sources.
14
+
15
+ ## Model Experience
16
+
17
+ ### Conversation search results
18
+
19
+ #### What the model sees
20
+
21
+ The [`tool-web`](../tool-web/README.md) consumer emits bounded normalized sources or shared safe errors. Registration adds no model input; Serper and Other do not invoke a language model.
22
+
23
+ #### Token effect
24
+
25
+ Result tokens scale with retained source text. Credentials and raw provider responses are excluded.
26
+
27
+ #### KV Cache effect
28
+
29
+ Only appended tool results affect subsequent context; switching the provider does not alter the tool argument schema.
30
+
31
+ ## Known Limitations and Deferred Work
32
+
33
+ - Only web search is implemented. There is no automatic fallback, aggregation across providers, or reranking. Operators choose custom endpoints; private search services are permitted because the endpoint is configuration, not model input. Non-root trailing-slash equivalence is not assumed. Arbitrary expressions and secret literals in static request configuration are unsupported.
package/lib/index.js ADDED
@@ -0,0 +1,433 @@
1
+ import z from "@hydraharness/schemastery";
2
+ import { credentialRef } from "@hydraharness/harness-credentials";
3
+ import { launchEnvironmentOf } from "@hydraharness/harness-launch-environment";
4
+ import { installSettingsSection, settingsNamespace } from "@hydraharness/harness-settings";
5
+ import { SearchProviderError, WEB_SEARCH_CAPABILITIES, WebError, searchHttpError } from "@hydraharness/harness-web";
6
+ //#region lib/types/provider.js
7
+ /** Bounded JSON HTTP requests and simple dot-path mapping for search providers. */
8
+ const forbidden = /^(?:__proto__|prototype|constructor)$/;
9
+ const secretName = /authorization|cookie|password|secret|token|api[-_]?key/i;
10
+ /** Accept only own-property paths; no indexing, expressions, or prototype traversal. */
11
+ function pathParts(path) {
12
+ const parts = path.split(".");
13
+ if (!parts.every((part) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(part) && !forbidden.test(part))) throw new Error("Search mapping paths must contain simple dot-separated field names.");
14
+ return parts;
15
+ }
16
+ /** Parse user JSON without allowing credential-like fields or prototype mutation. */
17
+ function objectJson(text, headers = false) {
18
+ let value;
19
+ try {
20
+ value = JSON.parse(text);
21
+ } catch {
22
+ throw new Error("Search JSON must be a valid object.");
23
+ }
24
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Search JSON must be an object.");
25
+ const visit = (item) => {
26
+ if (item === null || typeof item !== "object") return;
27
+ for (const [key, child] of Object.entries(item)) {
28
+ if (forbidden.test(key) || !headers && secretName.test(key)) throw new Error("Store search credentials through the credential controls, not request JSON.");
29
+ visit(child);
30
+ }
31
+ };
32
+ visit(value);
33
+ return value;
34
+ }
35
+ /**
36
+ * Reject unsafe paths, credential-bearing endpoints, and malformed JSON at save/load.
37
+ * An empty endpoint is a valid unconfigured draft, rejected at execution.
38
+ * @param config - resolved provider configuration.
39
+ */
40
+ function validateSearchConfig(config) {
41
+ credentialRef(config.apiKeyEnv);
42
+ if (config.endpoint !== "") {
43
+ let url;
44
+ try {
45
+ url = new URL(config.endpoint);
46
+ } catch {
47
+ throw new Error("Search endpoint must be an HTTP(S) URL.");
48
+ }
49
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || [...url.searchParams.keys()].some((key) => secretName.test(key))) throw new Error("Search endpoint must be HTTP(S) without embedded credentials or a fragment.");
50
+ }
51
+ for (const path of [
52
+ config.queryField,
53
+ config.resultsPath,
54
+ config.urlField
55
+ ]) pathParts(path);
56
+ for (const path of [
57
+ config.limitField,
58
+ config.titleField,
59
+ config.snippetField,
60
+ config.publishedAtField,
61
+ config.scoreField,
62
+ config.positionField,
63
+ config.countryField,
64
+ config.languageField,
65
+ config.typeField,
66
+ config.dateField
67
+ ]) if (path !== "") pathParts(path);
68
+ if (config.auth !== "none" && config.auth !== "bearer") new Headers({ [config.authHeader]: "validation" });
69
+ for (const [key, ref] of Object.entries(objectJson(config.headerRefs, true))) {
70
+ new Headers({ [key]: "validation" });
71
+ if (/^(host|content-length|transfer-encoding|connection)$/i.test(key)) throw new Error("Search transport headers cannot be overridden.");
72
+ if (typeof ref !== "string") throw new Error("Custom header values must be credential references.");
73
+ credentialRef(ref);
74
+ }
75
+ objectJson(config.staticBody);
76
+ }
77
+ /** Read only own properties from untrusted JSON. */
78
+ function at(value, path) {
79
+ if (path === "") return void 0;
80
+ for (const part of pathParts(path)) {
81
+ if (value === null || typeof value !== "object" || !Object.hasOwn(value, part)) return void 0;
82
+ value = value[part];
83
+ }
84
+ return value;
85
+ }
86
+ /** Assign one validated dot-path without mutating static input objects. */
87
+ function put(target, path, value) {
88
+ if (path === "" || value === void 0) return;
89
+ const parts = pathParts(path);
90
+ for (const part of parts.slice(0, -1)) {
91
+ const child = target[part];
92
+ if (child === null || typeof child !== "object" || Array.isArray(child)) target[part] = {};
93
+ target = target[part];
94
+ }
95
+ target[parts.at(-1)] = value;
96
+ }
97
+ /** Shared provider mechanics; provider routing remains in ctx.web. */
98
+ var HttpSearchProvider = class {
99
+ descriptor;
100
+ current;
101
+ resolveKey;
102
+ id;
103
+ constructor(descriptor, current, resolveKey) {
104
+ this.descriptor = descriptor;
105
+ this.current = current;
106
+ this.resolveKey = resolveKey;
107
+ this.id = descriptor.id;
108
+ }
109
+ available() {
110
+ return this.current().endpoint !== "";
111
+ }
112
+ async search(request, signal) {
113
+ const config = this.current();
114
+ const timeout = AbortSignal.timeout(config.timeoutMs);
115
+ const bounded = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
116
+ let status;
117
+ const keys = [];
118
+ try {
119
+ validateSearchConfig(config);
120
+ if (config.endpoint === "" || request.type !== void 0 && request.type !== "web") throw new SearchProviderError(this.id, "CONFIG_ERROR");
121
+ const headers = new Headers({
122
+ "Content-Type": "application/json",
123
+ Accept: "application/json"
124
+ });
125
+ const keyFor = async (ref) => {
126
+ bounded.throwIfAborted();
127
+ const cancelled = Promise.withResolvers();
128
+ const onAbort = () => {
129
+ cancelled.reject(new SearchProviderError(this.id, "TIMEOUT"));
130
+ };
131
+ bounded.addEventListener("abort", onAbort, { once: true });
132
+ let key;
133
+ try {
134
+ key = await Promise.race([this.resolveKey(ref), cancelled.promise]);
135
+ } catch {
136
+ throw new SearchProviderError(this.id, "CONFIG_ERROR");
137
+ } finally {
138
+ bounded.removeEventListener("abort", onAbort);
139
+ }
140
+ if (!key) throw new SearchProviderError(this.id, "CONFIG_ERROR");
141
+ keys.push(key);
142
+ return key;
143
+ };
144
+ for (const [header, ref] of Object.entries(objectJson(config.headerRefs, true))) headers.set(header, await keyFor(ref));
145
+ if (config.auth !== "none") {
146
+ const key = await keyFor(config.apiKeyEnv);
147
+ headers.set(config.auth === "bearer" ? "Authorization" : config.authHeader, config.auth === "bearer" ? `Bearer ${key}` : key);
148
+ }
149
+ const body = objectJson(config.staticBody);
150
+ put(body, config.queryField, request.query);
151
+ put(body, config.limitField, Math.min(request.maxResults ?? config.maxResults, config.maxResults));
152
+ put(body, config.countryField, request.country);
153
+ put(body, config.languageField, request.language);
154
+ put(body, config.typeField, request.type ?? "web");
155
+ put(body, config.dateField, request.date);
156
+ const endpoint = new URL(config.endpoint);
157
+ if (config.method === "GET") for (const [key, value] of Object.entries(body)) endpoint.searchParams.set(key, typeof value === "string" ? value : JSON.stringify(value));
158
+ const response = await fetch(endpoint, {
159
+ method: config.method,
160
+ headers,
161
+ redirect: "error",
162
+ signal: bounded,
163
+ ...config.method === "POST" ? { body: JSON.stringify(body) } : {}
164
+ });
165
+ status = response.status;
166
+ if (!response.ok) {
167
+ await response.body?.cancel();
168
+ throw searchHttpError(this.id, status);
169
+ }
170
+ let payload;
171
+ let bytes = 0;
172
+ const bodyStream = response.body?.pipeThrough(new TransformStream({ transform: (chunk, controller) => {
173
+ bytes += chunk.byteLength;
174
+ if (bytes > config.maxResponseBytes) throw new SearchProviderError(this.id, "INVALID_RESPONSE", status);
175
+ controller.enqueue(chunk);
176
+ } }));
177
+ try {
178
+ payload = await new Response(bodyStream).json();
179
+ } catch {
180
+ throw new SearchProviderError(this.id, "INVALID_RESPONSE", status);
181
+ }
182
+ const items = at(payload, config.resultsPath);
183
+ if (!Array.isArray(items)) throw new SearchProviderError(this.id, "INVALID_RESPONSE", status);
184
+ const sources = items.map((item) => {
185
+ const url = at(item, config.urlField);
186
+ if (typeof url !== "string" || !URL.canParse(url) || !["http:", "https:"].includes(new URL(url).protocol)) throw new SearchProviderError(this.id, "INVALID_RESPONSE", status);
187
+ const source = { url };
188
+ for (const [key, path] of [
189
+ ["title", config.titleField],
190
+ ["snippet", config.snippetField],
191
+ ["publishedAt", config.publishedAtField]
192
+ ]) {
193
+ const value = at(item, path);
194
+ if (value !== void 0 && value !== null && typeof value !== "string") throw new SearchProviderError(this.id, "INVALID_RESPONSE", status);
195
+ if (typeof value === "string") source[key] = value;
196
+ }
197
+ for (const [key, path] of [["score", config.scoreField], ["position", config.positionField]]) {
198
+ const value = at(item, path);
199
+ if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value))) throw new SearchProviderError(this.id, "INVALID_RESPONSE", status);
200
+ if (typeof value === "number") source[key] = value;
201
+ }
202
+ return source;
203
+ });
204
+ if (keys.some((key) => JSON.stringify(sources).includes(key))) throw new SearchProviderError(this.id, "INVALID_RESPONSE", status);
205
+ return {
206
+ sources,
207
+ truncated: false,
208
+ statusCode: status
209
+ };
210
+ } catch (error) {
211
+ if (timeout.aborted || signal?.aborted && signal.reason instanceof DOMException && signal.reason.name === "TimeoutError") throw new SearchProviderError(this.id, "TIMEOUT", status);
212
+ if (signal?.aborted) throw new WebError("Web search aborted", "WEB_ABORTED");
213
+ if (error instanceof SearchProviderError) throw error;
214
+ throw new SearchProviderError(this.id, "NETWORK_ERROR", status);
215
+ }
216
+ }
217
+ };
218
+ //#endregion
219
+ //#region lib/types/index.js
220
+ /** Serper and configurable JSON search providers registered on the existing web service. */
221
+ /** Cordis plugin name. */
222
+ const name = "web-search-http";
223
+ /** Search registry required by both provider registrations. */
224
+ const inject = ["web"];
225
+ const fields = [
226
+ {
227
+ key: "displayName",
228
+ label: "Provider Name",
229
+ kind: "text"
230
+ },
231
+ {
232
+ key: "endpoint",
233
+ label: "Endpoint",
234
+ kind: "text"
235
+ },
236
+ {
237
+ key: "method",
238
+ label: "HTTP Method",
239
+ kind: "select",
240
+ options: ["POST", "GET"]
241
+ },
242
+ {
243
+ key: "auth",
244
+ label: "Authentication",
245
+ kind: "select",
246
+ options: [
247
+ "none",
248
+ "bearer",
249
+ "api_key_header",
250
+ "custom_header"
251
+ ]
252
+ },
253
+ {
254
+ key: "authHeader",
255
+ label: "API Key / Custom Header Name",
256
+ kind: "text"
257
+ },
258
+ {
259
+ key: "queryField",
260
+ label: "Query Field",
261
+ kind: "text"
262
+ },
263
+ {
264
+ key: "limitField",
265
+ label: "Limit Field",
266
+ kind: "text"
267
+ },
268
+ {
269
+ key: "resultsPath",
270
+ label: "Results Path",
271
+ kind: "text"
272
+ },
273
+ {
274
+ key: "titleField",
275
+ label: "Title Field",
276
+ kind: "text"
277
+ },
278
+ {
279
+ key: "urlField",
280
+ label: "URL Field",
281
+ kind: "text"
282
+ },
283
+ {
284
+ key: "snippetField",
285
+ label: "Snippet Field",
286
+ kind: "text"
287
+ },
288
+ {
289
+ key: "maxResults",
290
+ label: "Provider Result Limit",
291
+ kind: "number"
292
+ },
293
+ {
294
+ key: "timeoutMs",
295
+ label: "Provider Timeout (ms)",
296
+ kind: "number",
297
+ advanced: true
298
+ },
299
+ {
300
+ key: "maxResponseBytes",
301
+ label: "Maximum Response Bytes",
302
+ kind: "number",
303
+ advanced: true
304
+ },
305
+ {
306
+ key: "headerRefs",
307
+ label: "Custom Headers",
308
+ kind: "json",
309
+ advanced: true,
310
+ hint: "JSON mapping header names to credential references; store values through Credentials."
311
+ },
312
+ {
313
+ key: "staticBody",
314
+ label: "Static Request JSON",
315
+ kind: "json",
316
+ advanced: true,
317
+ hint: "Non-secret request fields only."
318
+ },
319
+ ...[
320
+ ["countryField", "Country Field"],
321
+ ["languageField", "Language Field"],
322
+ ["typeField", "Search Type Field"],
323
+ ["dateField", "Date Field"],
324
+ ["publishedAtField", "Published Date Mapping"],
325
+ ["scoreField", "Score Mapping"],
326
+ ["apiKeyEnv", "Credential Reference"]
327
+ ].map(([key, label]) => ({
328
+ key,
329
+ label,
330
+ kind: "text",
331
+ advanced: true
332
+ }))
333
+ ];
334
+ /** Custom provider settings contain credential references and non-secret mapping values. */
335
+ const CustomConfig = z.object({
336
+ displayName: z.string().default("My Search Provider"),
337
+ endpoint: z.string().default(""),
338
+ method: z.union(["POST", "GET"]).default("POST"),
339
+ auth: z.union([
340
+ "none",
341
+ "bearer",
342
+ "api_key_header",
343
+ "custom_header"
344
+ ]).default("api_key_header"),
345
+ authHeader: z.string().default("X-API-KEY"),
346
+ apiKeyEnv: z.string().role("credential-ref").default("CUSTOM_SEARCH_API_KEY"),
347
+ queryField: z.string().default("q"),
348
+ limitField: z.string().default("num"),
349
+ resultsPath: z.string().default("results"),
350
+ titleField: z.string().default("title"),
351
+ urlField: z.string().default("url"),
352
+ snippetField: z.string().default("snippet"),
353
+ publishedAtField: z.string().default(""),
354
+ scoreField: z.string().default(""),
355
+ positionField: z.string().default(""),
356
+ countryField: z.string().default(""),
357
+ languageField: z.string().default(""),
358
+ typeField: z.string().default(""),
359
+ dateField: z.string().default(""),
360
+ headerRefs: z.string().default("{}"),
361
+ staticBody: z.string().default("{}"),
362
+ timeoutMs: z.number().step(1).min(1).max(6e5).default(6e4),
363
+ maxResponseBytes: z.number().step(1).min(1).default(5e6),
364
+ maxResults: z.number().step(1).min(1).max(100).default(8)
365
+ });
366
+ /** Both provider configurations are validated before registration. */
367
+ const Config = z.object({
368
+ serper: z.object({
369
+ apiKeyEnv: z.string().role("credential-ref").default("SERPER_API_KEY"),
370
+ maxResults: z.number().step(1).min(1).max(100).default(8),
371
+ timeoutMs: z.number().step(1).min(1).max(6e5).default(6e4),
372
+ maxResponseBytes: z.number().step(1).min(1).default(5e6)
373
+ }),
374
+ custom: CustomConfig
375
+ });
376
+ /**
377
+ * Mount both HTTP adapters, with independently hot-reloaded settings and credentials.
378
+ * @param ctx - search, settings, and credential services.
379
+ * @param config - optional deployment overrides.
380
+ */
381
+ function apply(ctx, config) {
382
+ register(ctx, "serper", "Serper.dev", CustomConfig({
383
+ ...config.serper,
384
+ endpoint: "https://google.serper.dev/search",
385
+ method: "POST",
386
+ auth: "api_key_header",
387
+ authHeader: "X-API-KEY",
388
+ apiKeyEnv: config.serper?.apiKeyEnv ?? "SERPER_API_KEY",
389
+ resultsPath: "organic",
390
+ titleField: "title",
391
+ urlField: "link",
392
+ snippetField: "snippet",
393
+ positionField: "position",
394
+ queryField: "q",
395
+ limitField: "num",
396
+ countryField: "gl",
397
+ languageField: "hl"
398
+ }), fields.filter((field) => [
399
+ "maxResults",
400
+ "apiKeyEnv",
401
+ "timeoutMs",
402
+ "maxResponseBytes"
403
+ ].includes(field.key)));
404
+ register(ctx, "custom", "Other", CustomConfig(config.custom ?? {}), fields);
405
+ }
406
+ /** Register one adapter and its saved configuration without retaining resolved keys. */
407
+ function register(ctx, id, displayName, base, controls) {
408
+ validateSearchConfig(base);
409
+ let current = () => base;
410
+ const descriptor = {
411
+ id,
412
+ displayName,
413
+ configurable: true,
414
+ capabilities: WEB_SEARCH_CAPABILITIES,
415
+ settingsNs: `web-search-${id}`,
416
+ credentialRef: base.apiKeyEnv,
417
+ fields: controls
418
+ };
419
+ installSettingsSection(ctx, settingsNamespace(descriptor.settingsNs), CustomConfig, base, {
420
+ setSource: (source) => {
421
+ current = source;
422
+ },
423
+ onChange: () => {},
424
+ validate: validateSearchConfig
425
+ });
426
+ ctx.web.registerSearchProvider(new HttpSearchProvider(descriptor, () => current(), async (ref) => {
427
+ const credentials = ctx.get("credentials");
428
+ if (credentials !== void 0) return (await credentials.resolve(credentialRef(ref)))?.value;
429
+ return launchEnvironmentOf(ctx).get(ref)?.value;
430
+ }));
431
+ }
432
+ //#endregion
433
+ export { Config, CustomConfig, HttpSearchProvider, apply, inject, name, validateSearchConfig };
@@ -0,0 +1,22 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@hydraharness/harness-web-search-http`.
4
+ * @module @hydraharness/harness-web-search-http/invariant
5
+ */
6
+ const PACKAGE_NAME = "@hydraharness/harness-web-search-http";
7
+ /** Cordis companion plugin name. */
8
+ const name = "web-search-http-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: request mapping has no independent mutable state; provider registration ownership is checked by the web service.
13
+ */
14
+ const install = () => {};
15
+ /**
16
+ * Register this package's invariant companion.
17
+ * @param ctx - Cordis context carrying the invariant service.
18
+ * @returns the installed registration's disposer after setup succeeds.
19
+ */
20
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
21
+ //#endregion
22
+ export { apply, inject, name };
@@ -0,0 +1,28 @@
1
+ /** Serper and configurable JSON search providers registered on the existing web service. */
2
+ import type { Context } from '@hydraharness/cordis';
3
+ import z from '@hydraharness/schemastery';
4
+ import type { HttpSearchConfig } from './provider.ts';
5
+ export { HttpSearchProvider, validateSearchConfig } from './provider.ts';
6
+ export type { HttpSearchConfig } from './provider.ts';
7
+ /** Cordis plugin name. */
8
+ export declare const name = "web-search-http";
9
+ /** Search registry required by both provider registrations. */
10
+ export declare const inject: string[];
11
+ /** Custom provider settings contain credential references and non-secret mapping values. */
12
+ export declare const CustomConfig: z<Partial<HttpSearchConfig>, HttpSearchConfig>;
13
+ /** Operator bases for the independent saved provider sections. */
14
+ export interface Config {
15
+ /** Serper credential reference and request limits. */
16
+ serper?: Pick<Partial<HttpSearchConfig>, 'apiKeyEnv' | 'maxResults' | 'timeoutMs' | 'maxResponseBytes'>;
17
+ /** Other provider endpoint, authentication, and JSON mappings. */
18
+ custom?: Partial<HttpSearchConfig>;
19
+ }
20
+ /** Both provider configurations are validated before registration. */
21
+ export declare const Config: z<Config>;
22
+ /**
23
+ * Mount both HTTP adapters, with independently hot-reloaded settings and credentials.
24
+ * @param ctx - search, settings, and credential services.
25
+ * @param config - optional deployment overrides.
26
+ */
27
+ export declare function apply(ctx: Context, config: Config): void;
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@hydraharness/harness-web-search-http`.
3
+ * @module @hydraharness/harness-web-search-http/invariant
4
+ */
5
+ import type { Context } from '@hydraharness/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "web-search-http-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,70 @@
1
+ /** Bounded JSON HTTP requests and simple dot-path mapping for search providers. */
2
+ import type { WebSearchProvider, WebSearchProviderDescriptor, WebSearchRequest, WebSearchResult } from '@hydraharness/harness-web';
3
+ /** Fully resolved, secret-free configuration captured once per request. */
4
+ export interface HttpSearchConfig {
5
+ /** Name shown for this configured search service. */
6
+ displayName: string;
7
+ /** HTTP(S) search URL; empty means unconfigured. */
8
+ endpoint: string;
9
+ /** Query parameters for GET, JSON body for POST. */
10
+ method: 'GET' | 'POST';
11
+ /** Authentication scheme for the primary credential. */
12
+ auth: 'none' | 'bearer' | 'api_key_header' | 'custom_header';
13
+ /** Header receiving the primary key for header authentication. */
14
+ authHeader: string;
15
+ /** Credential reference; never a plaintext key. */
16
+ apiKeyEnv: string;
17
+ /** Dot-path receiving the search query. */
18
+ queryField: string;
19
+ /** Dot-path receiving the provider result cap; empty omits it. */
20
+ limitField: string;
21
+ /** Dot-path to the response result array. */
22
+ resultsPath: string;
23
+ /** Result title dot-path; empty omits it. */
24
+ titleField: string;
25
+ /** Result URL dot-path; must resolve to an HTTP(S) URL. */
26
+ urlField: string;
27
+ /** Result excerpt dot-path; empty omits it. */
28
+ snippetField: string;
29
+ /** Publication date dot-path; empty omits it. */
30
+ publishedAtField: string;
31
+ /** Numeric relevance score dot-path; empty omits it. */
32
+ scoreField: string;
33
+ /** Numeric provider rank dot-path; empty omits it. */
34
+ positionField: string;
35
+ /** Country request dot-path; empty omits it. */
36
+ countryField: string;
37
+ /** Language request dot-path; empty omits it. */
38
+ languageField: string;
39
+ /** Search type request dot-path; empty omits it. */
40
+ typeField: string;
41
+ /** Date filter request dot-path; empty omits it. */
42
+ dateField: string;
43
+ /** JSON object mapping custom header names to credential references. */
44
+ headerRefs: string;
45
+ /** JSON object with non-secret constant request fields. */
46
+ staticBody: string;
47
+ /** Whole-request deadline in milliseconds, including credentials. */
48
+ timeoutMs: number;
49
+ /** Maximum JSON response size in bytes before parsing. */
50
+ maxResponseBytes: number;
51
+ /** Provider request limit; the common tool may impose a smaller total. */
52
+ maxResults: number;
53
+ }
54
+ /**
55
+ * Reject unsafe paths, credential-bearing endpoints, and malformed JSON at save/load.
56
+ * An empty endpoint is a valid unconfigured draft, rejected at execution.
57
+ * @param config - resolved provider configuration.
58
+ */
59
+ export declare function validateSearchConfig(config: HttpSearchConfig): void;
60
+ /** Shared provider mechanics; provider routing remains in ctx.web. */
61
+ export declare class HttpSearchProvider implements WebSearchProvider {
62
+ readonly descriptor: WebSearchProviderDescriptor;
63
+ private readonly current;
64
+ private readonly resolveKey;
65
+ readonly id: string;
66
+ constructor(descriptor: WebSearchProviderDescriptor, current: () => HttpSearchConfig, resolveKey: (ref: string) => Promise<string | undefined>);
67
+ available(): boolean;
68
+ search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
69
+ }
70
+ //# sourceMappingURL=provider.d.ts.map
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@hydraharness/harness-web-search-http",
3
+ "description": "Serper and configurable JSON HTTP search providers",
4
+ "hydra": {
5
+ "plugin": {
6
+ "application": "Search through Serper or a configured JSON HTTP search endpoint."
7
+ }
8
+ },
9
+ "version": "0.1.1-rc.6",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/MaiHongPhong1902/Hydra-Harness.git",
16
+ "directory": "packages/web/web-search-http"
17
+ },
18
+ "type": "module",
19
+ "main": "lib/index.js",
20
+ "types": "lib/types/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./lib/types/index.d.ts",
24
+ "default": "./lib/index.js"
25
+ },
26
+ "./invariant": {
27
+ "types": "./lib/types/invariant.d.ts",
28
+ "default": "./lib/invariant.js"
29
+ },
30
+ "./src/*": "./src/*",
31
+ "./package.json": "./package.json"
32
+ },
33
+ "files": [
34
+ "lib/index.js",
35
+ "lib/invariant.js",
36
+ "lib/types/**/*.d.ts"
37
+ ],
38
+ "license": "MIT",
39
+ "peerDependencies": {
40
+ "@hydraharness/harness-credentials": "^0.1.1-rc.6",
41
+ "@hydraharness/harness-launch-environment": "^0.1.1-rc.6",
42
+ "@hydraharness/harness-web": "^0.1.1-rc.6",
43
+ "@hydraharness/harness-settings": "^0.1.1-rc.6",
44
+ "@hydraharness/cordis": "^4.0.2",
45
+ "@hydraharness/harness-invariants": "^0.1.1-rc.6"
46
+ },
47
+ "dependencies": {
48
+ "@hydraharness/schemastery": "^3.18.2"
49
+ },
50
+ "devDependencies": {
51
+ "@hydraharness/harness-launch-environment": "^0.1.1-rc.6",
52
+ "@hydraharness/harness-invariants": "^0.1.1-rc.6",
53
+ "@hydraharness/harness-web": "^0.1.1-rc.6",
54
+ "@hydraharness/harness-credentials": "^0.1.1-rc.6",
55
+ "@hydraharness/cordis": "^4.0.2",
56
+ "@hydraharness/harness-web-search-deepseek": "^0.1.1-rc.6",
57
+ "@hydraharness/harness-settings": "^0.1.1-rc.6"
58
+ }
59
+ }