feat: developer platform — programmatic API keys, OpenAPI spec & webhooks - #12
Open
amal66 wants to merge 13 commits into
Open
feat: developer platform — programmatic API keys, OpenAPI spec & webhooks#12amal66 wants to merge 13 commits into
amal66 wants to merge 13 commits into
Conversation
amal66
force-pushed
the
feat/developer-platform
branch
6 times, most recently
from
July 6, 2026 00:26
d7b3e9e to
92f2498
Compare
…s, webhook_deliveries WHY THIS MATTERS A "developer platform" needs durable storage for three things: the long-lived credentials developers mint, the endpoints they want events pushed to, and the history of every push attempt. This commit lays that foundation in both the full schema snapshot (schema.sql, used for fresh DBs) and a dated, idempotent migration (used to evolve existing DBs) so the two never drift. WHAT IS RLS DENY-ALL? Supabase exposes tables to clients over HTTP, gated by Row Level Security. A table with RLS enabled but NO policy denies everyone — a "default-deny firewall". Every Mike table follows this posture: the backend service role is the only thing that touches data. We mirror it here: RLS enabled + an explicit deny-all policy + privileges revoked from anon/authenticated. See the existing 20260524000000_rls_deny_all.sql for the pattern we follow. HOW IT WORKS - api_keys stores only key_hash (a SHA-256 digest) + a short non-secret key_prefix. The raw secret is never persisted, so a DB leak can't be replayed. A partial index on (key_prefix) WHERE revoked_at IS NULL keeps auth lookups fast and small. - webhook_endpoints holds the destination URL, the per-endpoint HMAC secret, enabled flag, and subscribed event_types. - webhook_deliveries records event/payload/status/attempts/response per send, so delivery history is always inspectable (and replayable in future). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WHY THIS MATTERS
The security of the whole platform rests on a few small crypto helpers. Keeping
them pure (no DB, no env, no Express) means they are trivial to reason about and
to unit-test exhaustively — the riskiest code gets the most direct coverage.
WHAT IS A HASHED, OPAQUE API KEY?
An API key is a bearer secret: whoever holds the string can act as the user, so
we treat it exactly like a password. We never store the raw key — only its
SHA-256 hash. If the database leaks, the hashes can't be replayed because the
server only ever compares hashes. SHA-256 (not bcrypt/argon2) is the right
choice here: slow hashes defend LOW-entropy human passwords against brute force,
but a 40-char random base62 secret has ~238 bits of entropy and isn't
brute-forceable — so a fast hash is correct and cheaper (the Stripe/GitHub
"sk_"-key model).
WHAT IS A TIMING-SAFE COMPARE / AN HMAC SIGNATURE?
- A naive a===b on secrets returns early at the first differing byte, leaking
via response timing how many leading bytes a guess got right — enough to forge
a secret incrementally. crypto.timingSafeEqual always compares the full
buffers, removing that signal.
- An HMAC signature is hash(secret, body). Because only Mike and the receiver
share the secret, a valid signature proves authenticity (it came from Mike)
AND integrity (the body wasn't altered). It is a tamper-evident seal, not
encryption — the same idea as core/downloadTokens.ts.
HOW IT WORKS
- core/apiKeys.ts: generateApiKey() mints mike_sk_<base62> via rejection
sampling (no modulo bias), returns {token, prefix, hash}; verifyApiKeyHash()
does a length-guarded timingSafeEqual.
- core/webhookSignature.ts: signWebhookPayload()/verifyWebhookSignature()
(HMAC-SHA256, constant-time) plus generateWebhookSecret().
- webhookSignature.test.ts covers determinism, integrity/authenticity changes,
constant-time verification, and length-mismatch safety.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt routes WHY THIS MATTERS A real developer platform lets you authenticate WITHOUT an interactive browser login. This teaches the existing auth path one new trick — accept a long-lived API key — while leaving Supabase-JWT auth and MFA completely unchanged. Adding a capability without disturbing what works is the heart of safe evolution. WHAT IS THE AUTH BRANCH / WHY SESSION-ONLY MANAGEMENT? requireAuth now branches on the credential type: a token starting with mike_sk_ is a programmatic key; anything else is treated as a Supabase JWT exactly as before. The API-key branch sets res.locals.userId identically, so every downstream route works unchanged. Management routes (mint/list/revoke) are guarded by requireUserSession — they need a real logged-in user, NOT a key. That closes a privilege-escalation path: a leaked key can call the data API but can never mint MORE keys or replace itself. HOW IT WORKS - lib/apiKeys.ts is the DB layer (dependency-injectable `db`, matching userApiKeys.ts): createApiKey() returns the one-time token; authenticateApiKey() looks a key up by its non-secret prefix (cheap, indexed) then constant-time compares the full hash; revokeApiKey() soft-deletes via revoked_at; touchApiKeyLastUsed() is fire-and-forget so analytics never block a request. - middleware/auth.ts: API keys deliberately bypass the interactive MFA gate (a key is a possession factor the user minted and can revoke), and scope is enforced by mapping HTTP method -> read/write. - routes/apiKeys.ts: POST/GET/DELETE under /v1/api-keys, Zod-validated, secret returned exactly once on create. - apiKeys.test.ts: key format/hashing/timing-safe verify PLUS the DB layer accepting a valid key and rejecting a revoked, mismatched, or non-key bearer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l emit point WHY THIS MATTERS Polling an API to ask "did anything happen yet?" is wasteful and slow. Webhooks invert that: Mike PUSHES a signed HTTP request to the developer the moment an event occurs. This is the "push" half of the platform (the REST API is "pull"). WHAT IS AT-LEAST-ONCE DELIVERY WITH BACKOFF? Receivers go down. Rather than drop an event, we retry with EXPONENTIAL BACKOFF (0s, 5s, 30s, 2m, 10m) so a briefly-unavailable endpoint recovers without being hammered. Because a retry can repeat a delivery, we send a stable X-Mike-Delivery-Id the receiver can use as an IDEMPOTENCY KEY. Every attempt's status/response is persisted, so history is always auditable. WHY IN-PROCESS (NOT REDIS/BULLMQ)? Mike is self-hostable as a single service; requiring a queue would raise the bar for every operator to serve a feature many won't use on day one. We keep delivery in-process (setTimeout) and persist every delivery row up front to limit the downside (a durable queue is documented as future work in the ADR). HOW IT WORKS - lib/webhooks.ts: WEBHOOK_EVENT_TYPES catalogue; endpoint CRUD (secret shown once on create, mirroring API keys); emitWebhookEvent() fans an event out to subscribed endpoints, inserts delivery rows, and schedules attemptDelivery(), which signs the body (HMAC-SHA256 -> X-Mike-Signature), POSTs with a timeout, and reschedules on failure up to 5 attempts. - routes/webhooks.ts: /v1/webhooks endpoints + deliveries + event catalogue, session-guarded, HTTPS-required in production (URL parsed, not regex-matched, to avoid ReDoS). - projects.routes.ts: the first REAL emit point — `document.uploaded` fires (fire-and-forget) after an upload succeeds, so a webhook problem can never fail the user's upload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…UI at /docs WHY THIS MATTERS A platform you can build on needs a CONTRACT both humans and machines agree on. From one OpenAPI document you can render interactive docs, generate typed SDKs, drive contract tests, and import the whole API into Postman. It is the single source of truth for the public surface. WHAT IS OPENAPI — AND WHY HAND-AUTHORED HERE? OpenAPI is a standard, machine-readable description of every endpoint's inputs and outputs. The popular generator (@asteasolutions/zod-to-openapi) targets Zod v3, but this repo runs Zod v4, so wiring it cleanly would mean pinning an old Zod or shimming every schema. For a handful of public endpoints a curated, typed document is simpler and clearer — and keeping it in sync is part of "done". Generating SDKs FROM this spec is the planned next step (see ADR 0001). HOW IT WORKS - lib/openapi.ts exports a typed OpenAPI 3.1 object describing the API-key and webhook routes plus the existing projects/documents/chat endpoints the SDKs call, with a bearerAuth security scheme and reusable component schemas. - app.ts mounts the new routers under /v1, serves the raw spec at /openapi.json, and renders Swagger UI at /docs. Helmet's global CSP is default-src 'none', which would block Swagger UI's inline assets, so we relax the policy for the /docs subtree ONLY (all first-party, no third-party CDNs). - package.json: add swagger-ui-express; pin @types/express to v4 via a root override because the swagger types otherwise pull a conflicting v5 into the tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Python SDKs WHY THIS MATTERS The platform is only useful if the SDKs can actually use it. This wires the new credential and management endpoints through every layer of the SDK stack so a developer can authenticate with a key and manage keys/webhooks programmatically — while keeping the existing token option working unchanged. WHAT IS THE LAYERING CONTRACT? The repo's convention is: stable shapes live in packages/core, low-level endpoint calls in packages/api-client, and an ergonomic facade in packages/sdk-js. We respect it: types first, then client functions, then the class facade — one source of truth, no duplication. HOW IT WORKS - @mike/core: add ApiKey/ApiKeyCreateResponse/WebhookEndpoint/WebhookDelivery (+ scope/event-type unions) as the canonical public shapes. - @mike/api-client: add listApiKeys/createApiKey/revokeApiKey and the webhook endpoint/delivery/event functions, plus matching resources on createMikeApiClient; re-export the new types. - @mike/sdk-js: MikeClient already forwards `apiKey` as a Bearer header, so a mike_sk_ key works out of the box — clarified in the option's docs — and the facade gains `apiKeys` and `webhooks` namespaces. - Python `mike`: the client now accepts api_key=... (sent as Authorization: Bearer) alongside the existing session_token, with new api_keys and webhooks resources + pydantic models. test_developer_platform.py asserts the bearer header is sent and that the new resources round-trip (20 tests pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eries WHY THIS MATTERS Most developers meet a platform through its dashboard, not its API. This adds a "Developer" tab under Settings where a user can mint an API key, register webhook endpoints, and watch deliveries — without ever touching curl. It reuses the existing account-page design system so it feels native. WHAT IS "SHOW THE SECRET ONCE"? Because the backend stores only a hash, a freshly-created key (or webhook signing secret) is the ONLY moment the plaintext exists. The UI surfaces it in a prominent amber box with a copy button and an explicit "you won't see this again" warning, then lets the user dismiss it — teaching the security model through the interface itself. HOW IT WORKS - account/layout.tsx: add the "Developer" tab to the settings nav. - account/developer/page.tsx: a client component with two sections — API keys (create with a name, list prefix/scopes/last-used, revoke) and Webhooks (create with URL + event-type checkboxes fetched from the live catalogue, list, delete, and a "Recent deliveries" panel with status colours). All calls go through the configured @mike/api-client functions re-exported by mikeApi. - A shared SecretReveal component implements the one-time copy-with-warning box. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cation cookbook WHY THIS MATTERS A feature nobody can learn from is half-finished. This documents the platform at two altitudes: an ADR capturing WHY each decision was made (for maintainers and reviewers), and a user-facing guide showing HOW to use it (for developers). Good docs are also a security control — most webhook bugs are receivers that forget to verify the signature, so we ship a copy-paste-correct example. WHAT GOES WHERE - docs/adr/0001-developer-platform.md: an Architecture Decision Record — context, the decision, alternatives weighed (opaque hashed token vs JWT vs stored secret; hand-authored vs generated OpenAPI; in-process vs queued delivery), consequences, and a dedicated security section (why hash-not-store, why constant-time compare, the scope model, and why API keys bypass MFA). - docs/developer-platform.md: the guide — create a key, authenticate the SDKs and curl, find /openapi.json and /docs, the webhook event catalogue, the delivery format, and constant-time X-Mike-Signature verification in Node and Python. - docs/api.md & docs/sdk.md: short sections pointing at API-key auth and the OpenAPI endpoint, with links into the guide and ADR. - PR_BODY.md: the full PR description (summary, motivation, architecture diagram, design decisions, security/testing notes, a "how to try it" walkthrough, and future work) used as the pull-request body and kept in-tree as a record. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
amal66
force-pushed
the
feat/developer-platform
branch
from
July 10, 2026 02:56
92f2498 to
5e46a70
Compare
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC
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 programmatic API keys, OpenAPI 3.1 documentation, signed outbound webhooks, TypeScript/Python SDK support, and a Developer settings page.
Review hardening
Verification
Reconciled with current
main(b3166dd, olp UI sync) via a clean merge — no conflicts, no adaptations required.client.ssrf) + encryption (crypto) suites the delivery path relies on: 13 tests pass.apps/apisuite: 545 passed, 6 skipped (63 files).apps/apitsc build passes.@mike/api-client(10 tests) and@mike/sdk-js(7 tests) pass; both build, as does@mike/core.apps/webNext.js production build passes with the/account/developerroute.Webhook deliveries are persisted, but retry scheduling is still in-process; durable BullMQ delivery remains documented follow-up work.
Merge sequencing
Merge after #11. This PR's
20260711000001migration intentionally follows the billing migration (20260711000000) added there.🤖 Generated with Claude Code
https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC