# TikTok video 403 — hiển thị & tải bytes từ context extension

> Tài liệu cho agent: vì sao video TikTok **không hiển thị / không fetch được** từ trang
> extension (`chrome-extension://…`) và cách AutoShopee Kit xử lý. Áp dụng cho mọi feature
> đụng tới video TikTok: **Tải video** (download) và **Repost lên Shopee Video** (Compose).

## Triệu chứng

- `<video :src="tiktokCdnUrl">` trong popup/options extension → khung **đen**, time `0:00`.
- `fetch(tiktokCdnUrl)` từ trang extension → **HTTP 403 Forbidden** (không phải lỗi mạng).
- URL có dạng: `https://v16-webapp-prime.tiktok.com/video/tos/.../...mp4?...&signature=...&expire=...&tk=tt_chain_token`.

## Root cause — KHÔNG phải CORS của trình duyệt

Request **đã tới server và bị server trả 403**. Response header của 403:

```
Status: 403 Forbidden
Access-Control-Allow-Origin: https://www.tiktok.com
Content-Type: text/html        # trang lỗi, không phải video
```

CDN TikTok (`*.tiktok.com`, `*.susercontent`-style) **khoá theo Origin/Referer**: chỉ phục
vụ khi request đến từ origin `https://www.tiktok.com`. Trang Compose chạy ở origin
`chrome-extension://<id>` → CDN trả 403.

### Vì sao thêm quyền / host_permissions cũng vô ích

- Extension **đã có** `https://*.tiktok.com/*` trong `host_permissions` → request **không bị
  CORS policy của trình duyệt chặn**. 403 là do **server check Origin**, không phải thiếu quyền.
- `fetch()` từ trang extension **không thể giả** `Referer`/`Origin = https://www.tiktok.com`
  (trình duyệt cấm set cross-origin referer/origin từ script). Chỉ code **chạy trên trang
  tiktok.com** mới có Origin/Referer đúng.
- Signed URL có `signature`/`expire`/`tk=tt_chain_token` → còn **hết hạn theo thời gian** và
  ràng buộc theo phiên phát.

→ **Kết luận:** bytes video **bắt buộc** phải được lấy trong **context của trang tiktok.com**.

## Giải pháp đang dùng

### 1) Tải bytes trong context tiktok.com (page-script)

Content script tiktok inject một **page-script** chạy ở MAIN world của tiktok.com để
`fetch` video với `credentials: 'include'` + `referrer: 'https://www.tiktok.com/'` (Origin/
Referer đúng → CDN trả 200), rồi gửi `Blob` về content script qua `window.postMessage`.

- Page-script: `public/js/content/tiktok/script/fetch.js` (đặt trong `public/` để
  `browser.runtime.getURL('js/content/tiktok/script/fetch.js')` resolve đúng path cố định).
- Khai báo `web_accessible_resources` cho `*.tiktok.com` trong `wxt.config.ts`.
- Loader gọi qua `loadScriptUrl(SCRIPT_PATH)` + `requestBlob(mediaUrl)`:
  `src/content/tiktok/loader.js`, hằng số `src/content/tiktok/constant.js`.

Đây là cơ chế **nút Tải video** đang chạy ổn — dùng lại cho repost.

### 2) Chuyển Blob sang trang Compose (extension page)

`Blob`/object URL **không** dùng chung được giữa 2 context (tiktok page ↔ extension popup),
nên phải truyền bytes qua background:

```
[tiktok loader]  requestBlob → Blob → base64
      │  bridgeVideoBlobStore({base64, contentType})          (webext-bridge content→background)
      ▼
[background]     lưu vào chrome.storage.session theo blobId   (KHÔNG dùng raw URL)
      │  mở popup /shopee/video/compose?blobId=…&sourceUrl=…  (sourceUrl = URL gốc để lưu record)
      ▼
[Compose page]   bridgeVideoBlobGet(blobId) → base64 → Blob
                 • URL.createObjectURL(Blob) cho <video> preview  (không còn 403)
                 • dùng thẳng Blob cho publishVideo (không fetch lại URL)
```

File liên quan:
- `src/content/tiktok/loader.js` — `onClickPostShopee`: requestBlob → base64 → store → mở Compose.
- `src/app/bridge/config/event.js` — `EVENT_VIDEO_BLOB_STORE`, `EVENT_VIDEO_BLOB_GET`.
- `src/app/bridge/content/background.js` — `bridgeVideoBlobStore` (content→bg).
- `src/app/bridge/options/background.js` — `bridgeVideoBlobGet` (popup/options→bg).
- `src/background/bridge/global.js` — handler store/get (dùng `chrome.storage.session`).
- `src/app/vue/views/Shopee/Video/Compose.vue` — `loadVideoFromBlob`, `base64ToBlob`, publish dùng `videoBlob`.

### Vì sao `chrome.storage.session` (không phải memory Map)

- Sống qua **F5** trang Compose **và** qua **service worker restart** (MV3 SW hay bị kill).
- Không ghi đĩa, tự xoá khi đóng phiên trình duyệt.
- **KHÔNG xoá blob sau lần GET đầu** → F5 đọc lại được (one-shot delete từng gây lỗi
  "Không tìm thấy video. Hãy bấm lại nút Đăng Shopee Video").
- Chỉ giữ **1 video mới nhất** (xoá key cũ mỗi lần store) để tránh đầy quota (~10MB).
- Cần quyền `storage` (đã có) — **0 quyền manifest mới**.

## Lưu ý cho agent khi sửa/mở rộng

- **Đừng** set `<video :src=tiktokCdnUrl>` hay `fetch(tiktokCdnUrl)` trực tiếp ở trang
  extension — luôn 403. Phải đi qua page-script trên tiktok.com.
- Nguồn **Shopee** (CDN Shopee không khoá origin) thì `fetch(videoUrl)` trực tiếp vẫn được —
  chỉ TikTok mới cần đường blob.
- Blob đi qua messaging dạng **base64** (~1.3× kích thước). Video ngắn TikTok ổn; nếu video
  lớn gây chậm/đầy quota → cân nhắc chunk hoặc IndexedDB.
- `sourceUrl` (URL TikTok gốc) chỉ để **lưu record** campaign — KHÔNG dùng `blob:` URL làm
  sourceUrl (object URL là ephemeral, vô nghĩa khi lưu DB).

## Liên quan: 403 khi UPLOAD lên Shopee Video (S3 `SignatureDoesNotMatch`)

Khác bug trên (đây là chiều **upload**, không phải hiển thị):

- Bước `uploadVodBytes` PUT bytes lên S3 VOD Shopee (`*.vod.susercontent.com`) trả
  **403 `<Code>SignatureDoesNotMatch</Code>`**.
- Nguyên nhân: **axios (0.20) + axios-fetch-adapter tự gắn/đổi `Content-Type` mặc định** cho
  PUT → header thực gửi **lệch** header đã ký AWS4 → server tính chữ ký khác.
- Fix: bước PUT dùng **native `fetch()`** (gửi đúng y hệt `putHeaders` đã ký, browser tự set
  `host`/`content-length` khớp). File: `src/app/api/shopee/seller/video/mms.js`.
- Lưu ý: payload hash bằng `CryptoJS.lib.WordArray.create(arrayBuffer)` là **đúng** với
  crypto-js 4.x (đã hỗ trợ ArrayBuffer) — không phải nguồn lỗi.
