jiren 1.1.1 → 1.2.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/README.md +504 -40
- package/components/cache.ts +181 -0
- package/components/client.ts +610 -75
- package/components/index.ts +12 -2
- package/components/native.ts +21 -0
- package/components/types.ts +129 -4
- package/components/worker.ts +6 -1
- package/lib/libcurl-impersonate.dylib +0 -0
- package/lib/libhttpclient.dylib +0 -0
- package/lib/libidn2.0.dylib +0 -0
- package/lib/libintl.8.dylib +0 -0
- package/lib/libunistring.5.dylib +0 -0
- package/lib/libzstd.1.5.7.dylib +0 -0
- package/package.json +1 -1
- package/types/index.ts +6 -0
package/components/client.ts
CHANGED
|
@@ -1,23 +1,347 @@
|
|
|
1
1
|
import { CString, toArrayBuffer, type Pointer } from "bun:ffi";
|
|
2
2
|
import { lib } from "./native";
|
|
3
|
-
import
|
|
3
|
+
import { ResponseCache } from "./cache";
|
|
4
|
+
import type {
|
|
5
|
+
RequestOptions,
|
|
6
|
+
JirenResponse,
|
|
7
|
+
JirenResponseBody,
|
|
8
|
+
WarmupUrlConfig,
|
|
9
|
+
UrlRequestOptions,
|
|
10
|
+
UrlEndpoint,
|
|
11
|
+
CacheConfig,
|
|
12
|
+
} from "./types";
|
|
4
13
|
|
|
5
|
-
|
|
14
|
+
const STATUS_TEXT: Record<number, string> = {
|
|
15
|
+
200: "OK",
|
|
16
|
+
201: "Created",
|
|
17
|
+
204: "No Content",
|
|
18
|
+
301: "Moved Permanently",
|
|
19
|
+
302: "Found",
|
|
20
|
+
400: "Bad Request",
|
|
21
|
+
401: "Unauthorized",
|
|
22
|
+
403: "Forbidden",
|
|
23
|
+
404: "Not Found",
|
|
24
|
+
500: "Internal Server Error",
|
|
25
|
+
502: "Bad Gateway",
|
|
26
|
+
503: "Service Unavailable",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** URL configuration with optional cache */
|
|
30
|
+
export type UrlConfig = string | { url: string; cache?: boolean | CacheConfig };
|
|
31
|
+
|
|
32
|
+
/** Options for JirenClient constructor */
|
|
33
|
+
export interface JirenClientOptions<
|
|
34
|
+
T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig> =
|
|
35
|
+
| readonly WarmupUrlConfig[]
|
|
36
|
+
| Record<string, UrlConfig>
|
|
37
|
+
> {
|
|
6
38
|
/** URLs to warmup on client creation (pre-connect + handshake) */
|
|
7
|
-
warmup?: string[];
|
|
39
|
+
warmup?: string[] | T;
|
|
40
|
+
|
|
41
|
+
/** Enable benchmark mode (Force HTTP/2, disable probing) */
|
|
42
|
+
benchmark?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Helper to extract keys from Warmup Config */
|
|
46
|
+
export type ExtractWarmupKeys<
|
|
47
|
+
T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig>
|
|
48
|
+
> = T extends readonly WarmupUrlConfig[]
|
|
49
|
+
? T[number]["key"]
|
|
50
|
+
: T extends Record<string, UrlConfig>
|
|
51
|
+
? keyof T
|
|
52
|
+
: never;
|
|
53
|
+
|
|
54
|
+
/** Type-safe URL accessor - maps keys to UrlEndpoint objects */
|
|
55
|
+
export type UrlAccessor<
|
|
56
|
+
T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig>
|
|
57
|
+
> = {
|
|
58
|
+
[K in ExtractWarmupKeys<T>]: UrlEndpoint;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Helper function to define warmup URLs with type inference.
|
|
63
|
+
* This eliminates the need for 'as const'.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const client = new JirenClient({
|
|
68
|
+
* warmup: defineUrls([
|
|
69
|
+
* { key: "google", url: "https://google.com" },
|
|
70
|
+
* ])
|
|
71
|
+
* });
|
|
72
|
+
* // OR
|
|
73
|
+
* const client = new JirenClient({
|
|
74
|
+
* warmup: {
|
|
75
|
+
* google: "https://google.com"
|
|
76
|
+
* }
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function defineUrls<const T extends readonly WarmupUrlConfig[]>(
|
|
81
|
+
urls: T
|
|
82
|
+
): T {
|
|
83
|
+
return urls;
|
|
8
84
|
}
|
|
9
85
|
|
|
10
|
-
export class JirenClient
|
|
86
|
+
export class JirenClient<
|
|
87
|
+
T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig> =
|
|
88
|
+
| readonly WarmupUrlConfig[]
|
|
89
|
+
| Record<string, UrlConfig>
|
|
90
|
+
> {
|
|
11
91
|
private ptr: Pointer | null;
|
|
92
|
+
private urlMap: Map<string, string> = new Map();
|
|
93
|
+
private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
|
|
94
|
+
new Map();
|
|
95
|
+
private cache: ResponseCache;
|
|
96
|
+
|
|
97
|
+
/** Type-safe URL accessor for warmed-up URLs */
|
|
98
|
+
public readonly url: UrlAccessor<T>;
|
|
12
99
|
|
|
13
|
-
constructor(options?: JirenClientOptions) {
|
|
100
|
+
constructor(options?: JirenClientOptions<T>) {
|
|
14
101
|
this.ptr = lib.symbols.zclient_new();
|
|
15
102
|
if (!this.ptr) throw new Error("Failed to create native client instance");
|
|
16
103
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
104
|
+
// Initialize cache
|
|
105
|
+
this.cache = new ResponseCache(100);
|
|
106
|
+
|
|
107
|
+
// Enable benchmark mode if requested
|
|
108
|
+
if (options?.benchmark) {
|
|
109
|
+
lib.symbols.zclient_set_benchmark_mode(this.ptr, true);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Process warmup URLs
|
|
113
|
+
if (options?.warmup) {
|
|
114
|
+
const urls: string[] = [];
|
|
115
|
+
const warmup = options.warmup;
|
|
116
|
+
|
|
117
|
+
if (Array.isArray(warmup)) {
|
|
118
|
+
for (const item of warmup) {
|
|
119
|
+
if (typeof item === "string") {
|
|
120
|
+
urls.push(item);
|
|
121
|
+
} else {
|
|
122
|
+
// WarmupUrlConfig with key and optional cache
|
|
123
|
+
const config = item as WarmupUrlConfig;
|
|
124
|
+
urls.push(config.url);
|
|
125
|
+
this.urlMap.set(config.key, config.url);
|
|
126
|
+
|
|
127
|
+
// Store cache config
|
|
128
|
+
if (config.cache) {
|
|
129
|
+
const cacheConfig =
|
|
130
|
+
typeof config.cache === "boolean"
|
|
131
|
+
? { enabled: true, ttl: 60000 }
|
|
132
|
+
: { enabled: true, ttl: config.cache.ttl || 60000 };
|
|
133
|
+
this.cacheConfig.set(config.key, cacheConfig);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// Record<string, UrlConfig>
|
|
139
|
+
for (const [key, urlConfig] of Object.entries(warmup)) {
|
|
140
|
+
if (typeof urlConfig === "string") {
|
|
141
|
+
// Simple string URL
|
|
142
|
+
urls.push(urlConfig);
|
|
143
|
+
this.urlMap.set(key, urlConfig);
|
|
144
|
+
} else {
|
|
145
|
+
// URL config object with cache
|
|
146
|
+
urls.push(urlConfig.url);
|
|
147
|
+
this.urlMap.set(key, urlConfig.url);
|
|
148
|
+
|
|
149
|
+
// Store cache config
|
|
150
|
+
if (urlConfig.cache) {
|
|
151
|
+
const cacheConfig =
|
|
152
|
+
typeof urlConfig.cache === "boolean"
|
|
153
|
+
? { enabled: true, ttl: 60000 }
|
|
154
|
+
: { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
|
|
155
|
+
this.cacheConfig.set(key, cacheConfig);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (urls.length > 0) {
|
|
162
|
+
this.warmup(urls);
|
|
163
|
+
}
|
|
20
164
|
}
|
|
165
|
+
|
|
166
|
+
// Create proxy for type-safe URL access
|
|
167
|
+
this.url = this.createUrlAccessor();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Creates a proxy-based URL accessor for type-safe access to warmed-up URLs.
|
|
172
|
+
*/
|
|
173
|
+
private createUrlAccessor(): UrlAccessor<T> {
|
|
174
|
+
const self = this;
|
|
175
|
+
|
|
176
|
+
return new Proxy({} as UrlAccessor<T>, {
|
|
177
|
+
get(_target, prop: string) {
|
|
178
|
+
const baseUrl = self.urlMap.get(prop);
|
|
179
|
+
if (!baseUrl) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`URL key "${prop}" not found. Available keys: ${Array.from(
|
|
182
|
+
self.urlMap.keys()
|
|
183
|
+
).join(", ")}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Helper to build full URL with optional path
|
|
188
|
+
const buildUrl = (path?: string) =>
|
|
189
|
+
path
|
|
190
|
+
? `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`
|
|
191
|
+
: baseUrl;
|
|
192
|
+
|
|
193
|
+
// Return a UrlEndpoint object with all HTTP methods
|
|
194
|
+
return {
|
|
195
|
+
get: async <R = any>(
|
|
196
|
+
options?: UrlRequestOptions
|
|
197
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
198
|
+
// Check if caching is enabled for this URL
|
|
199
|
+
const cacheConfig = self.cacheConfig.get(prop);
|
|
200
|
+
|
|
201
|
+
if (cacheConfig?.enabled) {
|
|
202
|
+
// Try to get from cache
|
|
203
|
+
const cached = self.cache.get(baseUrl, options?.path, options);
|
|
204
|
+
if (cached) {
|
|
205
|
+
return cached as any;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Make the request
|
|
210
|
+
const response = await self.request<R>(
|
|
211
|
+
"GET",
|
|
212
|
+
buildUrl(options?.path),
|
|
213
|
+
null,
|
|
214
|
+
{
|
|
215
|
+
headers: options?.headers,
|
|
216
|
+
maxRedirects: options?.maxRedirects,
|
|
217
|
+
responseType: options?.responseType,
|
|
218
|
+
antibot: options?.antibot,
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Store in cache if enabled
|
|
223
|
+
if (
|
|
224
|
+
cacheConfig?.enabled &&
|
|
225
|
+
typeof response === "object" &&
|
|
226
|
+
"status" in response
|
|
227
|
+
) {
|
|
228
|
+
self.cache.set(
|
|
229
|
+
baseUrl,
|
|
230
|
+
response as JirenResponse,
|
|
231
|
+
cacheConfig.ttl,
|
|
232
|
+
options?.path,
|
|
233
|
+
options
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return response;
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
post: async <R = any>(
|
|
241
|
+
body?: string | null,
|
|
242
|
+
options?: UrlRequestOptions
|
|
243
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
244
|
+
return self.request<R>(
|
|
245
|
+
"POST",
|
|
246
|
+
buildUrl(options?.path),
|
|
247
|
+
body || null,
|
|
248
|
+
{
|
|
249
|
+
headers: options?.headers,
|
|
250
|
+
maxRedirects: options?.maxRedirects,
|
|
251
|
+
responseType: options?.responseType,
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
put: async <R = any>(
|
|
257
|
+
body?: string | null,
|
|
258
|
+
options?: UrlRequestOptions
|
|
259
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
260
|
+
return self.request<R>(
|
|
261
|
+
"PUT",
|
|
262
|
+
buildUrl(options?.path),
|
|
263
|
+
body || null,
|
|
264
|
+
{
|
|
265
|
+
headers: options?.headers,
|
|
266
|
+
maxRedirects: options?.maxRedirects,
|
|
267
|
+
responseType: options?.responseType,
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
patch: async <R = any>(
|
|
273
|
+
body?: string | null,
|
|
274
|
+
options?: UrlRequestOptions
|
|
275
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
276
|
+
return self.request<R>(
|
|
277
|
+
"PATCH",
|
|
278
|
+
buildUrl(options?.path),
|
|
279
|
+
body || null,
|
|
280
|
+
{
|
|
281
|
+
headers: options?.headers,
|
|
282
|
+
maxRedirects: options?.maxRedirects,
|
|
283
|
+
responseType: options?.responseType,
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
delete: async <R = any>(
|
|
289
|
+
body?: string | null,
|
|
290
|
+
options?: UrlRequestOptions
|
|
291
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
292
|
+
return self.request<R>(
|
|
293
|
+
"DELETE",
|
|
294
|
+
buildUrl(options?.path),
|
|
295
|
+
body || null,
|
|
296
|
+
{
|
|
297
|
+
headers: options?.headers,
|
|
298
|
+
maxRedirects: options?.maxRedirects,
|
|
299
|
+
responseType: options?.responseType,
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
head: async (
|
|
305
|
+
options?: UrlRequestOptions
|
|
306
|
+
): Promise<JirenResponse<any>> => {
|
|
307
|
+
return self.request("HEAD", buildUrl(options?.path), null, {
|
|
308
|
+
headers: options?.headers,
|
|
309
|
+
maxRedirects: options?.maxRedirects,
|
|
310
|
+
antibot: options?.antibot,
|
|
311
|
+
});
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
options: async (
|
|
315
|
+
options?: UrlRequestOptions
|
|
316
|
+
): Promise<JirenResponse<any>> => {
|
|
317
|
+
return self.request("OPTIONS", buildUrl(options?.path), null, {
|
|
318
|
+
headers: options?.headers,
|
|
319
|
+
maxRedirects: options?.maxRedirects,
|
|
320
|
+
antibot: options?.antibot,
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Prefetch/refresh cache for this URL
|
|
326
|
+
* Clears existing cache and makes a fresh request
|
|
327
|
+
*/
|
|
328
|
+
prefetch: async (options?: UrlRequestOptions): Promise<void> => {
|
|
329
|
+
// Clear cache for this URL
|
|
330
|
+
self.cache.clear(baseUrl);
|
|
331
|
+
|
|
332
|
+
// Make fresh request to populate cache
|
|
333
|
+
const cacheConfig = self.cacheConfig.get(prop);
|
|
334
|
+
if (cacheConfig?.enabled) {
|
|
335
|
+
await self.request("GET", buildUrl(options?.path), null, {
|
|
336
|
+
headers: options?.headers,
|
|
337
|
+
maxRedirects: options?.maxRedirects,
|
|
338
|
+
antibot: options?.antibot,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
} as UrlEndpoint;
|
|
343
|
+
},
|
|
344
|
+
});
|
|
21
345
|
}
|
|
22
346
|
|
|
23
347
|
/**
|
|
@@ -32,17 +356,24 @@ export class JirenClient {
|
|
|
32
356
|
}
|
|
33
357
|
|
|
34
358
|
/**
|
|
35
|
-
* Warm up connections to URLs (DNS resolve + QUIC handshake).
|
|
359
|
+
* Warm up connections to URLs (DNS resolve + QUIC handshake) in parallel.
|
|
36
360
|
* Call this early (e.g., at app startup) so subsequent requests are fast.
|
|
37
361
|
* @param urls - List of URLs to warm up
|
|
38
362
|
*/
|
|
39
|
-
public warmup(urls: string[]): void {
|
|
363
|
+
public async warmup(urls: string[]): Promise<void> {
|
|
40
364
|
if (!this.ptr) throw new Error("Client is closed");
|
|
41
365
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
366
|
+
// Warm up all URLs in parallel for faster startup
|
|
367
|
+
await Promise.all(
|
|
368
|
+
urls.map(
|
|
369
|
+
(url) =>
|
|
370
|
+
new Promise<void>((resolve) => {
|
|
371
|
+
const urlBuffer = Buffer.from(url + "\0");
|
|
372
|
+
lib.symbols.zclient_prefetch(this.ptr, urlBuffer);
|
|
373
|
+
resolve();
|
|
374
|
+
})
|
|
375
|
+
)
|
|
376
|
+
);
|
|
46
377
|
}
|
|
47
378
|
|
|
48
379
|
/**
|
|
@@ -58,27 +389,67 @@ export class JirenClient {
|
|
|
58
389
|
* @param url - The URL to request
|
|
59
390
|
* @param body - The body content string (optional)
|
|
60
391
|
* @param options - Request options (headers, maxRedirects, etc.) or just headers map
|
|
61
|
-
* @returns Promise resolving to Response object
|
|
392
|
+
* @returns Promise resolving to Response object or parsed body
|
|
62
393
|
*/
|
|
394
|
+
public async request<T = any>(
|
|
395
|
+
method: string,
|
|
396
|
+
url: string,
|
|
397
|
+
body?: string | null,
|
|
398
|
+
options?: RequestOptions & { responseType: "json" }
|
|
399
|
+
): Promise<T>;
|
|
400
|
+
public async request<T = any>(
|
|
401
|
+
method: string,
|
|
402
|
+
url: string,
|
|
403
|
+
body?: string | null,
|
|
404
|
+
options?: RequestOptions & { responseType: "text" }
|
|
405
|
+
): Promise<string>;
|
|
406
|
+
public async request<T = any>(
|
|
407
|
+
method: string,
|
|
408
|
+
url: string,
|
|
409
|
+
body?: string | null,
|
|
410
|
+
options?: RequestOptions & { responseType: "arraybuffer" }
|
|
411
|
+
): Promise<ArrayBuffer>;
|
|
412
|
+
public async request<T = any>(
|
|
413
|
+
method: string,
|
|
414
|
+
url: string,
|
|
415
|
+
body?: string | null,
|
|
416
|
+
options?: RequestOptions & { responseType: "blob" }
|
|
417
|
+
): Promise<Blob>;
|
|
418
|
+
public async request<T = any>(
|
|
419
|
+
method: string,
|
|
420
|
+
url: string,
|
|
421
|
+
body?: string | null,
|
|
422
|
+
options?: RequestOptions
|
|
423
|
+
): Promise<JirenResponse<T>>;
|
|
63
424
|
public async request<T = any>(
|
|
64
425
|
method: string,
|
|
65
426
|
url: string,
|
|
66
427
|
body?: string | null,
|
|
67
428
|
options?: RequestOptions | Record<string, string> | null
|
|
68
|
-
) {
|
|
429
|
+
): Promise<JirenResponse<T> | T | string | ArrayBuffer | Blob> {
|
|
69
430
|
if (!this.ptr) throw new Error("Client is closed");
|
|
70
431
|
|
|
71
432
|
// Normalize options
|
|
72
433
|
let headers: Record<string, string> = {};
|
|
73
434
|
let maxRedirects = 5; // Default
|
|
435
|
+
let responseType: RequestOptions["responseType"] | undefined;
|
|
436
|
+
let antibot = false; // Default
|
|
74
437
|
|
|
75
438
|
if (options) {
|
|
76
|
-
if (
|
|
439
|
+
if (
|
|
440
|
+
"maxRedirects" in options ||
|
|
441
|
+
"headers" in options ||
|
|
442
|
+
"responseType" in options ||
|
|
443
|
+
"method" in options || // Check for any RequestOptions specific key
|
|
444
|
+
"timeout" in options ||
|
|
445
|
+
"antibot" in options
|
|
446
|
+
) {
|
|
77
447
|
// It is RequestOptions
|
|
78
448
|
const opts = options as RequestOptions;
|
|
79
449
|
if (opts.headers) headers = opts.headers;
|
|
80
450
|
if (opts.maxRedirects !== undefined) maxRedirects = opts.maxRedirects;
|
|
81
|
-
|
|
451
|
+
if (opts.responseType) responseType = opts.responseType;
|
|
452
|
+
if (opts.antibot !== undefined) antibot = opts.antibot;
|
|
82
453
|
} else {
|
|
83
454
|
// Assume it's just headers Record<string, string> for backward compatibility
|
|
84
455
|
headers = options as Record<string, string>;
|
|
@@ -99,13 +470,52 @@ export class JirenClient {
|
|
|
99
470
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
100
471
|
accept:
|
|
101
472
|
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
102
|
-
"accept-encoding": "gzip
|
|
473
|
+
"accept-encoding": "gzip",
|
|
103
474
|
"accept-language": "en-US,en;q=0.9",
|
|
475
|
+
"sec-ch-ua":
|
|
476
|
+
'"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
|
477
|
+
"sec-ch-ua-mobile": "?0",
|
|
478
|
+
"sec-ch-ua-platform": '"macOS"',
|
|
479
|
+
"sec-fetch-dest": "document",
|
|
480
|
+
"sec-fetch-mode": "navigate",
|
|
481
|
+
"sec-fetch-site": "none",
|
|
482
|
+
"sec-fetch-user": "?1",
|
|
483
|
+
"upgrade-insecure-requests": "1",
|
|
104
484
|
};
|
|
105
485
|
|
|
106
486
|
const finalHeaders = { ...defaultHeaders, ...headers };
|
|
107
487
|
|
|
108
|
-
|
|
488
|
+
// Enforce Chrome header order
|
|
489
|
+
const orderedHeaders: Record<string, string> = {};
|
|
490
|
+
const keys = [
|
|
491
|
+
"sec-ch-ua",
|
|
492
|
+
"sec-ch-ua-mobile",
|
|
493
|
+
"sec-ch-ua-platform",
|
|
494
|
+
"upgrade-insecure-requests",
|
|
495
|
+
"user-agent",
|
|
496
|
+
"accept",
|
|
497
|
+
"sec-fetch-site",
|
|
498
|
+
"sec-fetch-mode",
|
|
499
|
+
"sec-fetch-user",
|
|
500
|
+
"sec-fetch-dest",
|
|
501
|
+
"accept-encoding",
|
|
502
|
+
"accept-language",
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
// Add priority headers in order
|
|
506
|
+
for (const key of keys) {
|
|
507
|
+
if (finalHeaders[key]) {
|
|
508
|
+
orderedHeaders[key] = finalHeaders[key];
|
|
509
|
+
delete finalHeaders[key];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Add remaining custom headers
|
|
514
|
+
for (const [key, value] of Object.entries(finalHeaders)) {
|
|
515
|
+
orderedHeaders[key] = value;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const headerStr = Object.entries(orderedHeaders)
|
|
109
519
|
.map(([k, v]) => `${k.toLowerCase()}: ${v}`)
|
|
110
520
|
.join("\r\n");
|
|
111
521
|
|
|
@@ -119,60 +529,27 @@ export class JirenClient {
|
|
|
119
529
|
urlBuffer,
|
|
120
530
|
headersBuffer,
|
|
121
531
|
bodyBuffer,
|
|
122
|
-
maxRedirects
|
|
532
|
+
maxRedirects,
|
|
533
|
+
antibot
|
|
123
534
|
);
|
|
124
535
|
|
|
125
|
-
|
|
126
|
-
}
|
|
536
|
+
const response = this.parseResponse<T>(respPtr, url);
|
|
127
537
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
public async post<T = any>(
|
|
136
|
-
url: string,
|
|
137
|
-
body: string,
|
|
138
|
-
options?: RequestOptions | Record<string, string>
|
|
139
|
-
) {
|
|
140
|
-
return this.request<T>("POST", url, body, options);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
public async put<T = any>(
|
|
144
|
-
url: string,
|
|
145
|
-
body: string,
|
|
146
|
-
options?: RequestOptions | Record<string, string>
|
|
147
|
-
) {
|
|
148
|
-
return this.request<T>("PUT", url, body, options);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
public async patch<T = any>(
|
|
152
|
-
url: string,
|
|
153
|
-
body: string,
|
|
154
|
-
options?: RequestOptions | Record<string, string>
|
|
155
|
-
) {
|
|
156
|
-
return this.request<T>("PATCH", url, body, options);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
public async delete<T = any>(
|
|
160
|
-
url: string,
|
|
161
|
-
body?: string,
|
|
162
|
-
options?: RequestOptions | Record<string, string>
|
|
163
|
-
) {
|
|
164
|
-
return this.request<T>("DELETE", url, body || null, options);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
public async head(url: string, headers?: Record<string, string>) {
|
|
168
|
-
return this.request("HEAD", url, null, headers);
|
|
169
|
-
}
|
|
538
|
+
// Auto-parse if requested
|
|
539
|
+
if (responseType) {
|
|
540
|
+
if (responseType === "json") return response.body.json();
|
|
541
|
+
if (responseType === "text") return response.body.text();
|
|
542
|
+
if (responseType === "arraybuffer") return response.body.arrayBuffer();
|
|
543
|
+
if (responseType === "blob") return response.body.blob();
|
|
544
|
+
}
|
|
170
545
|
|
|
171
|
-
|
|
172
|
-
return this.request("OPTIONS", url, null, headers);
|
|
546
|
+
return response;
|
|
173
547
|
}
|
|
174
548
|
|
|
175
|
-
private parseResponse<T = any>(
|
|
549
|
+
private parseResponse<T = any>(
|
|
550
|
+
respPtr: Pointer | null,
|
|
551
|
+
url: string
|
|
552
|
+
): JirenResponse<T> {
|
|
176
553
|
if (!respPtr)
|
|
177
554
|
throw new Error("Native request failed (returned null pointer)");
|
|
178
555
|
|
|
@@ -181,20 +558,178 @@ export class JirenClient {
|
|
|
181
558
|
const len = Number(lib.symbols.zclient_response_body_len(respPtr));
|
|
182
559
|
const bodyPtr = lib.symbols.zclient_response_body(respPtr);
|
|
183
560
|
|
|
184
|
-
|
|
561
|
+
const headersLen = Number(
|
|
562
|
+
lib.symbols.zclient_response_headers_len(respPtr)
|
|
563
|
+
);
|
|
564
|
+
let headersObj: Record<string, string> | NativeHeaders = {};
|
|
565
|
+
|
|
566
|
+
if (headersLen > 0) {
|
|
567
|
+
const rawHeadersPtr = lib.symbols.zclient_response_headers(respPtr);
|
|
568
|
+
if (rawHeadersPtr) {
|
|
569
|
+
// Copy headers to JS memory
|
|
570
|
+
// We need to copy because respPtr will be freed
|
|
571
|
+
const rawSrc = toArrayBuffer(rawHeadersPtr, 0, headersLen);
|
|
572
|
+
const raw = new Uint8Array(rawSrc.slice(0)); // Explicit copy
|
|
573
|
+
|
|
574
|
+
headersObj = new NativeHeaders(raw);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Proxy for backward compatibility
|
|
579
|
+
const headersProxy = new Proxy(
|
|
580
|
+
headersObj instanceof NativeHeaders ? headersObj : {},
|
|
581
|
+
{
|
|
582
|
+
get(target, prop) {
|
|
583
|
+
if (target instanceof NativeHeaders && typeof prop === "string") {
|
|
584
|
+
if (prop === "toJSON") return () => target.toJSON();
|
|
585
|
+
|
|
586
|
+
// Try to get from native headers
|
|
587
|
+
const val = target.get(prop);
|
|
588
|
+
if (val !== null) return val;
|
|
589
|
+
}
|
|
590
|
+
return Reflect.get(target, prop);
|
|
591
|
+
},
|
|
592
|
+
}
|
|
593
|
+
) as unknown as Record<string, string>; // Lie to TS
|
|
594
|
+
|
|
595
|
+
let buffer: ArrayBuffer | Buffer = new ArrayBuffer(0);
|
|
185
596
|
if (len > 0 && bodyPtr) {
|
|
186
|
-
|
|
187
|
-
|
|
597
|
+
// Create a copy of the buffer because the native response is freed immediately after
|
|
598
|
+
buffer = toArrayBuffer(bodyPtr, 0, len).slice(0);
|
|
188
599
|
}
|
|
189
600
|
|
|
601
|
+
let bodyUsed = false;
|
|
602
|
+
const consumeBody = () => {
|
|
603
|
+
if (bodyUsed) {
|
|
604
|
+
}
|
|
605
|
+
bodyUsed = true;
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const bodyObj: JirenResponseBody<T> = {
|
|
609
|
+
bodyUsed: false,
|
|
610
|
+
arrayBuffer: async () => {
|
|
611
|
+
consumeBody();
|
|
612
|
+
if (Buffer.isBuffer(buffer)) {
|
|
613
|
+
const buf = buffer as Buffer;
|
|
614
|
+
return buf.buffer.slice(
|
|
615
|
+
buf.byteOffset,
|
|
616
|
+
buf.byteOffset + buf.byteLength
|
|
617
|
+
) as ArrayBuffer;
|
|
618
|
+
}
|
|
619
|
+
return buffer as ArrayBuffer;
|
|
620
|
+
},
|
|
621
|
+
blob: async () => {
|
|
622
|
+
consumeBody();
|
|
623
|
+
return new Blob([buffer]);
|
|
624
|
+
},
|
|
625
|
+
text: async () => {
|
|
626
|
+
consumeBody();
|
|
627
|
+
return new TextDecoder().decode(buffer);
|
|
628
|
+
},
|
|
629
|
+
json: async <R = T>(): Promise<R> => {
|
|
630
|
+
consumeBody();
|
|
631
|
+
const text = new TextDecoder().decode(buffer);
|
|
632
|
+
return JSON.parse(text);
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Update bodyUsed getter to reflect local variable
|
|
637
|
+
Object.defineProperty(bodyObj, "bodyUsed", {
|
|
638
|
+
get: () => bodyUsed,
|
|
639
|
+
});
|
|
640
|
+
|
|
190
641
|
return {
|
|
642
|
+
url,
|
|
191
643
|
status,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
644
|
+
statusText: STATUS_TEXT[status] || "",
|
|
645
|
+
headers: headersProxy,
|
|
646
|
+
ok: status >= 200 && status < 300,
|
|
647
|
+
redirected: false,
|
|
648
|
+
type: "basic",
|
|
649
|
+
body: bodyObj,
|
|
650
|
+
} as JirenResponse<T>;
|
|
196
651
|
} finally {
|
|
197
652
|
lib.symbols.zclient_response_free(respPtr);
|
|
198
653
|
}
|
|
199
654
|
}
|
|
200
655
|
}
|
|
656
|
+
|
|
657
|
+
class NativeHeaders {
|
|
658
|
+
private raw: Uint8Array;
|
|
659
|
+
private len: number;
|
|
660
|
+
private decoder = new TextDecoder();
|
|
661
|
+
private cache = new Map<string, string>();
|
|
662
|
+
// We need a pointer to the raw buffer for FFI calls.
|
|
663
|
+
// Since we can't easily rely on ptr(this.raw) being stable if we stored it,
|
|
664
|
+
// we will pass this.raw to the FFI call directly each time.
|
|
665
|
+
|
|
666
|
+
constructor(raw: Uint8Array) {
|
|
667
|
+
this.raw = raw;
|
|
668
|
+
this.len = raw.byteLength;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
get(name: string): string | null {
|
|
672
|
+
const target = name.toLowerCase();
|
|
673
|
+
if (this.cache.has(target)) return this.cache.get(target)!;
|
|
674
|
+
|
|
675
|
+
const keyBuf = Buffer.from(target + "\0");
|
|
676
|
+
|
|
677
|
+
// Debug log
|
|
678
|
+
// Pass the raw buffer directly. Bun handles the pointer.
|
|
679
|
+
const resPtr = lib.symbols.z_find_header_value(
|
|
680
|
+
this.raw as any,
|
|
681
|
+
this.len,
|
|
682
|
+
keyBuf
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
if (!resPtr) return null;
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
// ZHeaderValue: { value_ptr: pointer, value_len: size_t }
|
|
689
|
+
// Assuming 64-bit architecture, pointers and size_t are 8 bytes.
|
|
690
|
+
// Struct size = 16 bytes.
|
|
691
|
+
const view = new DataView(toArrayBuffer(resPtr, 0, 16));
|
|
692
|
+
const valPtr = view.getBigUint64(0, true);
|
|
693
|
+
const valLen = Number(view.getBigUint64(8, true));
|
|
694
|
+
|
|
695
|
+
if (valLen === 0) {
|
|
696
|
+
this.cache.set(target, "");
|
|
697
|
+
return "";
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Convert valPtr to ArrayBuffer
|
|
701
|
+
// Note: valPtr points inside this.raw, but toArrayBuffer(ptr) creates a view on that memory.
|
|
702
|
+
const valBytes = toArrayBuffer(Number(valPtr) as any, 0, valLen);
|
|
703
|
+
const val = this.decoder.decode(valBytes);
|
|
704
|
+
|
|
705
|
+
this.cache.set(target, val);
|
|
706
|
+
return val;
|
|
707
|
+
} finally {
|
|
708
|
+
lib.symbols.zclient_header_value_free(resPtr);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Fallback for when full object is needed (e.g. debugging)
|
|
713
|
+
// This is expensive as it reparses everything using the old offset method
|
|
714
|
+
// BUT we don't have the offset method easily available on the raw buffer unless we expose a new one?
|
|
715
|
+
// Wait, `zclient_response_parse_header_offsets` takes `Response*`.
|
|
716
|
+
// We don't have Response* anymore.
|
|
717
|
+
// We need `z_parse_header_offsets_from_raw(ptr, len)`.
|
|
718
|
+
// Or just parse in JS since we have the full buffer?
|
|
719
|
+
// Actually, we can just do a JS parser since we have the buffer.
|
|
720
|
+
// It's a fallback anyway.
|
|
721
|
+
toJSON(): Record<string, string> {
|
|
722
|
+
const obj: Record<string, string> = {};
|
|
723
|
+
const text = this.decoder.decode(this.raw);
|
|
724
|
+
const lines = text.split("\r\n");
|
|
725
|
+
for (const line of lines) {
|
|
726
|
+
if (!line) continue;
|
|
727
|
+
const colon = line.indexOf(":");
|
|
728
|
+
if (colon === -1) continue;
|
|
729
|
+
const key = line.substring(0, colon).trim().toLowerCase();
|
|
730
|
+
const val = line.substring(colon + 1).trim();
|
|
731
|
+
obj[key] = val;
|
|
732
|
+
}
|
|
733
|
+
return obj;
|
|
734
|
+
}
|
|
735
|
+
}
|