clearcote 0.20.0 → 0.22.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/README.md +1 -1
- package/dist/humanize.d.ts.map +1 -1
- package/dist/humanize.js +307 -95
- 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 +55 -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 +43 -0
- package/dist/profileauto.d.ts.map +1 -0
- package/dist/profileauto.js +150 -0
- package/dist/profileauto.js.map +1 -0
- package/dist/profileimport.d.ts +22 -0
- package/dist/profileimport.d.ts.map +1 -0
- package/dist/profileimport.js +116 -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 +29 -0
- package/dist/profilesource.d.ts.map +1 -0
- package/dist/profilesource.js +180 -0
- package/dist/profilesource.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
/** Where an imported (e.g. converted chrome-fingerprints) directory is looked for by default. */
|
|
22
|
+
export const DEFAULT_LOCAL_DIR = process.env.CLEARCOTE_PROFILE_SOURCE ?? join(homedir(), ".clearcote", "profiles", "imported");
|
|
23
|
+
function localCachePath() {
|
|
24
|
+
const d = join(PROFILE_DIR, "library");
|
|
25
|
+
if (!existsSync(d))
|
|
26
|
+
mkdirSync(d, { recursive: true });
|
|
27
|
+
return join(d, "local-index.json");
|
|
28
|
+
}
|
|
29
|
+
export function loadLocalIndex(dir) {
|
|
30
|
+
const mtimeMs = statSync(dir).mtimeMs;
|
|
31
|
+
const path = localCachePath();
|
|
32
|
+
if (existsSync(path)) {
|
|
33
|
+
try {
|
|
34
|
+
const c = JSON.parse(readFileSync(path, "utf8"));
|
|
35
|
+
if (c.dir === dir && c.mtimeMs === mtimeMs)
|
|
36
|
+
return c.index;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* fall through and re-index */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const { index } = importDirectory(dir);
|
|
43
|
+
try {
|
|
44
|
+
writeFileSync(path, JSON.stringify({ dir, mtimeMs, index }));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* a cache write failure must never fail a launch */
|
|
48
|
+
}
|
|
49
|
+
return index;
|
|
50
|
+
}
|
|
51
|
+
function note(quiet, msg) {
|
|
52
|
+
if (!quiet)
|
|
53
|
+
process.stderr.write(`[clearcote] [profile] ${msg}\n`);
|
|
54
|
+
}
|
|
55
|
+
/** The optional-backup tip is worth saying once, not on every launch. */
|
|
56
|
+
let hintedThisProcess = false;
|
|
57
|
+
/** Test seam: reset the once-per-process tip. */
|
|
58
|
+
export function _resetHint() {
|
|
59
|
+
hintedThisProcess = false;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* How to populate a local backup directory. Printed when the service is unavailable and no local
|
|
63
|
+
* directory exists — the moment the caller actually needs it.
|
|
64
|
+
*
|
|
65
|
+
* OPTIONAL, AND NOT SOMETHING WE SHIP. Clearcote redistributes no third-party fingerprints; the
|
|
66
|
+
* dataset is fetched by the user from its own repository under its own terms, and we only read
|
|
67
|
+
* the converted format. Any directory of clearcote-profile JSON works just as well, which is
|
|
68
|
+
* why this is phrased as one example rather than the required path.
|
|
69
|
+
*/
|
|
70
|
+
export function localSetupHint(dir) {
|
|
71
|
+
return [
|
|
72
|
+
`A local profile directory is optional, and gives "auto" a backup when the service is`,
|
|
73
|
+
`unavailable. Any folder of clearcote-profile JSON works — your own captures, or a`,
|
|
74
|
+
`third-party dataset you convert yourself. For example, using the open chrome-fingerprints`,
|
|
75
|
+
`dataset (fetched by you, from its own repo, under its own licence):`,
|
|
76
|
+
``,
|
|
77
|
+
` pip install chrome-fingerprints`,
|
|
78
|
+
` python tools/fingerprint-collect/convert_dataset.py --out "${dir}" --count 500`,
|
|
79
|
+
``,
|
|
80
|
+
`Or capture your own with tools/fingerprint-collect (collect.html), or point`,
|
|
81
|
+
`CLEARCOTE_PROFILE_SOURCE at a directory you already have.`,
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
84
|
+
/** Resolve a persona from the local imported directory. */
|
|
85
|
+
export function resolveLocal(host, opts = {}) {
|
|
86
|
+
const dir = opts.localDir ?? DEFAULT_LOCAL_DIR;
|
|
87
|
+
if (!existsSync(dir)) {
|
|
88
|
+
throw new Error(`no local profile directory at ${dir}\n\n${localSetupHint(dir)}`);
|
|
89
|
+
}
|
|
90
|
+
const index = loadLocalIndex(dir);
|
|
91
|
+
if (index.length === 0) {
|
|
92
|
+
throw new Error(`local profile directory ${dir} contains no usable clearcote-profile JSON`);
|
|
93
|
+
}
|
|
94
|
+
const selection = selectProfile(index, host, opts);
|
|
95
|
+
return { profile: loadImportedProfile(dir, selection.entry.id), selection, source: "local" };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolve `profile: "auto"`: service first, local directory as backup.
|
|
99
|
+
*
|
|
100
|
+
* Any service failure falls back — including 401 (no license) and 429 (rate limited or daily
|
|
101
|
+
* distinct budget exhausted), because in all of those a local persona is better than none and
|
|
102
|
+
* far better than a hard launch failure.
|
|
103
|
+
*/
|
|
104
|
+
export async function resolveAuto(host, opts = {}) {
|
|
105
|
+
if (opts.preferLocal) {
|
|
106
|
+
const r = resolveLocal(host, opts);
|
|
107
|
+
note(opts.quiet, `using local profile ${r.selection.entry.id} (preferLocal)`);
|
|
108
|
+
return r;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const index = await fetchIndex(host, opts);
|
|
112
|
+
if (index.length === 0)
|
|
113
|
+
throw new Error("service returned no candidates for this host");
|
|
114
|
+
const selection = selectProfile(index, host, opts);
|
|
115
|
+
const profile = await fetchProfile(selection.entry.id, opts);
|
|
116
|
+
note(opts.quiet, `using service profile ${selection.entry.id} ` +
|
|
117
|
+
`(score ${selection.score.toFixed(2)}, pool ${selection.poolSize}, mode ${selection.mode})`);
|
|
118
|
+
// Mention the optional backup ONCE per process, and only while there is none configured.
|
|
119
|
+
// The useful moment to learn about a fallback is before you need it — but repeating it on
|
|
120
|
+
// every launch would be noise, and nagging someone who already set one up would be wrong.
|
|
121
|
+
if (!hintedThisProcess && !existsSync(opts.localDir ?? DEFAULT_LOCAL_DIR)) {
|
|
122
|
+
hintedThisProcess = true;
|
|
123
|
+
note(opts.quiet, `tip: no local backup directory configured. If the service is ever rate limited or
|
|
124
|
+
unreachable, "auto" fails rather than falling back. Setting one up is optional:
|
|
125
|
+
${localSetupHint(opts.localDir ?? DEFAULT_LOCAL_DIR)}`);
|
|
126
|
+
}
|
|
127
|
+
return { profile, selection, source: "service" };
|
|
128
|
+
}
|
|
129
|
+
catch (serviceErr) {
|
|
130
|
+
const reason = String(serviceErr instanceof Error ? serviceErr.message : serviceErr).slice(0, 200);
|
|
131
|
+
try {
|
|
132
|
+
const r = resolveLocal(host, opts);
|
|
133
|
+
// Both halves of the story: what failed, and what is being used instead.
|
|
134
|
+
note(opts.quiet, `service unavailable (${reason}) — falling back to local profile ${r.selection.entry.id}`);
|
|
135
|
+
return { ...r, fallbackReason: reason };
|
|
136
|
+
}
|
|
137
|
+
catch (localErr) {
|
|
138
|
+
const dir = opts.localDir ?? DEFAULT_LOCAL_DIR;
|
|
139
|
+
const noLocalDir = !existsSync(dir);
|
|
140
|
+
const lr = String(localErr instanceof Error ? localErr.message : localErr);
|
|
141
|
+
// Report BOTH failures. Surfacing only the local one would send the caller to go and fix
|
|
142
|
+
// a directory when the real problem was an expired license.
|
|
143
|
+
throw new Error(`profile: "auto" could not resolve a persona.\n` +
|
|
144
|
+
` service: ${reason}\n` +
|
|
145
|
+
` local: ${noLocalDir ? `no directory at ${dir}` : lr}\n\n` +
|
|
146
|
+
(noLocalDir ? localSetupHint(dir) : ""));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# 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,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,CA4C1B;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,CA8BzD;AAED,4FAA4F;AAC5F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASpF"}
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
// OS family from navigator.platform first (stable across UA reduction), UA as a fallback.
|
|
32
|
+
let os;
|
|
33
|
+
if (/win/i.test(platform) || /Windows/i.test(ua))
|
|
34
|
+
os = "windows";
|
|
35
|
+
else if (/mac/i.test(platform) || /Mac OS X/i.test(ua))
|
|
36
|
+
os = "macos";
|
|
37
|
+
else if (/linux|x11/i.test(platform) || /Linux/i.test(ua))
|
|
38
|
+
os = "linux";
|
|
39
|
+
else if (/android/i.test(ua))
|
|
40
|
+
os = "android";
|
|
41
|
+
else
|
|
42
|
+
return null; // unclassifiable: better to skip than to mislabel and mis-serve it
|
|
43
|
+
// Version is OPTIONAL by design. Hardware-only imports (e.g. converted chrome-fingerprints
|
|
44
|
+
// records) carry no browser version and inherit the engine's — see profilelib's browser_major.
|
|
45
|
+
const m = ua.match(/Chrome\/(\d+)/);
|
|
46
|
+
const browserMajor = m ? Number(m[1]) : null;
|
|
47
|
+
const renderer = debug.UNMASKED_RENDERER_WEBGL ?? null;
|
|
48
|
+
// The launch cost, measured the same way fingerprint.ts encodes it — so an entry that would
|
|
49
|
+
// blow the argv limit is excluded at selection time rather than failing at spawn.
|
|
50
|
+
const encodedSize = gzipSync(Buffer.from(JSON.stringify(profile)), { level: 9 }).toString("base64").length;
|
|
51
|
+
return {
|
|
52
|
+
id,
|
|
53
|
+
os_family: os,
|
|
54
|
+
browser_major: browserMajor,
|
|
55
|
+
gpu_vendor: gpuVendorClass(renderer),
|
|
56
|
+
renderer: renderer ?? undefined,
|
|
57
|
+
screen_width: screen.width,
|
|
58
|
+
screen_height: screen.height,
|
|
59
|
+
device_pixel_ratio: screen.device_pixel_ratio,
|
|
60
|
+
hardware_concurrency: profile.hardware_concurrency,
|
|
61
|
+
device_memory: profile.device_memory,
|
|
62
|
+
encoded_size: encodedSize,
|
|
63
|
+
captured_at: typeof meta.captured_at === "string" ? meta.captured_at : undefined,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Index every `*.json` in a directory.
|
|
68
|
+
*
|
|
69
|
+
* Reads each file once. For 10k profiles this is a few seconds and a lot of I/O, so callers
|
|
70
|
+
* should cache the result (`writeIndex`) rather than re-scanning per launch.
|
|
71
|
+
*/
|
|
72
|
+
export function importDirectory(dir) {
|
|
73
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
74
|
+
throw new Error(`profile source is not a directory: ${dir}`);
|
|
75
|
+
}
|
|
76
|
+
const index = [];
|
|
77
|
+
const skipped = [];
|
|
78
|
+
for (const f of readdirSync(dir)) {
|
|
79
|
+
if (!f.endsWith(".json"))
|
|
80
|
+
continue;
|
|
81
|
+
const path = join(dir, f);
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
skipped.push({ file: f, reason: `unreadable JSON: ${String(e).slice(0, 80)}` });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// A clearcote-profile always has navigator; without it this is some other kind of file.
|
|
91
|
+
if (!parsed || typeof parsed !== "object" || !parsed.navigator) {
|
|
92
|
+
skipped.push({ file: f, reason: "not a clearcote-profile (no navigator)" });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const entry = indexEntryFromProfile(f.replace(/\.json$/, ""), parsed);
|
|
96
|
+
if (!entry) {
|
|
97
|
+
skipped.push({ file: f, reason: "could not classify os_family" });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
index.push(entry);
|
|
101
|
+
}
|
|
102
|
+
return { index, skipped };
|
|
103
|
+
}
|
|
104
|
+
/** Load one profile from an imported directory, by the id `importDirectory` assigned it. */
|
|
105
|
+
export function loadImportedProfile(dir, id) {
|
|
106
|
+
// The id is a filename stem this module produced, but it can reach here from a cached index,
|
|
107
|
+
// so treat it as untrusted: reject anything that could escape the source directory.
|
|
108
|
+
if (id.includes("/") || id.includes("\\") || id.includes("..")) {
|
|
109
|
+
throw new Error(`invalid profile id '${id}'`);
|
|
110
|
+
}
|
|
111
|
+
const path = join(dir, `${id}.json`);
|
|
112
|
+
if (!existsSync(path))
|
|
113
|
+
throw new Error(`profile '${id}' not found in ${dir}`);
|
|
114
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=profileimport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profileimport.js","sourceRoot":"","sources":["../src/profileimport.ts"],"names":[],"mappings":"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"}
|
|
@@ -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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Profile library: index, host-coherence scoring, and selection.
|
|
2
|
+
//
|
|
3
|
+
// The persona path this serves is `--fingerprint-profile` WITHOUT `--fingerprint`. That
|
|
4
|
+
// distinction is load-bearing and is why this module exists: measured against a strict
|
|
5
|
+
// anti-bot device check, a seed persona failed 13/13 while a profile-only persona passed. With no
|
|
6
|
+
// seed the farbling machinery never engages, so canvas/WebGL/audio readbacks come back
|
|
7
|
+
// byte-identical to an unmodified browser — the profile is static value substitution over a
|
|
8
|
+
// real, coherent capture rather than perturbation of this machine's.
|
|
9
|
+
//
|
|
10
|
+
// SELECTION IS ABOUT COHERENCE WITH *THIS HOST*, NOT ABOUT MATCHING AN OS STRING.
|
|
11
|
+
// A profile claiming an RTX 3080 on an Intel UHD host says NVIDIA in the strings and paints
|
|
12
|
+
// Intel pixels — a contradiction a detector reads in one call. So the GPU term dominates the
|
|
13
|
+
// score, and `browser_major` is a HARD filter: a Chrome-138 capture on a 150 engine reports
|
|
14
|
+
// UA 138 while the engine behaves like 150, which is self-contradictory in a way no scoring
|
|
15
|
+
// weight should be able to outvote.
|
|
16
|
+
//
|
|
17
|
+
// ROTATION IS PER-IDENTITY, NOT PER-LAUNCH. Always serving the single best profile makes every
|
|
18
|
+
// customer on similar hardware converge on one identity, and that shared fingerprint becomes its
|
|
19
|
+
// own signal. But re-rolling every launch is worse: an account whose device changes on every
|
|
20
|
+
// visit is a harder tell than a slightly-suboptimal stable one. So the default ("rotate") is
|
|
21
|
+
// sticky-per-key — same key always resolves to the same profile, different keys spread across
|
|
22
|
+
// the top-N.
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
/** Windows caps a process command line at 32767 chars. The profile rides on argv as
|
|
25
|
+
* `--fingerprint-profile=<gzip+base64>`, and the rest of the switches need room too, so keep a
|
|
26
|
+
* wide margin. Exceeding this is not a soft failure — Chromium refuses to spawn and Playwright
|
|
27
|
+
* surfaces it as an opaque `spawn UNKNOWN`, which is why this is enforced at selection time. */
|
|
28
|
+
export const DEFAULT_MAX_ENCODED = 24000;
|
|
29
|
+
const DEFAULT_TOP_N = 25;
|
|
30
|
+
/** Coarse GPU vendor class from a renderer string (works for ANGLE and raw GL strings). */
|
|
31
|
+
export function gpuVendorClass(renderer) {
|
|
32
|
+
const r = (renderer ?? "").toLowerCase();
|
|
33
|
+
if (!r)
|
|
34
|
+
return "unknown";
|
|
35
|
+
if (r.includes("nvidia") || r.includes("geforce") || r.includes("quadro"))
|
|
36
|
+
return "nvidia";
|
|
37
|
+
if (r.includes("amd") || r.includes("radeon") || r.includes("ati "))
|
|
38
|
+
return "amd";
|
|
39
|
+
if (r.includes("intel"))
|
|
40
|
+
return "intel";
|
|
41
|
+
if (r.includes("apple"))
|
|
42
|
+
return "apple";
|
|
43
|
+
if (r.includes("swiftshader") || r.includes("llvmpipe") || r.includes("software"))
|
|
44
|
+
return "software";
|
|
45
|
+
if (r.includes("mali") || r.includes("adreno") || r.includes("powervr"))
|
|
46
|
+
return "mobile";
|
|
47
|
+
return "other";
|
|
48
|
+
}
|
|
49
|
+
/** Ratio-based closeness in [0,1] — 1 when equal, decaying as the values diverge. */
|
|
50
|
+
function closeness(a, b) {
|
|
51
|
+
if (a === undefined || b === undefined || a <= 0 || b <= 0)
|
|
52
|
+
return 0.5; // unknown: neutral
|
|
53
|
+
const hi = Math.max(a, b), lo = Math.min(a, b);
|
|
54
|
+
return lo / hi;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Score a candidate against the host. Higher is better; the maximum is 1.
|
|
58
|
+
*
|
|
59
|
+
* Weights encode what a detector can actually catch, in descending order of how cheaply it can
|
|
60
|
+
* be caught:
|
|
61
|
+
* - GPU class (0.45): string-vs-render mismatch is a single-call contradiction.
|
|
62
|
+
* - Screen (0.25): a persona whose screen cannot contain the real window is the documented
|
|
63
|
+
* "reliable block trigger"; a profile *larger* than the host display is the dangerous
|
|
64
|
+
* direction, so it is penalised harder than a smaller one.
|
|
65
|
+
* - DPR (0.10): interacts with screen and with CSS pixel math.
|
|
66
|
+
* - Cores/memory (0.10): weakly checkable against measured worker parallelism.
|
|
67
|
+
* - Freshness (0.10): a recent capture is likelier to match current Chrome behaviour.
|
|
68
|
+
*/
|
|
69
|
+
export function scoreProfile(entry, host) {
|
|
70
|
+
let score = 0;
|
|
71
|
+
// -- GPU: exact vendor match dominates. A software renderer on either side is disqualifying
|
|
72
|
+
// in practice (the pixels can never match a claimed discrete GPU), so it scores 0.
|
|
73
|
+
const hv = host.gpu_vendor ?? "unknown";
|
|
74
|
+
const ev = entry.gpu_vendor ?? "unknown";
|
|
75
|
+
let gpu;
|
|
76
|
+
if (ev === "software" || hv === "software")
|
|
77
|
+
gpu = 0;
|
|
78
|
+
else if (ev === hv)
|
|
79
|
+
gpu = 1;
|
|
80
|
+
else if (hv === "unknown" || ev === "unknown")
|
|
81
|
+
gpu = 0.5;
|
|
82
|
+
else
|
|
83
|
+
gpu = 0.15; // different real vendor: strings and pixels disagree
|
|
84
|
+
// tier proximity refines a same-vendor match
|
|
85
|
+
if (gpu === 1 && entry.gpu_tier !== undefined && host.gpu_tier !== undefined) {
|
|
86
|
+
gpu = 1 - Math.min(1, Math.abs(entry.gpu_tier - host.gpu_tier) / 3) * 0.4;
|
|
87
|
+
}
|
|
88
|
+
score += 0.45 * gpu;
|
|
89
|
+
// -- Screen. Asymmetric on purpose: claiming a LARGER display than the host has is the
|
|
90
|
+
// contradiction that gets caught (the window cannot be contained), so penalise it more.
|
|
91
|
+
const ew = entry.screen_width, eh = entry.screen_height;
|
|
92
|
+
const hw = host.screen_width, hh = host.screen_height;
|
|
93
|
+
let screen = 0.5;
|
|
94
|
+
if (ew && eh && hw && hh) {
|
|
95
|
+
const fits = ew <= hw && eh <= hh;
|
|
96
|
+
const areaRatio = closeness(ew * eh, hw * hh);
|
|
97
|
+
screen = fits ? 0.7 + 0.3 * areaRatio : 0.25 * areaRatio;
|
|
98
|
+
}
|
|
99
|
+
score += 0.25 * screen;
|
|
100
|
+
score += 0.10 * closeness(entry.device_pixel_ratio, host.device_pixel_ratio);
|
|
101
|
+
score += 0.05 * closeness(entry.hardware_concurrency, host.hardware_concurrency);
|
|
102
|
+
score += 0.05 * closeness(entry.device_memory, host.device_memory);
|
|
103
|
+
// -- Freshness: full credit inside 6 months, decaying to zero at ~2 years.
|
|
104
|
+
let fresh = 0.5;
|
|
105
|
+
if (entry.captured_at) {
|
|
106
|
+
const days = (Date.now() - Date.parse(entry.captured_at)) / 86_400_000;
|
|
107
|
+
fresh = Number.isFinite(days) ? Math.max(0, Math.min(1, 1 - (days - 180) / 550)) : 0.5;
|
|
108
|
+
}
|
|
109
|
+
score += 0.10 * fresh;
|
|
110
|
+
return score;
|
|
111
|
+
}
|
|
112
|
+
/** Hard filters. A candidate failing any of these is not merely low-scoring — it is incoherent,
|
|
113
|
+
* and no weight should be able to promote it back into the pool. */
|
|
114
|
+
export function eligible(entry, host, opts = {}) {
|
|
115
|
+
const os = (opts.os ?? host.os_family).toLowerCase();
|
|
116
|
+
if (entry.os_family.toLowerCase() !== os)
|
|
117
|
+
return false;
|
|
118
|
+
// A version-agnostic profile (no browser_major) carries hardware identity only and inherits
|
|
119
|
+
// the engine's version, so it cannot contradict it — those pass. A profile that PINS a
|
|
120
|
+
// version and gets it wrong is a genuine contradiction: the UA would claim 138 while the
|
|
121
|
+
// engine's observable behaviour is 150. Those are rejected however well they score.
|
|
122
|
+
if (entry.browser_major != null && entry.browser_major !== host.browser_major)
|
|
123
|
+
return false;
|
|
124
|
+
const max = opts.maxEncodedSize ?? DEFAULT_MAX_ENCODED;
|
|
125
|
+
if (entry.encoded_size !== undefined && entry.encoded_size > max)
|
|
126
|
+
return false;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
/** Stable 32-bit hash — used to map a stickiness key onto the candidate pool. */
|
|
130
|
+
function keyHash(key) {
|
|
131
|
+
return parseInt(createHash("sha256").update(key).digest("hex").slice(0, 8), 16);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Pick a profile for this host.
|
|
135
|
+
*
|
|
136
|
+
* Throws when nothing is eligible rather than silently falling back to an incoherent profile —
|
|
137
|
+
* a wrong persona is worse than no persona, because it converts a clean browser into a
|
|
138
|
+
* contradictory one.
|
|
139
|
+
*/
|
|
140
|
+
export function selectProfile(index, host, opts = {}) {
|
|
141
|
+
const mode = opts.mode ?? "rotate";
|
|
142
|
+
const candidates = index.filter((e) => eligible(e, host, opts));
|
|
143
|
+
if (candidates.length === 0) {
|
|
144
|
+
const os = opts.os ?? host.os_family;
|
|
145
|
+
throw new Error(`no profile matches host (os=${os}, chromium major=${host.browser_major}). ` +
|
|
146
|
+
`The index has ${index.length} entries; none passed the os/major/size filters. ` +
|
|
147
|
+
`Sync a newer index, or pass an explicit fingerprintProfile.`);
|
|
148
|
+
}
|
|
149
|
+
const scored = candidates
|
|
150
|
+
.map((entry) => ({ entry, score: scoreProfile(entry, host) }))
|
|
151
|
+
// tie-break by id so the ordering is deterministic across runs and machines
|
|
152
|
+
.sort((a, b) => b.score - a.score || (a.entry.id < b.entry.id ? -1 : 1));
|
|
153
|
+
const poolSize = scored.length;
|
|
154
|
+
const n = Math.max(1, Math.min(opts.topN ?? DEFAULT_TOP_N, poolSize));
|
|
155
|
+
const pool = scored.slice(0, n);
|
|
156
|
+
let chosen;
|
|
157
|
+
if (mode === "best") {
|
|
158
|
+
chosen = pool[0];
|
|
159
|
+
}
|
|
160
|
+
else if (mode === "random") {
|
|
161
|
+
chosen = pool[Math.floor(Math.random() * pool.length)];
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// "rotate": sticky per key. With no key the caller still gets a STABLE profile rather than a
|
|
165
|
+
// new one each launch — an identity whose device changes every visit is a worse tell than a
|
|
166
|
+
// slightly-suboptimal fixed one. Callers wanting spread should pass a key.
|
|
167
|
+
const key = opts.key ?? defaultStickyKey();
|
|
168
|
+
chosen = pool[keyHash(key) % pool.length];
|
|
169
|
+
}
|
|
170
|
+
return { entry: chosen.entry, score: chosen.score, poolSize, mode };
|
|
171
|
+
}
|
|
172
|
+
/** A stable per-machine key, so keyless `"rotate"` is consistent rather than random. */
|
|
173
|
+
let cachedStickyKey = null;
|
|
174
|
+
export function defaultStickyKey() {
|
|
175
|
+
if (cachedStickyKey)
|
|
176
|
+
return cachedStickyKey;
|
|
177
|
+
const bits = [process.env.CLEARCOTE_PROFILE_KEY, process.env.USERNAME ?? process.env.USER, process.platform];
|
|
178
|
+
cachedStickyKey = createHash("sha256").update(bits.filter(Boolean).join("|")).digest("hex").slice(0, 16);
|
|
179
|
+
return cachedStickyKey;
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=profilelib.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profilelib.js","sourceRoot":"","sources":["../src/profilelib.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type HostFacts, type ProfileIndexEntry, type SelectOptions, type Selection } from "./profilelib.js";
|
|
2
|
+
export interface ProfileSourceOptions extends SelectOptions {
|
|
3
|
+
licenseKey?: string;
|
|
4
|
+
apiBase?: string;
|
|
5
|
+
/** Skip the network entirely and use only what is already cached. */
|
|
6
|
+
offline?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Map a Node platform string onto the os_family values the corpus stores. */
|
|
9
|
+
export declare function hostOsFamily(): string;
|
|
10
|
+
export declare function readCachedHost(exe: string): HostFacts | null;
|
|
11
|
+
export declare function writeCachedHost(exe: string, facts: HostFacts): void;
|
|
12
|
+
/** Read the host's real GPU/display by rendering in the engine itself, with no persona applied. */
|
|
13
|
+
export declare function measureHost(launchFn: (opts: Record<string, unknown>) => Promise<{
|
|
14
|
+
newContext: () => Promise<{
|
|
15
|
+
newPage: () => Promise<unknown>;
|
|
16
|
+
}>;
|
|
17
|
+
close: () => Promise<void>;
|
|
18
|
+
}>, exe: string, browserMajor: number): Promise<HostFacts>;
|
|
19
|
+
/** Candidate index for a described host. Cached briefly so repeated launches do not re-ask. */
|
|
20
|
+
export declare function fetchIndex(host: HostFacts, opts?: ProfileSourceOptions): Promise<ProfileIndexEntry[]>;
|
|
21
|
+
/** Fetch one profile by id, preferring the on-disk copy — a repeat costs no distinct budget,
|
|
22
|
+
* but not spending a request at all is better still. */
|
|
23
|
+
export declare function fetchProfile(id: string, opts?: ProfileSourceOptions): Promise<Record<string, unknown>>;
|
|
24
|
+
/** Resolve a persona end to end: index -> score against this host -> fetch the winner. */
|
|
25
|
+
export declare function resolveAutoProfile(host: HostFacts, opts?: ProfileSourceOptions): Promise<{
|
|
26
|
+
profile: Record<string, unknown>;
|
|
27
|
+
selection: Selection;
|
|
28
|
+
}>;
|
|
29
|
+
//# sourceMappingURL=profilesource.d.ts.map
|