Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
94228be
feat: add browser-scoped session client
rgarcia Apr 13, 2026
ae9a739
fix: require base_url for browser-scoped routing
rgarcia Apr 13, 2026
d835f69
fix: enforce browser base_url routing
rgarcia Apr 13, 2026
e730af8
feat: generate browser-scoped session bindings
rgarcia Apr 13, 2026
2f12277
refactor: replace browser-scoped client with browser routing cache
rgarcia Apr 22, 2026
2082705
refactor: simplify browser routing cache
rgarcia Apr 22, 2026
7030d96
refactor: rename browser routing subresources config
rgarcia Apr 22, 2026
0d9ddce
docs: restore raw http example in browser routing demo
rgarcia Apr 22, 2026
b09434e
feat: restore node browser fetch helper
rgarcia Apr 22, 2026
2f16386
fix: drop node browser routing branch churn
rgarcia Apr 22, 2026
7a56ab6
fix: clean up node browser routing lint drift
rgarcia Apr 22, 2026
fdd3adf
fix: simplify node browser routing helpers
rgarcia Apr 22, 2026
4ba0696
chore(internal): more robust bootstrap script
stainless-app[bot] Apr 23, 2026
00c91ef
refactor: move node browser routing rollout to env
rgarcia Apr 24, 2026
9b24280
fix: preserve browser routing fetch options
rgarcia Apr 24, 2026
410b647
feat: Expose browser_session_id on managed auth connection
stainless-app[bot] Apr 24, 2026
0e0e88f
fix: limit browser route cache sniffing
rgarcia Apr 24, 2026
2d0056e
fix: evict deleted browser routes
rgarcia Apr 24, 2026
a76f7ae
fix: keep browser routing helpers out of generated code
rgarcia Apr 24, 2026
a7ff9bc
fix: restore generated types formatting
rgarcia Apr 24, 2026
81e47ca
fix: handle browser pool route cache updates
rgarcia Apr 24, 2026
e36b009
Merge pull request #99 from kernel/raf/browser-scoped-client
rgarcia Apr 24, 2026
2b2e3ca
release: 0.51.0
stainless-app[bot] Apr 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.50.0"
".": "0.51.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 112
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-686a9addd4f9356ca26ff3ff04e1a11466d77a412859829075566394922b715d.yml
openapi_spec_hash: 7a9e9c2023400d44bcbfb87b7ec07708
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e14974fd90680e5745b35d8718a1ccce2181f6d17a6e0a1fd35fc5bca88795ae.yml
openapi_spec_hash: 1b3aa75f0ab48b122d514047f9c82873
config_hash: 08d55086449943a8fec212b870061a3f
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
# Changelog

## 0.51.0 (2026-04-24)

