diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..11a0c0d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug Report +about: Create a report to help us improve +title: "[BUG] " +labels: bug +assignees: '' +--- + +## Description +A clear and concise description of what the bug is. + +## Steps to Reproduce +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +## Expected Behavior +A clear and concise description of what you expected to happen. + +## Actual Behavior +A clear and concise description of what actually happened. + +## Environment +- OS: [e.g. Windows 11, Ubuntu 22.04, macOS 14] +- Python Version: [e.g. 3.10.12] +- apiauth Version: [e.g. 0.2.0] + +## Additional Context +Add any other context about the problem here (logs, screenshots, etc.). + +## Checklist +- [ ] I have searched existing issues to avoid duplicates +- [ ] I have provided all relevant information above \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..daf18e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,32 @@ +--- +name: Feature Request +about: Suggest an idea for this project +title: "[FEATURE] " +labels: enhancement +assignees: '' +--- + +## Problem Statement +A clear and concise description of what problem this feature would solve. Is your feature request related to a problem? Please describe. + +## Proposed Solution +A clear and concise description of what you want to happen. + +## Alternative Solutions +A clear and concise description of any alternative solutions or features you've considered. + +## Use Cases +Describe specific use cases where this feature would be helpful: +1. Use case 1 +2. Use case 2 +3. Use case 3 + +## Implementation Ideas (Optional) +If you have ideas on how this in mind, share technical considerations or implementation approaches. + +## Additional Context +Add any other context or screenshots about the feature request here. + +## Checklist +- [ ] I have searched existing issues to avoid duplicates +- [ ] This feature aligns with the project's scope (API key/JWT lifecycle management) \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..34576ab --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,42 @@ +## Description + + +## Type of Change + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Dependency update +- [ ] Refactoring / Code quality +- [ ] CI/CD changes + +## Related Issues + + +## Changes Made + +1. +2. +3. + +## Testing + +- [ ] All existing tests pass (`pytest tests/ -v`) +- [ ] Linting passes (`ruff check src/ --target-version py310`) +- [ ] Manual testing performed: + - [ ] CLI commands work as expected + - [ ] Edge cases covered + +## Checklist +- [ ] My code follows the project's style guidelines (ruff) +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally +- [ ] I have updated documentation as needed (README, CHANGELOG, docstrings) +- [ ] Any dependent changes have been merged and published + +## Screenshots / Logs (if applicable) + + +## Additional Notes + \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index da6115b..3c56f47 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,12 @@ -version: 2 -updates: - - package-ecosystem: pip - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 5 - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: weekly +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 diff --git a/.github/workflows/auto-code-review.yml b/.github/workflows/auto-code-review.yml index 5022649..da486fb 100644 --- a/.github/workflows/auto-code-review.yml +++ b/.github/workflows/auto-code-review.yml @@ -1,28 +1,28 @@ -# Automated Code Review — caller workflow -# -# Drop this file into any Coding-Dev-Tools repo at -# .github/workflows/auto-code-review.yml to enable -# automated PR code review (lint, format, secret detection, -# TODO/FIXME check, large file check, and PR comment summary). -# -# The reusable workflow is defined in the org .github repo: -# Coding-Dev-Tools/.github/.github/workflows/auto-code-review.yml@main - -name: Auto Code Review - -on: - pull_request: - branches: [main, master] - types: [opened, synchronize, reopened] - push: - branches: [main, master] - workflow_dispatch: - -permissions: - contents: read - pull-requests: write - security-events: write - -jobs: - code-review: - uses: Coding-Dev-Tools/.github/.github/workflows/auto-code-review.yml@main +# Automated Code Review — caller workflow +# +# Drop this file into any Coding-Dev-Tools repo at +# .github/workflows/auto-code-review.yml to enable +# automated PR code review (lint, format, secret detection, +# TODO/FIXME check, large file check, and PR comment summary). +# +# The reusable workflow is defined in the org .github repo: +# Coding-Dev-Tools/.github/.github/workflows/auto-code-review.yml@main + +name: Auto Code Review + +on: + pull_request: + branches: [main, master] + types: [opened, synchronize, reopened] + push: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + security-events: write + +jobs: + code-review: + uses: Coding-Dev-Tools/.github/.github/workflows/auto-code-review.yml@main 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/CHANGELOG.md b/CHANGELOG.md index daecb85..987dff5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,45 +1,45 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Added - -- Directory listing badges: Open Source Alternative, LibHunt, Awesome Python -- Sibling tool cross-links in README footer - -### Changed - -- CI security hardened: `persist-credentials: false`, restricted permissions -- Documentation branding updated from DevForge to Revenue Holdings -- README rewritten with pricing table, Why hook, Revenue Holdings branding -- Tool count corrected to 11 -- Project URLs added to `pyproject.toml` -- CI badge corrected to reference ci.yml - -### Fixed - -- CI workflow: consolidated duplicate workflows, hardened security, updated actions -- CI publish workflow: downgraded actions/checkout@v6 and setup-python@v6 to v4/v5 (v6 does not exist) -- PyPI token check moved to job-level if (secrets context not available at step level) -- CI workflow simplified to avoid workflow parse failures -- UTF-8 encoding (mojibake) in file output -- Ruff lint issues: `datetime.UTC`, `X | None` syntax, `E501`, `B904`, `F821` -- Missing `ruff` dev dependency (caused CI `ruff: command not found`) -- Stray `verify.py` removed (logic lives in `keygen.py`) -- Tests updated for new `verify_api_key` return format (status instead of valid) - -## [0.1.0] — 2025-05-17 - -### Added - -- Initial beta release -- Core functionality -- CLI interface -- Test suite -- CI/CD workflows with ruff lint and pytest -- CONTRIBUTING.md +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Directory listing badges: Open Source Alternative, LibHunt, Awesome Python +- Sibling tool cross-links in README footer + +### Changed + +- CI security hardened: `persist-credentials: false`, restricted permissions +- Documentation branding updated from DevForge to Revenue Holdings +- README rewritten with pricing table, Why hook, Revenue Holdings branding +- Tool count corrected to 11 +- Project URLs added to `pyproject.toml` +- CI badge corrected to reference ci.yml + +### Fixed + +- CI workflow: consolidated duplicate workflows, hardened security, updated actions +- CI publish workflow: downgraded actions/checkout@v6 and setup-python@v6 to v4/v5 (v6 does not exist) +- PyPI token check moved to job-level if (secrets context not available at step level) +- CI workflow simplified to avoid workflow parse failures +- UTF-8 encoding (mojibake) in file output +- Ruff lint issues: `datetime.UTC`, `X | None` syntax, `E501`, `B904`, `F821` +- Missing `ruff` dev dependency (caused CI `ruff: command not found`) +- Stray `verify.py` removed (logic lives in `keygen.py`) +- Tests updated for new `verify_api_key` return format (status instead of valid) + +## [0.1.0] — 2025-05-17 + +### Added + +- Initial beta release +- Core functionality +- CLI interface +- Test suite +- CI/CD workflows with ruff lint and pytest +- CONTRIBUTING.md diff --git a/LICENSE b/LICENSE index 6052a2d..a575db2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2026 Revenue Holdings - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2026 Revenue Holdings + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index bb27722..fa8b66d 100644 --- a/README.md +++ b/README.md @@ -1,219 +1,219 @@ -# APIAuth - -[![GitHub stars](https://img.shields.io/github/stars/Coding-Dev-Tools/apiauth?style=social)](https://github.com/Coding-Dev-Tools/apiauth/stargazers) - -**CLI tool for API key and JWT lifecycle management — generate, store, verify, rotate, and revoke keys with an encrypted local keystore.** - -> ⭐ **Star this repo** if you manage API credentials — it helps other devs find APIAuth! - -[![CI](https://github.com/Coding-Dev-Tools/apiauth/actions/workflows/ci.yml/badge.svg)](https://github.com/Coding-Dev-Tools/apiauth/actions/workflows/ci.yml) -[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://python.org) -[![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/Coding-Dev-Tools/apiauth/blob/main/LICENSE) -[![Open Source Alternative](https://img.shields.io/badge/Open_Source_Alternative-%E2%87%92-blue?logo=opensourceinitiative)](https://www.opensourcealternative.to/project/apiauth) -[![LibHunt](https://img.shields.io/badge/LibHunt-%E2%87%92-blue?logo=codeigniter)](https://www.libhunt.com/r/Coding-Dev-Tools/apiauth) -[![PyPI](https://img.shields.io/pypi/v/apiauth)](https://pypi.org/project/apiauth/) - -## Installation - -```bash -pip install apiauth - -# Generate an API key -apiauth generate api-key --name "My API Key" --service "api-gateway" --expiry-days 90 - -# List all keys with expiry status -apiauth list - -# Export for CI/CD -apiauth export --format github-actions -``` - -## Features - -- **Generate** API keys and JWTs with a single command -- **Import** existing API keys into the encrypted keystore -- **Verify** API keys against stored hashes — check revocation and expiry -- **Rotate** keys and tokens safely — previous values are hashed out -- **Revoke** compromised keys instantly -- **List & search** keys by service with expiry status indicators -- **Export** as environment variables, dotenv, JSON, or GitHub Actions format -- **Audit** keystore for expired, expiring, and revoked keys -- **Encrypted local keystore** — AES-256-GCM, master key stored in `~/.apiauth/` -- **CI/CD integration** — export keys for GitHub Actions, GitLab CI, etc. - -## Commands - -### `apiauth generate` - -Generate a new API key or JWT. - -```bash -apiauth generate api-key --name "My API Key" --service "api-gateway" --expiry-days 90 -apiauth generate jwt --name "My JWT" --service "auth-service" --expiry-days 30 --claim role=admin -``` - -### `apiauth list` - -List all stored keys with expiry status. - -```bash -apiauth list -apiauth list --service "api-gateway" -apiauth list --json-output -``` - -### `apiauth show` - -Show details for a specific key. - -```bash -apiauth show -``` - -### `apiauth verify` - -Verify an API key against stored hashes. - -```bash -apiauth verify ak_xYz123abc... -``` - -### `apiauth import` - -Import an existing key into the keystore. - -```bash -apiauth import ak_existing_key_value --name "Legacy Key" --service "api" -``` - -### `apiauth rotate` - -Rotate a key and hash out the previous value. - -```bash -apiauth rotate -``` - -### `apiauth revoke` - -Revoke a key instantly. - -```bash -apiauth revoke -``` - -### `apiauth export` - -Export keys for external consumption. - -```bash -apiauth export --format env --service "api-gateway" -apiauth export --format dotenv -apiauth export --format github-actions -apiauth export --format json -``` - -### `apiauth audit` - -Audit keystore health. - -```bash -apiauth audit -``` - -### `apiauth stats` - -View keystore statistics. - -```bash -apiauth stats -``` - -## Export Formats - -| Format | Use Case | -|--------|----------| -| `env` | Shell source scripts (`export KEY=value`) | -| `dotenv` | `.env` files (no `export` prefix) | -| `github-actions` | `$GITHUB_ENV` and workflow YAML | -| `json` | Programmatic consumption | - -## Security - -- Master key never leaves `~/.apiauth/master.key` -- Key store is encrypted with AES-256-GCM -- Plaintext keys are only displayed once on creation -- Rotated keys have their previous values hashed -- Imported keys are stored as SHA-256 hashes only -- `verify` command checks against stored hashes — no plaintext stored - -## Pricing - -APIAuth is one of eleven tools in the Revenue Holdings suite. One license covers all CLI tools. - -| Plan | Price | Best For | -|------|-------|----------| -| **Free** | $0 | Individual devs, OSS — CLI only, 5 keys | -| **APIAuth Individual** | **$12/mo** ($10 billed annually) | Professional devs — unlimited keys, all export formats | -| **Suite (all 11 tools)** | **$49/mo** ($39 billed annually) | Full Revenue Holdings toolkit — 40% savings | -| **Team** | **$79/mo** ($63 billed annually) | Up to 5 devs — shared keystore, team dashboard, alerts | -| **Enterprise** | Custom | SSO, RBAC, compliance reports, dedicated support | - -🔹 **No lock-in**: CLI works fully offline on the free tier — no telemetry, no phone-home. -🔹 **Annual billing**: Save 20%. - -### Per-Tier Features - -| Feature | Free | Individual | Suite | Team | Enterprise | -|---------|:----:|:----------:|:-----:|:----:|:----------:| -| CLI: generate, verify, export | ✓ | ✓ | ✓ | ✓ | ✓ | -| Unlimited keys | 5 keys | ✓ | ✓ | ✓ | ✓ | -| All export formats | `env` only | ✓ | ✓ | ✓ | ✓ | -| JWT with custom claims | — | ✓ | ✓ | ✓ | ✓ | -| Audit & stats | — | ✓ | ✓ | ✓ | ✓ | -| Shared team keystore | — | — | — | ✓ | ✓ | -| Dashboard & analytics | — | — | — | ✓ | ✓ | -| Compliance reports | — | — | — | — | ✓ | -| RBAC / SSO / SAML / OIDC | — | — | — | — | ✓ | -| Priority support | Community | 24h | 24h | 8h | Dedicated | - ---- - -

