Metadata-Version: 2.4
Name: ranbval-sdk
Version: 2.2.0
Summary: Keep API secrets out of plaintext config: layered .ranbval* env files, decrypt vault tokens only when used, with built-in repo-allowlist enforcement and automatic usage telemetry to the Live Monitor—minimal deps, your own HTTP/SDK stack.
License-Expression: MIT
License-File: LICENSE
Author: Ahsan Tariq
Requires-Python: >=3.10
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Topic :: Security
Classifier: Topic :: Security :: Cryptography
Requires-Dist: certifi (>=2024.0.0)
Requires-Dist: cryptography (>=42.0.0)
Project-URL: Documentation, https://api.ranbval.com/docs
Project-URL: Homepage, https://www.ranbval.com
Project-URL: Repository, https://github.com/TariqDreamsTech/ranbval-sdk
Description-Content-Type: text/markdown

[![PyPI](https://img.shields.io/pypi/v/ranbval-sdk)](https://pypi.org/project/ranbval-sdk/)
[![Python](https://img.shields.io/pypi/pyversions/ranbval-sdk)](https://pypi.org/project/ranbval-sdk/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

# Ranbval SDK `v2.2.0`

**The Python client for Ranbval — a secret manager for API keys.** Encrypt secrets in the
Ranbval dashboard, store the encrypted tokens in `.ranbval` files, and decrypt them only at
runtime — AES-256-GCM with PBKDF2 key derivation, no plaintext ever touches source control.
Unlike a plain `.env`, a stolen config is useless off your allowlisted repos, and every use is
attributable in the Live Monitor.

```bash
pip install ranbval-sdk
```

---

## Why Ranbval Exists

Every team now juggles a pile of API keys — LLM providers, payment processors, databases,
third-party services. Those keys leak constantly, and almost always the same handful of ways:

- A key gets **committed to Git** — and bots scrape public repos within minutes.
- A `.env` file is **copied and shared** over Slack/email — then forwarded, forgotten, and
  lives forever with no expiry.
- A key is **accidentally printed to logs** or captured by an error reporter — and now it sits
  in Datadog/Sentry, readable by the whole org, retained for years.
- When a key *does* leak, **nobody knows who leaked it or which repo burned the tokens** — so
  you can't rotate with confidence.

`.env` + `load_dotenv()` does nothing about any of this: the secret is plaintext on disk, works
anywhere it's copied, forever, with zero visibility. Ranbval is built to close exactly these
gaps.

### What it actually protects (and what nothing can)

Be clear-eyed about the threat model — it's what makes the guarantees trustworthy:

- **What no tool can stop:** an attacker who already runs code *inside your process*. If they can
  execute in your app, they can read `os.environ`, hook functions, or dump memory — and **no**
  secret manager (Vault, AWS/GCP Secrets Manager, Doppler, Ranbval) prevents that. It isn't the
  real-world leak vector.
- **What Ranbval does stop** — the leaks that actually happen:

| Real-world leak | `.env` | Ranbval |
|---|---|---|
| Key committed to Git | 🔴 plaintext, public instantly | 🟢 encrypted token — a commit leaks nothing usable |
| Config file copied / shared | 🔴 works anywhere, forever | 🟢 **useless without the project secret *and* an allowlisted repo** |
| Key printed to logs / captured by Sentry | 🔴 sits in log storage for years | 🟢 `SecretString` masks every display path; can't be pickled into a cache/report |
| A key leaks — who? which repo? | 🔴 zero visibility | 🟢 **Live Monitor** flags the same credential on a new device/IP → rotate with proof |

The crown jewel is the **repo allowlist**: even if someone steals your entire `.ranbval` file
*and* your project secret, they still can't decrypt it from a repo that isn't on your
control-plane allowlist. A stolen config is a dead config.

### An analogy

You can't make a house key that opens *your* door but that a thief holding it can't use — if the
key opens the lock, whoever holds it gets in. That's physics, not a flaw. Real security comes
from three other things, and Ranbval gives you all three:

1. **The key isn't lying in the street** → plaintext never touches Git (encrypted tokens).
2. **The key only works at your house** → the repo allowlist makes a stolen file worthless elsewhere.
3. **An alarm rings if a stranger walks in** → leak detection alerts on a new device/IP.

### Why use it

- **Drop-in.** One `load_ranbval()` replaces scattered `load_dotenv()`; keys pass straight into
  your existing SDKs — Ranbval ships no vendor dependencies.
- **Safe by default.** Secrets are sealed `SecretString`s that refuse to print, log, or serialize;
  plain config is opt-in plaintext via `[public]` sections and `public()`.
- **Accountable.** Every decrypt is attributable, and misuse is detectable — something a plain
  `.env` can never offer.

---

## Quick Start

```python
from ranbval_sdk import load_ranbval, decrypt_key
import os, openai

# 1. Load encrypted config from .ranbval files (no network, no decryption)
load_ranbval()

# 2. Decrypt a vault token — returns a SecretString, never printable.
#    This also auto-reports the usage to your Live Monitor (no extra code).
api_key = decrypt_key("OPENAI_API_KEY")

# 3. Pass directly to the SDK — value is never exposed in logs or prints
client = openai.OpenAI(api_key=api_key.use())

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Hello"}],
)
```

`.ranbval.local` (never commit this file):
```bash
RANBVAL_PROJECT_SECRET=your_dashboard_project_secret
OPENAI_API_KEY=ranbval.4ii0a022aa.p1GOZ...ahsan
```

---

## Module Reference

| Symbol | Description |
|--------|-------------|
| `load_ranbval()` | Merges layered `.ranbval*` files into `os.environ` |
| `public()` | Read a plaintext (unencrypted) config value — never decrypts |
| `public_config()` | Dict of every key declared under `[public]` |
| `proxy_token()` | Raw encrypted token for a `[proxy]` key — pass to `proxy_request()` (never decrypted client-side) |
| `safe_decrypt()` | Decrypts a vault token string → `SecretString` |
| `decrypt_key()` | Reads an env var and decrypts it in one call |
| `SecretString` | Wrapper that blocks all display paths — value only via `.use()` |
| `proxy_request()` | Route an HTTP request through the Ranbval proxy (key injected server-side) |
| `emit_telemetry()` | Record a **custom** usage event (basic usage is auto-reported on every `decrypt_key()`) |
| `get_audit_log()` | Return the in-process audit log list |
| `clear_audit_log()` | Clear the in-process audit log |
| `get_project_key()` | Read `RANBVAL_PROJECT_SECRET` from env |
| `find_ranbval_file()` | Locate the nearest `.ranbval*` file on disk |
| `find_ranbval_directory()` | Locate the config root directory |
| `resolve_ranbval_mode()` | Determine the active mode from env/args |

---

## Package Layout

Everything is organized by concern. You only import from the top level
(`from ranbval_sdk import …`); the table shows where each piece lives.

```
ranbval_sdk/
├── __init__.py          # the public API (re-exports everything below)
├── exceptions.py        # RanbvalError hierarchy
├── py.typed             # ships type information (PEP 561)
├── config/              # your .ranbval configuration surface
│   ├── loader.py        #   load_ranbval, find_*, resolve_ranbval_mode, get_project_key
│   ├── access.py        #   imperative access — Vault, env, inject, secrets, iter_secrets
│   └── declarative.py   #   class-based access — Secret, SecretConfig
├── crypto/              # cryptography & sealed secrets (only crypto lives here)
│   ├── cipher.py        #   AES-256-GCM decrypt + project-secret resolution
│   ├── secret_string.py #   SecretString — the sealed, never-printable value
│   └── audit.py         #   in-memory log of every .use()
├── policy/              # provenance & access policy (the decrypt gate)
│   └── repo.py          #   git-remote allowlist enforcement (server-controlled)
├── serializers/         # wire (de)serializers — one module per payload shape
│   ├── telemetry.py     #   /api/telemetry body + security metadata
│   ├── proxy.py         #   /api/execute request body
│   ├── token.py         #   parse ranbval.<salt>.<blob>.<label>
│   └── audit.py         #   AuditEntry record shape
├── telemetry/           # usage reporting to the Live Monitor
│   ├── client.py        #   emit_telemetry / aemit_telemetry (I/O)
│   ├── context.py       #   collect_client_context — gather client runtime signals
│   ├── sampling.py      #   adaptive aggregation (first-seen send, repeats counted)
│   └── decorators.py    #   @track / tracked()
├── integrations/        # optional server-side proxy
│   └── proxy.py         #   proxy_request / aproxy_request (key never leaves the server)
└── _internal/           # private cross-cutting utilities
    ├── defaults.py      #   shared constants
    ├── logging.py       #   opt-in stderr diagnostics (RANBVAL_TELEMETRY_DEBUG)
    └── transport.py     #   HTTPS via urllib + certifi
```

> Layered by responsibility: **gather** (`telemetry.context`) → **shape** (`serializers/`)
> → **send** (`telemetry.client`). Policy enforcement (`policy/`) is separate from
> cryptography (`crypto/`). You still only import from the top level.

---

## Function Reference

### `load_ranbval()`

Loads configuration from `.ranbval*` files into `os.environ`. No network calls, no decryption, zero side effects on import.

```python
from ranbval_sdk import load_ranbval

load_ranbval()                              # auto-discover from cwd upward
load_ranbval(mode="production")             # force a specific mode
load_ranbval(start="/path/to/project")      # start search from a custom directory
load_ranbval("/absolute/path/to/file")      # single file, skip layer discovery
load_ranbval(override=True)                 # file values overwrite existing os.environ
```

**How it finds files**

Walks from `cwd` upward until it finds a directory containing `.ranbval` or any `.ranbval.*` file. That becomes the config root.

**Merge order** (later file wins for duplicate keys):

```
.ranbval                   ← shared base
.ranbval.{mode}            ← e.g. .ranbval.production
.ranbval.local             ← machine-only, add to .gitignore
.ranbval.{mode}.local      ← highest priority
```

**Mode resolution order:**
1. `load_ranbval(mode="...")` explicit argument
2. `RANBVAL_ENV` environment variable
3. `ENVIRONMENT` environment variable
4. `ENV` environment variable
5. Default: `development`

**Returns:** `True` if at least one file was read, `False` if none found.

**Example `.ranbval` file:**

```bash
# Plain values — safe to commit
APP_NAME=my-app
DATABASE_URL=postgresql://localhost/mydb

# Encrypted vault token — generated in the Ranbval dashboard
OPENAI_API_KEY=ranbval.4ii0a022aa.p1GOZ...ahsan
```

---

### `safe_decrypt()`

Decrypts a `ranbval.*` vault token string using AES-256-GCM with PBKDF2 key derivation.

```python
from ranbval_sdk import load_ranbval, safe_decrypt
import os

load_ranbval()

secret = safe_decrypt(
    os.environ["OPENAI_API_KEY"],          # the ranbval.* token string
    os.environ["RANBVAL_PROJECT_SECRET"],  # your project secret
)

client = openai.OpenAI(api_key=secret.use())
```

**Returns:** a [`SecretString`](#secretstring) — the decrypted value is never accessible via print, str, repr, f-strings, or logs.

```python
print(secret)        # → [ranbval:secret]
str(secret)          # → [ranbval:secret]
f"key={secret}"      # → key=[ranbval:secret]
repr(secret)         # → SecretString(***)
len(secret)          # → 164  (safe — reveals only length)

# Only correct usage:
client = openai.OpenAI(api_key=secret.use())
headers = {"Authorization": f"Bearer {secret.use()}"}
```

**Raises:**
- `RepoNotAllowedError` (a `PermissionError`) — this Git repo is not in the allowed list
- `RanbvalDecryptError` (a `ValueError`) — wrong project secret or corrupted token

The repo allowlist is **enforced by the control plane and cannot be skipped on the client** —
there is no local bypass flag. Manage the allowed repositories from the Ranbval dashboard.

---

### `decrypt_key()`

Convenience wrapper: reads an env var and decrypts it in one call. The project secret is read from `RANBVAL_PROJECT_SECRET` automatically.

```python
from ranbval_sdk import load_ranbval, decrypt_key

load_ranbval()

# Reads os.environ["OPENAI_API_KEY"] and os.environ["RANBVAL_PROJECT_SECRET"]
api_key = decrypt_key("OPENAI_API_KEY")

client = openai.OpenAI(api_key=api_key.use())
```

This is the recommended pattern for most applications — it reduces boilerplate and keeps the project secret out of your application code. Each call also **auto-reports the usage** to the Live Monitor.

**Raises:** `RanbvalConfigError` (env var not set / no project secret), `RanbvalDecryptError` (wrong secret or corrupt token), `RepoNotAllowedError` (repo not in the allowlist) — all subclasses of `RanbvalError`, and each also a subclass of the built-in it replaces (`ValueError` / `PermissionError`).

---

### `SecretString`

A wrapper that blocks the *accidental* ways a secret leaks — print, logging, f-strings, repr, and even serialization (pickle/copy). It cannot stop a *deliberate* reveal, and it makes no promise your OS/runtime can't keep (see **Honest limits** below).

```python
from ranbval_sdk import SecretString

# Created automatically by safe_decrypt() / decrypt_key()
# — but you can also wrap your own values:
secret = SecretString("sk-proj-super-secret-key", label="openai")

print(secret)           # [ranbval:secret]
repr(secret)            # SecretString(***)      ← what Sentry/error reporters capture
f"key={secret}"         # key=[ranbval:secret]
"key=%s" % secret       # key=[ranbval:secret]
str(secret)             # [ranbval:secret]
len(secret)             # 26  ← safe
pickle.dumps(secret)    # TypeError — can't ride out via cache/queue/error report
copy.deepcopy(secret)   # TypeError — no silent plaintext duplicate

# Only way to get the real value:
real_value = secret.use()
```

**The one rule that keeps a secret unseen:** call `.use()` **only inline, right where you hand it to the SDK** — never store it in a variable and never print it:

```python
client = openai.OpenAI(api_key=decrypt_key("OPENAI_KEY").use())   # ✓ correct
headers = {"Authorization": f"Bearer {decrypt_key('X').use()}"}    # ✓ correct

secret = decrypt_key("OPENAI_KEY")
print(f"Using key: {secret}")            # → Using key: [ranbval:secret]   (masked)
```

**Honest limits** (a security library must not over-promise):

- `.use()` returns a **real `str`** so third-party SDKs can build request headers with it. That means `secret.use()[:]` or `print(f"{secret.use()}")` *will* reveal the value — that is deliberate bypassing, not the accidental leak this guards. Anything the SDK can read to build a request, code can read too.
- Memory "zeroing" and `mlock` are **best-effort defence-in-depth, not guarantees.** In CPython the interpreter and SDK make immutable `str`/`bytes` copies this class can't pin or wipe. An attacker who can read your process memory (ptrace / core dump / debugger) is out of scope for any Python SDK.
- The real protection is upstream: plaintext never touches your repo, and the control plane governs who may decrypt. RAM hardening is a minor extra layer.

| Method / Property | Description |
|-------------------|-------------|
| `.use()` | Returns the raw string — the only access point |
| `len(secret)` | Length of the secret (safe to log) |
| `.label` | Optional name set at creation |
| `==` | Compares two `SecretString` values securely |
| `pickle` / `copy` | Refused with `TypeError` — a secret can't be serialized or duplicated |

---

### Use with **any** provider

Ranbval is provider-agnostic. There is no per-vendor wrapper to learn or wait for — you decrypt
the key with `decrypt_key(...)` and pass `.use()` wherever that provider wants it. This works
identically for OpenAI, Anthropic, Google Gemini, Mistral, Cohere, AWS Bedrock, or a raw HTTP call
— every one of them is just "give me the key, here's where it goes":

```python
from ranbval_sdk import load_ranbval, decrypt_key, public
load_ranbval()

# OpenAI — constructor kwarg
import openai
client = openai.OpenAI(api_key=decrypt_key("OPENAI_API_KEY").use())

# Anthropic — constructor kwarg
import anthropic
claude = anthropic.Anthropic(api_key=decrypt_key("ANTHROPIC_API_KEY").use())

# Google Gemini — module-level configure()
import google.generativeai as genai
genai.configure(api_key=decrypt_key("GEMINI_API_KEY").use())

# Raw HTTP — any client, any header
import httpx
httpx.post(
    public("SERVICE_URL"),                                   # plaintext config
    headers={"Authorization": f"Bearer {decrypt_key('MY_API_KEY').use()}"},
    json={"hello": "world"},
)
```

That's the whole contract: **`decrypt_key("X").use()` gives you the plaintext at the call site,
sealed everywhere else.** No SDK is special-cased, so a provider Ranbval has never heard of works
on day one. Every `decrypt_key()` still auto-reports usage to the Live Monitor.

> **Tip — cache the client, not the key.** Call `decrypt_key(...).use()` right where you build the
> client or the request; don't store the plaintext in a long-lived variable (see
> [`SecretString`](#secretstring) for why).

---

### `emit_telemetry()`

Posts a usage event to the Ranbval Live Monitor.

> **You usually don't need to call this.** `decrypt_key()` already reports usage to the Live
> Monitor automatically — and does it efficiently: the **first use of a credential is sent
> immediately**, then **repeats are counted locally and flushed as one aggregated event**
> (~every 30s and at process exit) carrying an `item_count` weight. So a hot loop that decrypts
> the same key 10,000× produces a handful of events, not 10,000 POSTs. Call `emit_telemetry()`
> only to record a *richer custom event* — e.g. model name and token counts after an LLM call.

```python
from ranbval_sdk import emit_telemetry

emit_telemetry(
    vault_token_env="OPENAI_API_KEY",   # env var holding a ranbval.* token
    model_used="gpt-4o",
    prompt_tokens=512,
    completion_tokens=128,
    event_kind="llm.chat",
    background=True,                    # non-blocking daemon thread
)
```

Or pass the salt directly if you have it:

```python
emit_telemetry(
    client_salt="4ii0a022aa",
    model_used="stripe.charge",
    background=True,
)
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `vault_token_env` | `str` | Env var name holding a `ranbval.*` token — salt extracted automatically |
| `client_salt` | `str` | Use instead of `vault_token_env` if you already have the salt |
| `model_used` | `str` | Label shown in the dashboard (e.g. `"gpt-4o"`, `"stripe.charge"`) |
| `prompt_tokens` | `int` | Input tokens (0 if not an LLM call) |
| `completion_tokens` | `int` | Output tokens (0 if not an LLM call) |
| `event_kind` | `str` | Event category (e.g. `"llm.chat"`, `"custom.request"`) |
| `item_count` | `int` | Aggregation weight — how many actual uses this event represents (default `1`) |
| `roundtrip_ms` | `float` | Client-measured decrypt/round-trip latency, if you want to report it |
| `background` | `bool` | `True` = fire-and-forget in a daemon thread |
| `host_url` | `str` | Override `RANBVAL_HOST` for this call |

If no `client_salt` can be resolved the call is a silent no-op — safe to call even with plain (non-ranbval) keys.

**What each event sends.** Only a non-reversible token salt (never the plaintext secret) plus operational
metadata: SDK/Python version and platform, transport scheme, git branch, a coarse `timezone` geo hint,
decrypt latency, and a **hashed, non-reversible `device_id`** (a truncated SHA-256 of the machine ID —
the raw MAC is never sent). The `device_id` is the signal the control plane uses for **leak detection**:
the same credential appearing on multiple distinct devices/IPs raises an alert in the Live Monitor.

**Always on.** Usage reporting is the leak-detection control plane, so it has **no client-side
off switch** — a control an attacker (or a curious insider) could flip off would defeat the
purpose. Only a non-reversible salt + operational metadata are sent, never plaintext.

**Privacy control.**
- `git config user.email` (developer identity) is **not** sent by default. Set `RANBVAL_TELEMETRY_IDENTITY=1`
  to opt in to attaching it (useful for attributing usage to a person on a shared machine).

---

### `proxy_request()`

Route an outbound HTTP request through the Ranbval secure proxy. The real API key is decrypted server-side and never returned to the caller. Raises `ProxyError` on failure.

```python
from ranbval_sdk import load_ranbval, proxy_request, ProxyError
import os

load_ranbval()

try:
    result = proxy_request(
        token=os.environ["OPENAI_API_KEY"],        # ranbval.* vault token
        target_url="https://api.openai.com/v1/chat/completions",
        method="POST",
        inject_as="bearer",                         # Authorization: Bearer <secret>
        body={"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}]},
    )
    print(result["status"])   # HTTP status from the target
    print(result["body"])     # parsed JSON response
except ProxyError as e:
    print(f"Proxy failed: {e}")
```

**Inject modes:** `"bearer"` · `"basic"` · `"header:X-Api-Key"` · `"query:api_key"`

**Return value:** dict with keys `status` (int), `ok` (bool), `body` (parsed JSON or str), `headers` (dict).

`ProxyError` is raised when the proxy rejects the request (bad credentials, unknown token) or is unreachable.

---

### `get_audit_log()` / `clear_audit_log()`

The SDK records every decrypt and telemetry event in an in-process audit log. Useful for testing and compliance verification.

```python
from ranbval_sdk import load_ranbval, decrypt_key, get_audit_log, clear_audit_log

load_ranbval()
decrypt_key("OPENAI_API_KEY")

log = get_audit_log()
# [{"label": "OPENAI_API_KEY", "timestamp": 1716000000.0, "caller": "app.py:12"}]

clear_audit_log()
assert get_audit_log() == []
```

---

## Three sections: `[public]` · `[secrets]` · `[proxy]`

Not every value needs the same protection. Ranbval lets you declare **how visible** each value
may ever be, using three section headers in `.ranbval`:

| Section | Encrypted at rest? | Can your app read the plaintext? | For |
|---|---|---|---|
| **`[public]`** | No | Yes — anyone (safe to show in a UI) | `DATABASE_URL`, `CORS_ORIGINS`, `PORT` |
| **`[secrets]`** | Yes | **Yes**, at runtime via `decrypt_key().use()` | a password you must display or use in a direct DB/driver connection |
| **`[proxy]`** | Yes | **No — never.** Usable only through the Ranbval proxy | `OPENAI_API_KEY`, `STRIPE_SECRET_KEY`, any HTTP API key |

```bash
# .ranbval
RANBVAL_PROJECT_SECRET=ranbval-proj-xxx     # (or keep in .ranbval.local)

[public]                                     # plaintext — anyone may read
DATABASE_URL=postgresql://localhost/mydb
CORS_ORIGINS=https://app.example.com,https://admin.example.com

[secrets]                                    # encrypted; app CAN decrypt & view at runtime
DASHBOARD_PASSWORD=ranbval.4ii0a022aa.p1GO...ahsan

[proxy]                                      # encrypted; plaintext NEVER reaches the client
OPENAI_API_KEY=ranbval.7cc2b931ff.xYz...openai
STRIPE_SECRET_KEY=ranbval.9dd4c012aa.aBc...stripe
```

```python
from ranbval_sdk import load_ranbval, public, decrypt_key, proxy_request, proxy_token

load_ranbval()

# [public] — plain str, safe to show anywhere
db = public("DATABASE_URL")

# [secrets] — app decrypts & may view/use the plaintext (e.g. show it, or open a DB connection)
pw = decrypt_key("DASHBOARD_PASSWORD").use()

# [proxy] — plaintext NEVER enters your process; the key is injected server-side
resp = proxy_request(
    token=proxy_token("OPENAI_API_KEY"),          # only the encrypted token leaves your code
    target_url="https://api.openai.com/v1/chat/completions",
    inject_as="bearer",
    body={"model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}]},
)
```

**How the three behave**

- `public("X")` returns plaintext — and **refuses** a `[secrets]` or `[proxy]` key (or any `ranbval.*` token).
- `decrypt_key("X").use()` returns plaintext for `[secrets]` — and **refuses `[proxy]` keys** (`RanbvalConfigError`, code `proxy_only`): their plaintext must never reach the client.
- `proxy_token("X")` returns the raw **encrypted** token for `[proxy]` keys, to pass to `proxy_request()` — the real key is decrypted and injected only on Ranbval's server.
- `is_public("X")` / `is_proxy("X")` report the declared section.

**Why `[proxy]` matters:** once plaintext reaches your process, any code there (including an AI
agent you gave code execution) can copy it — no library can prevent that. `[proxy]` keys never
become plaintext on the client, so there is nothing to copy. Pair it with **not shipping
`RANBVAL_PROJECT_SECRET` to that client** and it is cryptographically impossible for that
environment to produce the plaintext at all.

**Rules & safety rails**

- **Fully backward compatible.** Sections are optional. A flat `.ranbval` (no headers) behaves
  exactly as before — `ranbval.*` values are auto-detected as secrets, everything else is plain.
- Keys **before any header** (or under an unrecognised header) stay *unlabelled* and keep the
  auto-detect behaviour.
- `load_ranbval()` **warns** when a value contradicts its section (e.g. plaintext under `[proxy]`).
- Header aliases: `[public]` = `[plain]`/`[plaintext]`/`[config]`; `[secrets]` = `[secret]`/`[vault]`/`[encrypted]`; `[proxy]` = `[proxy-only]`/`[sealed]`.

The same policy is available on the `Vault` / `env` object, so a secret can never come out of a
public path on any access surface:

```python
from ranbval_sdk import env
env.public("DATABASE_URL")     # -> plain str
env.public("OPENAI_API_KEY")   # -> raises (declared [proxy]) — use proxy_request()
```

---

## `.ranbval` File Format

`.ranbval` files follow the same `KEY=VALUE` format as `.env` files. Lines starting with `#` are comments. Blank lines are ignored. Optional `[public]` / `[secrets]` section headers group keys (see above).

```bash
# Plain value — stored and used as-is
APP_NAME=my-app
DATABASE_URL=postgresql://localhost/mydb

# Encrypted vault token — generated in the Ranbval dashboard
# Format: ranbval.<client_salt>.<aes-gcm-blob>.<label>
OPENAI_API_KEY=ranbval.4ii0a022aa.p1GOZtBx...3Kq==.ahsan
STRIPE_SECRET_KEY=ranbval.7cc2b931ff.xYZabc...Pq==.stripe
```

**Token format:** `ranbval.<client_salt>.<aes-gcm-blob>.<label>`

| Part | Description |
|------|-------------|
| `client_salt` | 10-character identifier used for session lookup and telemetry |
| `aes-gcm-blob` | IV + ciphertext, base64url-encoded |
| `label` | Human-readable tag shown in the dashboard |

---

## File Layout Example

```
my-project/
├── .ranbval                   ← shared defaults (safe to commit if no secrets)
├── .ranbval.production        ← production overrides (safe to commit)
├── .ranbval.local             ← machine secrets (gitignore this)
├── .ranbval.production.local  ← production + local overrides (gitignore this)
└── src/
    └── main.py
```

`.gitignore`:
```
.ranbval.local
.ranbval.*.local
```

`.ranbval` (committed, no secrets):
```bash
APP_NAME=my-app
RANBVAL_ENV=development
```

`.ranbval.production` (committed, encrypted tokens only):
```bash
OPENAI_API_KEY=ranbval.4ii0a022aa.p1GOZtBx...3Kq==.ahsan
```

`.ranbval.local` (never committed):
```bash
RANBVAL_PROJECT_SECRET=your_project_secret_from_dashboard
```

---

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `RANBVAL_HOST` | `https://api.ranbval.com` | Ranbval API base URL |
| `RANBVAL_ENV` | `development` | Active mode for layered config |
| `RANBVAL_PROJECT_SECRET` | *(required)* | Project secret for `safe_decrypt()` / `decrypt_key()` |
| `RANBVAL_TELEMETRY_DEBUG` | `0` | `1` = print telemetry errors to stderr |
| `RANBVAL_TELEMETRY_IDENTITY` | `0` | `1` = opt in to sending `git config user.email` with events |

> **Repo-allowlist enforcement** and **usage telemetry** are both always on and controlled by the
> Ranbval control plane — there is **no client-side flag to skip either** (a disable switch would
> let an attacker turn off the very leak detection that catches them). `decrypt_key()` reports each
> use to the Live Monitor automatically; call `emit_telemetry()` only for richer custom events.

---

## n8n — HTTP Request + Telemetry

No Python needed. Use two **HTTP Request** nodes in your n8n workflow:

**Node 1** — Your API call (OpenAI, Stripe, etc.) via HTTPS.

**Node 2** — Telemetry log to Ranbval:

```
POST https://api.ranbval.com/api/telemetry
Content-Type: application/json

{
  "client_salt": "{{ $json.client_salt }}",
  "machine_name": "n8n",
  "repo_path": "{{ $workflow.name }}",
  "model_used": "openai.chat",
  "prompt_tokens": 0,
  "completion_tokens": 0,
  "security": {
    "event_kind": "custom.request",
    "transport": "https",
    "client_platform": "n8n"
  }
}
```

Extract `client_salt` from a `ranbval.*` token in a Code node:

```javascript
const token = $json.apiKey;
const salt = token.startsWith("ranbval.") ? token.split(".")[1] : null;
return [{ json: { client_salt: salt } }];
```

---

## Security Architecture

```
Your Code
    │
    ├── load_ranbval()          Reads .ranbval* files → os.environ (no network, no decrypt)
    │
    ├── decrypt_key("ENV_VAR")
    │       │
    │       ├── 1. Repo allowlist check  →  GET /api/public/repo-policy  (mandatory, server-controlled)
    │       ├── 2. AES-256-GCM decrypt   →  SecretString (value sealed, never printable)
    │       └── 3. Auto usage report     →  POST /api/telemetry → Live Monitor (automatic)
    │
    └── secret.use()            Only access point — pass directly to SDK / headers
```

AES-256-GCM encryption with PBKDF2 key derivation (100,000 iterations). The project secret
never leaves your environment — the decryption itself happens on your machine. The repo
allowlist check is always on and governed by the Ranbval control plane (no client-side bypass).
Usage reporting is always on (it is the leak-detection control plane; there is no client-side off switch).

**Network requirement:** because the allowlist is verified server-side on every decrypt,
resolving a vault token requires connectivity to the Ranbval control plane — the same as any
cloud secret manager (HashiCorp Vault, Doppler, AWS/GCP Secrets Manager). Plain (non-`ranbval.*`)
values in your `.ranbval` files resolve fully offline.

---

## License

MIT — see [LICENSE](LICENSE).

---

## Links

- PyPI: [pypi.org/project/ranbval-sdk](https://pypi.org/project/ranbval-sdk/)
- Dashboard: [ranbval.com](https://www.ranbval.com)
- API docs: [api.ranbval.com/docs](https://api.ranbval.com/docs)
- Repository: [github.com/TariqDreamsTech/ranbval-sdk](https://github.com/TariqDreamsTech/ranbval-sdk)

