-
Notifications
You must be signed in to change notification settings - Fork 0
Add API key management and webhook infrastructure with Express + PostgreSQL #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -57,6 +57,12 @@ The project uses Zod for runtime configuration validation. Configuration is load | |||||||||||||||||||||||||||||
| - `PORT`: Server port (default: 3000) | ||||||||||||||||||||||||||||||
| - `LOG_LEVEL`: Logging level (debug|info|warn|error, default: info) | ||||||||||||||||||||||||||||||
| - `TELEMETRY_*`: Optional telemetry settings | ||||||||||||||||||||||||||||||
| - `DATABASE_URL`: PostgreSQL connection string (optional, required for API keys and webhooks) | ||||||||||||||||||||||||||||||
| - `API_KEY_PREFIX`: API key prefix (optional, default: tltm) | ||||||||||||||||||||||||||||||
| - `API_KEY_PEPPER`: API key pepper for additional security (optional) | ||||||||||||||||||||||||||||||
| - `WEBHOOK_MAX_ATTEMPTS`: Maximum webhook delivery attempts (optional, default: 3) | ||||||||||||||||||||||||||||||
| - `WEBHOOK_BACKOFF_BASE_MS`: Webhook retry backoff base in milliseconds (optional, default: 1000) | ||||||||||||||||||||||||||||||
| - `WEBHOOK_TIMEOUT_MS`: Webhook delivery timeout in milliseconds (optional, default: 5000) | ||||||||||||||||||||||||||||||
| - `AI_*`: Optional AI/ML settings | ||||||||||||||||||||||||||||||
| - `VECTOR_DB_*`: Optional vector database settings | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -120,6 +126,260 @@ const span = tracer.startSpan('my-operation'); | |||||||||||||||||||||||||||||
| span.end(); | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ## API Key Management | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### Overview | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| The application includes a comprehensive API key management system with secure storage and authentication: | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - **Secure Storage**: API keys are hashed using Argon2 before storage | ||||||||||||||||||||||||||||||
| - **Bearer Token Authentication**: Standard HTTP Bearer token authentication | ||||||||||||||||||||||||||||||
| - **Scope-based Authorization**: Fine-grained access control using scopes | ||||||||||||||||||||||||||||||
| - **Audit Logging**: All key operations are logged for security auditing | ||||||||||||||||||||||||||||||
| - **Idempotency**: Support for idempotent operations using Idempotency-Key header | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### Database Setup | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Before using API keys and webhooks, you need to set up the PostgreSQL database: | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```bash | ||||||||||||||||||||||||||||||
| # Set DATABASE_URL in your .env file | ||||||||||||||||||||||||||||||
| DATABASE_URL=postgresql://user:password@localhost:5432/dbname | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Run the database migration | ||||||||||||||||||||||||||||||
| psql $DATABASE_URL < migrations/001_initial_schema.sql | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### API Endpoints | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #### Create API Key | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```bash | ||||||||||||||||||||||||||||||
| POST /api/keys | ||||||||||||||||||||||||||||||
| Content-Type: application/json | ||||||||||||||||||||||||||||||
| Idempotency-Key: <optional-unique-key> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| "name": "My API Key", | ||||||||||||||||||||||||||||||
| "owner": "user@example.com", | ||||||||||||||||||||||||||||||
| "scopes": ["read", "write"], | ||||||||||||||||||||||||||||||
| "prefix": "tltm", // optional, defaults to tltm | ||||||||||||||||||||||||||||||
| "pepper": "secret" // optional, for additional security | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Response: | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| "id": "uuid", | ||||||||||||||||||||||||||||||
| "name": "My API Key", | ||||||||||||||||||||||||||||||
| "owner": "user@example.com", | ||||||||||||||||||||||||||||||
| "scopes": ["read", "write"], | ||||||||||||||||||||||||||||||
| "prefix": "tltm", | ||||||||||||||||||||||||||||||
| "token": "tltm_...", // Only returned once! | ||||||||||||||||||||||||||||||
| "created_at": "2024-01-01T00:00:00Z" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| **Important**: The full token is only returned once during creation. Store it securely! If using an Idempotency-Key, the same response (including the token) will be returned for duplicate requests with that key within 24 hours. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #### List API Keys | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```bash | ||||||||||||||||||||||||||||||
| GET /api/keys | ||||||||||||||||||||||||||||||
| Authorization: Bearer <your-api-key> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Response: | ||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| "id": "uuid", | ||||||||||||||||||||||||||||||
| "name": "My API Key", | ||||||||||||||||||||||||||||||
| "owner": "user@example.com", | ||||||||||||||||||||||||||||||
| "scopes": ["read", "write"], | ||||||||||||||||||||||||||||||
| "prefix": "tltm", | ||||||||||||||||||||||||||||||
| "last_used_at": "2024-01-01T00:00:00Z", | ||||||||||||||||||||||||||||||
| "created_at": "2024-01-01T00:00:00Z" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #### Revoke API Key | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```bash | ||||||||||||||||||||||||||||||
| DELETE /api/keys/:id | ||||||||||||||||||||||||||||||
| Authorization: Bearer <your-api-key> | ||||||||||||||||||||||||||||||
| Idempotency-Key: <optional-unique-key> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Response: | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| "message": "API key revoked successfully" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ## Webhook Management | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### Overview | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| The webhook system enables real-time event notifications: | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - **Event-based Subscriptions**: Subscribe to specific event types | ||||||||||||||||||||||||||||||
| - **HMAC Signature Verification**: Secure webhook deliveries with SHA-256 signatures | ||||||||||||||||||||||||||||||
| - **Automatic Retries**: Configurable retry logic with exponential backoff | ||||||||||||||||||||||||||||||
| - **Delivery Tracking**: Track delivery status and response metrics | ||||||||||||||||||||||||||||||
| - **Replay Failed Deliveries**: Manually retry failed webhook deliveries | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### API Endpoints | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #### Create Webhook | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```bash | ||||||||||||||||||||||||||||||
| POST /api/webhooks | ||||||||||||||||||||||||||||||
| Authorization: Bearer <your-api-key> | ||||||||||||||||||||||||||||||
| Content-Type: application/json | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| "url": "https://example.com/webhook", | ||||||||||||||||||||||||||||||
| "events": ["user.created", "user.updated"], | ||||||||||||||||||||||||||||||
| "secret": "optional-webhook-secret", // auto-generated if not provided | ||||||||||||||||||||||||||||||
| "active": true // optional, defaults to true | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Response: | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| "id": "uuid", | ||||||||||||||||||||||||||||||
| "owner": "user@example.com", | ||||||||||||||||||||||||||||||
| "url": "https://example.com/webhook", | ||||||||||||||||||||||||||||||
| "events": ["user.created", "user.updated"], | ||||||||||||||||||||||||||||||
| "secret": "webhook-secret", | ||||||||||||||||||||||||||||||
| "active": true, | ||||||||||||||||||||||||||||||
| "created_at": "2024-01-01T00:00:00Z", | ||||||||||||||||||||||||||||||
| "updated_at": "2024-01-01T00:00:00Z" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #### List Webhooks | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```bash | ||||||||||||||||||||||||||||||
| GET /api/webhooks | ||||||||||||||||||||||||||||||
| Authorization: Bearer <your-api-key> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Response: | ||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| "id": "uuid", | ||||||||||||||||||||||||||||||
| "owner": "user@example.com", | ||||||||||||||||||||||||||||||
| "url": "https://example.com/webhook", | ||||||||||||||||||||||||||||||
| "events": ["user.created", "user.updated"], | ||||||||||||||||||||||||||||||
| "active": true, | ||||||||||||||||||||||||||||||
| "created_at": "2024-01-01T00:00:00Z", | ||||||||||||||||||||||||||||||
| "updated_at": "2024-01-01T00:00:00Z" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #### Delete Webhook | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```bash | ||||||||||||||||||||||||||||||
| DELETE /api/webhooks/:id | ||||||||||||||||||||||||||||||
| Authorization: Bearer <your-api-key> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Response: | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| "message": "Webhook deleted successfully" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #### Test Webhook | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```bash | ||||||||||||||||||||||||||||||
| POST /api/webhooks/test | ||||||||||||||||||||||||||||||
| Authorization: Bearer <your-api-key> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Response: | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| "message": "Test delivery sent to 2 webhook(s)", | ||||||||||||||||||||||||||||||
| "deliveries": [ | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| "id": "uuid", | ||||||||||||||||||||||||||||||
| "webhook_id": "uuid", | ||||||||||||||||||||||||||||||
| "status": "success", | ||||||||||||||||||||||||||||||
| "attempts": 1 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #### Replay Failed Delivery | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```bash | ||||||||||||||||||||||||||||||
| POST /api/webhooks/:id/replay?delivery_id=<delivery-uuid> | ||||||||||||||||||||||||||||||
| Authorization: Bearer <your-api-key> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Response: | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| "message": "Delivery replayed", | ||||||||||||||||||||||||||||||
| "delivery": { | ||||||||||||||||||||||||||||||
| "id": "uuid", | ||||||||||||||||||||||||||||||
| "webhook_id": "uuid", | ||||||||||||||||||||||||||||||
| "event_type": "user.created", | ||||||||||||||||||||||||||||||
| "status": "success", | ||||||||||||||||||||||||||||||
| "attempts": 1 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### Webhook Signature Verification | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| All webhook deliveries include an `X-Signature` header with HMAC SHA-256 signature: | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```javascript | ||||||||||||||||||||||||||||||
| // Verify webhook signature in your webhook handler | ||||||||||||||||||||||||||||||
| const crypto = require('crypto'); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function verifyWebhookSignature(payload, signature, secret) { | ||||||||||||||||||||||||||||||
| const hmac = crypto.createHmac('sha256', secret); | ||||||||||||||||||||||||||||||
| hmac.update(payload); | ||||||||||||||||||||||||||||||
| const expectedSignature = `sha256=${hmac.digest('hex')}`; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature)); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature)); | |
| // Safely compare signatures without throwing on length mismatch | |
| if (typeof signature !== 'string') { | |
| return false; | |
| } | |
| const receivedBuffer = Buffer.from(signature, 'utf8'); | |
| const expectedBuffer = Buffer.from(expectedSignature, 'utf8'); | |
| if (receivedBuffer.length !== expectedBuffer.length) { | |
| return false; | |
| } | |
| return crypto.timingSafeEqual(receivedBuffer, expectedBuffer); |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,80 @@ | ||||||||||||||
| -- Initial database schema for API keys and webhooks | ||||||||||||||
|
|
||||||||||||||
| -- Enable UUID extension | ||||||||||||||
| CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; | ||||||||||||||
|
|
||||||||||||||
| -- API Keys table | ||||||||||||||
| CREATE TABLE IF NOT EXISTS api_keys ( | ||||||||||||||
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), | ||||||||||||||
| name VARCHAR(255) NOT NULL, | ||||||||||||||
| owner VARCHAR(255) NOT NULL, | ||||||||||||||
| scopes TEXT[] NOT NULL DEFAULT '{}', | ||||||||||||||
| hashed_secret TEXT NOT NULL, | ||||||||||||||
| prefix VARCHAR(50) NOT NULL, | ||||||||||||||
| last_used_at TIMESTAMP WITH TIME ZONE, | ||||||||||||||
| created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||||||||||||
| revoked_at TIMESTAMP WITH TIME ZONE, | ||||||||||||||
| CONSTRAINT api_keys_name_owner_unique UNIQUE (name, owner) | ||||||||||||||
| ); | ||||||||||||||
|
|
||||||||||||||
| -- Index for faster lookups by prefix | ||||||||||||||
| CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix); | ||||||||||||||
|
|
||||||||||||||
| -- Index for faster lookups by owner | ||||||||||||||
| CREATE INDEX IF NOT EXISTS idx_api_keys_owner ON api_keys(owner); | ||||||||||||||
|
|
||||||||||||||
| -- Webhooks table | ||||||||||||||
| -- Note: Webhook secrets are stored in plaintext as they must be retrievable for HMAC signature generation. | ||||||||||||||
| -- In production environments with strict security requirements, consider implementing encryption at rest. | ||||||||||||||
| CREATE TABLE IF NOT EXISTS webhooks ( | ||||||||||||||
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), | ||||||||||||||
| owner VARCHAR(255) NOT NULL, | ||||||||||||||
| url TEXT NOT NULL, | ||||||||||||||
| events TEXT[] NOT NULL DEFAULT '{}', | ||||||||||||||
| secret VARCHAR(255) NOT NULL, | ||||||||||||||
| active BOOLEAN NOT NULL DEFAULT true, | ||||||||||||||
| created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||||||||||||
| updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP | ||||||||||||||
| ); | ||||||||||||||
|
|
||||||||||||||
| -- Index for faster lookups by owner | ||||||||||||||
| CREATE INDEX IF NOT EXISTS idx_webhooks_owner ON webhooks(owner); | ||||||||||||||
|
|
||||||||||||||
| -- Index for faster lookups by active status | ||||||||||||||
| CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(active); | ||||||||||||||
|
|
||||||||||||||
|
||||||||||||||
| -- Index to efficiently query active webhooks by event type | |
| CREATE INDEX IF NOT EXISTS idx_webhooks_active_events | |
| ON webhooks USING GIN (events) | |
| WHERE active = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation shows that 'pepper' can be provided in the request body for API key creation, which is a security vulnerability (see earlier comment about pepper being user-controllable). The documentation should be updated to remove this field from the examples once the security issue is fixed.