@onkernel/sdk 0.49.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 (85) hide show
  1. package/CHANGELOG.md +64 -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 +72 -1
  48. package/resources/browsers/browsers.d.mts.map +1 -1
  49. package/resources/browsers/browsers.d.ts +72 -1
  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/index.d.mts +1 -1
  62. package/resources/index.d.mts.map +1 -1
  63. package/resources/index.d.ts +1 -1
  64. package/resources/index.d.ts.map +1 -1
  65. package/resources/index.js.map +1 -1
  66. package/resources/index.mjs.map +1 -1
  67. package/resources/projects/projects.d.mts +2 -2
  68. package/resources/projects/projects.d.ts +2 -2
  69. package/resources/projects/projects.js +2 -2
  70. package/resources/projects/projects.mjs +2 -2
  71. package/src/client.ts +25 -2
  72. package/src/index.ts +2 -0
  73. package/src/lib/browser-fetch.ts +180 -0
  74. package/src/lib/browser-routing.ts +329 -0
  75. package/src/lib/join-url.ts +3 -0
  76. package/src/resources/auth/connections.ts +35 -2
  77. package/src/resources/browsers/browsers.ts +85 -0
  78. package/src/resources/browsers/index.ts +2 -0
  79. package/src/resources/index.ts +2 -0
  80. package/src/resources/projects/projects.ts +2 -2
  81. package/src/version.ts +1 -1
  82. package/version.d.mts +1 -1
  83. package/version.d.ts +1 -1
  84. package/version.js +1 -1
  85. package/version.mjs +1 -1
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
+ }
@@ -244,6 +244,13 @@ export interface ManagedAuth {
244
244
  */
245
245
  allowed_domains?: Array<string>;
246
246
 
247
+ /**
248
+ * ID of the underlying browser session driving the current flow (present when flow
249
+ * in progress). Use this to inspect or terminate the browser session via the
250
+ * `/browsers` API.
251
+ */
252
+ browser_session_id?: string | null;
253
+
247
254
  /**
248
255
  * Whether automatic re-authentication is possible (has credential, selectors, and
249
256
  * login_url)
@@ -286,7 +293,10 @@ export interface ManagedAuth {
286
293
  external_action_message?: string | null;
287
294
 
288
295
  /**
289
- * When the current flow expires (null when no flow in progress)
296
+ * When the current flow expires (null when no flow in progress). A flow past this
297
+ * timestamp is no longer valid and its `flow_status` will be `EXPIRED`. Clients
298
+ * may start a new login to supersede a stale `IN_PROGRESS` flow past this
299
+ * timestamp.
290
300
  */
291
301
  flow_expires_at?: string | null;
292
302
 
@@ -326,10 +336,21 @@ export interface ManagedAuth {
326
336
  hosted_url?: string | null;
327
337
 
328
338
  /**
329
- * When the profile was last successfully authenticated
339
+ * @deprecated Deprecated alias for `last_auth_check_at`. Despite the name, this is
340
+ * the last health-check timestamp, not the last successful authentication. Use
341
+ * `last_auth_check_at` instead.
330
342
  */
331
343
  last_auth_at?: string;
332
344
 
345
+ /**
346
+ * When the most recent auth health check ran for this connection, regardless of
347
+ * outcome. Updated on every health check and does not by itself indicate that the
348
+ * profile is currently authenticated - use `status` for that. May be newer than
349
+ * `flow_expires_at` when a flow is still in progress because health checks
350
+ * continue to run in parallel.
351
+ */
352
+ last_auth_check_at?: string;
353
+
333
354
  /**
334
355
  * Browser live view URL for debugging (present when flow in progress)
335
356
  */
@@ -433,6 +454,12 @@ export namespace ManagedAuth {
433
454
  */
434
455
  type: 'text' | 'email' | 'password' | 'tel' | 'number' | 'url' | 'code' | 'totp';
435
456
 
457
+ /**
458
+ * Contextual help text near the field that tells the user what to enter (e.g.,
459
+ * "Enter the phone ending in (**_) _**-\*\*92")
460
+ */
461
+ hint?: string;
462
+
436
463
  /**
437
464
  * If this field is associated with an MFA option, the type of that option (e.g.,
438
465
  * password field linked to "Enter password" option)
@@ -890,6 +917,12 @@ export namespace ConnectionFollowResponse {
890
917
  */
891
918
  type: 'text' | 'email' | 'password' | 'tel' | 'number' | 'url' | 'code' | 'totp';
892
919
 
920
+ /**
921
+ * Contextual help text near the field that tells the user what to enter (e.g.,
922
+ * "Enter the phone ending in (**_) _**-\*\*92")
923
+ */
924
+ hint?: string;
925
+
893
926
  /**
894
927
  * If this field is associated with an MFA option, the type of that option (e.g.,
895
928
  * password field linked to "Enter password" option)