@solarisdk/browserbase-compat 0.1.0 → 0.2.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+ - `timeout`, `keepAlive` and `userMetadata` are sent to Solari (they were warned about and ignored in 0.1.0). `userMetadata` comes back on `create`, `retrieve` and `list`, and `sessions.list({ q })` filters on it.
5
+ - **A base URL that does not point at Solari is now refused.** `baseURL` and `SOLARI_BASE_URL` must be `https` on `getsolari.com` (or a subdomain) or a loopback address; anything else throws `BrowserbaseError` ("Refusing to send a Solari API key to …"). In 0.1.0 such a URL was used as given, which could send your `slr_` key to another host.
6
+ - Errors use Browserbase's class names: `NotFoundError`, `RateLimitError`, `BadRequestError`, `AuthenticationError`, `PermissionDeniedError`, `ConflictError`, `UnprocessableEntityError`, `InternalServerError` (all `APIError` subclasses), `APIConnectionError` and `APIConnectionTimeoutError`. Network failures used to surface as the raw `fetch` error.
7
+ - A saved context's cookies are also present in the browser's default context (`browser.contexts()[0]`).
8
+ - `viewport` at create remains a documented gap (warns; `strict` throws).
9
+ - Added `NOTICE`; README links to docs.getsolari.com.
10
+
11
+ ## 0.1.0
12
+ - First release.
package/COMPAT.md CHANGED
@@ -22,7 +22,7 @@ Keep it private: the session id inside it is the credential.
22
22
  | | |
23
23
  |---|---|
24
24
  | Key | `apiKey` option, else `SOLARI_API_KEY`, else `BROWSERBASE_API_KEY`. **A key that does not start with `slr_` is never used or sent** (it is probably a Browserbase secret): it is skipped with a warning that does not print it, and if no `slr_` key remains the constructor throws. |
25
- | Endpoint | `baseURL` option or `SOLARI_BASE_URL`; default `https://api.getsolari.com`. |
25
+ | Endpoint | `baseURL` option or `SOLARI_BASE_URL`; default `https://api.getsolari.com`. Must be `https` on `getsolari.com` (or a subdomain) or loopback, otherwise the constructor throws `BrowserbaseError` ("Refusing to send a Solari API key to …"): the key is never sent to another host. |
26
26
  | `projectId` | Accepted and ignored: Solari keys belong to an organisation, there are no projects. |
27
27
 
28
28
  ## Works
@@ -42,9 +42,9 @@ Keep it private: the session id inside it is the credential.
42
42
  ## Differs (documented, not errors)
43
43
  - **Recording is a DOM-event log, not video.** `sessions.recording.retrieve(id)` returns rrweb events once the replay has been uploaded (a few seconds after release; until then it throws `APIError` with `code: "ReplayPending"`). There is no HLS playlist and no MP4.
44
44
  - **Contexts store cookies and localStorage only** (Playwright `storageState`), not IndexedDB, service workers or autofill data.
45
- - **No choice of operating system or viewport at create time.** Set the viewport from your automation client.
46
- - **`keepAlive`**: a disconnected session is released about 45 s after its socket closes (reconnect with the same `connectUrl` inside that window). Sessions end at your plan's maximum session length.
47
- - **`timeout`, `userMetadata`, `keepAlive: true`, `viewport`, `blockAds: true`, `logSession: true`, `os`** are accepted with a one-line warning and have no effect. Pass `strict: true` to make them throw instead.
45
+ - **No choice of operating system or viewport at create time (known gap).** A `viewport` setting is accepted with a one-line warning and has no effect; set the viewport from your automation client (`page.setViewportSize`). `strict: true` makes it throw instead.
46
+ - **`timeout`, `keepAlive` and `userMetadata` work as in Browserbase.** `timeout` (seconds, at least 60) ends the session at that time, capped at your plan's maximum session length. `keepAlive: true` keeps the session after your client disconnects, until its `timeout` (default 1 hour), the plan maximum, or `sessions.update(..., REQUEST_RELEASE)`. `userMetadata` (up to 50 keys, 8 KB) is stored, returned by `retrieve`/`list`, and filterable with `sessions.list({ q: "user_metadata['key']:'value'" })` (conditions joined with `AND`).
47
+ - **`viewport`, `blockAds: true`, `logSession: true`, `os`** are accepted with a one-line warning and have no effect. Pass `strict: true` to make them throw instead.
48
48
 
