@dominusnode/pi-extension 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.
@@ -0,0 +1,1109 @@
1
+ "use strict";
2
+ /**
3
+ * Dominus Node Pi Extension Toolkit
4
+ *
5
+ * Provides 26 tools for the pi-mono agent framework (github.com/badlogic/pi-mono).
6
+ * Covers proxied fetching, wallet management, agentic wallets, teams,
7
+ * Stripe/PayPal/crypto top-up, and x402 micropayment info.
8
+ *
9
+ * Security:
10
+ * - Full SSRF protection (private IP blocking, DNS rebinding, Teredo/6to4,
11
+ * IPv4-mapped/compatible IPv6, hex/octal/decimal normalization, zone ID
12
+ * stripping, .localhost/.local/.internal/.arpa TLD blocking, embedded
13
+ * credential blocking)
14
+ * - OFAC sanctioned country validation (CU, IR, KP, RU, SY)
15
+ * - Credential scrubbing in all error outputs
16
+ * - Prototype pollution prevention on all parsed JSON (recursive)
17
+ * - HTTP method restriction (GET/HEAD/OPTIONS only for proxied fetch)
18
+ * - 10 MB response cap, 4000 char truncation, 30 s timeout
19
+ * - Redirect following disabled to prevent open redirect abuse
20
+ * - DNS rebinding protection on all proxied requests
21
+ */
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.DominusNodeToolkit = exports.BLOCKED_HOSTNAMES = exports.SANCTIONED_COUNTRIES = void 0;
57
+ exports.scrubCredentials = scrubCredentials;
58
+ exports.truncate = truncate;
59
+ exports.normalizeIpv4 = normalizeIpv4;
60
+ exports.isPrivateIp = isPrivateIp;
61
+ exports.validateTargetUrl = validateTargetUrl;
62
+ exports.validateCountry = validateCountry;
63
+ exports.validateUuid = validateUuid;
64
+ exports.checkDnsRebinding = checkDnsRebinding;
65
+ exports.stripDangerousKeys = stripDangerousKeys;
66
+ exports.formatBytes = formatBytes;
67
+ exports.formatCents = formatCents;
68
+ const http = __importStar(require("node:http"));
69
+ const tls = __importStar(require("node:tls"));
70
+ const dns = __importStar(require("dns/promises"));
71
+ // ---------------------------------------------------------------------------
72
+ // Constants
73
+ // ---------------------------------------------------------------------------
74
+ const MAX_RESPONSE_CHARS = 4000;
75
+ const REQUEST_TIMEOUT_MS = 30_000;
76
+ const MAX_RESPONSE_BYTES = 10 * 1024 * 1024; // 10 MB hard cap
77
+ const MAX_PROXY_RESPONSE_BYTES = 1_048_576; // 1 MB for proxied fetch
78
+ const ALLOWED_PROXY_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
79
+ /** OFAC sanctioned countries — must never be used as geo-targeting destinations. */
80
+ exports.SANCTIONED_COUNTRIES = new Set(["CU", "IR", "KP", "RU", "SY"]);
81
+ /** ISO 3166-1 alpha-2 country code pattern. */
82
+ const COUNTRY_CODE_RE = /^[A-Z]{2}$/;
83
+ /** UUID v4 pattern for wallet/team IDs. */
84
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
85
+ exports.BLOCKED_HOSTNAMES = new Set([
86
+ "localhost",
87
+ "localhost.localdomain",
88
+ "ip6-localhost",
89
+ "ip6-loopback",
90
+ "[::1]",
91
+ "[::ffff:127.0.0.1]",
92
+ "0.0.0.0",
93
+ "[::]",
94
+ ]);
95
+ // ---------------------------------------------------------------------------
96
+ // Credential scrubbing
97
+ // ---------------------------------------------------------------------------
98
+ /** Remove any dn_live_* or dn_test_* tokens from error messages. */
99
+ function scrubCredentials(msg) {
100
+ return msg.replace(/dn_(live|test)_[A-Za-z0-9_-]+/g, "dn_$1_***REDACTED***");
101
+ }
102
+ function safeError(err) {
103
+ const raw = err instanceof Error ? err.message : String(err);
104
+ return scrubCredentials(raw);
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // Response truncation
108
+ // ---------------------------------------------------------------------------
109
+ function truncate(text, max = MAX_RESPONSE_CHARS) {
110
+ if (text.length <= max)
111
+ return text;
112
+ return text.slice(0, max) + `\n\n... [truncated, ${text.length - max} chars omitted]`;
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // SSRF Protection
116
+ // ---------------------------------------------------------------------------
117
+ /**
118
+ * Normalize non-standard IPv4 representations to dotted-decimal.
119
+ * Handles: decimal integers (2130706433), hex (0x7f000001), octal octets (0177.0.0.1).
120
+ */
121
+ function normalizeIpv4(hostname) {
122
+ // Helper: parse one numeric part (supports hex 0x.., octal 0.., decimal)
123
+ function parsePart(part) {
124
+ if (/^0x[0-9a-fA-F]+$/i.test(part))
125
+ return parseInt(part, 16);
126
+ if (/^0\d+$/.test(part))
127
+ return parseInt(part, 8);
128
+ if (/^\d+$/.test(part))
129
+ return parseInt(part, 10);
130
+ return NaN;
131
+ }
132
+ function u32ToDotted(n) {
133
+ return `${(n >>> 24) & 0xff}.${(n >>> 16) & 0xff}.${(n >>> 8) & 0xff}.${n & 0xff}`;
134
+ }
135
+ // Single integer form (decimal or hex) — e.g. 2130706433 or 0x7f000001
136
+ if (/^\d+$/.test(hostname)) {
137
+ const n = parseInt(hostname, 10);
138
+ if (n >= 0 && n <= 0xffffffff)
139
+ return u32ToDotted(n);
140
+ }
141
+ if (/^0x[0-9a-fA-F]+$/i.test(hostname)) {
142
+ const n = parseInt(hostname, 16);
143
+ if (n >= 0 && n <= 0xffffffff)
144
+ return u32ToDotted(n);
145
+ }
146
+ const parts = hostname.split(".");
147
+ if (parts.length === 4) {
148
+ // a.b.c.d — each octet 0-255
149
+ const octets = parts.map(parsePart);
150
+ if (octets.some((v) => isNaN(v) || v < 0 || v > 255))
151
+ return null;
152
+ return octets.join(".");
153
+ }
154
+ if (parts.length === 3) {
155
+ // a.b.cd — a,b: 0-255, cd: 0-65535 → a.b.(cd>>8).(cd&0xff)
156
+ const [a, b, cd] = parts.map(parsePart);
157
+ if ([a, b].some((v) => isNaN(v) || v < 0 || v > 255))
158
+ return null;
159
+ if (isNaN(cd) || cd < 0 || cd > 0xffff)
160
+ return null;
161
+ return `${a}.${b}.${(cd >>> 8) & 0xff}.${cd & 0xff}`;
162
+ }
163
+ if (parts.length === 2) {
164
+ // a.bcd — a: 0-255, bcd: 0-16777215 → a.(bcd>>16).(bcd>>8&0xff).(bcd&0xff)
165
+ const [a, bcd] = parts.map(parsePart);
166
+ if (isNaN(a) || a < 0 || a > 255)
167
+ return null;
168
+ if (isNaN(bcd) || bcd < 0 || bcd > 0xffffff)
169
+ return null;
170
+ return `${a}.${(bcd >>> 16) & 0xff}.${(bcd >>> 8) & 0xff}.${bcd & 0xff}`;
171
+ }
172
+ return null;
173
+ }
174
+ function isPrivateIp(hostname) {
175
+ let ip = hostname.replace(/^\[|\]$/g, "");
176
+ const zoneIdx = ip.indexOf("%");
177
+ if (zoneIdx !== -1)
178
+ ip = ip.substring(0, zoneIdx);
179
+ const normalized = normalizeIpv4(ip);
180
+ const checkIp = normalized ?? ip;
181
+ const ipv4Match = checkIp.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
182
+ if (ipv4Match) {
183
+ const a = Number(ipv4Match[1]);
184
+ const b = Number(ipv4Match[2]);
185
+ if (a === 0)
186
+ return true;
187
+ if (a === 10)
188
+ return true;
189
+ if (a === 127)
190
+ return true;
191
+ if (a === 169 && b === 254)
192
+ return true;
193
+ if (a === 172 && b >= 16 && b <= 31)
194
+ return true;
195
+ if (a === 192 && b === 168)
196
+ return true;
197
+ if (a === 100 && b >= 64 && b <= 127)
198
+ return true; // CGNAT
199
+ if (a >= 224)
200
+ return true; // Multicast / reserved
201
+ return false;
202
+ }
203
+ const ipLower = ip.toLowerCase();
204
+ if (ipLower === "::1")
205
+ return true;
206
+ if (ipLower === "::")
207
+ return true;
208
+ if (ipLower.startsWith("fc") || ipLower.startsWith("fd"))
209
+ return true; // ULA
210
+ if (ipLower.startsWith("fe80"))
211
+ return true; // Link-local
212
+ if (ipLower.startsWith("ff"))
213
+ return true; // Multicast
214
+ if (ipLower.startsWith("2001:0000:") || ipLower.startsWith("2001:0:"))
215
+ return true; // Teredo
216
+ if (ipLower.startsWith("2002:"))
217
+ return true; // 6to4
218
+ // IPv4-mapped ::ffff:x.x.x.x
219
+ if (ipLower.startsWith("::ffff:")) {
220
+ const embedded = ipLower.slice(7);
221
+ if (embedded.includes("."))
222
+ return isPrivateIp(embedded);
223
+ const hexParts = embedded.split(":");
224
+ if (hexParts.length === 2) {
225
+ const hi = parseInt(hexParts[0], 16);
226
+ const lo = parseInt(hexParts[1], 16);
227
+ if (!isNaN(hi) && !isNaN(lo)) {
228
+ return isPrivateIp(`${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`);
229
+ }
230
+ }
231
+ return isPrivateIp(embedded);
232
+ }
233
+ // IPv4-compatible ::x.x.x.x (deprecated but parsed)
234
+ if (ipLower.startsWith("::") && !ipLower.startsWith("::ffff:")) {
235
+ const rest = ipLower.slice(2);
236
+ if (rest && rest.includes("."))
237
+ return isPrivateIp(rest);
238
+ const hexParts = rest.split(":");
239
+ if (hexParts.length === 2 && hexParts[0] && hexParts[1]) {
240
+ const hi = parseInt(hexParts[0], 16);
241
+ const lo = parseInt(hexParts[1], 16);
242
+ if (!isNaN(hi) && !isNaN(lo)) {
243
+ return isPrivateIp(`${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`);
244
+ }
245
+ }
246
+ }
247
+ return false;
248
+ }
249
+ function validateTargetUrl(url) {
250
+ let parsed;
251
+ try {
252
+ parsed = new URL(url);
253
+ }
254
+ catch {
255
+ throw new Error(`Invalid URL: ${scrubCredentials(url)}`);
256
+ }
257
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
258
+ throw new Error(`Only http: and https: protocols are supported, got ${parsed.protocol}`);
259
+ }
260
+ if (parsed.username || parsed.password) {
261
+ throw new Error("URLs with embedded credentials are not allowed");
262
+ }
263
+ const hostname = parsed.hostname.toLowerCase();
264
+ if (exports.BLOCKED_HOSTNAMES.has(hostname)) {
265
+ throw new Error("Requests to localhost/loopback addresses are blocked");
266
+ }
267
+ if (isPrivateIp(hostname)) {
268
+ throw new Error("Requests to private/internal IP addresses are blocked");
269
+ }
270
+ if (hostname.endsWith(".localhost")) {
271
+ throw new Error("Requests to localhost/loopback addresses are blocked");
272
+ }
273
+ if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".arpa")) {
274
+ throw new Error("Requests to internal network hostnames are blocked");
275
+ }
276
+ return parsed;
277
+ }
278
+ function validateCountry(country) {
279
+ if (!country)
280
+ return undefined;
281
+ const upper = country.toUpperCase().trim();
282
+ if (!COUNTRY_CODE_RE.test(upper)) {
283
+ throw new Error(`Invalid country code: "${country}". Must be a 2-letter ISO 3166-1 code.`);
284
+ }
285
+ if (exports.SANCTIONED_COUNTRIES.has(upper)) {
286
+ throw new Error(`Country "${upper}" is OFAC sanctioned and cannot be used as a proxy target.`);
287
+ }
288
+ return upper;
289
+ }
290
+ function validateUuid(id, label) {
291
+ const trimmed = id.trim();
292
+ if (!UUID_RE.test(trimmed)) {
293
+ throw new Error(`Invalid ${label}: must be a valid UUID.`);
294
+ }
295
+ return trimmed;
296
+ }
297
+ // ---------------------------------------------------------------------------
298
+ // DNS rebinding protection
299
+ // ---------------------------------------------------------------------------
300
+ async function checkDnsRebinding(hostname) {
301
+ const stripped = hostname.replace(/^\[|\]$/g, "");
302
+ if (/^(\d{1,3}\.){3}\d{1,3}$/.test(stripped) || stripped.includes(":"))
303
+ return;
304
+ // M-4: Use stripped (bracket-removed) hostname for DNS resolution
305
+ const resolvedIps = [];
306
+ try {
307
+ resolvedIps.push(...(await dns.resolve4(stripped)));
308
+ }
309
+ catch { /* NODATA/NXDOMAIN for A is fine */ }
310
+ try {
311
+ resolvedIps.push(...(await dns.resolve6(stripped)));
312
+ }
313
+ catch { /* NODATA/NXDOMAIN for AAAA is fine */ }
314
+ for (const ip of resolvedIps) {
315
+ if (isPrivateIp(ip)) {
316
+ throw new Error(`DNS rebinding detected: hostname resolves to private IP ${ip}`);
317
+ }
318
+ }
319
+ }
320
+ // ---------------------------------------------------------------------------
321
+ // Prototype pollution prevention (recursive)
322
+ // ---------------------------------------------------------------------------
323
+ const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
324
+ function stripDangerousKeys(obj, depth = 0) {
325
+ if (depth > 50 || !obj || typeof obj !== "object")
326
+ return;
327
+ if (Array.isArray(obj)) {
328
+ for (const item of obj)
329
+ stripDangerousKeys(item, depth + 1);
330
+ return;
331
+ }
332
+ const record = obj;
333
+ for (const key of Object.keys(record)) {
334
+ if (DANGEROUS_KEYS.has(key)) {
335
+ delete record[key];
336
+ }
337
+ else if (record[key] && typeof record[key] === "object") {
338
+ stripDangerousKeys(record[key], depth + 1);
339
+ }
340
+ }
341
+ }
342
+ // ---------------------------------------------------------------------------
343
+ // Formatting helpers
344
+ // ---------------------------------------------------------------------------
345
+ function formatBytes(bytes) {
346
+ if (bytes < 1024)
347
+ return `${bytes} B`;
348
+ if (bytes < 1024 * 1024)
349
+ return `${(bytes / 1024).toFixed(1)} KB`;
350
+ if (bytes < 1024 * 1024 * 1024)
351
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
352
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(3)} GB`;
353
+ }
354
+ function formatCents(cents) {
355
+ return `$${(cents / 100).toFixed(2)}`;
356
+ }
357
+ function ok(text) {
358
+ return { content: [{ type: "text", text: truncate(text) }], details: {} };
359
+ }
360
+ /** C-1: Validate a label string — reject empty, control chars, and over-length values.
361
+ * M-1: Includes ASCII C0/C1, DEL, BOM, Unicode line/paragraph separators, bidi overrides. */
362
+ function validateLabel(label, fieldName = "label") {
363
+ const trimmed = label.trim();
364
+ if (!trimmed)
365
+ return `${fieldName} is required`;
366
+ if (trimmed.length > 100)
367
+ return `${fieldName} must be 1-100 characters`;
368
+ if (/[\x00-\x1f\x7f\x80-\x9f\u2028\u2029\ufeff\u200e\u200f\u202a-\u202e\u2066-\u2069]/.test(trimmed))
369
+ return `${fieldName} contains invalid control characters`;
370
+ return null;
371
+ }
372
+ function err(e) {
373
+ return ok(`Error: ${safeError(e)}`);
374
+ }
375
+ // ---------------------------------------------------------------------------
376
+ // DominusNodeToolkit
377
+ // ---------------------------------------------------------------------------
378
+ class DominusNodeToolkit {
379
+ apiKey;
380
+ baseUrl;
381
+ proxyHost;
382
+ proxyPort;
383
+ timeout;
384
+ token = null;
385
+ tokenExpiresAt = 0;
386
+ _authPromise = null; // H-1: mutex for concurrent _ensureAuth
387
+ constructor(options = {}) {
388
+ this.apiKey = options.apiKey || process.env["DOMINUSNODE_API_KEY"] || "";
389
+ // H-6: Validate baseUrl
390
+ const rawBase = (options.baseUrl || process.env["DOMINUSNODE_BASE_URL"] || "https://api.dominusnode.com").replace(/\/+$/, "");
391
+ try {
392
+ const parsedBase = new URL(rawBase);
393
+ if (parsedBase.protocol !== "https:" && parsedBase.protocol !== "http:")
394
+ throw new Error("only http/https allowed");
395
+ if (parsedBase.username || parsedBase.password)
396
+ throw new Error("embedded credentials not allowed in base URL");
397
+ // H-1: Warn on HTTP base URL in production
398
+ if (parsedBase.protocol === "http:" && parsedBase.hostname !== "localhost" && parsedBase.hostname !== "127.0.0.1") {
399
+ console.warn("[DominusNode] WARNING: baseUrl uses HTTP — API key will be sent unencrypted");
400
+ }
401
+ }
402
+ catch (e) {
403
+ throw new Error(`Invalid DOMINUSNODE_BASE_URL: ${e instanceof Error ? e.message : String(e)}`);
404
+ }
405
+ this.baseUrl = rawBase;
406
+ // H-2: Validate proxyHost against private/internal IPs
407
+ const rawProxyHost = options.proxyHost || process.env["DOMINUSNODE_PROXY_HOST"] || "proxy.dominusnode.com";
408
+ if (exports.BLOCKED_HOSTNAMES.has(rawProxyHost.toLowerCase()) || isPrivateIp(rawProxyHost)) {
409
+ throw new Error("DOMINUSNODE_PROXY_HOST must not be a private or loopback address");
410
+ }
411
+ if (rawProxyHost.endsWith(".localhost") || rawProxyHost.endsWith(".local") ||
412
+ rawProxyHost.endsWith(".internal") || rawProxyHost.endsWith(".arpa")) {
413
+ throw new Error("DOMINUSNODE_PROXY_HOST must not use internal network TLDs");
414
+ }
415
+ this.proxyHost = rawProxyHost;
416
+ const portVal = Number(options.proxyPort ?? process.env["DOMINUSNODE_PROXY_PORT"] ?? 8080);
417
+ this.proxyPort = isNaN(portVal) || portVal < 1 || portVal > 65535 ? 8080 : portVal;
418
+ // H-2: Validate timeout option; store but use REQUEST_TIMEOUT_MS constant for requests
419
+ const tVal = Number(options.timeout ?? 30000);
420
+ this.timeout = Number.isFinite(tVal) && tVal >= 1000 && tVal <= 120000 ? tVal : 30000;
421
+ }
422
+ // -----------------------------------------------------------------------
423
+ // Authentication
424
+ // -----------------------------------------------------------------------
425
+ async _authenticate() {
426
+ if (!this.apiKey)
427
+ throw new Error("Dominus Node API key is required. Set DOMINUSNODE_API_KEY env var.");
428
+ if (!this.apiKey.startsWith("dn_live_") && !this.apiKey.startsWith("dn_test_")) {
429
+ throw new Error('DOMINUSNODE_API_KEY must start with "dn_live_" or "dn_test_".');
430
+ }
431
+ const res = await fetch(`${this.baseUrl}/api/auth/verify-key`, {
432
+ method: "POST",
433
+ headers: { "Content-Type": "application/json", "User-Agent": "dominusnode-pi/1.0.0" },
434
+ body: JSON.stringify({ apiKey: this.apiKey }),
435
+ redirect: "error",
436
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
437
+ });
438
+ if (!res.ok) {
439
+ // L-1: Bounded error body read — avoid buffering large error responses
440
+ let errorText = "";
441
+ if (res.body) {
442
+ const reader = res.body.getReader();
443
+ let eb = 0;
444
+ const parts = [];
445
+ try {
446
+ while (eb < 4096) {
447
+ const { done, value } = await reader.read();
448
+ if (done)
449
+ break;
450
+ eb += value.length;
451
+ parts.push(value);
452
+ }
453
+ }
454
+ catch { /* ignore */ }
455
+ reader.cancel().catch(() => { });
456
+ errorText = Buffer.concat(parts).toString("utf-8").slice(0, 500);
457
+ }
458
+ throw new Error(`Auth failed (${res.status}): ${scrubCredentials(errorText)}`);
459
+ }
460
+ // H-5: Cap auth response size and validate token type
461
+ const rawText = await res.text().catch(() => "");
462
+ if (new TextEncoder().encode(rawText).length > 65536)
463
+ throw new Error("Auth response too large");
464
+ const data = JSON.parse(rawText);
465
+ stripDangerousKeys(data);
466
+ if (!data.token || typeof data.token !== "string" || data.token.length > 4096) {
467
+ throw new Error("Invalid token in auth response");
468
+ }
469
+ this.token = data.token;
470
+ this.tokenExpiresAt = Date.now() + 14 * 60 * 1000;
471
+ return this.token;
472
+ }
473
+ async _ensureAuth() {
474
+ if (this.token && Date.now() < this.tokenExpiresAt)
475
+ return;
476
+ // H-1: Serialize concurrent auth calls — all concurrent callers await the same promise
477
+ if (this._authPromise) {
478
+ await this._authPromise;
479
+ return;
480
+ }
481
+ this._authPromise = this._authenticate()
482
+ .then(() => { this._authPromise = null; })
483
+ .catch((e) => { this._authPromise = null; throw e; });
484
+ await this._authPromise;
485
+ }
486
+ async _apiRequest(method, path, body) {
487
+ await this._ensureAuth();
488
+ const url = `${this.baseUrl}${path}`;
489
+ const headers = {
490
+ Authorization: `Bearer ${this.token}`,
491
+ Accept: "application/json",
492
+ "User-Agent": "dominusnode-pi/1.0.0",
493
+ };
494
+ const init = { method, headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), redirect: "error" };
495
+ // M-6: Drop body on safe HTTP methods
496
+ if (body && !["GET", "HEAD"].includes(method) && ["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
497
+ headers["Content-Type"] = "application/json";
498
+ init.body = JSON.stringify(body);
499
+ }
500
+ let res;
501
+ try {
502
+ res = await fetch(url, init);
503
+ }
504
+ catch (e) {
505
+ throw new Error(`API request failed: ${safeError(e)}`);
506
+ }
507
+ // C-2: Stream response body with size cap to avoid memory exhaustion
508
+ let rawText = "";
509
+ if (res.body) {
510
+ const reader = res.body.getReader();
511
+ const chunks = [];
512
+ let totalBytes = 0;
513
+ try {
514
+ while (true) {
515
+ const { done, value } = await reader.read();
516
+ if (done)
517
+ break;
518
+ totalBytes += value.length;
519
+ if (totalBytes > MAX_RESPONSE_BYTES) {
520
+ reader.cancel().catch(() => { });
521
+ throw new Error("API response too large");
522
+ }
523
+ chunks.push(value);
524
+ }
525
+ }
526
+ catch (e) {
527
+ reader.cancel().catch(() => { });
528
+ throw e;
529
+ }
530
+ rawText = Buffer.concat(chunks).toString("utf-8"); // M-3: Uint8Array[] accepted directly
531
+ }
532
+ if (!res.ok) {
533
+ let msg = `API error ${res.status}`;
534
+ try {
535
+ const parsed = JSON.parse(rawText);
536
+ stripDangerousKeys(parsed);
537
+ const detail = parsed.error ?? parsed.message;
538
+ if (detail)
539
+ msg = `API error ${res.status}: ${scrubCredentials(String(detail))}`;
540
+ }
541
+ catch {
542
+ if (rawText)
543
+ msg = `API error ${res.status}: ${scrubCredentials(rawText.slice(0, 200))}`;
544
+ }
545
+ throw new Error(msg);
546
+ }
547
+ if (!rawText || !rawText.trim())
548
+ return {};
549
+ try {
550
+ const parsed = JSON.parse(rawText);
551
+ stripDangerousKeys(parsed);
552
+ return parsed;
553
+ }
554
+ catch {
555
+ throw new Error("Failed to parse API response as JSON");
556
+ }
557
+ }
558
+ async _req(method, path, body) {
559
+ try {
560
+ return await this._apiRequest(method, path, body);
561
+ }
562
+ catch (e) {
563
+ // H-7: Match exact "API error 401" pattern, not any string containing "401"
564
+ if (e instanceof Error && /^API error 401\b/.test(e.message)) {
565
+ this.token = null;
566
+ this.tokenExpiresAt = 0;
567
+ return await this._apiRequest(method, path, body);
568
+ }
569
+ throw e;
570
+ }
571
+ }
572
+ // -----------------------------------------------------------------------
573
+ // Tool 1: proxiedFetch
574
+ // -----------------------------------------------------------------------
575
+ async proxiedFetch(url, method = "GET", country, proxyType = "dc") {
576
+ try {
577
+ if (!url)
578
+ return err("url parameter is required");
579
+ const parsedUrl = validateTargetUrl(url);
580
+ await checkDnsRebinding(parsedUrl.hostname);
581
+ const validCountry = validateCountry(country);
582
+ const methodUpper = method.toUpperCase();
583
+ if (!ALLOWED_PROXY_METHODS.has(methodUpper))
584
+ return err(`HTTP method "${methodUpper}" is not allowed. Only GET, HEAD, OPTIONS permitted.`);
585
+ if (!["dc", "residential", "auto"].includes(proxyType))
586
+ return err("proxyType must be 'dc', 'residential', or 'auto'");
587
+ if (!this.apiKey)
588
+ return err("API key is required for proxied fetch");
589
+ const parts = [];
590
+ if (proxyType !== "auto")
591
+ parts.push(proxyType);
592
+ if (validCountry)
593
+ parts.push(`country-${validCountry}`);
594
+ const username = parts.length ? parts.join("-") : "auto";
595
+ const proxyAuth = "Basic " + Buffer.from(`${username}:${this.apiKey}`).toString("base64");
596
+ // C-3: Use the already-validated parsedUrl — do not re-parse the raw url string
597
+ const parsed = parsedUrl;
598
+ const result = await new Promise((resolve, reject) => {
599
+ // H-3: Shared cleanup reference — each branch sets this to destroy its own request socket
600
+ let cleanupFn = null;
601
+ const timer = setTimeout(() => {
602
+ cleanupFn?.();
603
+ reject(new Error("Proxy request timed out"));
604
+ }, REQUEST_TIMEOUT_MS);
605
+ if (parsed.protocol === "https:") {
606
+ const connectHost = parsed.hostname.includes(":") ? `[${parsed.hostname}]` : parsed.hostname;
607
+ const connectReq = http.request({
608
+ hostname: this.proxyHost, port: this.proxyPort, method: "CONNECT",
609
+ path: `${connectHost}:${parsed.port || 443}`,
610
+ headers: { "Proxy-Authorization": proxyAuth, Host: `${connectHost}:${parsed.port || 443}` },
611
+ });
612
+ cleanupFn = () => connectReq.destroy();
613
+ connectReq.on("connect", (_res, sock) => {
614
+ if (_res.statusCode !== 200) {
615
+ clearTimeout(timer);
616
+ sock.destroy();
617
+ reject(new Error(`CONNECT failed: ${_res.statusCode}`));
618
+ return;
619
+ }
620
+ const tlsSock = tls.connect({ host: parsed.hostname, socket: sock, servername: parsed.hostname, minVersion: "TLSv1.2" }, () => {
621
+ const reqLine = `${methodUpper} ${parsed.pathname + parsed.search} HTTP/1.1\r\nHost: ${parsed.host}\r\nUser-Agent: dominusnode-pi/1.0.0\r\nConnection: close\r\n\r\n`;
622
+ tlsSock.write(reqLine);
623
+ const chunks = [];
624
+ let bytes = 0;
625
+ tlsSock.on("data", (c) => {
626
+ bytes += c.length;
627
+ if (bytes > MAX_PROXY_RESPONSE_BYTES + 16384) {
628
+ tlsSock.destroy();
629
+ return;
630
+ } // H-4: destroy on oversize
631
+ chunks.push(c);
632
+ });
633
+ let done = false;
634
+ const fin = () => {
635
+ if (done)
636
+ return;
637
+ done = true;
638
+ clearTimeout(timer);
639
+ const raw = Buffer.concat(chunks).toString("utf-8");
640
+ const hEnd = raw.indexOf("\r\n\r\n");
641
+ if (hEnd === -1) {
642
+ reject(new Error("Malformed response"));
643
+ return;
644
+ }
645
+ const hdr = raw.substring(0, hEnd);
646
+ const body = raw.substring(hEnd + 4).substring(0, MAX_PROXY_RESPONSE_BYTES);
647
+ const sm = hdr.split("\r\n")[0].match(/^HTTP\/\d\.\d\s+(\d+)/);
648
+ resolve({ status: sm ? parseInt(sm[1], 10) : 0, body });
649
+ };
650
+ tlsSock.on("end", fin);
651
+ tlsSock.on("close", fin);
652
+ tlsSock.on("error", (e) => { clearTimeout(timer); reject(e); });
653
+ });
654
+ // M-1: Update cleanupFn to destroy tlsSock — covers the TLS handshake window
655
+ cleanupFn = () => tlsSock.destroy();
656
+ tlsSock.on("error", (e) => { clearTimeout(timer); reject(e); });
657
+ });
658
+ connectReq.on("error", (e) => { clearTimeout(timer); reject(e); });
659
+ connectReq.end();
660
+ }
661
+ else {
662
+ const reqPath = parsed.pathname + parsed.search;
663
+ const proxyReq = http.request({
664
+ hostname: this.proxyHost, port: this.proxyPort, method: methodUpper,
665
+ path: `http://${parsed.host}${reqPath}`,
666
+ headers: { "Proxy-Authorization": proxyAuth, Host: parsed.host, "User-Agent": "dominusnode-pi/1.0.0", Connection: "close" },
667
+ });
668
+ cleanupFn = () => proxyReq.destroy(); // H-3: destroy on timeout
669
+ const chunks = [];
670
+ let bytes = 0;
671
+ proxyReq.on("response", (r) => {
672
+ r.on("data", (c) => {
673
+ bytes += c.length;
674
+ if (bytes > MAX_PROXY_RESPONSE_BYTES + 16384) {
675
+ r.destroy();
676
+ return;
677
+ } // H-4: destroy on oversize
678
+ chunks.push(c);
679
+ });
680
+ r.on("end", () => { clearTimeout(timer); const body = Buffer.concat(chunks).toString("utf-8").substring(0, MAX_PROXY_RESPONSE_BYTES); resolve({ status: r.statusCode ?? 0, body }); });
681
+ r.on("error", (e) => { clearTimeout(timer); reject(e); });
682
+ });
683
+ proxyReq.on("error", (e) => { clearTimeout(timer); reject(e); });
684
+ proxyReq.end();
685
+ }
686
+ });
687
+ return ok(`HTTP ${result.status}\n\n${result.body}`);
688
+ }
689
+ catch (e) {
690
+ return err(e);
691
+ }
692
+ }
693
+ // -----------------------------------------------------------------------
694
+ // Tool 2: checkBalance
695
+ // -----------------------------------------------------------------------
696
+ async checkBalance() {
697
+ try {
698
+ const data = await this._req("GET", "/api/wallet");
699
+ return ok(JSON.stringify(data, null, 2));
700
+ }
701
+ catch (e) {
702
+ return err(e);
703
+ }
704
+ }
705
+ // -----------------------------------------------------------------------
706
+ // Tool 3: checkUsage
707
+ // -----------------------------------------------------------------------
708
+ async checkUsage(days = 30) {
709
+ try {
710
+ const rawDays = Number(days);
711
+ if (!Number.isFinite(rawDays))
712
+ return err("days must be a finite number between 1 and 365");
713
+ const d = Math.max(1, Math.min(365, Math.floor(rawDays)));
714
+ const until = new Date().toISOString();
715
+ const since = new Date(Date.now() - d * 86400000).toISOString();
716
+ const data = await this._req("GET", `/api/usage?since=${encodeURIComponent(since)}&until=${encodeURIComponent(until)}`);
717
+ return ok(JSON.stringify(data, null, 2));
718
+ }
719
+ catch (e) {
720
+ return err(e);
721
+ }
722
+ }
723
+ // -----------------------------------------------------------------------
724
+ // Tool 4: getProxyConfig
725
+ // -----------------------------------------------------------------------
726
+ async getProxyConfig() {
727
+ try {
728
+ const data = await this._req("GET", "/api/proxy/config");
729
+ return ok(JSON.stringify(data, null, 2));
730
+ }
731
+ catch (e) {
732
+ return err(e);
733
+ }
734
+ }
735
+ // -----------------------------------------------------------------------
736
+ // Tool 5: listSessions
737
+ // -----------------------------------------------------------------------
738
+ async listSessions() {
739
+ try {
740
+ const data = await this._req("GET", "/api/proxy/sessions");
741
+ return ok(JSON.stringify(data, null, 2));
742
+ }
743
+ catch (e) {
744
+ return err(e);
745
+ }
746
+ }
747
+ // -----------------------------------------------------------------------
748
+ // Tool 6: createAgenticWallet
749
+ // -----------------------------------------------------------------------
750
+ async createAgenticWallet(label, spendingLimitCents) {
751
+ try {
752
+ const labelErr = validateLabel(label);
753
+ if (labelErr)
754
+ return err(labelErr);
755
+ const body = { label: label.trim() };
756
+ if (spendingLimitCents !== undefined) {
757
+ const limit = Math.floor(Number(spendingLimitCents));
758
+ if (isNaN(limit) || limit < 1 || limit > 1000000)
759
+ return err("spendingLimitCents must be between 1 and 1,000,000");
760
+ body.spendingLimitCents = limit;
761
+ }
762
+ const data = await this._req("POST", "/api/agent-wallet", body);
763
+ return ok(JSON.stringify(data, null, 2));
764
+ }
765
+ catch (e) {
766
+ return err(e);
767
+ }
768
+ }
769
+ // -----------------------------------------------------------------------
770
+ // Tool 7: fundAgenticWallet
771
+ // -----------------------------------------------------------------------
772
+ async fundAgenticWallet(walletId, amountCents) {
773
+ try {
774
+ const id = validateUuid(walletId, "walletId");
775
+ const amount = Math.floor(Number(amountCents));
776
+ if (isNaN(amount) || amount < 1 || amount > 1000000)
777
+ return err("amountCents must be between 1 and 1,000,000");
778
+ const data = await this._req("POST", `/api/agent-wallet/${id}/fund`, { amountCents: amount });
779
+ return ok(JSON.stringify(data, null, 2));
780
+ }
781
+ catch (e) {
782
+ return err(e);
783
+ }
784
+ }
785
+ // -----------------------------------------------------------------------
786
+ // Tool 8: checkAgenticBalance
787
+ // -----------------------------------------------------------------------
788
+ async checkAgenticBalance(walletId) {
789
+ try {
790
+ const id = validateUuid(walletId, "walletId");
791
+ const data = await this._req("GET", `/api/agent-wallet/${id}`);
792
+ return ok(JSON.stringify(data, null, 2));
793
+ }
794
+ catch (e) {
795
+ return err(e);
796
+ }
797
+ }
798
+ // -----------------------------------------------------------------------
799
+ // Tool 9: listAgenticWallets
800
+ // -----------------------------------------------------------------------
801
+ async listAgenticWallets() {
802
+ try {
803
+ const data = await this._req("GET", "/api/agent-wallet");
804
+ return ok(JSON.stringify(data, null, 2));
805
+ }
806
+ catch (e) {
807
+ return err(e);
808
+ }
809
+ }
810
+ // -----------------------------------------------------------------------
811
+ // Tool 10: agenticTransactions
812
+ // -----------------------------------------------------------------------
813
+ async agenticTransactions(walletId, limit = 20) {
814
+ try {
815
+ const id = validateUuid(walletId, "walletId");
816
+ const rawLim = Number(limit);
817
+ if (!Number.isFinite(rawLim))
818
+ return err("limit must be a finite number");
819
+ const lim = Math.max(1, Math.min(100, Math.floor(rawLim)));
820
+ const data = await this._req("GET", `/api/agent-wallet/${id}/transactions?limit=${lim}`);
821
+ return ok(JSON.stringify(data, null, 2));
822
+ }
823
+ catch (e) {
824
+ return err(e);
825
+ }
826
+ }
827
+ // -----------------------------------------------------------------------
828
+ // Tool 11: freezeAgenticWallet
829
+ // -----------------------------------------------------------------------
830
+ async freezeAgenticWallet(walletId) {
831
+ try {
832
+ const id = validateUuid(walletId, "walletId");
833
+ const data = await this._req("POST", `/api/agent-wallet/${id}/freeze`);
834
+ return ok(JSON.stringify(data, null, 2));
835
+ }
836
+ catch (e) {
837
+ return err(e);
838
+ }
839
+ }
840
+ // -----------------------------------------------------------------------
841
+ // Tool 12: unfreezeAgenticWallet
842
+ // -----------------------------------------------------------------------
843
+ async unfreezeAgenticWallet(walletId) {
844
+ try {
845
+ const id = validateUuid(walletId, "walletId");
846
+ const data = await this._req("POST", `/api/agent-wallet/${id}/unfreeze`);
847
+ return ok(JSON.stringify(data, null, 2));
848
+ }
849
+ catch (e) {
850
+ return err(e);
851
+ }
852
+ }
853
+ // -----------------------------------------------------------------------
854
+ // Tool 13: deleteAgenticWallet
855
+ // -----------------------------------------------------------------------
856
+ async deleteAgenticWallet(walletId) {
857
+ try {
858
+ const id = validateUuid(walletId, "walletId");
859
+ const data = await this._req("DELETE", `/api/agent-wallet/${id}`);
860
+ return ok(JSON.stringify(data, null, 2));
861
+ }
862
+ catch (e) {
863
+ return err(e);
864
+ }
865
+ }
866
+ // -----------------------------------------------------------------------
867
+ // Tool 14: createTeam
868
+ // -----------------------------------------------------------------------
869
+ async createTeam(name, maxMembers = 10) {
870
+ try {
871
+ const nameErr = validateLabel(name, "name");
872
+ if (nameErr)
873
+ return err(nameErr);
874
+ const max = Math.max(1, Math.min(100, Math.floor(Number(maxMembers))));
875
+ if (isNaN(max))
876
+ return err("maxMembers must be a number between 1 and 100");
877
+ const data = await this._req("POST", "/api/teams", { name: name.trim(), maxMembers: max });
878
+ return ok(JSON.stringify(data, null, 2));
879
+ }
880
+ catch (e) {
881
+ return err(e);
882
+ }
883
+ }
884
+ // -----------------------------------------------------------------------
885
+ // Tool 15: listTeams
886
+ // -----------------------------------------------------------------------
887
+ async listTeams(limit = 20) {
888
+ try {
889
+ const rawLim = Number(limit);
890
+ if (!Number.isFinite(rawLim))
891
+ return err("limit must be a finite number");
892
+ const lim = Math.max(1, Math.min(100, Math.floor(rawLim)));
893
+ const data = await this._req("GET", `/api/teams?limit=${lim}`);
894
+ return ok(JSON.stringify(data, null, 2));
895
+ }
896
+ catch (e) {
897
+ return err(e);
898
+ }
899
+ }
900
+ // -----------------------------------------------------------------------
901
+ // Tool 16: teamDetails
902
+ // -----------------------------------------------------------------------
903
+ async teamDetails(teamId) {
904
+ try {
905
+ const id = validateUuid(teamId, "teamId");
906
+ const data = await this._req("GET", `/api/teams/${id}`);
907
+ return ok(JSON.stringify(data, null, 2));
908
+ }
909
+ catch (e) {
910
+ return err(e);
911
+ }
912
+ }
913
+ // -----------------------------------------------------------------------
914
+ // Tool 17: teamFund
915
+ // -----------------------------------------------------------------------
916
+ async teamFund(teamId, amountCents) {
917
+ try {
918
+ const id = validateUuid(teamId, "teamId");
919
+ const amount = Math.floor(Number(amountCents));
920
+ if (isNaN(amount) || amount < 1 || amount > 1000000)
921
+ return err("amountCents must be between 1 and 1,000,000");
922
+ const data = await this._req("POST", `/api/teams/${id}/wallet/fund`, { amountCents: amount });
923
+ return ok(JSON.stringify(data, null, 2));
924
+ }
925
+ catch (e) {
926
+ return err(e);
927
+ }
928
+ }
929
+ // -----------------------------------------------------------------------
930
+ // Tool 18: teamCreateApiKey
931
+ // -----------------------------------------------------------------------
932
+ async teamCreateApiKey(teamId, label) {
933
+ try {
934
+ const id = validateUuid(teamId, "teamId");
935
+ const labelErr = validateLabel(label);
936
+ if (labelErr)
937
+ return err(labelErr);
938
+ const data = await this._req("POST", `/api/teams/${id}/keys`, { label: label.trim() });
939
+ return ok(JSON.stringify(data, null, 2) + "\n\nIMPORTANT: Store this API key securely — it will not be shown again.");
940
+ }
941
+ catch (e) {
942
+ return err(e);
943
+ }
944
+ }
945
+ // -----------------------------------------------------------------------
946
+ // Tool 19: teamUsage
947
+ // -----------------------------------------------------------------------
948
+ async teamUsage(teamId, days = 30) {
949
+ try {
950
+ const id = validateUuid(teamId, "teamId");
951
+ const rawDays = Number(days);
952
+ if (!Number.isFinite(rawDays))
953
+ return err("days must be a finite number between 1 and 365");
954
+ const d = Math.max(1, Math.min(365, Math.floor(rawDays)));
955
+ const until = new Date().toISOString();
956
+ const since = new Date(Date.now() - d * 86400000).toISOString();
957
+ const data = await this._req("GET", `/api/teams/${id}/usage?since=${encodeURIComponent(since)}&until=${encodeURIComponent(until)}`);
958
+ return ok(JSON.stringify(data, null, 2));
959
+ }
960
+ catch (e) {
961
+ return err(e);
962
+ }
963
+ }
964
+ // -----------------------------------------------------------------------
965
+ // Tool 20: updateTeam
966
+ // -----------------------------------------------------------------------
967
+ async updateTeam(teamId, name, maxMembers) {
968
+ try {
969
+ const id = validateUuid(teamId, "teamId");
970
+ const body = {};
971
+ if (name !== undefined) {
972
+ const nameErr = validateLabel(name, "name");
973
+ if (nameErr)
974
+ return err(nameErr);
975
+ body.name = name.trim();
976
+ }
977
+ if (maxMembers !== undefined) {
978
+ const max = Math.max(1, Math.min(100, Math.floor(Number(maxMembers))));
979
+ if (isNaN(max))
980
+ return err("maxMembers must be a number");
981
+ body.maxMembers = max;
982
+ }
983
+ if (!Object.keys(body).length)
984
+ return err("At least one of name or maxMembers must be provided");
985
+ const data = await this._req("PATCH", `/api/teams/${id}`, body);
986
+ return ok(JSON.stringify(data, null, 2));
987
+ }
988
+ catch (e) {
989
+ return err(e);
990
+ }
991
+ }
992
+ // -----------------------------------------------------------------------
993
+ // Tool 21: updateTeamMemberRole
994
+ // -----------------------------------------------------------------------
995
+ async updateTeamMemberRole(teamId, userId, role) {
996
+ try {
997
+ const tid = validateUuid(teamId, "teamId");
998
+ const uid = validateUuid(userId, "userId");
999
+ if (!["admin", "member"].includes(role))
1000
+ return err("role must be 'admin' or 'member'");
1001
+ const data = await this._req("PATCH", `/api/teams/${tid}/members/${uid}`, { role });
1002
+ return ok(JSON.stringify(data, null, 2));
1003
+ }
1004
+ catch (e) {
1005
+ return err(e);
1006
+ }
1007
+ }
1008
+ // -----------------------------------------------------------------------
1009
+ // Tool 22: topupPaypal
1010
+ // -----------------------------------------------------------------------
1011
+ async topupPaypal(amountCents) {
1012
+ try {
1013
+ const amount = Math.floor(Number(amountCents));
1014
+ if (isNaN(amount) || amount < 500 || amount > 1000000)
1015
+ return err("amountCents must be between 500 ($5.00) and 1,000,000 ($10,000.00)");
1016
+ const data = await this._req("POST", "/api/wallet/topup/paypal", { amountCents: amount });
1017
+ return ok(JSON.stringify(data, null, 2));
1018
+ }
1019
+ catch (e) {
1020
+ return err(e);
1021
+ }
1022
+ }
1023
+ // -----------------------------------------------------------------------
1024
+ // Tool 23: topupStripe
1025
+ // -----------------------------------------------------------------------
1026
+ async topupStripe(amountCents) {
1027
+ try {
1028
+ const amount = Math.floor(Number(amountCents));
1029
+ if (isNaN(amount) || amount < 500 || amount > 1000000)
1030
+ return err("amountCents must be between 500 ($5.00) and 1,000,000 ($10,000.00)");
1031
+ const data = await this._req("POST", "/api/wallet/topup/stripe", { amountCents: amount });
1032
+ return ok(JSON.stringify(data, null, 2));
1033
+ }
1034
+ catch (e) {
1035
+ return err(e);
1036
+ }
1037
+ }
1038
+ // -----------------------------------------------------------------------
1039
+ // Tool 24: topupCrypto
1040
+ // -----------------------------------------------------------------------
1041
+ async topupCrypto(amountUsd, currency) {
1042
+ try {
1043
+ // M-2: Require whole-number dollars to avoid silent truncation (5.9 → 5)
1044
+ const rawAmount = Number(amountUsd);
1045
+ if (!Number.isInteger(rawAmount) || rawAmount < 5 || rawAmount > 10000)
1046
+ return err("amountUsd must be a whole number between 5 and 10,000");
1047
+ const amount = rawAmount;
1048
+ const VALID_CURRENCIES = ["BTC", "ETH", "LTC", "XMR", "ZEC", "USDC", "SOL", "USDT", "DAI", "BNB", "LINK"];
1049
+ const cur = (currency || "").toUpperCase().trim();
1050
+ if (!VALID_CURRENCIES.includes(cur))
1051
+ return err(`currency must be one of: ${VALID_CURRENCIES.join(", ")}`);
1052
+ const data = await this._req("POST", "/api/wallet/topup/crypto", { amountUsd: amount, currency: cur });
1053
+ return ok(JSON.stringify(data, null, 2));
1054
+ }
1055
+ catch (e) {
1056
+ return err(e);
1057
+ }
1058
+ }
1059
+ // -----------------------------------------------------------------------
1060
+ // Tool 25: x402Info
1061
+ // -----------------------------------------------------------------------
1062
+ async x402Info() {
1063
+ try {
1064
+ const data = await this._req("GET", "/api/wallet/x402/info");
1065
+ return ok(JSON.stringify(data, null, 2));
1066
+ }
1067
+ catch (e) {
1068
+ return err(e);
1069
+ }
1070
+ }
1071
+ // -----------------------------------------------------------------------
1072
+ // Tool 26: updateWalletPolicy
1073
+ // -----------------------------------------------------------------------
1074
+ async updateWalletPolicy(walletId, dailyLimitCents, allowedDomains) {
1075
+ try {
1076
+ const id = validateUuid(walletId, "walletId");
1077
+ const body = {};
1078
+ if (dailyLimitCents !== undefined) {
1079
+ const limit = Math.floor(Number(dailyLimitCents));
1080
+ if (isNaN(limit) || limit < 0 || limit > 1000000)
1081
+ return err("dailyLimitCents must be between 0 and 1,000,000");
1082
+ body.dailyLimitCents = limit;
1083
+ }
1084
+ if (allowedDomains !== undefined) {
1085
+ if (!Array.isArray(allowedDomains))
1086
+ return err("allowedDomains must be an array of strings");
1087
+ if (allowedDomains.length > 100)
1088
+ return err("allowedDomains must have at most 100 entries");
1089
+ for (let i = 0; i < allowedDomains.length; i++) {
1090
+ if (typeof allowedDomains[i] !== "string")
1091
+ return err(`allowedDomains[${i}] must be a string`);
1092
+ if (allowedDomains[i].length > 253)
1093
+ return err(`allowedDomains[${i}] exceeds 253 characters`);
1094
+ if (/[\x00-\x1f\x7f]/.test(allowedDomains[i]))
1095
+ return err(`allowedDomains[${i}] contains invalid characters`);
1096
+ }
1097
+ body.allowedDomains = allowedDomains.map((d) => String(d).trim().toLowerCase());
1098
+ }
1099
+ if (!Object.keys(body).length)
1100
+ return err("At least one of dailyLimitCents or allowedDomains must be provided");
1101
+ const data = await this._req("PATCH", `/api/agent-wallet/${id}/policy`, body);
1102
+ return ok(JSON.stringify(data, null, 2));
1103
+ }
1104
+ catch (e) {
1105
+ return err(e);
1106
+ }
1107
+ }
1108
+ }
1109
+ exports.DominusNodeToolkit = DominusNodeToolkit;