@memberjunction/web-search-engine 0.0.1-seed.1 → 6.2.0-edge.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/LICENSE +183 -0
- package/README.md +343 -3
- package/dist/BaseWebSearchProvider.d.ts +89 -0
- package/dist/BaseWebSearchProvider.d.ts.map +1 -0
- package/dist/BaseWebSearchProvider.js +130 -0
- package/dist/BaseWebSearchProvider.js.map +1 -0
- package/dist/WebSearchEngine.d.ts +107 -0
- package/dist/WebSearchEngine.d.ts.map +1 -0
- package/dist/WebSearchEngine.js +396 -0
- package/dist/WebSearchEngine.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/operations/WebSearchQueryOperation.d.ts +8 -0
- package/dist/operations/WebSearchQueryOperation.d.ts.map +1 -0
- package/dist/operations/WebSearchQueryOperation.js +72 -0
- package/dist/operations/WebSearchQueryOperation.js.map +1 -0
- package/dist/providers/BraveWebSearchProvider.d.ts +41 -0
- package/dist/providers/BraveWebSearchProvider.d.ts.map +1 -0
- package/dist/providers/BraveWebSearchProvider.js +169 -0
- package/dist/providers/BraveWebSearchProvider.js.map +1 -0
- package/dist/providers/DuckDuckGoWebSearchProvider.d.ts +45 -0
- package/dist/providers/DuckDuckGoWebSearchProvider.d.ts.map +1 -0
- package/dist/providers/DuckDuckGoWebSearchProvider.js +116 -0
- package/dist/providers/DuckDuckGoWebSearchProvider.js.map +1 -0
- package/dist/providers/GoogleCustomSearchWebSearchProvider.d.ts +36 -0
- package/dist/providers/GoogleCustomSearchWebSearchProvider.d.ts.map +1 -0
- package/dist/providers/GoogleCustomSearchWebSearchProvider.js +125 -0
- package/dist/providers/GoogleCustomSearchWebSearchProvider.js.map +1 -0
- package/dist/providers/PerplexityWebSearchProvider.d.ts +40 -0
- package/dist/providers/PerplexityWebSearchProvider.d.ts.map +1 -0
- package/dist/providers/PerplexityWebSearchProvider.js +148 -0
- package/dist/providers/PerplexityWebSearchProvider.js.map +1 -0
- package/dist/providers/TavilyWebSearchProvider.d.ts +30 -0
- package/dist/providers/TavilyWebSearchProvider.d.ts.map +1 -0
- package/dist/providers/TavilyWebSearchProvider.js +126 -0
- package/dist/providers/TavilyWebSearchProvider.js.map +1 -0
- package/dist/providers/httpFailure.d.ts +29 -0
- package/dist/providers/httpFailure.d.ts.map +1 -0
- package/dist/providers/httpFailure.js +136 -0
- package/dist/providers/httpFailure.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/package.json +28 -5
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Public surface of the MemberJunction web search engine.
|
|
3
|
+
*
|
|
4
|
+
* Importing this module runs every driver's `@RegisterClass` decorator as a side effect, which
|
|
5
|
+
* is what makes them resolvable by `DriverClass`. That is why the providers are exported here
|
|
6
|
+
* rather than left for consumers to import individually.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/web-search-engine
|
|
9
|
+
*/
|
|
10
|
+
export * from './types.js';
|
|
11
|
+
export * from './BaseWebSearchProvider.js';
|
|
12
|
+
export * from './WebSearchEngine.js';
|
|
13
|
+
export * from './providers/BraveWebSearchProvider.js';
|
|
14
|
+
export * from './providers/TavilyWebSearchProvider.js';
|
|
15
|
+
export * from './providers/PerplexityWebSearchProvider.js';
|
|
16
|
+
export * from './providers/GoogleCustomSearchWebSearchProvider.js';
|
|
17
|
+
export * from './providers/DuckDuckGoWebSearchProvider.js';
|
|
18
|
+
export * from './providers/httpFailure.js';
|
|
19
|
+
export * from './operations/WebSearchQueryOperation.js';
|
|
20
|
+
/**
|
|
21
|
+
* Keep every driver registration alive through tree-shaking.
|
|
22
|
+
*
|
|
23
|
+
* Bundlers drop a module whose exports are unused, and a dropped module never runs its
|
|
24
|
+
* decorator — so the driver silently vanishes from `ClassFactory` and the engine reports
|
|
25
|
+
* "no registered driver" for a `DriverClass` that plainly exists in the source.
|
|
26
|
+
*/
|
|
27
|
+
export declare function LoadWebSearchProviders(): void;
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAGlC,cAAc,oCAAoC,CAAC;AACnD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yCAAyC,CAAC;AACxD,cAAc,iDAAiD,CAAC;AAChE,cAAc,yCAAyC,CAAC;AACxD,cAAc,yBAAyB,CAAC;AACxC,cAAc,sCAAsC,CAAC;AASrD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAO7C"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Public surface of the MemberJunction web search engine.
|
|
3
|
+
*
|
|
4
|
+
* Importing this module runs every driver's `@RegisterClass` decorator as a side effect, which
|
|
5
|
+
* is what makes them resolvable by `DriverClass`. That is why the providers are exported here
|
|
6
|
+
* rather than left for consumers to import individually.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/web-search-engine
|
|
9
|
+
*/
|
|
10
|
+
export * from './types.js';
|
|
11
|
+
export * from './BaseWebSearchProvider.js';
|
|
12
|
+
export * from './WebSearchEngine.js';
|
|
13
|
+
// Drivers — exported for their registration side effect as much as for their types.
|
|
14
|
+
export * from './providers/BraveWebSearchProvider.js';
|
|
15
|
+
export * from './providers/TavilyWebSearchProvider.js';
|
|
16
|
+
export * from './providers/PerplexityWebSearchProvider.js';
|
|
17
|
+
export * from './providers/GoogleCustomSearchWebSearchProvider.js';
|
|
18
|
+
export * from './providers/DuckDuckGoWebSearchProvider.js';
|
|
19
|
+
export * from './providers/httpFailure.js';
|
|
20
|
+
export * from './operations/WebSearchQueryOperation.js';
|
|
21
|
+
import { LoadBraveWebSearchProvider } from './providers/BraveWebSearchProvider.js';
|
|
22
|
+
import { LoadTavilyWebSearchProvider } from './providers/TavilyWebSearchProvider.js';
|
|
23
|
+
import { LoadPerplexityWebSearchProvider } from './providers/PerplexityWebSearchProvider.js';
|
|
24
|
+
import { LoadGoogleCustomSearchWebSearchProvider } from './providers/GoogleCustomSearchWebSearchProvider.js';
|
|
25
|
+
import { LoadDuckDuckGoWebSearchProvider } from './providers/DuckDuckGoWebSearchProvider.js';
|
|
26
|
+
import { LoadWebSearchOperations } from './operations/WebSearchQueryOperation.js';
|
|
27
|
+
/**
|
|
28
|
+
* Keep every driver registration alive through tree-shaking.
|
|
29
|
+
*
|
|
30
|
+
* Bundlers drop a module whose exports are unused, and a dropped module never runs its
|
|
31
|
+
* decorator — so the driver silently vanishes from `ClassFactory` and the engine reports
|
|
32
|
+
* "no registered driver" for a `DriverClass` that plainly exists in the source.
|
|
33
|
+
*/
|
|
34
|
+
export function LoadWebSearchProviders() {
|
|
35
|
+
LoadBraveWebSearchProvider();
|
|
36
|
+
LoadTavilyWebSearchProvider();
|
|
37
|
+
LoadPerplexityWebSearchProvider();
|
|
38
|
+
LoadGoogleCustomSearchWebSearchProvider();
|
|
39
|
+
LoadDuckDuckGoWebSearchProvider();
|
|
40
|
+
LoadWebSearchOperations();
|
|
41
|
+
}
|
|
42
|
+
LoadWebSearchProviders();
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAElC,oFAAoF;AACpF,cAAc,oCAAoC,CAAC;AACnD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yCAAyC,CAAC;AACxD,cAAc,iDAAiD,CAAC;AAChE,cAAc,yCAAyC,CAAC;AACxD,cAAc,yBAAyB,CAAC;AACxC,cAAc,sCAAsC,CAAC;AAErD,OAAO,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAChF,OAAO,EAAE,2BAA2B,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,+BAA+B,EAAE,MAAM,yCAAyC,CAAC;AAC1F,OAAO,EAAE,uCAAuC,EAAE,MAAM,iDAAiD,CAAC;AAC1G,OAAO,EAAE,+BAA+B,EAAE,MAAM,yCAAyC,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AAE/E;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB;IAClC,0BAA0B,EAAE,CAAC;IAC7B,2BAA2B,EAAE,CAAC;IAC9B,+BAA+B,EAAE,CAAC;IAClC,uCAAuC,EAAE,CAAC;IAC1C,+BAA+B,EAAE,CAAC;IAClC,uBAAuB,EAAE,CAAC;AAC9B,CAAC;AAED,sBAAsB,EAAE,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
2
|
+
import { WebSearchQueryOperation, type WebSearchQueryInput, type WebSearchQueryOutput } from '@memberjunction/core-entities';
|
|
3
|
+
export declare class WebSearchQueryServerOperation extends WebSearchQueryOperation {
|
|
4
|
+
protected InternalExecute(input: WebSearchQueryInput, provider: IMetadataProvider, user: UserInfo): Promise<WebSearchQueryOutput>;
|
|
5
|
+
}
|
|
6
|
+
/** Keep the registration from being tree-shaken out of a bundled host. */
|
|
7
|
+
export declare function LoadWebSearchOperations(): void;
|
|
8
|
+
//# sourceMappingURL=WebSearchQueryOperation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebSearchQueryOperation.d.ts","sourceRoot":"","sources":["../../src/operations/WebSearchQueryOperation.ts"],"names":[],"mappings":"AASA,OAAO,EAA0B,iBAAiB,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EACH,uBAAuB,EACvB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EAC5B,MAAM,+BAA+B,CAAC;AAIvC,qBACa,6BAA8B,SAAQ,uBAAuB;cACtD,eAAe,CAC3B,KAAK,EAAE,mBAAmB,EAC1B,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,GACf,OAAO,CAAC,oBAAoB,CAAC;CA+CnC;AAED,0EAA0E;AAC1E,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview `WebSearch.Query` — server implementation of the Web Search remote operation.
|
|
9
|
+
*
|
|
10
|
+
* Exposes WebSearchEngine to client applications through MemberJunction's Remote Operations
|
|
11
|
+
* RPC gateway with `websearch:execute` API scope enforcement.
|
|
12
|
+
*
|
|
13
|
+
* @module @memberjunction/web-search-engine
|
|
14
|
+
*/
|
|
15
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
16
|
+
import { BaseRemotableOperation } from '@memberjunction/core';
|
|
17
|
+
import { WebSearchQueryOperation, } from '@memberjunction/core-entities';
|
|
18
|
+
import { WebSearchEngine } from '../WebSearchEngine.js';
|
|
19
|
+
let WebSearchQueryServerOperation = class WebSearchQueryServerOperation extends WebSearchQueryOperation {
|
|
20
|
+
async InternalExecute(input, provider, user) {
|
|
21
|
+
const query = input?.query?.trim();
|
|
22
|
+
if (!query) {
|
|
23
|
+
throw new Error('query is required');
|
|
24
|
+
}
|
|
25
|
+
await WebSearchEngine.Instance.Config(false, user, provider);
|
|
26
|
+
const searchParams = {
|
|
27
|
+
Query: query,
|
|
28
|
+
MaxResults: input.maxResults,
|
|
29
|
+
Provider: input.provider,
|
|
30
|
+
IncludeDomains: input.includeDomains,
|
|
31
|
+
ExcludeDomains: input.excludeDomains,
|
|
32
|
+
Freshness: input.freshness,
|
|
33
|
+
Country: input.country,
|
|
34
|
+
Language: input.language,
|
|
35
|
+
SafeSearch: input.safeSearch,
|
|
36
|
+
IncludeAnswer: input.includeAnswer,
|
|
37
|
+
};
|
|
38
|
+
const result = await WebSearchEngine.Instance.Search(searchParams, user);
|
|
39
|
+
if (!result.Success) {
|
|
40
|
+
throw new Error(`${result.ResultCode}: ${result.ErrorMessage ?? 'Search failed'}`);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
hits: (result.Hits ?? []).map((hit) => ({
|
|
44
|
+
title: hit.Title,
|
|
45
|
+
url: hit.URL,
|
|
46
|
+
snippet: hit.Snippet,
|
|
47
|
+
displayUrl: hit.DisplayURL,
|
|
48
|
+
publishedAt: hit.PublishedAt instanceof Date ? hit.PublishedAt.toISOString() : undefined,
|
|
49
|
+
score: hit.Score,
|
|
50
|
+
})),
|
|
51
|
+
answer: result.Answer,
|
|
52
|
+
providerUsed: result.ProviderUsed ?? '',
|
|
53
|
+
attempts: (result.Attempts ?? []).map((attempt) => ({
|
|
54
|
+
providerName: attempt.ProviderName,
|
|
55
|
+
succeeded: attempt.Succeeded,
|
|
56
|
+
durationMs: attempt.DurationMs,
|
|
57
|
+
hitCount: attempt.HitCount,
|
|
58
|
+
failureKind: attempt.FailureKind,
|
|
59
|
+
errorMessage: attempt.ErrorMessage,
|
|
60
|
+
})),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
WebSearchQueryServerOperation = __decorate([
|
|
65
|
+
RegisterClass(BaseRemotableOperation, 'WebSearch.Query')
|
|
66
|
+
], WebSearchQueryServerOperation);
|
|
67
|
+
export { WebSearchQueryServerOperation };
|
|
68
|
+
/** Keep the registration from being tree-shaken out of a bundled host. */
|
|
69
|
+
export function LoadWebSearchOperations() {
|
|
70
|
+
void WebSearchQueryServerOperation;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=WebSearchQueryOperation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebSearchQueryOperation.js","sourceRoot":"","sources":["../../src/operations/WebSearchQueryOperation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Brave Search driver.
|
|
3
|
+
* @module @memberjunction/web-search-engine
|
|
4
|
+
*/
|
|
5
|
+
import { UserInfo } from '@memberjunction/core';
|
|
6
|
+
import { BaseWebSearchProvider } from '../BaseWebSearchProvider.js';
|
|
7
|
+
import { WebSearchCapabilities, WebSearchParams, WebSearchProviderResponse } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Searches the web through the Brave Search API.
|
|
10
|
+
*
|
|
11
|
+
* Brave is the recommended default for a structural reason rather than a qualitative one: it
|
|
12
|
+
* serves from **its own index** rather than reselling Google's or Bing's. Every SERP-proxy
|
|
13
|
+
* vendor, and the Google Custom Search JSON API that this engine exists to replace, depends on
|
|
14
|
+
* a search engine continuing to permit that access. Brave does not, so it fails independently
|
|
15
|
+
* of every other driver here — which is the entire point of having a provider set.
|
|
16
|
+
*
|
|
17
|
+
* Credential key `apiKey`; environment fallback `BRAVE_SEARCH_API_KEY`.
|
|
18
|
+
*/
|
|
19
|
+
export declare class BraveWebSearchProvider extends BaseWebSearchProvider {
|
|
20
|
+
private static readonly ENDPOINT;
|
|
21
|
+
/** Brave rejects queries longer than this outright. */
|
|
22
|
+
private static readonly MAX_QUERY_CHARS;
|
|
23
|
+
/** Brave's cap on the pagination offset, in pages. */
|
|
24
|
+
private static readonly MAX_OFFSET;
|
|
25
|
+
readonly Capabilities: WebSearchCapabilities;
|
|
26
|
+
private apiKey;
|
|
27
|
+
CheckAvailability(_contextUser: UserInfo): Promise<void>;
|
|
28
|
+
ExecuteSearch(params: WebSearchParams, _contextUser: UserInfo): Promise<WebSearchProviderResponse>;
|
|
29
|
+
/**
|
|
30
|
+
* Brave has no domain-filter request parameter — it expresses domain scoping through query
|
|
31
|
+
* operators, so that is how the capability is honoured. Includes become an OR-group of
|
|
32
|
+
* `site:` terms; excludes become `-site:` terms.
|
|
33
|
+
*/
|
|
34
|
+
private applyDomainFilters;
|
|
35
|
+
/** Brave expresses relative windows as pd/pw/pm/py and ranges as `YYYY-MM-DDtoYYYY-MM-DD`. */
|
|
36
|
+
private mapFreshness;
|
|
37
|
+
private toHit;
|
|
38
|
+
}
|
|
39
|
+
/** Anti-tree-shaking anchor — importing this module is what runs the @RegisterClass decorator. */
|
|
40
|
+
export declare function LoadBraveWebSearchProvider(): void;
|
|
41
|
+
//# sourceMappingURL=BraveWebSearchProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BraveWebSearchProvider.d.ts","sourceRoot":"","sources":["../../src/providers/BraveWebSearchProvider.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EACH,qBAAqB,EAErB,eAAe,EACf,yBAAyB,EAC5B,MAAM,UAAU,CAAC;AAkBlB;;;;;;;;;;GAUG;AACH,qBACa,sBAAuB,SAAQ,qBAAqB;IAC7D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAoD;IACpF,uDAAuD;IACvD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAO;IAC9C,sDAAsD;IACtD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAK;IAEvC,SAAgB,YAAY,EAAE,qBAAqB,CAMjD;IAEF,OAAO,CAAC,MAAM,CAAqB;IAEtB,iBAAiB,CAAC,YAAY,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAWxD,aAAa,CACtB,MAAM,EAAE,eAAe,EACvB,YAAY,EAAE,QAAQ,GACvB,OAAO,CAAC,yBAAyB,CAAC;IA4DrC;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAgB1B,8FAA8F;IAC9F,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,KAAK;CA2BhB;AAED,kGAAkG;AAClG,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Brave Search driver.
|
|
3
|
+
* @module @memberjunction/web-search-engine
|
|
4
|
+
*/
|
|
5
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
6
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
7
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
8
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
var BraveWebSearchProvider_1;
|
|
12
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
13
|
+
import { HttpGet } from '@memberjunction/network-utils';
|
|
14
|
+
import { BaseWebSearchProvider } from '../BaseWebSearchProvider.js';
|
|
15
|
+
import { classifyHttpFailure, failure } from './httpFailure.js';
|
|
16
|
+
/**
|
|
17
|
+
* Searches the web through the Brave Search API.
|
|
18
|
+
*
|
|
19
|
+
* Brave is the recommended default for a structural reason rather than a qualitative one: it
|
|
20
|
+
* serves from **its own index** rather than reselling Google's or Bing's. Every SERP-proxy
|
|
21
|
+
* vendor, and the Google Custom Search JSON API that this engine exists to replace, depends on
|
|
22
|
+
* a search engine continuing to permit that access. Brave does not, so it fails independently
|
|
23
|
+
* of every other driver here — which is the entire point of having a provider set.
|
|
24
|
+
*
|
|
25
|
+
* Credential key `apiKey`; environment fallback `BRAVE_SEARCH_API_KEY`.
|
|
26
|
+
*/
|
|
27
|
+
let BraveWebSearchProvider = class BraveWebSearchProvider extends BaseWebSearchProvider {
|
|
28
|
+
constructor() {
|
|
29
|
+
super(...arguments);
|
|
30
|
+
this.Capabilities = {
|
|
31
|
+
Answer: false,
|
|
32
|
+
DomainFilter: true,
|
|
33
|
+
Freshness: true,
|
|
34
|
+
Region: true,
|
|
35
|
+
MaxResultsCap: 20,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
static { BraveWebSearchProvider_1 = this; }
|
|
39
|
+
static { this.ENDPOINT = 'https://api.search.brave.com/res/v1/web/search'; }
|
|
40
|
+
/** Brave rejects queries longer than this outright. */
|
|
41
|
+
static { this.MAX_QUERY_CHARS = 400; }
|
|
42
|
+
/** Brave's cap on the pagination offset, in pages. */
|
|
43
|
+
static { this.MAX_OFFSET = 9; }
|
|
44
|
+
async CheckAvailability(_contextUser) {
|
|
45
|
+
this.apiKey = this.GetSecret('apiKey', 'BRAVE_SEARCH_API_KEY');
|
|
46
|
+
if (this.apiKey) {
|
|
47
|
+
this.MarkAvailable();
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.MarkUnavailable('No Brave API key. Set it on the linked Credential (key `apiKey`) or as BRAVE_SEARCH_API_KEY.');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async ExecuteSearch(params, _contextUser) {
|
|
54
|
+
// Rejected rather than truncated: silently cutting a query changes what was asked and
|
|
55
|
+
// returns confident results for a different question. Permanent — no other provider
|
|
56
|
+
// will accept it either.
|
|
57
|
+
if (params.Query.length > BraveWebSearchProvider_1.MAX_QUERY_CHARS) {
|
|
58
|
+
return failure('permanent', `Query is ${params.Query.length} characters; Brave's limit is ${BraveWebSearchProvider_1.MAX_QUERY_CHARS}.`);
|
|
59
|
+
}
|
|
60
|
+
const q = this.applyDomainFilters(params);
|
|
61
|
+
if (q.length > BraveWebSearchProvider_1.MAX_QUERY_CHARS) {
|
|
62
|
+
return failure('permanent', `Query plus domain filters is ${q.length} characters; Brave's limit is ${BraveWebSearchProvider_1.MAX_QUERY_CHARS}.`);
|
|
63
|
+
}
|
|
64
|
+
const query = {
|
|
65
|
+
q,
|
|
66
|
+
count: this.ResolveMaxResults(params.MaxResults),
|
|
67
|
+
offset: 0,
|
|
68
|
+
safesearch: params.SafeSearch ?? 'moderate',
|
|
69
|
+
};
|
|
70
|
+
const freshness = this.mapFreshness(params);
|
|
71
|
+
if (freshness) {
|
|
72
|
+
query.freshness = freshness;
|
|
73
|
+
}
|
|
74
|
+
if (params.Country) {
|
|
75
|
+
query.country = params.Country.toUpperCase();
|
|
76
|
+
}
|
|
77
|
+
if (params.Language) {
|
|
78
|
+
query.search_lang = params.Language.toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const response = await HttpGet(BraveWebSearchProvider_1.ENDPOINT, {
|
|
82
|
+
Query: query,
|
|
83
|
+
Headers: {
|
|
84
|
+
'X-Subscription-Token': this.apiKey ?? '',
|
|
85
|
+
Accept: 'application/json',
|
|
86
|
+
},
|
|
87
|
+
Timeout: 15000,
|
|
88
|
+
});
|
|
89
|
+
if (!response.Data) {
|
|
90
|
+
return failure('transient', 'Empty response from Brave Search.');
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
Success: true,
|
|
94
|
+
Hits: (response.Data.web?.results ?? []).map((item) => this.toHit(item)),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
return classifyHttpFailure(e, 'Brave');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Brave has no domain-filter request parameter — it expresses domain scoping through query
|
|
103
|
+
* operators, so that is how the capability is honoured. Includes become an OR-group of
|
|
104
|
+
* `site:` terms; excludes become `-site:` terms.
|
|
105
|
+
*/
|
|
106
|
+
applyDomainFilters(params) {
|
|
107
|
+
const parts = [params.Query.trim()];
|
|
108
|
+
const include = (params.IncludeDomains ?? []).map((d) => d.trim()).filter(Boolean);
|
|
109
|
+
const exclude = (params.ExcludeDomains ?? []).map((d) => d.trim()).filter(Boolean);
|
|
110
|
+
if (include.length === 1) {
|
|
111
|
+
parts.push(`site:${include[0]}`);
|
|
112
|
+
}
|
|
113
|
+
else if (include.length > 1) {
|
|
114
|
+
parts.push(`(${include.map((d) => `site:${d}`).join(' OR ')})`);
|
|
115
|
+
}
|
|
116
|
+
for (const domain of exclude) {
|
|
117
|
+
parts.push(`-site:${domain}`);
|
|
118
|
+
}
|
|
119
|
+
return parts.join(' ');
|
|
120
|
+
}
|
|
121
|
+
/** Brave expresses relative windows as pd/pw/pm/py and ranges as `YYYY-MM-DDtoYYYY-MM-DD`. */
|
|
122
|
+
mapFreshness(params) {
|
|
123
|
+
const freshness = params.Freshness;
|
|
124
|
+
if (!freshness) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (typeof freshness === 'string') {
|
|
128
|
+
return { day: 'pd', week: 'pw', month: 'pm', year: 'py' }[freshness];
|
|
129
|
+
}
|
|
130
|
+
const iso = (d) => d.toISOString().slice(0, 10);
|
|
131
|
+
return `${iso(freshness.From)}to${iso(freshness.To)}`;
|
|
132
|
+
}
|
|
133
|
+
toHit(item) {
|
|
134
|
+
const hit = {
|
|
135
|
+
Title: item.title ?? '',
|
|
136
|
+
URL: item.url ?? '',
|
|
137
|
+
Snippet: item.description ?? '',
|
|
138
|
+
DisplayURL: item.meta_url?.netloc ?? item.meta_url?.hostname ?? undefined,
|
|
139
|
+
};
|
|
140
|
+
if (item.page_age) {
|
|
141
|
+
const parsed = new Date(item.page_age);
|
|
142
|
+
if (!isNaN(parsed.getTime())) {
|
|
143
|
+
hit.PublishedAt = parsed;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Brave's relative age string and extra snippets have no slot in the normalised shape,
|
|
147
|
+
// and they are part of why Brave is worth having — so they ride along here.
|
|
148
|
+
const extras = {};
|
|
149
|
+
if (item.age) {
|
|
150
|
+
extras.age = item.age;
|
|
151
|
+
}
|
|
152
|
+
if (item.extra_snippets?.length) {
|
|
153
|
+
extras.extraSnippets = item.extra_snippets;
|
|
154
|
+
}
|
|
155
|
+
if (Object.keys(extras).length > 0) {
|
|
156
|
+
hit.ProviderExtras = extras;
|
|
157
|
+
}
|
|
158
|
+
return hit;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
BraveWebSearchProvider = BraveWebSearchProvider_1 = __decorate([
|
|
162
|
+
RegisterClass(BaseWebSearchProvider, 'BraveWebSearchProvider')
|
|
163
|
+
], BraveWebSearchProvider);
|
|
164
|
+
export { BraveWebSearchProvider };
|
|
165
|
+
/** Anti-tree-shaking anchor — importing this module is what runs the @RegisterClass decorator. */
|
|
166
|
+
export function LoadBraveWebSearchProvider() {
|
|
167
|
+
void BraveWebSearchProvider;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=BraveWebSearchProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BraveWebSearchProvider.js","sourceRoot":"","sources":["../../src/providers/BraveWebSearchProvider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview DuckDuckGo driver — keyless last resort.
|
|
3
|
+
* @module @memberjunction/web-search-engine
|
|
4
|
+
*/
|
|
5
|
+
import { UserInfo } from '@memberjunction/core';
|
|
6
|
+
import { BaseWebSearchProvider } from '../BaseWebSearchProvider.js';
|
|
7
|
+
import { WebSearchCapabilities, WebSearchParams, WebSearchProviderResponse } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Searches through DuckDuckGo's Instant Answer API.
|
|
10
|
+
*
|
|
11
|
+
* **This is a last resort, and it is ordered last for good reasons.** The Instant Answer API is
|
|
12
|
+
* not a web search API: it returns disambiguation topics and an abstract, not a ranked result
|
|
13
|
+
* set, so it answers only a minority of queries and returns nothing at all for many ordinary
|
|
14
|
+
* ones. It also rate-limits aggressively and without documentation.
|
|
15
|
+
*
|
|
16
|
+
* It earns its place because it needs **no credential**, so a host that has configured nothing
|
|
17
|
+
* still gets some web capability rather than a hard failure. Treat a deployment that relies on
|
|
18
|
+
* it as unconfigured, not as working — configure Brave or Tavily for real coverage.
|
|
19
|
+
*
|
|
20
|
+
* ## Why there is no HTML fallback
|
|
21
|
+
*
|
|
22
|
+
* An earlier version fell back to fetching and regex-parsing DuckDuckGo's HTML results page,
|
|
23
|
+
* which answered far more queries. It was removed deliberately: running regular expressions over
|
|
24
|
+
* remote HTML is running them over **attacker-influenceable input**, and the parser carried two
|
|
25
|
+
* high-severity findings — polynomial backtracking in the result patterns (the `[^"]*…[^"]*`
|
|
26
|
+
* shape) and incomplete sanitization in its tag-stripping.
|
|
27
|
+
*
|
|
28
|
+
* Hardening those patterns would have narrowed the surface; deleting the parser removes it. For a
|
|
29
|
+
* fallback *inside* a last-resort provider that this engine already tells operators not to rely
|
|
30
|
+
* on, the capability was not worth the exposure — and "the regex looks safe now" is exactly the
|
|
31
|
+
* kind of claim that is true when written and wrong later. Anything needing real web coverage
|
|
32
|
+
* should configure a provider with an API.
|
|
33
|
+
*/
|
|
34
|
+
export declare class DuckDuckGoWebSearchProvider extends BaseWebSearchProvider {
|
|
35
|
+
private static readonly ENDPOINT;
|
|
36
|
+
readonly Capabilities: WebSearchCapabilities;
|
|
37
|
+
/** Keyless, so always available — which is the entire reason it is the fallback. */
|
|
38
|
+
CheckAvailability(_contextUser: UserInfo): Promise<void>;
|
|
39
|
+
ExecuteSearch(params: WebSearchParams, _contextUser: UserInfo): Promise<WebSearchProviderResponse>;
|
|
40
|
+
/** RelatedTopics nests one level for disambiguation groups; flatten to a single list. */
|
|
41
|
+
private flattenTopics;
|
|
42
|
+
}
|
|
43
|
+
/** Anti-tree-shaking anchor. */
|
|
44
|
+
export declare function LoadDuckDuckGoWebSearchProvider(): void;
|
|
45
|
+
//# sourceMappingURL=DuckDuckGoWebSearchProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DuckDuckGoWebSearchProvider.d.ts","sourceRoot":"","sources":["../../src/providers/DuckDuckGoWebSearchProvider.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAgB,eAAe,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAgB3G;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBACa,2BAA4B,SAAQ,qBAAqB;IAClE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAiC;IAEjE,SAAgB,YAAY,EAAE,qBAAqB,CAMjD;IAEF,oFAAoF;IACvE,iBAAiB,CAAC,YAAY,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxD,aAAa,CACtB,MAAM,EAAE,eAAe,EACvB,YAAY,EAAE,QAAQ,GACvB,OAAO,CAAC,yBAAyB,CAAC;IAgCrC,yFAAyF;IACzF,OAAO,CAAC,aAAa;CAmBxB;AAED,gCAAgC;AAChC,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview DuckDuckGo driver — keyless last resort.
|
|
3
|
+
* @module @memberjunction/web-search-engine
|
|
4
|
+
*/
|
|
5
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
6
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
7
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
8
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
var DuckDuckGoWebSearchProvider_1;
|
|
12
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
13
|
+
import { HttpGet } from '@memberjunction/network-utils';
|
|
14
|
+
import { BaseWebSearchProvider } from '../BaseWebSearchProvider.js';
|
|
15
|
+
import { classifyHttpFailure, failure } from './httpFailure.js';
|
|
16
|
+
/**
|
|
17
|
+
* Searches through DuckDuckGo's Instant Answer API.
|
|
18
|
+
*
|
|
19
|
+
* **This is a last resort, and it is ordered last for good reasons.** The Instant Answer API is
|
|
20
|
+
* not a web search API: it returns disambiguation topics and an abstract, not a ranked result
|
|
21
|
+
* set, so it answers only a minority of queries and returns nothing at all for many ordinary
|
|
22
|
+
* ones. It also rate-limits aggressively and without documentation.
|
|
23
|
+
*
|
|
24
|
+
* It earns its place because it needs **no credential**, so a host that has configured nothing
|
|
25
|
+
* still gets some web capability rather than a hard failure. Treat a deployment that relies on
|
|
26
|
+
* it as unconfigured, not as working — configure Brave or Tavily for real coverage.
|
|
27
|
+
*
|
|
28
|
+
* ## Why there is no HTML fallback
|
|
29
|
+
*
|
|
30
|
+
* An earlier version fell back to fetching and regex-parsing DuckDuckGo's HTML results page,
|
|
31
|
+
* which answered far more queries. It was removed deliberately: running regular expressions over
|
|
32
|
+
* remote HTML is running them over **attacker-influenceable input**, and the parser carried two
|
|
33
|
+
* high-severity findings — polynomial backtracking in the result patterns (the `[^"]*…[^"]*`
|
|
34
|
+
* shape) and incomplete sanitization in its tag-stripping.
|
|
35
|
+
*
|
|
36
|
+
* Hardening those patterns would have narrowed the surface; deleting the parser removes it. For a
|
|
37
|
+
* fallback *inside* a last-resort provider that this engine already tells operators not to rely
|
|
38
|
+
* on, the capability was not worth the exposure — and "the regex looks safe now" is exactly the
|
|
39
|
+
* kind of claim that is true when written and wrong later. Anything needing real web coverage
|
|
40
|
+
* should configure a provider with an API.
|
|
41
|
+
*/
|
|
42
|
+
let DuckDuckGoWebSearchProvider = class DuckDuckGoWebSearchProvider extends BaseWebSearchProvider {
|
|
43
|
+
constructor() {
|
|
44
|
+
super(...arguments);
|
|
45
|
+
this.Capabilities = {
|
|
46
|
+
Answer: false,
|
|
47
|
+
DomainFilter: false,
|
|
48
|
+
Freshness: false,
|
|
49
|
+
Region: false,
|
|
50
|
+
MaxResultsCap: 30,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
static { DuckDuckGoWebSearchProvider_1 = this; }
|
|
54
|
+
static { this.ENDPOINT = 'https://api.duckduckgo.com/'; }
|
|
55
|
+
/** Keyless, so always available — which is the entire reason it is the fallback. */
|
|
56
|
+
async CheckAvailability(_contextUser) {
|
|
57
|
+
this.MarkAvailable();
|
|
58
|
+
}
|
|
59
|
+
async ExecuteSearch(params, _contextUser) {
|
|
60
|
+
try {
|
|
61
|
+
const response = await HttpGet(DuckDuckGoWebSearchProvider_1.ENDPOINT, {
|
|
62
|
+
Query: { q: params.Query, format: 'json', no_html: 1, skip_disambig: 1 },
|
|
63
|
+
Timeout: 15000,
|
|
64
|
+
});
|
|
65
|
+
if (!response.Data) {
|
|
66
|
+
return failure('transient', 'Empty response from DuckDuckGo.');
|
|
67
|
+
}
|
|
68
|
+
const maxResults = this.ResolveMaxResults(params.MaxResults);
|
|
69
|
+
const hits = this.flattenTopics(response.Data.RelatedTopics ?? []);
|
|
70
|
+
// The abstract, when present, is the most relevant thing DDG returns — it leads.
|
|
71
|
+
if (response.Data.AbstractText && response.Data.AbstractURL) {
|
|
72
|
+
hits.unshift({
|
|
73
|
+
Title: response.Data.Heading ?? params.Query,
|
|
74
|
+
URL: response.Data.AbstractURL,
|
|
75
|
+
Snippet: response.Data.AbstractText,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// Zero hits is common here and is reported honestly rather than escalated: the engine
|
|
79
|
+
// treats it as a successful empty answer, so a caller that needs coverage sees the
|
|
80
|
+
// gap instead of a failure it might retry.
|
|
81
|
+
return { Success: true, Hits: hits.slice(0, maxResults) };
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
return classifyHttpFailure(e, 'DuckDuckGo');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** RelatedTopics nests one level for disambiguation groups; flatten to a single list. */
|
|
88
|
+
flattenTopics(topics) {
|
|
89
|
+
const hits = [];
|
|
90
|
+
for (const topic of topics) {
|
|
91
|
+
if (topic.Topics?.length) {
|
|
92
|
+
hits.push(...this.flattenTopics(topic.Topics));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (topic.FirstURL && topic.Text) {
|
|
96
|
+
// DDG puts "Title - description" in one string; split on the first separator only.
|
|
97
|
+
const separator = topic.Text.indexOf(' - ');
|
|
98
|
+
hits.push({
|
|
99
|
+
Title: separator > 0 ? topic.Text.slice(0, separator) : topic.Text,
|
|
100
|
+
URL: topic.FirstURL,
|
|
101
|
+
Snippet: separator > 0 ? topic.Text.slice(separator + 3) : topic.Text,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return hits;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
DuckDuckGoWebSearchProvider = DuckDuckGoWebSearchProvider_1 = __decorate([
|
|
109
|
+
RegisterClass(BaseWebSearchProvider, 'DuckDuckGoWebSearchProvider')
|
|
110
|
+
], DuckDuckGoWebSearchProvider);
|
|
111
|
+
export { DuckDuckGoWebSearchProvider };
|
|
112
|
+
/** Anti-tree-shaking anchor. */
|
|
113
|
+
export function LoadDuckDuckGoWebSearchProvider() {
|
|
114
|
+
void DuckDuckGoWebSearchProvider;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=DuckDuckGoWebSearchProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DuckDuckGoWebSearchProvider.js","sourceRoot":"","sources":["../../src/providers/DuckDuckGoWebSearchProvider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Google Custom Search driver — retiring 2027-01-01.
|
|
3
|
+
* @module @memberjunction/web-search-engine
|
|
4
|
+
*/
|
|
5
|
+
import { UserInfo } from '@memberjunction/core';
|
|
6
|
+
import { BaseWebSearchProvider } from '../BaseWebSearchProvider.js';
|
|
7
|
+
import { WebSearchCapabilities, WebSearchParams, WebSearchProviderResponse } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Searches through the Google Custom Search JSON API.
|
|
10
|
+
*
|
|
11
|
+
* **This provider has an end date.** Google closed the API to new customers during 2025 and
|
|
12
|
+
* discontinues it on **2027-01-01**. It ships so that hosts already holding a key keep working
|
|
13
|
+
* through the transition, and it is ordered behind Brave and Tavily deliberately.
|
|
14
|
+
*
|
|
15
|
+
* It needs no dated code change to retire: once Google stops serving, calls fail, the engine
|
|
16
|
+
* fails over, and removing the metadata row takes it out of rotation entirely. A host that
|
|
17
|
+
* never had a key sees `CheckAvailability` self-disable it on the first config, which is the
|
|
18
|
+
* ordinary "not configured here" path rather than an error.
|
|
19
|
+
*
|
|
20
|
+
* Credential keys `apiKey` and `cx`; environment fallbacks `GOOGLE_CUSTOM_SEARCH_API_KEY`
|
|
21
|
+
* and `GOOGLE_CUSTOM_SEARCH_CX`.
|
|
22
|
+
*/
|
|
23
|
+
export declare class GoogleCustomSearchWebSearchProvider extends BaseWebSearchProvider {
|
|
24
|
+
private static readonly ENDPOINT;
|
|
25
|
+
readonly Capabilities: WebSearchCapabilities;
|
|
26
|
+
private apiKey;
|
|
27
|
+
private cx;
|
|
28
|
+
CheckAvailability(_contextUser: UserInfo): Promise<void>;
|
|
29
|
+
ExecuteSearch(params: WebSearchParams, _contextUser: UserInfo): Promise<WebSearchProviderResponse>;
|
|
30
|
+
/** Google expresses recency as `d7`, `w2`, `m3`, `y1`. */
|
|
31
|
+
private mapFreshness;
|
|
32
|
+
private toHit;
|
|
33
|
+
}
|
|
34
|
+
/** Anti-tree-shaking anchor. */
|
|
35
|
+
export declare function LoadGoogleCustomSearchWebSearchProvider(): void;
|
|
36
|
+
//# sourceMappingURL=GoogleCustomSearchWebSearchProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GoogleCustomSearchWebSearchProvider.d.ts","sourceRoot":"","sources":["../../src/providers/GoogleCustomSearchWebSearchProvider.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAgB,eAAe,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAe3G;;;;;;;;;;;;;;GAcG;AACH,qBACa,mCAAoC,SAAQ,qBAAqB;IAC1E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAgD;IAEhF,SAAgB,YAAY,EAAE,qBAAqB,CAOjD;IAEF,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,EAAE,CAAqB;IAElB,iBAAiB,CAAC,YAAY,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAaxD,aAAa,CACtB,MAAM,EAAE,eAAe,EACvB,YAAY,EAAE,QAAQ,GACvB,OAAO,CAAC,yBAAyB,CAAC;IA4CrC,0DAA0D;IAC1D,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,KAAK;CAQhB;AAED,gCAAgC;AAChC,wBAAgB,uCAAuC,IAAI,IAAI,CAE9D"}
|