Metadata-Version: 2.4
Name: keyverify
Version: 3.0.0
Summary: Shared central API-key verification client with TTL cache, scope enforcement, and fail-open/fail-closed behavior.
License: MIT
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27.0
Provides-Extra: dev
Requires-Dist: build>=1.2.2; extra == 'dev'
Requires-Dist: pre-commit>=4.0.0; extra == 'dev'
Requires-Dist: pyright>=1.1.0; extra == 'dev'
Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
Requires-Dist: pytest>=8.2.0; extra == 'dev'
Requires-Dist: ruff>=0.8.0; extra == 'dev'
Requires-Dist: twine>=5.1.1; extra == 'dev'
Description-Content-Type: text/markdown

# key-verify-client

Shared **credential → principal** resolution client, used by every service that
resolves credentials against the central admin-platform: `agent-platform`,
`border_collie`, `upload-service`, `ocr-service`, `asr-service`,
`summary-service`.

> **3.0 is a breaking release.** `KeyVerifyClient`, `VerificationResult` and
> `fail_open` are gone. The abstraction moved from *"is this API key valid?"*
> to *"which Principal does this trusted credential map to?"*.
>
> | 2.x | 3.0 |
> |---|---|
> | `KeyVerifyClient(verify_url=…)` | `CredentialResolver(resolve_url=…, deployment_id=…)` |
> | `verify(api_key)` | `resolve_principal(credential)` |
> | `result.client` | `result.principal.client_scope` |
> | `result.tenant` | `result.principal.tenant_scope` |
> | `result.key_id` | `result.principal.key_id` |
> | `result.source` / `.degraded` | `result.resolution.source` / `.degraded` |
> | `fail_open=True` | **removed — see below** |
> | — | `principal.subject_type`, `.permissions`, `.allowed_tenants`, `.deployment_scope` |

## Package naming

The three names intentionally differ by responsibility:

- Git repository: `key-verify-client`
- PyPI project / distribution: **`keyverify`**
- Python import package: **`keyverify`**

Do not change `[project].name` in `pyproject.toml` to `key-verify-client` or
`key_verify_client`. The existing PyPI project is `keyverify`, and Trusted
Publishing authorizes that project name.

Install from PyPI:

```bash
pip install keyverify
```

Import from Python:

```python
from keyverify import CredentialPrincipal, CredentialResolver, Resolution
```

## Behavior

- Calls `POST /internal/resolve-principal` over HTTP(S) with this deployment's own
  service credential.
- Enforces the **local identity boundaries**: a `tenant-client` principal must match
  this process's `client_scope`; a `service` / `operator` principal must match its
  `deployment_id`. Central already checks caller ↔ target; this is target ↔ us.
- Caches **successful resolutions only**, and caches the *whole* principal
  (`subject_type`, three scopes, `allowed_tenants`, `permissions`) — so revoking a
  grant converges within the TTL instead of never.
- `cache_ttl=0` disables caching so revocation is immediately visible.
- Plaintext credentials are never used as cache dictionary keys; only SHA-256
  digests are retained.

### There is no fail-open

2.x had a `fail_open` switch. It did **not** simply let the request through: it
returned a *fabricated* identity whose `client` equalled the caller's own
`client_scope` — so the caller's local second line of defence compared against the
very value being forged, and the resulting audit record was indistinguishable from
a genuine request. It was one environment variable away from anyone firefighting at
3am.

3.0 does not offer it. When central cannot be reached you get:

```python
Resolution(valid=False, reason="verification_unavailable",
           resolution=ResolutionMeta(source="central", degraded=True))
```

`verification_unavailable` means **our** problem, not the holder's — map it to
`503`, never `401`. Telling a client their key is invalid sends them off to
investigate a perfectly good credential (this happened; it cost three days).

### What this library does not do

It does **not** do permission authorization. It hands you
`principal.permissions` and `principal.has_permission(...)`; deciding which
permission a given route requires is the caller's job, because only the caller
knows that.

## Local development

```bash
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest -q
```

## Build and verify release artifacts

Always clean old artifacts first:

```bash
rm -rf dist build *.egg-info src/*.egg-info
python -m build
python -m twine check dist/*
python scripts/verify_dist_metadata.py dist
```

For version `3.0.0`, the artifact names must be:

```text
keyverify-3.0.0-py3-none-any.whl
keyverify-3.0.0.tar.gz
```

The metadata verification command must report:

```text
Name=keyverify Version=3.0.0
```

Only after those checks pass should CI upload `dist/*` to PyPI.
