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.
@@ -0,0 +1,219 @@
1
+ /**
2
+ * @file scraper-utils.ts
3
+ * @description Shared utility functions for the Puppeteer scraper service.
4
+ * Handles request parameter parsing (GET query params + POST body merging)
5
+ * and Bearer/API-key authentication against the SCRAPER_API_KEY env var.
6
+ */
7
+
8
+ /** Cloudflare Worker environment bindings. */
9
+ export interface Env {
10
+ SCRAPER_API_KEY?: string;
11
+ PROXY_URL?: string;
12
+ PROXY_USER?: string;
13
+ PROXY_PASS?: string;
14
+ TWO_CAPTCHA_KEY?: string;
15
+ CHALLENGE_MATCH?: string;
16
+ /** Cloudflare Browser binding used to launch Puppeteer. */
17
+ MYBROWSER: unknown;
18
+ /** Cloudflare Durable Object namespace for the browser pool. */
19
+ BROWSER_DO: DurableObjectNamespace;
20
+ }
21
+
22
+ /** Durable Object namespace stub (provided by the Cloudflare runtime). */
23
+ export interface DurableObjectNamespace {
24
+ idFromName(name: string): DurableObjectId;
25
+ get(id: DurableObjectId): DurableObjectStub;
26
+ }
27
+
28
+ export interface DurableObjectId {
29
+ toString(): string;
30
+ }
31
+
32
+ export interface DurableObjectStub {
33
+ fetch(request: Request): Promise<Response>;
34
+ }
35
+
36
+ /** Normalised parameters extracted from an incoming scrape request. */
37
+ export interface RequestParams {
38
+ url: string;
39
+ /** API key sourced from body/query (lowercase alias kept for compat). */
40
+ scraper_api_key?: string;
41
+ SCRAPER_API_KEY?: string;
42
+ /** Extra milliseconds to wait after page load. */
43
+ wait: number;
44
+ blockImages: boolean;
45
+ /** Browser-pool session identifier; controls cookie persistence. */
46
+ sessionId: string;
47
+ /** Navigation timeout in milliseconds. */
48
+ timeout: number;
49
+ /** Puppeteer `waitUntil` condition. */
50
+ waitUntil: string;
51
+ /** JSON-serialised cookie array to inject before navigation. */
52
+ cookies?: string;
53
+ /** Additional HTTP headers to attach to every request. */
54
+ headers: Record<string, string>;
55
+ /** Response format: `"html"` (default) or `"json"`. */
56
+ format: string;
57
+ proxyUrl?: string;
58
+ proxyUser?: string;
59
+ proxyPass?: string;
60
+ /** Whether to attempt automatic Cloudflare challenge bypass. */
61
+ bypassCaptcha: boolean;
62
+ /** Custom HTML substring that signals a challenge page. */
63
+ challengeMatch?: string;
64
+ /** Maximum number of challenge-bypass retry iterations. */
65
+ maxRetries: number;
66
+ /** Per-retry navigation timeout in milliseconds. */
67
+ challengeTimeout: number;
68
+ /** 2captcha API key for programmatic reCAPTCHA / Turnstile solving. */
69
+ twoCaptchaKey?: string;
70
+ }
71
+
72
+ /** Result returned by {@link authenticateRequest}. */
73
+ export interface AuthResult {
74
+ success: boolean;
75
+ /** JSON-stringified error body (only present when `success` is false). */
76
+ error?: string;
77
+ }
78
+
79
+ /**
80
+ * Validates the API key supplied in the request against the environment.
81
+ * Accepts the key as a Bearer token, a `SCRAPER_API_KEY` query parameter,
82
+ * or a `SCRAPER_API_KEY` field in the POST body (already parsed into `params`).
83
+ * Auth is skipped entirely when `env.SCRAPER_API_KEY` is not set.
84
+ *
85
+ * @param request - The incoming Fetch request.
86
+ * @param env - Cloudflare Worker environment bindings.
87
+ * @param params - Pre-parsed request parameters (used for body-sourced key).
88
+ * @returns `{ success: true }` on pass, or `{ success: false, error }` on failure.
89
+ */
90
+ export async function authenticateRequest(
91
+ request: Request,
92
+ env: Env,
93
+ params: RequestParams,
94
+ ): Promise<AuthResult> {
95
+ if (!env.SCRAPER_API_KEY) {
96
+ return { success: true };
97
+ }
98
+
99
+ const authHeader = request.headers.get("Authorization");
100
+ const urlParams = new URL(request.url).searchParams;
101
+
102
+ let providedApiKey: string | null = null;
103
+
104
+ if (authHeader && authHeader.startsWith("Bearer ")) {
105
+ providedApiKey = authHeader.substring(7);
106
+ } else if (urlParams.get("SCRAPER_API_KEY")) {
107
+ providedApiKey = urlParams.get("SCRAPER_API_KEY");
108
+ } else if (params.SCRAPER_API_KEY) {
109
+ providedApiKey = params.SCRAPER_API_KEY;
110
+ }
111
+
112
+ if (providedApiKey !== env.SCRAPER_API_KEY) {
113
+ return {
114
+ success: false,
115
+ error: JSON.stringify({ error: "Invalid or missing API key" }),
116
+ };
117
+ }
118
+
119
+ return { success: true };
120
+ }
121
+
122
+ /**
123
+ * Merges GET query parameters and a POST JSON / form-urlencoded body into a
124
+ * single {@link RequestParams} object, applying sane defaults for every field.
125
+ *
126
+ * @param request - The incoming Fetch request (body is consumed here for POST).
127
+ * @returns Fully populated {@link RequestParams} with defaults applied.
128
+ */
129
+ export async function parseRequestParams(
130
+ request: Request,
131
+ ): Promise<RequestParams> {
132
+ const url = new URL(request.url);
133
+ const searchParams = url.searchParams;
134
+
135
+ let bodyParams: Partial<RequestParams> & Record<string, unknown> = {};
136
+ if (request.method === "POST") {
137
+ try {
138
+ const contentType = request.headers.get("content-type") ?? "";
139
+ if (contentType.includes("application/json")) {
140
+ bodyParams = await request.json();
141
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
142
+ const formData = await request.formData();
143
+ const entries: Array<[string, unknown]> = [];
144
+ formData.forEach((value, key) => {
145
+ entries.push([key, value]);
146
+ });
147
+ bodyParams = Object.fromEntries(entries);
148
+ }
149
+ } catch {
150
+ // Ignore body parsing errors; fall back to query params only.
151
+ }
152
+ }
153
+
154
+ return {
155
+ url: (searchParams.get("url") ?? bodyParams.url ?? "") as string,
156
+ scraper_api_key: (searchParams.get("SCRAPER_API_KEY") ??
157
+ bodyParams.SCRAPER_API_KEY) as string | undefined,
158
+ wait: parseInt(
159
+ (searchParams.get("wait") ?? String(bodyParams.wait) ?? "0") as string,
160
+ 10,
161
+ ),
162
+ blockImages:
163
+ searchParams.get("blockImages") === "true" ||
164
+ bodyParams.blockImages === true,
165
+ sessionId:
166
+ (searchParams.get("sessionId") ??
167
+ (bodyParams.sessionId as string | undefined) ??
168
+ "default"),
169
+ timeout: parseInt(
170
+ (searchParams.get("timeout") ??
171
+ String(bodyParams.timeout) ??
172
+ "30000") as string,
173
+ 10,
174
+ ),
175
+ waitUntil:
176
+ searchParams.get("waitUntil") ??
177
+ (bodyParams.waitUntil as string | undefined) ??
178
+ "networkidle2",
179
+ cookies:
180
+ searchParams.get("cookies") ??
181
+ (bodyParams.cookies as string | undefined),
182
+ headers: (bodyParams.headers as Record<string, string>) ?? {},
183
+ format:
184
+ searchParams.get("format") ??
185
+ (bodyParams.format as string | undefined) ??
186
+ "html",
187
+ proxyUrl:
188
+ searchParams.get("proxyUrl") ??
189
+ (bodyParams.proxyUrl as string | undefined),
190
+ proxyUser:
191
+ searchParams.get("proxyUser") ??
192
+ (bodyParams.proxyUser as string | undefined),
193
+ proxyPass:
194
+ searchParams.get("proxyPass") ??
195
+ (bodyParams.proxyPass as string | undefined),
196
+ bypassCaptcha:
197
+ searchParams.get("bypassCaptcha") === "true" ||
198
+ bodyParams.bypassCaptcha === true ||
199
+ true,
200
+ challengeMatch:
201
+ searchParams.get("challengeMatch") ??
202
+ (bodyParams.challengeMatch as string | undefined),
203
+ maxRetries: parseInt(
204
+ (searchParams.get("maxRetries") ??
205
+ String(bodyParams.maxRetries) ??
206
+ "10") as string,
207
+ 10,
208
+ ),
209
+ challengeTimeout: parseInt(
210
+ (searchParams.get("challengeTimeout") ??
211
+ String(bodyParams.challengeTimeout) ??
212
+ "5000") as string,
213
+ 10,
214
+ ),
215
+ twoCaptchaKey:
216
+ searchParams.get("twoCaptchaKey") ??
217
+ (bodyParams.twoCaptchaKey as string | undefined),
218
+ };
219
+ }