@moxxy/plugin-browser 0.27.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/dist/browser-session.d.ts +43 -0
  3. package/dist/browser-session.d.ts.map +1 -0
  4. package/dist/browser-session.js +500 -0
  5. package/dist/browser-session.js.map +1 -0
  6. package/dist/browser-surface.d.ts +3 -0
  7. package/dist/browser-surface.d.ts.map +1 -0
  8. package/dist/browser-surface.js +255 -0
  9. package/dist/browser-surface.js.map +1 -0
  10. package/dist/html-extract.d.ts +20 -0
  11. package/dist/html-extract.d.ts.map +1 -0
  12. package/dist/html-extract.js +122 -0
  13. package/dist/html-extract.js.map +1 -0
  14. package/dist/index.d.ts +10 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +24 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/sidecar/dispatch.d.ts +18 -0
  19. package/dist/sidecar/dispatch.d.ts.map +1 -0
  20. package/dist/sidecar/dispatch.js +294 -0
  21. package/dist/sidecar/dispatch.js.map +1 -0
  22. package/dist/sidecar/install.d.ts +37 -0
  23. package/dist/sidecar/install.d.ts.map +1 -0
  24. package/dist/sidecar/install.js +242 -0
  25. package/dist/sidecar/install.js.map +1 -0
  26. package/dist/sidecar/types.d.ts +97 -0
  27. package/dist/sidecar/types.d.ts.map +1 -0
  28. package/dist/sidecar/types.js +20 -0
  29. package/dist/sidecar/types.js.map +1 -0
  30. package/dist/sidecar.d.ts +31 -0
  31. package/dist/sidecar.d.ts.map +1 -0
  32. package/dist/sidecar.js +165 -0
  33. package/dist/sidecar.js.map +1 -0
  34. package/dist/ssrf-guard.d.ts +43 -0
  35. package/dist/ssrf-guard.d.ts.map +1 -0
  36. package/dist/ssrf-guard.js +164 -0
  37. package/dist/ssrf-guard.js.map +1 -0
  38. package/dist/web-fetch.d.ts +23 -0
  39. package/dist/web-fetch.d.ts.map +1 -0
  40. package/dist/web-fetch.js +253 -0
  41. package/dist/web-fetch.js.map +1 -0
  42. package/package.json +74 -0
  43. package/src/browser-session.test.ts +333 -0
  44. package/src/browser-session.ts +567 -0
  45. package/src/browser-surface.test.ts +367 -0
  46. package/src/browser-surface.ts +275 -0
  47. package/src/html-extract.ts +152 -0
  48. package/src/index.ts +35 -0
  49. package/src/sidecar/dispatch.test.ts +313 -0
  50. package/src/sidecar/dispatch.ts +314 -0
  51. package/src/sidecar/install.ts +283 -0
  52. package/src/sidecar/types.test.ts +26 -0
  53. package/src/sidecar/types.ts +114 -0
  54. package/src/sidecar.test.ts +57 -0
  55. package/src/sidecar.ts +167 -0
  56. package/src/ssrf-guard.test.ts +109 -0
  57. package/src/ssrf-guard.ts +165 -0
  58. package/src/web-fetch.test.ts +305 -0
  59. package/src/web-fetch.ts +311 -0
