@inetafrica/open-claudia 3.5.23 → 3.5.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/core/agent-browser-cli.js +98 -0
- package/core/browser-address.js +171 -0
- package/core/browser-control.js +258 -39
- package/core/browser-drive.js +589 -0
- package/core/browser-lock.js +33 -2
- package/core/browser-teach.js +59 -44
- package/core/browser-view.js +154 -8
- package/core/cards.js +18 -0
- package/core/loopback.js +4 -1
- package/core/providers/env.js +17 -0
- package/core/system-prompt.js +14 -1
- package/core/tool-guard.js +3 -0
- package/package.json +2 -2
- package/test-provider-env-isolation.js +24 -0
- package/test-provider-language.js +11 -1
- package/web.js +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.5.25
|
|
4
|
+
|
|
5
|
+
The Browser app feels like a browser on the phone and on the web (Sumeet, 10 Oct 08:53 EAT on his phone: "it feels like a browser on my phone… like Google Chrome app", and 09:18: "the web and mobile still need to also feel native… don't forget the website"). A canary build for the Browser canary bot first; the fleet waits for his OK.
|
|
6
|
+
|
|
7
|
+
- **The bot's tab takes the driver's screen.** With Chrome in its own container, a take (or a `fit` when the screen changes: a turned phone, a resized panel, a keyboard that opens) that carries the driver's screen (`viewport`: CSS size, pixel density, phone layout, touch) fits the bot's tab to it through `core/browser-drive.js`. A phone gets the page's phone layout, full screen and sharp; the web gets a desktop page at the panel's size. Handing back puts the tab back to its own size. The browser's identity (user agent) never changes, so sites signed into stay signed in after the hand back. `GET /api/browser/control` says `fit: true`.
|
|
8
|
+
- **The driver's input goes straight to Chrome,** through the view's own DevTools session, in order. Touches keep their timing, so a swipe flings; typed text is inserted whole (`input_text`, at most 500 characters: accents, emoji, the keyboard's corrections); every point is scaled by the view's density, because Chrome reads input in the drawn picture's pixels. A bot without the view takes `input_text` as key presses.
|
|
9
|
+
- **The page's text fields.** The view reads where the page's text fields are and what kind they are (never a value) and streams them as `fields` events, so the Browser page opens the phone's own keyboard of the right kind the moment a field is tapped.
|
|
10
|
+
- **Pictures.** The view streams its own screencast at the driver's size (at most 1.6 million pixels, 30 a second). `/api/browser/stream` serves it while someone drives, up to 30 fps, and keeps the newest frame while a slow viewer drains instead of dropping it, so the last picture of a scroll always arrives.
|
|
11
|
+
- **A hand back wakes the bot only when it waits for that person:** it sent them an Open Browser card (cards now record `openApp`), or one of its browser commands was refused while they drove (the tool guard marks it). Otherwise the hand back is only logged. On 9 Oct a test hand back set off a 28-minute tangent on the canary.
|
|
12
|
+
- **Correction to 3.5.24:** in container mode, sessions are not pinned to tabs (Agent Space dropped `AGENT_BROWSER_PIN_TAB` in a98f239); a session uses the tab that is open.
|
|
13
|
+
- **Tests:** `test-browser-drive.js` (new), `test-browser-control.js`, `test-web-browser-app.js`.
|
|
14
|
+
|
|
15
|
+
## 3.5.24
|
|
16
|
+
|
|
17
|
+
The Browser app's address bar, and Chrome in its own container (Sumeet, 9 Oct 21:27 EAT on his phone: "I can't change the url on the mobile app"). A canary build for the Browser canary bot first; the fleet waits for his OK.
|
|
18
|
+
|
|
19
|
+
- **The driver can change the page.** `POST /api/browser/navigate` (open, back, forward, reload) for the person who has taken over, from Agent Space's address bar. Public internet addresses only (`core/browser-address.js`): http or https, no user name or password; no localhost, single-label, .local, .internal, .svc or .home.arpa name; no private, loopback, link-local, shared, benchmarking, multicast or reserved IP, typed in any form the URL parser reads or found in any of the name's DNS answers (3 s at most). It needs the driver's live lock and extends it, runs in order with the driver's input, gives the page 20 s, and logs origin and path only. Teach me records it as a step ("Go to …"). `GET /api/browser/control` says `navigate: true`.
|
|
20
|
+
- **One way to run agent-browser on the bot's own sessions** (`core/agent-browser-cli.js`): Teach me's element lookup and the address bar run it with the agent's environment and only `--session`, `--json` and a command, so no launch setting can ride along. On a bot pod a session reached with other launch settings is relaunched and loses its page.
|
|
21
|
+
- **The agent gets agent-browser's settings.** Every `AGENT_BROWSER_*` variable now reaches the agent process (provider credentials and control-plane tokens are still stripped after), so its own browser commands and the Browser app share one set of launch settings. On a pod the agent used to run agent-browser without the image's `AGENT_BROWSER_EXECUTABLE_PATH`.
|
|
22
|
+
- **Container mode.** With Agent Space's browser container (`AGENT_BROWSER_CDP`, `AGENT_BROWSER_PIN_TAB`), Chrome runs in its own container without the bot's files or keys and every agent-browser call attaches to it, each session pinned to its own tab. One Chrome window paints only its front tab, so opening a viewer brings that session's tab forward (Chrome's `/json/activate`, loopback only). The prompt now says how this bot's Chrome really runs, and no longer claims agent-browser adds `--no-sandbox` by itself.
|
|
23
|
+
- **Tests:** `test-browser-address.js` (new), `test-browser-control.js`, `test-browser-teach.js`, `test-web-browser-app.js`, `test-browser-view.js`, `test-kazee-bot-apps.js`, `test-provider-env-isolation.js`.
|
|
24
|
+
|
|
3
25
|
## 3.5.23
|
|
4
26
|
|
|
5
27
|
Usage limits as one rule, /stop as a real cancel, the queue's ✕ and run-together, and background checks on Haiku 5.5 and Sonnet 5.5 (Sumeet, 9 Oct 15:56 EAT: "yeh release and upgrade all the bots, include rahil for the updates going forward"). 3.5.20 below, the Browser app, only ever went out as a canary build, so this is the first release that brings it to everyone. 3.5.21 and 3.5.22 were tagged with the same changes but never published: 3.5.21's build stopped on the new /stop and queue test, which needed the fake agent its neighbours run on (CI has no Claude or Codex installed), and 3.5.22's builds stopped on two timing checks that busy build machines pushed past their limits (the boot matrix's 30 s per boot, the /stop test's 3 s and 2.5 s windows); those checks now allow more time.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The bot's own agent-browser CLI calls on a session its agent already has open: Teach me's element
|
|
2
|
+
// lookup (core/browser-teach.js) and the Browser app's address bar (core/browser-control.js).
|
|
3
|
+
//
|
|
4
|
+
// agent-browser keeps one browser per session and remembers the settings it was launched with
|
|
5
|
+
// (the executable, its launch args, the idle timeout). A command that arrives with other launch
|
|
6
|
+
// settings restarts that browser, and the person's page is lost. So every call from here runs
|
|
7
|
+
// with the environment the bot's own agent-browser commands get, and never with a launch flag
|
|
8
|
+
// (--args, --idle-timeout, --executable-path, --headed, --cdp, ...): only what the CLI needs goes
|
|
9
|
+
// along (PATH, HOME, TMPDIR, LANG, LC_ALL, USER and every AGENT_BROWSER_* variable), none of the
|
|
10
|
+
// bot's tokens, and no XDG_RUNTIME_DIR (the agent's own commands run without it). The session is
|
|
11
|
+
// found by its socket folder, the one it was listed from: AGENT_BROWSER_SOCKET_DIR. Without that
|
|
12
|
+
// folder nothing runs, since agent-browser starts a new browser for a session it cannot find.
|
|
13
|
+
|
|
14
|
+
const childProcess = require("child_process");
|
|
15
|
+
const { SESSION_NAME } = require("./browser-view");
|
|
16
|
+
|
|
17
|
+
const ENV_KEYS = new Set(["PATH", "HOME", "TMPDIR", "LANG", "LC_ALL", "USER"]);
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 20 * 1000;
|
|
19
|
+
// After a kill the CLI's output can stay open (a child of its own holding it); the call settles
|
|
20
|
+
// this long after its time is up all the same, so a session's queue never waits on it for good.
|
|
21
|
+
const SETTLE_GRACE_MS = 2000;
|
|
22
|
+
const MAX_BUFFER = 512 * 1024;
|
|
23
|
+
// The only dash argument a command may carry: eval's -b (a base64 script follows).
|
|
24
|
+
const COMMAND_FLAGS = new Set(["-b"]);
|
|
25
|
+
|
|
26
|
+
function agentBrowserBin() {
|
|
27
|
+
return process.env.OC_AGENT_BROWSER_BIN || "agent-browser";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function agentBrowserEnv(session) {
|
|
31
|
+
const env = {};
|
|
32
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
33
|
+
if (ENV_KEYS.has(key) || key.startsWith("AGENT_BROWSER_")) env[key] = value;
|
|
34
|
+
}
|
|
35
|
+
if (session && session.dir) env.AGENT_BROWSER_SOCKET_DIR = session.dir;
|
|
36
|
+
return env;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ["--session", <name>, ("--json"), <command>, <its arguments>...]. The command's own arguments
|
|
40
|
+
// (an address, a script) are never flags, so no launch setting can ride along; null when the
|
|
41
|
+
// session name or the command is not one of these.
|
|
42
|
+
function commandArgs(session, command, { json = false } = {}) {
|
|
43
|
+
const name = String((session && session.name) || "");
|
|
44
|
+
if (!SESSION_NAME.test(name) || name.startsWith("-")) return null;
|
|
45
|
+
if (!Array.isArray(command) || !command.length || !/^[a-z]+$/.test(String(command[0]))) return null;
|
|
46
|
+
for (const arg of command.slice(1)) {
|
|
47
|
+
if (typeof arg !== "string" || (arg.startsWith("-") && !COMMAND_FLAGS.has(arg))) return null;
|
|
48
|
+
}
|
|
49
|
+
return ["--session", name, ...(json ? ["--json"] : []), ...command];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Run `agent-browser --session <name> [--json] <command...>` on that session:
|
|
53
|
+
// { ok, stdout, timedOut }. Never rejects. A call past timeoutMs is killed (SIGKILL).
|
|
54
|
+
function runAgentBrowser(session, command, { json = false, timeoutMs = DEFAULT_TIMEOUT_MS, maxBuffer = MAX_BUFFER, execFile = childProcess.execFile } = {}) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
let settled = false;
|
|
57
|
+
let guard = null;
|
|
58
|
+
const done = (ok, stdout, timedOut) => {
|
|
59
|
+
if (settled) return;
|
|
60
|
+
settled = true;
|
|
61
|
+
if (guard) clearTimeout(guard);
|
|
62
|
+
resolve({ ok, stdout: ok ? String(stdout || "") : "", timedOut });
|
|
63
|
+
};
|
|
64
|
+
const args = commandArgs(session, command, { json });
|
|
65
|
+
if (!args || !session.dir) return done(false, "", false);
|
|
66
|
+
let child = null;
|
|
67
|
+
try {
|
|
68
|
+
child = execFile(agentBrowserBin(), args, {
|
|
69
|
+
env: agentBrowserEnv(session),
|
|
70
|
+
timeout: timeoutMs,
|
|
71
|
+
killSignal: "SIGKILL",
|
|
72
|
+
maxBuffer,
|
|
73
|
+
windowsHide: true,
|
|
74
|
+
}, (error, stdout) => {
|
|
75
|
+
if (!error) return done(true, stdout, false);
|
|
76
|
+
done(false, "", error.killed === true && error.code !== "ERR_CHILD_PROCESS_STDIO_MAXBUFFER");
|
|
77
|
+
});
|
|
78
|
+
} catch (_) {
|
|
79
|
+
return done(false, "", false);
|
|
80
|
+
}
|
|
81
|
+
if (child && typeof child.on === "function") child.on("error", () => { /* reported through the callback */ });
|
|
82
|
+
if (child && child.stdin) child.stdin.end();
|
|
83
|
+
if (settled) return;
|
|
84
|
+
guard = setTimeout(() => {
|
|
85
|
+
try { if (child && typeof child.kill === "function") child.kill("SIGKILL"); } catch (_) { /* already gone */ }
|
|
86
|
+
done(false, "", true);
|
|
87
|
+
}, timeoutMs + SETTLE_GRACE_MS);
|
|
88
|
+
if (guard.unref) guard.unref();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
DEFAULT_TIMEOUT_MS,
|
|
94
|
+
agentBrowserBin,
|
|
95
|
+
agentBrowserEnv,
|
|
96
|
+
commandArgs,
|
|
97
|
+
runAgentBrowser,
|
|
98
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Where the Browser app's address bar may go (core/browser-control.js navigate): public internet
|
|
2
|
+
// addresses only. The bot's browser runs inside the pod, next to the cluster's services and the
|
|
3
|
+
// cloud's metadata endpoint, so a typed address must not reach what a person outside could not:
|
|
4
|
+
// - http or https, at most 2048 characters, no user name or password in it;
|
|
5
|
+
// - no localhost, no single-label name, nothing under .localhost, .local, .internal, .svc,
|
|
6
|
+
// .cluster.local or .home.arpa;
|
|
7
|
+
// - no private, loopback, link-local, shared, benchmarking, multicast or reserved IP: typed as
|
|
8
|
+
// one in any form the URL parser reads as an IP (decimal, octal or hex IPv4, IPv6, IPv4 inside
|
|
9
|
+
// IPv6), or found in ANY of the name's DNS answers (A and AAAA, 3 s at most).
|
|
10
|
+
//
|
|
11
|
+
// parseAddress(raw) { url, lookupName } | { error: "bad-url" | "blocked-address" }
|
|
12
|
+
// No network: lookupName is the name still to resolve, null for an IP.
|
|
13
|
+
// resolveAddress(name) { addresses } | { error: "blocked-address" | "unknown-host" }
|
|
14
|
+
// checkAddress(raw) both: { ok: true, url, addresses } | { ok: false, error }
|
|
15
|
+
//
|
|
16
|
+
// The browser resolves the name again when it loads the page; this check keeps a typed address
|
|
17
|
+
// from reaching the inside, it does not follow redirects.
|
|
18
|
+
|
|
19
|
+
const dns = require("dns");
|
|
20
|
+
const net = require("net");
|
|
21
|
+
|
|
22
|
+
const MAX_URL_LENGTH = 2048;
|
|
23
|
+
const DNS_TIMEOUT_MS = 3000;
|
|
24
|
+
|
|
25
|
+
function v4Bytes(text) {
|
|
26
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(String(text));
|
|
27
|
+
if (!m) return null;
|
|
28
|
+
const bytes = m.slice(1).map(Number);
|
|
29
|
+
return bytes.every((b) => b <= 255) ? bytes : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function v4Number(bytes) {
|
|
33
|
+
return ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const BLOCKED_V4 = [
|
|
37
|
+
["0.0.0.0", 8], // "this network"
|
|
38
|
+
["10.0.0.0", 8], // private
|
|
39
|
+
["100.64.0.0", 10], // shared address space (carrier-grade NAT)
|
|
40
|
+
["127.0.0.0", 8], // loopback
|
|
41
|
+
["169.254.0.0", 16], // link-local, the cloud metadata endpoint
|
|
42
|
+
["172.16.0.0", 12], // private
|
|
43
|
+
["192.0.0.0", 24], // IETF protocol assignments
|
|
44
|
+
["192.168.0.0", 16], // private
|
|
45
|
+
["198.18.0.0", 15], // benchmarking
|
|
46
|
+
["224.0.0.0", 4], // multicast
|
|
47
|
+
["240.0.0.0", 4], // reserved
|
|
48
|
+
["255.255.255.255", 32], // broadcast
|
|
49
|
+
].map(([base, bits]) => ({ base: v4Number(v4Bytes(base)), mask: (0xffffffff << (32 - bits)) >>> 0 }));
|
|
50
|
+
|
|
51
|
+
function v4Blocked(bytes) {
|
|
52
|
+
const n = v4Number(bytes);
|
|
53
|
+
return BLOCKED_V4.some(({ base, mask }) => ((n & mask) >>> 0) === base);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The eight 16-bit words of an IPv6 address (brackets and a zone allowed), or null.
|
|
57
|
+
function v6Words(text) {
|
|
58
|
+
let s = String(text).replace(/^\[(.*)\]$/, "$1").replace(/%.*$/, "");
|
|
59
|
+
if (!net.isIPv6(s)) return null;
|
|
60
|
+
const tail = /^(.*:)(\d{1,3}(?:\.\d{1,3}){3})$/.exec(s);
|
|
61
|
+
if (tail) {
|
|
62
|
+
const b = v4Bytes(tail[2]);
|
|
63
|
+
if (!b) return null;
|
|
64
|
+
s = `${tail[1]}${((b[0] << 8) | b[1]).toString(16)}:${((b[2] << 8) | b[3]).toString(16)}`;
|
|
65
|
+
}
|
|
66
|
+
const halves = s.split("::");
|
|
67
|
+
if (halves.length > 2) return null;
|
|
68
|
+
const head = halves[0] ? halves[0].split(":") : [];
|
|
69
|
+
const rest = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
|
|
70
|
+
const fill = halves.length === 2 ? 8 - head.length - rest.length : 0;
|
|
71
|
+
if (fill < 0) return null;
|
|
72
|
+
const words = [...head, ...Array(fill).fill("0"), ...rest].map((h) => (/^[0-9a-f]{1,4}$/i.test(h) ? parseInt(h, 16) : NaN));
|
|
73
|
+
return words.length === 8 && words.every(Number.isInteger) ? words : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function v6Blocked(words) {
|
|
77
|
+
const zero = (from, to) => words.slice(from, to).every((w) => w === 0);
|
|
78
|
+
if (zero(0, 8)) return true; // ::
|
|
79
|
+
if (zero(0, 7) && words[7] === 1) return true; // ::1
|
|
80
|
+
if ((words[0] & 0xfe00) === 0xfc00) return true; // fc00::/7, unique local
|
|
81
|
+
if ((words[0] & 0xffc0) === 0xfe80) return true; // fe80::/10, link-local
|
|
82
|
+
if ((words[0] & 0xff00) === 0xff00) return true; // ff00::/8, multicast
|
|
83
|
+
const v4 = [words[6] >> 8, words[6] & 0xff, words[7] >> 8, words[7] & 0xff];
|
|
84
|
+
if (zero(0, 5) && words[5] === 0xffff) return v4Blocked(v4); // ::ffff:a.b.c.d, IPv4-mapped
|
|
85
|
+
if (zero(0, 6)) return v4Blocked(v4); // ::a.b.c.d, IPv4-compatible
|
|
86
|
+
if (words[0] === 0x64 && words[1] === 0xff9b && zero(2, 6)) return v4Blocked(v4); // 64:ff9b::/96, NAT64
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Is this IP (as text) off limits? Anything that is not an IP is.
|
|
91
|
+
function ipBlocked(ip) {
|
|
92
|
+
const text = String(ip == null ? "" : ip);
|
|
93
|
+
if (net.isIPv4(text)) return v4Blocked(v4Bytes(text));
|
|
94
|
+
const words = v6Words(text);
|
|
95
|
+
return words ? v6Blocked(words) : true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A name that never means the public internet (an IP is judged by ipBlocked instead).
|
|
99
|
+
function blockedName(hostname) {
|
|
100
|
+
const host = String(hostname || "").toLowerCase().replace(/\.+$/, "");
|
|
101
|
+
if (!host.includes(".")) return true; // localhost, a bare name: a machine or a Service nearby
|
|
102
|
+
return /(^|\.)(localhost|local|internal|svc|home\.arpa)$/.test(host); // .local covers .cluster.local
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseAddress(raw) {
|
|
106
|
+
if (typeof raw !== "string" || !raw.trim() || raw.length > MAX_URL_LENGTH) return { error: "bad-url" };
|
|
107
|
+
let url;
|
|
108
|
+
try {
|
|
109
|
+
url = new URL(raw);
|
|
110
|
+
} catch (_) {
|
|
111
|
+
return { error: "bad-url" };
|
|
112
|
+
}
|
|
113
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return { error: "bad-url" };
|
|
114
|
+
if (url.username || url.password || !url.hostname) return { error: "bad-url" };
|
|
115
|
+
const host = url.hostname; // the URL parser's own reading: IPv4 in dotted decimal, IPv6 in brackets
|
|
116
|
+
if (host.startsWith("[")) {
|
|
117
|
+
const words = v6Words(host);
|
|
118
|
+
if (!words) return { error: "bad-url" };
|
|
119
|
+
return v6Blocked(words) ? { error: "blocked-address" } : { url, lookupName: null };
|
|
120
|
+
}
|
|
121
|
+
if (net.isIPv4(host)) return v4Blocked(v4Bytes(host)) ? { error: "blocked-address" } : { url, lookupName: null };
|
|
122
|
+
if (blockedName(host)) return { error: "blocked-address" };
|
|
123
|
+
return { url, lookupName: host };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function defaultLookup(hostname, options) {
|
|
127
|
+
return dns.promises.lookup(hostname, options);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function withTimeout(promise, ms) {
|
|
131
|
+
let timer = null;
|
|
132
|
+
const late = new Promise((_, reject) => {
|
|
133
|
+
timer = setTimeout(() => reject(new Error("timed out")), ms);
|
|
134
|
+
});
|
|
135
|
+
return Promise.race([promise, late]).finally(() => clearTimeout(timer));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Every A and AAAA answer must be public; none at all (or no answer in time) is unknown.
|
|
139
|
+
async function resolveAddress(hostname, { lookup = defaultLookup, timeoutMs = DNS_TIMEOUT_MS } = {}) {
|
|
140
|
+
let answers;
|
|
141
|
+
try {
|
|
142
|
+
answers = await withTimeout(Promise.resolve().then(() => lookup(hostname, { all: true })), timeoutMs);
|
|
143
|
+
} catch (_) {
|
|
144
|
+
return { error: "unknown-host" };
|
|
145
|
+
}
|
|
146
|
+
const addresses = (Array.isArray(answers) ? answers : [answers])
|
|
147
|
+
.filter((a) => a != null)
|
|
148
|
+
.map((a) => (typeof a === "object" ? a.address : a));
|
|
149
|
+
if (!addresses.length) return { error: "unknown-host" };
|
|
150
|
+
if (addresses.some((a) => typeof a !== "string" || ipBlocked(a))) return { error: "blocked-address" };
|
|
151
|
+
return { addresses };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function checkAddress(raw, options = {}) {
|
|
155
|
+
const parsed = parseAddress(raw);
|
|
156
|
+
if (parsed.error) return { ok: false, error: parsed.error };
|
|
157
|
+
if (!parsed.lookupName) return { ok: true, url: parsed.url, addresses: [parsed.url.hostname.replace(/^\[(.*)\]$/, "$1")] };
|
|
158
|
+
const found = await resolveAddress(parsed.lookupName, options);
|
|
159
|
+
if (found.error) return { ok: false, error: found.error };
|
|
160
|
+
return { ok: true, url: parsed.url, addresses: found.addresses };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
DNS_TIMEOUT_MS,
|
|
165
|
+
MAX_URL_LENGTH,
|
|
166
|
+
blockedName,
|
|
167
|
+
checkAddress,
|
|
168
|
+
ipBlocked,
|
|
169
|
+
parseAddress,
|
|
170
|
+
resolveAddress,
|
|
171
|
+
};
|