clearcote 0.21.0 → 0.22.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/dist/humanize.d.ts.map +1 -1
- package/dist/humanize.js +4 -2
- package/dist/humanize.js.map +1 -1
- package/dist/index.d.ts +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +75 -2
- package/dist/index.js.map +1 -1
- package/dist/motionscore.js +1 -1
- package/dist/motionscore.js.map +1 -1
- package/dist/profileauto.d.ts +66 -0
- package/dist/profileauto.d.ts.map +1 -0
- package/dist/profileauto.js +182 -0
- package/dist/profileauto.js.map +1 -0
- package/dist/profilefetch.d.ts +21 -0
- package/dist/profilefetch.d.ts.map +1 -0
- package/dist/profilefetch.js +124 -0
- package/dist/profilefetch.js.map +1 -0
- package/dist/profileimport.d.ts +22 -0
- package/dist/profileimport.d.ts.map +1 -0
- package/dist/profileimport.js +125 -0
- package/dist/profileimport.js.map +1 -0
- package/dist/profilelib.d.ts +100 -0
- package/dist/profilelib.d.ts.map +1 -0
- package/dist/profilelib.js +181 -0
- package/dist/profilelib.js.map +1 -0
- package/dist/profilesource.d.ts +38 -0
- package/dist/profilesource.d.ts.map +1 -0
- package/dist/profilesource.js +200 -0
- package/dist/profilesource.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// `profile: "auto"` — resolve a persona from the best source available.
|
|
2
|
+
//
|
|
3
|
+
// ORDER: the licensed clearcote service first, a local imported directory as backup.
|
|
4
|
+
//
|
|
5
|
+
// The service is preferred because its corpus is fresher, larger and curated. But it can be
|
|
6
|
+
// unreachable for reasons that are none of the caller's fault — no license, an exhausted daily
|
|
7
|
+
// budget, a rate limit, a network blip, an outage — and in every one of those cases a local
|
|
8
|
+
// profile is enormously better than no persona. So the fallback is by design, not a patch.
|
|
9
|
+
//
|
|
10
|
+
// THE FALLBACK IS ALWAYS ANNOUNCED. A silent downgrade would leave a caller believing they are
|
|
11
|
+
// on a fresh service profile while they are on a stale local one, and the whole product is about
|
|
12
|
+
// knowing exactly what identity you are presenting. `source` is returned on every result and a
|
|
13
|
+
// one-line reason is written to stderr unless `quiet`.
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { PROFILE_DIR } from "./profile.js";
|
|
18
|
+
import { importDirectory, loadImportedProfile } from "./profileimport.js";
|
|
19
|
+
import { selectProfile } from "./profilelib.js";
|
|
20
|
+
import { fetchIndex, fetchProfile } from "./profilesource.js";
|
|
21
|
+
import { ensureLocalProfiles, hasLocalProfiles } from "./profilefetch.js";
|
|
22
|
+
/** Where an imported (e.g. converted chrome-fingerprints) directory is looked for by default. */
|
|
23
|
+
export const DEFAULT_LOCAL_DIR = process.env.CLEARCOTE_PROFILE_SOURCE ?? join(homedir(), ".clearcote", "profiles", "imported");
|
|
24
|
+
/**
|
|
25
|
+
* First engine major that implements `--fingerprint-profile`.
|
|
26
|
+
*
|
|
27
|
+
* BACKWARDS COMPATIBILITY, AND WHY THIS IS A HARD GATE. The free 149 build ships a 15-patch
|
|
28
|
+
* stack with no persona-profile patch at all, so it does not merely apply the profile badly —
|
|
29
|
+
* it ignores the switch entirely. Chromium discards unknown switches silently, so a 149 user
|
|
30
|
+
* who asked for a profile would launch with NO persona while believing they had one. That is
|
|
31
|
+
* strictly worse than the seed farbling 149 does support, because the failure is invisible.
|
|
32
|
+
*
|
|
33
|
+
* So a profile is never sent to an engine that cannot read it. Callers on such an engine fall
|
|
34
|
+
* back to the seed path, which is what 149 was built for.
|
|
35
|
+
*/
|
|
36
|
+
export const MIN_PROFILE_ENGINE_MAJOR = 150;
|
|
37
|
+
/** Whether this engine can actually apply an imported profile. */
|
|
38
|
+
export function engineSupportsProfiles(browserMajor) {
|
|
39
|
+
return Number.isFinite(browserMajor) && browserMajor >= MIN_PROFILE_ENGINE_MAJOR;
|
|
40
|
+
}
|
|
41
|
+
function localCachePath() {
|
|
42
|
+
const d = join(PROFILE_DIR, "library");
|
|
43
|
+
if (!existsSync(d))
|
|
44
|
+
mkdirSync(d, { recursive: true });
|
|
45
|
+
return join(d, "local-index.json");
|
|
46
|
+
}
|
|
47
|
+
export function loadLocalIndex(dir) {
|
|
48
|
+
const mtimeMs = statSync(dir).mtimeMs;
|
|
49
|
+
const path = localCachePath();
|
|
50
|
+
if (existsSync(path)) {
|
|
51
|
+
try {
|
|
52
|
+
const c = JSON.parse(readFileSync(path, "utf8"));
|
|
53
|
+
if (c.dir === dir && c.mtimeMs === mtimeMs)
|
|
54
|
+
return c.index;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* fall through and re-index */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const { index } = importDirectory(dir);
|
|
61
|
+
try {
|
|
62
|
+
writeFileSync(path, JSON.stringify({ dir, mtimeMs, index }));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* a cache write failure must never fail a launch */
|
|
66
|
+
}
|
|
67
|
+
return index;
|
|
68
|
+
}
|
|
69
|
+
function note(quiet, msg) {
|
|
70
|
+
if (!quiet)
|
|
71
|
+
process.stderr.write(`[clearcote] [profile] ${msg}\n`);
|
|
72
|
+
}
|
|
73
|
+
/** The optional-backup tip is worth saying once, not on every launch. */
|
|
74
|
+
let hintedThisProcess = false;
|
|
75
|
+
/** Test seam: reset the once-per-process tip. */
|
|
76
|
+
export function _resetHint() {
|
|
77
|
+
hintedThisProcess = false;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* How to populate a local backup directory. Printed when the service is unavailable and no local
|
|
81
|
+
* directory exists — the moment the caller actually needs it.
|
|
82
|
+
*
|
|
83
|
+
* OPTIONAL, AND NOT SOMETHING WE SHIP. Clearcote redistributes no third-party fingerprints; the
|
|
84
|
+
* dataset is fetched by the user from its own repository under its own terms, and we only read
|
|
85
|
+
* the converted format. Any directory of clearcote-profile JSON works just as well, which is
|
|
86
|
+
* why this is phrased as one example rather than the required path.
|
|
87
|
+
*/
|
|
88
|
+
export function localSetupHint(dir) {
|
|
89
|
+
return [
|
|
90
|
+
`A local profile directory is optional, and gives "auto" a backup when the service is`,
|
|
91
|
+
`unavailable. Any folder of clearcote-profile JSON works — your own captures, or a`,
|
|
92
|
+
`third-party dataset you convert yourself. For example, using the open chrome-fingerprints`,
|
|
93
|
+
`dataset (fetched by you, from its own repo, under its own licence):`,
|
|
94
|
+
``,
|
|
95
|
+
` pip install chrome-fingerprints`,
|
|
96
|
+
` python tools/fingerprint-collect/convert_dataset.py --out "${dir}" --count 500`,
|
|
97
|
+
``,
|
|
98
|
+
`Or capture your own with tools/fingerprint-collect (collect.html), or point`,
|
|
99
|
+
`CLEARCOTE_PROFILE_SOURCE at a directory you already have.`,
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
/** Resolve a persona from the local imported directory. */
|
|
103
|
+
export function resolveLocal(host, opts = {}) {
|
|
104
|
+
const dir = opts.localDir ?? DEFAULT_LOCAL_DIR;
|
|
105
|
+
if (!existsSync(dir)) {
|
|
106
|
+
throw new Error(`no local profile directory at ${dir}\n\n${localSetupHint(dir)}`);
|
|
107
|
+
}
|
|
108
|
+
const index = loadLocalIndex(dir);
|
|
109
|
+
if (index.length === 0) {
|
|
110
|
+
throw new Error(`local profile directory ${dir} contains no usable clearcote-profile JSON`);
|
|
111
|
+
}
|
|
112
|
+
const selection = selectProfile(index, host, opts);
|
|
113
|
+
return { profile: loadImportedProfile(dir, selection.entry.id), selection, source: "local" };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve `profile: "auto"`: service first, local directory as backup.
|
|
117
|
+
*
|
|
118
|
+
* Any service failure falls back — including 401 (no license) and 429 (rate limited or daily
|
|
119
|
+
* distinct budget exhausted), because in all of those a local persona is better than none and
|
|
120
|
+
* far better than a hard launch failure.
|
|
121
|
+
*/
|
|
122
|
+
export async function resolveAuto(host, opts = {}) {
|
|
123
|
+
if (opts.preferLocal) {
|
|
124
|
+
const r = resolveLocal(host, opts);
|
|
125
|
+
note(opts.quiet, `using local profile ${r.selection.entry.id} (preferLocal)`);
|
|
126
|
+
return r;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const index = await fetchIndex(host, opts);
|
|
130
|
+
if (index.length === 0)
|
|
131
|
+
throw new Error("service returned no candidates for this host");
|
|
132
|
+
const selection = selectProfile(index, host, opts);
|
|
133
|
+
const profile = await fetchProfile(selection.entry.id, opts);
|
|
134
|
+
note(opts.quiet, `using service profile ${selection.entry.id} ` +
|
|
135
|
+
`(score ${selection.score.toFixed(2)}, pool ${selection.poolSize}, mode ${selection.mode})`);
|
|
136
|
+
// Mention the optional backup ONCE per process, and only while there is none configured.
|
|
137
|
+
// The useful moment to learn about a fallback is before you need it — but repeating it on
|
|
138
|
+
// every launch would be noise, and nagging someone who already set one up would be wrong.
|
|
139
|
+
if (!hintedThisProcess && !existsSync(opts.localDir ?? DEFAULT_LOCAL_DIR)) {
|
|
140
|
+
hintedThisProcess = true;
|
|
141
|
+
note(opts.quiet, `tip: no local backup directory configured. If the service is ever rate limited or
|
|
142
|
+
unreachable, "auto" fails rather than falling back. Setting one up is optional:
|
|
143
|
+
${localSetupHint(opts.localDir ?? DEFAULT_LOCAL_DIR)}`);
|
|
144
|
+
}
|
|
145
|
+
return { profile, selection, source: "service" };
|
|
146
|
+
}
|
|
147
|
+
catch (serviceErr) {
|
|
148
|
+
const reason = String(serviceErr instanceof Error ? serviceErr.message : serviceErr).slice(0, 200);
|
|
149
|
+
// First-run bootstrap: if there is no local library yet, try to build one before giving up.
|
|
150
|
+
// Only on the fallback path — a working service must never pay for this. Never throws: a
|
|
151
|
+
// bootstrap failure has to degrade to "no local backup", not take down the launch.
|
|
152
|
+
if (opts.autoDownload !== false) {
|
|
153
|
+
const dir = opts.localDir ?? DEFAULT_LOCAL_DIR;
|
|
154
|
+
if (!hasLocalProfiles(dir)) {
|
|
155
|
+
await ensureLocalProfiles(dir, join(PROFILE_DIR, "library"), {
|
|
156
|
+
quiet: opts.quiet,
|
|
157
|
+
count: opts.autoDownloadCount,
|
|
158
|
+
converter: opts.converter,
|
|
159
|
+
python: opts.python,
|
|
160
|
+
}).catch(() => false);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const r = resolveLocal(host, opts);
|
|
165
|
+
// Both halves of the story: what failed, and what is being used instead.
|
|
166
|
+
note(opts.quiet, `service unavailable (${reason}) — falling back to local profile ${r.selection.entry.id}`);
|
|
167
|
+
return { ...r, fallbackReason: reason };
|
|
168
|
+
}
|
|
169
|
+
catch (localErr) {
|
|
170
|
+
const dir = opts.localDir ?? DEFAULT_LOCAL_DIR;
|
|
171
|
+
const noLocalDir = !existsSync(dir);
|
|
172
|
+
const lr = String(localErr instanceof Error ? localErr.message : localErr);
|
|
173
|
+
// Report BOTH failures. Surfacing only the local one would send the caller to go and fix
|
|
174
|
+
// a directory when the real problem was an expired license.
|
|
175
|
+
throw new Error(`profile: "auto" could not resolve a persona.\n` +
|
|
176
|
+
` service: ${reason}\n` +
|
|
177
|
+
` local: ${noLocalDir ? `no directory at ${dir}` : lr}\n\n` +
|
|
178
|
+
(noLocalDir ? localSetupHint(dir) : ""));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=profileauto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profileauto.js","sourceRoot":"","sources":["../src/profileauto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface BootstrapOptions {
|
|
2
|
+
/** How many profiles to convert. The full set is 10,000; a few hundred is ample for rotation. */
|
|
3
|
+
count?: number;
|
|
4
|
+
/** Path to convert_dataset.py. Defaults to the copy in this checkout, if present. */
|
|
5
|
+
converter?: string;
|
|
6
|
+
/** Python executable. */
|
|
7
|
+
python?: string;
|
|
8
|
+
quiet?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** True when the directory already holds at least one profile — the cheap check that makes this
|
|
11
|
+
* a no-op on every run after the first. */
|
|
12
|
+
export declare function hasLocalProfiles(dir: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Populate `dir` with converted profiles, if it is empty.
|
|
15
|
+
*
|
|
16
|
+
* Returns true when profiles are present afterwards. Never throws for an ordinary missing
|
|
17
|
+
* prerequisite (no Python, no converter, no network) — the caller is a FALLBACK path, and a
|
|
18
|
+
* bootstrap failure there must degrade to "no local backup", not take down the launch.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ensureLocalProfiles(dir: string, cacheDir: string, opts?: BootstrapOptions): Promise<boolean>;
|
|
21
|
+
//# sourceMappingURL=profilefetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profilefetch.d.ts","sourceRoot":"","sources":["../src/profilefetch.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,gBAAgB;IAC/B,iGAAiG;IACjG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yBAAyB;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD;4CAC4C;AAC5C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMrD;AAkBD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAuClB"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Optional first-run bootstrap for the local profile directory.
|
|
2
|
+
//
|
|
3
|
+
// WE DOWNLOAD, WE DO NOT REDISTRIBUTE — and here that distinction is legal, not stylistic.
|
|
4
|
+
// The chrome-fingerprints dataset is **GPL-3.0**. Clearcote's SDK is BSD-3-Clause, so shipping
|
|
5
|
+
// those records inside the package would mean distributing GPL data from a permissively licensed
|
|
6
|
+
// artifact. Fetching it onto the user's own machine, from the upstream repository, at the user's
|
|
7
|
+
// request, is an entirely different act: no redistribution occurs and the dataset's terms are
|
|
8
|
+
// between the user and upstream.
|
|
9
|
+
//
|
|
10
|
+
// This mirrors what geoip.ts already does for the geoip-all-in-one MaxMind database, which is
|
|
11
|
+
// GPL-3.0 for exactly the same reason and is likewise downloaded on first use and never bundled.
|
|
12
|
+
//
|
|
13
|
+
// WHY IT SHELLS OUT TO PYTHON. The dataset is `fingerprints.json.xz` (LZMA — Node has no built-in
|
|
14
|
+
// decompressor) with strings INTERNED as integer indices into a 137 KB `vars.py`. Converting it
|
|
15
|
+
// means resolving that interning and remapping ~157 WebGL keys and ~108 audio keys onto the
|
|
16
|
+
// clearcote-profile schema. tools/fingerprint-collect/convert_dataset.py already does all of
|
|
17
|
+
// that, is tested, and is the file that gets updated when either schema moves. Reimplementing it
|
|
18
|
+
// in TypeScript (and again in C#) would be three copies of one mapping table drifting apart, plus
|
|
19
|
+
// an LZMA dependency in each. Python is a soft requirement for one optional bootstrap step.
|
|
20
|
+
import { spawn } from "node:child_process";
|
|
21
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
const RAW_BASE = "https://raw.githubusercontent.com/Vinyzu/chrome-fingerprints/main/chrome_fingerprints";
|
|
25
|
+
/** The two files convert_dataset.py's find_dataset() looks for. */
|
|
26
|
+
const DATASET_FILES = ["fingerprints.json.xz", "vars.py"];
|
|
27
|
+
function note(quiet, msg) {
|
|
28
|
+
if (!quiet)
|
|
29
|
+
process.stderr.write(`[clearcote] [profile] ${msg}\n`);
|
|
30
|
+
}
|
|
31
|
+
/** True when the directory already holds at least one profile — the cheap check that makes this
|
|
32
|
+
* a no-op on every run after the first. */
|
|
33
|
+
export function hasLocalProfiles(dir) {
|
|
34
|
+
try {
|
|
35
|
+
return existsSync(dir) && readdirSync(dir).some((f) => f.endsWith(".json"));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function download(url, dest) {
|
|
42
|
+
const res = await fetch(url);
|
|
43
|
+
if (!res.ok)
|
|
44
|
+
throw new Error(`${url} -> HTTP ${res.status}`);
|
|
45
|
+
writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
|
46
|
+
}
|
|
47
|
+
function run(cmd, args) {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const p = spawn(cmd, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
50
|
+
let stderr = "";
|
|
51
|
+
p.stderr?.on("data", (d) => { stderr += String(d); });
|
|
52
|
+
p.on("error", () => resolve({ code: -1, stderr: `could not run ${cmd}` }));
|
|
53
|
+
p.on("close", (code) => resolve({ code: code ?? -1, stderr }));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Populate `dir` with converted profiles, if it is empty.
|
|
58
|
+
*
|
|
59
|
+
* Returns true when profiles are present afterwards. Never throws for an ordinary missing
|
|
60
|
+
* prerequisite (no Python, no converter, no network) — the caller is a FALLBACK path, and a
|
|
61
|
+
* bootstrap failure there must degrade to "no local backup", not take down the launch.
|
|
62
|
+
*/
|
|
63
|
+
export async function ensureLocalProfiles(dir, cacheDir, opts = {}) {
|
|
64
|
+
if (hasLocalProfiles(dir))
|
|
65
|
+
return true;
|
|
66
|
+
const converter = opts.converter ?? defaultConverterPath();
|
|
67
|
+
if (!converter || !existsSync(converter)) {
|
|
68
|
+
note(opts.quiet, "no local profiles and no convert_dataset.py found — skipping bootstrap");
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const python = opts.python ?? (process.platform === "win32" ? "python" : "python3");
|
|
72
|
+
const count = opts.count ?? 500;
|
|
73
|
+
try {
|
|
74
|
+
const dataset = join(cacheDir, "chrome-fingerprints");
|
|
75
|
+
mkdirSync(dataset, { recursive: true });
|
|
76
|
+
// Fetch the dataset itself rather than requiring `pip install chrome-fingerprints`: these are
|
|
77
|
+
// the only two files the converter needs, and this keeps the prerequisite to "Python exists".
|
|
78
|
+
const missing = DATASET_FILES.filter((f) => !existsSync(join(dataset, f)));
|
|
79
|
+
if (missing.length) {
|
|
80
|
+
note(opts.quiet, `downloading the chrome-fingerprints dataset (GPL-3.0, ~1.3 MB) from its upstream repo — ` +
|
|
81
|
+
`not redistributed by clearcote`);
|
|
82
|
+
for (const f of missing)
|
|
83
|
+
await download(`${RAW_BASE}/${f}`, join(dataset, f));
|
|
84
|
+
}
|
|
85
|
+
mkdirSync(dir, { recursive: true });
|
|
86
|
+
note(opts.quiet, `converting ${count} profiles into ${dir} (first run only)`);
|
|
87
|
+
const r = await run(python, [converter, "--dataset", dataset, "--out", dir, "--count", String(count)]);
|
|
88
|
+
if (r.code !== 0) {
|
|
89
|
+
note(opts.quiet, `bootstrap failed (${python} exited ${r.code}): ${r.stderr.trim().slice(0, 200)}`);
|
|
90
|
+
return hasLocalProfiles(dir);
|
|
91
|
+
}
|
|
92
|
+
const ok = hasLocalProfiles(dir);
|
|
93
|
+
note(opts.quiet, ok ? `local profile library ready: ${dir}` : "converter produced no profiles");
|
|
94
|
+
return ok;
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
note(opts.quiet, `bootstrap skipped: ${String(e instanceof Error ? e.message : e).slice(0, 160)}`);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* convert_dataset.py inside this checkout, when the SDK is used from the repo.
|
|
103
|
+
*
|
|
104
|
+
* Returns null rather than throwing for ANY reason. This is called from a fallback path, and the
|
|
105
|
+
* published package has no `tools/` directory at all — an installed-from-npm user simply has no
|
|
106
|
+
* converter, which is a normal state and not an error.
|
|
107
|
+
*/
|
|
108
|
+
function defaultConverterPath() {
|
|
109
|
+
try {
|
|
110
|
+
const env = process.env.CLEARCOTE_PROFILE_CONVERTER;
|
|
111
|
+
if (env)
|
|
112
|
+
return env;
|
|
113
|
+
// `__dirname` does not exist here: this package is `"type": "module"`, so the CJS globals are
|
|
114
|
+
// absent and referencing one throws a ReferenceError at runtime that typechecking cannot see.
|
|
115
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
116
|
+
// dist/ (or src/) -> ../../../tools/fingerprint-collect/convert_dataset.py
|
|
117
|
+
const guess = join(here, "..", "..", "..", "tools", "fingerprint-collect", "convert_dataset.py");
|
|
118
|
+
return existsSync(guess) ? guess : null;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=profilefetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profilefetch.js","sourceRoot":"","sources":["../src/profilefetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ProfileIndexEntry } from "./profilelib.js";
|
|
2
|
+
/** Parse a clearcote-profile blob into an index entry. Returns null when the file is not one. */
|
|
3
|
+
export declare function indexEntryFromProfile(id: string, profile: Record<string, unknown>): ProfileIndexEntry | null;
|
|
4
|
+
export interface ImportResult {
|
|
5
|
+
index: ProfileIndexEntry[];
|
|
6
|
+
/** Files that were not usable, with the reason — surfaced rather than silently dropped, so a
|
|
7
|
+
* half-converted directory is visible instead of quietly producing a thin pool. */
|
|
8
|
+
skipped: {
|
|
9
|
+
file: string;
|
|
10
|
+
reason: string;
|
|
11
|
+
}[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Index every `*.json` in a directory.
|
|
15
|
+
*
|
|
16
|
+
* Reads each file once. For 10k profiles this is a few seconds and a lot of I/O, so callers
|
|
17
|
+
* should cache the result (`writeIndex`) rather than re-scanning per launch.
|
|
18
|
+
*/
|
|
19
|
+
export declare function importDirectory(dir: string): ImportResult;
|
|
20
|
+
/** Load one profile from an imported directory, by the id `importDirectory` assigned it. */
|
|
21
|
+
export declare function loadImportedProfile(dir: string, id: string): Record<string, unknown>;
|
|
22
|
+
//# sourceMappingURL=profileimport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profileimport.d.ts","sourceRoot":"","sources":["../src/profileimport.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAkB,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzE,iGAAiG;AACjG,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,iBAAiB,GAAG,IAAI,CAsD1B;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B;wFACoF;IACpF,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC7C;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAiCzD;AAED,4FAA4F;AAC5F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASpF"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Generic profile importer: turn a DIRECTORY of clearcote-profile JSON into a selectable index.
|
|
2
|
+
//
|
|
3
|
+
// WE ARE AN IMPORTER, NOT A HOST. Clearcote ships no third-party fingerprints and redistributes
|
|
4
|
+
// none. A source is just a folder of clearcote-profile JSON on the user's own machine, however
|
|
5
|
+
// it got there:
|
|
6
|
+
//
|
|
7
|
+
// * the open chrome-fingerprints dataset (Vinyzu), converted locally with
|
|
8
|
+
// tools/fingerprint-collect/convert_dataset.py — the dataset is fetched by the USER from the
|
|
9
|
+
// original repository, so nothing is re-published here and its terms are unaffected;
|
|
10
|
+
// * your own captures from tools/fingerprint-collect (collect.html / snippet.js);
|
|
11
|
+
// * the licensed clearcote profile service (see profilesource.ts);
|
|
12
|
+
// * any other producer that emits the same schema.
|
|
13
|
+
//
|
|
14
|
+
// Keeping the format as the ONLY contract is what makes this generic. There is no adapter per
|
|
15
|
+
// vendor, no vendored data, and no decompressor for someone else's archive format baked into
|
|
16
|
+
// three SDKs — a directory of JSON is something every source can produce and every SDK can read.
|
|
17
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { gzipSync } from "node:zlib";
|
|
20
|
+
import { gpuVendorClass } from "./profilelib.js";
|
|
21
|
+
/** Parse a clearcote-profile blob into an index entry. Returns null when the file is not one. */
|
|
22
|
+
export function indexEntryFromProfile(id, profile) {
|
|
23
|
+
const nav = (profile.navigator ?? {});
|
|
24
|
+
const screen = (profile.screen ?? {});
|
|
25
|
+
const webgl = (profile.webgl ?? {});
|
|
26
|
+
const gl1 = (webgl.webgl1 ?? {});
|
|
27
|
+
const debug = (gl1.debug ?? {});
|
|
28
|
+
const meta = (profile.meta ?? {});
|
|
29
|
+
const ua = String(nav.user_agent ?? "");
|
|
30
|
+
const platform = String(nav.platform ?? "");
|
|
31
|
+
// UA-CH. Checked FIRST because it is the only source present on some real inputs: converted
|
|
32
|
+
// chrome-fingerprints records carry `uadata.platform` while leaving `user_agent` and
|
|
33
|
+
// `platform` null (the converter deliberately drops the browser version). Reading only the UA
|
|
34
|
+
// string silently skipped every one of those — 10,000 usable profiles classified as garbage.
|
|
35
|
+
const uadata = (nav.uadata ?? {});
|
|
36
|
+
const chPlatform = String(uadata.platform ?? (uadata.high_entropy?.platform ?? ""));
|
|
37
|
+
const chMobile = Boolean(uadata.mobile ?? uadata.high_entropy?.mobile);
|
|
38
|
+
let os;
|
|
39
|
+
if (chMobile || /android/i.test(chPlatform) || /android/i.test(ua))
|
|
40
|
+
os = "android";
|
|
41
|
+
else if (/windows|win/i.test(chPlatform) || /win/i.test(platform) || /Windows/i.test(ua))
|
|
42
|
+
os = "windows";
|
|
43
|
+
else if (/macos|mac/i.test(chPlatform) || /mac/i.test(platform) || /Mac OS X/i.test(ua))
|
|
44
|
+
os = "macos";
|
|
45
|
+
else if (/linux|chrome ?os|x11/i.test(chPlatform) || /linux|x11/i.test(platform) || /Linux/i.test(ua))
|
|
46
|
+
os = "linux";
|
|
47
|
+
else
|
|
48
|
+
return null; // unclassifiable: better to skip than to mislabel and mis-serve it
|
|
49
|
+
// Version is OPTIONAL by design. Hardware-only imports (e.g. converted chrome-fingerprints
|
|
50
|
+
// records) carry no browser version and inherit the engine's — see profilelib's browser_major.
|
|
51
|
+
const m = ua.match(/Chrome\/(\d+)/);
|
|
52
|
+
const browserMajor = m ? Number(m[1]) : null;
|
|
53
|
+
const renderer = debug.UNMASKED_RENDERER_WEBGL ?? null;
|
|
54
|
+
// The launch cost, measured the same way fingerprint.ts encodes it — so an entry that would
|
|
55
|
+
// blow the argv limit is excluded at selection time rather than failing at spawn.
|
|
56
|
+
const encodedSize = gzipSync(Buffer.from(JSON.stringify(profile)), { level: 9 }).toString("base64").length;
|
|
57
|
+
return {
|
|
58
|
+
id,
|
|
59
|
+
os_family: os,
|
|
60
|
+
browser_major: browserMajor,
|
|
61
|
+
gpu_vendor: gpuVendorClass(renderer),
|
|
62
|
+
renderer: renderer ?? undefined,
|
|
63
|
+
screen_width: screen.width,
|
|
64
|
+
screen_height: screen.height,
|
|
65
|
+
device_pixel_ratio: screen.device_pixel_ratio,
|
|
66
|
+
hardware_concurrency: profile.hardware_concurrency,
|
|
67
|
+
device_memory: profile.device_memory,
|
|
68
|
+
encoded_size: encodedSize,
|
|
69
|
+
captured_at: typeof meta.captured_at === "string" ? meta.captured_at : undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Index every `*.json` in a directory.
|
|
74
|
+
*
|
|
75
|
+
* Reads each file once. For 10k profiles this is a few seconds and a lot of I/O, so callers
|
|
76
|
+
* should cache the result (`writeIndex`) rather than re-scanning per launch.
|
|
77
|
+
*/
|
|
78
|
+
export function importDirectory(dir) {
|
|
79
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
80
|
+
throw new Error(`profile source is not a directory: ${dir}`);
|
|
81
|
+
}
|
|
82
|
+
const index = [];
|
|
83
|
+
const skipped = [];
|
|
84
|
+
for (const f of readdirSync(dir)) {
|
|
85
|
+
if (!f.endsWith(".json"))
|
|
86
|
+
continue;
|
|
87
|
+
const path = join(dir, f);
|
|
88
|
+
let parsed;
|
|
89
|
+
try {
|
|
90
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
skipped.push({ file: f, reason: `unreadable JSON: ${String(e).slice(0, 80)}` });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// A clearcote-profile always has navigator; without it this is some other kind of file.
|
|
97
|
+
// NOTE the check is on the SECTION, not on any field inside it — a converted
|
|
98
|
+
// chrome-fingerprints record has `navigator` with null user_agent/platform and is perfectly
|
|
99
|
+
// usable via uadata.
|
|
100
|
+
if (!parsed || typeof parsed !== "object" || !parsed.navigator) {
|
|
101
|
+
skipped.push({ file: f, reason: "not a clearcote-profile (no navigator)" });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const entry = indexEntryFromProfile(f.replace(/\.json$/, ""), parsed);
|
|
105
|
+
if (!entry) {
|
|
106
|
+
skipped.push({ file: f, reason: "could not classify os_family" });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
index.push(entry);
|
|
110
|
+
}
|
|
111
|
+
return { index, skipped };
|
|
112
|
+
}
|
|
113
|
+
/** Load one profile from an imported directory, by the id `importDirectory` assigned it. */
|
|
114
|
+
export function loadImportedProfile(dir, id) {
|
|
115
|
+
// The id is a filename stem this module produced, but it can reach here from a cached index,
|
|
116
|
+
// so treat it as untrusted: reject anything that could escape the source directory.
|
|
117
|
+
if (id.includes("/") || id.includes("\\") || id.includes("..")) {
|
|
118
|
+
throw new Error(`invalid profile id '${id}'`);
|
|
119
|
+
}
|
|
120
|
+
const path = join(dir, `${id}.json`);
|
|
121
|
+
if (!existsSync(path))
|
|
122
|
+
throw new Error(`profile '${id}' not found in ${dir}`);
|
|
123
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=profileimport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profileimport.js","sourceRoot":"","sources":["../src/profileimport.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,EAAE;AACF,gGAAgG;AAChG,+FAA+F;AAC/F,gBAAgB;AAChB,EAAE;AACF,4EAA4E;AAC5E,iGAAiG;AACjG,yFAAyF;AACzF,oFAAoF;AACpF,qEAAqE;AACrE,qDAAqD;AACrD,EAAE;AACF,8FAA8F;AAC9F,6FAA6F;AAC7F,iGAAiG;AAEjG,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,cAAc,EAA0B,MAAM,iBAAiB,CAAC;AAEzE,iGAAiG;AACjG,MAAM,UAAU,qBAAqB,CACnC,EAAU,EACV,OAAgC;IAEhC,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAA4B,CAAC;IACjE,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAA2B,CAAC;IAChE,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAA4B,CAAC;IAC/D,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAA4B,CAAC;IAC5D,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAA2B,CAAC;IAC1D,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAA4B,CAAC;IAE7D,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;IAC5C,4FAA4F;IAC5F,qFAAqF;IACrF,8FAA8F;IAC9F,6FAA6F;IAC7F,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAA4B,CAAC;IAC7D,MAAM,UAAU,GAAG,MAAM,CACvB,MAAM,CAAC,QAAQ,IAAI,CAAE,MAAM,CAAC,YAAoD,EAAE,QAAQ,IAAI,EAAE,CAAC,CAClG,CAAC;IACF,MAAM,QAAQ,GAAG,OAAO,CACtB,MAAM,CAAC,MAAM,IAAK,MAAM,CAAC,YAAoD,EAAE,MAAM,CACtF,CAAC;IAEF,IAAI,EAAU,CAAC;IACf,IAAI,QAAQ,IAAI,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,EAAE,GAAG,SAAS,CAAC;SAC9E,IAAI,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,EAAE,GAAG,SAAS,CAAC;SACpG,IAAI,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,EAAE,GAAG,OAAO,CAAC;SACjG,IAAI,uBAAuB,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,EAAE,GAAG,OAAO,CAAC;;QAC/G,OAAO,IAAI,CAAC,CAAC,mEAAmE;IAErF,2FAA2F;IAC3F,+FAA+F;IAC/F,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IACpC,MAAM,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAE7C,MAAM,QAAQ,GAAG,KAAK,CAAC,uBAAuB,IAAI,IAAI,CAAC;IAEvD,4FAA4F;IAC5F,kFAAkF;IAClF,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;IAE3G,OAAO;QACL,EAAE;QACF,SAAS,EAAE,EAAE;QACb,aAAa,EAAE,YAAY;QAC3B,UAAU,EAAE,cAAc,CAAC,QAAQ,CAAC;QACpC,QAAQ,EAAE,QAAQ,IAAI,SAAS;QAC/B,YAAY,EAAE,MAAM,CAAC,KAAK;QAC1B,aAAa,EAAE,MAAM,CAAC,MAAM;QAC5B,kBAAkB,EAAE,MAAM,CAAC,kBAAkB;QAC7C,oBAAoB,EAAE,OAAO,CAAC,oBAA0C;QACxE,aAAa,EAAE,OAAO,CAAC,aAAmC;QAC1D,YAAY,EAAE,WAAW;QACzB,WAAW,EAAE,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;KACjF,CAAC;AACJ,CAAC;AASD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,sCAAsC,GAAG,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,MAAM,KAAK,GAAwB,EAAE,CAAC;IACtC,MAAM,OAAO,GAAuC,EAAE,CAAC;IAEvD,KAAK,MAAM,CAAC,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;QACjC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,SAAS;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QAC1B,IAAI,MAA+B,CAAC;QACpC,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAA4B,CAAC;QAC7E,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,oBAAoB,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;YAChF,SAAS;QACX,CAAC;QACD,wFAAwF;QACxF,6EAA6E;QAC7E,4FAA4F;QAC5F,qBAAqB;QACrB,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YAC/D,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,wCAAwC,EAAE,CAAC,CAAC;YAC5E,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAG,qBAAqB,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;QACtE,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,8BAA8B,EAAE,CAAC,CAAC;YAClE,SAAS;QACX,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAC5B,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,mBAAmB,CAAC,GAAW,EAAE,EAAU;IACzD,6FAA6F;IAC7F,oFAAoF;IACpF,IAAI,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;IAChD,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,YAAY,EAAE,kBAAkB,GAAG,EAAE,CAAC,CAAC;IAC9E,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAA4B,CAAC;AAC3E,CAAC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/** One row of the profile index. Deliberately slim: selection never touches a blob until it has
|
|
2
|
+
* picked a winner, so the index stays cheap to hold in memory and to ship. */
|
|
3
|
+
export interface ProfileIndexEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
os_family: "windows" | "macos" | "linux" | "android" | (string & {});
|
|
6
|
+
os_major?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Chromium major of the capture, HARD-filtered against the engine's own major.
|
|
9
|
+
*
|
|
10
|
+
* `null`/absent means VERSION-AGNOSTIC and matches any engine. That is not a loophole — it is
|
|
11
|
+
* the correct description of a hardware-only profile. Imports from the chrome-fingerprints
|
|
12
|
+
* dataset deliberately drop the browser version (its records are Chrome ~114/115) and carry
|
|
13
|
+
* only version-independent identity: GPU, screen, fonts, voices, audio, CPU/memory, keyboard.
|
|
14
|
+
* Such a profile inherits the running engine's version, so it cannot contradict it.
|
|
15
|
+
*
|
|
16
|
+
* A profile that DOES pin a version and gets it wrong is a real contradiction — UA says 138
|
|
17
|
+
* while the engine behaves like 150 — which is why a mismatched number is still rejected.
|
|
18
|
+
*/
|
|
19
|
+
browser_major?: number | null;
|
|
20
|
+
/** Coarse vendor class parsed from the renderer string: nvidia | amd | intel | apple | other. */
|
|
21
|
+
gpu_vendor: string;
|
|
22
|
+
/** Rough capability tier (0 = integrated, 1 = mainstream, 2 = high-end). */
|
|
23
|
+
gpu_tier?: number;
|
|
24
|
+
renderer?: string;
|
|
25
|
+
screen_width?: number;
|
|
26
|
+
screen_height?: number;
|
|
27
|
+
device_pixel_ratio?: number;
|
|
28
|
+
hardware_concurrency?: number;
|
|
29
|
+
device_memory?: number;
|
|
30
|
+
/** gzip+base64 length of the encoded profile — used to keep the launch under the argv limit. */
|
|
31
|
+
encoded_size?: number;
|
|
32
|
+
/** ISO date of capture; newer is mildly preferred. */
|
|
33
|
+
captured_at?: string;
|
|
34
|
+
}
|
|
35
|
+
/** What we know about the machine actually doing the rendering. */
|
|
36
|
+
export interface HostFacts {
|
|
37
|
+
os_family: string;
|
|
38
|
+
browser_major: number;
|
|
39
|
+
gpu_vendor?: string;
|
|
40
|
+
gpu_tier?: number;
|
|
41
|
+
screen_width?: number;
|
|
42
|
+
screen_height?: number;
|
|
43
|
+
device_pixel_ratio?: number;
|
|
44
|
+
hardware_concurrency?: number;
|
|
45
|
+
device_memory?: number;
|
|
46
|
+
}
|
|
47
|
+
export type SelectMode = "best" | "rotate" | "random";
|
|
48
|
+
export interface SelectOptions {
|
|
49
|
+
/** `"rotate"` (default) sticky-per-key; `"best"` always top-scoring; `"random"` fresh each call. */
|
|
50
|
+
mode?: SelectMode;
|
|
51
|
+
/** Stickiness key for `"rotate"` — an account id, proxy session, or profile name. */
|
|
52
|
+
key?: string;
|
|
53
|
+
/** Candidate pool the mode picks from. Wide enough to avoid herding, narrow enough to stay coherent. */
|
|
54
|
+
topN?: number;
|
|
55
|
+
/** Override the target OS (defaults to the host's). */
|
|
56
|
+
os?: string;
|
|
57
|
+
/** Hard ceiling on encoded size; entries above it cannot be launched on Windows. */
|
|
58
|
+
maxEncodedSize?: number;
|
|
59
|
+
}
|
|
60
|
+
/** Windows caps a process command line at 32767 chars. The profile rides on argv as
|
|
61
|
+
* `--fingerprint-profile=<gzip+base64>`, and the rest of the switches need room too, so keep a
|
|
62
|
+
* wide margin. Exceeding this is not a soft failure — Chromium refuses to spawn and Playwright
|
|
63
|
+
* surfaces it as an opaque `spawn UNKNOWN`, which is why this is enforced at selection time. */
|
|
64
|
+
export declare const DEFAULT_MAX_ENCODED = 24000;
|
|
65
|
+
/** Coarse GPU vendor class from a renderer string (works for ANGLE and raw GL strings). */
|
|
66
|
+
export declare function gpuVendorClass(renderer: string | undefined | null): string;
|
|
67
|
+
/**
|
|
68
|
+
* Score a candidate against the host. Higher is better; the maximum is 1.
|
|
69
|
+
*
|
|
70
|
+
* Weights encode what a detector can actually catch, in descending order of how cheaply it can
|
|
71
|
+
* be caught:
|
|
72
|
+
* - GPU class (0.45): string-vs-render mismatch is a single-call contradiction.
|
|
73
|
+
* - Screen (0.25): a persona whose screen cannot contain the real window is the documented
|
|
74
|
+
* "reliable block trigger"; a profile *larger* than the host display is the dangerous
|
|
75
|
+
* direction, so it is penalised harder than a smaller one.
|
|
76
|
+
* - DPR (0.10): interacts with screen and with CSS pixel math.
|
|
77
|
+
* - Cores/memory (0.10): weakly checkable against measured worker parallelism.
|
|
78
|
+
* - Freshness (0.10): a recent capture is likelier to match current Chrome behaviour.
|
|
79
|
+
*/
|
|
80
|
+
export declare function scoreProfile(entry: ProfileIndexEntry, host: HostFacts): number;
|
|
81
|
+
/** Hard filters. A candidate failing any of these is not merely low-scoring — it is incoherent,
|
|
82
|
+
* and no weight should be able to promote it back into the pool. */
|
|
83
|
+
export declare function eligible(entry: ProfileIndexEntry, host: HostFacts, opts?: SelectOptions): boolean;
|
|
84
|
+
export interface Selection {
|
|
85
|
+
entry: ProfileIndexEntry;
|
|
86
|
+
score: number;
|
|
87
|
+
/** How many candidates survived the hard filters — useful for spotting an over-narrow index. */
|
|
88
|
+
poolSize: number;
|
|
89
|
+
mode: SelectMode;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Pick a profile for this host.
|
|
93
|
+
*
|
|
94
|
+
* Throws when nothing is eligible rather than silently falling back to an incoherent profile —
|
|
95
|
+
* a wrong persona is worse than no persona, because it converts a clean browser into a
|
|
96
|
+
* contradictory one.
|
|
97
|
+
*/
|
|
98
|
+
export declare function selectProfile(index: ProfileIndexEntry[], host: HostFacts, opts?: SelectOptions): Selection;
|
|
99
|
+
export declare function defaultStickyKey(): string;
|
|
100
|
+
//# sourceMappingURL=profilelib.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profilelib.d.ts","sourceRoot":"","sources":["../src/profilelib.ts"],"names":[],"mappings":"AAyBA;+EAC+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iGAAiG;IACjG,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,mEAAmE;AACnE,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC5B,oGAAoG;IACpG,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,qFAAqF;IACrF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wGAAwG;IACxG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,oFAAoF;IACpF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;iGAGiG;AACjG,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAIzC,2FAA2F;AAC3F,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAU1E;AASD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CA2C9E;AAED;qEACqE;AACrE,wBAAgB,QAAQ,CACtB,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,SAAS,EACf,IAAI,GAAE,aAAkB,GACvB,OAAO,CAWT;AAOD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,iBAAiB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,gGAAgG;IAChG,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,iBAAiB,EAAE,EAC1B,IAAI,EAAE,SAAS,EACf,IAAI,GAAE,aAAkB,GACvB,SAAS,CAmCX;AAID,wBAAgB,gBAAgB,IAAI,MAAM,CAKzC"}
|