@koloss777/nokk 0.1.21-alpha
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 +79 -0
- package/bin/nokk.js +21 -0
- package/index.d.ts +47 -0
- package/index.js +157 -0
- package/install.js +74 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# nokk
|
|
2
|
+
|
|
3
|
+
**Undetectable headless-browser engine, one `npm install` away.**
|
|
4
|
+
|
|
5
|
+
`nokk` is a lightweight, Chrome-fingerprinted headless-browser engine (V8 + a
|
|
6
|
+
minimal DOM, no rendering) that speaks the Chrome DevTools Protocol. Install it,
|
|
7
|
+
call `launch()`, and connect your existing **Puppeteer** or **Playwright** script
|
|
8
|
+
over CDP — no Chromium download.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @koloss777/nokk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> **Alpha:** the prebuilt binary is fetched on install and is currently **Linux
|
|
15
|
+
> x64** only. macOS/Windows are on the roadmap.
|
|
16
|
+
|
|
17
|
+
## Puppeteer
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
const nokk = require("@koloss777/nokk");
|
|
21
|
+
const puppeteer = require("puppeteer");
|
|
22
|
+
|
|
23
|
+
(async () => {
|
|
24
|
+
const server = await nokk.launch({ rotateFingerprint: true });
|
|
25
|
+
const browser = await puppeteer.connect({ browserWSEndpoint: server.wsEndpoint });
|
|
26
|
+
const page = await browser.newPage();
|
|
27
|
+
await page.goto("https://example.com");
|
|
28
|
+
console.log(await page.title());
|
|
29
|
+
await browser.close();
|
|
30
|
+
await server.close();
|
|
31
|
+
})();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Playwright
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
const nokk = require("@koloss777/nokk");
|
|
38
|
+
const { chromium } = require("playwright");
|
|
39
|
+
|
|
40
|
+
(async () => {
|
|
41
|
+
const server = await nokk.launch();
|
|
42
|
+
const browser = await chromium.connectOverCDP(server.wsEndpoint);
|
|
43
|
+
const page = await browser.newPage();
|
|
44
|
+
await page.goto("https://example.com");
|
|
45
|
+
console.log(await page.title());
|
|
46
|
+
await browser.close();
|
|
47
|
+
await server.close();
|
|
48
|
+
})();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## `launch(options)`
|
|
52
|
+
|
|
53
|
+
Starts the CDP server on a free port and resolves to a `NokkServer`. Options
|
|
54
|
+
(all optional): `host`, `port`, `workers`, `maxContexts`, `proxy`,
|
|
55
|
+
`sessionStore`, `rotateFingerprint`, `geoipTimezone`, `allowTrackers`,
|
|
56
|
+
`chromeVersion`, `args`, `env`, `timeout`.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// Each browser context looks like a different machine, timezone matched to its proxy:
|
|
60
|
+
const server = await nokk.launch({ rotateFingerprint: true, geoipTimezone: true });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`server.wsEndpoint` → hand to `puppeteer.connect` / `chromium.connectOverCDP`.
|
|
64
|
+
`server.close()` stops it; it's also killed when the Node process exits.
|
|
65
|
+
|
|
66
|
+
## CLI
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx @koloss777/nokk --port 9222 --workers 4 # run the CDP server directly
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Point at a locally built binary during development with the `NOKK_BINARY`
|
|
73
|
+
environment variable.
|
|
74
|
+
|
|
75
|
+
## Links
|
|
76
|
+
|
|
77
|
+
- Source & docs: <https://github.com/koloss777/nokk>
|
|
78
|
+
|
|
79
|
+
Licensed under MIT OR Apache-2.0.
|
package/bin/nokk.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `npx nokk …` / the `nokk` bin — run the embedded binary, passing through args
|
|
3
|
+
// (e.g. `nokk --port 9222` starts the CDP server).
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const { spawn } = require("child_process");
|
|
7
|
+
const { binaryPath } = require("../index.js");
|
|
8
|
+
|
|
9
|
+
let bin;
|
|
10
|
+
try {
|
|
11
|
+
bin = binaryPath();
|
|
12
|
+
} catch (e) {
|
|
13
|
+
console.error("[nokk] " + e.message);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const child = spawn(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
18
|
+
child.on("exit", (code, signal) => {
|
|
19
|
+
if (signal) process.kill(process.pid, signal);
|
|
20
|
+
else process.exit(code == null ? 1 : code);
|
|
21
|
+
});
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface LaunchOptions {
|
|
2
|
+
/** Address to bind (default "127.0.0.1"). */
|
|
3
|
+
host?: string;
|
|
4
|
+
/** TCP port; 0 (default) picks a free one. */
|
|
5
|
+
port?: number;
|
|
6
|
+
/** Isolate worker threads. */
|
|
7
|
+
workers?: number;
|
|
8
|
+
/** Cap on concurrent live contexts before backpressure. */
|
|
9
|
+
maxContexts?: number;
|
|
10
|
+
/** Upstream proxy, e.g. "socks5://host:1080" or "http://user:pass@host:port". */
|
|
11
|
+
proxy?: string;
|
|
12
|
+
/** Directory for persistent named-session cookie jars. */
|
|
13
|
+
sessionStore?: string;
|
|
14
|
+
/** Give each browser context its own coherent fingerprint. */
|
|
15
|
+
rotateFingerprint?: boolean;
|
|
16
|
+
/** Derive each context's timezone/locale from its proxy's exit IP. */
|
|
17
|
+
geoipTimezone?: boolean;
|
|
18
|
+
/** Load ad/analytics/tracker subresources (blocked by default). */
|
|
19
|
+
allowTrackers?: boolean;
|
|
20
|
+
/** Chrome major version to emulate (TLS + JS together), e.g. 148. */
|
|
21
|
+
chromeVersion?: number;
|
|
22
|
+
/** Extra raw CLI arguments passed to the binary. */
|
|
23
|
+
args?: string[];
|
|
24
|
+
/** Extra environment variables for the server process. */
|
|
25
|
+
env?: Record<string, string>;
|
|
26
|
+
/** stdio option for the child process (default "inherit"). */
|
|
27
|
+
stdio?: any;
|
|
28
|
+
/** Milliseconds to wait for the server to become ready (default 30000). */
|
|
29
|
+
timeout?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export declare class NokkServer {
|
|
33
|
+
readonly host: string;
|
|
34
|
+
readonly port: number;
|
|
35
|
+
/** browserWSEndpoint for puppeteer.connect / chromium.connectOverCDP. */
|
|
36
|
+
readonly wsEndpoint: string;
|
|
37
|
+
readonly httpEndpoint: string;
|
|
38
|
+
readonly pid: number;
|
|
39
|
+
/** Stop the server. Idempotent. */
|
|
40
|
+
close(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Start a nokk CDP server and resolve to a NokkServer. */
|
|
44
|
+
export declare function launch(options?: LaunchOptions): Promise<NokkServer>;
|
|
45
|
+
|
|
46
|
+
/** Absolute path to the bundled `nokk` binary (override with NOKK_BINARY). */
|
|
47
|
+
export declare function binaryPath(): string;
|
package/index.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// nokk — launch the undetectable browser-engine CDP server and connect to it.
|
|
2
|
+
//
|
|
3
|
+
// const nokk = require("nokk");
|
|
4
|
+
// const puppeteer = require("puppeteer");
|
|
5
|
+
// const server = await nokk.launch({ rotateFingerprint: true });
|
|
6
|
+
// const browser = await puppeteer.connect({ browserWSEndpoint: server.wsEndpoint });
|
|
7
|
+
// ...
|
|
8
|
+
// await server.close();
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const { spawn } = require("child_process");
|
|
12
|
+
const http = require("http");
|
|
13
|
+
const net = require("net");
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
|
|
17
|
+
const EXE = process.platform === "win32" ? "nokk.exe" : "nokk";
|
|
18
|
+
const BUNDLED = path.join(__dirname, "vendor", EXE);
|
|
19
|
+
|
|
20
|
+
/** Absolute path to the bundled `nokk` binary (override with NOKK_BINARY). */
|
|
21
|
+
function binaryPath() {
|
|
22
|
+
const override = process.env.NOKK_BINARY;
|
|
23
|
+
if (override) {
|
|
24
|
+
if (fs.existsSync(override)) return override;
|
|
25
|
+
throw new Error(`NOKK_BINARY points at a missing file: ${override}`);
|
|
26
|
+
}
|
|
27
|
+
if (fs.existsSync(BUNDLED)) return BUNDLED;
|
|
28
|
+
throw new Error(
|
|
29
|
+
"the bundled `nokk` binary was not found — reinstall (`npm install nokk`). " +
|
|
30
|
+
"Prebuilt binaries are currently Linux x64 only."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function freePort(host) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const s = net.createServer();
|
|
37
|
+
s.once("error", reject);
|
|
38
|
+
s.listen(0, host, () => {
|
|
39
|
+
const { port } = s.address();
|
|
40
|
+
s.close(() => resolve(port));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ready(host, port) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const req = http.get(`http://${host}:${port}/json/version`, (r) => {
|
|
48
|
+
r.resume();
|
|
49
|
+
resolve(r.statusCode === 200);
|
|
50
|
+
});
|
|
51
|
+
req.on("error", () => resolve(false));
|
|
52
|
+
req.setTimeout(1000, () => {
|
|
53
|
+
req.destroy();
|
|
54
|
+
resolve(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
60
|
+
|
|
61
|
+
/** A running nokk CDP server. Returned by {@link launch}. */
|
|
62
|
+
class NokkServer {
|
|
63
|
+
constructor(proc, host, port) {
|
|
64
|
+
this._proc = proc;
|
|
65
|
+
this.host = host;
|
|
66
|
+
this.port = port;
|
|
67
|
+
}
|
|
68
|
+
/** browserWSEndpoint for puppeteer.connect / chromium.connectOverCDP. */
|
|
69
|
+
get wsEndpoint() {
|
|
70
|
+
return `ws://${this.host}:${this.port}/devtools/browser/nokk`;
|
|
71
|
+
}
|
|
72
|
+
get httpEndpoint() {
|
|
73
|
+
return `http://${this.host}:${this.port}`;
|
|
74
|
+
}
|
|
75
|
+
get pid() {
|
|
76
|
+
return this._proc.pid;
|
|
77
|
+
}
|
|
78
|
+
/** Stop the server. Idempotent. */
|
|
79
|
+
async close() {
|
|
80
|
+
const p = this._proc;
|
|
81
|
+
if (p.exitCode !== null || p.signalCode) return;
|
|
82
|
+
p.kill("SIGTERM");
|
|
83
|
+
await new Promise((resolve) => {
|
|
84
|
+
const t = setTimeout(() => {
|
|
85
|
+
try {
|
|
86
|
+
p.kill("SIGKILL");
|
|
87
|
+
} catch (_) {}
|
|
88
|
+
resolve();
|
|
89
|
+
}, 5000);
|
|
90
|
+
p.once("exit", () => {
|
|
91
|
+
clearTimeout(t);
|
|
92
|
+
resolve();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Start a `nokk` CDP server and resolve to a {@link NokkServer}.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} [opts]
|
|
102
|
+
* @param {string} [opts.host="127.0.0.1"]
|
|
103
|
+
* @param {number} [opts.port=0] 0 = pick a free port
|
|
104
|
+
* @param {number} [opts.workers]
|
|
105
|
+
* @param {number} [opts.maxContexts]
|
|
106
|
+
* @param {string} [opts.proxy] e.g. "socks5://host:1080"
|
|
107
|
+
* @param {string} [opts.sessionStore]
|
|
108
|
+
* @param {boolean} [opts.rotateFingerprint]
|
|
109
|
+
* @param {boolean} [opts.geoipTimezone]
|
|
110
|
+
* @param {boolean} [opts.allowTrackers]
|
|
111
|
+
* @param {number} [opts.chromeVersion] e.g. 148
|
|
112
|
+
* @param {string[]}[opts.args] extra raw CLI args
|
|
113
|
+
* @param {object} [opts.env]
|
|
114
|
+
* @param {number} [opts.timeout=30000] ms to wait for readiness
|
|
115
|
+
*/
|
|
116
|
+
async function launch(opts = {}) {
|
|
117
|
+
const host = opts.host || "127.0.0.1";
|
|
118
|
+
const port = opts.port || (await freePort(host));
|
|
119
|
+
const args = ["--host", host, "--port", String(port)];
|
|
120
|
+
if (opts.workers != null) args.push("--workers", String(opts.workers));
|
|
121
|
+
if (opts.maxContexts != null) args.push("--max-contexts", String(opts.maxContexts));
|
|
122
|
+
if (opts.proxy) args.push("--proxy", opts.proxy);
|
|
123
|
+
if (opts.sessionStore) args.push("--session-store", String(opts.sessionStore));
|
|
124
|
+
if (opts.rotateFingerprint) args.push("--rotate-fingerprint");
|
|
125
|
+
if (opts.geoipTimezone) args.push("--geoip-timezone");
|
|
126
|
+
if (opts.allowTrackers) args.push("--allow-trackers");
|
|
127
|
+
if (opts.chromeVersion != null) args.push("--chrome-version", String(opts.chromeVersion));
|
|
128
|
+
if (opts.args) args.push(...opts.args);
|
|
129
|
+
|
|
130
|
+
const proc = spawn(binaryPath(), args, {
|
|
131
|
+
stdio: opts.stdio || "inherit",
|
|
132
|
+
env: { ...process.env, ...(opts.env || {}) },
|
|
133
|
+
});
|
|
134
|
+
const server = new NokkServer(proc, host, port);
|
|
135
|
+
|
|
136
|
+
const timeout = opts.timeout != null ? opts.timeout : 30000;
|
|
137
|
+
const deadline = Date.now() + timeout;
|
|
138
|
+
const killOnExit = () => {
|
|
139
|
+
try {
|
|
140
|
+
proc.kill();
|
|
141
|
+
} catch (_) {}
|
|
142
|
+
};
|
|
143
|
+
while (Date.now() < deadline) {
|
|
144
|
+
if (proc.exitCode !== null) {
|
|
145
|
+
throw new Error(`nokk exited before becoming ready (code ${proc.exitCode})`);
|
|
146
|
+
}
|
|
147
|
+
if (await ready(host, port)) {
|
|
148
|
+
process.once("exit", killOnExit); // safety net
|
|
149
|
+
return server;
|
|
150
|
+
}
|
|
151
|
+
await sleep(50);
|
|
152
|
+
}
|
|
153
|
+
await server.close();
|
|
154
|
+
throw new Error(`nokk did not become ready within ${timeout}ms`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { launch, binaryPath, NokkServer };
|
package/install.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// postinstall: download the prebuilt `nokk` binary from the matching GitHub
|
|
2
|
+
// Release and extract it into vendor/, so the package ships no binary in the
|
|
3
|
+
// tarball (kept small) but `require('nokk')` / `npx nokk` work after install.
|
|
4
|
+
// Linux x64 only for now (matches what the release workflow builds).
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const https = require("https");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const os = require("os");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const { execFileSync } = require("child_process");
|
|
12
|
+
|
|
13
|
+
const pkg = require("./package.json");
|
|
14
|
+
const REPO = "koloss777/nokk";
|
|
15
|
+
const VENDOR = path.join(__dirname, "vendor");
|
|
16
|
+
const BIN = path.join(VENDOR, "nokk");
|
|
17
|
+
|
|
18
|
+
function fail(msg) {
|
|
19
|
+
console.error("[nokk] " + msg);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Allow skipping (e.g. CI that provides NOKK_BINARY, or offline installs).
|
|
24
|
+
if (process.env.NOKK_SKIP_DOWNLOAD || process.env.NOKK_BINARY) {
|
|
25
|
+
console.log("[nokk] skipping binary download (NOKK_BINARY / NOKK_SKIP_DOWNLOAD set)");
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (process.platform !== "linux" || process.arch !== "x64") {
|
|
30
|
+
fail(
|
|
31
|
+
`prebuilt binary is currently Linux x64 only (got ${process.platform}/${process.arch}). ` +
|
|
32
|
+
`See ${pkg.homepage} — macOS/Windows are on the roadmap.`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tag = "v" + pkg.version; // 0.1.20-alpha -> v0.1.20-alpha
|
|
37
|
+
const asset = `nokk-${tag}-linux-x86_64.tar.gz`;
|
|
38
|
+
const url = `https://github.com/${REPO}/releases/download/${tag}/${asset}`;
|
|
39
|
+
|
|
40
|
+
function download(from, dest, cb, redirects) {
|
|
41
|
+
redirects = redirects || 0;
|
|
42
|
+
https
|
|
43
|
+
.get(from, (res) => {
|
|
44
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
45
|
+
res.resume();
|
|
46
|
+
if (redirects > 10) return cb(new Error("too many redirects"));
|
|
47
|
+
return download(res.headers.location, dest, cb, redirects + 1);
|
|
48
|
+
}
|
|
49
|
+
if (res.statusCode !== 200) {
|
|
50
|
+
res.resume();
|
|
51
|
+
return cb(new Error(`HTTP ${res.statusCode} for ${from}`));
|
|
52
|
+
}
|
|
53
|
+
const file = fs.createWriteStream(dest);
|
|
54
|
+
res.pipe(file);
|
|
55
|
+
file.on("finish", () => file.close(() => cb(null)));
|
|
56
|
+
file.on("error", cb);
|
|
57
|
+
})
|
|
58
|
+
.on("error", cb);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fs.mkdirSync(VENDOR, { recursive: true });
|
|
62
|
+
const tmp = path.join(os.tmpdir(), asset);
|
|
63
|
+
console.log(`[nokk] downloading ${asset} …`);
|
|
64
|
+
download(url, tmp, (err) => {
|
|
65
|
+
if (err) fail(`download failed: ${err.message}\n ${url}`);
|
|
66
|
+
try {
|
|
67
|
+
execFileSync("tar", ["-xzf", tmp, "-C", VENDOR, "nokk"]);
|
|
68
|
+
fs.chmodSync(BIN, 0o755);
|
|
69
|
+
fs.unlinkSync(tmp);
|
|
70
|
+
console.log("[nokk] installed " + BIN);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
fail("extract failed: " + e.message);
|
|
73
|
+
}
|
|
74
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@koloss777/nokk",
|
|
3
|
+
"version": "0.1.21-alpha",
|
|
4
|
+
"description": "Undetectable headless-browser engine — a lightweight, Chrome-fingerprinted CDP server for Puppeteer/Playwright scraping. npm install and connect, no browser download.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"scraping",
|
|
7
|
+
"web-scraping",
|
|
8
|
+
"puppeteer",
|
|
9
|
+
"playwright",
|
|
10
|
+
"cdp",
|
|
11
|
+
"chrome-devtools-protocol",
|
|
12
|
+
"headless-browser",
|
|
13
|
+
"browser-automation",
|
|
14
|
+
"stealth",
|
|
15
|
+
"anti-bot",
|
|
16
|
+
"fingerprint",
|
|
17
|
+
"ja3",
|
|
18
|
+
"cloudflare"
|
|
19
|
+
],
|
|
20
|
+
"license": "(MIT OR Apache-2.0)",
|
|
21
|
+
"author": "koloss777",
|
|
22
|
+
"homepage": "https://github.com/koloss777/nokk",
|
|
23
|
+
"repository": { "type": "git", "url": "git+https://github.com/koloss777/nokk.git" },
|
|
24
|
+
"bugs": { "url": "https://github.com/koloss777/nokk/issues" },
|
|
25
|
+
"main": "index.js",
|
|
26
|
+
"types": "index.d.ts",
|
|
27
|
+
"bin": { "nokk": "bin/nokk.js" },
|
|
28
|
+
"files": ["index.js", "index.d.ts", "install.js", "bin/", "README.md"],
|
|
29
|
+
"scripts": { "postinstall": "node install.js" },
|
|
30
|
+
"engines": { "node": ">=16" },
|
|
31
|
+
"os": ["linux"],
|
|
32
|
+
"cpu": ["x64"]
|
|
33
|
+
}
|