diff --git a/.github/dependabot.yml b/.github/dependabot.yml index da6115b..866d7c8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,51 @@ -version: 2 -updates: - - package-ecosystem: pip - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 5 - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 3 \ No newline at end of file +# Dependabot configuration for automated dependency updates +version: 2 +updates: + # Python dependencies (pyproject.toml) + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "python" + commit-message: + prefix: "deps(python):" + groups: + dev-dependencies: + patterns: + - "pytest*" + - "ruff*" + - "httpx*" + - "freezegun*" + - "pytest-*" + update-types: + - "minor" + - "patch" + core-dependencies: + patterns: + - "click" + - "cryptography" + - "pyjwt" + - "rich" + - "python-dateutil" + update-types: + - "minor" + - "patch" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 3 + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "deps(actions):" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b50fba8..cdd947a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: permissions: contents: read + security-events: write jobs: test: @@ -37,6 +38,19 @@ jobs: - name: Run tests run: python -m pytest tests/ -v --tb=short + - name: Security audit (pip-audit) + run: | + pip install pip-audit + pip-audit -r <(pip freeze) --format json --output pip-audit.json || true + continue-on-error: true + + - name: Upload security audit + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: pip-audit.json + category: pip-audit + - name: Check CLI works run: | apiauth --version diff --git a/.gitignore b/.gitignore index 18afbeb..50a062d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ Thumbs.db .ruff_cache/ # Local opencode config -AGENTS.md .agents/ +# Temp files +temp_*.txt +*.tmp + 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/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."""