crawlforge-mcp-server 5.2.9 → 5.3.1
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/CLAUDE.md +13 -1
- package/README.md +11 -9
- package/package.json +3 -2
- package/server.js +175 -26
- package/src/cli/commands/stealth.js +7 -1
- package/src/constants/config.js +2 -1
- package/src/core/ActionExecutor.js +168 -16
- package/src/core/AlertNotificationSystem.js +2 -1
- package/src/core/AuthManager.js +19 -1
- package/src/core/ChangeTracker.js +34 -6
- package/src/core/LLMsTxtAnalyzer.js +94 -12
- package/src/core/LocalizationManager.js +2 -1
- package/src/core/ResearchOrchestrator.js +401 -86
- package/src/core/StealthBrowserManager.js +186 -105
- package/src/core/WebhookDispatcher.js +3 -4
- package/src/core/analysis/ContentAnalyzer.js +41 -15
- package/src/core/analysis/sentenceUtils.js +16 -5
- package/src/core/crawlers/BFSCrawler.js +44 -21
- package/src/core/llm/LLMManager.js +496 -0
- package/src/core/llm/OllamaProvider.js +14 -5
- package/src/core/processing/BrowserProcessor.js +27 -0
- package/src/core/processing/ContentProcessor.js +11 -39
- package/src/core/processing/PDFProcessor.js +2 -3
- package/src/core/research/claimFilters.js +235 -0
- package/src/schemas/toolOutputSchemas.js +5 -1
- package/src/security/wave3-security.js +2 -1
- package/src/server/requestContext.js +23 -0
- package/src/server/withAuth.js +21 -5
- package/src/skills/agent-skills/crawlforge-deep-research/SKILL.md +6 -1
- package/src/tools/advanced/ScrapeWithActionsTool.js +49 -1
- package/src/tools/advanced/batchScrape/schema.js +4 -0
- package/src/tools/advanced/batchScrape/worker.js +19 -10
- package/src/tools/basic/_fetch.js +19 -15
- package/src/tools/basic/extractLinks.js +8 -3
- package/src/tools/basic/extractMetadata.js +7 -3
- package/src/tools/basic/extractText.js +8 -3
- package/src/tools/basic/fetchUrl.js +7 -3
- package/src/tools/basic/scrapeStructured.js +76 -3
- package/src/tools/crawl/_sessionContext.js +10 -2
- package/src/tools/crawl/crawlDeep.js +29 -12
- package/src/tools/crawl/mapSite.js +39 -14
- package/src/tools/extract/_fetchAndParse.js +23 -8
- package/src/tools/extract/analyzeContent.js +5 -3
- package/src/tools/extract/extractContent.js +18 -4
- package/src/tools/extract/extractStructured.js +66 -12
- package/src/tools/extract/extractWithLlm.js +51 -4
- package/src/tools/extract/processDocument.js +45 -78
- package/src/tools/extract/summarizeContent.js +35 -1
- package/src/tools/llmstxt/generateLLMsTxt.js +19 -4
- package/src/tools/research/deepResearch.js +2 -1
- package/src/tools/scrape/_brandingExtractor.js +42 -3
- package/src/tools/scrape/_mainContent.js +105 -0
- package/src/tools/scrape/unifiedScrape.js +21 -14
- package/src/tools/search/adapters/redditOfficialApi.js +7 -6
- package/src/tools/search/redditSearch.js +6 -3
- package/src/tools/search/searchWeb.js +26 -3
- package/src/tools/templates/ScrapeTemplateTool.js +17 -6
- package/src/tools/tracking/trackChanges/differ.js +26 -3
- package/src/tools/tracking/trackChanges/index.js +12 -5
- package/src/tools/tracking/trackChanges/notifier.js +3 -1
- package/src/tools/tracking/trackChanges/schema.js +3 -0
- package/src/utils/complianceAudit.js +72 -0
- package/src/utils/contentUtils.js +12 -1
- package/src/utils/domainFilter.js +38 -19
- package/src/utils/fetchIdentity.js +62 -0
- package/src/utils/hostBlocklist.js +81 -0
- package/src/utils/hostRateLimiter.js +101 -2
- package/src/utils/ollamaConfig.js +36 -2
- package/src/utils/robotsChecker.js +90 -43
- package/src/utils/robotsGate.js +206 -0
- package/src/utils/sitemapParser.js +33 -15
- package/src/utils/ssrfProtection.js +2 -1
- package/src/utils/webBotAuth.js +193 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* webBotAuth — signs outbound requests so a site owner can verify who we are.
|
|
3
|
+
*
|
|
4
|
+
* A User-Agent is a claim anyone can make. Web Bot Auth turns our identity into
|
|
5
|
+
* something a site can check: an Ed25519 signature over the request, per
|
|
6
|
+
* RFC 9421 (HTTP Message Signatures) with the `web-bot-auth` profile from
|
|
7
|
+
* draft-meunier-web-bot-auth-architecture. The public key is published at
|
|
8
|
+
* `/.well-known/http-message-signatures-directory` on crawlforge.dev.
|
|
9
|
+
*
|
|
10
|
+
* This is the mechanism behind ground rule G4. Honest identification only helps
|
|
11
|
+
* a site owner if it cannot be spoofed by someone else claiming to be us.
|
|
12
|
+
*
|
|
13
|
+
* Signing is OPT-IN and absent by default: with no key configured every export
|
|
14
|
+
* here is a no-op and requests go out exactly as before. Key material lives
|
|
15
|
+
* only in the environment, never in the repo.
|
|
16
|
+
*
|
|
17
|
+
* Verified against the official test vectors (architecture draft Appendix A.2.1
|
|
18
|
+
* with the RFC 9421 Appendix B.1.4 key) — see tests/unit/webBotAuth.test.js.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createHash, createPrivateKey, createPublicKey, sign, randomBytes } from 'crypto';
|
|
22
|
+
|
|
23
|
+
/** The profile tag every web-bot-auth signature carries. */
|
|
24
|
+
const WEB_BOT_AUTH_TAG = 'web-bot-auth';
|
|
25
|
+
|
|
26
|
+
/** The draft RECOMMENDS an expiry no more than 24 hours; ours is far shorter. */
|
|
27
|
+
const DEFAULT_LIFETIME_SECONDS = 300;
|
|
28
|
+
|
|
29
|
+
let cachedKey; // undefined = not yet resolved, null = none configured
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The raw 32-byte Ed25519 public key as base64url, which is the JWK `x`.
|
|
33
|
+
* @param {import('crypto').KeyObject} publicKey
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function publicKeyX(publicKey) {
|
|
37
|
+
// An Ed25519 SPKI DER is a 12-byte header followed by the 32-byte key.
|
|
38
|
+
const der = publicKey.export({ type: 'spki', format: 'der' });
|
|
39
|
+
return Buffer.from(der.subarray(der.length - 32)).toString('base64url');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* RFC 8037 Appendix A.3 thumbprint: SHA-256 over the canonical JWK with its
|
|
44
|
+
* member names in lexicographic order and no whitespace, base64url unpadded.
|
|
45
|
+
* The member order is load-bearing — reordering it changes the key id.
|
|
46
|
+
* @param {string} x base64url raw public key
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function jwkThumbprint(x) {
|
|
50
|
+
const canonical = JSON.stringify({ crv: 'Ed25519', kty: 'OKP', x });
|
|
51
|
+
return createHash('sha256').update(canonical).digest('base64url');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The public JWK for a key pair, in the shape the directory publishes.
|
|
56
|
+
* @param {import('crypto').KeyObject} publicKey
|
|
57
|
+
*/
|
|
58
|
+
export function publicJwk(publicKey) {
|
|
59
|
+
const x = publicKeyX(publicKey);
|
|
60
|
+
return { kty: 'OKP', crv: 'Ed25519', kid: jwkThumbprint(x), x, use: 'sig', alg: 'ed25519' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the signing key from the environment, or null when none is set.
|
|
65
|
+
*
|
|
66
|
+
* `CRAWLFORGE_SIGNING_KEY` holds an Ed25519 private key as a PKCS#8 PEM —
|
|
67
|
+
* either literally (with real newlines) or base64-encoded, since most secret
|
|
68
|
+
* stores mangle multi-line values. A malformed key is a configuration error we
|
|
69
|
+
* surface once and then ignore: it must not take every fetch down with it.
|
|
70
|
+
*
|
|
71
|
+
* @returns {{ privateKey: import('crypto').KeyObject, jwk: object } | null}
|
|
72
|
+
*/
|
|
73
|
+
export function getSigningKey() {
|
|
74
|
+
if (cachedKey !== undefined) return cachedKey;
|
|
75
|
+
|
|
76
|
+
const raw = process.env.CRAWLFORGE_SIGNING_KEY;
|
|
77
|
+
if (!raw || !raw.trim()) {
|
|
78
|
+
cachedKey = null;
|
|
79
|
+
return cachedKey;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const pem = raw.includes('-----BEGIN')
|
|
84
|
+
? raw.replace(/\\n/g, '\n')
|
|
85
|
+
: Buffer.from(raw.trim(), 'base64').toString('utf8');
|
|
86
|
+
|
|
87
|
+
const privateKey = createPrivateKey(pem);
|
|
88
|
+
if (privateKey.asymmetricKeyType !== 'ed25519') {
|
|
89
|
+
throw new Error(`expected an ed25519 key, got ${privateKey.asymmetricKeyType}`);
|
|
90
|
+
}
|
|
91
|
+
cachedKey = { privateKey, jwk: publicJwk(createPublicKey(privateKey)) };
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(
|
|
94
|
+
`[web-bot-auth] CRAWLFORGE_SIGNING_KEY could not be loaded, so requests will go out unsigned: ${error.message}`
|
|
95
|
+
);
|
|
96
|
+
cachedKey = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return cachedKey;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Serialise the @signature-params of a signature base.
|
|
104
|
+
* Parameter order is part of the signed bytes, so it must match what the
|
|
105
|
+
* verifier reconstructs — it is the draft's order, not an arbitrary one.
|
|
106
|
+
*/
|
|
107
|
+
function signatureParams(components, { created, expires, keyid, nonce }) {
|
|
108
|
+
const covered = components.map((c) => `"${c}"`).join(' ');
|
|
109
|
+
return (
|
|
110
|
+
`(${covered})` +
|
|
111
|
+
`;created=${created}` +
|
|
112
|
+
`;keyid="${keyid}"` +
|
|
113
|
+
`;alg="ed25519"` +
|
|
114
|
+
`;expires=${expires}` +
|
|
115
|
+
`;nonce="${nonce}"` +
|
|
116
|
+
`;tag="${WEB_BOT_AUTH_TAG}"`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build the RFC 9421 signature base for a request.
|
|
122
|
+
*
|
|
123
|
+
* Exported for the test vectors: the base is the exact byte string that gets
|
|
124
|
+
* signed, so reproducing the published one is what proves the implementation
|
|
125
|
+
* interoperates rather than merely agreeing with itself.
|
|
126
|
+
*
|
|
127
|
+
* @param {{ authority: string, signatureAgent?: string|null }} request
|
|
128
|
+
* @param {{ created: number, expires: number, keyid: string, nonce: string }} params
|
|
129
|
+
* @returns {{ base: string, components: string[], params: string }}
|
|
130
|
+
*/
|
|
131
|
+
export function buildSignatureBase(request, params) {
|
|
132
|
+
const components = ['@authority'];
|
|
133
|
+
const lines = [`"@authority": ${request.authority}`];
|
|
134
|
+
|
|
135
|
+
// The draft requires Signature-Agent to be covered whenever it is sent.
|
|
136
|
+
if (request.signatureAgent) {
|
|
137
|
+
components.push('signature-agent');
|
|
138
|
+
lines.push(`"signature-agent": "${request.signatureAgent}"`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const serialised = signatureParams(components, params);
|
|
142
|
+
lines.push(`"@signature-params": ${serialised}`);
|
|
143
|
+
|
|
144
|
+
return { base: lines.join('\n'), components, params: serialised };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Signature headers for an outbound request, or null when signing is off.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} url the request target
|
|
151
|
+
* @param {object} [options]
|
|
152
|
+
* @param {string|null} [options.signatureAgent] directory URL to advertise
|
|
153
|
+
* @param {number} [options.now] epoch seconds, for deterministic tests
|
|
154
|
+
* @param {number} [options.expires] epoch seconds, for deterministic tests
|
|
155
|
+
* @param {string} [options.nonce] base64 nonce, for deterministic tests
|
|
156
|
+
* @returns {Record<string,string>|null}
|
|
157
|
+
*/
|
|
158
|
+
export function signRequestHeaders(url, options = {}) {
|
|
159
|
+
const key = getSigningKey();
|
|
160
|
+
if (!key) return null;
|
|
161
|
+
|
|
162
|
+
let authority;
|
|
163
|
+
try {
|
|
164
|
+
authority = new URL(url).host;
|
|
165
|
+
} catch {
|
|
166
|
+
return null; // not our job to validate URLs; the fetch path already does
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const created = options.now ?? Math.floor(Date.now() / 1000);
|
|
170
|
+
const expires = options.expires ?? created + DEFAULT_LIFETIME_SECONDS;
|
|
171
|
+
// The draft RECOMMENDS 64 random bytes, unique within the validity window.
|
|
172
|
+
const nonce = options.nonce ?? randomBytes(64).toString('base64');
|
|
173
|
+
const signatureAgent = options.signatureAgent ?? process.env.WEB_BOT_AUTH_DIRECTORY ?? null;
|
|
174
|
+
|
|
175
|
+
const { base, params } = buildSignatureBase(
|
|
176
|
+
{ authority, signatureAgent },
|
|
177
|
+
{ created, expires, keyid: key.jwk.kid, nonce }
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const signature = sign(null, Buffer.from(base, 'utf8'), key.privateKey).toString('base64');
|
|
181
|
+
|
|
182
|
+
const headers = {
|
|
183
|
+
'Signature-Input': `sig1=${params}`,
|
|
184
|
+
'Signature': `sig1=:${signature}:`
|
|
185
|
+
};
|
|
186
|
+
if (signatureAgent) headers['Signature-Agent'] = `"${signatureAgent}"`;
|
|
187
|
+
return headers;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Test hook: forget the resolved key so a changed env var is picked up. */
|
|
191
|
+
export function _resetSigningKey() {
|
|
192
|
+
cachedKey = undefined;
|
|
193
|
+
}
|