@dominusnode/openclaw-plugin 1.1.0 → 1.3.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 +89 -67
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +1459 -74
- package/package.json +3 -1
package/dist/plugin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dominus Node OpenClaw Plugin
|
|
3
3
|
*
|
|
4
|
-
* Implements
|
|
4
|
+
* Implements 53 tools for interacting with Dominus Node's rotating proxy service
|
|
5
5
|
* directly from OpenClaw AI coding sessions.
|
|
6
6
|
*
|
|
7
7
|
* Uses native fetch (no external dependencies). Runs via jiti runtime.
|
|
@@ -12,10 +12,63 @@
|
|
|
12
12
|
* - Response truncation at 4000 chars for LLM context efficiency
|
|
13
13
|
* - OFAC sanctioned country validation
|
|
14
14
|
*/
|
|
15
|
+
import * as crypto from "node:crypto";
|
|
15
16
|
import * as http from "node:http";
|
|
16
17
|
import * as tls from "node:tls";
|
|
17
18
|
import * as dns from "dns/promises";
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
20
|
+
// SHA-256 Proof-of-Work solver
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
function countLeadingZeroBits(buf) {
|
|
23
|
+
let count = 0;
|
|
24
|
+
for (const byte of buf) {
|
|
25
|
+
if (byte === 0) {
|
|
26
|
+
count += 8;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let mask = 0x80;
|
|
30
|
+
while (mask && !(byte & mask)) {
|
|
31
|
+
count++;
|
|
32
|
+
mask >>= 1;
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
return count;
|
|
37
|
+
}
|
|
38
|
+
async function solvePoW(solveBaseUrl) {
|
|
39
|
+
try {
|
|
40
|
+
const resp = await fetch(`${solveBaseUrl}/api/auth/pow/challenge`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "Content-Type": "application/json" },
|
|
43
|
+
redirect: "error",
|
|
44
|
+
});
|
|
45
|
+
if (!resp.ok)
|
|
46
|
+
return null;
|
|
47
|
+
const text = await resp.text();
|
|
48
|
+
if (text.length > 10_485_760)
|
|
49
|
+
return null;
|
|
50
|
+
const challenge = JSON.parse(text);
|
|
51
|
+
const prefix = challenge.prefix ?? "";
|
|
52
|
+
const difficulty = challenge.difficulty ?? 20;
|
|
53
|
+
const challengeId = challenge.challengeId ?? "";
|
|
54
|
+
if (!prefix || !challengeId)
|
|
55
|
+
return null;
|
|
56
|
+
for (let nonce = 0; nonce < 100_000_000; nonce++) {
|
|
57
|
+
const hash = crypto
|
|
58
|
+
.createHash("sha256")
|
|
59
|
+
.update(prefix + nonce.toString())
|
|
60
|
+
.digest();
|
|
61
|
+
if (countLeadingZeroBits(hash) >= difficulty) {
|
|
62
|
+
return { challengeId, nonce: nonce.toString() };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
19
72
|
// Configuration
|
|
20
73
|
// ---------------------------------------------------------------------------
|
|
21
74
|
const MAX_RESPONSE_CHARS = 4000;
|
|
@@ -69,7 +122,7 @@ function getAgentSecret() {
|
|
|
69
122
|
// ---------------------------------------------------------------------------
|
|
70
123
|
/** Remove any dn_live_* or dn_test_* tokens from error messages. */
|
|
71
124
|
function scrubCredentials(msg) {
|
|
72
|
-
return msg.replace(/dn_(live|test)_[A-Za-z0-9_-]+/g, "dn_$1_***REDACTED***");
|
|
125
|
+
return msg.replace(/dn_(live|test|proxy)_[A-Za-z0-9_-]+/g, "dn_$1_***REDACTED***");
|
|
73
126
|
}
|
|
74
127
|
function safeError(err) {
|
|
75
128
|
const raw = err instanceof Error ? err.message : String(err);
|
|
@@ -81,7 +134,8 @@ function safeError(err) {
|
|
|
81
134
|
function truncate(text, max = MAX_RESPONSE_CHARS) {
|
|
82
135
|
if (text.length <= max)
|
|
83
136
|
return text;
|
|
84
|
-
return text.slice(0, max) +
|
|
137
|
+
return (text.slice(0, max) +
|
|
138
|
+
`\n\n... [truncated, ${text.length - max} chars omitted]`);
|
|
85
139
|
}
|
|
86
140
|
// ---------------------------------------------------------------------------
|
|
87
141
|
// SSRF Protection
|
|
@@ -352,7 +406,9 @@ let jwtExpiresAt = 0;
|
|
|
352
406
|
let cachedApiKeyPrefix = null;
|
|
353
407
|
async function ensureAuth(apiKey, baseUrl) {
|
|
354
408
|
const keyPrefix = apiKey.slice(0, 16);
|
|
355
|
-
if (cachedJwt &&
|
|
409
|
+
if (cachedJwt &&
|
|
410
|
+
Date.now() < jwtExpiresAt &&
|
|
411
|
+
cachedApiKeyPrefix === keyPrefix)
|
|
356
412
|
return cachedJwt;
|
|
357
413
|
const authHeaders = {
|
|
358
414
|
"Content-Type": "application/json",
|
|
@@ -373,7 +429,7 @@ async function ensureAuth(apiKey, baseUrl) {
|
|
|
373
429
|
const text = await res.text().catch(() => "");
|
|
374
430
|
throw new Error(`Auth failed (${res.status}): ${scrubCredentials(text.slice(0, 500))}`);
|
|
375
431
|
}
|
|
376
|
-
const data = await res.json();
|
|
432
|
+
const data = (await res.json());
|
|
377
433
|
cachedApiKeyPrefix = keyPrefix;
|
|
378
434
|
cachedJwt = data.token;
|
|
379
435
|
// JWT expires in 15 min, refresh at 14 min for safety
|
|
@@ -386,8 +442,8 @@ async function apiRequest(method, path, body) {
|
|
|
386
442
|
const url = `${baseUrl}${path}`;
|
|
387
443
|
const jwt = await ensureAuth(apiKey, baseUrl);
|
|
388
444
|
const headers = {
|
|
389
|
-
|
|
390
|
-
|
|
445
|
+
Authorization: `Bearer ${jwt}`,
|
|
446
|
+
Accept: "application/json",
|
|
391
447
|
"User-Agent": "dominusnode-openclaw-plugin/1.0.0",
|
|
392
448
|
};
|
|
393
449
|
const agentSecret = getAgentSecret();
|
|
@@ -466,6 +522,83 @@ async function apiDelete(path) {
|
|
|
466
522
|
async function apiPatch(path, body) {
|
|
467
523
|
return apiRequest("PATCH", path, body);
|
|
468
524
|
}
|
|
525
|
+
async function apiPut(path, body) {
|
|
526
|
+
return apiRequest("PUT", path, body);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Make an unauthenticated API request (for register, login, verify-email).
|
|
530
|
+
* Does NOT call ensureAuth(). Includes agent headers if available.
|
|
531
|
+
*/
|
|
532
|
+
async function unauthenticatedRequest(method, path, body) {
|
|
533
|
+
const baseUrl = getBaseUrl();
|
|
534
|
+
const url = `${baseUrl}${path}`;
|
|
535
|
+
const headers = {
|
|
536
|
+
Accept: "application/json",
|
|
537
|
+
"User-Agent": "dominusnode-openclaw-plugin/1.0.0",
|
|
538
|
+
};
|
|
539
|
+
const agentSecret = getAgentSecret();
|
|
540
|
+
if (agentSecret) {
|
|
541
|
+
headers["X-DominusNode-Agent"] = "mcp";
|
|
542
|
+
headers["X-DominusNode-Agent-Secret"] = agentSecret;
|
|
543
|
+
}
|
|
544
|
+
const init = {
|
|
545
|
+
method,
|
|
546
|
+
headers,
|
|
547
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
548
|
+
redirect: "error",
|
|
549
|
+
};
|
|
550
|
+
if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
|
|
551
|
+
headers["Content-Type"] = "application/json";
|
|
552
|
+
init.body = JSON.stringify(body);
|
|
553
|
+
}
|
|
554
|
+
let res;
|
|
555
|
+
try {
|
|
556
|
+
res = await fetch(url, init);
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
throw new Error(`API request failed: ${safeError(err)}`);
|
|
560
|
+
}
|
|
561
|
+
let rawText;
|
|
562
|
+
try {
|
|
563
|
+
rawText = await res.text();
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
rawText = "";
|
|
567
|
+
}
|
|
568
|
+
if (new TextEncoder().encode(rawText).length > MAX_RESPONSE_BYTES) {
|
|
569
|
+
throw new Error("API response too large");
|
|
570
|
+
}
|
|
571
|
+
if (!res.ok) {
|
|
572
|
+
let errorMsg = `API error ${res.status}`;
|
|
573
|
+
if (rawText) {
|
|
574
|
+
try {
|
|
575
|
+
const parsed = JSON.parse(rawText);
|
|
576
|
+
stripDangerousKeys(parsed);
|
|
577
|
+
if (parsed.error) {
|
|
578
|
+
errorMsg = `API error ${res.status}: ${scrubCredentials(String(parsed.error))}`;
|
|
579
|
+
}
|
|
580
|
+
else if (parsed.message) {
|
|
581
|
+
errorMsg = `API error ${res.status}: ${scrubCredentials(String(parsed.message))}`;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
errorMsg = `API error ${res.status}: ${scrubCredentials(rawText.slice(0, 200))}`;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
throw new Error(errorMsg);
|
|
589
|
+
}
|
|
590
|
+
if (!rawText || rawText.trim().length === 0) {
|
|
591
|
+
return {};
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
const parsed = JSON.parse(rawText);
|
|
595
|
+
stripDangerousKeys(parsed);
|
|
596
|
+
return parsed;
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
throw new Error("Failed to parse API response as JSON");
|
|
600
|
+
}
|
|
601
|
+
}
|
|
469
602
|
// ---------------------------------------------------------------------------
|
|
470
603
|
// Formatting helpers
|
|
471
604
|
// ---------------------------------------------------------------------------
|
|
@@ -535,9 +668,19 @@ const proxiedFetchTool = {
|
|
|
535
668
|
}
|
|
536
669
|
const proxyType = String(args.pool ?? "dc");
|
|
537
670
|
// Validate and collect custom headers
|
|
538
|
-
const STRIPPED_HEADERS = new Set([
|
|
671
|
+
const STRIPPED_HEADERS = new Set([
|
|
672
|
+
"host",
|
|
673
|
+
"connection",
|
|
674
|
+
"content-length",
|
|
675
|
+
"transfer-encoding",
|
|
676
|
+
"proxy-authorization",
|
|
677
|
+
"authorization",
|
|
678
|
+
"user-agent",
|
|
679
|
+
]);
|
|
539
680
|
const customHeaders = {};
|
|
540
|
-
if (args.headers &&
|
|
681
|
+
if (args.headers &&
|
|
682
|
+
typeof args.headers === "object" &&
|
|
683
|
+
!Array.isArray(args.headers)) {
|
|
541
684
|
for (const [k, v] of Object.entries(args.headers)) {
|
|
542
685
|
const key = String(k);
|
|
543
686
|
const val = String(v ?? "");
|
|
@@ -562,15 +705,24 @@ const proxiedFetchTool = {
|
|
|
562
705
|
const parsed = new URL(url);
|
|
563
706
|
const MAX_RESP = 1_048_576; // 1MB
|
|
564
707
|
// Build custom header lines for raw HTTP request
|
|
565
|
-
const customHeaderLines = Object.entries(customHeaders)
|
|
708
|
+
const customHeaderLines = Object.entries(customHeaders)
|
|
709
|
+
.map(([k, v]) => `${k}: ${v}\r\n`)
|
|
710
|
+
.join("");
|
|
566
711
|
const result = await new Promise((resolve, reject) => {
|
|
567
712
|
const timer = setTimeout(() => reject(new Error("Proxy request timed out")), 30_000);
|
|
568
713
|
if (parsed.protocol === "https:") {
|
|
569
|
-
const connectHost = parsed.hostname.includes(":")
|
|
714
|
+
const connectHost = parsed.hostname.includes(":")
|
|
715
|
+
? `[${parsed.hostname}]`
|
|
716
|
+
: parsed.hostname;
|
|
570
717
|
const connectReq = http.request({
|
|
571
|
-
hostname: proxyHost,
|
|
718
|
+
hostname: proxyHost,
|
|
719
|
+
port: proxyPort,
|
|
720
|
+
method: "CONNECT",
|
|
572
721
|
path: `${connectHost}:${parsed.port || 443}`,
|
|
573
|
-
headers: {
|
|
722
|
+
headers: {
|
|
723
|
+
"Proxy-Authorization": proxyAuth,
|
|
724
|
+
Host: `${connectHost}:${parsed.port || 443}`,
|
|
725
|
+
},
|
|
574
726
|
});
|
|
575
727
|
connectReq.on("connect", (_res, sock) => {
|
|
576
728
|
if (_res.statusCode !== 200) {
|
|
@@ -579,13 +731,21 @@ const proxiedFetchTool = {
|
|
|
579
731
|
reject(new Error(`CONNECT failed: ${_res.statusCode}`));
|
|
580
732
|
return;
|
|
581
733
|
}
|
|
582
|
-
const tlsSock = tls.connect({
|
|
734
|
+
const tlsSock = tls.connect({
|
|
735
|
+
host: parsed.hostname,
|
|
736
|
+
socket: sock,
|
|
737
|
+
servername: parsed.hostname,
|
|
738
|
+
minVersion: "TLSv1.2",
|
|
739
|
+
}, () => {
|
|
583
740
|
const reqLine = `${method} ${parsed.pathname + parsed.search} HTTP/1.1\r\nHost: ${parsed.host}\r\nUser-Agent: dominusnode-openclaw/1.0.0\r\n${customHeaderLines}Connection: close\r\n\r\n`;
|
|
584
741
|
tlsSock.write(reqLine);
|
|
585
742
|
const chunks = [];
|
|
586
743
|
let bytes = 0;
|
|
587
|
-
tlsSock.on("data", (c) => {
|
|
588
|
-
|
|
744
|
+
tlsSock.on("data", (c) => {
|
|
745
|
+
bytes += c.length;
|
|
746
|
+
if (bytes <= MAX_RESP + 16384)
|
|
747
|
+
chunks.push(c);
|
|
748
|
+
});
|
|
589
749
|
let done = false;
|
|
590
750
|
const fin = () => {
|
|
591
751
|
if (done)
|
|
@@ -600,40 +760,69 @@ const proxiedFetchTool = {
|
|
|
600
760
|
}
|
|
601
761
|
const hdr = raw.substring(0, hEnd);
|
|
602
762
|
const body = raw.substring(hEnd + 4).substring(0, MAX_RESP);
|
|
603
|
-
const sm = hdr
|
|
763
|
+
const sm = hdr
|
|
764
|
+
.split("\r\n")[0]
|
|
765
|
+
.match(/^HTTP\/\d\.\d\s+(\d+)/);
|
|
604
766
|
const hdrs = {};
|
|
605
767
|
for (const l of hdr.split("\r\n").slice(1)) {
|
|
606
768
|
const ci = l.indexOf(":");
|
|
607
769
|
if (ci > 0)
|
|
608
|
-
hdrs[l.substring(0, ci).trim().toLowerCase()] = l
|
|
770
|
+
hdrs[l.substring(0, ci).trim().toLowerCase()] = l
|
|
771
|
+
.substring(ci + 1)
|
|
772
|
+
.trim();
|
|
609
773
|
}
|
|
610
|
-
resolve({
|
|
774
|
+
resolve({
|
|
775
|
+
status: sm ? parseInt(sm[1], 10) : 0,
|
|
776
|
+
headers: hdrs,
|
|
777
|
+
body,
|
|
778
|
+
});
|
|
611
779
|
};
|
|
612
780
|
tlsSock.on("end", fin);
|
|
613
781
|
tlsSock.on("close", fin);
|
|
614
|
-
tlsSock.on("error", (e) => {
|
|
782
|
+
tlsSock.on("error", (e) => {
|
|
783
|
+
clearTimeout(timer);
|
|
784
|
+
reject(e);
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
tlsSock.on("error", (e) => {
|
|
788
|
+
clearTimeout(timer);
|
|
789
|
+
reject(e);
|
|
615
790
|
});
|
|
616
|
-
tlsSock.on("error", (e) => { clearTimeout(timer); reject(e); });
|
|
617
791
|
});
|
|
618
|
-
connectReq.on("error", (e) => {
|
|
792
|
+
connectReq.on("error", (e) => {
|
|
793
|
+
clearTimeout(timer);
|
|
794
|
+
reject(e);
|
|
795
|
+
});
|
|
619
796
|
connectReq.end();
|
|
620
797
|
}
|
|
621
798
|
else {
|
|
622
799
|
const req = http.request({
|
|
623
|
-
hostname: proxyHost,
|
|
624
|
-
|
|
800
|
+
hostname: proxyHost,
|
|
801
|
+
port: proxyPort,
|
|
802
|
+
method,
|
|
803
|
+
path: url,
|
|
804
|
+
headers: {
|
|
805
|
+
"Proxy-Authorization": proxyAuth,
|
|
806
|
+
Host: parsed.host ?? "",
|
|
807
|
+
...customHeaders,
|
|
808
|
+
},
|
|
625
809
|
}, (res) => {
|
|
626
810
|
const chunks = [];
|
|
627
811
|
let bytes = 0;
|
|
628
|
-
res.on("data", (c) => {
|
|
629
|
-
|
|
812
|
+
res.on("data", (c) => {
|
|
813
|
+
bytes += c.length;
|
|
814
|
+
if (bytes <= MAX_RESP)
|
|
815
|
+
chunks.push(c);
|
|
816
|
+
});
|
|
630
817
|
let done = false;
|
|
631
818
|
const fin = () => {
|
|
632
819
|
if (done)
|
|
633
820
|
return;
|
|
634
821
|
done = true;
|
|
635
822
|
clearTimeout(timer);
|
|
636
|
-
const body = Buffer.concat(chunks)
|
|
823
|
+
const body = Buffer.concat(chunks)
|
|
824
|
+
.toString("utf-8")
|
|
825
|
+
.substring(0, MAX_RESP);
|
|
637
826
|
const hdrs = {};
|
|
638
827
|
for (const [k, v] of Object.entries(res.headers)) {
|
|
639
828
|
if (v)
|
|
@@ -643,9 +832,15 @@ const proxiedFetchTool = {
|
|
|
643
832
|
};
|
|
644
833
|
res.on("end", fin);
|
|
645
834
|
res.on("close", fin);
|
|
646
|
-
res.on("error", (e) => {
|
|
835
|
+
res.on("error", (e) => {
|
|
836
|
+
clearTimeout(timer);
|
|
837
|
+
reject(e);
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
req.on("error", (e) => {
|
|
841
|
+
clearTimeout(timer);
|
|
842
|
+
reject(e);
|
|
647
843
|
});
|
|
648
|
-
req.on("error", (e) => { clearTimeout(timer); reject(e); });
|
|
649
844
|
req.end();
|
|
650
845
|
}
|
|
651
846
|
});
|
|
@@ -656,7 +851,13 @@ const proxiedFetchTool = {
|
|
|
656
851
|
];
|
|
657
852
|
// Include relevant response headers
|
|
658
853
|
if (result.headers) {
|
|
659
|
-
const showHeaders = [
|
|
854
|
+
const showHeaders = [
|
|
855
|
+
"content-type",
|
|
856
|
+
"content-length",
|
|
857
|
+
"server",
|
|
858
|
+
"x-cache",
|
|
859
|
+
"cache-control",
|
|
860
|
+
];
|
|
660
861
|
for (const h of showHeaders) {
|
|
661
862
|
if (result.headers[h]) {
|
|
662
863
|
lines.push(`${h}: ${result.headers[h]}`);
|
|
@@ -843,9 +1044,12 @@ const createAgenticWalletTool = {
|
|
|
843
1044
|
spendingLimitCents,
|
|
844
1045
|
};
|
|
845
1046
|
// Validate optional daily_limit_cents
|
|
846
|
-
if (args.daily_limit_cents !== undefined &&
|
|
1047
|
+
if (args.daily_limit_cents !== undefined &&
|
|
1048
|
+
args.daily_limit_cents !== null) {
|
|
847
1049
|
const dailyLimit = Number(args.daily_limit_cents);
|
|
848
|
-
if (!Number.isInteger(dailyLimit) ||
|
|
1050
|
+
if (!Number.isInteger(dailyLimit) ||
|
|
1051
|
+
dailyLimit < 1 ||
|
|
1052
|
+
dailyLimit > 1000000) {
|
|
849
1053
|
return "Error: daily_limit_cents must be a positive integer between 1 and 1000000.";
|
|
850
1054
|
}
|
|
851
1055
|
body.dailyLimitCents = dailyLimit;
|
|
@@ -912,7 +1116,9 @@ const fundAgenticWalletTool = {
|
|
|
912
1116
|
try {
|
|
913
1117
|
const walletId = validateUuid(String(args.wallet_id ?? ""), "wallet_id");
|
|
914
1118
|
const amountCents = Number(args.amount_cents ?? 0);
|
|
915
|
-
if (!Number.isInteger(amountCents) ||
|
|
1119
|
+
if (!Number.isInteger(amountCents) ||
|
|
1120
|
+
amountCents < 100 ||
|
|
1121
|
+
amountCents > 1000000) {
|
|
916
1122
|
return "Error: amount_cents must be an integer between 100 ($1) and 1000000 ($10,000).";
|
|
917
1123
|
}
|
|
918
1124
|
const data = await apiPost(`/api/agent-wallet/${encodeURIComponent(walletId)}/fund`, {
|
|
@@ -1022,7 +1228,9 @@ const agenticTransactionsTool = {
|
|
|
1022
1228
|
const lines = [`Wallet Transactions (${txs.length})`, ""];
|
|
1023
1229
|
for (const tx of txs) {
|
|
1024
1230
|
const sign = tx.type === "fund" || tx.type === "refund" ? "+" : "-";
|
|
1025
|
-
const session = tx.sessionId
|
|
1231
|
+
const session = tx.sessionId
|
|
1232
|
+
? ` | Session: ${tx.sessionId.slice(0, 8)}`
|
|
1233
|
+
: "";
|
|
1026
1234
|
lines.push(` ${sign}${formatCents(tx.amountCents)} [${tx.type}] ${tx.description}`);
|
|
1027
1235
|
lines.push(` ${tx.createdAt}${session}`);
|
|
1028
1236
|
}
|
|
@@ -1061,7 +1269,9 @@ const createTeamTool = {
|
|
|
1061
1269
|
const body = { name };
|
|
1062
1270
|
if (args.max_members !== undefined) {
|
|
1063
1271
|
const maxMembers = Number(args.max_members);
|
|
1064
|
-
if (!Number.isInteger(maxMembers) ||
|
|
1272
|
+
if (!Number.isInteger(maxMembers) ||
|
|
1273
|
+
maxMembers < 1 ||
|
|
1274
|
+
maxMembers > 100) {
|
|
1065
1275
|
return "Error: max_members must be an integer between 1 and 100.";
|
|
1066
1276
|
}
|
|
1067
1277
|
body.maxMembers = maxMembers;
|
|
@@ -1173,7 +1383,9 @@ const teamFundTool = {
|
|
|
1173
1383
|
try {
|
|
1174
1384
|
const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
|
|
1175
1385
|
const amountCents = Number(args.amount_cents ?? 0);
|
|
1176
|
-
if (!Number.isInteger(amountCents) ||
|
|
1386
|
+
if (!Number.isInteger(amountCents) ||
|
|
1387
|
+
amountCents < 100 ||
|
|
1388
|
+
amountCents > 1000000) {
|
|
1177
1389
|
return "Error: amount_cents must be an integer between 100 ($1) and 1000000 ($10,000).";
|
|
1178
1390
|
}
|
|
1179
1391
|
const data = await apiPost(`/api/teams/${encodeURIComponent(teamId)}/wallet/fund`, {
|
|
@@ -1413,7 +1625,9 @@ const updateTeamTool = {
|
|
|
1413
1625
|
}
|
|
1414
1626
|
if (args.max_members !== undefined) {
|
|
1415
1627
|
const maxMembers = Number(args.max_members);
|
|
1416
|
-
if (!Number.isInteger(maxMembers) ||
|
|
1628
|
+
if (!Number.isInteger(maxMembers) ||
|
|
1629
|
+
maxMembers < 1 ||
|
|
1630
|
+
maxMembers > 100) {
|
|
1417
1631
|
return "Error: max_members must be an integer between 1 and 100.";
|
|
1418
1632
|
}
|
|
1419
1633
|
body.maxMembers = maxMembers;
|
|
@@ -1451,7 +1665,9 @@ const topupPaypalTool = {
|
|
|
1451
1665
|
execute: async (args) => {
|
|
1452
1666
|
try {
|
|
1453
1667
|
const amountCents = Number(args.amount_cents ?? 0);
|
|
1454
|
-
if (!Number.isInteger(amountCents) ||
|
|
1668
|
+
if (!Number.isInteger(amountCents) ||
|
|
1669
|
+
amountCents < 500 ||
|
|
1670
|
+
amountCents > 100000) {
|
|
1455
1671
|
return "Error: amount_cents must be an integer between 500 ($5) and 100000 ($1,000).";
|
|
1456
1672
|
}
|
|
1457
1673
|
const data = await apiPost("/api/wallet/topup/paypal", { amountCents });
|
|
@@ -1486,7 +1702,9 @@ const topupStripeTool = {
|
|
|
1486
1702
|
execute: async (args) => {
|
|
1487
1703
|
try {
|
|
1488
1704
|
const amountCents = Number(args.amount_cents ?? 0);
|
|
1489
|
-
if (!Number.isInteger(amountCents) ||
|
|
1705
|
+
if (!Number.isInteger(amountCents) ||
|
|
1706
|
+
amountCents < 500 ||
|
|
1707
|
+
amountCents > 100000) {
|
|
1490
1708
|
return "Error: amount_cents must be an integer between 500 ($5) and 100000 ($1,000).";
|
|
1491
1709
|
}
|
|
1492
1710
|
const data = await apiPost("/api/wallet/topup/stripe", { amountCents });
|
|
@@ -1508,7 +1726,17 @@ const topupStripeTool = {
|
|
|
1508
1726
|
};
|
|
1509
1727
|
// 23. topup_crypto
|
|
1510
1728
|
const VALID_CRYPTO_CURRENCIES = new Set([
|
|
1511
|
-
"BTC",
|
|
1729
|
+
"BTC",
|
|
1730
|
+
"ETH",
|
|
1731
|
+
"LTC",
|
|
1732
|
+
"XMR",
|
|
1733
|
+
"ZEC",
|
|
1734
|
+
"USDC",
|
|
1735
|
+
"SOL",
|
|
1736
|
+
"USDT",
|
|
1737
|
+
"DAI",
|
|
1738
|
+
"BNB",
|
|
1739
|
+
"LINK",
|
|
1512
1740
|
]);
|
|
1513
1741
|
const topupCryptoTool = {
|
|
1514
1742
|
name: "topup_crypto",
|
|
@@ -1525,20 +1753,38 @@ const topupCryptoTool = {
|
|
|
1525
1753
|
type: "string",
|
|
1526
1754
|
description: "Cryptocurrency to pay with",
|
|
1527
1755
|
required: true,
|
|
1528
|
-
enum: [
|
|
1756
|
+
enum: [
|
|
1757
|
+
"BTC",
|
|
1758
|
+
"ETH",
|
|
1759
|
+
"LTC",
|
|
1760
|
+
"XMR",
|
|
1761
|
+
"ZEC",
|
|
1762
|
+
"USDC",
|
|
1763
|
+
"SOL",
|
|
1764
|
+
"USDT",
|
|
1765
|
+
"DAI",
|
|
1766
|
+
"BNB",
|
|
1767
|
+
"LINK",
|
|
1768
|
+
],
|
|
1529
1769
|
},
|
|
1530
1770
|
},
|
|
1531
1771
|
execute: async (args) => {
|
|
1532
1772
|
try {
|
|
1533
1773
|
const amountUsd = Number(args.amount_usd ?? 0);
|
|
1534
|
-
if (typeof amountUsd !== "number" ||
|
|
1774
|
+
if (typeof amountUsd !== "number" ||
|
|
1775
|
+
!Number.isFinite(amountUsd) ||
|
|
1776
|
+
amountUsd < 5 ||
|
|
1777
|
+
amountUsd > 1000) {
|
|
1535
1778
|
return "Error: amount_usd must be a number between 5 and 1000.";
|
|
1536
1779
|
}
|
|
1537
1780
|
const currency = String(args.currency ?? "").toUpperCase();
|
|
1538
1781
|
if (!VALID_CRYPTO_CURRENCIES.has(currency)) {
|
|
1539
1782
|
return `Error: currency must be one of: ${[...VALID_CRYPTO_CURRENCIES].join(", ")}.`;
|
|
1540
1783
|
}
|
|
1541
|
-
const data = await apiPost("/api/wallet/topup/crypto", {
|
|
1784
|
+
const data = await apiPost("/api/wallet/topup/crypto", {
|
|
1785
|
+
amountUsd,
|
|
1786
|
+
currency: currency.toLowerCase(),
|
|
1787
|
+
});
|
|
1542
1788
|
return [
|
|
1543
1789
|
"Crypto Payment Invoice Created",
|
|
1544
1790
|
"",
|
|
@@ -1650,7 +1896,9 @@ const updateWalletPolicyTool = {
|
|
|
1650
1896
|
}
|
|
1651
1897
|
else {
|
|
1652
1898
|
const dailyLimit = Number(args.daily_limit_cents);
|
|
1653
|
-
if (!Number.isInteger(dailyLimit) ||
|
|
1899
|
+
if (!Number.isInteger(dailyLimit) ||
|
|
1900
|
+
dailyLimit < 1 ||
|
|
1901
|
+
dailyLimit > 1000000) {
|
|
1654
1902
|
return "Error: daily_limit_cents must be a positive integer between 1 and 1000000, or null to clear.";
|
|
1655
1903
|
}
|
|
1656
1904
|
body.dailyLimitCents = dailyLimit;
|
|
@@ -1699,36 +1947,1173 @@ const updateWalletPolicyTool = {
|
|
|
1699
1947
|
}
|
|
1700
1948
|
},
|
|
1701
1949
|
};
|
|
1702
|
-
//
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1950
|
+
// 27. get_proxy_status
|
|
1951
|
+
const getProxyStatusTool = {
|
|
1952
|
+
name: "get_proxy_status",
|
|
1953
|
+
description: "Get the current status of the proxy gateway including uptime, active connections, and pool health.",
|
|
1954
|
+
parameters: {},
|
|
1955
|
+
execute: async () => {
|
|
1956
|
+
try {
|
|
1957
|
+
const data = await apiGet("/api/proxy/status");
|
|
1958
|
+
const lines = [
|
|
1959
|
+
"Proxy Gateway Status",
|
|
1960
|
+
"",
|
|
1961
|
+
`Status: ${data.status ?? "unknown"}`,
|
|
1962
|
+
`Uptime: ${data.uptime != null ? `${Math.floor(data.uptime / 3600)}h ${Math.floor((data.uptime % 3600) / 60)}m` : "unknown"}`,
|
|
1963
|
+
`Active Connections: ${data.activeConnections ?? 0}`,
|
|
1964
|
+
];
|
|
1965
|
+
const pools = data.pools ?? [];
|
|
1966
|
+
if (pools.length > 0) {
|
|
1967
|
+
lines.push("", "Pool Health:");
|
|
1968
|
+
for (const p of pools) {
|
|
1969
|
+
lines.push(` ${p.name}: ${p.healthy ? "healthy" : "unhealthy"} (${p.activeIps} IPs)`);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
return lines.join("\n");
|
|
1973
|
+
}
|
|
1974
|
+
catch (err) {
|
|
1975
|
+
return `Error: ${safeError(err)}`;
|
|
1976
|
+
}
|
|
1977
|
+
},
|
|
1978
|
+
};
|
|
1979
|
+
// 28. get_transactions
|
|
1980
|
+
const getTransactionsTool = {
|
|
1981
|
+
name: "get_transactions",
|
|
1982
|
+
description: "Get your main wallet transaction history including top-ups, usage charges, and refunds.",
|
|
1983
|
+
parameters: {
|
|
1984
|
+
limit: {
|
|
1985
|
+
type: "number",
|
|
1986
|
+
description: "Number of transactions to return (1-100, default 20)",
|
|
1987
|
+
required: false,
|
|
1988
|
+
default: 20,
|
|
1989
|
+
},
|
|
1990
|
+
},
|
|
1991
|
+
execute: async (args) => {
|
|
1992
|
+
try {
|
|
1993
|
+
const limit = Math.min(Math.max(Number(args.limit ?? 20), 1), 100);
|
|
1994
|
+
const data = await apiGet(`/api/wallet/transactions?limit=${limit}`);
|
|
1995
|
+
const txs = data.transactions ?? [];
|
|
1996
|
+
if (txs.length === 0) {
|
|
1997
|
+
return "No wallet transactions found.";
|
|
1998
|
+
}
|
|
1999
|
+
const lines = [`Wallet Transactions (${txs.length})`, ""];
|
|
2000
|
+
for (const tx of txs) {
|
|
2001
|
+
const sign = tx.type === "topup" || tx.type === "refund" || tx.type === "fund"
|
|
2002
|
+
? "+"
|
|
2003
|
+
: "-";
|
|
2004
|
+
lines.push(` ${sign}${formatCents(Math.abs(tx.amountCents))} [${tx.type}] ${tx.description}`);
|
|
2005
|
+
lines.push(` ${tx.createdAt}`);
|
|
2006
|
+
}
|
|
2007
|
+
return truncate(lines.join("\n"));
|
|
2008
|
+
}
|
|
2009
|
+
catch (err) {
|
|
2010
|
+
return `Error: ${safeError(err)}`;
|
|
2011
|
+
}
|
|
2012
|
+
},
|
|
2013
|
+
};
|
|
2014
|
+
// 29. get_forecast
|
|
2015
|
+
const getForecastTool = {
|
|
2016
|
+
name: "get_forecast",
|
|
2017
|
+
description: "Get a spending forecast based on recent usage patterns. Shows projected balance depletion date and daily burn rate.",
|
|
2018
|
+
parameters: {},
|
|
2019
|
+
execute: async () => {
|
|
2020
|
+
try {
|
|
2021
|
+
const data = await apiGet("/api/wallet/forecast");
|
|
2022
|
+
const lines = [
|
|
2023
|
+
"Spending Forecast",
|
|
2024
|
+
"",
|
|
2025
|
+
`Current Balance: ${formatCents(data.currentBalanceCents ?? 0)}`,
|
|
2026
|
+
`Daily Burn Rate: ${formatCents(data.dailyBurnCents ?? 0)}/day`,
|
|
2027
|
+
`Avg Daily Bandwidth: ${formatBytes(data.avgDailyBytes ?? 0)}`,
|
|
2028
|
+
`Estimated Days Remaining: ${data.estimatedDaysRemaining ?? "N/A"}`,
|
|
2029
|
+
`Projected Depletion: ${data.projectedDepletionDate ?? "N/A"}`,
|
|
2030
|
+
"",
|
|
2031
|
+
"Use topup_stripe, topup_paypal, or topup_crypto to add funds.",
|
|
2032
|
+
];
|
|
2033
|
+
return lines.join("\n");
|
|
2034
|
+
}
|
|
2035
|
+
catch (err) {
|
|
2036
|
+
return `Error: ${safeError(err)}`;
|
|
2037
|
+
}
|
|
2038
|
+
},
|
|
2039
|
+
};
|
|
2040
|
+
// 30. check_payment
|
|
2041
|
+
const checkPaymentTool = {
|
|
2042
|
+
name: "check_payment",
|
|
2043
|
+
description: "Check the status of a crypto payment invoice. Use the invoice ID returned by topup_crypto.",
|
|
2044
|
+
parameters: {
|
|
2045
|
+
invoice_id: {
|
|
2046
|
+
type: "string",
|
|
2047
|
+
description: "Crypto invoice ID to check",
|
|
2048
|
+
required: true,
|
|
2049
|
+
},
|
|
2050
|
+
},
|
|
2051
|
+
execute: async (args) => {
|
|
2052
|
+
try {
|
|
2053
|
+
const invoiceId = String(args.invoice_id ?? "").trim();
|
|
2054
|
+
if (!invoiceId || invoiceId.length === 0 || invoiceId.length > 200) {
|
|
2055
|
+
return "Error: invoice_id is required (max 200 characters).";
|
|
2056
|
+
}
|
|
2057
|
+
if (/[\x00-\x1f\x7f]/.test(invoiceId)) {
|
|
2058
|
+
return "Error: invoice_id contains invalid control characters.";
|
|
2059
|
+
}
|
|
2060
|
+
const data = await apiGet(`/api/wallet/topup/crypto/${encodeURIComponent(invoiceId)}/status`);
|
|
2061
|
+
const lines = [
|
|
2062
|
+
"Crypto Payment Status",
|
|
2063
|
+
"",
|
|
2064
|
+
`Invoice ID: ${data.invoiceId ?? invoiceId}`,
|
|
2065
|
+
`Status: ${data.status}`,
|
|
2066
|
+
`Amount: $${data.amountUsd}`,
|
|
2067
|
+
`Currency: ${data.payCurrency?.toUpperCase() ?? "N/A"}`,
|
|
2068
|
+
];
|
|
2069
|
+
if (data.paidAmount != null) {
|
|
2070
|
+
lines.push(`Paid: ${data.paidAmount} ${data.payCurrency?.toUpperCase() ?? ""}`);
|
|
2071
|
+
}
|
|
2072
|
+
lines.push(`Created: ${data.createdAt}`, `Updated: ${data.updatedAt}`);
|
|
2073
|
+
return lines.join("\n");
|
|
2074
|
+
}
|
|
2075
|
+
catch (err) {
|
|
2076
|
+
return `Error: ${safeError(err)}`;
|
|
2077
|
+
}
|
|
2078
|
+
},
|
|
2079
|
+
};
|
|
2080
|
+
// 31. get_daily_usage
|
|
2081
|
+
const getDailyUsageTool = {
|
|
2082
|
+
name: "get_daily_usage",
|
|
2083
|
+
description: "Get daily bandwidth usage breakdown for the specified number of days. Shows per-day bytes, cost, and request count.",
|
|
2084
|
+
parameters: {
|
|
2085
|
+
days: {
|
|
2086
|
+
type: "number",
|
|
2087
|
+
description: "Number of days to look back (1-90, default 7)",
|
|
2088
|
+
required: false,
|
|
2089
|
+
default: 7,
|
|
2090
|
+
},
|
|
2091
|
+
},
|
|
2092
|
+
execute: async (args) => {
|
|
2093
|
+
try {
|
|
2094
|
+
const days = Math.min(Math.max(Number(args.days ?? 7), 1), 90);
|
|
2095
|
+
const data = await apiGet(`/api/usage/daily?days=${days}`);
|
|
2096
|
+
const daily = data.daily ?? [];
|
|
2097
|
+
if (daily.length === 0) {
|
|
2098
|
+
return `No daily usage data found for the last ${days} days.`;
|
|
2099
|
+
}
|
|
2100
|
+
const lines = [`Daily Usage (last ${days} days)`, ""];
|
|
2101
|
+
for (const d of daily) {
|
|
2102
|
+
lines.push(` ${d.date}: ${formatBytes(d.totalBytes)} | ${formatCents(d.totalCostCents)} | ${d.requestCount} reqs`);
|
|
2103
|
+
}
|
|
2104
|
+
return truncate(lines.join("\n"));
|
|
2105
|
+
}
|
|
2106
|
+
catch (err) {
|
|
2107
|
+
return `Error: ${safeError(err)}`;
|
|
2108
|
+
}
|
|
2109
|
+
},
|
|
2110
|
+
};
|
|
2111
|
+
// 32. get_top_hosts
|
|
2112
|
+
const getTopHostsTool = {
|
|
2113
|
+
name: "get_top_hosts",
|
|
2114
|
+
description: "Get the top hosts by bandwidth usage. Shows which domains consume the most proxy bandwidth.",
|
|
2115
|
+
parameters: {
|
|
2116
|
+
limit: {
|
|
2117
|
+
type: "number",
|
|
2118
|
+
description: "Number of top hosts to return (1-100, default 10)",
|
|
2119
|
+
required: false,
|
|
2120
|
+
default: 10,
|
|
2121
|
+
},
|
|
2122
|
+
},
|
|
2123
|
+
execute: async (args) => {
|
|
2124
|
+
try {
|
|
2125
|
+
const limit = Math.min(Math.max(Number(args.limit ?? 10), 1), 100);
|
|
2126
|
+
const data = await apiGet(`/api/usage/top-hosts?limit=${limit}`);
|
|
2127
|
+
const hosts = data.hosts ?? [];
|
|
2128
|
+
if (hosts.length === 0) {
|
|
2129
|
+
return "No host usage data found.";
|
|
2130
|
+
}
|
|
2131
|
+
const lines = [`Top Hosts by Bandwidth (${hosts.length})`, ""];
|
|
2132
|
+
for (let i = 0; i < hosts.length; i++) {
|
|
2133
|
+
const h = hosts[i];
|
|
2134
|
+
lines.push(` ${i + 1}. ${h.host}: ${formatBytes(h.totalBytes)} | ${h.requestCount} reqs | Last: ${h.lastSeen}`);
|
|
2135
|
+
}
|
|
2136
|
+
return truncate(lines.join("\n"));
|
|
2137
|
+
}
|
|
2138
|
+
catch (err) {
|
|
2139
|
+
return `Error: ${safeError(err)}`;
|
|
2140
|
+
}
|
|
2141
|
+
},
|
|
2142
|
+
};
|
|
2143
|
+
// 33. register
|
|
2144
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2145
|
+
const registerTool = {
|
|
2146
|
+
name: "register",
|
|
2147
|
+
description: "Register a new Dominus Node account. Returns user info and instructions for email verification. " +
|
|
2148
|
+
"This is an unauthenticated endpoint -- no API key required.",
|
|
2149
|
+
parameters: {
|
|
2150
|
+
email: {
|
|
2151
|
+
type: "string",
|
|
2152
|
+
description: "Email address for the new account",
|
|
2153
|
+
required: true,
|
|
2154
|
+
},
|
|
2155
|
+
password: {
|
|
2156
|
+
type: "string",
|
|
2157
|
+
description: "Password for the new account (8-128 characters)",
|
|
2158
|
+
required: true,
|
|
2159
|
+
},
|
|
2160
|
+
},
|
|
2161
|
+
execute: async (args) => {
|
|
2162
|
+
try {
|
|
2163
|
+
const email = String(args.email ?? "").trim();
|
|
2164
|
+
if (!email || !EMAIL_RE.test(email)) {
|
|
2165
|
+
return "Error: A valid email address is required.";
|
|
2166
|
+
}
|
|
2167
|
+
if (email.length > 254) {
|
|
2168
|
+
return "Error: Email address is too long (max 254 characters).";
|
|
2169
|
+
}
|
|
2170
|
+
const password = String(args.password ?? "");
|
|
2171
|
+
if (password.length < 8 || password.length > 128) {
|
|
2172
|
+
return "Error: Password must be 8-128 characters.";
|
|
2173
|
+
}
|
|
2174
|
+
// Solve PoW for CAPTCHA-free registration
|
|
2175
|
+
const pow = await solvePoW(getBaseUrl());
|
|
2176
|
+
const regBody = { email, password };
|
|
2177
|
+
if (pow)
|
|
2178
|
+
regBody.pow = pow;
|
|
2179
|
+
const data = await unauthenticatedRequest("POST", "/api/auth/register", regBody);
|
|
2180
|
+
return [
|
|
2181
|
+
"Account Registered",
|
|
2182
|
+
"",
|
|
2183
|
+
`User ID: ${data.id}`,
|
|
2184
|
+
`Email: ${data.email}`,
|
|
2185
|
+
`Email Verified: ${data.emailVerified ? "Yes" : "No"}`,
|
|
2186
|
+
`Created: ${data.createdAt}`,
|
|
2187
|
+
"",
|
|
2188
|
+
"Next steps:",
|
|
2189
|
+
" 1. Check your email for a verification code",
|
|
2190
|
+
" 2. Use verify_email to verify your email address",
|
|
2191
|
+
" 3. Set DOMINUSNODE_API_KEY to start using the proxy",
|
|
2192
|
+
].join("\n");
|
|
2193
|
+
}
|
|
2194
|
+
catch (err) {
|
|
2195
|
+
return `Error: ${safeError(err)}`;
|
|
2196
|
+
}
|
|
2197
|
+
},
|
|
2198
|
+
};
|
|
2199
|
+
// 34. login
|
|
2200
|
+
const loginTool = {
|
|
2201
|
+
name: "login",
|
|
2202
|
+
description: "Log in to a Dominus Node account and receive access/refresh tokens. " +
|
|
2203
|
+
"This is an unauthenticated endpoint -- no API key required.",
|
|
2204
|
+
parameters: {
|
|
2205
|
+
email: {
|
|
2206
|
+
type: "string",
|
|
2207
|
+
description: "Account email address",
|
|
2208
|
+
required: true,
|
|
2209
|
+
},
|
|
2210
|
+
password: {
|
|
2211
|
+
type: "string",
|
|
2212
|
+
description: "Account password (8-128 characters)",
|
|
2213
|
+
required: true,
|
|
2214
|
+
},
|
|
2215
|
+
},
|
|
2216
|
+
execute: async (args) => {
|
|
2217
|
+
try {
|
|
2218
|
+
const email = String(args.email ?? "").trim();
|
|
2219
|
+
if (!email || !EMAIL_RE.test(email)) {
|
|
2220
|
+
return "Error: A valid email address is required.";
|
|
2221
|
+
}
|
|
2222
|
+
if (email.length > 254) {
|
|
2223
|
+
return "Error: Email address is too long (max 254 characters).";
|
|
2224
|
+
}
|
|
2225
|
+
const password = String(args.password ?? "");
|
|
2226
|
+
if (password.length < 8 || password.length > 128) {
|
|
2227
|
+
return "Error: Password must be 8-128 characters.";
|
|
2228
|
+
}
|
|
2229
|
+
const data = await unauthenticatedRequest("POST", "/api/auth/login", { email, password });
|
|
2230
|
+
if (data.mfaRequired) {
|
|
2231
|
+
return [
|
|
2232
|
+
"MFA Required",
|
|
2233
|
+
"",
|
|
2234
|
+
"This account has multi-factor authentication enabled.",
|
|
2235
|
+
"Please complete MFA verification to continue.",
|
|
2236
|
+
].join("\n");
|
|
2237
|
+
}
|
|
2238
|
+
return [
|
|
2239
|
+
"Login Successful",
|
|
2240
|
+
"",
|
|
2241
|
+
`Access Token: ${scrubCredentials(data.accessToken ?? "")}`,
|
|
2242
|
+
`Refresh Token: ${scrubCredentials(data.refreshToken ?? "")}`,
|
|
2243
|
+
`Expires In: ${data.expiresIn ?? 900} seconds`,
|
|
2244
|
+
"",
|
|
2245
|
+
"Use these tokens for authenticated API requests.",
|
|
2246
|
+
].join("\n");
|
|
2247
|
+
}
|
|
2248
|
+
catch (err) {
|
|
2249
|
+
return `Error: ${safeError(err)}`;
|
|
2250
|
+
}
|
|
2251
|
+
},
|
|
2252
|
+
};
|
|
2253
|
+
// 35. get_account_info
|
|
2254
|
+
const getAccountInfoTool = {
|
|
2255
|
+
name: "get_account_info",
|
|
2256
|
+
description: "Get your Dominus Node account information including email, plan, verification status, and creation date.",
|
|
2257
|
+
parameters: {},
|
|
2258
|
+
execute: async () => {
|
|
2259
|
+
try {
|
|
2260
|
+
const data = await apiGet("/api/auth/me");
|
|
2261
|
+
return [
|
|
2262
|
+
"Account Info",
|
|
2263
|
+
"",
|
|
2264
|
+
`User ID: ${data.id}`,
|
|
2265
|
+
`Email: ${data.email}`,
|
|
2266
|
+
`Email Verified: ${data.emailVerified ? "Yes" : "No"}`,
|
|
2267
|
+
`Plan: ${data.plan}`,
|
|
2268
|
+
`Status: ${data.status}`,
|
|
2269
|
+
`MFA Enabled: ${data.mfaEnabled ? "Yes" : "No"}`,
|
|
2270
|
+
`Created: ${data.createdAt}`,
|
|
2271
|
+
].join("\n");
|
|
2272
|
+
}
|
|
2273
|
+
catch (err) {
|
|
2274
|
+
return `Error: ${safeError(err)}`;
|
|
2275
|
+
}
|
|
2276
|
+
},
|
|
2277
|
+
};
|
|
2278
|
+
// 36. verify_email
|
|
2279
|
+
const verifyEmailTool = {
|
|
2280
|
+
name: "verify_email",
|
|
2281
|
+
description: "Verify your email address using the code sent during registration. " +
|
|
2282
|
+
"This is an unauthenticated endpoint -- no API key required.",
|
|
2283
|
+
parameters: {
|
|
2284
|
+
email: {
|
|
2285
|
+
type: "string",
|
|
2286
|
+
description: "Email address to verify",
|
|
2287
|
+
required: true,
|
|
2288
|
+
},
|
|
2289
|
+
code: {
|
|
2290
|
+
type: "string",
|
|
2291
|
+
description: "Verification code from email",
|
|
2292
|
+
required: true,
|
|
2293
|
+
},
|
|
2294
|
+
},
|
|
2295
|
+
execute: async (args) => {
|
|
2296
|
+
try {
|
|
2297
|
+
const email = String(args.email ?? "").trim();
|
|
2298
|
+
if (!email || !EMAIL_RE.test(email)) {
|
|
2299
|
+
return "Error: A valid email address is required.";
|
|
2300
|
+
}
|
|
2301
|
+
if (email.length > 254) {
|
|
2302
|
+
return "Error: Email address is too long (max 254 characters).";
|
|
2303
|
+
}
|
|
2304
|
+
const code = String(args.code ?? "").trim();
|
|
2305
|
+
if (!code || code.length === 0 || code.length > 100) {
|
|
2306
|
+
return "Error: Verification code is required (max 100 characters).";
|
|
2307
|
+
}
|
|
2308
|
+
if (/[\x00-\x1f\x7f]/.test(code)) {
|
|
2309
|
+
return "Error: Verification code contains invalid control characters.";
|
|
2310
|
+
}
|
|
2311
|
+
await unauthenticatedRequest("POST", "/api/auth/verify-email", {
|
|
2312
|
+
email,
|
|
2313
|
+
code,
|
|
2314
|
+
});
|
|
2315
|
+
return [
|
|
2316
|
+
"Email Verified Successfully",
|
|
2317
|
+
"",
|
|
2318
|
+
`Email: ${email}`,
|
|
2319
|
+
"",
|
|
2320
|
+
"Your account is now fully activated. You can start using the proxy.",
|
|
2321
|
+
].join("\n");
|
|
2322
|
+
}
|
|
2323
|
+
catch (err) {
|
|
2324
|
+
return `Error: ${safeError(err)}`;
|
|
2325
|
+
}
|
|
2326
|
+
},
|
|
2327
|
+
};
|
|
2328
|
+
// 37. resend_verification
|
|
2329
|
+
const resendVerificationTool = {
|
|
2330
|
+
name: "resend_verification",
|
|
2331
|
+
description: "Resend the email verification code. Requires authentication.",
|
|
2332
|
+
parameters: {},
|
|
2333
|
+
execute: async () => {
|
|
2334
|
+
try {
|
|
2335
|
+
await apiPost("/api/auth/resend-verification");
|
|
2336
|
+
return [
|
|
2337
|
+
"Verification Email Resent",
|
|
2338
|
+
"",
|
|
2339
|
+
"A new verification code has been sent to your registered email address.",
|
|
2340
|
+
"Use verify_email with the new code to complete verification.",
|
|
2341
|
+
].join("\n");
|
|
2342
|
+
}
|
|
2343
|
+
catch (err) {
|
|
2344
|
+
return `Error: ${safeError(err)}`;
|
|
2345
|
+
}
|
|
2346
|
+
},
|
|
2347
|
+
};
|
|
2348
|
+
// 38. update_password
|
|
2349
|
+
const updatePasswordTool = {
|
|
2350
|
+
name: "update_password",
|
|
2351
|
+
description: "Change your account password. Requires your current password for verification.",
|
|
2352
|
+
parameters: {
|
|
2353
|
+
current_password: {
|
|
2354
|
+
type: "string",
|
|
2355
|
+
description: "Your current password (8-128 characters)",
|
|
2356
|
+
required: true,
|
|
2357
|
+
},
|
|
2358
|
+
new_password: {
|
|
2359
|
+
type: "string",
|
|
2360
|
+
description: "Your new password (8-128 characters)",
|
|
2361
|
+
required: true,
|
|
2362
|
+
},
|
|
2363
|
+
},
|
|
2364
|
+
execute: async (args) => {
|
|
2365
|
+
try {
|
|
2366
|
+
const currentPassword = String(args.current_password ?? "");
|
|
2367
|
+
if (currentPassword.length < 8 || currentPassword.length > 128) {
|
|
2368
|
+
return "Error: Current password must be 8-128 characters.";
|
|
2369
|
+
}
|
|
2370
|
+
const newPassword = String(args.new_password ?? "");
|
|
2371
|
+
if (newPassword.length < 8 || newPassword.length > 128) {
|
|
2372
|
+
return "Error: New password must be 8-128 characters.";
|
|
2373
|
+
}
|
|
2374
|
+
await apiPost("/api/auth/change-password", {
|
|
2375
|
+
currentPassword,
|
|
2376
|
+
newPassword,
|
|
2377
|
+
});
|
|
2378
|
+
return [
|
|
2379
|
+
"Password Updated Successfully",
|
|
2380
|
+
"",
|
|
2381
|
+
"Your password has been changed.",
|
|
2382
|
+
"All existing sessions remain valid.",
|
|
2383
|
+
].join("\n");
|
|
2384
|
+
}
|
|
2385
|
+
catch (err) {
|
|
2386
|
+
return `Error: ${safeError(err)}`;
|
|
2387
|
+
}
|
|
2388
|
+
},
|
|
2389
|
+
};
|
|
2390
|
+
// 39. list_keys
|
|
2391
|
+
const listKeysTool = {
|
|
2392
|
+
name: "list_keys",
|
|
2393
|
+
description: "List all your personal API keys. Shows key prefix, label, status, and creation date.",
|
|
2394
|
+
parameters: {},
|
|
2395
|
+
execute: async () => {
|
|
2396
|
+
try {
|
|
2397
|
+
const data = await apiGet("/api/keys");
|
|
2398
|
+
const keys = data.keys ?? [];
|
|
2399
|
+
if (keys.length === 0) {
|
|
2400
|
+
return "No API keys found. Use create_key to create one.";
|
|
2401
|
+
}
|
|
2402
|
+
const lines = [`API Keys (${keys.length})`, ""];
|
|
2403
|
+
for (const k of keys) {
|
|
2404
|
+
const lastUsed = k.lastUsedAt ? ` | Last used: ${k.lastUsedAt}` : "";
|
|
2405
|
+
lines.push(` ${k.prefix}... | ${k.label} | ${k.status}${lastUsed}`);
|
|
2406
|
+
lines.push(` ID: ${k.id} | Created: ${k.createdAt}`);
|
|
2407
|
+
lines.push("");
|
|
2408
|
+
}
|
|
2409
|
+
return truncate(lines.join("\n"));
|
|
2410
|
+
}
|
|
2411
|
+
catch (err) {
|
|
2412
|
+
return `Error: ${safeError(err)}`;
|
|
2413
|
+
}
|
|
2414
|
+
},
|
|
2415
|
+
};
|
|
2416
|
+
// 40. create_key
|
|
2417
|
+
const createKeyTool = {
|
|
2418
|
+
name: "create_key",
|
|
2419
|
+
description: "Create a new personal API key. The full key is shown only once -- save it immediately.",
|
|
2420
|
+
parameters: {
|
|
2421
|
+
label: {
|
|
2422
|
+
type: "string",
|
|
2423
|
+
description: 'Label for the API key (e.g., "production", "dev")',
|
|
2424
|
+
required: true,
|
|
2425
|
+
},
|
|
2426
|
+
},
|
|
2427
|
+
execute: async (args) => {
|
|
2428
|
+
try {
|
|
2429
|
+
const label = String(args.label ?? "").trim();
|
|
2430
|
+
if (!label || label.length === 0 || label.length > 100) {
|
|
2431
|
+
return "Error: label is required and must be 1-100 characters.";
|
|
2432
|
+
}
|
|
2433
|
+
if (/[\x00-\x1f\x7f]/.test(label)) {
|
|
2434
|
+
return "Error: label contains invalid control characters.";
|
|
2435
|
+
}
|
|
2436
|
+
const data = await apiPost("/api/keys", { label });
|
|
2437
|
+
return [
|
|
2438
|
+
"API Key Created",
|
|
2439
|
+
"",
|
|
2440
|
+
`Key ID: ${data.id}`,
|
|
2441
|
+
`API Key: ${data.key}`,
|
|
2442
|
+
`Prefix: ${data.prefix}`,
|
|
2443
|
+
`Label: ${data.label}`,
|
|
2444
|
+
`Created: ${data.createdAt}`,
|
|
2445
|
+
"",
|
|
2446
|
+
"IMPORTANT: Save this API key now -- it will not be shown again.",
|
|
2447
|
+
].join("\n");
|
|
2448
|
+
}
|
|
2449
|
+
catch (err) {
|
|
2450
|
+
return `Error: ${safeError(err)}`;
|
|
2451
|
+
}
|
|
2452
|
+
},
|
|
2453
|
+
};
|
|
2454
|
+
// 41. revoke_key
|
|
2455
|
+
const revokeKeyTool = {
|
|
2456
|
+
name: "revoke_key",
|
|
2457
|
+
description: "Revoke (delete) a personal API key. This immediately invalidates the key.",
|
|
2458
|
+
parameters: {
|
|
2459
|
+
key_id: {
|
|
2460
|
+
type: "string",
|
|
2461
|
+
description: "API key ID (UUID) to revoke",
|
|
2462
|
+
required: true,
|
|
2463
|
+
},
|
|
2464
|
+
},
|
|
2465
|
+
execute: async (args) => {
|
|
2466
|
+
try {
|
|
2467
|
+
const keyId = validateUuid(String(args.key_id ?? ""), "key_id");
|
|
2468
|
+
await apiDelete(`/api/keys/${encodeURIComponent(keyId)}`);
|
|
2469
|
+
return [
|
|
2470
|
+
"API Key Revoked",
|
|
2471
|
+
"",
|
|
2472
|
+
`Key ID: ${keyId}`,
|
|
2473
|
+
"",
|
|
2474
|
+
"The key has been permanently revoked and can no longer be used.",
|
|
2475
|
+
].join("\n");
|
|
2476
|
+
}
|
|
2477
|
+
catch (err) {
|
|
2478
|
+
return `Error: ${safeError(err)}`;
|
|
2479
|
+
}
|
|
2480
|
+
},
|
|
2481
|
+
};
|
|
2482
|
+
// 42. get_plan
|
|
2483
|
+
const getPlanTool = {
|
|
2484
|
+
name: "get_plan",
|
|
2485
|
+
description: "Get your current plan details including limits, pricing, and features.",
|
|
2486
|
+
parameters: {},
|
|
2487
|
+
execute: async () => {
|
|
2488
|
+
try {
|
|
2489
|
+
const data = await apiGet("/api/plans/user/plan");
|
|
2490
|
+
const lines = [
|
|
2491
|
+
"Current Plan",
|
|
2492
|
+
"",
|
|
2493
|
+
`Plan: ${data.displayName ?? data.plan}`,
|
|
2494
|
+
`Bandwidth Limit: ${data.bandwidthLimitBytes != null ? formatBytes(data.bandwidthLimitBytes) : "Unlimited"}`,
|
|
2495
|
+
`Rate Limit: ${data.requestsPerMinute} req/min`,
|
|
2496
|
+
`Max API Keys: ${data.maxApiKeys}`,
|
|
2497
|
+
`Max Agentic Wallets: ${data.maxAgenticWallets}`,
|
|
2498
|
+
`Max Teams: ${data.maxTeams}`,
|
|
2499
|
+
];
|
|
2500
|
+
const features = data.features ?? [];
|
|
2501
|
+
if (features.length > 0) {
|
|
2502
|
+
lines.push("", "Features:");
|
|
2503
|
+
for (const f of features) {
|
|
2504
|
+
lines.push(` - ${f}`);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
lines.push("", "Use list_plans to see available plan options.");
|
|
2508
|
+
return lines.join("\n");
|
|
2509
|
+
}
|
|
2510
|
+
catch (err) {
|
|
2511
|
+
return `Error: ${safeError(err)}`;
|
|
2512
|
+
}
|
|
2513
|
+
},
|
|
2514
|
+
};
|
|
2515
|
+
// 43. list_plans
|
|
2516
|
+
const listPlansTool = {
|
|
2517
|
+
name: "list_plans",
|
|
2518
|
+
description: "List all available plans with pricing, limits, and features.",
|
|
2519
|
+
parameters: {},
|
|
2520
|
+
execute: async () => {
|
|
2521
|
+
try {
|
|
2522
|
+
const data = await apiGet("/api/plans");
|
|
2523
|
+
const plans = data.plans ?? [];
|
|
2524
|
+
if (plans.length === 0) {
|
|
2525
|
+
return "No plans available.";
|
|
2526
|
+
}
|
|
2527
|
+
const lines = [`Available Plans (${plans.length})`, ""];
|
|
2528
|
+
for (const p of plans) {
|
|
2529
|
+
lines.push(` ${p.displayName ?? p.name} ($${(p.priceMonthly / 100).toFixed(2)}/mo)`);
|
|
2530
|
+
lines.push(` Bandwidth: ${p.bandwidthLimitBytes != null ? formatBytes(p.bandwidthLimitBytes) : "Unlimited"} | Rate: ${p.requestsPerMinute} req/min | Keys: ${p.maxApiKeys}`);
|
|
2531
|
+
if (p.features && p.features.length > 0) {
|
|
2532
|
+
lines.push(` Features: ${p.features.join(", ")}`);
|
|
2533
|
+
}
|
|
2534
|
+
lines.push("");
|
|
2535
|
+
}
|
|
2536
|
+
lines.push("Use change_plan to switch plans.");
|
|
2537
|
+
return truncate(lines.join("\n"));
|
|
2538
|
+
}
|
|
2539
|
+
catch (err) {
|
|
2540
|
+
return `Error: ${safeError(err)}`;
|
|
2541
|
+
}
|
|
2542
|
+
},
|
|
2543
|
+
};
|
|
2544
|
+
// 44. change_plan
|
|
2545
|
+
const changePlanTool = {
|
|
2546
|
+
name: "change_plan",
|
|
2547
|
+
description: "Change your account plan. Use list_plans to see available options first.",
|
|
2548
|
+
parameters: {
|
|
2549
|
+
plan: {
|
|
2550
|
+
type: "string",
|
|
2551
|
+
description: "Plan name/ID to switch to",
|
|
2552
|
+
required: true,
|
|
2553
|
+
},
|
|
2554
|
+
},
|
|
2555
|
+
execute: async (args) => {
|
|
2556
|
+
try {
|
|
2557
|
+
const plan = String(args.plan ?? "").trim();
|
|
2558
|
+
if (!plan || plan.length === 0 || plan.length > 100) {
|
|
2559
|
+
return "Error: plan is required and must be 1-100 characters.";
|
|
2560
|
+
}
|
|
2561
|
+
if (/[\x00-\x1f\x7f]/.test(plan)) {
|
|
2562
|
+
return "Error: plan name contains invalid control characters.";
|
|
2563
|
+
}
|
|
2564
|
+
const data = await apiPut("/api/plans/user/plan", { plan });
|
|
2565
|
+
return [
|
|
2566
|
+
"Plan Changed Successfully",
|
|
2567
|
+
"",
|
|
2568
|
+
`New Plan: ${data.displayName ?? data.plan}`,
|
|
2569
|
+
`Effective: ${data.effectiveAt ?? "immediately"}`,
|
|
2570
|
+
"",
|
|
2571
|
+
"Use get_plan to view your updated plan details.",
|
|
2572
|
+
].join("\n");
|
|
2573
|
+
}
|
|
2574
|
+
catch (err) {
|
|
2575
|
+
return `Error: ${safeError(err)}`;
|
|
2576
|
+
}
|
|
2577
|
+
},
|
|
2578
|
+
};
|
|
2579
|
+
// 45. team_delete
|
|
2580
|
+
const teamDeleteTool = {
|
|
2581
|
+
name: "team_delete",
|
|
2582
|
+
description: "Delete a team. Only the team owner can delete a team. Any remaining team balance is refunded.",
|
|
2583
|
+
parameters: {
|
|
2584
|
+
team_id: {
|
|
2585
|
+
type: "string",
|
|
2586
|
+
description: "Team ID (UUID) to delete",
|
|
2587
|
+
required: true,
|
|
2588
|
+
},
|
|
2589
|
+
},
|
|
2590
|
+
execute: async (args) => {
|
|
2591
|
+
try {
|
|
2592
|
+
const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
|
|
2593
|
+
await apiDelete(`/api/teams/${encodeURIComponent(teamId)}`);
|
|
2594
|
+
return [
|
|
2595
|
+
"Team Deleted",
|
|
2596
|
+
"",
|
|
2597
|
+
`Team ID: ${teamId}`,
|
|
2598
|
+
"",
|
|
2599
|
+
"The team has been permanently deleted.",
|
|
2600
|
+
"Any remaining balance has been refunded to the owner's wallet.",
|
|
2601
|
+
].join("\n");
|
|
2602
|
+
}
|
|
2603
|
+
catch (err) {
|
|
2604
|
+
return `Error: ${safeError(err)}`;
|
|
2605
|
+
}
|
|
2606
|
+
},
|
|
2607
|
+
};
|
|
2608
|
+
// 46. team_revoke_key
|
|
2609
|
+
const teamRevokeKeyTool = {
|
|
2610
|
+
name: "team_revoke_key",
|
|
2611
|
+
description: "Revoke a shared API key for a team. Only owners and admins can revoke keys.",
|
|
2612
|
+
parameters: {
|
|
2613
|
+
team_id: {
|
|
2614
|
+
type: "string",
|
|
2615
|
+
description: "Team ID (UUID)",
|
|
2616
|
+
required: true,
|
|
2617
|
+
},
|
|
2618
|
+
key_id: {
|
|
2619
|
+
type: "string",
|
|
2620
|
+
description: "API key ID (UUID) to revoke",
|
|
2621
|
+
required: true,
|
|
2622
|
+
},
|
|
2623
|
+
},
|
|
2624
|
+
execute: async (args) => {
|
|
2625
|
+
try {
|
|
2626
|
+
const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
|
|
2627
|
+
const keyId = validateUuid(String(args.key_id ?? ""), "key_id");
|
|
2628
|
+
await apiDelete(`/api/teams/${encodeURIComponent(teamId)}/keys/${encodeURIComponent(keyId)}`);
|
|
2629
|
+
return [
|
|
2630
|
+
"Team API Key Revoked",
|
|
2631
|
+
"",
|
|
2632
|
+
`Team ID: ${teamId}`,
|
|
2633
|
+
`Key ID: ${keyId}`,
|
|
2634
|
+
"",
|
|
2635
|
+
"The key has been permanently revoked and can no longer be used.",
|
|
2636
|
+
].join("\n");
|
|
2637
|
+
}
|
|
2638
|
+
catch (err) {
|
|
2639
|
+
return `Error: ${safeError(err)}`;
|
|
2640
|
+
}
|
|
2641
|
+
},
|
|
2642
|
+
};
|
|
2643
|
+
// 47. team_list_keys
|
|
2644
|
+
const teamListKeysTool = {
|
|
2645
|
+
name: "team_list_keys",
|
|
2646
|
+
description: "List all API keys for a team. Shows key prefix, label, status, and creation date.",
|
|
2647
|
+
parameters: {
|
|
2648
|
+
team_id: {
|
|
2649
|
+
type: "string",
|
|
2650
|
+
description: "Team ID (UUID)",
|
|
2651
|
+
required: true,
|
|
2652
|
+
},
|
|
2653
|
+
},
|
|
2654
|
+
execute: async (args) => {
|
|
2655
|
+
try {
|
|
2656
|
+
const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
|
|
2657
|
+
const data = await apiGet(`/api/teams/${encodeURIComponent(teamId)}/keys`);
|
|
2658
|
+
const keys = data.keys ?? [];
|
|
2659
|
+
if (keys.length === 0) {
|
|
2660
|
+
return "No API keys found for this team. Use team_create_key to create one.";
|
|
2661
|
+
}
|
|
2662
|
+
const lines = [`Team API Keys (${keys.length})`, ""];
|
|
2663
|
+
for (const k of keys) {
|
|
2664
|
+
const lastUsed = k.lastUsedAt ? ` | Last used: ${k.lastUsedAt}` : "";
|
|
2665
|
+
lines.push(` ${k.prefix}... | ${k.label} | ${k.status}${lastUsed}`);
|
|
2666
|
+
lines.push(` ID: ${k.id} | Created: ${k.createdAt}`);
|
|
2667
|
+
lines.push("");
|
|
2668
|
+
}
|
|
2669
|
+
return truncate(lines.join("\n"));
|
|
2670
|
+
}
|
|
2671
|
+
catch (err) {
|
|
2672
|
+
return `Error: ${safeError(err)}`;
|
|
2673
|
+
}
|
|
2674
|
+
},
|
|
2675
|
+
};
|
|
2676
|
+
// 48. team_list_members
|
|
2677
|
+
const teamListMembersTool = {
|
|
2678
|
+
name: "team_list_members",
|
|
2679
|
+
description: "List all members of a team with their roles and join dates.",
|
|
2680
|
+
parameters: {
|
|
2681
|
+
team_id: {
|
|
2682
|
+
type: "string",
|
|
2683
|
+
description: "Team ID (UUID)",
|
|
2684
|
+
required: true,
|
|
2685
|
+
},
|
|
2686
|
+
},
|
|
2687
|
+
execute: async (args) => {
|
|
2688
|
+
try {
|
|
2689
|
+
const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
|
|
2690
|
+
const data = await apiGet(`/api/teams/${encodeURIComponent(teamId)}/members`);
|
|
2691
|
+
const members = data.members ?? [];
|
|
2692
|
+
if (members.length === 0) {
|
|
2693
|
+
return "No members found in this team.";
|
|
2694
|
+
}
|
|
2695
|
+
const lines = [`Team Members (${members.length})`, ""];
|
|
2696
|
+
for (const m of members) {
|
|
2697
|
+
lines.push(` ${m.email} | ${m.role} | Joined: ${m.joinedAt}`);
|
|
2698
|
+
lines.push(` User ID: ${m.userId}`);
|
|
2699
|
+
lines.push("");
|
|
2700
|
+
}
|
|
2701
|
+
return truncate(lines.join("\n"));
|
|
2702
|
+
}
|
|
2703
|
+
catch (err) {
|
|
2704
|
+
return `Error: ${safeError(err)}`;
|
|
2705
|
+
}
|
|
2706
|
+
},
|
|
2707
|
+
};
|
|
2708
|
+
// 49. team_add_member
|
|
2709
|
+
const teamAddMemberTool = {
|
|
2710
|
+
name: "team_add_member",
|
|
2711
|
+
description: "Add a member directly to a team by user ID. Only owners and admins can add members.",
|
|
2712
|
+
parameters: {
|
|
2713
|
+
team_id: {
|
|
2714
|
+
type: "string",
|
|
2715
|
+
description: "Team ID (UUID)",
|
|
2716
|
+
required: true,
|
|
2717
|
+
},
|
|
2718
|
+
user_id: {
|
|
2719
|
+
type: "string",
|
|
2720
|
+
description: "User ID (UUID) of the person to add",
|
|
2721
|
+
required: true,
|
|
2722
|
+
},
|
|
2723
|
+
role: {
|
|
2724
|
+
type: "string",
|
|
2725
|
+
description: "Role for the new member",
|
|
2726
|
+
required: false,
|
|
2727
|
+
enum: ["member", "admin"],
|
|
2728
|
+
default: "member",
|
|
2729
|
+
},
|
|
2730
|
+
},
|
|
2731
|
+
execute: async (args) => {
|
|
2732
|
+
try {
|
|
2733
|
+
const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
|
|
2734
|
+
const userId = validateUuid(String(args.user_id ?? ""), "user_id");
|
|
2735
|
+
const role = String(args.role ?? "member").trim();
|
|
2736
|
+
if (role !== "member" && role !== "admin") {
|
|
2737
|
+
return "Error: role must be 'member' or 'admin'.";
|
|
2738
|
+
}
|
|
2739
|
+
const data = await apiPost(`/api/teams/${encodeURIComponent(teamId)}/members`, {
|
|
2740
|
+
userId,
|
|
2741
|
+
role,
|
|
2742
|
+
});
|
|
2743
|
+
return [
|
|
2744
|
+
"Team Member Added",
|
|
2745
|
+
"",
|
|
2746
|
+
`Team ID: ${data.teamId ?? teamId}`,
|
|
2747
|
+
`User ID: ${data.userId ?? userId}`,
|
|
2748
|
+
`Role: ${data.role ?? role}`,
|
|
2749
|
+
`Joined: ${data.joinedAt ?? "now"}`,
|
|
2750
|
+
].join("\n");
|
|
2751
|
+
}
|
|
2752
|
+
catch (err) {
|
|
2753
|
+
return `Error: ${safeError(err)}`;
|
|
2754
|
+
}
|
|
2755
|
+
},
|
|
2756
|
+
};
|
|
2757
|
+
// 50. team_remove_member
|
|
2758
|
+
const teamRemoveMemberTool = {
|
|
2759
|
+
name: "team_remove_member",
|
|
2760
|
+
description: "Remove a member from a team. Only owners and admins can remove members.",
|
|
2761
|
+
parameters: {
|
|
2762
|
+
team_id: {
|
|
2763
|
+
type: "string",
|
|
2764
|
+
description: "Team ID (UUID)",
|
|
2765
|
+
required: true,
|
|
2766
|
+
},
|
|
2767
|
+
user_id: {
|
|
2768
|
+
type: "string",
|
|
2769
|
+
description: "User ID (UUID) of the member to remove",
|
|
2770
|
+
required: true,
|
|
2771
|
+
},
|
|
2772
|
+
},
|
|
2773
|
+
execute: async (args) => {
|
|
2774
|
+
try {
|
|
2775
|
+
const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
|
|
2776
|
+
const userId = validateUuid(String(args.user_id ?? ""), "user_id");
|
|
2777
|
+
await apiDelete(`/api/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(userId)}`);
|
|
2778
|
+
return [
|
|
2779
|
+
"Team Member Removed",
|
|
2780
|
+
"",
|
|
2781
|
+
`Team ID: ${teamId}`,
|
|
2782
|
+
`User ID: ${userId}`,
|
|
2783
|
+
"",
|
|
2784
|
+
"The member has been removed from the team.",
|
|
2785
|
+
].join("\n");
|
|
2786
|
+
}
|
|
2787
|
+
catch (err) {
|
|
2788
|
+
return `Error: ${safeError(err)}`;
|
|
2789
|
+
}
|
|
2790
|
+
},
|
|
2791
|
+
};
|
|
2792
|
+
// 51. team_invite_member
|
|
2793
|
+
const teamInviteMemberTool = {
|
|
2794
|
+
name: "team_invite_member",
|
|
2795
|
+
description: "Send an invitation to join a team by email. The invitee receives an email with a link to accept.",
|
|
2796
|
+
parameters: {
|
|
2797
|
+
team_id: {
|
|
2798
|
+
type: "string",
|
|
2799
|
+
description: "Team ID (UUID)",
|
|
2800
|
+
required: true,
|
|
2801
|
+
},
|
|
2802
|
+
email: {
|
|
2803
|
+
type: "string",
|
|
2804
|
+
description: "Email address of the person to invite",
|
|
2805
|
+
required: true,
|
|
2806
|
+
},
|
|
2807
|
+
role: {
|
|
2808
|
+
type: "string",
|
|
2809
|
+
description: "Role for the invited member",
|
|
2810
|
+
required: false,
|
|
2811
|
+
enum: ["member", "admin"],
|
|
2812
|
+
default: "member",
|
|
2813
|
+
},
|
|
2814
|
+
},
|
|
2815
|
+
execute: async (args) => {
|
|
2816
|
+
try {
|
|
2817
|
+
const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
|
|
2818
|
+
const email = String(args.email ?? "").trim();
|
|
2819
|
+
if (!email || !EMAIL_RE.test(email)) {
|
|
2820
|
+
return "Error: A valid email address is required.";
|
|
2821
|
+
}
|
|
2822
|
+
if (email.length > 254) {
|
|
2823
|
+
return "Error: Email address is too long (max 254 characters).";
|
|
2824
|
+
}
|
|
2825
|
+
const role = String(args.role ?? "member").trim();
|
|
2826
|
+
if (role !== "member" && role !== "admin") {
|
|
2827
|
+
return "Error: role must be 'member' or 'admin'.";
|
|
2828
|
+
}
|
|
2829
|
+
const data = await apiPost(`/api/teams/${encodeURIComponent(teamId)}/invites`, {
|
|
2830
|
+
email,
|
|
2831
|
+
role,
|
|
2832
|
+
});
|
|
2833
|
+
return [
|
|
2834
|
+
"Team Invitation Sent",
|
|
2835
|
+
"",
|
|
2836
|
+
`Invite ID: ${data.id}`,
|
|
2837
|
+
`Team ID: ${data.teamId ?? teamId}`,
|
|
2838
|
+
`Email: ${data.email ?? email}`,
|
|
2839
|
+
`Role: ${data.role ?? role}`,
|
|
2840
|
+
`Status: ${data.status ?? "pending"}`,
|
|
2841
|
+
`Expires: ${data.expiresAt}`,
|
|
2842
|
+
"",
|
|
2843
|
+
"The invitee will receive an email with instructions to accept.",
|
|
2844
|
+
].join("\n");
|
|
2845
|
+
}
|
|
2846
|
+
catch (err) {
|
|
2847
|
+
return `Error: ${safeError(err)}`;
|
|
2848
|
+
}
|
|
2849
|
+
},
|
|
2850
|
+
};
|
|
2851
|
+
// 52. team_list_invites
|
|
2852
|
+
const teamListInvitesTool = {
|
|
2853
|
+
name: "team_list_invites",
|
|
2854
|
+
description: "List all pending invitations for a team.",
|
|
2855
|
+
parameters: {
|
|
2856
|
+
team_id: {
|
|
2857
|
+
type: "string",
|
|
2858
|
+
description: "Team ID (UUID)",
|
|
2859
|
+
required: true,
|
|
2860
|
+
},
|
|
2861
|
+
},
|
|
2862
|
+
execute: async (args) => {
|
|
2863
|
+
try {
|
|
2864
|
+
const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
|
|
2865
|
+
const data = await apiGet(`/api/teams/${encodeURIComponent(teamId)}/invites`);
|
|
2866
|
+
const invites = data.invites ?? [];
|
|
2867
|
+
if (invites.length === 0) {
|
|
2868
|
+
return "No pending invitations for this team.";
|
|
2869
|
+
}
|
|
2870
|
+
const lines = [`Team Invitations (${invites.length})`, ""];
|
|
2871
|
+
for (const inv of invites) {
|
|
2872
|
+
lines.push(` ${inv.email} | ${inv.role} | ${inv.status}`);
|
|
2873
|
+
lines.push(` ID: ${inv.id} | Expires: ${inv.expiresAt}`);
|
|
2874
|
+
lines.push("");
|
|
2875
|
+
}
|
|
2876
|
+
return truncate(lines.join("\n"));
|
|
2877
|
+
}
|
|
2878
|
+
catch (err) {
|
|
2879
|
+
return `Error: ${safeError(err)}`;
|
|
2880
|
+
}
|
|
2881
|
+
},
|
|
2882
|
+
};
|
|
2883
|
+
// 53. team_cancel_invite
|
|
2884
|
+
const teamCancelInviteTool = {
|
|
2885
|
+
name: "team_cancel_invite",
|
|
2886
|
+
description: "Cancel a pending team invitation. Only owners and admins can cancel invites.",
|
|
2887
|
+
parameters: {
|
|
2888
|
+
team_id: {
|
|
2889
|
+
type: "string",
|
|
2890
|
+
description: "Team ID (UUID)",
|
|
2891
|
+
required: true,
|
|
2892
|
+
},
|
|
2893
|
+
invite_id: {
|
|
2894
|
+
type: "string",
|
|
2895
|
+
description: "Invite ID (UUID) to cancel",
|
|
2896
|
+
required: true,
|
|
2897
|
+
},
|
|
2898
|
+
},
|
|
2899
|
+
execute: async (args) => {
|
|
2900
|
+
try {
|
|
2901
|
+
const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
|
|
2902
|
+
const inviteId = validateUuid(String(args.invite_id ?? ""), "invite_id");
|
|
2903
|
+
await apiDelete(`/api/teams/${encodeURIComponent(teamId)}/invites/${encodeURIComponent(inviteId)}`);
|
|
2904
|
+
return [
|
|
2905
|
+
"Team Invitation Cancelled",
|
|
2906
|
+
"",
|
|
2907
|
+
`Team ID: ${teamId}`,
|
|
2908
|
+
`Invite ID: ${inviteId}`,
|
|
2909
|
+
"",
|
|
2910
|
+
"The invitation has been cancelled.",
|
|
2911
|
+
].join("\n");
|
|
2912
|
+
}
|
|
2913
|
+
catch (err) {
|
|
2914
|
+
return `Error: ${safeError(err)}`;
|
|
2915
|
+
}
|
|
2916
|
+
},
|
|
2917
|
+
};
|
|
2918
|
+
// ---------------------------------------------------------------------------
|
|
2919
|
+
// MPP (Machine Payment Protocol) tools
|
|
2920
|
+
// ---------------------------------------------------------------------------
|
|
2921
|
+
const mppInfoTool = {
|
|
2922
|
+
name: "mpp_info",
|
|
2923
|
+
description: "Get Machine Payment Protocol (MPP) information including enabled status, " +
|
|
2924
|
+
"supported payment methods, pricing, and session limits.",
|
|
2925
|
+
parameters: {},
|
|
2926
|
+
execute: async () => {
|
|
2927
|
+
try {
|
|
2928
|
+
const data = await apiGet("/api/mpp/info");
|
|
2929
|
+
return JSON.stringify(data, null, 2);
|
|
2930
|
+
}
|
|
2931
|
+
catch (err) {
|
|
2932
|
+
return `Error: ${safeError(err)}`;
|
|
2933
|
+
}
|
|
2934
|
+
},
|
|
2935
|
+
};
|
|
2936
|
+
const payMppTool = {
|
|
2937
|
+
name: "pay_mpp",
|
|
2938
|
+
description: "Top up wallet via Machine Payment Protocol (MPP). " +
|
|
2939
|
+
"Supports tempo, stripe_spt, and lightning payment methods.",
|
|
2940
|
+
parameters: {
|
|
2941
|
+
amount_cents: {
|
|
2942
|
+
type: "number",
|
|
2943
|
+
description: "Amount in cents to top up (min 500 = $5, max 100000 = $1,000)",
|
|
2944
|
+
required: true,
|
|
2945
|
+
},
|
|
2946
|
+
method: {
|
|
2947
|
+
type: "string",
|
|
2948
|
+
description: "MPP payment method",
|
|
2949
|
+
required: true,
|
|
2950
|
+
enum: ["tempo", "stripe_spt", "lightning"],
|
|
2951
|
+
},
|
|
2952
|
+
},
|
|
2953
|
+
execute: async (args) => {
|
|
2954
|
+
const amountCents = Number(args.amount_cents);
|
|
2955
|
+
const method = String(args.method ?? "");
|
|
2956
|
+
if (!Number.isInteger(amountCents) ||
|
|
2957
|
+
amountCents < 500 ||
|
|
2958
|
+
amountCents > 100_000) {
|
|
2959
|
+
return "Error: amount_cents must be an integer between 500 and 100,000";
|
|
2960
|
+
}
|
|
2961
|
+
if (!["tempo", "stripe_spt", "lightning"].includes(method)) {
|
|
2962
|
+
return "Error: method must be one of: tempo, stripe_spt, lightning";
|
|
2963
|
+
}
|
|
2964
|
+
try {
|
|
2965
|
+
const data = await apiPost("/api/mpp/topup", {
|
|
2966
|
+
amountCents,
|
|
2967
|
+
method,
|
|
2968
|
+
});
|
|
2969
|
+
return JSON.stringify(data, null, 2);
|
|
2970
|
+
}
|
|
2971
|
+
catch (err) {
|
|
2972
|
+
return `Error: ${safeError(err)}`;
|
|
2973
|
+
}
|
|
2974
|
+
},
|
|
2975
|
+
};
|
|
2976
|
+
const mppSessionOpenTool = {
|
|
2977
|
+
name: "mpp_session_open",
|
|
2978
|
+
description: "Open a pay-as-you-go MPP session. Returns a channelId for metered proxy usage.",
|
|
2979
|
+
parameters: {
|
|
2980
|
+
max_deposit_cents: {
|
|
2981
|
+
type: "number",
|
|
2982
|
+
description: "Maximum deposit in cents (min 500, max 100000)",
|
|
2983
|
+
required: true,
|
|
2984
|
+
},
|
|
2985
|
+
method: {
|
|
2986
|
+
type: "string",
|
|
2987
|
+
description: "MPP payment method",
|
|
2988
|
+
required: true,
|
|
2989
|
+
enum: ["tempo", "stripe_spt", "lightning"],
|
|
2990
|
+
},
|
|
2991
|
+
pool_type: {
|
|
2992
|
+
type: "string",
|
|
2993
|
+
description: "Proxy pool type: dc ($3/GB) or residential ($5/GB)",
|
|
2994
|
+
default: "dc",
|
|
2995
|
+
enum: ["dc", "residential"],
|
|
2996
|
+
},
|
|
2997
|
+
},
|
|
2998
|
+
execute: async (args) => {
|
|
2999
|
+
const maxDepositCents = Number(args.max_deposit_cents);
|
|
3000
|
+
const method = String(args.method ?? "");
|
|
3001
|
+
const poolType = String(args.pool_type ?? "dc");
|
|
3002
|
+
if (!Number.isInteger(maxDepositCents) ||
|
|
3003
|
+
maxDepositCents < 500 ||
|
|
3004
|
+
maxDepositCents > 100_000) {
|
|
3005
|
+
return "Error: max_deposit_cents must be an integer between 500 and 100,000";
|
|
3006
|
+
}
|
|
3007
|
+
if (!["tempo", "stripe_spt", "lightning"].includes(method)) {
|
|
3008
|
+
return "Error: method must be one of: tempo, stripe_spt, lightning";
|
|
3009
|
+
}
|
|
3010
|
+
if (!["dc", "residential"].includes(poolType)) {
|
|
3011
|
+
return "Error: pool_type must be dc or residential";
|
|
3012
|
+
}
|
|
3013
|
+
try {
|
|
3014
|
+
const data = await apiPost("/api/mpp/session/open", { maxDepositCents, method, poolType });
|
|
3015
|
+
return JSON.stringify(data, null, 2);
|
|
3016
|
+
}
|
|
3017
|
+
catch (err) {
|
|
3018
|
+
return `Error: ${safeError(err)}`;
|
|
3019
|
+
}
|
|
3020
|
+
},
|
|
3021
|
+
};
|
|
3022
|
+
const mppSessionCloseTool = {
|
|
3023
|
+
name: "mpp_session_close",
|
|
3024
|
+
description: "Close an MPP pay-as-you-go session. Returns the amount spent and refunded.",
|
|
3025
|
+
parameters: {
|
|
3026
|
+
channel_id: {
|
|
3027
|
+
type: "string",
|
|
3028
|
+
description: "The channelId returned from mpp_session_open",
|
|
3029
|
+
required: true,
|
|
3030
|
+
},
|
|
3031
|
+
},
|
|
3032
|
+
execute: async (args) => {
|
|
3033
|
+
const channelId = String(args.channel_id ?? "");
|
|
3034
|
+
if (!channelId) {
|
|
3035
|
+
return "Error: channel_id is required";
|
|
3036
|
+
}
|
|
3037
|
+
try {
|
|
3038
|
+
const data = await apiPost("/api/mpp/session/close", { channelId });
|
|
3039
|
+
return JSON.stringify(data, null, 2);
|
|
3040
|
+
}
|
|
3041
|
+
catch (err) {
|
|
3042
|
+
return `Error: ${safeError(err)}`;
|
|
3043
|
+
}
|
|
3044
|
+
},
|
|
3045
|
+
};
|
|
3046
|
+
// ---------------------------------------------------------------------------
|
|
3047
|
+
// Plugin export — the tools array that OpenClaw discovers
|
|
3048
|
+
// ---------------------------------------------------------------------------
|
|
3049
|
+
export const tools = [
|
|
3050
|
+
// Proxy (3)
|
|
3051
|
+
proxiedFetchTool,
|
|
3052
|
+
getProxyConfigTool,
|
|
3053
|
+
getProxyStatusTool,
|
|
3054
|
+
// Sessions (1)
|
|
3055
|
+
listSessionsTool,
|
|
3056
|
+
// Wallet (8)
|
|
3057
|
+
checkBalanceTool,
|
|
3058
|
+
getTransactionsTool,
|
|
3059
|
+
getForecastTool,
|
|
3060
|
+
topupPaypalTool,
|
|
3061
|
+
topupStripeTool,
|
|
3062
|
+
topupCryptoTool,
|
|
3063
|
+
checkPaymentTool,
|
|
3064
|
+
x402InfoTool,
|
|
3065
|
+
// Usage (3)
|
|
3066
|
+
checkUsageTool,
|
|
3067
|
+
getDailyUsageTool,
|
|
3068
|
+
getTopHostsTool,
|
|
3069
|
+
// Account (6)
|
|
3070
|
+
registerTool,
|
|
3071
|
+
loginTool,
|
|
3072
|
+
getAccountInfoTool,
|
|
3073
|
+
verifyEmailTool,
|
|
3074
|
+
resendVerificationTool,
|
|
3075
|
+
updatePasswordTool,
|
|
3076
|
+
// API Keys (3)
|
|
3077
|
+
listKeysTool,
|
|
3078
|
+
createKeyTool,
|
|
3079
|
+
revokeKeyTool,
|
|
3080
|
+
// Plans (3)
|
|
3081
|
+
getPlanTool,
|
|
3082
|
+
listPlansTool,
|
|
3083
|
+
changePlanTool,
|
|
3084
|
+
// Agentic Wallets (9)
|
|
3085
|
+
createAgenticWalletTool,
|
|
3086
|
+
fundAgenticWalletTool,
|
|
3087
|
+
checkAgenticBalanceTool,
|
|
3088
|
+
listAgenticWalletsTool,
|
|
3089
|
+
agenticTransactionsTool,
|
|
3090
|
+
freezeAgenticWalletTool,
|
|
3091
|
+
unfreezeAgenticWalletTool,
|
|
3092
|
+
deleteAgenticWalletTool,
|
|
3093
|
+
updateWalletPolicyTool,
|
|
3094
|
+
// Teams (17)
|
|
3095
|
+
createTeamTool,
|
|
3096
|
+
listTeamsTool,
|
|
3097
|
+
teamDetailsTool,
|
|
3098
|
+
updateTeamTool,
|
|
3099
|
+
teamDeleteTool,
|
|
3100
|
+
teamFundTool,
|
|
3101
|
+
teamCreateKeyTool,
|
|
3102
|
+
teamRevokeKeyTool,
|
|
3103
|
+
teamListKeysTool,
|
|
3104
|
+
teamUsageTool,
|
|
3105
|
+
teamListMembersTool,
|
|
3106
|
+
teamAddMemberTool,
|
|
3107
|
+
teamRemoveMemberTool,
|
|
3108
|
+
updateTeamMemberRoleTool,
|
|
3109
|
+
teamInviteMemberTool,
|
|
3110
|
+
teamListInvitesTool,
|
|
3111
|
+
teamCancelInviteTool,
|
|
3112
|
+
// MPP (4)
|
|
3113
|
+
mppInfoTool,
|
|
3114
|
+
payMppTool,
|
|
3115
|
+
mppSessionOpenTool,
|
|
3116
|
+
mppSessionCloseTool,
|
|
1732
3117
|
];
|
|
1733
3118
|
/**
|
|
1734
3119
|
* Plugin metadata for OpenClaw discovery.
|