wreq-js 0.2.0 → 1.1.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/dist/wreq-js.js CHANGED
@@ -1,12 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WebSocket = void 0;
3
+ exports.RequestError = exports.WebSocket = exports.Session = exports.Response = exports.Headers = void 0;
4
+ exports.fetch = fetch;
5
+ exports.createSession = createSession;
6
+ exports.withSession = withSession;
4
7
  exports.request = request;
5
8
  exports.getProfiles = getProfiles;
6
9
  exports.get = get;
7
10
  exports.post = post;
8
11
  exports.websocket = websocket;
12
+ const node_crypto_1 = require("node:crypto");
13
+ const node_http_1 = require("node:http");
9
14
  const types_1 = require("./types");
15
+ Object.defineProperty(exports, "RequestError", { enumerable: true, get: function () { return types_1.RequestError; } });
10
16
  let nativeBinding;
11
17
  let cachedProfiles;
12
18
  function loadNativeBinding() {
@@ -55,44 +61,554 @@ const websocketFinalizer = typeof FinalizationRegistry === "function"
55
61
  void nativeBinding.websocketClose(connection).catch(() => undefined);
56
62
  })
57
63
  : undefined;
64
+ const DEFAULT_BROWSER = "chrome_142";
65
+ function generateSessionId() {
66
+ const cryptoGlobal = globalThis.crypto;
67
+ if (cryptoGlobal?.randomUUID) {
68
+ return cryptoGlobal.randomUUID();
69
+ }
70
+ return (0, node_crypto_1.randomBytes)(16).toString("hex");
71
+ }
72
+ function normalizeSessionOptions(options) {
73
+ const sessionId = options?.sessionId ?? generateSessionId();
74
+ const defaults = {
75
+ browser: options?.browser ?? DEFAULT_BROWSER,
76
+ };
77
+ if (options?.proxy !== undefined) {
78
+ defaults.proxy = options.proxy;
79
+ }
80
+ if (options?.timeout !== undefined) {
81
+ defaults.timeout = options.timeout;
82
+ }
83
+ return { sessionId, defaults };
84
+ }
85
+ function isIterable(value) {
86
+ return Boolean(value) && typeof value[Symbol.iterator] === "function";
87
+ }
88
+ function isPlainObject(value) {
89
+ if (typeof value !== "object" || value === null) {
90
+ return false;
91
+ }
92
+ const proto = Object.getPrototypeOf(value);
93
+ return proto === Object.prototype || proto === null;
94
+ }
95
+ function coerceHeaderValue(value) {
96
+ return String(value);
97
+ }
98
+ class Headers {
99
+ constructor(init) {
100
+ this.store = new Map();
101
+ if (init) {
102
+ this.applyInit(init);
103
+ }
104
+ }
105
+ applyInit(init) {
106
+ if (init instanceof Headers) {
107
+ for (const [name, value] of init) {
108
+ this.append(name, value);
109
+ }
110
+ return;
111
+ }
112
+ if (Array.isArray(init) || isIterable(init)) {
113
+ for (const tuple of init) {
114
+ if (!tuple) {
115
+ continue;
116
+ }
117
+ const [name, value] = tuple;
118
+ this.append(name, value);
119
+ }
120
+ return;
121
+ }
122
+ if (isPlainObject(init)) {
123
+ for (const [name, value] of Object.entries(init)) {
124
+ if (value === undefined || value === null) {
125
+ continue;
126
+ }
127
+ this.set(name, coerceHeaderValue(value));
128
+ }
129
+ }
130
+ }
131
+ normalizeName(name) {
132
+ if (typeof name !== "string") {
133
+ throw new TypeError("Header name must be a string");
134
+ }
135
+ const trimmed = name.trim();
136
+ if (!trimmed) {
137
+ throw new TypeError("Header name must not be empty");
138
+ }
139
+ return { key: trimmed.toLowerCase(), display: trimmed };
140
+ }
141
+ assertValue(value) {
142
+ if (value === undefined || value === null) {
143
+ throw new TypeError("Header value must not be null or undefined");
144
+ }
145
+ return coerceHeaderValue(value);
146
+ }
147
+ append(name, value) {
148
+ const normalized = this.normalizeName(name);
149
+ const existing = this.store.get(normalized.key);
150
+ const coercedValue = this.assertValue(value);
151
+ if (existing) {
152
+ existing.values.push(coercedValue);
153
+ return;
154
+ }
155
+ this.store.set(normalized.key, {
156
+ name: normalized.display,
157
+ values: [coercedValue],
158
+ });
159
+ }
160
+ set(name, value) {
161
+ const normalized = this.normalizeName(name);
162
+ const coercedValue = this.assertValue(value);
163
+ this.store.set(normalized.key, {
164
+ name: normalized.display,
165
+ values: [coercedValue],
166
+ });
167
+ }
168
+ get(name) {
169
+ const normalized = this.normalizeName(name);
170
+ const entry = this.store.get(normalized.key);
171
+ return entry ? entry.values.join(", ") : null;
172
+ }
173
+ has(name) {
174
+ const normalized = this.normalizeName(name);
175
+ return this.store.has(normalized.key);
176
+ }
177
+ delete(name) {
178
+ const normalized = this.normalizeName(name);
179
+ this.store.delete(normalized.key);
180
+ }
181
+ entries() {
182
+ return this[Symbol.iterator]();
183
+ }
184
+ *keys() {
185
+ for (const [name] of this) {
186
+ yield name;
187
+ }
188
+ }
189
+ *values() {
190
+ for (const [, value] of this) {
191
+ yield value;
192
+ }
193
+ }
194
+ forEach(callback, thisArg) {
195
+ for (const [name, value] of this) {
196
+ callback.call(thisArg, value, name, this);
197
+ }
198
+ }
199
+ [Symbol.iterator]() {
200
+ const generator = function* (store) {
201
+ for (const entry of store.values()) {
202
+ yield [entry.name, entry.values.join(", ")];
203
+ }
204
+ };
205
+ return generator(this.store);
206
+ }
207
+ toObject() {
208
+ const result = {};
209
+ for (const [name, value] of this) {
210
+ result[name] = value;
211
+ }
212
+ return result;
213
+ }
214
+ }
215
+ exports.Headers = Headers;
216
+ function cloneNativeResponse(payload) {
217
+ return {
218
+ status: payload.status,
219
+ headers: { ...payload.headers },
220
+ body: payload.body,
221
+ cookies: { ...payload.cookies },
222
+ url: payload.url,
223
+ };
224
+ }
225
+ class Response {
226
+ constructor(payload, requestUrl) {
227
+ this.type = "basic";
228
+ this.bodyUsed = false;
229
+ this.payload = cloneNativeResponse(payload);
230
+ this.requestUrl = requestUrl;
231
+ this.status = payload.status;
232
+ this.statusText = node_http_1.STATUS_CODES[payload.status] ?? "";
233
+ this.ok = this.status >= 200 && this.status < 300;
234
+ this.headers = new Headers(payload.headers);
235
+ this.url = payload.url;
236
+ this.redirected = this.url !== requestUrl;
237
+ this.cookies = { ...payload.cookies };
238
+ this.body = payload.body;
239
+ }
240
+ async json() {
241
+ const text = await this.text();
242
+ return JSON.parse(text);
243
+ }
244
+ async text() {
245
+ this.assertBodyAvailable();
246
+ this.bodyUsed = true;
247
+ return this.body;
248
+ }
249
+ clone() {
250
+ if (this.bodyUsed) {
251
+ throw new TypeError("Cannot clone a Response whose body is already used");
252
+ }
253
+ return new Response(cloneNativeResponse(this.payload), this.requestUrl);
254
+ }
255
+ assertBodyAvailable() {
256
+ if (this.bodyUsed) {
257
+ throw new TypeError("Response body is already used");
258
+ }
259
+ }
260
+ }
261
+ exports.Response = Response;
262
+ class Session {
263
+ constructor(id, defaults) {
264
+ this.disposed = false;
265
+ this.id = id;
266
+ this.defaults = defaults;
267
+ }
268
+ get closed() {
269
+ return this.disposed;
270
+ }
271
+ ensureActive() {
272
+ if (this.disposed) {
273
+ throw new types_1.RequestError("Session has been closed");
274
+ }
275
+ }
276
+ enforceBrowser(browser) {
277
+ const resolved = browser ?? this.defaults.browser;
278
+ if (resolved !== this.defaults.browser) {
279
+ throw new types_1.RequestError("Session browser cannot be changed after creation");
280
+ }
281
+ return resolved;
282
+ }
283
+ enforceProxy(proxy) {
284
+ if (proxy === undefined) {
285
+ return this.defaults.proxy;
286
+ }
287
+ if ((this.defaults.proxy ?? null) !== (proxy ?? null)) {
288
+ throw new types_1.RequestError("Session proxy cannot be changed after creation");
289
+ }
290
+ return proxy;
291
+ }
292
+ async fetch(input, init) {
293
+ this.ensureActive();
294
+ const config = {
295
+ ...(init ?? {}),
296
+ session: this,
297
+ cookieMode: "session",
298
+ };
299
+ config.browser = this.enforceBrowser(config.browser);
300
+ const proxy = this.enforceProxy(config.proxy);
301
+ if (proxy !== undefined || config.proxy !== undefined) {
302
+ if (proxy === undefined) {
303
+ delete config.proxy;
304
+ }
305
+ else {
306
+ config.proxy = proxy;
307
+ }
308
+ }
309
+ if (config.timeout === undefined && this.defaults.timeout !== undefined) {
310
+ config.timeout = this.defaults.timeout;
311
+ }
312
+ return fetch(input, config);
313
+ }
314
+ async clearCookies() {
315
+ this.ensureActive();
316
+ try {
317
+ nativeBinding.clearSession(this.id);
318
+ }
319
+ catch (error) {
320
+ throw new types_1.RequestError(String(error));
321
+ }
322
+ }
323
+ async close() {
324
+ if (this.disposed) {
325
+ return;
326
+ }
327
+ this.disposed = true;
328
+ try {
329
+ nativeBinding.dropSession(this.id);
330
+ }
331
+ catch (error) {
332
+ throw new types_1.RequestError(String(error));
333
+ }
334
+ }
335
+ }
336
+ exports.Session = Session;
337
+ function resolveSessionContext(config) {
338
+ const requestedMode = config.cookieMode ?? "ephemeral";
339
+ const sessionCandidate = config.session;
340
+ const providedSessionId = typeof config.sessionId === "string" ? config.sessionId.trim() : undefined;
341
+ if (sessionCandidate && providedSessionId) {
342
+ throw new types_1.RequestError("Provide either `session` or `sessionId`, not both.");
343
+ }
344
+ if (sessionCandidate) {
345
+ if (!(sessionCandidate instanceof Session)) {
346
+ throw new types_1.RequestError("`session` must be created via createSession()");
347
+ }
348
+ if (sessionCandidate.closed) {
349
+ throw new types_1.RequestError("Session has been closed");
350
+ }
351
+ return {
352
+ sessionId: sessionCandidate.id,
353
+ cookieMode: "session",
354
+ dropAfterRequest: false,
355
+ };
356
+ }
357
+ if (providedSessionId) {
358
+ if (!providedSessionId) {
359
+ throw new types_1.RequestError("sessionId must not be empty");
360
+ }
361
+ if (requestedMode === "ephemeral") {
362
+ throw new types_1.RequestError("cookieMode 'ephemeral' cannot be combined with sessionId");
363
+ }
364
+ return {
365
+ sessionId: providedSessionId,
366
+ cookieMode: "session",
367
+ dropAfterRequest: false,
368
+ };
369
+ }
370
+ if (requestedMode === "session") {
371
+ throw new types_1.RequestError("cookieMode 'session' requires a session or sessionId");
372
+ }
373
+ return {
374
+ sessionId: generateSessionId(),
375
+ cookieMode: "ephemeral",
376
+ dropAfterRequest: true,
377
+ };
378
+ }
379
+ function createAbortError(reason) {
380
+ const fallbackMessage = typeof reason === "string" ? reason : "The operation was aborted";
381
+ if (typeof DOMException !== "undefined" && reason instanceof DOMException) {
382
+ return reason.name === "AbortError" ? reason : new DOMException(reason.message || fallbackMessage, "AbortError");
383
+ }
384
+ if (reason instanceof Error) {
385
+ reason.name = "AbortError";
386
+ return reason;
387
+ }
388
+ if (typeof DOMException !== "undefined") {
389
+ return new DOMException(fallbackMessage, "AbortError");
390
+ }
391
+ const error = new Error(fallbackMessage);
392
+ error.name = "AbortError";
393
+ return error;
394
+ }
395
+ function isAbortError(error) {
396
+ return Boolean(error) && typeof error.name === "string" && error.name === "AbortError";
397
+ }
398
+ function setupAbort(signal) {
399
+ if (!signal) {
400
+ return null;
401
+ }
402
+ if (signal.aborted) {
403
+ throw createAbortError(signal.reason);
404
+ }
405
+ let onAbort;
406
+ const promise = new Promise((_, reject) => {
407
+ onAbort = () => {
408
+ reject(createAbortError(signal.reason));
409
+ };
410
+ signal.addEventListener("abort", onAbort, { once: true });
411
+ });
412
+ const cleanup = () => {
413
+ if (onAbort) {
414
+ signal.removeEventListener("abort", onAbort);
415
+ onAbort = undefined;
416
+ }
417
+ };
418
+ return { promise, cleanup };
419
+ }
420
+ function normalizeUrlInput(input) {
421
+ const value = typeof input === "string" ? input : input.toString();
422
+ if (!value) {
423
+ throw new types_1.RequestError("URL is required");
424
+ }
425
+ try {
426
+ return new URL(value).toString();
427
+ }
428
+ catch {
429
+ throw new types_1.RequestError(`Invalid URL: ${value}`);
430
+ }
431
+ }
432
+ function validateRedirectMode(mode) {
433
+ if (!mode || mode === "follow") {
434
+ return;
435
+ }
436
+ throw new types_1.RequestError(`Redirect mode '${mode}' is not supported`);
437
+ }
438
+ function serializeBody(body) {
439
+ if (body === null || body === undefined) {
440
+ return undefined;
441
+ }
442
+ if (typeof body === "string") {
443
+ return body;
444
+ }
445
+ if (Buffer.isBuffer(body)) {
446
+ return body.toString();
447
+ }
448
+ if (body instanceof URLSearchParams) {
449
+ return body.toString();
450
+ }
451
+ if (body instanceof ArrayBuffer) {
452
+ return Buffer.from(body).toString();
453
+ }
454
+ if (ArrayBuffer.isView(body)) {
455
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString();
456
+ }
457
+ throw new TypeError("Unsupported body type; expected string, Buffer, ArrayBuffer, or URLSearchParams");
458
+ }
459
+ const SUPPORTED_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"];
460
+ function ensureMethod(method) {
461
+ const normalized = method?.trim().toUpperCase();
462
+ return normalized && normalized.length > 0 ? normalized : "GET";
463
+ }
464
+ function assertSupportedMethod(method) {
465
+ if (!SUPPORTED_METHODS.includes(method)) {
466
+ throw new types_1.RequestError(`Unsupported HTTP method: ${method}`);
467
+ }
468
+ }
469
+ function ensureBodyAllowed(method, body) {
470
+ if (!body) {
471
+ return;
472
+ }
473
+ if (method === "GET" || method === "HEAD") {
474
+ throw new types_1.RequestError(`Request with ${method} method cannot have a body`);
475
+ }
476
+ }
477
+ function validateBrowserProfile(browser) {
478
+ if (!browser) {
479
+ return;
480
+ }
481
+ const profiles = getProfiles();
482
+ if (!profiles.includes(browser)) {
483
+ throw new types_1.RequestError(`Invalid browser profile: ${browser}. Available profiles: ${profiles.join(", ")}`);
484
+ }
485
+ }
486
+ async function dispatchRequest(options, requestUrl, signal) {
487
+ const abortHandler = setupAbort(signal);
488
+ const nativePromise = nativeBinding.request(options);
489
+ const pending = abortHandler ? Promise.race([nativePromise, abortHandler.promise]) : nativePromise;
490
+ let payload;
491
+ try {
492
+ payload = (await pending);
493
+ }
494
+ catch (error) {
495
+ if (isAbortError(error)) {
496
+ throw error;
497
+ }
498
+ if (error instanceof types_1.RequestError) {
499
+ throw error;
500
+ }
501
+ throw new types_1.RequestError(String(error));
502
+ }
503
+ finally {
504
+ abortHandler?.cleanup();
505
+ }
506
+ return new Response(payload, requestUrl);
507
+ }
58
508
  /**
59
- * Make an HTTP request with browser impersonation
509
+ * Fetch-compatible entry point that adds browser impersonation controls.
60
510
  *
61
- * @param options - Request options
62
- * @returns Promise that resolves to the response
63
- *
64
- * @example
65
- * ```typescript
66
- * import { request } from 'wreq-js';
67
- *
68
- * const response = await request({
69
- * url: 'https://example.com/api',
70
- * browser: 'chrome_137',
71
- * headers: {
72
- * 'Custom-Header': 'value'
73
- * }
74
- * });
75
- *
76
- * console.log(response.status); // 200
77
- * console.log(response.body); // Response body
78
- * ```
511
+ * @param input - Request URL (string or URL instance)
512
+ * @param init - Fetch-compatible init options
79
513
  */
