# Tiện ích trình duyệt

> Thêm tiện ích Chrome vào thư viện của không gian làm việc, duyệt quyền từng phiên bản, gán cho hồ sơ và xem tiện ích nào thật sự đã nạp.

Hồ sơ chỉ nạp tiện ích từ **thư viện tiện ích** của không gian làm việc. Không ai cài tiện ích thẳng vào một hồ sơ: tiện ích được thêm một lần, người có quyền duyệt xem chính xác mỗi phiên bản được làm gì, và lượt gán quyết định hồ sơ nào nạp nó. Cùng một tệp đã duyệt, cùng một mã tiện ích, đến mọi máy.

## Thêm tiện ích

Hai nguồn, cả hai đều do máy chủ Browlix tự đọc và kiểm — không bao giờ tin vào điều người dùng khai về tệp:

- **Chrome Web Store**: đường dẫn `chromewebstore.google.com` hoặc mã 32 chữ cái. Máy chủ tải bản dành cho phiên bản trình duyệt Browlix và đòi chữ ký của chính Chrome Web Store.
- **Tệp**: `.crx` đã ký, hoặc `.zip` của thư mục tiện ích (có `manifest.json`), tối đa 32 MB. CRX chỉ được nhận khi mọi chữ ký trong tệp khớp nội dung và mã tiện ích khớp khoá. ZIP không có chữ ký; Browlix tạo khoá cho nó để mã tiện ích giống nhau trên mọi máy.

Chỉ nhận tiện ích Manifest V3. Máy chủ đọc tên, phiên bản, quyền, quyền theo trang, quyền tuỳ chọn và tệp giấy phép (nếu có) từ chính gói.

```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": chưa hồ sơ nào nạp
```

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

Với tệp: xin một chỗ tải lên dùng một lần, `PUT` nguyên tệp vào đó, rồi thêm:

```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);
```

Thêm lại đúng tệp cũ sẽ trả về phiên bản đã có (`created: false`). Thêm **phiên bản mới** của một tiện ích bằng cách gửi kèm `package_id`; tệp của tiện ích khác bị từ chối.

## Duyệt: chấp nhận đúng những gì phiên bản thêm vào

Mọi phiên bản mới bắt đầu ở trạng thái **chưa duyệt** và không hồ sơ nào nạp. Duyệt là một quyền riêng (`extension.approve`), tách khỏi quyền thêm và gán (`extension.manage`).

- Với phiên bản đầu tiên, lượt duyệt phải nêu mọi quyền tiện ích xin.
- Với bản cập nhật, phải nêu đúng những quyền **thêm vào** so với phiên bản hồ sơ đang dùng (`added_permissions`). Chưa duyệt thì hồ sơ vẫn dùng bản cũ.
- Tệp khác với tệp đã có nhưng **cùng số phiên bản** được đánh dấu (`same_version_changed`) — hãy kiểm nguồn.

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

for (const ext of library) {
  const pending = ext.pending_version;
  if (!pending) continue;
  // cho người duyệt xem pending.permissions / pending.added_permissions, rồi:
  await bx.call("extensions.review", {
    path: { id: pending.id },
    body: { decision: "approve", acknowledged_permissions: pending.added_permissions ?? pending.permissions },
  });
}
```

Thiếu một quyền mà phiên bản thêm vào thì trả `VALIDATION_FAILED` kèm danh sách quyền đó. `activate: false` duyệt nhưng chưa chuyển hồ sơ sang bản mới; sau này `extensions.set_active` chọn phiên bản đã duyệt nào hồ sơ nạp (lùi hoặc tiến).

## Gán cho hồ sơ

Tiện ích được nạp ở mọi hồ sơ của không gian, của một nhóm, hoặc một hồ sơ — theo các lượt gán bao phủ hồ sơ đó:

```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); // tên tiện ích bắt buộc đang chặn lượt mở (null khi không có)
```

- **Bắt buộc** (mặc định): hồ sơ không mở khi không nạp được tiện ích — chưa có bản duyệt, hoặc không tải/kiểm được tệp. `sessions.start` trả `VALIDATION_FAILED` và nêu tên.
- **Tuỳ chọn**: trình duyệt mở mà không có tiện ích đó và phiên báo lý do.
- `extensions.set_state` với `disabled` dừng nạp ở mọi hồ sơ, kể cả nơi gán bắt buộc, cho tới khi bật lại.

Thay đổi có hiệu lực ở **lần mở tiếp theo** của mỗi hồ sơ; trình duyệt đang mở giữ nguyên những gì nó đã nạp.

## Máy chạy làm gì

Máy cần worker **0.15.0** trở lên để nạp tiện ích. Worker cũ hơn không bao giờ nhận tiện ích: hồ sơ có tiện ích bắt buộc không mở trên máy đó (`UNSUPPORTED`, hãy cập nhật worker), tiện ích tuỳ chọn được báo là bỏ qua.

Khi hồ sơ mở, chỉ máy đang giữ lượt mở đó nhận đường dẫn ngắn hạn tới từng tệp đã duyệt. Máy kiểm kích thước và SHA-256 mà thư viện đã duyệt, giải nén với khoá của chính tiện ích (để mã tiện ích — và dữ liệu tiện ích lưu — giống nhau giữa các máy), giữ bản đã kiểm cho lần mở sau rồi giao cho trình duyệt.

Sau đó `sessions.get` cho thấy báo cáo của chính máy cho từng tiện ích:

| Trạng thái | Nghĩa |
|---|---|
| `running` | đã thấy trang hoặc tiến trình nền của tiện ích trong trình duyệt |
| `loaded` | đã giao cho trình duyệt; chưa có phần nào chạy (nhiều tiện ích chỉ chạy trên một số trang) |
| `failed` | không tải, kiểm hoặc nạp được tệp (lý do nêu rõ) |
| `skipped` | không nạp: worker quá cũ, hoặc tiện ích tuỳ chọn không chuẩn bị được |

## Các lỗi có thể gặp

| Mã | Khi nào |
|---|---|
| `FORBIDDEN` | thêm/gán khi thiếu `extension.manage`, hoặc duyệt khi thiếu `extension.approve` |
| `VALIDATION_FAILED` | không phải tiện ích, thiếu `manifest.json`, quá 32 MB, tệp của tiện ích khác làm phiên bản mới, duyệt thiếu quyền thêm vào, hoặc tiện ích bắt buộc chặn lượt mở |
| `INTEGRITY_FAILED` | chữ ký hoặc mã tiện ích của CRX không khớp nội dung, hoặc bản tải từ cửa hàng không có chữ ký Chrome Web Store |
| `UNSUPPORTED` | Manifest V2, phiên bản cần Chromium mới hơn, hoặc worker cũ hơn 0.15.0 với tiện ích bắt buộc |
| `NOT_FOUND` | Chrome Web Store không cung cấp tiện ích đó cho trình duyệt Browlix |

Mọi quyết định đều có trong nhật ký kiểm toán: `extension.add`, `extension.approve` (kèm các quyền đã thêm), `extension.reject`, `extension.set_active`, `extension.assign`, `extension.unassign`, `extension.disable`, `extension.enable`, `extension.delete`.
