diff --git a/.github/dependabot.yml b/.github/dependabot.yml index da6115b..028a26d 100644 Binary files a/.github/dependabot.yml and b/.github/dependabot.yml differ diff --git a/.gitignore b/.gitignore index 18afbeb..4013739 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,5 @@ Thumbs.db .ruff_cache/ # Local opencode config -AGENTS.md .agents/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b703b8d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,30 @@ +# apiauth + +## Purpose +CLI tool for API key and JWT lifecycle management with encrypted local store — generate, store, verify, rotate, and revoke keys with an encrypted local keystore. + +## Build & Test Commands +- Install: `pip install -e .` or `pip install apiauth` +- Test: `pytest tests/` (or `python -m pytest tests/ -v --tb=short`) +- Lint: `ruff check src/ --target-version py310` +- Build: `pip wheel . --wheel-dir dist/` +- CLI check: `apiauth --version && apiauth --help` + +## Architecture +Key directories: +- `src/apiauth/` — Main package (CLI, keystore, crypto, commands) +- `tests/` — Test suite +- `.github/workflows/` — CI/CD (auto-code-review.yml, ci.yml, publish.yml) +- `dist/` — Built distributions + +## Conventions +- Language: Python 3.10+ +- Test framework: pytest +- CI: GitHub Actions (matrix: Python 3.10, 3.11, 3.12, 3.13) +- Linting: ruff (line-length 120, target py310) +- Formatting: ruff +- Package layout: src/ layout with setuptools +- Type checking: py.typed included +- Dependencies: click, cryptography, pyjwt, rich, python-dateutil +- CLI entry point: apiauth.cli:cli +- Master branch: master \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 245b54b..d05b18d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,11 +24,11 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] dependencies = [ - "click>=8.1.0", - "cryptography>=46.0.6", + "click>=8.4.1", + "cryptography>=48.0.1", "pyjwt>=2.12.0", "rich>=13.0.0", - "python-dateutil>=2.8.0", + "python-dateutil>=2.9.0.post0", ] [project.urls] diff --git a/src/apiauth/cli.py b/src/apiauth/cli.py index 759ee5c..72099a6 100644 --- a/src/apiauth/cli.py +++ b/src/apiauth/cli.py @@ -9,6 +9,14 @@ from rich.table import Table from typing import Any +# Ensure UTF-8 output on Windows consoles that default to cp1252 +if sys.platform == "win32": + try: + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + except Exception: + pass + from . import __version__ from .keygen import create_api_key_entry, create_jwt_entry, rotate_jwt, rotate_key from .keystore import Keystore @@ -176,7 +184,7 @@ def list_keys(ctx: click.Context, service: str | None, json_output: bool, show_e # ── show ────────────────────────────────────────────────────────────── -@cli.command(name="list") +@cli.command(name="show") @click.argument("key_id") @click.pass_context def show(ctx: click.Context, key_id: str) -> None: @@ -199,7 +207,7 @@ def show(ctx: click.Context, key_id: str) -> None: # ── rotate ──────────────────────────────────────────────────────────── -@cli.command(name="list") +@cli.command(name="rotate") @click.argument("key_id") @click.option("--expiry-days", "-e", type=int, default=None, help="New expiry in days") @click.pass_context @@ -230,7 +238,7 @@ def rotate(ctx: click.Context, key_id: str, expiry_days: int | None) -> None: # ── revoke ──────────────────────────────────────────────────────────── -@cli.command(name="list") +@cli.command(name="revoke") @click.argument("key_id") @click.pass_context def revoke(ctx: click.Context, key_id: str) -> None: @@ -249,7 +257,7 @@ def revoke(ctx: click.Context, key_id: str) -> None: # ── verify ──────────────────────────────────────────────────────────── -@cli.command(name="list") +@cli.command(name="verify") @click.argument("api_key") @click.option("--json-output", "-j", is_flag=True, help="Output as JSON") @click.pass_context @@ -347,7 +355,7 @@ def import_key( # ── export ──────────────────────────────────────────────────────────── -@cli.command(name="list") +@cli.command(name="export") @click.option("--format", "-f", "fmt", type=click.Choice(["env", "json", "dotenv", "github-actions"]), default="env") @click.option("--service", "-s", default=None, help="Filter by service") @click.pass_context @@ -432,7 +440,7 @@ def _export_github_actions(active: list[dict]) -> None: # ── audit ───────────────────────────────────────────────────────────── -@cli.command(name="list") +@cli.command(name="audit") @click.pass_context def audit(ctx: click.Context) -> None: """Audit keystore: find expired, expiring, and revoked keys.""" @@ -491,7 +499,7 @@ def audit(ctx: click.Context) -> None: # ── stats ───────────────────────────────────────────────────────────── -@cli.command(name="list") +@cli.command(name="stats") @click.pass_context def stats(ctx: click.Context) -> None: """Show keystore statistics."""