diff --git a/create-db-worker/src/index.ts b/create-db-worker/src/index.ts index f202f7c..72c5d8a 100644 --- a/create-db-worker/src/index.ts +++ b/create-db-worker/src/index.ts @@ -6,6 +6,7 @@ interface Env { DELETE_DB_WORKFLOW: Workflow; DELETE_STALE_WORKFLOW: Workflow; CREATE_DB_RATE_LIMITER: RateLimit; + PROGRAMMATIC_RATE_LIMITER: RateLimit; CREATE_DB_DATASET: AnalyticsEngineDataset; POSTHOG_API_KEY?: string; POSTHOG_API_HOST?: string; @@ -130,6 +131,7 @@ export default { name?: string; analytics?: { eventName?: string; properties?: Record }; userAgent?: string; + source?: 'programmatic' | 'cli'; }; let body: CreateDbBody = {}; @@ -140,7 +142,51 @@ export default { return new Response('Invalid JSON body', { status: 400 }); } - const { region, name, analytics: analyticsData, userAgent } = body; + const { region, name, analytics: analyticsData, userAgent, source } = body; + + // Apply stricter rate limiting for programmatic requests + if (source === 'programmatic') { + const programmaticKey = `programmatic:${clientIP}`; + try { + const res = await env.PROGRAMMATIC_RATE_LIMITER.limit({ + key: programmaticKey, + }); + + if (!res.success) { + return new Response( + JSON.stringify({ + error: 'RATE_LIMIT_EXCEEDED', + message: 'Rate limit exceeded for programmatic database creation. You can create up to 1 database per minute. Please try again later.', + rateLimitInfo: { + retryAfterMs: 60000, // Approximate - Cloudflare doesn't expose exact timing + currentCount: 1, + maxRequests: 1, + }, + }), + { + status: 429, + headers: { + 'Content-Type': 'application/json', + 'Retry-After': '60', + }, + }, + ); + } + } catch (e) { + console.error('Programmatic rate limiter error:', e); + // Fail closed for programmatic requests + return new Response( + JSON.stringify({ + error: 'rate_limiter_error', + message: 'Rate limiter temporarily unavailable. Please try again later.', + }), + { + status: 503, + headers: { 'Content-Type': 'application/json' }, + }, + ); + } + } if (!region || !name) { return new Response('Missing region or name in request body', { status: 400 }); } diff --git a/create-db-worker/wrangler.jsonc b/create-db-worker/wrangler.jsonc index 07913a8..dd03219 100644 --- a/create-db-worker/wrangler.jsonc +++ b/create-db-worker/wrangler.jsonc @@ -40,6 +40,15 @@ "period": 60, }, }, + { + "name": "PROGRAMMATIC_RATE_LIMITER", + "type": "ratelimit", + "namespace_id": "1006", + "simple": { + "limit": 5, + "period": 60, + }, + }, ], }, } diff --git a/create-db/src/database.ts b/create-db/src/database.ts index 8c21252..904b1ee 100644 --- a/create-db/src/database.ts +++ b/create-db/src/database.ts @@ -14,7 +14,8 @@ export async function createDatabaseCore( createDbWorkerUrl: string, claimDbWorkerUrl: string, userAgent?: string, - cliRunId?: string + cliRunId?: string, + source?: "programmatic" | "cli" ): Promise { const name = new Date().toISOString(); const runId = cliRunId ?? randomUUID(); @@ -27,6 +28,7 @@ export async function createDatabaseCore( name, utm_source: getCommandName(), userAgent, + source: source || "cli", }), }); @@ -37,6 +39,23 @@ export async function createDatabaseCore( runId, createDbWorkerUrl ); + + // Try to parse the rate limit response from the server + try { + const errorData = await resp.json(); + if (errorData.error === "RATE_LIMIT_EXCEEDED" && errorData.rateLimitInfo) { + return { + success: false, + error: "RATE_LIMIT_EXCEEDED", + message: errorData.message, + rateLimitInfo: errorData.rateLimitInfo, + status: 429, + }; + } + } catch { + // If parsing fails, fall through to generic message + } + return { success: false, error: "rate_limit_exceeded", diff --git a/create-db/src/index.ts b/create-db/src/index.ts index d9f3aee..baed48d 100644 --- a/create-db/src/index.ts +++ b/create-db/src/index.ts @@ -77,14 +77,16 @@ const validateRegionWithUrl = (region: string) => const createDatabaseCoreWithUrl = ( region: string, userAgent?: string, - cliRunId?: string + cliRunId?: string, + source?: "programmatic" | "cli" ) => createDatabaseCore( region, CREATE_DB_WORKER_URL, CLAIM_DB_WORKER_URL, userAgent, - cliRunId + cliRunId, + source ); const router = os.router({ @@ -392,7 +394,9 @@ export async function create( ): Promise { return createDatabaseCoreWithUrl( options?.region || "us-east-1", - options?.userAgent + options?.userAgent, + undefined, + "programmatic" ); } diff --git a/create-db/src/types.ts b/create-db/src/types.ts index 3654c0f..b8a6a08 100644 --- a/create-db/src/types.ts +++ b/create-db/src/types.ts @@ -54,6 +54,11 @@ export interface DatabaseError { raw?: string; details?: unknown; status?: number; + rateLimitInfo?: { + retryAfterMs: number; + currentCount: number; + maxRequests: number; + }; } export type CreateDatabaseResult = DatabaseResult | DatabaseError;