@crawlcheck/sdk 1.15.1 → 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -2
- package/package.json +1 -1
- package/test/guarded-headers.test.js +32 -0
package/dist/index.d.ts
CHANGED
|
@@ -1136,3 +1136,5 @@ export interface LangChainToolsModule {
|
|
|
1136
1136
|
* Same three tools as agentTools(); each returns its result as a JSON string; they fail closed. */
|
|
1137
1137
|
export declare function langchainTools(cc: CrawlCheck, lc: LangChainToolsModule, opts?: AgentToolOptions): unknown[];
|
|
1138
1138
|
export default CrawlCheck;
|
|
1139
|
+
/** Headers guardedFetch still sends after a redirect leaves the origin the caller named. Everything else stays behind. */
|
|
1140
|
+
export declare const GUARDED_SAFE_HEADERS: string[];
|
package/dist/index.js
CHANGED
|
@@ -495,7 +495,8 @@ export class CrawlCheck {
|
|
|
495
495
|
const action = opts.action ?? "read", max = opts.maxHops ?? 10, f = opts.fetch ? opts.fetch.bind(globalThis) : this.f;
|
|
496
496
|
const init = Object.assign({}, opts.init || {}), method = String(init.method || "GET").toUpperCase();
|
|
497
497
|
const hops = [], seen = new Map();
|
|
498
|
-
let cur = url;
|
|
498
|
+
let cur = url, origin0 = "";
|
|
499
|
+
try { origin0 = new URL(url).origin; } catch { origin0 = ""; }
|
|
499
500
|
for (let n = 0; n <= max; n++) {
|
|
500
501
|
let u;
|
|
501
502
|
try {
|
|
@@ -519,7 +520,10 @@ export class CrawlCheck {
|
|
|
519
520
|
hops.push(hop);
|
|
520
521
|
if (!g.proceed)
|
|
521
522
|
return { proceed: false, response: null, url: cur, hops, why: [dom + ": " + (g.why.join("; ") || "refused")] };
|
|
522
|
-
|
|
523
|
+
// hdr1 (1.15.2): caller headers stay on the origin the caller named; elsewhere only the allowlist travels
|
|
524
|
+
const sameOrigin = u.origin === origin0;
|
|
525
|
+
const h = new Headers();
|
|
526
|
+
new Headers(init.headers || {}).forEach((v, k) => { if (sameOrigin || GUARDED_SAFE_HEADERS.includes(k.toLowerCase())) h.set(k, v); });
|
|
523
527
|
if (g.header)
|
|
524
528
|
for (const [k, v] of Object.entries(g.header))
|
|
525
529
|
h.set(k, v);
|
|
@@ -791,3 +795,5 @@ export function langchainTools(cc, lc, opts = {}) {
|
|
|
791
795
|
return agentTools(cc, opts).map((t) => lc.tool(async (input) => JSON.stringify(await t.execute(input ?? {})), { name: t.name, description: t.description, schema: t.parameters }));
|
|
792
796
|
}
|
|
793
797
|
export default CrawlCheck;
|
|
798
|
+
/** Headers guardedFetch still sends after a redirect leaves the origin the caller named. */
|
|
799
|
+
export const GUARDED_SAFE_HEADERS = ["accept", "accept-language", "accept-encoding", "user-agent", "cache-control", "pragma"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlcheck/sdk",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.2",
|
|
4
4
|
"description": "Typed client for the CrawlCheck API (generated from its OpenAPI document) and the zero-dependency offline verifier for evidence bundles and remediation receipts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// hdr1: caller headers stay on the origin the caller named; after a redirect to another origin only the allowlist travels.
|
|
2
|
+
import test from "node:test"; import assert from "node:assert/strict";
|
|
3
|
+
import { CrawlCheck, GUARDED_SAFE_HEADERS } from "../dist/index.js";
|
|
4
|
+
function client() { const cc = new CrawlCheck(); cc.guard = async () => ({ proceed: true, why: [], receipt: { decision: "allow" }, header: { "crawlcheck-receipt": "r" } }); return cc; }
|
|
5
|
+
function recorder(map) { const seen = []; const f = async (u, init) => { seen.push({ url: String(u), headers: Object.fromEntries(new Headers(init.headers).entries()) }); const to = map[String(u)]; return to ? new Response(null, { status: 302, headers: { location: to } }) : new Response("ok", { status: 200 }); }; return { f, seen }; }
|
|
6
|
+
const H = { authorization: "Bearer secret", cookie: "sid=1", "x-api-key": "k", accept: "text/html", "user-agent": "t/1" };
|
|
7
|
+
test("another host: Authorization, Cookie and custom headers are dropped; the allowlist and the receipt stay", async () => {
|
|
8
|
+
const { f, seen } = recorder({ "https://a.example/x": "https://b.example/y" });
|
|
9
|
+
const r = await client().guardedFetch("https://a.example/x", { init: { headers: H }, fetch: f });
|
|
10
|
+
assert.equal(r.proceed, true); assert.equal(seen.length, 2);
|
|
11
|
+
assert.equal(seen[0].headers.authorization, "Bearer secret"); assert.equal(seen[0].headers.cookie, "sid=1");
|
|
12
|
+
for (const k of ["authorization", "cookie", "x-api-key"]) assert.equal(seen[1].headers[k], undefined, k + " crossed origins");
|
|
13
|
+
assert.equal(seen[1].headers.accept, "text/html"); assert.equal(seen[1].headers["user-agent"], "t/1"); assert.equal(seen[1].headers["crawlcheck-receipt"], "r");
|
|
14
|
+
});
|
|
15
|
+
test("same origin: every caller header is kept", async () => {
|
|
16
|
+
const { f, seen } = recorder({ "https://a.example/x": "https://a.example/y" });
|
|
17
|
+
await client().guardedFetch("https://a.example/x", { init: { headers: H }, fetch: f });
|
|
18
|
+
assert.equal(seen[1].headers.authorization, "Bearer secret"); assert.equal(seen[1].headers["x-api-key"], "k");
|
|
19
|
+
});
|
|
20
|
+
test("scheme downgrade and port change count as another origin", async () => {
|
|
21
|
+
for (const to of ["http://a.example/y", "https://a.example:8443/y"]) {
|
|
22
|
+
const { f, seen } = recorder({ "https://a.example/x": to });
|
|
23
|
+
await client().guardedFetch("https://a.example/x", { init: { headers: H }, fetch: f });
|
|
24
|
+
assert.equal(seen[1].headers.authorization, undefined, to); assert.equal(seen[1].headers.cookie, undefined, to);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
test("back to the first origin: headers return", async () => {
|
|
28
|
+
const { f, seen } = recorder({ "https://a.example/x": "https://b.example/y", "https://b.example/y": "https://a.example/z" });
|
|
29
|
+
await client().guardedFetch("https://a.example/x", { init: { headers: H }, fetch: f });
|
|
30
|
+
assert.equal(seen[1].headers.authorization, undefined); assert.equal(seen[2].headers.authorization, "Bearer secret");
|
|
31
|
+
});
|
|
32
|
+
test("the allowlist holds no credential header", () => { for (const k of ["authorization", "cookie", "proxy-authorization"]) assert.equal(GUARDED_SAFE_HEADERS.includes(k), false); });
|