noxy-sdk 1.0.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.
- noxy_sdk-1.0.0/LICENSE +21 -0
- noxy_sdk-1.0.0/PKG-INFO +148 -0
- noxy_sdk-1.0.0/README.md +120 -0
- noxy_sdk-1.0.0/noxy/__init__.py +42 -0
- noxy_sdk-1.0.0/noxy/client.py +55 -0
- noxy_sdk-1.0.0/noxy/config.py +16 -0
- noxy_sdk-1.0.0/noxy/crypto.py +28 -0
- noxy_sdk-1.0.0/noxy/grpc/__init__.py +0 -0
- noxy_sdk-1.0.0/noxy/grpc/noxy_pb2.py +54 -0
- noxy_sdk-1.0.0/noxy/grpc/noxy_pb2_grpc.py +183 -0
- noxy_sdk-1.0.0/noxy/kyber_provider.py +19 -0
- noxy_sdk-1.0.0/noxy/retries.py +64 -0
- noxy_sdk-1.0.0/noxy/services/__init__.py +7 -0
- noxy_sdk-1.0.0/noxy/services/identity.py +34 -0
- noxy_sdk-1.0.0/noxy/services/push.py +87 -0
- noxy_sdk-1.0.0/noxy/services/quota.py +25 -0
- noxy_sdk-1.0.0/noxy/transport.py +32 -0
- noxy_sdk-1.0.0/noxy/types.py +55 -0
- noxy_sdk-1.0.0/noxy_sdk.egg-info/PKG-INFO +148 -0
- noxy_sdk-1.0.0/noxy_sdk.egg-info/SOURCES.txt +23 -0
- noxy_sdk-1.0.0/noxy_sdk.egg-info/dependency_links.txt +1 -0
- noxy_sdk-1.0.0/noxy_sdk.egg-info/requires.txt +9 -0
- noxy_sdk-1.0.0/noxy_sdk.egg-info/top_level.txt +1 -0
- noxy_sdk-1.0.0/pyproject.toml +41 -0
- noxy_sdk-1.0.0/setup.cfg +4 -0
noxy_sdk-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Noxy Network (noxy.network)
|
|
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.
|
noxy_sdk-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: noxy-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Backend SDK for Python servers to integrate with the Noxy push notification network
|
|
5
|
+
Author: Noxy Network
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: noxy,push-notifications,web3,post-quantum,grpc
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Security :: Cryptography
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: grpcio>=1.60.0
|
|
20
|
+
Requires-Dist: grpcio-tools>=1.60.0
|
|
21
|
+
Requires-Dist: protobuf>=4.25.0
|
|
22
|
+
Requires-Dist: kybercffi>=1.0.0
|
|
23
|
+
Requires-Dist: cryptography>=42.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# π¦ @noxy-network/python-sdk
|
|
30
|
+
|
|
31
|
+
Backend SDK for Python servers to integrate with the [Noxy](https://noxy.network) push notification network. Send encrypted push notifications to Web3 wallet addresses via the Noxy relay infrastructure.
|
|
32
|
+
|
|
33
|
+
## Overview
|
|
34
|
+
|
|
35
|
+
This SDK enables server-side applications to:
|
|
36
|
+
|
|
37
|
+
- **Send push notifications** to users by their Web3 wallet address (EVM `0x` format)
|
|
38
|
+
- **Query quota usage** for your application's relay allocation
|
|
39
|
+
- **Resolve identity devices** to deliver notifications to all registered devices
|
|
40
|
+
|
|
41
|
+
Communication with the Noxy relay is performed over **gRPC** using Protocol Buffers. All notifications are **encrypted end-to-end** on the backend before transmission; decryption occurs only on the recipient's Noxy device. The SDK uses **post-quantum encryption** (Kyber768) to protect payloads against future quantum attacks.
|
|
42
|
+
|
|
43
|
+
## Architecture
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
βββββββββββββββββββ gRPC (TLS) βββββββββββββββββββ E2E Encrypted βββββββββββββββββββ
|
|
47
|
+
β Your Backend β βββββββββββββββββββΊ β Noxy Relay β ββββββββββββββββββββΊ β Noxy Device β
|
|
48
|
+
β (this SDK) β PushNotification β β Ciphertext only β (decrypts) β
|
|
49
|
+
β β GetQuota β β β β
|
|
50
|
+
β β GetIdentityDevicesβ β β β
|
|
51
|
+
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **Encryption**: Kyber768 (post-quantum KEM) + AES-256-GCM. Each notification is encrypted per-device using the device's post-quantum public key.
|
|
55
|
+
- **Transport**: gRPC over TLS with Bearer token authentication.
|
|
56
|
+
- **Relay**: The Noxy relay forwards ciphertext only; it cannot decrypt notification payloads.
|
|
57
|
+
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
- Python **>= 3.10**
|
|
61
|
+
- C compiler (for kybercffi)
|
|
62
|
+
|
|
63
|
+
## π Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install noxy-sdk
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## π Quick Start
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from noxy import NoxyConfig, init_noxy_client
|
|
73
|
+
|
|
74
|
+
config = NoxyConfig(
|
|
75
|
+
endpoint="https://relay.noxy.network",
|
|
76
|
+
auth_token="your-api-token",
|
|
77
|
+
notification_ttl_seconds=3600,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
with init_noxy_client(config) as client:
|
|
81
|
+
# Send a push notification to a wallet address
|
|
82
|
+
results = client.send_push(
|
|
83
|
+
"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1",
|
|
84
|
+
{"title": "New message", "body": "You have a new notification", "data": {"action": "open_chat", "id": "123"}},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Check quota usage
|
|
88
|
+
quota = client.get_quota()
|
|
89
|
+
print(f"{quota.quota_remaining} remaining")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
| Option | Type | Required | Description |
|
|
95
|
+
|--------|------|----------|-------------|
|
|
96
|
+
| `endpoint` | `str` | Yes | Noxy relay gRPC endpoint (e.g. `https://relay.noxy.network`). Scheme is stripped; TLS is used by default. |
|
|
97
|
+
| `auth_token` | `str` | Yes | Bearer token for relay authentication. Sent in the `Authorization` header on every request. |
|
|
98
|
+
| `notification_ttl_seconds` | `int` | Yes | Time-to-live for notifications in seconds. |
|
|
99
|
+
|
|
100
|
+
## API Reference
|
|
101
|
+
|
|
102
|
+
### `init_noxy_client(config: NoxyConfig) -> NoxyPushClient`
|
|
103
|
+
|
|
104
|
+
Initializes the SDK client. Normalizes the endpoint and establishes the gRPC connection.
|
|
105
|
+
|
|
106
|
+
### `NoxyPushClient`
|
|
107
|
+
|
|
108
|
+
#### `send_push(identity_address, push_notification) -> List[NoxyPushResponse]`
|
|
109
|
+
|
|
110
|
+
Sends a push notification to all devices registered for the given Web3 identity address.
|
|
111
|
+
|
|
112
|
+
- **`identity_address`**: EVM address in `0x` format (e.g. `0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1`)
|
|
113
|
+
- **`push_notification`**: Any JSON-serializable object (e.g. `dict`). Encrypted before transmission.
|
|
114
|
+
- **Returns**: List of `NoxyPushResponse` per device, with `status` and `request_id`.
|
|
115
|
+
|
|
116
|
+
#### `get_quota() -> NoxyGetQuotaResponse`
|
|
117
|
+
|
|
118
|
+
Returns quota usage for your application.
|
|
119
|
+
|
|
120
|
+
- **Returns**: `NoxyGetQuotaResponse` with `request_id`, `app_name`, `quota_total`, `quota_remaining`, `status`.
|
|
121
|
+
|
|
122
|
+
### Types
|
|
123
|
+
|
|
124
|
+
- **`NoxyPushDeliveryStatus`**: `DELIVERED` (0) | `QUEUED` (1) | `NO_DEVICES` (2) | `REJECTED` (3) | `ERROR` (4)
|
|
125
|
+
- **`NoxyQuotaStatus`**: `QUOTA_ACTIVE` (0) | `QUOTA_SUSPENDED` (1) | `QUOTA_DELETED` (2)
|
|
126
|
+
|
|
127
|
+
## Encryption Details
|
|
128
|
+
|
|
129
|
+
1. **Key encapsulation**: For each device, the SDK encapsulates a shared secret using the device's Kyber768 post-quantum public key (`pq_public_key`).
|
|
130
|
+
2. **Key derivation**: The shared secret is expanded via HKDF-SHA256 to a 256-bit AES key.
|
|
131
|
+
3. **Payload encryption**: The notification payload (JSON) is encrypted with AES-256-GCM. The ciphertext includes the GCM auth tag appended for integrity verification.
|
|
132
|
+
4. **Transmission**: Only `kyber_ct`, `nonce`, and `ciphertext` are sent to the relay. The relay cannot decrypt; only the target device (with its secret key) can decrypt.
|
|
133
|
+
|
|
134
|
+
## Building from source
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
python -m venv .venv
|
|
138
|
+
source .venv/bin/activate # or .venv\Scripts\activate on Windows
|
|
139
|
+
pip install -e .
|
|
140
|
+
|
|
141
|
+
# Regenerate proto (if proto file changes)
|
|
142
|
+
python -m grpc_tools.protoc -I proto --python_out=noxy/grpc --grpc_python_out=noxy/grpc proto/noxy.proto
|
|
143
|
+
# Fix import in noxy/grpc/noxy_pb2_grpc.py: change "import noxy_pb2" to "from . import noxy_pb2"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## π License
|
|
147
|
+
|
|
148
|
+
MIT
|
noxy_sdk-1.0.0/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# π¦ @noxy-network/python-sdk
|
|
2
|
+
|
|
3
|
+
Backend SDK for Python servers to integrate with the [Noxy](https://noxy.network) push notification network. Send encrypted push notifications to Web3 wallet addresses via the Noxy relay infrastructure.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This SDK enables server-side applications to:
|
|
8
|
+
|
|
9
|
+
- **Send push notifications** to users by their Web3 wallet address (EVM `0x` format)
|
|
10
|
+
- **Query quota usage** for your application's relay allocation
|
|
11
|
+
- **Resolve identity devices** to deliver notifications to all registered devices
|
|
12
|
+
|
|
13
|
+
Communication with the Noxy relay is performed over **gRPC** using Protocol Buffers. All notifications are **encrypted end-to-end** on the backend before transmission; decryption occurs only on the recipient's Noxy device. The SDK uses **post-quantum encryption** (Kyber768) to protect payloads against future quantum attacks.
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
βββββββββββββββββββ gRPC (TLS) βββββββββββββββββββ E2E Encrypted βββββββββββββββββββ
|
|
19
|
+
β Your Backend β βββββββββββββββββββΊ β Noxy Relay β ββββββββββββββββββββΊ β Noxy Device β
|
|
20
|
+
β (this SDK) β PushNotification β β Ciphertext only β (decrypts) β
|
|
21
|
+
β β GetQuota β β β β
|
|
22
|
+
β β GetIdentityDevicesβ β β β
|
|
23
|
+
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- **Encryption**: Kyber768 (post-quantum KEM) + AES-256-GCM. Each notification is encrypted per-device using the device's post-quantum public key.
|
|
27
|
+
- **Transport**: gRPC over TLS with Bearer token authentication.
|
|
28
|
+
- **Relay**: The Noxy relay forwards ciphertext only; it cannot decrypt notification payloads.
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Python **>= 3.10**
|
|
33
|
+
- C compiler (for kybercffi)
|
|
34
|
+
|
|
35
|
+
## π Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install noxy-sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## π Quick Start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from noxy import NoxyConfig, init_noxy_client
|
|
45
|
+
|
|
46
|
+
config = NoxyConfig(
|
|
47
|
+
endpoint="https://relay.noxy.network",
|
|
48
|
+
auth_token="your-api-token",
|
|
49
|
+
notification_ttl_seconds=3600,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
with init_noxy_client(config) as client:
|
|
53
|
+
# Send a push notification to a wallet address
|
|
54
|
+
results = client.send_push(
|
|
55
|
+
"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1",
|
|
56
|
+
{"title": "New message", "body": "You have a new notification", "data": {"action": "open_chat", "id": "123"}},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Check quota usage
|
|
60
|
+
quota = client.get_quota()
|
|
61
|
+
print(f"{quota.quota_remaining} remaining")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
| Option | Type | Required | Description |
|
|
67
|
+
|--------|------|----------|-------------|
|
|
68
|
+
| `endpoint` | `str` | Yes | Noxy relay gRPC endpoint (e.g. `https://relay.noxy.network`). Scheme is stripped; TLS is used by default. |
|
|
69
|
+
| `auth_token` | `str` | Yes | Bearer token for relay authentication. Sent in the `Authorization` header on every request. |
|
|
70
|
+
| `notification_ttl_seconds` | `int` | Yes | Time-to-live for notifications in seconds. |
|
|
71
|
+
|
|
72
|
+
## API Reference
|
|
73
|
+
|
|
74
|
+
### `init_noxy_client(config: NoxyConfig) -> NoxyPushClient`
|
|
75
|
+
|
|
76
|
+
Initializes the SDK client. Normalizes the endpoint and establishes the gRPC connection.
|
|
77
|
+
|
|
78
|
+
### `NoxyPushClient`
|
|
79
|
+
|
|
80
|
+
#### `send_push(identity_address, push_notification) -> List[NoxyPushResponse]`
|
|
81
|
+
|
|
82
|
+
Sends a push notification to all devices registered for the given Web3 identity address.
|
|
83
|
+
|
|
84
|
+
- **`identity_address`**: EVM address in `0x` format (e.g. `0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1`)
|
|
85
|
+
- **`push_notification`**: Any JSON-serializable object (e.g. `dict`). Encrypted before transmission.
|
|
86
|
+
- **Returns**: List of `NoxyPushResponse` per device, with `status` and `request_id`.
|
|
87
|
+
|
|
88
|
+
#### `get_quota() -> NoxyGetQuotaResponse`
|
|
89
|
+
|
|
90
|
+
Returns quota usage for your application.
|
|
91
|
+
|
|
92
|
+
- **Returns**: `NoxyGetQuotaResponse` with `request_id`, `app_name`, `quota_total`, `quota_remaining`, `status`.
|
|
93
|
+
|
|
94
|
+
### Types
|
|
95
|
+
|
|
96
|
+
- **`NoxyPushDeliveryStatus`**: `DELIVERED` (0) | `QUEUED` (1) | `NO_DEVICES` (2) | `REJECTED` (3) | `ERROR` (4)
|
|
97
|
+
- **`NoxyQuotaStatus`**: `QUOTA_ACTIVE` (0) | `QUOTA_SUSPENDED` (1) | `QUOTA_DELETED` (2)
|
|
98
|
+
|
|
99
|
+
## Encryption Details
|
|
100
|
+
|
|
101
|
+
1. **Key encapsulation**: For each device, the SDK encapsulates a shared secret using the device's Kyber768 post-quantum public key (`pq_public_key`).
|
|
102
|
+
2. **Key derivation**: The shared secret is expanded via HKDF-SHA256 to a 256-bit AES key.
|
|
103
|
+
3. **Payload encryption**: The notification payload (JSON) is encrypted with AES-256-GCM. The ciphertext includes the GCM auth tag appended for integrity verification.
|
|
104
|
+
4. **Transmission**: Only `kyber_ct`, `nonce`, and `ciphertext` are sent to the relay. The relay cannot decrypt; only the target device (with its secret key) can decrypt.
|
|
105
|
+
|
|
106
|
+
## Building from source
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python -m venv .venv
|
|
110
|
+
source .venv/bin/activate # or .venv\Scripts\activate on Windows
|
|
111
|
+
pip install -e .
|
|
112
|
+
|
|
113
|
+
# Regenerate proto (if proto file changes)
|
|
114
|
+
python -m grpc_tools.protoc -I proto --python_out=noxy/grpc --grpc_python_out=noxy/grpc proto/noxy.proto
|
|
115
|
+
# Fix import in noxy/grpc/noxy_pb2_grpc.py: change "import noxy_pb2" to "from . import noxy_pb2"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## π License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Noxy SDK - Backend SDK for Python servers to integrate with the Noxy push notification network.
|
|
2
|
+
|
|
3
|
+
Send encrypted push notifications to Web3 wallet addresses via the Noxy relay.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from noxy.client import NoxyPushClient
|
|
8
|
+
from noxy.config import NoxyConfig
|
|
9
|
+
from noxy.types import (
|
|
10
|
+
NoxyGetQuotaResponse,
|
|
11
|
+
NoxyIdentityDevice,
|
|
12
|
+
NoxyPushDeliveryStatus,
|
|
13
|
+
NoxyPushResponse,
|
|
14
|
+
NoxyQuotaStatus,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def init_noxy_client(config: NoxyConfig) -> NoxyPushClient:
|
|
19
|
+
"""Initialize the Noxy client.
|
|
20
|
+
|
|
21
|
+
Normalizes the endpoint (strips https:// or http://) and establishes the gRPC connection.
|
|
22
|
+
"""
|
|
23
|
+
endpoint = re.sub(r"^https?://", "", config.endpoint)
|
|
24
|
+
endpoint = endpoint.rstrip("/")
|
|
25
|
+
normalized_config = NoxyConfig(
|
|
26
|
+
endpoint=endpoint,
|
|
27
|
+
auth_token=config.auth_token,
|
|
28
|
+
notification_ttl_seconds=config.notification_ttl_seconds,
|
|
29
|
+
)
|
|
30
|
+
return NoxyPushClient(normalized_config)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"init_noxy_client",
|
|
35
|
+
"NoxyPushClient",
|
|
36
|
+
"NoxyConfig",
|
|
37
|
+
"NoxyPushResponse",
|
|
38
|
+
"NoxyPushDeliveryStatus",
|
|
39
|
+
"NoxyGetQuotaResponse",
|
|
40
|
+
"NoxyQuotaStatus",
|
|
41
|
+
"NoxyIdentityDevice",
|
|
42
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Noxy push client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, List
|
|
4
|
+
|
|
5
|
+
from noxy.config import NoxyConfig
|
|
6
|
+
from noxy.kyber_provider import KyberProvider
|
|
7
|
+
from noxy.services.identity import IdentityService
|
|
8
|
+
from noxy.services.push import PushService
|
|
9
|
+
from noxy.services.quota import QuotaService
|
|
10
|
+
from noxy.transport import create_client
|
|
11
|
+
from noxy.types import NoxyGetQuotaResponse, NoxyPushResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NoxyPushClient:
|
|
15
|
+
"""Main SDK client for sending push notifications."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, config: NoxyConfig) -> None:
|
|
18
|
+
self._config = config
|
|
19
|
+
self._stub, self._channel = create_client(config.endpoint)
|
|
20
|
+
self._identity = IdentityService()
|
|
21
|
+
self._push = PushService(KyberProvider())
|
|
22
|
+
self._quota = QuotaService()
|
|
23
|
+
|
|
24
|
+
def send_push(
|
|
25
|
+
self,
|
|
26
|
+
identity_address: str,
|
|
27
|
+
push_notification: Any,
|
|
28
|
+
) -> List[NoxyPushResponse]:
|
|
29
|
+
"""Send a push notification to all devices registered for the given Web3 identity address."""
|
|
30
|
+
devices = self._identity.get_devices(
|
|
31
|
+
self._stub,
|
|
32
|
+
self._config.auth_token,
|
|
33
|
+
identity_address,
|
|
34
|
+
)
|
|
35
|
+
return self._push.send(
|
|
36
|
+
self._stub,
|
|
37
|
+
self._config.auth_token,
|
|
38
|
+
devices,
|
|
39
|
+
push_notification,
|
|
40
|
+
self._config.notification_ttl_seconds,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def get_quota(self) -> NoxyGetQuotaResponse:
|
|
44
|
+
"""Return quota usage for your application."""
|
|
45
|
+
return self._quota.get(self._stub, self._config.auth_token)
|
|
46
|
+
|
|
47
|
+
def close(self) -> None:
|
|
48
|
+
"""Close the gRPC channel."""
|
|
49
|
+
self._channel.close()
|
|
50
|
+
|
|
51
|
+
def __enter__(self) -> "NoxyPushClient":
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, *args: Any) -> None:
|
|
55
|
+
self.close()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Configuration for the Noxy SDK client."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class NoxyConfig:
|
|
8
|
+
"""Configuration for the Noxy SDK client."""
|
|
9
|
+
|
|
10
|
+
# Noxy relay gRPC endpoint (e.g. "https://relay.noxy.network:443").
|
|
11
|
+
# Scheme is stripped; TLS is used by default.
|
|
12
|
+
endpoint: str
|
|
13
|
+
# Bearer token for relay authentication.
|
|
14
|
+
auth_token: str
|
|
15
|
+
# Time-to-live for notifications in seconds.
|
|
16
|
+
notification_ttl_seconds: int
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""AES-256-GCM encryption with HKDF key derivation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
7
|
+
from cryptography.hazmat.primitives import hashes
|
|
8
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def encrypt(shared_secret: bytes, plaintext: bytes) -> Tuple[bytes, bytes]:
|
|
12
|
+
"""Encrypt plaintext with AES-256-GCM using a key derived from the shared secret via HKDF-SHA256.
|
|
13
|
+
|
|
14
|
+
Returns (ciphertext_with_auth_tag, nonce). The auth tag (16 bytes) is appended to ciphertext.
|
|
15
|
+
"""
|
|
16
|
+
hkdf = HKDF(
|
|
17
|
+
algorithm=hashes.SHA256(),
|
|
18
|
+
length=32,
|
|
19
|
+
salt=None,
|
|
20
|
+
info=b"",
|
|
21
|
+
)
|
|
22
|
+
key = hkdf.derive(shared_secret)
|
|
23
|
+
|
|
24
|
+
nonce = os.urandom(12)
|
|
25
|
+
aesgcm = AESGCM(key)
|
|
26
|
+
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
|
|
27
|
+
|
|
28
|
+
return ciphertext, nonce
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# NO CHECKED-IN PROTOBUF GENCODE
|
|
4
|
+
# source: noxy.proto
|
|
5
|
+
# Protobuf Python Version: 6.31.1
|
|
6
|
+
"""Generated protocol buffer code."""
|
|
7
|
+
from google.protobuf import descriptor as _descriptor
|
|
8
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
9
|
+
from google.protobuf import runtime_version as _runtime_version
|
|
10
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
11
|
+
from google.protobuf.internal import builder as _builder
|
|
12
|
+
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
|
+
_runtime_version.Domain.PUBLIC,
|
|
14
|
+
6,
|
|
15
|
+
31,
|
|
16
|
+
1,
|
|
17
|
+
'',
|
|
18
|
+
'noxy.proto'
|
|
19
|
+
)
|
|
20
|
+
# @@protoc_insertion_point(imports)
|
|
21
|
+
|
|
22
|
+
_sym_db = _symbol_database.Default()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nnoxy.proto\x12\tnoxy.push\"\x91\x01\n\x17PushNotificationRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x12\n\nciphertext\x18\x02 \x01(\x0c\x12\x13\n\x0bttl_seconds\x18\x03 \x01(\r\x12\x18\n\x10target_device_id\x18\x04 \x01(\t\x12\x10\n\x08kyber_ct\x18\x05 \x01(\x0c\x12\r\n\x05nonce\x18\x06 \x01(\x0c\"M\n\x0cPushResponse\x12)\n\x06status\x18\x01 \x01(\x0e\x32\x19.noxy.push.DeliveryStatus\x12\x12\n\nrequest_id\x18\x02 \x01(\t\"%\n\x0fGetQuotaRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\"\x8e\x01\n\x10GetQuotaResponse\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x10\n\x08\x61pp_name\x18\x02 \x01(\t\x12\x13\n\x0bquota_total\x18\x03 \x01(\x04\x12\x17\n\x0fquota_remaining\x18\x04 \x01(\x04\x12&\n\x06status\x18\x05 \x01(\x0e\x32\x16.noxy.push.QuotaStatus\"D\n\x19GetIdentityDevicesRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x13\n\x0bidentity_id\x18\x02 \x01(\t\"N\n\x0eIdentityDevice\x12\x11\n\tdevice_id\x18\x01 \x01(\t\x12\x12\n\npublic_key\x18\x02 \x01(\x0c\x12\x15\n\rpq_public_key\x18\x03 \x01(\x0c\"\\\n\x1aGetIdentityDevicesResponse\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12*\n\x07\x64\x65vices\x18\x02 \x03(\x0b\x32\x19.noxy.push.IdentityDevice*T\n\x0e\x44\x65liveryStatus\x12\r\n\tDELIVERED\x10\x00\x12\n\n\x06QUEUED\x10\x01\x12\x0e\n\nNO_DEVICES\x10\x02\x12\x0c\n\x08REJECTED\x10\x03\x12\t\n\x05\x45RROR\x10\x04*G\n\x0bQuotaStatus\x12\x10\n\x0cQUOTA_ACTIVE\x10\x00\x12\x13\n\x0fQUOTA_SUSPENDED\x10\x01\x12\x11\n\rQUOTA_DELETED\x10\x02\x32\x86\x02\n\x0bPushService\x12O\n\x10PushNotification\x12\".noxy.push.PushNotificationRequest\x1a\x17.noxy.push.PushResponse\x12\x43\n\x08GetQuota\x12\x1a.noxy.push.GetQuotaRequest\x1a\x1b.noxy.push.GetQuotaResponse\x12\x61\n\x12GetIdentityDevices\x12$.noxy.push.GetIdentityDevicesRequest\x1a%.noxy.push.GetIdentityDevicesResponseb\x06proto3')
|
|
28
|
+
|
|
29
|
+
_globals = globals()
|
|
30
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
31
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'noxy_pb2', _globals)
|
|
32
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
|
33
|
+
DESCRIPTOR._loaded_options = None
|
|
34
|
+
_globals['_DELIVERYSTATUS']._serialized_start=680
|
|
35
|
+
_globals['_DELIVERYSTATUS']._serialized_end=764
|
|
36
|
+
_globals['_QUOTASTATUS']._serialized_start=766
|
|
37
|
+
_globals['_QUOTASTATUS']._serialized_end=837
|
|
38
|
+
_globals['_PUSHNOTIFICATIONREQUEST']._serialized_start=26
|
|
39
|
+
_globals['_PUSHNOTIFICATIONREQUEST']._serialized_end=171
|
|
40
|
+
_globals['_PUSHRESPONSE']._serialized_start=173
|
|
41
|
+
_globals['_PUSHRESPONSE']._serialized_end=250
|
|
42
|
+
_globals['_GETQUOTAREQUEST']._serialized_start=252
|
|
43
|
+
_globals['_GETQUOTAREQUEST']._serialized_end=289
|
|
44
|
+
_globals['_GETQUOTARESPONSE']._serialized_start=292
|
|
45
|
+
_globals['_GETQUOTARESPONSE']._serialized_end=434
|
|
46
|
+
_globals['_GETIDENTITYDEVICESREQUEST']._serialized_start=436
|
|
47
|
+
_globals['_GETIDENTITYDEVICESREQUEST']._serialized_end=504
|
|
48
|
+
_globals['_IDENTITYDEVICE']._serialized_start=506
|
|
49
|
+
_globals['_IDENTITYDEVICE']._serialized_end=584
|
|
50
|
+
_globals['_GETIDENTITYDEVICESRESPONSE']._serialized_start=586
|
|
51
|
+
_globals['_GETIDENTITYDEVICESRESPONSE']._serialized_end=678
|
|
52
|
+
_globals['_PUSHSERVICE']._serialized_start=840
|
|
53
|
+
_globals['_PUSHSERVICE']._serialized_end=1102
|
|
54
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
|
2
|
+
"""Client and server classes corresponding to protobuf-defined services."""
|
|
3
|
+
import grpc
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
from . import noxy_pb2 as noxy__pb2
|
|
7
|
+
|
|
8
|
+
GRPC_GENERATED_VERSION = '1.78.0'
|
|
9
|
+
GRPC_VERSION = grpc.__version__
|
|
10
|
+
_version_not_supported = False
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from grpc._utilities import first_version_is_lower
|
|
14
|
+
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
|
15
|
+
except ImportError:
|
|
16
|
+
_version_not_supported = True
|
|
17
|
+
|
|
18
|
+
if _version_not_supported:
|
|
19
|
+
raise RuntimeError(
|
|
20
|
+
f'The grpc package installed is at version {GRPC_VERSION},'
|
|
21
|
+
+ ' but the generated code in noxy_pb2_grpc.py depends on'
|
|
22
|
+
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
|
23
|
+
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
|
24
|
+
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PushServiceStub(object):
|
|
29
|
+
"""Missing associated documentation comment in .proto file."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, channel):
|
|
32
|
+
"""Constructor.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
channel: A grpc.Channel.
|
|
36
|
+
"""
|
|
37
|
+
self.PushNotification = channel.unary_unary(
|
|
38
|
+
'/noxy.push.PushService/PushNotification',
|
|
39
|
+
request_serializer=noxy__pb2.PushNotificationRequest.SerializeToString,
|
|
40
|
+
response_deserializer=noxy__pb2.PushResponse.FromString,
|
|
41
|
+
_registered_method=True)
|
|
42
|
+
self.GetQuota = channel.unary_unary(
|
|
43
|
+
'/noxy.push.PushService/GetQuota',
|
|
44
|
+
request_serializer=noxy__pb2.GetQuotaRequest.SerializeToString,
|
|
45
|
+
response_deserializer=noxy__pb2.GetQuotaResponse.FromString,
|
|
46
|
+
_registered_method=True)
|
|
47
|
+
self.GetIdentityDevices = channel.unary_unary(
|
|
48
|
+
'/noxy.push.PushService/GetIdentityDevices',
|
|
49
|
+
request_serializer=noxy__pb2.GetIdentityDevicesRequest.SerializeToString,
|
|
50
|
+
response_deserializer=noxy__pb2.GetIdentityDevicesResponse.FromString,
|
|
51
|
+
_registered_method=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PushServiceServicer(object):
|
|
55
|
+
"""Missing associated documentation comment in .proto file."""
|
|
56
|
+
|
|
57
|
+
def PushNotification(self, request, context):
|
|
58
|
+
"""Missing associated documentation comment in .proto file."""
|
|
59
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
60
|
+
context.set_details('Method not implemented!')
|
|
61
|
+
raise NotImplementedError('Method not implemented!')
|
|
62
|
+
|
|
63
|
+
def GetQuota(self, request, context):
|
|
64
|
+
"""Missing associated documentation comment in .proto file."""
|
|
65
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
66
|
+
context.set_details('Method not implemented!')
|
|
67
|
+
raise NotImplementedError('Method not implemented!')
|
|
68
|
+
|
|
69
|
+
def GetIdentityDevices(self, request, context):
|
|
70
|
+
"""Missing associated documentation comment in .proto file."""
|
|
71
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
72
|
+
context.set_details('Method not implemented!')
|
|
73
|
+
raise NotImplementedError('Method not implemented!')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def add_PushServiceServicer_to_server(servicer, server):
|
|
77
|
+
rpc_method_handlers = {
|
|
78
|
+
'PushNotification': grpc.unary_unary_rpc_method_handler(
|
|
79
|
+
servicer.PushNotification,
|
|
80
|
+
request_deserializer=noxy__pb2.PushNotificationRequest.FromString,
|
|
81
|
+
response_serializer=noxy__pb2.PushResponse.SerializeToString,
|
|
82
|
+
),
|
|
83
|
+
'GetQuota': grpc.unary_unary_rpc_method_handler(
|
|
84
|
+
servicer.GetQuota,
|
|
85
|
+
request_deserializer=noxy__pb2.GetQuotaRequest.FromString,
|
|
86
|
+
response_serializer=noxy__pb2.GetQuotaResponse.SerializeToString,
|
|
87
|
+
),
|
|
88
|
+
'GetIdentityDevices': grpc.unary_unary_rpc_method_handler(
|
|
89
|
+
servicer.GetIdentityDevices,
|
|
90
|
+
request_deserializer=noxy__pb2.GetIdentityDevicesRequest.FromString,
|
|
91
|
+
response_serializer=noxy__pb2.GetIdentityDevicesResponse.SerializeToString,
|
|
92
|
+
),
|
|
93
|
+
}
|
|
94
|
+
generic_handler = grpc.method_handlers_generic_handler(
|
|
95
|
+
'noxy.push.PushService', rpc_method_handlers)
|
|
96
|
+
server.add_generic_rpc_handlers((generic_handler,))
|
|
97
|
+
server.add_registered_method_handlers('noxy.push.PushService', rpc_method_handlers)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# This class is part of an EXPERIMENTAL API.
|
|
101
|
+
class PushService(object):
|
|
102
|
+
"""Missing associated documentation comment in .proto file."""
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def PushNotification(request,
|
|
106
|
+
target,
|
|
107
|
+
options=(),
|
|
108
|
+
channel_credentials=None,
|
|
109
|
+
call_credentials=None,
|
|
110
|
+
insecure=False,
|
|
111
|
+
compression=None,
|
|
112
|
+
wait_for_ready=None,
|
|
113
|
+
timeout=None,
|
|
114
|
+
metadata=None):
|
|
115
|
+
return grpc.experimental.unary_unary(
|
|
116
|
+
request,
|
|
117
|
+
target,
|
|
118
|
+
'/noxy.push.PushService/PushNotification',
|
|
119
|
+
noxy__pb2.PushNotificationRequest.SerializeToString,
|
|
120
|
+
noxy__pb2.PushResponse.FromString,
|
|
121
|
+
options,
|
|
122
|
+
channel_credentials,
|
|
123
|
+
insecure,
|
|
124
|
+
call_credentials,
|
|
125
|
+
compression,
|
|
126
|
+
wait_for_ready,
|
|
127
|
+
timeout,
|
|
128
|
+
metadata,
|
|
129
|
+
_registered_method=True)
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def GetQuota(request,
|
|
133
|
+
target,
|
|
134
|
+
options=(),
|
|
135
|
+
channel_credentials=None,
|
|
136
|
+
call_credentials=None,
|
|
137
|
+
insecure=False,
|
|
138
|
+
compression=None,
|
|
139
|
+
wait_for_ready=None,
|
|
140
|
+
timeout=None,
|
|
141
|
+
metadata=None):
|
|
142
|
+
return grpc.experimental.unary_unary(
|
|
143
|
+
request,
|
|
144
|
+
target,
|
|
145
|
+
'/noxy.push.PushService/GetQuota',
|
|
146
|
+
noxy__pb2.GetQuotaRequest.SerializeToString,
|
|
147
|
+
noxy__pb2.GetQuotaResponse.FromString,
|
|
148
|
+
options,
|
|
149
|
+
channel_credentials,
|
|
150
|
+
insecure,
|
|
151
|
+
call_credentials,
|
|
152
|
+
compression,
|
|
153
|
+
wait_for_ready,
|
|
154
|
+
timeout,
|
|
155
|
+
metadata,
|
|
156
|
+
_registered_method=True)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def GetIdentityDevices(request,
|
|
160
|
+
target,
|
|
161
|
+
options=(),
|
|
162
|
+
channel_credentials=None,
|
|
163
|
+
call_credentials=None,
|
|
164
|
+
insecure=False,
|
|
165
|
+
compression=None,
|
|
166
|
+
wait_for_ready=None,
|
|
167
|
+
timeout=None,
|
|
168
|
+
metadata=None):
|
|
169
|
+
return grpc.experimental.unary_unary(
|
|
170
|
+
request,
|
|
171
|
+
target,
|
|
172
|
+
'/noxy.push.PushService/GetIdentityDevices',
|
|
173
|
+
noxy__pb2.GetIdentityDevicesRequest.SerializeToString,
|
|
174
|
+
noxy__pb2.GetIdentityDevicesResponse.FromString,
|
|
175
|
+
options,
|
|
176
|
+
channel_credentials,
|
|
177
|
+
insecure,
|
|
178
|
+
call_credentials,
|
|
179
|
+
compression,
|
|
180
|
+
wait_for_ready,
|
|
181
|
+
timeout,
|
|
182
|
+
metadata,
|
|
183
|
+
_registered_method=True)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Kyber768 post-quantum key encapsulation for notification encryption."""
|
|
2
|
+
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
import kybercffi
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class KyberProvider:
|
|
9
|
+
"""Kyber768 provider for encapsulating shared secrets with device public keys."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self._kyber = kybercffi.Kyber768()
|
|
13
|
+
|
|
14
|
+
def encapsulate(self, public_key: bytes) -> Tuple[bytes, bytes]:
|
|
15
|
+
"""Encapsulate a shared secret using the device's post-quantum public key.
|
|
16
|
+
|
|
17
|
+
Returns (kyber_ciphertext, shared_secret). Ciphertext is 1088 bytes, shared secret is 32 bytes.
|
|
18
|
+
"""
|
|
19
|
+
return self._kyber.encapsulate(public_key)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Retry logic for transient network errors."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from typing import Callable, TypeVar
|
|
6
|
+
|
|
7
|
+
import grpc
|
|
8
|
+
|
|
9
|
+
# gRPC status codes that indicate retryable errors
|
|
10
|
+
RETRYABLE_CODES = frozenset({
|
|
11
|
+
grpc.StatusCode.UNKNOWN,
|
|
12
|
+
grpc.StatusCode.DEADLINE_EXCEEDED,
|
|
13
|
+
grpc.StatusCode.RESOURCE_EXHAUSTED,
|
|
14
|
+
grpc.StatusCode.UNAVAILABLE,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_retryable(err: Exception) -> bool:
|
|
19
|
+
"""Check if an error is retryable (network/unavailable)."""
|
|
20
|
+
if isinstance(err, grpc.RpcError):
|
|
21
|
+
return err.code() in RETRYABLE_CODES
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def with_retry_async(
|
|
29
|
+
fn: Callable[[], T],
|
|
30
|
+
retries: int = 3,
|
|
31
|
+
) -> T:
|
|
32
|
+
"""Execute an async callable with retries on retryable errors."""
|
|
33
|
+
last_err = None
|
|
34
|
+
for attempt in range(retries):
|
|
35
|
+
try:
|
|
36
|
+
result = fn()
|
|
37
|
+
if asyncio.iscoroutine(result):
|
|
38
|
+
return await result
|
|
39
|
+
return result
|
|
40
|
+
except Exception as e:
|
|
41
|
+
last_err = e
|
|
42
|
+
if attempt < retries - 1 and is_retryable(e):
|
|
43
|
+
await asyncio.sleep((2**attempt) * 0.1)
|
|
44
|
+
else:
|
|
45
|
+
raise
|
|
46
|
+
raise last_err
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def with_retry_sync(
|
|
50
|
+
fn: Callable[[], T],
|
|
51
|
+
retries: int = 3,
|
|
52
|
+
) -> T:
|
|
53
|
+
"""Execute a sync callable with retries on retryable errors."""
|
|
54
|
+
last_err = None
|
|
55
|
+
for attempt in range(retries):
|
|
56
|
+
try:
|
|
57
|
+
return fn()
|
|
58
|
+
except Exception as e:
|
|
59
|
+
last_err = e
|
|
60
|
+
if attempt < retries - 1 and is_retryable(e):
|
|
61
|
+
time.sleep((2**attempt) * 0.1)
|
|
62
|
+
else:
|
|
63
|
+
raise
|
|
64
|
+
raise last_err
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Identity service for fetching devices."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from noxy.grpc import noxy_pb2
|
|
6
|
+
from noxy.transport import auth_metadata
|
|
7
|
+
from noxy.types import NoxyIdentityDevice
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IdentityService:
|
|
11
|
+
"""Fetches identity devices from the relay."""
|
|
12
|
+
|
|
13
|
+
def get_devices(
|
|
14
|
+
self,
|
|
15
|
+
stub,
|
|
16
|
+
auth_token: str,
|
|
17
|
+
identity_id: str,
|
|
18
|
+
) -> list[NoxyIdentityDevice]:
|
|
19
|
+
"""Return all devices registered for the given identity address."""
|
|
20
|
+
req = noxy_pb2.GetIdentityDevicesRequest(
|
|
21
|
+
request_id=str(uuid.uuid4()),
|
|
22
|
+
identity_id=identity_id,
|
|
23
|
+
)
|
|
24
|
+
metadata = auth_metadata(auth_token)
|
|
25
|
+
resp = stub.GetIdentityDevices(req, metadata=metadata)
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
NoxyIdentityDevice(
|
|
29
|
+
device_id=d.device_id,
|
|
30
|
+
public_key=bytes(d.public_key),
|
|
31
|
+
pq_public_key=bytes(d.pq_public_key),
|
|
32
|
+
)
|
|
33
|
+
for d in resp.devices
|
|
34
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Push service for sending encrypted notifications."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any, List
|
|
6
|
+
|
|
7
|
+
from noxy.crypto import encrypt
|
|
8
|
+
from noxy.grpc import noxy_pb2
|
|
9
|
+
from noxy.kyber_provider import KyberProvider
|
|
10
|
+
from noxy.retries import with_retry_sync
|
|
11
|
+
from noxy.transport import auth_metadata
|
|
12
|
+
from noxy.types import NoxyIdentityDevice, NoxyPushDeliveryStatus, NoxyPushResponse
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PushService:
|
|
16
|
+
"""Sends encrypted push notifications."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, kyber_provider: KyberProvider) -> None:
|
|
19
|
+
self._kyber = kyber_provider
|
|
20
|
+
|
|
21
|
+
def _encrypt_notification(
|
|
22
|
+
self,
|
|
23
|
+
device_pq_public_key: bytes,
|
|
24
|
+
plaintext: bytes,
|
|
25
|
+
) -> tuple[bytes, bytes, bytes]:
|
|
26
|
+
"""Encrypt notification for a device. Returns (kyber_ct, nonce, ciphertext)."""
|
|
27
|
+
kyber_ct, shared_secret = self._kyber.encapsulate(device_pq_public_key)
|
|
28
|
+
ciphertext, nonce = encrypt(shared_secret, plaintext)
|
|
29
|
+
return kyber_ct, nonce, ciphertext
|
|
30
|
+
|
|
31
|
+
def _send_to_network(
|
|
32
|
+
self,
|
|
33
|
+
stub,
|
|
34
|
+
auth_token: str,
|
|
35
|
+
ciphertext: bytes,
|
|
36
|
+
ttl_seconds: int,
|
|
37
|
+
target_device_id: str,
|
|
38
|
+
kyber_ct: bytes,
|
|
39
|
+
nonce: bytes,
|
|
40
|
+
) -> NoxyPushResponse:
|
|
41
|
+
"""Send encrypted payload to the relay with retries."""
|
|
42
|
+
def _do_send() -> NoxyPushResponse:
|
|
43
|
+
req = noxy_pb2.PushNotificationRequest(
|
|
44
|
+
request_id=str(uuid.uuid4()),
|
|
45
|
+
ciphertext=ciphertext,
|
|
46
|
+
ttl_seconds=ttl_seconds,
|
|
47
|
+
target_device_id=target_device_id,
|
|
48
|
+
kyber_ct=kyber_ct,
|
|
49
|
+
nonce=nonce,
|
|
50
|
+
)
|
|
51
|
+
metadata = auth_metadata(auth_token)
|
|
52
|
+
resp = stub.PushNotification(req, metadata=metadata)
|
|
53
|
+
return NoxyPushResponse(
|
|
54
|
+
status=NoxyPushDeliveryStatus(resp.status),
|
|
55
|
+
request_id=resp.request_id,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return with_retry_sync(_do_send, retries=3)
|
|
59
|
+
|
|
60
|
+
def send(
|
|
61
|
+
self,
|
|
62
|
+
stub,
|
|
63
|
+
auth_token: str,
|
|
64
|
+
devices: List[NoxyIdentityDevice],
|
|
65
|
+
push_notification: Any,
|
|
66
|
+
ttl_seconds: int,
|
|
67
|
+
) -> List[NoxyPushResponse]:
|
|
68
|
+
"""Send a push notification to all devices, encrypting per device."""
|
|
69
|
+
plaintext = json.dumps(push_notification, default=str).encode("utf-8")
|
|
70
|
+
results: List[NoxyPushResponse] = []
|
|
71
|
+
|
|
72
|
+
for device in devices:
|
|
73
|
+
kyber_ct, nonce, ciphertext = self._encrypt_notification(
|
|
74
|
+
device.pq_public_key, plaintext
|
|
75
|
+
)
|
|
76
|
+
resp = self._send_to_network(
|
|
77
|
+
stub,
|
|
78
|
+
auth_token,
|
|
79
|
+
ciphertext,
|
|
80
|
+
ttl_seconds,
|
|
81
|
+
device.device_id,
|
|
82
|
+
kyber_ct,
|
|
83
|
+
nonce,
|
|
84
|
+
)
|
|
85
|
+
results.append(resp)
|
|
86
|
+
|
|
87
|
+
return results
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Quota service for fetching usage."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from noxy.grpc import noxy_pb2
|
|
6
|
+
from noxy.transport import auth_metadata
|
|
7
|
+
from noxy.types import NoxyGetQuotaResponse, NoxyQuotaStatus
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QuotaService:
|
|
11
|
+
"""Fetches quota usage from the relay."""
|
|
12
|
+
|
|
13
|
+
def get(self, stub, auth_token: str) -> NoxyGetQuotaResponse:
|
|
14
|
+
"""Return quota usage for the application."""
|
|
15
|
+
req = noxy_pb2.GetQuotaRequest(request_id=str(uuid.uuid4()))
|
|
16
|
+
metadata = auth_metadata(auth_token)
|
|
17
|
+
resp = stub.GetQuota(req, metadata=metadata)
|
|
18
|
+
|
|
19
|
+
return NoxyGetQuotaResponse(
|
|
20
|
+
request_id=resp.request_id,
|
|
21
|
+
app_name=resp.app_name,
|
|
22
|
+
quota_total=resp.quota_total,
|
|
23
|
+
quota_remaining=resp.quota_remaining,
|
|
24
|
+
status=NoxyQuotaStatus(resp.status),
|
|
25
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""gRPC transport layer for Noxy relay communication."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
import grpc
|
|
7
|
+
|
|
8
|
+
from noxy.grpc import noxy_pb2, noxy_pb2_grpc
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _normalize_endpoint(endpoint: str) -> str:
|
|
12
|
+
"""Strip https:// or http:// and trailing slashes."""
|
|
13
|
+
s = re.sub(r"^https?://", "", endpoint)
|
|
14
|
+
return s.rstrip("/")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_channel(endpoint: str) -> grpc.Channel:
|
|
18
|
+
"""Create a secure gRPC channel to the Noxy relay."""
|
|
19
|
+
addr = _normalize_endpoint(endpoint)
|
|
20
|
+
return grpc.secure_channel(addr, grpc.ssl_channel_credentials())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_client(endpoint: str) -> Tuple[noxy_pb2_grpc.PushServiceStub, grpc.Channel]:
|
|
24
|
+
"""Create a PushService stub and channel."""
|
|
25
|
+
channel = create_channel(endpoint)
|
|
26
|
+
stub = noxy_pb2_grpc.PushServiceStub(channel)
|
|
27
|
+
return stub, channel
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def auth_metadata(auth_token: str) -> list[tuple[str, str]]:
|
|
31
|
+
"""Return metadata with Bearer token for gRPC calls."""
|
|
32
|
+
return [("authorization", f"Bearer {auth_token}")]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Type definitions matching the proto and SDK API."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# EVM wallet address in 0x format
|
|
9
|
+
NoxyIdentityAddress = str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NoxyPushDeliveryStatus(IntEnum):
|
|
13
|
+
"""Delivery status for a push notification."""
|
|
14
|
+
|
|
15
|
+
DELIVERED = 0
|
|
16
|
+
QUEUED = 1
|
|
17
|
+
NO_DEVICES = 2
|
|
18
|
+
REJECTED = 3
|
|
19
|
+
ERROR = 4
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class NoxyPushResponse:
|
|
24
|
+
"""Response for a push notification send."""
|
|
25
|
+
|
|
26
|
+
status: NoxyPushDeliveryStatus
|
|
27
|
+
request_id: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NoxyQuotaStatus(IntEnum):
|
|
31
|
+
"""Quota status for the application."""
|
|
32
|
+
|
|
33
|
+
QUOTA_ACTIVE = 0
|
|
34
|
+
QUOTA_SUSPENDED = 1
|
|
35
|
+
QUOTA_DELETED = 2
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class NoxyGetQuotaResponse:
|
|
40
|
+
"""Response for a quota query."""
|
|
41
|
+
|
|
42
|
+
request_id: str
|
|
43
|
+
app_name: str
|
|
44
|
+
quota_total: int
|
|
45
|
+
quota_remaining: int
|
|
46
|
+
status: NoxyQuotaStatus
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class NoxyIdentityDevice:
|
|
51
|
+
"""Identity device with keys for encryption."""
|
|
52
|
+
|
|
53
|
+
device_id: str
|
|
54
|
+
public_key: bytes
|
|
55
|
+
pq_public_key: bytes
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: noxy-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Backend SDK for Python servers to integrate with the Noxy push notification network
|
|
5
|
+
Author: Noxy Network
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: noxy,push-notifications,web3,post-quantum,grpc
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Security :: Cryptography
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: grpcio>=1.60.0
|
|
20
|
+
Requires-Dist: grpcio-tools>=1.60.0
|
|
21
|
+
Requires-Dist: protobuf>=4.25.0
|
|
22
|
+
Requires-Dist: kybercffi>=1.0.0
|
|
23
|
+
Requires-Dist: cryptography>=42.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# π¦ @noxy-network/python-sdk
|
|
30
|
+
|
|
31
|
+
Backend SDK for Python servers to integrate with the [Noxy](https://noxy.network) push notification network. Send encrypted push notifications to Web3 wallet addresses via the Noxy relay infrastructure.
|
|
32
|
+
|
|
33
|
+
## Overview
|
|
34
|
+
|
|
35
|
+
This SDK enables server-side applications to:
|
|
36
|
+
|
|
37
|
+
- **Send push notifications** to users by their Web3 wallet address (EVM `0x` format)
|
|
38
|
+
- **Query quota usage** for your application's relay allocation
|
|
39
|
+
- **Resolve identity devices** to deliver notifications to all registered devices
|
|
40
|
+
|
|
41
|
+
Communication with the Noxy relay is performed over **gRPC** using Protocol Buffers. All notifications are **encrypted end-to-end** on the backend before transmission; decryption occurs only on the recipient's Noxy device. The SDK uses **post-quantum encryption** (Kyber768) to protect payloads against future quantum attacks.
|
|
42
|
+
|
|
43
|
+
## Architecture
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
βββββββββββββββββββ gRPC (TLS) βββββββββββββββββββ E2E Encrypted βββββββββββββββββββ
|
|
47
|
+
β Your Backend β βββββββββββββββββββΊ β Noxy Relay β ββββββββββββββββββββΊ β Noxy Device β
|
|
48
|
+
β (this SDK) β PushNotification β β Ciphertext only β (decrypts) β
|
|
49
|
+
β β GetQuota β β β β
|
|
50
|
+
β β GetIdentityDevicesβ β β β
|
|
51
|
+
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **Encryption**: Kyber768 (post-quantum KEM) + AES-256-GCM. Each notification is encrypted per-device using the device's post-quantum public key.
|
|
55
|
+
- **Transport**: gRPC over TLS with Bearer token authentication.
|
|
56
|
+
- **Relay**: The Noxy relay forwards ciphertext only; it cannot decrypt notification payloads.
|
|
57
|
+
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
- Python **>= 3.10**
|
|
61
|
+
- C compiler (for kybercffi)
|
|
62
|
+
|
|
63
|
+
## π Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install noxy-sdk
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## π Quick Start
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from noxy import NoxyConfig, init_noxy_client
|
|
73
|
+
|
|
74
|
+
config = NoxyConfig(
|
|
75
|
+
endpoint="https://relay.noxy.network",
|
|
76
|
+
auth_token="your-api-token",
|
|
77
|
+
notification_ttl_seconds=3600,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
with init_noxy_client(config) as client:
|
|
81
|
+
# Send a push notification to a wallet address
|
|
82
|
+
results = client.send_push(
|
|
83
|
+
"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1",
|
|
84
|
+
{"title": "New message", "body": "You have a new notification", "data": {"action": "open_chat", "id": "123"}},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Check quota usage
|
|
88
|
+
quota = client.get_quota()
|
|
89
|
+
print(f"{quota.quota_remaining} remaining")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
| Option | Type | Required | Description |
|
|
95
|
+
|--------|------|----------|-------------|
|
|
96
|
+
| `endpoint` | `str` | Yes | Noxy relay gRPC endpoint (e.g. `https://relay.noxy.network`). Scheme is stripped; TLS is used by default. |
|
|
97
|
+
| `auth_token` | `str` | Yes | Bearer token for relay authentication. Sent in the `Authorization` header on every request. |
|
|
98
|
+
| `notification_ttl_seconds` | `int` | Yes | Time-to-live for notifications in seconds. |
|
|
99
|
+
|
|
100
|
+
## API Reference
|
|
101
|
+
|
|
102
|
+
### `init_noxy_client(config: NoxyConfig) -> NoxyPushClient`
|
|
103
|
+
|
|
104
|
+
Initializes the SDK client. Normalizes the endpoint and establishes the gRPC connection.
|
|
105
|
+
|
|
106
|
+
### `NoxyPushClient`
|
|
107
|
+
|
|
108
|
+
#### `send_push(identity_address, push_notification) -> List[NoxyPushResponse]`
|
|
109
|
+
|
|
110
|
+
Sends a push notification to all devices registered for the given Web3 identity address.
|
|
111
|
+
|
|
112
|
+
- **`identity_address`**: EVM address in `0x` format (e.g. `0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1`)
|
|
113
|
+
- **`push_notification`**: Any JSON-serializable object (e.g. `dict`). Encrypted before transmission.
|
|
114
|
+
- **Returns**: List of `NoxyPushResponse` per device, with `status` and `request_id`.
|
|
115
|
+
|
|
116
|
+
#### `get_quota() -> NoxyGetQuotaResponse`
|
|
117
|
+
|
|
118
|
+
Returns quota usage for your application.
|
|
119
|
+
|
|
120
|
+
- **Returns**: `NoxyGetQuotaResponse` with `request_id`, `app_name`, `quota_total`, `quota_remaining`, `status`.
|
|
121
|
+
|
|
122
|
+
### Types
|
|
123
|
+
|
|
124
|
+
- **`NoxyPushDeliveryStatus`**: `DELIVERED` (0) | `QUEUED` (1) | `NO_DEVICES` (2) | `REJECTED` (3) | `ERROR` (4)
|
|
125
|
+
- **`NoxyQuotaStatus`**: `QUOTA_ACTIVE` (0) | `QUOTA_SUSPENDED` (1) | `QUOTA_DELETED` (2)
|
|
126
|
+
|
|
127
|
+
## Encryption Details
|
|
128
|
+
|
|
129
|
+
1. **Key encapsulation**: For each device, the SDK encapsulates a shared secret using the device's Kyber768 post-quantum public key (`pq_public_key`).
|
|
130
|
+
2. **Key derivation**: The shared secret is expanded via HKDF-SHA256 to a 256-bit AES key.
|
|
131
|
+
3. **Payload encryption**: The notification payload (JSON) is encrypted with AES-256-GCM. The ciphertext includes the GCM auth tag appended for integrity verification.
|
|
132
|
+
4. **Transmission**: Only `kyber_ct`, `nonce`, and `ciphertext` are sent to the relay. The relay cannot decrypt; only the target device (with its secret key) can decrypt.
|
|
133
|
+
|
|
134
|
+
## Building from source
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
python -m venv .venv
|
|
138
|
+
source .venv/bin/activate # or .venv\Scripts\activate on Windows
|
|
139
|
+
pip install -e .
|
|
140
|
+
|
|
141
|
+
# Regenerate proto (if proto file changes)
|
|
142
|
+
python -m grpc_tools.protoc -I proto --python_out=noxy/grpc --grpc_python_out=noxy/grpc proto/noxy.proto
|
|
143
|
+
# Fix import in noxy/grpc/noxy_pb2_grpc.py: change "import noxy_pb2" to "from . import noxy_pb2"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## π License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
noxy/__init__.py
|
|
5
|
+
noxy/client.py
|
|
6
|
+
noxy/config.py
|
|
7
|
+
noxy/crypto.py
|
|
8
|
+
noxy/kyber_provider.py
|
|
9
|
+
noxy/retries.py
|
|
10
|
+
noxy/transport.py
|
|
11
|
+
noxy/types.py
|
|
12
|
+
noxy/grpc/__init__.py
|
|
13
|
+
noxy/grpc/noxy_pb2.py
|
|
14
|
+
noxy/grpc/noxy_pb2_grpc.py
|
|
15
|
+
noxy/services/__init__.py
|
|
16
|
+
noxy/services/identity.py
|
|
17
|
+
noxy/services/push.py
|
|
18
|
+
noxy/services/quota.py
|
|
19
|
+
noxy_sdk.egg-info/PKG-INFO
|
|
20
|
+
noxy_sdk.egg-info/SOURCES.txt
|
|
21
|
+
noxy_sdk.egg-info/dependency_links.txt
|
|
22
|
+
noxy_sdk.egg-info/requires.txt
|
|
23
|
+
noxy_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
noxy
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "noxy-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Backend SDK for Python servers to integrate with the Noxy push notification network"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Noxy Network" }]
|
|
13
|
+
keywords = ["noxy", "push-notifications", "web3", "post-quantum", "grpc"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Security :: Cryptography",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
dependencies = [
|
|
26
|
+
"grpcio>=1.60.0",
|
|
27
|
+
"grpcio-tools>=1.60.0",
|
|
28
|
+
"protobuf>=4.25.0",
|
|
29
|
+
"kybercffi>=1.0.0",
|
|
30
|
+
"cryptography>=42.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.0",
|
|
36
|
+
"pytest-asyncio>=0.21.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["."]
|
|
41
|
+
include = ["noxy*"]
|
noxy_sdk-1.0.0/setup.cfg
ADDED