@duckmind/dm-windows-x64 0.63.0 → 0.63.4
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/dm.exe +0 -0
- package/extensions/.dm-extensions.json +138 -2
- package/extensions/dm-web-access/SECURITY.md +5 -0
- package/extensions/dm-web-access/activity.js +65 -0
- package/extensions/dm-web-access/anysearch.js +158 -0
- package/extensions/dm-web-access/auth-fetch.js +131 -0
- package/extensions/dm-web-access/bocha.js +214 -0
- package/extensions/dm-web-access/brave.js +196 -0
- package/extensions/dm-web-access/brightdata-unlocker.js +202 -0
- package/extensions/dm-web-access/brightdata.js +334 -0
- package/extensions/dm-web-access/chrome-cookies.js +627 -0
- package/extensions/dm-web-access/content-find.js +114 -0
- package/extensions/dm-web-access/credential-source.js +150 -0
- package/extensions/dm-web-access/curator-page.js +3559 -0
- package/extensions/dm-web-access/curator-server.js +691 -0
- package/extensions/dm-web-access/data-uri-sanitize.js +312 -0
- package/extensions/dm-web-access/datalab-pdf-extract.js +346 -0
- package/extensions/dm-web-access/declared-web-links.js +167 -0
- package/extensions/dm-web-access/dm-web-fetch-demo.mp4 +0 -0
- package/extensions/dm-web-access/duckduckgo.js +118 -0
- package/extensions/dm-web-access/exa.js +401 -0
- package/extensions/dm-web-access/extract.js +1220 -0
- package/extensions/dm-web-access/feature-config.js +24 -0
- package/extensions/dm-web-access/fetch-params.js +81 -0
- package/extensions/dm-web-access/firecrawl.js +378 -0
- package/extensions/dm-web-access/gemini-adc.js +241 -0
- package/extensions/dm-web-access/gemini-api.js +258 -0
- package/extensions/dm-web-access/gemini-pdf-extract.js +74 -0
- package/extensions/dm-web-access/gemini-search.js +889 -0
- package/extensions/dm-web-access/gemini-url-context.js +97 -0
- package/extensions/dm-web-access/gemini-web-config.js +84 -0
- package/extensions/dm-web-access/gemini-web.js +351 -0
- package/extensions/dm-web-access/github-api.js +166 -0
- package/extensions/dm-web-access/github-extract.js +991 -0
- package/extensions/dm-web-access/github-issue-pr.js +750 -0
- package/extensions/dm-web-access/index.js +3117 -0
- package/extensions/dm-web-access/jina-search.js +242 -0
- package/extensions/dm-web-access/kagi.js +255 -0
- package/extensions/dm-web-access/kimi-search.js +214 -0
- package/extensions/dm-web-access/ollama.js +209 -0
- package/extensions/dm-web-access/openai-search.js +510 -0
- package/extensions/dm-web-access/package.json +34 -0
- package/extensions/dm-web-access/page-query.js +124 -0
- package/extensions/dm-web-access/parallel-mcp.js +223 -0
- package/extensions/dm-web-access/parallel.js +345 -0
- package/extensions/dm-web-access/pdf-extract.js +257 -0
- package/extensions/dm-web-access/perplexity.js +151 -0
- package/extensions/dm-web-access/querit.js +327 -0
- package/extensions/dm-web-access/query-rewrite.js +40 -0
- package/extensions/dm-web-access/render-search-error.js +80 -0
- package/extensions/dm-web-access/rsc-extract.js +347 -0
- package/extensions/dm-web-access/search1api.js +245 -0
- package/extensions/dm-web-access/searchinfinity.js +221 -0
- package/extensions/dm-web-access/searxng.js +223 -0
- package/extensions/dm-web-access/serpbase.js +205 -0
- package/extensions/dm-web-access/serpdive.js +238 -0
- package/extensions/dm-web-access/serper.js +200 -0
- package/extensions/dm-web-access/source-check.js +198 -0
- package/extensions/dm-web-access/ssrf-protection.js +436 -0
- package/extensions/dm-web-access/storage.js +451 -0
- package/extensions/dm-web-access/summary-model-scope.js +83 -0
- package/extensions/dm-web-access/summary-review.js +364 -0
- package/extensions/dm-web-access/tavily.js +199 -0
- package/extensions/dm-web-access/tinyfish.js +325 -0
- package/extensions/dm-web-access/utils.js +476 -0
- package/extensions/dm-web-access/valyu.js +189 -0
- package/extensions/dm-web-access/video-extract.js +336 -0
- package/extensions/dm-web-access/xai-search.js +285 -0
- package/extensions/dm-web-access/xcrawl.js +221 -0
- package/extensions/dm-web-access/youtube-extract.js +279 -0
- package/package.json +9 -1
package/dm.exe
CHANGED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"status": "ok",
|
|
3
|
-
"prepared_at": "2026-08-
|
|
3
|
+
"prepared_at": "2026-08-31T16:25:06.094476+00:00",
|
|
4
4
|
"managed_entries": [
|
|
5
5
|
{
|
|
6
6
|
"id": "dm-context",
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"id": "dm-localllm-provider",
|
|
37
37
|
"target_dir": "extensions/dm-localllm-provider",
|
|
38
38
|
"bundle_mode": "source-package"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "dm-web-access",
|
|
42
|
+
"target_dir": "extensions/dm-web-access",
|
|
43
|
+
"bundle_mode": "source-package-with-runtime-deps"
|
|
39
44
|
}
|
|
40
45
|
],
|
|
41
46
|
"local_entries": [
|
|
@@ -593,6 +598,121 @@
|
|
|
593
598
|
"declaration_files_removed": 0
|
|
594
599
|
}
|
|
595
600
|
},
|
|
601
|
+
{
|
|
602
|
+
"id": "dm-web-access",
|
|
603
|
+
"staged_dir": "extensions/dm-web-access",
|
|
604
|
+
"bundle_mode": "source-package-with-runtime-deps",
|
|
605
|
+
"dependencies_installed": false,
|
|
606
|
+
"dependency_mode": "platform-package-dependency",
|
|
607
|
+
"dependencies_declared": [
|
|
608
|
+
"@duckmind/dm-ai",
|
|
609
|
+
"@duckmind/dm-coding-agent",
|
|
610
|
+
"@duckmind/dm-tui",
|
|
611
|
+
"@mozilla/readability",
|
|
612
|
+
"defuddle",
|
|
613
|
+
"linkedom",
|
|
614
|
+
"p-limit",
|
|
615
|
+
"promise.try",
|
|
616
|
+
"turndown",
|
|
617
|
+
"typebox",
|
|
618
|
+
"undici",
|
|
619
|
+
"unpdf"
|
|
620
|
+
],
|
|
621
|
+
"dependency_patches": [
|
|
622
|
+
"dm.extensions ts->js",
|
|
623
|
+
"drop-dependencies",
|
|
624
|
+
"drop-devDependencies",
|
|
625
|
+
"drop-peerDependencies",
|
|
626
|
+
"drop-repository",
|
|
627
|
+
"drop-bugs",
|
|
628
|
+
"drop-homepage",
|
|
629
|
+
"drop-scripts",
|
|
630
|
+
"drop-files"
|
|
631
|
+
],
|
|
632
|
+
"stripped_documentation": [
|
|
633
|
+
"CHANGELOG.md",
|
|
634
|
+
"LICENSE",
|
|
635
|
+
"README.md",
|
|
636
|
+
"banner.png"
|
|
637
|
+
],
|
|
638
|
+
"javascript_compile": {
|
|
639
|
+
"tool": "bun build --no-bundle",
|
|
640
|
+
"mode": "module-preserving-transpile",
|
|
641
|
+
"files": [
|
|
642
|
+
"activity.js",
|
|
643
|
+
"anysearch.js",
|
|
644
|
+
"auth-fetch.js",
|
|
645
|
+
"bocha.js",
|
|
646
|
+
"brave.js",
|
|
647
|
+
"brightdata-unlocker.js",
|
|
648
|
+
"brightdata.js",
|
|
649
|
+
"chrome-cookies.js",
|
|
650
|
+
"content-find.js",
|
|
651
|
+
"credential-source.js",
|
|
652
|
+
"curator-page.js",
|
|
653
|
+
"curator-server.js",
|
|
654
|
+
"data-uri-sanitize.js",
|
|
655
|
+
"datalab-pdf-extract.js",
|
|
656
|
+
"declared-web-links.js",
|
|
657
|
+
"duckduckgo.js",
|
|
658
|
+
"exa.js",
|
|
659
|
+
"extract.js",
|
|
660
|
+
"feature-config.js",
|
|
661
|
+
"fetch-params.js",
|
|
662
|
+
"firecrawl.js",
|
|
663
|
+
"gemini-adc.js",
|
|
664
|
+
"gemini-api.js",
|
|
665
|
+
"gemini-pdf-extract.js",
|
|
666
|
+
"gemini-search.js",
|
|
667
|
+
"gemini-url-context.js",
|
|
668
|
+
"gemini-web-config.js",
|
|
669
|
+
"gemini-web.js",
|
|
670
|
+
"github-api.js",
|
|
671
|
+
"github-extract.js",
|
|
672
|
+
"github-issue-pr.js",
|
|
673
|
+
"index.js",
|
|
674
|
+
"jina-search.js",
|
|
675
|
+
"kagi.js",
|
|
676
|
+
"kimi-search.js",
|
|
677
|
+
"ollama.js",
|
|
678
|
+
"openai-search.js",
|
|
679
|
+
"page-query.js",
|
|
680
|
+
"parallel-mcp.js",
|
|
681
|
+
"parallel.js",
|
|
682
|
+
"pdf-extract.js",
|
|
683
|
+
"perplexity.js",
|
|
684
|
+
"querit.js",
|
|
685
|
+
"query-rewrite.js",
|
|
686
|
+
"render-search-error.js",
|
|
687
|
+
"rsc-extract.js",
|
|
688
|
+
"search1api.js",
|
|
689
|
+
"searchinfinity.js",
|
|
690
|
+
"searxng.js",
|
|
691
|
+
"serpbase.js",
|
|
692
|
+
"serpdive.js",
|
|
693
|
+
"serper.js",
|
|
694
|
+
"source-check.js",
|
|
695
|
+
"ssrf-protection.js",
|
|
696
|
+
"storage.js",
|
|
697
|
+
"summary-model-scope.js",
|
|
698
|
+
"summary-review.js",
|
|
699
|
+
"tavily.js",
|
|
700
|
+
"tinyfish.js",
|
|
701
|
+
"utils.js",
|
|
702
|
+
"valyu.js",
|
|
703
|
+
"video-extract.js",
|
|
704
|
+
"xai-search.js",
|
|
705
|
+
"xcrawl.js",
|
|
706
|
+
"youtube-extract.js"
|
|
707
|
+
],
|
|
708
|
+
"file_count": 65,
|
|
709
|
+
"before_bytes": 1052831,
|
|
710
|
+
"after_bytes": 943319,
|
|
711
|
+
"runtime_specifier_rewrites": 54,
|
|
712
|
+
"runtime_filename_rewrites": 65,
|
|
713
|
+
"declaration_files_removed": 1
|
|
714
|
+
}
|
|
715
|
+
},
|
|
596
716
|
{
|
|
597
717
|
"id": "dm-cua",
|
|
598
718
|
"staged_dir": "extensions/dm-cua",
|
|
@@ -674,17 +794,33 @@
|
|
|
674
794
|
"jiti": "2.7.0",
|
|
675
795
|
"typebox": "^1.1.34",
|
|
676
796
|
"yaml": "2.8.3",
|
|
797
|
+
"@mozilla/readability": "^0.6.0",
|
|
798
|
+
"defuddle": "0.19.3",
|
|
799
|
+
"linkedom": "^0.16.0",
|
|
800
|
+
"p-limit": "^6.1.0",
|
|
801
|
+
"promise.try": "^2.0.1",
|
|
802
|
+
"turndown": "^7.2.0",
|
|
803
|
+
"undici": "^8.9.0",
|
|
804
|
+
"unpdf": "^1.6.2",
|
|
677
805
|
"@sinclair/typebox": "^0.34.48"
|
|
678
806
|
},
|
|
679
807
|
"runtime_loader": {
|
|
680
808
|
"status": "verified",
|
|
681
|
-
"entries":
|
|
809
|
+
"entries": 11,
|
|
682
810
|
"loader": "dm-coding-agent",
|
|
683
811
|
"platform_dependencies": [
|
|
812
|
+
"@mozilla/readability",
|
|
684
813
|
"@sinclair/typebox",
|
|
685
814
|
"acorn",
|
|
815
|
+
"defuddle",
|
|
686
816
|
"jiti",
|
|
817
|
+
"linkedom",
|
|
818
|
+
"p-limit",
|
|
819
|
+
"promise.try",
|
|
820
|
+
"turndown",
|
|
687
821
|
"typebox",
|
|
822
|
+
"undici",
|
|
823
|
+
"unpdf",
|
|
688
824
|
"yaml"
|
|
689
825
|
]
|
|
690
826
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
Please report suspected vulnerabilities through GitHub private vulnerability reporting for this repository. Do not post exploit details, secrets, or proof-of-concept payloads in public issues or pull requests.
|
|
4
|
+
|
|
5
|
+
If private vulnerability reporting is unavailable for your account or this repository, open a minimal public issue asking for a private contact path without including technical details.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export class ActivityMonitor {
|
|
2
|
+
entries = [];
|
|
3
|
+
maxEntries = 10;
|
|
4
|
+
listeners = new Set;
|
|
5
|
+
rateLimitInfo = { used: 0, max: 10, oldestTimestamp: null, windowMs: 60000 };
|
|
6
|
+
nextId = 1;
|
|
7
|
+
logStart(partial) {
|
|
8
|
+
const id = `act-${this.nextId++}`;
|
|
9
|
+
const entry = {
|
|
10
|
+
...partial,
|
|
11
|
+
id,
|
|
12
|
+
startTime: Date.now(),
|
|
13
|
+
status: null
|
|
14
|
+
};
|
|
15
|
+
this.entries.push(entry);
|
|
16
|
+
if (this.entries.length > this.maxEntries) {
|
|
17
|
+
this.entries.shift();
|
|
18
|
+
}
|
|
19
|
+
this.notify();
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
logComplete(id, status) {
|
|
23
|
+
const entry = this.entries.find((e) => e.id === id);
|
|
24
|
+
if (entry) {
|
|
25
|
+
entry.endTime = Date.now();
|
|
26
|
+
entry.status = status;
|
|
27
|
+
this.notify();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
logError(id, error) {
|
|
31
|
+
const entry = this.entries.find((e) => e.id === id);
|
|
32
|
+
if (entry) {
|
|
33
|
+
entry.endTime = Date.now();
|
|
34
|
+
entry.error = error;
|
|
35
|
+
this.notify();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
getEntries() {
|
|
39
|
+
return this.entries;
|
|
40
|
+
}
|
|
41
|
+
getRateLimitInfo() {
|
|
42
|
+
return this.rateLimitInfo;
|
|
43
|
+
}
|
|
44
|
+
updateRateLimit(info) {
|
|
45
|
+
this.rateLimitInfo = info;
|
|
46
|
+
this.notify();
|
|
47
|
+
}
|
|
48
|
+
onUpdate(callback) {
|
|
49
|
+
this.listeners.add(callback);
|
|
50
|
+
return () => this.listeners.delete(callback);
|
|
51
|
+
}
|
|
52
|
+
clear() {
|
|
53
|
+
this.entries = [];
|
|
54
|
+
this.rateLimitInfo = { used: 0, max: 10, oldestTimestamp: null, windowMs: 60000 };
|
|
55
|
+
this.notify();
|
|
56
|
+
}
|
|
57
|
+
notify() {
|
|
58
|
+
for (const cb of this.listeners) {
|
|
59
|
+
try {
|
|
60
|
+
cb();
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export const activityMonitor = new ActivityMonitor;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { activityMonitor } from "./activity.js";
|
|
3
|
+
import { redactCredential, resolveCredential } from "./credential-source.js";
|
|
4
|
+
import { getWebSearchConfigPath } from "./utils.js";
|
|
5
|
+
const ANYSEARCH_API_URL = "https://api.anysearch.com/v1/search";
|
|
6
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
7
|
+
const SEARCH_TIMEOUT_MS = 30000;
|
|
8
|
+
let cachedConfig = null;
|
|
9
|
+
function loadConfig() {
|
|
10
|
+
if (cachedConfig)
|
|
11
|
+
return cachedConfig;
|
|
12
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
13
|
+
cachedConfig = {};
|
|
14
|
+
return cachedConfig;
|
|
15
|
+
}
|
|
16
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(raw);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
22
|
+
throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
|
|
23
|
+
}
|
|
24
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
25
|
+
throw new Error(`Invalid config in ${CONFIG_PATH}: expected a JSON object`);
|
|
26
|
+
}
|
|
27
|
+
cachedConfig = parsed;
|
|
28
|
+
return cachedConfig;
|
|
29
|
+
}
|
|
30
|
+
async function getApiKey(signal) {
|
|
31
|
+
return resolveCredential({
|
|
32
|
+
provider: "AnySearch",
|
|
33
|
+
configuredValue: loadConfig().anysearchApiKey,
|
|
34
|
+
environmentValue: process.env.ANYSEARCH_API_KEY,
|
|
35
|
+
signal
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function normalizeCount(value) {
|
|
39
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
40
|
+
return 5;
|
|
41
|
+
return Math.max(1, Math.min(Math.floor(value), 20));
|
|
42
|
+
}
|
|
43
|
+
function errorMessage(err) {
|
|
44
|
+
return err instanceof Error ? err.message : String(err);
|
|
45
|
+
}
|
|
46
|
+
function invalidResponse(message) {
|
|
47
|
+
return new Error(`AnySearch API returned invalid response: ${message}`);
|
|
48
|
+
}
|
|
49
|
+
function parseResponse(value) {
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
51
|
+
throw invalidResponse("expected an object envelope");
|
|
52
|
+
}
|
|
53
|
+
const envelope = value;
|
|
54
|
+
if (envelope.code !== 0)
|
|
55
|
+
throw invalidResponse("expected code 0");
|
|
56
|
+
if (!envelope.data || typeof envelope.data !== "object" || Array.isArray(envelope.data)) {
|
|
57
|
+
throw invalidResponse("expected data object");
|
|
58
|
+
}
|
|
59
|
+
const data = envelope.data;
|
|
60
|
+
if (!Array.isArray(data.results))
|
|
61
|
+
throw invalidResponse("expected data.results array");
|
|
62
|
+
if (!data.metadata || typeof data.metadata !== "object" || Array.isArray(data.metadata)) {
|
|
63
|
+
throw invalidResponse("expected data.metadata object");
|
|
64
|
+
}
|
|
65
|
+
const results = [];
|
|
66
|
+
for (const [index, value] of data.results.entries()) {
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
68
|
+
throw invalidResponse(`expected data.results[${index}] object`);
|
|
69
|
+
}
|
|
70
|
+
const result = value;
|
|
71
|
+
const { title, url, snippet, content } = result;
|
|
72
|
+
if (typeof title !== "string")
|
|
73
|
+
throw invalidResponse(`expected data.results[${index}].title string`);
|
|
74
|
+
if (typeof url !== "string")
|
|
75
|
+
throw invalidResponse(`expected data.results[${index}].url string`);
|
|
76
|
+
if (typeof snippet !== "string")
|
|
77
|
+
throw invalidResponse(`expected data.results[${index}].snippet string`);
|
|
78
|
+
if (content !== undefined && content !== null && typeof content !== "string") {
|
|
79
|
+
throw invalidResponse(`expected data.results[${index}].content string`);
|
|
80
|
+
}
|
|
81
|
+
if (!url)
|
|
82
|
+
throw invalidResponse(`expected data.results[${index}].url to be non-empty`);
|
|
83
|
+
results.push({ title, url, snippet, content: typeof content === "string" ? content : "" });
|
|
84
|
+
}
|
|
85
|
+
return { code: 0, data: { results, metadata: data.metadata } };
|
|
86
|
+
}
|
|
87
|
+
function buildAnswer(results) {
|
|
88
|
+
return results.map((result) => result.snippet ? `${result.snippet}
|
|
89
|
+
Source: ${result.title} (${result.url})` : `Source: ${result.title} (${result.url})`).join(`
|
|
90
|
+
|
|
91
|
+
`);
|
|
92
|
+
}
|
|
93
|
+
export function isAnySearchAvailable() {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
export async function searchWithAnySearch(query, options = {}) {
|
|
97
|
+
const apiKey = await getApiKey(options.signal);
|
|
98
|
+
const numResults = normalizeCount(options.numResults);
|
|
99
|
+
const body = { query, max_results: numResults };
|
|
100
|
+
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
101
|
+
let response;
|
|
102
|
+
try {
|
|
103
|
+
response = await fetch(ANYSEARCH_API_URL, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
107
|
+
"Content-Type": "application/json"
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify(body),
|
|
110
|
+
signal: options.signal ? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS), options.signal]) : AbortSignal.timeout(SEARCH_TIMEOUT_MS)
|
|
111
|
+
});
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const message = errorMessage(err);
|
|
114
|
+
const redactedMessage = redactCredential(message, apiKey);
|
|
115
|
+
if (redactedMessage.toLowerCase().includes("abort"))
|
|
116
|
+
activityMonitor.logComplete(activityId, 0);
|
|
117
|
+
else
|
|
118
|
+
activityMonitor.logError(activityId, redactedMessage);
|
|
119
|
+
if (redactedMessage === message)
|
|
120
|
+
throw err;
|
|
121
|
+
const redactedError = new Error(redactedMessage);
|
|
122
|
+
if (err instanceof Error)
|
|
123
|
+
redactedError.name = err.name;
|
|
124
|
+
throw redactedError;
|
|
125
|
+
}
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
128
|
+
const errorText = redactCredential(await response.text(), apiKey);
|
|
129
|
+
throw new Error(`AnySearch API error ${response.status}: ${errorText.slice(0, 300)}`);
|
|
130
|
+
}
|
|
131
|
+
let rawData;
|
|
132
|
+
try {
|
|
133
|
+
rawData = await response.json();
|
|
134
|
+
} catch (err) {
|
|
135
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
136
|
+
throw new Error(`AnySearch API returned invalid JSON: ${errorMessage(err)}`);
|
|
137
|
+
}
|
|
138
|
+
let data;
|
|
139
|
+
try {
|
|
140
|
+
data = parseResponse(rawData);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
146
|
+
const results = data.data.results.slice(0, numResults).map((result) => ({
|
|
147
|
+
title: result.title,
|
|
148
|
+
url: result.url,
|
|
149
|
+
snippet: result.snippet
|
|
150
|
+
}));
|
|
151
|
+
const mapped = { answer: buildAnswer(results), results };
|
|
152
|
+
if (options.includeContent) {
|
|
153
|
+
const inlineContent = data.data.results.slice(0, numResults).filter((result) => result.content.length > 0).map((result) => ({ url: result.url, title: result.title, content: result.content, error: null }));
|
|
154
|
+
if (inlineContent.length > 0)
|
|
155
|
+
mapped.inlineContent = inlineContent;
|
|
156
|
+
}
|
|
157
|
+
return mapped;
|
|
158
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { getWebSearchConfigPath } from "./utils.js";
|
|
3
|
+
const WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();
|
|
4
|
+
const AUTH_PROFILE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
5
|
+
export function resolveAuthFetchProfile(request) {
|
|
6
|
+
const profiles = loadAuthFetchProfiles();
|
|
7
|
+
if (profiles.length === 0) {
|
|
8
|
+
throw new Error(`auth requires at least one authFetch profile in ${WEB_SEARCH_CONFIG_PATH}`);
|
|
9
|
+
}
|
|
10
|
+
if (request === true) {
|
|
11
|
+
if (profiles.length !== 1) {
|
|
12
|
+
throw new Error("auth: true requires exactly one authFetch profile; use a profile name instead");
|
|
13
|
+
}
|
|
14
|
+
return profiles[0];
|
|
15
|
+
}
|
|
16
|
+
const name = request.trim();
|
|
17
|
+
const profile = profiles.find((candidate) => candidate.name === name);
|
|
18
|
+
if (!profile)
|
|
19
|
+
throw new Error(`Unknown authFetch profile: ${name}`);
|
|
20
|
+
return profile;
|
|
21
|
+
}
|
|
22
|
+
export function assertAuthFetchUrl(profile, rawUrl) {
|
|
23
|
+
let url;
|
|
24
|
+
try {
|
|
25
|
+
url = new URL(rawUrl);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28
|
+
throw new Error(`Authenticated fetch requires an HTTPS URL: ${message}`);
|
|
29
|
+
}
|
|
30
|
+
if (url.protocol !== "https:")
|
|
31
|
+
throw new Error("Authenticated fetch requires an HTTPS URL");
|
|
32
|
+
const hostname = normalizeHostname(url.hostname);
|
|
33
|
+
if (!profile.hosts.some((host) => hostMatches(hostname, host))) {
|
|
34
|
+
throw new Error(`URL host ${hostname} is not allowed by authFetch profile ${profile.name}`);
|
|
35
|
+
}
|
|
36
|
+
return url;
|
|
37
|
+
}
|
|
38
|
+
export function authFetchRedirectGuard(profile, from, to) {
|
|
39
|
+
if (profile.redirects === "same-origin" && to.origin !== from.origin) {
|
|
40
|
+
throw new Error(`Authenticated fetch refused cross-origin redirect: ${from.origin} -> ${to.origin}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function loadAuthFetchProfiles() {
|
|
44
|
+
if (!existsSync(WEB_SEARCH_CONFIG_PATH))
|
|
45
|
+
return [];
|
|
46
|
+
const raw = readFileSync(WEB_SEARCH_CONFIG_PATH, "utf-8");
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
const value = JSON.parse(raw);
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
51
|
+
throw new Error("expected a JSON object");
|
|
52
|
+
parsed = value;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
55
|
+
throw new Error(`Failed to parse ${WEB_SEARCH_CONFIG_PATH}: ${message}`);
|
|
56
|
+
}
|
|
57
|
+
if (parsed.authFetch === undefined || parsed.authFetch === null)
|
|
58
|
+
return [];
|
|
59
|
+
if (typeof parsed.authFetch !== "object" || Array.isArray(parsed.authFetch)) {
|
|
60
|
+
throw new Error(`authFetch in ${WEB_SEARCH_CONFIG_PATH} must be an object`);
|
|
61
|
+
}
|
|
62
|
+
return Object.entries(parsed.authFetch).map(([name, value]) => parseProfile(name, value));
|
|
63
|
+
}
|
|
64
|
+
function parseProfile(name, value) {
|
|
65
|
+
if (!AUTH_PROFILE_NAME_PATTERN.test(name)) {
|
|
66
|
+
throw new Error(`authFetch profile name ${JSON.stringify(name)} must start with a letter and contain only letters, numbers, underscores, or hyphens`);
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return { name, hosts: parseHosts(value, `authFetch.${name}`), redirects: "same-origin", cache: "session" };
|
|
70
|
+
}
|
|
71
|
+
if (!value || typeof value !== "object") {
|
|
72
|
+
throw new Error(`authFetch.${name} in ${WEB_SEARCH_CONFIG_PATH} must be an array of hosts or an object`);
|
|
73
|
+
}
|
|
74
|
+
const config = value;
|
|
75
|
+
if (!Array.isArray(config.hosts)) {
|
|
76
|
+
throw new Error(`authFetch.${name}.hosts in ${WEB_SEARCH_CONFIG_PATH} must be a non-empty array of hostnames`);
|
|
77
|
+
}
|
|
78
|
+
const redirects = config.redirects ?? "same-origin";
|
|
79
|
+
if (redirects !== "same-origin") {
|
|
80
|
+
throw new Error(`authFetch.${name}.redirects in ${WEB_SEARCH_CONFIG_PATH} must be "same-origin"`);
|
|
81
|
+
}
|
|
82
|
+
const cache = config.cache ?? "session";
|
|
83
|
+
if (cache !== "session" && cache !== "off") {
|
|
84
|
+
throw new Error(`authFetch.${name}.cache in ${WEB_SEARCH_CONFIG_PATH} must be "session" or "off"`);
|
|
85
|
+
}
|
|
86
|
+
const chromeProfile = parseChromeProfile(config.chromeProfile, `authFetch.${name}.chromeProfile`);
|
|
87
|
+
return {
|
|
88
|
+
name,
|
|
89
|
+
hosts: parseHosts(config.hosts, `authFetch.${name}.hosts`),
|
|
90
|
+
...chromeProfile ? { chromeProfile } : {},
|
|
91
|
+
redirects,
|
|
92
|
+
cache
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function parseHosts(value, label) {
|
|
96
|
+
if (value.length === 0)
|
|
97
|
+
throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must be a non-empty array of hostnames`);
|
|
98
|
+
const hosts = value.map((entry) => {
|
|
99
|
+
if (typeof entry !== "string")
|
|
100
|
+
throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must contain only hostnames`);
|
|
101
|
+
return parseHost(entry, label);
|
|
102
|
+
});
|
|
103
|
+
return [...new Set(hosts)];
|
|
104
|
+
}
|
|
105
|
+
function parseHost(value, label) {
|
|
106
|
+
const host = normalizeHostname(value.trim());
|
|
107
|
+
if (!host || host.startsWith(".") || host.endsWith(".") || /\s|[\\/?:#@*]/.test(host)) {
|
|
108
|
+
throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} contains an invalid hostname: ${JSON.stringify(value)}`);
|
|
109
|
+
}
|
|
110
|
+
if (host.length > 253 || !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(host)) {
|
|
111
|
+
throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} contains an invalid hostname: ${JSON.stringify(value)}`);
|
|
112
|
+
}
|
|
113
|
+
return host;
|
|
114
|
+
}
|
|
115
|
+
function parseChromeProfile(value, label) {
|
|
116
|
+
if (value === undefined || value === null)
|
|
117
|
+
return;
|
|
118
|
+
if (typeof value !== "string")
|
|
119
|
+
throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must be a string`);
|
|
120
|
+
const normalized = value.trim();
|
|
121
|
+
if (!normalized || normalized === "." || normalized === ".." || normalized.includes("/") || normalized.includes("\\")) {
|
|
122
|
+
throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must be a profile directory name, not a path`);
|
|
123
|
+
}
|
|
124
|
+
return normalized;
|
|
125
|
+
}
|
|
126
|
+
function normalizeHostname(hostname) {
|
|
127
|
+
return hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
128
|
+
}
|
|
129
|
+
function hostMatches(hostname, allowedHost) {
|
|
130
|
+
return hostname === allowedHost || hostname.endsWith(`.${allowedHost}`);
|
|
131
|
+
}
|