@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.
Files changed (90) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/client.d.mts +5 -2
  3. package/client.d.mts.map +1 -1
  4. package/client.d.ts +5 -2
  5. package/client.d.ts.map +1 -1
  6. package/client.js +15 -2
  7. package/client.js.map +1 -1
  8. package/client.mjs +15 -2
  9. package/client.mjs.map +1 -1
  10. package/index.d.mts +2 -0
  11. package/index.d.mts.map +1 -1
  12. package/index.d.ts +2 -0
  13. package/index.d.ts.map +1 -1
  14. package/index.js +3 -1
  15. package/index.js.map +1 -1
  16. package/index.mjs +1 -0
  17. package/index.mjs.map +1 -1
  18. package/lib/browser-fetch.d.mts +7 -0
  19. package/lib/browser-fetch.d.mts.map +1 -0
  20. package/lib/browser-fetch.d.ts +7 -0
  21. package/lib/browser-fetch.d.ts.map +1 -0
  22. package/lib/browser-fetch.js +128 -0
  23. package/lib/browser-fetch.js.map +1 -0
  24. package/lib/browser-fetch.mjs +125 -0
  25. package/lib/browser-fetch.mjs.map +1 -0
  26. package/lib/browser-routing.d.mts +20 -0
  27. package/lib/browser-routing.d.mts.map +1 -0
  28. package/lib/browser-routing.d.ts +20 -0
  29. package/lib/browser-routing.d.ts.map +1 -0
  30. package/lib/browser-routing.js +246 -0
  31. package/lib/browser-routing.js.map +1 -0
  32. package/lib/browser-routing.mjs +240 -0
  33. package/lib/browser-routing.mjs.map +1 -0
  34. package/lib/join-url.d.mts +2 -0
  35. package/lib/join-url.d.mts.map +1 -0
  36. package/lib/join-url.d.ts +2 -0
  37. package/lib/join-url.d.ts.map +1 -0
  38. package/lib/join-url.js +7 -0
  39. package/lib/join-url.js.map +1 -0
  40. package/lib/join-url.mjs +4 -0
  41. package/lib/join-url.mjs.map +1 -0
  42. package/package.json +11 -1
  43. package/resources/auth/connections.d.mts +31 -2
  44. package/resources/auth/connections.d.mts.map +1 -1
  45. package/resources/auth/connections.d.ts +31 -2
  46. package/resources/auth/connections.d.ts.map +1 -1
  47. package/resources/browsers/browsers.d.mts +73 -2
  48. package/resources/browsers/browsers.d.mts.map +1 -1
  49. package/resources/browsers/browsers.d.ts +73 -2
  50. package/resources/browsers/browsers.d.ts.map +1 -1
  51. package/resources/browsers/browsers.js +23 -0
  52. package/resources/browsers/browsers.js.map +1 -1
  53. package/resources/browsers/browsers.mjs +23 -0
  54. package/resources/browsers/browsers.mjs.map +1 -1
  55. package/resources/browsers/index.d.mts +1 -1
  56. package/resources/browsers/index.d.mts.map +1 -1
  57. package/resources/browsers/index.d.ts +1 -1
  58. package/resources/browsers/index.d.ts.map +1 -1
  59. package/resources/browsers/index.js.map +1 -1
  60. package/resources/browsers/index.mjs.map +1 -1
  61. package/resources/browsers/replays.d.mts +2 -1
  62. package/resources/browsers/replays.d.mts.map +1 -1
  63. package/resources/browsers/replays.d.ts +2 -1
  64. package/resources/browsers/replays.d.ts.map +1 -1
  65. package/resources/index.d.mts +1 -1
  66. package/resources/index.d.mts.map +1 -1
  67. package/resources/index.d.ts +1 -1
  68. package/resources/index.d.ts.map +1 -1
  69. package/resources/index.js.map +1 -1
  70. package/resources/index.mjs.map +1 -1
  71. package/resources/projects/projects.d.mts +2 -2
  72. package/resources/projects/projects.d.ts +2 -2
  73. package/resources/projects/projects.js +2 -2
  74. package/resources/projects/projects.mjs +2 -2
  75. package/src/client.ts +25 -2
  76. package/src/index.ts +2 -0
  77. package/src/lib/browser-fetch.ts +180 -0
  78. package/src/lib/browser-routing.ts +329 -0
  79. package/src/lib/join-url.ts +3 -0
  80. package/src/resources/auth/connections.ts +35 -2
  81. package/src/resources/browsers/browsers.ts +86 -1
  82. package/src/resources/browsers/index.ts +2 -0
  83. package/src/resources/browsers/replays.ts +2 -1
  84. package/src/resources/index.ts +2 -0
  85. package/src/resources/projects/projects.ts +2 -2
  86. package/src/version.ts +1 -1
  87. package/version.d.mts +1 -1
  88. package/version.d.ts +1 -1
  89. package/version.js +1 -1
  90. 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 a paid plan
14
- * and the projects feature flag.
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 a paid plan
14
- * and the projects feature flag.
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 a paid plan
22
- * and the projects feature flag.
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 a paid plan
18
- * and the projects feature flag.
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.fetch = options.fetch ?? Shims.getDefaultFetch();
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.fetch,
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
+ }
@@ -0,0 +1,3 @@
1
+ export function joinURL(baseURL: string, path: string): string {
2
+ return `${baseURL.replace(/\/+$/, '')}${path.startsWith('/') ? path : `/${path}`}`;
3
+ }