Metadata-Version: 2.4
Name: keyban-api-client
Version: 1.0.0
Summary: Python client for Keyban API
Home-page: https://github.com/keyban-io/dap
Author: Keyban
Author-email: support@keyban.io
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.31.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: typing-extensions>=4.0.0
Requires-Dist: cryptography>=41.0.0
Dynamic: author
Dynamic: author-email
Dynamic: home-page
Dynamic: license-file
Dynamic: requires-python

# Keyban API Client

A Python client library for interacting with the Keyban DPP API. Supports passport creation with on-chain certification, field-level encryption, and UNTP granularity levels (model, batch, item).

## Features

- **Passport management** — create, update, list, delete passports at any granularity
- **On-chain certification** — automatic W3C Verifiable Credential (VC) issuance with Starknet anchoring
- **Selective certification** — `certifiedPaths` to choose which fields from `data` are included in the certificate
- **Field-level encryption** — SHA-256 hashing and AES-256-GCM encryption
- **Type-safe models** — Pydantic v2 request/response validation
- **Filtering & pagination** — query passports with flexible filters

## Quick Start

### Create a Certified Passport (Model Level)

```python
import base64, os
from uuid import UUID, uuid4
from keyban_api_client import (
    DppClient, ProductFields, CreatePassportRequest,
)

client = DppClient(
    base_url="https://api.keyban.localtest.me",
    api_key="your-api-key",
)

# Generate an encryption key for sensitive fields
encryption_key = base64.b64encode(os.urandom(32)).decode("ascii")

# Build data with encrypted fields
data = ProductFields.create_encrypted(
    confidential_paths=["serial_number"],
    enc_algorithm="aes-256-gcm",
    enc_key=encryption_key,
    name="My Certified Product",
    serial_number="SN-CONFIDENTIAL-123",
    brand="MyBrand",
)

# Create passport — certification triggers automatically for model granularity
passport = client.create_passport(CreatePassportRequest(
    application=UUID("your-application-id"),
    network="StarknetSepolia",
    granularity="model",
    modelNumber=f"my-model-{uuid4().hex[:8]}",
    data=data.model_dump(),
))

print(f"Passport ID: {passport.id}")
print(f"Encryption Key: {encryption_key}")

client.close()
```

When `data` is provided for a model-level passport, the backend automatically:
1. Builds a W3C Verifiable Credential with a Data Integrity proof (P-256 `ecdsa-jcs-2019`); `credentialSubject` is the certified content byte-for-byte (no injected identifier)
2. Uploads the signed VC to IPFS
3. Publishes a certification event on Starknet with the CID, a SHA-256 canonical content hash (enables trustless verification without knowing the passport id), and the certifier public key

## Encryption & Hashing

Protect sensitive fields before sending to the API using `ProductFields.create_encrypted()`.

| Algorithm | Reversible | Use Case |
|-----------|------------|----------|
| `sha256` | No | Integrity verification (prove value existed) |
| `aes-256-gcm` | Yes | Confidential data (decryptable with key) |

```python
from keyban_api_client import ProductFields
import base64, os

# SHA256 hashing (irreversible)
data = ProductFields.create_encrypted(
    confidential_paths=["supplier_id"],
    enc_algorithm="sha256",
    name="My Product",
    supplier_id="SECRET-123",
)

# AES-256-GCM encryption (reversible)
key = base64.b64encode(os.urandom(32)).decode("ascii")
data = ProductFields.create_encrypted(
    confidential_paths=["serial_number"],
    enc_algorithm="aes-256-gcm",
    enc_key=key,
    name="My Product",
    serial_number="SN-CONFIDENTIAL-789",
)
print(f"Save this key: {data.encryption_key}")

# Nested paths supported
data = ProductFields.create_encrypted(
    confidential_paths=["brand.supplier_id"],
    enc_algorithm="sha256",
    brand={"name": "Public", "supplier_id": "SECRET"},
)
```

## Advanced Usage

### Filtering & Pagination

```python
from keyban_api_client import FilterOperator

# Filter passports by model number
f = FilterOperator(field="modelNumber", operator="contains", value="lead")
passports = client.list_passports(filters=[f], page_size=20)

for p in passports.data:
    print(f"- {p.model_number} ({p.granularity})")

# Paginate
all_passports = []
page = 1
while True:
    response = client.list_passports(current_page=page, page_size=50)
    all_passports.extend(response.data)
    if len(response.data) < 50:
        break
    page += 1
```

### Selective Certification with `certifiedPaths`

By default, the entire `data` object is certified. Use `certifiedPaths` to certify only specific fields — updating non-certified fields won't trigger re-certification.

