# ChatGPT Sites 機制、能力、限制與驗證指南

> 文件快照：2026-07-15  
> Sites plugin：`sites@openai-bundled` `0.1.27`  
> 標準 starter：Vinext  
> 適用範圍：Codex／ChatGPT Work 中目前可用的 Sites 建置與託管流程

本文件整理 Sites 的建置層、控制平面、執行層、資料層、安全模型與部署邊界。內容以目前安裝的 Sites plugin skill、starter、打包腳本及 connector contract 為主要證據；Cloudflare 文件只用來說明相容 runtime 的底層行為，不直接代表 Sites 對使用者承諾的額度或 SLA。

## 目錄

- [閱讀方式與可信度標記](#閱讀方式與可信度標記)
- [核心結論](#核心結論)
- [三層架構](#三層架構)
- [核心物件與生命週期](#核心物件與生命週期)
- [標準技術棧與建置契約](#標準技術棧與建置契約)
- [部署、版本與回滾](#部署版本與回滾)
- [環境變數與 Secrets](#環境變數與-secrets)
- [網站存取政策](#網站存取政策)
- [登入、身分驗證與授權](#登入身分驗證與授權)
- [Browser／Web Platform 能力](#browserweb-platform-能力)
- [資料持久化](#資料持久化)
- [Runtime、CPU、記憶體與冷啟動](#runtimecpu記憶體與冷啟動)
- [Streaming、SSE 與 WebSocket](#streamingsse-與-websocket)
- [自訂網域與網站 metadata](#自訂網域與網站-metadata)
- [外部 Backend、Python、Realtime 與 GPU](#外部-backendpythonrealtime-與-gpu)
- [框架替換與相容性](#框架替換與相容性)
- [完整能力矩陣](#完整能力矩陣)
- [適用場景與架構選擇](#適用場景與架構選擇)
- [可觀測性、故障與營運邊界](#可觀測性故障與營運邊界)
- [驗證狀態與實測規格](#驗證狀態與實測規格)
- [尚未確認的問題](#尚未確認的問題)
- [參考資料](#參考資料)

## 閱讀方式與可信度標記

| 標記 | 意義 | 使用方式 |
| --- | --- | --- |
| `SITES` | 由目前 Sites plugin、starter 或 connector contract 直接確認 | 可當作目前產品契約使用 |
| `WEB` | 由 Web standard／browser 文件確認的前端能力 | 在使用者裝置執行，依 browser、HTTPS、permission 與硬體而異 |
| `CF` | 由 Cloudflare 官方文件確認的底層產品行為 | 只能作相容 runtime 的參考 |
| `TEST` | 必須在正式 Sites deployment 驗證 | 不應只依賴本機 preview |
| `UNKNOWN` | Sites 尚未公開或現行 connector 未暴露 | 不應自行補成既定能力 |

判讀原則：

1. `WEB` 不等於 `SITES`。WebGPU、Web Speech、File System API 等是在使用者 browser 執行，不代表 Sites server 取得 GPU、語音引擎或使用者檔案系統。
2. `CF` 不等於 `SITES`。Cloudflare Workers 支援的 binding、API 或額度，不代表 Sites 一定暴露相同能力。
3. 本機 Vinext／Miniflare preview 成功，不代表 Sites dispatcher、正式存取政策、SIWC、網域或 production deployment 行為相同。
4. 版本回滾、資料回滾、環境變數回滾是三件不同的事。
5. 沒有公開配額時，不以 Cloudflare 一般方案數字推定 Sites 配額。
6. 文件中的 package version 是 2026-07-15 的 starter 快照，不是永久固定版本。

## 核心結論

ChatGPT Sites 是 OpenAI 管理的全端網站建置、版本保存、存取控制與 production deployment 平台。其標準產物是 Cloudflare Workers 相容的 ESM Worker，而不是長時間常駐的 Node.js application server。

Sites 的核心能力可概括為：

```text
Vinext／React UI
+ SSR／React Server Components
+ Route Handler／HTTP API／Server Action
+ Sites 存取政策與 Sign in with ChatGPT
+ D1 結構化資料
+ R2 物件儲存
+ Production environment variables／secrets
+ 不可變 saved versions 與 production deployments
+ Custom domains
+ 外部 API orchestration／HTTP streaming
```

Sites 不等同於：

- Linux VM、Docker、Kubernetes 或 ECS。
- 可任意啟動的 Node.js、Python 或 Java server process。
- 可自行部署 PostgreSQL、Redis、queue consumer 或 scheduler 的主機。
- 具固定 vCPU、RAM、磁碟或 GPU SLA 的運算環境。
- 已承諾支援 Durable Objects、KV、Queues、Cron Triggers 或 WebSocket Hibernation 的完整 Cloudflare 帳戶。
- 可直接把任意 Cloudflare Worker 教學原封不動部署的通用 Cloudflare 控制台。

## 三層架構

Sites 應分成建置層、控制平面與執行層理解。

```mermaid
flowchart TD
    subgraph Build["建置層：Codex 與本機專案"]
        Prompt["需求與內容"] --> Source["Vinext／React source"]
        Source --> Local["Local preview"]
        Source --> BuildOut["Production build"]
        BuildOut --> Artifact["dist + hosting metadata + migrations"]
    end

    subgraph Control["控制平面：Sites"]
        Project["Site project"]
        Repo["Bound source repository"]
        Version["Saved immutable version"]
        Deploy["Production deployment"]
        Access["Access policy"]
        Env["Environment revision"]
        Domain["Custom domains"]
    end

    subgraph Runtime["執行層：正式請求"]
        Browser["Browser／API client"] --> Dispatch["Sites dispatch／SIWC／access"]
        Dispatch --> Worker["Worker-compatible ESM"]
        Worker --> App["Vinext SSR／API"]
        Worker --> D1["D1"]
        Worker --> R2["R2"]
        Worker --> External["External APIs／backends"]
    end

    Artifact --> Repo
    Repo --> Version
    Project --> Version
    Version --> Deploy
    Access --> Deploy
    Env --> Deploy
    Deploy --> Dispatch
    Domain --> Dispatch
```

### 建置層

`SITES`

- Codex 使用 Sites starter 建立或修改網站。
- 標準開發流程為 Vinext dev server、Vite／Cloudflare local runtime 與 production build。
- 新網站先顯示 starter loading skeleton，正式內容完成後移除 preview skeleton、starter metadata 與不再使用的相依套件。
- Build 必須產生 Worker-compatible ESM 及 Sites 所需 metadata。
- Local preview 是開發工具，不是 production deployment。

### 控制平面

`SITES`

- 建立與查詢 site project。
- 管理短效 source repository write credential。
- 保存具有 source provenance 的不可變 version。
- 將 saved version 部署至 production。
- 管理 access policy、production environment variables、secrets 與 custom domains。
- 查詢 deployment、domain、version 與 access 狀態。

### 執行層

`SITES`

- Request 先經過 Sites dispatch 與網站存取政策。
- 需要登入的 route 可使用 dispatcher-owned Sign in with ChatGPT。
- Worker 執行 SSR、HTTP API、Server Action、D1／R2 存取與外部 API 呼叫。
- 實際 D1／R2 resource 及 deployment wiring 由 Sites 管理。

## 核心物件與生命週期

### Site project

`SITES`

Site project 是網站的長期控制平面物件。建立新網站後取得 opaque `project_id`，必須原樣保存於 `.openai/hosting.json`。

重要規則：

- `.openai/hosting.json` 已有 `project_id` 時必須重用，不可再次建立同一網站。
- 不可從 title、slug 或 URL 推導 `project_id`。
- Site title 可更新；目前 title 更新不會改變 slug 或 URL。
- Site 可能具有 `active`、`archived` 或 `suspended` 等狀態，但現行可用 connector 不代表所有狀態都能由使用者直接修改。

### Source repository

`SITES`

建立 site 時會取得短效、repo-scoped source repository credential。它用來推送之後由 `commit_sha` 指向的 source state。

安全規則：

- Token 只能作每次命令的 HTTP authorization，不得寫入 remote URL、Git config、source code、README 或 log。
- Credential 未過期前可重用；過期後重新取得。
- Version 所引用的 `commit_sha` 必須是已推送 branch head。

### Saved version

`SITES`

Saved version 是已驗證 source 與 build artifact 的不可變快照。Version 具有：

- Opaque `version_id`。
- 使用者可讀的 `version_number`。
- Source `commit_sha`。
- Archive content hash。
- 可能包含 archive size、file count 與 screenshot URL。

保存 version 不會自動部署。

### Deployment

`SITES`

Deployment 是把特定 saved version 套用到 production 的一次作業。它具有獨立的 opaque `deployment_id`，並經過以下狀態：

```text
pending → building → publishing → succeeded
                              └→ failed
```

每個 Sites deployment URL 都是 production URL。現行 connector 未提供可當作穩定 staging 環境使用的 preview deployment 契約。

### Environment revision

`SITES`

Production environment variables 有獨立、單調遞增的 revision。Deployment 記錄其使用的 environment revision，因此：

- Source version 與 environment revision 是分離的。
- Connector contract 指示更新 environment 後再部署 saved version；本次 production 實測發現，重新部署同一 saved version 雖記錄新 revision，live runtime 仍保留舊值，建立並部署新的 saved version 後才套用新值。
- 重新部署舊 source version 不必然代表回到舊 environment values。

### Access policy

`SITES`

Access policy 決定誰能開啟整個網站。它與 route 內的 application authorization 分離，並有自己的 revision。

### Custom domain

`SITES`

Published site 可附加多個 custom domain。Domain 有獨立 validation、provider、SSL 與生命週期狀態。

## 標準技術棧與建置契約

### Starter 快照

`SITES`

目前 starter 的主要版本：

| 元件 | 版本／設定 |
| --- | --- |
| Node.js | `>=22.13.0`，只用於本機建置與工具流程 |
| Vinext | `0.0.50` |
| Next package | `16.2.6` |
| React／React DOM | `19.2.6` |
| Vite | `8.0.13` |
| Cloudflare Vite plugin | `1.37.1` |
| Wrangler | `4.92.0` |
| Drizzle ORM／Kit | `0.45.2`／`0.31.10` |
| TypeScript | `5.9.3` |
| Worker compatibility flag | `nodejs_compat` |

這些是 starter snapshot，不是 Sites 對未來版本的固定承諾。

### App shape

標準 starter 採 Next.js App Router 風格，可建立：

- React Server Components 與 Client Components。
- 多頁 route 與 layout。
- SSR 頁面。
- Route Handler／HTTP API。
- Server Action。
- Form submission。
- D1／R2 server-side access。
- 外部 API request 與 HTTP streaming。

### Worker entry

Production 產物必須具 Worker-compatible module entry：

```ts
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    return new Response("Hello from Sites");
  },
};
```

這不是 `app.listen()`、Express、Uvicorn 或長時間執行的 Node.js process。

### Required artifact

`SITES`

標準部署封包至少包含：

```text
dist/
├── server/
│   └── index.js
├── 靜態資源                         # build 有輸出時
└── .openai/
    ├── hosting.json
    └── drizzle/                     # 有 migration 時
```

打包驗證要求：

- `dist/server/index.js` 必須存在。
- `dist/.openai/hosting.json` 必須存在。
- 有 migration 時，`dist/.openai/drizzle/**` 必須存在。
- Archive、source push 與 `commit_sha` 必須代表同一個 source state。

### Hosting manifest

新 starter 初始值：

```json
{
  "d1": null,
  "r2": null
}
```

Site 建立且需要 storage 後可能成為：

```json
{
  "project_id": "opaque-project-id",
  "d1": "DB",
  "r2": "BUCKET"
}
```

規則：

- `project_id` 是 opaque ID。
- `d1` 與 `r2` 是 logical binding name，不是實際 Cloudflare resource ID。
- 未使用的 binding 保持 `null`。
- Environment values 與 secrets 不得放入此 manifest。
- 真實 D1／R2 resource 與 production wiring 由 Sites 控制平面建立及注入。

### Node.js compatibility

`SITES` + `CF`

Starter 啟用 `nodejs_compat`，但這不代表完整 Node.js server runtime。Cloudflare Workers 的 Node.js API 分為完整、部分實作與只能 import 的 non-functional stub；套件能安裝或 build 成功，不代表 runtime path 必然可用。

應逐一驗證依賴套件是否要求：

- 長期可寫的 local filesystem。
- Child process 或 shell。
- Native addon。
- 完整 TCP server ownership。
- Cluster、VM、HTTP/2 等 stub 或不完整 API。
- Process-global、永久存在的記憶體狀態。

## 部署、版本與回滾

### 完整發布流程

`SITES`

```mermaid
sequenceDiagram
    participant Local as Local project
    participant Repo as Bound source repository
    participant Sites as Sites control plane
    participant Runtime as Production runtime

    Local->>Local: npm run build
    Local->>Sites: create or reuse site
    Sites-->>Local: project_id + short-lived repo credential
    Local->>Repo: push exact validated source
    Local->>Local: package dist + hosting metadata + migrations
    Local->>Sites: save version(commit_sha, archive)
    Sites-->>Local: version_id + version_number
    Local->>Sites: deploy saved version
    Sites->>Runtime: build／publish
    Local->>Sites: poll deployment status
    Sites-->>Local: production URL or failure
```

發布步驟：

1. 完成 production build。
2. `.openai/hosting.json` 沒有 `project_id` 時建立 site；已有時重用。
3. Commit 已驗證 source。
4. 使用短效 credential 推送 exact source state。
5. 以 pushed branch head SHA 作為 `commit_sha`。
6. 打包 `dist`、hosting metadata 與 migrations。
7. 保存一個 saved version。
8. 根據 access policy 進行 private 或 shared/public deployment。
9. 輪詢 deployment 至 `succeeded` 或 `failed`。
10. 成功後使用 response 中的 exact production URL。

### Private 與 open-world deployment

`SITES`

| 狀況 | 部署路徑 | 安全含義 |
| --- | --- | --- |
| 可確認只有 owner 可存取，沒有其他 user 或 group | Private deployment | 可直接發布給 owner |
| Shared、public、無法確認 owner-only | Open-world deployment | 發布前需要明確授權 |

這項區分是部署安全規則，不等於網站內 route 的 authorization。

### Version history

`SITES`

Version history 可依新到舊列出，適合：

- 稽核 source provenance。
- 確認目前 deployment 對應哪個 version。
- 選擇舊 version 重新部署。
- 比對 archive hash、檔案數與大小。

### 回滾邊界

`SITES` + `TEST`

重新部署舊 saved version 可回滾程式碼與打包的靜態資產，但下列狀態不應假定同時回滾：

- D1 schema。
- D1 production data。
- R2 objects。
- Production environment values。
- Access policy。
- Custom-domain configuration。

尤其 D1 migration 可能已改變 schema。舊程式碼若無法相容新 schema，單純部署舊 version 可能失敗或產生 runtime error。正式系統應採 backward-compatible migration、expand-and-contract 或明確的 data recovery runbook。

## 環境變數與 Secrets

### 三種設定來源

`SITES`

| 來源 | 用途 | 是否進 production |
| --- | --- | --- |
| `.env`／`.env.local` | 本機開發 | 否，除非另外同步到 Sites |
| `.env.example` | 文件化 key，不放 secret value | 否 |
| Sites production environment | Production runtime values／secrets | 是，透過 environment revision 套用 |

### 更新語意

`SITES`

- Key 大小寫敏感。
- Update 只修改明確列出的 key，其餘保持不變。
- Remove 只刪除明確列出的 key。
- 同一 key 不可同時出現在 set 與 remove。
- Sensitive value 應標為 secret。
- Secret 讀回時不應取得明文。
- 更新 environment 會產生新 revision。
- 更新後需部署；但本次實測顯示不能只相信 deployment 記錄的 revision。重新部署同一 saved version 顯示 revision `3` 但 runtime 仍為 revision `1` 值；建立新 saved version 並部署 revision `3` 後才生效。部署後必須由 runtime synthetic check 驗證實際值。

### 安全規則

- 不把 API key、database password、SIWC bypass token 或 repo credential 寫進 source。
- 不將 environment value 寫進 `.openai/hosting.json`。
- 不將 secret 送到 Client Component、browser bundle 或公開 route。
- 外部 backend credential 只在 server route／Worker 使用。
- Secret rotation 應包含重新部署與失效舊值的程序。

### 尚未承諾

`UNKNOWN`

- 獨立 staging environment。
- Per-branch environment。
- Preview deployment environment。
- Environment revision 的歷史還原 UI。
- Secret audit log、rotation policy 與 managed secret store SLA。

## 網站存取政策

### Access modes

`SITES`

| Mode | 誰可以開啟網站 |
| --- | --- |
| `public` | 任何取得 URL 的人 |
| `workspace_all` | 該 Workspace 的所有有效使用者 |
| `custom` | Owner 加上指定 user email、Workspace group 或 tenant group |

Owner 永遠保持可存取。新增 group 前必須取得該 site／workspace 實際可用的 group ID，不可自行建立或猜測 ID。

### Access policy 與 application authorization

```text
Access policy
└── 決定 request 能否進入整個網站

Authentication
└── 決定 request 對應哪個使用者

Application authorization
└── 決定該使用者可讀寫哪些 row、object、route 與 action
```

即使網站是 `workspace_all`，仍需在 server-side query 內限制資料 ownership。即使 route 完成 SIWC，仍不能僅靠前端隱藏按鈕實作權限。

### Access policy revision

`SITES`

存取政策有 revision 與更新時間。應將 access change 視為 production security change，而不是一般 UI 設定。

## 登入、身分驗證與授權

### 可用身分模型

`SITES`

| 模型 | 適用情境 | 來源 |
| --- | --- | --- |
| Workspace-authenticated header | 網站已受 Workspace access policy 保護，只需辨識目前使用者 | Sites dispatcher forwarding headers |
| Dispatcher-owned SIWC | Route 需要讓匿名使用者進入 ChatGPT sign-in flow | Starter auth helper + reserved dispatch routes |
| App-owned public auth／external OAuth | 需要外部 identity provider 或公開帳號系統 | 現行 starter 不應直接自行搭建；先確認平台路徑 |

### Workspace identity headers

Internal Workspace site 可讀取：

```text
oai-authenticated-user-email
```

SIWC-authenticated request 可能另有：

```text
oai-authenticated-user-full-name
oai-authenticated-user-full-name-encoding: percent-encoded-utf-8
```

Full name 是 optional。只有 encoding header 完全符合時才 decode，否則回退到 email。

```tsx
import { headers } from "next/headers";

export default async function Account() {
  const requestHeaders = await headers();
  const email = requestHeaders.get("oai-authenticated-user-email");
  const encodedName = requestHeaders.get("oai-authenticated-user-full-name");
  const encoding = requestHeaders.get(
    "oai-authenticated-user-full-name-encoding",
  );

  const fullName =
    encodedName && encoding === "percent-encoded-utf-8"
      ? decodeURIComponent(encodedName)
      : null;

  const displayName = fullName ?? email;
  // Render identity-aware UI without exposing authorization decisions to client code.
}
```

### Sign in with ChatGPT

`SITES`

Starter 提供：

- `getChatGPTUser()`：取得 optional signed-in user。
- `requireChatGPTUser(returnTo)`：在 server-rendered page 要求登入。
- `chatGPTSignInPath(returnTo)`：建立登入 link。
- `chatGPTSignOutPath(returnTo)`：建立登出 link。

Dispatcher 保留並管理：

```text
/signin-with-chatgpt
/signout-with-chatgpt
/callback
```

應用程式不得自行實作這些 route。`returnTo` 必須是 same-origin relative path，例如 `/profile`，不可接受任意外部 URL。

受保護 page 應標記：

```ts
export const dynamic = "force-dynamic";
```

API route 或 Server Action 必須在 server 重新檢查 user；Client Component 顯示狀態不能當作 authorization。

### Authentication 不等於 Workspace membership

`SITES`

SIWC 證明 ChatGPT identity，但不必然證明該使用者屬於指定 Workspace。Workspace-only 系統仍需：

- Sites `workspace_all` 或 `custom` access policy；或
- Server-side membership／allowlist check。

### Row 與 object authorization

```sql
SELECT *
FROM notes
WHERE id = ?
  AND owner_id = ?;
```

```ts
if (record.ownerId !== currentUser.id) {
  return new Response("Forbidden", { status: 403 });
}
```

R2 object key 也應帶 ownership namespace，例如：

```text
uploads/{owner_id}/{uuid}
```

不要接受 client 傳入任意 object key 後直接讀寫。

### SIWC bypass token

`SITES`

Identity-less API client 可使用 SIWC bypass bearer token 通過整站 SIWC gate：

```http
OAI-Sites-Authorization: Bearer {siwc_bypass_bearer_token}
```

安全邊界：

- Bypass token 只繞過 SIWC gate，不應取代 application-level API authorization。
- 產生新 token 會旋轉並立即使舊 token 失效。
- Token 不得進入 browser bundle、URL、Git、README 或 log。
- Machine client 仍應有自己的最小權限、rate limit 與資料 authorization。

## Browser／Web Platform 能力

### 核心邊界

`WEB` + `SITES`

前端功能是否可行，必須先判斷程式實際在哪一層執行。相同的「檔案」、「語音」或「session」在不同層代表完全不同的生命週期與安全邊界。

| 層 | 執行位置 | 可用能力 | 不應假設 |
| --- | --- | --- | --- |
| Browser | 使用者裝置的 browser process | DOM、File API、IndexedDB、OPFS、Web Speech、WebGPU、Web Workers、WASM、media APIs | 不保證跨 browser、跨裝置、背景常駐或權威持久化 |
| Sites application | Worker-compatible request handler | SSR、HTTP route、cookie、D1、R2、external `fetch()`、streaming response | 沒有 DOM、browser storage、browser GPU、任意 local disk 或常駐 process |
| External service | 另一個受控 backend／SaaS | TTS／STT model、GPU inference、大檔 resumable upload、queue、TURN、長任務 | 不會自動繼承 Sites identity、access policy 或資料 authorization |

判斷規則：

1. `navigator`、`window`、`document`、browser storage 與硬體 API 只能在 Client Component／browser code 使用。
2. D1、R2、secret、可信 identity 與 application authorization 只能在 server route／Server Action 使用。
3. Browser feature 可用不等於 Sites 原生提供該服務。WebGPU 在 client 可用，不代表 Sites runtime 有 GPU。
4. Browser compatibility、secure context、user permission 與 device capability 都是 runtime condition，必須 feature-detect，不能只依 build success 判斷。
5. 跨裝置、跨使用者、可稽核或影響權限的狀態必須進 D1／R2 或外部權威系統。

### 快速能力判斷

| 需求 | 可行性 | 正確執行層 | 主要限制 |
| --- | --- | --- | --- |
| 使用者選檔、drag-and-drop | 可行 | Browser File API | 必須由使用者明確提供檔案；不能任意掃描 local disk |
| 開啟／儲存使用者本機檔案 | 條件式可行 | Browser File System API | HTTPS、permission、browser support；需 fallback |
| 離線本機資料庫 | 可行 | IndexedDB | Same-origin、device-local、quota／eviction |
| Origin 私有檔案 | 條件式可行 | OPFS | Same-origin、browser support、quota／eviction；不是 server storage |
| 跨裝置檔案儲存 | 可行 | Sites R2 + D1 metadata | Server authorization、size／type validation、streaming |
| 上傳／下載 | 可行 | Browser + Sites route + R2 | 大檔策略、Range、cache、object authorization |
| 裝置端文字轉語音播放 | 條件式可行 | Web Speech synthesis | Voice／quality／availability 因裝置而異，通常不能取得穩定音檔 bytes |
| 產生可下載 MP3／WAV | 可行但需外部服務 | Sites BFF + external TTS + R2 | Provider secret、cost、latency、content policy、job lifecycle |
| Browser GPU compute／AI inference | 條件式可行 | WebGPU | HTTPS、browser／GPU support、memory、模型下載與 fallback |
| Server GPU inference | Sites 本身不可行 | External GPU service | Sites 只做代理、authorization、streaming 與結果持久化 |
| Application cookie | 可行 | Sites response／browser cookie jar | `Secure`、`HttpOnly`、`SameSite`、scope、CSRF |
| Server-side session | 可行 | Cookie session ID + D1 | 不可用 isolate memory 當 session store |
| `sessionStorage` UI state | 可行 | Browser | 每 origin、每 tab；不是登入 session |
| Same-origin API request | 可行 | Browser → Sites route | 不需要 CORS |
| Cross-origin browser request | 條件式可行 | Browser + target server | Target 必須正確回 CORS／preflight headers |
| Server-side external request | 可行 | Sites route → upstream | 不受 browser CORS 限制；仍受 upstream auth／network policy 限制 |
| API proxy／BFF | 可行 | Sites route | 必須固定 upstream、限縮 method／headers／body，避免 SSRF／open proxy |

### File API、File System API、IndexedDB 與 OPFS

`WEB`

四者用途不同：

| API | 適合用途 | 資料生命週期 | 主要風險 |
| --- | --- | --- | --- |
| File API | `<input type="file">`、drag-and-drop、讀取使用者交付的 `File`／`Blob` | Page 取得 object reference 的期間 | 不能自行瀏覽任意 local path；檔名與 MIME 不可信 |
| File System API | 使用 picker 開啟、另存或選擇 directory | Handle 可在獲授權後使用，permission 可能需重新確認 | 非所有 browser 都支援；只能在 secure context；需使用者互動與授權 |
| IndexedDB | 大量結構化 client data、Blob、離線 cache、draft | Origin-local，通常跨 reload 保留 | Quota、eviction、schema migration、private browsing 行為 |
| OPFS | Origin 私有、高效能 local files、離線 work data、resumable upload chunks | Origin-local，不呈現在一般檔案系統 UI | 不是備份；清除 site data 或 storage eviction 可能消失 |

File System API 可使用 `showOpenFilePicker()`、`showSaveFilePicker()` 與 `showDirectoryPicker()`；支援情況不一致，實作時必須先檢查 function 是否存在，再 fallback 到 `<input type="file">` 與一般 download。

```ts
export async function pickTextFile(): Promise<string | null> {
  if (!("showOpenFilePicker" in window)) return null;

  const [handle] = await window.showOpenFilePicker({
    types: [{
      description: "Text",
      accept: { "text/plain": [".txt", ".md"] },
    }],
    multiple: false,
  });

  const file = await handle.getFile();
  return file.text();
}
```

實作限制：

- Picker 必須由 user gesture 觸發，不可在 page load 自動要求權限。
- `File.name`、`File.type`、client checksum 與 extension 都只能當輸入提示；server 必須重驗。
- File／directory handle 可序列化進 IndexedDB，但重新使用前仍要查詢或要求 permission。
- IndexedDB／OPFS 不適合存 access control、billing、共享資料或唯一副本。
- 需要 best-effort 降低 eviction 時可檢查 Storage API persistence，但即使成功也不能視為 backup。
- Browser storage 的 schema、quota exceeded、資料清除與 downgrade 路徑都需要測試。

### Upload

`WEB` + `SITES`

推薦資料路徑：

```text
<input type=file>／drag-and-drop
  → browser-side UX validation
  → same-origin Sites route
  → application authentication／authorization
  → server-side size、signature、type 與 ownership validation
  → stream bytes to R2
  → write metadata／status to D1
```

R2 object key 應由 server 產生，不能直接採用 filename：

```text
uploads/{owner_id}/{uuid}
```

D1 至少記錄：

```text
id
owner_id
object_key
original_name
detected_content_type
size
checksum            # optional；視成本與需求
status              # pending／ready／failed／deleted
created_at
```

必要控制：

- 在讀完整 body 前先檢查 authorization 與可取得的 request size signal。
- 同時限制單檔大小、request body、檔案數、使用者 quota 與 tenant quota。
- 驗證 magic bytes／file signature；不要只信 `Content-Type` 或 extension。
- 正規化顯示檔名，但 storage key 使用 server-generated opaque ID。
- 對 HTML、SVG、PDF 或其他 active content 設定隔離的 download／render policy。
- Upload 完成與 D1 metadata 寫入不是同一個 atomic transaction；以 `pending → ready` 狀態與 cleanup job／repair path 處理 partial failure。
- 對 malware scanning、content moderation 或 media processing，使用外部 asynchronous service；Sites 目前沒有原生 queue contract。
- 避免 `await request.arrayBuffer()` 後一次持有完整大檔；使用 stream，並以 production test 確認 R2 binding 與 runtime 實際接受的 body size。
- R2 `put()` 的 stream 必須有已知長度。實測中，經一般 `TransformStream` 後的 body 因失去 known-length metadata 而被拒絕；可用 request `Content-Length` 驗證上限，再以 `FixedLengthStream(contentLength)` 邊讀邊寫並核對實際 byte count。

小／中型檔案可由 same-origin route 接收。大檔或不穩定網路需要 resumable protocol、multipart upload 或專門 upload service。Browser 可先把 chunk／progress 暫存在 OPFS；Sites 是否能發 R2 presigned URL、開 public bucket 或直接 multipart upload 仍是 `UNKNOWN`，不能當作既有 contract。

### Download

`WEB` + `SITES`

受保護檔案應經 same-origin route：

```text
GET /api/files/{id}
  → authenticate
  → SELECT metadata WHERE id = ? AND owner_id = ?
  → R2.get(server-owned object_key)
  → stream response
```

Response 應依需求設定：

```http
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="report.pdf"; filename*=UTF-8''report.pdf
Cache-Control: private, no-store
ETag: "..."
X-Content-Type-Options: nosniff
```

控制原則：

- `Content-Disposition: attachment` 用於強制下載；inline preview 只開放已評估類型。
- `filename` 必須移除 CR／LF、path separators、control characters 與危險 extension ambiguity。
- Private object 不要暴露可猜測 R2 key，也不要使用 public URL 取代 authorization。
- 大型音訊／影片若需要 seek，route 必須正確處理 `Range`、`206 Partial Content`、`Content-Range`、`Accept-Ranges` 與 invalid range。
- 使用 `ETag`／conditional request 降低傳輸，但 private personalized response 不得被 shared cache 錯誤共用。
- Blob URL 只存在目前 browser context；完成後以 `URL.revokeObjectURL()` 釋放。

### TTS／STT

`WEB` + `SITES` + 外部服務

Browser 端 `speechSynthesis` 可直接播放裝置提供的 voice：

```ts
export function speak(text: string, lang = "zh-TW") {
  if (!("speechSynthesis" in window)) {
    throw new Error("Speech synthesis is unavailable");
  }

  const utterance = new SpeechSynthesisUtterance(text);
  utterance.lang = lang;
  window.speechSynthesis.cancel();
  window.speechSynthesis.speak(utterance);
}
```

這條路徑適合 accessibility、local preview 與低風險朗讀；不適合把 voice 視為產品固定資產，原因包括：

- Voice list、語言、音色、品質與下載狀態依 OS／browser／device 而異。
- `voiceschanged` timing 與 background behavior 需實測。
- `speechSynthesis` 主要提供 playback control，不能假設可取得一致的 MP3／WAV bytes。
- 長文字需要 chunking、pause／resume、取消與 navigation cleanup。

需要固定 voice、可下載音檔、batch generation、跨裝置一致或 server-side accessibility asset 時，使用：

```text
Browser
  → POST /api/tts
  → Sites route validates user、text length、language、rate limit
  → external TTS API using server-side secret
  → stream audio response，或 store audio in R2 + metadata in D1
```

不得把 provider API key 放在 browser。長音訊生成應採 job model；Sites 沒有已確認的 queue／scheduler binding時，job orchestration 交給外部服務。Speech recognition 同理：Web Speech recognition 可作 capability-enhanced path，但若需要穩定 browser coverage、可稽核 transcript 或 domain-specific model，使用外部 STT API。

### WebGPU、WASM 與 client-side AI

`WEB`

WebGPU 可在支援的 browser 與 GPU 上執行 compute／render workload。它只使用使用者裝置 GPU，與 Sites production runtime 無關。

```ts
export async function getWebGpuDevice(): Promise<GPUDevice | null> {
  if (!("gpu" in navigator)) return null;
  const adapter = await navigator.gpu.requestAdapter();
  if (!adapter) return null;
  return adapter.requestDevice();
}
```

實作必須處理：

- HTTPS secure context、browser／OS／driver support 與 feature detection。
- `requestAdapter()` 回傳 `null`、device loss、out-of-memory 與 required limits／features 不足。
- Model weights 的首次下載時間、版本、integrity、cache invalidation 與 storage quota。
- Tensor／texture／buffer 明確釋放，避免 GPU memory 累積。
- Low-end device、battery、thermal throttling 與 background tab suspension。
- Cross-origin model asset 的 CORS、Content-Type 與 cache headers。
- WASM／CPU 或 external inference fallback；不可讓「沒有 WebGPU」等同整站不可用。
- 若使用 WASM threads／`SharedArrayBuffer`，需另行驗證 COOP／COEP cross-origin isolation headers 是否能在 Sites production 正確送出，且所有 subresource 相容。

模型或 dataset 可 cache 在 IndexedDB／OPFS，但這只是 device-local cache。使用者資料若要送進 external model，仍需明示資料流、縮限 payload、server-side secret、retention policy 與 authorization。

### Cookie、server session 與 browser session

`WEB` + `SITES`

三種概念不可混用：

| 機制 | 正確用途 | 不應承擔 |
| --- | --- | --- |
| Cookie | Browser 與 same-site／same-origin server 間的小型 request state／session identifier | 大量 application data、secret 明文、唯一權威資料 |
| D1-backed server session | 登入狀態、session rotation、expiry、revocation、CSRF-related state | Module global memory 或只靠 client state |
| `sessionStorage` | 每個 origin、每個 tab 的暫時 UI state | Authentication、跨 tab 同步、server authorization |

Application session 的保守設計：

```text
Browser cookie: opaque random session ID
  ↓
Sites route: hash／lookup session ID
  ↓
D1 sessions: user_id、expires_at、revoked_at、created_at、last_seen_at
```

```http
Set-Cookie: __Host-app_session={opaque-id}; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=3600
```

安全控制：

- 優先使用 host-only `__Host-` cookie；不要無必要設定 `Domain`。
- `HttpOnly` 阻止 JavaScript 讀取 session cookie；`Secure` 限制 HTTPS；`SameSite` 降低部分 CSRF 風險，但不取代 CSRF defense。
- Login／privilege change 後旋轉 session ID；支援 absolute expiry、idle expiry、server-side revoke 與 logout deletion。
- State-changing route 驗證 `Origin`／CSRF token，並限制 method 與 content type。
- 不在 cookie 存 PII、access token 或可由 client 篡改的 authorization claim；若採 signed／encrypted cookie，仍需 key rotation、expiry、replay 與 revocation 策略。
- 不使用 module-level `Map` 保存 session。Worker isolate 可被回收、分散到多個 instance，也不保證同一使用者回到同一 isolate。
- `SameSite=None` 必須搭配 `Secure`；credentialed cross-origin flow 還要正確 CORS，且受 third-party cookie policy 影響。
- Default Sites domain 與 custom domain 是不同 cookie origin／scope；切換 domain 時不能假設既有 application cookie 自動搬移。

SIWC cookie 與 `/cdn-cgi/*` authentication route 屬 Sites dispatcher。Application 不得覆寫、解析或自行模擬 SIWC session；app session 只能作為 SIWC 之外的 application state，authorization 仍以可信 server identity 與資料 ownership 為準。

### CORS request

`WEB` + `SITES`

CORS 是 browser 對跨 origin JavaScript request 的讀取限制，不是 server-to-server firewall，也不是 authentication。

| 路徑 | 是否受 browser CORS 約束 | 設計 |
| --- | --- | --- |
| `https://app.example.com` → 同 origin `/api/*` | 否 | 優先使用；cookie、SIWC 與 API route 最單純 |
| Browser → `https://api.vendor.com` | 是 | Vendor 必須允許 origin、method、headers；可能先送 `OPTIONS` preflight |
| Sites route → `https://api.vendor.com` | 否 | Worker `fetch()` 不受 browser CORS；以 server secret 驗證 upstream |
| Browser → 另一個自有 Sites／API domain | 是 | 明確 allowlist origin，處理 preflight 與 credential policy |

Credentialed CORS 最小規則：

```http
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
```

不能把 `Access-Control-Allow-Origin: *` 與 credentialed request 混用。Preflight route 還需針對 allowlist 回應：

```http
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token
Access-Control-Max-Age: 600
```

控制原則：

- Origin 必須與 canonical allowlist 精確比較；不要用 substring、suffix 或反射任意 `Origin`。
- `null` origin 預設拒絕，除非有明確 sandbox／local-file use case。
- CORS 只決定 browser 是否能讀 response；route 仍需 authentication、authorization、rate limit 與 input validation。
- Same-origin route 不應無目的加入 wildcard CORS。
- CORS error 常在 browser 顯示為 generic fetch failure；production test 同時記錄 preflight、actual response 與 server log correlation ID。

### API proxy／Backend for Frontend

`SITES`

Sites route 可作 API proxy／BFF，適合隱藏 provider secret、正規化 upstream、附加 application authorization、聚合 response 與串流轉送：

```text
Browser → same-origin /api/vendor/* → Sites Worker → fixed upstream API
```

安全 proxy 不是接受任意 `url` 的通用轉送器。最低控制：

- Upstream scheme／hostname／port 固定或使用 exact allowlist；禁止 private、loopback、link-local、metadata IP 與 redirect escape，避免 SSRF。
- Route 明確列出允許 method 與 path；不要拼接未正規化的 user path。
- 只轉送必要 request headers。不得盲目轉送 browser `Cookie`、`Authorization`、`Host`、`Forwarded` 或 provider response 的 `Set-Cookie`。
- Provider credential 從 Sites secret 取得，不能接受 client 傳入或回傳 client。
- 設定 connect／overall timeout、`AbortController`、client disconnect cancellation、body／response size 與 redirect limit。
- 對 retry 僅處理安全或具 idempotency key 的操作；採 bounded exponential backoff，尊重 `Retry-After`。
- 正規化 upstream error，避免洩漏 internal header、stack、request ID 中的敏感資訊。
- 對 user／tenant／upstream 設 rate limit 與 concurrency limit。Sites 未暴露原生 rate-limit binding 時，可由 D1／external gateway 實作，但需評估一致性與 hot-row 問題。
- 可直接串流 upstream body，不要為了轉送而完整 buffer；若需要 inspect／transform，設定嚴格大小上限。
- Cache key 必須包含真正影響 response 的 identity／query／locale；含個資或 authorization 的 response 預設 `private, no-store`。

概念性 route：

```ts
const ALLOWED_METHODS = new Set(["GET", "POST"]);

export async function proxy(request: Request): Promise<Response> {
  if (!ALLOWED_METHODS.has(request.method)) {
    return new Response("Method Not Allowed", { status: 405 });
  }

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 10_000);

  try {
    const upstream = await fetch("https://api.vendor.example/v1/fixed-path", {
      method: request.method,
      headers: {
        "content-type": request.headers.get("content-type") ?? "application/json",
        authorization: `Bearer ${getServerSecret()}`,
      },
      body: request.method === "GET" ? undefined : request.body,
      // Workers fetch 不接受 redirect: "error"；使用 manual 後自行拒絕 3xx。
      redirect: "manual",
      signal: controller.signal,
    });

    if (upstream.status >= 300 && upstream.status < 400) {
      await upstream.body?.cancel();
      return new Response("Upstream redirect rejected", { status: 502 });
    }

    const headers = new Headers();
    headers.set("content-type", upstream.headers.get("content-type") ?? "application/octet-stream");
    headers.set("cache-control", "private, no-store");

    return new Response(upstream.body, {
      status: upstream.status,
      headers,
    });
  } finally {
    clearTimeout(timeout);
  }
}
```

這段只表達固定 upstream、縮限 headers、manual redirect rejection、timeout 與 streaming response；實際 route 仍需 Sites framework 的 env access、identity、request size、error mapping 與 disconnect wiring。

### 其他常見前端能力

`WEB` + `TEST`

| 能力 | 可行性與邊界 |
| --- | --- |
| Web Workers | 可用於 browser CPU work，避免阻塞 UI；不是 Sites background worker，不保證 page 關閉後繼續 |
| WebAssembly | Browser 可執行；WASM binary 必須正確 MIME／CORS／cache，memory 與 thread support 要 feature-test |
| Service Worker／PWA／Cache API | Web standard 可實作 offline shell、request cache；custom domain 的 install／update／cache 已驗證，navigation fallback 與 SIWC／logout cache interaction 仍需補證據 |
| Camera／microphone／screen capture | `getUserMedia()`／`getDisplayMedia()`；需 HTTPS、permission、visible UI、track cleanup 與 privacy disclosure |
| Web Audio／MediaRecorder | 可錄音、處理與輸出 Blob；codec／container support 依 browser，長錄音需避免無界 memory |
| WebRTC | Browser peer connection 可行；production 仍需 signaling、STUN／TURN、authorization 與 authoritative room state；Sites 未確認 Durable Objects |
| Clipboard／Web Share | 通常需要 HTTPS、user gesture、permission 與 mobile／browser feature detection |
| Geolocation | 需 HTTPS 與 permission；定位是敏感資料，需 retention／purpose control |
| Notifications／Web Push | Browser API 可用；Push 需要 Service Worker、subscription、VAPID／push service 與外部 sender；Sites 沒有已確認 scheduler／queue |
| WebAuthn／passkeys | Browser ceremony 可做；credential registration、challenge、counter 與 account recovery 需要可信 server state；不是 SIWC 的無痛替代 |
| Web Bluetooth／USB／Serial／MIDI | Browser、OS 與 hardware support 有限；需要 user gesture／permission，不適合作為唯一主流程 |
| WebSocket／SSE | SSE／HTTP streaming 符合 Sites 模型；WebSocket production 穿透與 coordination 仍依本文件測試矩陣 |

所有 permissioned API 都必須有拒絕、撤銷、裝置缺少能力與 browser 不支援的 degraded path。功能檢查應在實際呼叫前執行，不以 user-agent 字串推測。

## 資料持久化

### 選擇原則

`SITES`

| 資料類型 | 儲存位置 |
| --- | --- |
| Users、tasks、notes、workflow state、ownership、metadata | D1 |
| 圖片、PDF、音訊、影片、匯出檔、generated assets | R2 |
| File ownership、filename、content type、status、search fields | D1 |
| 實際 file bytes | R2 |
| Theme、dismissed banner、device-local draft | Browser storage |
| 跨 session、跨裝置、多人共享或權威資料 | 不可只使用 browser storage |

### D1

`SITES`

D1 是 Sites 目前明確暴露的結構化持久層，採 SQLite dialect，適合：

- Profile、設定、任務、筆記、文章、留言。
- Workflow state、分數、進度與 audit metadata。
- Filtering、sorting、join、index 與 ownership check。
- R2 object metadata。

標準流程：

```text
db/schema.ts
→ drizzle-kit generate
→ inspect generated SQL
→ save migration with source
→ package into dist/.openai/drizzle
→ Sites applies migration to bound D1 resource during deployment flow
```

Prepared statement 規則：

- 一個 `prepare()` 只放一個 SQL statement。
- Multiline SQL 可以是一個 statement。
- 不要把多個分號分隔 statement 塞進同一個 `prepare()`。
- 多 statement operation 使用 `batch([...])`。
- Runtime schema initialization 使用 `prepare().run()`；`exec()` 留給 maintenance 或 one-shot task。

```ts
const d1 = env.DB;

await d1.batch([
  d1.prepare(
    "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, owner_id TEXT NOT NULL)",
  ),
  d1.prepare(
    "CREATE INDEX IF NOT EXISTS notes_owner_idx ON notes (owner_id)",
  ),
]);
```

### D1 migration 與 recovery

`SITES` + `CF` + `TEST`

- Sites 可打包並部署 Drizzle migration。
- Sites connector 未明確提供 D1 restore 或 migration rollback 操作。
- Cloudflare D1 底層有 Time Travel／point-in-time recovery，但不能直接推定 Sites 使用者可取得相同控制權。
- Redeploy 舊 version 不應視為 schema／data rollback。

正式 schema change 應符合：

1. 新 schema 先允許新舊程式共同運作。
2. 部署可同時讀取舊欄位與新欄位。
3. 完成 backfill。
4. 切換 write path。
5. 確認無舊程式後再移除欄位。

### D1 底層限制參考

`CF`

截至 2026-07-15，Cloudflare 公開限制包含：

| 項目 | Workers Paid | Free |
| --- | ---: | ---: |
| 單一 database 最大容量 | 10 GB | 500 MB |
| 每 account storage | 1 TB | 5 GB |
| 每次 Worker invocation 的 D1 queries | 1,000 | 50 |
| Time Travel | 30 天 | 7 天 |

這些不是 Sites quota。Sites 未公開 D1 plan mapping、帳戶容量、超額行為或 restore 操作權限。

### R2

`SITES`

R2 是 Sites 目前明確暴露的 object storage binding。建議資料模型：

```text
D1 files
├── id
├── owner_id
├── object_key
├── original_name
├── content_type
├── size
├── status
└── created_at

R2
└── uploads/{owner_id}/{uuid}
    └── bytes
```

避免：

- 把大型 file 轉 Base64 寫入 D1。
- 將使用者提供的 filename 直接當 object key。
- 在記憶體一次 buffer 完整大型檔案。
- 未驗證 content type、size、owner 與 object key。

若 runtime path 支援，應以 stream 寫入 R2；目前 Sites 未公開 direct-to-R2 presigned upload、public bucket 或 R2 custom-domain 管理能力，不能直接假定存在。

### R2 底層限制與一致性參考

`CF`

| 項目 | Cloudflare R2 公開值 |
| --- | ---: |
| 單 bucket 儲存量 | Unlimited |
| 單 bucket object 數 | Unlimited |
| Object key | 1,024 bytes |
| Object metadata | 8,192 bytes |
| Object size | 文件表格標示 5 TiB；實際約 4.995 TiB |
| Single-part upload | 文件表格標示 5 GiB；實際約 4.995 GiB |
| Multipart upload | 約 4.995 TiB |

R2 原生 object read-after-write、metadata update、delete 與 list 為強一致；若透過 cache 或其他中介層存取，觀察結果可能不同。這些仍只是 R2 底層行為，不是 Sites 上傳流量、request body 或帳戶配額承諾。

### Browser storage

`WEB`

只適合：

- Theme 或顯示偏好。
- Dismissed banner。
- 非權威、可丟失的 draft。
- 單一裝置 UI 狀態。

不適合作為：

- Account record。
- 跨裝置進度。
- 多人資料。
- 付費、權限、訂單或稽核資料。
- Server 必須信任的 authorization state。

## Runtime、CPU、記憶體與冷啟動

### Runtime 模型

`SITES`

Production artifact 是 Worker-compatible ESM，不是固定 VM。Request 會進入 fetch handler，並取得平台注入的 `env` binding 與 execution context。

因此不可依賴：

- 固定 CPU 型號、vCPU 或 GHz。
- 永久 process。
- 永久 local filesystem。
- 全域變數作為權威持久狀態。
- 自行控制 OS package、daemon 或 listening port。

Module-level cache 只能作效能優化：

```ts
let cachedClient: Client | undefined;

function getClient() {
  cachedClient ??= createClient();
  return cachedClient;
}
```

Isolate 可被回收，其他 request 也可能落在不同 isolate。

### Cloudflare 底層參考值

`CF`

截至 2026-07-15，Cloudflare Workers 公開資料：

- Paid HTTP request 預設 CPU time 30 秒，可配置最高 5 分鐘。
- Free HTTP request CPU time 10 ms。
- Memory 為每 isolate 128 MB，不是每 request 128 MB。
- HTTP wall-clock duration 在 client 保持連線時沒有相同形式的硬上限，但仍受 CPU、disconnect、subrequest 與平台行為影響。

`UNKNOWN`

Sites 未公開：

- 實際 Workers plan mapping。
- Sites 的 `cpu_ms` 是否可調整。
- Request、subrequest、asset、body size 與 monthly traffic quota。
- Memory、startup、cold-start 或 regional SLA。
- Build machine 顯示的 vCPU 不代表 production runtime vCPU。

### 適合的運算

- SSR。
- 身分與權限檢查。
- JSON parsing／normalization。
- D1 query。
- 外部 API aggregation。
- 小型加密、hash、signature。
- Streaming proxy。
- 輕量資料轉換。

### 不適合的運算

- 大型 image／video transcoding。
- 大型壓縮與 ETL。
- 長時間 crawler。
- 模型 training／inference。
- 常駐 queue consumer。
- 需要 process memory coordination 的多人即時狀態。
- 依賴永久 local disk 的 database。

### 冷啟動

`CF` + `TEST`

Worker isolate 初始化通常不同於 VM／container scale-to-zero：不需啟動完整 OS、Node process 或 Uvicorn。但首次 request 仍可能包含：

- JavaScript bundle load。
- Module initialization。
- Large dependency、JSON 或 WASM initialization。
- 新 version 剛部署。
- Isolate 被回收。
- 外部 API／database 自身 cold start。

Sites 沒有公開 cold-start SLA。正式延遲應以 production deployment 測量，不以本機 dev server 或 Cloudflare 一般 benchmark 代替。

## Streaming、SSE 與 WebSocket

### HTTP streaming／SSE

`SITES`

Sites 適合以 `ReadableStream` 或 SSE 回傳 OpenAI API 等外部服務的 streaming response。適用：

- AI token streaming。
- 單向通知。
- Background job progress。
- 長查詢的逐步結果。

實作時處理：

- Client disconnect／abort signal。
- Backpressure。
- Heartbeat 與 proxy idle timeout。
- Error frame 與 retry policy。
- 不在 stream 結束後依賴未保證的長時間 background work。

### WebSocket 底層能力

`CF`

Cloudflare Workers 支援 WebSocket client／server 與 `WebSocketPair`。這只能證明底層 Workers API 存在。

### Sites WebSocket 狀態

`TEST` + `UNKNOWN`

WebSocket endpoint「可用一般網址到達」與「成功建立 WebSocket」是兩件事。判讀必須看握手狀態碼：

| 回應 | 能證明 | 不能證明／結論 |
| --- | --- | --- |
| `426 Upgrade Required` | HTTP route 存在，server 辨識出這次請求沒有 `Upgrade: websocket` | 未建立 WebSocket，不能傳送 frame |
| `101 Switching Protocols` | Server 接受 Upgrade，HTTP 已切換為 WebSocket protocol | 握手成功；仍需以 message／close／reconnect 驗證完整行為 |
| `500 Internal Server Error` | Upgrade path 在 hosting／runtime 邊界失敗 | WebSocket 未建立，不可視為可用 |

直接在網址列開啟 `https://sites.eraser.tw/api/ws` 是一般 HTTP GET，因此目前會正確回 `426`。Browser `new WebSocket("wss://sites.eraser.tw/api/ws")` 或標準 WebSocket client 才會送出真正 Upgrade；成功條件是 `101`，本次 production 實測則為 `500`。

Sites 尚未公開承諾：

- `101 Switching Protocols` 是否完整穿透 Sites dispatcher。
- Production connection duration、concurrency 與 reconnect 行為。
- Redeploy 時如何處理既有 connection。
- WebSocket SLA。
- Durable Objects binding。
- WebSocket Hibernation。

因此 production realtime system 不應只依賴本機 WebSocket preview。

### 多連線協調

普通 Worker isolate 不共享可靠 process memory：

```text
User A → isolate 1
User B → isolate 2
User C → isolate 3
```

下列寫法不能作聊天室或協作系統的權威狀態：

```ts
const rooms = new Map<string, Room>();
```

需要 room coordination、presence、fan-out 或 multiplayer state 時，使用外部 realtime backend、Durable Objects service、Redis 或專用 realtime provider。

### 技術選擇

| 需求 | 建議 |
| --- | --- |
| AI token streaming | HTTP streaming／SSE |
| 單向通知 | SSE |
| 低頻狀態更新 | Polling |
| 雙向聊天 | WebSocket + coordination backend |
| 多人文件 | WebSocket + authoritative state service |
| 遊戲同步 | WebSocket + room／state server |
| Job progress | SSE 或 Polling |

## 自訂網域與網站 metadata

### Custom domain flow

`SITES`

Published site 可新增 custom hostname：

```text
add domain
→ receive DNS／validation records
→ configure DNS provider
→ refresh validation status
→ pending／active／failed
→ route production traffic
```

回傳資訊可能包含：

- Subdomain 用 CNAME target。
- Zone apex 用 A record IPv4 targets。
- App Garden 與 Cloudflare validation records。
- `provider_status`。
- `ssl_status`。
- `last_error`。
- `pending`、`active`、`failed` status。

可列出、刷新與移除 custom domain。

### 未確認的網域行為

`UNKNOWN`

- DNS TTL 建議值。
- Certificate issue／renewal SLA。
- CAA、DNSSEC、proxy mode 的限制。
- Domain ownership transfer。
- Apex、subdomain 與多 Workspace 間的衝突規則。
- 自訂 404、redirect、header 與 cache-control 的平台層設定。

### Site metadata

`SITES`

- Display title 可更新。
- 更新 title 不會修改 URL 或 slug。
- Starter title、description、favicon 與 temporary preview metadata 應在正式 build 前替換。
- Social preview 可由應用的 Open Graph／X metadata 提供。
- `public/screenshot.jpeg` 是 optional deployment thumbnail，缺少時不應阻擋發布。

## 外部 Backend、Python、Realtime 與 GPU

### Python 不能直接部署到目前 Sites artifact

`SITES`

目前 Sites package contract 要求 `dist/server/index.js`，starter 與 runtime path 是 JavaScript／TypeScript Worker-compatible ESM。它沒有提供把 FastAPI、Django、Flask、Uvicorn 或 Gunicorn 作為常駐 process 的部署入口。

Cloudflare 底層另有 Python Workers 產品，不代表 Sites connector 已提供 Python artifact、binding 或 deployment contract。

### Sites + external Python

```mermaid
flowchart LR
    Browser["Browser"] --> Sites["Sites UI／SSR／BFF"]
    Sites --> Python["External FastAPI／Python service"]
    Python --> PG["PostgreSQL"]
    Python --> Redis["Redis／queue"]
    Sites --> D1["D1 metadata"]
    Sites --> R2["R2 objects"]
```

Sites Worker 可負責：

- 驗證 Sites／SIWC identity。
- 簽發短效 backend token。
- 隱藏 external API secret。
- 正規化 request／response。
- 聚合多個 backend。
- Streaming proxy。
- 基本 rate limit 與 access check。

External backend 可負責：

- Python／Java business logic。
- PostgreSQL、Redis、queue。
- 長時間 background jobs。
- CPU-intensive data processing。
- Realtime coordination。
- GPU inference。

### External realtime

```mermaid
flowchart LR
    UI["Sites frontend"] -->|"HTTPS auth／ticket"| Worker["Sites Worker"]
    UI -->|"WSS + short-lived ticket"| RT["Realtime backend"]
    RT --> State["Durable Objects／Redis／DB"]
    Worker --> D1["D1"]
```

短效 ticket 應具 audience、expiry、subject、scope 與 replay 防護，不把長效 secret 交給 browser。

### GPU 與 AI

`SITES`

Sites runtime 不直接提供 CUDA、PyTorch／TensorFlow GPU、本機 LLM、Stable Diffusion 或 GPU FFmpeg。應呼叫：

- OpenAI API。
- Hugging Face／其他 model endpoint。
- 自建 GPU service。

Sites 可管理 UI、identity、task metadata、streaming 與 R2 output；模型執行留在外部服務。

## 框架替換與相容性

### 判定標準

`SITES`

框架名稱不是唯一判斷依據。非 Vinext 專案至少要滿足：

1. 產生 Cloudflare Workers-compatible ESM。
2. 具有 `dist/server/index.js` 或經過可驗證的 staging adapter。
3. 靜態資源路徑正確。
4. `.openai/hosting.json` 與 migration 被放入 artifact。
5. D1／R2 logical binding 可注入。
6. 不依賴不可用的 Node.js API、local filesystem 或 process lifecycle。
7. 通過 Sites artifact validation。
8. 通過 production deployment，而不只本機 preview。

### 支援程度

| 技術 | 現行判斷 | 說明 |
| --- | --- | --- |
| Vinext／React | `SITES` 標準路徑 | Starter、build、preview、binding、migration 與 package flow 已整合 |
| 既有 Worker-compatible ESM | `SITES` 條件式 | Hosting skill 允許 non-Vinext established output，但需適配 staging |
| Vite SPA + Worker API | `TEST` | 純 SPA build 不足，仍需 Worker entry 與 Sites artifact |
| 純靜態網站 | `TEST` | 靜態 assets 仍需包在符合 Sites 契約的 Worker artifact 中 |
| Nuxt 4 static output | `TEST` | 靜態輸出本身不足；需 Worker entry 與 packaging adapter |
| Nuxt 4 + Nitro Cloudflare preset | `TEST` | 底層可能產生 Worker output，但不是 Sites 標準 workflow |
| Express／NestJS Node server | 不適用 | 常駐 `listen()` process 不符合 artifact model |
| FastAPI／Django／Flask server | 不適用 | 現行 Sites 無 Python server deployment contract |

### Nuxt 驗證清單

Nuxt／Nitro 若要評估，至少驗證：

- Server entry 是否為 Worker module，而非 Node server。
- Output 是否能 staging 為 `dist/server/index.js`。
- Static asset routing。
- SSR、islands、server routes 與 streaming。
- `nodejs_compat` package behavior。
- D1／R2 binding type 與 access。
- Migration packaging。
- Local preview 與 production dispatch 差異。
- Production artifact validation。

在上述項目完成前，只能標示「理論可適配」，不能寫成正式支援。

## 完整能力矩陣

| 能力 | 狀態 | 證據／邊界 |
| --- | --- | --- |
| Vinext／React starter | `SITES` | Plugin 內建標準 starter |
| SSR／RSC／Client Components | `SITES` | Vinext App Router stack |
| Multi-route／layout | `SITES` | App Router shape |
| Route Handler／HTTP API | `SITES` | Starter auth 與 runtime flow 明確支援 |
| Server Action | `SITES` | Auth reference 明確要求 server-side identity check |
| Production Worker ESM | `SITES` | `dist/server/index.js` 為必要 artifact |
| Local preview | `SITES` | Vinext dev server／Cloudflare local config |
| Stable preview deployment | `UNKNOWN` | 現行 deployment connector 對使用者承諾 production URL |
| D1 binding | `SITES` | `.openai/hosting.json` logical binding |
| Drizzle migration packaging | `SITES` | `dist/.openai/drizzle/**` |
| D1 restore／Time Travel control | `UNKNOWN` | Cloudflare 底層有能力，Sites 未暴露操作 |
| R2 binding | `SITES` | `.openai/hosting.json` logical binding |
| Direct-to-R2 browser upload | `UNKNOWN` | 未提供 presigned upload／public bucket contract |
| File API／drag-and-drop | `WEB` | 使用者明確交付的 `File`／`Blob`；server 仍需重驗 |
| File System API | `WEB` 條件式 | HTTPS、permission、browser support；需 input／download fallback |
| IndexedDB | `WEB` | Same-origin structured／Blob storage；device-local、quota／eviction |
| OPFS | `WEB` 條件式 | Origin-private local files；不是 server storage 或 backup |
| Browser storage | `WEB` 限縮用途 | 只用於 device-local、non-authoritative state |
| Same-origin route upload | `WEB` + `SITES` | Browser 選檔；Sites 驗證並 stream 至 R2，D1 存 metadata |
| Protected／streaming download | `SITES` | Route authorization + R2 stream；大檔需驗證 Range behavior |
| Browser speech synthesis | `WEB` 條件式 | 裝置 voice／quality／support 不一致；不保證可匯出音檔 |
| Stable TTS／STT asset | 外部服務 | Sites BFF 呼叫 provider；音檔可存 R2，metadata 存 D1 |
| Browser WebGPU | `WEB` 條件式 | Client GPU、HTTPS、feature detection、memory 與 fallback |
| WebAssembly／Web Workers | `WEB` | Client compute；threads 可能需要 cross-origin isolation |
| Application cookie | `WEB` + `SITES` | Server `Set-Cookie`；Secure／HttpOnly／SameSite／scope／CSRF |
| D1-backed application session | `SITES` | Opaque cookie ID + D1；不可用 isolate memory |
| `sessionStorage` | `WEB` | Per-origin、per-tab UI state；不是 auth session |
| CORS endpoint | `SITES` | 只在確有 cross-origin consumer 時採 exact allowlist／preflight |
| API proxy／BFF | `SITES` | Fixed upstream、server secret、timeout、streaming、SSRF defense |
| Production env vars | `SITES` | 與 `.env`／manifest 分離 |
| Secret values | `SITES` | 可標記 secret，讀回不提供明文 |
| Environment revision | `SITES` | Deployment 記錄 revision，更新後需 redeploy |
| Site access policy | `SITES` | `public`／`workspace_all`／`custom` |
| User／group allowlist | `SITES` | Email、Workspace group、tenant group |
| Workspace identity header | `SITES` | `oai-authenticated-user-email` |
| Sign in with ChatGPT | `SITES` | Dispatcher-owned SIWC |
| SIWC bypass bearer token | `SITES` | Identity-less API client；rotation 立即失效舊 token |
| Saved versions | `SITES` | Version number、commit SHA、archive provenance |
| Code rollback | `SITES` | 可選舊 saved version 重新部署 |
| Data／schema rollback | `UNKNOWN` | 不隨 code rollback 自動成立 |
| Private deployment | `SITES` | 需 verified owner-only access |
| Shared／public deployment | `SITES` | Open-world publishing，需要明確授權 |
| Custom domain | `SITES` | DNS／validation／SSL status flow |
| Site title update | `SITES` | 不改 slug／URL |
| HTTP streaming／SSE | `SITES` | Worker response streaming pattern |
| WebSocket | `TEST` | Workers 底層可用；Sites dispatch 未承諾 |
| Durable Objects | `UNKNOWN` | 現行 manifest 未暴露 binding |
| KV／Queues／Cron／Workflows | `UNKNOWN` | 現行 Sites contract 未暴露 |
| Arbitrary Node server | 不支援 | 無 process／listen lifecycle |
| Python server | 不支援 | 無 Python artifact contract |
| Docker／VM | 不支援 | Worker artifact model |
| Server-side GPU | 不支援 | Sites runtime 無 GPU contract；使用外部 model service |
| External HTTP APIs | `SITES` | Worker `fetch()`／BFF pattern |
| Custom runtime logs／metrics | `UNKNOWN` | 現行 Sites connector 未提供完整 observability contract |
| Sites quota／pricing | `UNKNOWN` | 無完整公開表 |

## 適用場景與架構選擇

### 純 Sites

適合：

- Landing page、品牌網站、作品集。
- Dashboard、CRUD 後台、小型 CMS。
- 問卷、報名、Todo、筆記與行程工具。
- Workspace internal tool。
- User-specific records、file upload 與管理。
- OpenAI API streaming UI。
- 外部 SaaS API aggregation。

典型組合：

```text
Vinext／React
+ SSR／HTTP API
+ Access policy／SIWC
+ D1 CRUD
+ R2 files
+ Production env／secrets
```

### Sites + external backend

使用條件：

- 已有 Python／Java domain logic。
- 需要 PostgreSQL extension 或複雜 transaction。
- Redis、queue、background worker。
- 大量資料處理。
- Realtime coordination。
- GPU inference。

### 不應單獨依賴 Sites

- 任意 Docker workload。
- 自訂 Linux package 或 daemon。
- 固定 vCPU／RAM SLA。
- 長時間 CPU computation。
- Scheduler／queue consumer。
- Local-disk SQLite。
- 高可靠 room coordination。
- 依賴未暴露 Durable Objects binding 的設計。

### 決策準則

| 問題 | 是 | 否 |
| --- | --- | --- |
| 主要是 UI、SSR、CRUD、files 與 API orchestration？ | 優先純 Sites | 繼續判斷 |
| 需要 Python／Java 或既有 backend？ | Sites + external backend | 繼續判斷 |
| 需要多人長連線與 authoritative room state？ | Sites + realtime layer | 繼續判斷 |
| 需要 GPU 或長時間運算？ | Sites + model／compute endpoint | 純 Sites 可能足夠 |

## 可觀測性、故障與營運邊界

### 已有狀態面

`SITES`

- Deployment status 與 failure message。
- Site status、current URL 與 latest version number。
- Version source provenance。
- Environment revision。
- Access policy revision。
- Custom-domain provider、SSL、validation 與 error status。

### 尚未公開的營運能力

`UNKNOWN`

- Production request logs 的查詢介面與 retention。
- Metrics、trace、CPU、memory、D1／R2 observability。
- Alert、SLO、error budget 與 incident webhook。
- Source map、runtime exception 與 correlation ID 管理。
- Audit log coverage。
- Backup／restore 管理介面。
- Deployment cancellation、traffic splitting、canary 或 gradual rollout。

### 建議應用層補強

- 每個 request 建立 correlation ID。
- External API error 使用 structured log，避免記錄 secret／PII。
- API response 區分 validation、authorization、dependency 與 internal failure。
- 對外部 backend 設定 timeout、abort、retry budget 與 circuit-breaker policy。
- Migration、environment rotation、access change 與 rollback 建立 runbook。
- 對關鍵 route 建立 synthetic check。

## 驗證狀態與實測規格

### 已完成的靜態核對

本文件已完成下列 contract inspection：

| ID | 核對項目 | 結果 | 證據 |
| --- | --- | --- | --- |
| C-01 | Standard starter 與 package versions | 通過 | Sites plugin `0.1.27` starter files |
| C-02 | Artifact 必要路徑 | 通過 | Hosting skill 與 package helper 要求 `dist/server/index.js`、hosting metadata、migrations |
| C-03 | Hosting manifest | 通過 | Starter 預設 `d1: null`、`r2: null` |
| C-04 | D1／R2 storage guidance | 通過 | Sites persistence reference |
| C-05 | Workspace headers 與 SIWC reserved routes | 通過 | Sites authentication reference 與 starter helper |
| C-06 | Version／deployment 分離 | 通過 | Save、deploy、status connector contracts |
| C-07 | Environment revision 與 secrets | 通過 | Environment connector contracts |
| C-08 | Access modes 與 private deployment | 通過 | Access／deployment connector contracts |
| C-09 | Custom-domain lifecycle | 通過 | Domain connector contracts |
| C-10 | Browser API responsibility boundary | 通過 | MDN File、storage、speech、WebGPU、Fetch、Streams、CORS、cookie 與 HTTP references |
| C-11 | Capability Lab build／types／security contracts | 通過 | Vinext production build、TypeScript、11 個 Node contract tests |

這些是 contract inspection，不是 production behavior test。

### Capability Lab 實作與本機結果

`TEST`

已建立 [capability-lab](./capability-lab/)；它不是 mock 說明頁，而是會實際讀寫本機 Miniflare D1／R2、執行 browser APIs、建立 cookie session、串流 SSE、升級 WebSocket 與呼叫固定外部 upstream 的測試站。

本機環境：

```text
Date: 2026-07-16
Sites plugin: 0.1.27
Vinext: 0.0.50
Vite: 8.0.13
Wrangler: 4.92.0
Source commit: 1ba6a9cfc662f4011975bd3f88f36b6feef2b44f
Browser surfaces: Codex in-app browser + controlled Chrome
Result: 24 PASS／0 LIMITED／0 FAIL on each browser surface
```

| Probe | 本機結果 | 觀察證據 |
| --- | --- | --- |
| File／Blob | PASS | 13-byte generated file read-back |
| File picker fallback | PASS | Open／save picker 與 `<input type="file">` fallback 均存在；未讀取使用者檔案 |
| IndexedDB schema upgrade | PASS | Schema v2 upgrade、write／read／delete |
| OPFS | PASS | Write／read／delete round-trip |
| Storage quota／persistence | PASS | In-app profile quota 約 `10.74 GB`；Chrome quota `10737420913`、usage `2673`；兩者 persisted `false`；數值只代表各自 profile |
| `sessionStorage` | PASS | Write／read／delete round-trip |
| Silent Web Speech control | PASS | In-app browser voices `0`、Chrome voices `563`；兩者皆以 `volume=0` enqueue／cancel 長文；未播放音訊，因此不是發聲品質測試 |
| WebGPU device lifecycle | PASS | Adapter／device 成功；19 features；`maxTextureDimension2D=16384`；destroy 後 `device.lost=destroyed` |
| Service Worker lifecycle | PASS | Install／update／cache；scope `/`；synthetic failed navigation 實際取得 offline fallback；只快取靜態 fallback，不快取頁面／API |
| Worker runtime／bindings | PASS | D1、R2、ReadableStream presence |
| D1 | PASS | Schema ensure、insert、select、retention cleanup |
| R2 upload／download | PASS | 25-byte fixed-length streaming put 後立即 `head`／prefix `list`／get；safe attachment／nosniff；delete |
| Upload rejection | PASS | Type `415`；empty／oversize `413`；invalid JSON `422` |
| HTTP Range valid | PASS | Prefix／suffix `206`、`bytes 0-4/25`、body `sites`／`dtrip` |
| HTTP Range rejection | PASS | Multi／out-of-bounds `416`；unknown object `404` |
| Cookie + D1 session | PASS | Browser cookie create／read／revoke |
| Session rotation／expiry | PASS | Invalid TTL `400`；rotation；1-second expiry；cleanup |
| CORS allowlist | PASS | Exact same-origin allow header；無 wildcard credentials |
| CORS rejection | PASS | Sandboxed null-origin PUT／custom-header preflight 被阻擋 |
| SSE | PASS | 3 events 循序到達；第二條 stream 讀到第一 chunk 後 abort |
| WebSocket | PASS | `101` upgrade、echo、normal close |
| WebSocket reconnect | PASS | 兩次獨立 `101`／echo／normal close |
| Fixed API proxy | PASS | Fixed upstream `200`、559 bytes streamed |
| Proxy failure policy | PASS | Method `405`、unknown scenario `400`；header filtered；redirect／503／429／timeout／oversize 映射 `502` |

本機實測直接推翻兩個不精確假設並已修正實作與本文：

1. 對 R2 使用一般 `TransformStream` 會丟失 known-length metadata；改用 `FixedLengthStream` 後通過。
2. Workers runtime 明確拒絕 `fetch(..., { redirect: "error" })`；改用 `manual` 並自行拒絕 3xx 後通過。
3. 本機 Miniflare 將 `Response.error()` 轉成 HTTP `500`，Service Worker 不會進入單純的 fetch rejection catch；probe route 與 Service Worker 加入限定路徑的 `500` synthetic failure 判定後，實際 navigation fallback 通過。

本機結果是兩次實際受控瀏覽器操作：Codex 內建瀏覽器與 Chrome 都開啟 dashboard、點擊「執行全部 probes」，完成後各自顯示 `24 PASS／0 LIMITED／0 FAIL`。這不是從 source code 推測，也不是只執行 Node contract tests。

### Capability Lab 實作結構

測試站目前以公開分享部署在 `https://sites.eraser.tw`。所有公開探針只接受受限輸入、使用 server-generated key 與合成資料，並在完成後盡力清理。實際路由如下：

```text
/
├── Browser dashboard  # File/fallback、IndexedDB/OPFS、quota、silent TTS、WebGPU lifecycle、SW lifecycle
├── /api/runtime       # Runtime 與 D1／R2 binding presence；不回傳 secret
├── /api/db            # D1 schema、insert、select、retention cleanup
├── /api/upload        # FixedLengthStream → R2；type／size／JSON／origin validation；server-generated key；immediate head/list
├── /api/files/:id     # Download、Content-Disposition、prefix／suffix／invalid Range
├── /api/session       # D1-backed create／rotate／expire／revoke；cookie flags／CSRF
├── /api/cors          # Exact-origin OPTIONS allowlist；null/method/header rejection
├── /api/proxy         # Fixed scenario allowlist；timeout／redirect／5xx／429／oversize／header filtering
├── /api/stream        # SSE／ReadableStream
└── /api/ws            # WebSocketPair 101／echo probe
```

測試站不可回傳：

- 完整 identity header。
- Secret value。
- Repo credential。
- SIWC bypass token。
- 任意 D1／R2 internal ID。

尚未實作在測試站內的項目是外部付費 TTS、camera／microphone permission、跨使用者 ownership、受控冷啟動分布與非 Vinext artifact。它們需要額外 provider、測試帳號、權限情境、平台控制面或不同 artifact，不能由單一公開 deployment 產生有效結論。Custom subdomain 已完成；另已完成 12 次 sequential authenticated request 延遲樣本，但它不是 cold-start benchmark。

### Sites 正式環境結果

```text
Date: 2026-07-16
Sites plugin: 0.1.27
Default Sites URL: https://sites-capability-lab.eraser.chatgpt.site
Primary custom URL: https://sites.eraser.tw
Access mode: public sharing, access revision 2
Custom domain: active；provider active；SSL active
Environment revision: 3
Anonymous request: HTTP 200 on default and custom domains
Custom-domain browser run: raw 20 PASS／0 LIMITED／4 FAIL
Adjudicated result: 21 PASS／1 PARTIAL／2 FAIL
```

同日完成第三輪公開自訂網域驗證。`sites.eraser.tw` 的 CNAME 指向 `custom-domains.chatgpt.site`，Sites control plane 回報 domain、provider 與 SSL 全部 `active`。匿名請求可取得首頁、`sites-research.md` 與 `/api/runtime`；受控瀏覽器直接在 custom origin 執行 24 項 Live Lab。原始 DOM 顯示 `20 PASS／4 FAIL`；獨立 HTTP 複驗證明 API proxy header-filter 與全部 failure mapping 正常，因此最終判定為 `21 PASS／1 PARTIAL／2 FAIL`。兩項 FAIL 都是 WebSocket；PARTIAL 是 Service Worker navigation fallback。

Custom domain 建立後曾處於 pending 並短暫回 HTTP `500`；DNS propagation 與憑證完成後轉為 active，之後首頁、Markdown、runtime 與 `sw.js` 均穩定回 HTTP `200`。此觀察只記錄本次啟用流程，不構成憑證生效 SLA。

證據面必須分開解讀：

| 證據面 | 實際執行方式 | 已證明 | 未證明 |
| --- | --- | --- | --- |
| 本機可視瀏覽器 | 受控瀏覽器實際點擊 dashboard 按鈕並讀取完成後 DOM | 9 個 browser probes 與 15 個本機 Worker/runtime probes，共 24 項 | Sites production dispatcher 行為 |
| 正式站可視瀏覽器 | 匿名開啟 `https://sites.eraser.tw` 並點擊 Live Lab | Custom-origin browser APIs、D1、R2、Range、Session、CORS、SSE、WebSocket 與 Proxy 的真實 production 行為 | Safari／Firefox、第二身份、SIWC callback／logout |
| 正式站公開 HTTP | 無 bearer 對 default 與 custom URL 執行 HTTP probes | 首頁、Markdown、runtime、D1、R2、Range、Session、CORS、SSE、WebSocket handshake 與 Proxy response | Browser layout、WebGPU、Web Speech 等 client-only 行為 |
| Sites control plane／DNS | 讀取 custom-domain lifecycle，並向 authoritative、Cloudflare 與 Google DNS 查詢 | CNAME／TXT validation、provider、SSL 與 HTTPS active | 憑證 renewal／transfer SLA、apex domain |

因此「公開 production browser 已實測」仍不等於 workspace identity 或 SIWC 已驗證。切換 public 後不會進入 ChatGPT login／callback；這些身份測項需要另一個 private／workspace access 情境。WebSocket 的 HTTP `500` 則確實來自 production URL 的真實 upgrade request。

WebSocket 路徑另以兩種請求核對：一般 HTTP GET 對 default 與 custom domain 均回 `426 Upgrade Required`，證明 `/api/ws` route 可到達；帶 `Connection: Upgrade`、`Upgrade: websocket`、`Sec-WebSocket-Version` 與 `Sec-WebSocket-Key` 的標準握手，兩個網域均回 Sites `500` 錯誤頁，而非成功所需的 `101 Switching Protocols`。因此 custom domain、DNS、TLS 與 route existence 均不是本次失敗點，失敗發生在 protocol upgrade path。

| Production probe | 結果 | 觀察證據 |
| --- | --- | --- |
| Root／custom domain | PASS | Default 與 `sites.eraser.tw` 匿名 HTTP `200`；custom domain、provider、SSL 均 active；D1／R2 bindings 存在 |
| Public Markdown | PASS | `/sites-research.md` 匿名 HTTP `200`、`text/markdown`；可直接瀏覽與下載 |
| D1 persistence | PASS | Insert 後重新 select 成功；最新重跑 retained rows `9`，retention cleanup 正常 |
| Upload rejection | PASS | Unsupported `415`；empty／oversize `413`；invalid JSON `422`；cross-origin `403` |
| R2 upload／download／cleanup | PASS | 25 bytes put 後 immediate `head`／prefix `list`／get 均成功；content type／length、safe attachment、nosniff 正確；重複 delete `404` |
| HTTP Range | PASS | Prefix／suffix `206`；multi／out-of-bounds `416`；unknown object `404`；unsatisfied header `bytes */25` |
| D1-backed session | PASS | App `__Host-` cookie 為 Secure／HttpOnly／SameSite=Lax／host-only；rotation、old-token invalidation、expiry、revoke 正常 |
| Session CSRF／TTL | PASS | Cross-origin create／delete `403`；invalid TTL `400`；1-second expiry 生效 |
| CORS | PASS | Allowlisted preflight `204`；exact origin／credentials；未知／`null` origin、非法 method／header 均 `403` |
| SSE | PASS | HTTP `200`；3 events 依序抵達；第二條 stream 第一 chunk 後由 client abort |
| Fixed API proxy | PASS | Baseline streaming `200`；任意 `url` 參數無效；inbound secret header 未轉送；POST `405`；unknown scenario `400` |
| Proxy failure mapping | PASS | Redirect `302`、upstream `503`、`429`、timeout、100000-byte oversize 均映射為受控 `502` |
| Service Worker／custom domain | PARTIAL | `sw.js` HTTP `200`；install／update／offline cache 成功；首次 production navigation fallback 未取得預期離線頁，SIWC／logout cache 未測 |
| Identity header spoofing | PASS | Client 偽造 email／full-name headers 後，runtime 兩者皆為 absent；dispatcher 在進入 Worker 前移除偽造值 |
| SIWC bypass rotation | PASS | 產生新 token 後，舊 token 立即 `401`；新 token `200` |
| Environment／secret | MIXED | Secret 控制面回讀為 `null` 且未出現在 runtime；update 前後 revision 為 `1 → 2 → 3`。同一 version 重部署雖標示 revision `3` 仍讀到舊 label；新 version 部署後才讀到新 label |
| Sequential latency sample | PARTIAL | 12 次授權 `/api/runtime`：min `1127.2 ms`、median `1241.3 ms`、p95／max `1486.1 ms`；不是受控 cold-start、idle 或地理延遲 benchmark |
| WebSocket | **FAIL** | 一般 HTTP GET 對兩個網域均回預期 `426`；本機 `101`、echo／reconnect 通過；Sites production 兩個網域的真實 Upgrade 均回 HTTP `500`，未建立 WebSocket |

WebSocket 結果只證明目前 `sites@openai-bundled` `0.1.27`、此 deployment 與此 dispatch path 的實際行為。它不是「Cloudflare Workers 不支援 WebSocket」；底層 Workers API 在本機相同 artifact 已通過。正式設計應把 Sites WebSocket 視為未承諾且目前不可用，直到平台 contract 或新版 production probe 改變此結果。

Environment revision 測試先在 revision `2` 設定新 build label 與短暫 secret；control-plane read 回傳 `is_secret=true`、`value=null`，production 尚未部署時仍讀到舊 label。Secret 隨後於 revision `3` 移除。Version 5 的重新部署 `appgdep_6a584355e91c8191880a6a64248cfba5` 回報 `succeeded`、`env_set_revision=3`，但六次 live request 仍是舊 label。建立 version 6 後部署 `appgdep_6a58440d73f88191a7fc3c1b2fe2b320`，四次 live request 都讀到新 label。此證據表示目前 runtime environment 對 saved version 具有快照效應，且 deployment metadata 不能單獨證明 live value 已更新。

依賴稽核另發現 Next runtime 依賴鏈內的 PostCSS 有 2 個 moderate advisory。`npm audit fix --force` 會把 Next 強制降到 `9.3.3`，與目前 Vinext starter 不相容，因此未套用破壞性降版；應由新版 starter／Vinext dependency graph 解決。

### Production 實測矩陣

狀態定義：`PASS` 表示成功判定的核心路徑已在 production 重現；`PARTIAL` 表示只覆蓋部分判定；`FAIL` 表示判定已被反例推翻；`未執行` 表示沒有有效 production evidence。

目前統計：`PASS 7`、`PARTIAL 16`、`FAIL 2`、`未執行 6`。

| ID | 測試 | 成功判定 | 目前狀態 |
| --- | --- | --- | --- |
| L-01 | Owner-only private deployment | 非 owner 無法存取；owner 可開啟 production URL | PARTIAL（歷史狀態）：早期 custom policy 僅含 owner、匿名 `401`、owner browser flow 停在 MFA；2026-07-16 已切換 public，匿名改為 `200` |
| L-02 | `workspace_all` access | Workspace member 可開啟；外部 user 不可開啟 | 未執行 |
| L-03 | `custom` user／group access | Allowlist 精確生效；owner 始終保留 | 未執行 |
| L-04 | Workspace identity headers | Email 存在；full name optional 且 encoding 正確 | PARTIAL：bypass request 沒有 workspace identity；client 偽造 email／full-name headers 會被 dispatcher 移除；尚未完成 MFA 後真實 workspace identity read |
| L-05 | SIWC page flow | Anonymous redirect、returnTo、callback、sign-out 正常 | PARTIAL：早期 private 模式進入 ChatGPT 登入與 MFA；callback／sign-out 未執行；目前 public 模式不觸發 SIWC |
| L-06 | SIWC bypass rotation | 新 token 可用；舊 token 立即失效；app auth 仍生效 | PASS：rotation 後舊 token `401`、新 token `200`；token 只存於測試程序記憶體 |
| L-07 | Environment revision | Update 後未 redeploy 不生效；redeploy 後新 revision 生效 | **FAIL：未部署前維持舊值符合預期；同一 version 重部署雖回報 revision `3`，六次 live request 仍是舊值；建立新 version 後才套用 revision `3`** |
| L-08 | Secret confidentiality | Read API 不回傳明文；client bundle 無 secret | PASS：短暫 secret 回讀為 `null`、標記 `is_secret=true`，未出現在 runtime／source／client bundle；final revision 已移除 secret |
| L-09 | D1 CRUD／ownership | 跨 user row access 被拒絕；prepared query 正常 | PARTIAL：production insert／select／cleanup 通過；未建立第二 user 測跨 user 拒絕 |
| L-10 | D1 migration | Migration 套用一次且 schema version 可查 | PASS：deployment 套用 migration；production schema 可 insert／select；重複探針未重建資料表 |
| L-11 | Code rollback after migration | 舊 version 對新 schema 的相容性結果被記錄 | 未執行 |
| L-12 | R2 stream upload | 不完整 buffer 大檔；owner、type、size、key 驗證正常 | PARTIAL：25-byte `FixedLengthStream`、server-generated key、type／size、oversize／empty／invalid JSON rejection 與 cleanup 通過；未測傳輸中斷及接近平台上限的大檔 |
| L-13 | R2 consistency path | Put 後立即 get/list；cache layer 差異被記錄 | PASS：production put 後 immediate `head`、prefix `list`、get 全部成功；測試站沒有 application cache layer，故無額外 cache 差異 |
| L-14 | SSE／streaming | Token 逐步抵達；disconnect 能 abort upstream | PARTIAL：3 events 逐步抵達；client 在第一 chunk 後 abort 成功；此測試沒有外部 upstream 可觀察取消傳播 |
| L-15 | WebSocket production probe | Production URL 成功回傳 101 並可 echo／reconnect | **FAIL：production 兩次 upgrade 均 HTTP 500；相同 artifact 本機 echo／reconnect 成功** |
| L-16 | Cold-start measurement | 新 deployment、idle 後與 warm request 分布被記錄 | PARTIAL：12 次 sequential authenticated request 為 min `1127.2 ms`、median `1241.3 ms`、p95 `1486.1 ms`；未控制新 isolate、idle、地理位置或 dispatcher latency |
| L-17 | Custom subdomain | CNAME／validation／SSL 至 `active`，HTTPS 正常 | PASS：`sites.eraser.tw` CNAME 指向 `custom-domains.chatgpt.site`；verification TXT、provider、SSL 全部 active；首頁、Markdown、runtime、`sw.js` 均 HTTPS `200` |
| L-18 | Apex domain | A records／validation／SSL 至 `active` | 未執行 |
| L-19 | External API failure | Timeout、abort、5xx、rate limit 被正確映射 | PARTIAL：stream、timeout、503、429 mapping 通過；client-abort propagation 未取得 upstream 可觀察證據 |
| L-20 | Non-Vinext artifact | Build、package、deploy、static assets、SSR 全部通過 | 未執行 |
| L-21 | File API fallback | 支援 picker 的 browser 可開檔；不支援時退回 input；拒絕 permission 不破壞主流程 | PARTIAL：Codex in-app browser 與 Chrome 的 picker／input fallback contract、File／Blob read-back 均通過；兩者都支援 picker，未取得 unsupported／permission-denied 實例 |
| L-22 | IndexedDB／OPFS lifecycle | Reload 後資料仍在；quota failure、清除 site data 與 schema upgrade 有可恢復結果 | PARTIAL：IndexedDB v2 upgrade、IndexedDB／OPFS round-trip、quota／persisted state 通過；未強制 quota failure／eviction／clear-site-data recovery |
| L-23 | Upload rejection paths | Oversize、偽造 MIME／signature、未授權 object、partial upload 均被拒絕或清理 | PARTIAL：oversize／empty／unsupported type／invalid JSON／cross-origin 均拒絕且 cleanup 為 0；跨 owner 與傳輸中斷需第二 identity／raw transport |
| L-24 | Protected download／Range | 非 owner 為 403／404；attachment filename 安全；valid／invalid Range 正確 | PARTIAL：safe attachment、nosniff、prefix／suffix Range、multi／out-of-bounds `416`、unknown `404` 通過；跨 owner 未執行 |
| L-25 | Browser speech synthesis | Voice load、中文、cancel、長文、background 與 unsupported fallback 被記錄 | PARTIAL：長文以 volume `0` enqueue／cancel 通過；in-app voices `0`、Chrome voices `563`；未測實際發聲、中文品質、background 與非 Chromium fallback |
| L-26 | External TTS | Secret 不出 client；timeout／quota／provider error 可控；stream 或 R2 asset 可授權下載 | 未執行 |
| L-27 | WebGPU capability | Chrome／Edge／Safari／Firefox 目標版本記錄 adapter、limits、device loss 與 fallback | PARTIAL：Codex in-app browser 與 Chrome 都完成 adapter／device、19 features、`maxTextureDimension2D=16384`、destroy/lost；未做 Safari／Firefox／WASM fallback |
| L-28 | Cookie／session security | `Secure`／`HttpOnly`／`SameSite`／host-only、rotation、expiry、logout、CSRF 全部生效 | PASS：只解析 app `__Host-` cookie；flags／host-only、rotation、old-token invalidation、1-second expiry、revoke、cross-origin create/delete rejection 均通過 |
| L-29 | Credentialed CORS | Allowlist origin 通過；未知／`null` origin、wildcard credentials 與非法 preflight 被拒絕 | PASS：allowlisted preflight `204`、exact origin／credentials；未知／`null` origin及非法 method／header 均 `403`；無 wildcard credentials |
| L-30 | API proxy hardening | 任意 host／redirect SSRF／header smuggling／oversize／timeout 被拒絕；stream 不完整 buffer | PARTIAL：任意 URL 無效、固定 allowlist、inbound header filtering、redirect、oversize、timeout、503／429 mapping 與 stream 通過；Fetch 會正規化 header，未做 raw-socket smuggling |
| L-31 | Service Worker／custom domain | Install、update、offline、SIWC redirect、logout 與 custom-domain scope 無 stale auth cache | PARTIAL：本機兩個 browser surface 完成 install／update／offline fallback；custom domain 完成 `sw.js` delivery、install／update／cache，但首次 navigation fallback 未取得預期離線頁；SIWC／logout cache 未執行 |

### 實測記錄格式

每次實測至少保存：

```text
Test ID:
Date:
Sites plugin version:
Source commit SHA:
Saved version number:
Environment revision:
Access mode:
Production URL classification:
Expected result:
Observed result:
Evidence:
Conclusion: PASS／FAIL／INCONCLUSIVE
Notes:
```

不要只記錄「沒有報錯」。成功判定必須包含正向結果、拒絕路徑、錯誤行為與資料是否持久化。

## 尚未確認的問題

以下項目仍缺完整 contract 或 production evidence：

1. Sites 對每個 Workspace、site、deployment、request、D1、R2 的正式 quota 與 pricing。
2. Sites 使用的實際 Cloudflare plan mapping。
3. Production runtime 是否允許調整 `cpu_ms`。
4. Production request body、asset count、asset size 與 archive size 上限。
5. D1 Time Travel／restore 是否可由 Sites 使用者操作。
6. R2 direct upload、presigned URL、public bucket、lifecycle rule 與 event notification。
7. WebSocket 在目前 production dispatch 回 `500` 的原因、修正版本、concurrency、duration、hibernation 與 SLA。
8. Durable Objects、KV、Queues、Cron、Workflows、Images、Vectorize 等 bindings。
9. Runtime logs、metrics、traces、retention、alerts 與 export。
10. Staging／preview deployment、traffic splitting、canary 與 gradual rollout。
11. Data residency 是否及如何涵蓋 Sites source、D1、R2、logs 與 backups。
12. Custom-domain certificate renewal SLA、apex constraints 與 domain transfer；本次 subdomain activation 已通過。
13. Site archive／delete、resource cleanup、D1／R2 retention 與 recovery policy。
14. Non-Vinext framework 的正式支援矩陣。
15. Sites production 對 COOP／COEP、Web App Manifest、custom-domain Service Worker navigation fallback 與 SIWC/logout cache interaction 的完整行為；custom-domain install／update／cache 已通過。
16. Sites request handler 對 multipart、接近上限的 streaming upload、超大型 download Range 與 client disconnect 的實際限制；25-byte upload 與 prefix／suffix／invalid Range 已通過。
17. Custom domain、預設 Sites domain 與 SIWC dispatcher 間的跨 host cookie／SameSite 邊界；custom-domain app `__Host-` session 與 exact-origin CORS 已通過。
18. Environment revision 為何在同一 saved version 重部署時只更新 deployment metadata、未更新 live runtime，以及此快照語意是否為正式 contract 或平台缺陷。

## 參考資料

### Sites 本機契約快照

本文件依據目前安裝的 `sites@openai-bundled` `0.1.27`：

- `sites-building` skill。
- `sites-hosting` skill。
- Vinext starter、Vite config、Worker entry、auth helper。
- Persistence／authentication references。
- Artifact packaging helper。
- Sites connector descriptions and schemas。

這些資料用來判斷 `SITES` 標記。產品更新後應重新核對 plugin version、starter 與 connector schema。

### Vinext

- [Cloudflare Vinext](https://github.com/cloudflare/vinext)

### Browser／Web Platform

- [MDN File API](https://developer.mozilla.org/en-US/docs/Web/API/File_API)
- [MDN File System API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API)
- [MDN IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
- [MDN Web Storage `sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)
- [MDN Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API)
- [MDN WebGPU API](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API)
- [MDN Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
- [MDN Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
- [MDN CORS guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS)
- [MDN HTTP cookies guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies)
- [MDN `Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Disposition)
- [MDN HTTP range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests)

### Cloudflare Workers

- [Workers limits](https://developers.cloudflare.com/workers/platform/limits/)
- [Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/)
- [Workers best practices](https://developers.cloudflare.com/workers/best-practices/workers-best-practices/)
- [Workers WebSockets API](https://developers.cloudflare.com/workers/runtime-apis/websockets/)
- [EventSource API](https://developers.cloudflare.com/workers/runtime-apis/eventsource/)

### Cloudflare D1

- [D1 overview](https://developers.cloudflare.com/d1/)
- [D1 limits](https://developers.cloudflare.com/d1/platform/limits/)
- [D1 Time Travel and backups](https://developers.cloudflare.com/d1/reference/time-travel/)
- [Drizzle D1 guide](https://orm.drizzle.team/docs/get-started/d1-new)

### Cloudflare R2

- [R2 overview](https://developers.cloudflare.com/r2/)
- [R2 limits](https://developers.cloudflare.com/r2/platform/limits/)
- [R2 consistency model](https://developers.cloudflare.com/r2/reference/consistency/)

### Cloudflare Durable Objects

- [Durable Objects overview](https://developers.cloudflare.com/durable-objects/)
- [Durable Objects WebSockets](https://developers.cloudflare.com/durable-objects/best-practices/websockets/)

## 最終定位

Sites 的核心不是「一台由使用者控制的 Cloudflare 主機」，而是：

> 由 Codex 建置 Worker-compatible full-stack artifact，再由 Sites 控制平面管理 source provenance、saved versions、production deployment、access policy、environment、D1／R2 wiring 與 custom domains。

當需求位於 UI、SSR、CRUD、files、ChatGPT identity、HTTP streaming 與 API orchestration 範圍內，Sites 可作主要 application platform。當需求涉及固定主機資源、Python／Java process、長時間背景運算、可靠 realtime coordination、未暴露的 Cloudflare binding、PostgreSQL／Redis 特殊能力或 GPU 時，將該能力部署為外部服務，Sites 保留網站、身分、版本與 edge integration 職責。