Full Changelog: [v0.50.0...v0.51.0](https://github.com/kernel/kernel-node-sdk/compare/v0.50.0...v0.51.0)

### Features

* add browser-scoped session client ([94228be](https://github.com/kernel/kernel-node-sdk/commit/94228be699cc42b42caccba4b179875fc6bfcab3))
* Expose browser_session_id on managed auth connection ([410b647](https://github.com/kernel/kernel-node-sdk/commit/410b647f81d64dc632ccc6f8fb09bbb2a364b49d))
* generate browser-scoped session bindings ([e730af8](https://github.com/kernel/kernel-node-sdk/commit/e730af816f72663041170b4602c009ae5e86ba9c))
* restore node browser fetch helper ([b09434e](https://github.com/kernel/kernel-node-sdk/commit/b09434ec0642ec166a749beccc985510ed3a4723))


### Bug Fixes

* clean up node browser routing lint drift ([7a56ab6](https://github.com/kernel/kernel-node-sdk/commit/7a56ab6c6c08697f1a6c4ddc104a9c6abab5bd98))
* drop node browser routing branch churn ([2f16386](https://github.com/kernel/kernel-node-sdk/commit/2f1638684e9e687f26c24bd7e2d54bb335810ef8))
* enforce browser base_url routing ([d835f69](https://github.com/kernel/kernel-node-sdk/commit/d835f6925b8940bbf58703ddbb03858117f0e070))
* evict deleted browser routes ([2d0056e](https://github.com/kernel/kernel-node-sdk/commit/2d0056e0dabe2e4716d50d053a12e2b8e95048a0))
* handle browser pool route cache updates ([81e47ca](https://github.com/kernel/kernel-node-sdk/commit/81e47caffea08415750b6a3480b59a4f698c4187))
* keep browser routing helpers out of generated code ([a76f7ae](https://github.com/kernel/kernel-node-sdk/commit/a76f7ae7e8d040835cd38eb026fad626dfa718db))
* limit browser route cache sniffing ([0e0e88f](https://github.com/kernel/kernel-node-sdk/commit/0e0e88fd88d3f1c11a9b3dc5f10c3c379a10bf96))
* preserve browser routing fetch options ([9b24280](https://github.com/kernel/kernel-node-sdk/commit/9b242808521a9960ce48174af2e3e2ccb30a7a84))
* require base_url for browser-scoped routing ([ae9a739](https://github.com/kernel/kernel-node-sdk/commit/ae9a739d0b1026b3ebf71d6f7d11ce52070cf42e))
* restore generated types formatting ([a7ff9bc](https://github.com/kernel/kernel-node-sdk/commit/a7ff9bcae419cefbebaccaf4e07545504ec39356))
* simplify node browser routing helpers ([fdd3adf](https://github.com/kernel/kernel-node-sdk/commit/fdd3adf1cd0bbd669a6aa2cd053359256735d9a7))


### Chores

* **internal:** more robust bootstrap script ([4ba0696](https://github.com/kernel/kernel-node-sdk/commit/4ba0696f2b38548f3dcb6763cddd3490b090f658))


### Documentation

* restore raw http example in browser routing demo ([0d9ddce](https://github.com/kernel/kernel-node-sdk/commit/0d9ddced05e9b030efd055ccf9fc75080ec6dcec))


### Refactors

* move node browser routing rollout to env ([00c91ef](https://github.com/kernel/kernel-node-sdk/commit/00c91ef1de5a18604fe6e66a57227a2cc794cdba))
* rename browser routing subresources config ([7030d96](https://github.com/kernel/kernel-node-sdk/commit/7030d969217535fd7e29215ef1e45e8625e6c3e4))
* replace browser-scoped client with browser routing cache ([2f12277](https://github.com/kernel/kernel-node-sdk/commit/2f1227728f83205144169be48de7b4f145f920d7))
* simplify browser routing cache ([2082705](https://github.com/kernel/kernel-node-sdk/commit/20827054115bd07042359acc6a167c72ab8a581f))

## 0.50.0 (2026-04-20)

Full Changelog: [v0.49.0...v0.50.0](https://github.com/kernel/kernel-node-sdk/compare/v0.49.0...v0.50.0)
Expand Down
13 changes: 13 additions & 0 deletions examples/browser-routing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Kernel from '@onkernel/sdk';

async function main() {
const kernel = new Kernel();

const browser = await kernel.browsers.create({});
const response = await kernel.browsers.fetch(browser.session_id, 'https://example.com', { method: 'GET' });
console.log('status', response.status);

await kernel.browsers.deleteByID(browser.session_id);
}

void main();
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@onkernel/sdk",
"version": "0.50.0",
"version": "0.51.0",
"description": "The official TypeScript library for the Kernel API",
"author": "Kernel <>",
"types": "dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion scripts/bootstrap
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ set -e

cd "$(dirname "$0")/.."

if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then
if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then
brew bundle check >/dev/null 2>&1 || {
echo -n "==> Install Homebrew dependencies? (y/N): "
read -r response
Expand Down
23 changes: 21 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import * as Uploads from './core/uploads';
import * as API from './resources/index';
import { APIPromise } from './core/api-promise';
import { AppListParams, AppListResponse, AppListResponsesOffsetPagination, Apps } from './resources/apps';
import {
BrowserRouteCache,
browserRoutingSubresourcesFromEnv,
createRoutingFetch,
} from './lib/browser-routing';
import {
BrowserPool,
BrowserPoolAcquireParams,
Expand Down Expand Up @@ -247,9 +252,11 @@ export class Kernel {
fetchOptions: MergedRequestInit | undefined;

private fetch: Fetch;
private rawFetch: Fetch;
#encoder: Opts.RequestEncoder;
protected idempotencyHeader?: string;
private _options: ClientOptions;
public browserRouteCache: BrowserRouteCache;

/**
* API Client for interfacing with the Kernel API.
Expand Down Expand Up @@ -312,7 +319,13 @@ export class Kernel {
defaultLogLevel;
this.fetchOptions = options.fetchOptions;
this.maxRetries = options.maxRetries ?? 2;
this.fetch = options.fetch ?? Shims.getDefaultFetch();
this.rawFetch = options.fetch ?? Shims.getDefaultFetch();
this.browserRouteCache = new BrowserRouteCache();
this.fetch = createRoutingFetch(this.rawFetch, {
apiBaseURL: this.baseURL,
subresources: browserRoutingSubresourcesFromEnv(),
cache: this.browserRouteCache,
});
this.#encoder = Opts.FallbackEncoder;

this._options = options;
Expand All @@ -332,11 +345,17 @@ export class Kernel {
timeout: this.timeout,
logger: this.logger,
logLevel: this.logLevel,
fetch: this.fetch,
fetch: this.rawFetch,
fetchOptions: this.fetchOptions,
apiKey: this.apiKey,
...options,
});
client.browserRouteCache = this.browserRouteCache;
client.fetch = createRoutingFetch(client.rawFetch, {
apiBaseURL: client.baseURL,
subresources: browserRoutingSubresourcesFromEnv(),
cache: client.browserRouteCache,
});
return client;
}

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export { Kernel as default } from './client';
export { type Uploadable, toFile } from './core/uploads';
export { APIPromise } from './core/api-promise';
export { Kernel, type ClientOptions } from './client';
export { type BrowserFetchInit } from './lib/browser-fetch';
export { BrowserRouteCache, type BrowserRoute } from './lib/browser-routing';
export { PagePromise } from './core/pagination';
export {
KernelError,
Expand Down
180 changes: 180 additions & 0 deletions src/lib/browser-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import type { RequestInfo, RequestInit } from '../internal/builtin-types';
import { KernelError } from '../core/error';
import { buildHeaders } from '../internal/headers';
import type { FinalRequestOptions, RequestOptions } from '../internal/request-options';
import type { HTTPMethod } from '../internal/types';
import { joinURL } from './join-url';
import type { Kernel } from '../client';

export interface BrowserFetchInit extends RequestInit {
timeout_ms?: number;
}

export async function browserFetch(
client: Kernel,
sessionId: string,
input: RequestInfo | URL,
init?: BrowserFetchInit,
): Promise<Response> {
const route = client.browserRouteCache.get(sessionId);
if (!route) {
throw new KernelError(
`browser route cache does not contain session ${sessionId}; create, retrieve, or list the browser before calling browser.fetch`,
);
}

const { url: targetURL, method, headers, body, signal, duplex, timeout_ms } = splitFetchArgs(input, init);
assertHTTPURL(targetURL);

const query: Record<string, unknown> = { url: targetURL, jwt: route.jwt };
if (timeout_ms !== undefined) {
query['timeout_ms'] = timeout_ms;
}

const accept = headers.get('accept');
const requestOptions: FinalRequestOptions = {
method: normalizeMethod(method),
path: joinURL(route.baseURL, '/curl/raw'),
query,
body: body as RequestOptions['body'],
headers: buildHeaders([
{ Authorization: null },
accept ? { Accept: accept } : { Accept: '*/*' },
headersToRequestOptionsHeaders(headers),
]),
signal: signal ?? null,
__binaryResponse: true,
};
if (duplex) {
requestOptions.fetchOptions = { duplex } as NonNullable<RequestOptions['fetchOptions']>;
}

return client.request(requestOptions).asResponse();
}

function normalizeMethod(method: string): HTTPMethod {
const methodLower = method.toLowerCase();
const allowed = new Set(['get', 'post', 'put', 'patch', 'delete']);
if (!allowed.has(methodLower)) {
throw new KernelError(`browser.fetch unsupported HTTP method: ${method}`);
}
return methodLower as HTTPMethod;
}

function splitFetchArgs(
input: RequestInfo | URL,
init?: BrowserFetchInit,
): {
url: string;
method: string;
headers: Headers;
body?: RequestInit['body'];
signal?: AbortSignal | null;
duplex?: RequestInit['duplex'];
timeout_ms?: number;
} {
const timeoutFromInit = init && 'timeout_ms' in init ? init['timeout_ms'] : undefined;

if (input instanceof Request) {
const headers = new Headers(input.headers);
if (init?.headers) {
const extra = new Headers(init.headers);
extra.forEach((value, key) => {
headers.set(key, value);
});
}

const out: {
url: string;
method: string;
headers: Headers;
body?: RequestInit['body'];
signal?: AbortSignal | null;
duplex?: RequestInit['duplex'];
timeout_ms?: number;
} = {
url: input.url,
method: (init?.method ?? input.method)?.toUpperCase() || 'GET',
headers,
};
const body = init?.body ?? input.body;
if (body !== undefined && body !== null) {
out.body = body;
}
const signal = init?.signal ?? input.signal;
if (signal !== undefined) {
out.signal = signal;
}
if (init?.duplex !== undefined) {
out.duplex = init.duplex;
}
if (timeoutFromInit !== undefined) {
out.timeout_ms = timeoutFromInit;
}
return out;
}

const out: {
url: string;
method: string;
headers: Headers;
body?: RequestInit['body'];
signal?: AbortSignal | null;
duplex?: RequestInit['duplex'];
timeout_ms?: number;
} = {
url: input instanceof URL ? input.href : String(input),
method: (init?.method ?? 'GET').toUpperCase(),
headers: new Headers(init?.headers),
};
if (init?.body !== undefined) {
out.body = init.body;
}
if (init?.signal !== undefined) {
out.signal = init.signal;
}
if (init?.duplex !== undefined) {
out.duplex = init.duplex;
}
if (timeoutFromInit !== undefined) {
out.timeout_ms = timeoutFromInit;
}
return out;
}

function assertHTTPURL(url: string): void {
let parsed: URL;
try {
parsed = new URL(url);
} catch {
throw new KernelError(`browser.fetch target must be an absolute URL; received: ${url}`);
}

if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new KernelError(`browser.fetch only supports http(s) URLs; received: ${parsed.protocol}`);
}
}

function headersToRequestOptionsHeaders(headers: Headers): Record<string, string | null | undefined> {
const out: Record<string, string | null | undefined> = {};

headers.forEach((value, key) => {
switch (key.toLowerCase()) {
case 'accept':
case 'content-length':
case 'connection':
case 'keep-alive':
case 'proxy-authenticate':
case 'proxy-authorization':
case 'te':
case 'trailers':
case 'transfer-encoding':
case 'upgrade':
return;
default:
out[key] = value;
}
});

return out;
}
Loading
Loading