Repository navigation
feat: API keys and GET /v1/me behind one auth hook (step 3.1) - #31
devinder-dev wants to merge 1 commit into
Conversation
|
Read all 20 files. No blockers — this is careful work, and the reasoning is written down where it The hook placement is the important choice. SHA-256 with no salt is right here, and I can say why. The secret is 32 bytes from
The duplicate Two cosmetic notes, neither worth another round:
Approving separately so the chain can move. Reviewing #32 and #33 next, in order. |
AbdikhaniAbdirashid
left a comment
There was a problem hiding this comment.
Approved. Comments above are notes and questions, not changes I need before merge.
What
Step 3.1 of Phase 3: API keys, and the first route behind them,
GET /v1/me.Why it works this way
/v1.authenticateis anonRequesthook inside the/v1plugin,so every route registered there is protected and
/health/*stays open (Fastifyencapsulation).
request.owneris set by the hook; routes never read the header themselves.relay_live_+ 43 base64url characters (32 random bytes). Postgres stores onlythe SHA-256 hash (
bytea) and the first 15 characters as a prefix for display. A lookup isone indexed query by hash. No bcrypt: a slow hash protects guessable passwords, and a
256-bit random key isn't guessable; bcrypt would also make every request slow.
Bearerin any letter case, exactly onespace, then the strict key pattern. Every failure gives the same
401 unauthorized, so theanswer never says which part was wrong. Two
Authorizationheaders are refused too (countedin the raw headers, because the runtime keeps only the first one in
request.headers).revoked_at IS NULL. It's theone query without an owner filter, because it is how we find the owner.
text/plainbodies are refused with 415 (the parser is removed), so only JSON reachesthe routes.
code,message,severity, table,constraint, routine). Postgres puts row values in
detailandwhere, and later those rowshold signing secrets.
bun src/cli/create-owner.ts --name "Acme". The name is checked first (1–100 characters, no control characters, the samelimit as the table's CHECK). The key is printed once and never stored in plain text.
Changes
auth/api-key.ts,auth/authenticate.ts,routes/v1.ts,routes/me.ts,repositories/owners.ts,services/owners.ts,schemas/owners.ts,cli/create-owner.ts;app.ts(buildApptakes the database and an optional log stream for tests,request.owner,no
text/plain),errors.ts(unauthorized(), optionaldetails),error-handler.ts.real Postgres: a fresh key gets its owner, owner B's key gets B, revoked and unknown keys
get 401, two headers over a real socket, the CLI, a closed database port gives 503 with
Retry-After, and no log line holds a full key.docs/api.md(the auth rows and the 415 row).No migration: the tables are from migration 001.
Tested
bun run typecheck,bun run test:db: 97 pass, 0 fail.create-ownerin the built image with a read-only filesystem as uid 1000, against the test database.GET /v1/metwice a second while the test Postgres restarted gave one503withRetry-After: 5, then200again without a restart of the API./health/readystayed
200the whole time. The key appeared 0 times in the logs.