@monoes/monobrowse 1.0.2 → 1.0.4
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/dist/src/__tests__/batch-eval-parsing.test.d.ts +2 -0
- package/dist/src/__tests__/batch-eval-parsing.test.d.ts.map +1 -0
- package/dist/src/__tests__/batch-eval-parsing.test.js +47 -0
- package/dist/src/__tests__/batch-eval-parsing.test.js.map +1 -0
- package/dist/src/__tests__/output.test.d.ts +2 -0
- package/dist/src/__tests__/output.test.d.ts.map +1 -0
- package/dist/src/__tests__/output.test.js +54 -0
- package/dist/src/__tests__/output.test.js.map +1 -0
- package/dist/src/__tests__/ref-cache.test.d.ts +2 -0
- package/dist/src/__tests__/ref-cache.test.d.ts.map +1 -0
- package/dist/src/__tests__/ref-cache.test.js +145 -0
- package/dist/src/__tests__/ref-cache.test.js.map +1 -0
- package/dist/src/browser/action-builder/analyzer.d.ts +0 -0
- package/dist/src/browser/action-builder/analyzer.d.ts.map +0 -0
- package/dist/src/browser/action-builder/analyzer.js +0 -0
- package/dist/src/browser/action-builder/analyzer.js.map +1 -1
- package/dist/src/browser/action-builder/types.d.ts +0 -0
- package/dist/src/browser/action-builder/types.d.ts.map +0 -0
- package/dist/src/browser/action-builder/types.js +0 -0
- package/dist/src/browser/action-builder/types.js.map +0 -0
- package/dist/src/browser/actions.d.ts +1 -1
- package/dist/src/browser/actions.d.ts.map +1 -1
- package/dist/src/browser/actions.js +19 -2
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/adapters/gemini.d.ts +0 -0
- package/dist/src/browser/adapters/gemini.d.ts.map +0 -0
- package/dist/src/browser/adapters/gemini.js +0 -0
- package/dist/src/browser/adapters/gemini.js.map +0 -0
- package/dist/src/browser/adapters/google.d.ts +0 -0
- package/dist/src/browser/adapters/google.d.ts.map +0 -0
- package/dist/src/browser/adapters/google.js +0 -0
- package/dist/src/browser/adapters/google.js.map +0 -0
- package/dist/src/browser/adapters/index.d.ts +0 -0
- package/dist/src/browser/adapters/index.d.ts.map +0 -0
- package/dist/src/browser/adapters/index.js +0 -0
- package/dist/src/browser/adapters/index.js.map +0 -0
- package/dist/src/browser/adapters/instagram.d.ts +0 -0
- package/dist/src/browser/adapters/instagram.d.ts.map +0 -0
- package/dist/src/browser/adapters/instagram.js +0 -0
- package/dist/src/browser/adapters/instagram.js.map +0 -0
- package/dist/src/browser/adapters/linkedin.d.ts +0 -0
- package/dist/src/browser/adapters/linkedin.d.ts.map +0 -0
- package/dist/src/browser/adapters/linkedin.js +0 -0
- package/dist/src/browser/adapters/linkedin.js.map +0 -0
- package/dist/src/browser/adapters/microsoft.d.ts +0 -0
- package/dist/src/browser/adapters/microsoft.d.ts.map +0 -0
- package/dist/src/browser/adapters/microsoft.js +0 -0
- package/dist/src/browser/adapters/microsoft.js.map +0 -0
- package/dist/src/browser/adapters/x.d.ts +0 -0
- package/dist/src/browser/adapters/x.d.ts.map +0 -0
- package/dist/src/browser/adapters/x.js +0 -0
- package/dist/src/browser/adapters/x.js.map +0 -0
- package/dist/src/browser/batch.d.ts +0 -0
- package/dist/src/browser/batch.d.ts.map +0 -0
- package/dist/src/browser/batch.js +0 -0
- package/dist/src/browser/batch.js.map +0 -0
- package/dist/src/browser/browser.d.ts +10 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +111 -3
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts +0 -0
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +1 -1
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/console-log.d.ts +0 -0
- package/dist/src/browser/console-log.d.ts.map +0 -0
- package/dist/src/browser/console-log.js +0 -0
- package/dist/src/browser/console-log.js.map +0 -0
- package/dist/src/browser/dashboard/api-types.d.ts +0 -0
- package/dist/src/browser/dashboard/api-types.d.ts.map +0 -0
- package/dist/src/browser/dashboard/api-types.js +0 -0
- package/dist/src/browser/dashboard/api-types.js.map +0 -0
- package/dist/src/browser/dashboard/server.d.ts +0 -0
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +37 -2
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/browser/dashboard/ui.html +1 -1
- package/dist/src/browser/dialog.d.ts +0 -0
- package/dist/src/browser/dialog.d.ts.map +0 -0
- package/dist/src/browser/dialog.js +0 -0
- package/dist/src/browser/dialog.js.map +0 -0
- package/dist/src/browser/emulation.d.ts +0 -0
- package/dist/src/browser/emulation.d.ts.map +0 -0
- package/dist/src/browser/emulation.js +0 -0
- package/dist/src/browser/emulation.js.map +0 -0
- package/dist/src/browser/find.d.ts +0 -0
- package/dist/src/browser/find.d.ts.map +0 -0
- package/dist/src/browser/find.js +0 -0
- package/dist/src/browser/find.js.map +1 -1
- package/dist/src/browser/har.d.ts +0 -0
- package/dist/src/browser/har.d.ts.map +0 -0
- package/dist/src/browser/har.js +0 -0
- package/dist/src/browser/har.js.map +0 -0
- package/dist/src/browser/index.d.ts +1 -0
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +1 -0
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/network.d.ts +7 -0
- package/dist/src/browser/network.d.ts.map +1 -1
- package/dist/src/browser/network.js +10 -0
- package/dist/src/browser/network.js.map +1 -1
- package/dist/src/browser/pdf.d.ts +0 -0
- package/dist/src/browser/pdf.d.ts.map +0 -0
- package/dist/src/browser/pdf.js +0 -0
- package/dist/src/browser/pdf.js.map +1 -1
- package/dist/src/browser/profiler.d.ts +0 -0
- package/dist/src/browser/profiler.d.ts.map +0 -0
- package/dist/src/browser/profiler.js +0 -0
- package/dist/src/browser/profiler.js.map +1 -1
- package/dist/src/browser/record.d.ts +3 -0
- package/dist/src/browser/record.d.ts.map +1 -1
- package/dist/src/browser/record.js +37 -5
- package/dist/src/browser/record.js.map +1 -1
- package/dist/src/browser/ref-cache.d.ts +41 -0
- package/dist/src/browser/ref-cache.d.ts.map +1 -0
- package/dist/src/browser/ref-cache.js +120 -0
- package/dist/src/browser/ref-cache.js.map +1 -0
- package/dist/src/browser/screenshot.d.ts +0 -0
- package/dist/src/browser/screenshot.d.ts.map +0 -0
- package/dist/src/browser/screenshot.js +0 -0
- package/dist/src/browser/screenshot.js.map +1 -1
- package/dist/src/browser/session.d.ts +0 -0
- package/dist/src/browser/session.d.ts.map +1 -1
- package/dist/src/browser/session.js +12 -7
- package/dist/src/browser/session.js.map +1 -1
- package/dist/src/browser/snapshot.d.ts +0 -0
- package/dist/src/browser/snapshot.d.ts.map +0 -0
- package/dist/src/browser/snapshot.js +0 -0
- package/dist/src/browser/snapshot.js.map +1 -1
- package/dist/src/browser/storage.d.ts +0 -0
- package/dist/src/browser/storage.d.ts.map +0 -0
- package/dist/src/browser/storage.js +0 -0
- package/dist/src/browser/storage.js.map +0 -0
- package/dist/src/browser/tabs.d.ts +1 -1
- package/dist/src/browser/tabs.d.ts.map +1 -1
- package/dist/src/browser/tabs.js +20 -4
- package/dist/src/browser/tabs.js.map +1 -1
- package/dist/src/browser/trace.d.ts +0 -0
- package/dist/src/browser/trace.d.ts.map +0 -0
- package/dist/src/browser/trace.js +0 -0
- package/dist/src/browser/trace.js.map +1 -1
- package/dist/src/browser/types.d.ts +0 -0
- package/dist/src/browser/types.d.ts.map +0 -0
- package/dist/src/browser/types.js +0 -0
- package/dist/src/browser/types.js.map +0 -0
- package/dist/src/browser/vitals.d.ts +0 -0
- package/dist/src/browser/vitals.d.ts.map +0 -0
- package/dist/src/browser/vitals.js +0 -0
- package/dist/src/browser/vitals.js.map +0 -0
- package/dist/src/browser/wait.d.ts +0 -0
- package/dist/src/browser/wait.d.ts.map +0 -0
- package/dist/src/browser/wait.js +0 -0
- package/dist/src/browser/wait.js.map +0 -0
- package/dist/src/cli/action.d.ts +0 -0
- package/dist/src/cli/action.d.ts.map +1 -1
- package/dist/src/cli/action.js +87 -2
- package/dist/src/cli/action.js.map +1 -1
- package/dist/src/cli/commands.d.ts +22 -0
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +276 -28
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/output.d.ts +0 -0
- package/dist/src/cli/output.d.ts.map +1 -1
- package/dist/src/cli/output.js +0 -0
- package/dist/src/cli/output.js.map +0 -0
- package/dist/src/cli/platform.d.ts +0 -0
- package/dist/src/cli/platform.d.ts.map +0 -0
- package/dist/src/cli/platform.js +0 -0
- package/dist/src/cli/platform.js.map +0 -0
- package/dist/src/cli/types.d.ts +0 -0
- package/dist/src/cli/types.d.ts.map +0 -0
- package/dist/src/cli/types.js +0 -0
- package/dist/src/cli/types.js.map +0 -0
- package/dist/src/cli.d.ts +0 -0
- package/dist/src/cli.d.ts.map +0 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +0 -0
- package/dist/src/index.d.ts.map +0 -0
- package/dist/src/index.js +0 -0
- package/dist/src/index.js.map +0 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/__tests__/batch-eval-parsing.test.ts +52 -0
- package/src/__tests__/output.test.ts +62 -0
- package/src/__tests__/ref-cache.test.ts +175 -0
- package/src/browser/action-builder/analyzer.ts +0 -0
- package/src/browser/action-builder/types.ts +0 -0
- package/src/browser/actions.ts +22 -2
- package/src/browser/adapters/gemini.ts +0 -0
- package/src/browser/adapters/google.ts +0 -0
- package/src/browser/adapters/index.ts +0 -0
- package/src/browser/adapters/instagram.ts +0 -0
- package/src/browser/adapters/linkedin.ts +0 -0
- package/src/browser/adapters/microsoft.ts +0 -0
- package/src/browser/adapters/x.ts +0 -0
- package/src/browser/batch.ts +0 -0
- package/src/browser/browser.ts +115 -2
- package/src/browser/cdp.ts +1 -1
- package/src/browser/console-log.ts +0 -0
- package/src/browser/dashboard/api-types.ts +0 -0
- package/src/browser/dashboard/server.ts +37 -2
- package/src/browser/dashboard/ui.html +1 -1
- package/src/browser/dialog.ts +0 -0
- package/src/browser/emulation.ts +0 -0
- package/src/browser/find.ts +0 -0
- package/src/browser/har.ts +0 -0
- package/src/browser/index.ts +1 -0
- package/src/browser/network.ts +11 -0
- package/src/browser/pdf.ts +0 -0
- package/src/browser/profiler.ts +0 -0
- package/src/browser/record.ts +43 -6
- package/src/browser/ref-cache.ts +141 -0
- package/src/browser/screenshot.ts +0 -0
- package/src/browser/session.ts +13 -7
- package/src/browser/snapshot.ts +0 -0
- package/src/browser/storage.ts +0 -0
- package/src/browser/tabs.ts +36 -7
- package/src/browser/trace.ts +0 -0
- package/src/browser/types.ts +0 -0
- package/src/browser/vitals.ts +0 -0
- package/src/browser/wait.ts +0 -0
- package/src/cli/action.ts +85 -3
- package/src/cli/commands.ts +272 -28
- package/src/cli/output.ts +0 -0
- package/src/cli/platform.ts +0 -0
- package/src/cli/types.ts +0 -0
- package/src/cli.ts +0 -0
- package/src/index.ts +0 -0
- package/dist/package.json +0 -71
- package/dist/src/browser/playbook/browser-handlers.d.ts +0 -3
- package/dist/src/browser/playbook/browser-handlers.d.ts.map +0 -1
- package/dist/src/browser/playbook/browser-handlers.js +0 -377
- package/dist/src/browser/playbook/browser-handlers.js.map +0 -1
- package/dist/src/browser/playbook/builtin-handlers.d.ts +0 -3
- package/dist/src/browser/playbook/builtin-handlers.d.ts.map +0 -1
- package/dist/src/browser/playbook/builtin-handlers.js +0 -364
- package/dist/src/browser/playbook/builtin-handlers.js.map +0 -1
- package/dist/src/browser/playbook/index.d.ts +0 -3
- package/dist/src/browser/playbook/index.d.ts.map +0 -1
- package/dist/src/browser/playbook/index.js +0 -3
- package/dist/src/browser/playbook/index.js.map +0 -1
- package/dist/src/browser/playbook/store.d.ts +0 -8
- package/dist/src/browser/playbook/store.d.ts.map +0 -1
- package/dist/src/browser/playbook/store.js +0 -87
- package/dist/src/browser/playbook/store.js.map +0 -1
- package/dist/src/browser/workflow/builtin-handlers.d.ts +0 -3
- package/dist/src/browser/workflow/builtin-handlers.d.ts.map +0 -1
- package/dist/src/browser/workflow/builtin-handlers.js +0 -343
- package/dist/src/browser/workflow/builtin-handlers.js.map +0 -1
- package/dist/src/browser/workflow/engine.d.ts +0 -10
- package/dist/src/browser/workflow/engine.d.ts.map +0 -1
- package/dist/src/browser/workflow/engine.js +0 -174
- package/dist/src/browser/workflow/engine.js.map +0 -1
- package/dist/src/browser/workflow/expression.d.ts +0 -4
- package/dist/src/browser/workflow/expression.d.ts.map +0 -1
- package/dist/src/browser/workflow/expression.js +0 -89
- package/dist/src/browser/workflow/expression.js.map +0 -1
- package/dist/src/browser/workflow/index.d.ts +0 -6
- package/dist/src/browser/workflow/index.d.ts.map +0 -1
- package/dist/src/browser/workflow/index.js +0 -6
- package/dist/src/browser/workflow/index.js.map +0 -1
- package/dist/src/browser/workflow/store.d.ts +0 -8
- package/dist/src/browser/workflow/store.d.ts.map +0 -1
- package/dist/src/browser/workflow/store.js +0 -87
- package/dist/src/browser/workflow/store.js.map +0 -1
- package/dist/src/browser/workflow/types.d.ts +0 -62
- package/dist/src/browser/workflow/types.d.ts.map +0 -1
- package/dist/src/browser/workflow/types.js +0 -2
- package/dist/src/browser/workflow/types.js.map +0 -1
- package/dist/src/cli/playbook.d.ts +0 -3
- package/dist/src/cli/playbook.d.ts.map +0 -1
- package/dist/src/cli/playbook.js +0 -265
- package/dist/src/cli/playbook.js.map +0 -1
- package/dist/src/cli/workflow.d.ts +0 -2
- package/dist/src/cli/workflow.d.ts.map +0 -1
- package/dist/src/cli/workflow.js +0 -3
- package/dist/src/cli/workflow.js.map +0 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseBatchCommandLine } from '../cli/commands.js';
|
|
3
|
+
|
|
4
|
+
describe('parseBatchCommandLine — eval expressions inside a batch command string', () => {
|
|
5
|
+
it('does not strip string-literal quotes from a raw JS expression passed to eval', () => {
|
|
6
|
+
// Regression: tokenizeBatchCommand's shell-style quote-stripping (correct for
|
|
7
|
+
// space-separated args like `fill @e1 "some value"`) previously ran on eval's
|
|
8
|
+
// expression too, silently deleting the expression's OWN string-literal quotes
|
|
9
|
+
// and turning `document.querySelector('a')` into `document.querySelector(a)` —
|
|
10
|
+
// a reference to an undefined identifier instead of a string literal.
|
|
11
|
+
const input = "eval document.querySelector('meta[name=mm-token]').content";
|
|
12
|
+
const { subName, subArgs } = parseBatchCommandLine(input);
|
|
13
|
+
expect(subName).toBe('eval');
|
|
14
|
+
expect(subArgs).toEqual(["document.querySelector('meta[name=mm-token]').content"]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('preserves double-quoted string literals in the expression too', () => {
|
|
18
|
+
const input = 'eval document.querySelector("body").tagName';
|
|
19
|
+
const { subName, subArgs } = parseBatchCommandLine(input);
|
|
20
|
+
expect(subName).toBe('eval');
|
|
21
|
+
expect(subArgs).toEqual(['document.querySelector("body").tagName']);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('preserves an expression with multiple quoted segments and internal spaces', () => {
|
|
25
|
+
const input = "eval [...document.querySelectorAll('li')].map(x => x.textContent.trim())";
|
|
26
|
+
const { subName, subArgs } = parseBatchCommandLine(input);
|
|
27
|
+
expect(subName).toBe('eval');
|
|
28
|
+
expect(subArgs).toEqual(["[...document.querySelectorAll('li')].map(x => x.textContent.trim())"]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('still recognizes --json and --max-output flags placed before the expression', () => {
|
|
32
|
+
const input = "eval --json --max-output 100 document.querySelector('a').href";
|
|
33
|
+
const { subName, subArgs, flags } = parseBatchCommandLine(input);
|
|
34
|
+
expect(subName).toBe('eval');
|
|
35
|
+
expect(flags).toMatchObject({ json: true, 'max-output': 100 });
|
|
36
|
+
expect(subArgs).toEqual(["document.querySelector('a').href"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('leaves non-eval commands tokenized exactly as before (no behavior change)', () => {
|
|
40
|
+
const input = 'fill @e1 "some value with spaces"';
|
|
41
|
+
const { subName, subArgs } = parseBatchCommandLine(input);
|
|
42
|
+
expect(subName).toBe('fill');
|
|
43
|
+
expect(subArgs).toEqual(['@e1', 'some value with spaces']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('leaves a simple multi-token command tokenized exactly as before', () => {
|
|
47
|
+
const input = 'open https://example.com --port 9333';
|
|
48
|
+
const { subName, subArgs } = parseBatchCommandLine(input);
|
|
49
|
+
expect(subName).toBe('open');
|
|
50
|
+
expect(subArgs).toEqual(['https://example.com', '--port', '9333']);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// The module exports a singleton `output` which reads process.stdout.isTTY at
|
|
4
|
+
// construction time. We need to control that, so we reset modules per test.
|
|
5
|
+
|
|
6
|
+
async function importOutput(isTTY: boolean) {
|
|
7
|
+
vi.resetModules();
|
|
8
|
+
// Stub isTTY before the module loads
|
|
9
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: isTTY, configurable: true });
|
|
10
|
+
return import('../cli/output.js');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('OutputFormatter', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
16
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
17
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('printSuccess calls console.log with the message', async () => {
|
|
21
|
+
const { output } = await importOutput(false);
|
|
22
|
+
output.printSuccess('done');
|
|
23
|
+
expect(console.log).toHaveBeenCalledOnce();
|
|
24
|
+
expect((console.log as any).mock.calls[0][0]).toContain('done');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('printError calls console.error with the message and optional details', async () => {
|
|
28
|
+
const { output } = await importOutput(false);
|
|
29
|
+
output.printError('fail', 'reason');
|
|
30
|
+
expect(console.error).toHaveBeenCalledTimes(2);
|
|
31
|
+
expect((console.error as any).mock.calls[0][0]).toContain('fail');
|
|
32
|
+
expect((console.error as any).mock.calls[1][0]).toContain('reason');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('printWarning calls console.warn', async () => {
|
|
36
|
+
const { output } = await importOutput(false);
|
|
37
|
+
output.printWarning('careful');
|
|
38
|
+
expect(console.warn).toHaveBeenCalledOnce();
|
|
39
|
+
expect((console.warn as any).mock.calls[0][0]).toContain('careful');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('printInfo calls console.log with info prefix', async () => {
|
|
43
|
+
const { output } = await importOutput(false);
|
|
44
|
+
output.printInfo('note');
|
|
45
|
+
expect(console.log).toHaveBeenCalledOnce();
|
|
46
|
+
expect((console.log as any).mock.calls[0][0]).toContain('note');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('with color disabled (non-TTY), output contains no ANSI escape codes', async () => {
|
|
50
|
+
const { output } = await importOutput(false);
|
|
51
|
+
output.printSuccess('plain');
|
|
52
|
+
const logged = (console.log as any).mock.calls[0][0] as string;
|
|
53
|
+
expect(logged).not.toContain('\x1b[');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('with color enabled (TTY), output contains ANSI escape codes', async () => {
|
|
57
|
+
const { output } = await importOutput(true);
|
|
58
|
+
output.printSuccess('colored');
|
|
59
|
+
const logged = (console.log as any).mock.calls[0][0] as string;
|
|
60
|
+
expect(logged).toContain('\x1b[');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { mkdtemp, readFile, rm, stat } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
|
|
6
|
+
// We need to mock process.cwd() before importing ref-cache so CACHE_DIR
|
|
7
|
+
// resolves to our temp directory.
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
tempDir = await mkdtemp(join(tmpdir(), 'monobrowse-test-'));
|
|
12
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tempDir);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Dynamic import so each test picks up the mocked cwd.
|
|
21
|
+
// We must bust the module cache between tests.
|
|
22
|
+
async function importRefCache() {
|
|
23
|
+
// Vitest module cache means re-import returns the same module.
|
|
24
|
+
// Since CACHE_DIR is set at module-load time, we need resetModules.
|
|
25
|
+
vi.resetModules();
|
|
26
|
+
return import('../browser/ref-cache.js');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeRefs(count = 2): Map<string, { ref: string; role: string; name: string; nodeId: number }> {
|
|
30
|
+
const map = new Map();
|
|
31
|
+
for (let i = 1; i <= count; i++) {
|
|
32
|
+
map.set(`ref${i}`, { ref: `ref${i}`, role: 'button', name: `Button ${i}`, nodeId: i });
|
|
33
|
+
}
|
|
34
|
+
return map;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('ref-cache', () => {
|
|
38
|
+
it('round-trip: save then load returns equivalent data', async () => {
|
|
39
|
+
const mod = await importRefCache();
|
|
40
|
+
const refs = makeRefs(2);
|
|
41
|
+
|
|
42
|
+
await mod.saveRefCache('target-1', 'https://example.com', refs as any);
|
|
43
|
+
const loaded = await mod.loadRefCache('target-1');
|
|
44
|
+
|
|
45
|
+
expect(loaded).not.toBeNull();
|
|
46
|
+
expect(loaded!.url).toBe('https://example.com');
|
|
47
|
+
expect(loaded!.refs.size).toBe(2);
|
|
48
|
+
expect(loaded!.refs.get('ref1')!.role).toBe('button');
|
|
49
|
+
expect(loaded!.refs.get('ref2')!.name).toBe('Button 2');
|
|
50
|
+
expect(typeof loaded!.savedAt).toBe('number');
|
|
51
|
+
expect(typeof loaded!.ageMs).toBe('number');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('targetId mismatch returns null', async () => {
|
|
55
|
+
const mod = await importRefCache();
|
|
56
|
+
const refs = makeRefs(1);
|
|
57
|
+
|
|
58
|
+
await mod.saveRefCache('target-A', 'https://a.com', refs as any);
|
|
59
|
+
const loaded = await mod.loadRefCache('target-B');
|
|
60
|
+
|
|
61
|
+
expect(loaded).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('corrupt JSON file returns null', async () => {
|
|
65
|
+
const mod = await importRefCache();
|
|
66
|
+
// First save a valid cache to create the directory structure
|
|
67
|
+
await mod.saveRefCache('t', 'https://x.com', makeRefs(1) as any);
|
|
68
|
+
|
|
69
|
+
// Now overwrite the file with invalid JSON
|
|
70
|
+
const cacheFile = join(tempDir, '.monomind', 'monobrowse', 'ax-snapshot.json');
|
|
71
|
+
const { writeFile: wf } = await import('fs/promises');
|
|
72
|
+
await wf(cacheFile, '{{{not valid json!!!');
|
|
73
|
+
|
|
74
|
+
const loaded = await mod.loadRefCache('t');
|
|
75
|
+
expect(loaded).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('clearRefCache removes the cache file', async () => {
|
|
79
|
+
const mod = await importRefCache();
|
|
80
|
+
await mod.saveRefCache('t1', 'https://x.com', makeRefs(1) as any);
|
|
81
|
+
|
|
82
|
+
const cacheFile = join(tempDir, '.monomind', 'monobrowse', 'ax-snapshot.json');
|
|
83
|
+
// Verify file exists
|
|
84
|
+
const before = await stat(cacheFile).catch(() => null);
|
|
85
|
+
expect(before).not.toBeNull();
|
|
86
|
+
|
|
87
|
+
await mod.clearRefCache();
|
|
88
|
+
|
|
89
|
+
const after = await stat(cacheFile).catch(() => null);
|
|
90
|
+
expect(after).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('load after clear returns null', async () => {
|
|
94
|
+
const mod = await importRefCache();
|
|
95
|
+
await mod.saveRefCache('t1', 'https://x.com', makeRefs(1) as any);
|
|
96
|
+
await mod.clearRefCache();
|
|
97
|
+
|
|
98
|
+
const loaded = await mod.loadRefCache('t1');
|
|
99
|
+
expect(loaded).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('freshly saved cache is not stale', async () => {
|
|
103
|
+
const mod = await importRefCache();
|
|
104
|
+
await mod.saveRefCache('t1', 'https://x.com', makeRefs(1) as any);
|
|
105
|
+
|
|
106
|
+
const loaded = await mod.loadRefCache('t1');
|
|
107
|
+
expect(loaded).not.toBeNull();
|
|
108
|
+
expect(loaded!.stale).toBe(false);
|
|
109
|
+
expect(loaded!.ageMs).toBeLessThan(mod.REF_CACHE_STALE_MS);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('cache older than REF_CACHE_STALE_MS is flagged stale', async () => {
|
|
113
|
+
const mod = await importRefCache();
|
|
114
|
+
await mod.saveRefCache('t1', 'https://x.com', makeRefs(1) as any);
|
|
115
|
+
|
|
116
|
+
// Manually backdate the savedAt timestamp
|
|
117
|
+
const cacheFile = join(tempDir, '.monomind', 'monobrowse', 'ax-snapshot.json');
|
|
118
|
+
const raw = JSON.parse(await readFile(cacheFile, 'utf8'));
|
|
119
|
+
raw.savedAt = Date.now() - mod.REF_CACHE_STALE_MS - 5000;
|
|
120
|
+
const { writeFile: wf } = await import('fs/promises');
|
|
121
|
+
await wf(cacheFile, JSON.stringify(raw));
|
|
122
|
+
|
|
123
|
+
const loaded = await mod.loadRefCache('t1');
|
|
124
|
+
expect(loaded).not.toBeNull();
|
|
125
|
+
expect(loaded!.stale).toBe(true);
|
|
126
|
+
expect(loaded!.ageMs).toBeGreaterThan(mod.REF_CACHE_STALE_MS);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('loading with no cache file returns null (no crash)', async () => {
|
|
130
|
+
const mod = await importRefCache();
|
|
131
|
+
const loaded = await mod.loadRefCache('nonexistent');
|
|
132
|
+
expect(loaded).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('active-port persistence', () => {
|
|
137
|
+
it('round-trip: save then load returns the port; launched defaults true', async () => {
|
|
138
|
+
const mod = await importRefCache();
|
|
139
|
+
await mod.saveActivePort(9333);
|
|
140
|
+
expect(await mod.loadActivePort()).toBe(9333);
|
|
141
|
+
expect(await mod.loadActivePortInfo()).toEqual({ port: 9333, launched: true });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('connect provenance: launched:false survives the round-trip', async () => {
|
|
145
|
+
const mod = await importRefCache();
|
|
146
|
+
await mod.saveActivePort(9229, { launched: false });
|
|
147
|
+
expect(await mod.loadActivePortInfo()).toEqual({ port: 9229, launched: false });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('clear removes the file; load returns null afterwards', async () => {
|
|
151
|
+
const mod = await importRefCache();
|
|
152
|
+
await mod.saveActivePort(9333);
|
|
153
|
+
await mod.clearActivePort();
|
|
154
|
+
expect(await mod.loadActivePort()).toBeNull();
|
|
155
|
+
expect(await mod.loadActivePortInfo()).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('clear with no file resolves without throwing', async () => {
|
|
159
|
+
const mod = await importRefCache();
|
|
160
|
+
await mod.clearActivePort();
|
|
161
|
+
await expect(mod.clearActivePort()).resolves.toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('rejects out-of-range or non-integer persisted ports', async () => {
|
|
165
|
+
const mod = await importRefCache();
|
|
166
|
+
const { writeFile, mkdir } = await import('fs/promises');
|
|
167
|
+
const dir = join(process.cwd(), '.monomind', 'monobrowse');
|
|
168
|
+
await mkdir(dir, { recursive: true });
|
|
169
|
+
for (const bad of [80, 70000, 1.5, '9222', null]) {
|
|
170
|
+
await writeFile(join(dir, 'active-port.json'), JSON.stringify({ port: bad }));
|
|
171
|
+
expect(await mod.loadActivePort()).toBeNull();
|
|
172
|
+
expect(await mod.loadActivePortInfo()).toBeNull();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
File without changes
|
|
File without changes
|
package/src/browser/actions.ts
CHANGED
|
@@ -496,12 +496,22 @@ export async function removeInitScript(client: CdpClient, sessionId: string, ide
|
|
|
496
496
|
await client.send('Page.removeScriptToEvaluateOnNewDocument', { identifier }, sessionId);
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
+
// Default cap on how long an evaluateJs() call waits for Runtime.evaluate to
|
|
500
|
+
// resolve. Because this always passes `awaitPromise: true`, an expression
|
|
501
|
+
// like `new Promise(() => {})` never settles — the underlying CdpClient.send()
|
|
502
|
+
// has no timeout of its own (the CDP_RESPONSE_SIZE_LIMIT in cdp.ts only guards
|
|
503
|
+
// the HTTP /json/* endpoints, not WebSocket command responses), so without
|
|
504
|
+
// this the caller (e.g. the `eval` CLI command) would hang forever with
|
|
505
|
+
// nothing to kill it.
|
|
506
|
+
const DEFAULT_EVAL_TIMEOUT_MS = 30_000;
|
|
507
|
+
|
|
499
508
|
export async function evaluateJs(
|
|
500
509
|
client: CdpClient,
|
|
501
510
|
sessionId: string,
|
|
502
|
-
expression: string
|
|
511
|
+
expression: string,
|
|
512
|
+
timeoutMs: number = DEFAULT_EVAL_TIMEOUT_MS
|
|
503
513
|
): Promise<unknown> {
|
|
504
|
-
const
|
|
514
|
+
const evalPromise = client.send<{
|
|
505
515
|
result: { value?: unknown; type: string; description?: string };
|
|
506
516
|
exceptionDetails?: { text: string; exception?: { description?: string } };
|
|
507
517
|
}>('Runtime.evaluate', {
|
|
@@ -510,6 +520,16 @@ export async function evaluateJs(
|
|
|
510
520
|
awaitPromise: true,
|
|
511
521
|
}, sessionId);
|
|
512
522
|
|
|
523
|
+
const result = await (timeoutMs > 0
|
|
524
|
+
? Promise.race([
|
|
525
|
+
evalPromise,
|
|
526
|
+
new Promise<never>((_, reject) => {
|
|
527
|
+
const t = setTimeout(() => reject(new Error(`JS evaluation timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
528
|
+
t.unref?.();
|
|
529
|
+
}),
|
|
530
|
+
])
|
|
531
|
+
: evalPromise);
|
|
532
|
+
|
|
513
533
|
if (result.exceptionDetails) {
|
|
514
534
|
throw new Error(`JS evaluation error: ${result.exceptionDetails.exception?.description ?? result.exceptionDetails.text}`);
|
|
515
535
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/browser/batch.ts
CHANGED
|
File without changes
|
package/src/browser/browser.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn, execSync } from 'child_process';
|
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import { tmpdir } from 'os';
|
|
4
4
|
import { join } from 'path';
|
|
5
|
+
import { connect } from 'net';
|
|
5
6
|
import { CdpClient, fetchTargets, fetchNewTarget } from './cdp.js';
|
|
6
7
|
import type { BrowserConfig, CdpTarget } from './types.js';
|
|
7
8
|
import { CHROME_EXECUTABLES } from './types.js';
|
|
@@ -11,6 +12,14 @@ import { setupDialogAutoHandling } from './dialog.js';
|
|
|
11
12
|
const DEFAULT_PORT = 9222;
|
|
12
13
|
const LAUNCH_TIMEOUT = 10_000;
|
|
13
14
|
const POLL_INTERVAL = 200;
|
|
15
|
+
const BROWSER_CLOSE_TIMEOUT_MS = 3000;
|
|
16
|
+
|
|
17
|
+
// Tracks the PID of Chrome instances *we* spawned, keyed by CDP port, so
|
|
18
|
+
// closeBrowser() has a kill fallback when the graceful `Browser.close` CDP
|
|
19
|
+
// command fails or times out (e.g. a hung renderer). Ports we merely attached
|
|
20
|
+
// to (already-running browser) are never recorded here — we only ever kill
|
|
21
|
+
// processes we launched ourselves.
|
|
22
|
+
const launchedPids = new Map<number, number>();
|
|
14
23
|
|
|
15
24
|
function findChrome(executablePath?: string): string {
|
|
16
25
|
if (executablePath) {
|
|
@@ -42,6 +51,38 @@ export async function isPortOpen(port: number): Promise<boolean> {
|
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Confirm the CDP endpoint on `port` actually identifies as Chrome/Chromium
|
|
56
|
+
* via `/json/version`'s `Browser` field, rather than assuming any CDP-speaking
|
|
57
|
+
* responder is "ours". Reduces (does not eliminate) the risk of silently
|
|
58
|
+
* attaching to an unrelated real browser that happens to be listening there.
|
|
59
|
+
*/
|
|
60
|
+
async function isChromeIdentity(port: number): Promise<boolean> {
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
63
|
+
if (!res.ok) return false;
|
|
64
|
+
const info = (await res.json()) as { Browser?: string };
|
|
65
|
+
return typeof info.Browser === 'string' && /chrom(e|ium)/i.test(info.Browser);
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Raw TCP connect probe — distinguishes "nothing listening" from "something listening that isn't CDP". */
|
|
72
|
+
function isTcpPortOpen(port: number, timeoutMs = 1000): Promise<boolean> {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const socket = connect({ host: '127.0.0.1', port, timeout: timeoutMs });
|
|
75
|
+
const done = (result: boolean) => {
|
|
76
|
+
socket.removeAllListeners();
|
|
77
|
+
socket.destroy();
|
|
78
|
+
resolve(result);
|
|
79
|
+
};
|
|
80
|
+
socket.once('connect', () => done(true));
|
|
81
|
+
socket.once('timeout', () => done(false));
|
|
82
|
+
socket.once('error', () => done(false));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
45
86
|
export async function launchBrowser(config: BrowserConfig = {}): Promise<number> {
|
|
46
87
|
const rawPort = config.port ?? DEFAULT_PORT;
|
|
47
88
|
// Validate port is in a safe range for localhost CDP debugging
|
|
@@ -51,7 +92,16 @@ export async function launchBrowser(config: BrowserConfig = {}): Promise<number>
|
|
|
51
92
|
const port = rawPort;
|
|
52
93
|
|
|
53
94
|
if (await isPortOpen(port)) {
|
|
54
|
-
|
|
95
|
+
// Something CDP-speaking is already there — verify it's actually Chrome/Chromium
|
|
96
|
+
// before attaching, so we don't silently take over an unrelated real browser
|
|
97
|
+
// (e.g. the user's own personal Chrome) that happens to be on this port.
|
|
98
|
+
if (await isChromeIdentity(port)) {
|
|
99
|
+
return port;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Port ${port} is occupied by a CDP-speaking process that does not identify as Chrome/Chromium. ` +
|
|
103
|
+
`Refusing to attach — pass a different port or free port ${port}.`
|
|
104
|
+
);
|
|
55
105
|
}
|
|
56
106
|
|
|
57
107
|
const chromePath = findChrome(config.executablePath);
|
|
@@ -89,11 +139,28 @@ export async function launchBrowser(config: BrowserConfig = {}): Promise<number>
|
|
|
89
139
|
stdio: 'ignore',
|
|
90
140
|
});
|
|
91
141
|
child.unref();
|
|
142
|
+
if (child.pid) launchedPids.set(port, child.pid);
|
|
92
143
|
|
|
93
144
|
const deadline = Date.now() + LAUNCH_TIMEOUT;
|
|
94
145
|
while (Date.now() < deadline) {
|
|
95
146
|
await sleep(POLL_INTERVAL);
|
|
96
|
-
if (await isPortOpen(port))
|
|
147
|
+
if (await isPortOpen(port)) {
|
|
148
|
+
if (await isChromeIdentity(port)) return port;
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Port ${port} is occupied by a CDP-speaking process that does not identify as Chrome/Chromium. ` +
|
|
151
|
+
`Refusing to attach — pass a different port or free port ${port}.`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Timed out waiting for our Chrome to come up on the port. Distinguish
|
|
157
|
+
// "nothing is listening" (real launch failure) from "something non-CDP is
|
|
158
|
+
// squatting the port" (confusing generic timeout otherwise) via a raw TCP probe.
|
|
159
|
+
if (await isTcpPortOpen(port)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Port ${port} is occupied by a non-Chrome process (TCP connection succeeds but no CDP response within ${LAUNCH_TIMEOUT}ms). ` +
|
|
162
|
+
`Free the port or pass a different one.`
|
|
163
|
+
);
|
|
97
164
|
}
|
|
98
165
|
|
|
99
166
|
throw new Error(`Chrome failed to start on port ${port} within ${LAUNCH_TIMEOUT}ms`);
|
|
@@ -140,6 +207,52 @@ export async function connectToTarget(port: number, targetId?: string): Promise<
|
|
|
140
207
|
return { client, target, sessionId };
|
|
141
208
|
}
|
|
142
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Cleanly terminate a Chrome/Chromium instance we launched on `port`.
|
|
212
|
+
* Sends the `Browser.close` CDP command (the correct graceful shutdown —
|
|
213
|
+
* closes all tabs and exits the process cleanly) over `client`'s connection.
|
|
214
|
+
* If that command fails, times out, or the connection is already gone,
|
|
215
|
+
* falls back to killing the tracked PID directly so a headed/interactive
|
|
216
|
+
* browser window (e.g. one spawned for a login/CAPTCHA flow) never lingers
|
|
217
|
+
* as a visible, authenticated, still-debuggable orphan process.
|
|
218
|
+
*/
|
|
219
|
+
export async function closeBrowser(client: CdpClient, port: number): Promise<void> {
|
|
220
|
+
let gracefullyClosed = false;
|
|
221
|
+
try {
|
|
222
|
+
await Promise.race([
|
|
223
|
+
client.send('Browser.close', {}),
|
|
224
|
+
new Promise<never>((_, reject) => {
|
|
225
|
+
const t = setTimeout(() => reject(new Error('Browser.close timed out')), BROWSER_CLOSE_TIMEOUT_MS);
|
|
226
|
+
t.unref?.();
|
|
227
|
+
}),
|
|
228
|
+
]);
|
|
229
|
+
gracefullyClosed = true;
|
|
230
|
+
} catch {
|
|
231
|
+
// fall through to PID-kill fallback below
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const pid = launchedPids.get(port);
|
|
235
|
+
if (pid === undefined) return; // not a process we launched — nothing to kill
|
|
236
|
+
launchedPids.delete(port);
|
|
237
|
+
|
|
238
|
+
if (!gracefullyClosed) {
|
|
239
|
+
try { process.kill(pid, 'SIGKILL'); } catch { /* already exited */ }
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Browser.close was acknowledged — give the process a moment to exit on
|
|
244
|
+
// its own, then verify and force-kill as a safety net in case it hung.
|
|
245
|
+
const t = setTimeout(() => {
|
|
246
|
+
try {
|
|
247
|
+
process.kill(pid, 0); // throws if the process is already gone
|
|
248
|
+
process.kill(pid, 'SIGKILL');
|
|
249
|
+
} catch {
|
|
250
|
+
/* already exited — expected path */
|
|
251
|
+
}
|
|
252
|
+
}, 1000);
|
|
253
|
+
t.unref?.();
|
|
254
|
+
}
|
|
255
|
+
|
|
143
256
|
export async function openUrl(client: CdpClient, sessionId: string, url: string): Promise<void> {
|
|
144
257
|
// Cap to 2 MB to prevent OOM in CDP message serializer (e.g. data: URI attacks)
|
|
145
258
|
if (url.length > 2_097_152) throw new Error('URL exceeds 2 MB limit');
|
package/src/browser/cdp.ts
CHANGED
|
@@ -151,7 +151,7 @@ export async function fetchTargets(port: number): Promise<CdpTarget[]> {
|
|
|
151
151
|
|
|
152
152
|
export async function fetchNewTarget(port: number, url: string): Promise<CdpTarget> {
|
|
153
153
|
// Chrome v92+ requires PUT for /json/new; GET returns 405. URL must be encoded.
|
|
154
|
-
const res = await fetch(`http://127.0.0.1:${port}/json/new?${url}`, { method: 'PUT' });
|
|
154
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/new?${encodeURIComponent(url)}`, { method: 'PUT' });
|
|
155
155
|
if (!res.ok) throw new Error(`Failed to create target: ${res.statusText}`);
|
|
156
156
|
return readCdpJson(res) as Promise<CdpTarget>;
|
|
157
157
|
}
|
|
File without changes
|
|
File without changes
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createServer, IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
8
9
|
|
|
9
10
|
export interface StepEvent {
|
|
10
11
|
type: string;
|
|
@@ -57,6 +58,21 @@ let instance: DashboardServer | null = null;
|
|
|
57
58
|
export function startDashboard(port = DEFAULT_PORT): DashboardServer {
|
|
58
59
|
if (instance) return instance;
|
|
59
60
|
|
|
61
|
+
// ── Security: per-process auth credential for mutating (non-GET) requests ──
|
|
62
|
+
// Generated once per server start and written to a well-known location so
|
|
63
|
+
// trusted local callers (CLI) can read it and pass it back via an auth
|
|
64
|
+
// header or query param on non-GET routes. Mirrors the pattern used by the
|
|
65
|
+
// main CLI dashboard (packages/@monomind/cli/src/ui/server.ts).
|
|
66
|
+
const dashboardAuthValue = randomBytes(24).toString('hex');
|
|
67
|
+
try {
|
|
68
|
+
const authFileDir = join(homedir(), '.monomind');
|
|
69
|
+
mkdirSync(authFileDir, { recursive: true });
|
|
70
|
+
writeFileSync(join(authFileDir, 'monobrowse-dashboard-token'), dashboardAuthValue, { mode: 0o600 });
|
|
71
|
+
} catch {
|
|
72
|
+
// best-effort: if we can't persist the token, mutating routes still
|
|
73
|
+
// enforce it in-memory — trusted callers just won't be able to read it.
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
const runHistory: RunRecord[] = [];
|
|
61
77
|
const stopRequests = new Set<string>();
|
|
62
78
|
/** Map from client (WebSocket or SSE response) to the subscribed project dir,
|
|
@@ -79,6 +95,13 @@ export function startDashboard(port = DEFAULT_PORT): DashboardServer {
|
|
|
79
95
|
} catch {
|
|
80
96
|
uiHtml = `<!DOCTYPE html><html><head><title>monobrowse dashboard</title></head><body style="background:#0f0f1a;color:#ccc;font-family:system-ui;padding:20px"><h1>monobrowse dashboard</h1><p>Dashboard UI not found. Run the build to include ui.html.</p><script>let _retryDelay=1000;function connectSSE(){const es=new EventSource('/events');es.onmessage=e=>{console.log(JSON.parse(e.data));_retryDelay=1000;};es.onerror=()=>{es.close();setTimeout(connectSSE,Math.min(_retryDelay*=2,30000));};};connectSSE();</script></body></html>`;
|
|
81
97
|
}
|
|
98
|
+
// Hand the per-process auth token to the served page so its own JS (e.g. the
|
|
99
|
+
// stop button) can pass it back on mutating requests. Injected once at
|
|
100
|
+
// startup — the loopback-only page is the sole reader.
|
|
101
|
+
const tokenScript = `<script>window.__MONOBROWSE_TOKEN__=${JSON.stringify(dashboardAuthValue)};</script>`;
|
|
102
|
+
uiHtml = uiHtml.includes('</head>')
|
|
103
|
+
? uiHtml.replace('</head>', `${tokenScript}</head>`)
|
|
104
|
+
: tokenScript + uiHtml;
|
|
82
105
|
|
|
83
106
|
const server = createServer(async (req, res) => {
|
|
84
107
|
const url = req.url ?? '/';
|
|
@@ -100,8 +123,20 @@ export function startDashboard(port = DEFAULT_PORT): DashboardServer {
|
|
|
100
123
|
return;
|
|
101
124
|
}
|
|
102
125
|
|
|
126
|
+
// ── Security: require a matching auth token on all mutating routes ──────
|
|
127
|
+
// Binding to loopback only stops remote attackers, not a malicious page
|
|
128
|
+
// open in the user's own browser (DNS rebinding / same-origin fetch to
|
|
129
|
+
// 127.0.0.1). Require the per-process token via header or query param on
|
|
130
|
+
// every non-GET/HEAD route; GET/SSE reads stay open on the loopback baseline.
|
|
103
131
|
if (url.startsWith('/stop/') && req.method === 'POST') {
|
|
104
|
-
const
|
|
132
|
+
const parsedAuth = new URL(url, 'http://localhost');
|
|
133
|
+
const suppliedAuth = req.headers['x-monobrowse-token'] || parsedAuth.searchParams.get('token') || '';
|
|
134
|
+
if (suppliedAuth !== dashboardAuthValue) {
|
|
135
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
136
|
+
res.end(JSON.stringify({ error: 'Unauthorized: missing or invalid auth token' }));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const runId = url.slice(6).split('?')[0];
|
|
105
140
|
stopRequests.add(runId);
|
|
106
141
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
107
142
|
res.end(JSON.stringify({ ok: true, runId }));
|
|
@@ -678,7 +678,7 @@ function renderCards() {
|
|
|
678
678
|
'</div>';
|
|
679
679
|
}).join('');
|
|
680
680
|
}
|
|
681
|
-
function stopRun(id) { fetch('/stop/' + id, { method: 'POST' }).catch(function() {}); }
|
|
681
|
+
function stopRun(id) { fetch('/stop/' + id, { method: 'POST', headers: { 'x-monobrowse-token': (window.__MONOBROWSE_TOKEN__ || '') } }).catch(function() {}); }
|
|
682
682
|
|
|
683
683
|
// ═══════════════════════════════════════════════════════════════════
|
|
684
684
|
// NODE PALETTE DEFINITIONS
|
package/src/browser/dialog.ts
CHANGED
|
File without changes
|
package/src/browser/emulation.ts
CHANGED
|
File without changes
|
package/src/browser/find.ts
CHANGED
|
File without changes
|
package/src/browser/har.ts
CHANGED
|
File without changes
|
package/src/browser/index.ts
CHANGED
package/src/browser/network.ts
CHANGED
|
@@ -6,6 +6,17 @@ export async function getCookies(client: CdpClient, sessionId: string): Promise<
|
|
|
6
6
|
return result.cookies ?? [];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Get cookies scoped to specific page URLs via `Network.getCookies`, instead of
|
|
11
|
+
* `Network.getAllCookies` which returns cookies for every origin in the browser
|
|
12
|
+
* profile. Used for session save/export so we don't leak session tokens for
|
|
13
|
+
* unrelated domains the user happens to be logged into.
|
|
14
|
+
*/
|
|
15
|
+
export async function getCookiesForUrls(client: CdpClient, sessionId: string, urls: string[]): Promise<CdpCookie[]> {
|
|
16
|
+
const result = await client.send<{ cookies: CdpCookie[] }>('Network.getCookies', { urls }, sessionId);
|
|
17
|
+
return result.cookies ?? [];
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
export async function setCookies(client: CdpClient, sessionId: string, cookies: CdpCookie[]): Promise<void> {
|
|
10
21
|
await client.send('Network.setCookies', { cookies }, sessionId);
|
|
11
22
|
}
|
package/src/browser/pdf.ts
CHANGED
|
File without changes
|
package/src/browser/profiler.ts
CHANGED
|
File without changes
|