← Public packages
@kody/browser-run
Call Cloudflare Browser Run Quick Actions and reuse shared sessions with createBrowserContext.
src/sessions/acquire.ts
115 lines · 3.6 KB · TypeScriptimport { any, boolean, number, object, optional, parse, string } from 'remix/data-schema'
import {
ISOLATION_INSTRUCTIONS,
accountPath,
authorizationHeader,
browserRunFetch,
browserWsEndpoint,
browserWsEndpointAlias,
pickAuth,
requireAccountId,
} from '../client.ts'
const acquireInput = object(
{
accountId: string(),
apiTokenSecret: optional(string()),
keepAliveMs: optional(number()),
lab: optional(boolean()),
recording: optional(boolean()),
targets: optional(boolean()),
guardrails: optional(any()),
dryRun: optional(boolean()),
},
{ unknownKeys: 'error' },
)
/**
* Acquire / launch a new Browser Run session
* (`POST .../devtools/browser`) and return `{ sessionId, browserWSEndpoint }`.
* For the pit-of-success shared path (list → reuse → launch), prefer
* `./open-isolated` instead of calling acquire alone in a loop.
*
* @param raw.accountId - Cloudflare account id
* @param raw.keepAliveMs - Keep-alive ms for the new session (10_000–1_200_000)
* @param raw.guardrails - Optional outbound domain guardrails body
* @param raw.dryRun - Preview without launching
* @param raw.apiTokenSecret - Optional alternate secret name
* @returns Session id, WebSocket endpoint, and Authorization guidance
*
* @example
* import acquire from 'kody:@kody/browser-run/sessions/acquire'
* const session = await acquire({
* accountId: 'YOUR_ACCOUNT_ID',
* keepAliveMs: 600000,
* })
*/
export default async function acquireSession(raw: unknown = {}) {
const input = parse(acquireInput, raw ?? {})
const auth = pickAuth(input)
const accountId = requireAccountId(auth)
const path = accountPath(accountId, '/browser-rendering/devtools/browser')
const query: Record<string, string | number | boolean> = {}
if (input.keepAliveMs != null) query.keep_alive = Math.floor(input.keepAliveMs)
if (input.lab === true) query.lab = true
if (input.recording === true) query.recording = true
if (input.targets === true) query.targets = true
const body =
input.guardrails != null
? { guardrails: input.guardrails }
: undefined
if (input.dryRun === true) {
return {
dryRun: true as const,
method: 'POST' as const,
path,
query,
body: body ?? null,
accountId,
note: 'Would launch a new Browser Run session. Prefer open-isolated to reuse first.',
}
}
const response = await browserRunFetch(path, {
...auth,
method: 'POST',
query,
body,
})
const result =
(response.kind === 'json' ? (response.result as Record<string, unknown>) : null) ||
{}
const sessionId = String(result.sessionId ?? result.session_id ?? '').trim()
const fromApi = String(
result.webSocketDebuggerUrl ?? result.browserWSEndpoint ?? '',
).trim()
const browserWSEndpoint =
fromApi ||
(sessionId
? browserWsEndpoint(accountId, sessionId, input.keepAliveMs)
: browserWsEndpoint(accountId, undefined, input.keepAliveMs))
return {
ok: true,
accountId,
sessionId: sessionId || null,
browserWSEndpoint,
browserWSEndpointAlias: browserWsEndpointAlias(
accountId,
sessionId || undefined,
input.keepAliveMs,
),
authorizationHeaderTemplate: authorizationHeader(auth),
authorizationGuidance:
'Pass headers: { Authorization: "Bearer <cloudflareApiToken>" } to puppeteer.connect / connectOverCDP. In Kody, the Authorization placeholder resolves on approved hosts; for external Node use the real token from your environment — never paste tokens into chat.',
launched: true,
isolation: 'createBrowserContext' as const,
disconnectNotClose: true as const,
keepAliveMs: input.keepAliveMs ?? null,
instructions: ISOLATION_INSTRUCTIONS,
browserMsUsed: response.browserMsUsed,
}
}