Repository navigation
feat(api): add API keys with bearer auth and a per-key rate limit - #228
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
resolveHost resolves Authorization: Bearer dforms_... to the key's member and workspace with the current role and via api_key. A dead key or an inactive member is a 401 INVALID_API_KEY, another x-quill-workspace a 403, and past 120 calls a minute a 429 RATE_LIMITED with Retry-After. last_used_at_ms moves at most once a minute. A key is never forwarded to the identity service. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
GET, POST and DELETE /v1/api-keys, session only, with the spec contract: role visibility, the key in clear once, 409 KEY_LIMIT on the 11th. An app-wide guard answers 403 KEY_NOT_ALLOWED to a key on any admin route outside the allowlist, so new routes fail closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
…blished Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
…penapi Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
INSERT ... SELECT ... WHERE count < limit is atomic on SQLite but not under Postgres READ COMMITTED: concurrent creates all counted nine and all inserted (15 parallel creates left 11 to 15 active keys). On Postgres the insert now runs in a transaction behind an advisory lock on the member in the workspace, so the next create counts again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds API keys to API v1 so integrations can call the API without a browser session. Members create and revoke keys per workspace, send them as
Authorization: Bearer dforms_..., and each key gets its own rate limit. Forms now record whether they were created from the web app or the API.Changes
api_keytable (only the hash is stored,key_hashis unique-indexed), list/create/resolve/revoke helpers, a cap of 10 active keys per member, andform.created_viawritten on create and duplicate.dforms_tokens. Session callers take the exact same path as before. The workspace header must match the key's account; a revoked key or an inactive member gets 401./v1/api-keysroutes (list, create, revoke) with role and workspace rules. The key in clear appears only in the POST response.KEY_NOT_ALLOWEDon other routes), enforced from real route metadata with a drift test.Retry-After.created_via: 'api'on forms andvia: 'api'onform_created/form_publishedevents.How it was tested
pnpm teston SQLite green (api 776, db 363, web 1248).@quill/dbsuite on Postgres 16.Review notes
An independent review found one issue, now fixed (3e78c77): on Postgres, the
INSERT ... SELECT ... WHERE count < 10was not atomic under READ COMMITTED, so parallel creates could exceed the per-member key cap. The insert now runs in a transaction behind a per-member advisory lock, and a race test covers it.Left as follow-ups (not required by the spec):
dforms_keys are not rate limited, since the per-key limiter runs after a key resolves.KEY_NOT_ALLOWEDbefore the 401 check (fails closed).🤖 Generated with Claude Code