@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 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
- const h = new Headers(init.headers || {});
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.1",
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); });