@rxflex/rom 0.0.8 → 0.0.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  [package]
2
2
  name = "rom-node-native"
3
3
  description = "Native napi-rs bridge for the ROM Node.js bindings"
4
- version = "0.0.8"
4
+ version = "0.0.9"
5
5
  edition = "2024"
6
6
  homepage = "https://github.com/Rxflex/rom"
7
7
  license = "MIT"
package/README.md CHANGED
@@ -46,6 +46,9 @@ console.log(await runtime.evalAsync("(async () => String(globalThis.__romValue))
46
46
  Config keys use the Rust runtime field names, so use snake_case such as `cors_enabled` and `proxy_url`.
47
47
  `cors_enabled` is `false` by default.
48
48
  When the native addon is loaded, one `RomRuntime` instance keeps JS globals alive across multiple `eval()` and `evalAsync()` calls.
49
+ For cookie seeding, the wrapper accepts either serialized `cookie_store`, a raw cookie header string such as `"api_uid=seeded; _nano_fp=abc"`, or a `cookies` alias with string/object/array inputs.
50
+ For storage seeding, the wrapper accepts `local_storage` and `session_storage` as serialized JSON objects, plain JS objects, or entry arrays such as `[['VerifyAuthToken', 'seeded']]`.
51
+ The default navigator surface is Chrome-like, including `navigator.userAgent`, `navigator.vendor`, and `navigator.userAgentData`.
49
52
 
50
53
  ## Optional native build
51
54
 
package/native/lib.rs CHANGED
@@ -78,6 +78,18 @@ impl NativeRomRuntime {
78
78
  .map(|value| value.to_owned())
79
79
  }
80
80
 
81
+ #[napi(js_name = "exportLocalStorage")]
82
+ pub fn export_local_storage(&self) -> Result<String> {
83
+ map_runtime_error(self.runtime.export_local_storage())
84
+ .map(|value| value.to_owned())
85
+ }
86
+
87
+ #[napi(js_name = "exportSessionStorage")]
88
+ pub fn export_session_storage(&self) -> Result<String> {
89
+ map_runtime_error(self.runtime.export_session_storage())
90
+ .map(|value| value.to_owned())
91
+ }
92
+
81
93
  #[napi(js_name = "evalJson")]
82
94
  pub fn eval_json(&self, script: String, asynchronous: bool) -> Result<String> {
83
95
  let value = if asynchronous {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rxflex/rom",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "description": "Node.js wrapper for the ROM browser-like runtime",
6
6
  "license": "MIT",
package/scripts/smoke.mjs CHANGED
@@ -1,10 +1,16 @@
1
1
  import { RomRuntime, hasNativeBinding } from "../src/index.js";
2
2
 
3
3
  async function main() {
4
- const runtime = new RomRuntime({ href: "https://example.test/" });
4
+ const runtime = new RomRuntime({
5
+ href: "https://example.test/",
6
+ cookie_store: "seed=1; path=/",
7
+ local_storage: { VerifyAuthToken: "seeded-storage" },
8
+ });
5
9
  const href = await runtime.evalAsync("(async () => location.href)()");
6
10
  await runtime.evalAsync("(async () => { globalThis.__romSmokeValue = 42; return 'ok'; })()");
7
11
  const persisted = await runtime.evalAsync("(async () => String(globalThis.__romSmokeValue))()");
12
+ const cookie = await runtime.evalAsync("(async () => document.cookie)()");
13
+ const storage = await runtime.evalAsync("(async () => localStorage.getItem('VerifyAuthToken'))()");
8
14
  const snapshot = await runtime.surfaceSnapshot();
9
15
 
10
16
  if (!hasNativeBinding()) {
@@ -23,7 +29,15 @@ async function main() {
23
29
  throw new Error(`Expected persisted global state, got: ${persisted}`);
24
30
  }
25
31
 
26
- console.log(JSON.stringify({ native: true, href, persisted }));
32
+ if (cookie !== "seed=1") {
33
+ throw new Error(`Expected seeded cookie, got: ${cookie}`);
34
+ }
35
+
36
+ if (storage !== "seeded-storage") {
37
+ throw new Error(`Expected seeded localStorage, got: ${storage}`);
38
+ }
39
+
40
+ console.log(JSON.stringify({ native: true, href, persisted, cookie, storage }));
27
41
  }
28
42
 
29
43
  main().catch((error) => {
package/src/index.d.ts CHANGED
@@ -1,3 +1,25 @@
1
+ export interface CookieEntry {
2
+ name: string;
3
+ value: string;
4
+ domain?: string;
5
+ hostOnly?: boolean;
6
+ path?: string;
7
+ secure?: boolean;
8
+ httpOnly?: boolean;
9
+ sameSite?: "Lax" | "Strict" | "None";
10
+ expiresAt?: number | null;
11
+ }
12
+
13
+ export type CookieInput =
14
+ | string
15
+ | CookieEntry[]
16
+ | Record<string, string | number | boolean | null | undefined>;
17
+
18
+ export type StorageInput =
19
+ | string
20
+ | Record<string, string | number | boolean | null | undefined>
21
+ | Array<[string, string | number | boolean | null | undefined]>;
22
+
1
23
  export interface RuntimeConfig {
2
24
  href?: string;
3
25
  referrer?: string;
@@ -11,7 +33,12 @@ export interface RuntimeConfig {
11
33
  webdriver?: boolean;
12
34
  cors_enabled?: boolean;
13
35
  proxy_url?: string | null;
14
- cookie_store?: string | null;
36
+ cookie_store?: CookieInput | null;
37
+ cookies?: CookieInput | null;
38
+ local_storage?: StorageInput | null;
39
+ session_storage?: StorageInput | null;
40
+ localStorage?: StorageInput | null;
41
+ sessionStorage?: StorageInput | null;
15
42
  }
16
43
 
17
44
  export declare class RomRuntime {
package/src/index.js CHANGED
@@ -8,6 +8,15 @@ const __dirname = path.dirname(__filename);
8
8
  const repoRoot = path.resolve(__dirname, "..", "..", "..");
9
9
  const nativeBridge = loadNativeBridge();
10
10
  const NativeRomRuntime = nativeBridge?.NativeRomRuntime ?? null;
11
+ const COOKIE_ATTRIBUTE_NAMES = new Set([
12
+ "domain",
13
+ "path",
14
+ "expires",
15
+ "max-age",
16
+ "samesite",
17
+ "secure",
18
+ "httponly",
19
+ ]);
11
20
 
12
21
  function resolveBridgeCommand() {
13
22
  if (process.env.ROM_BRIDGE_BIN) {
@@ -90,21 +99,256 @@ function runBridge(command, payload) {
90
99
  }
91
100
 
92
101
  function applyBridgeState(targetConfig, state) {
93
- if (!state || typeof state.cookie_store !== "string") {
102
+ if (!state || typeof state !== "object") {
94
103
  return targetConfig;
95
104
  }
96
105
 
106
+ const nextConfig = { ...targetConfig };
107
+ if (typeof state.cookie_store === "string") {
108
+ nextConfig.cookie_store = state.cookie_store;
109
+ }
110
+ if (typeof state.local_storage === "string") {
111
+ nextConfig.local_storage = state.local_storage;
112
+ }
113
+ if (typeof state.session_storage === "string") {
114
+ nextConfig.session_storage = state.session_storage;
115
+ }
116
+
117
+ return nextConfig;
118
+ }
119
+
120
+ function normalizeRuntimeConfig(config = {}) {
121
+ const normalized = { ...config };
122
+ const cookieStore = normalizeCookieStoreInput(
123
+ normalized.cookie_store ?? normalized.cookies ?? null,
124
+ normalized.href,
125
+ );
126
+ const localStorage = normalizeStorageInput(
127
+ normalized.local_storage ?? normalized.localStorage ?? null,
128
+ );
129
+ const sessionStorage = normalizeStorageInput(
130
+ normalized.session_storage ?? normalized.sessionStorage ?? null,
131
+ );
132
+
133
+ delete normalized.cookies;
134
+ delete normalized.localStorage;
135
+ delete normalized.sessionStorage;
136
+ if (cookieStore !== null) {
137
+ normalized.cookie_store = cookieStore;
138
+ }
139
+ if (localStorage !== null) {
140
+ normalized.local_storage = localStorage;
141
+ }
142
+ if (sessionStorage !== null) {
143
+ normalized.session_storage = sessionStorage;
144
+ }
145
+
146
+ return normalized;
147
+ }
148
+
149
+ function normalizeCookieStoreInput(value, href) {
150
+ if (value === null || value === undefined || value === "") {
151
+ return null;
152
+ }
153
+
154
+ if (typeof value === "string") {
155
+ const trimmed = value.trim();
156
+ if (!trimmed) {
157
+ return null;
158
+ }
159
+
160
+ if (looksLikeSerializedCookieStore(trimmed)) {
161
+ return trimmed;
162
+ }
163
+
164
+ return serializeCookieEntries(parseCookieHeaderString(trimmed, href));
165
+ }
166
+
167
+ if (Array.isArray(value)) {
168
+ return serializeCookieEntries(normalizeCookieEntries(value, href));
169
+ }
170
+
171
+ if (typeof value === "object") {
172
+ return serializeCookieEntries(normalizeCookieEntries(value, href));
173
+ }
174
+
175
+ return null;
176
+ }
177
+
178
+ function normalizeStorageInput(value) {
179
+ if (value === null || value === undefined || value === "") {
180
+ return null;
181
+ }
182
+
183
+ if (typeof value === "string") {
184
+ const trimmed = value.trim();
185
+ if (!trimmed || !looksLikeSerializedStorage(trimmed)) {
186
+ return null;
187
+ }
188
+ return serializeStorageEntries(JSON.parse(trimmed));
189
+ }
190
+
191
+ if (Array.isArray(value) || typeof value === "object") {
192
+ return serializeStorageEntries(value);
193
+ }
194
+
195
+ return null;
196
+ }
197
+
198
+ function looksLikeSerializedCookieStore(value) {
199
+ try {
200
+ const parsed = JSON.parse(value);
201
+ return Array.isArray(parsed);
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
206
+
207
+ function looksLikeSerializedStorage(value) {
208
+ try {
209
+ const parsed = JSON.parse(value);
210
+ return Array.isArray(parsed) || (!!parsed && typeof parsed === "object");
211
+ } catch {
212
+ return false;
213
+ }
214
+ }
215
+
216
+ function normalizeCookieEntries(value, href) {
217
+ if (Array.isArray(value)) {
218
+ return value
219
+ .map((entry) => normalizeCookieEntryObject(entry, href))
220
+ .filter(Boolean);
221
+ }
222
+
223
+ return Object.entries(value).map(([name, entryValue]) =>
224
+ createCookieEntry(name, entryValue, href),
225
+ );
226
+ }
227
+
228
+ function normalizeCookieEntryObject(entry, href) {
229
+ if (!entry || typeof entry !== "object") {
230
+ return null;
231
+ }
232
+
233
+ const url = safeCookieUrl(href);
234
+ const path = typeof entry.path === "string" && entry.path.startsWith("/") ? entry.path : "/";
235
+ const domain =
236
+ typeof entry.domain === "string" && entry.domain.trim()
237
+ ? entry.domain.trim().replace(/^\./, "").toLowerCase()
238
+ : url.hostname.toLowerCase();
239
+
240
+ return {
241
+ name: String(entry.name ?? ""),
242
+ value: String(entry.value ?? ""),
243
+ domain,
244
+ hostOnly: entry.hostOnly ?? !("domain" in entry),
245
+ path,
246
+ secure: Boolean(entry.secure ?? (url.protocol === "https:")),
247
+ httpOnly: Boolean(entry.httpOnly),
248
+ sameSite: normalizeSameSite(entry.sameSite),
249
+ expiresAt: normalizeExpiresAt(entry.expiresAt),
250
+ };
251
+ }
252
+
253
+ function parseCookieHeaderString(value, href) {
254
+ return value
255
+ .split(";")
256
+ .map((part) => part.trim())
257
+ .filter(Boolean)
258
+ .map((part) => {
259
+ const separator = part.indexOf("=");
260
+ if (separator <= 0) {
261
+ return null;
262
+ }
263
+
264
+ const name = part.slice(0, separator).trim();
265
+ if (!name || COOKIE_ATTRIBUTE_NAMES.has(name.toLowerCase())) {
266
+ return null;
267
+ }
268
+
269
+ return createCookieEntry(name, part.slice(separator + 1), href);
270
+ })
271
+ .filter(Boolean);
272
+ }
273
+
274
+ function createCookieEntry(name, value, href) {
275
+ const url = safeCookieUrl(href);
97
276
  return {
98
- ...targetConfig,
99
- cookie_store: state.cookie_store,
277
+ name: String(name),
278
+ value: String(value),
279
+ domain: url.hostname.toLowerCase(),
280
+ hostOnly: true,
281
+ path: "/",
282
+ secure: url.protocol === "https:",
283
+ httpOnly: false,
284
+ sameSite: "Lax",
285
+ expiresAt: null,
100
286
  };
101
287
  }
102
288
 
289
+ function safeCookieUrl(href) {
290
+ try {
291
+ return new URL(href || "https://rom.local/");
292
+ } catch {
293
+ return new URL("https://rom.local/");
294
+ }
295
+ }
296
+
297
+ function normalizeSameSite(value) {
298
+ const normalized = String(value ?? "Lax").toLowerCase();
299
+ if (normalized === "strict") {
300
+ return "Strict";
301
+ }
302
+ if (normalized === "none") {
303
+ return "None";
304
+ }
305
+ return "Lax";
306
+ }
307
+
308
+ function normalizeExpiresAt(value) {
309
+ if (value === null || value === undefined || value === "") {
310
+ return null;
311
+ }
312
+
313
+ const numeric = Number(value);
314
+ return Number.isFinite(numeric) ? numeric : null;
315
+ }
316
+
317
+ function serializeCookieEntries(entries) {
318
+ if (!Array.isArray(entries) || entries.length === 0) {
319
+ return null;
320
+ }
321
+
322
+ return JSON.stringify(entries.filter((entry) => entry && entry.name));
323
+ }
324
+
325
+ function serializeStorageEntries(value) {
326
+ if (Array.isArray(value)) {
327
+ return JSON.stringify(
328
+ Object.fromEntries(
329
+ value
330
+ .filter((entry) => Array.isArray(entry) && entry.length >= 2)
331
+ .map(([key, entryValue]) => [String(key), String(entryValue)]),
332
+ ),
333
+ );
334
+ }
335
+
336
+ if (value && typeof value === "object") {
337
+ return JSON.stringify(
338
+ Object.fromEntries(
339
+ Object.entries(value).map(([key, entryValue]) => [key, String(entryValue)]),
340
+ ),
341
+ );
342
+ }
343
+
344
+ return null;
345
+ }
346
+
103
347
  export class RomRuntime {
104
348
  #nativeRuntime = null;
105
349
 
106
350
  constructor(config = {}) {
107
- this.config = config;
351
+ this.config = normalizeRuntimeConfig(config);
108
352
  if (typeof NativeRomRuntime === "function") {
109
353
  this.#nativeRuntime = new NativeRomRuntime(JSON.stringify(this.config));
110
354
  }
@@ -121,12 +365,31 @@ export class RomRuntime {
121
365
  };
122
366
  }
123
367
 
368
+ #applyStorageState(key, value) {
369
+ if (typeof value !== "string") {
370
+ return;
371
+ }
372
+
373
+ this.config = {
374
+ ...this.config,
375
+ [key]: value,
376
+ };
377
+ }
378
+
124
379
  #syncNativeState() {
125
- if (!this.#nativeRuntime || typeof this.#nativeRuntime.exportCookieStore !== "function") {
380
+ if (!this.#nativeRuntime) {
126
381
  return;
127
382
  }
128
383
 
129
- this.#applyCookieStore(this.#nativeRuntime.exportCookieStore());
384
+ if (typeof this.#nativeRuntime.exportCookieStore === "function") {
385
+ this.#applyCookieStore(this.#nativeRuntime.exportCookieStore());
386
+ }
387
+ if (typeof this.#nativeRuntime.exportLocalStorage === "function") {
388
+ this.#applyStorageState("local_storage", this.#nativeRuntime.exportLocalStorage());
389
+ }
390
+ if (typeof this.#nativeRuntime.exportSessionStorage === "function") {
391
+ this.#applyStorageState("session_storage", this.#nativeRuntime.exportSessionStorage());
392
+ }
130
393
  }
131
394
 
132
395
  async #runNative(method, ...args) {