Metadata-Version: 2.4
Name: keyverify
Version: 1.0.0
Summary: Shared client for verifying API keys against the central admin key server.
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: fastapi>=0.115.0; extra == 'dev'
Requires-Dist: pre-commit>=4.6.0; extra == 'dev'
Requires-Dist: pyright==1.1.410; extra == 'dev'
Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
Requires-Dist: pytest>=8.2.0; extra == 'dev'
Requires-Dist: ruff==0.9.6; extra == 'dev'
Requires-Dist: twine>=6.0.0; extra == 'dev'
Description-Content-Type: text/markdown

# keyverify

Shared client for verifying API keys against the central admin key server
(`admin-platform` `/internal/verify-key`). Used by every service instance
(asr, ocr, agent, ...) so key verification logic lives in one place.

## Use

```python
from keyverify import KeyVerifier

verifier = KeyVerifier(
    verify_url="https://admin.internal/internal/verify-key",
    service_token="tok_asr_xxx",   # this instance's X-Service-Token
    scope="cust-A",                # the customer this instance serves (optional)
    cache_ttl=300,                 # cache a result for 5 min
    fail_open=False,               # if central is down and nothing cached: deny
)

result = verifier.verify(incoming_api_key)
if result.valid:
    client, tenant = result.client, result.tenant
else:
    ...  # reject with 401; result.reason explains why
```

## Behavior

- **Cache**: a verified key is cached for `cache_ttl` seconds, so the central
  server is not hit on every request, and a revocation takes effect within that
  window.
- **Outage tolerance**: if the central server is unreachable, a cached result is
  still honored. With no cached result, `fail_open` decides allow vs deny
  (default deny).
- **Scope**: when `scope` is set, the server rejects keys belonging to other
  customers (returns `valid=false, reason="scope_mismatch"`).

## Install (per service)

Each service adds this as a dependency (path, git, or internal index). E.g. in a
service's pyproject:

```toml
dependencies = ["keyverify @ file:///path/to/key-verify-client"]
```

## Development

Install the project and its development tools:

```bash
python -m pip install -e ".[dev]"
npm ci
pre-commit install
pre-commit install --hook-type commit-msg
pre-commit install --hook-type pre-push
```

Run the same checks used by GitHub Actions:

```bash
pre-commit run --all-files
pytest --cov=keyverify --cov-report=term-missing --cov-fail-under=90
python -m build
python -m twine check dist/*
```

## Automated releases

The repository uses Conventional Commits and `semantic-release`.

- Pull requests and feature branches run linting, type checking, tests, coverage,
  wheel/source-distribution builds, and package metadata checks.
- Commits pushed to `dev` create a prerelease such as `0.2.0-dev.1`. The
  corresponding Python distribution version is normalized to `0.2.0.dev1` and
  attached to the GitHub prerelease.
- Commits pushed to `main` create a stable GitHub release and publish the wheel
  and source distribution to PyPI.
- After a stable release, the workflow merges `main` back into `dev`.

### Required GitHub and PyPI settings

1. Keep the existing `package-lock.json` committed because the workflows use
   `npm ci`.
2. Create a GitHub Environment named `pypi`.
3. In the PyPI project, add a GitHub Trusted Publisher with:
   - repository owner and repository name matching this repository;
   - workflow filename `release.yml`;
   - environment name `pypi`.
4. Give GitHub Actions permission to create repository contents. If branch
   protection prevents the release commit or the `main` to `dev` synchronization,
   create a repository secret named `SEMANTIC_RELEASE_TOKEN` containing a
   fine-grained token that can push to both branches and create releases.

Only stable releases from `main` are uploaded to PyPI. The `dev` branch produces
GitHub prereleases only. Package version metadata is stored in
`src/keyverify/version.py`.
