/** * @file scraper-openapi.ts * @description OpenAPI 3.0 specification and Swagger UI endpoints for the * Puppeteer rendering API. `serveSwagger` returns the interactive Swagger UI * page; `serveOpenAPI` returns the raw JSON spec consumed by that UI. */ /** * Returns an HTML page that renders the Swagger UI pointed at `./openapi.json`. * * @returns HTTP response with `Content-Type: text/html`. */ export function serveSwagger(): Response { const html = ` Puppeteer API Documentation
`; return new Response(html, { headers: { "Content-Type": "text/html" }, }); } /** Minimal OpenAPI 3.0 schema type used for the spec object below. */ interface OpenAPISpec { openapi: string; info: { title: string; description: string; version: string; contact: { name: string }; }; servers: Array<{ url: string; description: string }>; security: Array>; paths: Record; components: { securitySchemes: Record; }; } /** * Returns the OpenAPI 3.0.3 JSON specification for the rendering API. * Documents both the GET and POST `/render` endpoints including all * parameters, request body fields, and response schemas. * * @returns HTTP response with `Content-Type: application/json`. */ export function serveOpenAPI(): Response { const spec: OpenAPISpec = { openapi: "3.0.3", info: { title: "Puppeteer Rendering API", description: "A powerful web scraping and rendering API using Puppeteer with Cloudflare Workers and Durable Objects. Includes automatic Cloudflare challenge bypass.", version: "2.1.0", contact: { name: "API Support" }, }, servers: [{ url: "/api", description: "API Server" }], security: [{ bearerAuth: [] }, { passwordAuth: [] }], paths: { "/render": { get: { summary: "Render webpage (GET)", description: "Render a webpage using Puppeteer and return the HTML content. Automatically bypasses Cloudflare challenges.", parameters: [ { name: "url", in: "query", required: true, schema: { type: "string", format: "uri" }, description: "The URL to render", }, { name: "SCRAPER_API_KEY", in: "query", required: false, schema: { type: "string" }, description: "API key (if required)", }, { name: "wait", in: "query", required: false, schema: { type: "integer", minimum: 0, maximum: 30000 }, description: "Additional wait time in milliseconds", }, { name: "blockImages", in: "query", required: false, schema: { type: "boolean" }, description: "Block image loading to save bandwidth", }, { name: "sessionId", in: "query", required: false, schema: { type: "string" }, description: "Session ID for browser reuse and cookie persistence", }, { name: "timeout", in: "query", required: false, schema: { type: "integer", minimum: 5000, maximum: 60000 }, description: "Page load timeout in milliseconds", }, { name: "waitUntil", in: "query", required: false, schema: { type: "string", enum: [ "load", "domcontentloaded", "networkidle0", "networkidle2", ], }, description: "When to consider navigation succeeded", }, { name: "cookies", in: "query", required: false, schema: { type: "string" }, description: "JSON string of cookies to set", }, { name: "format", in: "query", required: false, schema: { type: "string", enum: ["html", "json"] }, description: "Response format", }, { name: "proxyUrl", in: "query", required: false, schema: { type: "string", format: "uri" }, description: "Proxy server URL (e.g., http://proxy.example.com:8080)", }, { name: "proxyUser", in: "query", required: false, schema: { type: "string" }, description: "Proxy username (if proxy requires authentication)", }, { name: "proxyPass", in: "query", required: false, schema: { type: "string" }, description: "Proxy password (if proxy requires authentication)", }, { name: "bypassCaptcha", in: "query", required: false, schema: { type: "boolean", default: true }, description: "Enable Cloudflare challenge bypass (enabled by default)", }, { name: "challengeMatch", in: "query", required: false, schema: { type: "string" }, description: "Custom string to detect challenge pages (default: 'challenge-platform')", }, { name: "maxRetries", in: "query", required: false, schema: { type: "integer", minimum: 1, maximum: 20, default: 10 }, description: "Maximum retries for challenge bypass", }, { name: "challengeTimeout", in: "query", required: false, schema: { type: "integer", minimum: 1000, maximum: 30000, default: 5000, }, description: "Timeout for each challenge retry in milliseconds", }, { name: "twoCaptchaKey", in: "query", required: false, schema: { type: "string" }, description: "2captcha API key for solving reCAPTCHA/Turnstile challenges (optional, for sites with harder protection)", }, ], responses: { "200": { description: "Successfully rendered webpage", content: { "text/html": { schema: { type: "string" } }, "application/json": { schema: { type: "object", properties: { html: { type: "string" }, url: { type: "string" }, title: { type: "string" }, cookies: { type: "array" }, performance: { type: "object" }, challengeBypassed: { type: "boolean" }, retryCount: { type: "integer" }, }, }, }, }, }, "400": { description: "Bad request - missing URL or invalid parameters" }, "401": { description: "Unauthorized - invalid or missing password" }, "500": { description: "Internal server error" }, }, }, post: { summary: "Render webpage (POST)", description: "Render a webpage using Puppeteer with advanced options via POST body. Automatically bypasses Cloudflare challenges.", requestBody: { required: true, content: { "application/json": { schema: { type: "object", required: ["url"], properties: { url: { type: "string", format: "uri" }, SCRAPER_API_KEY: { type: "string" }, wait: { type: "integer", minimum: 0, maximum: 30000 }, blockImages: { type: "boolean" }, sessionId: { type: "string" }, timeout: { type: "integer", minimum: 5000, maximum: 60000, }, waitUntil: { type: "string", enum: [ "load", "domcontentloaded", "networkidle0", "networkidle2", ], }, cookies: { type: "string" }, headers: { type: "object", additionalProperties: { type: "string" }, }, format: { type: "string", enum: ["html", "json"] }, proxyUrl: { type: "string", format: "uri" }, proxyUser: { type: "string" }, proxyPass: { type: "string" }, bypassCaptcha: { type: "boolean", default: true }, challengeMatch: { type: "string" }, maxRetries: { type: "integer", minimum: 1, maximum: 20, default: 10, }, challengeTimeout: { type: "integer", minimum: 1000, maximum: 30000, default: 5000, }, twoCaptchaKey: { type: "string" }, }, }, }, }, }, responses: { "200": { description: "Successfully rendered webpage", content: { "text/html": { schema: { type: "string" } }, "application/json": { schema: { type: "object", properties: { html: { type: "string" }, url: { type: "string" }, title: { type: "string" }, cookies: { type: "array" }, performance: { type: "object" }, challengeBypassed: { type: "boolean" }, retryCount: { type: "integer" }, }, }, }, }, }, }, }, }, }, components: { securitySchemes: { bearerAuth: { type: "http", scheme: "bearer", description: "Use your API key as the bearer token", }, apiKeyAuth: { type: "apiKey", in: "query", name: "SCRAPER_API_KEY", description: "API key as query parameter", }, }, }, }; return new Response(JSON.stringify(spec, null, 2), { headers: { "Content-Type": "application/json" }, }); }