@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 +1 -1
- package/README.md +3 -0
- package/native/lib.rs +12 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/rom_node_native.node +0 -0
- package/prebuilds/darwin-x64/rom_node_native.node +0 -0
- package/prebuilds/linux-x64-gnu/rom_node_native.node +0 -0
- package/prebuilds/win32-x64-msvc/rom_node_native.node +0 -0
- package/scripts/smoke.mjs +16 -2
- package/src/index.d.ts +28 -1
- package/src/index.js +269 -6
package/Cargo.toml
CHANGED
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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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({
|
|
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
|
-
|
|
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?:
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
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
|
|
380
|
+
if (!this.#nativeRuntime) {
|
|
126
381
|
return;
|
|
127
382
|
}
|
|
128
383
|
|
|
129
|
-
|
|
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) {
|