@cookiepal-oss/agent 0.0.0-stage → 0.2.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 +21 -0
- package/README.md +32 -2
- package/dist/check.mjs +3481 -0
- package/dist/cookies.json +1 -0
- package/dist/scan.mjs +335 -0
- package/dist/tldts.mjs +808 -0
- package/package.json +34 -3
package/dist/scan.mjs
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Built from src/ by pnpm build. Do not edit.
|
|
3
|
+
|
|
4
|
+
// src/scan.ts
|
|
5
|
+
import { rmSync as rmSync3 } from "node:fs";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
// src/browser.ts
|
|
9
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
10
|
+
import { delimiter, join } from "node:path";
|
|
11
|
+
|
|
12
|
+
// src/url.ts
|
|
13
|
+
import { getDomain } from "./tldts.mjs";
|
|
14
|
+
|
|
15
|
+
// src/browser.ts
|
|
16
|
+
var CheckError = class extends Error {
|
|
17
|
+
};
|
|
18
|
+
var MAC_APPS = [
|
|
19
|
+
"Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
20
|
+
"Chromium.app/Contents/MacOS/Chromium",
|
|
21
|
+
"Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
22
|
+
"Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
23
|
+
];
|
|
24
|
+
var LINUX_BINARIES = [
|
|
25
|
+
"google-chrome",
|
|
26
|
+
"google-chrome-stable",
|
|
27
|
+
"chromium",
|
|
28
|
+
"chromium-browser",
|
|
29
|
+
"microsoft-edge",
|
|
30
|
+
"brave-browser"
|
|
31
|
+
];
|
|
32
|
+
var WINDOWS_EXES = [
|
|
33
|
+
"Google\\Chrome\\Application\\chrome.exe",
|
|
34
|
+
"Microsoft\\Edge\\Application\\msedge.exe",
|
|
35
|
+
"BraveSoftware\\Brave-Browser\\Application\\brave.exe",
|
|
36
|
+
"Chromium\\Application\\chrome.exe"
|
|
37
|
+
];
|
|
38
|
+
var findBrowser = () => {
|
|
39
|
+
const override = process.env.COOKIEPAL_BROWSER;
|
|
40
|
+
if (override) return existsSync(override) ? override : null;
|
|
41
|
+
let candidates;
|
|
42
|
+
if (process.platform === "darwin") {
|
|
43
|
+
const roots = ["/Applications", join(process.env.HOME ?? "", "Applications")];
|
|
44
|
+
candidates = roots.flatMap((root) => MAC_APPS.map((app) => join(root, app)));
|
|
45
|
+
} else if (process.platform === "win32") {
|
|
46
|
+
const roots = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA];
|
|
47
|
+
candidates = roots.filter((root) => root !== void 0).flatMap((root) => WINDOWS_EXES.map((exe) => join(root, exe)));
|
|
48
|
+
} else {
|
|
49
|
+
const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
50
|
+
candidates = LINUX_BINARIES.flatMap((name) => dirs.map((dir) => join(dir, name)));
|
|
51
|
+
}
|
|
52
|
+
return candidates.find((path) => existsSync(path)) ?? null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/engine.ts
|
|
56
|
+
import { spawnSync } from "node:child_process";
|
|
57
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync } from "node:fs";
|
|
58
|
+
import { join as join2, win32 } from "node:path";
|
|
59
|
+
var ENGINE_VERSION = "1.3.1";
|
|
60
|
+
var engineDir = (dataDir) => join2(dataDir, "engine");
|
|
61
|
+
var enginePackageJson = (dataDir) => join2(engineDir(dataDir), "node_modules", "@cookiepal-oss", "cli", "package.json");
|
|
62
|
+
var engineEntry = (dataDir) => join2(engineDir(dataDir), "node_modules", "@cookiepal-oss", "cli", "dist", "engine.js");
|
|
63
|
+
var installedVersion = (dataDir) => {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(readFileSync(enginePackageJson(dataDir), "utf8"));
|
|
66
|
+
return typeof parsed === "object" && parsed !== null && typeof parsed.version === "string" ? parsed.version : null;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var installArgs = (dataDir) => [
|
|
72
|
+
"install",
|
|
73
|
+
"--prefix",
|
|
74
|
+
engineDir(dataDir),
|
|
75
|
+
"--no-audit",
|
|
76
|
+
"--no-fund",
|
|
77
|
+
"--ignore-scripts",
|
|
78
|
+
`@cookiepal-oss/cli@${ENGINE_VERSION}`
|
|
79
|
+
];
|
|
80
|
+
var npmCommand = (platform = process.platform, node = process.execPath) => platform === "win32" ? { file: node, args: [win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npm-cli.js")] } : { file: "npm", args: [] };
|
|
81
|
+
var ensureEngine = (dataDir) => {
|
|
82
|
+
if (installedVersion(dataDir) === ENGINE_VERSION) return;
|
|
83
|
+
if (!existsSync2(engineDir(dataDir))) mkdirSync(engineDir(dataDir), { recursive: true });
|
|
84
|
+
const npm = npmCommand();
|
|
85
|
+
const npmCli = npm.args[0];
|
|
86
|
+
if (npmCli && !existsSync2(npmCli)) {
|
|
87
|
+
throw new CheckError(
|
|
88
|
+
`npm was not found beside ${npm.file}, so the crawl engine can't be installed. Reinstall Node.js with npm included, then try again.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const result = spawnSync(npm.file, [...npm.args, ...installArgs(dataDir)], { encoding: "utf8" });
|
|
92
|
+
if (result.error) {
|
|
93
|
+
throw new CheckError(`Could not run npm to install the crawl engine: ${result.error.message}`);
|
|
94
|
+
}
|
|
95
|
+
if (result.status !== 0) {
|
|
96
|
+
const lastLines = (result.stderr ?? "").trim().split("\n").slice(-5).join("\n");
|
|
97
|
+
throw new CheckError(
|
|
98
|
+
`Installing the crawl engine failed.${lastLines ? ` ${lastLines}` : ""} Check this machine has npm and a network connection, then try again.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (installedVersion(dataDir) !== ENGINE_VERSION) {
|
|
102
|
+
throw new CheckError("The crawl engine did not install correctly. Try again.");
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/savedPlans.ts
|
|
107
|
+
import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, rmSync as rmSync2, statSync, writeFileSync } from "node:fs";
|
|
108
|
+
import { join as join3 } from "node:path";
|
|
109
|
+
var SCANS = "scans";
|
|
110
|
+
var SCAN_PROGRESS = "scan-progress";
|
|
111
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
112
|
+
var MAX_AGE_MS = 30 * 60 * 1e3;
|
|
113
|
+
var PRIVATE_DIR = { recursive: true, mode: 448 };
|
|
114
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
115
|
+
var isExpired = (path) => Date.now() - statSync(path).mtimeMs > MAX_AGE_MS;
|
|
116
|
+
var liveFiles = (dir) => {
|
|
117
|
+
let names;
|
|
118
|
+
try {
|
|
119
|
+
names = readdirSync(dir);
|
|
120
|
+
} catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
const live = [];
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
const path = join3(dir, name);
|
|
126
|
+
try {
|
|
127
|
+
if (isExpired(path)) rmSync2(path, { force: true });
|
|
128
|
+
else live.push({ path, mtime: statSync(path).mtimeMs });
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return live.sort((a, b) => b.mtime - a.mtime).map(({ path }) => path);
|
|
133
|
+
};
|
|
134
|
+
var write = (dir, name, value) => {
|
|
135
|
+
mkdirSync2(dir, PRIVATE_DIR);
|
|
136
|
+
liveFiles(dir);
|
|
137
|
+
writeFileSync(join3(dir, name), JSON.stringify(value), { mode: 384 });
|
|
138
|
+
};
|
|
139
|
+
var isScanId = (value) => typeof value === "string" && UUID.test(value);
|
|
140
|
+
var isCrawlableUrl = (value) => {
|
|
141
|
+
let url;
|
|
142
|
+
try {
|
|
143
|
+
url = new URL(value);
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
148
|
+
const host = url.hostname.toLowerCase().replace(/^\x5B|\x5D$/g, "").replace(/\.$/, "");
|
|
149
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
if (host.includes(":")) return !/^(::|f[cd]|fe[89abc])/.test(host);
|
|
153
|
+
const octets = host.split(".").map(Number);
|
|
154
|
+
if (octets.length === 4 && octets.every((n) => Number.isInteger(n) && n >= 0 && n <= 255)) {
|
|
155
|
+
const [a, b] = octets;
|
|
156
|
+
return !(a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168);
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
};
|
|
160
|
+
var readScan = (dataDir, scanId) => {
|
|
161
|
+
if (!isScanId(scanId)) return null;
|
|
162
|
+
const path = join3(dataDir, SCANS, `${scanId}.json`);
|
|
163
|
+
try {
|
|
164
|
+
if (isExpired(path)) {
|
|
165
|
+
rmSync2(path, { force: true });
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const saved = JSON.parse(readFileSync2(path, "utf8"));
|
|
169
|
+
if (!isRecord(saved)) return null;
|
|
170
|
+
const { scanId: id, domainId, url, maxPages, savedAt } = saved;
|
|
171
|
+
if (typeof id !== "string" || typeof domainId !== "string" || typeof url !== "string") return null;
|
|
172
|
+
if (typeof maxPages !== "number" || typeof savedAt !== "number") return null;
|
|
173
|
+
return { scanId: id, domainId, url, maxPages, savedAt };
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
var saveScanProgress = (dataDir, scanId, progress) => {
|
|
179
|
+
if (!isScanId(scanId)) return false;
|
|
180
|
+
write(join3(dataDir, SCAN_PROGRESS), `${scanId}.json`, { scanId, ...progress, savedAt: Date.now() });
|
|
181
|
+
return true;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/categorise.ts
|
|
185
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
186
|
+
var catalogue;
|
|
187
|
+
var loadCatalogue = () => {
|
|
188
|
+
const cookies = JSON.parse(
|
|
189
|
+
readFileSync3(new URL(true ? "./cookies.json" : "../dist/cookies.json", import.meta.url), "utf8")
|
|
190
|
+
);
|
|
191
|
+
const exact = /* @__PURE__ */ new Map();
|
|
192
|
+
const prefixes = [];
|
|
193
|
+
for (const entry of cookies) {
|
|
194
|
+
const match = { category: entry.category, vendor: entry.vendor === "Unknown" ? null : entry.vendor, known: true };
|
|
195
|
+
const name = entry.name.toLowerCase();
|
|
196
|
+
if (entry.wildcard) prefixes.push({ prefix: name, match });
|
|
197
|
+
else if (!exact.has(name)) exact.set(name, match);
|
|
198
|
+
}
|
|
199
|
+
prefixes.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
200
|
+
return { exact, prefixes };
|
|
201
|
+
};
|
|
202
|
+
var UNKNOWN = { category: "other", vendor: null, known: false };
|
|
203
|
+
var categorise = (name) => {
|
|
204
|
+
catalogue ??= loadCatalogue();
|
|
205
|
+
const lower = name.toLowerCase();
|
|
206
|
+
const found = catalogue.exact.get(lower);
|
|
207
|
+
if (found) return found;
|
|
208
|
+
for (const { prefix, match } of catalogue.prefixes) {
|
|
209
|
+
if (lower.startsWith(prefix) && lower.length > prefix.length) return match;
|
|
210
|
+
}
|
|
211
|
+
return UNKNOWN;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/result.ts
|
|
215
|
+
var COOKIE_NAME = /^[\x21-\x3A\x3C\x3E-\x7E]*$/;
|
|
216
|
+
var COOKIE_DOMAIN = /^\.?[A-Za-z0-9_.:\x5B\x5D\x2D]+$/;
|
|
217
|
+
var COOKIE_PATH = /^\/[\x21-\x3A\x3C-\x7E]*$/;
|
|
218
|
+
var fits = (value, pattern, max, min = 1) => value.length >= min && value.length <= max && pattern.test(value);
|
|
219
|
+
var cookieFits = (cookie, limits) => fits(cookie.name, COOKIE_NAME, limits.maxCookieNameLength, 0) && fits(cookie.domain, COOKIE_DOMAIN, limits.maxCookieDomainLength) && fits(cookie.path, COOKIE_PATH, limits.maxCookiePathLength);
|
|
220
|
+
|
|
221
|
+
// src/scanReport.ts
|
|
222
|
+
var SCAN_COOKIE_LIMITS = { maxCookieNameLength: 128, maxCookieDomainLength: 253, maxCookiePathLength: 128 };
|
|
223
|
+
var MAX_SCAN_COOKIES = 500;
|
|
224
|
+
var reportCookies = (cookies) => {
|
|
225
|
+
const fitting = cookies.filter((cookie) => cookieFits(cookie, SCAN_COOKIE_LIMITS));
|
|
226
|
+
return {
|
|
227
|
+
cookies: fitting.slice(0, MAX_SCAN_COOKIES).map((cookie) => {
|
|
228
|
+
const { category, vendor } = categorise(cookie.name);
|
|
229
|
+
return {
|
|
230
|
+
name: cookie.name,
|
|
231
|
+
domain: cookie.domain,
|
|
232
|
+
path: cookie.path,
|
|
233
|
+
duration: cookie.duration,
|
|
234
|
+
category,
|
|
235
|
+
...vendor ? { description: vendor } : {}
|
|
236
|
+
};
|
|
237
|
+
}),
|
|
238
|
+
capped: fitting.length > MAX_SCAN_COOKIES
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/scanStart.ts
|
|
243
|
+
import { getDomain as getDomain2 } from "./tldts.mjs";
|
|
244
|
+
var START_TIMEOUT_MS = 15e3;
|
|
245
|
+
var resolveStart = async (url, fetchImpl = fetch) => {
|
|
246
|
+
let landed;
|
|
247
|
+
try {
|
|
248
|
+
const response = await fetchImpl(url, { redirect: "follow", signal: AbortSignal.timeout(START_TIMEOUT_MS) });
|
|
249
|
+
await response.body?.cancel();
|
|
250
|
+
landed = response.url || url;
|
|
251
|
+
} catch {
|
|
252
|
+
return url;
|
|
253
|
+
}
|
|
254
|
+
if (landed === url) return url;
|
|
255
|
+
const from = new URL(url).hostname;
|
|
256
|
+
const to = new URL(landed).hostname;
|
|
257
|
+
if (getDomain2(to) !== getDomain2(from) || !isCrawlableUrl(landed)) {
|
|
258
|
+
throw new CheckError(
|
|
259
|
+
`${from} redirects to another site, so a crawl from it would not cover this domain. Check the domain's URL in CookiePal.`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return landed;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// src/scan.ts
|
|
266
|
+
var MAX_DEPTH = 3;
|
|
267
|
+
var CONCURRENCY = 3;
|
|
268
|
+
var run = async () => {
|
|
269
|
+
const scanId = process.argv[2];
|
|
270
|
+
const dataDir = process.env.COOKIEPAL_DATA_DIR || null;
|
|
271
|
+
if (!scanId || !dataDir) return;
|
|
272
|
+
const scan = readScan(dataDir, scanId);
|
|
273
|
+
if (!scan) {
|
|
274
|
+
saveScanProgress(dataDir, scanId, {
|
|
275
|
+
status: "failed",
|
|
276
|
+
error: "The scan could not be found. Start a new one with start_scan."
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const storageDir = `${dataDir}/crawl-storage/${scanId}`;
|
|
281
|
+
try {
|
|
282
|
+
ensureEngine(dataDir);
|
|
283
|
+
saveScanProgress(dataDir, scanId, { status: "running", pagesVisited: 0 });
|
|
284
|
+
const browserPath = findBrowser();
|
|
285
|
+
if (!browserPath) {
|
|
286
|
+
throw new CheckError(
|
|
287
|
+
"No Chrome, Edge, Brave or Chromium was found. Install one, or set COOKIEPAL_BROWSER to its executable."
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
process.env.PUPPETEER_EXECUTABLE_PATH = browserPath;
|
|
291
|
+
const engineUrl = pathToFileURL(engineEntry(dataDir)).href;
|
|
292
|
+
const { crawl } = await import(engineUrl);
|
|
293
|
+
saveScanProgress(dataDir, scanId, { status: "running", pagesVisited: 0 });
|
|
294
|
+
const start = await resolveStart(scan.url);
|
|
295
|
+
let pagesVisited = 0;
|
|
296
|
+
const result = await crawl({
|
|
297
|
+
url: start,
|
|
298
|
+
maxPages: scan.maxPages,
|
|
299
|
+
maxDepth: MAX_DEPTH,
|
|
300
|
+
concurrency: CONCURRENCY,
|
|
301
|
+
storageDir,
|
|
302
|
+
keepImages: true,
|
|
303
|
+
markRequests: false,
|
|
304
|
+
respectRobots: true,
|
|
305
|
+
sandbox: true,
|
|
306
|
+
onPageCrawled: () => {
|
|
307
|
+
pagesVisited += 1;
|
|
308
|
+
saveScanProgress(dataDir, scanId, { status: "running", pagesVisited });
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
if (result.startBlockedByRobots) {
|
|
312
|
+
saveScanProgress(dataDir, scanId, {
|
|
313
|
+
status: "blocked",
|
|
314
|
+
blockedUrl: result.robotsBlocked[0] ?? start
|
|
315
|
+
});
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const { cookies, capped } = reportCookies(result.cookies);
|
|
319
|
+
saveScanProgress(dataDir, scanId, {
|
|
320
|
+
status: "done",
|
|
321
|
+
pagesScanned: result.pagesVisited,
|
|
322
|
+
cookies,
|
|
323
|
+
thirdPartyHosts: [],
|
|
324
|
+
truncated: result.pagesVisited >= scan.maxPages || capped
|
|
325
|
+
});
|
|
326
|
+
} catch (error) {
|
|
327
|
+
saveScanProgress(dataDir, scanId, {
|
|
328
|
+
status: "failed",
|
|
329
|
+
error: error instanceof CheckError ? error.message : "The crawl could not be completed."
|
|
330
|
+
});
|
|
331
|
+
} finally {
|
|
332
|
+
rmSync3(storageDir, { recursive: true, force: true });
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
run();
|