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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominusnode-dify
3
- Version: 1.0.1
3
+ Version: 1.2.0
4
4
  Summary: Dominus Node tool provider for Dify -- 22 proxy, wallet, and team management tools
5
5
  License: MIT
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
- """Dominus Node Dify integration -- 24 tools for the Dify AI platform.
1
+ """Dominus Node Dify integration -- 26 tools for the Dify AI platform.
2
2
 
3
- Provides a Dify-compatible tool provider that exposes 24 tools for
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 -- 24 tools with full security hardening.
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 24 tool methods that
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 22 tool names (plus topup_paypal = 23 entries).
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
  ])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominusnode-dify
3
- Version: 1.0.1
3
+ Version: 1.2.0
4
4
  Summary: Dominus Node tool provider for Dify -- 22 proxy, wallet, and team management tools
5
5
  License: MIT
6
6
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominusnode-dify"
7
- version = "1.0.1"
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) == 24
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