feat(multi-tenant): data layer, encryption, and per-request credential resolver
build-image / test (push) Successful in 19s
build-image / build (push) Successful in 19s

Foundation for multi-user operation (Phase 1):
- crypto.py: AES-256-GCM encrypt/decrypt for the per-user Intervals API key,
  key from INTERVALS_ENC_KEY (base64 32 bytes). Random nonce per message.
- db/models.py + db/session.py: SQLAlchemy 2.0 async User model (keyed on the
  Authentik sub; api_key stored encrypted; enabled = admin-approval gate) and a
  lazy async engine/sessionmaker from DATABASE_URL.
- store.py: async CRUD. New users created disabled; login never flips enabled;
  get_active_credentials returns decrypted creds only for an enabled user that
  has them.
- credentials.py: resolve_caller_credentials() maps get_access_token().subject
  to that user's stored creds, falling back to env config only when unauthenticated
  (stdio/local).
- Tests (SQLite in-memory, no infra): crypto round-trip/tamper, store gating,
  resolver paths. Suite 199 passing at 90.4% (gate holds).

Deps: sqlalchemy[asyncio], asyncpg, alembic (+ aiosqlite for tests). Pin py3.12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 19:07:10 -04:00
parent 7c36850b72
commit 31eb45c3f8
12 changed files with 836 additions and 1 deletions
+4 -1
View File
@@ -15,6 +15,9 @@ dependencies = [
"httpx>=0.25.0",
"python-dotenv>=1.0.0",
"pyjwt[crypto]>=2.8.0",
"sqlalchemy[asyncio]>=2.0.30",
"asyncpg>=0.29",
"alembic>=1.13",
]
classifiers = [
"Development Status :: 3 - Alpha",
@@ -31,7 +34,7 @@ keywords = ["intervals", "cycling", "running", "mcp", "ai"]
"Upstream" = "https://github.com/mvilanova/intervals-mcp-server"
[project.optional-dependencies]
dev = ["pytest>=8.3.5", "mypy>=1.0.0", "ruff>=0.1.0", "pytest-asyncio>=0.21", "pre-commit", "hatch", "pytest-mock==3.12.0", "pytest-cov>=5.0", "cryptography>=42.0"]
dev = ["pytest>=8.3.5", "mypy>=1.0.0", "ruff>=0.1.0", "pytest-asyncio>=0.21", "pre-commit", "hatch", "pytest-mock==3.12.0", "pytest-cov>=5.0", "cryptography>=42.0", "aiosqlite>=0.20"]
[tool.hatch.build]
include = ["server.py", "utils/*.py", "README.md", ".env.example"]