html-renderer-api 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INTEGRATION.md +319 -0
- package/QUICKSTART.md +270 -0
- package/package.json +19 -0
- package/readme.md +317 -0
- package/src/config/setup.sh +60 -0
- package/src/durable-objects/browser-durable-object.ts +534 -0
- package/src/scrapers/scraper-captcha.ts +479 -0
- package/src/scrapers/scraper-cloudflare.ts +93 -0
- package/src/scrapers/scraper-login.ts +271 -0
- package/src/scrapers/scraper-openapi.ts +363 -0
- package/src/scrapers/scraper-stealth.ts +521 -0
- package/src/utils/scraper-utils.ts +219 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file scraper-login.ts
|
|
3
|
+
* @description Google login automation via the BrowserDurableObject.
|
|
4
|
+
* Handles the multi-step Google sign-in flow (email → password → redirect)
|
|
5
|
+
* and returns the resulting auth cookies for NotebookLM access.
|
|
6
|
+
*
|
|
7
|
+
* Also provides a fetch-as-session endpoint that executes HTTP requests
|
|
8
|
+
* with the session's stored cookies.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import puppeteer from "@cloudflare/puppeteer";
|
|
12
|
+
import type { Browser, Page, Cookie } from "@cloudflare/puppeteer";
|
|
13
|
+
import { applyStealthEvasions } from "./scraper-stealth.js";
|
|
14
|
+
import type { Env } from "./scraper-utils.js";
|
|
15
|
+
|
|
16
|
+
export interface LoginRequest {
|
|
17
|
+
action: "google-login";
|
|
18
|
+
sessionId: string;
|
|
19
|
+
email: string;
|
|
20
|
+
password: string;
|
|
21
|
+
targetUrl: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LoginResponse {
|
|
25
|
+
success: boolean;
|
|
26
|
+
cookies?: Cookie[];
|
|
27
|
+
finalUrl?: string;
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FetchAsSessionRequest {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
url: string;
|
|
34
|
+
method?: string;
|
|
35
|
+
body?: string;
|
|
36
|
+
cookies?: Cookie[];
|
|
37
|
+
headers?: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Handles POST /api/login — automates Google OAuth login flow via Puppeteer.
|
|
42
|
+
*
|
|
43
|
+
* Flow:
|
|
44
|
+
* 1. Navigate to targetUrl (e.g. NotebookLM) which redirects to Google login
|
|
45
|
+
* 2. Enter email, click Next
|
|
46
|
+
* 3. Enter password, click Next
|
|
47
|
+
* 4. Wait for redirect back to targetUrl
|
|
48
|
+
* 5. Return all cookies from the authenticated session
|
|
49
|
+
*/
|
|
50
|
+
export async function handleLogin(
|
|
51
|
+
request: Request,
|
|
52
|
+
env: Env,
|
|
53
|
+
): Promise<Response> {
|
|
54
|
+
let body: LoginRequest;
|
|
55
|
+
try {
|
|
56
|
+
body = await request.json();
|
|
57
|
+
} catch {
|
|
58
|
+
return jsonResponse({ success: false, error: "Invalid JSON body" }, 400);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!body.email || !body.password || !body.targetUrl) {
|
|
62
|
+
return jsonResponse(
|
|
63
|
+
{ success: false, error: "email, password, and targetUrl are required" },
|
|
64
|
+
400,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const id = env.BROWSER_DO.idFromName(`browser-${body.sessionId || "login"}`);
|
|
69
|
+
const browserDO = env.BROWSER_DO.get(id);
|
|
70
|
+
|
|
71
|
+
// Forward the login request to the Durable Object
|
|
72
|
+
const doRequest = new Request(request.url, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify({ ...body, _action: "login" }),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return browserDO.fetch(doRequest);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Handles POST /api/fetch — execute an HTTP request using the session's
|
|
83
|
+
* stored browser cookies. This allows the qwksearch-web backend to make
|
|
84
|
+
* authenticated requests to NotebookLM's internal APIs.
|
|
85
|
+
*/
|
|
86
|
+
export async function handleFetchAsSession(
|
|
87
|
+
request: Request,
|
|
88
|
+
env: Env,
|
|
89
|
+
): Promise<Response> {
|
|
90
|
+
let body: FetchAsSessionRequest;
|
|
91
|
+
try {
|
|
92
|
+
body = await request.json();
|
|
93
|
+
} catch {
|
|
94
|
+
return jsonResponse({ error: "Invalid JSON body" }, 400);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!body.url || !body.sessionId) {
|
|
98
|
+
return jsonResponse({ error: "url and sessionId are required" }, 400);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Build the proxied request with the user's cookies
|
|
102
|
+
const cookieHeader = body.cookies
|
|
103
|
+
?.map((c) => `${c.name}=${c.value}`)
|
|
104
|
+
.join("; ");
|
|
105
|
+
|
|
106
|
+
const headers: Record<string, string> = {
|
|
107
|
+
...body.headers,
|
|
108
|
+
"User-Agent":
|
|
109
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (cookieHeader) {
|
|
113
|
+
headers["Cookie"] = cookieHeader;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(body.url, {
|
|
118
|
+
method: body.method || "GET",
|
|
119
|
+
headers,
|
|
120
|
+
body: body.body,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const responseHeaders = new Headers();
|
|
124
|
+
responseHeaders.set(
|
|
125
|
+
"Content-Type",
|
|
126
|
+
response.headers.get("Content-Type") || "application/json",
|
|
127
|
+
);
|
|
128
|
+
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
|
129
|
+
|
|
130
|
+
return new Response(response.body, {
|
|
131
|
+
status: response.status,
|
|
132
|
+
headers: responseHeaders,
|
|
133
|
+
});
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return jsonResponse(
|
|
136
|
+
{ error: `Fetch failed: ${(err as Error).message}` },
|
|
137
|
+
502,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Google login flow executed inside a Durable Object's persistent browser.
|
|
144
|
+
* Called from BrowserDurableObject when it receives a login action request.
|
|
145
|
+
*/
|
|
146
|
+
export async function executeGoogleLogin(
|
|
147
|
+
page: Page,
|
|
148
|
+
email: string,
|
|
149
|
+
password: string,
|
|
150
|
+
targetUrl: string,
|
|
151
|
+
): Promise<LoginResponse> {
|
|
152
|
+
try {
|
|
153
|
+
// Navigate to the target (NotebookLM) which will redirect to Google login
|
|
154
|
+
await page.goto(targetUrl, {
|
|
155
|
+
waitUntil: "networkidle2",
|
|
156
|
+
timeout: 30000,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Check if already logged in
|
|
160
|
+
if (page.url().includes("notebooklm.google.com") && !page.url().includes("accounts.google.com")) {
|
|
161
|
+
const cookies = await page.cookies();
|
|
162
|
+
return { success: true, cookies, finalUrl: page.url() };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Wait for the email input field
|
|
166
|
+
await page.waitForSelector('input[type="email"]', { timeout: 10000 });
|
|
167
|
+
await randomDelay(500, 1000);
|
|
168
|
+
|
|
169
|
+
// Type email with human-like delays
|
|
170
|
+
await page.type('input[type="email"]', email, { delay: 50 + Math.random() * 80 });
|
|
171
|
+
await randomDelay(300, 700);
|
|
172
|
+
|
|
173
|
+
// Click "Next" button
|
|
174
|
+
const nextButton = await page.$(
|
|
175
|
+
'#identifierNext, button[type="submit"], [data-idom-class*="next"]',
|
|
176
|
+
);
|
|
177
|
+
if (nextButton) {
|
|
178
|
+
await nextButton.click();
|
|
179
|
+
} else {
|
|
180
|
+
await page.keyboard.press("Enter");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Wait for password field to appear
|
|
184
|
+
await page.waitForSelector('input[type="password"]', {
|
|
185
|
+
visible: true,
|
|
186
|
+
timeout: 10000,
|
|
187
|
+
});
|
|
188
|
+
await randomDelay(800, 1500);
|
|
189
|
+
|
|
190
|
+
// Type password
|
|
191
|
+
await page.type('input[type="password"]', password, { delay: 40 + Math.random() * 60 });
|
|
192
|
+
await randomDelay(300, 700);
|
|
193
|
+
|
|
194
|
+
// Click "Next" / "Sign in"
|
|
195
|
+
const signInButton = await page.$(
|
|
196
|
+
'#passwordNext, button[type="submit"], [data-idom-class*="next"]',
|
|
197
|
+
);
|
|
198
|
+
if (signInButton) {
|
|
199
|
+
await signInButton.click();
|
|
200
|
+
} else {
|
|
201
|
+
await page.keyboard.press("Enter");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Wait for navigation to complete (either to target or 2FA/consent)
|
|
205
|
+
await page.waitForNavigation({
|
|
206
|
+
waitUntil: "networkidle2",
|
|
207
|
+
timeout: 30000,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Check for 2FA challenge
|
|
211
|
+
const currentUrl = page.url();
|
|
212
|
+
if (
|
|
213
|
+
currentUrl.includes("challenge") ||
|
|
214
|
+
currentUrl.includes("signin/v2/challenge")
|
|
215
|
+
) {
|
|
216
|
+
return {
|
|
217
|
+
success: false,
|
|
218
|
+
error:
|
|
219
|
+
"Two-factor authentication required. Please use an app password or disable 2FA temporarily.",
|
|
220
|
+
finalUrl: currentUrl,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check for consent screen
|
|
225
|
+
if (currentUrl.includes("consent") || currentUrl.includes("oauthchooseaccount")) {
|
|
226
|
+
// Try to click "Allow" or "Continue"
|
|
227
|
+
const allowBtn = await page.$(
|
|
228
|
+
'button[id="submit_approve_access"], [data-value="true"], button:has-text("Allow"), button:has-text("Continue")',
|
|
229
|
+
);
|
|
230
|
+
if (allowBtn) {
|
|
231
|
+
await allowBtn.click();
|
|
232
|
+
await page.waitForNavigation({ waitUntil: "networkidle2", timeout: 15000 });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Give the page a moment to settle after login
|
|
237
|
+
await randomDelay(2000, 3000);
|
|
238
|
+
|
|
239
|
+
const finalUrl = page.url();
|
|
240
|
+
const cookies = await page.cookies();
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
success: finalUrl.includes("notebooklm.google.com"),
|
|
244
|
+
cookies,
|
|
245
|
+
finalUrl,
|
|
246
|
+
error: finalUrl.includes("notebooklm.google.com")
|
|
247
|
+
? undefined
|
|
248
|
+
: "Did not reach NotebookLM after login",
|
|
249
|
+
};
|
|
250
|
+
} catch (err) {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
error: `Login flow error: ${(err as Error).message}`,
|
|
254
|
+
finalUrl: page.url(),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function randomDelay(min: number, max: number): Promise<void> {
|
|
260
|
+
return new Promise((r) => setTimeout(r, min + Math.random() * (max - min)));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function jsonResponse(data: unknown, status = 200): Response {
|
|
264
|
+
return new Response(JSON.stringify(data), {
|
|
265
|
+
status,
|
|
266
|
+
headers: {
|
|
267
|
+
"Content-Type": "application/json",
|
|
268
|
+
"Access-Control-Allow-Origin": "*",
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file scraper-openapi.ts
|
|
3
|
+
* @description OpenAPI 3.0 specification and Swagger UI endpoints for the
|
|
4
|
+
* Puppeteer rendering API. `serveSwagger` returns the interactive Swagger UI
|
|
5
|
+
* page; `serveOpenAPI` returns the raw JSON spec consumed by that UI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns an HTML page that renders the Swagger UI pointed at `./openapi.json`.
|
|
10
|
+
*
|
|
11
|
+
* @returns HTTP response with `Content-Type: text/html`.
|
|
12
|
+
*/
|
|
13
|
+
export function serveSwagger(): Response {
|
|
14
|
+
const html = `
|
|
15
|
+
<!DOCTYPE html>
|
|
16
|
+
<html>
|
|
17
|
+
<head>
|
|
18
|
+
<title>Puppeteer API Documentation</title>
|
|
19
|
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
|
|
20
|
+
<style>
|
|
21
|
+
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
|
|
22
|
+
*, *:before, *:after { box-sizing: inherit; }
|
|
23
|
+
body { margin:0; background: #fafafa; }
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<div id="swagger-ui"></div>
|
|
28
|
+
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-bundle.js"></script>
|
|
29
|
+
<script>
|
|
30
|
+
SwaggerUIBundle({
|
|
31
|
+
url: './openapi.json',
|
|
32
|
+
dom_id: '#swagger-ui',
|
|
33
|
+
deepLinking: true,
|
|
34
|
+
presets: [
|
|
35
|
+
SwaggerUIBundle.presets.apis,
|
|
36
|
+
SwaggerUIBundle.presets.standalone
|
|
37
|
+
],
|
|
38
|
+
plugins: [
|
|
39
|
+
SwaggerUIBundle.plugins.DownloadUrl
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
</script>
|
|
43
|
+
</body>
|
|
44
|
+
</html>`;
|
|
45
|
+
|
|
46
|
+
return new Response(html, {
|
|
47
|
+
headers: { "Content-Type": "text/html" },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Minimal OpenAPI 3.0 schema type used for the spec object below. */
|
|
52
|
+
interface OpenAPISpec {
|
|
53
|
+
openapi: string;
|
|
54
|
+
info: {
|
|
55
|
+
title: string;
|
|
56
|
+
description: string;
|
|
57
|
+
version: string;
|
|
58
|
+
contact: { name: string };
|
|
59
|
+
};
|
|
60
|
+
servers: Array<{ url: string; description: string }>;
|
|
61
|
+
security: Array<Record<string, string[]>>;
|
|
62
|
+
paths: Record<string, unknown>;
|
|
63
|
+
components: {
|
|
64
|
+
securitySchemes: Record<string, unknown>;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns the OpenAPI 3.0.3 JSON specification for the rendering API.
|
|
70
|
+
* Documents both the GET and POST `/render` endpoints including all
|
|
71
|
+
* parameters, request body fields, and response schemas.
|
|
72
|
+
*
|
|
73
|
+
* @returns HTTP response with `Content-Type: application/json`.
|
|
74
|
+
*/
|
|
75
|
+
export function serveOpenAPI(): Response {
|
|
76
|
+
const spec: OpenAPISpec = {
|
|
77
|
+
openapi: "3.0.3",
|
|
78
|
+
info: {
|
|
79
|
+
title: "Puppeteer Rendering API",
|
|
80
|
+
description:
|
|
81
|
+
"A powerful web scraping and rendering API using Puppeteer with Cloudflare Workers and Durable Objects. Includes automatic Cloudflare challenge bypass.",
|
|
82
|
+
version: "2.1.0",
|
|
83
|
+
contact: { name: "API Support" },
|
|
84
|
+
},
|
|
85
|
+
servers: [{ url: "/api", description: "API Server" }],
|
|
86
|
+
security: [{ bearerAuth: [] }, { passwordAuth: [] }],
|
|
87
|
+
paths: {
|
|
88
|
+
"/render": {
|
|
89
|
+
get: {
|
|
90
|
+
summary: "Render webpage (GET)",
|
|
91
|
+
description:
|
|
92
|
+
"Render a webpage using Puppeteer and return the HTML content. Automatically bypasses Cloudflare challenges.",
|
|
93
|
+
parameters: [
|
|
94
|
+
{
|
|
95
|
+
name: "url",
|
|
96
|
+
in: "query",
|
|
97
|
+
required: true,
|
|
98
|
+
schema: { type: "string", format: "uri" },
|
|
99
|
+
description: "The URL to render",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "SCRAPER_API_KEY",
|
|
103
|
+
in: "query",
|
|
104
|
+
required: false,
|
|
105
|
+
schema: { type: "string" },
|
|
106
|
+
description: "API key (if required)",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "wait",
|
|
110
|
+
in: "query",
|
|
111
|
+
required: false,
|
|
112
|
+
schema: { type: "integer", minimum: 0, maximum: 30000 },
|
|
113
|
+
description: "Additional wait time in milliseconds",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "blockImages",
|
|
117
|
+
in: "query",
|
|
118
|
+
required: false,
|
|
119
|
+
schema: { type: "boolean" },
|
|
120
|
+
description: "Block image loading to save bandwidth",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "sessionId",
|
|
124
|
+
in: "query",
|
|
125
|
+
required: false,
|
|
126
|
+
schema: { type: "string" },
|
|
127
|
+
description:
|
|
128
|
+
"Session ID for browser reuse and cookie persistence",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "timeout",
|
|
132
|
+
in: "query",
|
|
133
|
+
required: false,
|
|
134
|
+
schema: { type: "integer", minimum: 5000, maximum: 60000 },
|
|
135
|
+
description: "Page load timeout in milliseconds",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "waitUntil",
|
|
139
|
+
in: "query",
|
|
140
|
+
required: false,
|
|
141
|
+
schema: {
|
|
142
|
+
type: "string",
|
|
143
|
+
enum: [
|
|
144
|
+
"load",
|
|
145
|
+
"domcontentloaded",
|
|
146
|
+
"networkidle0",
|
|
147
|
+
"networkidle2",
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
description: "When to consider navigation succeeded",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "cookies",
|
|
154
|
+
in: "query",
|
|
155
|
+
required: false,
|
|
156
|
+
schema: { type: "string" },
|
|
157
|
+
description: "JSON string of cookies to set",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "format",
|
|
161
|
+
in: "query",
|
|
162
|
+
required: false,
|
|
163
|
+
schema: { type: "string", enum: ["html", "json"] },
|
|
164
|
+
description: "Response format",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "proxyUrl",
|
|
168
|
+
in: "query",
|
|
169
|
+
required: false,
|
|
170
|
+
schema: { type: "string", format: "uri" },
|
|
171
|
+
description:
|
|
172
|
+
"Proxy server URL (e.g., http://proxy.example.com:8080)",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "proxyUser",
|
|
176
|
+
in: "query",
|
|
177
|
+
required: false,
|
|
178
|
+
schema: { type: "string" },
|
|
179
|
+
description: "Proxy username (if proxy requires authentication)",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "proxyPass",
|
|
183
|
+
in: "query",
|
|
184
|
+
required: false,
|
|
185
|
+
schema: { type: "string" },
|
|
186
|
+
description: "Proxy password (if proxy requires authentication)",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "bypassCaptcha",
|
|
190
|
+
in: "query",
|
|
191
|
+
required: false,
|
|
192
|
+
schema: { type: "boolean", default: true },
|
|
193
|
+
description:
|
|
194
|
+
"Enable Cloudflare challenge bypass (enabled by default)",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "challengeMatch",
|
|
198
|
+
in: "query",
|
|
199
|
+
required: false,
|
|
200
|
+
schema: { type: "string" },
|
|
201
|
+
description:
|
|
202
|
+
"Custom string to detect challenge pages (default: 'challenge-platform')",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "maxRetries",
|
|
206
|
+
in: "query",
|
|
207
|
+
required: false,
|
|
208
|
+
schema: { type: "integer", minimum: 1, maximum: 20, default: 10 },
|
|
209
|
+
description: "Maximum retries for challenge bypass",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "challengeTimeout",
|
|
213
|
+
in: "query",
|
|
214
|
+
required: false,
|
|
215
|
+
schema: {
|
|
216
|
+
type: "integer",
|
|
217
|
+
minimum: 1000,
|
|
218
|
+
maximum: 30000,
|
|
219
|
+
default: 5000,
|
|
220
|
+
},
|
|
221
|
+
description: "Timeout for each challenge retry in milliseconds",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "twoCaptchaKey",
|
|
225
|
+
in: "query",
|
|
226
|
+
required: false,
|
|
227
|
+
schema: { type: "string" },
|
|
228
|
+
description:
|
|
229
|
+
"2captcha API key for solving reCAPTCHA/Turnstile challenges (optional, for sites with harder protection)",
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
responses: {
|
|
233
|
+
"200": {
|
|
234
|
+
description: "Successfully rendered webpage",
|
|
235
|
+
content: {
|
|
236
|
+
"text/html": { schema: { type: "string" } },
|
|
237
|
+
"application/json": {
|
|
238
|
+
schema: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
html: { type: "string" },
|
|
242
|
+
url: { type: "string" },
|
|
243
|
+
title: { type: "string" },
|
|
244
|
+
cookies: { type: "array" },
|
|
245
|
+
performance: { type: "object" },
|
|
246
|
+
challengeBypassed: { type: "boolean" },
|
|
247
|
+
retryCount: { type: "integer" },
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
"400": { description: "Bad request - missing URL or invalid parameters" },
|
|
254
|
+
"401": { description: "Unauthorized - invalid or missing password" },
|
|
255
|
+
"500": { description: "Internal server error" },
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
post: {
|
|
259
|
+
summary: "Render webpage (POST)",
|
|
260
|
+
description:
|
|
261
|
+
"Render a webpage using Puppeteer with advanced options via POST body. Automatically bypasses Cloudflare challenges.",
|
|
262
|
+
requestBody: {
|
|
263
|
+
required: true,
|
|
264
|
+
content: {
|
|
265
|
+
"application/json": {
|
|
266
|
+
schema: {
|
|
267
|
+
type: "object",
|
|
268
|
+
required: ["url"],
|
|
269
|
+
properties: {
|
|
270
|
+
url: { type: "string", format: "uri" },
|
|
271
|
+
SCRAPER_API_KEY: { type: "string" },
|
|
272
|
+
wait: { type: "integer", minimum: 0, maximum: 30000 },
|
|
273
|
+
blockImages: { type: "boolean" },
|
|
274
|
+
sessionId: { type: "string" },
|
|
275
|
+
timeout: {
|
|
276
|
+
type: "integer",
|
|
277
|
+
minimum: 5000,
|
|
278
|
+
maximum: 60000,
|
|
279
|
+
},
|
|
280
|
+
waitUntil: {
|
|
281
|
+
type: "string",
|
|
282
|
+
enum: [
|
|
283
|
+
"load",
|
|
284
|
+
"domcontentloaded",
|
|
285
|
+
"networkidle0",
|
|
286
|
+
"networkidle2",
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
cookies: { type: "string" },
|
|
290
|
+
headers: {
|
|
291
|
+
type: "object",
|
|
292
|
+
additionalProperties: { type: "string" },
|
|
293
|
+
},
|
|
294
|
+
format: { type: "string", enum: ["html", "json"] },
|
|
295
|
+
proxyUrl: { type: "string", format: "uri" },
|
|
296
|
+
proxyUser: { type: "string" },
|
|
297
|
+
proxyPass: { type: "string" },
|
|
298
|
+
bypassCaptcha: { type: "boolean", default: true },
|
|
299
|
+
challengeMatch: { type: "string" },
|
|
300
|
+
maxRetries: {
|
|
301
|
+
type: "integer",
|
|
302
|
+
minimum: 1,
|
|
303
|
+
maximum: 20,
|
|
304
|
+
default: 10,
|
|
305
|
+
},
|
|
306
|
+
challengeTimeout: {
|
|
307
|
+
type: "integer",
|
|
308
|
+
minimum: 1000,
|
|
309
|
+
maximum: 30000,
|
|
310
|
+
default: 5000,
|
|
311
|
+
},
|
|
312
|
+
twoCaptchaKey: { type: "string" },
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
responses: {
|
|
319
|
+
"200": {
|
|
320
|
+
description: "Successfully rendered webpage",
|
|
321
|
+
content: {
|
|
322
|
+
"text/html": { schema: { type: "string" } },
|
|
323
|
+
"application/json": {
|
|
324
|
+
schema: {
|
|
325
|
+
type: "object",
|
|
326
|
+
properties: {
|
|
327
|
+
html: { type: "string" },
|
|
328
|
+
url: { type: "string" },
|
|
329
|
+
title: { type: "string" },
|
|
330
|
+
cookies: { type: "array" },
|
|
331
|
+
performance: { type: "object" },
|
|
332
|
+
challengeBypassed: { type: "boolean" },
|
|
333
|
+
retryCount: { type: "integer" },
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
components: {
|
|
344
|
+
securitySchemes: {
|
|
345
|
+
bearerAuth: {
|
|
346
|
+
type: "http",
|
|
347
|
+
scheme: "bearer",
|
|
348
|
+
description: "Use your API key as the bearer token",
|
|
349
|
+
},
|
|
350
|
+
apiKeyAuth: {
|
|
351
|
+
type: "apiKey",
|
|
352
|
+
in: "query",
|
|
353
|
+
name: "SCRAPER_API_KEY",
|
|
354
|
+
description: "API key as query parameter",
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
return new Response(JSON.stringify(spec, null, 2), {
|
|
361
|
+
headers: { "Content-Type": "application/json" },
|
|
362
|
+
});
|
|
363
|
+
}
|