@dominusnode/chatgpt 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/LICENSE +21 -0
- package/README.md +175 -0
- package/ai-plugin.json +18 -0
- package/dist/handler.d.ts +86 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +1915 -0
- package/dist/handler.js.map +1 -0
- package/functions.json +477 -0
- package/openapi.yaml +1851 -0
- package/package.json +44 -0
package/dist/handler.js
ADDED
|
@@ -0,0 +1,1915 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dominus Node ChatGPT Actions / function calling handler (TypeScript).
|
|
3
|
+
*
|
|
4
|
+
* 53 tools covering proxy, wallet, usage, account lifecycle, API keys,
|
|
5
|
+
* plans, and teams.
|
|
6
|
+
*
|
|
7
|
+
* Provides a factory function that creates a handler for dispatching
|
|
8
|
+
* ChatGPT function calls to the Dominus Node REST API. Designed for
|
|
9
|
+
* ChatGPT Custom GPTs (Actions), OpenAI Assistants API, and any
|
|
10
|
+
* OpenAI-compatible function-calling system.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { createDominusNodeFunctionHandler } from "./handler";
|
|
15
|
+
*
|
|
16
|
+
* const handler = createDominusNodeFunctionHandler({
|
|
17
|
+
* apiKey: "dn_live_...",
|
|
18
|
+
* baseUrl: "https://api.dominusnode.com",
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Handle an action call from a ChatGPT Custom GPT
|
|
22
|
+
* const result = await handler("dominusnode_check_balance", {});
|
|
23
|
+
* console.log(result); // JSON string with balance info
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import * as crypto from "node:crypto";
|
|
29
|
+
import dns from "dns/promises";
|
|
30
|
+
import * as http from "node:http";
|
|
31
|
+
import * as tls from "node:tls";
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// SHA-256 Proof-of-Work solver
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
function countLeadingZeroBits(buf) {
|
|
36
|
+
let count = 0;
|
|
37
|
+
for (const byte of buf) {
|
|
38
|
+
if (byte === 0) {
|
|
39
|
+
count += 8;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
let mask = 0x80;
|
|
43
|
+
while (mask && !(byte & mask)) {
|
|
44
|
+
count++;
|
|
45
|
+
mask >>= 1;
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
return count;
|
|
50
|
+
}
|
|
51
|
+
async function solvePoW(baseUrl) {
|
|
52
|
+
try {
|
|
53
|
+
const resp = await fetch(`${baseUrl}/api/auth/pow/challenge`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
redirect: "error",
|
|
57
|
+
});
|
|
58
|
+
if (!resp.ok)
|
|
59
|
+
return null;
|
|
60
|
+
const text = await resp.text();
|
|
61
|
+
if (text.length > 10_485_760)
|
|
62
|
+
return null;
|
|
63
|
+
const challenge = JSON.parse(text);
|
|
64
|
+
const prefix = challenge.prefix ?? "";
|
|
65
|
+
const difficulty = challenge.difficulty ?? 20;
|
|
66
|
+
const challengeId = challenge.challengeId ?? "";
|
|
67
|
+
if (!prefix || !challengeId)
|
|
68
|
+
return null;
|
|
69
|
+
for (let nonce = 0; nonce < 100_000_000; nonce++) {
|
|
70
|
+
const hash = crypto
|
|
71
|
+
.createHash("sha256")
|
|
72
|
+
.update(prefix + nonce.toString())
|
|
73
|
+
.digest();
|
|
74
|
+
if (countLeadingZeroBits(hash) >= difficulty) {
|
|
75
|
+
return { challengeId, nonce: nonce.toString() };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// SSRF Prevention -- URL validation
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
const BLOCKED_HOSTNAMES = new Set([
|
|
88
|
+
"localhost",
|
|
89
|
+
"localhost.localdomain",
|
|
90
|
+
"ip6-localhost",
|
|
91
|
+
"ip6-loopback",
|
|
92
|
+
"[::1]",
|
|
93
|
+
"[::ffff:127.0.0.1]",
|
|
94
|
+
"0.0.0.0",
|
|
95
|
+
"[::]",
|
|
96
|
+
]);
|
|
97
|
+
/**
|
|
98
|
+
* Normalize non-standard IPv4 representations (hex, octal, decimal integer)
|
|
99
|
+
* to standard dotted-decimal to prevent SSRF bypasses like 0x7f000001,
|
|
100
|
+
* 2130706433, or 0177.0.0.1.
|
|
101
|
+
*/
|
|
102
|
+
function normalizeIpv4(hostname) {
|
|
103
|
+
// Single decimal integer (e.g., 2130706433 = 127.0.0.1)
|
|
104
|
+
if (/^\d+$/.test(hostname)) {
|
|
105
|
+
const n = parseInt(hostname, 10);
|
|
106
|
+
if (n >= 0 && n <= 0xffffffff) {
|
|
107
|
+
return `${(n >>> 24) & 0xff}.${(n >>> 16) & 0xff}.${(n >>> 8) & 0xff}.${n & 0xff}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Hex notation (e.g., 0x7f000001)
|
|
111
|
+
if (/^0x[0-9a-fA-F]+$/i.test(hostname)) {
|
|
112
|
+
const n = parseInt(hostname, 16);
|
|
113
|
+
if (n >= 0 && n <= 0xffffffff) {
|
|
114
|
+
return `${(n >>> 24) & 0xff}.${(n >>> 16) & 0xff}.${(n >>> 8) & 0xff}.${n & 0xff}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Octal or mixed-radix octets (e.g., 0177.0.0.1)
|
|
118
|
+
const parts = hostname.split(".");
|
|
119
|
+
if (parts.length === 4) {
|
|
120
|
+
const octets = [];
|
|
121
|
+
for (const part of parts) {
|
|
122
|
+
let val;
|
|
123
|
+
if (/^0x[0-9a-fA-F]+$/i.test(part)) {
|
|
124
|
+
val = parseInt(part, 16);
|
|
125
|
+
}
|
|
126
|
+
else if (/^0\d+$/.test(part)) {
|
|
127
|
+
val = parseInt(part, 8);
|
|
128
|
+
}
|
|
129
|
+
else if (/^\d+$/.test(part)) {
|
|
130
|
+
val = parseInt(part, 10);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (isNaN(val) || val < 0 || val > 255)
|
|
136
|
+
return null;
|
|
137
|
+
octets.push(val);
|
|
138
|
+
}
|
|
139
|
+
return octets.join(".");
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
function isPrivateIp(hostname) {
|
|
144
|
+
let ip = hostname.replace(/^\[|\]$/g, "");
|
|
145
|
+
// Strip IPv6 zone ID (%25eth0, %eth0)
|
|
146
|
+
const zoneIdx = ip.indexOf("%");
|
|
147
|
+
if (zoneIdx !== -1) {
|
|
148
|
+
ip = ip.substring(0, zoneIdx);
|
|
149
|
+
}
|
|
150
|
+
const normalized = normalizeIpv4(ip);
|
|
151
|
+
const checkIp = normalized ?? ip;
|
|
152
|
+
// IPv4 private ranges
|
|
153
|
+
const ipv4Match = checkIp.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
154
|
+
if (ipv4Match) {
|
|
155
|
+
const a = Number(ipv4Match[1]);
|
|
156
|
+
const b = Number(ipv4Match[2]);
|
|
157
|
+
if (a === 0)
|
|
158
|
+
return true; // 0.0.0.0/8
|
|
159
|
+
if (a === 10)
|
|
160
|
+
return true; // 10.0.0.0/8
|
|
161
|
+
if (a === 127)
|
|
162
|
+
return true; // 127.0.0.0/8
|
|
163
|
+
if (a === 169 && b === 254)
|
|
164
|
+
return true; // 169.254.0.0/16
|
|
165
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
166
|
+
return true; // 172.16.0.0/12
|
|
167
|
+
if (a === 192 && b === 168)
|
|
168
|
+
return true; // 192.168.0.0/16
|
|
169
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
170
|
+
return true; // 100.64.0.0/10 CGNAT
|
|
171
|
+
if (a >= 224)
|
|
172
|
+
return true; // multicast + reserved
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
// IPv6 private ranges
|
|
176
|
+
const ipLower = ip.toLowerCase();
|
|
177
|
+
if (ipLower === "::1")
|
|
178
|
+
return true;
|
|
179
|
+
if (ipLower === "::")
|
|
180
|
+
return true;
|
|
181
|
+
if (ipLower.startsWith("fc") || ipLower.startsWith("fd"))
|
|
182
|
+
return true;
|
|
183
|
+
if (ipLower.startsWith("fe80"))
|
|
184
|
+
return true;
|
|
185
|
+
if (ipLower.startsWith("::ffff:")) {
|
|
186
|
+
const embedded = ipLower.slice(7);
|
|
187
|
+
if (embedded.includes("."))
|
|
188
|
+
return isPrivateIp(embedded);
|
|
189
|
+
const hexParts = embedded.split(":");
|
|
190
|
+
if (hexParts.length === 2) {
|
|
191
|
+
const hi = parseInt(hexParts[0], 16);
|
|
192
|
+
const lo = parseInt(hexParts[1], 16);
|
|
193
|
+
if (!isNaN(hi) && !isNaN(lo)) {
|
|
194
|
+
const reconstructed = `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;
|
|
195
|
+
return isPrivateIp(reconstructed);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return isPrivateIp(embedded);
|
|
199
|
+
}
|
|
200
|
+
// IPv4-compatible IPv6 (::x.x.x.x) — deprecated but still parsed
|
|
201
|
+
if (ipLower.startsWith("::") && !ipLower.startsWith("::ffff:")) {
|
|
202
|
+
const rest = ipLower.slice(2);
|
|
203
|
+
if (rest && rest.includes("."))
|
|
204
|
+
return isPrivateIp(rest);
|
|
205
|
+
const hexParts = rest.split(":");
|
|
206
|
+
if (hexParts.length === 2 && hexParts[0] && hexParts[1]) {
|
|
207
|
+
const hi = parseInt(hexParts[0], 16);
|
|
208
|
+
const lo = parseInt(hexParts[1], 16);
|
|
209
|
+
if (!isNaN(hi) && !isNaN(lo)) {
|
|
210
|
+
const reconstructed = `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;
|
|
211
|
+
return isPrivateIp(reconstructed);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Teredo (2001:0000::/32) — block unconditionally
|
|
216
|
+
if (ipLower.startsWith("2001:0000:") || ipLower.startsWith("2001:0:"))
|
|
217
|
+
return true;
|
|
218
|
+
// 6to4 (2002::/16) — block unconditionally
|
|
219
|
+
if (ipLower.startsWith("2002:"))
|
|
220
|
+
return true;
|
|
221
|
+
// IPv6 multicast (ff00::/8)
|
|
222
|
+
if (ipLower.startsWith("ff"))
|
|
223
|
+
return true;
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Validate a URL for safety before sending through the proxy.
|
|
228
|
+
* Blocks private IPs, localhost, internal hostnames, and non-HTTP(S) protocols.
|
|
229
|
+
*
|
|
230
|
+
* @throws {Error} If the URL is invalid or targets a private/blocked address.
|
|
231
|
+
*/
|
|
232
|
+
function validateUrl(url) {
|
|
233
|
+
let parsed;
|
|
234
|
+
try {
|
|
235
|
+
parsed = new URL(url);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
throw new Error(`Invalid URL: ${url}`);
|
|
239
|
+
}
|
|
240
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
241
|
+
throw new Error(`Only http: and https: protocols are supported, got ${parsed.protocol}`);
|
|
242
|
+
}
|
|
243
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
244
|
+
if (BLOCKED_HOSTNAMES.has(hostname)) {
|
|
245
|
+
throw new Error("Requests to localhost/loopback addresses are blocked");
|
|
246
|
+
}
|
|
247
|
+
if (isPrivateIp(hostname)) {
|
|
248
|
+
throw new Error("Requests to private/internal IP addresses are blocked");
|
|
249
|
+
}
|
|
250
|
+
if (hostname.endsWith(".localhost")) {
|
|
251
|
+
throw new Error("Requests to localhost/loopback addresses are blocked");
|
|
252
|
+
}
|
|
253
|
+
if (hostname.endsWith(".local") ||
|
|
254
|
+
hostname.endsWith(".internal") ||
|
|
255
|
+
hostname.endsWith(".arpa")) {
|
|
256
|
+
throw new Error("Requests to internal network hostnames are blocked");
|
|
257
|
+
}
|
|
258
|
+
// Block embedded credentials in URL
|
|
259
|
+
if (parsed.username || parsed.password) {
|
|
260
|
+
throw new Error("URLs with embedded credentials are not allowed");
|
|
261
|
+
}
|
|
262
|
+
return parsed;
|
|
263
|
+
}
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Sanctioned countries (OFAC)
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
const SANCTIONED_COUNTRIES = new Set(["CU", "IR", "KP", "RU", "SY"]);
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Prototype pollution prevention
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
272
|
+
function stripDangerousKeys(obj, depth = 0) {
|
|
273
|
+
if (depth > 50 || !obj || typeof obj !== "object")
|
|
274
|
+
return;
|
|
275
|
+
if (Array.isArray(obj)) {
|
|
276
|
+
for (const item of obj)
|
|
277
|
+
stripDangerousKeys(item, depth + 1);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const record = obj;
|
|
281
|
+
for (const key of Object.keys(record)) {
|
|
282
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
283
|
+
delete record[key];
|
|
284
|
+
}
|
|
285
|
+
else if (record[key] && typeof record[key] === "object") {
|
|
286
|
+
stripDangerousKeys(record[key], depth + 1);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function safeJsonParse(text) {
|
|
291
|
+
const parsed = JSON.parse(text);
|
|
292
|
+
stripDangerousKeys(parsed);
|
|
293
|
+
return parsed;
|
|
294
|
+
}
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// DNS rebinding protection
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
/**
|
|
299
|
+
* Resolve a hostname and verify none of the resolved IPs are private.
|
|
300
|
+
* Prevents DNS rebinding attacks where a hostname initially resolves to a
|
|
301
|
+
* public IP during validation but later resolves to a private IP.
|
|
302
|
+
*/
|
|
303
|
+
async function checkDnsRebinding(hostname) {
|
|
304
|
+
// Skip if hostname is already an IP literal
|
|
305
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname) || hostname.startsWith("[")) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
// Check IPv4 addresses
|
|
309
|
+
try {
|
|
310
|
+
const addresses = await dns.resolve4(hostname);
|
|
311
|
+
for (const addr of addresses) {
|
|
312
|
+
if (isPrivateIp(addr)) {
|
|
313
|
+
throw new Error(`Hostname resolves to private IP ${addr}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
if (err.code === "ENOTFOUND") {
|
|
319
|
+
throw new Error(`Could not resolve hostname: ${hostname}`);
|
|
320
|
+
}
|
|
321
|
+
if (err instanceof Error && err.message.includes("private IP"))
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
// Check IPv6 addresses
|
|
325
|
+
try {
|
|
326
|
+
const addresses = await dns.resolve6(hostname);
|
|
327
|
+
for (const addr of addresses) {
|
|
328
|
+
if (isPrivateIp(addr)) {
|
|
329
|
+
throw new Error(`Hostname resolves to private IPv6 ${addr}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// IPv6 resolution failure is acceptable
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// Credential sanitization
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
const CREDENTIAL_RE = /dn_(live|test|proxy)_[a-zA-Z0-9]+/g;
|
|
341
|
+
function sanitizeError(message) {
|
|
342
|
+
return message.replace(CREDENTIAL_RE, "***");
|
|
343
|
+
}
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Allowed HTTP methods for proxied fetch
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
const ALLOWED_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Configuration
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
function getProxyHost() {
|
|
352
|
+
return process.env.DOMINUSNODE_PROXY_HOST || "proxy.dominusnode.com";
|
|
353
|
+
}
|
|
354
|
+
function getProxyPort() {
|
|
355
|
+
const port = parseInt(process.env.DOMINUSNODE_PROXY_PORT || "8080", 10);
|
|
356
|
+
if (isNaN(port) || port < 1 || port > 65535)
|
|
357
|
+
return 8080;
|
|
358
|
+
return port;
|
|
359
|
+
}
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Internal HTTP helper
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
const MAX_RESPONSE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
364
|
+
async function apiRequest(config, method, path, body) {
|
|
365
|
+
const url = `${config.baseUrl}${path}`;
|
|
366
|
+
const headers = {
|
|
367
|
+
"User-Agent": "dominusnode-chatgpt/1.0.0",
|
|
368
|
+
"Content-Type": "application/json",
|
|
369
|
+
Authorization: `Bearer ${config.token}`,
|
|
370
|
+
};
|
|
371
|
+
if (config.agentSecret) {
|
|
372
|
+
headers["X-DominusNode-Agent"] = "mcp";
|
|
373
|
+
headers["X-DominusNode-Agent-Secret"] = config.agentSecret;
|
|
374
|
+
}
|
|
375
|
+
const response = await fetch(url, {
|
|
376
|
+
method,
|
|
377
|
+
headers,
|
|
378
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
379
|
+
signal: AbortSignal.timeout(config.timeoutMs),
|
|
380
|
+
redirect: "error",
|
|
381
|
+
});
|
|
382
|
+
const contentLength = parseInt(response.headers.get("content-length") ?? "0", 10);
|
|
383
|
+
if (contentLength > MAX_RESPONSE_BYTES) {
|
|
384
|
+
throw new Error("Response body too large");
|
|
385
|
+
}
|
|
386
|
+
const responseText = await response.text();
|
|
387
|
+
if (responseText.length > MAX_RESPONSE_BYTES) {
|
|
388
|
+
throw new Error("Response body exceeds size limit");
|
|
389
|
+
}
|
|
390
|
+
if (!response.ok) {
|
|
391
|
+
let message;
|
|
392
|
+
try {
|
|
393
|
+
const parsed = JSON.parse(responseText);
|
|
394
|
+
message = parsed.error ?? parsed.message ?? responseText;
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
message = responseText;
|
|
398
|
+
}
|
|
399
|
+
if (message.length > 500)
|
|
400
|
+
message = message.slice(0, 500) + "... [truncated]";
|
|
401
|
+
throw new Error(`API error ${response.status}: ${sanitizeError(message)}`);
|
|
402
|
+
}
|
|
403
|
+
return responseText ? safeJsonParse(responseText) : {};
|
|
404
|
+
}
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
// Period to date range helper
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
function periodToDateRange(period) {
|
|
409
|
+
const now = new Date();
|
|
410
|
+
const until = now.toISOString();
|
|
411
|
+
let since;
|
|
412
|
+
switch (period) {
|
|
413
|
+
case "day":
|
|
414
|
+
since = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
415
|
+
break;
|
|
416
|
+
case "week":
|
|
417
|
+
since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
418
|
+
break;
|
|
419
|
+
case "month":
|
|
420
|
+
default:
|
|
421
|
+
since = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
return { since: since.toISOString(), until };
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Create a Dominus Node function handler for OpenAI-compatible function calling.
|
|
428
|
+
*
|
|
429
|
+
* Authenticates using the provided API key, then returns a handler function
|
|
430
|
+
* that dispatches function calls to the appropriate Dominus Node REST API endpoint.
|
|
431
|
+
*
|
|
432
|
+
* @param config - API key and optional base URL / timeout.
|
|
433
|
+
* @returns A handler function: (name, args) => Promise<string>
|
|
434
|
+
*
|
|
435
|
+
* @example
|
|
436
|
+
* ```ts
|
|
437
|
+
* import { createDominusNodeFunctionHandler } from "./handler";
|
|
438
|
+
*
|
|
439
|
+
* const handler = createDominusNodeFunctionHandler({
|
|
440
|
+
* apiKey: "dn_live_abc123",
|
|
441
|
+
* baseUrl: "https://api.dominusnode.com",
|
|
442
|
+
* });
|
|
443
|
+
*
|
|
444
|
+
* // Handle a function call from an LLM
|
|
445
|
+
* const result = await handler("dominusnode_check_balance", {});
|
|
446
|
+
* console.log(JSON.parse(result));
|
|
447
|
+
* // { balanceCents: 5000, balanceUsd: 50.00, currency: "USD", lastToppedUp: "..." }
|
|
448
|
+
* ```
|
|
449
|
+
*/
|
|
450
|
+
// Test exports — used by handler.test.ts
|
|
451
|
+
export { isPrivateIp, validateUrl, normalizeIpv4, sanitizeError, stripDangerousKeys, safeJsonParse, };
|
|
452
|
+
export function createDominusNodeFunctionHandler(config) {
|
|
453
|
+
const baseUrl = config.baseUrl ?? "https://api.dominusnode.com";
|
|
454
|
+
const timeoutMs = config.timeoutMs ?? 30_000;
|
|
455
|
+
const agentSecret = config.agentSecret || process.env.DOMINUSNODE_AGENT_SECRET;
|
|
456
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
457
|
+
throw new Error("apiKey is required and must be a non-empty string");
|
|
458
|
+
}
|
|
459
|
+
// Authentication state -- lazily initialized on first call
|
|
460
|
+
let authToken = null;
|
|
461
|
+
let authPromise = null;
|
|
462
|
+
async function authenticate() {
|
|
463
|
+
const authHeaders = {
|
|
464
|
+
"User-Agent": "dominusnode-chatgpt/1.0.0",
|
|
465
|
+
"Content-Type": "application/json",
|
|
466
|
+
};
|
|
467
|
+
if (agentSecret) {
|
|
468
|
+
authHeaders["X-DominusNode-Agent"] = "mcp";
|
|
469
|
+
authHeaders["X-DominusNode-Agent-Secret"] = agentSecret;
|
|
470
|
+
}
|
|
471
|
+
const response = await fetch(`${baseUrl}/api/auth/verify-key`, {
|
|
472
|
+
method: "POST",
|
|
473
|
+
headers: authHeaders,
|
|
474
|
+
body: JSON.stringify({ apiKey: config.apiKey }),
|
|
475
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
476
|
+
redirect: "error",
|
|
477
|
+
});
|
|
478
|
+
if (!response.ok) {
|
|
479
|
+
const text = await response.text();
|
|
480
|
+
throw new Error(`Authentication failed (${response.status}): ${sanitizeError(text.slice(0, 500))}`);
|
|
481
|
+
}
|
|
482
|
+
const data = safeJsonParse(await response.text());
|
|
483
|
+
if (!data.token) {
|
|
484
|
+
throw new Error("Authentication response missing token");
|
|
485
|
+
}
|
|
486
|
+
authToken = data.token;
|
|
487
|
+
}
|
|
488
|
+
async function ensureAuth() {
|
|
489
|
+
if (authToken)
|
|
490
|
+
return;
|
|
491
|
+
if (!authPromise) {
|
|
492
|
+
authPromise = authenticate().finally(() => {
|
|
493
|
+
authPromise = null;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
await authPromise;
|
|
497
|
+
}
|
|
498
|
+
function api(method, path, body) {
|
|
499
|
+
if (!authToken)
|
|
500
|
+
throw new Error("Not authenticated");
|
|
501
|
+
return apiRequest({ baseUrl, timeoutMs, token: authToken, agentSecret }, method, path, body);
|
|
502
|
+
}
|
|
503
|
+
// -----------------------------------------------------------------------
|
|
504
|
+
// Function handlers
|
|
505
|
+
// -----------------------------------------------------------------------
|
|
506
|
+
async function handleProxiedFetch(args) {
|
|
507
|
+
const url = args.url;
|
|
508
|
+
if (!url || typeof url !== "string") {
|
|
509
|
+
return JSON.stringify({ error: "url is required and must be a string" });
|
|
510
|
+
}
|
|
511
|
+
// SSRF validation
|
|
512
|
+
let parsedUrl;
|
|
513
|
+
try {
|
|
514
|
+
parsedUrl = validateUrl(url);
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
return JSON.stringify({
|
|
518
|
+
error: err instanceof Error ? err.message : "URL validation failed",
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
// DNS rebinding protection
|
|
522
|
+
try {
|
|
523
|
+
await checkDnsRebinding(parsedUrl.hostname);
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
return JSON.stringify({
|
|
527
|
+
error: err instanceof Error ? err.message : "DNS validation failed",
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
// Country validation
|
|
531
|
+
const country = args.country;
|
|
532
|
+
if (country) {
|
|
533
|
+
const upper = country.toUpperCase();
|
|
534
|
+
if (SANCTIONED_COUNTRIES.has(upper)) {
|
|
535
|
+
return JSON.stringify({
|
|
536
|
+
error: `Country '${upper}' is blocked (OFAC sanctioned country)`,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const method = (args.method ?? "GET").toUpperCase();
|
|
541
|
+
// Restrict to read-only HTTP methods
|
|
542
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
543
|
+
return JSON.stringify({
|
|
544
|
+
error: `HTTP method '${method}' is not allowed. Only GET, HEAD, OPTIONS are permitted.`,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
const proxyType = args.proxy_type ?? "dc";
|
|
548
|
+
const headers = args.headers;
|
|
549
|
+
// Build proxy username for geo-targeting
|
|
550
|
+
const userParts = [proxyType];
|
|
551
|
+
if (country) {
|
|
552
|
+
userParts.push(`country-${country.toUpperCase()}`);
|
|
553
|
+
}
|
|
554
|
+
const username = userParts.join("-");
|
|
555
|
+
// Use the proxy gateway for the actual request
|
|
556
|
+
// The handler routes through the Dominus Node proxy endpoint
|
|
557
|
+
try {
|
|
558
|
+
// Strip security-sensitive headers from user-provided headers
|
|
559
|
+
const BLOCKED_HEADERS = new Set([
|
|
560
|
+
"host",
|
|
561
|
+
"connection",
|
|
562
|
+
"content-length",
|
|
563
|
+
"transfer-encoding",
|
|
564
|
+
"proxy-authorization",
|
|
565
|
+
"authorization",
|
|
566
|
+
"user-agent",
|
|
567
|
+
]);
|
|
568
|
+
const safeHeaders = {};
|
|
569
|
+
if (headers) {
|
|
570
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
571
|
+
if (!BLOCKED_HEADERS.has(key.toLowerCase())) {
|
|
572
|
+
// CRLF injection prevention
|
|
573
|
+
if (/[\r\n\0]/.test(key) || /[\r\n\0]/.test(value)) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
safeHeaders[key] = value;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Route through proxy gateway directly
|
|
581
|
+
const proxyHost = getProxyHost();
|
|
582
|
+
const proxyPort = getProxyPort();
|
|
583
|
+
const apiKey = config.apiKey;
|
|
584
|
+
const parts = [];
|
|
585
|
+
if (proxyType && proxyType !== "auto")
|
|
586
|
+
parts.push(proxyType);
|
|
587
|
+
if (country)
|
|
588
|
+
parts.push(`country-${country.toUpperCase()}`);
|
|
589
|
+
const username = parts.length > 0 ? parts.join("-") : "auto";
|
|
590
|
+
const proxyAuth = "Basic " + Buffer.from(`${username}:${apiKey}`).toString("base64");
|
|
591
|
+
const parsed = new URL(url);
|
|
592
|
+
const MAX_BODY_BYTES = 1_048_576; // 1MB response cap
|
|
593
|
+
const result = await new Promise((resolve, reject) => {
|
|
594
|
+
const timeout = setTimeout(() => reject(new Error("Proxy request timed out after 30000ms")), 30_000);
|
|
595
|
+
if (parsed.protocol === "https:") {
|
|
596
|
+
// HTTPS: CONNECT tunnel + TLS
|
|
597
|
+
const connectHost = parsed.hostname.includes(":")
|
|
598
|
+
? `[${parsed.hostname}]`
|
|
599
|
+
: parsed.hostname;
|
|
600
|
+
const connectReq = http.request({
|
|
601
|
+
hostname: proxyHost,
|
|
602
|
+
port: proxyPort,
|
|
603
|
+
method: "CONNECT",
|
|
604
|
+
path: `${connectHost}:${parsed.port || 443}`,
|
|
605
|
+
headers: {
|
|
606
|
+
"Proxy-Authorization": proxyAuth,
|
|
607
|
+
Host: `${connectHost}:${parsed.port || 443}`,
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
connectReq.on("connect", (_res, tunnelSocket) => {
|
|
611
|
+
if (_res.statusCode !== 200) {
|
|
612
|
+
clearTimeout(timeout);
|
|
613
|
+
tunnelSocket.destroy();
|
|
614
|
+
reject(new Error(`CONNECT failed: ${_res.statusCode}`));
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const tlsSocket = tls.connect({
|
|
618
|
+
host: parsed.hostname,
|
|
619
|
+
socket: tunnelSocket,
|
|
620
|
+
servername: parsed.hostname,
|
|
621
|
+
minVersion: "TLSv1.2",
|
|
622
|
+
}, () => {
|
|
623
|
+
const reqPath = parsed.pathname + parsed.search;
|
|
624
|
+
let reqLine = `${method} ${reqPath} HTTP/1.1\r\nHost: ${parsed.host}\r\nUser-Agent: dominusnode-chatgpt/1.0.0\r\nAccept: */*\r\nConnection: close\r\n`;
|
|
625
|
+
for (const [k, v] of Object.entries(safeHeaders)) {
|
|
626
|
+
if (!["host", "user-agent", "connection"].includes(k.toLowerCase())) {
|
|
627
|
+
reqLine += `${k}: ${v}\r\n`;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
reqLine += "\r\n";
|
|
631
|
+
tlsSocket.write(reqLine);
|
|
632
|
+
const chunks = [];
|
|
633
|
+
let byteCount = 0;
|
|
634
|
+
tlsSocket.on("data", (chunk) => {
|
|
635
|
+
byteCount += chunk.length;
|
|
636
|
+
if (byteCount <= MAX_BODY_BYTES + 16384)
|
|
637
|
+
chunks.push(chunk);
|
|
638
|
+
});
|
|
639
|
+
let finalized = false;
|
|
640
|
+
const finalize = () => {
|
|
641
|
+
if (finalized)
|
|
642
|
+
return;
|
|
643
|
+
finalized = true;
|
|
644
|
+
clearTimeout(timeout);
|
|
645
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
646
|
+
const headerEnd = raw.indexOf("\r\n\r\n");
|
|
647
|
+
if (headerEnd === -1) {
|
|
648
|
+
reject(new Error("Malformed response"));
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const headerSection = raw.substring(0, headerEnd);
|
|
652
|
+
const body = raw
|
|
653
|
+
.substring(headerEnd + 4)
|
|
654
|
+
.substring(0, MAX_BODY_BYTES);
|
|
655
|
+
const statusLine = headerSection.split("\r\n")[0];
|
|
656
|
+
const statusMatch = statusLine.match(/^HTTP\/\d\.\d\s+(\d+)/);
|
|
657
|
+
const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
|
|
658
|
+
const headers = {};
|
|
659
|
+
for (const line of headerSection.split("\r\n").slice(1)) {
|
|
660
|
+
const ci = line.indexOf(":");
|
|
661
|
+
if (ci > 0)
|
|
662
|
+
headers[line.substring(0, ci).trim().toLowerCase()] = line
|
|
663
|
+
.substring(ci + 1)
|
|
664
|
+
.trim();
|
|
665
|
+
}
|
|
666
|
+
stripDangerousKeys(headers);
|
|
667
|
+
resolve({ status, headers, body });
|
|
668
|
+
};
|
|
669
|
+
tlsSocket.on("end", finalize);
|
|
670
|
+
tlsSocket.on("close", finalize);
|
|
671
|
+
tlsSocket.on("error", (err) => {
|
|
672
|
+
clearTimeout(timeout);
|
|
673
|
+
reject(err);
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
tlsSocket.on("error", (err) => {
|
|
677
|
+
clearTimeout(timeout);
|
|
678
|
+
reject(err);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
connectReq.on("error", (err) => {
|
|
682
|
+
clearTimeout(timeout);
|
|
683
|
+
reject(err);
|
|
684
|
+
});
|
|
685
|
+
connectReq.end();
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
// HTTP: direct proxy request
|
|
689
|
+
const req = http.request({
|
|
690
|
+
hostname: proxyHost,
|
|
691
|
+
port: proxyPort,
|
|
692
|
+
method,
|
|
693
|
+
path: url,
|
|
694
|
+
headers: {
|
|
695
|
+
...safeHeaders,
|
|
696
|
+
"Proxy-Authorization": proxyAuth,
|
|
697
|
+
Host: parsed.host,
|
|
698
|
+
},
|
|
699
|
+
}, (res) => {
|
|
700
|
+
const chunks = [];
|
|
701
|
+
let byteCount = 0;
|
|
702
|
+
res.on("data", (chunk) => {
|
|
703
|
+
byteCount += chunk.length;
|
|
704
|
+
if (byteCount <= MAX_BODY_BYTES)
|
|
705
|
+
chunks.push(chunk);
|
|
706
|
+
});
|
|
707
|
+
let httpFinalized = false;
|
|
708
|
+
const finalize = () => {
|
|
709
|
+
if (httpFinalized)
|
|
710
|
+
return;
|
|
711
|
+
httpFinalized = true;
|
|
712
|
+
clearTimeout(timeout);
|
|
713
|
+
const body = Buffer.concat(chunks)
|
|
714
|
+
.toString("utf-8")
|
|
715
|
+
.substring(0, MAX_BODY_BYTES);
|
|
716
|
+
const headers = {};
|
|
717
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
718
|
+
if (v)
|
|
719
|
+
headers[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
720
|
+
}
|
|
721
|
+
stripDangerousKeys(headers);
|
|
722
|
+
resolve({ status: res.statusCode ?? 0, headers, body });
|
|
723
|
+
};
|
|
724
|
+
res.on("end", finalize);
|
|
725
|
+
res.on("close", finalize);
|
|
726
|
+
res.on("error", (err) => {
|
|
727
|
+
clearTimeout(timeout);
|
|
728
|
+
reject(err);
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
req.on("error", (err) => {
|
|
732
|
+
clearTimeout(timeout);
|
|
733
|
+
reject(err);
|
|
734
|
+
});
|
|
735
|
+
req.end();
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
return JSON.stringify({
|
|
739
|
+
status: result.status,
|
|
740
|
+
headers: result.headers,
|
|
741
|
+
body: result.body.substring(0, 4000), // Truncate for AI consumption
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
catch (err) {
|
|
745
|
+
return JSON.stringify({
|
|
746
|
+
error: `Proxy fetch failed: ${sanitizeError(err instanceof Error ? err.message : String(err))}`,
|
|
747
|
+
hint: "Ensure the Dominus Node proxy gateway is running and accessible.",
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function handleCheckBalance() {
|
|
752
|
+
const result = await api("GET", "/api/wallet");
|
|
753
|
+
return JSON.stringify(result);
|
|
754
|
+
}
|
|
755
|
+
async function handleCheckUsage(args) {
|
|
756
|
+
const period = args.period ?? "month";
|
|
757
|
+
const { since, until } = periodToDateRange(period);
|
|
758
|
+
const params = new URLSearchParams({ since, until });
|
|
759
|
+
const result = await api("GET", `/api/usage?${params.toString()}`);
|
|
760
|
+
return JSON.stringify(result);
|
|
761
|
+
}
|
|
762
|
+
async function handleGetProxyConfig() {
|
|
763
|
+
const result = await api("GET", "/api/proxy/config");
|
|
764
|
+
return JSON.stringify(result);
|
|
765
|
+
}
|
|
766
|
+
async function handleListSessions() {
|
|
767
|
+
const result = await api("GET", "/api/sessions/active");
|
|
768
|
+
return JSON.stringify(result);
|
|
769
|
+
}
|
|
770
|
+
async function handleCreateAgenticWallet(args) {
|
|
771
|
+
const label = args.label;
|
|
772
|
+
const spendingLimitCents = args.spending_limit_cents;
|
|
773
|
+
if (!label || typeof label !== "string") {
|
|
774
|
+
return JSON.stringify({
|
|
775
|
+
error: "label is required and must be a string",
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
if (label.length > 100) {
|
|
779
|
+
return JSON.stringify({ error: "label must be 100 characters or fewer" });
|
|
780
|
+
}
|
|
781
|
+
if (/[\x00-\x1f\x7f]/.test(label)) {
|
|
782
|
+
return JSON.stringify({
|
|
783
|
+
error: "label contains invalid control characters",
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
if (!Number.isInteger(spendingLimitCents) ||
|
|
787
|
+
spendingLimitCents <= 0 ||
|
|
788
|
+
spendingLimitCents > 2_147_483_647) {
|
|
789
|
+
return JSON.stringify({
|
|
790
|
+
error: "spending_limit_cents must be a positive integer <= 2,147,483,647",
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
const body = {
|
|
794
|
+
label,
|
|
795
|
+
spendingLimitCents,
|
|
796
|
+
};
|
|
797
|
+
// Optional daily_limit_cents
|
|
798
|
+
if (args.daily_limit_cents !== undefined) {
|
|
799
|
+
const dailyLimit = args.daily_limit_cents;
|
|
800
|
+
if (!Number.isInteger(dailyLimit) ||
|
|
801
|
+
dailyLimit < 1 ||
|
|
802
|
+
dailyLimit > 1_000_000) {
|
|
803
|
+
return JSON.stringify({
|
|
804
|
+
error: "daily_limit_cents must be an integer between 1 and 1,000,000",
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
body.dailyLimitCents = dailyLimit;
|
|
808
|
+
}
|
|
809
|
+
// Optional allowed_domains
|
|
810
|
+
if (args.allowed_domains !== undefined) {
|
|
811
|
+
const domains = args.allowed_domains;
|
|
812
|
+
if (!Array.isArray(domains)) {
|
|
813
|
+
return JSON.stringify({
|
|
814
|
+
error: "allowed_domains must be an array of strings",
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
if (domains.length > 100) {
|
|
818
|
+
return JSON.stringify({
|
|
819
|
+
error: "allowed_domains must have 100 or fewer entries",
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
const domainRe = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/;
|
|
823
|
+
for (const d of domains) {
|
|
824
|
+
if (typeof d !== "string") {
|
|
825
|
+
return JSON.stringify({
|
|
826
|
+
error: "Each allowed_domains entry must be a string",
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
if (d.length > 253) {
|
|
830
|
+
return JSON.stringify({
|
|
831
|
+
error: "Each allowed_domains entry must be 253 characters or fewer",
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
if (!domainRe.test(d)) {
|
|
835
|
+
return JSON.stringify({ error: `Invalid domain format: ${d}` });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
body.allowedDomains = domains;
|
|
839
|
+
}
|
|
840
|
+
const result = await api("POST", "/api/agent-wallet", body);
|
|
841
|
+
return JSON.stringify(result);
|
|
842
|
+
}
|
|
843
|
+
async function handleFundAgenticWallet(args) {
|
|
844
|
+
const walletId = args.wallet_id;
|
|
845
|
+
const amountCents = args.amount_cents;
|
|
846
|
+
if (!walletId || typeof walletId !== "string") {
|
|
847
|
+
return JSON.stringify({
|
|
848
|
+
error: "wallet_id is required and must be a string",
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
if (!Number.isInteger(amountCents) ||
|
|
852
|
+
amountCents <= 0 ||
|
|
853
|
+
amountCents > 2_147_483_647) {
|
|
854
|
+
return JSON.stringify({
|
|
855
|
+
error: "amount_cents must be a positive integer <= 2,147,483,647",
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
const result = await api("POST", `/api/agent-wallet/${encodeURIComponent(walletId)}/fund`, { amountCents });
|
|
859
|
+
return JSON.stringify(result);
|
|
860
|
+
}
|
|
861
|
+
async function handleAgenticWalletBalance(args) {
|
|
862
|
+
const walletId = args.wallet_id;
|
|
863
|
+
if (!walletId || typeof walletId !== "string") {
|
|
864
|
+
return JSON.stringify({
|
|
865
|
+
error: "wallet_id is required and must be a string",
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
const result = await api("GET", `/api/agent-wallet/${encodeURIComponent(walletId)}`);
|
|
869
|
+
return JSON.stringify(result);
|
|
870
|
+
}
|
|
871
|
+
async function handleListAgenticWallets() {
|
|
872
|
+
const result = await api("GET", "/api/agent-wallet");
|
|
873
|
+
return JSON.stringify(result);
|
|
874
|
+
}
|
|
875
|
+
async function handleAgenticTransactions(args) {
|
|
876
|
+
const walletId = args.wallet_id;
|
|
877
|
+
if (!walletId || typeof walletId !== "string") {
|
|
878
|
+
return JSON.stringify({
|
|
879
|
+
error: "wallet_id is required and must be a string",
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
const limit = args.limit;
|
|
883
|
+
const params = new URLSearchParams();
|
|
884
|
+
if (limit !== undefined) {
|
|
885
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
886
|
+
return JSON.stringify({
|
|
887
|
+
error: "limit must be an integer between 1 and 100",
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
params.set("limit", String(limit));
|
|
891
|
+
}
|
|
892
|
+
const qs = params.toString();
|
|
893
|
+
const result = await api("GET", `/api/agent-wallet/${encodeURIComponent(walletId)}/transactions${qs ? `?${qs}` : ""}`);
|
|
894
|
+
return JSON.stringify(result);
|
|
895
|
+
}
|
|
896
|
+
async function handleFreezeAgenticWallet(args) {
|
|
897
|
+
const walletId = args.wallet_id;
|
|
898
|
+
if (!walletId || typeof walletId !== "string") {
|
|
899
|
+
return JSON.stringify({
|
|
900
|
+
error: "wallet_id is required and must be a string",
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
const result = await api("POST", `/api/agent-wallet/${encodeURIComponent(walletId)}/freeze`);
|
|
904
|
+
return JSON.stringify(result);
|
|
905
|
+
}
|
|
906
|
+
async function handleUnfreezeAgenticWallet(args) {
|
|
907
|
+
const walletId = args.wallet_id;
|
|
908
|
+
if (!walletId || typeof walletId !== "string") {
|
|
909
|
+
return JSON.stringify({
|
|
910
|
+
error: "wallet_id is required and must be a string",
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
const result = await api("POST", `/api/agent-wallet/${encodeURIComponent(walletId)}/unfreeze`);
|
|
914
|
+
return JSON.stringify(result);
|
|
915
|
+
}
|
|
916
|
+
async function handleDeleteAgenticWallet(args) {
|
|
917
|
+
const walletId = args.wallet_id;
|
|
918
|
+
if (!walletId || typeof walletId !== "string") {
|
|
919
|
+
return JSON.stringify({
|
|
920
|
+
error: "wallet_id is required and must be a string",
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
const result = await api("DELETE", `/api/agent-wallet/${encodeURIComponent(walletId)}`);
|
|
924
|
+
return JSON.stringify(result);
|
|
925
|
+
}
|
|
926
|
+
async function handleCreateTeam(args) {
|
|
927
|
+
const name = args.name;
|
|
928
|
+
if (!name || typeof name !== "string") {
|
|
929
|
+
return JSON.stringify({ error: "name is required and must be a string" });
|
|
930
|
+
}
|
|
931
|
+
if (name.length > 100) {
|
|
932
|
+
return JSON.stringify({ error: "name must be 100 characters or fewer" });
|
|
933
|
+
}
|
|
934
|
+
if (/[\x00-\x1f\x7f]/.test(name)) {
|
|
935
|
+
return JSON.stringify({
|
|
936
|
+
error: "name contains invalid control characters",
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
const body = { name };
|
|
940
|
+
if (args.max_members !== undefined) {
|
|
941
|
+
const maxMembers = Number(args.max_members);
|
|
942
|
+
if (!Number.isInteger(maxMembers) || maxMembers < 1 || maxMembers > 100) {
|
|
943
|
+
return JSON.stringify({
|
|
944
|
+
error: "max_members must be an integer between 1 and 100",
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
body.maxMembers = maxMembers;
|
|
948
|
+
}
|
|
949
|
+
const result = await api("POST", "/api/teams", body);
|
|
950
|
+
return JSON.stringify(result);
|
|
951
|
+
}
|
|
952
|
+
async function handleListTeams() {
|
|
953
|
+
const result = await api("GET", "/api/teams");
|
|
954
|
+
return JSON.stringify(result);
|
|
955
|
+
}
|
|
956
|
+
async function handleTeamDetails(args) {
|
|
957
|
+
const teamId = args.team_id;
|
|
958
|
+
if (!teamId || typeof teamId !== "string") {
|
|
959
|
+
return JSON.stringify({
|
|
960
|
+
error: "team_id is required and must be a string",
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
const result = await api("GET", `/api/teams/${encodeURIComponent(teamId)}`);
|
|
964
|
+
return JSON.stringify(result);
|
|
965
|
+
}
|
|
966
|
+
async function handleTeamFund(args) {
|
|
967
|
+
const teamId = args.team_id;
|
|
968
|
+
const amountCents = args.amount_cents;
|
|
969
|
+
if (!teamId || typeof teamId !== "string") {
|
|
970
|
+
return JSON.stringify({
|
|
971
|
+
error: "team_id is required and must be a string",
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
if (!Number.isInteger(amountCents) ||
|
|
975
|
+
amountCents < 100 ||
|
|
976
|
+
amountCents > 1_000_000) {
|
|
977
|
+
return JSON.stringify({
|
|
978
|
+
error: "amount_cents must be an integer between 100 ($1) and 1,000,000 ($10,000)",
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
const result = await api("POST", `/api/teams/${encodeURIComponent(teamId)}/wallet/fund`, { amountCents });
|
|
982
|
+
return JSON.stringify(result);
|
|
983
|
+
}
|
|
984
|
+
async function handleTeamCreateKey(args) {
|
|
985
|
+
const teamId = args.team_id;
|
|
986
|
+
const label = args.label;
|
|
987
|
+
if (!teamId || typeof teamId !== "string") {
|
|
988
|
+
return JSON.stringify({
|
|
989
|
+
error: "team_id is required and must be a string",
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
if (!label || typeof label !== "string") {
|
|
993
|
+
return JSON.stringify({
|
|
994
|
+
error: "label is required and must be a string",
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
if (label.length > 100) {
|
|
998
|
+
return JSON.stringify({ error: "label must be 100 characters or fewer" });
|
|
999
|
+
}
|
|
1000
|
+
if (/[\x00-\x1f\x7f]/.test(label)) {
|
|
1001
|
+
return JSON.stringify({
|
|
1002
|
+
error: "label contains invalid control characters",
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
const result = await api("POST", `/api/teams/${encodeURIComponent(teamId)}/keys`, { label });
|
|
1006
|
+
return JSON.stringify(result);
|
|
1007
|
+
}
|
|
1008
|
+
async function handleTeamUsage(args) {
|
|
1009
|
+
const teamId = args.team_id;
|
|
1010
|
+
if (!teamId || typeof teamId !== "string") {
|
|
1011
|
+
return JSON.stringify({
|
|
1012
|
+
error: "team_id is required and must be a string",
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
const limit = args.limit;
|
|
1016
|
+
const params = new URLSearchParams();
|
|
1017
|
+
if (limit !== undefined) {
|
|
1018
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
1019
|
+
return JSON.stringify({
|
|
1020
|
+
error: "limit must be an integer between 1 and 100",
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
params.set("limit", String(limit));
|
|
1024
|
+
}
|
|
1025
|
+
const qs = params.toString();
|
|
1026
|
+
const result = await api("GET", `/api/teams/${encodeURIComponent(teamId)}/wallet/transactions${qs ? `?${qs}` : ""}`);
|
|
1027
|
+
return JSON.stringify(result);
|
|
1028
|
+
}
|
|
1029
|
+
async function handleUpdateTeam(args) {
|
|
1030
|
+
const teamId = args.team_id;
|
|
1031
|
+
if (!teamId || typeof teamId !== "string") {
|
|
1032
|
+
return JSON.stringify({
|
|
1033
|
+
error: "team_id is required and must be a string",
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(teamId)) {
|
|
1037
|
+
return JSON.stringify({ error: "team_id must be a valid UUID" });
|
|
1038
|
+
}
|
|
1039
|
+
const body = {};
|
|
1040
|
+
if (args.name !== undefined) {
|
|
1041
|
+
const name = args.name;
|
|
1042
|
+
if (!name || typeof name !== "string") {
|
|
1043
|
+
return JSON.stringify({ error: "name must be a non-empty string" });
|
|
1044
|
+
}
|
|
1045
|
+
if (name.length > 100) {
|
|
1046
|
+
return JSON.stringify({
|
|
1047
|
+
error: "name must be 100 characters or fewer",
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
if (/[\x00-\x1f\x7f]/.test(name)) {
|
|
1051
|
+
return JSON.stringify({
|
|
1052
|
+
error: "name contains invalid control characters",
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
body.name = name;
|
|
1056
|
+
}
|
|
1057
|
+
if (args.max_members !== undefined) {
|
|
1058
|
+
const maxMembers = Number(args.max_members);
|
|
1059
|
+
if (!Number.isInteger(maxMembers) || maxMembers < 1 || maxMembers > 100) {
|
|
1060
|
+
return JSON.stringify({
|
|
1061
|
+
error: "max_members must be an integer between 1 and 100",
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
body.maxMembers = maxMembers;
|
|
1065
|
+
}
|
|
1066
|
+
if (Object.keys(body).length === 0) {
|
|
1067
|
+
return JSON.stringify({
|
|
1068
|
+
error: "At least one of name or max_members must be provided",
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
const result = await api("PATCH", `/api/teams/${encodeURIComponent(teamId)}`, body);
|
|
1072
|
+
return JSON.stringify(result);
|
|
1073
|
+
}
|
|
1074
|
+
async function handleTopupPaypal(args) {
|
|
1075
|
+
const amountCents = args.amount_cents;
|
|
1076
|
+
if (!Number.isInteger(amountCents) ||
|
|
1077
|
+
amountCents < 500 ||
|
|
1078
|
+
amountCents > 100_000) {
|
|
1079
|
+
return JSON.stringify({
|
|
1080
|
+
error: "amount_cents must be an integer between 500 ($5) and 100,000 ($1,000)",
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
const result = await api("POST", "/api/wallet/topup/paypal", {
|
|
1084
|
+
amountCents,
|
|
1085
|
+
});
|
|
1086
|
+
return JSON.stringify(result);
|
|
1087
|
+
}
|
|
1088
|
+
async function handleTopupStripe(args) {
|
|
1089
|
+
const amountCents = args.amount_cents;
|
|
1090
|
+
if (!Number.isInteger(amountCents) ||
|
|
1091
|
+
amountCents < 500 ||
|
|
1092
|
+
amountCents > 100_000) {
|
|
1093
|
+
return JSON.stringify({
|
|
1094
|
+
error: "amount_cents must be an integer between 500 ($5) and 100,000 ($1,000)",
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
const result = await api("POST", "/api/wallet/topup/stripe", {
|
|
1098
|
+
amountCents,
|
|
1099
|
+
});
|
|
1100
|
+
return JSON.stringify(result);
|
|
1101
|
+
}
|
|
1102
|
+
async function handleTopupCrypto(args) {
|
|
1103
|
+
const amountUsd = args.amount_usd;
|
|
1104
|
+
const currency = args.currency;
|
|
1105
|
+
if (typeof amountUsd !== "number" ||
|
|
1106
|
+
!Number.isFinite(amountUsd) ||
|
|
1107
|
+
amountUsd < 5 ||
|
|
1108
|
+
amountUsd > 1000) {
|
|
1109
|
+
return JSON.stringify({
|
|
1110
|
+
error: "amount_usd must be a number between 5 and 1,000",
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
const validCurrencies = new Set([
|
|
1114
|
+
"BTC",
|
|
1115
|
+
"ETH",
|
|
1116
|
+
"LTC",
|
|
1117
|
+
"XMR",
|
|
1118
|
+
"ZEC",
|
|
1119
|
+
"USDC",
|
|
1120
|
+
"SOL",
|
|
1121
|
+
"USDT",
|
|
1122
|
+
"DAI",
|
|
1123
|
+
"BNB",
|
|
1124
|
+
"LINK",
|
|
1125
|
+
]);
|
|
1126
|
+
if (!currency ||
|
|
1127
|
+
typeof currency !== "string" ||
|
|
1128
|
+
!validCurrencies.has(currency.toUpperCase())) {
|
|
1129
|
+
return JSON.stringify({
|
|
1130
|
+
error: "currency must be one of: BTC, ETH, LTC, XMR, ZEC, USDC, SOL, USDT, DAI, BNB, LINK",
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
const result = await api("POST", "/api/wallet/topup/crypto", {
|
|
1134
|
+
amountUsd,
|
|
1135
|
+
currency: currency.toLowerCase(),
|
|
1136
|
+
});
|
|
1137
|
+
return JSON.stringify(result);
|
|
1138
|
+
}
|
|
1139
|
+
async function handleUpdateWalletPolicy(args) {
|
|
1140
|
+
const walletId = args.wallet_id;
|
|
1141
|
+
if (!walletId || typeof walletId !== "string") {
|
|
1142
|
+
return JSON.stringify({
|
|
1143
|
+
error: "wallet_id is required and must be a string",
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(walletId)) {
|
|
1147
|
+
return JSON.stringify({ error: "wallet_id must be a valid UUID" });
|
|
1148
|
+
}
|
|
1149
|
+
const body = {};
|
|
1150
|
+
// daily_limit_cents: integer or null (to remove)
|
|
1151
|
+
if (args.daily_limit_cents !== undefined) {
|
|
1152
|
+
if (args.daily_limit_cents === null) {
|
|
1153
|
+
body.dailyLimitCents = null;
|
|
1154
|
+
}
|
|
1155
|
+
else {
|
|
1156
|
+
const dailyLimit = args.daily_limit_cents;
|
|
1157
|
+
if (!Number.isInteger(dailyLimit) ||
|
|
1158
|
+
dailyLimit < 1 ||
|
|
1159
|
+
dailyLimit > 1_000_000) {
|
|
1160
|
+
return JSON.stringify({
|
|
1161
|
+
error: "daily_limit_cents must be an integer between 1 and 1,000,000 (or null to remove)",
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
body.dailyLimitCents = dailyLimit;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
// allowed_domains: array or null (to remove)
|
|
1168
|
+
if (args.allowed_domains !== undefined) {
|
|
1169
|
+
if (args.allowed_domains === null) {
|
|
1170
|
+
body.allowedDomains = null;
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
const domains = args.allowed_domains;
|
|
1174
|
+
if (!Array.isArray(domains)) {
|
|
1175
|
+
return JSON.stringify({
|
|
1176
|
+
error: "allowed_domains must be an array of strings or null",
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
if (domains.length > 100) {
|
|
1180
|
+
return JSON.stringify({
|
|
1181
|
+
error: "allowed_domains must have 100 or fewer entries",
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
const domainRe = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/;
|
|
1185
|
+
for (const d of domains) {
|
|
1186
|
+
if (typeof d !== "string") {
|
|
1187
|
+
return JSON.stringify({
|
|
1188
|
+
error: "Each allowed_domains entry must be a string",
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
if (d.length > 253) {
|
|
1192
|
+
return JSON.stringify({
|
|
1193
|
+
error: "Each allowed_domains entry must be 253 characters or fewer",
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
if (!domainRe.test(d)) {
|
|
1197
|
+
return JSON.stringify({ error: `Invalid domain format: ${d}` });
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
body.allowedDomains = domains;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (Object.keys(body).length === 0) {
|
|
1204
|
+
return JSON.stringify({
|
|
1205
|
+
error: "At least one of daily_limit_cents or allowed_domains must be provided",
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
const result = await api("PATCH", `/api/agent-wallet/${encodeURIComponent(walletId)}/policy`, body);
|
|
1209
|
+
return JSON.stringify(result);
|
|
1210
|
+
}
|
|
1211
|
+
async function handleUpdateTeamMemberRole(args) {
|
|
1212
|
+
const teamId = args.team_id;
|
|
1213
|
+
const userId = args.user_id;
|
|
1214
|
+
const role = args.role;
|
|
1215
|
+
if (!teamId || typeof teamId !== "string") {
|
|
1216
|
+
return JSON.stringify({
|
|
1217
|
+
error: "team_id is required and must be a string",
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(teamId)) {
|
|
1221
|
+
return JSON.stringify({ error: "team_id must be a valid UUID" });
|
|
1222
|
+
}
|
|
1223
|
+
if (!userId || typeof userId !== "string") {
|
|
1224
|
+
return JSON.stringify({
|
|
1225
|
+
error: "user_id is required and must be a string",
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(userId)) {
|
|
1229
|
+
return JSON.stringify({ error: "user_id must be a valid UUID" });
|
|
1230
|
+
}
|
|
1231
|
+
if (!role || typeof role !== "string") {
|
|
1232
|
+
return JSON.stringify({ error: "role is required and must be a string" });
|
|
1233
|
+
}
|
|
1234
|
+
if (role !== "member" && role !== "admin") {
|
|
1235
|
+
return JSON.stringify({ error: "role must be 'member' or 'admin'" });
|
|
1236
|
+
}
|
|
1237
|
+
const result = await api("PATCH", `/api/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(userId)}`, { role });
|
|
1238
|
+
return JSON.stringify(result);
|
|
1239
|
+
}
|
|
1240
|
+
// -----------------------------------------------------------------------
|
|
1241
|
+
// Proxy extended
|
|
1242
|
+
// -----------------------------------------------------------------------
|
|
1243
|
+
async function handleGetProxyStatus() {
|
|
1244
|
+
const result = await api("GET", "/api/proxy/status");
|
|
1245
|
+
return JSON.stringify(result);
|
|
1246
|
+
}
|
|
1247
|
+
// -----------------------------------------------------------------------
|
|
1248
|
+
// Wallet extended
|
|
1249
|
+
// -----------------------------------------------------------------------
|
|
1250
|
+
async function handleGetTransactions(args) {
|
|
1251
|
+
const params = new URLSearchParams();
|
|
1252
|
+
if (args.limit !== undefined) {
|
|
1253
|
+
const limit = args.limit;
|
|
1254
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
1255
|
+
return JSON.stringify({
|
|
1256
|
+
error: "limit must be an integer between 1 and 100",
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
params.set("limit", String(limit));
|
|
1260
|
+
}
|
|
1261
|
+
const qs = params.toString();
|
|
1262
|
+
const result = await api("GET", `/api/wallet/transactions${qs ? `?${qs}` : ""}`);
|
|
1263
|
+
return JSON.stringify(result);
|
|
1264
|
+
}
|
|
1265
|
+
async function handleGetForecast() {
|
|
1266
|
+
const result = await api("GET", "/api/wallet/forecast");
|
|
1267
|
+
return JSON.stringify(result);
|
|
1268
|
+
}
|
|
1269
|
+
async function handleCheckPayment(args) {
|
|
1270
|
+
const invoiceId = args.invoice_id;
|
|
1271
|
+
if (!invoiceId || typeof invoiceId !== "string") {
|
|
1272
|
+
return JSON.stringify({
|
|
1273
|
+
error: "invoice_id is required and must be a string",
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
const result = await api("GET", `/api/wallet/topup/crypto/${encodeURIComponent(invoiceId)}/status`);
|
|
1277
|
+
return JSON.stringify(result);
|
|
1278
|
+
}
|
|
1279
|
+
// -----------------------------------------------------------------------
|
|
1280
|
+
// Usage extended
|
|
1281
|
+
// -----------------------------------------------------------------------
|
|
1282
|
+
async function handleGetDailyUsage(args) {
|
|
1283
|
+
const days = args.days ?? 30;
|
|
1284
|
+
if (!Number.isInteger(days) || days < 1 || days > 90) {
|
|
1285
|
+
return JSON.stringify({
|
|
1286
|
+
error: "days must be an integer between 1 and 90",
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
const params = new URLSearchParams({ days: String(days) });
|
|
1290
|
+
const result = await api("GET", `/api/usage/daily?${params.toString()}`);
|
|
1291
|
+
return JSON.stringify(result);
|
|
1292
|
+
}
|
|
1293
|
+
async function handleGetTopHosts(args) {
|
|
1294
|
+
const params = new URLSearchParams();
|
|
1295
|
+
if (args.limit !== undefined) {
|
|
1296
|
+
const limit = args.limit;
|
|
1297
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
|
|
1298
|
+
return JSON.stringify({
|
|
1299
|
+
error: "limit must be an integer between 1 and 50",
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
params.set("limit", String(limit));
|
|
1303
|
+
}
|
|
1304
|
+
const qs = params.toString();
|
|
1305
|
+
const result = await api("GET", `/api/usage/top-hosts${qs ? `?${qs}` : ""}`);
|
|
1306
|
+
return JSON.stringify(result);
|
|
1307
|
+
}
|
|
1308
|
+
// -----------------------------------------------------------------------
|
|
1309
|
+
// Account lifecycle
|
|
1310
|
+
// -----------------------------------------------------------------------
|
|
1311
|
+
async function handleRegister(args) {
|
|
1312
|
+
const email = args.email;
|
|
1313
|
+
const password = args.password;
|
|
1314
|
+
if (!email || typeof email !== "string") {
|
|
1315
|
+
return JSON.stringify({
|
|
1316
|
+
error: "email is required and must be a string",
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
if (!/^[^@]+@[^@]+\.[^@]+$/.test(email)) {
|
|
1320
|
+
return JSON.stringify({ error: "email must be a valid email address" });
|
|
1321
|
+
}
|
|
1322
|
+
if (!password || typeof password !== "string") {
|
|
1323
|
+
return JSON.stringify({
|
|
1324
|
+
error: "password is required and must be a string",
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
if (password.length < 8 || password.length > 128) {
|
|
1328
|
+
return JSON.stringify({
|
|
1329
|
+
error: "password must be between 8 and 128 characters",
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
const headers = {
|
|
1333
|
+
"User-Agent": "dominusnode-chatgpt/1.0.0",
|
|
1334
|
+
"Content-Type": "application/json",
|
|
1335
|
+
};
|
|
1336
|
+
if (agentSecret) {
|
|
1337
|
+
headers["X-DominusNode-Agent"] = "mcp";
|
|
1338
|
+
headers["X-DominusNode-Agent-Secret"] = agentSecret;
|
|
1339
|
+
}
|
|
1340
|
+
// Solve PoW for CAPTCHA-free registration
|
|
1341
|
+
const pow = await solvePoW(baseUrl);
|
|
1342
|
+
const regBody = { email, password };
|
|
1343
|
+
if (pow)
|
|
1344
|
+
regBody.pow = pow;
|
|
1345
|
+
const response = await fetch(`${baseUrl}/api/auth/register`, {
|
|
1346
|
+
method: "POST",
|
|
1347
|
+
headers,
|
|
1348
|
+
body: JSON.stringify(regBody),
|
|
1349
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1350
|
+
redirect: "error",
|
|
1351
|
+
});
|
|
1352
|
+
const text = await response.text();
|
|
1353
|
+
if (text.length > MAX_RESPONSE_BYTES) {
|
|
1354
|
+
return JSON.stringify({ error: "Response body exceeds size limit" });
|
|
1355
|
+
}
|
|
1356
|
+
if (!response.ok) {
|
|
1357
|
+
let message;
|
|
1358
|
+
try {
|
|
1359
|
+
const parsed = JSON.parse(text);
|
|
1360
|
+
message = parsed.error ?? parsed.message ?? text;
|
|
1361
|
+
}
|
|
1362
|
+
catch {
|
|
1363
|
+
message = text;
|
|
1364
|
+
}
|
|
1365
|
+
if (message.length > 500)
|
|
1366
|
+
message = message.slice(0, 500) + "... [truncated]";
|
|
1367
|
+
return JSON.stringify({
|
|
1368
|
+
error: `Registration failed: ${sanitizeError(message)}`,
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
const data = safeJsonParse(text);
|
|
1372
|
+
stripDangerousKeys(data);
|
|
1373
|
+
return JSON.stringify({
|
|
1374
|
+
userId: data.userId,
|
|
1375
|
+
email: data.email,
|
|
1376
|
+
message: data.message,
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
async function handleLogin(args) {
|
|
1380
|
+
const email = args.email;
|
|
1381
|
+
const password = args.password;
|
|
1382
|
+
if (!email || typeof email !== "string") {
|
|
1383
|
+
return JSON.stringify({
|
|
1384
|
+
error: "email is required and must be a string",
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
if (!/^[^@]+@[^@]+\.[^@]+$/.test(email)) {
|
|
1388
|
+
return JSON.stringify({ error: "email must be a valid email address" });
|
|
1389
|
+
}
|
|
1390
|
+
if (!password || typeof password !== "string") {
|
|
1391
|
+
return JSON.stringify({
|
|
1392
|
+
error: "password is required and must be a string",
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
if (password.length < 8 || password.length > 128) {
|
|
1396
|
+
return JSON.stringify({
|
|
1397
|
+
error: "password must be between 8 and 128 characters",
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
const headers = {
|
|
1401
|
+
"User-Agent": "dominusnode-chatgpt/1.0.0",
|
|
1402
|
+
"Content-Type": "application/json",
|
|
1403
|
+
};
|
|
1404
|
+
if (agentSecret) {
|
|
1405
|
+
headers["X-DominusNode-Agent"] = "mcp";
|
|
1406
|
+
headers["X-DominusNode-Agent-Secret"] = agentSecret;
|
|
1407
|
+
}
|
|
1408
|
+
const response = await fetch(`${baseUrl}/api/auth/login`, {
|
|
1409
|
+
method: "POST",
|
|
1410
|
+
headers,
|
|
1411
|
+
body: JSON.stringify({ email, password }),
|
|
1412
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1413
|
+
redirect: "error",
|
|
1414
|
+
});
|
|
1415
|
+
const text = await response.text();
|
|
1416
|
+
if (text.length > MAX_RESPONSE_BYTES) {
|
|
1417
|
+
return JSON.stringify({ error: "Response body exceeds size limit" });
|
|
1418
|
+
}
|
|
1419
|
+
if (!response.ok) {
|
|
1420
|
+
let message;
|
|
1421
|
+
try {
|
|
1422
|
+
const parsed = JSON.parse(text);
|
|
1423
|
+
message = parsed.error ?? parsed.message ?? text;
|
|
1424
|
+
}
|
|
1425
|
+
catch {
|
|
1426
|
+
message = text;
|
|
1427
|
+
}
|
|
1428
|
+
if (message.length > 500)
|
|
1429
|
+
message = message.slice(0, 500) + "... [truncated]";
|
|
1430
|
+
return JSON.stringify({
|
|
1431
|
+
error: `Login failed: ${sanitizeError(message)}`,
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
const data = safeJsonParse(text);
|
|
1435
|
+
stripDangerousKeys(data);
|
|
1436
|
+
return JSON.stringify({ token: data.token, message: data.message });
|
|
1437
|
+
}
|
|
1438
|
+
async function handleGetAccountInfo() {
|
|
1439
|
+
const result = await api("GET", "/api/auth/me");
|
|
1440
|
+
return JSON.stringify(result);
|
|
1441
|
+
}
|
|
1442
|
+
async function handleVerifyEmail(args) {
|
|
1443
|
+
const token = args.token;
|
|
1444
|
+
if (!token || typeof token !== "string") {
|
|
1445
|
+
return JSON.stringify({
|
|
1446
|
+
error: "token is required and must be a string",
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
const headers = {
|
|
1450
|
+
"User-Agent": "dominusnode-chatgpt/1.0.0",
|
|
1451
|
+
"Content-Type": "application/json",
|
|
1452
|
+
};
|
|
1453
|
+
if (agentSecret) {
|
|
1454
|
+
headers["X-DominusNode-Agent"] = "mcp";
|
|
1455
|
+
headers["X-DominusNode-Agent-Secret"] = agentSecret;
|
|
1456
|
+
}
|
|
1457
|
+
const response = await fetch(`${baseUrl}/api/auth/verify-email`, {
|
|
1458
|
+
method: "POST",
|
|
1459
|
+
headers,
|
|
1460
|
+
body: JSON.stringify({ token }),
|
|
1461
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1462
|
+
redirect: "error",
|
|
1463
|
+
});
|
|
1464
|
+
const text = await response.text();
|
|
1465
|
+
if (text.length > MAX_RESPONSE_BYTES) {
|
|
1466
|
+
return JSON.stringify({ error: "Response body exceeds size limit" });
|
|
1467
|
+
}
|
|
1468
|
+
if (!response.ok) {
|
|
1469
|
+
let message;
|
|
1470
|
+
try {
|
|
1471
|
+
const parsed = JSON.parse(text);
|
|
1472
|
+
message = parsed.error ?? parsed.message ?? text;
|
|
1473
|
+
}
|
|
1474
|
+
catch {
|
|
1475
|
+
message = text;
|
|
1476
|
+
}
|
|
1477
|
+
if (message.length > 500)
|
|
1478
|
+
message = message.slice(0, 500) + "... [truncated]";
|
|
1479
|
+
return JSON.stringify({
|
|
1480
|
+
error: `Email verification failed: ${sanitizeError(message)}`,
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
const data = safeJsonParse(text);
|
|
1484
|
+
stripDangerousKeys(data);
|
|
1485
|
+
return JSON.stringify(data);
|
|
1486
|
+
}
|
|
1487
|
+
async function handleResendVerification() {
|
|
1488
|
+
const result = await api("POST", "/api/auth/resend-verification");
|
|
1489
|
+
return JSON.stringify(result);
|
|
1490
|
+
}
|
|
1491
|
+
async function handleUpdatePassword(args) {
|
|
1492
|
+
const currentPassword = args.current_password;
|
|
1493
|
+
const newPassword = args.new_password;
|
|
1494
|
+
if (!currentPassword || typeof currentPassword !== "string") {
|
|
1495
|
+
return JSON.stringify({
|
|
1496
|
+
error: "current_password is required and must be a string",
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
if (!newPassword || typeof newPassword !== "string") {
|
|
1500
|
+
return JSON.stringify({
|
|
1501
|
+
error: "new_password is required and must be a string",
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
if (newPassword.length < 8 || newPassword.length > 128) {
|
|
1505
|
+
return JSON.stringify({
|
|
1506
|
+
error: "new_password must be between 8 and 128 characters",
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
const result = await api("POST", "/api/auth/change-password", {
|
|
1510
|
+
currentPassword,
|
|
1511
|
+
newPassword,
|
|
1512
|
+
});
|
|
1513
|
+
return JSON.stringify(result);
|
|
1514
|
+
}
|
|
1515
|
+
// -----------------------------------------------------------------------
|
|
1516
|
+
// API Keys
|
|
1517
|
+
// -----------------------------------------------------------------------
|
|
1518
|
+
async function handleListKeys() {
|
|
1519
|
+
const result = await api("GET", "/api/keys");
|
|
1520
|
+
return JSON.stringify(result);
|
|
1521
|
+
}
|
|
1522
|
+
async function handleCreateKey(args) {
|
|
1523
|
+
const label = args.label;
|
|
1524
|
+
if (!label || typeof label !== "string") {
|
|
1525
|
+
return JSON.stringify({
|
|
1526
|
+
error: "label is required and must be a string",
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
if (label.length > 100) {
|
|
1530
|
+
return JSON.stringify({ error: "label must be 100 characters or fewer" });
|
|
1531
|
+
}
|
|
1532
|
+
if (/[\x00-\x1f\x7f]/.test(label)) {
|
|
1533
|
+
return JSON.stringify({
|
|
1534
|
+
error: "label contains invalid control characters",
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
const result = await api("POST", "/api/keys", { label });
|
|
1538
|
+
return JSON.stringify(result);
|
|
1539
|
+
}
|
|
1540
|
+
async function handleRevokeKey(args) {
|
|
1541
|
+
const keyId = args.key_id;
|
|
1542
|
+
if (!keyId || typeof keyId !== "string") {
|
|
1543
|
+
return JSON.stringify({
|
|
1544
|
+
error: "key_id is required and must be a string",
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(keyId)) {
|
|
1548
|
+
return JSON.stringify({ error: "key_id must be a valid UUID" });
|
|
1549
|
+
}
|
|
1550
|
+
const result = await api("DELETE", `/api/keys/${encodeURIComponent(keyId)}`);
|
|
1551
|
+
return JSON.stringify(result);
|
|
1552
|
+
}
|
|
1553
|
+
// -----------------------------------------------------------------------
|
|
1554
|
+
// Plans
|
|
1555
|
+
// -----------------------------------------------------------------------
|
|
1556
|
+
async function handleGetPlan() {
|
|
1557
|
+
const result = await api("GET", "/api/plans/user/plan");
|
|
1558
|
+
return JSON.stringify(result);
|
|
1559
|
+
}
|
|
1560
|
+
async function handleListPlans() {
|
|
1561
|
+
const result = await api("GET", "/api/plans");
|
|
1562
|
+
return JSON.stringify(result);
|
|
1563
|
+
}
|
|
1564
|
+
async function handleChangePlan(args) {
|
|
1565
|
+
const planId = args.plan_id;
|
|
1566
|
+
if (!planId || typeof planId !== "string") {
|
|
1567
|
+
return JSON.stringify({
|
|
1568
|
+
error: "plan_id is required and must be a string",
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(planId)) {
|
|
1572
|
+
return JSON.stringify({ error: "plan_id must be a valid UUID" });
|
|
1573
|
+
}
|
|
1574
|
+
const result = await api("PUT", "/api/plans/user/plan", { planId });
|
|
1575
|
+
return JSON.stringify(result);
|
|
1576
|
+
}
|
|
1577
|
+
// -----------------------------------------------------------------------
|
|
1578
|
+
// Teams extended
|
|
1579
|
+
// -----------------------------------------------------------------------
|
|
1580
|
+
async function handleTeamDelete(args) {
|
|
1581
|
+
const teamId = args.team_id;
|
|
1582
|
+
if (!teamId || typeof teamId !== "string") {
|
|
1583
|
+
return JSON.stringify({
|
|
1584
|
+
error: "team_id is required and must be a string",
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(teamId)) {
|
|
1588
|
+
return JSON.stringify({ error: "team_id must be a valid UUID" });
|
|
1589
|
+
}
|
|
1590
|
+
const result = await api("DELETE", `/api/teams/${encodeURIComponent(teamId)}`);
|
|
1591
|
+
return JSON.stringify(result);
|
|
1592
|
+
}
|
|
1593
|
+
async function handleTeamRevokeKey(args) {
|
|
1594
|
+
const teamId = args.team_id;
|
|
1595
|
+
const keyId = args.key_id;
|
|
1596
|
+
if (!teamId || typeof teamId !== "string") {
|
|
1597
|
+
return JSON.stringify({
|
|
1598
|
+
error: "team_id is required and must be a string",
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(teamId)) {
|
|
1602
|
+
return JSON.stringify({ error: "team_id must be a valid UUID" });
|
|
1603
|
+
}
|
|
1604
|
+
if (!keyId || typeof keyId !== "string") {
|
|
1605
|
+
return JSON.stringify({
|
|
1606
|
+
error: "key_id is required and must be a string",
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(keyId)) {
|
|
1610
|
+
return JSON.stringify({ error: "key_id must be a valid UUID" });
|
|
1611
|
+
}
|
|
1612
|
+
const result = await api("DELETE", `/api/teams/${encodeURIComponent(teamId)}/keys/${encodeURIComponent(keyId)}`);
|
|
1613
|
+
return JSON.stringify(result);
|
|
1614
|
+
}
|
|
1615
|
+
async function handleTeamListKeys(args) {
|
|
1616
|
+
const teamId = args.team_id;
|
|
1617
|
+
if (!teamId || typeof teamId !== "string") {
|
|
1618
|
+
return JSON.stringify({
|
|
1619
|
+
error: "team_id is required and must be a string",
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(teamId)) {
|
|
1623
|
+
return JSON.stringify({ error: "team_id must be a valid UUID" });
|
|
1624
|
+
}
|
|
1625
|
+
const result = await api("GET", `/api/teams/${encodeURIComponent(teamId)}/keys`);
|
|
1626
|
+
return JSON.stringify(result);
|
|
1627
|
+
}
|
|
1628
|
+
async function handleTeamListMembers(args) {
|
|
1629
|
+
const teamId = args.team_id;
|
|
1630
|
+
if (!teamId || typeof teamId !== "string") {
|
|
1631
|
+
return JSON.stringify({
|
|
1632
|
+
error: "team_id is required and must be a string",
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(teamId)) {
|
|
1636
|
+
return JSON.stringify({ error: "team_id must be a valid UUID" });
|
|
1637
|
+
}
|
|
1638
|
+
const result = await api("GET", `/api/teams/${encodeURIComponent(teamId)}/members`);
|
|
1639
|
+
return JSON.stringify(result);
|
|
1640
|
+
}
|
|
1641
|
+
async function handleTeamAddMember(args) {
|
|
1642
|
+
const teamId = args.team_id;
|
|
1643
|
+
const userId = args.user_id;
|
|
1644
|
+
if (!teamId || typeof teamId !== "string") {
|
|
1645
|
+
return JSON.stringify({
|
|
1646
|
+
error: "team_id is required and must be a string",
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(teamId)) {
|
|
1650
|
+
return JSON.stringify({ error: "team_id must be a valid UUID" });
|
|
1651
|
+
}
|
|
1652
|
+
if (!userId || typeof userId !== "string") {
|
|
1653
|
+
return JSON.stringify({
|
|
1654
|
+
error: "user_id is required and must be a string",
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(userId)) {
|
|
1658
|
+
return JSON.stringify({ error: "user_id must be a valid UUID" });
|
|
1659
|
+
}
|
|
1660
|
+
const result = await api("POST", `/api/teams/${encodeURIComponent(teamId)}/members`, { userId });
|
|
1661
|
+
return JSON.stringify(result);
|
|
1662
|
+
}
|
|
1663
|
+
async function handleTeamRemoveMember(args) {
|
|
1664
|
+
const teamId = args.team_id;
|
|
1665
|
+
const userId = args.user_id;
|
|
1666
|
+
if (!teamId || typeof teamId !== "string") {
|
|
1667
|
+
return JSON.stringify({
|
|
1668
|
+
error: "team_id is required and must be a string",
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(teamId)) {
|
|
1672
|
+
return JSON.stringify({ error: "team_id must be a valid UUID" });
|
|
1673
|
+
}
|
|
1674
|
+
if (!userId || typeof userId !== "string") {
|
|
1675
|
+
return JSON.stringify({
|
|
1676
|
+
error: "user_id is required and must be a string",
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(userId)) {
|
|
1680
|
+
return JSON.stringify({ error: "user_id must be a valid UUID" });
|
|
1681
|
+
}
|
|
1682
|
+
const result = await api("DELETE", `/api/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(userId)}`);
|
|
1683
|
+
return JSON.stringify(result);
|
|
1684
|
+
}
|
|
1685
|
+
async function handleTeamInviteMember(args) {
|
|
1686
|
+
const teamId = args.team_id;
|
|
1687
|
+
const email = args.email;
|
|
1688
|
+
const role = args.role;
|
|
1689
|
+
if (!teamId || typeof teamId !== "string") {
|
|
1690
|
+
return JSON.stringify({
|
|
1691
|
+
error: "team_id is required and must be a string",
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(teamId)) {
|
|
1695
|
+
return JSON.stringify({ error: "team_id must be a valid UUID" });
|
|
1696
|
+
}
|
|
1697
|
+
if (!email || typeof email !== "string") {
|
|
1698
|
+
return JSON.stringify({
|
|
1699
|
+
error: "email is required and must be a string",
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
if (!/^[^@]+@[^@]+\.[^@]+$/.test(email)) {
|
|
1703
|
+
return JSON.stringify({ error: "email must be a valid email address" });
|
|
1704
|
+
}
|
|
1705
|
+
if (!role || typeof role !== "string") {
|
|
1706
|
+
return JSON.stringify({ error: "role is required and must be a string" });
|
|
1707
|
+
}
|
|
1708
|
+
if (role !== "member" && role !== "admin") {
|
|
1709
|
+
return JSON.stringify({ error: "role must be 'member' or 'admin'" });
|
|
1710
|
+
}
|
|
1711
|
+
const result = await api("POST", `/api/teams/${encodeURIComponent(teamId)}/invites`, { email, role });
|
|
1712
|
+
return JSON.stringify(result);
|
|
1713
|
+
}
|
|
1714
|
+
async function handleTeamListInvites(args) {
|
|
1715
|
+
const teamId = args.team_id;
|
|
1716
|
+
if (!teamId || typeof teamId !== "string") {
|
|
1717
|
+
return JSON.stringify({
|
|
1718
|
+
error: "team_id is required and must be a string",
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(teamId)) {
|
|
1722
|
+
return JSON.stringify({ error: "team_id must be a valid UUID" });
|
|
1723
|
+
}
|
|
1724
|
+
const result = await api("GET", `/api/teams/${encodeURIComponent(teamId)}/invites`);
|
|
1725
|
+
return JSON.stringify(result);
|
|
1726
|
+
}
|
|
1727
|
+
async function handleTeamCancelInvite(args) {
|
|
1728
|
+
const teamId = args.team_id;
|
|
1729
|
+
const inviteId = args.invite_id;
|
|
1730
|
+
if (!teamId || typeof teamId !== "string") {
|
|
1731
|
+
return JSON.stringify({
|
|
1732
|
+
error: "team_id is required and must be a string",
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(teamId)) {
|
|
1736
|
+
return JSON.stringify({ error: "team_id must be a valid UUID" });
|
|
1737
|
+
}
|
|
1738
|
+
if (!inviteId || typeof inviteId !== "string") {
|
|
1739
|
+
return JSON.stringify({
|
|
1740
|
+
error: "invite_id is required and must be a string",
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(inviteId)) {
|
|
1744
|
+
return JSON.stringify({ error: "invite_id must be a valid UUID" });
|
|
1745
|
+
}
|
|
1746
|
+
const result = await api("DELETE", `/api/teams/${encodeURIComponent(teamId)}/invites/${encodeURIComponent(inviteId)}`);
|
|
1747
|
+
return JSON.stringify(result);
|
|
1748
|
+
}
|
|
1749
|
+
// -----------------------------------------------------------------------
|
|
1750
|
+
// Dispatch table
|
|
1751
|
+
// -----------------------------------------------------------------------
|
|
1752
|
+
const handlers = {
|
|
1753
|
+
// Proxy (3)
|
|
1754
|
+
dominusnode_proxied_fetch: handleProxiedFetch,
|
|
1755
|
+
dominusnode_get_proxy_config: handleGetProxyConfig,
|
|
1756
|
+
dominusnode_get_proxy_status: handleGetProxyStatus,
|
|
1757
|
+
// Wallet (7)
|
|
1758
|
+
dominusnode_check_balance: handleCheckBalance,
|
|
1759
|
+
dominusnode_get_transactions: handleGetTransactions,
|
|
1760
|
+
dominusnode_get_forecast: handleGetForecast,
|
|
1761
|
+
dominusnode_check_payment: handleCheckPayment,
|
|
1762
|
+
dominusnode_topup_paypal: handleTopupPaypal,
|
|
1763
|
+
dominusnode_topup_stripe: handleTopupStripe,
|
|
1764
|
+
dominusnode_topup_crypto: handleTopupCrypto,
|
|
1765
|
+
// Usage (3)
|
|
1766
|
+
dominusnode_check_usage: handleCheckUsage,
|
|
1767
|
+
dominusnode_get_daily_usage: handleGetDailyUsage,
|
|
1768
|
+
dominusnode_get_top_hosts: handleGetTopHosts,
|
|
1769
|
+
// Sessions (1)
|
|
1770
|
+
dominusnode_list_sessions: handleListSessions,
|
|
1771
|
+
// Account lifecycle (6)
|
|
1772
|
+
dominusnode_register: handleRegister,
|
|
1773
|
+
dominusnode_login: handleLogin,
|
|
1774
|
+
dominusnode_get_account_info: handleGetAccountInfo,
|
|
1775
|
+
dominusnode_verify_email: handleVerifyEmail,
|
|
1776
|
+
dominusnode_resend_verification: handleResendVerification,
|
|
1777
|
+
dominusnode_update_password: handleUpdatePassword,
|
|
1778
|
+
// API Keys (3)
|
|
1779
|
+
dominusnode_list_keys: handleListKeys,
|
|
1780
|
+
dominusnode_create_key: handleCreateKey,
|
|
1781
|
+
dominusnode_revoke_key: handleRevokeKey,
|
|
1782
|
+
// Plans (3)
|
|
1783
|
+
dominusnode_get_plan: handleGetPlan,
|
|
1784
|
+
dominusnode_list_plans: handleListPlans,
|
|
1785
|
+
dominusnode_change_plan: handleChangePlan,
|
|
1786
|
+
// Agentic wallets (7)
|
|
1787
|
+
dominusnode_create_agentic_wallet: handleCreateAgenticWallet,
|
|
1788
|
+
dominusnode_fund_agentic_wallet: handleFundAgenticWallet,
|
|
1789
|
+
dominusnode_agentic_wallet_balance: handleAgenticWalletBalance,
|
|
1790
|
+
dominusnode_list_agentic_wallets: handleListAgenticWallets,
|
|
1791
|
+
dominusnode_agentic_transactions: handleAgenticTransactions,
|
|
1792
|
+
dominusnode_freeze_agentic_wallet: handleFreezeAgenticWallet,
|
|
1793
|
+
dominusnode_unfreeze_agentic_wallet: handleUnfreezeAgenticWallet,
|
|
1794
|
+
dominusnode_delete_agentic_wallet: handleDeleteAgenticWallet,
|
|
1795
|
+
dominusnode_update_wallet_policy: handleUpdateWalletPolicy,
|
|
1796
|
+
// Teams (17)
|
|
1797
|
+
dominusnode_create_team: handleCreateTeam,
|
|
1798
|
+
dominusnode_list_teams: handleListTeams,
|
|
1799
|
+
dominusnode_team_details: handleTeamDetails,
|
|
1800
|
+
dominusnode_team_fund: handleTeamFund,
|
|
1801
|
+
dominusnode_team_create_key: handleTeamCreateKey,
|
|
1802
|
+
dominusnode_team_usage: handleTeamUsage,
|
|
1803
|
+
dominusnode_update_team: handleUpdateTeam,
|
|
1804
|
+
dominusnode_update_team_member_role: handleUpdateTeamMemberRole,
|
|
1805
|
+
dominusnode_team_delete: handleTeamDelete,
|
|
1806
|
+
dominusnode_team_revoke_key: handleTeamRevokeKey,
|
|
1807
|
+
dominusnode_team_list_keys: handleTeamListKeys,
|
|
1808
|
+
dominusnode_team_list_members: handleTeamListMembers,
|
|
1809
|
+
dominusnode_team_add_member: handleTeamAddMember,
|
|
1810
|
+
dominusnode_team_remove_member: handleTeamRemoveMember,
|
|
1811
|
+
dominusnode_team_invite_member: handleTeamInviteMember,
|
|
1812
|
+
dominusnode_team_list_invites: handleTeamListInvites,
|
|
1813
|
+
dominusnode_team_cancel_invite: handleTeamCancelInvite,
|
|
1814
|
+
// x402 (1)
|
|
1815
|
+
dominusnode_x402_info: handleX402Info,
|
|
1816
|
+
// MPP (4)
|
|
1817
|
+
dominusnode_mpp_info: handleMppInfo,
|
|
1818
|
+
dominusnode_pay_mpp: handlePayMpp,
|
|
1819
|
+
dominusnode_mpp_session_open: handleMppSessionOpen,
|
|
1820
|
+
dominusnode_mpp_session_close: handleMppSessionClose,
|
|
1821
|
+
};
|
|
1822
|
+
async function handleX402Info() {
|
|
1823
|
+
const result = await api("GET", "/api/x402/info");
|
|
1824
|
+
return JSON.stringify(result);
|
|
1825
|
+
}
|
|
1826
|
+
// -----------------------------------------------------------------------
|
|
1827
|
+
// MPP (Machine Payment Protocol) handlers
|
|
1828
|
+
// -----------------------------------------------------------------------
|
|
1829
|
+
async function handleMppInfo() {
|
|
1830
|
+
const result = await api("GET", "/api/mpp/info");
|
|
1831
|
+
return JSON.stringify(result);
|
|
1832
|
+
}
|
|
1833
|
+
async function handlePayMpp(args) {
|
|
1834
|
+
const amountCents = args.amount_cents;
|
|
1835
|
+
const method = String(args.method ?? "");
|
|
1836
|
+
if (!Number.isInteger(amountCents) ||
|
|
1837
|
+
amountCents < 500 ||
|
|
1838
|
+
amountCents > 100_000) {
|
|
1839
|
+
return JSON.stringify({
|
|
1840
|
+
error: "amount_cents must be an integer between 500 ($5) and 100,000 ($1,000)",
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
if (!["tempo", "stripe_spt", "lightning"].includes(method)) {
|
|
1844
|
+
return JSON.stringify({
|
|
1845
|
+
error: "method must be one of: tempo, stripe_spt, lightning",
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
const result = await api("POST", "/api/mpp/topup", { amountCents, method });
|
|
1849
|
+
return JSON.stringify(result);
|
|
1850
|
+
}
|
|
1851
|
+
async function handleMppSessionOpen(args) {
|
|
1852
|
+
const maxDepositCents = args.max_deposit_cents;
|
|
1853
|
+
const method = String(args.method ?? "");
|
|
1854
|
+
const poolType = String(args.pool_type ?? "dc");
|
|
1855
|
+
if (!Number.isInteger(maxDepositCents) ||
|
|
1856
|
+
maxDepositCents < 500 ||
|
|
1857
|
+
maxDepositCents > 100_000) {
|
|
1858
|
+
return JSON.stringify({
|
|
1859
|
+
error: "max_deposit_cents must be an integer between 500 and 100,000",
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
if (!["tempo", "stripe_spt", "lightning"].includes(method)) {
|
|
1863
|
+
return JSON.stringify({
|
|
1864
|
+
error: "method must be one of: tempo, stripe_spt, lightning",
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
if (!["dc", "residential"].includes(poolType)) {
|
|
1868
|
+
return JSON.stringify({
|
|
1869
|
+
error: "pool_type must be one of: dc, residential",
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
const result = await api("POST", "/api/mpp/session/open", {
|
|
1873
|
+
maxDepositCents,
|
|
1874
|
+
method,
|
|
1875
|
+
poolType,
|
|
1876
|
+
});
|
|
1877
|
+
return JSON.stringify(result);
|
|
1878
|
+
}
|
|
1879
|
+
async function handleMppSessionClose(args) {
|
|
1880
|
+
const channelId = String(args.channel_id ?? "");
|
|
1881
|
+
if (!channelId) {
|
|
1882
|
+
return JSON.stringify({ error: "channel_id is required" });
|
|
1883
|
+
}
|
|
1884
|
+
const result = await api("POST", "/api/mpp/session/close", { channelId });
|
|
1885
|
+
return JSON.stringify(result);
|
|
1886
|
+
}
|
|
1887
|
+
// -----------------------------------------------------------------------
|
|
1888
|
+
// Main handler
|
|
1889
|
+
// -----------------------------------------------------------------------
|
|
1890
|
+
return async function handler(name, args) {
|
|
1891
|
+
// Authenticate on first call
|
|
1892
|
+
await ensureAuth();
|
|
1893
|
+
const fn = handlers[name];
|
|
1894
|
+
if (!fn) {
|
|
1895
|
+
return JSON.stringify({
|
|
1896
|
+
error: `Unknown function: ${name}`,
|
|
1897
|
+
available: Object.keys(handlers),
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
try {
|
|
1901
|
+
return await fn(args);
|
|
1902
|
+
}
|
|
1903
|
+
catch (err) {
|
|
1904
|
+
if (err instanceof Error && err.message.includes("401")) {
|
|
1905
|
+
authToken = null;
|
|
1906
|
+
await ensureAuth();
|
|
1907
|
+
return await fn(args);
|
|
1908
|
+
}
|
|
1909
|
+
return JSON.stringify({
|
|
1910
|
+
error: sanitizeError(err instanceof Error ? err.message : String(err)),
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
//# sourceMappingURL=handler.js.map
|