- Part of Revenue Holdings — CLI tools built by autonomous AI. -

- -## Storage - -Keys and configuration are stored in `~/.apiauth/`: -- `~/.apiauth/master.key` — AES-256-GCM master key (never shared) -- `~/.apiauth/keystore.enc` — encrypted key-value store -- `~/.apiauth/config.yaml` — user configuration - -## CI/CD Integration - -```bash -# In your deployment pipeline -export $(apiauth export --format env --service production) - -# Audit before release -apiauth audit --exit-on-expired -``` - -## Roadmap - -- [ ] Vault-backed remote keystore (HashiCorp Vault, AWS Secrets Manager) -- [ ] Auto-expiry notifications via CLI or webhook -- [ ] GPG key support -- [ ] MCP server for AI-assisted key management -- [ ] Web UI for team keystore management -- [ ] Terraform provider for secret provisioning - -## License - -MIT — see [LICENSE](LICENSE) - ---- - -Part of [Revenue Holdings](https://coding-dev-tools.github.io/revenueholdings.dev/) — a suite of 11 developer CLI tools built by autonomous AI agents. Also check out [API Contract Guardian](https://github.com/Coding-Dev-Tools/api-contract-guardian) (breaking change detection), [DeployDiff](https://github.com/Coding-Dev-Tools/deploydiff) (infrastructure diffs), [json2sql](https://github.com/Coding-Dev-Tools/json2sql) (JSON → SQL), [ConfigDrift](https://github.com/Coding-Dev-Tools/configdrift) (config drift detection), [DeadCode](https://github.com/Coding-Dev-Tools/deadcode) (dead code cleanup), [APIGhost](https://github.com/Coding-Dev-Tools/apighost) (mock API server), [Envault](https://github.com/Coding-Dev-Tools/envault) (env sync), [SchemaForge](https://github.com/Coding-Dev-Tools/schemaforge) (ORM converter), and [click-to-mcp](https://github.com/Coding-Dev-Tools/click-to-mcp) (CLI → MCP server). - +# APIAuth + +[![GitHub stars](https://img.shields.io/github/stars/Coding-Dev-Tools/apiauth?style=social)](https://github.com/Coding-Dev-Tools/apiauth/stargazers) + +**CLI tool for API key and JWT lifecycle management — generate, store, verify, rotate, and revoke keys with an encrypted local keystore.** + +> ⭐ **Star this repo** if you manage API credentials — it helps other devs find APIAuth! + +[![CI](https://github.com/Coding-Dev-Tools/apiauth/actions/workflows/ci.yml/badge.svg)](https://github.com/Coding-Dev-Tools/apiauth/actions/workflows/ci.yml) +[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://python.org) +[![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/Coding-Dev-Tools/apiauth/blob/main/LICENSE) +[![Open Source Alternative](https://img.shields.io/badge/Open_Source_Alternative-%E2%87%92-blue?logo=opensourceinitiative)](https://www.opensourcealternative.to/project/apiauth) +[![LibHunt](https://img.shields.io/badge/LibHunt-%E2%87%92-blue?logo=codeigniter)](https://www.libhunt.com/r/Coding-Dev-Tools/apiauth) +[![PyPI](https://img.shields.io/pypi/v/apiauth)](https://pypi.org/project/apiauth/) + +## Installation + +```bash +pip install apiauth + +# Generate an API key +apiauth generate api-key --name "My API Key" --service "api-gateway" --expiry-days 90 + +# List all keys with expiry status +apiauth list + +# Export for CI/CD +apiauth export --format github-actions +``` + +## Features + +- **Generate** API keys and JWTs with a single command +- **Import** existing API keys into the encrypted keystore +- **Verify** API keys against stored hashes — check revocation and expiry +- **Rotate** keys and tokens safely — previous values are hashed out +- **Revoke** compromised keys instantly +- **List & search** keys by service with expiry status indicators +- **Export** as environment variables, dotenv, JSON, or GitHub Actions format +- **Audit** keystore for expired, expiring, and revoked keys +- **Encrypted local keystore** — AES-256-GCM, master key stored in `~/.apiauth/` +- **CI/CD integration** — export keys for GitHub Actions, GitLab CI, etc. + +## Commands + +### `apiauth generate` + +Generate a new API key or JWT. + +```bash +apiauth generate api-key --name "My API Key" --service "api-gateway" --expiry-days 90 +apiauth generate jwt --name "My JWT" --service "auth-service" --expiry-days 30 --claim role=admin +``` + +### `apiauth list` + +List all stored keys with expiry status. + +```bash +apiauth list +apiauth list --service "api-gateway" +apiauth list --json-output +``` + +### `apiauth show` + +Show details for a specific key. + +```bash +apiauth show +``` + +### `apiauth verify` + +Verify an API key against stored hashes. + +```bash +apiauth verify ak_xYz123abc... +``` + +### `apiauth import` + +Import an existing key into the keystore. + +```bash +apiauth import ak_existing_key_value --name "Legacy Key" --service "api" +``` + +### `apiauth rotate` + +Rotate a key and hash out the previous value. + +```bash +apiauth rotate +``` + +### `apiauth revoke` + +Revoke a key instantly. + +```bash +apiauth revoke +``` + +### `apiauth export` + +Export keys for external consumption. + +```bash +apiauth export --format env --service "api-gateway" +apiauth export --format dotenv +apiauth export --format github-actions +apiauth export --format json +``` + +### `apiauth audit` + +Audit keystore health. + +```bash +apiauth audit +``` + +### `apiauth stats` + +View keystore statistics. + +```bash +apiauth stats +``` + +## Export Formats + +| Format | Use Case | +|--------|----------| +| `env` | Shell source scripts (`export KEY=value`) | +| `dotenv` | `.env` files (no `export` prefix) | +| `github-actions` | `$GITHUB_ENV` and workflow YAML | +| `json` | Programmatic consumption | + +## Security + +- Master key never leaves `~/.apiauth/master.key` +- Key store is encrypted with AES-256-GCM +- Plaintext keys are only displayed once on creation +- Rotated keys have their previous values hashed +- Imported keys are stored as SHA-256 hashes only +- `verify` command checks against stored hashes — no plaintext stored + +## Pricing + +APIAuth is one of eleven tools in the Revenue Holdings suite. One license covers all CLI tools. + +| Plan | Price | Best For | +|------|-------|----------| +| **Free** | $0 | Individual devs, OSS — CLI only, 5 keys | +| **APIAuth Individual** | **$12/mo** ($10 billed annually) | Professional devs — unlimited keys, all export formats | +| **Suite (all 11 tools)** | **$49/mo** ($39 billed annually) | Full Revenue Holdings toolkit — 40% savings | +| **Team** | **$79/mo** ($63 billed annually) | Up to 5 devs — shared keystore, team dashboard, alerts | +| **Enterprise** | Custom | SSO, RBAC, compliance reports, dedicated support | + +🔹 **No lock-in**: CLI works fully offline on the free tier — no telemetry, no phone-home. +🔹 **Annual billing**: Save 20%. + +### Per-Tier Features + +| Feature | Free | Individual | Suite | Team | Enterprise | +|---------|:----:|:----------:|:-----:|:----:|:----------:| +| CLI: generate, verify, export | ✓ | ✓ | ✓ | ✓ | ✓ | +| Unlimited keys | 5 keys | ✓ | ✓ | ✓ | ✓ | +| All export formats | `env` only | ✓ | ✓ | ✓ | ✓ | +| JWT with custom claims | — | ✓ | ✓ | ✓ | ✓ | +| Audit & stats | — | ✓ | ✓ | ✓ | ✓ | +| Shared team keystore | — | — | — | ✓ | ✓ | +| Dashboard & analytics | — | — | — | ✓ | ✓ | +| Compliance reports | — | — | — | — | ✓ | +| RBAC / SSO / SAML / OIDC | — | — | — | — | ✓ | +| Priority support | Community | 24h | 24h | 8h | Dedicated | + +--- + +

