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 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.
@@ -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,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,7 @@
1
+ """Noxy services."""
2
+
3
+ from noxy.services.identity import IdentityService
4
+ from noxy.services.quota import QuotaService
5
+ from noxy.services.push import PushService
6
+
7
+ __all__ = ["IdentityService", "QuotaService", "PushService"]
@@ -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,9 @@
1
+ grpcio>=1.60.0
2
+ grpcio-tools>=1.60.0
3
+ protobuf>=4.25.0
4
+ kybercffi>=1.0.0
5
+ cryptography>=42.0.0
6
+
7
+ [dev]
8
+ pytest>=7.0
9
+ pytest-asyncio>=0.21.0
@@ -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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+