dominusnode-dify 1.1.0__tar.gz → 1.2.1__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.1
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)
@@ -12,6 +12,7 @@ protections.
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
+ import hashlib
15
16
  import ipaddress
16
17
  import json
17
18
  import math
@@ -351,6 +352,46 @@ def _sanitize_error(message: str) -> str:
351
352
  return _CREDENTIAL_RE.sub("***", message)
352
353
 
353
354
 
355
+ def _count_leading_zero_bits(data: bytes) -> int:
356
+ """Count leading zero bits in a byte array."""
357
+ count = 0
358
+ for byte in data:
359
+ if byte == 0:
360
+ count += 8
361
+ else:
362
+ mask = 0x80
363
+ while mask and not (byte & mask):
364
+ count += 1
365
+ mask >>= 1
366
+ break
367
+ return count
368
+
369
+
370
+ def _solve_pow(base_url: str) -> Optional[dict]:
371
+ """Solve a Proof-of-Work challenge for CAPTCHA-free registration."""
372
+ try:
373
+ pow_url = f"{base_url.rstrip('/')}/api/auth/pow/challenge"
374
+ with httpx.Client(timeout=30.0, follow_redirects=False) as client:
375
+ resp = client.post(pow_url, headers={"Content-Type": "application/json"})
376
+ if resp.status_code >= 400:
377
+ return None
378
+ challenge = resp.json()
379
+ prefix = challenge.get("prefix", "")
380
+ difficulty = challenge.get("difficulty", 20)
381
+ challenge_id = challenge.get("challengeId", "")
382
+ if not prefix or not challenge_id:
383
+ return None
384
+ nonce = 0
385
+ while nonce < 100_000_000:
386
+ h = hashlib.sha256((prefix + str(nonce)).encode()).digest()
387
+ if _count_leading_zero_bits(h) >= difficulty:
388
+ return {"challengeId": challenge_id, "nonce": str(nonce)}
389
+ nonce += 1
390
+ return None
391
+ except Exception:
392
+ return None
393
+
394
+
354
395
  # ---------------------------------------------------------------------------
355
396
  # Prototype pollution prevention
356
397
  # ---------------------------------------------------------------------------
@@ -393,6 +434,8 @@ _DOMAIN_RE = re.compile(
393
434
  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
435
  )
395
436
 
437
+ _BASIC_EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
438
+
396
439
 
397
440
  # ---------------------------------------------------------------------------
398
441
  # Period to date range helper
@@ -477,7 +520,7 @@ def _validate_allowed_domains(domains: Any) -> Optional[str]:
477
520
  class DominusNodeProvider:
