From 8acf584d28578455c8340d202394bdec335a3182 Mon Sep 17 00:00:00 2001
From: Oracle
Date: Tue, 21 Apr 2026 17:24:11 +0200
Subject: [PATCH] Initial commit
---
.gitignore | 39 ++
.idea/.gitignore | 10 +
.idea/encodings.xml | 7 +
.idea/misc.xml | 18 +
.idea/vcs.xml | 7 +
TRANSLATION_REFERENCE.md | 478 ++++++++++++++++
pom.xml | 42 ++
src/main/java/ai/nomyo/Constants.java | 199 +++++++
src/main/java/ai/nomyo/Main.java | 27 +
.../java/ai/nomyo/SecureChatCompletion.java | 188 +++++++
.../java/ai/nomyo/SecureCompletionClient.java | 513 ++++++++++++++++++
src/main/java/ai/nomyo/SecureMemory.java | 237 ++++++++
.../ai/nomyo/errors/APIConnectionError.java | 41 ++
src/main/java/ai/nomyo/errors/APIError.java | 59 ++
.../ai/nomyo/errors/AuthenticationError.java | 41 ++
.../java/ai/nomyo/errors/ForbiddenError.java | 41 ++
.../ai/nomyo/errors/InvalidRequestError.java | 41 ++
.../java/ai/nomyo/errors/RateLimitError.java | 41 ++
.../java/ai/nomyo/errors/SecurityError.java | 34 ++
.../java/ai/nomyo/errors/ServerError.java | 41 ++
.../nomyo/errors/ServiceUnavailableError.java | 41 ++
src/main/java/ai/nomyo/util/PEMConverter.java | 24 +
src/main/java/ai/nomyo/util/Pass2Key.java | 202 +++++++
src/main/java/ai/nomyo/util/Splitter.java | 37 ++
24 files changed, 2408 insertions(+)
create mode 100644 .gitignore
create mode 100644 .idea/.gitignore
create mode 100644 .idea/encodings.xml
create mode 100644 .idea/misc.xml
create mode 100644 .idea/vcs.xml
create mode 100644 TRANSLATION_REFERENCE.md
create mode 100644 pom.xml
create mode 100644 src/main/java/ai/nomyo/Constants.java
create mode 100644 src/main/java/ai/nomyo/Main.java
create mode 100644 src/main/java/ai/nomyo/SecureChatCompletion.java
create mode 100644 src/main/java/ai/nomyo/SecureCompletionClient.java
create mode 100644 src/main/java/ai/nomyo/SecureMemory.java
create mode 100644 src/main/java/ai/nomyo/errors/APIConnectionError.java
create mode 100644 src/main/java/ai/nomyo/errors/APIError.java
create mode 100644 src/main/java/ai/nomyo/errors/AuthenticationError.java
create mode 100644 src/main/java/ai/nomyo/errors/ForbiddenError.java
create mode 100644 src/main/java/ai/nomyo/errors/InvalidRequestError.java
create mode 100644 src/main/java/ai/nomyo/errors/RateLimitError.java
create mode 100644 src/main/java/ai/nomyo/errors/SecurityError.java
create mode 100644 src/main/java/ai/nomyo/errors/ServerError.java
create mode 100644 src/main/java/ai/nomyo/errors/ServiceUnavailableError.java
create mode 100644 src/main/java/ai/nomyo/util/PEMConverter.java
create mode 100644 src/main/java/ai/nomyo/util/Pass2Key.java
create mode 100644 src/main/java/ai/nomyo/util/Splitter.java
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..480bdf5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,39 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+.kotlin
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..30cf57e
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..aa00ffa
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..4f35448
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..8306744
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/TRANSLATION_REFERENCE.md b/TRANSLATION_REFERENCE.md
new file mode 100644
index 0000000..213c271
--- /dev/null
+++ b/TRANSLATION_REFERENCE.md
@@ -0,0 +1,478 @@
+# NOMYO Python Client — Translation Reference
+
+> Target: Port this library to another language. Every class, method, signature, constant, wire format, and error mapping is documented below.
+
+---
+
+## 1. Package Layout
+
+| File (relative to package root) | Purpose |
+|---|---|
+| `nomyo/__init__.py` | Public exports, version string |
+| `nomyo/nomyo.py` | `SecureChatCompletion` — OpenAI-compatible entrypoint |
+| `nomyo/SecureCompletionClient.py` | Key mgmt, hybrid encryption, HTTP roundtrip, retries |
+| `nomyo/SecureMemory.py` | Cross-platform memory locking + secure zeroing (optional, platform-specific) |
+
+**Python version:** `>= 3.10`
+**Build:** `hatchling` (pyproject.toml)
+**Dependencies:** `anyio`, `certifi`, `cffi`, `cryptography`, `exceptiongroup`, `h11`, `httpcore`, `httpx`, `idna`, `pycparser`, `typing_extensions`
+
+---
+
+## 2. Public API Surface (`__all__`)
+
+| Export | Type | Source file |
+|---|---|---|
+| `SecureChatCompletion` | class | `nomyo.py` |
+| `SecurityError` | exception | `SecureCompletionClient.py` |
+| `APIError` | exception (base) | `SecureCompletionClient.py` |
+| `AuthenticationError` | exception (401) | `SecureCompletionClient.py` |
+| `InvalidRequestError` | exception (400) | `SecureCompletionClient.py` |
+| `APIConnectionError` | exception (network) | `SecureCompletionClient.py` |
+| `ForbiddenError` | exception (403) | `SecureCompletionClient.py` |
+| `RateLimitError` | exception (429) | `SecureCompletionClient.py` |
+| `ServerError` | exception (500) | `SecureCompletionClient.py` |
+| `ServiceUnavailableError` | exception (503) | `SecureCompletionClient.py` |
+| `get_memory_protection_info` | function | `SecureMemory.py` |
+| `disable_secure_memory` | function | `SecureMemory.py` |
+| `enable_secure_memory` | function | `SecureMemory.py` |
+| `secure_bytearray` | context manager | `SecureMemory.py` |
+| `secure_bytes` | context manager (deprecated) | `SecureMemory.py` |
+| `SecureBuffer` | class | `SecureMemory.py` |
+
+---
+
+## 3. `SecureChatCompletion` (entrypoint)
+
+### Constructor
+
+```python
+SecureChatCompletion(
+ base_url: str = "https://api.nomyo.ai",
+ allow_http: bool = False,
+ api_key: Optional[str] = None,
+ secure_memory: bool = True,
+ key_dir: Optional[str] = None,
+ max_retries: int = 2
+)
+```
+
+| Param | Default | Description |
+|---|---|---|
+| `base_url` | `"https://api.nomyo.ai"` | NOMYO Router base URL. HTTPS enforced unless `allow_http=True`. |
+| `allow_http` | `False` | Permit `http://` URLs (dev only). |
+| `api_key` | `None` | Bearer token for auth. Can also be passed per-call via `create()`. |
+| `secure_memory` | `True` | Enable memory locking/zeroing. Warns if unavailable. |
+| `key_dir` | `None` | Directory to persist RSA keys. `None` = ephemeral (in-memory only). |
+| `max_retries` | `2` | Retries on 429/500/502/503/504 + network errors. Exponential backoff: 1s, 2s, 4s… |
+
+### `create(model, messages, **kwargs) -> Dict[str, Any]`
+
+Async method. Returns a **dict** (not an object). Same signature as `openai.ChatCompletion.create()`.
+
+| Param | Type | Required | Description |
+|---|---|---|---|
+| `model` | `str` | yes | Model identifier, e.g. `"Qwen/Qwen3-0.6B"` |
+| `messages` | `List[Dict]` | yes | OpenAI-format messages: `[{"role": "user", "content": "..."}]` |
+| `temperature` | `float` | no | 0–2 |
+| `max_tokens` | `int` | no | |
+| `top_p` | `float` | no | |
+| `stop` | `str \| List[str]` | no | |
+| `presence_penalty` | `float` | no | -2.0 to 2.0 |
+| `frequency_penalty` | `float` | no | -2.0 to 2.0 |
+| `n` | `int` | no | Number of completions |
+| `best_of` | `int` | no | |
+| `seed` | `int` | no | |
+| `logit_bias` | `Dict[str, float]` | no | |
+| `user` | `str` | no | |
+| `tools` | `List[Dict]` | no | Tool definitions passed through to llama.cpp |
+| `tool_choice` | `str` | no | `"auto"`, `"none"`, or specific tool name |
+| `response_format` | `Dict` | no | `{"type": "json_object"}` or `{"type": "json_schema", ...}` |
+| `stream` | `bool` | no | **NOT supported.** Server rejects with HTTP 400. Always use `False`. |
+| `base_url` | `str` | no | Per-call override (creates temp client internally). |
+| `security_tier` | `str` | no | `"standard"`, `"high"`, or `"maximum"`. Invalid values raise `ValueError`. |
+| `api_key` | `str` | no | Per-call override of instance `api_key`. |
+
+**Return value:** `Dict[str, Any]` — OpenAI-compatible response dict (see §6.2).
+
+### `acreate(model, messages, **kwargs) -> Dict[str, Any]`
+
+Async alias for `create()`. Identical behavior.
+
+---
+
+## 4. `SecureCompletionClient` (low-level)
+
+### Constructor
+
+```python
+SecureCompletionClient(
+ router_url: str = "https://api.nomyo.ai",
+ allow_http: bool = False,
+ secure_memory: bool = True,
+ max_retries: int = 2
+)
+```
+
+Same semantics as `SecureChatCompletion` constructor (maps directly to inner client).
+
+### Instance attributes
+
+| Attribute | Type | Description |
+|---|---|---|
+| `router_url` | `str` | Base URL (trailing slash stripped). |
+| `private_key` | `rsa.RSAPrivateKey \| None` | Loaded/generated RSA private key. |
+| `public_key_pem` | `str \| None` | PEM-encoded public key string. |
+| `key_size` | `int` | Always `4096`. |
+| `allow_http` | `bool` | HTTP allowance flag. |
+| `max_retries` | `int` | Retry count. |
+| `_use_secure_memory` | `bool` | Whether secure memory ops are active. |
+
+### `generate_keys(save_to_file: bool = False, key_dir: str = "client_keys", password: Optional[str] = None) -> None`
+
+Generates a 4096-bit RSA key pair (public exponent `65537`). If `save_to_file=True`:
+- Creates `key_dir/` (mode 755).
+- Writes `private_key.pem` with mode `0o600`.
+- Writes `public_key.pem` with mode `0o644`.
+- If `password` is given, private key is encrypted with `BestAvailableEncryption`.
+
+### `load_keys(private_key_path: str, public_key_path: Optional[str] = None, password: Optional[str] = None) -> None`
+
+Loads an RSA private key from disk. If `public_key_path` is omitted, derives the public key from the loaded private key. Validates key size >= 2048 bits.
+
+### `fetch_server_public_key() -> str` (async)
+
+`GET {router_url}/pki/public_key`
+- Returns server PEM public key as string.
+- Validates it parses as a valid PEM public key.
+- Raises `SecurityError` if URL is not HTTPS and `allow_http=False`.
+
+### `encrypt_payload(payload: Dict[str, Any]) -> bytes` (async)
+
+Encrypts a dict payload using hybrid encryption. Returns raw encrypted bytes (JSON package, serialized to bytes).
+
+**Encryption process:**
+1. Serialize payload to JSON → `bytearray`.
+2. Validate size <= 10 MB.
+3. Generate 256-bit AES key via `secrets.token_bytes(32)` → `bytearray`.
+4. If secure memory enabled: lock both payload and AES key in memory.
+5. Call `_do_encrypt()` (see below).
+6. Zero/destroy payload and AES key from memory on exit.
+
+### `_do_encrypt(payload_bytes: bytes \| bytearray, aes_key: bytes \| bytearray) -> bytes` (async)
+
+Core hybrid encryption routine. **This is the wire format constructor.**
+
+```
+1. nonce = secrets.token_bytes(12) # 96-bit GCM nonce
+2. ciphertext = AES-256-GCM_encrypt(aes_key, nonce, payload_bytes)
+3. tag = GCM_tag
+4. server_pubkey = await fetch_server_public_key()
+5. encrypted_aes_key = RSA-OAEP-SHA256_encrypt(server_pubkey, aes_key_bytes)
+6. Build JSON package (see §6.1)
+7. Return json.dumps(package).encode('utf-8')
+```
+
+### `decrypt_response(encrypted_response: bytes, payload_id: str) -> Dict[str, Any]` (async)
+
+Decrypts a server response.
+
+**Validation chain:**
+1. Parse JSON.
+2. Check `version == "1.0"` — raises `ValueError` if mismatch.
+3. Check `algorithm == "hybrid-aes256-rsa4096"` — raises `ValueError` if mismatch.
+4. Validate `encrypted_payload` has `ciphertext`, `nonce`, `tag`.
+5. Require `self.private_key` is not `None`.
+6. Decrypt AES key: `RSA-OAEP-SHA256_decrypt(private_key, encrypted_aes_key)`.
+7. Decrypt payload: `AES-256-GCM_decrypt(aes_key, nonce, tag, ciphertext)`.
+8. Parse decrypted bytes as JSON → response dict.
+9. Attach `_metadata` if not present (see §6.2).
+
+Any decryption failure (except JSON parse errors) raises `SecurityError("Decryption failed: integrity check or authentication failed")`.
+
+### `send_secure_request(payload, payload_id, api_key=None, security_tier=None) -> Dict[str, Any]` (async)
+
+Full request lifecycle: encrypt → HTTP POST → retry → decrypt → return.
+
+**Request headers:**
+```
+Content-Type: application/octet-stream
+X-Payload-ID: {payload_id}
+X-Public-Key: {url_encoded_pem_public_key}
+Authorization: Bearer {api_key} (if api_key is provided)
+X-Security-Tier: {tier} (if security_tier is provided)
+```
+
+**POST** to `{router_url}/v1/chat/secure_completion` with encrypted payload as body.
+
+**Retry logic:**
+- Retryable status codes: `{429, 500, 502, 503, 504}`.
+- Backoff: `2^(attempt-1)` seconds (1s, 2s, 4s…).
+- Total attempts: `max_retries + 1`.
+- Network errors also retry.
+- Non-retryable exceptions propagate immediately.
+
+**Status → exception mapping:**
+
+| Status | Exception |
+|---|---|
+| 200 | Return decrypted response dict |
+| 400 | `InvalidRequestError` |
+| 401 | `AuthenticationError` |
+| 403 | `ForbiddenError` |
+| 404 | `APIError` |
+| 429 | `RateLimitError` |
+| 500 | `ServerError` |
+| 503 | `ServiceUnavailableError` |
+| 502/504 | `APIError` (retryable) |
+| other | `APIError` (non-retryable) |
+| network error | `APIConnectionError` |
+
+---
+
+## 5. Encryption Wire Format
+
+The encrypted package is a JSON object sent as `application/octet-stream`:
+
+```json
+{
+ "version": "1.0",
+ "algorithm": "hybrid-aes256-rsa4096",
+ "encrypted_payload": {
+ "ciphertext": "",
+ "nonce": "",
+ "tag": ""
+ },
+ "encrypted_aes_key": "",
+ "key_algorithm": "RSA-OAEP-SHA256",
+ "payload_algorithm": "AES-256-GCM"
+}
+```
+
+| Field | Encoding | Description |
+|---|---|---|
+| `version` | string | Protocol version. **Never change** — used for downgrade detection. |
+| `algorithm` | string | `"hybrid-aes256-rsa4096"`. **Never change** — used for downgrade detection. |
+| `encrypted_payload.ciphertext` | base64 | AES-256-GCM encrypted payload. |
+| `encrypted_payload.nonce` | base64 | 12-byte GCM nonce. |
+| `encrypted_payload.tag` | base64 | 16-byte GCM authentication tag. |
+| `encrypted_aes_key` | base64 | RSA-OAEP-SHA256 encrypted 32-byte AES key. |
+| `key_algorithm` | string | `"RSA-OAEP-SHA256"` |
+| `payload_algorithm` | string | `"AES-256-GCM"` |
+
+---
+
+## 6. Data Structures
+
+### 6.1 Encrypted Request Payload (before encryption)
+
+The dict passed to `encrypt_payload()` has this structure:
+
+```json
+{
+ "model": "Qwen/Qwen3-0.6B",
+ "messages": [
+ {"role": "user", "content": "Hello"}
+ ],
+ "temperature": 0.7,
+ "...": "any other OpenAI-compatible param"
+}
+```
+
+**Important:** `api_key` is **never** included in the encrypted payload. It is sent only as the `Authorization: Bearer` HTTP header.
+
+### 6.2 Response Dict (after decryption)
+
+```json
+{
+ "id": "chatcmpl-123",
+ "object": "chat.completion",
+ "created": 1234567890,
+ "model": "Qwen/Qwen3-0.6B",
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": "The capital of France is Paris.",
+ "tool_calls": [...],
+ "reasoning_content": "..."
+ },
+ "finish_reason": "stop"
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 10,
+ "completion_tokens": 20,
+ "total_tokens": 30
+ },
+ "_metadata": {
+ "payload_id": "openai-compat-abc123",
+ "processed_at": 1765250382,
+ "is_encrypted": true,
+ "encryption_algorithm": "hybrid-aes256-rsa4096",
+ "security_tier": "standard",
+ "memory_protection": {
+ "platform": "linux",
+ "memory_locking": true,
+ "secure_zeroing": true,
+ "core_dump_prevention": true
+ },
+ "cuda_device": {
+ "available": true,
+ "device_hash": "sha256_hex"
+ }
+ }
+}
+```
+
+### 6.3 Security Tier Values
+
+| Value | Hardware | Use case |
+|---|---|---|
+| `"standard"` | GPU | General secure inference |
+| `"high"` | CPU/GPU | Sensitive business data |
+| `"maximum"` | CPU only | PHI, classified data |
+
+Sent as `X-Security-Tier` HTTP header. Invalid values raise `ValueError`.
+
+---
+
+## 7. Error Class Hierarchy
+
+All errors are exceptions. `APIError` subclasses carry `status_code` and `error_details`.
+
+```
+Exception
+└── APIError (base, has message/status_code/error_details)
+ ├── AuthenticationError (status_code=401)
+ ├── InvalidRequestError (status_code=400)
+ ├── RateLimitError (status_code=429)
+ ├── ForbiddenError (status_code=403)
+ ├── ServerError (status_code=500)
+ └── ServiceUnavailableError (status_code=503)
+
+Exception
+└── SecurityError (crypto/key failure, no status_code)
+
+Exception
+└── APIConnectionError (network failure, no status_code)
+```
+
+**APIError constructor:**
+```python
+APIError(message: str, status_code: Optional[int] = None, error_details: Optional[Dict] = None)
+```
+
+---
+
+## 8. SecureMemory Module
+
+Optional, platform-specific. Fails gracefully if unavailable (e.g. Windows on some Python builds).
+
+### `SecureBuffer` class
+
+Wraps a `bytearray` with memory locking and guaranteed zeroing on exit.
+
+| Attribute/Method | Type | Description |
+|---|---|---|
+| `data` | `bytearray` | Underlying mutable buffer |
+| `address` | `int` | Memory address (via ctypes) |
+| `size` | `int` | Buffer size in bytes |
+| `lock() -> bool` | method | Attempt memory lock |
+| `unlock() -> bool` | method | Unlock memory |
+| `zero()` | method | Securely zero contents |
+| `__enter__` / `__exit__` | context mgr | Auto-lock on enter, auto-zero+unlock on exit |
+
+### `secure_bytearray(data: bytes \| bytearray, lock: bool = True) -> SecureBuffer` (context manager)
+
+Recommended secure handling. Converts input to `bytearray`, locks (best-effort), yields `SecureBuffer`. Always zeros on exit, even on exception.
+
+### `secure_bytes(data: bytes, lock: bool = True) -> SecureBuffer` (context manager, **deprecated**)
+
+Same as `secure_bytearray` but accepts immutable `bytes`. Emits deprecation warning. Original bytes cannot be zeroed.
+
+### `get_memory_protection_info() -> Dict[str, Any]`
+
+Returns protection capabilities:
+
+```json
+{
+ "enabled": true,
+ "platform": "linux",
+ "protection_level": "full",
+ "has_memory_locking": true,
+ "has_secure_zeroing": true,
+ "supports_full_protection": true,
+ "page_size": 4096
+}
+```
+
+`protection_level` values: `"full"`, `"zeroing_only"`, `"none"`.
+
+### `disable_secure_memory()` / `enable_secure_memory()`
+
+Globally disable/re-enable secure memory operations.
+
+---
+
+## 9. Constants
+
+| Constant | Value | Location | Notes |
+|---|---|---|---|
+| Protocol version | `"1.0"` | `SecureCompletionClient.py` | **Never change** — downgrade detection |
+| Algorithm string | `"hybrid-aes256-rsa4096"` | `SecureCompletionClient.py` | **Never change** — downgrade detection |
+| RSA key size | `4096` | `SecureCompletionClient.py` | Fixed |
+| RSA public exponent | `65537` | `SecureCompletionClient.py` | Fixed |
+| AES key size | `32` bytes (256-bit) | `SecureCompletionClient.py` | Per-request ephemeral |
+| GCM nonce size | `12` bytes (96-bit) | `SecureCompletionClient.py` | Per-request via `secrets.token_bytes` |
+| Max payload size | `10 * 1024 * 1024` (10 MB) | `SecureCompletionClient.py` | DoS protection |
+| Default max retries | `2` | Both client classes | Exponential backoff: 1s, 2s, 4s… |
+| Private key file mode | `0o600` | `SecureCompletionClient.py` | Owner read/write only |
+| Public key file mode | `0o644` | `SecureCompletionClient.py` | Owner rw, group/others r |
+| Min RSA key size (validation) | `2048` | `SecureCompletionClient.py` | `_validate_rsa_key` |
+| Valid security tiers | `["standard", "high", "maximum"]` | `SecureCompletionClient.py` | Case-sensitive |
+| Retryable status codes | `{429, 500, 502, 503, 504}` | `SecureCompletionClient.py` | |
+| Package version | `"0.2.7"` | `pyproject.toml` + `__init__.py` | Bump both |
+
+---
+
+## 10. Endpoint URLs
+
+| Endpoint | Method | Purpose |
+|---|---|---|
+| `{router_url}/pki/public_key` | GET | Fetch server RSA public key |
+| `{router_url}/v1/chat/secure_completion` | POST | Encrypted chat completion |
+
+---
+
+## 11. Key Lifecycle
+
+1. **First `create()` call** → `_ensure_keys()` runs (async, double-checked locking via `asyncio.Lock`).
+2. If `key_dir` is set:
+ - Try `load_keys()` from `{key_dir}/private_key.pem` + `{key_dir}/public_key.pem`.
+ - If that fails → `generate_keys(save_to_file=True, key_dir=key_dir)`.
+3. If `key_dir` is `None` → `generate_keys()` (ephemeral, in-memory only).
+4. Keys are reused across all subsequent calls until the client is discarded.
+
+---
+
+## 12. HTTP Client Details
+
+- Uses `httpx.AsyncClient` with `timeout=60.0`.
+- SSL verification enabled for HTTPS URLs; disabled for `http://`.
+- Request body is raw bytes (not JSON) — `Content-Type: application/octet-stream`.
+- Public key is URL-encoded in the `X-Public-Key` header.
+
+---
+
+## 13. Memory Protection Platform Matrix
+
+| Platform | Locking | Zeroing |
+|---|---|---|
+| Linux | `mlock()` via `libc.so.6` | `memset()` via `libc.so.6` |
+| Windows | `VirtualLock()` via `kernel32` | `RtlZeroMemory()` via `ntdll` + Python-level fallback |
+| macOS | `mlock()` via `libc.dylib` | `memset()` via `libc.dylib` |
+| Other | No lock | Python-level byte-by-byte zeroing |
+
+mlock may fail with `EPERM` (need `CAP_IPC_LOCK` or `ulimit -l` increase) — degrades to zeroing-only gracefully.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..2fae3b5
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+ ai.nomyo
+ nomyo4J
+ 1.0
+
+
+ 25
+ 25
+ UTF-8
+
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.44
+ compile
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 6.0.3
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.0
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/ai/nomyo/Constants.java b/src/main/java/ai/nomyo/Constants.java
new file mode 100644
index 0000000..831f5c0
--- /dev/null
+++ b/src/main/java/ai/nomyo/Constants.java
@@ -0,0 +1,199 @@
+package ai.nomyo;
+
+
+import java.util.Set;
+
+/**
+ * Constants used throughout the NOMYO Java client library.
+ *
+ *
These values correspond to the documented constants in the Python
+ * reference. Protocol version and algorithm strings are immutable and
+ * must never be changed — they are used for downgrade detection.
This class provides a familiar API surface matching {@code openai.ChatCompletion.create()}.
+ * All requests are automatically encrypted using hybrid AES-256-GCM + RSA-4096 encryption
+ * before being sent to the NOMYO router.
{@code "high"} — CPU/GPU (sensitive business data)
+ *
{@code "maximum"} — CPU only (PHI, classified data)
+ *
+ *
+ *
Key Persistence
+ *
Set {@code keyDir} to a directory path to persist RSA keys to disk.
+ * Keys are generated on first use and reused across all calls.
+ * Set {@code keyDir} to {@code null} for ephemeral keys (in-memory only, lost on restart).
+ */
+@Getter
+public class SecureChatCompletion {
+
+ private final SecureCompletionClient client;
+ private final String apiKey;
+ private final String keyDir;
+
+ /**
+ * Constructs a {@code SecureChatCompletion} with default settings.
+ *
+ *
Uses the default NOMYO router URL ({@code https://api.nomyo.ai}),
+ * HTTPS-only, secure memory enabled, ephemeral keys, and 2 retries.
+ */
+ public SecureChatCompletion() {
+ this(Constants.DEFAULT_BASE_URL, false, null, true, null, Constants.DEFAULT_MAX_RETRIES);
+ }
+
+ /**
+ * Constructs a {@code SecureChatCompletion} with the specified settings.
+ *
+ * @param baseUrl NOMYO Router base URL (HTTPS enforced unless {@code allowHttp} is {@code true})
+ * @param allowHttp permit {@code http://} URLs (development only)
+ * @param apiKey Bearer token for authentication (can also be passed per-call via {@link #create})
+ * @param secureMemory enable memory locking/zeroing (warns if unavailable)
+ * @param keyDir directory to persist RSA keys; {@code null} = ephemeral (in-memory only)
+ * @param maxRetries retries on 429/500/502/503/504 + network errors (exponential backoff: 1s, 2s, 4s…)
+ */
+ public SecureChatCompletion(
+ String baseUrl,
+ boolean allowHttp,
+ String apiKey,
+ boolean secureMemory,
+ String keyDir,
+ int maxRetries
+ ) {
+ this.client = new SecureCompletionClient(baseUrl, allowHttp, secureMemory, maxRetries);
+ this.apiKey = apiKey;
+ this.keyDir = keyDir;
+ }
+
+ /**
+ * Creates a chat completion with the specified parameters.
+ *
+ *
This is the main entrypoint, with the same signature as
+ * {@code openai.ChatCompletion.create()}. Returns a map (not an object)
+ * containing the OpenAI-compatible response.
+ *
+ *
Parameters
+ *
+ *
Param
Type
Required
Description
+ *
{@code model}
{@code String}
yes
Model identifier, e.g. "Qwen/Qwen3-0.6B"
+ *
{@code messages}
{@code List
yes
OpenAI-format messages
+ *
{@code temperature}
{@code Double}
no
0–2
+ *
{@code maxTokens}
{@code Integer}
no
Maximum tokens in response
+ *
{@code topP}
{@code Double}
no
Top-p sampling parameter
+ *
{@code stop}
{@code String | List}
no
Stop sequences
+ *
{@code presencePenalty}
{@code Double}
no
-2.0 to 2.0
+ *
{@code frequencyPenalty}
{@code Double}
no
-2.0 to 2.0
+ *
{@code n}
{@code Integer}
no
Number of completions
+ *
{@code bestOf}
{@code Integer}
no
+ *
{@code seed}
{@code Integer}
no
Reproducibility seed
+ *
{@code logitBias}
{@code Map}
no
Token bias map
+ *
{@code user}
{@code String}
no
End-user identifier
+ *
{@code tools}
{@code List
no
Tool definitions passed through to llama.cpp
+ *
{@code toolChoice}
{@code String}
no
"auto", "none", or specific tool name
+ *
{@code responseFormat}
{@code Map}
no
{"type": "json_object"} or {"type": "json_schema", ...}
+ *
{@code stream}
{@code Boolean}
no
NOT supported. Server rejects with HTTP 400. Always use {@code false}.
"standard", "high", or "maximum". Invalid values raise {@code ValueError}.
+ *
{@code apiKey}
{@code String}
no
Per-call override of instance {@code apiKey}.
+ *
+ *
+ * @param model model identifier (required)
+ * @param messages OpenAI-format message list (required)
+ * @param kwargs additional OpenAI-compatible parameters
+ * @return OpenAI-compatible response map (see §6.2 of reference docs)
+ * @throws SecurityError if encryption/decryption fails
+ * @throws APIConnectionError if a network error occurs
+ * @throws InvalidRequestError if the API returns 400
+ * @throws AuthenticationError if the API returns 401
+ * @throws ForbiddenError if the API returns 403
+ * @throws RateLimitError if the API returns 429
+ * @throws ServerError if the API returns 500
+ * @throws ServiceUnavailableError if the API returns 503
+ * @throws APIError for other errors
+ */
+ public Map create(String model, List
+ *
+ * @param payload the payload to encrypt (OpenAI-compatible chat parameters)
+ * @return raw encrypted bytes (JSON package serialized to bytes)
+ * @throws SecurityError if encryption fails or keys are not loaded
+ */
+ public CompletableFuture encryptPayload(Map payload) {
+ throw new UnsupportedOperationException("Not yet implemented");
+ }
+
+ /**
+ * Core hybrid encryption routine.
+ */
+ public CompletableFuture doEncrypt(byte[] payloadBytes, byte[] aesKey) {
+ throw new UnsupportedOperationException("Not yet implemented");
+ }
+
+ // ── Decryption ──────────────────────────────────────────────────
+
+ /**
+ * Decrypts a server response.
+ */
+ public CompletableFuture> decryptResponse(byte[] encryptedResponse, String payloadId) {
+ throw new UnsupportedOperationException("Not yet implemented");
+ }
+
+ // ── Secure Request Lifecycle ────────────────────────────────────
+
+ /**
+ * Full request lifecycle: encrypt → HTTP POST → retry → decrypt → return.
+ *
+ *
+ *
+ * @param payload the payload to send (OpenAI-compatible chat parameters)
+ * @param payloadId unique payload identifier
+ * @param apiKey optional API key for authentication
+ * @param securityTier optional security tier ({@code "standard"}, {@code "high"}, or {@code "maximum"})
+ * @return the decrypted response map
+ * @throws SecurityError if encryption/decryption fails
+ * @throws APIConnectionError if a network error occurs
+ * @throws InvalidRequestError if the API returns 400
+ * @throws AuthenticationError if the API returns 401
+ * @throws ForbiddenError if the API returns 403
+ * @throws RateLimitError if the API returns 429
+ * @throws ServerError if the API returns 500
+ * @throws ServiceUnavailableError if the API returns 503
+ * @throws APIError for other non-retryable errors
+ */
+ public CompletableFuture> sendSecureRequest(
+ Map payload,
+ String payloadId,
+ String apiKey,
+ String securityTier
+ ) {
+ throw new UnsupportedOperationException("Not yet implemented");
+ }
+
+ /**
+ * Sends a secure request without a security tier.
+ *
+ * @param payload the payload to send
+ * @param payloadId unique payload identifier
+ * @param apiKey optional API key for authentication
+ * @return the decrypted response map
+ */
+ public CompletableFuture> sendSecureRequest(
+ Map payload,
+ String payloadId,
+ String apiKey
+ ) {
+ return sendSecureRequest(payload, payloadId, apiKey, null);
+ }
+
+ /**
+ * Sends a secure request with no API key or security tier.
+ *
+ * @param payload the payload to send
+ * @param payloadId unique payload identifier
+ * @return the decrypted response map
+ */
+ public CompletableFuture> sendSecureRequest(
+ Map payload,
+ String payloadId
+ ) {
+ return sendSecureRequest(payload, payloadId, null, null);
+ }
+
+ // ── Key Initialization ──────────────────────────────────────────
+
+ /**
+ * Ensures RSA keys are loaded or generated.
+ *
+ *
Uses double-checked locking via {@link ReentrantLock} to ensure
+ * thread-safe initialization. If {@code keyDir} is set, attempts to
+ * load keys from disk first; if that fails, generates new keys.
+ *
+ * @param keyDir directory to persist keys, or {@code null} for ephemeral
+ */
+ public void ensureKeys(String keyDir) {
+ if (keysInitialized) return;
+ keyInitLock.lock();
+ try {
+ if (keysInitialized) return;
+ // TODO: implement key loading/generation
+ keysInitialized = true;
+ } finally {
+ keyInitLock.unlock();
+ }
+ }
+
+ // ── Key Validation ──────────────────────────────────────────────
+
+ /**
+ * Validates that an RSA key meets the minimum size requirement.
+ *
+ * @param key the RSA key to validate
+ * @throws SecurityError if the key size is less than {@link Constants#MIN_RSA_KEY_SIZE} bits
+ */
+ public void validateRsaKey(PrivateKey key) throws SecurityError {
+ if (key == null) {
+ throw new SecurityError("RSA key is null");
+ }
+ int keySize = key.getEncoded() != null ? key.getEncoded().length * 8 : 0;
+
+ System.out.println("Keysize: " + keySize);
+
+ if (keySize < Constants.MIN_RSA_KEY_SIZE) {
+ throw new SecurityError(
+ "RSA key size " + keySize + " bits is below minimum " + Constants.MIN_RSA_KEY_SIZE + " bits"
+ );
+ }
+ }
+
+ // ── HTTP Status → Exception Mapping ─────────────────────────────
+
+ /**
+ * Maps an HTTP status code to the appropriate exception.
+ */
+ public Exception mapHttpStatus(int statusCode, String responseBody) {
+ switch (statusCode) {
+ case 200:
+ return null;
+ case 400:
+ return new InvalidRequestError("Invalid request: " + (responseBody != null ? responseBody : "no body"));
+ case 401:
+ return new AuthenticationError("Authentication failed: " + (responseBody != null ? responseBody : "no body"));
+ case 403:
+ return new ForbiddenError("Access forbidden: " + (responseBody != null ? responseBody : "no body"));
+ case 404:
+ return new APIError("Not found: " + (responseBody != null ? responseBody : "no body"));
+ case 429:
+ return new RateLimitError("Rate limit exceeded: " + (responseBody != null ? responseBody : "no body"));
+ case 500:
+ return new ServerError("Internal server error: " + (responseBody != null ? responseBody : "no body"));
+ case 503:
+ return new ServiceUnavailableError("Service unavailable: " + (responseBody != null ? responseBody : "no body"));
+ case 502:
+ case 504:
+ return new APIError("Gateway error: " + (responseBody != null ? responseBody : "no body"));
+ default:
+ return new APIError("Unexpected status " + statusCode + ": " + (responseBody != null ? responseBody : "no body"));
+ }
+ }
+
+ // ── URL Encoding ────────────────────────────────────────────────
+
+ /**
+ * URL-encodes a public key PEM string for use in the {@code X-Public-Key} header.
+ */
+ public String urlEncodePublicKey(String pemKey) {
+ return java.net.URLEncoder.encode(pemKey, StandardCharsets.UTF_8);
+ }
+
+ // ── Getters ─────────────────────────────────────────────────────
+
+ /**
+ * Closes the client and releases any resources.
+ */
+ public void close() {
+ throw new UnsupportedOperationException("Not yet implemented");
+ }
+}
diff --git a/src/main/java/ai/nomyo/SecureMemory.java b/src/main/java/ai/nomyo/SecureMemory.java
new file mode 100644
index 0000000..e16d7fc
--- /dev/null
+++ b/src/main/java/ai/nomyo/SecureMemory.java
@@ -0,0 +1,237 @@
+package ai.nomyo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.util.Map;
+
+/**
+ * Cross-platform memory locking and secure zeroing utilities.
+ *
+ *
This module provides optional memory protection for sensitive
+ * cryptographic buffers. It fails gracefully if memory locking is
+ * unavailable on the current platform (e.g., Windows on some JVM
+ * configurations).
+ *
+ *
Protection Levels
+ *
+ *
"full" — Memory locking and secure zeroing both available
+ *
"zeroing_only" — Only secure zeroing available
+ *
"none" — No memory protection available
+ *
+ *
+ *
Usage
+ *
{@code
+ * try (SecureBuffer buf = SecureMemory.secureBytearray(sensitiveData)) {
+ * // buf is locked and ready to use
+ * process(buf.getData());
+ * }
+ * // buf is automatically zeroed and unlocked on exit
+ * }
+ */
+public final class SecureMemory {
+
+ @Getter
+ @Setter
+ private static volatile boolean secureMemoryEnabled = true;
+
+ @Getter
+ private static final boolean HAS_MEMORY_LOCKING;
+ @Getter
+ private static final boolean HAS_SECURE_ZEROING;
+
+ static {
+ boolean locking = false;
+ boolean zeroing = false;
+ try {
+ locking = initMemoryLocking();
+ zeroing = true; // Secure zeroing is always available at the JVM level
+ } catch (Throwable t) {
+ // Degrade gracefully
+ }
+
+ HAS_MEMORY_LOCKING = locking;
+ HAS_SECURE_ZEROING = zeroing;
+ }
+
+ private static boolean initMemoryLocking() {
+ // FFM doesn't support memory locking at this point in time
+ // TODO: Bypass this with native libraries
+ return false;
+ }
+
+ /**
+ * Wraps a byte array with memory locking and guaranteed zeroing on exit.
+ *
+ *
Implements {@link AutoCloseable} for use with try-with-resources.
+ * The buffer is automatically zeroed and unlocked when closed, even
+ * if an exception occurs.
+ */
+ public static class SecureBuffer implements AutoCloseable {
+
+ private final Arena arena;
+
+ @Getter
+ private final MemorySegment data;
+
+ @Getter
+ private final long size;
+
+ @Getter
+ private final long address;
+ private boolean locked;
+ private boolean closed;
+
+ /**
+ * Creates a new SecureBuffer wrapping the given data.
+ *
+ * @param data the byte array to wrap
+ * @param lock whether to attempt memory locking
+ */
+ public SecureBuffer(byte[] data, boolean lock) {
+ this.arena = Arena.ofConfined();
+ this.data = data != null ? this.arena.allocate(data.length) : MemorySegment.NULL;
+
+ if (data != null) {
+ this.data.asByteBuffer().put(data);
+ }
+
+ this.size = this.data.byteSize();
+ this.address = this.data.address();
+
+ this.locked = false;
+ this.closed = false;
+
+ if (lock && SecureMemory.secureMemoryEnabled) {
+ this.locked = lock();
+ }
+ }
+
+ /**
+ * Attempts to lock the buffer in memory, preventing swapping to disk.
+ *
+ * @return {@code true} if locking succeeded, {@code false} otherwise
+ */
+ public boolean lock() {
+ //data = data.asReadOnly();
+ return false;
+ }
+
+ /**
+ * Unlocks the buffer, allowing it to be swapped to disk.
+ *
+ * @return {@code true} if unlocking succeeded, {@code false} otherwise
+ */
+ public boolean unlock() {
+ if (!locked) return false;
+ locked = false;
+ return false;
+ }
+
+ /**
+ * Securely zeros the buffer contents.
+ */
+ public void zero() {
+ if (data != null) {
+ data.fill((byte) 0);
+ }
+ }
+
+ @Override
+ public void close() {
+ if (closed) return;
+
+ zero();
+ unlock();
+
+ arena.close();
+ closed = true;
+ }
+ }
+
+ /**
+ * Creates a SecureBuffer for the given data with memory locking.
+ *
+ *
This is the recommended way to handle sensitive data. The returned
+ * buffer should be used within a try-with-resources block to ensure
+ * secure zeroing on exit.
+ *
+ * @param data the sensitive data bytes
+ * @param lock whether to attempt memory locking
+ * @return a new SecureBuffer
+ */
+ public static SecureBuffer secureByteArray(byte[] data, boolean lock) {
+ return new SecureBuffer(data, lock);
+ }
+
+ /**
+ * Creates a SecureBuffer for the given data with memory locking.
+ * Convenience variant that always attempts locking.
+ *
+ * @param data the sensitive data bytes
+ * @return a new SecureBuffer
+ */
+ public static SecureBuffer secureByteArray(byte[] data) {
+ return secureByteArray(data, true);
+ }
+
+ /**
+ * Creates a SecureBuffer for the given data with memory locking.
+ *
+ *
Deprecated: Use {@link #secureByteArray(byte[])} instead.
+ * This method exists for compatibility with the Python reference.
+ *
+ * @param data the sensitive data bytes
+ * @param lock whether to attempt memory locking
+ * @return a new SecureBuffer
+ * @deprecated Use {@link #secureByteArray(byte[])} instead
+ */
+ @Deprecated
+ public static SecureBuffer secureBytes(byte[] data, boolean lock) {
+ return new SecureBuffer(data, lock);
+ }
+
+ /**
+ * Creates a SecureBuffer for the given data with memory locking.
+ *
+ *
Deprecated: Use {@link #secureByteArray(byte[])} instead.
+ * This method exists for compatibility with the Python reference.
+ *
+ * @param data the sensitive data bytes
+ * @return a new SecureBuffer
+ * @deprecated Use {@link #secureByteArray(byte[])} instead
+ */
+ @Deprecated
+ public static SecureBuffer secureBytes(byte[] data) {
+ return secureBytes(data, true);
+ }
+
+ /**
+ * Returns information about the current memory protection capabilities.
+ *
+ * @return a map of protection capabilities
+ */
+ public static Map getMemoryProtectionInfo() {
+ String protectionLevel;
+ if (HAS_MEMORY_LOCKING && HAS_SECURE_ZEROING) {
+ protectionLevel = "full";
+ } else if (HAS_SECURE_ZEROING) {
+ protectionLevel = "zeroing_only";
+ } else {
+ protectionLevel = "none";
+ }
+
+ boolean supportsFull = HAS_MEMORY_LOCKING && HAS_SECURE_ZEROING && secureMemoryEnabled;
+
+ return Map.of(
+ "enabled", secureMemoryEnabled,
+ "protection_level", protectionLevel,
+ "has_memory_locking", HAS_MEMORY_LOCKING,
+ "has_secure_zeroing", HAS_SECURE_ZEROING,
+ "supports_full_protection", supportsFull,
+ "page_size", Constants.PAGE_SIZE
+ );
+ }
+}
diff --git a/src/main/java/ai/nomyo/errors/APIConnectionError.java b/src/main/java/ai/nomyo/errors/APIConnectionError.java
new file mode 100644
index 0000000..f1a6d02
--- /dev/null
+++ b/src/main/java/ai/nomyo/errors/APIConnectionError.java
@@ -0,0 +1,41 @@
+package ai.nomyo.errors;
+
+/**
+ * Exception thrown when a network failure occurs during communication
+ * with the NOMYO API server.
+ *
+ *
This includes connection timeouts, DNS resolution failures,
+ * TLS handshake failures, and other network-level errors. Unlike
+ * {@link APIError}, this exception does not carry an HTTP status code.
+ */
+public class APIConnectionError extends Exception {
+
+ /**
+ * Constructs an {@code APIConnectionError} with the specified detail message.
+ *
+ * @param message the detail message
+ */
+ public APIConnectionError(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs an {@code APIConnectionError} with the specified detail message
+ * and cause.
+ *
+ * @param message the detail message
+ * @param cause the cause of this exception, or {@code null}
+ */
+ public APIConnectionError(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs an {@code APIConnectionError} with the specified cause.
+ *
+ * @param cause the cause of this exception
+ */
+ public APIConnectionError(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/ai/nomyo/errors/APIError.java b/src/main/java/ai/nomyo/errors/APIError.java
new file mode 100644
index 0000000..df4dc36
--- /dev/null
+++ b/src/main/java/ai/nomyo/errors/APIError.java
@@ -0,0 +1,59 @@
+package ai.nomyo.errors;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Base exception for all NOMYO API errors.
+ *
+ *
All API error subclasses carry a {@code status_code} and optional
+ * {@code error_details} from the server response.
+ */
+public class APIError extends Exception {
+
+ private final Integer statusCode;
+ private final Map errorDetails;
+
+ /**
+ * Constructs an {@code APIError} with the specified detail message,
+ * status code, and error details.
+ *
+ * @param message the detail message
+ * @param statusCode the HTTP status code, or {@code null} if not applicable
+ * @param errorDetails additional error details from the server, or {@code null}
+ */
+ public APIError(String message, Integer statusCode, Map errorDetails) {
+ super(message);
+ this.statusCode = statusCode;
+ this.errorDetails = errorDetails != null
+ ? Collections.unmodifiableMap(errorDetails)
+ : Collections.emptyMap();
+ }
+
+ /**
+ * Constructs an {@code APIError} with the specified detail message.
+ *
+ * @param message the detail message
+ */
+ public APIError(String message) {
+ this(message, null, null);
+ }
+
+ /**
+ * Returns the HTTP status code associated with this error, or {@code null}.
+ *
+ * @return the status code, or {@code null}
+ */
+ public Integer getStatusCode() {
+ return statusCode;
+ }
+
+ /**
+ * Returns an unmodifiable map of additional error details from the server.
+ *
+ * @return the error details map (never {@code null})
+ */
+ public Map getErrorDetails() {
+ return errorDetails;
+ }
+}
diff --git a/src/main/java/ai/nomyo/errors/AuthenticationError.java b/src/main/java/ai/nomyo/errors/AuthenticationError.java
new file mode 100644
index 0000000..a219789
--- /dev/null
+++ b/src/main/java/ai/nomyo/errors/AuthenticationError.java
@@ -0,0 +1,41 @@
+package ai.nomyo.errors;
+
+import java.util.Map;
+
+/**
+ * Exception thrown when the API returns a 401 (Unauthorized) status.
+ */
+public class AuthenticationError extends APIError {
+
+ /**
+ * Constructs an {@code AuthenticationError} with the specified detail message,
+ * status code, and error details.
+ *
+ * @param message the detail message
+ * @param statusCode the HTTP status code (must be 401)
+ * @param errorDetails additional error details from the server, or {@code null}
+ */
+ public AuthenticationError(String message, Integer statusCode, Map errorDetails) {
+ super(message, statusCode, errorDetails);
+ }
+
+ /**
+ * Constructs an {@code AuthenticationError} with the specified detail message.
+ *
+ * @param message the detail message
+ */
+ public AuthenticationError(String message) {
+ super(message, 401, null);
+ }
+
+ /**
+ * Constructs an {@code AuthenticationError} with the specified detail message
+ * and error details.
+ *
+ * @param message the detail message
+ * @param errorDetails additional error details from the server, or {@code null}
+ */
+ public AuthenticationError(String message, Map errorDetails) {
+ super(message, 401, errorDetails);
+ }
+}
diff --git a/src/main/java/ai/nomyo/errors/ForbiddenError.java b/src/main/java/ai/nomyo/errors/ForbiddenError.java
new file mode 100644
index 0000000..7d9125b
--- /dev/null
+++ b/src/main/java/ai/nomyo/errors/ForbiddenError.java
@@ -0,0 +1,41 @@
+package ai.nomyo.errors;
+
+import java.util.Map;
+
+/**
+ * Exception thrown when the API returns a 403 (Forbidden) status.
+ */
+public class ForbiddenError extends APIError {
+
+ /**
+ * Constructs a {@code ForbiddenError} with the specified detail message,
+ * status code, and error details.
+ *
+ * @param message the detail message
+ * @param statusCode the HTTP status code (must be 403)
+ * @param errorDetails additional error details from the server, or {@code null}
+ */
+ public ForbiddenError(String message, Integer statusCode, Map errorDetails) {
+ super(message, statusCode, errorDetails);
+ }
+
+ /**
+ * Constructs a {@code ForbiddenError} with the specified detail message.
+ *
+ * @param message the detail message
+ */
+ public ForbiddenError(String message) {
+ super(message, 403, null);
+ }
+
+ /**
+ * Constructs a {@code ForbiddenError} with the specified detail message
+ * and error details.
+ *
+ * @param message the detail message
+ * @param errorDetails additional error details from the server, or {@code null}
+ */
+ public ForbiddenError(String message, Map errorDetails) {
+ super(message, 403, errorDetails);
+ }
+}
diff --git a/src/main/java/ai/nomyo/errors/InvalidRequestError.java b/src/main/java/ai/nomyo/errors/InvalidRequestError.java
new file mode 100644
index 0000000..357b2b8
--- /dev/null
+++ b/src/main/java/ai/nomyo/errors/InvalidRequestError.java
@@ -0,0 +1,41 @@
+package ai.nomyo.errors;
+
+import java.util.Map;
+
+/**
+ * Exception thrown when the API returns a 400 (Bad Request) status.
+ */
+public class InvalidRequestError extends APIError {
+
+ /**
+ * Constructs an {@code InvalidRequestError} with the specified detail message,
+ * status code, and error details.
+ *
+ * @param message the detail message
+ * @param statusCode the HTTP status code (must be 400)
+ * @param errorDetails additional error details from the server, or {@code null}
+ */
+ public InvalidRequestError(String message, Integer statusCode, Map errorDetails) {
+ super(message, statusCode, errorDetails);
+ }
+
+ /**
+ * Constructs an {@code InvalidRequestError} with the specified detail message.
+ *
+ * @param message the detail message
+ */
+ public InvalidRequestError(String message) {
+ super(message, 400, null);
+ }
+
+ /**
+ * Constructs an {@code InvalidRequestError} with the specified detail message
+ * and error details.
+ *
+ * @param message the detail message
+ * @param errorDetails additional error details from the server, or {@code null}
+ */
+ public InvalidRequestError(String message, Map errorDetails) {
+ super(message, 400, errorDetails);
+ }
+}
diff --git a/src/main/java/ai/nomyo/errors/RateLimitError.java b/src/main/java/ai/nomyo/errors/RateLimitError.java
new file mode 100644
index 0000000..b56da8d
--- /dev/null
+++ b/src/main/java/ai/nomyo/errors/RateLimitError.java
@@ -0,0 +1,41 @@
+package ai.nomyo.errors;
+
+import java.util.Map;
+
+/**
+ * Exception thrown when the API returns a 429 (Too Many Requests) status.
+ */
+public class RateLimitError extends APIError {
+
+ /**
+ * Constructs a {@code RateLimitError} with the specified detail message,
+ * status code, and error details.
+ *
+ * @param message the detail message
+ * @param statusCode the HTTP status code (must be 429)
+ * @param errorDetails additional error details from the server, or {@code null}
+ */
+ public RateLimitError(String message, Integer statusCode, Map errorDetails) {
+ super(message, statusCode, errorDetails);
+ }
+
+ /**
+ * Constructs a {@code RateLimitError} with the specified detail message.
+ *
+ * @param message the detail message
+ */
+ public RateLimitError(String message) {
+ super(message, 429, null);
+ }
+
+ /**
+ * Constructs a {@code RateLimitError} with the specified detail message
+ * and error details.
+ *
+ * @param message the detail message
+ * @param errorDetails additional error details from the server, or {@code null}
+ */
+ public RateLimitError(String message, Map errorDetails) {
+ super(message, 429, errorDetails);
+ }
+}
diff --git a/src/main/java/ai/nomyo/errors/SecurityError.java b/src/main/java/ai/nomyo/errors/SecurityError.java
new file mode 100644
index 0000000..8dd550e
--- /dev/null
+++ b/src/main/java/ai/nomyo/errors/SecurityError.java
@@ -0,0 +1,34 @@
+package ai.nomyo.errors;
+
+/**
+ * Exception thrown for cryptographic and key-related failures.
+ *
+ *
Unlike {@link APIError}, this exception carries no HTTP status code.
+ * It is used for issues such as invalid key sizes, encryption/decryption
+ * failures, and missing keys.
+ *
+ *
Any decryption failure (except JSON parse errors) raises
+ * {@code SecurityError("Decryption failed: integrity check or authentication failed")}.
The encrypted output format stores salt + IV + ciphertext in that order,
+ * all base64-encoded. This ensures the salt and IV are persisted alongside
+ * the ciphertext for decryption.
+ *
+ *
Binary Layout
+ *
+ * [ 16 bytes salt ][ 12 bytes IV ][ variable bytes ciphertext ]
+ *