lockwatch 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -0,0 +1,62 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*" # trigger on v0.1.0, v1.2.3, etc.
7
+ workflow_dispatch: # allow manual trigger for testing
8
+
9
+ jobs:
10
+ test:
11
+ name: Run tests before publish
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Set up Python 3.11
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.11"
20
+
21
+ - name: Install Hatch
22
+ run: pip install hatch
23
+
24
+ - name: Run tests
25
+ run: hatch run test
26
+
27
+ publish:
28
+ name: Build and publish to PyPI
29
+ runs-on: ubuntu-latest
30
+ needs: test
31
+ # Use PyPI's trusted publisher (OIDC) — no API token needed in secrets
32
+ permissions:
33
+ id-token: write # required for OIDC token exchange with PyPI
34
+ contents: read
35
+
36
+ environment:
37
+ name: pypi
38
+ url: https://pypi.org/project/lockwatch/
39
+
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+
43
+ - name: Set up Python 3.11
44
+ uses: actions/setup-python@v5
45
+ with:
46
+ python-version: "3.11"
47
+
48
+ - name: Install build tools
49
+ run: pip install hatch build
50
+
51
+ - name: Build wheel and sdist
52
+ run: python -m build
53
+
54
+ - name: Verify distribution files
55
+ run: |
56
+ pip install twine
57
+ twine check dist/*
58
+
59
+ - name: Publish to PyPI
60
+ # Uses OIDC trusted publisher — configure at https://pypi.org/manage/account/publishing/
61
+ # Project name: lockwatch, workflow: publish.yml, environment: pypi
62
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,309 @@
1
+ Metadata-Version: 2.4
2
+ Name: lockwatch
3
+ Version: 0.1.0
4
+ Summary: API security middleware for FastAPI and Flask: rate limiting, JWT rotation, anomaly detection, audit logging
5
+ Project-URL: Homepage, https://github.com/jordanho/lockwatch
6
+ Project-URL: Repository, https://github.com/jordanho/lockwatch
7
+ Project-URL: Issues, https://github.com/jordanho/lockwatch/issues
8
+ Author-email: Jordan Ho <jordanjho@gmail.com>
9
+ License: MIT
10
+ Keywords: fastapi,flask,jwt,middleware,rate-limiting,redis,security
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
16
+ Classifier: Topic :: Security
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: httpx>=0.27.0
19
+ Requires-Dist: python-jose[cryptography]>=3.3.0
20
+ Requires-Dist: redis[asyncio]>=5.0.0
21
+ Provides-Extra: all
22
+ Requires-Dist: alembic>=1.13.0; extra == 'all'
23
+ Requires-Dist: asyncpg>=0.29.0; extra == 'all'
24
+ Requires-Dist: fastapi>=0.111.0; extra == 'all'
25
+ Requires-Dist: flask>=3.0.0; extra == 'all'
26
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.0; extra == 'all'
27
+ Requires-Dist: starlette>=0.37.0; extra == 'all'
28
+ Requires-Dist: werkzeug>=3.0.0; extra == 'all'
29
+ Provides-Extra: audit
30
+ Requires-Dist: alembic>=1.13.0; extra == 'audit'
31
+ Requires-Dist: asyncpg>=0.29.0; extra == 'audit'
32
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.0; extra == 'audit'
33
+ Provides-Extra: fastapi
34
+ Requires-Dist: fastapi>=0.111.0; extra == 'fastapi'
35
+ Requires-Dist: starlette>=0.37.0; extra == 'fastapi'
36
+ Provides-Extra: flask
37
+ Requires-Dist: flask>=3.0.0; extra == 'flask'
38
+ Requires-Dist: werkzeug>=3.0.0; extra == 'flask'
39
+ Description-Content-Type: text/markdown
40
+
41
+ [![PyPI version](https://badge.fury.io/py/lockwatch.svg)](https://badge.fury.io/py/lockwatch)
42
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
43
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
44
+
45
+ # LockWatch
46
+
47
+ Drop-in API security middleware for FastAPI and Flask: sliding-window rate limiting (Redis sorted set), JWT rotation with refresh-token blacklist, IP burst anomaly detection with webhook alerts, and Postgres audit logging — all wired into a single `add_middleware` call.
48
+
49
+ ---
50
+
51
+ ## Architecture
52
+
53
+ ```
54
+ ┌─────────────────────────────────────────────┐
55
+ Incoming │ LockWatchMiddleware │
56
+ Request ────────►│ │
57
+ │ ┌──────────────┐ ┌───────────────────┐ │
58
+ │ │ RateLimiter │ │ AnomalyDetector │ │
59
+ │ │ │ │ │ │
60
+ │ │ Redis sorted │ │ burst window + │ │
61
+ │ │ set sliding │ │ webhook alert │ │
62
+ │ │ window │ │ (fire-and-forget) │ │
63
+ │ └──────┬───────┘ └────────┬──────────┘ │
64
+ │ │ 429 if over limit │ │
65
+ │ ▼ ▼ │
66
+ │ ┌─────────────────────────────────────┐ │
67
+ │ │ Application Handler │ │
68
+ │ └─────────────────┬───────────────────┘ │
69
+ │ │ │
70
+ │ ▼ │
71
+ │ ┌─────────────────────────────────────┐ │
72
+ │ │ AuditLogger (background task) │ │
73
+ │ │ → Postgres lockwatch_audit_log │ │
74
+ │ └─────────────────────────────────────┘ │
75
+ └─────────────────────────────────────────────┘
76
+
77
+ Response ◄┘
78
+ ```
79
+
80
+ Every request flows through the rate limiter first (hard block at 429), then the anomaly detector (non-blocking webhook alert), then your application, then an async audit log write that is never on the response critical path.
81
+
82
+ ---
83
+
84
+ ## Why I Built This
85
+
86
+ Bolting rate limiters, audit logs, and JWT rotation into every FastAPI project is tedious boilerplate — the same 50 lines of Redis plumbing, every time. LockWatch makes it a single `add_middleware` call so the security layer doesn't crowd the application logic.
87
+
88
+ ---
89
+
90
+ ## Install
91
+
92
+ ```bash
93
+ # FastAPI / Starlette
94
+ pip install lockwatch[fastapi]
95
+
96
+ # Flask / WSGI
97
+ pip install lockwatch[flask]
98
+
99
+ # Postgres audit log
100
+ pip install lockwatch[audit]
101
+
102
+ # Everything
103
+ pip install lockwatch[all]
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Quickstart — FastAPI
109
+
110
+ ```python
111
+ from fastapi import FastAPI
112
+ from lockwatch import LockWatchMiddleware
113
+
114
+ app = FastAPI()
115
+
116
+ app.add_middleware(
117
+ LockWatchMiddleware,
118
+ redis_url="redis://localhost:6379",
119
+ rate_limit_requests=100, # allow 100 requests …
120
+ rate_limit_window_seconds=60, # … per 60-second sliding window
121
+ burst_threshold=50, # anomaly alert if >50 req in 10s
122
+ burst_window_seconds=10,
123
+ webhook_urls=["https://hooks.example.com/lockwatch-alert"],
124
+ audit_database_url="postgresql+asyncpg://user:pass@host/db", # optional
125
+ )
126
+
127
+ @app.get("/items")
128
+ async def list_items():
129
+ return {"items": []}
130
+ ```
131
+
132
+ When a client exceeds the limit, LockWatch automatically returns:
133
+
134
+ ```json
135
+ HTTP 429 Too Many Requests
136
+ Retry-After: 42
137
+ X-RateLimit-Limit: 100
138
+ X-RateLimit-Remaining: 0
139
+ X-RateLimit-Reset: 1717516800
140
+
141
+ {"error": "rate_limit_exceeded", "retry_after": 42}
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Quickstart — Flask
147
+
148
+ ```python
149
+ from flask import Flask
150
+ from lockwatch.middleware import LockWatchFlaskMiddleware
151
+
152
+ app = Flask(__name__)
153
+ app.wsgi_app = LockWatchFlaskMiddleware(
154
+ app.wsgi_app,
155
+ redis_url="redis://localhost:6379",
156
+ rate_limit_requests=200,
157
+ rate_limit_window_seconds=60,
158
+ )
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Flask — Anomaly Detection + JWT Verification
164
+
165
+ ```python
166
+ from flask import Flask, g
167
+ from lockwatch import JWTRotationManager, JWTConfig
168
+ from lockwatch.middleware import flask_jwt_required, LockWatchFlaskMiddleware
169
+
170
+ app = Flask(__name__)
171
+
172
+ # Rate limiting + anomaly detection wired at the WSGI layer
173
+ app.wsgi_app = LockWatchFlaskMiddleware(
174
+ app.wsgi_app,
175
+ redis_url="redis://localhost:6379",
176
+ rate_limit_requests=200,
177
+ rate_limit_window_seconds=60,
178
+ burst_threshold=50, # enables anomaly detection; 0 = disabled
179
+ burst_window_seconds=10,
180
+ webhook_urls=["https://hooks.example.com/alert"],
181
+ )
182
+
183
+ # JWT verification per route via decorator
184
+ jwt_mgr = JWTRotationManager(redis_client, JWTConfig(rsa_private_key_pem=pem))
185
+
186
+ @app.route("/protected")
187
+ @flask_jwt_required(jwt_mgr)
188
+ def protected():
189
+ return {"user": g.jwt_claims["sub"]}
190
+ ```
191
+
192
+ ---
193
+
194
+ ## JWT Rotation
195
+
196
+ Manage short-lived access tokens and refresh-token blacklisting without a database round-trip on every request:
197
+
198
+ ```python
199
+ from cryptography.hazmat.primitives.asymmetric import rsa
200
+ from cryptography.hazmat.primitives.serialization import (
201
+ Encoding, PrivateFormat, NoEncryption,
202
+ )
203
+ from lockwatch import JWTRotationManager, JWTConfig
204
+
205
+ # Generate once; persist the PEM in your secrets store
206
+ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
207
+ pem = private_key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()).decode()
208
+
209
+ manager = JWTRotationManager(
210
+ redis_client=redis,
211
+ config=JWTConfig(rsa_private_key_pem=pem),
212
+ )
213
+
214
+ pair = await manager.issue_token_pair(subject="user:42", claims={"role": "admin"})
215
+ # pair.access_token — RS256 JWT, 15-minute TTL
216
+ # pair.refresh_token — RS256 JWT, 7-day TTL, hash stored in Redis
217
+
218
+ new_pair = await manager.rotate(pair.refresh_token) # atomic swap via WATCH/MULTI/EXEC
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Anomaly Detection
224
+
225
+ Standalone burst detector — usable without the full middleware:
226
+
227
+ ```python
228
+ from lockwatch import AnomalyDetector, AnomalyConfig
229
+
230
+ detector = AnomalyDetector(
231
+ redis_client=redis,
232
+ config=AnomalyConfig(
233
+ burst_threshold=50,
234
+ burst_window_seconds=10,
235
+ webhook_urls=["https://hooks.example.com/alert"],
236
+ ),
237
+ )
238
+
239
+ is_burst = await detector.check(ip="1.2.3.4", endpoint="/api/login", method="POST")
240
+ ```
241
+
242
+ ---
243
+
244
+ ## Audit Log
245
+
246
+ Query who did what and when:
247
+
248
+ ```python
249
+ from lockwatch import AuditLogger, AuditQuery
250
+
251
+ logger = AuditLogger(database_url="postgresql+asyncpg://user:pass@host/db")
252
+ await logger.init() # creates table if not exists (idempotent)
253
+
254
+ entries = await logger.query(AuditQuery(
255
+ user_id="user:42",
256
+ anomalies_only=True,
257
+ limit=50,
258
+ ))
259
+ ```
260
+
261
+ Run Alembic migrations against your Postgres instance:
262
+
263
+ ```bash
264
+ export LOCKWATCH_DATABASE_URL="postgresql+asyncpg://user:pass@host/db"
265
+ alembic upgrade head
266
+ ```
267
+
268
+ ---
269
+
270
+ ## Configuration Reference
271
+
272
+ | Parameter | Env var override | Default | Description |
273
+ |---|---|---|---|
274
+ | `redis_url` | `REDIS_URL` | — | Redis connection URL (required) |
275
+ | `rate_limit_requests` | — | `100` | Max requests per window |
276
+ | `rate_limit_window_seconds` | — | `60` | Sliding window size in seconds |
277
+ | `burst_threshold` | — | `50` | Request count that triggers anomaly alert |
278
+ | `burst_window_seconds` | — | `10` | Burst detection window in seconds |
279
+ | `webhook_urls` | — | `[]` | List of HTTPS endpoints for anomaly alerts |
280
+ | `audit_database_url` | `LOCKWATCH_DATABASE_URL` | `None` | Postgres URL for audit log |
281
+ | `on_redis_failure` | — | `"allow"` | `"allow"` (fail open) or `"deny"` (fail closed) |
282
+ | `jwt_secret` | `JWT_SECRET` | — | HMAC secret for JWT signing |
283
+ | `access_token_ttl` | — | `900` | Access token lifetime in seconds |
284
+ | `anomaly_window` | — | `10` | Alias for `burst_window_seconds` |
285
+
286
+ ---
287
+
288
+ ## What it Does
289
+
290
+ - **Sliding-window rate limiter** — Redis sorted set records each request timestamp; window is computed by pruning entries older than `rate_limit_window_seconds`. O(log N) per operation, true sliding semantics (no thundering-herd at window boundary), atomic pipeline.
291
+ - **JWT rotation with refresh blacklist** — issues short-lived access tokens backed by opaque refresh tokens stored in Redis. `rotate()` atomically swaps old for new and blacklists the old refresh token with a TTL — no stale token reuse.
292
+ - **IP burst anomaly detection** — separate Redis sorted set per IP in a short burst window. When the threshold is crossed, fires async webhook alerts with a 60-second dedup window (one alert per IP per minute). Non-blocking — never delays the response.
293
+ - **Postgres audit log** — every request gets one row in `lockwatch_audit_log` including timestamp, IP, user_id, endpoint, method, status code, rate_limit_hit flag, anomaly_detected flag, and response latency. Writes are background tasks (`asyncio.create_task`) — a failed write logs a warning but never affects the response.
294
+
295
+ ---
296
+
297
+ ## Running Tests
298
+
299
+ ```bash
300
+ pip install lockwatch[all]
301
+ pip install pytest pytest-asyncio fakeredis[aioredis] aiosqlite
302
+ pytest --cov=lockwatch --cov-report=term-missing
303
+ ```
304
+
305
+ ---
306
+
307
+ ## License
308
+
309
+ MIT © Jordan Ho
@@ -0,0 +1,173 @@
1
+ # LockWatch — Implementation Plan
2
+
3
+ > Status: 📋 Plan written | Last updated: 2026-06-03
4
+
5
+ ## Overview
6
+
7
+ LockWatch is an open-source API security middleware library for Python (FastAPI and Flask), published to PyPI. It provides production-grade security primitives — sliding-window rate limiting, JWT rotation with refresh token blacklisting, IP burst anomaly detection with webhook alerts, and a Postgres audit log — as drop-in middleware. The goal is a single `pip install lockwatch` that gives any FastAPI or Flask app the security layer that teams typically wire together from multiple half-baked packages.
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ ┌─────────────────────────────────────────────────────────┐
13
+ │ FastAPI / Flask App │
14
+ │ │
15
+ │ ┌─────────────────────────────────────────────────┐ │
16
+ │ │ LockWatch Middleware │ │
17
+ │ │ │ │
18
+ │ │ ┌──────────────┐ ┌───────────────────────┐ │ │
19
+ │ │ │ RateLimiter │ │ AnomalyDetector │ │ │
20
+ │ │ │ (per-key │ │ (burst detection + │ │ │
21
+ │ │ │ sliding │ │ webhook alerts) │ │ │
22
+ │ │ │ window) │ └───────────┬───────────┘ │ │
23
+ │ │ └──────┬───────┘ │ │ │
24
+ │ │ │ │ │ │
25
+ │ │ ┌──────▼───────────────────────▼───────────┐ │ │
26
+ │ │ │ Redis │ │ │
27
+ │ │ │ rate_limit:{key} → sorted set │ │ │
28
+ │ │ │ refresh_token:{jti} → hash │ │ │
29
+ │ │ │ blacklist:{jti} → 1 │ │ │
30
+ │ │ └──────────────────────────────────────────┘ │ │
31
+ │ │ │ │
32
+ │ │ ┌─────────────────────────────────────────┐ │ │
33
+ │ │ │ JWTRotationManager │ │ │
34
+ │ │ │ access (15min RS256) + refresh (7d) │ │ │
35
+ │ │ └─────────────────────────────────────────┘ │ │
36
+ │ │ │ │
37
+ │ │ ┌─────────────────────────────────────────┐ │ │
38
+ │ │ │ AuditLogger │ │ │
39
+ │ │ │ Postgres: requests, user, endpoint, │ │ │
40
+ │ │ │ status_code, rate_limit_hit │ │ │
41
+ │ │ └─────────────────────────────────────────┘ │ │
42
+ │ └─────────────────────────────────────────────────┘ │
43
+ └─────────────────────────────────────────────────────────┘
44
+ ```
45
+
46
+ ## Tech Stack
47
+
48
+ - **Python 3.11** — match modern FastAPI/production deployments; 3.11 perf improvements matter for middleware hot path
49
+ - **FastAPI** — target framework; async-first design maps cleanly to starlette middleware base
50
+ - **Flask** — second target; WSGI middleware via `werkzeug.wsgi.DispatcherMiddleware`; covers the large Flask user base
51
+ - **redis-py (async)** — `aioredis`-compatible async client; sorted set primitives are exactly what sliding-window needs (ZADD/ZREMRANGEBYSCORE/ZCARD in a pipeline = atomic window check)
52
+ - **SQLAlchemy 2 + asyncpg** — async ORM for audit log writes; asyncpg is the fastest Postgres driver available; SQLAlchemy gives portability
53
+ - **python-jose[cryptography]** — RS256 JWT signing/verification; JWK support needed for JWKS endpoint later; well-audited
54
+ - **hatchling** — PEP 517 build backend; simpler than setuptools; native pyproject.toml support; used by major OSS projects (FastAPI itself)
55
+ - **pytest + pytest-asyncio + fakeredis** — unit tests without needing a live Redis; fakeredis implements the full sorted set API
56
+
57
+ ## Implementation Checklist
58
+
59
+ ### Phase 1 — Project scaffold
60
+ - [x] Create directory structure (done by scaffolding agent)
61
+ - [ ] Write `pyproject.toml` (hatchling, optional deps: `[fastapi]`, `[flask]`, `[audit]`)
62
+ - [ ] Write `src/lockwatch/__init__.py` with public API exports
63
+ - [ ] Set up `tests/conftest.py` with fakeredis + SQLite fixtures
64
+
65
+ ### Phase 2 — Sliding-window rate limiter
66
+ - [ ] Implement `RateLimiter` class in `rate_limiter.py`
67
+ - [ ] `async def is_allowed(key: str) -> tuple[bool, RateLimitState]`
68
+ - [ ] Redis pipeline: `ZADD`, `ZREMRANGEBYSCORE` (prune old), `ZCARD`, `EXPIRE`
69
+ - [ ] Config dataclass: `requests_per_window`, `window_seconds`, `key_func` (callable)
70
+ - [ ] Built-in key functions: `key_by_ip`, `key_by_user_id`, `key_by_api_key`
71
+ - [ ] Return headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`
72
+ - [ ] Write `tests/test_rate_limiter.py` (5 test cases, fakeredis)
73
+
74
+ ### Phase 3 — JWT rotation
75
+ - [ ] Implement `JWTRotationManager` in `jwt_rotation.py`
76
+ - [ ] `issue_token_pair(subject, claims) -> TokenPair` (access 15min + refresh 7d)
77
+ - [ ] Store `SHA256(refresh_token)` in Redis with 7d TTL
78
+ - [ ] `rotate(refresh_token) -> TokenPair` — validate hash, blacklist old jti, issue new pair
79
+ - [ ] `blacklist(jti)` — add to Redis blacklist set (TTL = remaining token lifetime)
80
+ - [ ] `verify_access(token) -> Claims` — check signature + blacklist
81
+ - [ ] Config: RSA private key path or inline PEM, issuer, audience
82
+
83
+ ### Phase 4 — Anomaly detection
84
+ - [ ] Implement `AnomalyDetector` in `anomaly.py`
85
+ - [ ] Per-IP sliding window (separate Redis sorted set from rate limiter)
86
+ - [ ] Config: `burst_threshold` (requests), `burst_window_seconds`, `webhook_urls: list[str]`
87
+ - [ ] `async def check(ip: str, request_info: dict) -> bool` — returns True if anomaly
88
+ - [ ] Async webhook dispatch (httpx, non-blocking, fire-and-forget with timeout)
89
+ - [ ] Alert payload: `{ ip, timestamp, request_count, window_seconds, endpoint }`
90
+ - [ ] Deduplicate alerts: one alert per IP per 60s (Redis key with TTL)
91
+
92
+ ### Phase 5 — Postgres audit log
93
+ - [ ] Implement `AuditLogger` in `audit.py`
94
+ - [ ] SQLAlchemy model `AuditLogEntry`: `id`, `timestamp`, `user_id`, `ip`, `endpoint`, `method`, `status_code`, `rate_limit_hit`, `anomaly_detected`, `latency_ms`
95
+ - [ ] `async def log(entry: AuditLogEntry) -> None` (background task — don't block response)
96
+ - [ ] `async def query(filters: AuditQuery) -> list[AuditLogEntry]`
97
+ - [ ] Alembic migration in `src/lockwatch/migrations/`
98
+ - [ ] Index on `(timestamp, user_id)` and `(timestamp, ip)`
99
+
100
+ ### Phase 6 — Middleware classes
101
+ - [ ] `LockWatchMiddleware` (FastAPI/Starlette) in `middleware.py`
102
+ - [ ] Extends `starlette.middleware.base.BaseHTTPMiddleware`
103
+ - [ ] Call order: rate check → anomaly check → dispatch → audit log
104
+ - [ ] Inject `request.state.lockwatch` with rate limit state for downstream use
105
+ - [ ] 429 response with `Retry-After` header on rate limit
106
+ - [ ] Config object passed at init time
107
+ - [ ] `LockWatchFlaskMiddleware` (WSGI) in `middleware.py`
108
+ - [ ] Wrap WSGI app, extract IP + headers pre-dispatch
109
+ - [ ] Note: Flask middleware is sync-only; use `asyncio.run()` bridge or sync Redis client fallback
110
+
111
+ ### Phase 7 — PyPI publish
112
+ - [ ] Add `__version__` to `__init__.py` (semver, start at `0.1.0`)
113
+ - [ ] Write `.github/workflows/publish.yml` — trigger on `v*` tag push, build + twine upload
114
+ - [ ] Set up PyPI trusted publisher (OIDC, no token in secrets)
115
+ - [ ] Write `CHANGELOG.md` template
116
+
117
+ ### Phase 8 — Tests + docs
118
+ - [ ] Achieve 80%+ pytest coverage
119
+ - [ ] Integration test: spin up fakeredis + SQLite, run full request through FastAPI test client
120
+ - [ ] Write `README.md` with install instructions + 10-line quickstart
121
+ - [ ] Add Chronobot + Livestotell as example integrations
122
+
123
+ ## File Structure
124
+
125
+ ```
126
+ LockWatch/
127
+ ├── PLAN.md
128
+ ├── pyproject.toml
129
+ ├── README.md (create later)
130
+ ├── CHANGELOG.md (create later)
131
+ ├── .github/
132
+ │ └── workflows/
133
+ │ └── publish.yml
134
+ ├── src/
135
+ │ └── lockwatch/
136
+ │ ├── __init__.py (public API exports)
137
+ │ ├── rate_limiter.py (RateLimiter class)
138
+ │ ├── jwt_rotation.py (JWTRotationManager)
139
+ │ ├── anomaly.py (AnomalyDetector)
140
+ │ ├── audit.py (AuditLogger + SQLAlchemy model)
141
+ │ ├── middleware.py (FastAPI + Flask middleware)
142
+ │ ├── config.py (LockWatchConfig dataclass) [create later]
143
+ │ └── migrations/
144
+ │ ├── env.py [create later]
145
+ │ └── versions/ [create later]
146
+ └── tests/
147
+ ├── conftest.py [create later]
148
+ ├── test_rate_limiter.py
149
+ ├── test_jwt_rotation.py [create later]
150
+ ├── test_anomaly.py [create later]
151
+ └── test_middleware.py [create later]
152
+ ```
153
+
154
+ ## Resume Bullets (multi-domain)
155
+
156
+ **V3 Security/Infra:**
157
+ - Built and published LockWatch to PyPI — a Python middleware library (FastAPI/Flask) implementing sliding-window rate limiting via Redis sorted sets, RS256 JWT rotation with Redis-backed refresh token blacklisting, and IP burst anomaly detection with configurable webhook alerting; integrated into Chronobot and Livestotell
158
+
159
+ **V1 General SWE / V5 Systems:**
160
+ - Designed LockWatch as a zero-dependency security layer: atomic Redis pipeline (ZADD → ZREMRANGEBYSCORE → ZCARD) achieves O(log N) rate limit checks at <1ms overhead per request; Postgres audit log uses async background tasks to avoid adding latency to the hot path
161
+
162
+ **V7 Data / Observability angle:**
163
+ - Implemented per-request audit logging to Postgres via async SQLAlchemy with indexed queries on (timestamp, user_id); anomaly detector emits structured webhook payloads enabling downstream SIEM ingestion
164
+
165
+ ## Interview Narrative
166
+
167
+ - **Origin (30s):** At Dialpad I saw how teams bolt on rate limiting as an afterthought — usually a janky NGINX config that doesn't know about users, just IPs. I wanted a Python-native library where you say "limit this user to 100 requests per minute" in two lines, with JWT rotation and audit logging included, not as separate packages.
168
+
169
+ - **Differentiator (30s):** Most rate limit libraries use fixed windows — they have a thundering herd problem at window boundaries. LockWatch uses Redis sorted sets for true sliding windows: ZADD the current timestamp, ZREMRANGEBYSCORE to prune entries older than the window, ZCARD for the count — all in one pipeline, atomic. The JWT rotation piece is rarer: refresh tokens are stored as SHA-256 hashes in Redis (never the raw token), and rotating always blacklists the old JTI before issuing a new pair, so token theft gets caught on the next use.
170
+
171
+ - **Tradeoffs (2 min):** The main tradeoff is Redis dependency — if Redis goes down, you're choosing between failing open (allow all requests, security risk) or failing closed (block all requests, availability risk). I made it configurable: `on_redis_failure='allow'` vs `'deny'`, defaulting to `allow` with an error log, because most APIs would rather serve traffic than go dark. The audit log is async/fire-and-forget — if the Postgres write fails, the request still completes. That's intentional: audit logging should never be in the critical path. The Flask middleware is necessarily sync because WSGI is sync; I bridge to async Redis with a dedicated event loop, which adds ~0.5ms. A pure sync Redis client would be faster but I wanted one code path, not two.
172
+
173
+ - **Extension (1 min):** Natural extensions: (1) distributed rate limiting across multiple Redis nodes using Redlock for the counter key — right now two app servers could both think a key is at N-1 and both allow a request that should be blocked; (2) machine learning on the audit log to detect credential stuffing patterns that don't trigger the burst detector (slow, distributed attacks); (3) a Rust extension module for the hot-path key hashing to get sub-100μs overhead.