capiumbrowser 0.1.0

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/playwright.js ADDED
@@ -0,0 +1,260 @@
1
+ /**
2
+ * capiumbrowser/playwright -- launch a stealth Capium browser through vanilla Playwright.
3
+ *
4
+ * npm install capiumbrowser playwright-core
5
+ *
6
+ * const { launch, launchContext } = require('capiumbrowser/playwright');
7
+ * const { browser, page } = await launchContext({ seed: 42, platform: 'windows' });
8
+ * await page.goto('https://fingerprint.com/demo/');
9
+ *
10
+ * We use Playwright's NATIVE launch()/launchPersistentContext() with executablePath pointed
11
+ * at the capium bash wrapper (which adds GL/geoip/font/bluetooth/storage flags). No stealth
12
+ * plugin: capium's in-binary patches (001 -> developer_tools=false, 009 ->
13
+ * navigator.webdriver=false) provide the CDP hardening, so plain Playwright avoids the
14
+ * common automation tells.
15
+ *
16
+ * Public API:
17
+ * launch(options) -> Browser
18
+ * launchContext(options) -> {browser, context, page}
19
+ * launchPersistentContext(userDataDir, opts) -> BrowserContext (cookies/state persist)
20
+ */
21
+ 'use strict';
22
+
23
+ const config = require('./lib/config');
24
+ const download = require('./lib/download');
25
+ const human = require('./lib/human');
26
+ const license = require('./lib/license');
27
+ const { CapiumError, translateLaunchError, readLaunchStatus } = require('./lib/errors');
28
+ const { proxyAndGeoArgs, buildArgs, newStatusFile, clearStatusFile } =
29
+ require('./lib/launch-common');
30
+
31
+ function requireDriver() {
32
+ for (const name of ['playwright-core', 'playwright']) {
33
+ try {
34
+ return require(name);
35
+ } catch (e) {
36
+ if (e.code !== 'MODULE_NOT_FOUND') throw e;
37
+ }
38
+ }
39
+ throw new CapiumError(
40
+ 'playwright-core is not installed. Run: npm install capiumbrowser playwright-core');
41
+ }
42
+
43
+ /** Find the capium wrapper; if absent, try to download it (see lib/download). */
44
+ async function resolveBinary(binary, licenseKey) {
45
+ try {
46
+ return config.findBinary(binary);
47
+ } catch (e) {
48
+ if (e.code !== 'ENOENT') throw e;
49
+ return download.ensureBinary({ licenseKey });
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Common prologue for both launch flavors: preflight, binary, seed, args, env, status file.
55
+ */
56
+ async function prepare(opts) {
57
+ const {
58
+ seed = null, platform = 'windows', proxy = null, geoip = null, args = null,
59
+ stealthArgs = true, timezone = null, locale = null, extensionPaths = null,
60
+ binary = null, licenseKey = null, licenseServer = null,
61
+ licenseThroughProxy = false, licensePreflight = true,
62
+ } = opts;
63
+ if (licensePreflight) license.preflight(licenseKey, licenseServer);
64
+ const { key } = license.effective(licenseKey, licenseServer);
65
+ const binPath = await resolveBinary(binary, key);
66
+ const finalSeed = seed === null || seed === undefined ? config.newSeed() : seed;
67
+ const { launchOptions, args: proxyArgs } = proxyAndGeoArgs(proxy, geoip);
68
+ const launchArgs = proxyArgs.concat(buildArgs({
69
+ seed: finalSeed, platform, stealthArgs, timezone, locale, extensionPaths, extra: args,
70
+ }));
71
+ if (licenseThroughProxy) launchArgs.push('--license-through-proxy');
72
+ const env = license.childEnv(licenseKey, licenseServer);
73
+ const statusPath = newStatusFile(env);
74
+ return { binPath, finalSeed, launchOptions, launchArgs, env, statusPath };
75
+ }
76
+
77
+ async function throwTranslated(e, statusPath) {
78
+ const t = readLaunchStatus(statusPath) || translateLaunchError(e);
79
+ clearStatusFile(statusPath);
80
+ if (t) throw t;
81
+ throw e;
82
+ }
83
+
84
+ /**
85
+ * Escape hatch: the ready-made options object for Playwright's own chromium.launch /
86
+ * launchPersistentContext -- for callers that need to hold the launch themselves
87
+ * (framework integrations like Crawlee, custom pools). Same inputs as launch():
88
+ *
89
+ * const { chromium } = require('playwright-core');
90
+ * const browser = await chromium.launch(await buildLaunchOptions({ seed: 42 }));
91
+ *
92
+ * Note: launch()'s extra safety net (the fail-closed status file -> typed license errors)
93
+ * only reports through launch(); with a raw chromium.launch a license refusal surfaces as
94
+ * Playwright's own launch error.
95
+ */
96
+ async function buildLaunchOptions(opts = {}) {
97
+ const {
98
+ seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
99
+ binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
100
+ headless = false, ...rest
101
+ } = opts;
102
+ const prep = await prepare({
103
+ seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
104
+ binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
105
+ });
106
+ return {
107
+ executablePath: prep.binPath, headless, args: prep.launchArgs, env: prep.env,
108
+ ...prep.launchOptions, ...rest,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Default a launched Browser's newContext()/newPage() to viewport: null (no emulation)
114
+ * unless the caller passes a viewport themselves. Playwright's fixed 1280x720 emulated
115
+ * viewport leaves the page smaller than the window AND mismatching window.screen -- an
116
+ * incoherent fingerprint (the exact tell launchContext/launchPersistentContext already
117
+ * avoid; this closes the plain-launch() path too). An explicit viewport (including a
118
+ * user-passed null) is always honored.
119
+ */
120
+ function applyViewportDefaults(browser) {
121
+ for (const method of ['newContext', 'newPage']) {
122
+ const orig = browser[method];
123
+ if (typeof orig !== 'function') continue;
124
+ browser[method] = function (options = {}, ...more) {
125
+ if (!('viewport' in options)) options = { ...options, viewport: null };
126
+ return orig.call(this, options, ...more);
127
+ };
128
+ }
129
+ return browser;
130
+ }
131
+
132
+ /**
133
+ * Launch a Capium Browser (non-persistent; Playwright manages a temp profile).
134
+ *
135
+ * seed/platform : identity. stealthArgs: false drops the default fingerprint flags.
136
+ * proxy : "http://user:pass@host:port" | "socks5://host:port" | {server, username, ...}.
137
+ * geoip : geo coherence, resolved inside the binary. null (default) = OFF: no lookup
138
+ * runs, even with a proxy. true = ON: pins timezone/geo/WebRTC/language to the
139
+ * egress (proxy exit, or the host IP if proxyless). false = explicit opt-out.
140
+ * timezone/locale: explicit override (IANA tz + BCP-47 locale; in-binary spoof).
141
+ * licenseKey/licenseServer: pass the license programmatically (else read from
142
+ * CAPIUM_LICENSE_KEY / ~/.capium/license). The binary self-licenses; the key
143
+ * is handed to it via the environment, never on argv.
144
+ * licensePreflight: fail fast with a typed CapiumConfigError when no license key is set
145
+ * (default true; instant + offline). Set false for a dev build that runs
146
+ * without enforcement.
147
+ * Remaining options are forwarded to Playwright's chromium.launch (e.g. slowMo: ...).
148
+ */
149
+ async function launch(opts = {}) {
150
+ const {
151
+ seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
152
+ binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
153
+ headless = false, ...rest
154
+ } = opts;
155
+ const prep = await prepare({
156
+ seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
157
+ binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
158
+ });
159
+ const pw = requireDriver();
160
+ let browser;
161
+ try {
162
+ browser = await pw.chromium.launch({
163
+ executablePath: prep.binPath, headless, args: prep.launchArgs, env: prep.env,
164
+ ...prep.launchOptions, ...rest,
165
+ });
166
+ } catch (e) {
167
+ await throwTranslated(e, prep.statusPath);
168
+ }
169
+ clearStatusFile(prep.statusPath);
170
+ browser._capiumSeed = prep.finalSeed;
171
+ return applyViewportDefaults(browser);
172
+ }
173
+
174
+ /**
175
+ * Launch a persistent context (cookies/localStorage/state persist in userDataDir).
176
+ *
177
+ * Same options as launch(); returns a BrowserContext.
178
+ */
179
+ async function launchPersistentContext(userDataDir, opts = {}) {
180
+ const {
181
+ seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
182
+ binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
183
+ headless = false, ...rest
184
+ } = opts;
185
+ const prep = await prepare({
186
+ seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
187
+ binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
188
+ });
189
+ // Default to the page filling the window (see launchContext) unless the caller pinned a
190
+ // viewport themselves. viewport: null is Playwright-JS for "no fixed viewport".
191
+ if (!('viewport' in rest)) rest.viewport = null;
192
+ const pw = requireDriver();
193
+ let ctx;
194
+ try {
195
+ ctx = await pw.chromium.launchPersistentContext(userDataDir, {
196
+ executablePath: prep.binPath, headless, args: prep.launchArgs, env: prep.env,
197
+ ...prep.launchOptions, ...rest,
198
+ });
199
+ } catch (e) {
200
+ await throwTranslated(e, prep.statusPath);
201
+ }
202
+ clearStatusFile(prep.statusPath);
203
+ ctx._capiumSeed = prep.finalSeed;
204
+ return ctx;
205
+ }
206
+
207
+ /**
208
+ * Size a modest windowed browser that fits within the seed's spoofed screen.
209
+ *
210
+ * A real window is never larger than its display and is usually NOT maximized. Caps the
211
+ * window to <= the available screen with a slight offset. No-op on failure/headless.
212
+ */
213
+ async function fitWindow(browser, context, page) {
214
+ try {
215
+ const scr = await page.evaluate(() => ({ w: screen.availWidth, h: screen.availHeight }));
216
+ if (!scr || !scr.w || !scr.h) return;
217
+ const aw = Math.trunc(scr.w);
218
+ const ah = Math.trunc(scr.h);
219
+ const w = Math.min(aw, 1440);
220
+ const h = Math.min(ah, 900);
221
+ const left = Math.max(0, Math.floor((aw - w) / 5));
222
+ const top = Math.max(0, Math.floor((ah - h) / 6));
223
+ const pcdp = await context.newCDPSession(page);
224
+ const { targetInfo } = await pcdp.send('Target.getTargetInfo');
225
+ const bcdp = await browser.newBrowserCDPSession();
226
+ const { windowId } = await bcdp.send('Browser.getWindowForTarget',
227
+ { targetId: targetInfo.targetId });
228
+ await bcdp.send('Browser.setWindowBounds', {
229
+ windowId,
230
+ bounds: { left, top, width: w, height: h, windowState: 'normal' },
231
+ });
232
+ } catch {}
233
+ }
234
+
235
+ /**
236
+ * Convenience: launch() + first context + a page (optionally navigated to `url`).
237
+ *
238
+ * humanize: true attaches page.humanMove/humanClick/humanType/humanScroll (see lib/human).
239
+ * Returns {browser, context, page}. page.goto(url) is done here so tampering stays clean
240
+ * (driven navigation), matching our launcher behavior.
241
+ */
242
+ async function launchContext(opts = {}) {
243
+ const { url = null, humanize = false, humanPreset = 'default',
244
+ headless = false, ...rest } = opts;
245
+ const browser = await launch({ headless, ...rest });
246
+ // viewport: null -- let the page fill the real window (fitWindow sizes the window to the
247
+ // persona's screen). A fixed 1280x720 emulated viewport would leave the page smaller than
248
+ // the window AND mismatch window.screen -- an incoherent fingerprint.
249
+ const context = browser.contexts()[0] || (await browser.newContext({ viewport: null }));
250
+ if (humanize) human.humanize(context, humanPreset);
251
+ const page = context.pages()[0] || (await context.newPage());
252
+ if (!headless) await fitWindow(browser, context, page); // modest window <= spoofed screen
253
+ if (url) await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
254
+ return { browser, context, page };
255
+ }
256
+
257
+ module.exports = {
258
+ launch, launchContext, launchPersistentContext, fitWindow, buildLaunchOptions,
259
+ _applyViewportDefaults: applyViewportDefaults,
260
+ };
package/puppeteer.js ADDED
@@ -0,0 +1,233 @@
1
+ /**
2
+ * capiumbrowser/puppeteer -- launch a stealth Capium browser through vanilla Puppeteer.
3
+ *
4
+ * npm install capiumbrowser puppeteer-core
5
+ *
6
+ * const { launch, launchContext } = require('capiumbrowser/puppeteer');
7
+ * const { browser, page } = await launchContext({ seed: 42, platform: 'windows' });
8
+ * await page.goto('https://fingerprint.com/demo/');
9
+ *
10
+ * Same contract as the Playwright front-end: Puppeteer's NATIVE launch() with executablePath
11
+ * pointed at the capium wrapper; the binary's own patches (001/009) do the CDP hardening.
12
+ * Puppeteer-specific defaults applied here:
13
+ * * headless: false (Puppeteer defaults to headless; a windowed browser is the
14
+ * coherent persona default, matching the Playwright path)
15
+ * * defaultViewport: null (Puppeteer's fixed 800x600 emulated viewport would leave the
16
+ * page smaller than the window and mismatch window.screen)
17
+ * * '--enable-automation' is stripped from Puppeteer's default args (an automation banner +
18
+ * webdriver tell the Playwright native path never adds)
19
+ *
20
+ * Public API:
21
+ * launch(options) -> Browser
22
+ * launchContext(options) -> {browser, page}
23
+ * launchPersistentContext(userDataDir, opts) -> Browser (profile persists in userDataDir)
24
+ */
25
+ 'use strict';
26
+
27
+ const config = require('./lib/config');
28
+ const download = require('./lib/download');
29
+ const human = require('./lib/human');
30
+ const license = require('./lib/license');
31
+ const { CapiumError, translateLaunchError, readLaunchStatus } = require('./lib/errors');
32
+ const { proxyAndGeoArgs, buildArgs, newStatusFile, clearStatusFile } =
33
+ require('./lib/launch-common');
34
+
35
+ function requireDriver() {
36
+ for (const name of ['puppeteer-core', 'puppeteer']) {
37
+ try {
38
+ return require(name);
39
+ } catch (e) {
40
+ if (e.code !== 'MODULE_NOT_FOUND') throw e;
41
+ }
42
+ }
43
+ throw new CapiumError(
44
+ 'puppeteer-core is not installed. Run: npm install capiumbrowser puppeteer-core');
45
+ }
46
+
47
+ async function resolveBinary(binary, licenseKey) {
48
+ try {
49
+ return config.findBinary(binary);
50
+ } catch (e) {
51
+ if (e.code !== 'ENOENT') throw e;
52
+ return download.ensureBinary({ licenseKey });
53
+ }
54
+ }
55
+
56
+ async function prepare(opts) {
57
+ const {
58
+ seed = null, platform = 'windows', proxy = null, geoip = null, args = null,
59
+ stealthArgs = true, timezone = null, locale = null, extensionPaths = null,
60
+ binary = null, licenseKey = null, licenseServer = null,
61
+ licenseThroughProxy = false, licensePreflight = true,
62
+ } = opts;
63
+ if (licensePreflight) license.preflight(licenseKey, licenseServer);
64
+ const { key } = license.effective(licenseKey, licenseServer);
65
+ const binPath = await resolveBinary(binary, key);
66
+ const finalSeed = seed === null || seed === undefined ? config.newSeed() : seed;
67
+ const { launchOptions, args: proxyArgs } = proxyAndGeoArgs(proxy, geoip);
68
+ const launchArgs = proxyArgs.concat(buildArgs({
69
+ seed: finalSeed, platform, stealthArgs, timezone, locale, extensionPaths, extra: args,
70
+ }));
71
+ if (licenseThroughProxy) launchArgs.push('--license-through-proxy');
72
+ const env = license.childEnv(licenseKey, licenseServer);
73
+ const statusPath = newStatusFile(env);
74
+ return { binPath, finalSeed, launchOptions, launchArgs, env, statusPath };
75
+ }
76
+
77
+ /**
78
+ * Wire the CDP-auth escape-hatch credentials (CAPIUM_INLINE_PROXY_AUTH=0) into every page.
79
+ * Puppeteer has no launch-level proxy credential option; page.authenticate answers the 407.
80
+ */
81
+ function wireProxyAuth(browser, proxyOption) {
82
+ if (!proxyOption || !proxyOption.username) return;
83
+ const creds = { username: proxyOption.username, password: proxyOption.password || '' };
84
+ browser.on('targetcreated', async (target) => {
85
+ try {
86
+ const page = await target.page();
87
+ if (page) await page.authenticate(creds);
88
+ } catch {}
89
+ });
90
+ }
91
+
92
+ async function throwTranslated(e, statusPath) {
93
+ const t = readLaunchStatus(statusPath) || translateLaunchError(e);
94
+ clearStatusFile(statusPath);
95
+ if (t) throw t;
96
+ throw e;
97
+ }
98
+
99
+ /**
100
+ * Escape hatch: the ready-made options object for puppeteer.launch itself -- for callers
101
+ * that need to hold the launch (framework integrations, custom pools). Same inputs as
102
+ * launch():
103
+ *
104
+ * const puppeteer = require('puppeteer-core');
105
+ * const browser = await puppeteer.launch(await buildLaunchOptions({ seed: 42 }));
106
+ *
107
+ * Note: launch()'s extra safety nets (fail-closed status file -> typed license errors, and
108
+ * the CDP proxy-auth wiring) only apply through launch().
109
+ */
110
+ async function buildLaunchOptions(opts = {}) {
111
+ const {
112
+ seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
113
+ binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
114
+ headless = false, ignoreDefaultArgs, ...rest
115
+ } = opts;
116
+ const prep = await prepare({
117
+ seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
118
+ binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
119
+ });
120
+ const proxyOption = prep.launchOptions.proxy || null;
121
+ const launchArgs = [...prep.launchArgs];
122
+ if (proxyOption) {
123
+ launchArgs.push(`--proxy-server=${proxyOption.server}`);
124
+ if (proxyOption.bypass) launchArgs.push(`--proxy-bypass-list=${proxyOption.bypass}`);
125
+ }
126
+ return {
127
+ executablePath: prep.binPath,
128
+ headless,
129
+ args: launchArgs,
130
+ env: prep.env,
131
+ defaultViewport: 'defaultViewport' in rest ? rest.defaultViewport : null,
132
+ ignoreDefaultArgs: ignoreDefaultArgs === undefined ? ['--enable-automation'] : ignoreDefaultArgs,
133
+ ...rest,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Launch a Capium Browser via Puppeteer. Same options as the Playwright front-end
139
+ * (seed, platform, proxy, geoip, timezone, locale, licenseKey, ...); remaining options are
140
+ * forwarded to puppeteer.launch (e.g. slowMo, userDataDir, ...).
141
+ */
142
+ async function launch(opts = {}) {
143
+ const {
144
+ seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
145
+ binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
146
+ headless = false, ignoreDefaultArgs, ...rest
147
+ } = opts;
148
+ const prep = await prepare({
149
+ seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
150
+ binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
151
+ });
152
+ const puppeteer = requireDriver();
153
+ // The CDP escape hatch puts {proxy: {server, username?, ...}} in launchOptions; Puppeteer
154
+ // takes the server as a plain flag and the credentials via page.authenticate.
155
+ const proxyOption = prep.launchOptions.proxy || null;
156
+ const launchArgs = [...prep.launchArgs];
157
+ if (proxyOption) {
158
+ launchArgs.push(`--proxy-server=${proxyOption.server}`);
159
+ if (proxyOption.bypass) launchArgs.push(`--proxy-bypass-list=${proxyOption.bypass}`);
160
+ }
161
+ // Strip Puppeteer's --enable-automation unless the caller took over ignoreDefaultArgs.
162
+ const ida = ignoreDefaultArgs === undefined ? ['--enable-automation'] : ignoreDefaultArgs;
163
+ let browser;
164
+ try {
165
+ browser = await puppeteer.launch({
166
+ executablePath: prep.binPath,
167
+ headless,
168
+ args: launchArgs,
169
+ env: prep.env,
170
+ defaultViewport: 'defaultViewport' in rest ? rest.defaultViewport : null,
171
+ ignoreDefaultArgs: ida,
172
+ ...rest,
173
+ });
174
+ } catch (e) {
175
+ await throwTranslated(e, prep.statusPath);
176
+ }
177
+ clearStatusFile(prep.statusPath);
178
+ wireProxyAuth(browser, proxyOption);
179
+ browser._capiumSeed = prep.finalSeed;
180
+ return browser;
181
+ }
182
+
183
+ /**
184
+ * Launch with a persistent profile (cookies/localStorage/state persist in userDataDir).
185
+ * Puppeteer has no separate persistent-context API -- it's launch() + userDataDir.
186
+ * Returns the Browser.
187
+ */
188
+ async function launchPersistentContext(userDataDir, opts = {}) {
189
+ return launch({ ...opts, userDataDir });
190
+ }
191
+
192
+ /** Size a modest windowed browser that fits within the seed's spoofed screen. */
193
+ async function fitWindow(browser, page) {
194
+ try {
195
+ const scr = await page.evaluate(() => ({ w: screen.availWidth, h: screen.availHeight }));
196
+ if (!scr || !scr.w || !scr.h) return;
197
+ const aw = Math.trunc(scr.w);
198
+ const ah = Math.trunc(scr.h);
199
+ const w = Math.min(aw, 1440);
200
+ const h = Math.min(ah, 900);
201
+ const left = Math.max(0, Math.floor((aw - w) / 5));
202
+ const top = Math.max(0, Math.floor((ah - h) / 6));
203
+ const cdp = await page.createCDPSession();
204
+ const { targetInfo } = await cdp.send('Target.getTargetInfo');
205
+ const { windowId } = await cdp.send('Browser.getWindowForTarget',
206
+ { targetId: targetInfo.targetId });
207
+ await cdp.send('Browser.setWindowBounds', {
208
+ windowId,
209
+ bounds: { left, top, width: w, height: h, windowState: 'normal' },
210
+ });
211
+ await cdp.detach();
212
+ } catch {}
213
+ }
214
+
215
+ /**
216
+ * Convenience: launch() + first page (optionally navigated to `url`).
217
+ *
218
+ * humanize: true attaches page.humanMove/humanClick/humanType/humanScroll (see lib/human).
219
+ * Returns {browser, page}.
220
+ */
221
+ async function launchContext(opts = {}) {
222
+ const { url = null, humanize = false, humanPreset = 'default',
223
+ headless = false, ...rest } = opts;
224
+ const browser = await launch({ headless, ...rest });
225
+ const pages = await browser.pages();
226
+ const page = pages[0] || (await browser.newPage());
227
+ if (humanize) human.humanize(page, humanPreset);
228
+ if (!headless) await fitWindow(browser, page);
229
+ if (url) await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
230
+ return { browser, page };
231
+ }
232
+
233
+ module.exports = { launch, launchContext, launchPersistentContext, fitWindow, buildLaunchOptions };