dominusnode-dify 1.1.0__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.1.0
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,4 +1,4 @@
1
- """Dominus Node Dify tool provider -- 26 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)
@@ -393,6 +393,8 @@ _DOMAIN_RE = re.compile(
393
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])?)*$"
394
394
  )
395
395
 
396
+ _BASIC_EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
397
+
396
398
 
397
399
  # ---------------------------------------------------------------------------
398
400
  # Period to date range helper
@@ -477,7 +479,7 @@ def _validate_allowed_domains(domains: Any) -> Optional[str]:
477
479
  class DominusNodeProvider:
478
480
  """Dify tool provider for Dominus Node rotating proxy service.
479
481
 
480
- Authenticates using a Dominus Node API key and exposes 26 tool methods that
482
+ Authenticates using a Dominus Node API key and exposes 53 tool methods that
481
483
  Dify can invoke. Each tool method receives ``credentials`` and ``params``
482
484
  dicts from the Dify runtime and returns a dict result.
483
485
 
@@ -646,6 +648,69 @@ class DominusNodeProvider:
646
648
  return self._api_request(method, path, body)
647
649
  raise
648
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
+
649
714
  # ==================================================================
650
715
  # Tool 1: proxied_fetch
651
716
  # ==================================================================
@@ -1585,6 +1650,827 @@ class DominusNodeProvider:
1585
1650
  except Exception as e:
1586
1651
  return {"error": _sanitize_error(str(e))}
1587
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
+
1588
2474
  # ==================================================================
1589
2475
  # Tool dispatcher (Dify invokes tools by name)
1590
2476
  # ==================================================================
@@ -1628,6 +2514,34 @@ class DominusNodeProvider:
1628
2514
  "topup_paypal": self.topup_paypal,
1629
2515
  "topup_stripe": self.topup_stripe,
1630
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,
1631
2545
  }
1632
2546
 
1633
2547
  handler = dispatch.get(tool_name)
@@ -1644,7 +2558,7 @@ class DominusNodeProvider:
1644
2558
 
1645
2559
  @staticmethod
1646
2560
  def get_tools() -> list[str]:
1647
- """Return the list of all 26 tool names.
2561
+ """Return the list of all 53 tool names.
1648
2562
 
1649
2563
  Returns:
1650
2564
  Sorted list of tool name strings.
@@ -1676,4 +2590,31 @@ class DominusNodeProvider:
1676
2590
  "topup_paypal",
1677
2591
  "topup_stripe",
1678
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",
1679
2620
  ])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominusnode-dify
3
- Version: 1.1.0
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.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"}