```python
# Only certify brand and gtin — other fields in data are stored but not signed
passport = client.create_passport(CreatePassportRequest(
    application=UUID("your-application-id"),
    network="StarknetSepolia",
    granularity="model",
    modelNumber="selective-cert",
    data={"brand": "Acme", "gtin": "3760001000001", "notes": "internal memo"},
    certifiedPaths=["brand", "gtin"],
))

# Update a non-certified field — NO re-certification
client.update_passport(passport.id, UpdatePassportRequest(
    data={"brand": "Acme", "gtin": "3760001000001", "notes": "updated memo"},
))

# Update a certified field — re-certification triggers automatically
client.update_passport(passport.id, UpdatePassportRequest(
    data={"brand": "NewBrand", "gtin": "3760001000001", "notes": "updated memo"},
))

# Change the selection itself — re-certification triggers
client.update_passport(passport.id, UpdatePassportRequest(
    certifiedPaths=["brand", "gtin", "notes"],
))
```

### Update a Passport (Re-certification)

When `data` or `certifiedPaths` changes on a model-level passport, re-certification is triggered automatically (only if the resulting certificate content actually changed).

```python
from keyban_api_client import UpdatePassportRequest

updated = client.update_passport(
    passport.id,
    UpdatePassportRequest(
        data={"name": "Updated Product", "brand": "NewBrand"},
    ),
)
```

### Error Handling

```python
from keyban_api_client import DppClient, KeybanAPIError

client = DppClient(base_url="...", api_key="...")

try:
    passport = client.create_passport(request)
except KeybanAPIError as e:
    print(e.status_code)  # 400
    print(e.detail)       # Full API response body

    if e.status_code == 401:
        print("Authentication failed - check your API key")
    elif e.status_code == 404:
        print("Resource not found")
finally:
    client.close()
```

### Context Manager

```python
with DppClient(base_url="...", api_key="...") as client:
    passports = client.list_passports()
```

## API Reference

### DppClient

Main client class. Constructor:

```python
DppClient(
    base_url: str,
    api_key: str,
    api_version: str = "v1",
    timeout: int = 30,
)
```

#### Methods

| Method | Description |
|--------|-------------|
| `list_passports(filters, current_page, page_size)` | List passports with filtering |
| `get_passport(passport_id)` | Get a passport by ID |
| `create_passport(data)` | Create passport (certifies if model + data) |
| `update_passport(passport_id, data)` | Update passport (re-certifies if certificate content changed) |
| `delete_passport(passport_id)` | Delete a passport |
| `create_passport_model(data)` | Shorthand for `create_passport` with model granularity |

### Data Models

#### DppPassport

- `id`: UUID
- `application`: Application
- `network`: str (`StarknetSepolia`, `StarknetMainnet`, etc.)
- `granularity`: str (`model`, `batch`, or `item`)
- `status`: str (always `published`)
- `model_number`: str (nullable)
- `batch_number`: str (nullable)
- `item_number`: str (nullable)
- `data`: Dict[str, Any] (all passport data)
- `certified_paths`: List[str] (nullable, model only — fields to certify; empty = all)
- `token_id`: str
- `ipfs_cid`: str (nullable — IPFS CID of the signed VC after certification)
- `allowed_claim_email`: str (nullable, item only)
- `created_at`, `updated_at`: datetime

#### CreatePassportRequest

- `application`: UUID (required)
- `network`: str (default: `StarknetSepolia`)
- `granularity`: str (required — `model`, `batch`, or `item`)
- `modelNumber`: str (identifier for model-level)
- `batchNumber`: str (identifier for batch-level)
- `itemNumber`: str (identifier for item-level)
- `data`: Dict[str, Any] (passport data)
- `certifiedPaths`: List[str] (model only — dot-notation paths to certify; omit or `[]` = all)
- `allowedClaimEmail`: str (item only — triggers minting)

#### UpdatePassportRequest

- `data`: Dict[str, Any] (passport data)
- `certifiedPaths`: List[str] (model only — dot-notation paths to certify)
- `allowedClaimEmail`: str (item only)

## API Endpoints

All operations go through a single unified endpoint:

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/v1/dpp/passports` | GET | List passports (filterable by granularity) |
| `/v1/dpp/passports/:id` | GET | Get a passport |
| `/v1/dpp/passports` | POST | Create a passport |
| `/v1/dpp/passports/:id` | PATCH | Update a passport |
| `/v1/dpp/passports/:id` | DELETE | Delete a passport |

## Migrating from 0.0.x to 1.0.0

### Breaking changes

- **Passport status** is now always `published`. The `status` field is no longer accepted in create or update requests.

### Restored

- **`certifiedPaths`** — available again on `CreatePassportRequest`, `UpdatePassportRequest`, and `DppPassport`. Was present in the old product API (0.0.6), lost during the passport migration (0.0.8), now restored on passport models. Selects which fields from `data` are included in the certificate. Omit or set to `[]` to certify everything.

### Migration steps

1. Update the client: `pip install --upgrade keyban-api-client`
2. Remove any `status` field from your create/update calls (or ignore — the API will reject it).
3. Existing calls without `certifiedPaths` continue to work as before (all `data` is certified).

## Changelog

### 1.0.0 (2026-04-14)

- Restore `certifiedPaths` on passport models (create, update, response)
- Passports are always `published` — status no longer configurable via API
- Smart re-certification: only re-certifies when the resulting certificate content actually changes
- Remove `last_certificate_hash` from response model (internal field)

## License

This client is part of the DAP (Digital Asset Platform) by Keyban project.
