@rxflex/rom 0.0.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/Cargo.toml ADDED
@@ -0,0 +1,23 @@
1
+ [package]
2
+ name = "rom-node-native"
3
+ description = "Native napi-rs bridge for the ROM Node.js bindings"
4
+ version = "0.0.1"
5
+ edition = "2024"
6
+ homepage = "https://github.com/Rxflex/rom"
7
+ license = "MIT"
8
+ repository = "https://github.com/Rxflex/rom"
9
+
10
+ [lib]
11
+ name = "rom_node_native"
12
+ path = "native/lib.rs"
13
+ crate-type = ["cdylib"]
14
+
15
+ [dependencies]
16
+ napi = "3"
17
+ napi-derive = "3"
18
+ rom-runtime = { path = "../../crates/rom-runtime" }
19
+
20
+ [build-dependencies]
21
+ napi-build = "2"
22
+
23
+ [workspace]
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # `@rxflex/rom`
2
+
3
+ Node.js bindings for the ROM browser-like runtime.
4
+
5
+ This package exposes a small JavaScript API on top of ROM:
6
+
7
+ - `eval()`
8
+ - `evalAsync()`
9
+ - `evalJson()`
10
+ - `surfaceSnapshot()`
11
+ - `fingerprintProbe()`
12
+ - `runFingerprintJsHarness()`
13
+ - `fingerprintJsVersion()`
14
+
15
+ It prefers a native `napi-rs` bridge when available and falls back to the ROM CLI bridge otherwise.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install @rxflex/rom
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```js
26
+ import { RomRuntime, hasNativeBinding } from "@rxflex/rom";
27
+
28
+ const runtime = new RomRuntime({
29
+ href: "https://example.test/",
30
+ cors_enabled: false,
31
+ proxy_url: process.env.ROM_PROXY_URL ?? null,
32
+ });
33
+
34
+ const href = await runtime.evalAsync("(async () => location.href)()");
35
+ const snapshot = await runtime.surfaceSnapshot();
36
+
37
+ console.log("native:", hasNativeBinding());
38
+ console.log(href);
39
+ console.log(snapshot.fetch);
40
+ ```
41
+
42
+ Config keys use the Rust runtime field names, so use snake_case such as `cors_enabled` and `proxy_url`.
43
+ `cors_enabled` is `false` by default.
44
+
45
+ ## Optional native build
46
+
47
+ ```bash
48
+ npm run build:native
49
+ ```
50
+
51
+ `npm pack` and `npm publish` now run the native release build automatically via `prepack`, and the produced `rom_node_native.node` is included in the published tarball for the platform that performed the publish.
52
+
53
+ ## Common methods
54
+
55
+ - `eval(script)`
56
+ - `evalAsync(script)`
57
+ - `evalJson(script, { async })`
58
+ - `surfaceSnapshot()`
59
+ - `fingerprintProbe()`
60
+ - `runFingerprintJsHarness()`
61
+ - `fingerprintJsVersion()`
62
+
63
+ ## Environment
64
+
65
+ - `ROM_NATIVE_NODE_BINDING`: explicit path to a compiled `.node` addon
66
+ - `ROM_FORCE_CLI_BRIDGE=1`: disable the native path and force CLI fallback
67
+ - `ROM_BRIDGE_BIN`: explicit path to the `rom_bridge` executable
68
+ - `ROM_BRIDGE_CWD`: working directory used by the CLI fallback
69
+ - `ROM_PROXY_URL`: convenience env var you can forward into `proxy_url`
70
+
71
+ ## More docs
72
+
73
+ - Root guide: [../../README.md](../../README.md)
74
+ - LLM guide: [../../LLMS.md](../../LLMS.md)
package/build.rs ADDED
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ napi_build::setup();
3
+ }
package/native/lib.rs ADDED
@@ -0,0 +1,7 @@
1
+ use napi::Result;
2
+ use napi_derive::napi;
3
+
4
+ #[napi]
5
+ pub fn execute_bridge(request_json: String) -> Result<String> {
6
+ Ok(rom_runtime::execute_bridge_request_json(&request_json))
7
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@rxflex/rom",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Node.js wrapper for the ROM browser-like runtime",
6
+ "license": "MIT",
7
+ "author": "Rxflex",
8
+ "homepage": "https://github.com/Rxflex/rom",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Rxflex/rom.git",
12
+ "directory": "bindings/gom-node"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/Rxflex/rom/issues"
16
+ },
17
+ "keywords": [
18
+ "rom",
19
+ "runtime",
20
+ "browser",
21
+ "web-api",
22
+ "fingerprint"
23
+ ],
24
+ "sideEffects": false,
25
+ "main": "./src/index.js",
26
+ "scripts": {
27
+ "prepack": "node ./scripts/build-native.mjs --release",
28
+ "build:native": "node ./scripts/build-native.mjs --release",
29
+ "build:native:debug": "node ./scripts/build-native.mjs",
30
+ "smoke": "node ./scripts/smoke.mjs",
31
+ "pack:check": "npm pack --dry-run"
32
+ },
33
+ "exports": {
34
+ ".": "./src/index.js"
35
+ },
36
+ "types": "./src/index.d.ts",
37
+ "files": [
38
+ "README.md",
39
+ "rom_node_native.node",
40
+ "src",
41
+ "scripts",
42
+ "native",
43
+ "Cargo.toml",
44
+ "build.rs"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }
Binary file
@@ -0,0 +1,44 @@
1
+ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const packageRoot = path.resolve(__dirname, "..");
9
+ const profile = process.argv.includes("--release") ? "release" : "debug";
10
+ const targetDir = process.env.CARGO_TARGET_DIR || path.join(packageRoot, "target");
11
+ const artifactDir = path.join(targetDir, profile);
12
+ const outputPath = path.join(packageRoot, "rom_node_native.node");
13
+
14
+ const cargoArgs = ["build", "--manifest-path", "Cargo.toml"];
15
+ if (profile === "release") {
16
+ cargoArgs.push("--release");
17
+ }
18
+
19
+ const build = spawnSync("cargo", cargoArgs, {
20
+ cwd: packageRoot,
21
+ stdio: "inherit",
22
+ env: process.env,
23
+ });
24
+
25
+ if (build.status !== 0) {
26
+ process.exit(build.status ?? 1);
27
+ }
28
+
29
+ const artifactName =
30
+ process.platform === "win32"
31
+ ? "rom_node_native.dll"
32
+ : process.platform === "darwin"
33
+ ? "librom_node_native.dylib"
34
+ : "librom_node_native.so";
35
+ const artifactPath = path.join(artifactDir, artifactName);
36
+
37
+ if (!existsSync(artifactPath)) {
38
+ console.error(`Native addon artifact not found: ${artifactPath}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ mkdirSync(path.dirname(outputPath), { recursive: true });
43
+ copyFileSync(artifactPath, outputPath);
44
+ console.log(`Built ${outputPath}`);
@@ -0,0 +1,26 @@
1
+ import { RomRuntime, hasNativeBinding } from "../src/index.js";
2
+
3
+ async function main() {
4
+ const runtime = new RomRuntime({ href: "https://example.test/" });
5
+ const href = await runtime.evalAsync("(async () => location.href)()");
6
+ const snapshot = await runtime.surfaceSnapshot();
7
+
8
+ if (!hasNativeBinding()) {
9
+ throw new Error("Expected Node native binding to be loaded.");
10
+ }
11
+
12
+ if (href !== "https://example.test/") {
13
+ throw new Error(`Unexpected href: ${href}`);
14
+ }
15
+
16
+ if (snapshot?.globals?.window !== true) {
17
+ throw new Error("Surface snapshot did not expose window.");
18
+ }
19
+
20
+ console.log(JSON.stringify({ native: true, href }));
21
+ }
22
+
23
+ main().catch((error) => {
24
+ console.error(error);
25
+ process.exit(1);
26
+ });
package/src/index.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ export interface RuntimeConfig {
2
+ href?: string;
3
+ user_agent?: string;
4
+ app_name?: string;
5
+ platform?: string;
6
+ language?: string;
7
+ languages?: string[];
8
+ hardware_concurrency?: number;
9
+ device_memory?: number;
10
+ webdriver?: boolean;
11
+ }
12
+
13
+ export declare class RomRuntime {
14
+ constructor(config?: RuntimeConfig);
15
+ eval(script: string): Promise<string>;
16
+ evalAsync(script: string): Promise<string>;
17
+ evalJson<T = unknown>(script: string, options?: { async?: boolean }): Promise<T>;
18
+ surfaceSnapshot(): Promise<unknown>;
19
+ fingerprintProbe(): Promise<unknown>;
20
+ runFingerprintJsHarness(): Promise<unknown>;
21
+ fingerprintJsVersion(): Promise<string>;
22
+ }
23
+
24
+ export declare function createRuntime(config?: RuntimeConfig): RomRuntime;
25
+ export declare function hasNativeBinding(): boolean;
package/src/index.js ADDED
@@ -0,0 +1,129 @@
1
+ import { execFile } from "node:child_process";
2
+ import { loadNativeBridge } from "./native.js";
3
+ import { fileURLToPath } from "node:url";
4
+ import path from "node:path";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const repoRoot = path.resolve(__dirname, "..", "..", "..");
9
+ const nativeBridge = loadNativeBridge();
10
+
11
+ function resolveBridgeCommand() {
12
+ if (process.env.ROM_BRIDGE_BIN) {
13
+ return {
14
+ file: process.env.ROM_BRIDGE_BIN,
15
+ args: [],
16
+ cwd: process.env.ROM_BRIDGE_CWD || repoRoot,
17
+ };
18
+ }
19
+
20
+ return {
21
+ file: "cargo",
22
+ args: ["run", "--quiet", "-p", "rom-runtime", "--bin", "rom_bridge"],
23
+ cwd: process.env.ROM_BRIDGE_CWD || repoRoot,
24
+ };
25
+ }
26
+
27
+ function parseBridgeResponse(stdout, stderr, error) {
28
+ const trimmed = stdout.trim();
29
+
30
+ if (!trimmed) {
31
+ throw error ?? new Error(`ROM bridge produced no output.\n${stderr}`);
32
+ }
33
+
34
+ let response;
35
+ try {
36
+ response = JSON.parse(trimmed);
37
+ } catch (parseError) {
38
+ throw new Error(
39
+ `ROM bridge returned invalid JSON: ${parseError.message}\n${stdout}\n${stderr}`,
40
+ );
41
+ }
42
+
43
+ if (error || !response.ok) {
44
+ throw new Error(response.error || error?.message || "ROM bridge command failed.");
45
+ }
46
+
47
+ return response.result;
48
+ }
49
+
50
+ function runNativeBridge(command, payload) {
51
+ if (!nativeBridge || typeof nativeBridge.executeBridge !== "function") {
52
+ return null;
53
+ }
54
+
55
+ const responseText = nativeBridge.executeBridge(JSON.stringify({ command, ...payload }));
56
+ return Promise.resolve(parseBridgeResponse(responseText, "", null));
57
+ }
58
+
59
+ function runCliBridge(command, payload) {
60
+ const bridge = resolveBridgeCommand();
61
+
62
+ return new Promise((resolve, reject) => {
63
+ const child = execFile(
64
+ bridge.file,
65
+ bridge.args,
66
+ {
67
+ cwd: bridge.cwd,
68
+ env: process.env,
69
+ maxBuffer: 10 * 1024 * 1024,
70
+ },
71
+ (error, stdout, stderr) => {
72
+ try {
73
+ resolve(parseBridgeResponse(stdout, stderr, error));
74
+ } catch (bridgeError) {
75
+ reject(bridgeError);
76
+ }
77
+ },
78
+ );
79
+
80
+ child.stdin.end(JSON.stringify({ command, ...payload }));
81
+ });
82
+ }
83
+
84
+ function runBridge(command, payload) {
85
+ return runNativeBridge(command, payload) ?? runCliBridge(command, payload);
86
+ }
87
+
88
+ export class RomRuntime {
89
+ constructor(config = {}) {
90
+ this.config = config;
91
+ }
92
+
93
+ eval(script) {
94
+ return runBridge("eval", { config: this.config, script });
95
+ }
96
+
97
+ evalAsync(script) {
98
+ return runBridge("eval-async", { config: this.config, script });
99
+ }
100
+
101
+ async evalJson(script, { async = true } = {}) {
102
+ const result = async ? await this.evalAsync(script) : await this.eval(script);
103
+ return JSON.parse(result);
104
+ }
105
+
106
+ surfaceSnapshot() {
107
+ return runBridge("surface-snapshot", { config: this.config });
108
+ }
109
+
110
+ fingerprintProbe() {
111
+ return runBridge("fingerprint-probe", { config: this.config });
112
+ }
113
+
114
+ runFingerprintJsHarness() {
115
+ return runBridge("fingerprint-js-harness", { config: this.config });
116
+ }
117
+
118
+ fingerprintJsVersion() {
119
+ return runBridge("fingerprint-js-version", { config: this.config });
120
+ }
121
+ }
122
+
123
+ export function createRuntime(config = {}) {
124
+ return new RomRuntime(config);
125
+ }
126
+
127
+ export function hasNativeBinding() {
128
+ return !!nativeBridge;
129
+ }
package/src/native.js ADDED
@@ -0,0 +1,40 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const packageRoot = path.resolve(__dirname, "..");
10
+
11
+ function candidatePaths() {
12
+ const candidates = [];
13
+
14
+ if (process.env.ROM_NATIVE_NODE_BINDING) {
15
+ candidates.push(process.env.ROM_NATIVE_NODE_BINDING);
16
+ }
17
+
18
+ candidates.push(path.join(packageRoot, "rom_node_native.node"));
19
+ return candidates;
20
+ }
21
+
22
+ export function loadNativeBridge() {
23
+ if (process.env.ROM_FORCE_CLI_BRIDGE === "1") {
24
+ return null;
25
+ }
26
+
27
+ for (const candidate of candidatePaths()) {
28
+ if (!existsSync(candidate)) {
29
+ continue;
30
+ }
31
+
32
+ try {
33
+ return require(candidate);
34
+ } catch {
35
+ continue;
36
+ }
37
+ }
38
+
39
+ return null;
40
+ }