rw-sdk 2.8.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.
- rw_sdk-2.8.1/.gitignore +15 -0
- rw_sdk-2.8.1/LICENSE +21 -0
- rw_sdk-2.8.1/PKG-INFO +374 -0
- rw_sdk-2.8.1/README.md +339 -0
- rw_sdk-2.8.1/codegen/generate.py +825 -0
- rw_sdk-2.8.1/codegen/naming.py +146 -0
- rw_sdk-2.8.1/codegen/refify.py +391 -0
- rw_sdk-2.8.1/codegen/spec_patches.py +178 -0
- rw_sdk-2.8.1/codegen/version.py +38 -0
- rw_sdk-2.8.1/codegen/zod_names.py +193 -0
- rw_sdk-2.8.1/pyproject.toml +72 -0
- rw_sdk-2.8.1/specs/openapi-2.8.1.json +48947 -0
- rw_sdk-2.8.1/src/rw_sdk/__init__.py +60 -0
- rw_sdk-2.8.1/src/rw_sdk/_error_codes.py +479 -0
- rw_sdk-2.8.1/src/rw_sdk/_meta.py +2427 -0
- rw_sdk-2.8.1/src/rw_sdk/_resource.py +137 -0
- rw_sdk-2.8.1/src/rw_sdk/_resources_mixin.py +128 -0
- rw_sdk-2.8.1/src/rw_sdk/_types.py +91 -0
- rw_sdk-2.8.1/src/rw_sdk/_version.py +3 -0
- rw_sdk-2.8.1/src/rw_sdk/client.py +346 -0
- rw_sdk-2.8.1/src/rw_sdk/errors.py +185 -0
- rw_sdk-2.8.1/src/rw_sdk/models.py +6135 -0
- rw_sdk-2.8.1/src/rw_sdk/py.typed +0 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/__init__.py +89 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/api_tokens.py +187 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/auth.py +236 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/bandwidth_stats.py +354 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/config_profiles.py +285 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/external_squads.py +286 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/hosts.py +501 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/hosts_bulk.py +280 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/hwid.py +410 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/infra_billing.py +500 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/internal_squads.py +290 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/ip_control.py +186 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/keygen.py +43 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/metadata.py +155 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/node_plugins.py +468 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/nodes.py +627 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/passkeys.py +212 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/settings.py +117 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/snippets.py +137 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/sub.py +102 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/subscription_page_configs.py +242 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/subscription_request_history.py +168 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/subscription_settings.py +136 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/subscription_template.py +229 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/subscriptions.py +319 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/system.py +278 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/users.py +1032 -0
- rw_sdk-2.8.1/src/rw_sdk/resources/users_bulk.py +405 -0
- rw_sdk-2.8.1/tests/test_contract.py +384 -0
- rw_sdk-2.8.1/tests/test_live.py +351 -0
rw_sdk-2.8.1/.gitignore
ADDED
rw_sdk-2.8.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Akenai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rw_sdk-2.8.1/PKG-INFO
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rw-sdk
|
|
3
|
+
Version: 2.8.1
|
|
4
|
+
Summary: Full Python SDK for the Remnawave panel API — every endpoint, typed, sync and async
|
|
5
|
+
Project-URL: Homepage, https://github.com/akenai-vpn/rw-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/akenai-vpn/rw-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/akenai-vpn/rw-sdk/issues
|
|
8
|
+
Project-URL: Remnawave backend, https://github.com/remnawave/backend
|
|
9
|
+
Author: Akenai
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,client,proxy,remnawave,sdk,vpn,xray
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: niquests>=3.14
|
|
25
|
+
Requires-Dist: pydantic>=2.7
|
|
26
|
+
Provides-Extra: codegen
|
|
27
|
+
Requires-Dist: black>=24.0; extra == 'codegen'
|
|
28
|
+
Requires-Dist: datamodel-code-generator>=0.28; extra == 'codegen'
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# rw-sdk
|
|
37
|
+
|
|
38
|
+
Full Python SDK for the [Remnawave](https://github.com/remnawave/backend) panel API.
|
|
39
|
+
Every endpoint, typed, sync and async.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install rw-sdk
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from rw_sdk import Remnawave
|
|
47
|
+
|
|
48
|
+
with Remnawave("https://panel.example.com", token="...") as rw:
|
|
49
|
+
user = rw.users.create(
|
|
50
|
+
username="alice",
|
|
51
|
+
expire_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
|
|
52
|
+
traffic_limit_bytes=100 * 1024**3,
|
|
53
|
+
)
|
|
54
|
+
print(user.subscription_url, user.status)
|
|
55
|
+
|
|
56
|
+
for u in rw.users.iter_all(): # pages fetched automatically
|
|
57
|
+
print(u.username, u.user_traffic.used_traffic_bytes)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| | |
|
|
61
|
+
|---|---|
|
|
62
|
+
| API version | **2.8.1** — 186 operations across 27 resources, all implemented |
|
|
63
|
+
| Models | 580 pydantic v2 models, named after Remnawave's own zod contract schemas |
|
|
64
|
+
| Async | every method mirrored on `AsyncRemnawave` |
|
|
65
|
+
| Verified | offline contract tests + live integration run against a real 2.8.1 panel |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Authentication
|
|
70
|
+
|
|
71
|
+
Use an **API token** created on the panel's *API Tokens* page. That is the supported
|
|
72
|
+
mode for automation and what everything here is built around.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
rw = Remnawave("https://panel.example.com", token="eyJhbGci...")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 11 endpoints an API token cannot reach
|
|
79
|
+
|
|
80
|
+
An API token always carries role `API`. Three controllers declare `@Roles(ROLE.ADMIN)`
|
|
81
|
+
without `ROLE.API`, so calling them with a token returns `403 A004` no matter its
|
|
82
|
+
scopes — verified against a live 2.8.1 panel:
|
|
83
|
+
|
|
84
|
+
| Endpoints | Why |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `rw.api_tokens.*` (4) | tokens are minted from the panel UI or an admin session |
|
|
87
|
+
| `rw.passkeys.*` (5) | passkey management is a dashboard flow |
|
|
88
|
+
| `rw.settings.*` (2) | `/api/remnawave-settings` is admin-only |
|
|
89
|
+
|
|
90
|
+
They are still generated — this SDK covers all 186 operations — and each one says so in
|
|
91
|
+
its docstring. `rw_sdk.ENDPOINTS` carries an `admin_only` flag if you want to check
|
|
92
|
+
programmatically.
|
|
93
|
+
|
|
94
|
+
Everything else (users, nodes, hosts, squads, config profiles, subscriptions, stats,
|
|
95
|
+
HWID, IP control, infra billing, snippets, templates …) works with an API token.
|
|
96
|
+
|
|
97
|
+
### Admin JWT, if you need those 11
|
|
98
|
+
|
|
99
|
+
`auth.login` returns an admin-role JWT, which `JwtDefaultGuard` rejects unless the
|
|
100
|
+
request also carries `X-Remnawave-Client-Type: browser` ([`def-jwt-guard.ts`][guard]).
|
|
101
|
+
The SDK does **not** send that header — it targets API tokens. Supply it yourself if
|
|
102
|
+
you need the admin path:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
rw = Remnawave(
|
|
106
|
+
"https://panel.example.com",
|
|
107
|
+
headers={"X-Remnawave-Client-Type": "browser"},
|
|
108
|
+
)
|
|
109
|
+
rw.set_token(rw.auth.login(username="admin", password="...").access_token)
|
|
110
|
+
token = rw.api_tokens.create(name="ci", expires_in_days=90, scopes=["users:*"])
|
|
111
|
+
print(token.token) # returned once
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Without the header every call returns `403`. Per-endpoint API-token scopes are in each
|
|
115
|
+
method's docstring.
|
|
116
|
+
|
|
117
|
+
## The HTTPS/proxy requirement
|
|
118
|
+
|
|
119
|
+
In production Remnawave **destroys the socket** of any request that lacks
|
|
120
|
+
`x-forwarded-for` or does not carry `x-forwarded-proto: https`
|
|
121
|
+
([`proxy-check.middleware.ts`][proxy]) — you get a connection reset, not an HTTP error.
|
|
122
|
+
|
|
123
|
+
`proxy_headers="auto"` (the default) sends those headers only when the target looks
|
|
124
|
+
like a direct connection — plain `http://`, or a loopback/private host. Behind a real
|
|
125
|
+
reverse proxy the headers already exist, and forging `x-forwarded-for` there would
|
|
126
|
+
corrupt the panel's client-IP accounting, so they are left alone.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
Remnawave("http://127.0.0.1:3000", token=t) # headers sent
|
|
130
|
+
Remnawave("https://panel.example.com", token=t) # not sent
|
|
131
|
+
Remnawave("https://panel.example.com", token=t, proxy_headers="always") # forced
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A reset connection is surfaced as `ProxyRestrictionError` with the fix in the message.
|
|
135
|
+
|
|
136
|
+
## Pagination
|
|
137
|
+
|
|
138
|
+
Seven endpoints paginate. Each gets an iterator that handles the offset or cursor for
|
|
139
|
+
you:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
for user in rw.users.iter_all(page_size=500): # offset: start/size/total
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
for user in rw.users.iter_stream(page_size=1000): # cursor: nextCursor/hasMore
|
|
146
|
+
...
|
|
147
|
+
|
|
148
|
+
async for device in arw.hwid.iter_all(): # same on the async client
|
|
149
|
+
...
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The underlying single-page calls (`rw.users.get_all(start=..., size=...)`) are still
|
|
153
|
+
there when you want them.
|
|
154
|
+
|
|
155
|
+
## Filtering and sorting
|
|
156
|
+
|
|
157
|
+
Four list endpoints take TanStack-table style filters. None of them appear in the
|
|
158
|
+
OpenAPI document — the panel reads them with `JSON.parse`, and this SDK serialises them
|
|
159
|
+
for you:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from rw_sdk.models import TanstackFilter, TanstackSorting
|
|
163
|
+
|
|
164
|
+
page = rw.users.get_all(
|
|
165
|
+
filters=[TanstackFilter(id="username", value="alice")],
|
|
166
|
+
filter_modes={"username": "startsWith"},
|
|
167
|
+
sorting=[TanstackSorting(id="createdAt", desc=True)],
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# plain dicts work too, and iterators forward the filter
|
|
171
|
+
for user in rw.users.iter_all(filters=[{"id": "tag", "value": "VIP"}]):
|
|
172
|
+
...
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Modes: `equals`, `startsWith`, `endsWith`, `greaterThan`, `greaterThanOrEqualTo`,
|
|
176
|
+
`lessThan`, `lessThanOrEqualTo`, `between`. Anything else falls through to a `contains`
|
|
177
|
+
match rather than erroring. A `sorting` id is passed straight to the query builder, so
|
|
178
|
+
an unknown column returns 500.
|
|
179
|
+
|
|
180
|
+
## Omitted vs null
|
|
181
|
+
|
|
182
|
+
Remnawave's PATCH endpoints distinguish an absent key ("leave unchanged") from an
|
|
183
|
+
explicit `null` ("clear this field"). Passing `None` means `null`; leaving an argument
|
|
184
|
+
out sends nothing.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
rw.users.update(uuid=u.uuid, description="new") # tag untouched
|
|
188
|
+
rw.users.update(uuid=u.uuid, tag=None) # tag cleared
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Raw subscriptions
|
|
192
|
+
|
|
193
|
+
`GET /api/sub/{shortUuid}` and `/api/sub/{shortUuid}/{clientType}` are written straight
|
|
194
|
+
to the socket by the panel — the body may be base64, YAML, JSON or an encrypted blob
|
|
195
|
+
depending on the matched response rule, and the useful metadata is in the headers.
|
|
196
|
+
They return a `RawSubscription` instead of a model:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
raw = rw.sub.get_raw(user.short_uuid)
|
|
200
|
+
raw.text # decoded body
|
|
201
|
+
raw.user_info # {'upload': 0, 'download': 0, 'total': ..., 'expire': ...}
|
|
202
|
+
raw.headers["profile-title"]
|
|
203
|
+
|
|
204
|
+
from rw_sdk.models import ClientType
|
|
205
|
+
rw.sub.get_by_client_type(user.short_uuid, ClientType.SINGBOX).json()
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Errors
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from rw_sdk import errors
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
rw.users.get_by_uuid(uuid)
|
|
215
|
+
except errors.NotFoundError as e:
|
|
216
|
+
print(e.status_code, e.error_code, e.error) # 404 A019 ErrorCode.USER_NOT_FOUND
|
|
217
|
+
except errors.ValidationError as e:
|
|
218
|
+
for issue in e.errors: # zod field-level failures
|
|
219
|
+
print(issue["path"], issue["message"])
|
|
220
|
+
except errors.PermissionDeniedError:
|
|
221
|
+
... # role or token scope too narrow
|
|
222
|
+
except errors.APIConnectionError:
|
|
223
|
+
... # network, timeout, or proxy check
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
All 236 panel error codes are available as `errors.ErrorCode`. 5xx, 429 and connection
|
|
227
|
+
failures are retried twice with jittered backoff by default (`max_retries=`).
|
|
228
|
+
|
|
229
|
+
## Transport
|
|
230
|
+
|
|
231
|
+
Built on [niquests](https://github.com/jawah/niquests) — sync and async from one
|
|
232
|
+
library, HTTP/1.1, HTTP/2 and HTTP/3.
|
|
233
|
+
|
|
234
|
+
Measured against a real 2.8.1 panel, same workload, versus the same SDK on httpx:
|
|
235
|
+
|
|
236
|
+
| | httpx | niquests |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| one user by name | 1.74 ms | 1.68 ms |
|
|
239
|
+
| page of 300 users | 14.56 ms | 10.73 ms |
|
|
240
|
+
| 100 concurrent lookups | 176.6 ms | **42.7 ms** |
|
|
241
|
+
|
|
242
|
+
The concurrency gap is the one that matters — httpx's per-request overhead multiplies
|
|
243
|
+
under `asyncio.gather`, and raising its connection limits makes it worse, not better.
|
|
244
|
+
`benchmarks/http_clients.py` reproduces the comparison across httpx, urllib3, rnet,
|
|
245
|
+
curl_cffi, aiohttp and aiosonic on your own hardware and latency.
|
|
246
|
+
|
|
247
|
+
Pass your own session to tune it:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
import niquests
|
|
251
|
+
Remnawave(url, token=t, http_client=niquests.Session(base_url=url, pool_maxsize=100))
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Escape hatch
|
|
255
|
+
|
|
256
|
+
Newer panel than this SDK? Call anything directly — auth, retries, headers and error
|
|
257
|
+
mapping still apply:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
resp = rw.request("POST", "/api/some/new/endpoint", json={"foo": 1})
|
|
261
|
+
resp.json()
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Versioning
|
|
265
|
+
|
|
266
|
+
The SDK version tracks the panel API version it was generated from.
|
|
267
|
+
|
|
268
|
+
| Branch | Targets |
|
|
269
|
+
|---|---|
|
|
270
|
+
| `main` | latest released Remnawave |
|
|
271
|
+
| `2.8.1` | pinned to panel 2.8.1 |
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
pip install "rw-sdk==2.8.1.*" # stay on the 2.8.1 API
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The `X.Y.Z` part is the panel API version. A `.postN` suffix means an SDK-only fix
|
|
278
|
+
against that same API — `2.8.1` < `2.8.1.post1` < `2.8.2`.
|
|
279
|
+
|
|
280
|
+
`extra="allow"` on every model means a panel that *adds* fields will not break your
|
|
281
|
+
client; a panel that *removes* or retypes them can, which is what the pinned branches
|
|
282
|
+
are for.
|
|
283
|
+
|
|
284
|
+
## How this is built
|
|
285
|
+
|
|
286
|
+
The Remnawave OpenAPI spec is generated by nestjs-zod and inlines every schema — 268
|
|
287
|
+
components with zero `$ref`s between them. Point a stock generator at it and you get
|
|
288
|
+
either ~20 mutually-incompatible copies of the User entity (openapi-python-client) or
|
|
289
|
+
deduped-but-anonymous `Response19` classes (datamodel-code-generator). Neither unwraps
|
|
290
|
+
the `{"response": ...}` envelope every endpoint uses.
|
|
291
|
+
|
|
292
|
+
So the pipeline is three stages:
|
|
293
|
+
|
|
294
|
+
1. **`codegen/refify.py`** rewrites the spec into one with real `$ref`s, deduping shapes
|
|
295
|
+
by structural fingerprint and naming them from Remnawave's own zod contract models
|
|
296
|
+
(`libs/contract/models/*.schema.ts`, including `.extend()` / `.merge()` composition).
|
|
297
|
+
2. **datamodel-code-generator** turns that into pydantic v2 models. Model generation is
|
|
298
|
+
a solved problem; we do not reimplement it.
|
|
299
|
+
3. **`codegen/generate.py`** emits the layer no generator provides — resource classes,
|
|
300
|
+
envelope unwrapping, auto-paging iterators, raw-subscription handling, and scope docs
|
|
301
|
+
read out of the contract sources.
|
|
302
|
+
|
|
303
|
+
Regenerate for a new panel release:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
git clone --depth 1 --branch 2.9.0 https://github.com/remnawave/backend /tmp/rw
|
|
307
|
+
curl -o specs/openapi-2.9.0.json https://panel.example.com/docs-json # IS_DOCS_ENABLED=true
|
|
308
|
+
pip install "rw-sdk[codegen]"
|
|
309
|
+
python3 codegen/generate.py specs/openapi-2.9.0.json --backend /tmp/rw
|
|
310
|
+
pytest
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### What the spec gets wrong
|
|
314
|
+
|
|
315
|
+
All 186 operations were cross-checked line-by-line against the backend sources at tag
|
|
316
|
+
`2.8.1` and against a running panel. What that turned up, and how it is handled:
|
|
317
|
+
|
|
318
|
+
- **22 query parameters are missing from the document.** A hand-written `@ApiQuery`
|
|
319
|
+
decorator replaces the zod-derived parameter list wholesale, so controllers that
|
|
320
|
+
document `start`/`size` silently drop everything else. Six operations were affected —
|
|
321
|
+
four lose the whole TanStack filter/sort set, `/api/infra-billing/history` loses its
|
|
322
|
+
pagination, `/api/system/stats/bandwidth` loses `tz`. `codegen/spec_patches.py` puts
|
|
323
|
+
them back, deriving the TanStack ones from which commands import
|
|
324
|
+
`TanstackQueryRequestQuerySchema` rather than from a hardcoded list.
|
|
325
|
+
- **12 auth/passkey operations** document no 2xx response at all (no `@ApiResponse(200)`
|
|
326
|
+
decorator), so a spec-only generator types them `Any`. Their return types are read from
|
|
327
|
+
the NestJS handlers' `Promise<XxxResponseDto>` declarations instead.
|
|
328
|
+
- **`templateJson`** is `z.nullable(z.unknown())` — optional in zod, but emitted as
|
|
329
|
+
*required*, while the panel omits the key entirely. Trusting the spec makes
|
|
330
|
+
`subscription_template.get_all()` raise on a real panel. `z.unknown()`-shaped
|
|
331
|
+
properties are dropped from `required`.
|
|
332
|
+
- **`type: number`** (zod `z.number()` without `.int()`) covers ids and byte counters.
|
|
333
|
+
Mapped to `int | float` so an 8-byte traffic total stays exact instead of becoming
|
|
334
|
+
a float.
|
|
335
|
+
- **A short page is not the last page.** `getAllSubscriptions` skips users whose
|
|
336
|
+
subscription fails to build but still reports `total` as the user count, so a page can
|
|
337
|
+
come back short — or empty — with rows remaining. The iterators advance by the
|
|
338
|
+
requested page size and stop on `total`, never on a short page.
|
|
339
|
+
- **11 endpoints are unreachable with an API token** (`@Roles(ROLE.ADMIN)`); flagged in
|
|
340
|
+
their docstrings and in `ENDPOINTS[...]["admin_only"]`.
|
|
341
|
+
- **Documented status codes are unreliable** — several POSTs documented as `200` really
|
|
342
|
+
return `201`, and some "not found" cases arrive as `500` with an `errorCode`. The
|
|
343
|
+
client accepts any 2xx and maps exceptions by HTTP status, never by the documented
|
|
344
|
+
response list.
|
|
345
|
+
- **A 5xx carrying an `errorCode` is not retried.** `HttpExceptionFilter` stamps that
|
|
346
|
+
field only onto deliberate failures — config-profile (`A112`/`A061`) and settings
|
|
347
|
+
(`A199`/`A193`) validation both surface as coded 500s — so retrying only delays the
|
|
348
|
+
error. Uncoded 5xx, 429 and connection failures still retry.
|
|
349
|
+
- **The two raw subscription endpoints** are `@Res()` handlers with no schema; they are
|
|
350
|
+
special-cased rather than typed as `Any`.
|
|
351
|
+
|
|
352
|
+
## Running against a local panel
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
git clone --depth 1 --branch 2.8.1 https://github.com/remnawave/backend /tmp/rw
|
|
356
|
+
cd /tmp/rw && cp .env.sample .env # set APP_SECRET, IS_DOCS_ENABLED=true
|
|
357
|
+
docker compose -f docker-compose-prod.yml up -d
|
|
358
|
+
|
|
359
|
+
export RW_SDK_TEST_URL=http://127.0.0.1:3000
|
|
360
|
+
export RW_SDK_TEST_TOKEN=... # an API token from the panel
|
|
361
|
+
export RW_SDK_TEST_ADMIN_TOKEN=... # optional: admin JWT, unlocks the admin-only tests
|
|
362
|
+
pytest tests/test_live.py
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
The panel runs in production mode, so requests need the forwarded headers — the live
|
|
366
|
+
tests pass `proxy_headers="always"`.
|
|
367
|
+
|
|
368
|
+
## License
|
|
369
|
+
|
|
370
|
+
MIT. Remnawave itself is AGPL-3.0; this is an independent client, not affiliated with
|
|
371
|
+
the Remnawave project.
|
|
372
|
+
|
|
373
|
+
[guard]: https://github.com/remnawave/backend/blob/2.8.1/src/common/guards/jwt-guards/def-jwt-guard.ts
|
|
374
|
+
[proxy]: https://github.com/remnawave/backend/blob/2.8.1/src/common/middlewares/proxy-check.middleware.ts
|