@dominusnode/ai-tools 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -0
- package/dist/create-tools.d.ts +100 -0
- package/dist/create-tools.d.ts.map +1 -0
- package/dist/create-tools.js +88 -0
- package/dist/create-tools.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/tools.d.ts +86 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +857 -0
- package/dist/tools.js.map +1 -0
- package/package.json +32 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import dns from "dns/promises";
|
|
4
|
+
import * as http from "node:http";
|
|
5
|
+
import * as tls from "node:tls";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// SSRF protection helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/** RFC-1918, loopback, link-local, and other non-routable CIDRs. */
|
|
10
|
+
function isPrivateIp(hostname) {
|
|
11
|
+
// Strip IPv6 brackets if present
|
|
12
|
+
const bare = hostname.startsWith("[") && hostname.endsWith("]")
|
|
13
|
+
? hostname.slice(1, -1)
|
|
14
|
+
: hostname;
|
|
15
|
+
// Strip IPv6 zone ID (%...) before validation
|
|
16
|
+
const noZone = bare.replace(/%.*$/, "");
|
|
17
|
+
// IPv4 patterns
|
|
18
|
+
if (/^127\./.test(noZone))
|
|
19
|
+
return true;
|
|
20
|
+
if (/^10\./.test(noZone))
|
|
21
|
+
return true;
|
|
22
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(noZone))
|
|
23
|
+
return true;
|
|
24
|
+
if (/^192\.168\./.test(noZone))
|
|
25
|
+
return true;
|
|
26
|
+
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(noZone))
|
|
27
|
+
return true; // CGNAT
|
|
28
|
+
if (/^(22[4-9]|2[3-5]\d)\./.test(noZone))
|
|
29
|
+
return true; // multicast + reserved
|
|
30
|
+
if (/^169\.254\./.test(noZone))
|
|
31
|
+
return true; // link-local
|
|
32
|
+
if (/^0\./.test(noZone))
|
|
33
|
+
return true; // 0.0.0.0/8
|
|
34
|
+
if (noZone === "255.255.255.255")
|
|
35
|
+
return true;
|
|
36
|
+
// Decimal/octal/hex IP obfuscation — reject anything that looks numeric but isn't a normal dotted-quad
|
|
37
|
+
if (/^0x[0-9a-fA-F]+$/.test(noZone))
|
|
38
|
+
return true; // hex single-int
|
|
39
|
+
if (/^0\d+$/.test(noZone))
|
|
40
|
+
return true; // octal single-int
|
|
41
|
+
if (/^\d+$/.test(noZone) && !noZone.includes("."))
|
|
42
|
+
return true; // decimal single-int
|
|
43
|
+
// IPv6 patterns
|
|
44
|
+
if (noZone === "::1")
|
|
45
|
+
return true;
|
|
46
|
+
if (/^::ffff:/i.test(noZone)) {
|
|
47
|
+
const embedded = noZone.slice(7);
|
|
48
|
+
if (embedded.includes("."))
|
|
49
|
+
return isPrivateIp(embedded);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
// IPv4-compatible (::x.x.x.x) — deprecated but still parsed
|
|
53
|
+
const ipv4CompatMatch = noZone.match(/^::(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
54
|
+
if (ipv4CompatMatch) {
|
|
55
|
+
return isPrivateIp(ipv4CompatMatch[1]);
|
|
56
|
+
}
|
|
57
|
+
if (/^fd[0-9a-fA-F]{2}:/i.test(noZone))
|
|
58
|
+
return true; // ULA fd00::/8
|
|
59
|
+
if (/^fc[0-9a-fA-F]{2}:/i.test(noZone))
|
|
60
|
+
return true; // ULA fc00::/7
|
|
61
|
+
if (/^fe[89abAB][0-9a-fA-F]:/i.test(noZone))
|
|
62
|
+
return true; // link-local fe80::/10
|
|
63
|
+
if (noZone === "::")
|
|
64
|
+
return true; // unspecified
|
|
65
|
+
// Teredo (2001:0000::/32) — block unconditionally
|
|
66
|
+
if (noZone.startsWith("2001:0000:") || noZone.startsWith("2001:0:"))
|
|
67
|
+
return true;
|
|
68
|
+
// 6to4 (2002::/16) — block unconditionally
|
|
69
|
+
if (noZone.startsWith("2002:"))
|
|
70
|
+
return true;
|
|
71
|
+
// IPv6 multicast (ff00::/8)
|
|
72
|
+
if (noZone.startsWith("ff"))
|
|
73
|
+
return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/** Validate a URL is safe to fetch through the proxy. */
|
|
77
|
+
function validateUrl(urlStr) {
|
|
78
|
+
// Length limit
|
|
79
|
+
if (urlStr.length > 2048) {
|
|
80
|
+
return { valid: false, error: "URL exceeds maximum length of 2048 characters" };
|
|
81
|
+
}
|
|
82
|
+
let url;
|
|
83
|
+
try {
|
|
84
|
+
url = new URL(urlStr);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { valid: false, error: "Invalid URL format" };
|
|
88
|
+
}
|
|
89
|
+
// Protocol check — only http(s)
|
|
90
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
91
|
+
return { valid: false, error: `Unsupported protocol: ${url.protocol} — only http and https are allowed` };
|
|
92
|
+
}
|
|
93
|
+
// Hostname must be present
|
|
94
|
+
if (!url.hostname) {
|
|
95
|
+
return { valid: false, error: "URL must contain a hostname" };
|
|
96
|
+
}
|
|
97
|
+
// Block localhost variants
|
|
98
|
+
const lowerHost = url.hostname.toLowerCase();
|
|
99
|
+
if (lowerHost === "localhost" ||
|
|
100
|
+
lowerHost === "localhost." ||
|
|
101
|
+
lowerHost.endsWith(".localhost") ||
|
|
102
|
+
lowerHost.endsWith(".localhost.")) {
|
|
103
|
+
return { valid: false, error: "Requests to localhost are not allowed" };
|
|
104
|
+
}
|
|
105
|
+
// Block internal network TLDs
|
|
106
|
+
if (lowerHost.endsWith(".local") || lowerHost.endsWith(".internal") || lowerHost.endsWith(".arpa")) {
|
|
107
|
+
return { valid: false, error: "Requests to internal network hostnames are not allowed" };
|
|
108
|
+
}
|
|
109
|
+
// Block private/reserved IPs
|
|
110
|
+
if (isPrivateIp(url.hostname)) {
|
|
111
|
+
return { valid: false, error: "Requests to private/reserved IP addresses are not allowed" };
|
|
112
|
+
}
|
|
113
|
+
// Block credentials in URL (user:pass@host)
|
|
114
|
+
if (url.username || url.password) {
|
|
115
|
+
return { valid: false, error: "URLs with embedded credentials are not allowed" };
|
|
116
|
+
}
|
|
117
|
+
return { valid: true, url };
|
|
118
|
+
}
|
|
119
|
+
/** Maximum response body length returned to the AI model. */
|
|
120
|
+
const MAX_BODY_LENGTH = 4000;
|
|
121
|
+
/** Truncate a string and append a notice if it exceeds the limit. */
|
|
122
|
+
function truncateBody(body) {
|
|
123
|
+
if (body.length <= MAX_BODY_LENGTH)
|
|
124
|
+
return body;
|
|
125
|
+
return body.slice(0, MAX_BODY_LENGTH) + `\n...[truncated, ${body.length - MAX_BODY_LENGTH} chars omitted]`;
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Safe header subset — never forward sensitive headers to the AI
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
const SAFE_RESPONSE_HEADERS = new Set([
|
|
131
|
+
"content-type",
|
|
132
|
+
"content-length",
|
|
133
|
+
"date",
|
|
134
|
+
"server",
|
|
135
|
+
"cache-control",
|
|
136
|
+
"etag",
|
|
137
|
+
"last-modified",
|
|
138
|
+
"x-request-id",
|
|
139
|
+
"x-ratelimit-limit",
|
|
140
|
+
"x-ratelimit-remaining",
|
|
141
|
+
]);
|
|
142
|
+
function filterHeaders(headers) {
|
|
143
|
+
const result = {};
|
|
144
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
145
|
+
if (SAFE_RESPONSE_HEADERS.has(key.toLowerCase())) {
|
|
146
|
+
result[key.toLowerCase()] = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// OFAC sanctioned countries
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
const SANCTIONED_COUNTRIES = new Set(["CU", "IR", "KP", "RU", "SY"]);
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// DNS rebinding protection
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
/**
|
|
159
|
+
* Resolve a hostname and verify none of the resolved IPs are private.
|
|
160
|
+
* Prevents DNS rebinding attacks where a hostname initially resolves to a
|
|
161
|
+
* public IP during validation but later resolves to a private IP.
|
|
162
|
+
*/
|
|
163
|
+
async function checkDnsRebinding(hostname) {
|
|
164
|
+
// Skip if hostname is already an IP literal
|
|
165
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname) || hostname.startsWith("[")) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Check IPv4 addresses
|
|
169
|
+
try {
|
|
170
|
+
const addresses = await dns.resolve4(hostname);
|
|
171
|
+
for (const addr of addresses) {
|
|
172
|
+
if (isPrivateIp(addr)) {
|
|
173
|
+
throw new Error(`Hostname resolves to private IP ${addr}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
if (err.code === "ENOTFOUND") {
|
|
179
|
+
throw new Error(`Could not resolve hostname: ${hostname}`);
|
|
180
|
+
}
|
|
181
|
+
if (err instanceof Error && err.message.includes("private IP"))
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
// Check IPv6 addresses
|
|
185
|
+
try {
|
|
186
|
+
const addresses = await dns.resolve6(hostname);
|
|
187
|
+
for (const addr of addresses) {
|
|
188
|
+
if (isPrivateIp(addr)) {
|
|
189
|
+
throw new Error(`Hostname resolves to private IPv6 ${addr}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// IPv6 resolution failure is acceptable
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Credential sanitization for error messages
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
const CREDENTIAL_RE = /dn_(live|test)_[a-zA-Z0-9]+|eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g;
|
|
201
|
+
function sanitizeError(message) {
|
|
202
|
+
return message.replace(CREDENTIAL_RE, "***");
|
|
203
|
+
}
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Prototype pollution prevention
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
208
|
+
function stripDangerousKeys(obj, depth = 0) {
|
|
209
|
+
if (depth > 50 || obj == null || typeof obj !== "object")
|
|
210
|
+
return;
|
|
211
|
+
if (Array.isArray(obj)) {
|
|
212
|
+
for (const item of obj)
|
|
213
|
+
stripDangerousKeys(item, depth + 1);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
for (const key of Object.keys(obj)) {
|
|
217
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
218
|
+
delete obj[key];
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
stripDangerousKeys(obj[key], depth + 1);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Agentic wallet validation helpers
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
230
|
+
const DOMAIN_RE = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Response body size limit (10 MB)
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
const MAX_RESPONSE_BODY_BYTES = 10 * 1024 * 1024;
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Tool definitions
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
/**
|
|
239
|
+
* Creates the `proxiedFetch` tool that makes HTTP requests through DomiNode's
|
|
240
|
+
* rotating proxy network.
|
|
241
|
+
*/
|
|
242
|
+
export function createProxiedFetchTool(client, apiKey) {
|
|
243
|
+
return tool({
|
|
244
|
+
description: "Make an HTTP request through DomiNode's rotating proxy network. " +
|
|
245
|
+
"Supports geo-targeting by country and choice of datacenter (dc) or residential proxy. " +
|
|
246
|
+
"Use this to fetch web pages, APIs, or any HTTP resource through a proxy IP.",
|
|
247
|
+
parameters: z.object({
|
|
248
|
+
url: z.string().max(2048).url().describe("The URL to fetch through the proxy"),
|
|
249
|
+
method: z
|
|
250
|
+
.enum(["GET", "HEAD", "OPTIONS"])
|
|
251
|
+
.default("GET")
|
|
252
|
+
.describe("HTTP method to use (only read-only methods allowed)"),
|
|
253
|
+
country: z
|
|
254
|
+
.string()
|
|
255
|
+
.max(2)
|
|
256
|
+
.optional()
|
|
257
|
+
.describe("ISO 3166-1 alpha-2 country code for geo-targeting (e.g. 'US', 'GB', 'DE')"),
|
|
258
|
+
proxyType: z
|
|
259
|
+
.enum(["dc", "residential"])
|
|
260
|
+
.default("dc")
|
|
261
|
+
.describe("Proxy pool type: 'dc' for datacenter ($3/GB) or 'residential' ($5/GB)"),
|
|
262
|
+
headers: z
|
|
263
|
+
.record(z.string(), z.string())
|
|
264
|
+
.optional()
|
|
265
|
+
.describe("Optional HTTP headers to include in the request"),
|
|
266
|
+
}),
|
|
267
|
+
execute: async ({ url, method, country, proxyType, headers }) => {
|
|
268
|
+
// SSRF validation
|
|
269
|
+
const validation = validateUrl(url);
|
|
270
|
+
if (!validation.valid) {
|
|
271
|
+
return { error: validation.error };
|
|
272
|
+
}
|
|
273
|
+
// OFAC sanctioned country check
|
|
274
|
+
if (country && SANCTIONED_COUNTRIES.has(country.toUpperCase())) {
|
|
275
|
+
return { error: `Country '${country.toUpperCase()}' is blocked (OFAC sanctioned)` };
|
|
276
|
+
}
|
|
277
|
+
// DNS rebinding protection
|
|
278
|
+
try {
|
|
279
|
+
await checkDnsRebinding(validation.url.hostname);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
return { error: err instanceof Error ? err.message : "DNS validation failed" };
|
|
283
|
+
}
|
|
284
|
+
// Build the proxy URL using the SDK
|
|
285
|
+
const proxyUrl = client.proxy.buildUrl(apiKey, {
|
|
286
|
+
protocol: "http",
|
|
287
|
+
country,
|
|
288
|
+
});
|
|
289
|
+
try {
|
|
290
|
+
const proxyUrlObj = new URL(proxyUrl);
|
|
291
|
+
const proxyHost = proxyUrlObj.hostname;
|
|
292
|
+
const proxyPort = parseInt(proxyUrlObj.port || "8080", 10);
|
|
293
|
+
const proxyAuth = "Basic " +
|
|
294
|
+
Buffer.from(`${proxyUrlObj.username}:${proxyUrlObj.password}`).toString("base64");
|
|
295
|
+
// Validate custom headers for CRLF injection
|
|
296
|
+
const STRIPPED_HEADERS = new Set(["host", "connection", "content-length", "transfer-encoding", "proxy-authorization", "authorization", "user-agent"]);
|
|
297
|
+
const safeHeaders = {};
|
|
298
|
+
if (headers) {
|
|
299
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
300
|
+
if (/[\r\n\0]/.test(k) || /[\r\n\0]/.test(v)) {
|
|
301
|
+
return { error: `Header "${k.replace(/[\r\n\0]/g, "")}" contains invalid characters` };
|
|
302
|
+
}
|
|
303
|
+
if (!STRIPPED_HEADERS.has(k.toLowerCase())) {
|
|
304
|
+
safeHeaders[k] = v;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const targetUrl = validation.url;
|
|
309
|
+
const MAX_RESP = 1_048_576; // 1MB
|
|
310
|
+
const result = await new Promise((resolve, reject) => {
|
|
311
|
+
const timer = setTimeout(() => reject(new Error("Proxy request timed out")), 30_000);
|
|
312
|
+
const customHeaderLines = Object.entries(safeHeaders).map(([k, v]) => `${k}: ${v}\r\n`).join("");
|
|
313
|
+
if (targetUrl.protocol === "https:") {
|
|
314
|
+
// HTTPS: use CONNECT tunnel through proxy
|
|
315
|
+
const connectHost = targetUrl.hostname.includes(":") ? `[${targetUrl.hostname}]` : targetUrl.hostname;
|
|
316
|
+
const connectReq = http.request({
|
|
317
|
+
hostname: proxyHost, port: proxyPort, method: "CONNECT",
|
|
318
|
+
path: `${connectHost}:${targetUrl.port || 443}`,
|
|
319
|
+
headers: { "Proxy-Authorization": proxyAuth, Host: `${connectHost}:${targetUrl.port || 443}` },
|
|
320
|
+
});
|
|
321
|
+
connectReq.on("connect", (_res, sock) => {
|
|
322
|
+
if (_res.statusCode !== 200) {
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
sock.destroy();
|
|
325
|
+
reject(new Error(`CONNECT failed: ${_res.statusCode}`));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const tlsSock = tls.connect({ host: targetUrl.hostname, socket: sock, servername: targetUrl.hostname, minVersion: "TLSv1.2" }, () => {
|
|
329
|
+
const reqLine = `${method} ${targetUrl.pathname + targetUrl.search} HTTP/1.1\r\nHost: ${targetUrl.host}\r\nUser-Agent: dominusnode-vercel-ai/1.0.0\r\n${customHeaderLines}Connection: close\r\n\r\n`;
|
|
330
|
+
tlsSock.write(reqLine);
|
|
331
|
+
const chunks = [];
|
|
332
|
+
let bytes = 0;
|
|
333
|
+
tlsSock.on("data", (c) => { bytes += c.length; if (bytes <= MAX_RESP + 16384)
|
|
334
|
+
chunks.push(c); });
|
|
335
|
+
let done = false;
|
|
336
|
+
const fin = () => {
|
|
337
|
+
if (done)
|
|
338
|
+
return;
|
|
339
|
+
done = true;
|
|
340
|
+
clearTimeout(timer);
|
|
341
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
342
|
+
const hEnd = raw.indexOf("\r\n\r\n");
|
|
343
|
+
if (hEnd === -1) {
|
|
344
|
+
reject(new Error("Malformed response"));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const hdr = raw.substring(0, hEnd);
|
|
348
|
+
const body = raw.substring(hEnd + 4).substring(0, MAX_RESP);
|
|
349
|
+
const sm = hdr.split("\r\n")[0].match(/^HTTP\/\d\.\d\s+(\d+)\s*(.*)/);
|
|
350
|
+
const hdrs = {};
|
|
351
|
+
for (const l of hdr.split("\r\n").slice(1)) {
|
|
352
|
+
const ci = l.indexOf(":");
|
|
353
|
+
if (ci > 0)
|
|
354
|
+
hdrs[l.substring(0, ci).trim().toLowerCase()] = l.substring(ci + 1).trim();
|
|
355
|
+
}
|
|
356
|
+
stripDangerousKeys(hdrs);
|
|
357
|
+
resolve({ status: sm ? parseInt(sm[1], 10) : 0, statusText: sm ? sm[2] ?? "" : "", headers: hdrs, body });
|
|
358
|
+
};
|
|
359
|
+
tlsSock.on("end", fin);
|
|
360
|
+
tlsSock.on("close", fin);
|
|
361
|
+
tlsSock.on("error", (e) => { clearTimeout(timer); reject(e); });
|
|
362
|
+
});
|
|
363
|
+
tlsSock.on("error", (e) => { clearTimeout(timer); reject(e); });
|
|
364
|
+
});
|
|
365
|
+
connectReq.on("error", (e) => { clearTimeout(timer); reject(e); });
|
|
366
|
+
connectReq.end();
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// HTTP: route through proxy with full URL as request path
|
|
370
|
+
const req = http.request({
|
|
371
|
+
hostname: proxyHost, port: proxyPort, method, path: targetUrl.toString(),
|
|
372
|
+
headers: { "Proxy-Authorization": proxyAuth, Host: targetUrl.host ?? "", ...safeHeaders },
|
|
373
|
+
}, (res) => {
|
|
374
|
+
const chunks = [];
|
|
375
|
+
let bytes = 0;
|
|
376
|
+
res.on("data", (c) => { bytes += c.length; if (bytes <= MAX_RESP)
|
|
377
|
+
chunks.push(c); });
|
|
378
|
+
let done = false;
|
|
379
|
+
const fin = () => {
|
|
380
|
+
if (done)
|
|
381
|
+
return;
|
|
382
|
+
done = true;
|
|
383
|
+
clearTimeout(timer);
|
|
384
|
+
const body = Buffer.concat(chunks).toString("utf-8").substring(0, MAX_RESP);
|
|
385
|
+
const hdrs = {};
|
|
386
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
387
|
+
if (v)
|
|
388
|
+
hdrs[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
389
|
+
}
|
|
390
|
+
stripDangerousKeys(hdrs);
|
|
391
|
+
resolve({ status: res.statusCode ?? 0, statusText: res.statusMessage ?? "", headers: hdrs, body });
|
|
392
|
+
};
|
|
393
|
+
res.on("end", fin);
|
|
394
|
+
res.on("close", fin);
|
|
395
|
+
res.on("error", (e) => { clearTimeout(timer); reject(e); });
|
|
396
|
+
});
|
|
397
|
+
req.on("error", (e) => { clearTimeout(timer); reject(e); });
|
|
398
|
+
req.end();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
// Check response size
|
|
402
|
+
if (result.body.length > MAX_RESPONSE_BODY_BYTES) {
|
|
403
|
+
return { error: "Response too large (exceeds 10MB limit)" };
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
status: result.status,
|
|
407
|
+
statusText: result.statusText,
|
|
408
|
+
headers: filterHeaders(result.headers),
|
|
409
|
+
body: truncateBody(result.body),
|
|
410
|
+
proxyType,
|
|
411
|
+
country: country ?? "auto",
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
416
|
+
return {
|
|
417
|
+
error: `Proxy request failed: ${sanitizeError(message)}`,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Creates the `checkBalance` tool that retrieves the current wallet balance.
|
|
425
|
+
*/
|
|
426
|
+
export function createCheckBalanceTool(client) {
|
|
427
|
+
return tool({
|
|
428
|
+
description: "Check the current DomiNode wallet balance. " +
|
|
429
|
+
"Returns balance in cents and USD. Use this before making proxy requests " +
|
|
430
|
+
"to ensure sufficient funds.",
|
|
431
|
+
parameters: z.object({}),
|
|
432
|
+
execute: async () => {
|
|
433
|
+
try {
|
|
434
|
+
const balance = await client.wallet.getBalance();
|
|
435
|
+
return {
|
|
436
|
+
balanceCents: balance.balanceCents,
|
|
437
|
+
balanceUsd: balance.balanceUsd,
|
|
438
|
+
currency: balance.currency,
|
|
439
|
+
lastToppedUp: balance.lastToppedUp,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
444
|
+
return { error: `Failed to check balance: ${sanitizeError(message)}` };
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Creates the `checkUsage` tool that retrieves usage statistics.
|
|
451
|
+
*/
|
|
452
|
+
export function createCheckUsageTool(client) {
|
|
453
|
+
return tool({
|
|
454
|
+
description: "Check DomiNode proxy usage statistics for a given time period. " +
|
|
455
|
+
"Returns total bytes transferred, cost, and request count.",
|
|
456
|
+
parameters: z.object({
|
|
457
|
+
period: z
|
|
458
|
+
.enum(["day", "week", "month"])
|
|
459
|
+
.optional()
|
|
460
|
+
.describe("Time period for usage statistics: 'day' (last 24h), 'week' (last 7d), or 'month' (last 30d)"),
|
|
461
|
+
}),
|
|
462
|
+
execute: async ({ period }) => {
|
|
463
|
+
try {
|
|
464
|
+
// Calculate date range based on period
|
|
465
|
+
const now = new Date();
|
|
466
|
+
let since;
|
|
467
|
+
if (period === "day") {
|
|
468
|
+
const d = new Date(now);
|
|
469
|
+
d.setDate(d.getDate() - 1);
|
|
470
|
+
since = d.toISOString();
|
|
471
|
+
}
|
|
472
|
+
else if (period === "week") {
|
|
473
|
+
const d = new Date(now);
|
|
474
|
+
d.setDate(d.getDate() - 7);
|
|
475
|
+
since = d.toISOString();
|
|
476
|
+
}
|
|
477
|
+
else if (period === "month") {
|
|
478
|
+
const d = new Date(now);
|
|
479
|
+
d.setDate(d.getDate() - 30);
|
|
480
|
+
since = d.toISOString();
|
|
481
|
+
}
|
|
482
|
+
const usage = await client.usage.get({
|
|
483
|
+
from: since,
|
|
484
|
+
to: now.toISOString(),
|
|
485
|
+
limit: 100,
|
|
486
|
+
});
|
|
487
|
+
const summary = usage.summary;
|
|
488
|
+
const records = usage.records;
|
|
489
|
+
return {
|
|
490
|
+
summary: {
|
|
491
|
+
totalBytes: summary.totalBytes,
|
|
492
|
+
totalGB: summary.totalGB,
|
|
493
|
+
totalCostCents: summary.totalCostCents,
|
|
494
|
+
totalCostUsd: summary.totalCostUsd,
|
|
495
|
+
requestCount: summary.requestCount,
|
|
496
|
+
},
|
|
497
|
+
period: usage.period,
|
|
498
|
+
recordCount: records.length,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
503
|
+
return { error: `Failed to check usage: ${sanitizeError(message)}` };
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Creates the `getProxyConfig` tool that retrieves proxy endpoint configuration.
|
|
510
|
+
*/
|
|
511
|
+
export function createGetProxyConfigTool(client) {
|
|
512
|
+
return tool({
|
|
513
|
+
description: "Get DomiNode proxy endpoint configuration including supported countries, " +
|
|
514
|
+
"geo-targeting capabilities, and connection details. Use this to discover " +
|
|
515
|
+
"available proxy options before making requests.",
|
|
516
|
+
parameters: z.object({}),
|
|
517
|
+
execute: async () => {
|
|
518
|
+
try {
|
|
519
|
+
const config = await client.proxy.getConfig();
|
|
520
|
+
const httpProxy = config.httpProxy;
|
|
521
|
+
const socks5Proxy = config.socks5Proxy;
|
|
522
|
+
return {
|
|
523
|
+
endpoints: {
|
|
524
|
+
http: `${httpProxy.host}:${httpProxy.port}`,
|
|
525
|
+
socks5: `${socks5Proxy.host}:${socks5Proxy.port}`,
|
|
526
|
+
},
|
|
527
|
+
supportedCountries: config.supportedCountries,
|
|
528
|
+
blockedCountries: config.blockedCountries,
|
|
529
|
+
geoTargeting: config.geoTargeting ?? {
|
|
530
|
+
stateSupport: false,
|
|
531
|
+
citySupport: false,
|
|
532
|
+
asnSupport: false,
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
538
|
+
return { error: `Failed to get proxy config: ${sanitizeError(message)}` };
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Creates the `listSessions` tool that lists active proxy sessions.
|
|
545
|
+
*/
|
|
546
|
+
export function createListSessionsTool(client) {
|
|
547
|
+
return tool({
|
|
548
|
+
description: "List all currently active proxy sessions. Shows session ID, " +
|
|
549
|
+
"start time, and status for each active connection.",
|
|
550
|
+
parameters: z.object({}),
|
|
551
|
+
execute: async () => {
|
|
552
|
+
try {
|
|
553
|
+
const result = await client.sessions.getActive();
|
|
554
|
+
return {
|
|
555
|
+
sessions: result.sessions.map((s) => ({
|
|
556
|
+
id: String(s.id ?? ""),
|
|
557
|
+
startedAt: String(s.startedAt ?? ""),
|
|
558
|
+
status: String(s.status ?? ""),
|
|
559
|
+
})),
|
|
560
|
+
count: result.sessions.length,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
catch (err) {
|
|
564
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
565
|
+
return { error: `Failed to list sessions: ${sanitizeError(message)}` };
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Creates the `topupPaypal` tool that initiates a PayPal wallet top-up.
|
|
572
|
+
*/
|
|
573
|
+
export function createTopupPaypalTool(client) {
|
|
574
|
+
return tool({
|
|
575
|
+
description: "Top up your DomiNode wallet balance via PayPal. " +
|
|
576
|
+
"Creates a PayPal order and returns an approval URL to complete payment. " +
|
|
577
|
+
"Minimum $5 (500 cents), maximum $1,000 (100,000 cents).",
|
|
578
|
+
parameters: z.object({
|
|
579
|
+
amount_cents: z
|
|
580
|
+
.number()
|
|
581
|
+
.int()
|
|
582
|
+
.min(500)
|
|
583
|
+
.max(100000)
|
|
584
|
+
.describe("Amount in cents to top up (min 500 = $5, max 100000 = $1,000)"),
|
|
585
|
+
}),
|
|
586
|
+
execute: async ({ amount_cents }) => {
|
|
587
|
+
try {
|
|
588
|
+
const result = await client.wallet.topupPaypal({ amountCents: amount_cents });
|
|
589
|
+
return {
|
|
590
|
+
orderId: result.orderId,
|
|
591
|
+
approvalUrl: result.approvalUrl,
|
|
592
|
+
amountCents: result.amountCents,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
597
|
+
return { error: `Failed to create PayPal top-up: ${sanitizeError(message)}` };
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Creates the `x402Info` tool that retrieves x402 micropayment protocol information.
|
|
604
|
+
*/
|
|
605
|
+
export function createX402InfoTool(client) {
|
|
606
|
+
return tool({
|
|
607
|
+
description: "Get x402 micropayment protocol information including supported " +
|
|
608
|
+
"facilitators, pricing, and payment options.",
|
|
609
|
+
parameters: z.object({}),
|
|
610
|
+
execute: async () => {
|
|
611
|
+
try {
|
|
612
|
+
const result = await client.x402.getInfo();
|
|
613
|
+
if (result && typeof result === "object") {
|
|
614
|
+
stripDangerousKeys(result);
|
|
615
|
+
}
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
catch (err) {
|
|
619
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
620
|
+
return { error: `Failed to get x402 info: ${sanitizeError(message)}` };
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
// Authenticated API request helper
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
/** Make an authenticated JSON request to the DomiNode REST API. */
|
|
629
|
+
async function apiRequest(client, method, path, body) {
|
|
630
|
+
// Use client's baseUrl and apiKey for authentication
|
|
631
|
+
const baseUrl = client.baseUrl || "https://api.dominusnode.com";
|
|
632
|
+
const apiKey = client.apiKey || "";
|
|
633
|
+
const url = `${baseUrl}${path}`;
|
|
634
|
+
const headers = {
|
|
635
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
636
|
+
"Content-Type": "application/json",
|
|
637
|
+
};
|
|
638
|
+
const fetchOptions = { method, headers, redirect: "error" };
|
|
639
|
+
if (body && method !== "GET") {
|
|
640
|
+
fetchOptions.body = JSON.stringify(body);
|
|
641
|
+
}
|
|
642
|
+
const resp = await fetch(url, fetchOptions);
|
|
643
|
+
if (!resp.ok) {
|
|
644
|
+
const text = await resp.text().catch(() => "");
|
|
645
|
+
throw new Error(`API returned ${resp.status}: ${sanitizeError(text.slice(0, 200))}`);
|
|
646
|
+
}
|
|
647
|
+
const text = await resp.text();
|
|
648
|
+
if (text.length > MAX_RESPONSE_BODY_BYTES) {
|
|
649
|
+
throw new Error("Response body exceeds 10MB size limit");
|
|
650
|
+
}
|
|
651
|
+
const data = JSON.parse(text);
|
|
652
|
+
stripDangerousKeys(data);
|
|
653
|
+
return data;
|
|
654
|
+
}
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
// Agentic wallet tool definitions
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
/**
|
|
659
|
+
* Creates a tool to create a new agentic sub-wallet with a spending limit.
|
|
660
|
+
*/
|
|
661
|
+
export function createCreateAgenticWalletTool(client) {
|
|
662
|
+
return tool({
|
|
663
|
+
description: "Create a new agentic sub-wallet with a spending limit. " +
|
|
664
|
+
"Agentic wallets are custodial sub-wallets for AI agents with per-transaction spending caps.",
|
|
665
|
+
parameters: z.object({
|
|
666
|
+
label: z.string().min(1).max(100).describe("Human-readable label for the wallet"),
|
|
667
|
+
spending_limit_cents: z.number().int().min(1).max(2147483647).describe("Per-transaction spending limit in cents"),
|
|
668
|
+
daily_limit_cents: z.number().int().min(1).max(1000000).optional().describe("Optional daily spending limit in cents"),
|
|
669
|
+
allowed_domains: z.array(z.string().max(253).regex(DOMAIN_RE)).max(100).optional().describe("Optional list of allowed domains"),
|
|
670
|
+
}),
|
|
671
|
+
execute: async ({ label, spending_limit_cents, daily_limit_cents, allowed_domains }) => {
|
|
672
|
+
// Validate no control chars in label
|
|
673
|
+
if (/[\x00-\x1F\x7F]/.test(label)) {
|
|
674
|
+
return { error: "label contains invalid control characters" };
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
const body = { label, spendingLimitCents: spending_limit_cents };
|
|
678
|
+
if (daily_limit_cents !== undefined)
|
|
679
|
+
body.dailyLimitCents = daily_limit_cents;
|
|
680
|
+
if (allowed_domains !== undefined)
|
|
681
|
+
body.allowedDomains = allowed_domains;
|
|
682
|
+
const result = await apiRequest(client, "POST", "/api/agent-wallet", body);
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
return { error: `Failed to create agentic wallet: ${sanitizeError(err instanceof Error ? err.message : "Unknown error")}` };
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Creates a tool to transfer funds from the main wallet to an agentic sub-wallet.
|
|
693
|
+
*/
|
|
694
|
+
export function createFundAgenticWalletTool(client) {
|
|
695
|
+
return tool({
|
|
696
|
+
description: "Transfer funds from the main wallet to an agentic sub-wallet.",
|
|
697
|
+
parameters: z.object({
|
|
698
|
+
wallet_id: z.string().regex(UUID_RE, "Must be a valid UUID").describe("UUID of the agentic wallet"),
|
|
699
|
+
amount_cents: z.number().int().min(1).max(2147483647).describe("Amount in cents to transfer"),
|
|
700
|
+
}),
|
|
701
|
+
execute: async ({ wallet_id, amount_cents }) => {
|
|
702
|
+
try {
|
|
703
|
+
return await apiRequest(client, "POST", `/api/agent-wallet/${encodeURIComponent(wallet_id)}/fund`, { amountCents: amount_cents });
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
return { error: `Failed to fund wallet: ${sanitizeError(err instanceof Error ? err.message : "Unknown error")}` };
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Creates a tool to check the balance and details of an agentic sub-wallet.
|
|
713
|
+
*/
|
|
714
|
+
export function createAgenticWalletBalanceTool(client) {
|
|
715
|
+
return tool({
|
|
716
|
+
description: "Check the balance and details of an agentic sub-wallet.",
|
|
717
|
+
parameters: z.object({
|
|
718
|
+
wallet_id: z.string().regex(UUID_RE, "Must be a valid UUID").describe("UUID of the agentic wallet"),
|
|
719
|
+
}),
|
|
720
|
+
execute: async ({ wallet_id }) => {
|
|
721
|
+
try {
|
|
722
|
+
return await apiRequest(client, "GET", `/api/agent-wallet/${encodeURIComponent(wallet_id)}`);
|
|
723
|
+
}
|
|
724
|
+
catch (err) {
|
|
725
|
+
return { error: `Failed to get wallet balance: ${sanitizeError(err instanceof Error ? err.message : "Unknown error")}` };
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Creates a tool to list all agentic sub-wallets for the current user.
|
|
732
|
+
*/
|
|
733
|
+
export function createListAgenticWalletsTool(client) {
|
|
734
|
+
return tool({
|
|
735
|
+
description: "List all agentic sub-wallets for the current user.",
|
|
736
|
+
parameters: z.object({}),
|
|
737
|
+
execute: async () => {
|
|
738
|
+
try {
|
|
739
|
+
return await apiRequest(client, "GET", "/api/agent-wallet");
|
|
740
|
+
}
|
|
741
|
+
catch (err) {
|
|
742
|
+
return { error: `Failed to list wallets: ${sanitizeError(err instanceof Error ? err.message : "Unknown error")}` };
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Creates a tool to list recent transactions for an agentic sub-wallet.
|
|
749
|
+
*/
|
|
750
|
+
export function createAgenticTransactionsTool(client) {
|
|
751
|
+
return tool({
|
|
752
|
+
description: "List recent transactions for an agentic sub-wallet.",
|
|
753
|
+
parameters: z.object({
|
|
754
|
+
wallet_id: z.string().regex(UUID_RE, "Must be a valid UUID").describe("UUID of the agentic wallet"),
|
|
755
|
+
limit: z.number().int().min(1).max(100).optional().describe("Maximum transactions to return (1-100)"),
|
|
756
|
+
}),
|
|
757
|
+
execute: async ({ wallet_id, limit }) => {
|
|
758
|
+
try {
|
|
759
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
760
|
+
return await apiRequest(client, "GET", `/api/agent-wallet/${encodeURIComponent(wallet_id)}/transactions${qs}`);
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
return { error: `Failed to get transactions: ${sanitizeError(err instanceof Error ? err.message : "Unknown error")}` };
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Creates a tool to freeze an agentic sub-wallet to prevent further spending.
|
|
770
|
+
*/
|
|
771
|
+
export function createFreezeAgenticWalletTool(client) {
|
|
772
|
+
return tool({
|
|
773
|
+
description: "Freeze an agentic sub-wallet to prevent further spending.",
|
|
774
|
+
parameters: z.object({
|
|
775
|
+
wallet_id: z.string().regex(UUID_RE, "Must be a valid UUID").describe("UUID of the agentic wallet"),
|
|
776
|
+
}),
|
|
777
|
+
execute: async ({ wallet_id }) => {
|
|
778
|
+
try {
|
|
779
|
+
return await apiRequest(client, "POST", `/api/agent-wallet/${encodeURIComponent(wallet_id)}/freeze`);
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
return { error: `Failed to freeze wallet: ${sanitizeError(err instanceof Error ? err.message : "Unknown error")}` };
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Creates a tool to unfreeze a previously frozen agentic sub-wallet.
|
|
789
|
+
*/
|
|
790
|
+
export function createUnfreezeAgenticWalletTool(client) {
|
|
791
|
+
return tool({
|
|
792
|
+
description: "Unfreeze a previously frozen agentic sub-wallet to re-enable spending.",
|
|
793
|
+
parameters: z.object({
|
|
794
|
+
wallet_id: z.string().regex(UUID_RE, "Must be a valid UUID").describe("UUID of the agentic wallet"),
|
|
795
|
+
}),
|
|
796
|
+
execute: async ({ wallet_id }) => {
|
|
797
|
+
try {
|
|
798
|
+
return await apiRequest(client, "POST", `/api/agent-wallet/${encodeURIComponent(wallet_id)}/unfreeze`);
|
|
799
|
+
}
|
|
800
|
+
catch (err) {
|
|
801
|
+
return { error: `Failed to unfreeze wallet: ${sanitizeError(err instanceof Error ? err.message : "Unknown error")}` };
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Creates a tool to delete an agentic sub-wallet. Must be active (not frozen).
|
|
808
|
+
* Remaining balance returns to main wallet.
|
|
809
|
+
*/
|
|
810
|
+
export function createDeleteAgenticWalletTool(client) {
|
|
811
|
+
return tool({
|
|
812
|
+
description: "Delete an agentic sub-wallet. Must be active (not frozen). Remaining balance returns to main wallet.",
|
|
813
|
+
parameters: z.object({
|
|
814
|
+
wallet_id: z.string().regex(UUID_RE, "Must be a valid UUID").describe("UUID of the agentic wallet"),
|
|
815
|
+
}),
|
|
816
|
+
execute: async ({ wallet_id }) => {
|
|
817
|
+
try {
|
|
818
|
+
return await apiRequest(client, "DELETE", `/api/agent-wallet/${encodeURIComponent(wallet_id)}`);
|
|
819
|
+
}
|
|
820
|
+
catch (err) {
|
|
821
|
+
return { error: `Failed to delete wallet: ${sanitizeError(err instanceof Error ? err.message : "Unknown error")}` };
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Creates a tool to update the policy (daily limit, allowed domains) of an agentic sub-wallet.
|
|
828
|
+
*/
|
|
829
|
+
export function createUpdateWalletPolicyTool(client) {
|
|
830
|
+
return tool({
|
|
831
|
+
description: "Update the policy (daily limit, allowed domains) of an agentic sub-wallet.",
|
|
832
|
+
parameters: z.object({
|
|
833
|
+
wallet_id: z.string().regex(UUID_RE, "Must be a valid UUID").describe("UUID of the agentic wallet"),
|
|
834
|
+
daily_limit_cents: z.number().int().min(1).max(1000000).optional().describe("New daily spending limit in cents"),
|
|
835
|
+
allowed_domains: z.array(z.string().max(253).regex(DOMAIN_RE)).max(100).optional().describe("New allowed domains list"),
|
|
836
|
+
}),
|
|
837
|
+
execute: async ({ wallet_id, daily_limit_cents, allowed_domains }) => {
|
|
838
|
+
const body = {};
|
|
839
|
+
if (daily_limit_cents !== undefined)
|
|
840
|
+
body.dailyLimitCents = daily_limit_cents;
|
|
841
|
+
if (allowed_domains !== undefined)
|
|
842
|
+
body.allowedDomains = allowed_domains;
|
|
843
|
+
if (Object.keys(body).length === 0) {
|
|
844
|
+
return { error: "At least one of daily_limit_cents or allowed_domains must be provided" };
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
return await apiRequest(client, "PATCH", `/api/agent-wallet/${encodeURIComponent(wallet_id)}/policy`, body);
|
|
848
|
+
}
|
|
849
|
+
catch (err) {
|
|
850
|
+
return { error: `Failed to update policy: ${sanitizeError(err instanceof Error ? err.message : "Unknown error")}` };
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
// Re-export validation helpers for testing
|
|
856
|
+
export { validateUrl, isPrivateIp, truncateBody, filterHeaders, apiRequest, UUID_RE, DOMAIN_RE };
|
|
857
|
+
//# sourceMappingURL=tools.js.map
|