@@ -0,0 +1,97 @@
1
+ export type BrowserKind = 'chromium' | 'firefox' | 'webkit';
2
+ export type ErrorKind = 'init' | 'needs-install' | 'navigation' | 'runtime' | 'timeout' | 'unknown';
3
+ export interface Req {
4
+ readonly id: string;
5
+ readonly method: string;
6
+ readonly params?: Record<string, unknown>;
7
+ }
8
+ export interface Ok {
9
+ readonly id: string;
10
+ readonly ok: true;
11
+ readonly result?: unknown;
12
+ /** One-shot human-readable note (e.g. "Auto-installed Chromium"). */
13
+ readonly notice?: string;
14
+ }
15
+ export interface Err {
16
+ readonly id: string;
17
+ readonly ok: false;
18
+ readonly error: {
19
+ message: string;
20
+ kind: ErrorKind;
21
+ };
22
+ }
23
+ export type Reply = Ok | Err;
24
+ export interface BrowserType {
25
+ launch(opts: {
26
+ headless: boolean;
27
+ }): Promise<{
28
+ close(): Promise<void>;
29
+ newContext(opts?: unknown): Promise<unknown>;
30
+ }>;
31
+ }
32
+ export interface PageHandle {
33
+ goto(url: string, opts?: unknown): Promise<unknown>;
34
+ click(selector: string, opts?: unknown): Promise<void>;
35
+ fill(selector: string, value: string, opts?: unknown): Promise<void>;
36
+ textContent(selector: string): Promise<string | null>;
37
+ content(): Promise<string>;
38
+ screenshot(opts?: unknown): Promise<Buffer>;
39
+ evaluate(fn: string): Promise<unknown>;
40
+ url(): string;
41
+ close(): Promise<void>;
42
+ viewportSize(): {
43
+ width: number;
44
+ height: number;
45
+ } | null;
46
+ setViewportSize(size: {
47
+ width: number;
48
+ height: number;
49
+ }): Promise<void>;
50
+ goBack(opts?: unknown): Promise<unknown>;
51
+ goForward(opts?: unknown): Promise<unknown>;
52
+ reload(opts?: unknown): Promise<unknown>;
53
+ readonly mouse: {
54
+ move(x: number, y: number, opts?: unknown): Promise<void>;
55
+ click(x: number, y: number, opts?: unknown): Promise<void>;
56
+ wheel(dx: number, dy: number): Promise<void>;
57
+ };
58
+ readonly keyboard: {
59
+ press(key: string): Promise<void>;
60
+ type(text: string): Promise<void>;
61
+ };
62
+ }
63
+ /** Minimal slice of Playwright's `Request` used by the navigation SSRF guard. */
64
+ export interface RouteRequest {
65
+ url(): string;
66
+ isNavigationRequest(): boolean;
67
+ }
68
+ /** Minimal slice of Playwright's `Route` used by the navigation SSRF guard. */
69
+ export interface RouteHandle {
70
+ request(): RouteRequest;
71
+ abort(errorCode?: string): Promise<void>;
72
+ continue(): Promise<void>;
73
+ }
74
+ export interface PlaywrightHandle {
75
+ readonly browser: {
76
+ close(): Promise<void>;
77
+ };
78
+ readonly context: {
79
+ newPage(): Promise<unknown>;
80
+ close(): Promise<void>;
81
+ /** Optional because the type is a loose projection; real Playwright contexts always have it. */
82
+ route?(pattern: string, handler: (route: RouteHandle) => Promise<void> | void): Promise<void>;
83
+ };
84
+ readonly page: PageHandle;
85
+ }
86
+ export declare function errMsg(err: unknown): string;
87
+ /**
88
+ * An error carrying a typed {@link ErrorKind} so the dispatch layer can map it
89
+ * onto the wire reply's `error.kind` without poking an untyped `kind` property
90
+ * onto a bare `Error`.
91
+ */
92
+ export declare class SidecarError extends Error {
93
+ readonly kind: ErrorKind;
94
+ constructor(message: string, kind: ErrorKind);
95
+ }
96
+ export declare function badParams(msg: string): SidecarError;
97
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/sidecar/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export function errMsg(err) {
2
+ return err instanceof Error ? err.message : String(err);
3
+ }
4
+ /**
5
+ * An error carrying a typed {@link ErrorKind} so the dispatch layer can map it
6
+ * onto the wire reply's `error.kind` without poking an untyped `kind` property
7
+ * onto a bare `Error`.
8
+ */
9
+ export class SidecarError extends Error {
10
+ kind;
11
+ constructor(message, kind) {
12
+ super(message);
13
+ this.kind = kind;
14
+ this.name = 'SidecarError';
15
+ }
16
+ }
17
+ export function badParams(msg) {
18
+ return new SidecarError(msg, 'runtime');
19
+ }
20
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/sidecar/types.ts"],"names":[],"mappings":"AA4FA,MAAM,UAAU,MAAM,CAAC,GAAY;IACjC,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IAG1B;IAFX,YACE,OAAe,EACN,IAAe;QAExB,KAAK,CAAC,OAAO,CAAC,CAAC;QAFN,SAAI,GAAJ,IAAI,CAAW;QAGxB,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF;AAED,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,OAAO,IAAI,YAAY,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Playwright sidecar — owns a single browser context. Speaks newline-delimited
4
+ * JSON-RPC over stdio so the parent (`browser_session` tool) doesn't load
5
+ * Playwright into its own process. Crash isolation, lazy install: the parent
6
+ * only spawns this when the heavy tier is actually needed.
7
+ *
8
+ * Wire format (one line per message):
9
+ * { "id": "uuid", "method": "goto"|"click"|"fill"|"text"|"html"|"screenshot"|"eval"|"close", "params": {...} }
10
+ * { "id": "uuid", "ok": true, "result": ... }
11
+ * { "id": "uuid", "ok": false, "error": { "message": "...", "kind": "init"|"navigation"|"runtime"|"timeout" } }
12
+ *
13
+ * Run with `node dist/sidecar.js` (the package's `bin` entry is
14
+ * `moxxy-browser-sidecar`). Parent terminates by closing stdin or sending
15
+ * `{method:'close'}`.
16
+ */
17
+ import { type Reply } from './sidecar/types.js';
18
+ /**
19
+ * Parse one input line and chain its dispatch onto the serial request queue.
20
+ * `out` is the sink for replies (real run: {@link write}; tests inject a stub).
21
+ * Exported so a test can drive the queue without spinning up the whole stdio
22
+ * loop. Returns the queue tail so callers can await it.
23
+ *
24
+ * The `.catch` on the chained link is load-bearing: a throw from `out` (e.g. a
25
+ * broken stdout pipe) would otherwise reject `queue` permanently and strand
26
+ * EVERY subsequent request, since each link chains off the previous one.
27
+ * Swallow it here — after emitting a best-effort error reply — so the next
28
+ * request still serves.
29
+ */
30
+ export declare function enqueueLine(line: string, out: (reply: Reply) => void): Promise<void>;
31
+ //# sourceMappingURL=sidecar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,EAAU,KAAK,KAAK,EAAY,MAAM,oBAAoB,CAAC;AAoElE;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCpF"}
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Playwright sidecar — owns a single browser context. Speaks newline-delimited
4
+ * JSON-RPC over stdio so the parent (`browser_session` tool) doesn't load
5
+ * Playwright into its own process. Crash isolation, lazy install: the parent
6
+ * only spawns this when the heavy tier is actually needed.
7
+ *
8
+ * Wire format (one line per message):
9
+ * { "id": "uuid", "method": "goto"|"click"|"fill"|"text"|"html"|"screenshot"|"eval"|"close", "params": {...} }
10
+ * { "id": "uuid", "ok": true, "result": ... }
11
+ * { "id": "uuid", "ok": false, "error": { "message": "...", "kind": "init"|"navigation"|"runtime"|"timeout" } }
12
+ *
13
+ * Run with `node dist/sidecar.js` (the package's `bin` entry is
14
+ * `moxxy-browser-sidecar`). Parent terminates by closing stdin or sending
15
+ * `{method:'close'}`.
16
+ */
17
+ import * as readline from 'node:readline';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { dispatch, teardown } from './sidecar/dispatch.js';
20
+ import { errMsg } from './sidecar/types.js';
21
+ const state = { handle: null, pendingInstallNotice: null };
22
+ function write(reply) {
23
+ // Drain the install-notice flag into the first reply that goes out
24
+ // after the install completed, then clear it. Errors get the notice
25
+ // too — sometimes the launch retry surfaces a different problem and
26
+ // the user still wants to know we tried to install.
27
+ if (state.pendingInstallNotice) {
28
+ if (reply.ok) {
29
+ reply = { ...reply, notice: state.pendingInstallNotice };
30
+ }
31
+ else {
32
+ reply = {
33
+ ...reply,
34
+ error: {
35
+ ...reply.error,
36
+ message: `${state.pendingInstallNotice} Then: ${reply.error.message}`,
37
+ },
38
+ };
39
+ }
40
+ state.pendingInstallNotice = null;
41
+ }
42
+ process.stdout.write(JSON.stringify(reply) + '\n');
43
+ }
44
+ /**
45
+ * Tears down the browser context AND exits the process. Used both by
46
+ * the explicit `close` RPC path and the parent-loss / stdin-close
47
+ * paths so all cleanup goes through one routine.
48
+ */
49
+ async function shutdownAndExit(code) {
50
+ await teardown(state);
51
+ process.exit(code);
52
+ }
53
+ /**
54
+ * Parent watchdog: if the moxxy process that spawned this sidecar
55
+ * disappears (crash, SIGKILL, terminal hangup), there's no stdin EOF
56
+ * to rely on — orphan Chromium would keep running and chew CPU/memory
57
+ * until the user notices. Poll the parent PID every few seconds and
58
+ * self-terminate when it goes away.
59
+ *
60
+ * `process.kill(pid, 0)` is the POSIX trick for "does this PID
61
+ * exist?" — no signal is actually delivered. Throws ESRCH when the
62
+ * process is gone (or EPERM when it exists but we can't signal it —
63
+ * still alive, so don't treat as gone).
64
+ */
65
+ function startParentWatchdog() {
66
+ const parentPid = process.ppid;
67
+ if (!parentPid || parentPid <= 1)
68
+ return; // already orphaned (init), nothing to watch
69
+ const interval = setInterval(() => {
70
+ try {
71
+ process.kill(parentPid, 0);
72
+ }
73
+ catch (err) {
74
+ const code = err.code;
75
+ if (code === 'ESRCH') {
76
+ clearInterval(interval);
77
+ void shutdownAndExit(0);
78
+ }
79
+ // EPERM means the process exists but we can't signal it — still alive.
80
+ }
81
+ }, 2000);
82
+ interval.unref?.(); // never block the event loop from exiting
83
+ }
84
+ let queue = Promise.resolve();
85
+ /**
86
+ * Parse one input line and chain its dispatch onto the serial request queue.
87
+ * `out` is the sink for replies (real run: {@link write}; tests inject a stub).
88
+ * Exported so a test can drive the queue without spinning up the whole stdio
89
+ * loop. Returns the queue tail so callers can await it.
90
+ *
91
+ * The `.catch` on the chained link is load-bearing: a throw from `out` (e.g. a
92
+ * broken stdout pipe) would otherwise reject `queue` permanently and strand
93
+ * EVERY subsequent request, since each link chains off the previous one.
94
+ * Swallow it here — after emitting a best-effort error reply — so the next
95
+ * request still serves.
96
+ */
97
+ export function enqueueLine(line, out) {
98
+ if (!line.trim())
99
+ return queue;
100
+ let req;
101
+ try {
102
+ req = JSON.parse(line);
103
+ }
104
+ catch {
105
+ // Wholly unparseable — there's no id to recover, so the parent can't match
106
+ // this reply; it's best-effort diagnostic output only.
107
+ out({ id: 'unknown', ok: false, error: { message: 'invalid JSON', kind: 'runtime' } });
108
+ return queue;
109
+ }
110
+ if (!req.id || !req.method) {
111
+ // Echo the request's real id whenever it round-tripped (only `method` was
112
+ // missing), so the parent can SETTLE the matching pending call instead of
113
+ // dropping the reply and stranding the caller until its timeout. Fall back
114
+ // to 'unknown' only when the id genuinely can't be recovered.
115
+ out({
116
+ id: typeof req.id === 'string' && req.id ? req.id : 'unknown',
117
+ ok: false,
118
+ error: { message: 'request requires { id, method }', kind: 'runtime' },
119
+ });
120
+ return queue;
121
+ }
122
+ // Sequentially serve requests on the single page. Parent can pipeline by
123
+ // sending more requests; we serialize them inside the sidecar so a goto
124
+ // doesn't race a click.
125
+ queue = queue
126
+ .then(async () => {
127
+ const reply = await dispatch(state, req);
128
+ out(reply);
129
+ })
130
+ .catch((err) => {
131
+ try {
132
+ out({ id: req.id, ok: false, error: { message: errMsg(err), kind: 'runtime' } });
133
+ }
134
+ catch {
135
+ /* stdout is gone too — nothing left to do but keep the queue alive */
136
+ }
137
+ });
138
+ return queue;
139
+ }
140
+ async function main() {
141
+ startParentWatchdog();
142
+ const rl = readline.createInterface({ input: process.stdin });
143
+ rl.on('line', (line) => {
144
+ void enqueueLine(line, write);
145
+ });
146
+ rl.once('close', () => {
147
+ void shutdownAndExit(0);
148
+ });
149
+ // Defensive: if our stdout pipe breaks (parent died mid-write), Node
150
+ // would otherwise throw an uncaught EPIPE on the next write. Treat
151
+ // it as a signal to clean up gracefully instead.
152
+ process.stdout.on('error', (err) => {
153
+ if (err.code === 'EPIPE') {
154
+ void shutdownAndExit(0);
155
+ }
156
+ });
157
+ }
158
+ // Auto-run only when executed as the bin script — not when imported (tests).
159
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
160
+ main().catch((err) => {
161
+ process.stderr.write(`sidecar fatal: ${errMsg(err)}\n`);
162
+ void shutdownAndExit(1);
163
+ });
164
+ }
165
+ //# sourceMappingURL=sidecar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar.js","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shared SSRF guard for every model-drivable navigation/fetch path in this
3
+ * plugin: `web_fetch` (initial URL + every redirect hop), `browser_session`'s
4
+ * goto (parent side), the sidecar's goto dispatch (child-process side), and
5
+ * the sidecar's in-page navigation interceptor.
6
+ *
7
+ * Blocks non-HTTP(S) schemes, loopback (incl. `localhost`/`*.localhost`),
8
+ * 0.0.0.0/8, RFC-1918 private ranges, link-local 169.254/16 (incl. the
9
+ * 169.254.169.254 cloud metadata endpoint), CGNAT 100.64/10, multicast/
10
+ * reserved, and IPv6 loopback/link-local/unique-local (+ v4-mapped). Hostnames
11
+ * are resolved so an internal DNS name (or rebinding) can't smuggle a private
12
+ * target past the literal-IP checks.
13
+ *
14
+ * NOTE: deliberately NO `@moxxy/sdk` import. The Playwright sidecar — a
15
+ * separate child process whose bundle must stay free of non-builtin deps —
16
+ * imports this module too. Parent-side callers wrap `SsrfBlockedError` into
17
+ * `MoxxyError` themselves.
18
+ */
19
+ /** Thrown for every guard rejection (bad scheme, private/loopback target, unparseable URL). */
20
+ export declare class SsrfBlockedError extends Error {
21
+ constructor(message: string);
22
+ }
23
+ export type DnsResolver = (host: string) => Promise<ReadonlyArray<string>>;
24
+ /** Test seam: override DNS resolution so SSRF tests stay hermetic. Pass null to reset. */
25
+ export declare function setSsrfDnsResolver(fn: DnsResolver | null): void;
26
+ export declare function isBlockedIp(ip: string): boolean;
27
+ /**
28
+ * Reject `raw` unless it is an http(s) URL whose host is (and resolves to)
29
+ * a public address. `label` prefixes the error message so each caller's
30
+ * rejections stay attributable (e.g. "web_fetch", "browser_session").
31
+ *
32
+ * Returns the vetted resolved addresses so callers can PIN the subsequent
33
+ * connection to exactly what was checked (closing the DNS-rebinding TOCTOU
34
+ * where the guard resolves one answer and the fetch independently resolves
35
+ * another — see web-fetch's pinned undici dispatcher). Returns `null` when
36
+ * there is nothing to pin: the host is already an IP literal (vetted above,
37
+ * no DNS involved) or resolution failed (fail-open — a name the guard can't
38
+ * resolve, the fetch's identical system resolver can't reach either).
39
+ */
40
+ export declare function assertPublicUrl(raw: string, label?: string, opts?: {
41
+ failClosed?: boolean;
42
+ }): Promise<ReadonlyArray<string> | null>;
43
+ //# sourceMappingURL=ssrf-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssrf-guard.d.ts","sourceRoot":"","sources":["../src/ssrf-guard.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;GAiBG;AAEH,+FAA+F;AAC/F,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;AAK3E,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAE/D;AA0DD,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAK/C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,KAAK,SAAY,EACjB,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GAClC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CA4CvC"}
@@ -0,0 +1,164 @@
1
+ import { isIP } from 'node:net';
2
+ import { lookup as dnsLookup } from 'node:dns/promises';
3
+ /**
4
+ * Shared SSRF guard for every model-drivable navigation/fetch path in this
5
+ * plugin: `web_fetch` (initial URL + every redirect hop), `browser_session`'s
6
+ * goto (parent side), the sidecar's goto dispatch (child-process side), and
7
+ * the sidecar's in-page navigation interceptor.
8
+ *
9
+ * Blocks non-HTTP(S) schemes, loopback (incl. `localhost`/`*.localhost`),
10
+ * 0.0.0.0/8, RFC-1918 private ranges, link-local 169.254/16 (incl. the
11
+ * 169.254.169.254 cloud metadata endpoint), CGNAT 100.64/10, multicast/
12
+ * reserved, and IPv6 loopback/link-local/unique-local (+ v4-mapped). Hostnames
13
+ * are resolved so an internal DNS name (or rebinding) can't smuggle a private
14
+ * target past the literal-IP checks.
15
+ *
16
+ * NOTE: deliberately NO `@moxxy/sdk` import. The Playwright sidecar — a
17
+ * separate child process whose bundle must stay free of non-builtin deps —
18
+ * imports this module too. Parent-side callers wrap `SsrfBlockedError` into
19
+ * `MoxxyError` themselves.
20
+ */
21
+ /** Thrown for every guard rejection (bad scheme, private/loopback target, unparseable URL). */
22
+ export class SsrfBlockedError extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = 'SsrfBlockedError';
26
+ }
27
+ }
28
+ const defaultResolver = async (host) => (await dnsLookup(host, { all: true })).map((a) => a.address);
29
+ let resolver = defaultResolver;
30
+ /** Test seam: override DNS resolution so SSRF tests stay hermetic. Pass null to reset. */
31
+ export function setSsrfDnsResolver(fn) {
32
+ resolver = fn ?? defaultResolver;
33
+ }
34
+ function isBlockedIpv4(ip) {
35
+ const parts = ip.split('.').map((p) => Number(p));
36
+ if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255))
37
+ return true;
38
+ const [a, b] = parts;
39
+ if (a === 0)
40
+ return true; // 0.0.0.0/8 "this host"
41
+ if (a === 127)
42
+ return true; // loopback
43
+ if (a === 10)
44
+ return true; // private
45
+ if (a === 172 && b >= 16 && b <= 31)
46
+ return true; // private
47
+ if (a === 192 && b === 168)
48
+ return true; // private
49
+ if (a === 169 && b === 254)
50
+ return true; // link-local incl. 169.254.169.254 metadata
51
+ if (a === 100 && b >= 64 && b <= 127)
52
+ return true; // CGNAT 100.64/10
53
+ if (a >= 224)
54
+ return true; // multicast / reserved
55
+ return false;
56
+ }
57
+ function isBlockedIpv6(ip) {
58
+ const addr = ip.toLowerCase().replace(/^\[|\]$/g, '').split('%')[0]; // strip zone id
59
+ if (addr === '::1' || addr === '::')
60
+ return true; // loopback / unspecified
61
+ if (addr.startsWith('fe80'))
62
+ return true; // link-local
63
+ if (addr.startsWith('fc') || addr.startsWith('fd'))
64
+ return true; // unique-local fc00::/7
65
+ // Embedded-v4 forms: re-run the v4 policy against the embedded address.
66
+ // The WHATWG URL parser canonicalizes [::ffff:127.0.0.1] to its HEX form
67
+ // ::ffff:7f00:1, so the dotted regex alone NEVER matches a real URL — we must
68
+ // also reconstruct the dotted quad from the trailing hex groups. Covers
69
+ // v4-mapped (::ffff:a.b.c.d / ::ffff:0:a.b.c.d), v4-compatible (::a.b.c.d),
70
+ // and NAT64 (64:ff9b::/96) — any of which could otherwise smuggle a private
71
+ // v4 target (loopback / 169.254.169.254 metadata) past the guard.
72
+ const embeddedV4 = extractEmbeddedV4(addr);
73
+ if (embeddedV4)
74
+ return isBlockedIpv4(embeddedV4);
75
+ return false;
76
+ }
77
+ /**
78
+ * Pull the embedded IPv4 out of any v4-mapped / v4-compatible / NAT64 IPv6
79
+ * address, returning it as a dotted quad, or null if there is none. Handles
80
+ * both the dotted spelling (`::ffff:127.0.0.1`) and — critically — the HEX
81
+ * spelling the URL parser actually produces (`::ffff:7f00:1`).
82
+ */
83
+ function extractEmbeddedV4(addr) {
84
+ // Dotted spelling: ::ffff:a.b.c.d, ::ffff:0:a.b.c.d, ::a.b.c.d, 64:ff9b::a.b.c.d
85
+ const dotted = addr.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
86
+ if (dotted && (addr.startsWith('::ffff:') || addr.startsWith('::') || addr.startsWith('64:ff9b:'))) {
87
+ return dotted[1];
88
+ }
89
+ // Hex spelling: the embedded v4 lives in the two trailing 16-bit groups.
90
+ const hex = addr.match(/^(?:::ffff:|::ffff:0:|64:ff9b:(?::|.*:)|::)([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
91
+ if (hex) {
92
+ const hi = parseInt(hex[1], 16);
93
+ const lo = parseInt(hex[2], 16);
94
+ if (Number.isInteger(hi) && Number.isInteger(lo) && hi <= 0xffff && lo <= 0xffff) {
95
+ return `${hi >> 8}.${hi & 0xff}.${lo >> 8}.${lo & 0xff}`;
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ export function isBlockedIp(ip) {
101
+ const kind = isIP(ip);
102
+ if (kind === 4)
103
+ return isBlockedIpv4(ip);
104
+ if (kind === 6)
105
+ return isBlockedIpv6(ip);
106
+ return true; // not a parseable IP → block
107
+ }
108
+ /**
109
+ * Reject `raw` unless it is an http(s) URL whose host is (and resolves to)
110
+ * a public address. `label` prefixes the error message so each caller's
111
+ * rejections stay attributable (e.g. "web_fetch", "browser_session").
112
+ *
113
+ * Returns the vetted resolved addresses so callers can PIN the subsequent
114
+ * connection to exactly what was checked (closing the DNS-rebinding TOCTOU
115
+ * where the guard resolves one answer and the fetch independently resolves
116
+ * another — see web-fetch's pinned undici dispatcher). Returns `null` when
117
+ * there is nothing to pin: the host is already an IP literal (vetted above,
118
+ * no DNS involved) or resolution failed (fail-open — a name the guard can't
119
+ * resolve, the fetch's identical system resolver can't reach either).
120
+ */
121
+ export async function assertPublicUrl(raw, label = 'request', opts = {}) {
122
+ let u;
123
+ try {
124
+ u = new URL(raw);
125
+ }
126
+ catch {
127
+ throw new SsrfBlockedError(`${label}: invalid URL: ${raw}`);
128
+ }
129
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') {
130
+ throw new SsrfBlockedError(`${label}: refusing non-HTTP(S) scheme "${u.protocol}"`);
131
+ }
132
+ const host = u.hostname.replace(/^\[|\]$/g, '');
133
+ if (host === 'localhost' || host.endsWith('.localhost')) {
134
+ throw new SsrfBlockedError(`${label}: refusing to fetch loopback host "${host}"`);
135
+ }
136
+ if (isIP(host)) {
137
+ if (isBlockedIp(host))
138
+ throw new SsrfBlockedError(`${label}: refusing private/loopback address "${host}"`);
139
+ return null;
140
+ }
141
+ // Resolve the name and block if it maps to a private range (internal name or
142
+ // DNS rebinding). web_fetch fails OPEN on resolution error (its identical
143
+ // system resolver + pinned dispatcher mean an unresolvable name is no vector),
144
+ // but the BROWSER paths hand the URL to Chromium's own resolver/cache — which
145
+ // may resolve a name node:dns just failed on — so they pass `failClosed` to
146
+ // SOFT-BLOCK instead of allowing an un-vetted name through to the browser.
147
+ let addrs;
148
+ try {
149
+ addrs = await resolver(host);
150
+ }
151
+ catch {
152
+ if (opts.failClosed) {
153
+ throw new SsrfBlockedError(`${label}: refusing host "${host}" — DNS resolution failed (cannot vet against private ranges)`);
154
+ }
155
+ return null;
156
+ }
157
+ for (const addr of addrs) {
158
+ if (isBlockedIp(addr)) {
159
+ throw new SsrfBlockedError(`${label}: host "${host}" resolves to a private/loopback address (${addr})`);
160
+ }
161
+ }
162
+ return addrs;
163
+ }
164
+ //# sourceMappingURL=ssrf-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssrf-guard.js","sourceRoot":"","sources":["../src/ssrf-guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { LookupFunction } from 'node:net';
2
+ import { type DnsResolver } from './ssrf-guard.js';
3
+ /**
4
+ * Light-tier web fetch: a single HTTP GET (or HEAD) with HTML→text/markdown
5
+ * post-processing for the common case of "read this page". Zero new
6
+ * dependencies — uses Node's built-in `fetch`.
7
+ *
8
+ * For JS-heavy / interactive pages, use `browser_session` (Playwright
9
+ * sidecar) instead. The web-research skill picks the tier.
10
+ */
11
+ export { htmlToMarkdown, htmlToPlainText } from './html-extract.js';
12
+ export type { DnsResolver } from './ssrf-guard.js';
13
+ /** Test seam: override DNS resolution so SSRF tests stay hermetic. Pass null to reset. */
14
+ export declare function setWebFetchDnsResolver(fn: DnsResolver | null): void;
15
+ /**
16
+ * DNS-rebinding (TOCTOU) defense: a lookup function that always answers with
17
+ * the addresses the SSRF guard just vetted, so the connection provably goes
18
+ * to the IP that was checked — a second, attacker-controlled DNS answer
19
+ * between check and connect can never be consulted. Exported for tests.
20
+ */
21
+ export declare function createPinnedLookup(addresses: ReadonlyArray<string>): LookupFunction;
22
+ export declare const webFetchTool: import("@moxxy/sdk").ToolDef;
23
+ //# sourceMappingURL=web-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-fetch.d.ts","sourceRoot":"","sources":["../src/web-fetch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAI/C,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;GAOG;AAEH,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAcpE,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,0FAA0F;AAC1F,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAEnE;AAmBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,cAAc,CAwBnF;AAOD,eAAO,MAAM,YAAY,8BAsFvB,CAAC"}