# Browser extensions

> Add Chrome extensions to a workspace library, approve each version's permissions, assign them to profiles, and see what really loaded.

Profiles load extensions only from the workspace's **extension library**. Nobody installs an extension straight into a profile: an extension is added once, a person with the approve right reviews exactly what each version is allowed to do, and assignments decide which profiles load it. The same approved file, with the same extension ID, reaches every machine.

## Add an extension

Two sources, both read and verified by the Browlix server — never from what someone typed about the file:

- **Chrome Web Store**: a `chromewebstore.google.com` link or the 32-letter ID. The server downloads the build made for the Browlix browser version and requires the Chrome Web Store's own signature.
- **A file**: a signed `.crx`, or a `.zip` of the extension folder (with `manifest.json`), at most 32 MB. A CRX is accepted only when every signature in it matches its contents and its ID matches its key. A ZIP has no signature; Browlix gives it a key so its ID is the same on every machine.

Only Manifest V3 extensions are accepted. The server reads the name, version, permissions, host permissions, optional permissions and any licence file from the package itself.

```ts tab=TypeScript
const { data } = await bx.call("extensions.add", {
  body: { source: "store", store: "https://chromewebstore.google.com/detail/json-formatter/bcjindcccaagfpapjjmafapmmgkkhgoa" },
});
console.log(data.package.state); // "unreviewed": no profile loads it yet
```

```json tab=MCP
{
  "name": "extensions_add",
  "arguments": { "source": "store", "store": "bcjindcccaagfpapjjmafapmmgkkhgoa" }
}
```

For a file, ask for a one-time upload place, `PUT` the raw bytes there, then add it:

```ts tab=TypeScript
import { readFile } from "node:fs/promises";

const slot = await bx.call("extensions.upload", {});
await fetch(slot.data.upload_url, {
  method: "PUT",
  headers: { "Content-Type": "application/octet-stream" },
  body: await readFile("./my-extension.zip"),
});
const added = await bx.call("extensions.add", { body: { source: "upload", upload_path: slot.data.upload_path } });
console.log(added.data.package.chrome_id);
```

Adding the exact same file again returns the existing version (`created: false`). Add a **new version** of an extension by passing its `package_id`; a file of a different extension is refused.

## Review: approve exactly what a version adds

Every new version starts **unreviewed** and no profile loads it. Approving it is a separate right (`extension.approve`) from adding and assigning (`extension.manage`).

- For a first version, the approval must name every permission it asks for.
- For an update, it must name exactly the permissions it **adds** over the version profiles load now (`added_permissions`). Until it is approved, profiles keep the old version.
- A file that differs from one already in the library under the **same version number** is flagged (`same_version_changed`) — check where it came from.

```ts tab=TypeScript
const { data: library } = await bx.call("extensions.list", {});

for (const ext of library) {
  const pending = ext.pending_version;
  if (!pending) continue;
  // show pending.permissions / pending.added_permissions to the person approving, then:
  await bx.call("extensions.review", {
    path: { id: pending.id },
    body: { decision: "approve", acknowledged_permissions: pending.added_permissions ?? pending.permissions },
  });
}
```

Leaving out a permission the version adds is `VALIDATION_FAILED`, and the answer lists them. `activate: false` approves without switching profiles to it yet; `extensions.set_active` later chooses which approved version profiles load (roll back or forward).

## Assign to profiles

An extension loads in every profile of the workspace, of one group, or one profile — whichever assignments cover it:

```ts tab=TypeScript
const formatter = (await bx.call("extensions.list", {})).data.find((e) => e.name === "JSON Formatter");

if (formatter) {
  await bx.call("extensions.assign", {
    path: { id: formatter.id },
    body: { target_kind: "profile", target_id: profileId, required: true },
  });
}

const loads = await bx.call("profiles.extensions", { path: { id: profileId } });
console.log(loads.extra.blocked_by); // names of the required ones blocking its start (null when none)
```

- **Required** (the default): the profile does not open when the extension cannot be loaded — no approved version yet, or its file cannot be fetched and checked. `sessions.start` answers `VALIDATION_FAILED` and names it.
- **Optional**: the browser starts without it and the session reports why.
- `extensions.set_state` with `disabled` stops every profile loading it, required ones included, until it is enabled again.

Changes take effect at each profile's **next start**; a browser that is already open keeps what it started with.

## What the machine does

A machine needs worker **0.15.0** or newer to load extensions. An older worker never receives them: a profile with a required extension does not start there (`UNSUPPORTED`, update the worker), optional ones are reported as skipped.

When a profile starts, only the machine holding that start gets a short-lived link to each approved file. It checks the size and SHA-256 the library approved, unpacks it with the extension's own key (so the ID — and the extension's stored data — stay the same across machines), keeps a verified copy for the next start and hands it to the browser.

`sessions.get` then shows the machine's own report for each extension:

| State | Meaning |
|---|---|
| `running` | a page or background worker of the extension was seen in the browser |
| `loaded` | handed to the browser; nothing of it has run yet (many extensions only run on some sites) |
| `failed` | the file could not be fetched, checked or loaded (the reason says which) |
| `skipped` | not loaded: the worker is too old, or the extension was optional and could not be prepared |

## Errors you will see

| Code | When |
|---|---|
| `FORBIDDEN` | adding/assigning without `extension.manage`, or approving without `extension.approve` |
| `VALIDATION_FAILED` | not an extension, no `manifest.json`, over 32 MB, a file of a different extension as a new version, an approval missing added permissions, or a required extension blocking a start |
| `INTEGRITY_FAILED` | a CRX signature or ID that does not match its contents, or a store download without the Chrome Web Store signature |
| `UNSUPPORTED` | Manifest V2, a version that needs a newer Chromium, or a worker older than 0.15.0 for a required extension |
| `NOT_FOUND` | the Chrome Web Store does not offer that extension for the Browlix browser |

Every decision is in the audit log: `extension.add`, `extension.approve` (with the permissions it added), `extension.reject`, `extension.set_active`, `extension.assign`, `extension.unassign`, `extension.disable`, `extension.enable`, `extension.delete`.
