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.
Files changed (53) hide show
  1. rw_sdk-2.8.1/.gitignore +15 -0
  2. rw_sdk-2.8.1/LICENSE +21 -0
  3. rw_sdk-2.8.1/PKG-INFO +374 -0
  4. rw_sdk-2.8.1/README.md +339 -0
  5. rw_sdk-2.8.1/codegen/generate.py +825 -0
  6. rw_sdk-2.8.1/codegen/naming.py +146 -0
  7. rw_sdk-2.8.1/codegen/refify.py +391 -0
  8. rw_sdk-2.8.1/codegen/spec_patches.py +178 -0
  9. rw_sdk-2.8.1/codegen/version.py +38 -0
  10. rw_sdk-2.8.1/codegen/zod_names.py +193 -0
  11. rw_sdk-2.8.1/pyproject.toml +72 -0
  12. rw_sdk-2.8.1/specs/openapi-2.8.1.json +48947 -0
  13. rw_sdk-2.8.1/src/rw_sdk/__init__.py +60 -0
  14. rw_sdk-2.8.1/src/rw_sdk/_error_codes.py +479 -0
  15. rw_sdk-2.8.1/src/rw_sdk/_meta.py +2427 -0
  16. rw_sdk-2.8.1/src/rw_sdk/_resource.py +137 -0
  17. rw_sdk-2.8.1/src/rw_sdk/_resources_mixin.py +128 -0
  18. rw_sdk-2.8.1/src/rw_sdk/_types.py +91 -0
  19. rw_sdk-2.8.1/src/rw_sdk/_version.py +3 -0
  20. rw_sdk-2.8.1/src/rw_sdk/client.py +346 -0
  21. rw_sdk-2.8.1/src/rw_sdk/errors.py +185 -0
  22. rw_sdk-2.8.1/src/rw_sdk/models.py +6135 -0
  23. rw_sdk-2.8.1/src/rw_sdk/py.typed +0 -0
  24. rw_sdk-2.8.1/src/rw_sdk/resources/__init__.py +89 -0
  25. rw_sdk-2.8.1/src/rw_sdk/resources/api_tokens.py +187 -0
  26. rw_sdk-2.8.1/src/rw_sdk/resources/auth.py +236 -0
  27. rw_sdk-2.8.1/src/rw_sdk/resources/bandwidth_stats.py +354 -0
  28. rw_sdk-2.8.1/src/rw_sdk/resources/config_profiles.py +285 -0
  29. rw_sdk-2.8.1/src/rw_sdk/resources/external_squads.py +286 -0
  30. rw_sdk-2.8.1/src/rw_sdk/resources/hosts.py +501 -0
  31. rw_sdk-2.8.1/src/rw_sdk/resources/hosts_bulk.py +280 -0
  32. rw_sdk-2.8.1/src/rw_sdk/resources/hwid.py +410 -0
  33. rw_sdk-2.8.1/src/rw_sdk/resources/infra_billing.py +500 -0
  34. rw_sdk-2.8.1/src/rw_sdk/resources/internal_squads.py +290 -0
  35. rw_sdk-2.8.1/src/rw_sdk/resources/ip_control.py +186 -0
  36. rw_sdk-2.8.1/src/rw_sdk/resources/keygen.py +43 -0
  37. rw_sdk-2.8.1/src/rw_sdk/resources/metadata.py +155 -0
  38. rw_sdk-2.8.1/src/rw_sdk/resources/node_plugins.py +468 -0
  39. rw_sdk-2.8.1/src/rw_sdk/resources/nodes.py +627 -0
  40. rw_sdk-2.8.1/src/rw_sdk/resources/passkeys.py +212 -0
  41. rw_sdk-2.8.1/src/rw_sdk/resources/settings.py +117 -0
  42. rw_sdk-2.8.1/src/rw_sdk/resources/snippets.py +137 -0
  43. rw_sdk-2.8.1/src/rw_sdk/resources/sub.py +102 -0
  44. rw_sdk-2.8.1/src/rw_sdk/resources/subscription_page_configs.py +242 -0
  45. rw_sdk-2.8.1/src/rw_sdk/resources/subscription_request_history.py +168 -0
  46. rw_sdk-2.8.1/src/rw_sdk/resources/subscription_settings.py +136 -0
  47. rw_sdk-2.8.1/src/rw_sdk/resources/subscription_template.py +229 -0
  48. rw_sdk-2.8.1/src/rw_sdk/resources/subscriptions.py +319 -0
  49. rw_sdk-2.8.1/src/rw_sdk/resources/system.py +278 -0
  50. rw_sdk-2.8.1/src/rw_sdk/resources/users.py +1032 -0
  51. rw_sdk-2.8.1/src/rw_sdk/resources/users_bulk.py +405 -0
  52. rw_sdk-2.8.1/tests/test_contract.py +384 -0
  53. rw_sdk-2.8.1/tests/test_live.py +351 -0
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ *.refs.json
14
+ .env
15
+ .DS_Store
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