+ Part of Revenue Holdings — CLI tools built by autonomous AI. +

+ +## Storage + +Keys and configuration are stored in `~/.apiauth/`: +- `~/.apiauth/master.key` — AES-256-GCM master key (never shared) +- `~/.apiauth/keystore.enc` — encrypted key-value store +- `~/.apiauth/config.yaml` — user configuration + +## CI/CD Integration + +```bash +# In your deployment pipeline +export $(apiauth export --format env --service production) + +# Audit before release +apiauth audit --exit-on-expired +``` + +## Roadmap + +- [ ] Vault-backed remote keystore (HashiCorp Vault, AWS Secrets Manager) +- [ ] Auto-expiry notifications via CLI or webhook +- [ ] GPG key support +- [ ] MCP server for AI-assisted key management +- [ ] Web UI for team keystore management +- [ ] Terraform provider for secret provisioning + +## License + +MIT — see [LICENSE](LICENSE) + +--- + +Part of [Revenue Holdings](https://coding-dev-tools.github.io/revenueholdings.dev/) — a suite of 11 developer CLI tools built by autonomous AI agents. Also check out [API Contract Guardian](https://github.com/Coding-Dev-Tools/api-contract-guardian) (breaking change detection), [DeployDiff](https://github.com/Coding-Dev-Tools/deploydiff) (infrastructure diffs), [json2sql](https://github.com/Coding-Dev-Tools/json2sql) (JSON → SQL), [ConfigDrift](https://github.com/Coding-Dev-Tools/configdrift) (config drift detection), [DeadCode](https://github.com/Coding-Dev-Tools/deadcode) (dead code cleanup), [APIGhost](https://github.com/Coding-Dev-Tools/apighost) (mock API server), [Envault](https://github.com/Coding-Dev-Tools/envault) (env sync), [SchemaForge](https://github.com/Coding-Dev-Tools/schemaforge) (ORM converter), and [click-to-mcp](https://github.com/Coding-Dev-Tools/click-to-mcp) (CLI → MCP server). + diff --git a/SECURITY.md b/SECURITY.md index 3ead69b..7390bb8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,23 +1,23 @@ -# Security Policy - -## Supported Versions - -We release patches for security vulnerabilities in the latest version. - -## Reporting a Vulnerability - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead, please report them via GitHub's private vulnerability reporting feature: - -1. Go to the repository's Security tab -2. Click "Report a vulnerability" -3. Fill in the details - -We aim to respond within 48 hours and will keep you updated on the fix. - -## Security Best Practices - -- Keep your dependencies up to date -- Use `pip audit` to check for known vulnerabilities +# Security Policy + +## Supported Versions + +We release patches for security vulnerabilities in the latest version. + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them via GitHub's private vulnerability reporting feature: + +1. Go to the repository's Security tab +2. Click "Report a vulnerability" +3. Fill in the details + +We aim to respond within 48 hours and will keep you updated on the fix. + +## Security Best Practices + +- Keep your dependencies up to date +- Use `pip audit` to check for known vulnerabilities - Report any security concerns promptly \ No newline at end of file diff --git a/src/apiauth/__init__.py b/src/apiauth/__init__.py index bd33af4..88f5fda 100644 --- a/src/apiauth/__init__.py +++ b/src/apiauth/__init__.py @@ -1,3 +1,3 @@ -"""APIAuth CLI — API key and JWT lifecycle management.""" - -__version__ = "0.2.0" +"""APIAuth CLI — API key and JWT lifecycle management.""" + +__version__ = "0.2.0" 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.""" diff --git a/src/apiauth/keystore.py b/src/apiauth/keystore.py index 778a332..d9de030 100644 --- a/src/apiauth/keystore.py +++ b/src/apiauth/keystore.py @@ -1,108 +1,108 @@ -"""Encrypted local keystore for API keys using AES-256-GCM.""" - -from __future__ import annotations - -import json -import os -from cryptography.hazmat.primitives.ciphers.aead import AESGCM -from pathlib import Path -from typing import Any - -_DEFAULT_KEY_DIR = Path.home() / ".apiauth" -_KEY_FILE = "master.key" -_STORE_FILE = "keys.json" -_NONCE_BYTES = 12 - - -def _get_or_create_master_key(key_dir: Path) -> bytes: - """Load existing master key or generate a new one.""" - key_path = key_dir / _KEY_FILE - if key_path.exists(): - return key_path.read_bytes() - - key_dir.mkdir(parents=True, exist_ok=True) - key = AESGCM.generate_key(bit_length=256) - key_path.write_bytes(key) - os.chmod(str(key_path), 0o600) # Restrict permissions - return key - - -class Keystore: - """AES-256-GCM encrypted local keystore for API keys and JWTs.""" - - def __init__(self, key_dir: str | Path | None = None) -> None: - self.key_dir = Path(key_dir or _DEFAULT_KEY_DIR) - self._master_key = _get_or_create_master_key(self.key_dir) - self._aesgcm = AESGCM(self._master_key) - self._store_path = self.key_dir / _STORE_FILE - self._entries: dict[str, dict[str, Any]] = {} - self._load() - - def _load(self) -> None: - if not self._store_path.exists(): - self._entries = {} - return - - raw = self._store_path.read_bytes() - if not raw: - self._entries = {} - return - - try: - nonce = raw[:12] - ciphertext = raw[12:] - plaintext = self._aesgcm.decrypt(nonce, ciphertext, None) - self._entries = json.loads(plaintext.decode("utf-8")) - except Exception: - self._entries = {} - - def _save(self) -> None: - plaintext = json.dumps(self._entries, indent=2, default=str).encode("utf-8") - nonce = os.urandom(12) - ciphertext = self._aesgcm.encrypt(nonce, plaintext, None) - self._store_path.write_bytes(nonce + ciphertext) - os.chmod(str(self._store_path), 0o600) - - def get_all(self) -> dict[str, dict[str, Any]]: - """Return all stored entries.""" - return dict(self._entries) - - def get(self, key_id: str) -> dict[str, Any] | None: - """Get a single entry by its key ID.""" - return self._entries.get(key_id) - - def put(self, key_id: str, entry: dict[str, Any]) -> None: - """Store or update an entry.""" - self._entries[key_id] = entry - self._save() - - def delete(self, key_id: str) -> bool: - """Delete an entry. Returns True if it existed.""" - existed = key_id in self._entries - if existed: - del self._entries[key_id] - self._save() - return existed - - def list_keys(self, service: str | None = None) -> list[dict[str, Any]]: - """List all entries, optionally filtered by service.""" - results = [] - for kid, entry in self._entries.items(): - if service and entry.get("service", "") != service: - continue - results.append({"id": kid, **entry}) - return sorted(results, key=lambda e: e.get("created_at", "")) - - def get_stats(self) -> dict[str, Any]: - """Get storage statistics.""" - total = len(self._entries) - by_service: dict[str, int] = {} - for entry in self._entries.values(): - s = entry.get("service", "unknown") - by_service[s] = by_service.get(s, 0) + 1 - return { - "total_keys": total, - "by_service": by_service, - "store_path": str(self._store_path), - "key_path": str(self.key_dir / _KEY_FILE), - } +"""Encrypted local keystore for API keys using AES-256-GCM.""" + +from __future__ import annotations + +import json +import os +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from pathlib import Path +from typing import Any + +_DEFAULT_KEY_DIR = Path.home() / ".apiauth" +_KEY_FILE = "master.key" +_STORE_FILE = "keys.json" +_NONCE_BYTES = 12 + + +def _get_or_create_master_key(key_dir: Path) -> bytes: + """Load existing master key or generate a new one.""" + key_path = key_dir / _KEY_FILE + if key_path.exists(): + return key_path.read_bytes() + + key_dir.mkdir(parents=True, exist_ok=True) + key = AESGCM.generate_key(bit_length=256) + key_path.write_bytes(key) + os.chmod(str(key_path), 0o600) # Restrict permissions + return key + + +class Keystore: + """AES-256-GCM encrypted local keystore for API keys and JWTs.""" + + def __init__(self, key_dir: str | Path | None = None) -> None: + self.key_dir = Path(key_dir or _DEFAULT_KEY_DIR) + self._master_key = _get_or_create_master_key(self.key_dir) + self._aesgcm = AESGCM(self._master_key) + self._store_path = self.key_dir / _STORE_FILE + self._entries: dict[str, dict[str, Any]] = {} + self._load() + + def _load(self) -> None: + if not self._store_path.exists(): + self._entries = {} + return + + raw = self._store_path.read_bytes() + if not raw: + self._entries = {} + return + + try: + nonce = raw[:12] + ciphertext = raw[12:] + plaintext = self._aesgcm.decrypt(nonce, ciphertext, None) + self._entries = json.loads(plaintext.decode("utf-8")) + except Exception: + self._entries = {} + + def _save(self) -> None: + plaintext = json.dumps(self._entries, indent=2, default=str).encode("utf-8") + nonce = os.urandom(12) + ciphertext = self._aesgcm.encrypt(nonce, plaintext, None) + self._store_path.write_bytes(nonce + ciphertext) + os.chmod(str(self._store_path), 0o600) + + def get_all(self) -> dict[str, dict[str, Any]]: + """Return all stored entries.""" + return dict(self._entries) + + def get(self, key_id: str) -> dict[str, Any] | None: + """Get a single entry by its key ID.""" + return self._entries.get(key_id) + + def put(self, key_id: str, entry: dict[str, Any]) -> None: + """Store or update an entry.""" + self._entries[key_id] = entry + self._save() + + def delete(self, key_id: str) -> bool: + """Delete an entry. Returns True if it existed.""" + existed = key_id in self._entries + if existed: + del self._entries[key_id] + self._save() + return existed + + def list_keys(self, service: str | None = None) -> list[dict[str, Any]]: + """List all entries, optionally filtered by service.""" + results = [] + for kid, entry in self._entries.items(): + if service and entry.get("service", "") != service: + continue + results.append({"id": kid, **entry}) + return sorted(results, key=lambda e: e.get("created_at", "")) + + def get_stats(self) -> dict[str, Any]: + """Get storage statistics.""" + total = len(self._entries) + by_service: dict[str, int] = {} + for entry in self._entries.values(): + s = entry.get("service", "unknown") + by_service[s] = by_service.get(s, 0) + 1 + return { + "total_keys": total, + "by_service": by_service, + "store_path": str(self._store_path), + "key_path": str(self.key_dir / _KEY_FILE), + }