49
49
  ## Contexts
50
50
  ```ts
@@ -59,7 +59,7 @@ const s2 = await bb.sessions.create({ browserSettings: { context: { id: ctx.id }
59
59
  const b2 = await chromium.connectOverCDP(s2.connectUrl);
60
60
  const page = await (await b2.newContext({ storageState: s2.storageState })).newPage();
61
61
  ```
62
- Two differences from Browserbase: Solari applies a saved context only to a browser context created *with* it (use `newContext({ storageState })`, not `browser.contexts()[0]`), and `persist: true` saves when you release **through the same client object** (it is remembered in memory).
62
+ Two differences from Browserbase: a saved context's **cookies** are also present in the default browser context (`browser.contexts()[0]`), but localStorage is applied only to a browser context created *with* the state (use `newContext({ storageState })` to get both), and `persist: true` saves when you release **through the same client object** (it is remembered in memory).
63
63
 
64
64
  ## Raises `SolariCompatUnsupported`
65
- Extensions (`extensionId`), the Selenium endpoint (`seleniumRemoteUrl` is always empty), downloads (`sessions.downloads`) and uploads (`sessions.uploads`) APIs, video replays (`sessions.replays`), session logs (`sessions.logs`), external/bring-your-own proxies, per-domain proxy rules (`domainPattern`), custom CA certificates, `ignoreCertificateErrors: true`, custom captcha selectors, `os: "mobile" | "tablet"`, a `region` other than `us-west-2` (or `us-west`), `sessions.list({ q })` (metadata queries), `contexts.update`, and the separate `projects`, `extensions`, `agents`, `fetchAPI` and `search` resources.
65
+ Extensions (`extensionId`), the Selenium endpoint (`seleniumRemoteUrl` is always empty), downloads (`sessions.downloads`) and uploads (`sessions.uploads`) APIs, video replays (`sessions.replays`), session logs (`sessions.logs`), external/bring-your-own proxies, per-domain proxy rules (`domainPattern`), custom CA certificates, `ignoreCertificateErrors: true`, custom captcha selectors, `os: "mobile" | "tablet"`, a `region` other than `us-west-2` (or `us-west`), `contexts.update`, and the separate `projects`, `extensions`, `agents`, `fetchAPI` and `search` resources.
package/NOTICE ADDED
@@ -0,0 +1,10 @@
1
+ Solari Browserbase compatibility SDK
2
+ Copyright (c) 2026 Solari. Licensed under the MIT License (see LICENSE).
3
+
4
+ This package implements a client whose method names and parameters follow the
5
+ publicly documented Browserbase SDK, so that code written for that SDK can run
6
+ against Solari Browser. It contains no Browserbase source code.
7
+
8
+ It is not affiliated with, endorsed by, or sponsored by Browserbase, Inc.
9
+ "Browserbase" is a trademark of its owner and is used only to describe API
10
+ compatibility.
package/README.md CHANGED
@@ -20,7 +20,9 @@ await bb.sessions.update(session.id, { status: "REQUEST_RELEASE" });
20
20
  ```
21
21
 
22
22
  Sessions, contexts, live view and recording are supported; extensions, downloads/uploads APIs, video replays,
23
- bring-your-own proxies and Selenium raise `SolariCompatUnsupported`. See [COMPAT.md](./COMPAT.md) for the full list
23
+ bring-your-own proxies and Selenium raise `SolariCompatUnsupported`. See [docs.getsolari.com/browserbase](https://docs.getsolari.com/browserbase) (and `COMPAT.md` in this package) for the full list
24
24
  and every difference. Requires Node 20+. No Playwright version pin is needed: `connectUrl` is a plain CDP endpoint.
25
25
 
26
- Licence: MIT.
26
+ Licence: MIT. See `NOTICE`.
27
+
28
+ This is an independent project. It is not affiliated with, endorsed by, or sponsored by Browserbase, Inc. "Browserbase" is a trademark of its owner and is used here only to describe API compatibility.
package/dist/client.d.ts CHANGED
@@ -15,6 +15,8 @@ export interface BrowserbaseOptions {
15
15
  /** Test hook. */
16
16
  fetch?: typeof fetch;
17
17
  }
18
+ /** Whether a base URL may receive a Solari key: https on getsolari.com or a subdomain, or loopback. */
19
+ export declare function isSolariBaseUrl(raw: string): boolean;
18
20
  export declare class Browserbase {
19
21
  readonly sessions: Sessions;
20
22
  readonly contexts: Contexts;
package/dist/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { gunzipSync } from "node:zlib";
2
- import { APIError, BrowserbaseError, SolariCompatUnsupported } from "./errors.js";
2
+ import { APIError, BrowserbaseError, SolariCompatUnsupported, apiErrorFor, connectionErrorFor } from "./errors.js";
3
3
  import { mapCreateParams, mapStatus } from "./mapping.js";
4
4
  export const DEFAULT_BASE_URL = "https://api.getsolari.com";
5
5
  const REGION = "us-west-2"; // reported for Browserbase-shaped objects; Solari's single region (us-west)
@@ -19,6 +19,40 @@ function resolveApiKey(explicit, warn) {
19
19
  }
20
20
  throw new BrowserbaseError("No Solari API key: pass apiKey or set SOLARI_API_KEY (keys start with slr_)");
21
21
  }
22
+ /** Whether a base URL may receive a Solari key: https on getsolari.com or a subdomain, or loopback. */
23
+ export function isSolariBaseUrl(raw) {
24
+ let u;
25
+ try {
26
+ u = new URL(raw);
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ if (u.username || u.password)
32
+ return false; // never a URL with credentials in it
33
+ const host = u.hostname.toLowerCase().replace(/\.$/, "");
34
+ if (u.protocol === "https:" && (host === "getsolari.com" || host.endsWith(".getsolari.com")))
35
+ return true;
36
+ const loopback = host === "localhost" || host === "[::1]" || /^127(\.\d{1,3}){3}$/.test(host);
37
+ return loopback && (u.protocol === "http:" || u.protocol === "https:");
38
+ }
39
+ function describeHost(raw) {
40
+ try {
41
+ return new URL(raw).hostname || "unparsable URL";
42
+ }
43
+ catch {
44
+ return "unparsable URL";
45
+ }
46
+ }
47
+ function resolveBaseUrl(explicit) {
48
+ const given = explicit || process.env.SOLARI_BASE_URL;
49
+ if (!given)
50
+ return DEFAULT_BASE_URL;
51
+ if (!isSolariBaseUrl(given)) {
52
+ throw new BrowserbaseError(`Refusing to send a Solari API key to ${describeHost(given)}: it does not point at Solari (getsolari.com or loopback).`);
53
+ }
54
+ return given.replace(/\/+$/, "");
55
+ }
22
56
  function sleep(ms) {
23
57
  return new Promise((r) => setTimeout(r, ms));
24
58
  }
@@ -43,7 +77,7 @@ export class Browserbase {
43
77
  this.warn = opts.onWarning ?? ((m) => console.warn(`[solari-browserbase-compat] ${m}`));
44
78
  // Non-enumerable so console.log(client) / JSON.stringify(client) never print the key.
45
79
  Object.defineProperty(this, "apiKey", { value: resolveApiKey(opts.apiKey, this.warn), enumerable: false });
46
- this.baseURL = (opts.baseURL ?? process.env.SOLARI_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
80
+ this.baseURL = resolveBaseUrl(opts.baseURL);
47
81
  this.strict = opts.strict === true;
48
82
  this.fetchImpl = opts.fetch ?? fetch;
49
83
  this.maxRetries = opts.maxRetries ?? 2;
@@ -92,12 +126,12 @@ export class Browserbase {
92
126
  catch (e) {
93
127
  clearTimeout(timer);
94
128
  if (!replaySafe || attempt === this.maxRetries)
95
- throw e;
129
+ throw connectionErrorFor(e);
96
130
  lastErr = e;
97
131
  }
98
132
  await sleep(300 * (attempt + 1));
99
133
  }
100
- throw lastErr instanceof Error ? lastErr : new BrowserbaseError("request failed");
134
+ throw connectionErrorFor(lastErr ?? new Error("request failed"));
101
135
  }
102
136
  /** @internal */
103
137
  async json(method, path, body, opts) {
@@ -114,7 +148,7 @@ export class Browserbase {
114
148
  }
115
149
  if (!res.ok) {
116
150
  const o = (parsed ?? {});
117
- throw new APIError(res.status, `Solari ${method} ${path}: ${res.status} ${o.error ?? text.slice(0, 200)}`, o.code, parsed);
151
+ throw apiErrorFor(res.status, `Solari ${method} ${path}: ${res.status} ${o.error ?? text.slice(0, 200)}`, o.code, parsed);
118
152
  }
119
153
  return parsed;
120
154
  }
@@ -157,6 +191,7 @@ function toSession(row, extra = {}) {
157
191
  status,
158
192
  proxyBytes: 0,
159
193
  keepAlive: extra.keepAlive ?? false,
194
+ ...(row.userMetadata ? { userMetadata: row.userMetadata } : {}),
160
195
  ...(extra.contextId ? { contextId: extra.contextId } : {}),
161
196
  region: REGION,
162
197
  };
@@ -190,7 +225,13 @@ class Sessions {
190
225
  if (mapped.persistProfileId)
191
226
  this.c.persist.set(data.sessionId, mapped.persistProfileId);
192
227
  const ctx = params?.browserSettings?.context?.id;
193
- const s = toSession({ id: data.sessionId, status: "active", createdAt: new Date().toISOString(), ...(data.expiresAt ? { expiresAt: data.expiresAt } : {}) }, { keepAlive: params?.keepAlive === true, ...(ctx ? { contextId: ctx } : {}), status: "RUNNING" });
228
+ const s = toSession({
229
+ id: data.sessionId,
230
+ status: "active",
231
+ createdAt: new Date().toISOString(),
232
+ ...(data.expiresAt ? { expiresAt: data.expiresAt } : {}),
233
+ ...(data.userMetadata ? { userMetadata: data.userMetadata } : {}),
234
+ }, { keepAlive: params?.keepAlive === true, ...(ctx ? { contextId: ctx } : {}), status: "RUNNING" });
194
235
  const out = withConnect(s, data.cdpEndpoint);
195
236
  if (ctx) {
196
237
  let state = data.storageState ?? null;
@@ -210,9 +251,7 @@ class Sessions {
210
251
  return withConnect(toSession({ ...row, id: row.id ?? id }), row.cdpEndpoint ?? "");
211
252
  }
212
253
  async list(query) {
213
- if (query?.q)
214
- throw new SolariCompatUnsupported("sessions.list q (userMetadata query)", "metadata is not stored");
215
- const data = await this.c.json("GET", "/sessions");
254
+ const data = await this.c.json("GET", query?.q ? `/sessions?q=${encodeURIComponent(query.q)}` : "/sessions");
216
255
  const all = (data?.sessions ?? []).map((r) => toSession(r));
217
256
  return query?.status ? all.filter((s) => s.status === query.status) : all;
218
257
  }
package/dist/errors.d.ts CHANGED
@@ -9,6 +9,43 @@ export declare class APIError extends BrowserbaseError {
9
9
  readonly body: unknown;
10
10
  constructor(status: number, message: string, code?: string, body?: unknown);
11
11
  }
12
+ /** 400 */
13
+ export declare class BadRequestError extends APIError {
14
+ }
15
+ /** 401 */
16
+ export declare class AuthenticationError extends APIError {
17
+ }
18
+ /** 403 */
19
+ export declare class PermissionDeniedError extends APIError {
20
+ }
21
+ /** 404 */
22
+ export declare class NotFoundError extends APIError {
23
+ }
24
+ /** 409 */
25
+ export declare class ConflictError extends APIError {
26
+ }
27
+ /** 422 */
28
+ export declare class UnprocessableEntityError extends APIError {
29
+ }
30
+ /** 429 */
31
+ export declare class RateLimitError extends APIError {
32
+ }
33
+ /** 5xx */
34
+ export declare class InternalServerError extends APIError {
35
+ }
36
+ /** The request never produced an HTTP response (DNS, refused, reset, ...). Same name as Browserbase's SDK. */
37
+ export declare class APIConnectionError extends BrowserbaseError {
38
+ readonly cause: unknown;
39
+ constructor(message: string, cause?: unknown);
40
+ }
41
+ /** The request timed out. */
42
+ export declare class APIConnectionTimeoutError extends APIConnectionError {
43
+ constructor(message?: string, cause?: unknown);
44
+ }
45
+ /** The APIError subclass for an HTTP status, as Browserbase's SDK raises them. */
46
+ export declare function apiErrorFor(status: number, message: string, code?: string, body?: unknown): APIError;
47
+ /** A fetch failure as a Browserbase-style connection error (abort/timeout vs. everything else). */
48
+ export declare function connectionErrorFor(e: unknown): BrowserbaseError;
12
49
  /** A Browserbase feature Solari Browser does not offer (see COMPAT.md). Never silently ignored when it would change results. */
13
50
  export declare class SolariCompatUnsupported extends BrowserbaseError {
14
51
  readonly feature: string;
package/dist/errors.js CHANGED
@@ -17,6 +17,66 @@ export class APIError extends BrowserbaseError {
17
17
  this.body = body;
18
18
  }
19
19
  }
20
+ /** 400 */
21
+ export class BadRequestError extends APIError {
22
+ }
23
+ /** 401 */
24
+ export class AuthenticationError extends APIError {
25
+ }
26
+ /** 403 */
27
+ export class PermissionDeniedError extends APIError {
28
+ }
29
+ /** 404 */
30
+ export class NotFoundError extends APIError {
31
+ }
32
+ /** 409 */
33
+ export class ConflictError extends APIError {
34
+ }
35
+ /** 422 */
36
+ export class UnprocessableEntityError extends APIError {
37
+ }
38
+ /** 429 */
39
+ export class RateLimitError extends APIError {
40
+ }
41
+ /** 5xx */
42
+ export class InternalServerError extends APIError {
43
+ }
44
+ /** The request never produced an HTTP response (DNS, refused, reset, ...). Same name as Browserbase's SDK. */
45
+ export class APIConnectionError extends BrowserbaseError {
46
+ cause;
47
+ constructor(message, cause) {
48
+ super(message);
49
+ this.cause = cause;
50
+ }
51
+ }
52
+ /** The request timed out. */
53
+ export class APIConnectionTimeoutError extends APIConnectionError {
54
+ constructor(message = "Request timed out.", cause) {
55
+ super(message, cause);
56
+ }
57
+ }
58
+ /** The APIError subclass for an HTTP status, as Browserbase's SDK raises them. */
59
+ export function apiErrorFor(status, message, code, body) {
60
+ const C = status === 400 ? BadRequestError
61
+ : status === 401 ? AuthenticationError
62
+ : status === 403 ? PermissionDeniedError
63
+ : status === 404 ? NotFoundError
64
+ : status === 409 ? ConflictError
65
+ : status === 422 ? UnprocessableEntityError
66
+ : status === 429 ? RateLimitError
67
+ : status >= 500 ? InternalServerError
68
+ : APIError;
69
+ return new C(status, message, code, body);
70
+ }
71
+ /** A fetch failure as a Browserbase-style connection error (abort/timeout vs. everything else). */
72
+ export function connectionErrorFor(e) {
73
+ if (e instanceof BrowserbaseError)
74
+ return e;
75
+ const name = e?.name;
76
+ if (name === "AbortError" || name === "TimeoutError")
77
+ return new APIConnectionTimeoutError("Request timed out.", e);
78
+ return new APIConnectionError(`Connection error: ${e?.message ?? "request failed"}`, e);
79
+ }
20
80
  /** A Browserbase feature Solari Browser does not offer (see COMPAT.md). Never silently ignored when it would change results. */
21
81
  export class SolariCompatUnsupported extends BrowserbaseError {
22
82
  feature;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Browserbase } from "./client.js";
2
2
  export { Browserbase, DEFAULT_BASE_URL } from "./client.js";
3
3
  export type { BrowserbaseOptions, UnsupportedResource } from "./client.js";
4
- export { APIError, BrowserbaseError, SolariCompatUnsupported } from "./errors.js";
4
+ export { APIConnectionError, APIConnectionTimeoutError, APIError, AuthenticationError, BadRequestError, BrowserbaseError, ConflictError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, SolariCompatUnsupported, UnprocessableEntityError, } from "./errors.js";
5
5
  export { mapCreateParams, mapStatus } from "./mapping.js";
6
6
  export type * from "./types.js";
7
7
  export default Browserbase;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Browserbase } from "./client.js";
2
2
  export { Browserbase, DEFAULT_BASE_URL } from "./client.js";
3
- export { APIError, BrowserbaseError, SolariCompatUnsupported } from "./errors.js";
3
+ export { APIConnectionError, APIConnectionTimeoutError, APIError, AuthenticationError, BadRequestError, BrowserbaseError, ConflictError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, SolariCompatUnsupported, UnprocessableEntityError, } from "./errors.js";
4
4
  export { mapCreateParams, mapStatus } from "./mapping.js";
5
5
  export default Browserbase;
package/dist/mapping.js CHANGED
@@ -62,14 +62,8 @@ export function mapCreateParams(params, strict) {
62
62
  throw new SolariCompatUnsupported(`region ${p.region}`, "Solari Browser runs in one region (us-west)");
63
63
  }
64
64
  // ── soft gaps ────────────────────────────────────────────────────────────────
65
- if (p.timeout !== undefined)
66
- soft("timeout", "sessions end at your plan's maximum session length, not at this value; call sessions.update to release earlier");
67
- if (p.keepAlive === true)
68
- soft("keepAlive", "a disconnected session is released about 45 s after the socket closes; reconnect inside that window or hold the connection");
69
- if (p.userMetadata !== undefined)
70
- soft("userMetadata", "metadata is not stored or queryable");
71
65
  if (bs.viewport !== undefined)
72
- soft("browserSettings.viewport", "set the viewport from your automation client (page.setViewportSize)");
66
+ soft("browserSettings.viewport", "known gap: the viewport cannot be set at create; call page.setViewportSize() from your automation client");
73
67
  if (bs.blockAds === true)
74
68
  soft("browserSettings.blockAds", "ad blocking is not available");
75
69
  if (bs.logSession === true)
@@ -94,6 +88,12 @@ export function mapCreateParams(params, strict) {
94
88
  body.allowedDomains = allowedDomains;
95
89
  if (bs.context?.id)
96
90
  body.profileId = bs.context.id;
91
+ if (p.timeout !== undefined)
92
+ body.timeoutSeconds = p.timeout; // the gateway validates (>= 60) and clamps to the plan maximum
93
+ if (p.keepAlive === true)
94
+ body.keepAlive = true;
95
+ if (p.userMetadata !== undefined)
96
+ body.userMetadata = p.userMetadata;
97
97
  return {
98
98
  body,
99
99
  ...(bs.context?.id && bs.context.persist === true ? { persistProfileId: bs.context.id } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solarisdk/browserbase-compat",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Drop-in Browserbase-style SDK surface for Solari Browser: sessions, connectUrl (CDP), contexts, live view, replay events.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,7 +12,7 @@
12
12
  "import": "./dist/index.js"
13
13
  }
14
14
  },
15
- "files": ["dist", "README.md", "COMPAT.md", "LICENSE"],
15
+ "files": ["dist", "README.md", "COMPAT.md", "CHANGELOG.md", "LICENSE", "NOTICE"],
16
16
  "keywords": ["browserbase", "solari", "browser", "playwright", "puppeteer", "cdp", "compat"],
17
17
  "engines": { "node": ">=20" },
18
18
  "publishConfig": { "access": "public" },