@fargowealth/fargo-ai-cli 1.4.0 → 1.4.2
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/CHANGELOG.md +16 -0
- package/README.md +32 -43
- package/package.json +1 -5
- package/src/cli.mjs +52 -7
- package/src/constants.mjs +1 -1
- package/src/upload-transfer.mjs +422 -0
- package/node_modules/@fargowealth/website-connector-runtime/README.md +0 -8
- package/node_modules/@fargowealth/website-connector-runtime/package.json +0 -24
- package/node_modules/@fargowealth/website-connector-runtime/src/index.d.mts +0 -116
- package/node_modules/@fargowealth/website-connector-runtime/src/index.mjs +0 -614
- package/src/website-browser-profile.mjs +0 -377
- package/src/website-cli.mjs +0 -364
- package/src/website-connector-definition.d.mts +0 -1
- package/src/website-connector-definition.mjs +0 -1
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { access, chmod, mkdir, readFile, readdir, unlink } from "node:fs/promises";
|
|
3
|
-
import { homedir, platform } from "node:os";
|
|
4
|
-
import { delimiter, join, resolve } from "node:path";
|
|
5
|
-
import { createInterface } from "node:readline/promises";
|
|
6
|
-
|
|
7
|
-
export const WEBSITE_BROWSER_PROFILE_SCHEMA_VERSION = "fargo.browser-profile/v1";
|
|
8
|
-
|
|
9
|
-
export async function captureLocalWebsiteProfile(spec, options = {}) {
|
|
10
|
-
const captureSpec = parseCaptureSpec(spec);
|
|
11
|
-
const profileDirectory = resolve(options.profileDirectory ?? defaultLocalWebsiteProfileDirectory(captureSpec.connectorId, {
|
|
12
|
-
...options,
|
|
13
|
-
profileKey: captureSpec.localProfileKey
|
|
14
|
-
}));
|
|
15
|
-
await mkdir(profileDirectory, { recursive: true, mode: 0o700 });
|
|
16
|
-
if (platform() !== "win32") await chmod(profileDirectory, 0o700);
|
|
17
|
-
const chromePath = await findChromeExecutable(options.chromePath, options);
|
|
18
|
-
const activePortPath = join(profileDirectory, "DevToolsActivePort");
|
|
19
|
-
await unlink(activePortPath).catch((error) => {
|
|
20
|
-
if (error?.code !== "ENOENT") throw error;
|
|
21
|
-
});
|
|
22
|
-
const child = spawn(chromePath, [
|
|
23
|
-
"--remote-debugging-address=127.0.0.1",
|
|
24
|
-
"--remote-debugging-port=0",
|
|
25
|
-
`--user-data-dir=${profileDirectory}`,
|
|
26
|
-
...(options.headless ? ["--headless=new"] : []),
|
|
27
|
-
"--no-first-run",
|
|
28
|
-
"--no-default-browser-check",
|
|
29
|
-
"--new-window",
|
|
30
|
-
captureSpec.loginUrl
|
|
31
|
-
], { stdio: "ignore" });
|
|
32
|
-
let client;
|
|
33
|
-
try {
|
|
34
|
-
const endpoint = await waitForDevToolsEndpoint(activePortPath, child, options);
|
|
35
|
-
client = await CdpClient.connect(endpoint);
|
|
36
|
-
const sessionId = await pageSession(client, captureSpec);
|
|
37
|
-
if (!options.noPrompt) await waitForLoginConfirmation(captureSpec, options);
|
|
38
|
-
else await delay(boundedInteger(options.settleMs, 3_000, 0, 60_000));
|
|
39
|
-
const cookies = await browserCookies(client);
|
|
40
|
-
const origins = await originStorage(client, sessionId, captureSpec);
|
|
41
|
-
return buildWebsiteBrowserProfile(captureSpec, { cookies, origins }, options.now?.() ?? new Date());
|
|
42
|
-
} finally {
|
|
43
|
-
if (client) await client.call("Browser.close").catch(() => {});
|
|
44
|
-
await waitForExit(child, 5_000).catch(() => child.kill("SIGTERM"));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function buildWebsiteBrowserProfile(spec, state, capturedAt = new Date()) {
|
|
49
|
-
const captureSpec = parseCaptureSpec(spec);
|
|
50
|
-
const allowedHosts = captureSpec.allowedOrigins.map((origin) => new URL(origin).hostname.toLowerCase());
|
|
51
|
-
const cookies = captureSpec.auth.sendCookies
|
|
52
|
-
? array(state.cookies).filter((cookie) => cookieAllowed(cookie.domain, allowedHosts)).map(normalizeCookie)
|
|
53
|
-
: [];
|
|
54
|
-
const suffixesByOrigin = new Map();
|
|
55
|
-
for (const source of captureSpec.auth.headerSources) {
|
|
56
|
-
const suffixes = suffixesByOrigin.get(source.origin) ?? new Set();
|
|
57
|
-
suffixes.add(source.keySuffix);
|
|
58
|
-
suffixesByOrigin.set(source.origin, suffixes);
|
|
59
|
-
}
|
|
60
|
-
const origins = array(state.origins).flatMap((entry) => {
|
|
61
|
-
const origin = String(entry.origin || "");
|
|
62
|
-
const suffixes = suffixesByOrigin.get(origin);
|
|
63
|
-
if (!suffixes) return [];
|
|
64
|
-
const localStorage = array(entry.localStorage).filter((item) => (
|
|
65
|
-
[...suffixes].some((suffix) => String(item.name || "").endsWith(suffix))
|
|
66
|
-
)).map((item) => ({ name: String(item.name), value: String(item.value) }));
|
|
67
|
-
return localStorage.length > 0 ? [{ localStorage, origin }] : [];
|
|
68
|
-
});
|
|
69
|
-
return {
|
|
70
|
-
capturedAt: new Date(capturedAt).toISOString(),
|
|
71
|
-
connectorId: captureSpec.connectorId,
|
|
72
|
-
cookies,
|
|
73
|
-
origins,
|
|
74
|
-
schemaVersion: WEBSITE_BROWSER_PROFILE_SCHEMA_VERSION
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function defaultLocalWebsiteProfileDirectory(connectorId, options = {}) {
|
|
79
|
-
const safeId = requiredConnectorId(connectorId);
|
|
80
|
-
const profileKey = typeof options.profileKey === "string" && /^bgc_[A-Za-z0-9_-]{43}$/.test(options.profileKey)
|
|
81
|
-
? options.profileKey
|
|
82
|
-
: "default";
|
|
83
|
-
const currentPlatform = options.platform ?? platform();
|
|
84
|
-
const homeDirectory = options.home ?? homedir();
|
|
85
|
-
if (currentPlatform === "darwin") {
|
|
86
|
-
return join(homeDirectory, "Library", "Application Support", "Fargo AI", "Website Profiles", safeId, profileKey);
|
|
87
|
-
}
|
|
88
|
-
if (currentPlatform === "win32") {
|
|
89
|
-
return join(options.appData ?? process.env.APPDATA ?? join(homeDirectory, "AppData", "Roaming"), "Fargo AI", "Website Profiles", safeId, profileKey);
|
|
90
|
-
}
|
|
91
|
-
return join(options.xdgConfigHome ?? process.env.XDG_CONFIG_HOME ?? join(homeDirectory, ".config"), "fargo", "website-profiles", safeId, profileKey);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function resolveLocalWebsiteProfileDirectory(connectorId, options = {}) {
|
|
95
|
-
if (options.profileDirectory) return resolve(options.profileDirectory);
|
|
96
|
-
const fallback = defaultLocalWebsiteProfileDirectory(connectorId, options);
|
|
97
|
-
const connectorDirectory = resolve(fallback, "..");
|
|
98
|
-
let entries;
|
|
99
|
-
try {
|
|
100
|
-
entries = await readdir(connectorDirectory, { withFileTypes: true });
|
|
101
|
-
} catch (error) {
|
|
102
|
-
if (error?.code === "ENOENT") return fallback;
|
|
103
|
-
throw error;
|
|
104
|
-
}
|
|
105
|
-
const connectionProfiles = entries
|
|
106
|
-
.filter((entry) => entry.isDirectory() && /^bgc_[A-Za-z0-9_-]{43}$/.test(entry.name))
|
|
107
|
-
.map((entry) => join(connectorDirectory, entry.name));
|
|
108
|
-
if (connectionProfiles.length === 0) return fallback;
|
|
109
|
-
if (connectionProfiles.length === 1) return connectionProfiles[0];
|
|
110
|
-
throw new Error(`Multiple local Profiles exist for ${connectorId}; pass --profile-dir <path>`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function documentIsReadyAtOrigin(value, expectedOrigin) {
|
|
114
|
-
if (!["interactive", "complete"].includes(value?.readyState)) return false;
|
|
115
|
-
try {
|
|
116
|
-
return new URL(value.url).origin === expectedOrigin;
|
|
117
|
-
} catch {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function parseCaptureSpec(value) {
|
|
123
|
-
const source = record(value, "Website Profile capture spec is invalid");
|
|
124
|
-
if (source.profileSchemaVersion !== WEBSITE_BROWSER_PROFILE_SCHEMA_VERSION) throw new Error("Website Profile schema is not supported");
|
|
125
|
-
const connectorId = requiredConnectorId(source.connectorId);
|
|
126
|
-
const allowedOrigins = array(source.allowedOrigins).map(exactHttpsOrigin);
|
|
127
|
-
if (allowedOrigins.length < 1 || allowedOrigins.length > 12 || new Set(allowedOrigins).size !== allowedOrigins.length) {
|
|
128
|
-
throw new Error("Website Profile allowed origins are invalid");
|
|
129
|
-
}
|
|
130
|
-
const loginUrl = new URL(String(source.loginUrl || ""));
|
|
131
|
-
if (loginUrl.protocol !== "https:" || !allowedOrigins.includes(loginUrl.origin) || loginUrl.username || loginUrl.password) {
|
|
132
|
-
throw new Error("Website Profile login URL is invalid");
|
|
133
|
-
}
|
|
134
|
-
const auth = record(source.auth, "Website Profile authentication recipe is invalid");
|
|
135
|
-
const connection = record(source.connection ?? {}, "Website Connection descriptor is invalid");
|
|
136
|
-
const localProfileKey = typeof connection.connectionId === "string" && /^bgc_[A-Za-z0-9_-]{43}$/.test(connection.connectionId)
|
|
137
|
-
? connection.connectionId
|
|
138
|
-
: undefined;
|
|
139
|
-
const headerSources = array(auth.headerSources).map((item) => {
|
|
140
|
-
const binding = record(item, "Website Profile storage selector is invalid");
|
|
141
|
-
const origin = exactHttpsOrigin(binding.origin);
|
|
142
|
-
const keySuffix = String(binding.keySuffix || "");
|
|
143
|
-
if (!allowedOrigins.includes(origin) || !keySuffix || keySuffix.length > 240) {
|
|
144
|
-
throw new Error("Website Profile storage selector is invalid");
|
|
145
|
-
}
|
|
146
|
-
return { keySuffix, origin };
|
|
147
|
-
});
|
|
148
|
-
if (headerSources.length > 12) throw new Error("Website Profile storage selector is invalid");
|
|
149
|
-
if (auth.sendCookies !== true && headerSources.length === 0) throw new Error("Website Profile authentication recipe is empty");
|
|
150
|
-
return {
|
|
151
|
-
allowedOrigins,
|
|
152
|
-
auth: { headerSources, sendCookies: auth.sendCookies === true },
|
|
153
|
-
connectorId,
|
|
154
|
-
...(localProfileKey ? { localProfileKey } : {}),
|
|
155
|
-
loginUrl: loginUrl.toString(),
|
|
156
|
-
profileSchemaVersion: WEBSITE_BROWSER_PROFILE_SCHEMA_VERSION
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function originStorage(client, sessionId, spec) {
|
|
161
|
-
await client.call("Page.enable", {}, sessionId);
|
|
162
|
-
await client.call("Runtime.enable", {}, sessionId);
|
|
163
|
-
const origins = [...new Set(spec.auth.headerSources.map((source) => source.origin))];
|
|
164
|
-
const result = [];
|
|
165
|
-
for (const origin of origins) {
|
|
166
|
-
await client.call("Page.navigate", { url: `${origin}/` }, sessionId);
|
|
167
|
-
await waitForDocument(client, sessionId, origin);
|
|
168
|
-
const evaluated = await client.call("Runtime.evaluate", {
|
|
169
|
-
expression: "Object.entries(window.localStorage).map(([name,value])=>({name,value}))",
|
|
170
|
-
returnByValue: true
|
|
171
|
-
}, sessionId);
|
|
172
|
-
result.push({ localStorage: array(evaluated?.result?.value), origin });
|
|
173
|
-
}
|
|
174
|
-
return result;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function browserCookies(client) {
|
|
178
|
-
const result = await client.call("Storage.getCookies");
|
|
179
|
-
return array(result.cookies);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function pageSession(client, spec) {
|
|
183
|
-
const targets = await client.call("Target.getTargets");
|
|
184
|
-
const pages = array(targets.targetInfos).filter((target) => target.type === "page");
|
|
185
|
-
const page = pages.find((target) => {
|
|
186
|
-
try {
|
|
187
|
-
return spec.allowedOrigins.includes(new URL(target.url).origin);
|
|
188
|
-
} catch {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
}) ?? pages[0];
|
|
192
|
-
if (!page?.targetId) throw new Error("Chrome did not create a page for Website Profile capture");
|
|
193
|
-
const attached = await client.call("Target.attachToTarget", { flatten: true, targetId: page.targetId });
|
|
194
|
-
if (!attached.sessionId) throw new Error("Could not attach to the local Chrome page");
|
|
195
|
-
return attached.sessionId;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async function waitForDocument(client, sessionId, expectedOrigin) {
|
|
199
|
-
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
200
|
-
const result = await client.call("Runtime.evaluate", {
|
|
201
|
-
expression: "({readyState:document.readyState,url:location.href})",
|
|
202
|
-
returnByValue: true
|
|
203
|
-
}, sessionId).catch(() => ({}));
|
|
204
|
-
const value = result?.result?.value;
|
|
205
|
-
if (documentIsReadyAtOrigin(value, expectedOrigin)) return;
|
|
206
|
-
await delay(100);
|
|
207
|
-
}
|
|
208
|
-
throw new Error(`Timed out while reading local storage for ${expectedOrigin}`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function waitForLoginConfirmation(spec, options) {
|
|
212
|
-
if (options.waitForLogin) return options.waitForLogin(spec);
|
|
213
|
-
const input = options.stdin ?? process.stdin;
|
|
214
|
-
const output = options.stderr ?? process.stderr;
|
|
215
|
-
if (!input.isTTY) throw new Error("Interactive login requires a TTY; use --no-prompt only for an already authenticated local Profile");
|
|
216
|
-
const nextStep = options.purpose === "local-execution"
|
|
217
|
-
? "Complete login, then press Enter here to run locally; nothing will be uploaded."
|
|
218
|
-
: "Complete login, then press Enter here to upload only the allowlisted session state.";
|
|
219
|
-
output.write(`Fargo opened an isolated local Chrome Profile for ${spec.connectorId}.\n${nextStep}\n`);
|
|
220
|
-
const prompt = createInterface({ input, output });
|
|
221
|
-
try {
|
|
222
|
-
await prompt.question("");
|
|
223
|
-
} finally {
|
|
224
|
-
prompt.close();
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async function findChromeExecutable(explicitPath, options) {
|
|
229
|
-
if (explicitPath) {
|
|
230
|
-
await access(explicitPath);
|
|
231
|
-
return resolve(explicitPath);
|
|
232
|
-
}
|
|
233
|
-
const currentPlatform = options.platform ?? platform();
|
|
234
|
-
const candidates = currentPlatform === "darwin"
|
|
235
|
-
? [
|
|
236
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
237
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
238
|
-
]
|
|
239
|
-
: currentPlatform === "win32"
|
|
240
|
-
? [
|
|
241
|
-
join(process.env.PROGRAMFILES ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"),
|
|
242
|
-
join(process.env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe")
|
|
243
|
-
]
|
|
244
|
-
: executableCandidates(["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"]);
|
|
245
|
-
for (const candidate of candidates) {
|
|
246
|
-
try {
|
|
247
|
-
await access(candidate);
|
|
248
|
-
return candidate;
|
|
249
|
-
} catch { /* Try the next supported browser path. */ }
|
|
250
|
-
}
|
|
251
|
-
throw new Error("Google Chrome or Chromium was not found; pass --chrome-path <path>");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function executableCandidates(names) {
|
|
255
|
-
return String(process.env.PATH || "").split(delimiter).flatMap((directory) => names.map((name) => join(directory, name)));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async function waitForDevToolsEndpoint(path, child, options) {
|
|
259
|
-
const timeoutMs = boundedInteger(options.launchTimeoutMs, 15_000, 1_000, 60_000);
|
|
260
|
-
const startedAt = Date.now();
|
|
261
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
262
|
-
if (child.exitCode !== null) throw new Error("Chrome exited before Website Profile capture started");
|
|
263
|
-
try {
|
|
264
|
-
const [port, browserPath] = (await readFile(path, "utf8")).trim().split(/\r?\n/);
|
|
265
|
-
if (/^\d+$/.test(port) && browserPath?.startsWith("/devtools/browser/")) {
|
|
266
|
-
return `ws://127.0.0.1:${port}${browserPath}`;
|
|
267
|
-
}
|
|
268
|
-
} catch (error) {
|
|
269
|
-
if (error?.code !== "ENOENT") throw error;
|
|
270
|
-
}
|
|
271
|
-
await delay(100);
|
|
272
|
-
}
|
|
273
|
-
throw new Error("Timed out waiting for Chrome DevTools");
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function waitForExit(child, timeoutMs) {
|
|
277
|
-
if (child.exitCode !== null) return Promise.resolve();
|
|
278
|
-
return new Promise((resolvePromise, reject) => {
|
|
279
|
-
const timeout = setTimeout(() => reject(new Error("Chrome did not close")), timeoutMs);
|
|
280
|
-
child.once("exit", () => {
|
|
281
|
-
clearTimeout(timeout);
|
|
282
|
-
resolvePromise();
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
class CdpClient {
|
|
288
|
-
constructor(socket) {
|
|
289
|
-
this.socket = socket;
|
|
290
|
-
this.nextId = 1;
|
|
291
|
-
this.pending = new Map();
|
|
292
|
-
socket.addEventListener("message", (event) => {
|
|
293
|
-
const message = JSON.parse(String(event.data));
|
|
294
|
-
if (!message.id) return;
|
|
295
|
-
const pending = this.pending.get(message.id);
|
|
296
|
-
if (!pending) return;
|
|
297
|
-
this.pending.delete(message.id);
|
|
298
|
-
if (message.error) pending.reject(new Error(message.error.message || "Chrome DevTools request failed"));
|
|
299
|
-
else pending.resolve(message.result ?? {});
|
|
300
|
-
});
|
|
301
|
-
socket.addEventListener("close", () => this.rejectPending(new Error("Chrome DevTools connection closed")));
|
|
302
|
-
socket.addEventListener("error", () => this.rejectPending(new Error("Chrome DevTools connection failed")));
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
static async connect(endpoint) {
|
|
306
|
-
const socket = new WebSocket(endpoint);
|
|
307
|
-
await new Promise((resolvePromise, reject) => {
|
|
308
|
-
socket.addEventListener("open", resolvePromise, { once: true });
|
|
309
|
-
socket.addEventListener("error", () => reject(new Error("Could not connect to local Chrome DevTools")), { once: true });
|
|
310
|
-
});
|
|
311
|
-
return new CdpClient(socket);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
call(method, params = {}, sessionId) {
|
|
315
|
-
const id = this.nextId++;
|
|
316
|
-
return new Promise((resolvePromise, reject) => {
|
|
317
|
-
this.pending.set(id, { reject, resolve: resolvePromise });
|
|
318
|
-
this.socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }));
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
rejectPending(error) {
|
|
323
|
-
for (const pending of this.pending.values()) pending.reject(error);
|
|
324
|
-
this.pending.clear();
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function normalizeCookie(cookie) {
|
|
329
|
-
const sameSite = ["Strict", "Lax", "None"].includes(cookie.sameSite) ? cookie.sameSite : undefined;
|
|
330
|
-
const expires = Number(cookie.expires);
|
|
331
|
-
return {
|
|
332
|
-
domain: String(cookie.domain),
|
|
333
|
-
...(Number.isFinite(expires) && expires > 0 ? { expires } : {}),
|
|
334
|
-
httpOnly: cookie.httpOnly === true,
|
|
335
|
-
name: String(cookie.name),
|
|
336
|
-
path: String(cookie.path || "/"),
|
|
337
|
-
...(sameSite ? { sameSite } : {}),
|
|
338
|
-
secure: cookie.secure === true,
|
|
339
|
-
value: String(cookie.value)
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function cookieAllowed(domain, allowedHosts) {
|
|
344
|
-
const host = String(domain || "").toLowerCase().replace(/^\./, "");
|
|
345
|
-
return allowedHosts.some((allowed) => host === allowed || host.endsWith(`.${allowed}`));
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function exactHttpsOrigin(value) {
|
|
349
|
-
const url = new URL(String(value || ""));
|
|
350
|
-
if (url.protocol !== "https:" || url.origin !== value || url.username || url.password) throw new Error("Website Profile origin is invalid");
|
|
351
|
-
return url.origin;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function requiredConnectorId(value) {
|
|
355
|
-
const id = String(value || "");
|
|
356
|
-
if (!/^[a-z][a-z0-9-]{1,62}[a-z0-9]$/.test(id)) throw new Error("Website Connector id is invalid");
|
|
357
|
-
return id;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function boundedInteger(value, fallback, minimum, maximum) {
|
|
361
|
-
const candidate = value === undefined ? fallback : Number(value);
|
|
362
|
-
if (!Number.isInteger(candidate) || candidate < minimum || candidate > maximum) throw new Error("Website browser timeout is invalid");
|
|
363
|
-
return candidate;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function array(value) {
|
|
367
|
-
return Array.isArray(value) ? value : [];
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function record(value, message) {
|
|
371
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(message);
|
|
372
|
-
return value;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function delay(milliseconds) {
|
|
376
|
-
return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
|
|
377
|
-
}
|