dominusnode-dify 1.0.1__tar.gz → 1.2.0__tar.gz
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.
Potentially problematic release.
This version of dominusnode-dify might be problematic. Click here for more details.
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/PKG-INFO +1 -1
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/dominusnode_dify/__init__.py +3 -3
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/dominusnode_dify/tools.py +1062 -12
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/dominusnode_dify.egg-info/PKG-INFO +1 -1
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/pyproject.toml +1 -1
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/tests/test_tools.py +1 -1
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/LICENSE +0 -0
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/README.md +0 -0
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/dominusnode_dify.egg-info/SOURCES.txt +0 -0
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/dominusnode_dify.egg-info/dependency_links.txt +0 -0
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/dominusnode_dify.egg-info/requires.txt +0 -0
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/dominusnode_dify.egg-info/top_level.txt +0 -0
- {dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""Dominus Node Dify integration --
|
|
1
|
+
"""Dominus Node Dify integration -- 26 tools for the Dify AI platform.
|
|
2
2
|
|
|
3
|
-
Provides a Dify-compatible tool provider that exposes
|
|
3
|
+
Provides a Dify-compatible tool provider that exposes 26 tools for
|
|
4
4
|
interacting with the Dominus Node rotating proxy-as-a-service platform.
|
|
5
5
|
|
|
6
6
|
Tools cover:
|
|
@@ -10,7 +10,7 @@ Tools cover:
|
|
|
10
10
|
- Agentic wallet management (create, fund, freeze, unfreeze, delete)
|
|
11
11
|
- Team management (create, list, fund, keys, usage, update, role changes)
|
|
12
12
|
- x402 micropayment info
|
|
13
|
-
- PayPal top-up
|
|
13
|
+
- PayPal, Stripe, and crypto top-up
|
|
14
14
|
|
|
15
15
|
Security:
|
|
16
16
|
- Full SSRF prevention (private IP blocking, DNS rebinding, Teredo/6to4,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Dominus Node Dify tool provider --
|
|
1
|
+
"""Dominus Node Dify tool provider -- 53 tools with full security hardening.
|
|
2
2
|
|
|
3
3
|
This module implements the DominusNodeProvider class that Dify invokes.
|
|
4
4
|
Each tool is a method that receives credentials (from the provider YAML)
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import ipaddress
|
|
16
16
|
import json
|
|
17
|
+
import math
|
|
17
18
|
import os
|
|
18
19
|
import re
|
|
19
20
|
import socket
|
|
@@ -392,6 +393,8 @@ _DOMAIN_RE = re.compile(
|
|
|
392
393
|
r"^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$"
|
|
393
394
|
)
|
|
394
395
|
|
|
396
|
+
_BASIC_EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
|
|
397
|
+
|
|
395
398
|
|
|
396
399
|
# ---------------------------------------------------------------------------
|
|
397
400
|
# Period to date range helper
|
|
@@ -476,7 +479,7 @@ def _validate_allowed_domains(domains: Any) -> Optional[str]:
|
|
|
476
479
|
class DominusNodeProvider:
|
|
477
480
|
"""Dify tool provider for Dominus Node rotating proxy service.
|
|
478
481
|
|
|
479
|
-
Authenticates using a Dominus Node API key and exposes
|
|
482
|
+
Authenticates using a Dominus Node API key and exposes 53 tool methods that
|
|
480
483
|
Dify can invoke. Each tool method receives ``credentials`` and ``params``
|
|
481
484
|
dicts from the Dify runtime and returns a dict result.
|
|
482
485
|
|
|
@@ -502,6 +505,7 @@ class DominusNodeProvider:
|
|
|
502
505
|
self.proxy_host: str = creds.get("proxy_host", "") or os.environ.get("DOMINUSNODE_PROXY_HOST", "proxy.dominusnode.com")
|
|
503
506
|
self.proxy_port: int = int(os.environ.get("DOMINUSNODE_PROXY_PORT", "8080"))
|
|
504
507
|
self.timeout: float = 30.0
|
|
508
|
+
self.agent_secret: str | None = creds.get("agent_secret") or os.environ.get("DOMINUSNODE_AGENT_SECRET")
|
|
505
509
|
self._token: str | None = None
|
|
506
510
|
|
|
507
511
|
# ------------------------------------------------------------------
|
|
@@ -523,6 +527,14 @@ class DominusNodeProvider:
|
|
|
523
527
|
"or DOMINUSNODE_API_KEY environment variable."
|
|
524
528
|
)
|
|
525
529
|
|
|
530
|
+
auth_headers = {
|
|
531
|
+
"User-Agent": "dominusnode-dify/1.0.0",
|
|
532
|
+
"Content-Type": "application/json",
|
|
533
|
+
}
|
|
534
|
+
if self.agent_secret:
|
|
535
|
+
auth_headers["X-DominusNode-Agent"] = "mcp"
|
|
536
|
+
auth_headers["X-DominusNode-Agent-Secret"] = self.agent_secret
|
|
537
|
+
|
|
526
538
|
with httpx.Client(
|
|
527
539
|
timeout=self.timeout,
|
|
528
540
|
follow_redirects=False,
|
|
@@ -530,10 +542,7 @@ class DominusNodeProvider:
|
|
|
530
542
|
resp = client.post(
|
|
531
543
|
f"{self.base_url}/api/auth/verify-key",
|
|
532
544
|
json={"apiKey": self.api_key},
|
|
533
|
-
headers=
|
|
534
|
-
"User-Agent": "dominusnode-dify/1.0.0",
|
|
535
|
-
"Content-Type": "application/json",
|
|
536
|
-
},
|
|
545
|
+
headers=auth_headers,
|
|
537
546
|
)
|
|
538
547
|
|
|
539
548
|
if resp.status_code != 200:
|
|
@@ -584,12 +593,17 @@ class DominusNodeProvider:
|
|
|
584
593
|
timeout=self.timeout,
|
|
585
594
|
follow_redirects=False,
|
|
586
595
|
) as client:
|
|
596
|
+
req_headers = {
|
|
597
|
+
"User-Agent": "dominusnode-dify/1.0.0",
|
|
598
|
+
"Content-Type": "application/json",
|
|
599
|
+
"Authorization": f"Bearer {self._token}",
|
|
600
|
+
}
|
|
601
|
+
if self.agent_secret:
|
|
602
|
+
req_headers["X-DominusNode-Agent"] = "mcp"
|
|
603
|
+
req_headers["X-DominusNode-Agent-Secret"] = self.agent_secret
|
|
604
|
+
|
|
587
605
|
kwargs: Dict[str, Any] = {
|
|
588
|
-
"headers":
|
|
589
|
-
"User-Agent": "dominusnode-dify/1.0.0",
|
|
590
|
-
"Content-Type": "application/json",
|
|
591
|
-
"Authorization": f"Bearer {self._token}",
|
|
592
|
-
},
|
|
606
|
+
"headers": req_headers,
|
|
593
607
|
}
|
|
594
608
|
|
|
595
609
|
if body is not None and method.upper() not in ("GET", "HEAD", "OPTIONS"):
|
|
@@ -634,6 +648,69 @@ class DominusNodeProvider:
|
|
|
634
648
|
return self._api_request(method, path, body)
|
|
635
649
|
raise
|
|
636
650
|
|
|
651
|
+
# ------------------------------------------------------------------
|
|
652
|
+
# Unauthenticated request helper (register, login, verify_email)
|
|
653
|
+
# ------------------------------------------------------------------
|
|
654
|
+
|
|
655
|
+
def _unauthenticated_request(
|
|
656
|
+
self,
|
|
657
|
+
method: str,
|
|
658
|
+
path: str,
|
|
659
|
+
body: Optional[Dict[str, Any]] = None,
|
|
660
|
+
) -> Any:
|
|
661
|
+
"""Make an unauthenticated API request (no Bearer token).
|
|
662
|
+
|
|
663
|
+
Used for register, login, and verify_email endpoints.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
method: HTTP method (POST).
|
|
667
|
+
path: API path (e.g., ``/api/auth/register``).
|
|
668
|
+
body: Optional JSON body.
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
Parsed JSON response (with dangerous keys stripped).
|
|
672
|
+
|
|
673
|
+
Raises:
|
|
674
|
+
RuntimeError: On API errors or oversized responses.
|
|
675
|
+
"""
|
|
676
|
+
headers: Dict[str, str] = {
|
|
677
|
+
"User-Agent": "dominusnode-dify/1.0.0",
|
|
678
|
+
"Content-Type": "application/json",
|
|
679
|
+
}
|
|
680
|
+
if self.agent_secret:
|
|
681
|
+
headers["X-DominusNode-Agent"] = "mcp"
|
|
682
|
+
headers["X-DominusNode-Agent-Secret"] = self.agent_secret
|
|
683
|
+
|
|
684
|
+
with httpx.Client(
|
|
685
|
+
timeout=self.timeout,
|
|
686
|
+
follow_redirects=False,
|
|
687
|
+
) as client:
|
|
688
|
+
kwargs: Dict[str, Any] = {"headers": headers}
|
|
689
|
+
if body is not None:
|
|
690
|
+
kwargs["json"] = body
|
|
691
|
+
|
|
692
|
+
resp = client.request(method, f"{self.base_url}{path}", **kwargs)
|
|
693
|
+
|
|
694
|
+
if len(resp.content) > MAX_RESPONSE_BYTES:
|
|
695
|
+
raise RuntimeError("Response body too large")
|
|
696
|
+
|
|
697
|
+
if resp.status_code >= 400:
|
|
698
|
+
try:
|
|
699
|
+
err_data = resp.json()
|
|
700
|
+
msg = err_data.get("error", resp.text)
|
|
701
|
+
except Exception:
|
|
702
|
+
msg = resp.text
|
|
703
|
+
msg = str(msg)[:500]
|
|
704
|
+
raise RuntimeError(
|
|
705
|
+
f"API error {resp.status_code}: {_sanitize_error(msg)}"
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
if resp.text:
|
|
709
|
+
data = resp.json()
|
|
710
|
+
_strip_dangerous_keys(data)
|
|
711
|
+
return data
|
|
712
|
+
return {}
|
|
713
|
+
|
|
637
714
|
# ==================================================================
|
|
638
715
|
# Tool 1: proxied_fetch
|
|
639
716
|
# ==================================================================
|
|
@@ -1480,6 +1557,920 @@ class DominusNodeProvider:
|
|
|
1480
1557
|
except Exception as e:
|
|
1481
1558
|
return {"error": _sanitize_error(str(e))}
|
|
1482
1559
|
|
|
1560
|
+
# ==================================================================
|
|
1561
|
+
# Tool 24: topup_stripe
|
|
1562
|
+
# ==================================================================
|
|
1563
|
+
|
|
1564
|
+
def topup_stripe(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1565
|
+
"""Create a Stripe checkout session for wallet top-up.
|
|
1566
|
+
|
|
1567
|
+
Args:
|
|
1568
|
+
params: Dict with key: amount_cents (required, 500-100000).
|
|
1569
|
+
|
|
1570
|
+
Returns:
|
|
1571
|
+
Dict with checkout session details.
|
|
1572
|
+
"""
|
|
1573
|
+
amount_cents = params.get("amount_cents")
|
|
1574
|
+
if amount_cents is None:
|
|
1575
|
+
return {"error": "amount_cents is required"}
|
|
1576
|
+
|
|
1577
|
+
# Dify sends numbers as float; coerce to int
|
|
1578
|
+
if isinstance(amount_cents, float):
|
|
1579
|
+
if not math.isfinite(amount_cents):
|
|
1580
|
+
return {"error": "amount_cents must be an integer between 500 and 100000"}
|
|
1581
|
+
if amount_cents == int(amount_cents):
|
|
1582
|
+
amount_cents = int(amount_cents)
|
|
1583
|
+
|
|
1584
|
+
err = _validate_positive_int(
|
|
1585
|
+
amount_cents, "amount_cents", min_value=500, max_value=100_000
|
|
1586
|
+
)
|
|
1587
|
+
if err:
|
|
1588
|
+
return {"error": err}
|
|
1589
|
+
|
|
1590
|
+
try:
|
|
1591
|
+
result = self._request_with_retry(
|
|
1592
|
+
"POST",
|
|
1593
|
+
"/api/wallet/topup/stripe",
|
|
1594
|
+
{"amountCents": amount_cents},
|
|
1595
|
+
)
|
|
1596
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
1597
|
+
except Exception as e:
|
|
1598
|
+
return {"error": _sanitize_error(str(e))}
|
|
1599
|
+
|
|
1600
|
+
# ==================================================================
|
|
1601
|
+
# Tool 25: topup_crypto
|
|
1602
|
+
# ==================================================================
|
|
1603
|
+
|
|
1604
|
+
_VALID_CRYPTO_CURRENCIES = {
|
|
1605
|
+
"BTC", "ETH", "LTC", "XMR", "ZEC",
|
|
1606
|
+
"USDC", "SOL", "USDT", "DAI", "BNB", "LINK",
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
def topup_crypto(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1610
|
+
"""Create a crypto payment invoice for wallet top-up.
|
|
1611
|
+
|
|
1612
|
+
Args:
|
|
1613
|
+
params: Dict with keys: amount_usd (required, 5-1000),
|
|
1614
|
+
currency (required, one of BTC/ETH/LTC/XMR/ZEC/USDC/SOL/
|
|
1615
|
+
USDT/DAI/BNB/LINK).
|
|
1616
|
+
|
|
1617
|
+
Returns:
|
|
1618
|
+
Dict with invoice details for completing the crypto payment.
|
|
1619
|
+
"""
|
|
1620
|
+
amount_usd = params.get("amount_usd")
|
|
1621
|
+
currency = params.get("currency")
|
|
1622
|
+
|
|
1623
|
+
if amount_usd is None:
|
|
1624
|
+
return {"error": "amount_usd is required"}
|
|
1625
|
+
|
|
1626
|
+
# Dify sends numbers as float; keep as-is but validate
|
|
1627
|
+
if isinstance(amount_usd, bool) or not isinstance(amount_usd, (int, float)):
|
|
1628
|
+
return {"error": "amount_usd must be a number between 5 and 1,000"}
|
|
1629
|
+
if not math.isfinite(amount_usd):
|
|
1630
|
+
return {"error": "amount_usd must be a number between 5 and 1,000"}
|
|
1631
|
+
if amount_usd < 5 or amount_usd > 1000:
|
|
1632
|
+
return {"error": "amount_usd must be a number between 5 and 1,000"}
|
|
1633
|
+
|
|
1634
|
+
if not currency or not isinstance(currency, str):
|
|
1635
|
+
return {
|
|
1636
|
+
"error": "currency must be one of: BTC, ETH, LTC, XMR, ZEC, USDC, SOL, USDT, DAI, BNB, LINK",
|
|
1637
|
+
}
|
|
1638
|
+
if currency.upper() not in self._VALID_CRYPTO_CURRENCIES:
|
|
1639
|
+
return {
|
|
1640
|
+
"error": "currency must be one of: BTC, ETH, LTC, XMR, ZEC, USDC, SOL, USDT, DAI, BNB, LINK",
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
try:
|
|
1644
|
+
result = self._request_with_retry(
|
|
1645
|
+
"POST",
|
|
1646
|
+
"/api/wallet/topup/crypto",
|
|
1647
|
+
{"amountUsd": amount_usd, "currency": currency.lower()},
|
|
1648
|
+
)
|
|
1649
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
1650
|
+
except Exception as e:
|
|
1651
|
+
return {"error": _sanitize_error(str(e))}
|
|
1652
|
+
|
|
1653
|
+
# ==================================================================
|
|
1654
|
+
# Tool 26: get_proxy_status
|
|
1655
|
+
# ==================================================================
|
|
1656
|
+
|
|
1657
|
+
def get_proxy_status(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1658
|
+
"""Get the current status of the proxy gateway.
|
|
1659
|
+
|
|
1660
|
+
Args:
|
|
1661
|
+
params: Empty dict (no parameters needed).
|
|
1662
|
+
|
|
1663
|
+
Returns:
|
|
1664
|
+
Dict with proxy status information.
|
|
1665
|
+
"""
|
|
1666
|
+
try:
|
|
1667
|
+
result = self._request_with_retry("GET", "/api/proxy/status")
|
|
1668
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
1669
|
+
except Exception as e:
|
|
1670
|
+
return {"error": _sanitize_error(str(e))}
|
|
1671
|
+
|
|
1672
|
+
# ==================================================================
|
|
1673
|
+
# Tool 27: get_transactions
|
|
1674
|
+
# ==================================================================
|
|
1675
|
+
|
|
1676
|
+
def get_transactions(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1677
|
+
"""Get wallet transaction history.
|
|
1678
|
+
|
|
1679
|
+
Args:
|
|
1680
|
+
params: Dict with optional key: limit (1-100, default 20).
|
|
1681
|
+
|
|
1682
|
+
Returns:
|
|
1683
|
+
Dict with wallet transactions.
|
|
1684
|
+
"""
|
|
1685
|
+
limit = params.get("limit", 20)
|
|
1686
|
+
# Dify sends numbers as float; coerce to int
|
|
1687
|
+
if isinstance(limit, float) and limit == int(limit):
|
|
1688
|
+
limit = int(limit)
|
|
1689
|
+
|
|
1690
|
+
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 100:
|
|
1691
|
+
return {"error": "limit must be an integer between 1 and 100"}
|
|
1692
|
+
|
|
1693
|
+
qs = f"?limit={limit}"
|
|
1694
|
+
|
|
1695
|
+
try:
|
|
1696
|
+
result = self._request_with_retry("GET", f"/api/wallet/transactions{qs}")
|
|
1697
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
1698
|
+
except Exception as e:
|
|
1699
|
+
return {"error": _sanitize_error(str(e))}
|
|
1700
|
+
|
|
1701
|
+
# ==================================================================
|
|
1702
|
+
# Tool 28: get_forecast
|
|
1703
|
+
# ==================================================================
|
|
1704
|
+
|
|
1705
|
+
def get_forecast(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1706
|
+
"""Get wallet balance forecast (estimated days remaining, depletion date).
|
|
1707
|
+
|
|
1708
|
+
Args:
|
|
1709
|
+
params: Empty dict (no parameters needed).
|
|
1710
|
+
|
|
1711
|
+
Returns:
|
|
1712
|
+
Dict with forecast information.
|
|
1713
|
+
"""
|
|
1714
|
+
try:
|
|
1715
|
+
result = self._request_with_retry("GET", "/api/wallet/forecast")
|
|
1716
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
1717
|
+
except Exception as e:
|
|
1718
|
+
return {"error": _sanitize_error(str(e))}
|
|
1719
|
+
|
|
1720
|
+
# ==================================================================
|
|
1721
|
+
# Tool 29: check_payment
|
|
1722
|
+
# ==================================================================
|
|
1723
|
+
|
|
1724
|
+
def check_payment(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1725
|
+
"""Check the status of a crypto payment invoice.
|
|
1726
|
+
|
|
1727
|
+
Args:
|
|
1728
|
+
params: Dict with key: invoice_id (required).
|
|
1729
|
+
|
|
1730
|
+
Returns:
|
|
1731
|
+
Dict with payment status information.
|
|
1732
|
+
"""
|
|
1733
|
+
invoice_id = params.get("invoice_id")
|
|
1734
|
+
if not invoice_id or not isinstance(invoice_id, str):
|
|
1735
|
+
return {"error": "invoice_id is required and must be a string"}
|
|
1736
|
+
|
|
1737
|
+
invoice_id = invoice_id.strip()
|
|
1738
|
+
if not invoice_id:
|
|
1739
|
+
return {"error": "invoice_id is required and must be a string"}
|
|
1740
|
+
|
|
1741
|
+
if any(0 <= ord(c) <= 0x1F or ord(c) == 0x7F for c in invoice_id):
|
|
1742
|
+
return {"error": "invoice_id contains invalid control characters"}
|
|
1743
|
+
|
|
1744
|
+
try:
|
|
1745
|
+
result = self._request_with_retry(
|
|
1746
|
+
"GET",
|
|
1747
|
+
f"/api/wallet/topup/crypto/{quote(invoice_id, safe='')}/status",
|
|
1748
|
+
)
|
|
1749
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
1750
|
+
except Exception as e:
|
|
1751
|
+
return {"error": _sanitize_error(str(e))}
|
|
1752
|
+
|
|
1753
|
+
# ==================================================================
|
|
1754
|
+
# Tool 30: get_daily_usage
|
|
1755
|
+
# ==================================================================
|
|
1756
|
+
|
|
1757
|
+
def get_daily_usage(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1758
|
+
"""Get daily bandwidth usage statistics.
|
|
1759
|
+
|
|
1760
|
+
Args:
|
|
1761
|
+
params: Dict with optional key: days (1-365, default 7).
|
|
1762
|
+
|
|
1763
|
+
Returns:
|
|
1764
|
+
Dict with daily usage breakdown.
|
|
1765
|
+
"""
|
|
1766
|
+
days = params.get("days", 7)
|
|
1767
|
+
# Dify sends numbers as float; coerce to int
|
|
1768
|
+
if isinstance(days, float) and days == int(days):
|
|
1769
|
+
days = int(days)
|
|
1770
|
+
|
|
1771
|
+
if not isinstance(days, int) or isinstance(days, bool) or days < 1 or days > 365:
|
|
1772
|
+
return {"error": "days must be an integer between 1 and 365"}
|
|
1773
|
+
|
|
1774
|
+
try:
|
|
1775
|
+
result = self._request_with_retry("GET", f"/api/usage/daily?days={days}")
|
|
1776
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
1777
|
+
except Exception as e:
|
|
1778
|
+
return {"error": _sanitize_error(str(e))}
|
|
1779
|
+
|
|
1780
|
+
# ==================================================================
|
|
1781
|
+
# Tool 31: get_top_hosts
|
|
1782
|
+
# ==================================================================
|
|
1783
|
+
|
|
1784
|
+
def get_top_hosts(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1785
|
+
"""Get top hosts by bandwidth usage.
|
|
1786
|
+
|
|
1787
|
+
Args:
|
|
1788
|
+
params: Dict with optional key: limit (1-100, default 10).
|
|
1789
|
+
|
|
1790
|
+
Returns:
|
|
1791
|
+
Dict with top hosts ranked by bandwidth.
|
|
1792
|
+
"""
|
|
1793
|
+
limit = params.get("limit", 10)
|
|
1794
|
+
# Dify sends numbers as float; coerce to int
|
|
1795
|
+
if isinstance(limit, float) and limit == int(limit):
|
|
1796
|
+
limit = int(limit)
|
|
1797
|
+
|
|
1798
|
+
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 100:
|
|
1799
|
+
return {"error": "limit must be an integer between 1 and 100"}
|
|
1800
|
+
|
|
1801
|
+
try:
|
|
1802
|
+
result = self._request_with_retry("GET", f"/api/usage/top-hosts?limit={limit}")
|
|
1803
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
1804
|
+
except Exception as e:
|
|
1805
|
+
return {"error": _sanitize_error(str(e))}
|
|
1806
|
+
|
|
1807
|
+
# ==================================================================
|
|
1808
|
+
# Tool 32: register (unauthenticated)
|
|
1809
|
+
# ==================================================================
|
|
1810
|
+
|
|
1811
|
+
def register(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1812
|
+
"""Register a new Dominus Node account.
|
|
1813
|
+
|
|
1814
|
+
This is an unauthenticated endpoint -- no API key or token needed.
|
|
1815
|
+
|
|
1816
|
+
Args:
|
|
1817
|
+
params: Dict with keys: email (required), password (required, 8-128 chars).
|
|
1818
|
+
|
|
1819
|
+
Returns:
|
|
1820
|
+
Dict with registration result.
|
|
1821
|
+
"""
|
|
1822
|
+
email = params.get("email")
|
|
1823
|
+
password = params.get("password")
|
|
1824
|
+
|
|
1825
|
+
if not email or not isinstance(email, str):
|
|
1826
|
+
return {"error": "email is required and must be a string"}
|
|
1827
|
+
if not _BASIC_EMAIL_RE.match(email):
|
|
1828
|
+
return {"error": "email must be a valid email address"}
|
|
1829
|
+
if len(email) > 254:
|
|
1830
|
+
return {"error": "email must be 254 characters or fewer"}
|
|
1831
|
+
|
|
1832
|
+
if not password or not isinstance(password, str):
|
|
1833
|
+
return {"error": "password is required and must be a string"}
|
|
1834
|
+
if len(password) < 8:
|
|
1835
|
+
return {"error": "password must be at least 8 characters"}
|
|
1836
|
+
if len(password) > 128:
|
|
1837
|
+
return {"error": "password must be 128 characters or fewer"}
|
|
1838
|
+
|
|
1839
|
+
try:
|
|
1840
|
+
result = self._unauthenticated_request(
|
|
1841
|
+
"POST",
|
|
1842
|
+
"/api/auth/register",
|
|
1843
|
+
{"email": email, "password": password},
|
|
1844
|
+
)
|
|
1845
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
1846
|
+
except Exception as e:
|
|
1847
|
+
return {"error": _sanitize_error(str(e))}
|
|
1848
|
+
|
|
1849
|
+
# ==================================================================
|
|
1850
|
+
# Tool 33: login (unauthenticated)
|
|
1851
|
+
# ==================================================================
|
|
1852
|
+
|
|
1853
|
+
def login(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1854
|
+
"""Log in to a Dominus Node account.
|
|
1855
|
+
|
|
1856
|
+
This is an unauthenticated endpoint -- no API key or token needed.
|
|
1857
|
+
Detects MFA challenges and surfaces them to the caller.
|
|
1858
|
+
|
|
1859
|
+
Args:
|
|
1860
|
+
params: Dict with keys: email (required), password (required).
|
|
1861
|
+
|
|
1862
|
+
Returns:
|
|
1863
|
+
Dict with login result (tokens or MFA challenge).
|
|
1864
|
+
"""
|
|
1865
|
+
email = params.get("email")
|
|
1866
|
+
password = params.get("password")
|
|
1867
|
+
|
|
1868
|
+
if not email or not isinstance(email, str):
|
|
1869
|
+
return {"error": "email is required and must be a string"}
|
|
1870
|
+
if not _BASIC_EMAIL_RE.match(email):
|
|
1871
|
+
return {"error": "email must be a valid email address"}
|
|
1872
|
+
if len(email) > 254:
|
|
1873
|
+
return {"error": "email must be 254 characters or fewer"}
|
|
1874
|
+
|
|
1875
|
+
if not password or not isinstance(password, str):
|
|
1876
|
+
return {"error": "password is required and must be a string"}
|
|
1877
|
+
if len(password) < 1:
|
|
1878
|
+
return {"error": "password is required"}
|
|
1879
|
+
if len(password) > 128:
|
|
1880
|
+
return {"error": "password must be 128 characters or fewer"}
|
|
1881
|
+
|
|
1882
|
+
try:
|
|
1883
|
+
result = self._unauthenticated_request(
|
|
1884
|
+
"POST",
|
|
1885
|
+
"/api/auth/login",
|
|
1886
|
+
{"email": email, "password": password},
|
|
1887
|
+
)
|
|
1888
|
+
if isinstance(result, dict) and result.get("mfaRequired"):
|
|
1889
|
+
return {
|
|
1890
|
+
"mfaRequired": True,
|
|
1891
|
+
"challengeToken": result.get("challengeToken"),
|
|
1892
|
+
"message": "Multi-factor authentication is required. Complete the MFA challenge to proceed.",
|
|
1893
|
+
}
|
|
1894
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
1895
|
+
except Exception as e:
|
|
1896
|
+
return {"error": _sanitize_error(str(e))}
|
|
1897
|
+
|
|
1898
|
+
# ==================================================================
|
|
1899
|
+
# Tool 34: get_account_info
|
|
1900
|
+
# ==================================================================
|
|
1901
|
+
|
|
1902
|
+
def get_account_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1903
|
+
"""Get current account information.
|
|
1904
|
+
|
|
1905
|
+
Args:
|
|
1906
|
+
params: Empty dict (no parameters needed).
|
|
1907
|
+
|
|
1908
|
+
Returns:
|
|
1909
|
+
Dict with account details (id, email, plan, status, etc.).
|
|
1910
|
+
"""
|
|
1911
|
+
try:
|
|
1912
|
+
result = self._request_with_retry("GET", "/api/auth/me")
|
|
1913
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
1914
|
+
except Exception as e:
|
|
1915
|
+
return {"error": _sanitize_error(str(e))}
|
|
1916
|
+
|
|
1917
|
+
# ==================================================================
|
|
1918
|
+
# Tool 35: verify_email (unauthenticated)
|
|
1919
|
+
# ==================================================================
|
|
1920
|
+
|
|
1921
|
+
def verify_email(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1922
|
+
"""Verify an email address using a verification token.
|
|
1923
|
+
|
|
1924
|
+
This is an unauthenticated endpoint.
|
|
1925
|
+
|
|
1926
|
+
Args:
|
|
1927
|
+
params: Dict with key: token (required).
|
|
1928
|
+
|
|
1929
|
+
Returns:
|
|
1930
|
+
Dict confirming email verification.
|
|
1931
|
+
"""
|
|
1932
|
+
token = params.get("token")
|
|
1933
|
+
if not token or not isinstance(token, str):
|
|
1934
|
+
return {"error": "token is required and must be a string"}
|
|
1935
|
+
|
|
1936
|
+
token = token.strip()
|
|
1937
|
+
if not token:
|
|
1938
|
+
return {"error": "token is required and must be a string"}
|
|
1939
|
+
|
|
1940
|
+
if any(0 <= ord(c) <= 0x1F or ord(c) == 0x7F for c in token):
|
|
1941
|
+
return {"error": "token contains invalid control characters"}
|
|
1942
|
+
|
|
1943
|
+
try:
|
|
1944
|
+
result = self._unauthenticated_request(
|
|
1945
|
+
"POST",
|
|
1946
|
+
"/api/auth/verify-email",
|
|
1947
|
+
{"token": token},
|
|
1948
|
+
)
|
|
1949
|
+
return result if isinstance(result, dict) else {"message": "Email verified successfully"}
|
|
1950
|
+
except Exception as e:
|
|
1951
|
+
return {"error": _sanitize_error(str(e))}
|
|
1952
|
+
|
|
1953
|
+
# ==================================================================
|
|
1954
|
+
# Tool 36: resend_verification
|
|
1955
|
+
# ==================================================================
|
|
1956
|
+
|
|
1957
|
+
def resend_verification(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1958
|
+
"""Resend the email verification message.
|
|
1959
|
+
|
|
1960
|
+
Args:
|
|
1961
|
+
params: Empty dict (no parameters needed).
|
|
1962
|
+
|
|
1963
|
+
Returns:
|
|
1964
|
+
Dict confirming the verification email was sent.
|
|
1965
|
+
"""
|
|
1966
|
+
try:
|
|
1967
|
+
result = self._request_with_retry("POST", "/api/auth/resend-verification")
|
|
1968
|
+
return result if isinstance(result, dict) else {"message": "Verification email sent"}
|
|
1969
|
+
except Exception as e:
|
|
1970
|
+
return {"error": _sanitize_error(str(e))}
|
|
1971
|
+
|
|
1972
|
+
# ==================================================================
|
|
1973
|
+
# Tool 37: update_password
|
|
1974
|
+
# ==================================================================
|
|
1975
|
+
|
|
1976
|
+
def update_password(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1977
|
+
"""Update the current account password.
|
|
1978
|
+
|
|
1979
|
+
Args:
|
|
1980
|
+
params: Dict with keys: current_password (required),
|
|
1981
|
+
new_password (required, 8-128 chars).
|
|
1982
|
+
|
|
1983
|
+
Returns:
|
|
1984
|
+
Dict confirming the password was updated.
|
|
1985
|
+
"""
|
|
1986
|
+
current_password = params.get("current_password")
|
|
1987
|
+
new_password = params.get("new_password")
|
|
1988
|
+
|
|
1989
|
+
if not current_password or not isinstance(current_password, str):
|
|
1990
|
+
return {"error": "current_password is required and must be a string"}
|
|
1991
|
+
if len(current_password) < 1:
|
|
1992
|
+
return {"error": "current_password is required"}
|
|
1993
|
+
|
|
1994
|
+
if not new_password or not isinstance(new_password, str):
|
|
1995
|
+
return {"error": "new_password is required and must be a string"}
|
|
1996
|
+
if len(new_password) < 8:
|
|
1997
|
+
return {"error": "new_password must be at least 8 characters"}
|
|
1998
|
+
if len(new_password) > 128:
|
|
1999
|
+
return {"error": "new_password must be 128 characters or fewer"}
|
|
2000
|
+
|
|
2001
|
+
try:
|
|
2002
|
+
result = self._request_with_retry(
|
|
2003
|
+
"POST",
|
|
2004
|
+
"/api/auth/change-password",
|
|
2005
|
+
{"currentPassword": current_password, "newPassword": new_password},
|
|
2006
|
+
)
|
|
2007
|
+
return result if isinstance(result, dict) else {"message": "Password updated successfully"}
|
|
2008
|
+
except Exception as e:
|
|
2009
|
+
return {"error": _sanitize_error(str(e))}
|
|
2010
|
+
|
|
2011
|
+
# ==================================================================
|
|
2012
|
+
# Tool 38: list_keys
|
|
2013
|
+
# ==================================================================
|
|
2014
|
+
|
|
2015
|
+
def list_keys(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2016
|
+
"""List all API keys for the current account.
|
|
2017
|
+
|
|
2018
|
+
Args:
|
|
2019
|
+
params: Empty dict (no parameters needed).
|
|
2020
|
+
|
|
2021
|
+
Returns:
|
|
2022
|
+
Dict with list of API keys.
|
|
2023
|
+
"""
|
|
2024
|
+
try:
|
|
2025
|
+
result = self._request_with_retry("GET", "/api/keys")
|
|
2026
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
2027
|
+
except Exception as e:
|
|
2028
|
+
return {"error": _sanitize_error(str(e))}
|
|
2029
|
+
|
|
2030
|
+
# ==================================================================
|
|
2031
|
+
# Tool 39: create_key
|
|
2032
|
+
# ==================================================================
|
|
2033
|
+
|
|
2034
|
+
def create_key(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2035
|
+
"""Create a new API key for the current account.
|
|
2036
|
+
|
|
2037
|
+
Args:
|
|
2038
|
+
params: Dict with optional key: label (string, 1-100 chars).
|
|
2039
|
+
|
|
2040
|
+
Returns:
|
|
2041
|
+
Dict with the created key details (key shown once).
|
|
2042
|
+
"""
|
|
2043
|
+
label = params.get("label")
|
|
2044
|
+
|
|
2045
|
+
body: Dict[str, Any] = {}
|
|
2046
|
+
if label is not None:
|
|
2047
|
+
if not isinstance(label, str):
|
|
2048
|
+
return {"error": "label must be a string"}
|
|
2049
|
+
err = _validate_label(label, "label")
|
|
2050
|
+
if err:
|
|
2051
|
+
return {"error": err}
|
|
2052
|
+
body["label"] = label
|
|
2053
|
+
|
|
2054
|
+
try:
|
|
2055
|
+
result = self._request_with_retry("POST", "/api/keys", body)
|
|
2056
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
2057
|
+
except Exception as e:
|
|
2058
|
+
return {"error": _sanitize_error(str(e))}
|
|
2059
|
+
|
|
2060
|
+
# ==================================================================
|
|
2061
|
+
# Tool 40: revoke_key
|
|
2062
|
+
# ==================================================================
|
|
2063
|
+
|
|
2064
|
+
def revoke_key(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2065
|
+
"""Revoke (delete) an API key by its ID.
|
|
2066
|
+
|
|
2067
|
+
Args:
|
|
2068
|
+
params: Dict with key: key_id (required, UUID).
|
|
2069
|
+
|
|
2070
|
+
Returns:
|
|
2071
|
+
Dict confirming the key was revoked.
|
|
2072
|
+
"""
|
|
2073
|
+
key_id = params.get("key_id")
|
|
2074
|
+
err = _validate_uuid(key_id, "key_id")
|
|
2075
|
+
if err:
|
|
2076
|
+
return {"error": err}
|
|
2077
|
+
|
|
2078
|
+
try:
|
|
2079
|
+
result = self._request_with_retry(
|
|
2080
|
+
"DELETE",
|
|
2081
|
+
f"/api/keys/{quote(key_id, safe='')}",
|
|
2082
|
+
)
|
|
2083
|
+
return result if isinstance(result, dict) else {"message": "API key revoked"}
|
|
2084
|
+
except Exception as e:
|
|
2085
|
+
return {"error": _sanitize_error(str(e))}
|
|
2086
|
+
|
|
2087
|
+
# ==================================================================
|
|
2088
|
+
# Tool 41: get_plan
|
|
2089
|
+
# ==================================================================
|
|
2090
|
+
|
|
2091
|
+
def get_plan(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2092
|
+
"""Get the current user's plan details.
|
|
2093
|
+
|
|
2094
|
+
Args:
|
|
2095
|
+
params: Empty dict (no parameters needed).
|
|
2096
|
+
|
|
2097
|
+
Returns:
|
|
2098
|
+
Dict with the current plan information.
|
|
2099
|
+
"""
|
|
2100
|
+
try:
|
|
2101
|
+
result = self._request_with_retry("GET", "/api/plans/user/plan")
|
|
2102
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
2103
|
+
except Exception as e:
|
|
2104
|
+
return {"error": _sanitize_error(str(e))}
|
|
2105
|
+
|
|
2106
|
+
# ==================================================================
|
|
2107
|
+
# Tool 42: list_plans
|
|
2108
|
+
# ==================================================================
|
|
2109
|
+
|
|
2110
|
+
def list_plans(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2111
|
+
"""List all available plans.
|
|
2112
|
+
|
|
2113
|
+
Args:
|
|
2114
|
+
params: Empty dict (no parameters needed).
|
|
2115
|
+
|
|
2116
|
+
Returns:
|
|
2117
|
+
Dict with list of available plans.
|
|
2118
|
+
"""
|
|
2119
|
+
try:
|
|
2120
|
+
result = self._request_with_retry("GET", "/api/plans")
|
|
2121
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
2122
|
+
except Exception as e:
|
|
2123
|
+
return {"error": _sanitize_error(str(e))}
|
|
2124
|
+
|
|
2125
|
+
# ==================================================================
|
|
2126
|
+
# Tool 43: change_plan
|
|
2127
|
+
# ==================================================================
|
|
2128
|
+
|
|
2129
|
+
def change_plan(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2130
|
+
"""Change the current user's plan.
|
|
2131
|
+
|
|
2132
|
+
Args:
|
|
2133
|
+
params: Dict with key: plan_id (required).
|
|
2134
|
+
|
|
2135
|
+
Returns:
|
|
2136
|
+
Dict confirming the plan change.
|
|
2137
|
+
"""
|
|
2138
|
+
plan_id = params.get("plan_id")
|
|
2139
|
+
if not plan_id or not isinstance(plan_id, str):
|
|
2140
|
+
return {"error": "plan_id is required and must be a string"}
|
|
2141
|
+
|
|
2142
|
+
plan_id = plan_id.strip()
|
|
2143
|
+
if not plan_id:
|
|
2144
|
+
return {"error": "plan_id is required and must be a string"}
|
|
2145
|
+
|
|
2146
|
+
if any(0 <= ord(c) <= 0x1F or ord(c) == 0x7F for c in plan_id):
|
|
2147
|
+
return {"error": "plan_id contains invalid control characters"}
|
|
2148
|
+
|
|
2149
|
+
try:
|
|
2150
|
+
result = self._request_with_retry(
|
|
2151
|
+
"PUT",
|
|
2152
|
+
"/api/plans/user/plan",
|
|
2153
|
+
{"planId": plan_id},
|
|
2154
|
+
)
|
|
2155
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
2156
|
+
except Exception as e:
|
|
2157
|
+
return {"error": _sanitize_error(str(e))}
|
|
2158
|
+
|
|
2159
|
+
# ==================================================================
|
|
2160
|
+
# Tool 44: team_delete
|
|
2161
|
+
# ==================================================================
|
|
2162
|
+
|
|
2163
|
+
def team_delete(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2164
|
+
"""Delete a team permanently.
|
|
2165
|
+
|
|
2166
|
+
Args:
|
|
2167
|
+
params: Dict with key: team_id (required, UUID).
|
|
2168
|
+
|
|
2169
|
+
Returns:
|
|
2170
|
+
Dict confirming the team was deleted.
|
|
2171
|
+
"""
|
|
2172
|
+
team_id = params.get("team_id")
|
|
2173
|
+
err = _validate_uuid(team_id, "team_id")
|
|
2174
|
+
if err:
|
|
2175
|
+
return {"error": err}
|
|
2176
|
+
|
|
2177
|
+
try:
|
|
2178
|
+
result = self._request_with_retry(
|
|
2179
|
+
"DELETE",
|
|
2180
|
+
f"/api/teams/{quote(team_id, safe='')}",
|
|
2181
|
+
)
|
|
2182
|
+
return result if isinstance(result, dict) else {"message": "Team deleted"}
|
|
2183
|
+
except Exception as e:
|
|
2184
|
+
return {"error": _sanitize_error(str(e))}
|
|
2185
|
+
|
|
2186
|
+
# ==================================================================
|
|
2187
|
+
# Tool 45: team_revoke_key
|
|
2188
|
+
# ==================================================================
|
|
2189
|
+
|
|
2190
|
+
def team_revoke_key(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2191
|
+
"""Revoke (delete) an API key from a team.
|
|
2192
|
+
|
|
2193
|
+
Args:
|
|
2194
|
+
params: Dict with keys: team_id (required, UUID),
|
|
2195
|
+
key_id (required, UUID).
|
|
2196
|
+
|
|
2197
|
+
Returns:
|
|
2198
|
+
Dict confirming the team key was revoked.
|
|
2199
|
+
"""
|
|
2200
|
+
team_id = params.get("team_id")
|
|
2201
|
+
key_id = params.get("key_id")
|
|
2202
|
+
|
|
2203
|
+
if team_id is None or key_id is None:
|
|
2204
|
+
return {"error": "team_id and key_id are required"}
|
|
2205
|
+
|
|
2206
|
+
err = _validate_uuid(team_id, "team_id")
|
|
2207
|
+
if err:
|
|
2208
|
+
return {"error": err}
|
|
2209
|
+
|
|
2210
|
+
err = _validate_uuid(key_id, "key_id")
|
|
2211
|
+
if err:
|
|
2212
|
+
return {"error": err}
|
|
2213
|
+
|
|
2214
|
+
try:
|
|
2215
|
+
result = self._request_with_retry(
|
|
2216
|
+
"DELETE",
|
|
2217
|
+
f"/api/teams/{quote(team_id, safe='')}/keys/{quote(key_id, safe='')}",
|
|
2218
|
+
)
|
|
2219
|
+
return result if isinstance(result, dict) else {"message": "Team API key revoked"}
|
|
2220
|
+
except Exception as e:
|
|
2221
|
+
return {"error": _sanitize_error(str(e))}
|
|
2222
|
+
|
|
2223
|
+
# ==================================================================
|
|
2224
|
+
# Tool 46: team_list_keys
|
|
2225
|
+
# ==================================================================
|
|
2226
|
+
|
|
2227
|
+
def team_list_keys(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2228
|
+
"""List all API keys for a team.
|
|
2229
|
+
|
|
2230
|
+
Args:
|
|
2231
|
+
params: Dict with key: team_id (required, UUID).
|
|
2232
|
+
|
|
2233
|
+
Returns:
|
|
2234
|
+
Dict with list of team API keys.
|
|
2235
|
+
"""
|
|
2236
|
+
team_id = params.get("team_id")
|
|
2237
|
+
err = _validate_uuid(team_id, "team_id")
|
|
2238
|
+
if err:
|
|
2239
|
+
return {"error": err}
|
|
2240
|
+
|
|
2241
|
+
try:
|
|
2242
|
+
result = self._request_with_retry(
|
|
2243
|
+
"GET",
|
|
2244
|
+
f"/api/teams/{quote(team_id, safe='')}/keys",
|
|
2245
|
+
)
|
|
2246
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
2247
|
+
except Exception as e:
|
|
2248
|
+
return {"error": _sanitize_error(str(e))}
|
|
2249
|
+
|
|
2250
|
+
# ==================================================================
|
|
2251
|
+
# Tool 47: team_list_members
|
|
2252
|
+
# ==================================================================
|
|
2253
|
+
|
|
2254
|
+
def team_list_members(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2255
|
+
"""List all members of a team.
|
|
2256
|
+
|
|
2257
|
+
Args:
|
|
2258
|
+
params: Dict with key: team_id (required, UUID).
|
|
2259
|
+
|
|
2260
|
+
Returns:
|
|
2261
|
+
Dict with list of team members.
|
|
2262
|
+
"""
|
|
2263
|
+
team_id = params.get("team_id")
|
|
2264
|
+
err = _validate_uuid(team_id, "team_id")
|
|
2265
|
+
if err:
|
|
2266
|
+
return {"error": err}
|
|
2267
|
+
|
|
2268
|
+
try:
|
|
2269
|
+
result = self._request_with_retry(
|
|
2270
|
+
"GET",
|
|
2271
|
+
f"/api/teams/{quote(team_id, safe='')}/members",
|
|
2272
|
+
)
|
|
2273
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
2274
|
+
except Exception as e:
|
|
2275
|
+
return {"error": _sanitize_error(str(e))}
|
|
2276
|
+
|
|
2277
|
+
# ==================================================================
|
|
2278
|
+
# Tool 48: team_add_member
|
|
2279
|
+
# ==================================================================
|
|
2280
|
+
|
|
2281
|
+
def team_add_member(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2282
|
+
"""Add a member to a team by email.
|
|
2283
|
+
|
|
2284
|
+
Args:
|
|
2285
|
+
params: Dict with keys: team_id (required, UUID),
|
|
2286
|
+
email (required), role (optional, 'member' or 'admin').
|
|
2287
|
+
|
|
2288
|
+
Returns:
|
|
2289
|
+
Dict with the added member details.
|
|
2290
|
+
"""
|
|
2291
|
+
team_id = params.get("team_id")
|
|
2292
|
+
email = params.get("email")
|
|
2293
|
+
role = params.get("role")
|
|
2294
|
+
|
|
2295
|
+
if team_id is None or email is None:
|
|
2296
|
+
return {"error": "team_id and email are required"}
|
|
2297
|
+
|
|
2298
|
+
err = _validate_uuid(team_id, "team_id")
|
|
2299
|
+
if err:
|
|
2300
|
+
return {"error": err}
|
|
2301
|
+
|
|
2302
|
+
if not email or not isinstance(email, str):
|
|
2303
|
+
return {"error": "email is required and must be a string"}
|
|
2304
|
+
if not _BASIC_EMAIL_RE.match(email):
|
|
2305
|
+
return {"error": "email must be a valid email address"}
|
|
2306
|
+
|
|
2307
|
+
if role is not None:
|
|
2308
|
+
if not isinstance(role, str) or role not in ("member", "admin"):
|
|
2309
|
+
return {"error": "role must be 'member' or 'admin'"}
|
|
2310
|
+
|
|
2311
|
+
body: Dict[str, Any] = {"email": email}
|
|
2312
|
+
if role is not None:
|
|
2313
|
+
body["role"] = role
|
|
2314
|
+
|
|
2315
|
+
try:
|
|
2316
|
+
result = self._request_with_retry(
|
|
2317
|
+
"POST",
|
|
2318
|
+
f"/api/teams/{quote(team_id, safe='')}/members",
|
|
2319
|
+
body,
|
|
2320
|
+
)
|
|
2321
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
2322
|
+
except Exception as e:
|
|
2323
|
+
return {"error": _sanitize_error(str(e))}
|
|
2324
|
+
|
|
2325
|
+
# ==================================================================
|
|
2326
|
+
# Tool 49: team_remove_member
|
|
2327
|
+
# ==================================================================
|
|
2328
|
+
|
|
2329
|
+
def team_remove_member(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2330
|
+
"""Remove a member from a team.
|
|
2331
|
+
|
|
2332
|
+
Args:
|
|
2333
|
+
params: Dict with keys: team_id (required, UUID),
|
|
2334
|
+
user_id (required, UUID).
|
|
2335
|
+
|
|
2336
|
+
Returns:
|
|
2337
|
+
Dict confirming the member was removed.
|
|
2338
|
+
"""
|
|
2339
|
+
team_id = params.get("team_id")
|
|
2340
|
+
user_id = params.get("user_id")
|
|
2341
|
+
|
|
2342
|
+
if team_id is None or user_id is None:
|
|
2343
|
+
return {"error": "team_id and user_id are required"}
|
|
2344
|
+
|
|
2345
|
+
err = _validate_uuid(team_id, "team_id")
|
|
2346
|
+
if err:
|
|
2347
|
+
return {"error": err}
|
|
2348
|
+
|
|
2349
|
+
err = _validate_uuid(user_id, "user_id")
|
|
2350
|
+
if err:
|
|
2351
|
+
return {"error": err}
|
|
2352
|
+
|
|
2353
|
+
try:
|
|
2354
|
+
result = self._request_with_retry(
|
|
2355
|
+
"DELETE",
|
|
2356
|
+
f"/api/teams/{quote(team_id, safe='')}/members/{quote(user_id, safe='')}",
|
|
2357
|
+
)
|
|
2358
|
+
return result if isinstance(result, dict) else {"message": "Team member removed"}
|
|
2359
|
+
except Exception as e:
|
|
2360
|
+
return {"error": _sanitize_error(str(e))}
|
|
2361
|
+
|
|
2362
|
+
# ==================================================================
|
|
2363
|
+
# Tool 50: team_invite_member
|
|
2364
|
+
# ==================================================================
|
|
2365
|
+
|
|
2366
|
+
def team_invite_member(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2367
|
+
"""Send an invitation to join a team.
|
|
2368
|
+
|
|
2369
|
+
Args:
|
|
2370
|
+
params: Dict with keys: team_id (required, UUID),
|
|
2371
|
+
email (required), role (optional, 'member' or 'admin').
|
|
2372
|
+
|
|
2373
|
+
Returns:
|
|
2374
|
+
Dict with the invitation details.
|
|
2375
|
+
"""
|
|
2376
|
+
team_id = params.get("team_id")
|
|
2377
|
+
email = params.get("email")
|
|
2378
|
+
role = params.get("role")
|
|
2379
|
+
|
|
2380
|
+
if team_id is None or email is None:
|
|
2381
|
+
return {"error": "team_id and email are required"}
|
|
2382
|
+
|
|
2383
|
+
err = _validate_uuid(team_id, "team_id")
|
|
2384
|
+
if err:
|
|
2385
|
+
return {"error": err}
|
|
2386
|
+
|
|
2387
|
+
if not email or not isinstance(email, str):
|
|
2388
|
+
return {"error": "email is required and must be a string"}
|
|
2389
|
+
if not _BASIC_EMAIL_RE.match(email):
|
|
2390
|
+
return {"error": "email must be a valid email address"}
|
|
2391
|
+
|
|
2392
|
+
if role is not None:
|
|
2393
|
+
if not isinstance(role, str) or role not in ("member", "admin"):
|
|
2394
|
+
return {"error": "role must be 'member' or 'admin'"}
|
|
2395
|
+
|
|
2396
|
+
body: Dict[str, Any] = {"email": email}
|
|
2397
|
+
if role is not None:
|
|
2398
|
+
body["role"] = role
|
|
2399
|
+
|
|
2400
|
+
try:
|
|
2401
|
+
result = self._request_with_retry(
|
|
2402
|
+
"POST",
|
|
2403
|
+
f"/api/teams/{quote(team_id, safe='')}/invites",
|
|
2404
|
+
body,
|
|
2405
|
+
)
|
|
2406
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
2407
|
+
except Exception as e:
|
|
2408
|
+
return {"error": _sanitize_error(str(e))}
|
|
2409
|
+
|
|
2410
|
+
# ==================================================================
|
|
2411
|
+
# Tool 51: team_list_invites
|
|
2412
|
+
# ==================================================================
|
|
2413
|
+
|
|
2414
|
+
def team_list_invites(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2415
|
+
"""List all pending invitations for a team.
|
|
2416
|
+
|
|
2417
|
+
Args:
|
|
2418
|
+
params: Dict with key: team_id (required, UUID).
|
|
2419
|
+
|
|
2420
|
+
Returns:
|
|
2421
|
+
Dict with list of team invitations.
|
|
2422
|
+
"""
|
|
2423
|
+
team_id = params.get("team_id")
|
|
2424
|
+
err = _validate_uuid(team_id, "team_id")
|
|
2425
|
+
if err:
|
|
2426
|
+
return {"error": err}
|
|
2427
|
+
|
|
2428
|
+
try:
|
|
2429
|
+
result = self._request_with_retry(
|
|
2430
|
+
"GET",
|
|
2431
|
+
f"/api/teams/{quote(team_id, safe='')}/invites",
|
|
2432
|
+
)
|
|
2433
|
+
return result if isinstance(result, dict) else {"data": result}
|
|
2434
|
+
except Exception as e:
|
|
2435
|
+
return {"error": _sanitize_error(str(e))}
|
|
2436
|
+
|
|
2437
|
+
# ==================================================================
|
|
2438
|
+
# Tool 52: team_cancel_invite
|
|
2439
|
+
# ==================================================================
|
|
2440
|
+
|
|
2441
|
+
def team_cancel_invite(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2442
|
+
"""Cancel a pending team invitation.
|
|
2443
|
+
|
|
2444
|
+
Args:
|
|
2445
|
+
params: Dict with keys: team_id (required, UUID),
|
|
2446
|
+
invite_id (required, UUID).
|
|
2447
|
+
|
|
2448
|
+
Returns:
|
|
2449
|
+
Dict confirming the invitation was cancelled.
|
|
2450
|
+
"""
|
|
2451
|
+
team_id = params.get("team_id")
|
|
2452
|
+
invite_id = params.get("invite_id")
|
|
2453
|
+
|
|
2454
|
+
if team_id is None or invite_id is None:
|
|
2455
|
+
return {"error": "team_id and invite_id are required"}
|
|
2456
|
+
|
|
2457
|
+
err = _validate_uuid(team_id, "team_id")
|
|
2458
|
+
if err:
|
|
2459
|
+
return {"error": err}
|
|
2460
|
+
|
|
2461
|
+
err = _validate_uuid(invite_id, "invite_id")
|
|
2462
|
+
if err:
|
|
2463
|
+
return {"error": err}
|
|
2464
|
+
|
|
2465
|
+
try:
|
|
2466
|
+
result = self._request_with_retry(
|
|
2467
|
+
"DELETE",
|
|
2468
|
+
f"/api/teams/{quote(team_id, safe='')}/invites/{quote(invite_id, safe='')}",
|
|
2469
|
+
)
|
|
2470
|
+
return result if isinstance(result, dict) else {"message": "Team invitation cancelled"}
|
|
2471
|
+
except Exception as e:
|
|
2472
|
+
return {"error": _sanitize_error(str(e))}
|
|
2473
|
+
|
|
1483
2474
|
# ==================================================================
|
|
1484
2475
|
# Tool dispatcher (Dify invokes tools by name)
|
|
1485
2476
|
# ==================================================================
|
|
@@ -1521,6 +2512,36 @@ class DominusNodeProvider:
|
|
|
1521
2512
|
"update_team_member_role": self.update_team_member_role,
|
|
1522
2513
|
"x402_info": self.x402_info,
|
|
1523
2514
|
"topup_paypal": self.topup_paypal,
|
|
2515
|
+
"topup_stripe": self.topup_stripe,
|
|
2516
|
+
"topup_crypto": self.topup_crypto,
|
|
2517
|
+
# --- Tools 26-52 (added to reach 53) ---
|
|
2518
|
+
"get_proxy_status": self.get_proxy_status,
|
|
2519
|
+
"get_transactions": self.get_transactions,
|
|
2520
|
+
"get_forecast": self.get_forecast,
|
|
2521
|
+
"check_payment": self.check_payment,
|
|
2522
|
+
"get_daily_usage": self.get_daily_usage,
|
|
2523
|
+
"get_top_hosts": self.get_top_hosts,
|
|
2524
|
+
"register": self.register,
|
|
2525
|
+
"login": self.login,
|
|
2526
|
+
"get_account_info": self.get_account_info,
|
|
2527
|
+
"verify_email": self.verify_email,
|
|
2528
|
+
"resend_verification": self.resend_verification,
|
|
2529
|
+
"update_password": self.update_password,
|
|
2530
|
+
"list_keys": self.list_keys,
|
|
2531
|
+
"create_key": self.create_key,
|
|
2532
|
+
"revoke_key": self.revoke_key,
|
|
2533
|
+
"get_plan": self.get_plan,
|
|
2534
|
+
"list_plans": self.list_plans,
|
|
2535
|
+
"change_plan": self.change_plan,
|
|
2536
|
+
"team_delete": self.team_delete,
|
|
2537
|
+
"team_revoke_key": self.team_revoke_key,
|
|
2538
|
+
"team_list_keys": self.team_list_keys,
|
|
2539
|
+
"team_list_members": self.team_list_members,
|
|
2540
|
+
"team_add_member": self.team_add_member,
|
|
2541
|
+
"team_remove_member": self.team_remove_member,
|
|
2542
|
+
"team_invite_member": self.team_invite_member,
|
|
2543
|
+
"team_list_invites": self.team_list_invites,
|
|
2544
|
+
"team_cancel_invite": self.team_cancel_invite,
|
|
1524
2545
|
}
|
|
1525
2546
|
|
|
1526
2547
|
handler = dispatch.get(tool_name)
|
|
@@ -1537,7 +2558,7 @@ class DominusNodeProvider:
|
|
|
1537
2558
|
|
|
1538
2559
|
@staticmethod
|
|
1539
2560
|
def get_tools() -> list[str]:
|
|
1540
|
-
"""Return the list of all
|
|
2561
|
+
"""Return the list of all 53 tool names.
|
|
1541
2562
|
|
|
1542
2563
|
Returns:
|
|
1543
2564
|
Sorted list of tool name strings.
|
|
@@ -1567,4 +2588,33 @@ class DominusNodeProvider:
|
|
|
1567
2588
|
"update_team_member_role",
|
|
1568
2589
|
"x402_info",
|
|
1569
2590
|
"topup_paypal",
|
|
2591
|
+
"topup_stripe",
|
|
2592
|
+
"topup_crypto",
|
|
2593
|
+
"get_proxy_status",
|
|
2594
|
+
"get_transactions",
|
|
2595
|
+
"get_forecast",
|
|
2596
|
+
"check_payment",
|
|
2597
|
+
"get_daily_usage",
|
|
2598
|
+
"get_top_hosts",
|
|
2599
|
+
"register",
|
|
2600
|
+
"login",
|
|
2601
|
+
"get_account_info",
|
|
2602
|
+
"verify_email",
|
|
2603
|
+
"resend_verification",
|
|
2604
|
+
"update_password",
|
|
2605
|
+
"list_keys",
|
|
2606
|
+
"create_key",
|
|
2607
|
+
"revoke_key",
|
|
2608
|
+
"get_plan",
|
|
2609
|
+
"list_plans",
|
|
2610
|
+
"change_plan",
|
|
2611
|
+
"team_delete",
|
|
2612
|
+
"team_revoke_key",
|
|
2613
|
+
"team_list_keys",
|
|
2614
|
+
"team_list_members",
|
|
2615
|
+
"team_add_member",
|
|
2616
|
+
"team_remove_member",
|
|
2617
|
+
"team_invite_member",
|
|
2618
|
+
"team_list_invites",
|
|
2619
|
+
"team_cancel_invite",
|
|
1570
2620
|
])
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dominusnode-dify"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.2.0"
|
|
8
8
|
description = "Dominus Node tool provider for Dify -- 22 proxy, wallet, and team management tools"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -453,7 +453,7 @@ class TestProviderInit:
|
|
|
453
453
|
|
|
454
454
|
def test_get_tools_returns_24(self):
|
|
455
455
|
tools = DominusNodeProvider.get_tools()
|
|
456
|
-
assert len(tools) ==
|
|
456
|
+
assert len(tools) == 26
|
|
457
457
|
assert "proxied_fetch" in tools
|
|
458
458
|
assert "check_balance" in tools
|
|
459
459
|
assert "topup_paypal" in tools
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dominusnode_dify-1.0.1 → dominusnode_dify-1.2.0}/dominusnode_dify.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|