80
- async function request(options) {
81
- if (!options.url) {
82
- throw new types_1.RequestError("URL is required");
514
+ async function fetch(input, init) {
515
+ const url = normalizeUrlInput(input);
516
+ const config = init ?? {};
517
+ const sessionContext = resolveSessionContext(config);
518
+ validateRedirectMode(config.redirect);
519
+ validateBrowserProfile(config.browser);
520
+ const headers = new Headers(config.headers);
521
+ const method = ensureMethod(config.method);
522
+ assertSupportedMethod(method);
523
+ const body = serializeBody(config.body ?? null);
524
+ ensureBodyAllowed(method, body);
525
+ const headerRecord = headers.toObject();
526
+ const hasHeaders = Object.keys(headerRecord).length > 0;
527
+ const requestOptions = {
528
+ url,
529
+ method,
530
+ ...(config.browser && { browser: config.browser }),
531
+ ...(hasHeaders && { headers: headerRecord }),
532
+ ...(body !== undefined && { body }),
533
+ ...(config.proxy !== undefined && { proxy: config.proxy }),
534
+ ...(config.timeout !== undefined && { timeout: config.timeout }),
535
+ ...(config.disableDefaultHeaders !== undefined && { disableDefaultHeaders: config.disableDefaultHeaders }),
536
+ sessionId: sessionContext.sessionId,
537
+ ephemeral: sessionContext.dropAfterRequest,
538
+ };
539
+ try {
540
+ return await dispatchRequest(requestOptions, url, config.signal ?? null);
83
541
  }
84
- if (options.browser) {
85
- const profiles = getProfiles();
86
- if (!profiles.includes(options.browser)) {
87
- throw new types_1.RequestError(`Invalid browser profile: ${options.browser}. Available profiles: ${profiles.join(", ")}`);
542
+ finally {
543
+ if (sessionContext.dropAfterRequest) {
544
+ try {
545
+ nativeBinding.dropSession(sessionContext.sessionId);
546
+ }
547
+ catch {
548
+ // ignore cleanup errors for ephemeral sessions
549
+ }
88
550
  }
89
551
  }
552
+ }
553
+ async function createSession(options) {
554
+ const { sessionId, defaults } = normalizeSessionOptions(options);
555
+ validateBrowserProfile(defaults.browser);
556
+ let createdId;
90
557
  try {
91
- return await nativeBinding.request(options);
558
+ createdId = nativeBinding.createSession({
559
+ sessionId,
560
+ browser: defaults.browser,
561
+ ...(defaults.proxy !== undefined && { proxy: defaults.proxy }),
562
+ });
92
563
  }
93
564
  catch (error) {
94
565
  throw new types_1.RequestError(String(error));
95
566
  }
567
+ return new Session(createdId, defaults);
568
+ }
569
+ async function withSession(fn, options) {
570
+ const session = await createSession(options);
571
+ try {
572
+ return await fn(session);
573
+ }
574
+ finally {
575
+ await session.close();
576
+ }
577
+ }
578
+ /**
579
+ * @deprecated Use {@link fetch} instead.
580
+ */
581
+ async function request(options) {
582
+ if (!options.url) {
583
+ throw new types_1.RequestError("URL is required");
584
+ }
585
+ const { url, ...rest } = options;
586
+ const init = {};
587
+ if (rest.method !== undefined) {
588
+ init.method = rest.method;
589
+ }
590
+ if (rest.headers !== undefined) {
591
+ init.headers = rest.headers;
592
+ }
593
+ if (rest.body !== undefined) {
594
+ init.body = rest.body;
595
+ }
596
+ if (rest.browser !== undefined) {
597
+ init.browser = rest.browser;
598
+ }
599
+ if (rest.proxy !== undefined) {
600
+ init.proxy = rest.proxy;
601
+ }
602
+ if (rest.timeout !== undefined) {
603
+ init.timeout = rest.timeout;
604
+ }
605
+ if (rest.sessionId !== undefined) {
606
+ init.sessionId = rest.sessionId;
607
+ }
608
+ if (rest.disableDefaultHeaders !== undefined) {
609
+ init.disableDefaultHeaders = rest.disableDefaultHeaders;
610
+ }
611
+ return fetch(url, init);
96
612
  }
97
613
  /**
98
614
  * Get list of available browser profiles
@@ -114,43 +630,21 @@ function getProfiles() {
114
630
  return cachedProfiles;
115
631
  }
116
632
  /**
117
- * Convenience function for GET requests
118
- *
119
- * @param url - URL to request
120
- * @param options - Additional request options
121
- * @returns Promise that resolves to the response
122
- *
123
- * @example
124
- * ```typescript
125
- * import { get } from 'wreq-js';
126
- *
127
- * const response = await get('https://example.com/api');
128
- * ```
633
+ * Convenience helper for GET requests using {@link fetch}.
129
634
  */
130
- async function get(url, options) {
131
- return request({ ...options, url, method: "GET" });
635
+ async function get(url, init) {
636
+ return fetch(url, { ...(init ?? {}), method: "GET" });
132
637
  }
133
638
  /**
134
- * Convenience function for POST requests
135
- *
136
- * @param url - URL to request
137
- * @param body - Request body
138
- * @param options - Additional request options
139
- * @returns Promise that resolves to the response
140
- *
141
- * @example
142
- * ```typescript
143
- * import { post } from 'wreq-js';
144
- *
145
- * const response = await post(
146
- * 'https://example.com/api',
147
- * JSON.stringify({ foo: 'bar' }),
148
- * { headers: { 'Content-Type': 'application/json' } }
149
- * );
150
- * ```
639
+ * Convenience helper for POST requests using {@link fetch}.
151
640
  */
152
- async function post(url, body, options) {
153
- return request({ ...options, url, method: "POST", ...(body !== undefined && { body }) });
641
+ async function post(url, body, init) {
642
+ const config = {
643
+ ...(init ?? {}),
644
+ method: "POST",
645
+ ...(body !== undefined ? { body } : {}),
646
+ };
647
+ return fetch(url, config);
154
648
  }
155
649
  /**
156
650
  * WebSocket connection class
@@ -161,7 +655,7 @@ async function post(url, body, options) {
161
655
  *
162
656
  * const ws = await websocket({
163
657
  * url: 'wss://echo.websocket.org',
164
- * browser: 'chrome_137',
658
+ * browser: 'chrome_142',
165
659
  * onMessage: (data) => {
166
660
  * console.log('Received:', data);
167
661
  * },
@@ -246,7 +740,7 @@ async function websocket(options) {
246
740
  try {
247
741
  const connection = await nativeBinding.websocketConnect({
248
742
  url: options.url,
249
- browser: options.browser || "chrome_137",
743
+ browser: options.browser || DEFAULT_BROWSER,
250
744
  headers: options.headers || {},
251
745
  ...(options.proxy !== undefined && { proxy: options.proxy }),
252
746
  onMessage: options.onMessage,
@@ -260,11 +754,17 @@ async function websocket(options) {
260
754
  }
261
755
  }
262
756
  exports.default = {
757
+ fetch,
263
758
  request,
264
759
  get,
265
760
  post,
266
761
  getProfiles,
762
+ createSession,
763
+ withSession,
267
764
  websocket,
268
765
  WebSocket,
766
+ Headers,
767
+ Response,
768
+ Session,
269
769
  };
270
770
  //# sourceMappingURL=wreq-js.js.map