478
521
  """Dify tool provider for Dominus Node rotating proxy service.
479
522
 
480
- Authenticates using a Dominus Node API key and exposes 26 tool methods that
523
+ Authenticates using a Dominus Node API key and exposes 53 tool methods that
481
524
  Dify can invoke. Each tool method receives ``credentials`` and ``params``
482
525
  dicts from the Dify runtime and returns a dict result.
483
526
 
@@ -646,6 +689,69 @@ class DominusNodeProvider:
646
689
  return self._api_request(method, path, body)
647
690
  raise
648
691
 
692
+ # ------------------------------------------------------------------
693
+ # Unauthenticated request helper (register, login, verify_email)
694
+ # ------------------------------------------------------------------
695
+
696
+ def _unauthenticated_request(
697
+ self,
698
+ method: str,
699
+ path: str,
700
+ body: Optional[Dict[str, Any]] = None,
701
+ ) -> Any:
702
+ """Make an unauthenticated API request (no Bearer token).
703
+
704
+ Used for register, login, and verify_email endpoints.
705
+
706
+ Args:
707
+ method: HTTP method (POST).
708
+ path: API path (e.g., ``/api/auth/register``).
709
+ body: Optional JSON body.
710
+
711
+ Returns:
712
+ Parsed JSON response (with dangerous keys stripped).
713
+
714
+ Raises:
715
+ RuntimeError: On API errors or oversized responses.
716
+ """
717
+ headers: Dict[str, str] = {
718
+ "User-Agent": "dominusnode-dify/1.0.0",
719
+ "Content-Type": "application/json",
720
+ }
721
+ if self.agent_secret:
722
+ headers["X-DominusNode-Agent"] = "mcp"
723
+ headers["X-DominusNode-Agent-Secret"] = self.agent_secret
724
+
725
+ with httpx.Client(
726
+ timeout=self.timeout,
727
+ follow_redirects=False,
728
+ ) as client:
729
+ kwargs: Dict[str, Any] = {"headers": headers}
730
+ if body is not None:
731
+ kwargs["json"] = body
732
+
733
+ resp = client.request(method, f"{self.base_url}{path}", **kwargs)
734
+
735
+ if len(resp.content) > MAX_RESPONSE_BYTES:
736
+ raise RuntimeError("Response body too large")
737
+
738
+ if resp.status_code >= 400:
739
+ try:
740
+ err_data = resp.json()
741
+ msg = err_data.get("error", resp.text)
742
+ except Exception:
743
+ msg = resp.text
744
+ msg = str(msg)[:500]
745
+ raise RuntimeError(
746
+ f"API error {resp.status_code}: {_sanitize_error(msg)}"
747
+ )
748
+
749
+ if resp.text:
750
+ data = resp.json()
751
+ _strip_dangerous_keys(data)
752
+ return data
753
+ return {}
754
+
649
755
  # ==================================================================
650
756
  # Tool 1: proxied_fetch
651
757
  # ==================================================================
@@ -1585,6 +1691,835 @@ class DominusNodeProvider:
1585
1691
  except Exception as e:
1586
1692
  return {"error": _sanitize_error(str(e))}
1587
1693
 
1694
+ # ==================================================================
1695
+ # Tool 26: get_proxy_status
1696
+ # ==================================================================
1697
+
1698
+ def get_proxy_status(self, params: Dict[str, Any]) -> Dict[str, Any]:
1699
+ """Get the current status of the proxy gateway.
1700
+
1701
+ Args:
1702
+ params: Empty dict (no parameters needed).
1703
+
1704
+ Returns:
1705
+ Dict with proxy status information.
1706
+ """
1707
+ try:
1708
+ result = self._request_with_retry("GET", "/api/proxy/status")
1709
+ return result if isinstance(result, dict) else {"data": result}
1710
+ except Exception as e:
1711
+ return {"error": _sanitize_error(str(e))}
1712
+
1713
+ # ==================================================================
1714
+ # Tool 27: get_transactions
1715
+ # ==================================================================
1716
+
1717
+ def get_transactions(self, params: Dict[str, Any]) -> Dict[str, Any]:
1718
+ """Get wallet transaction history.
1719
+
1720
+ Args:
1721
+ params: Dict with optional key: limit (1-100, default 20).
1722
+
1723
+ Returns:
1724
+ Dict with wallet transactions.
1725
+ """
1726
+ limit = params.get("limit", 20)
1727
+ # Dify sends numbers as float; coerce to int
1728
+ if isinstance(limit, float) and limit == int(limit):
1729
+ limit = int(limit)
1730
+
1731
+ if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 100:
1732
+ return {"error": "limit must be an integer between 1 and 100"}
1733
+
1734
+ qs = f"?limit={limit}"
1735
+
1736
+ try:
1737
+ result = self._request_with_retry("GET", f"/api/wallet/transactions{qs}")
1738
+ return result if isinstance(result, dict) else {"data": result}
1739
+ except Exception as e:
1740
+ return {"error": _sanitize_error(str(e))}
1741
+
1742
+ # ==================================================================
1743
+ # Tool 28: get_forecast
1744
+ # ==================================================================
1745
+
1746
+ def get_forecast(self, params: Dict[str, Any]) -> Dict[str, Any]:
1747
+ """Get wallet balance forecast (estimated days remaining, depletion date).
1748
+
1749
+ Args:
1750
+ params: Empty dict (no parameters needed).
1751
+
1752
+ Returns:
1753
+ Dict with forecast information.
1754
+ """
1755
+ try:
1756
+ result = self._request_with_retry("GET", "/api/wallet/forecast")
1757
+ return result if isinstance(result, dict) else {"data": result}
1758
+ except Exception as e:
1759
+ return {"error": _sanitize_error(str(e))}
1760
+
1761
+ # ==================================================================
1762
+ # Tool 29: check_payment
1763
+ # ==================================================================
1764
+
1765
+ def check_payment(self, params: Dict[str, Any]) -> Dict[str, Any]:
1766
+ """Check the status of a crypto payment invoice.
1767
+
1768
+ Args:
1769
+ params: Dict with key: invoice_id (required).
1770
+
1771
+ Returns:
1772
+ Dict with payment status information.
1773
+ """
1774
+ invoice_id = params.get("invoice_id")
1775
+ if not invoice_id or not isinstance(invoice_id, str):
1776
+ return {"error": "invoice_id is required and must be a string"}
1777
+
1778
+ invoice_id = invoice_id.strip()
1779
+ if not invoice_id:
1780
+ return {"error": "invoice_id is required and must be a string"}
1781
+
1782
+ if any(0 <= ord(c) <= 0x1F or ord(c) == 0x7F for c in invoice_id):
1783
+ return {"error": "invoice_id contains invalid control characters"}
1784
+
1785
+ try:
1786
+ result = self._request_with_retry(
1787
+ "GET",
1788
+ f"/api/wallet/topup/crypto/{quote(invoice_id, safe='')}/status",
1789
+ )
1790
+ return result if isinstance(result, dict) else {"data": result}
1791
+ except Exception as e:
1792
+ return {"error": _sanitize_error(str(e))}
1793
+
1794
+ # ==================================================================
1795
+ # Tool 30: get_daily_usage
1796
+ # ==================================================================
1797
+
1798
+ def get_daily_usage(self, params: Dict[str, Any]) -> Dict[str, Any]:
1799
+ """Get daily bandwidth usage statistics.
1800
+
1801
+ Args:
1802
+ params: Dict with optional key: days (1-365, default 7).
1803
+
1804
+ Returns:
1805
+ Dict with daily usage breakdown.
1806
+ """
1807
+ days = params.get("days", 7)
1808
+ # Dify sends numbers as float; coerce to int
1809
+ if isinstance(days, float) and days == int(days):
1810
+ days = int(days)
1811
+
1812
+ if not isinstance(days, int) or isinstance(days, bool) or days < 1 or days > 365:
1813
+ return {"error": "days must be an integer between 1 and 365"}
1814
+
1815
+ try:
1816
+ result = self._request_with_retry("GET", f"/api/usage/daily?days={days}")
1817
+ return result if isinstance(result, dict) else {"data": result}
1818
+ except Exception as e:
1819
+ return {"error": _sanitize_error(str(e))}
1820
+
1821
+ # ==================================================================
1822
+ # Tool 31: get_top_hosts
1823
+ # ==================================================================
1824
+
1825
+ def get_top_hosts(self, params: Dict[str, Any]) -> Dict[str, Any]:
1826
+ """Get top hosts by bandwidth usage.
1827
+
1828
+ Args:
1829
+ params: Dict with optional key: limit (1-100, default 10).
1830
+
1831
+ Returns:
1832
+ Dict with top hosts ranked by bandwidth.
1833
+ """
1834
+ limit = params.get("limit", 10)
1835
+ # Dify sends numbers as float; coerce to int
1836
+ if isinstance(limit, float) and limit == int(limit):
1837
+ limit = int(limit)
1838
+
1839
+ if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 100:
1840
+ return {"error": "limit must be an integer between 1 and 100"}
1841
+
1842
+ try:
1843
+ result = self._request_with_retry("GET", f"/api/usage/top-hosts?limit={limit}")
1844
+ return result if isinstance(result, dict) else {"data": result}
1845
+ except Exception as e:
1846
+ return {"error": _sanitize_error(str(e))}
1847
+
1848
+ # ==================================================================
1849
+ # Tool 32: register (unauthenticated)
1850
+ # ==================================================================
1851
+
1852
+ def register(self, params: Dict[str, Any]) -> Dict[str, Any]:
1853
+ """Register a new Dominus Node account.
1854
+
1855
+ This is an unauthenticated endpoint -- no API key or token needed.
1856
+
1857
+ Args:
1858
+ params: Dict with keys: email (required), password (required, 8-128 chars).
1859
+
1860
+ Returns:
1861
+ Dict with registration result.
1862
+ """
1863
+ email = params.get("email")
1864
+ password = params.get("password")
1865
+
1866
+ if not email or not isinstance(email, str):
1867
+ return {"error": "email is required and must be a string"}
1868
+ if not _BASIC_EMAIL_RE.match(email):
1869
+ return {"error": "email must be a valid email address"}
1870
+ if len(email) > 254:
1871
+ return {"error": "email must be 254 characters or fewer"}
1872
+
1873
+ if not password or not isinstance(password, str):
1874
+ return {"error": "password is required and must be a string"}
1875
+ if len(password) < 8:
1876
+ return {"error": "password must be at least 8 characters"}
1877
+ if len(password) > 128:
1878
+ return {"error": "password must be 128 characters or fewer"}
1879
+
1880
+ try:
1881
+ body: dict = {"email": email, "password": password}
1882
+ pow_result = _solve_pow(self.base_url)
1883
+ if pow_result:
1884
+ body["pow"] = pow_result
1885
+ result = self._unauthenticated_request(
1886
+ "POST",
1887
+ "/api/auth/register",
1888
+ body,
1889
+ )
1890
+ if not isinstance(result, dict):
1891
+ result = {"data": result}
1892
+ if pow_result:
1893
+ result["_pow_verified"] = True
1894
+ return result
1895
+ except Exception as e:
1896
+ return {"error": _sanitize_error(str(e))}
1897
+
1898
+ # ==================================================================
1899
+ # Tool 33: login (unauthenticated)
1900
+ # ==================================================================
1901
+
1902
+ def login(self, params: Dict[str, Any]) -> Dict[str, Any]:
1903
+ """Log in to a Dominus Node account.
1904
+
1905
+ This is an unauthenticated endpoint -- no API key or token needed.
1906
+ Detects MFA challenges and surfaces them to the caller.
1907
+
1908
+ Args:
1909
+ params: Dict with keys: email (required), password (required).
1910
+
1911
+ Returns:
1912
+ Dict with login result (tokens or MFA challenge).
1913
+ """
1914
+ email = params.get("email")
1915
+ password = params.get("password")
1916
+
1917
+ if not email or not isinstance(email, str):
1918
+ return {"error": "email is required and must be a string"}
1919
+ if not _BASIC_EMAIL_RE.match(email):
1920
+ return {"error": "email must be a valid email address"}
1921
+ if len(email) > 254:
1922
+ return {"error": "email must be 254 characters or fewer"}
1923
+
1924
+ if not password or not isinstance(password, str):
1925
+ return {"error": "password is required and must be a string"}
1926
+ if len(password) < 1:
1927
+ return {"error": "password is required"}
1928
+ if len(password) > 128:
1929
+ return {"error": "password must be 128 characters or fewer"}
1930
+
1931
+ try:
1932
+ result = self._unauthenticated_request(
1933
+ "POST",
1934
+ "/api/auth/login",
1935
+ {"email": email, "password": password},
1936
+ )
1937
+ if isinstance(result, dict) and result.get("mfaRequired"):
1938
+ return {
1939
+ "mfaRequired": True,
1940
+ "challengeToken": result.get("challengeToken"),
1941
+ "message": "Multi-factor authentication is required. Complete the MFA challenge to proceed.",
1942
+ }
1943
+ return result if isinstance(result, dict) else {"data": result}
1944
+ except Exception as e:
1945
+ return {"error": _sanitize_error(str(e))}
1946
+
1947
+ # ==================================================================
1948
+ # Tool 34: get_account_info
1949
+ # ==================================================================
1950
+
1951
+ def get_account_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
1952
+ """Get current account information.
1953
+
1954
+ Args:
1955
+ params: Empty dict (no parameters needed).
1956
+
1957
+ Returns:
1958
+ Dict with account details (id, email, plan, status, etc.).
1959
+ """
1960
+ try:
1961
+ result = self._request_with_retry("GET", "/api/auth/me")
1962
+ return result if isinstance(result, dict) else {"data": result}
1963
+ except Exception as e:
1964
+ return {"error": _sanitize_error(str(e))}
1965
+
1966
+ # ==================================================================
1967
+ # Tool 35: verify_email (unauthenticated)
1968
+ # ==================================================================
1969
+
1970
+ def verify_email(self, params: Dict[str, Any]) -> Dict[str, Any]:
1971
+ """Verify an email address using a verification token.
1972
+
1973
+ This is an unauthenticated endpoint.
1974
+
1975
+ Args:
1976
+ params: Dict with key: token (required).
1977
+
1978
+ Returns:
1979
+ Dict confirming email verification.
1980
+ """
1981
+ token = params.get("token")
1982
+ if not token or not isinstance(token, str):
1983
+ return {"error": "token is required and must be a string"}
1984
+
1985
+ token = token.strip()
1986
+ if not token:
1987
+ return {"error": "token is required and must be a string"}
1988
+
1989
+ if any(0 <= ord(c) <= 0x1F or ord(c) == 0x7F for c in token):
1990
+ return {"error": "token contains invalid control characters"}
1991
+
1992
+ try:
1993
+ result = self._unauthenticated_request(
1994
+ "POST",
1995
+ "/api/auth/verify-email",
1996
+ {"token": token},
1997
+ )
1998
+ return result if isinstance(result, dict) else {"message": "Email verified successfully"}
1999
+ except Exception as e:
2000
+ return {"error": _sanitize_error(str(e))}
2001
+
2002
+ # ==================================================================
2003
+ # Tool 36: resend_verification
2004
+ # ==================================================================
2005
+
2006
+ def resend_verification(self, params: Dict[str, Any]) -> Dict[str, Any]:
2007
+ """Resend the email verification message.
2008
+
2009
+ Args:
2010
+ params: Empty dict (no parameters needed).
2011
+
2012
+ Returns:
2013
+ Dict confirming the verification email was sent.
2014
+ """
2015
+ try:
2016
+ result = self._request_with_retry("POST", "/api/auth/resend-verification")
2017
+ return result if isinstance(result, dict) else {"message": "Verification email sent"}
2018
+ except Exception as e:
2019
+ return {"error": _sanitize_error(str(e))}
2020
+
2021
+ # ==================================================================
2022
+ # Tool 37: update_password
2023
+ # ==================================================================
2024
+
2025
+ def update_password(self, params: Dict[str, Any]) -> Dict[str, Any]:
2026
+ """Update the current account password.
2027
+
2028
+ Args:
2029
+ params: Dict with keys: current_password (required),
2030
+ new_password (required, 8-128 chars).
2031
+
2032
+ Returns:
2033
+ Dict confirming the password was updated.
2034
+ """
2035
+ current_password = params.get("current_password")
2036
+ new_password = params.get("new_password")
2037
+
2038
+ if not current_password or not isinstance(current_password, str):
2039
+ return {"error": "current_password is required and must be a string"}
2040
+ if len(current_password) < 1:
2041
+ return {"error": "current_password is required"}
2042
+
2043
+ if not new_password or not isinstance(new_password, str):
2044
+ return {"error": "new_password is required and must be a string"}
2045
+ if len(new_password) < 8:
2046
+ return {"error": "new_password must be at least 8 characters"}
2047
+ if len(new_password) > 128:
2048
+ return {"error": "new_password must be 128 characters or fewer"}
2049
+
2050
+ try:
2051
+ result = self._request_with_retry(
2052
+ "POST",
2053
+ "/api/auth/change-password",
2054
+ {"currentPassword": current_password, "newPassword": new_password},
2055
+ )
2056
+ return result if isinstance(result, dict) else {"message": "Password updated successfully"}
2057
+ except Exception as e:
2058
+ return {"error": _sanitize_error(str(e))}
2059
+
2060
+ # ==================================================================
2061
+ # Tool 38: list_keys
2062
+ # ==================================================================
2063
+
2064
+ def list_keys(self, params: Dict[str, Any]) -> Dict[str, Any]:
2065
+ """List all API keys for the current account.
2066
+
2067
+ Args:
2068
+ params: Empty dict (no parameters needed).
2069
+
2070
+ Returns:
2071
+ Dict with list of API keys.
2072
+ """
2073
+ try:
2074
+ result = self._request_with_retry("GET", "/api/keys")
2075
+ return result if isinstance(result, dict) else {"data": result}
2076
+ except Exception as e:
2077
+ return {"error": _sanitize_error(str(e))}
2078
+
2079
+ # ==================================================================
2080
+ # Tool 39: create_key
2081
+ # ==================================================================
2082
+
2083
+ def create_key(self, params: Dict[str, Any]) -> Dict[str, Any]:
2084
+ """Create a new API key for the current account.
2085
+
2086
+ Args:
2087
+ params: Dict with optional key: label (string, 1-100 chars).
2088
+
2089
+ Returns:
2090
+ Dict with the created key details (key shown once).
2091
+ """
2092
+ label = params.get("label")
2093
+
2094
+ body: Dict[str, Any] = {}
2095
+ if label is not None:
2096
+ if not isinstance(label, str):
2097
+ return {"error": "label must be a string"}
2098
+ err = _validate_label(label, "label")
2099
+ if err:
2100
+ return {"error": err}
2101
+ body["label"] = label
2102
+
2103
+ try:
2104
+ result = self._request_with_retry("POST", "/api/keys", body)
2105
+ return result if isinstance(result, dict) else {"data": result}
2106
+ except Exception as e:
2107
+ return {"error": _sanitize_error(str(e))}
2108
+
2109
+ # ==================================================================
2110
+ # Tool 40: revoke_key
2111
+ # ==================================================================
2112
+
2113
+ def revoke_key(self, params: Dict[str, Any]) -> Dict[str, Any]:
2114
+ """Revoke (delete) an API key by its ID.
2115
+
2116
+ Args:
2117
+ params: Dict with key: key_id (required, UUID).
2118
+
2119
+ Returns:
2120
+ Dict confirming the key was revoked.
2121
+ """
2122
+ key_id = params.get("key_id")
2123
+ err = _validate_uuid(key_id, "key_id")
2124
+ if err:
2125
+ return {"error": err}
2126
+
2127
+ try:
2128
+ result = self._request_with_retry(
2129
+ "DELETE",
2130
+ f"/api/keys/{quote(key_id, safe='')}",
2131
+ )
2132
+ return result if isinstance(result, dict) else {"message": "API key revoked"}
2133
+ except Exception as e:
2134
+ return {"error": _sanitize_error(str(e))}
2135
+
2136
+ # ==================================================================
2137
+ # Tool 41: get_plan
2138
+ # ==================================================================
2139
+
2140
+ def get_plan(self, params: Dict[str, Any]) -> Dict[str, Any]:
2141
+ """Get the current user's plan details.
2142
+
2143
+ Args:
2144
+ params: Empty dict (no parameters needed).
2145
+
2146
+ Returns:
2147
+ Dict with the current plan information.
2148
+ """
2149
+ try:
2150
+ result = self._request_with_retry("GET", "/api/plans/user/plan")
2151
+ return result if isinstance(result, dict) else {"data": result}
2152
+ except Exception as e:
2153
+ return {"error": _sanitize_error(str(e))}
2154
+
2155
+ # ==================================================================
2156
+ # Tool 42: list_plans
2157
+ # ==================================================================
2158
+
2159
+ def list_plans(self, params: Dict[str, Any]) -> Dict[str, Any]:
2160
+ """List all available plans.
2161
+
2162
+ Args:
2163
+ params: Empty dict (no parameters needed).
2164
+
2165
+ Returns:
2166
+ Dict with list of available plans.
2167
+ """
2168
+ try:
2169
+ result = self._request_with_retry("GET", "/api/plans")
2170
+ return result if isinstance(result, dict) else {"data": result}
2171
+ except Exception as e:
2172
+ return {"error": _sanitize_error(str(e))}
2173
+
2174
+ # ==================================================================
2175
+ # Tool 43: change_plan
2176
+ # ==================================================================
2177
+
2178
+ def change_plan(self, params: Dict[str, Any]) -> Dict[str, Any]:
2179
+ """Change the current user's plan.
2180
+
2181
+ Args:
2182
+ params: Dict with key: plan_id (required).
2183
+
2184
+ Returns:
2185
+ Dict confirming the plan change.
2186
+ """
2187
+ plan_id = params.get("plan_id")
2188
+ if not plan_id or not isinstance(plan_id, str):
2189
+ return {"error": "plan_id is required and must be a string"}
2190
+
2191
+ plan_id = plan_id.strip()
2192
+ if not plan_id:
2193
+ return {"error": "plan_id is required and must be a string"}
2194
+
2195
+ if any(0 <= ord(c) <= 0x1F or ord(c) == 0x7F for c in plan_id):
2196
+ return {"error": "plan_id contains invalid control characters"}
2197
+
2198
+ try:
2199
+ result = self._request_with_retry(
2200
+ "PUT",
2201
+ "/api/plans/user/plan",
2202
+ {"planId": plan_id},
2203
+ )
2204
+ return result if isinstance(result, dict) else {"data": result}
2205
+ except Exception as e:
2206
+ return {"error": _sanitize_error(str(e))}
2207
+
2208
+ # ==================================================================
2209
+ # Tool 44: team_delete
2210
+ # ==================================================================
2211
+
2212
+ def team_delete(self, params: Dict[str, Any]) -> Dict[str, Any]:
2213
+ """Delete a team permanently.
2214
+
2215
+ Args:
2216
+ params: Dict with key: team_id (required, UUID).
2217
+
2218
+ Returns:
2219
+ Dict confirming the team was deleted.
2220
+ """
2221
+ team_id = params.get("team_id")
2222
+ err = _validate_uuid(team_id, "team_id")
2223
+ if err:
2224
+ return {"error": err}
2225
+
2226
+ try:
2227
+ result = self._request_with_retry(
2228
+ "DELETE",
2229
+ f"/api/teams/{quote(team_id, safe='')}",
2230
+ )
2231
+ return result if isinstance(result, dict) else {"message": "Team deleted"}
2232
+ except Exception as e:
2233
+ return {"error": _sanitize_error(str(e))}
2234
+
2235
+ # ==================================================================
2236
+ # Tool 45: team_revoke_key
2237
+ # ==================================================================
2238
+
2239
+ def team_revoke_key(self, params: Dict[str, Any]) -> Dict[str, Any]:
2240
+ """Revoke (delete) an API key from a team.
2241
+
2242
+ Args:
2243
+ params: Dict with keys: team_id (required, UUID),
2244
+ key_id (required, UUID).
2245
+
2246
+ Returns:
2247
+ Dict confirming the team key was revoked.
2248
+ """
2249
+ team_id = params.get("team_id")
2250
+ key_id = params.get("key_id")
2251
+
2252
+ if team_id is None or key_id is None:
2253
+ return {"error": "team_id and key_id are required"}
2254
+
2255
+ err = _validate_uuid(team_id, "team_id")
2256
+ if err:
2257
+ return {"error": err}
2258
+
2259
+ err = _validate_uuid(key_id, "key_id")
2260
+ if err:
2261
+ return {"error": err}
2262
+
2263
+ try:
2264
+ result = self._request_with_retry(
2265
+ "DELETE",
2266
+ f"/api/teams/{quote(team_id, safe='')}/keys/{quote(key_id, safe='')}",
2267
+ )
2268
+ return result if isinstance(result, dict) else {"message": "Team API key revoked"}
2269
+ except Exception as e:
2270
+ return {"error": _sanitize_error(str(e))}
2271
+
2272
+ # ==================================================================
2273
+ # Tool 46: team_list_keys
2274
+ # ==================================================================
2275
+
2276
+ def team_list_keys(self, params: Dict[str, Any]) -> Dict[str, Any]:
2277
+ """List all API keys for a team.
2278
+
2279
+ Args:
2280
+ params: Dict with key: team_id (required, UUID).
2281
+
2282
+ Returns:
2283
+ Dict with list of team API keys.
2284
+ """
2285
+ team_id = params.get("team_id")
2286
+ err = _validate_uuid(team_id, "team_id")
2287
+ if err:
2288
+ return {"error": err}
2289
+
2290
+ try:
2291
+ result = self._request_with_retry(
2292
+ "GET",
2293
+ f"/api/teams/{quote(team_id, safe='')}/keys",
2294
+ )
2295
+ return result if isinstance(result, dict) else {"data": result}
2296
+ except Exception as e:
2297
+ return {"error": _sanitize_error(str(e))}
2298
+
2299
+ # ==================================================================
2300
+ # Tool 47: team_list_members
2301
+ # ==================================================================
2302
+
2303
+ def team_list_members(self, params: Dict[str, Any]) -> Dict[str, Any]:
2304
+ """List all members of a team.
2305
+
2306
+ Args:
2307
+ params: Dict with key: team_id (required, UUID).
2308
+
2309
+ Returns:
2310
+ Dict with list of team members.
2311
+ """
2312
+ team_id = params.get("team_id")
2313
+ err = _validate_uuid(team_id, "team_id")
2314
+ if err:
2315
+ return {"error": err}
2316
+
2317
+ try:
2318
+ result = self._request_with_retry(
2319
+ "GET",
2320
+ f"/api/teams/{quote(team_id, safe='')}/members",
2321
+ )
2322
+ return result if isinstance(result, dict) else {"data": result}
2323
+ except Exception as e:
2324
+ return {"error": _sanitize_error(str(e))}
2325
+
2326
+ # ==================================================================
2327
+ # Tool 48: team_add_member
2328
+ # ==================================================================
2329
+
2330
+ def team_add_member(self, params: Dict[str, Any]) -> Dict[str, Any]:
2331
+ """Add a member to a team by email.
2332
+
2333
+ Args:
2334
+ params: Dict with keys: team_id (required, UUID),
2335
+ email (required), role (optional, 'member' or 'admin').
2336
+
2337
+ Returns:
2338
+ Dict with the added member details.
2339
+ """
2340
+ team_id = params.get("team_id")
2341
+ email = params.get("email")
2342
+ role = params.get("role")
2343
+
2344
+ if team_id is None or email is None:
2345
+ return {"error": "team_id and email are required"}
2346
+
2347
+ err = _validate_uuid(team_id, "team_id")
2348
+ if err:
2349
+ return {"error": err}
2350
+
2351
+ if not email or not isinstance(email, str):
2352
+ return {"error": "email is required and must be a string"}
2353
+ if not _BASIC_EMAIL_RE.match(email):
2354
+ return {"error": "email must be a valid email address"}
2355
+
2356
+ if role is not None:
2357
+ if not isinstance(role, str) or role not in ("member", "admin"):
2358
+ return {"error": "role must be 'member' or 'admin'"}
2359
+
2360
+ body: Dict[str, Any] = {"email": email}
2361
+ if role is not None:
2362
+ body["role"] = role
2363
+
2364
+ try:
2365
+ result = self._request_with_retry(
2366
+ "POST",
2367
+ f"/api/teams/{quote(team_id, safe='')}/members",
2368
+ body,
2369
+ )
2370
+ return result if isinstance(result, dict) else {"data": result}
2371
+ except Exception as e:
2372
+ return {"error": _sanitize_error(str(e))}
2373
+
2374
+ # ==================================================================
2375
+ # Tool 49: team_remove_member
2376
+ # ==================================================================
2377
+
2378
+ def team_remove_member(self, params: Dict[str, Any]) -> Dict[str, Any]:
2379
+ """Remove a member from a team.
2380
+
2381
+ Args:
2382
+ params: Dict with keys: team_id (required, UUID),
2383
+ user_id (required, UUID).
2384
+
2385
+ Returns:
2386
+ Dict confirming the member was removed.
2387
+ """
2388
+ team_id = params.get("team_id")
2389
+ user_id = params.get("user_id")
2390
+
2391
+ if team_id is None or user_id is None:
2392
+ return {"error": "team_id and user_id are required"}
2393
+
2394
+ err = _validate_uuid(team_id, "team_id")
2395
+ if err:
2396
+ return {"error": err}
2397
+
2398
+ err = _validate_uuid(user_id, "user_id")
2399
+ if err:
2400
+ return {"error": err}
2401
+
2402
+ try:
2403
+ result = self._request_with_retry(
2404
+ "DELETE",
2405
+ f"/api/teams/{quote(team_id, safe='')}/members/{quote(user_id, safe='')}",
2406
+ )
2407
+ return result if isinstance(result, dict) else {"message": "Team member removed"}
2408
+ except Exception as e:
2409
+ return {"error": _sanitize_error(str(e))}
2410
+
2411
+ # ==================================================================
2412
+ # Tool 50: team_invite_member
2413
+ # ==================================================================
2414
+
2415
+ def team_invite_member(self, params: Dict[str, Any]) -> Dict[str, Any]:
2416
+ """Send an invitation to join a team.
2417
+
2418
+ Args:
2419
+ params: Dict with keys: team_id (required, UUID),
2420
+ email (required), role (optional, 'member' or 'admin').
2421
+
2422
+ Returns:
2423
+ Dict with the invitation details.
2424
+ """
2425
+ team_id = params.get("team_id")
2426
+ email = params.get("email")
2427
+ role = params.get("role")
2428
+
2429
+ if team_id is None or email is None:
2430
+ return {"error": "team_id and email are required"}
2431
+
2432
+ err = _validate_uuid(team_id, "team_id")
2433
+ if err:
2434
+ return {"error": err}
2435
+
2436
+ if not email or not isinstance(email, str):
2437
+ return {"error": "email is required and must be a string"}
2438
+ if not _BASIC_EMAIL_RE.match(email):
2439
+ return {"error": "email must be a valid email address"}
2440
+
2441
+ if role is not None:
2442
+ if not isinstance(role, str) or role not in ("member", "admin"):
2443
+ return {"error": "role must be 'member' or 'admin'"}
2444
+
2445
+ body: Dict[str, Any] = {"email": email}
2446
+ if role is not None:
2447
+ body["role"] = role
2448
+
2449
+ try:
2450
+ result = self._request_with_retry(
2451
+ "POST",
2452
+ f"/api/teams/{quote(team_id, safe='')}/invites",
2453
+ body,
2454
+ )
2455
+ return result if isinstance(result, dict) else {"data": result}
2456
+ except Exception as e:
2457
+ return {"error": _sanitize_error(str(e))}
2458
+
2459
+ # ==================================================================
2460
+ # Tool 51: team_list_invites
2461
+ # ==================================================================
2462
+
2463
+ def team_list_invites(self, params: Dict[str, Any]) -> Dict[str, Any]:
2464
+ """List all pending invitations for a team.
2465
+
2466
+ Args:
2467
+ params: Dict with key: team_id (required, UUID).
2468
+
2469
+ Returns:
2470
+ Dict with list of team invitations.
2471
+ """
2472
+ team_id = params.get("team_id")
2473
+ err = _validate_uuid(team_id, "team_id")
2474
+ if err:
2475
+ return {"error": err}
2476
+
2477
+ try:
2478
+ result = self._request_with_retry(
2479
+ "GET",
2480
+ f"/api/teams/{quote(team_id, safe='')}/invites",
2481
+ )
2482
+ return result if isinstance(result, dict) else {"data": result}
2483
+ except Exception as e:
2484
+ return {"error": _sanitize_error(str(e))}
2485
+
2486
+ # ==================================================================
2487
+ # Tool 52: team_cancel_invite
2488
+ # ==================================================================
2489
+
2490
+ def team_cancel_invite(self, params: Dict[str, Any]) -> Dict[str, Any]:
2491
+ """Cancel a pending team invitation.
2492
+
2493
+ Args:
2494
+ params: Dict with keys: team_id (required, UUID),
2495
+ invite_id (required, UUID).
2496
+
2497
+ Returns:
2498
+ Dict confirming the invitation was cancelled.
2499
+ """
2500
+ team_id = params.get("team_id")
2501
+ invite_id = params.get("invite_id")
2502
+
2503
+ if team_id is None or invite_id is None:
2504
+ return {"error": "team_id and invite_id are required"}
2505
+
2506
+ err = _validate_uuid(team_id, "team_id")
2507
+ if err:
2508
+ return {"error": err}
2509
+
2510
+ err = _validate_uuid(invite_id, "invite_id")
2511
+ if err:
2512
+ return {"error": err}
2513
+
2514
+ try:
2515
+ result = self._request_with_retry(
2516
+ "DELETE",
2517
+ f"/api/teams/{quote(team_id, safe='')}/invites/{quote(invite_id, safe='')}",
2518
+ )
2519
+ return result if isinstance(result, dict) else {"message": "Team invitation cancelled"}
2520
+ except Exception as e:
2521
+ return {"error": _sanitize_error(str(e))}
2522
+
1588
2523
  # ==================================================================
1589
2524
  # Tool dispatcher (Dify invokes tools by name)
1590
2525
  # ==================================================================
@@ -1628,6 +2563,34 @@ class DominusNodeProvider:
1628
2563
  "topup_paypal": self.topup_paypal,
1629
2564
  "topup_stripe": self.topup_stripe,
1630
2565
  "topup_crypto": self.topup_crypto,
2566
+ # --- Tools 26-52 (added to reach 53) ---
2567
+ "get_proxy_status": self.get_proxy_status,
2568
+ "get_transactions": self.get_transactions,
2569
+ "get_forecast": self.get_forecast,
2570
+ "check_payment": self.check_payment,
2571
+ "get_daily_usage": self.get_daily_usage,
2572
+ "get_top_hosts": self.get_top_hosts,
2573
+ "register": self.register,
2574
+ "login": self.login,
2575
+ "get_account_info": self.get_account_info,
2576
+ "verify_email": self.verify_email,
2577
+ "resend_verification": self.resend_verification,
2578
+ "update_password": self.update_password,
2579
+ "list_keys": self.list_keys,
2580
+ "create_key": self.create_key,
2581
+ "revoke_key": self.revoke_key,
2582
+ "get_plan": self.get_plan,
2583
+ "list_plans": self.list_plans,
2584
+ "change_plan": self.change_plan,
2585
+ "team_delete": self.team_delete,
2586
+ "team_revoke_key": self.team_revoke_key,
2587
+ "team_list_keys": self.team_list_keys,
2588
+ "team_list_members": self.team_list_members,
2589
+ "team_add_member": self.team_add_member,
2590
+ "team_remove_member": self.team_remove_member,
2591
+ "team_invite_member": self.team_invite_member,
2592
+ "team_list_invites": self.team_list_invites,
2593
+ "team_cancel_invite": self.team_cancel_invite,
1631
2594
  }
1632
2595
 
1633
2596
  handler = dispatch.get(tool_name)
@@ -1644,7 +2607,7 @@ class DominusNodeProvider:
1644
2607
 
1645
2608
  @staticmethod
1646
2609
  def get_tools() -> list[str]:
1647
- """Return the list of all 26 tool names.
2610
+ """Return the list of all 53 tool names.
1648
2611
 
1649
2612
  Returns:
1650
2613
  Sorted list of tool name strings.
@@ -1676,4 +2639,31 @@ class DominusNodeProvider:
1676
2639
  "topup_paypal",
1677
2640
  "topup_stripe",
1678
2641
  "topup_crypto",
2642
+ "get_proxy_status",
2643
+ "get_transactions",
2644
+ "get_forecast",
2645
+ "check_payment",
2646
+ "get_daily_usage",
2647
+ "get_top_hosts",
2648
+ "register",
2649
+ "login",
2650
+ "get_account_info",
2651
+ "verify_email",
2652
+ "resend_verification",
2653
+ "update_password",
2654
+ "list_keys",
2655
+ "create_key",
2656
+ "revoke_key",
2657
+ "get_plan",
2658
+ "list_plans",
2659
+ "change_plan",
2660
+ "team_delete",
2661
+ "team_revoke_key",
2662
+ "team_list_keys",
2663
+ "team_list_members",
2664
+ "team_add_member",
2665
+ "team_remove_member",
2666
+ "team_invite_member",
2667
+ "team_list_invites",
2668
+ "team_cancel_invite",
1679
2669
  ])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominusnode-dify
3
- Version: 1.1.0
3
+ Version: 1.2.1
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.1"
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"}