websession-api-client 0.0.1 → 0.0.3
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/index.js +437 -0
- package/package.json +18 -12
- package/readme.md +79 -0
package/index.js
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
const puppeteer = require('puppeteer-extra');
|
|
2
|
+
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
|
|
3
|
+
|
|
4
|
+
puppeteer.use(StealthPlugin());
|
|
5
|
+
|
|
6
|
+
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
|
|
7
|
+
|
|
8
|
+
class ApiExtractor {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.browser = null;
|
|
11
|
+
this.page = null;
|
|
12
|
+
this.TargetCookie = null;
|
|
13
|
+
this.TargetCookieValue = null;
|
|
14
|
+
this.TargetDomain = null;
|
|
15
|
+
|
|
16
|
+
this.authPage = null;
|
|
17
|
+
this.bearerToken = null;
|
|
18
|
+
this.tokenExpiry = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async init(options = {}) {
|
|
22
|
+
console.log('starting browser...');
|
|
23
|
+
|
|
24
|
+
const defaultArgs = [
|
|
25
|
+
'--no-sandbox',
|
|
26
|
+
'--disable-setuid-sandbox',
|
|
27
|
+
'--disable-web-security',
|
|
28
|
+
'--disable-features=VizDisplayCompositor',
|
|
29
|
+
'--disable-background-timer-throttling',
|
|
30
|
+
'--disable-backgrounding-occluded-windows',
|
|
31
|
+
'--disable-renderer-backgrounding',
|
|
32
|
+
'--disable-dev-shm-usage',
|
|
33
|
+
'--no-first-run',
|
|
34
|
+
'--no-default-browser-check',
|
|
35
|
+
'--disable-gpu',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const puppeteerConfig = {
|
|
39
|
+
headless: true,
|
|
40
|
+
...options,
|
|
41
|
+
args: options.args ? [...defaultArgs, ...options.args] : defaultArgs
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
this.browser = await puppeteer.launch(puppeteerConfig);
|
|
45
|
+
|
|
46
|
+
this.page = await this.browser.newPage();
|
|
47
|
+
await this.page.setUserAgent(DEFAULT_USER_AGENT);
|
|
48
|
+
|
|
49
|
+
console.log('Browser initialized');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getBearer(timeoutMs = 15000) {
|
|
53
|
+
console.log('Getting fresh Bearer token...');
|
|
54
|
+
|
|
55
|
+
if (!this.browser) {
|
|
56
|
+
throw new Error('Browser not initialized. Call init() first.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tokenPage = await this.browser.newPage();
|
|
60
|
+
await tokenPage.setUserAgent(DEFAULT_USER_AGENT);
|
|
61
|
+
await tokenPage.setRequestInterception(true);
|
|
62
|
+
|
|
63
|
+
let settled = false;
|
|
64
|
+
let timeoutId;
|
|
65
|
+
|
|
66
|
+
const tokenPromise = new Promise((resolve) => {
|
|
67
|
+
const onRequest = (request) => {
|
|
68
|
+
try {
|
|
69
|
+
const authHeader = request.headers()['authorization'];
|
|
70
|
+
if (!settled && authHeader && authHeader.startsWith('Bearer ') && authHeader.length > 100) {
|
|
71
|
+
settled = true;
|
|
72
|
+
const capturedToken = authHeader.substring(7);
|
|
73
|
+
console.log('Fresh Bearer token captured:', capturedToken.substring(0, 10) + '...');
|
|
74
|
+
clearTimeout(timeoutId);
|
|
75
|
+
tokenPage.off('request', onRequest);
|
|
76
|
+
resolve(capturedToken);
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
request.continue();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
tokenPage.on('request', onRequest);
|
|
84
|
+
|
|
85
|
+
timeoutId = setTimeout(() => {
|
|
86
|
+
if (settled) return;
|
|
87
|
+
settled = true;
|
|
88
|
+
tokenPage.off('request', onRequest);
|
|
89
|
+
resolve(null);
|
|
90
|
+
}, timeoutMs);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
this.authPage = tokenPage;
|
|
94
|
+
return { tokenPromise };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// CookieName is what the cookie is named in devtools, value is the value from devtools, and domain is the cookie domain
|
|
98
|
+
async setAuth(cookieName, domain, cookieValue) {
|
|
99
|
+
this.TargetDomain = domain;
|
|
100
|
+
this.TargetCookie = cookieName;
|
|
101
|
+
this.TargetCookieValue = cookieValue;
|
|
102
|
+
console.log('Setting auth cookie and attempting token capture...');
|
|
103
|
+
try {
|
|
104
|
+
const { tokenPromise } = await this.getBearer();
|
|
105
|
+
|
|
106
|
+
const domainparts = this.TargetDomain.split('.').slice(-2);
|
|
107
|
+
const baseDomain = domainparts.join('.');
|
|
108
|
+
|
|
109
|
+
await this.authPage.setCookie({
|
|
110
|
+
name: this.TargetCookie,
|
|
111
|
+
value: this.TargetCookieValue,
|
|
112
|
+
domain: `.${baseDomain}`,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const navigationPromise = this.authPage.goto(`https://${this.TargetDomain}/`, {
|
|
116
|
+
waitUntil: 'domcontentloaded',
|
|
117
|
+
timeout: 15000
|
|
118
|
+
}).catch((error) => {
|
|
119
|
+
if (error && error.name === 'TimeoutError') {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const capturedToken = await tokenPromise;
|
|
126
|
+
await navigationPromise;
|
|
127
|
+
|
|
128
|
+
if (capturedToken) {
|
|
129
|
+
this.bearerToken = capturedToken;
|
|
130
|
+
this.tokenExpiry = Date.now() + (55 * 60 * 1000);
|
|
131
|
+
console.log('Bearer token set successfully');
|
|
132
|
+
await this.authPage.close();
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log('No Bearer token captured');
|
|
137
|
+
await this.authPage.close();
|
|
138
|
+
return false;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('Bearer token extraction error:', error.message);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async callApi(url, options = {}) {
|
|
146
|
+
const {
|
|
147
|
+
auth = 'auto',
|
|
148
|
+
transport = 'node',
|
|
149
|
+
headers = {},
|
|
150
|
+
json,
|
|
151
|
+
...fetchOptions
|
|
152
|
+
} = options;
|
|
153
|
+
|
|
154
|
+
const shouldUseAuth = auth === true || (auth === 'auto' && !!this.bearerToken);
|
|
155
|
+
|
|
156
|
+
if (auth === true && !this.bearerToken) {
|
|
157
|
+
throw new Error('This request requires auth, but no bearer token is set. Call setAuth() first.');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const finalHeaders = {
|
|
161
|
+
'Accept': 'application/json',
|
|
162
|
+
'User-Agent': DEFAULT_USER_AGENT,
|
|
163
|
+
...headers
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (shouldUseAuth) {
|
|
167
|
+
finalHeaders.Authorization = `Bearer ${this.bearerToken}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Convenience: pass { json: {...} } instead of manually stringifying body
|
|
171
|
+
if (json !== undefined) {
|
|
172
|
+
fetchOptions.body = JSON.stringify(json);
|
|
173
|
+
const hasContentTypeHeader = Object.keys(finalHeaders).some(
|
|
174
|
+
(key) => key.toLowerCase() === 'content-type'
|
|
175
|
+
);
|
|
176
|
+
if (!hasContentTypeHeader) {
|
|
177
|
+
finalHeaders['Content-Type'] = 'application/json';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (transport === 'browser') {
|
|
182
|
+
return this.callApiInBrowser(url, {
|
|
183
|
+
...fetchOptions,
|
|
184
|
+
headers: finalHeaders
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const response = await fetch(url, {
|
|
190
|
+
...fetchOptions,
|
|
191
|
+
headers: finalHeaders
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
const errorText = await response.text().catch(() => '');
|
|
196
|
+
console.error(`API call failed (${response.status} ${response.statusText})`, errorText);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const contentType = response.headers.get('content-type') || '';
|
|
201
|
+
if (contentType.includes('application/json')) {
|
|
202
|
+
return await response.json();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return await response.text();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('API call error:', error.message);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async callAuthApi(url, options = {}) {
|
|
213
|
+
return this.callApi(url, { ...options, auth: true });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async callApiInBrowser(url, options = {}) {
|
|
217
|
+
if (!this.page) {
|
|
218
|
+
throw new Error('Browser page not initialized. Call init() first.');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const browserFetchOptions = { ...options };
|
|
222
|
+
|
|
223
|
+
if (browserFetchOptions.body !== undefined && typeof browserFetchOptions.body !== 'string') {
|
|
224
|
+
throw new Error('Browser transport only supports string bodies. Use `json` or a string `body`.');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!browserFetchOptions.credentials) {
|
|
228
|
+
browserFetchOptions.credentials = 'include';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let currentOrigin = null;
|
|
232
|
+
let targetOrigin = null;
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
currentOrigin = new URL(this.page.url()).origin;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
currentOrigin = null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
targetOrigin = new URL(url).origin;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
targetOrigin = null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Reduce CORS issues by putting the browser page on the target origin first.
|
|
247
|
+
if (targetOrigin && currentOrigin !== targetOrigin) {
|
|
248
|
+
try {
|
|
249
|
+
await this.page.goto(targetOrigin, {
|
|
250
|
+
waitUntil: 'domcontentloaded',
|
|
251
|
+
timeout: 15000
|
|
252
|
+
});
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (!error || error.name !== 'TimeoutError') {
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const response = await this.page.evaluate(async ({ requestUrl, requestOptions }) => {
|
|
262
|
+
try {
|
|
263
|
+
const res = await fetch(requestUrl, requestOptions);
|
|
264
|
+
const headers = {};
|
|
265
|
+
|
|
266
|
+
res.headers.forEach((value, key) => {
|
|
267
|
+
headers[key] = value;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
ok: res.ok,
|
|
272
|
+
status: res.status,
|
|
273
|
+
statusText: res.statusText,
|
|
274
|
+
headers,
|
|
275
|
+
text: await res.text()
|
|
276
|
+
};
|
|
277
|
+
} catch (error) {
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
status: 0,
|
|
281
|
+
statusText: 'BrowserFetchError',
|
|
282
|
+
headers: {},
|
|
283
|
+
text: error && error.message ? error.message : String(error)
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}, {
|
|
287
|
+
requestUrl: url,
|
|
288
|
+
requestOptions: browserFetchOptions
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (!response.ok) {
|
|
292
|
+
console.error(`API call failed (${response.status} ${response.statusText})`, response.text);
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const contentType = (response.headers && response.headers['content-type']) || '';
|
|
297
|
+
if (contentType.includes('application/json')) {
|
|
298
|
+
try {
|
|
299
|
+
return JSON.parse(response.text);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error('API call error: failed to parse JSON response from browser transport');
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return response.text;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error('API call error (browser transport):', error.message);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Navigate to any webpage and capture matching network requests/responses.
|
|
315
|
+
*
|
|
316
|
+
* @param {string} targetUrl - The URL to navigate to.
|
|
317
|
+
* @param {object} [options]
|
|
318
|
+
* @param {function} [options.filter] - Called with a request summary object.
|
|
319
|
+
* Return true to capture that request. If omitted, all requests are captured.
|
|
320
|
+
* Summary shape: { url, method, headers, postData, resourceType }
|
|
321
|
+
* @param {boolean} [options.captureResponse] - Also capture response body (slower). Default false.
|
|
322
|
+
* @param {number} [options.timeout] - Max ms to wait for page + requests. Default 30000.
|
|
323
|
+
* @param {number} [options.waitAfterLoad] - Extra ms to wait after page load for late requests. Default 3000.
|
|
324
|
+
* @param {number} [options.maxCaptures] - Stop early after capturing this many. Default Infinity.
|
|
325
|
+
* @param {object[]} [options.cookies] - Cookies to set before navigating.
|
|
326
|
+
* Each: { name, value, domain, ... }
|
|
327
|
+
* @returns {Promise<object[]>} Array of captured request (and optionally response) objects.
|
|
328
|
+
*/
|
|
329
|
+
async captureRequests(targetUrl, options = {}) {
|
|
330
|
+
const {
|
|
331
|
+
filter = null,
|
|
332
|
+
captureResponse = false,
|
|
333
|
+
timeout = 30000,
|
|
334
|
+
waitAfterLoad = 3000,
|
|
335
|
+
maxCaptures = Infinity,
|
|
336
|
+
cookies = []
|
|
337
|
+
} = options;
|
|
338
|
+
|
|
339
|
+
if (!this.browser) {
|
|
340
|
+
throw new Error('Browser not initialized. Call init() first.');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const page = await this.browser.newPage();
|
|
344
|
+
await page.setUserAgent(DEFAULT_USER_AGENT);
|
|
345
|
+
|
|
346
|
+
if (cookies.length > 0) {
|
|
347
|
+
await page.setCookie(...cookies);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const captured = [];
|
|
351
|
+
const responseBodyPromises = [];
|
|
352
|
+
|
|
353
|
+
// Use CDP for response body capture since request interception
|
|
354
|
+
// can interfere with page behavior on some sites.
|
|
355
|
+
const client = await page.createCDPSession();
|
|
356
|
+
await client.send('Network.enable');
|
|
357
|
+
|
|
358
|
+
// Map requestId -> request data for pairing requests with responses
|
|
359
|
+
const pendingRequests = new Map();
|
|
360
|
+
|
|
361
|
+
client.on('Network.requestWillBeSent', (event) => {
|
|
362
|
+
const summary = {
|
|
363
|
+
requestId: event.requestId,
|
|
364
|
+
url: event.request.url,
|
|
365
|
+
method: event.request.method,
|
|
366
|
+
headers: event.request.headers,
|
|
367
|
+
postData: event.request.postData || null,
|
|
368
|
+
resourceType: event.type
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const shouldCapture = filter ? filter(summary) : true;
|
|
372
|
+
if (shouldCapture) {
|
|
373
|
+
pendingRequests.set(event.requestId, summary);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
client.on('Network.responseReceived', async (event) => {
|
|
378
|
+
const reqData = pendingRequests.get(event.requestId);
|
|
379
|
+
if (!reqData) return;
|
|
380
|
+
|
|
381
|
+
const entry = {
|
|
382
|
+
request: {
|
|
383
|
+
url: reqData.url,
|
|
384
|
+
method: reqData.method,
|
|
385
|
+
headers: reqData.headers,
|
|
386
|
+
postData: reqData.postData,
|
|
387
|
+
resourceType: reqData.resourceType
|
|
388
|
+
},
|
|
389
|
+
response: {
|
|
390
|
+
status: event.response.status,
|
|
391
|
+
headers: event.response.headers,
|
|
392
|
+
mimeType: event.response.mimeType
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
if (captureResponse) {
|
|
397
|
+
const p = client.send('Network.getResponseBody', {
|
|
398
|
+
requestId: event.requestId
|
|
399
|
+
}).then(({ body, base64Encoded }) => {
|
|
400
|
+
entry.response.body = base64Encoded
|
|
401
|
+
? Buffer.from(body, 'base64').toString('utf-8')
|
|
402
|
+
: body;
|
|
403
|
+
}).catch(() => {
|
|
404
|
+
entry.response.body = null;
|
|
405
|
+
});
|
|
406
|
+
responseBodyPromises.push(p);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
captured.push(entry);
|
|
410
|
+
pendingRequests.delete(event.requestId);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
await page.goto(targetUrl, {
|
|
415
|
+
waitUntil: 'domcontentloaded',
|
|
416
|
+
timeout
|
|
417
|
+
}).catch((err) => {
|
|
418
|
+
if (err && err.name === 'TimeoutError') return null;
|
|
419
|
+
throw err;
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Wait for late-firing requests (XHR, fetch, GraphQL, etc.)
|
|
423
|
+
if (captured.length < maxCaptures) {
|
|
424
|
+
await new Promise((r) => setTimeout(r, waitAfterLoad));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
await Promise.all(responseBodyPromises);
|
|
428
|
+
} finally {
|
|
429
|
+
await client.detach();
|
|
430
|
+
await page.close();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return captured;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
module.exports = ApiExtractor;
|
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "websession-api-client",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"license": "ISC",
|
|
6
|
-
"author": "",
|
|
7
|
-
"type": "commonjs",
|
|
8
|
-
"main": "index.js",
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
-
|
|
12
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "websession-api-client",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
11
|
+
"dev": "node index.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"puppeteer": "^24.37.5",
|
|
15
|
+
"puppeteer-extra": "^3.3.6",
|
|
16
|
+
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
|
17
|
+
}
|
|
18
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# websession-api-client
|
|
2
|
+
|
|
3
|
+
Call APIs using a browser-captured bearer token, or call public APIs without auth.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
const ApiExtractor = require('./index');
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
const client = new ApiExtractor();
|
|
12
|
+
await client.init();
|
|
13
|
+
|
|
14
|
+
// 1) Public API (no auth header sent)
|
|
15
|
+
const publicData = await client.callApi('https://api.example.com/public', {
|
|
16
|
+
auth: false
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// 2) Set auth (captures a bearer token from site traffic)
|
|
20
|
+
await client.setAuth('cookie_name', 'app.example.com', 'cookie_value');
|
|
21
|
+
|
|
22
|
+
// 3) Authenticated API (explicit)
|
|
23
|
+
const privateData = await client.callApi('https://api.example.com/private', {
|
|
24
|
+
auth: true
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// 4) Authenticated API with helper
|
|
28
|
+
const privateData2 = await client.callAuthApi('https://api.example.com/private');
|
|
29
|
+
|
|
30
|
+
// 5) POST JSON (auth optional)
|
|
31
|
+
const created = await client.callApi('https://api.example.com/items', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
auth: true,
|
|
34
|
+
json: { name: 'test' }
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 6) Browser-session request (useful for sites protected by Cloudflare/anti-bot checks)
|
|
38
|
+
const protectedData = await client.callApi('https://www.example.com/api/data.json', {
|
|
39
|
+
auth: false,
|
|
40
|
+
transport: 'browser'
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log({ publicData, privateData, privateData2, created, protectedData });
|
|
44
|
+
})();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## `callApi(url, options)`
|
|
48
|
+
|
|
49
|
+
Supports normal `fetch` options plus:
|
|
50
|
+
|
|
51
|
+
- `auth`: `true` | `false` | `'auto'` (default)
|
|
52
|
+
- `true`: require bearer token and send `Authorization`
|
|
53
|
+
- `false`: never send `Authorization`
|
|
54
|
+
- `'auto'`: send `Authorization` only if a bearer token is available
|
|
55
|
+
- `json`: object to JSON.stringify into `body` and auto-set `Content-Type: application/json`
|
|
56
|
+
- `transport`: `'node'` (default) | `'browser'`
|
|
57
|
+
- `'node'`: regular Node.js `fetch`
|
|
58
|
+
- `'browser'`: runs `fetch` inside the Puppeteer page session (uses browser cookies/session)
|
|
59
|
+
|
|
60
|
+
All other `fetch` options (`method`, `headers`, `body`, etc.) are passed through.
|
|
61
|
+
|
|
62
|
+
## Cloudflare / Bot Protection Note
|
|
63
|
+
|
|
64
|
+
If a public endpoint returns an HTML challenge page (403) instead of JSON, that is usually a bot-protection challenge, not an auth issue.
|
|
65
|
+
|
|
66
|
+
Use browser transport:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
const data = await client.callApi('https://www.artstation.com/users/yourname/projects.json', {
|
|
70
|
+
auth: false,
|
|
71
|
+
transport: 'browser'
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If the site still challenges headless Chrome, start Puppeteer in a visible window and complete the check once:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
await client.init({ headless: false });
|
|
79
|
+
```
|