@computesdk/browserless 0.0.0-stage → 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 computesdk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,45 @@
1
- # Temporary Holding Version
1
+ # @computesdk/browserless
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Browserless browser provider for ComputeSDK. Cloud browser sessions via [Browserless](https://www.browserless.io).
4
+
5
+ ```bash
6
+ npm install @computesdk/browserless
7
+ ```
8
+
9
+ ```ts
10
+ import { browserless } from '@computesdk/browserless';
11
+ import { chromium } from 'playwright-core';
12
+
13
+ const bl = browserless({ apiKey: process.env.BROWSERLESS_API_KEY });
14
+
15
+ const session = await bl.session.create();
16
+ const browser = await chromium.connectOverCDP(session.connectUrl);
17
+ const page = browser.contexts()[0].pages()[0];
18
+ await page.goto('https://example.com');
19
+
20
+ await bl.session.destroy(session.sessionId);
21
+ ```
22
+
23
+ ## Config
24
+
25
+ | option | env | default |
26
+ |---|---|---|
27
+ | `apiKey` | `BROWSERLESS_API_KEY` | — (required) |
28
+ | `baseUrl` | `BROWSERLESS_URL` | `https://production-sfo.browserless.io` |
29
+
30
+ Shared fleet regions: `production-sfo`, `production-lon`, `production-ams`.
31
+ Private fleet: `chrome.browserless.io` or `chrome-<region>.browserless.io` — a
32
+ private-fleet token against a shared-fleet host returns 401.
33
+
34
+ ## Session model
35
+
36
+ Browserless has no create-session REST call on the shared fleet: a session
37
+ materializes when the client opens the CDP websocket. `create()` mints a
38
+ `trackingId` and returns its connect URL (`/chromium?token=…&trackingId=…`);
39
+ the session ends on disconnect or via `destroy()` (`/kill/<trackingId>`).
40
+
41
+ `list()`/`getById()` use `GET /sessions`, which is private/enterprise-fleet
42
+ only — on the shared fleet those calls surface the HTTP error.
43
+
44
+ SECURITY: `connectUrl` embeds the API token as a query param — treat it as a
45
+ secret (don't log or persist).
package/dist/index.cjs ADDED
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ browserless: () => browserless
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var import_node_crypto = require("crypto");
27
+ var import_provider = require("@computesdk/provider");
28
+ function resolveConfig(config) {
29
+ const apiKey = config.apiKey || typeof process !== "undefined" && process.env?.BROWSERLESS_API_KEY || "";
30
+ if (!apiKey) {
31
+ throw new Error(
32
+ `Missing Browserless API key. Provide 'apiKey' in config or set BROWSERLESS_API_KEY environment variable. Get your API key from https://account.browserless.io`
33
+ );
34
+ }
35
+ const baseUrl = (config.baseUrl || typeof process !== "undefined" && process.env?.BROWSERLESS_URL || "https://production-sfo.browserless.io").replace(/\/+$/, "");
36
+ return { apiKey, baseUrl };
37
+ }
38
+ function wsBase(baseUrl) {
39
+ return baseUrl.replace(/^https/, "wss").replace(/^http/, "ws");
40
+ }
41
+ function enc(value) {
42
+ return encodeURIComponent(value);
43
+ }
44
+ var warnedUnsupported = /* @__PURE__ */ new Set();
45
+ function warnOnce(field, reason) {
46
+ if (warnedUnsupported.has(field)) return;
47
+ warnedUnsupported.add(field);
48
+ console.warn(`[@computesdk/browserless] '${field}' is ignored: ${reason}`);
49
+ }
50
+ var sessionOptions = /* @__PURE__ */ new Map();
51
+ function redact(body, apiKey) {
52
+ return body.split(apiKey).join("***");
53
+ }
54
+ function buildConnectUrl(baseUrl, apiKey, trackingId, options) {
55
+ const url = new URL(`${wsBase(baseUrl)}/chromium`);
56
+ url.searchParams.set("token", apiKey);
57
+ url.searchParams.set("trackingId", trackingId);
58
+ if (!options) return url.toString();
59
+ if (options.stealth) url.searchParams.set("stealth", "true");
60
+ if (options.viewport) {
61
+ url.searchParams.set("launch", JSON.stringify({
62
+ defaultViewport: { width: options.viewport.width, height: options.viewport.height }
63
+ }));
64
+ }
65
+ if (options.proxies === true) url.searchParams.set("proxy", "residential");
66
+ if (options.keepAlive === true) {
67
+ warnOnce("keepAlive", "Browserless sessions live until disconnect or /kill; no keep-alive flag.");
68
+ }
69
+ if (options.recording === false) {
70
+ warnOnce("recording", "Session recording is managed server-side; no per-session toggle on the connect URL.");
71
+ }
72
+ if (options.logging === false) {
73
+ warnOnce("logging", "Session logging is managed server-side; no per-session toggle on the connect URL.");
74
+ }
75
+ if (options.timeout !== void 0) {
76
+ warnOnce("timeout", "Session TTL is a fleet-level setting on Browserless; no per-session timeout param.");
77
+ }
78
+ if (options.profileId !== void 0) {
79
+ warnOnce("profileId", "Browserless has no persistent-profile concept on the connect URL.");
80
+ }
81
+ if (options.extensionIds && options.extensionIds.length > 0) {
82
+ warnOnce("extensionIds", "Extensions must be baked into the Browserless fleet image; no per-session IDs.");
83
+ }
84
+ if (options.region !== void 0) {
85
+ warnOnce("region", "Region is selected by the baseUrl host (e.g. production-sfo|lon|ams), not a connect param.");
86
+ }
87
+ if (options.saveDownloads === true) {
88
+ warnOnce("saveDownloads", "Browserless has no server-side download retrieval API.");
89
+ }
90
+ if (options.userMetadata && Object.keys(options.userMetadata).length > 0) {
91
+ warnOnce("userMetadata", "Browserless does not support arbitrary user metadata on sessions.");
92
+ }
93
+ if (Array.isArray(options.proxies) && options.proxies.length > 0) {
94
+ warnOnce("proxies", "Custom proxy servers are a Browserless enterprise feature; only proxies=true maps (residential).");
95
+ }
96
+ return url.toString();
97
+ }
98
+ async function listSessions(baseUrl, apiKey) {
99
+ const r = await fetch(`${baseUrl}/sessions?token=${enc(apiKey)}`);
100
+ if (!r.ok) throw new Error(`browserless /sessions ${r.status}: ${redact(await r.text(), apiKey)}`);
101
+ return await r.json();
102
+ }
103
+ function toEntry(s) {
104
+ return {
105
+ session: s,
106
+ sessionId: s.trackingId ?? s.browserId,
107
+ status: s.running ? "running" : "created"
108
+ };
109
+ }
110
+ var browserless = (0, import_provider.defineBrowserProvider)({
111
+ name: "browserless",
112
+ methods: {
113
+ session: {
114
+ create: async (config, options) => {
115
+ const { apiKey, baseUrl } = resolveConfig(config);
116
+ const trackingId = (0, import_node_crypto.randomUUID)();
117
+ if (options) sessionOptions.set(trackingId, options);
118
+ return {
119
+ session: { browserId: trackingId, trackingId, running: false, startedOn: Date.now() },
120
+ sessionId: trackingId,
121
+ connectUrl: buildConnectUrl(baseUrl, apiKey, trackingId, options),
122
+ status: "created"
123
+ };
124
+ },
125
+ getById: async (config, sessionId) => {
126
+ const { apiKey, baseUrl } = resolveConfig(config);
127
+ const found = (await listSessions(baseUrl, apiKey)).find(
128
+ (s) => s.trackingId === sessionId || s.browserId === sessionId
129
+ );
130
+ if (!found) return null;
131
+ const trackingId = found.trackingId ?? sessionId;
132
+ return { ...toEntry(found), connectUrl: buildConnectUrl(baseUrl, apiKey, trackingId, sessionOptions.get(trackingId)) };
133
+ },
134
+ list: async (config) => {
135
+ const { apiKey, baseUrl } = resolveConfig(config);
136
+ return (await listSessions(baseUrl, apiKey)).map(toEntry);
137
+ },
138
+ destroy: async (config, sessionId) => {
139
+ const { apiKey, baseUrl } = resolveConfig(config);
140
+ const r = await fetch(`${baseUrl}/kill/${enc(sessionId)}?token=${enc(apiKey)}`);
141
+ sessionOptions.delete(sessionId);
142
+ if (!r.ok && r.status !== 404) {
143
+ throw new Error(`browserless destroy ${r.status}: ${redact(await r.text(), apiKey)}`);
144
+ }
145
+ },
146
+ getConnectUrl: async (config, sessionId) => {
147
+ const { apiKey, baseUrl } = resolveConfig(config);
148
+ return buildConnectUrl(baseUrl, apiKey, sessionId, sessionOptions.get(sessionId));
149
+ }
150
+ }
151
+ }
152
+ });
153
+ // Annotate the CommonJS export names for ESM import in node:
154
+ 0 && (module.exports = {
155
+ browserless
156
+ });
157
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Browserless Browser Provider - Factory-based Implementation\n *\n * Cloud browser sessions via Browserless (https://browserless.io). Browserless\n * has no create-session REST call on the shared fleet: a session materializes\n * when the client opens the CDP websocket. `session.create()` therefore mints\n * a `trackingId` and returns the connect URL for it; the session starts when\n * `chromium.connectOverCDP(connectUrl)` opens, and ends on disconnect or via\n * `session.destroy()` (`/kill/<trackingId>`).\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { defineBrowserProvider } from '@computesdk/provider';\n\nimport type { CreateBrowserSessionOptions } from '@computesdk/provider';\n\n/**\n * Shape of a running session as returned by `GET /sessions`.\n * `/sessions` is available on private/enterprise fleets; on the shared fleet\n * it may return 401 — list/getById surface that error rather than masking it.\n */\nexport interface BrowserlessSession {\n browserId: string;\n trackingId: string | null;\n running: boolean;\n startedOn: number;\n timeAliveMs?: number;\n browser?: string;\n type?: string;\n}\n\n/**\n * Browserless-specific configuration options.\n */\nexport interface BrowserlessConfig {\n /** Browserless API token — falls back to BROWSERLESS_API_KEY env var */\n apiKey?: string;\n /**\n * Fleet base URL — falls back to BROWSERLESS_URL env var, then the shared\n * US West fleet. Shared fleet: production-sfo|lon|ams.browserless.io.\n * Private fleet: chrome.browserless.io or chrome-<region>.browserless.io\n * (a private-fleet token against a shared-fleet host returns 401).\n */\n baseUrl?: string;\n}\n\nfunction resolveConfig(config: BrowserlessConfig) {\n const apiKey =\n config.apiKey || (typeof process !== 'undefined' && process.env?.BROWSERLESS_API_KEY) || '';\n if (!apiKey) {\n throw new Error(\n `Missing Browserless API key. Provide 'apiKey' in config or set BROWSERLESS_API_KEY environment variable. ` +\n `Get your API key from https://account.browserless.io`,\n );\n }\n const baseUrl = (\n config.baseUrl ||\n (typeof process !== 'undefined' && process.env?.BROWSERLESS_URL) ||\n 'https://production-sfo.browserless.io'\n ).replace(/\\/+$/, '');\n return { apiKey, baseUrl };\n}\n\n/** http(s) base URL → ws(s) equivalent for the websocket endpoint. */\nfunction wsBase(baseUrl: string): string {\n return baseUrl.replace(/^https/, 'wss').replace(/^http/, 'ws');\n}\n\nfunction enc(value: string): string {\n return encodeURIComponent(value);\n}\n\nconst warnedUnsupported = new Set<string>();\nfunction warnOnce(field: string, reason: string) {\n if (warnedUnsupported.has(field)) return;\n warnedUnsupported.add(field);\n console.warn(`[@computesdk/browserless] '${field}' is ignored: ${reason}`);\n}\n\n/**\n * Create options by sessionId, so getConnectUrl/getById can rebuild the same\n * URL the session was created with. Same-process only — a URL rebuilt in a\n * fresh process carries no launch options. Entries are dropped on destroy.\n */\nconst sessionOptions = new Map<string, CreateBrowserSessionOptions>();\n\n/** Strip the API token from provider error bodies before throwing. */\nfunction redact(body: string, apiKey: string): string {\n return body.split(apiKey).join('***');\n}\n\n/**\n * Build the CDP connect URL for a session. The token authenticates the\n * websocket; `trackingId` is the session handle used by list/destroy.\n * `launch` query params are only meaningful if they land before the client\n * connects — they live on the connect URL by Browserless design.\n *\n * SECURITY: the returned URL embeds the account API token as a query\n * parameter. Treat it as a secret — do not log or persist it.\n */\nfunction buildConnectUrl(baseUrl: string, apiKey: string, trackingId: string, options?: CreateBrowserSessionOptions): string {\n const url = new URL(`${wsBase(baseUrl)}/chromium`);\n url.searchParams.set('token', apiKey);\n url.searchParams.set('trackingId', trackingId);\n if (!options) return url.toString();\n\n if (options.stealth) url.searchParams.set('stealth', 'true');\n if (options.viewport) {\n url.searchParams.set('launch', JSON.stringify({\n defaultViewport: { width: options.viewport.width, height: options.viewport.height },\n }));\n }\n if (options.proxies === true) url.searchParams.set('proxy', 'residential');\n\n if (options.keepAlive === true) {\n warnOnce('keepAlive', 'Browserless sessions live until disconnect or /kill; no keep-alive flag.');\n }\n if (options.recording === false) {\n warnOnce('recording', 'Session recording is managed server-side; no per-session toggle on the connect URL.');\n }\n if (options.logging === false) {\n warnOnce('logging', 'Session logging is managed server-side; no per-session toggle on the connect URL.');\n }\n if (options.timeout !== undefined) {\n warnOnce('timeout', 'Session TTL is a fleet-level setting on Browserless; no per-session timeout param.');\n }\n if (options.profileId !== undefined) {\n warnOnce('profileId', 'Browserless has no persistent-profile concept on the connect URL.');\n }\n if (options.extensionIds && options.extensionIds.length > 0) {\n warnOnce('extensionIds', 'Extensions must be baked into the Browserless fleet image; no per-session IDs.');\n }\n if (options.region !== undefined) {\n warnOnce('region', 'Region is selected by the baseUrl host (e.g. production-sfo|lon|ams), not a connect param.');\n }\n if (options.saveDownloads === true) {\n warnOnce('saveDownloads', 'Browserless has no server-side download retrieval API.');\n }\n if (options.userMetadata && Object.keys(options.userMetadata).length > 0) {\n warnOnce('userMetadata', 'Browserless does not support arbitrary user metadata on sessions.');\n }\n if (Array.isArray(options.proxies) && options.proxies.length > 0) {\n warnOnce('proxies', 'Custom proxy servers are a Browserless enterprise feature; only proxies=true maps (residential).');\n }\n\n return url.toString();\n}\n\nasync function listSessions(baseUrl: string, apiKey: string): Promise<BrowserlessSession[]> {\n const r = await fetch(`${baseUrl}/sessions?token=${enc(apiKey)}`);\n if (!r.ok) throw new Error(`browserless /sessions ${r.status}: ${redact(await r.text(), apiKey)}`);\n return (await r.json()) as BrowserlessSession[];\n}\n\nfunction toEntry(s: BrowserlessSession) {\n return {\n session: s,\n sessionId: s.trackingId ?? s.browserId,\n status: (s.running ? 'running' : 'created') as 'running' | 'created',\n };\n}\n\n/**\n * Create a Browserless browser provider instance.\n *\n * @example\n * ```ts\n * import { browserless } from '@computesdk/browserless';\n * import { chromium } from 'playwright-core';\n *\n * const bl = browserless({ apiKey: process.env.BROWSERLESS_API_KEY });\n *\n * // Mints a trackingId; the session starts when the socket opens.\n * const session = await bl.session.create();\n *\n * const browser = await chromium.connectOverCDP(session.connectUrl);\n * const page = browser.contexts()[0].pages()[0];\n * await page.goto('https://example.com');\n *\n * await session.destroy(); // or just browser.close() — disconnect ends it\n * ```\n */\nexport const browserless = defineBrowserProvider<BrowserlessSession, BrowserlessConfig>({\n name: 'browserless',\n methods: {\n session: {\n create: async (config: BrowserlessConfig, options?: CreateBrowserSessionOptions) => {\n const { apiKey, baseUrl } = resolveConfig(config);\n // No REST create call — the trackingId becomes the session identity\n // and the browser allocates when the client connects to the URL.\n const trackingId = randomUUID();\n if (options) sessionOptions.set(trackingId, options);\n // Nothing exists server-side yet — running stays false until the\n // client opens the websocket and Browserless allocates the browser.\n return {\n session: { browserId: trackingId, trackingId, running: false, startedOn: Date.now() },\n sessionId: trackingId,\n connectUrl: buildConnectUrl(baseUrl, apiKey, trackingId, options),\n status: 'created',\n };\n },\n\n getById: async (config: BrowserlessConfig, sessionId: string) => {\n const { apiKey, baseUrl } = resolveConfig(config);\n const found = (await listSessions(baseUrl, apiKey)).find(\n (s) => s.trackingId === sessionId || s.browserId === sessionId,\n );\n if (!found) return null;\n const trackingId = found.trackingId ?? sessionId;\n return { ...toEntry(found), connectUrl: buildConnectUrl(baseUrl, apiKey, trackingId, sessionOptions.get(trackingId)) };\n },\n\n list: async (config: BrowserlessConfig) => {\n const { apiKey, baseUrl } = resolveConfig(config);\n return (await listSessions(baseUrl, apiKey)).map(toEntry);\n },\n\n destroy: async (config: BrowserlessConfig, sessionId: string) => {\n const { apiKey, baseUrl } = resolveConfig(config);\n // /kill accepts a browserId or trackingId; a missing session is a no-op.\n const r = await fetch(`${baseUrl}/kill/${enc(sessionId)}?token=${enc(apiKey)}`);\n sessionOptions.delete(sessionId);\n if (!r.ok && r.status !== 404) {\n throw new Error(`browserless destroy ${r.status}: ${redact(await r.text(), apiKey)}`);\n }\n },\n\n getConnectUrl: async (config: BrowserlessConfig, sessionId: string) => {\n const { apiKey, baseUrl } = resolveConfig(config);\n return buildConnectUrl(baseUrl, apiKey, sessionId, sessionOptions.get(sessionId));\n },\n },\n },\n});\n"],"mappings":"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","names":[]}
@@ -0,0 +1,63 @@
1
+ import * as _computesdk_provider from '@computesdk/provider';
2
+
3
+ /**
4
+ * Browserless Browser Provider - Factory-based Implementation
5
+ *
6
+ * Cloud browser sessions via Browserless (https://browserless.io). Browserless
7
+ * has no create-session REST call on the shared fleet: a session materializes
8
+ * when the client opens the CDP websocket. `session.create()` therefore mints
9
+ * a `trackingId` and returns the connect URL for it; the session starts when
10
+ * `chromium.connectOverCDP(connectUrl)` opens, and ends on disconnect or via
11
+ * `session.destroy()` (`/kill/<trackingId>`).
12
+ */
13
+ /**
14
+ * Shape of a running session as returned by `GET /sessions`.
15
+ * `/sessions` is available on private/enterprise fleets; on the shared fleet
16
+ * it may return 401 — list/getById surface that error rather than masking it.
17
+ */
18
+ interface BrowserlessSession {
19
+ browserId: string;
20
+ trackingId: string | null;
21
+ running: boolean;
22
+ startedOn: number;
23
+ timeAliveMs?: number;
24
+ browser?: string;
25
+ type?: string;
26
+ }
27
+ /**
28
+ * Browserless-specific configuration options.
29
+ */
30
+ interface BrowserlessConfig {
31
+ /** Browserless API token — falls back to BROWSERLESS_API_KEY env var */
32
+ apiKey?: string;
33
+ /**
34
+ * Fleet base URL — falls back to BROWSERLESS_URL env var, then the shared
35
+ * US West fleet. Shared fleet: production-sfo|lon|ams.browserless.io.
36
+ * Private fleet: chrome.browserless.io or chrome-<region>.browserless.io
37
+ * (a private-fleet token against a shared-fleet host returns 401).
38
+ */
39
+ baseUrl?: string;
40
+ }
41
+ /**
42
+ * Create a Browserless browser provider instance.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { browserless } from '@computesdk/browserless';
47
+ * import { chromium } from 'playwright-core';
48
+ *
49
+ * const bl = browserless({ apiKey: process.env.BROWSERLESS_API_KEY });
50
+ *
51
+ * // Mints a trackingId; the session starts when the socket opens.
52
+ * const session = await bl.session.create();
53
+ *
54
+ * const browser = await chromium.connectOverCDP(session.connectUrl);
55
+ * const page = browser.contexts()[0].pages()[0];
56
+ * await page.goto('https://example.com');
57
+ *
58
+ * await session.destroy(); // or just browser.close() — disconnect ends it
59
+ * ```
60
+ */
61
+ declare const browserless: (config: BrowserlessConfig) => _computesdk_provider.BrowserProvider<BrowserlessSession>;
62
+
63
+ export { type BrowserlessConfig, type BrowserlessSession, browserless };
@@ -0,0 +1,63 @@
1
+ import * as _computesdk_provider from '@computesdk/provider';
2
+
3
+ /**
4
+ * Browserless Browser Provider - Factory-based Implementation
5
+ *
6
+ * Cloud browser sessions via Browserless (https://browserless.io). Browserless
7
+ * has no create-session REST call on the shared fleet: a session materializes
8
+ * when the client opens the CDP websocket. `session.create()` therefore mints
9
+ * a `trackingId` and returns the connect URL for it; the session starts when
10
+ * `chromium.connectOverCDP(connectUrl)` opens, and ends on disconnect or via
11
+ * `session.destroy()` (`/kill/<trackingId>`).
12
+ */
13
+ /**
14
+ * Shape of a running session as returned by `GET /sessions`.
15
+ * `/sessions` is available on private/enterprise fleets; on the shared fleet
16
+ * it may return 401 — list/getById surface that error rather than masking it.
17
+ */
18
+ interface BrowserlessSession {
19
+ browserId: string;
20
+ trackingId: string | null;
21
+ running: boolean;
22
+ startedOn: number;
23
+ timeAliveMs?: number;
24
+ browser?: string;
25
+ type?: string;
26
+ }
27
+ /**
28
+ * Browserless-specific configuration options.
29
+ */
30
+ interface BrowserlessConfig {
31
+ /** Browserless API token — falls back to BROWSERLESS_API_KEY env var */
32
+ apiKey?: string;
33
+ /**
34
+ * Fleet base URL — falls back to BROWSERLESS_URL env var, then the shared
35
+ * US West fleet. Shared fleet: production-sfo|lon|ams.browserless.io.
36
+ * Private fleet: chrome.browserless.io or chrome-<region>.browserless.io
37
+ * (a private-fleet token against a shared-fleet host returns 401).
38
+ */
39
+ baseUrl?: string;
40
+ }
41
+ /**
42
+ * Create a Browserless browser provider instance.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { browserless } from '@computesdk/browserless';
47
+ * import { chromium } from 'playwright-core';
48
+ *
49
+ * const bl = browserless({ apiKey: process.env.BROWSERLESS_API_KEY });
50
+ *
51
+ * // Mints a trackingId; the session starts when the socket opens.
52
+ * const session = await bl.session.create();
53
+ *
54
+ * const browser = await chromium.connectOverCDP(session.connectUrl);
55
+ * const page = browser.contexts()[0].pages()[0];
56
+ * await page.goto('https://example.com');
57
+ *
58
+ * await session.destroy(); // or just browser.close() — disconnect ends it
59
+ * ```
60
+ */
61
+ declare const browserless: (config: BrowserlessConfig) => _computesdk_provider.BrowserProvider<BrowserlessSession>;
62
+
63
+ export { type BrowserlessConfig, type BrowserlessSession, browserless };
package/dist/index.js ADDED
@@ -0,0 +1,132 @@
1
+ // src/index.ts
2
+ import { randomUUID } from "crypto";
3
+ import { defineBrowserProvider } from "@computesdk/provider";
4
+ function resolveConfig(config) {
5
+ const apiKey = config.apiKey || typeof process !== "undefined" && process.env?.BROWSERLESS_API_KEY || "";
6
+ if (!apiKey) {
7
+ throw new Error(
8
+ `Missing Browserless API key. Provide 'apiKey' in config or set BROWSERLESS_API_KEY environment variable. Get your API key from https://account.browserless.io`
9
+ );
10
+ }
11
+ const baseUrl = (config.baseUrl || typeof process !== "undefined" && process.env?.BROWSERLESS_URL || "https://production-sfo.browserless.io").replace(/\/+$/, "");
12
+ return { apiKey, baseUrl };
13
+ }
14
+ function wsBase(baseUrl) {
15
+ return baseUrl.replace(/^https/, "wss").replace(/^http/, "ws");
16
+ }
17
+ function enc(value) {
18
+ return encodeURIComponent(value);
19
+ }
20
+ var warnedUnsupported = /* @__PURE__ */ new Set();
21
+ function warnOnce(field, reason) {
22
+ if (warnedUnsupported.has(field)) return;
23
+ warnedUnsupported.add(field);
24
+ console.warn(`[@computesdk/browserless] '${field}' is ignored: ${reason}`);
25
+ }
26
+ var sessionOptions = /* @__PURE__ */ new Map();
27
+ function redact(body, apiKey) {
28
+ return body.split(apiKey).join("***");
29
+ }
30
+ function buildConnectUrl(baseUrl, apiKey, trackingId, options) {
31
+ const url = new URL(`${wsBase(baseUrl)}/chromium`);
32
+ url.searchParams.set("token", apiKey);
33
+ url.searchParams.set("trackingId", trackingId);
34
+ if (!options) return url.toString();
35
+ if (options.stealth) url.searchParams.set("stealth", "true");
36
+ if (options.viewport) {
37
+ url.searchParams.set("launch", JSON.stringify({
38
+ defaultViewport: { width: options.viewport.width, height: options.viewport.height }
39
+ }));
40
+ }
41
+ if (options.proxies === true) url.searchParams.set("proxy", "residential");
42
+ if (options.keepAlive === true) {
43
+ warnOnce("keepAlive", "Browserless sessions live until disconnect or /kill; no keep-alive flag.");
44
+ }
45
+ if (options.recording === false) {
46
+ warnOnce("recording", "Session recording is managed server-side; no per-session toggle on the connect URL.");
47
+ }
48
+ if (options.logging === false) {
49
+ warnOnce("logging", "Session logging is managed server-side; no per-session toggle on the connect URL.");
50
+ }
51
+ if (options.timeout !== void 0) {
52
+ warnOnce("timeout", "Session TTL is a fleet-level setting on Browserless; no per-session timeout param.");
53
+ }
54
+ if (options.profileId !== void 0) {
55
+ warnOnce("profileId", "Browserless has no persistent-profile concept on the connect URL.");
56
+ }
57
+ if (options.extensionIds && options.extensionIds.length > 0) {
58
+ warnOnce("extensionIds", "Extensions must be baked into the Browserless fleet image; no per-session IDs.");
59
+ }
60
+ if (options.region !== void 0) {
61
+ warnOnce("region", "Region is selected by the baseUrl host (e.g. production-sfo|lon|ams), not a connect param.");
62
+ }
63
+ if (options.saveDownloads === true) {
64
+ warnOnce("saveDownloads", "Browserless has no server-side download retrieval API.");
65
+ }
66
+ if (options.userMetadata && Object.keys(options.userMetadata).length > 0) {
67
+ warnOnce("userMetadata", "Browserless does not support arbitrary user metadata on sessions.");
68
+ }
69
+ if (Array.isArray(options.proxies) && options.proxies.length > 0) {
70
+ warnOnce("proxies", "Custom proxy servers are a Browserless enterprise feature; only proxies=true maps (residential).");
71
+ }
72
+ return url.toString();
73
+ }
74
+ async function listSessions(baseUrl, apiKey) {
75
+ const r = await fetch(`${baseUrl}/sessions?token=${enc(apiKey)}`);
76
+ if (!r.ok) throw new Error(`browserless /sessions ${r.status}: ${redact(await r.text(), apiKey)}`);
77
+ return await r.json();
78
+ }
79
+ function toEntry(s) {
80
+ return {
81
+ session: s,
82
+ sessionId: s.trackingId ?? s.browserId,
83
+ status: s.running ? "running" : "created"
84
+ };
85
+ }
86
+ var browserless = defineBrowserProvider({
87
+ name: "browserless",
88
+ methods: {
89
+ session: {
90
+ create: async (config, options) => {
91
+ const { apiKey, baseUrl } = resolveConfig(config);
92
+ const trackingId = randomUUID();
93
+ if (options) sessionOptions.set(trackingId, options);
94
+ return {
95
+ session: { browserId: trackingId, trackingId, running: false, startedOn: Date.now() },
96
+ sessionId: trackingId,
97
+ connectUrl: buildConnectUrl(baseUrl, apiKey, trackingId, options),
98
+ status: "created"
99
+ };
100
+ },
101
+ getById: async (config, sessionId) => {
102
+ const { apiKey, baseUrl } = resolveConfig(config);
103
+ const found = (await listSessions(baseUrl, apiKey)).find(
104
+ (s) => s.trackingId === sessionId || s.browserId === sessionId
105
+ );
106
+ if (!found) return null;
107
+ const trackingId = found.trackingId ?? sessionId;
108
+ return { ...toEntry(found), connectUrl: buildConnectUrl(baseUrl, apiKey, trackingId, sessionOptions.get(trackingId)) };
109
+ },
110
+ list: async (config) => {
111
+ const { apiKey, baseUrl } = resolveConfig(config);
112
+ return (await listSessions(baseUrl, apiKey)).map(toEntry);
113
+ },
114
+ destroy: async (config, sessionId) => {
115
+ const { apiKey, baseUrl } = resolveConfig(config);
116
+ const r = await fetch(`${baseUrl}/kill/${enc(sessionId)}?token=${enc(apiKey)}`);
117
+ sessionOptions.delete(sessionId);
118
+ if (!r.ok && r.status !== 404) {
119
+ throw new Error(`browserless destroy ${r.status}: ${redact(await r.text(), apiKey)}`);
120
+ }
121
+ },
122
+ getConnectUrl: async (config, sessionId) => {
123
+ const { apiKey, baseUrl } = resolveConfig(config);
124
+ return buildConnectUrl(baseUrl, apiKey, sessionId, sessionOptions.get(sessionId));
125
+ }
126
+ }
127
+ }
128
+ });
129
+ export {
130
+ browserless
131
+ };
132
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Browserless Browser Provider - Factory-based Implementation\n *\n * Cloud browser sessions via Browserless (https://browserless.io). Browserless\n * has no create-session REST call on the shared fleet: a session materializes\n * when the client opens the CDP websocket. `session.create()` therefore mints\n * a `trackingId` and returns the connect URL for it; the session starts when\n * `chromium.connectOverCDP(connectUrl)` opens, and ends on disconnect or via\n * `session.destroy()` (`/kill/<trackingId>`).\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { defineBrowserProvider } from '@computesdk/provider';\n\nimport type { CreateBrowserSessionOptions } from '@computesdk/provider';\n\n/**\n * Shape of a running session as returned by `GET /sessions`.\n * `/sessions` is available on private/enterprise fleets; on the shared fleet\n * it may return 401 — list/getById surface that error rather than masking it.\n */\nexport interface BrowserlessSession {\n browserId: string;\n trackingId: string | null;\n running: boolean;\n startedOn: number;\n timeAliveMs?: number;\n browser?: string;\n type?: string;\n}\n\n/**\n * Browserless-specific configuration options.\n */\nexport interface BrowserlessConfig {\n /** Browserless API token — falls back to BROWSERLESS_API_KEY env var */\n apiKey?: string;\n /**\n * Fleet base URL — falls back to BROWSERLESS_URL env var, then the shared\n * US West fleet. Shared fleet: production-sfo|lon|ams.browserless.io.\n * Private fleet: chrome.browserless.io or chrome-<region>.browserless.io\n * (a private-fleet token against a shared-fleet host returns 401).\n */\n baseUrl?: string;\n}\n\nfunction resolveConfig(config: BrowserlessConfig) {\n const apiKey =\n config.apiKey || (typeof process !== 'undefined' && process.env?.BROWSERLESS_API_KEY) || '';\n if (!apiKey) {\n throw new Error(\n `Missing Browserless API key. Provide 'apiKey' in config or set BROWSERLESS_API_KEY environment variable. ` +\n `Get your API key from https://account.browserless.io`,\n );\n }\n const baseUrl = (\n config.baseUrl ||\n (typeof process !== 'undefined' && process.env?.BROWSERLESS_URL) ||\n 'https://production-sfo.browserless.io'\n ).replace(/\\/+$/, '');\n return { apiKey, baseUrl };\n}\n\n/** http(s) base URL → ws(s) equivalent for the websocket endpoint. */\nfunction wsBase(baseUrl: string): string {\n return baseUrl.replace(/^https/, 'wss').replace(/^http/, 'ws');\n}\n\nfunction enc(value: string): string {\n return encodeURIComponent(value);\n}\n\nconst warnedUnsupported = new Set<string>();\nfunction warnOnce(field: string, reason: string) {\n if (warnedUnsupported.has(field)) return;\n warnedUnsupported.add(field);\n console.warn(`[@computesdk/browserless] '${field}' is ignored: ${reason}`);\n}\n\n/**\n * Create options by sessionId, so getConnectUrl/getById can rebuild the same\n * URL the session was created with. Same-process only — a URL rebuilt in a\n * fresh process carries no launch options. Entries are dropped on destroy.\n */\nconst sessionOptions = new Map<string, CreateBrowserSessionOptions>();\n\n/** Strip the API token from provider error bodies before throwing. */\nfunction redact(body: string, apiKey: string): string {\n return body.split(apiKey).join('***');\n}\n\n/**\n * Build the CDP connect URL for a session. The token authenticates the\n * websocket; `trackingId` is the session handle used by list/destroy.\n * `launch` query params are only meaningful if they land before the client\n * connects — they live on the connect URL by Browserless design.\n *\n * SECURITY: the returned URL embeds the account API token as a query\n * parameter. Treat it as a secret — do not log or persist it.\n */\nfunction buildConnectUrl(baseUrl: string, apiKey: string, trackingId: string, options?: CreateBrowserSessionOptions): string {\n const url = new URL(`${wsBase(baseUrl)}/chromium`);\n url.searchParams.set('token', apiKey);\n url.searchParams.set('trackingId', trackingId);\n if (!options) return url.toString();\n\n if (options.stealth) url.searchParams.set('stealth', 'true');\n if (options.viewport) {\n url.searchParams.set('launch', JSON.stringify({\n defaultViewport: { width: options.viewport.width, height: options.viewport.height },\n }));\n }\n if (options.proxies === true) url.searchParams.set('proxy', 'residential');\n\n if (options.keepAlive === true) {\n warnOnce('keepAlive', 'Browserless sessions live until disconnect or /kill; no keep-alive flag.');\n }\n if (options.recording === false) {\n warnOnce('recording', 'Session recording is managed server-side; no per-session toggle on the connect URL.');\n }\n if (options.logging === false) {\n warnOnce('logging', 'Session logging is managed server-side; no per-session toggle on the connect URL.');\n }\n if (options.timeout !== undefined) {\n warnOnce('timeout', 'Session TTL is a fleet-level setting on Browserless; no per-session timeout param.');\n }\n if (options.profileId !== undefined) {\n warnOnce('profileId', 'Browserless has no persistent-profile concept on the connect URL.');\n }\n if (options.extensionIds && options.extensionIds.length > 0) {\n warnOnce('extensionIds', 'Extensions must be baked into the Browserless fleet image; no per-session IDs.');\n }\n if (options.region !== undefined) {\n warnOnce('region', 'Region is selected by the baseUrl host (e.g. production-sfo|lon|ams), not a connect param.');\n }\n if (options.saveDownloads === true) {\n warnOnce('saveDownloads', 'Browserless has no server-side download retrieval API.');\n }\n if (options.userMetadata && Object.keys(options.userMetadata).length > 0) {\n warnOnce('userMetadata', 'Browserless does not support arbitrary user metadata on sessions.');\n }\n if (Array.isArray(options.proxies) && options.proxies.length > 0) {\n warnOnce('proxies', 'Custom proxy servers are a Browserless enterprise feature; only proxies=true maps (residential).');\n }\n\n return url.toString();\n}\n\nasync function listSessions(baseUrl: string, apiKey: string): Promise<BrowserlessSession[]> {\n const r = await fetch(`${baseUrl}/sessions?token=${enc(apiKey)}`);\n if (!r.ok) throw new Error(`browserless /sessions ${r.status}: ${redact(await r.text(), apiKey)}`);\n return (await r.json()) as BrowserlessSession[];\n}\n\nfunction toEntry(s: BrowserlessSession) {\n return {\n session: s,\n sessionId: s.trackingId ?? s.browserId,\n status: (s.running ? 'running' : 'created') as 'running' | 'created',\n };\n}\n\n/**\n * Create a Browserless browser provider instance.\n *\n * @example\n * ```ts\n * import { browserless } from '@computesdk/browserless';\n * import { chromium } from 'playwright-core';\n *\n * const bl = browserless({ apiKey: process.env.BROWSERLESS_API_KEY });\n *\n * // Mints a trackingId; the session starts when the socket opens.\n * const session = await bl.session.create();\n *\n * const browser = await chromium.connectOverCDP(session.connectUrl);\n * const page = browser.contexts()[0].pages()[0];\n * await page.goto('https://example.com');\n *\n * await session.destroy(); // or just browser.close() — disconnect ends it\n * ```\n */\nexport const browserless = defineBrowserProvider<BrowserlessSession, BrowserlessConfig>({\n name: 'browserless',\n methods: {\n session: {\n create: async (config: BrowserlessConfig, options?: CreateBrowserSessionOptions) => {\n const { apiKey, baseUrl } = resolveConfig(config);\n // No REST create call — the trackingId becomes the session identity\n // and the browser allocates when the client connects to the URL.\n const trackingId = randomUUID();\n if (options) sessionOptions.set(trackingId, options);\n // Nothing exists server-side yet — running stays false until the\n // client opens the websocket and Browserless allocates the browser.\n return {\n session: { browserId: trackingId, trackingId, running: false, startedOn: Date.now() },\n sessionId: trackingId,\n connectUrl: buildConnectUrl(baseUrl, apiKey, trackingId, options),\n status: 'created',\n };\n },\n\n getById: async (config: BrowserlessConfig, sessionId: string) => {\n const { apiKey, baseUrl } = resolveConfig(config);\n const found = (await listSessions(baseUrl, apiKey)).find(\n (s) => s.trackingId === sessionId || s.browserId === sessionId,\n );\n if (!found) return null;\n const trackingId = found.trackingId ?? sessionId;\n return { ...toEntry(found), connectUrl: buildConnectUrl(baseUrl, apiKey, trackingId, sessionOptions.get(trackingId)) };\n },\n\n list: async (config: BrowserlessConfig) => {\n const { apiKey, baseUrl } = resolveConfig(config);\n return (await listSessions(baseUrl, apiKey)).map(toEntry);\n },\n\n destroy: async (config: BrowserlessConfig, sessionId: string) => {\n const { apiKey, baseUrl } = resolveConfig(config);\n // /kill accepts a browserId or trackingId; a missing session is a no-op.\n const r = await fetch(`${baseUrl}/kill/${enc(sessionId)}?token=${enc(apiKey)}`);\n sessionOptions.delete(sessionId);\n if (!r.ok && r.status !== 404) {\n throw new Error(`browserless destroy ${r.status}: ${redact(await r.text(), apiKey)}`);\n }\n },\n\n getConnectUrl: async (config: BrowserlessConfig, sessionId: string) => {\n const { apiKey, baseUrl } = resolveConfig(config);\n return buildConnectUrl(baseUrl, apiKey, sessionId, sessionOptions.get(sessionId));\n },\n },\n },\n});\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@computesdk/browserless",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.2",
4
+ "description": "Browserless browser provider for ComputeSDK - cloud browser sessions via Browserless",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "dependencies": {
21
+ "@computesdk/provider": "2.1.20",
22
+ "computesdk": "4.1.14"
23
+ },
24
+ "keywords": [
25
+ "computesdk",
26
+ "browserless",
27
+ "browser",
28
+ "headless",
29
+ "playwright",
30
+ "puppeteer",
31
+ "provider"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/computesdk/computesdk.git",
36
+ "directory": "packages/browserless"
37
+ },
38
+ "homepage": "https://www.browserless.io",
39
+ "bugs": {
40
+ "url": "https://github.com/computesdk/computesdk/issues"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^20.0.0",
44
+ "@vitest/coverage-v8": "^1.0.0",
45
+ "eslint": "^8.37.0",
46
+ "rimraf": "^5.0.0",
47
+ "tsup": "^8.0.0",
48
+ "typescript": "^5.0.0",
49
+ "vitest": "^1.0.0",
50
+ "dotenv": "^16.0.0",
51
+ "playwright-core": "^1.40.0",
52
+ "@computesdk/test-utils": "2.0.5"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup",
56
+ "clean": "rimraf dist",
57
+ "dev": "tsup --watch",
58
+ "test": "vitest run",
59
+ "typecheck": "tsc --noEmit",
60
+ "lint": "eslint",
61
+ "test:watch": "vitest watch",
62
+ "test:coverage": "vitest run --coverage"
63
+ }
6
64
  }