@onkernel/sdk 0.48.0 → 0.51.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/CHANGELOG.md +73 -0
- package/client.d.mts +5 -2
- package/client.d.mts.map +1 -1
- package/client.d.ts +5 -2
- package/client.d.ts.map +1 -1
- package/client.js +15 -2
- package/client.js.map +1 -1
- package/client.mjs +15 -2
- package/client.mjs.map +1 -1
- package/index.d.mts +2 -0
- package/index.d.mts.map +1 -1
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +3 -1
- package/index.js.map +1 -1
- package/index.mjs +1 -0
- package/index.mjs.map +1 -1
- package/lib/browser-fetch.d.mts +7 -0
- package/lib/browser-fetch.d.mts.map +1 -0
- package/lib/browser-fetch.d.ts +7 -0
- package/lib/browser-fetch.d.ts.map +1 -0
- package/lib/browser-fetch.js +128 -0
- package/lib/browser-fetch.js.map +1 -0
- package/lib/browser-fetch.mjs +125 -0
- package/lib/browser-fetch.mjs.map +1 -0
- package/lib/browser-routing.d.mts +20 -0
- package/lib/browser-routing.d.mts.map +1 -0
- package/lib/browser-routing.d.ts +20 -0
- package/lib/browser-routing.d.ts.map +1 -0
- package/lib/browser-routing.js +246 -0
- package/lib/browser-routing.js.map +1 -0
- package/lib/browser-routing.mjs +240 -0
- package/lib/browser-routing.mjs.map +1 -0
- package/lib/join-url.d.mts +2 -0
- package/lib/join-url.d.mts.map +1 -0
- package/lib/join-url.d.ts +2 -0
- package/lib/join-url.d.ts.map +1 -0
- package/lib/join-url.js +7 -0
- package/lib/join-url.js.map +1 -0
- package/lib/join-url.mjs +4 -0
- package/lib/join-url.mjs.map +1 -0
- package/package.json +11 -1
- package/resources/auth/connections.d.mts +31 -2
- package/resources/auth/connections.d.mts.map +1 -1
- package/resources/auth/connections.d.ts +31 -2
- package/resources/auth/connections.d.ts.map +1 -1
- package/resources/browsers/browsers.d.mts +73 -2
- package/resources/browsers/browsers.d.mts.map +1 -1
- package/resources/browsers/browsers.d.ts +73 -2
- package/resources/browsers/browsers.d.ts.map +1 -1
- package/resources/browsers/browsers.js +23 -0
- package/resources/browsers/browsers.js.map +1 -1
- package/resources/browsers/browsers.mjs +23 -0
- package/resources/browsers/browsers.mjs.map +1 -1
- package/resources/browsers/index.d.mts +1 -1
- package/resources/browsers/index.d.mts.map +1 -1
- package/resources/browsers/index.d.ts +1 -1
- package/resources/browsers/index.d.ts.map +1 -1
- package/resources/browsers/index.js.map +1 -1
- package/resources/browsers/index.mjs.map +1 -1
- package/resources/browsers/replays.d.mts +2 -1
- package/resources/browsers/replays.d.mts.map +1 -1
- package/resources/browsers/replays.d.ts +2 -1
- package/resources/browsers/replays.d.ts.map +1 -1
- package/resources/index.d.mts +1 -1
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +1 -1
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js.map +1 -1
- package/resources/index.mjs.map +1 -1
- package/resources/projects/projects.d.mts +2 -2
- package/resources/projects/projects.d.ts +2 -2
- package/resources/projects/projects.js +2 -2
- package/resources/projects/projects.mjs +2 -2
- package/src/client.ts +25 -2
- package/src/index.ts +2 -0
- package/src/lib/browser-fetch.ts +180 -0
- package/src/lib/browser-routing.ts +329 -0
- package/src/lib/join-url.ts +3 -0
- package/src/resources/auth/connections.ts +35 -2
- package/src/resources/browsers/browsers.ts +86 -1
- package/src/resources/browsers/index.ts +2 -0
- package/src/resources/browsers/replays.ts +2 -1
- package/src/resources/index.ts +2 -0
- package/src/resources/projects/projects.ts +2 -2
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -10,8 +10,8 @@ import { RequestOptions } from "../../internal/request-options.mjs";
|
|
|
10
10
|
export declare class Projects extends APIResource {
|
|
11
11
|
limits: LimitsAPI.Limits;
|
|
12
12
|
/**
|
|
13
|
-
* Create a new project within the authenticated organization. Requires
|
|
14
|
-
*
|
|
13
|
+
* Create a new project within the authenticated organization. Requires the
|
|
14
|
+
* projects feature flag.
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* ```ts
|
|
@@ -10,8 +10,8 @@ import { RequestOptions } from "../../internal/request-options.js";
|
|
|
10
10
|
export declare class Projects extends APIResource {
|
|
11
11
|
limits: LimitsAPI.Limits;
|
|
12
12
|
/**
|
|
13
|
-
* Create a new project within the authenticated organization. Requires
|
|
14
|
-
*
|
|
13
|
+
* Create a new project within the authenticated organization. Requires the
|
|
14
|
+
* projects feature flag.
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* ```ts
|
|
@@ -18,8 +18,8 @@ class Projects extends resource_1.APIResource {
|
|
|
18
18
|
this.limits = new LimitsAPI.Limits(this._client);
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* Create a new project within the authenticated organization. Requires
|
|
22
|
-
*
|
|
21
|
+
* Create a new project within the authenticated organization. Requires the
|
|
22
|
+
* projects feature flag.
|
|
23
23
|
*
|
|
24
24
|
* @example
|
|
25
25
|
* ```ts
|
|
@@ -14,8 +14,8 @@ export class Projects extends APIResource {
|
|
|
14
14
|
this.limits = new LimitsAPI.Limits(this._client);
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Create a new project within the authenticated organization. Requires
|
|
18
|
-
*
|
|
17
|
+
* Create a new project within the authenticated organization. Requires the
|
|
18
|
+
* projects feature flag.
|
|
19
19
|
*
|
|
20
20
|
* @example
|
|
21
21
|
* ```ts
|
package/src/client.ts
CHANGED
|
@@ -20,6 +20,11 @@ import * as Uploads from './core/uploads';
|
|
|
20
20
|
import * as API from './resources/index';
|
|
21
21
|
import { APIPromise } from './core/api-promise';
|
|
22
22
|
import { AppListParams, AppListResponse, AppListResponsesOffsetPagination, Apps } from './resources/apps';
|
|
23
|
+
import {
|
|
24
|
+
BrowserRouteCache,
|
|
25
|
+
browserRoutingSubresourcesFromEnv,
|
|
26
|
+
createRoutingFetch,
|
|
27
|
+
} from './lib/browser-routing';
|
|
23
28
|
import {
|
|
24
29
|
BrowserPool,
|
|
25
30
|
BrowserPoolAcquireParams,
|
|
@@ -103,6 +108,8 @@ import { Auth } from './resources/auth/auth';
|
|
|
103
108
|
import {
|
|
104
109
|
BrowserCreateParams,
|
|
105
110
|
BrowserCreateResponse,
|
|
111
|
+
BrowserCurlParams,
|
|
112
|
+
BrowserCurlResponse,
|
|
106
113
|
BrowserDeleteParams,
|
|
107
114
|
BrowserListParams,
|
|
108
115
|
BrowserListResponse,
|
|
@@ -245,9 +252,11 @@ export class Kernel {
|
|
|
245
252
|
fetchOptions: MergedRequestInit | undefined;
|
|
246
253
|
|
|
247
254
|
private fetch: Fetch;
|
|
255
|
+
private rawFetch: Fetch;
|
|
248
256
|
#encoder: Opts.RequestEncoder;
|
|
249
257
|
protected idempotencyHeader?: string;
|
|
250
258
|
private _options: ClientOptions;
|
|
259
|
+
public browserRouteCache: BrowserRouteCache;
|
|
251
260
|
|
|
252
261
|
/**
|
|
253
262
|
* API Client for interfacing with the Kernel API.
|
|
@@ -310,7 +319,13 @@ export class Kernel {
|
|
|
310
319
|
defaultLogLevel;
|
|
311
320
|
this.fetchOptions = options.fetchOptions;
|
|
312
321
|
this.maxRetries = options.maxRetries ?? 2;
|
|
313
|
-
this.
|
|
322
|
+
this.rawFetch = options.fetch ?? Shims.getDefaultFetch();
|
|
323
|
+
this.browserRouteCache = new BrowserRouteCache();
|
|
324
|
+
this.fetch = createRoutingFetch(this.rawFetch, {
|
|
325
|
+
apiBaseURL: this.baseURL,
|
|
326
|
+
subresources: browserRoutingSubresourcesFromEnv(),
|
|
327
|
+
cache: this.browserRouteCache,
|
|
328
|
+
});
|
|
314
329
|
this.#encoder = Opts.FallbackEncoder;
|
|
315
330
|
|
|
316
331
|
this._options = options;
|
|
@@ -330,11 +345,17 @@ export class Kernel {
|
|
|
330
345
|
timeout: this.timeout,
|
|
331
346
|
logger: this.logger,
|
|
332
347
|
logLevel: this.logLevel,
|
|
333
|
-
fetch: this.
|
|
348
|
+
fetch: this.rawFetch,
|
|
334
349
|
fetchOptions: this.fetchOptions,
|
|
335
350
|
apiKey: this.apiKey,
|
|
336
351
|
...options,
|
|
337
352
|
});
|
|
353
|
+
client.browserRouteCache = this.browserRouteCache;
|
|
354
|
+
client.fetch = createRoutingFetch(client.rawFetch, {
|
|
355
|
+
apiBaseURL: client.baseURL,
|
|
356
|
+
subresources: browserRoutingSubresourcesFromEnv(),
|
|
357
|
+
cache: client.browserRouteCache,
|
|
358
|
+
});
|
|
338
359
|
return client;
|
|
339
360
|
}
|
|
340
361
|
|
|
@@ -1013,12 +1034,14 @@ export declare namespace Kernel {
|
|
|
1013
1034
|
type BrowserRetrieveResponse as BrowserRetrieveResponse,
|
|
1014
1035
|
type BrowserUpdateResponse as BrowserUpdateResponse,
|
|
1015
1036
|
type BrowserListResponse as BrowserListResponse,
|
|
1037
|
+
type BrowserCurlResponse as BrowserCurlResponse,
|
|
1016
1038
|
type BrowserListResponsesOffsetPagination as BrowserListResponsesOffsetPagination,
|
|
1017
1039
|
type BrowserCreateParams as BrowserCreateParams,
|
|
1018
1040
|
type BrowserRetrieveParams as BrowserRetrieveParams,
|
|
1019
1041
|
type BrowserUpdateParams as BrowserUpdateParams,
|
|
1020
1042
|
type BrowserListParams as BrowserListParams,
|
|
1021
1043
|
type BrowserDeleteParams as BrowserDeleteParams,
|
|
1044
|
+
type BrowserCurlParams as BrowserCurlParams,
|
|
1022
1045
|
type BrowserLoadExtensionsParams as BrowserLoadExtensionsParams,
|
|
1023
1046
|
};
|
|
1024
1047
|
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { Kernel as default } from './client';
|
|
|
5
5
|
export { type Uploadable, toFile } from './core/uploads';
|
|
6
6
|
export { APIPromise } from './core/api-promise';
|
|
7
7
|
export { Kernel, type ClientOptions } from './client';
|
|
8
|
+
export { type BrowserFetchInit } from './lib/browser-fetch';
|
|
9
|
+
export { BrowserRouteCache, type BrowserRoute } from './lib/browser-routing';
|
|
8
10
|
export { PagePromise } from './core/pagination';
|
|
9
11
|
export {
|
|
10
12
|
KernelError,
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { RequestInfo, RequestInit } from '../internal/builtin-types';
|
|
2
|
+
import { KernelError } from '../core/error';
|
|
3
|
+
import { buildHeaders } from '../internal/headers';
|
|
4
|
+
import type { FinalRequestOptions, RequestOptions } from '../internal/request-options';
|
|
5
|
+
import type { HTTPMethod } from '../internal/types';
|
|
6
|
+
import { joinURL } from './join-url';
|
|
7
|
+
import type { Kernel } from '../client';
|
|
8
|
+
|
|
9
|
+
export interface BrowserFetchInit extends RequestInit {
|
|
10
|
+
timeout_ms?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function browserFetch(
|
|
14
|
+
client: Kernel,
|
|
15
|
+
sessionId: string,
|
|
16
|
+
input: RequestInfo | URL,
|
|
17
|
+
init?: BrowserFetchInit,
|
|
18
|
+
): Promise<Response> {
|
|
19
|
+
const route = client.browserRouteCache.get(sessionId);
|
|
20
|
+
if (!route) {
|
|
21
|
+
throw new KernelError(
|
|
22
|
+
`browser route cache does not contain session ${sessionId}; create, retrieve, or list the browser before calling browser.fetch`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { url: targetURL, method, headers, body, signal, duplex, timeout_ms } = splitFetchArgs(input, init);
|
|
27
|
+
assertHTTPURL(targetURL);
|
|
28
|
+
|
|
29
|
+
const query: Record<string, unknown> = { url: targetURL, jwt: route.jwt };
|
|
30
|
+
if (timeout_ms !== undefined) {
|
|
31
|
+
query['timeout_ms'] = timeout_ms;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const accept = headers.get('accept');
|
|
35
|
+
const requestOptions: FinalRequestOptions = {
|
|
36
|
+
method: normalizeMethod(method),
|
|
37
|
+
path: joinURL(route.baseURL, '/curl/raw'),
|
|
38
|
+
query,
|
|
39
|
+
body: body as RequestOptions['body'],
|
|
40
|
+
headers: buildHeaders([
|
|
41
|
+
{ Authorization: null },
|
|
42
|
+
accept ? { Accept: accept } : { Accept: '*/*' },
|
|
43
|
+
headersToRequestOptionsHeaders(headers),
|
|
44
|
+
]),
|
|
45
|
+
signal: signal ?? null,
|
|
46
|
+
__binaryResponse: true,
|
|
47
|
+
};
|
|
48
|
+
if (duplex) {
|
|
49
|
+
requestOptions.fetchOptions = { duplex } as NonNullable<RequestOptions['fetchOptions']>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return client.request(requestOptions).asResponse();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeMethod(method: string): HTTPMethod {
|
|
56
|
+
const methodLower = method.toLowerCase();
|
|
57
|
+
const allowed = new Set(['get', 'post', 'put', 'patch', 'delete']);
|
|
58
|
+
if (!allowed.has(methodLower)) {
|
|
59
|
+
throw new KernelError(`browser.fetch unsupported HTTP method: ${method}`);
|
|
60
|
+
}
|
|
61
|
+
return methodLower as HTTPMethod;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function splitFetchArgs(
|
|
65
|
+
input: RequestInfo | URL,
|
|
66
|
+
init?: BrowserFetchInit,
|
|
67
|
+
): {
|
|
68
|
+
url: string;
|
|
69
|
+
method: string;
|
|
70
|
+
headers: Headers;
|
|
71
|
+
body?: RequestInit['body'];
|
|
72
|
+
signal?: AbortSignal | null;
|
|
73
|
+
duplex?: RequestInit['duplex'];
|
|
74
|
+
timeout_ms?: number;
|
|
75
|
+
} {
|
|
76
|
+
const timeoutFromInit = init && 'timeout_ms' in init ? init['timeout_ms'] : undefined;
|
|
77
|
+
|
|
78
|
+
if (input instanceof Request) {
|
|
79
|
+
const headers = new Headers(input.headers);
|
|
80
|
+
if (init?.headers) {
|
|
81
|
+
const extra = new Headers(init.headers);
|
|
82
|
+
extra.forEach((value, key) => {
|
|
83
|
+
headers.set(key, value);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const out: {
|
|
88
|
+
url: string;
|
|
89
|
+
method: string;
|
|
90
|
+
headers: Headers;
|
|
91
|
+
body?: RequestInit['body'];
|
|
92
|
+
signal?: AbortSignal | null;
|
|
93
|
+
duplex?: RequestInit['duplex'];
|
|
94
|
+
timeout_ms?: number;
|
|
95
|
+
} = {
|
|
96
|
+
url: input.url,
|
|
97
|
+
method: (init?.method ?? input.method)?.toUpperCase() || 'GET',
|
|
98
|
+
headers,
|
|
99
|
+
};
|
|
100
|
+
const body = init?.body ?? input.body;
|
|
101
|
+
if (body !== undefined && body !== null) {
|
|
102
|
+
out.body = body;
|
|
103
|
+
}
|
|
104
|
+
const signal = init?.signal ?? input.signal;
|
|
105
|
+
if (signal !== undefined) {
|
|
106
|
+
out.signal = signal;
|
|
107
|
+
}
|
|
108
|
+
if (init?.duplex !== undefined) {
|
|
109
|
+
out.duplex = init.duplex;
|
|
110
|
+
}
|
|
111
|
+
if (timeoutFromInit !== undefined) {
|
|
112
|
+
out.timeout_ms = timeoutFromInit;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const out: {
|
|
118
|
+
url: string;
|
|
119
|
+
method: string;
|
|
120
|
+
headers: Headers;
|
|
121
|
+
body?: RequestInit['body'];
|
|
122
|
+
signal?: AbortSignal | null;
|
|
123
|
+
duplex?: RequestInit['duplex'];
|
|
124
|
+
timeout_ms?: number;
|
|
125
|
+
} = {
|
|
126
|
+
url: input instanceof URL ? input.href : String(input),
|
|
127
|
+
method: (init?.method ?? 'GET').toUpperCase(),
|
|
128
|
+
headers: new Headers(init?.headers),
|
|
129
|
+
};
|
|
130
|
+
if (init?.body !== undefined) {
|
|
131
|
+
out.body = init.body;
|
|
132
|
+
}
|
|
133
|
+
if (init?.signal !== undefined) {
|
|
134
|
+
out.signal = init.signal;
|
|
135
|
+
}
|
|
136
|
+
if (init?.duplex !== undefined) {
|
|
137
|
+
out.duplex = init.duplex;
|
|
138
|
+
}
|
|
139
|
+
if (timeoutFromInit !== undefined) {
|
|
140
|
+
out.timeout_ms = timeoutFromInit;
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function assertHTTPURL(url: string): void {
|
|
146
|
+
let parsed: URL;
|
|
147
|
+
try {
|
|
148
|
+
parsed = new URL(url);
|
|
149
|
+
} catch {
|
|
150
|
+
throw new KernelError(`browser.fetch target must be an absolute URL; received: ${url}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
154
|
+
throw new KernelError(`browser.fetch only supports http(s) URLs; received: ${parsed.protocol}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function headersToRequestOptionsHeaders(headers: Headers): Record<string, string | null | undefined> {
|
|
159
|
+
const out: Record<string, string | null | undefined> = {};
|
|
160
|
+
|
|
161
|
+
headers.forEach((value, key) => {
|
|
162
|
+
switch (key.toLowerCase()) {
|
|
163
|
+
case 'accept':
|
|
164
|
+
case 'content-length':
|
|
165
|
+
case 'connection':
|
|
166
|
+
case 'keep-alive':
|
|
167
|
+
case 'proxy-authenticate':
|
|
168
|
+
case 'proxy-authorization':
|
|
169
|
+
case 'te':
|
|
170
|
+
case 'trailers':
|
|
171
|
+
case 'transfer-encoding':
|
|
172
|
+
case 'upgrade':
|
|
173
|
+
return;
|
|
174
|
+
default:
|
|
175
|
+
out[key] = value;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import type { Fetch, RequestInfo, RequestInit } from '../internal/builtin-types';
|
|
2
|
+
import { joinURL } from './join-url';
|
|
3
|
+
|
|
4
|
+
export type BrowserRoute = {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
baseURL: string;
|
|
7
|
+
jwt: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class BrowserRouteCache {
|
|
11
|
+
private entries = new Map<string, BrowserRoute>();
|
|
12
|
+
|
|
13
|
+
get(sessionId: string): BrowserRoute | undefined {
|
|
14
|
+
return this.entries.get(sessionId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
set(route: BrowserRoute): void {
|
|
18
|
+
this.entries.set(route.sessionId, route);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
delete(sessionId: string): void {
|
|
22
|
+
this.entries.delete(sessionId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
clear(): void {
|
|
26
|
+
this.entries.clear();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const BROWSER_ROUTING_SUBRESOURCES_ENV = 'KERNEL_BROWSER_ROUTING_SUBRESOURCES';
|
|
31
|
+
const DEFAULT_BROWSER_ROUTING_SUBRESOURCES = ['curl'];
|
|
32
|
+
const BROWSER_ROUTE_CACHEABLE_PATH = /^\/(?:v\d+\/)?browsers(?:\/[^/]+)?\/?$/;
|
|
33
|
+
const BROWSER_POOL_ACQUIRE_PATH = /^\/(?:v\d+\/)?browser_pools\/[^/]+\/acquire\/?$/;
|
|
34
|
+
const BROWSER_DELETE_BY_ID_PATH = /^\/(?:v\d+\/)?browsers\/([^/]+)\/?$/;
|
|
35
|
+
const BROWSER_POOL_RELEASE_PATH = /^\/(?:v\d+\/)?browser_pools\/[^/]+\/release\/?$/;
|
|
36
|
+
|
|
37
|
+
export function browserRoutingSubresourcesFromEnv(): string[] {
|
|
38
|
+
const raw = readBrowserRoutingSubresourcesEnv();
|
|
39
|
+
if (raw === undefined) {
|
|
40
|
+
return [...DEFAULT_BROWSER_ROUTING_SUBRESOURCES];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (raw.trim() === '') {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return raw
|
|
48
|
+
.split(',')
|
|
49
|
+
.map((value) => value.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createRoutingFetch(
|
|
54
|
+
innerFetch: Fetch,
|
|
55
|
+
{
|
|
56
|
+
apiBaseURL,
|
|
57
|
+
subresources,
|
|
58
|
+
cache,
|
|
59
|
+
}: {
|
|
60
|
+
apiBaseURL: string;
|
|
61
|
+
subresources: Iterable<string>;
|
|
62
|
+
cache: BrowserRouteCache;
|
|
63
|
+
},
|
|
64
|
+
): Fetch {
|
|
65
|
+
const allowed = new Set([...subresources].map((value) => value.trim()).filter(Boolean));
|
|
66
|
+
const apiOrigin = new URL(apiBaseURL).origin;
|
|
67
|
+
|
|
68
|
+
return async (input, init) => {
|
|
69
|
+
const request = new Request(input, init);
|
|
70
|
+
const shouldSniff = shouldSniffAndPopulateCache(request, apiOrigin);
|
|
71
|
+
const response = await routeRequest(innerFetch, { input, init, request }, apiOrigin, allowed, cache);
|
|
72
|
+
if (shouldSniff) {
|
|
73
|
+
await sniffAndPopulateCache(response, cache);
|
|
74
|
+
}
|
|
75
|
+
await maybeEvictBrowserRoute(request, response, apiOrigin, cache);
|
|
76
|
+
return response;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function shouldSniffAndPopulateCache(request: Request, apiOrigin: string): boolean {
|
|
81
|
+
const url = new URL(request.url);
|
|
82
|
+
return (
|
|
83
|
+
url.origin === apiOrigin &&
|
|
84
|
+
(BROWSER_ROUTE_CACHEABLE_PATH.test(url.pathname) || BROWSER_POOL_ACQUIRE_PATH.test(url.pathname))
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function maybeEvictBrowserRoute(
|
|
89
|
+
request: Request,
|
|
90
|
+
response: Response,
|
|
91
|
+
apiOrigin: string,
|
|
92
|
+
cache: BrowserRouteCache,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const url = new URL(request.url);
|
|
99
|
+
if (url.origin !== apiOrigin) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const sessionId =
|
|
104
|
+
deletedSessionIdFromPath(request, url.pathname) ??
|
|
105
|
+
(await releasedSessionIdFromRequest(request, url.pathname));
|
|
106
|
+
if (sessionId) {
|
|
107
|
+
cache.delete(sessionId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function deletedSessionIdFromPath(request: Request, pathname: string): string | undefined {
|
|
112
|
+
if (request.method.toUpperCase() !== 'DELETE') {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const match = pathname.match(BROWSER_DELETE_BY_ID_PATH);
|
|
117
|
+
if (!match) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const sessionId = decodeURIComponent(match[1] ?? '');
|
|
122
|
+
return sessionId || undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function releasedSessionIdFromRequest(request: Request, pathname: string): Promise<string | undefined> {
|
|
126
|
+
if (request.method.toUpperCase() !== 'POST' || !BROWSER_POOL_RELEASE_PATH.test(pathname)) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const body = await request.clone().json();
|
|
132
|
+
if (!body || typeof body !== 'object') {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sessionId = (body as Record<string, unknown>)['session_id'];
|
|
137
|
+
return typeof sessionId === 'string' && sessionId.trim() ? sessionId.trim() : undefined;
|
|
138
|
+
} catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function browserRouteFromValue(value: unknown): BrowserRoute | undefined {
|
|
144
|
+
if (!value || typeof value !== 'object') {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const record = value as Record<string, unknown>;
|
|
149
|
+
const sessionId = typeof record['session_id'] === 'string' ? record['session_id'].trim() : '';
|
|
150
|
+
const baseURL = typeof record['base_url'] === 'string' ? record['base_url'].trim() : '';
|
|
151
|
+
if (!sessionId || !baseURL) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const explicitJWT = typeof record['jwt'] === 'string' ? record['jwt'].trim() : '';
|
|
156
|
+
const cdpWsURL = typeof record['cdp_ws_url'] === 'string' ? record['cdp_ws_url'] : undefined;
|
|
157
|
+
const jwt = explicitJWT || parseJwtFromCdpWsUrl(cdpWsURL) || '';
|
|
158
|
+
if (!jwt) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
sessionId,
|
|
163
|
+
baseURL,
|
|
164
|
+
jwt,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function sniffAndPopulateCache(response: Response, cache: BrowserRouteCache): Promise<void> {
|
|
169
|
+
const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
|
|
170
|
+
if (!contentType.includes('application/json')) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
populateCache(await response.clone().json(), cache);
|
|
176
|
+
} catch {
|
|
177
|
+
// Ignore malformed JSON in routing cache population.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function populateCache(value: unknown, cache: BrowserRouteCache): void {
|
|
182
|
+
const route = browserRouteFromValue(value);
|
|
183
|
+
if (route) {
|
|
184
|
+
cache.set(route);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (Array.isArray(value)) {
|
|
188
|
+
for (const item of value) {
|
|
189
|
+
populateCache(item, cache);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!value || typeof value !== 'object') {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const child of Object.values(value as Record<string, unknown>)) {
|
|
199
|
+
if (typeof child === 'object' && child !== null) {
|
|
200
|
+
populateCache(child, cache);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function routeRequest(
|
|
206
|
+
innerFetch: Fetch,
|
|
207
|
+
{
|
|
208
|
+
input,
|
|
209
|
+
init,
|
|
210
|
+
request,
|
|
211
|
+
}: {
|
|
212
|
+
input: RequestInfo;
|
|
213
|
+
init: RequestInit | undefined;
|
|
214
|
+
request: Request;
|
|
215
|
+
},
|
|
216
|
+
apiOrigin: string,
|
|
217
|
+
allowed: ReadonlySet<string>,
|
|
218
|
+
cache: BrowserRouteCache,
|
|
219
|
+
): Promise<Response> {
|
|
220
|
+
const url = new URL(request.url);
|
|
221
|
+
if (url.origin !== apiOrigin) {
|
|
222
|
+
return innerFetch(input, init);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const match = url.pathname.match(/^\/(?:v\d+\/)?browsers\/([^/]+)\/([^/]+)(\/.*)?$/);
|
|
226
|
+
if (!match) {
|
|
227
|
+
return innerFetch(input, init);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const sessionId = decodeURIComponent(match[1] ?? '');
|
|
231
|
+
const subresource = match[2] ?? '';
|
|
232
|
+
if (!sessionId || !allowed.has(subresource)) {
|
|
233
|
+
return innerFetch(input, init);
|
|
234
|
+
}
|
|
235
|
+
const route = cache.get(sessionId);
|
|
236
|
+
if (route === undefined) {
|
|
237
|
+
return innerFetch(input, init);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const target = new URL(joinURL(route.baseURL, `/${subresource}${match[3] ?? ''}`));
|
|
241
|
+
url.searchParams.forEach((value, key) => {
|
|
242
|
+
if (key !== 'jwt') {
|
|
243
|
+
target.searchParams.append(key, value);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
if (!target.searchParams.get('jwt')) {
|
|
247
|
+
target.searchParams.set('jwt', route.jwt);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const headers = new Headers(request.headers);
|
|
251
|
+
headers.delete('authorization');
|
|
252
|
+
return innerFetch(target.toString(), buildRoutedInit(request, init, headers));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildRoutedInit(
|
|
256
|
+
request: Request,
|
|
257
|
+
originalInit: RequestInit | undefined,
|
|
258
|
+
headers: Headers,
|
|
259
|
+
): RequestInit {
|
|
260
|
+
const method = request.method.toUpperCase();
|
|
261
|
+
const routedInit = {
|
|
262
|
+
...((originalInit ?? {}) as Record<string, unknown>),
|
|
263
|
+
method,
|
|
264
|
+
headers,
|
|
265
|
+
redirect: request.redirect,
|
|
266
|
+
signal: request.signal,
|
|
267
|
+
} as RequestInit & Record<string, unknown>;
|
|
268
|
+
|
|
269
|
+
delete routedInit['body'];
|
|
270
|
+
delete routedInit['duplex'];
|
|
271
|
+
|
|
272
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
273
|
+
const body = requestBodyForFetch(request, originalInit);
|
|
274
|
+
if (body !== undefined) {
|
|
275
|
+
routedInit.body = body;
|
|
276
|
+
}
|
|
277
|
+
if (originalInit?.duplex !== undefined) {
|
|
278
|
+
routedInit.duplex = originalInit.duplex;
|
|
279
|
+
} else if (requiresHalfDuplex(body)) {
|
|
280
|
+
routedInit.duplex = 'half';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return routedInit;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function requestBodyForFetch(
|
|
288
|
+
request: Request,
|
|
289
|
+
originalInit: RequestInit | undefined,
|
|
290
|
+
): RequestInit['body'] | undefined {
|
|
291
|
+
if (originalInit?.body !== undefined && originalInit.body !== null) {
|
|
292
|
+
return originalInit.body;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return request.body ?? undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function requiresHalfDuplex(body: RequestInit['body'] | undefined): boolean {
|
|
299
|
+
return (
|
|
300
|
+
((globalThis as any).ReadableStream && body instanceof (globalThis as any).ReadableStream) ||
|
|
301
|
+
(typeof body === 'object' && body !== null && Symbol.asyncIterator in body)
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function parseJwtFromCdpWsUrl(cdpWsUrl: string | undefined): string | undefined {
|
|
306
|
+
if (!cdpWsUrl) {
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
return new URL(cdpWsUrl).searchParams.get('jwt') ?? undefined;
|
|
312
|
+
} catch {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function readBrowserRoutingSubresourcesEnv(): string | undefined {
|
|
318
|
+
if (typeof (globalThis as any).process !== 'undefined') {
|
|
319
|
+
const value = (globalThis as any).process.env?.[BROWSER_ROUTING_SUBRESOURCES_ENV];
|
|
320
|
+
return typeof value === 'string' ? value : undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (typeof (globalThis as any).Deno !== 'undefined') {
|
|
324
|
+
const value = (globalThis as any).Deno.env?.get?.(BROWSER_ROUTING_SUBRESOURCES_ENV);
|
|
325
|
+
return typeof value === 'string' ? value : undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|