# Construction: Tích hợp API uy tín vào web test

Tài liệu này ghi lại cách web test trong thư mục `tích hợp thử nghiệm` được tích hợp với nền tảng API uy tín.

> Phạm vi: web test độc lập, chạy bằng Cloudflare Pages Functions.
>
> Nguyên tắc: `PK` có thể đi qua browser để bắt đầu OAuth; `SK`, `CHUOTAPI_...` và `SESSION_SECRET` chỉ được dùng ở server-side.
>
> Không ghi credential thật vào tài liệu, Git, HTML hoặc JavaScript frontend.

## 1. Tổng quan kiến trúc

Web test gồm hai phần:

```text
Browser
  |
  | GET /auth/google
  v
Web test Pages Function
  |
  | redirect với PK + redirect_uri + state
  v
API uy tín
  |
  | kiểm tra PK và redirect_uri của project
  v
Google Account Chooser
  |
  | Google xác thực rồi trả code về callback nền tảng của API uy tín
  v
API uy tín: /oauth/google/callback
  |
  | redirect code/state về callback của web test
  v
Web test: /auth/callback
  |
  | POST PK + SK + code + redirect_uri
  v
API uy tín: /api/auth/oauth/token
  |
  | trả access_token và user
  v
Web test tạo session HttpOnly và hiển thị user
```

Các URL production:

```text
Web test:       https://apitestingtichchopoauth.pages.dev
Test callback:  https://apitestingtichchopoauth.pages.dev/auth/callback
API base:       https://apiuytin.pages.dev/api/auth
OAuth start:    https://apiuytin.pages.dev/oauth/google
Token endpoint: https://apiuytin.pages.dev/api/auth/oauth/token
Userinfo:       https://apiuytin.pages.dev/api/auth/userinfo
```

## 2. Cấu trúc thư mục web test

```text
`tích hợp thử nghiệm/`
├── .env                         # credential runtime local, không commit
├── .deploy.env                  # Cloudflare deploy credential, không commit
├── package.json                 # Wrangler và lệnh deploy
├── wrangler.toml                # Pages project và public vars
├── functions/
│   ├── _lib/test-auth.js        # HMAC cookie, cookie parser, API helpers
│   ├── auth/google.js           # bắt đầu login
│   ├── auth/callback.js         # nhận code và đổi token qua API uy tín
│   └── api/
│       ├── session.js           # đọc session hiện tại
│       ├── logout.js            # xóa session cookie
│       └── project-status.js    # gọi CHUOTAPI_ server-side
└── public/
    ├── index.html               # giao diện test
    ├── styles.css               # giao diện code-dark/run-green
    ├── app.js                   # tương tác browser
    └── avatar-placeholder.svg   # ảnh dự phòng
```

## 3. Biến môi trường

File `.env` của web test dùng các biến sau:

```dotenv
PK=pk_...
API-key=CHUOTAPI_...
SK=sk_...
direct_callback_url=https://apitestingtichchopoauth.pages.dev/auth/callback
```

Mapping khi đưa lên Cloudflare Pages Functions:

```text
PK       -> secret PK
SK       -> secret SK
API-key  -> secret API_KEY
SESSION_SECRET -> secret SESSION_SECRET được sinh riêng cho web test
```

File `wrangler.toml` chứa biến public, không chứa secret:

```toml
name = "apitestingtichchopoauth"
compatibility_date = "2026-09-13"
pages_build_output_dir = "public"

[vars]
APIUYTIN_BASE_URL = "https://apiuytin.pages.dev"
APIUYTIN_CALLBACK_URL = "https://apitestingtichchopoauth.pages.dev/auth/callback"
```

### Quy tắc bảo mật

- Không đặt `SK` hoặc `CHUOTAPI_...` trong `public/index.html`.
- Không đặt `SK` hoặc `CHUOTAPI_...` trong `public/app.js`.
- Không gửi secret qua query string.
- Không log giá trị token, authorization code hoặc secret.
- `PK` là publish key nên có thể dùng để khởi động OAuth, nhưng vẫn phải gửi callback URL đã đăng ký.
- `SK` chỉ gửi từ Pages Function tới API token endpoint.
- `CHUOTAPI_...` chỉ gửi từ Pages Function tới API project endpoint.

## 4. Flow đăng nhập hoàn chỉnh

### Bước 1: User bấm nút đăng nhập

Trong `public/index.html` có nút:

```html
<button id="login" class="button primary" type="button">
  <span class="google-mark">G</span>
  Đăng nhập bằng Google
</button>
```

`public/app.js` không cầm secret. Browser chỉ gọi route nội bộ của web test:

```js
login.addEventListener('click', () => {
  login.disabled = true;
  showMessage('Đang chuyển tới Google Account Chooser...');
  location.href = '/auth/google';
});
```

### Bước 2: Web test tạo state và redirect tới API uy tín

File `functions/auth/google.js`:

```js
import {
  cookie,
  randomState,
  redirect,
  sign
} from '../_lib/test-auth.js';

export async function onRequestGet({ env }) {
  const state = randomState();
  const secret = env.SESSION_SECRET || env.SK;

  const signedState = await sign(
    JSON.stringify({
      state,
      exp: Date.now() + 10 * 60 * 1000
    }),
    secret
  );

  const authorizeUrl = new URL(
    'https://apiuytin.pages.dev/oauth/google'
  );

  authorizeUrl.searchParams.set('client_id', env.PK);
  authorizeUrl.searchParams.set(
    'redirect_uri',
    env.APIUYTIN_CALLBACK_URL
  );
  authorizeUrl.searchParams.set('state', state);

  return redirect(authorizeUrl.toString(), {
    'set-cookie': cookie(
      'testapi_oauth_state',
      signedState,
      600
    )
  });
}
```

Request thực tế có dạng:

```text
GET https://apiuytin.pages.dev/oauth/google
  ?client_id=pk_YOUR_PUBLISH_KEY
  &redirect_uri=https%3A%2F%2Fapitestingtichchopoauth.pages.dev%2Fauth%2Fcallback
  &state=RANDOM_STATE
```

API uy tín kiểm tra:

1. `client_id` có phải `pk_...` tồn tại không.
2. `redirect_uri` có khớp tuyệt đối project không.
3. Project còn active không.
4. Sau đó API uy tín redirect tới Google với `prompt=select_account`.

### Bước 3: Google Account Chooser

API uy tín sử dụng Google OAuth với các scope:

```text
openid email profile
```

Và tham số:

```text
prompt=select_account
response_type=code
redirect_uri=https://apiuytin.pages.dev/oauth/google/callback
```

Callback Google cố định của nền tảng là:

```text
https://apiuytin.pages.dev/oauth/google/callback
```

Callback này phải được whitelist trong Google Cloud Console.

### Bước 4: API uy tín redirect về callback của web test

Sau khi Google xác thực, API uy tín kiểm tra lại project và redirect về:

```text
https://apitestingtichchopoauth.pages.dev/auth/callback?code=ONE_TIME_CODE&state=RANDOM_STATE
```

URL callback chỉ nhận `code` và `state`.

Không bao giờ đưa những giá trị này vào URL:

```text
SK=...
CHUOTAPI_...
access_token=...
```

### Bước 5: Web test đổi code lấy token

Đây là điểm quan trọng nhất.

`code` do API uy tín phát hành không được gửi trực tiếp tới Google Token API. Web test phải gửi code tới token endpoint của API uy tín bằng `PK`, `SK` và callback URL chính xác.

File `functions/auth/callback.js`:

```js
import {
  cookie,
  getCookie,
  read,
  redirect,
  sign
} from '../_lib/test-auth.js';

export async function onRequestGet({ request, env }) {
  const requestUrl = new URL(request.url);
  const code = requestUrl.searchParams.get('code');
  const state = requestUrl.searchParams.get('state');
  const secret = env.SESSION_SECRET || env.SK;

  const signedState = await read(
    getCookie(request, 'testapi_oauth_state'),
    secret
  );

  if (!code || !signedState) {
    return new Response(
      'OAuth state missing or expired',
      { status: 400 }
    );
  }

  try {
    const flow = JSON.parse(signedState);

    if (flow.state !== state || flow.exp < Date.now()) {
      throw new Error('Invalid OAuth state');
    }

    const tokenResponse = await fetch(
      `${env.APIUYTIN_BASE_URL}/api/auth/oauth/token`,
      {
        method: 'POST',
        headers: {
          'content-type': 'application/json'
        },
        body: JSON.stringify({
          grant_type: 'authorization_code',
          code,
          client_id: env.PK,
          client_secret: env.SK,
          redirect_uri: env.APIUYTIN_CALLBACK_URL
        })
      }
    );

    const tokenPayload = await tokenResponse.json();

    if (!tokenResponse.ok) {
      throw new Error(
        tokenPayload.error ||
        'APIUYTIN token exchange failed'
      );
    }

    const user = tokenPayload.user;
    const session = await sign(
      JSON.stringify({
        sub: user.id,
        email: user.email,
        name: user.name || user.email,
        picture: user.picture || '',
        exp: Date.now() + 7 * 24 * 60 * 60 * 1000
      }),
      secret
    );

    const response = redirect('/', {
      'set-cookie': cookie(
        'testapi_session',
        session,
        7 * 24 * 60 * 60
      )
    });

    response.headers.append(
      'Set-Cookie',
      cookie('testapi_oauth_state', '', 0)
    );

    return response;
  } catch (error) {
    return new Response(
      error.message || 'Authentication failed',
      { status: 400 }
    );
  }
}
```

### Bước 6: Session HttpOnly

Sau token exchange thành công, web test ký session bằng `SESSION_SECRET` và trả cookie:

```text
Set-Cookie: testapi_session=...; Path=/; HttpOnly; Secure; SameSite=Lax
```

Cookie không thể đọc bằng JavaScript browser.

`functions/api/session.js` đọc cookie và trả profile an toàn:

```js
import { getCookie, json, read } from '../_lib/test-auth.js';

export async function onRequestGet({ request, env }) {
  const raw = await read(
    getCookie(request, 'testapi_session'),
    env.SESSION_SECRET || env.SK
  );

  if (!raw) return json({ user: null });

  try {
    const user = JSON.parse(raw);
    return user.exp > Date.now()
      ? json({ user })
      : json({ user: null });
  } catch {
    return json({ user: null });
  }
}
```

## 5. Frontend hiển thị user

Frontend không đổi code/token. Frontend chỉ gọi session endpoint:

```js
async function loadSession() {
  const response = await fetch('/api/session');
  const data = await response.json();

  if (data.user) {
    document.querySelector('#avatar').src =
      data.user.picture || '/avatar-placeholder.svg';
    document.querySelector('#display-name').textContent =
      data.user.name || data.user.email;
    document.querySelector('#email').textContent =
      data.user.email;
  }
}
```

Giao diện có các trạng thái:

- Chưa đăng nhập: nút Đăng nhập Google.
- Đang redirect: thông báo đang mở Account Chooser.
- Đã đăng nhập: avatar, display name, email.
- Đăng xuất: xóa cookie session qua server.

## 6. Dùng CHUOTAPI_ thật ở server-side

Trang test có nút “Kiểm tra project API”. Browser gọi endpoint nội bộ:

```js
checkApi.addEventListener('click', async () => {
  checkApi.disabled = true;
  apiResult.textContent = 'Đang gọi API server-side...';

  const response = await fetch('/api/project-status');
  const data = await response.json();

  apiResult.textContent = JSON.stringify(data, null, 2);
  checkApi.disabled = false;
});
```

Browser không biết `CHUOTAPI_`. Pages Function dùng secret `API_KEY`:

```js
export async function apiuytinProject(env) {
  const response = await fetch(
    `${env.APIUYTIN_BASE_URL}/api/auth/project-info`,
    {
      headers: {
        Authorization: `Bearer ${env.API_KEY}`
      }
    }
  );

  if (!response.ok) {
    throw new Error(
      'APIUYTIN project API rejected the test API key'
    );
  }

  return response.json();
}
```

Route `functions/api/project-status.js` chỉ trả metadata không nhạy cảm:

```js
import { apiuytinProject, json } from '../_lib/test-auth.js';

export async function onRequestGet({ env }) {
  try {
    const project = await apiuytinProject(env);

    return json({
      ok: true,
      project: {
        id: project.id,
        name: project.name,
        redirectUri: project.redirectUri,
        status: project.status
      }
    });
  } catch (error) {
    return json({
      ok: false,
      error: error.message
    }, 502);
  }
}
```

Request server-side tương đương:

```http
GET https://apiuytin.pages.dev/api/auth/project-info
Authorization: Bearer CHUOTAPI_YOUR_PROJECT_API_KEY
```

Response thành công:

```json
{
  "id": "project-...",
  "name": "TEST",
  "publishKey": "pk_...",
  "redirectUri": "https://apitestingtichchopoauth.pages.dev/auth/callback",
  "status": "active"
}
```

## 7. Đăng xuất

Frontend gọi:

```js
await fetch('/api/logout', { method: 'POST' });
location.reload();
```

Function:

```js
import { cookie, json } from '../_lib/test-auth.js';

export async function onRequestPost() {
  return json(
    { ok: true },
    200,
    { 'set-cookie': cookie('testapi_session', '', 0) }
  );
}
```

## 8. Tích hợp từ một web khác

Web khác không nên copy code secret vào frontend. Cấu trúc khuyến nghị:

```text
Frontend web khác
  |
  | redirect tới APIUYTIN bằng pk_
  v
APIUYTIN OAuth
  |
  | Google Account Chooser
  v
Callback backend của web khác
  |
  | gửi code + pk_ + sk_ + redirect_uri
  v
APIUYTIN /api/auth/oauth/token
  |
  v
Session riêng của web khác
```

### Frontend của web khác

```js
function loginWithApiUyTin() {
  const state = crypto.randomUUID();
  sessionStorage.setItem('oauth_state', state);

  const url = new URL(
    'https://apiuytin.pages.dev/oauth/google'
  );

  url.searchParams.set(
    'client_id',
    'pk_YOUR_PROJECT_PUBLISH_KEY'
  );
  url.searchParams.set(
    'redirect_uri',
    'https://your-site.example/auth/callback'
  );
  url.searchParams.set('state', state);

  window.location.assign(url.toString());
}
```

### Express/Node.js backend của web khác

```js
import express from 'express';

const app = express();

app.get('/auth/callback', async (request, response) => {
  const { code, state } = request.query;

  if (!code || !state) {
    return response.status(400).send('Missing OAuth response');
  }

  // So sánh state với giá trị đã lưu trong session của ứng dụng.
  const tokenResponse = await fetch(
    'https://apiuytin.pages.dev/api/auth/oauth/token',
    {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'authorization_code',
        code,
        client_id: process.env.APIUYTIN_PUBLISH_KEY,
        client_secret: process.env.APIUYTIN_SECRET_KEY,
        redirect_uri: 'https://your-site.example/auth/callback'
      })
    }
  );

  const token = await tokenResponse.json();

  if (!tokenResponse.ok) {
    return response.status(401).json(token);
  }

  // Lưu token/session ở server của ứng dụng bạn.
  request.session.user = token.user;
  request.session.accessToken = token.access_token;

  return response.redirect('/account');
});
```

### Python/Flask backend

```python
import os
import requests
from flask import Flask, request, redirect, session

app = Flask(__name__)
app.secret_key = os.environ["APP_SESSION_SECRET"]

@app.get("/auth/callback")
def auth_callback():
    code = request.args.get("code")
    state = request.args.get("state")

    if not code or not state:
        return {"error": "missing_oauth_response"}, 400

    result = requests.post(
        "https://apiuytin.pages.dev/api/auth/oauth/token",
        json={
            "grant_type": "authorization_code",
            "code": code,
            "client_id": os.environ["APIUYTIN_PUBLISH_KEY"],
            "client_secret": os.environ["APIUYTIN_SECRET_KEY"],
            "redirect_uri": "https://your-site.example/auth/callback"
        },
        timeout=15
    )

    if not result.ok:
        return result.json(), result.status_code

    token = result.json()
    session["user"] = token["user"]
    session["access_token"] = token["access_token"]
    return redirect("/account")
```

### cURL server-to-server

```bash
curl -X POST \
  https://apiuytin.pages.dev/api/auth/oauth/token \
  -H 'content-type: application/json' \
  -d '{
    "grant_type": "authorization_code",
    "code": "ONE_TIME_CODE",
    "client_id": "pk_YOUR_PUBLISH_KEY",
    "client_secret": "sk_YOUR_SECRET_KEY",
    "redirect_uri": "https://your-site.example/auth/callback"
  }'
```

## 9. Userinfo sau khi có access token

```js
const response = await fetch(
  'https://apiuytin.pages.dev/api/auth/userinfo',
  {
    headers: {
      Authorization: `Bearer ${accessToken}`
    }
  }
);

const user = await response.json();
```

Response:

```json
{
  "sub": "google-subject-id",
  "email": "user@example.com",
  "name": "Google Display Name",
  "picture": "https://lh3.googleusercontent.com/..."
}
```

## 10. Các kiểm tra bảo mật của API uy tín

API uy tín kiểm tra các giá trị sau khi đổi code:

1. `client_id` phải là publish key của project.
2. `client_secret` phải hash khớp secret key của cùng project.
3. `redirect_uri` phải khớp tuyệt đối callback đã đăng ký.
4. Authorization code phải thuộc cùng project.
5. Authorization code phải cùng redirect URI.
6. Authorization code chưa hết hạn.
7. Authorization code chưa được dùng.
8. Project chưa bị revoke.
9. Token sau revoke bị từ chối khi gọi userinfo.

Không được dùng callback khác với callback đã đăng ký:

```text
Đúng:
https://apitestingtichchopoauth.pages.dev/auth/callback

Sai:
https://apitestingtichchopoauth.pages.dev/auth/callback/
https://www.apitestingtichchopoauth.pages.dev/auth/callback
http://apitestingtichchopoauth.pages.dev/auth/callback
```

## 11. Chạy local

Từ thư mục test:

```powershell
cd "tích hợp thử nghiệm"
npm install
npx wrangler pages dev public
```

Nếu chạy local, callback phải được cấu hình riêng trong project API uy tín. Không dùng callback production cho localhost trừ khi project đã đăng ký chính xác URL đó.

## 12. Deploy web test

Deploy dùng credential trong `.deploy.env` của thư mục test:

```powershell
cd "tích hợp thử nghiệm"
$env:CLOUDFLARE_API_TOKEN = "..."
$env:CLOUDFLARE_ACCOUNT_ID = "..."
npx wrangler pages deploy public `
  --project-name apitestingtichchopoauth `
  --commit-dirty=true
```

Trong thực tế, dùng PowerShell đọc `.deploy.env` hoặc Wrangler profile; không commit `.deploy.env`.

Các secret Pages cần có:

```text
PK
SK
API_KEY
SESSION_SECRET
```

Kiểm tra production:

```powershell
Invoke-WebRequest https://apitestingtichchopoauth.pages.dev/
Invoke-WebRequest https://apitestingtichchopoauth.pages.dev/api/project-status
```

Kết quả kỳ vọng:

```text
Homepage: 200
/api/project-status: 200
/auth/google: 302
```

## 13. Những lỗi thường gặp

### `Unknown client or redirect_uri is not registered`

Nguyên nhân thường là:

- `PK` không thuộc project đang được truy vấn.
- Callback trên web test khác callback trong Sanity.
- Có slash cuối URL không đồng nhất.
- Pages đang dùng secret cũ.
- Project đã bị revoke.

Cách kiểm tra:

1. Đọc callback hiện tại trong `.env`.
2. Kiểm tra `APIUYTIN_CALLBACK_URL` trong `wrangler.toml`.
3. Kiểm tra project record trên API uy tín.
4. Nạp lại secret `PK` cho Pages.
5. Redeploy Pages.

### `APIUYTIN project API rejected the test API key`

Nguyên nhân thường là:

- Secret Pages không có `API_KEY`.
- Secret có tên sai, ví dụ dùng `API-key` thay vì `API_KEY`.
- API key thuộc project khác.
- Project bị revoke.
- Pages deploy ở account khác.

### `Google token exchange failed`

Với flow API uy tín, web test không được gửi code APIUYTIN trực tiếp tới Google Token API.

Sai:

```js
await fetch('https://oauth2.googleapis.com/token', {
  // code do API uy tín phát hành
});
```

Đúng:

```js
await fetch(
  'https://apiuytin.pages.dev/api/auth/oauth/token',
  {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      code,
      client_id: env.PK,
      client_secret: env.SK,
      redirect_uri: env.APIUYTIN_CALLBACK_URL
    })
  }
);
```

### Session không tồn tại sau callback

Kiểm tra:

- Callback gửi hai `Set-Cookie` header riêng.
- Cookie có `HttpOnly; Secure; SameSite=Lax`.
- `SESSION_SECRET` có tồn tại và ổn định.
- Browser đang chạy HTTPS.
- Request `/api/session` gửi cookie cùng origin.

## 14. Tóm tắt contract tích hợp

```text
1. Browser dùng PK để gọi /oauth/google.
2. API uy tín mở Google Account Chooser.
3. Google trả code về callback nền tảng.
4. API uy tín redirect code về callback project.
5. Backend project gửi code + PK + SK + redirect_uri tới /api/auth/oauth/token.
6. API uy tín trả access_token và user.
7. Backend project tạo session riêng.
8. Backend có thể gọi /userinfo bằng access_token.
9. Backend có thể gọi /project-info bằng CHUOTAPI_.
```

Đó là toàn bộ cách web test tích hợp và sử dụng API uy tín theo flow thực tế.

---

# PHỤ LỤC CHI TIẾT: RUNBOOK, KIẾN TRÚC VÀ KIỂM THỬ WEB TEST

Phụ lục này bổ sung mô tả triển khai, vận hành, bảo mật, quan sát, kiểm thử
và xử lý sự cố cho đúng project trong thư mục `tích hợp thử nghiệm`.

Các nội dung bên dưới mô tả trạng thái và hành vi của source hiện tại.

Không đặt credential thật, token thật, authorization code thật hoặc nội dung
cookie thật vào tài liệu.

## A. Phạm vi tài liệu bổ sung

- Project được triển khai như Cloudflare Pages project.

- Thư mục build public là `public`.

- Pages Functions được phát hiện từ thư mục `functions`.

- Frontend chính nằm tại `public/index.html`.

- Frontend behavior nằm tại `public/app.js`.

- CSS chính nằm tại `public/styles.css`.

- OAuth start route nằm tại `functions/auth/google.js`.

- OAuth callback route nằm tại `functions/auth/callback.js`.

- Session route nằm tại `functions/api/session.js`.

- Logout route nằm tại `functions/api/logout.js`.

- Project status route nằm tại `functions/api/project-status.js`.

- Shared helper nằm tại `functions/_lib/test-auth.js`.

- Wrangler manifest nằm tại `wrangler.toml`.

- Node package manifest nằm tại `package.json`.

- Lockfile là `package-lock.json`.

- Deploy credential nằm trong `.deploy.env`.

- Runtime credential local nằm trong `.env`.

- Secret files phải nằm ngoài Git.

- Frontend không được chứa secret.

- Backend route mới phải giữ nguyên secret boundary.

- Tài liệu này không thay thế cấu hình Cloudflare.

- Tài liệu này không thay thế Google Cloud Console.

- Tài liệu này không thay thế API contract production.

## B. Nguyên tắc bất biến của project

- Không đổi project name nếu không có kế hoạch migration.

- Không đổi callback URL tùy tiện.

- Không thêm slash vào callback đã đăng ký.

- Không xóa `APIUYTIN_BASE_URL`.

- Không xóa `APIUYTIN_CALLBACK_URL`.

- Không đưa `PK` vào secret của frontend.

- Không đưa `SK` vào asset public.

- Không đưa API key vào HTML.

- Không đưa API key vào JavaScript browser.

- Không đưa session secret vào query string.

- Không log giá trị của cookie.

- Không log giá trị của authorization code.

- Không log giá trị của access token.

- Không log giá trị của API key.

- Không commit `.env`.

- Không commit `.deploy.env`.

- Không commit `.dev.vars`.

- Không dùng credential của page chính cho page test.

- Không deploy từ nhầm thư mục.

- Không chạy deploy nếu chưa xác định project name.

- Không sửa backend khi chỉ thay đổi giao diện.

- Không sửa API contract vì lỗi hiển thị frontend.

- Không dùng Google callback trực tiếp cho project test.

- Không gửi code APIUYTIN tới Google token endpoint.

- Không coi `project-status` là endpoint chứa secret.

- Không trả secret trong response metadata.

- Không dùng response frontend làm nơi lưu credential.

## C. Bản đồ file chính xác

### C.1. `package.json`

- Khai báo package name `apiuytin-integration-test`.

- Package được đánh dấu `private`.

- Module system là ESM.

- Script deploy gọi Wrangler Pages.

- Thư mục deploy mặc định là `public`.

- Wrangler được khai báo ở devDependencies.

- Không có script build frontend riêng.

- Không có bundler frontend riêng.

- Không có test runner trong manifest.

- Không có framework UI bên thứ ba.

- Không có server Node chạy thường trực.

- Pages Functions do Cloudflare runtime thực thi.

- Lệnh deploy không tự nạp secret file.

- Credential phải được export vào environment.

- Project name lấy từ manifest Wrangler.

- `npm install` tạo dependency tree local.

- `package-lock.json` khóa dependency resolution.

- Không nên sửa lockfile bằng tay.

- Nâng Wrangler cần kiểm tra compatibility.

### C.2. `wrangler.toml`

- `name` là tên Cloudflare Pages project.

- `compatibility_date` xác định runtime compatibility.

- `pages_build_output_dir` trỏ tới `public`.

- `[vars]` chỉ chứa biến public.

- `APIUYTIN_BASE_URL` là base URL của nền tảng.

- `APIUYTIN_CALLBACK_URL` là callback của page test.

- Callback trong manifest dùng HTTPS.

- Callback dùng hostname Pages production.

- Callback không có trailing slash.

- Callback phải khớp record project.

- Callback phải khớp request token exchange.

- Không đặt `PK` trong `[vars]`.

- Không đặt `SK` trong `[vars]`.

- Không đặt API key trong `[vars]`.

- Không đặt session secret trong `[vars]`.

- Secret nên cấu hình bằng Pages secret.

- Thay đổi manifest cần redeploy để có hiệu lực.

- Thay đổi secret không nhất thiết đổi asset.

- Manifest không chứa logic OAuth.

- Manifest không chứa logic session.

### C.3. `public/index.html`

- Khai báo doctype HTML5.

- Ngôn ngữ document là tiếng Việt.

- Có viewport cho thiết bị di động.

- Có description cho page test.

- Có favicon dùng `logoweb.png`.

- Có apple touch icon dùng `logoweb.png`.

- Title phân biệt integration test.

- Stylesheet được tải từ cùng origin.

- Navigation chứa brand link.

- Brand link trỏ về `/`.

- Brand image có alt text.

- Environment tag nhận diện test page.

- Hero giới thiệu flow OAuth.

- Nút login có id `login`.

- Nút login là button type button.

- Message area có id `message`.

- Message area dùng role status.

- Proof panel hiển thị integration metadata.

- Account card mặc định hidden.

- Account card chứa avatar user.

- Avatar có fallback SVG.

- Display name có id riêng.

- Email có id riêng.

- Logout có id `logout`.

- API card hiển thị server-side intent.

- Check button có id `check-api`.

- API result có id `api-result`.

- API result có aria-live polite.

- Script được tải dạng module.

- HTML không chứa credential.

- HTML không chứa authorization code.

- HTML không chứa token.

### C.4. `public/app.js`

- Chọn message element khi module khởi chạy.

- Chọn account card khi module khởi chạy.

- Chọn login button khi module khởi chạy.

- Chọn logout button khi module khởi chạy.

- Chọn check API button khi module khởi chạy.

- Chọn result element khi module khởi chạy.

- `showMessage` cập nhật textContent.

- `showMessage` đặt class success/error.

- `showUser(null)` ẩn account card.

- `showUser(user)` hiển thị account card.

- Avatar ưu tiên `user.picture`.

- Avatar fallback về placeholder.

- Tên fallback về Google user.

- Email lấy từ profile response.

- Login button được disable khi bắt đầu.

- Login chuyển browser tới `/auth/google`.

- Logout dùng HTTP POST.

- Logout gọi route cùng origin.

- Logout xóa profile trên giao diện.

- Logout hiển thị trạng thái thành công.

- Check API button được disable lúc gọi.

- Check API hiển thị trạng thái đang gọi.

- Check API gọi `/api/project-status`.

- Check API parse JSON response.

- Check API hiển thị JSON pretty printed.

- Check API mở lại button sau response.

- Startup gọi `/api/session`.

- Startup chỉ hiển thị user khi có user.

- Frontend không đọc cookie HttpOnly.

- Frontend không ký cookie.

- Frontend không đổi token.

- Frontend không gọi Google token endpoint.

- Frontend không gọi API project bằng secret.

### C.5. `public/styles.css`

- Font chính là Fira Sans.

- Font code là Fira Code.

- Background dùng màu xanh đen.

- Text chính có độ tương phản cao.

- Muted text tách biệt với text chính.

- Accent chính là màu xanh lime.

- Error color được khai báo riêng.

- Grid nền không nhận pointer event.

- Page shell giới hạn chiều rộng.

- Navigation căn giữa theo trục dọc.

- Brand giữ layout ngang ổn định.

- Brand mark có kích thước cố định.

- Logo ảnh phủ đầy brand mark.

- Hero dùng hai cột trên desktop.

- Hero chuyển một cột trên mobile.

- Button có cursor pointer.

- Button có trạng thái hover.

- Button có transition ngắn.

- Panel có border phân tách.

- Panel header có separator.

- Dot biểu thị live integration.

- Metric dùng flex để căn giá trị.

- Account card giữ profile spacing.

- Profile avatar có border accent.

- API result giữ font monospace.

- Footer dùng text nhỏ.

- Media query breakpoint là 780px.

- CSS không chứa URL secret.

- CSS không gọi backend.

### C.6. `public/logoweb.png`

- Là asset logo dùng cho page test.

- Asset nằm trong thư mục public.

- Asset được phục vụ ở `/logoweb.png`.

- Asset dùng cho favicon.

- Asset dùng cho apple touch icon.

- Asset dùng trong brand mark.

- Asset phải được deploy cùng HTML.

- Asset không cần JavaScript để hiển thị.

- Asset không chứa credential.

- Asset không thay đổi OAuth behavior.

### C.7. `public/avatar-placeholder.svg`

- Là fallback khi Google không trả picture.

- SVG được phục vụ cùng origin.

- SVG không phụ thuộc API.

- SVG có viewBox 80x80.

- SVG dùng màu accent của test page.

- SVG hiển thị hình người tối giản.

- SVG chỉ dùng cho avatar.

- SVG không phải brand logo.

- SVG không nên dùng làm favicon.

- SVG không chứa dữ liệu người dùng.

## D. Cloudflare Pages routing

- Request `/` lấy file `public/index.html`.

- Request `/styles.css` lấy stylesheet public.

- Request `/app.js` lấy module frontend.

- Request `/logoweb.png` lấy image asset.

- Request `/avatar-placeholder.svg` lấy fallback asset.

- Request `/auth/google` vào Pages Function.

- Request `/auth/callback` vào Pages Function.

- Request `GET /api/session` vào Pages Function.

- Request `POST /api/logout` vào Pages Function.

- Request `GET /api/project-status` vào Pages Function.

- File path dưới `functions` map thành route.

- Folder `auth` map thành URL prefix.

- Folder `api` map thành URL prefix.

- File `google.js` map thành `/auth/google`.

- File `callback.js` map thành `/auth/callback`.

- File `session.js` map thành `/api/session`.

- File `logout.js` map thành `/api/logout`.

- File `project-status.js` map thành `/api/project-status`.

- `_lib` là code dùng chung.

- `_lib` không là route public.

- Route method được quyết định bởi export.

- `onRequestGet` xử lý GET.

- `onRequestPost` xử lý POST.

- Static asset không chạy qua helper.

- Function nhận `request` từ runtime.

- Function nhận `env` từ runtime.

- `env` chứa vars và secrets.

- `request.url` dùng để parse callback.

- Response redirect dùng status 302.

- JSON response dùng content type JSON.

- JSON response có no-store cache header.

## E. Chi tiết helper `test-auth.js`

### E.1. Encoder và Base64URL

- Helper tạo một `TextEncoder`.

- TextEncoder biến chuỗi thành bytes.

- `base64url` tạo representation an toàn URL.

- Dấu cộng được thay bằng dấu gạch ngang.

- Dấu slash được thay bằng dấu gạch dưới.

- Padding dấu bằng được loại bỏ.

- Representation phù hợp cookie value.

- Representation phù hợp query state.

- Decode khôi phục padding trước khi decode.

- Decode chuyển URL-safe ký tự về Base64.

- Decode dùng `atob` của runtime.

- Decode chuyển binary thành Uint8Array.

- Dữ liệu session được mã hóa Base64URL.

- Base64URL không phải mã hóa bí mật.

- HMAC mới là lớp xác thực dữ liệu.

- Không coi Base64URL là encryption.

### E.2. HMAC key derivation

- Helper hash secret bằng SHA-256.

- Digest tạo bytes làm raw key.

- Key dùng HMAC SHA-256.

- Key chỉ cho phép sign.

- Key không được export.

- Secret cần có độ ngẫu nhiên tốt.

- `SESSION_SECRET` là secret ưu tiên.

- `SK` là fallback hiện hữu.

- Fallback không nên là thiết kế mới.

- Nên dùng secret riêng cho session.

- Không dùng API key làm session secret dài hạn.

- Không hiển thị secret trong error.

### E.3. Sign và read

- `sign` nhận chuỗi cần ký.

- `sign` tạo encoded payload.

- `sign` tạo HMAC signature.

- Kết quả có dạng `payload.signature`.

- `read` từ chối value rỗng.

- `read` tách payload và signature.

- `read` từ chối cấu trúc thiếu phần.

- `read` decode payload.

- `read` tạo signature kỳ vọng.

- `read` so sánh signature hậu tố.

- Signature sai trả về null.

- Payload hỏng có thể phát sinh lỗi decode.

- Caller session xử lý payload JSON riêng.

- Signed state có thời hạn trong payload.

- Signed session có thời hạn trong payload.

- Cookie Max-Age hỗ trợ browser expiry.

- Payload expiry hỗ trợ server validation.

- Hai lớp expiry nên luôn đồng bộ.

### E.4. Cookie helper

- Cookie helper đặt `Path=/`.

- Cookie helper đặt `HttpOnly`.

- Cookie helper đặt `Secure`.

- Cookie helper đặt `SameSite=Lax`.

- Cookie helper đặt `Max-Age`.

- OAuth state cookie sống 600 giây.

- Session cookie sống 604800 giây.

- Logout cookie có Max-Age bằng zero.

- HttpOnly ngăn JavaScript đọc cookie.

- Secure yêu cầu HTTPS trong production.

- SameSite Lax hỗ trợ callback top-level.

- Cookie name state là `testapi_oauth_state`.

- Cookie name session là `testapi_session`.

- Không dùng chung cookie với page chính.

### E.5. Response helpers

- `json` stringify data.

- `json` đặt `content-type`.

- `json` đặt `cache-control: no-store`.

- `json` nhận status tùy chọn.

- `json` nhận headers bổ sung.

- `redirect` tạo response 302.

- `redirect` đặt location header.

- `redirect` nhận headers bổ sung.

- Logout dùng JSON response kèm Set-Cookie.

- Callback dùng redirect kèm Set-Cookie.

- Header tên cookie phải giữ đúng casing.

### E.6. Random state

- State dùng `crypto.getRandomValues`.

- State dài 24 bytes ngẫu nhiên.

- State được Base64URL encode.

- State liên kết request bắt đầu OAuth.

- State được ký trước khi ghi cookie.

- State được so sánh tại callback.

- State có thời hạn 10 phút.

- State ngăn callback giả mạo cơ bản.

- State không phải access token.

- State không được ghi log.

## F. Flow `/auth/google`

- Route được gọi bởi browser.

- Route đọc `env.SESSION_SECRET`.

- Route fallback sang `env.SK`.

- Route tạo state mới mỗi lần.

- Route nhúng state vào signed payload.

- Route đặt expiry 600000 milliseconds.

- Route ký state bằng HMAC.

- Route tạo URL nền tảng APIUYTIN.

- Route đặt `client_id` bằng `env.PK`.

- Route đặt `redirect_uri` bằng public var.

- Route đặt `state` bằng state plaintext.

- Route không gửi secret qua query.

- Route ghi signed state vào HttpOnly cookie.

- Route cookie sống 600 giây.

- Route trả redirect tới APIUYTIN.

- Browser theo redirect tới nền tảng.

- Nền tảng xác nhận PK.

- Nền tảng xác nhận callback.

- Nền tảng tiếp tục Google flow.

- Không nên cache response route này.

- Không nên prefetch route này.

- Không nên iframe route này.

## G. Flow `/auth/callback`

- Route nhận request từ nền tảng.

- Route parse `code` từ query.

- Route parse `state` từ query.

- Route đọc state cookie.

- Route dùng cùng secret để verify.

- Thiếu code trả HTTP 400.

- Thiếu signed state trả HTTP 400.

- Payload state được JSON parse.

- State query phải khớp state cookie.

- State expiry phải lớn hơn hiện tại.

- State sai phát sinh lỗi invalid state.

- Route gọi token endpoint nền tảng.

- Method token exchange là POST.

- Content type là application/json.

- Body chứa grant type.

- Body chứa authorization code.

- Body chứa client id.

- Body chứa client secret.

- Body chứa callback URI.

- Callback URI phải khớp tuyệt đối.

- Route parse token JSON.

- HTTP lỗi tạo lỗi token exchange.

- User profile lấy từ token response.

- Session chứa subject user.

- Session chứa email.

- Session chứa display name.

- Session chứa picture fallback rỗng.

- Session expiry là bảy ngày.

- Session được ký server-side.

- Response redirect về `/`.

- Response đặt session cookie.

- Response xóa state cookie.

- State cookie phải bị xóa sau success.

- Error response không trả secret.

- Error response không trả token.

## H. Flow `/api/session`

- Route nhận GET.

- Route đọc header Cookie.

- Route tìm `testapi_session`.

- Route verify HMAC session.

- Session invalid trả user null.

- Session thiếu trả user null.

- Session JSON được parse trong try block.

- Session hết hạn trả user null.

- Session hợp lệ trả profile.

- Response dùng no-store.

- Profile response không cần access token.

- Browser gọi route khi startup.

- Browser không đọc cookie trực tiếp.

- Session route không gọi Google.

- Session route không gọi APIUYTIN.

- Session route không tạo session mới.

- Session route không refresh token.

- Session route chỉ đọc trạng thái.

- Session route nên phản hồi nhanh.

## I. Flow `/api/logout`

- Route chỉ nhận POST.

- Route trả `{ ok: true }`.

- Route set session cookie rỗng.

- Max-Age bằng zero yêu cầu xóa.

- Cookie attributes phải đồng nhất.

- Browser gọi route khi click logout.

- Browser ẩn account card sau response.

- Browser hiển thị logout message.

- Logout không gọi nền tảng OAuth.

- Logout không revoke project.

- Logout không revoke Google account.

- Logout không xóa credential project.

- Logout chỉ kết thúc session test.

- Logout nên dùng CSRF policy phù hợp.

- SameSite Lax giảm cross-site POST.

## J. Flow `/api/project-status`

- Route nhận GET.

- Route gọi helper project API.

- Helper gọi APIUYTIN project-info.

- Helper dùng secret API key server-side.

- Route không nhận key từ browser.

- Route lấy project metadata.

- Route trả project id.

- Route trả project name.

- Route trả redirect URI.

- Route trả status.

- Route không trả publish key.

- Route không trả secret key.

- Route không trả API key.

- API error chuyển thành status 502.

- Error message giữ mục đích chẩn đoán.

- Error message không nên chứa secret.

- Frontend hiển thị JSON metadata.

- Route không tạo session.

- Route không thực hiện OAuth.

- Route không thay đổi project.

## K. Mô hình dữ liệu tạm thời

### K.1. OAuth state payload

- Payload là JSON string.

- Trường `state` chứa random state.

- Trường `exp` chứa millisecond timestamp.

- Payload được ký bằng HMAC.

- Payload nằm trong cookie HttpOnly.

- Payload không chứa user profile.

- Payload không chứa access token.

- Payload không chứa API key.

- Payload chỉ dùng một OAuth attempt.

- Payload bị xóa sau callback thành công.

### K.2. Session payload

- Payload là JSON string.

- Trường `sub` là profile id.

- Trường `email` là email user.

- Trường `name` là display name.

- Trường `picture` là avatar URL.

- Trường `exp` là expiry timestamp.

- Payload được HMAC sign.

- Payload nằm trong cookie HttpOnly.

- Payload không chứa access token.

- Payload không chứa secret key.

- Payload không chứa API key.

- Payload được đọc bởi session route.

- Payload hết hạn sau bảy ngày.

### K.3. Project status payload

- Response có trường `ok`.

- Success response có trường `project`.

- Project có id.

- Project có name.

- Project có redirectUri.

- Project có status.

- Failure response có `ok: false`.

- Failure response có error message.

- Failure HTTP status là 502.

- Failure response không expose credential.

## L. Bảo mật theo lớp

- Lớp browser chỉ biết public UI.

- Lớp browser biết publish key gián tiếp.

- Lớp browser không biết secret key.

- Lớp browser không biết API key.

- Lớp Pages Function nhận secret.

- Lớp Pages Function ký cookie.

- Lớp Pages Function gọi token exchange.

- Lớp Pages Function lọc metadata response.

- Lớp APIUYTIN xác nhận project.

- Lớp APIUYTIN xác nhận redirect URI.

- Lớp APIUYTIN xác nhận secret hash.

- Lớp Google xác nhận user identity.

- HTTPS bảo vệ truyền tải production.

- HttpOnly giảm nguy cơ XSS đọc cookie.

- Secure ngăn cookie qua HTTP.

- SameSite giảm CSRF cross-site.

- State giảm OAuth CSRF.

- Expiry giảm thời gian replay.

- One-time code giảm replay token.

- No-store giảm cache dữ liệu session.

## M. Checklist trước mỗi thay đổi giao diện

- Xác định file UI cần sửa.

- Xác định file asset cần sửa.

- Không sửa Functions nếu không cần.

- Không sửa helper auth vì CSS.

- Không sửa callback vì logo.

- Kiểm tra đường dẫn asset bắt đầu bằng `/`.

- Kiểm tra tên file phân biệt đúng.

- Kiểm tra asset nằm trong public.

- Kiểm tra alt text có ý nghĩa.

- Kiểm tra favicon trả HTTP 200.

- Kiểm tra brand image hiển thị.

- Kiểm tra mobile layout.

- Kiểm tra desktop layout.

- Kiểm tra không có horizontal overflow.

- Kiểm tra nút login vẫn có id.

- Kiểm tra nút logout vẫn có id.

- Kiểm tra nút API vẫn có id.

- Kiểm tra script module vẫn tải.

- Kiểm tra CSS không che button.

- Kiểm tra không thay đổi API route.

## N. Checklist trước mỗi thay đổi backend

- Ghi rõ route bị tác động.

- Xác định HTTP method.

- Xác định input query.

- Xác định input cookie.

- Xác định secret cần dùng.

- Xác định response contract.

- Không log credential.

- Không log user token.

- Không đổi callback ngoài kế hoạch.

- Không đổi cookie name tùy tiện.

- Không đổi cookie attributes tùy tiện.

- Không đổi expiry mà không kiểm tra.

- Không nuốt lỗi bất ngờ.

- Không trả stack trace production.

- Không trả raw token frontend.

- Kiểm tra response status.

- Kiểm tra header cache.

- Kiểm tra CORS nếu thêm origin.

- Kiểm tra replay behavior.

- Kiểm tra invalid input behavior.

## O. Checklist credential Cloudflare

- File credential đúng thư mục test.

- Account ID thuộc account test.

- API token có quyền Pages deploy.

- API token không ghi vào command history.

- Environment variable chỉ tồn tại trong process deploy.

- Không paste token vào Markdown.

- Không paste token vào issue.

- Không paste token vào commit.

- Không paste token vào screenshot.

- Không paste token vào browser URL.

- Project name đúng manifest.

- Account context đúng account.

- Deploy từ thư mục đúng.

- Static output là `public`.

- Functions được upload cùng deploy.

- Deploy output không chứa secret.

- Deployment URL được ghi lại an toàn.

- Preview URL phân biệt production URL.

- Sau deploy kiểm tra HTTP status.

- Sau deploy kiểm tra asset status.

## P. Quy trình deploy chuẩn

- Mở PowerShell mới.

- Chuyển tới thư mục tích hợp.

- Xác nhận `package.json` tồn tại.

- Xác nhận `wrangler.toml` tồn tại.

- Xác nhận `public` tồn tại.

- Xác nhận `functions` tồn tại.

- Xác nhận `.deploy.env` tồn tại.

- Đọc tên biến, không in giá trị.

- Export `CLOUDFLARE_ACCOUNT_ID`.

- Export `CLOUDFLARE_API_TOKEN`.

- Không export runtime API secrets nhầm.

- Chạy Wrangler từ local dependency.

- Dùng đúng `--project-name`.

- Dùng đúng output directory.

- Dùng `--commit-dirty=true` chỉ khi hiểu tác dụng.

- Quan sát upload count.

- Quan sát Functions bundle.

- Quan sát deployment complete.

- Ghi URL deploy.

- Kiểm tra URL production.

- Dừng nếu upload sai project.

## Q. Quy trình kiểm tra sau deploy

- Gọi homepage bằng HTTPS.

- Xác nhận status 200.

- Xác nhận content type HTML.

- Tìm title integration test.

- Tìm brand image markup.

- Gọi `/styles.css`.

- Xác nhận content type CSS.

- Gọi `/app.js`.

- Xác nhận content type JavaScript.

- Gọi `/logoweb.png`.

- Xác nhận content type image/png.

- Gọi `/avatar-placeholder.svg`.

- Xác nhận content type image/svg+xml.

- Gọi `/api/session` không cookie.

- Kỳ vọng user null.

- Gọi `/api/logout` bằng POST.

- Kỳ vọng JSON ok.

- Gọi `/api/project-status` không secret.

- Ghi nhận status theo cấu hình.

- Không gửi credential trong probe.

## R. Ma trận endpoint

| Endpoint | Method | Public | Secret use | Expected role |
|---|---|---:|---:|---|
| `/` | GET | Có | Không | Giao diện |
| `/styles.css` | GET | Có | Không | CSS |
| `/app.js` | GET | Có | Không | Frontend logic |
| `/logoweb.png` | GET | Có | Không | Brand asset |
| `/auth/google` | GET | Có | Có | OAuth start |
| `/auth/callback` | GET | Có | Có | OAuth exchange |
| `/api/session` | GET | Có | Có | Session read |
| `/api/logout` | POST | Có | Không | Session clear |
| `/api/project-status` | GET | Có | Có | Project metadata |

## S. Ma trận trạng thái HTTP

- Homepage thành công trả 200.

- Static CSS thành công trả 200.

- Static JS thành công trả 200.

- Static logo thành công trả 200.

- OAuth start thành công trả 302.

- OAuth callback thiếu code trả 400.

- OAuth callback thiếu state cookie trả 400.

- OAuth callback sai state trả 400.

- Token exchange thất bại trả 400.

- Session thiếu cookie trả JSON 200.

- Session cookie invalid trả JSON 200.

- Logout POST thành công trả JSON 200.

- Project status thành công trả JSON 200.

- Project status upstream fail trả 502.

- Method không đúng phụ thuộc Pages routing.

## T. Kiểm thử frontend thủ công

- Mở homepage ở cửa sổ riêng.

- Quan sát logo trong navigation.

- Quan sát favicon trên tab.

- Kiểm tra text API uy tín.

- Kiểm tra environment tag.

- Bấm login một lần.

- Kiểm tra button bị disable.

- Kiểm tra message đang redirect.

- Kiểm tra browser tới OAuth provider.

- Quay lại page sau callback.

- Kiểm tra account card hiển thị.

- Kiểm tra avatar hoặc fallback.

- Kiểm tra display name.

- Kiểm tra email.

- Bấm logout.

- Kiểm tra account card ẩn.

- Kiểm tra message logout.

- Bấm check API.

- Kiểm tra loading text.

- Kiểm tra kết quả JSON.

- Kiểm tra button hoạt động lại.

## U. Kiểm thử session

- Bắt đầu khi chưa có cookie.

- `/api/session` trả user null.

- Không hiển thị account card.

- Tạo OAuth state mới.

- Xác nhận state cookie HttpOnly.

- Xác nhận state cookie Secure.

- Xác nhận state cookie SameSite Lax.

- Hoàn tất callback hợp lệ.

- Xác nhận session cookie xuất hiện.

- Gọi `/api/session` có cookie.

- Xác nhận user profile.

- Thử cookie bị sửa payload.

- Kỳ vọng user null.

- Thử cookie signature bị sửa.

- Kỳ vọng user null.

- Thử session expiry quá hạn.

- Kỳ vọng user null.

- Logout bằng POST.

- Xác nhận cookie bị xóa.

- Gọi session sau logout.

- Kỳ vọng user null.

## V. Kiểm thử OAuth state

- State phải khác nhau giữa hai lần start.

- State phải đủ ngẫu nhiên.

- State cookie phải được ký.

- State cookie không chứa secret.

- Callback không có `code` phải fail.

- Callback không có `state` phải fail.

- Callback sai state phải fail.

- Callback state hết hạn phải fail.

- Callback đúng state tiếp tục exchange.

- Callback thành công xóa state cookie.

- Callback lỗi không log code.

- Callback lỗi không log secret.

- Callback lỗi không trả token.

- State không nên dùng lại sau success.

- State cookie không nên sống quá lâu.

## W. Kiểm thử project status

- Gọi route bằng GET.

- Không gửi API key từ browser.

- Function lấy secret từ env.

- Function gọi đúng base URL.

- Function dùng Authorization header.

- Upstream 200 được map thành 200.

- Upstream project metadata được lọc.

- Publish key không được trả.

- Secret key không được trả.

- API key không được trả.

- Upstream 401 được map thành 502.

- Upstream timeout cần được quan sát.

- Error text không chứa header.

- Frontend parse JSON.

- Frontend hiển thị status.

## X. Kiểm tra responsive

- Viewport 375px không overflow ngang.

- Viewport 414px giữ hero gọn.

- Viewport 768px dùng mobile breakpoint.

- Viewport 1024px hiển thị hai cột.

- Viewport 1440px giữ max width.

- Navigation không làm vỡ layout.

- Environment tag ẩn ở mobile.

- Hero chuyển một cột.

- Account top chuyển block.

- API card chuyển block.

- Button không bị tràn khung.

- Preformatted JSON wrap đúng.

- Logo không méo tỷ lệ.

- Avatar không méo tỷ lệ.

- Footer không gây scroll ngang.

## Y. Quan sát và chẩn đoán

- Ghi deployment timestamp.

- Ghi deployment URL.

- Ghi commit hoặc workspace revision.

- Ghi Wrangler version.

- Ghi compatibility date.

- Ghi project name.

- Không ghi token.

- Không ghi cookie.

- Không ghi state.

- Không ghi authorization code.

- Theo dõi status homepage.

- Theo dõi status OAuth start.

- Theo dõi status callback.

- Theo dõi status session.

- Theo dõi status project status.

- Theo dõi latency upstream.

- Theo dõi lỗi 400.

- Theo dõi lỗi 502.

- Theo dõi lỗi static asset.

- Theo dõi lỗi Function bundle.

## Z. Runbook lỗi deploy

- Xác nhận đang ở đúng thư mục.

- Xác nhận project name đúng.

- Xác nhận account ID đúng.

- Xác nhận API token chưa hết hạn.

- Xác nhận API token có Pages scope.

- Xác nhận `public` có index.

- Xác nhận `functions` không syntax error.

- Xác nhận dependency đã cài.

- Chạy Wrangler local version.

- Không dùng credential page chính.

- Đọc thông báo upload.

- Đọc thông báo Functions bundle.

- Nếu project không tồn tại, dừng.

- Nếu account sai, dừng.

- Nếu unauthorized, kiểm tra scope.

- Nếu asset thiếu, kiểm tra public.

- Nếu function thiếu, kiểm tra functions.

- Nếu route 404, kiểm tra tên file.

- Nếu deploy hoàn tất, probe endpoint.

## AA. Runbook lỗi OAuth

- Kiểm tra hostname callback.

- Kiểm tra scheme HTTPS.

- Kiểm tra trailing slash.

- Kiểm tra project publish key.

- Kiểm tra project status active.

- Kiểm tra APIUYTIN base URL.

- Kiểm tra callback trong manifest.

- Kiểm tra callback trong project record.

- Kiểm tra callback Google nền tảng.

- Kiểm tra state cookie.

- Kiểm tra browser cho phép cookie.

- Kiểm tra clock hệ thống.

- Kiểm tra code chưa dùng.

- Kiểm tra code chưa hết hạn.

- Kiểm tra secret Pages.

- Kiểm tra session secret.

- Không thử đổi code lần hai.

- Không gửi code sang Google.

- Không sửa nhiều lớp cùng lúc.

## AB. Runbook lỗi session

- Gọi `/api/session` trực tiếp.

- Kiểm tra response user null.

- Kiểm tra cookie có được gửi.

- Kiểm tra cookie path.

- Kiểm tra HTTPS.

- Kiểm tra SameSite.

- Kiểm tra secret ổn định.

- Kiểm tra payload expiry.

- Kiểm tra signature.

- Kiểm tra profile response.

- Kiểm tra callback redirect.

- Kiểm tra Set-Cookie đầu tiên.

- Kiểm tra state cookie thứ hai.

- Không đọc HttpOnly bằng console.

- Không copy cookie vào ticket.

## AC. Runbook lỗi project API

- Kiểm tra route có gọi server.

- Kiểm tra secret name trên Pages.

- Kiểm tra API key thuộc project.

- Kiểm tra project chưa revoke.

- Kiểm tra base URL không sai.

- Kiểm tra endpoint `/project-info`.

- Kiểm tra Authorization scheme.

- Kiểm tra account deployment.

- Kiểm tra function version.

- Kiểm tra upstream status.

- Không đưa API key vào frontend.

- Không test API key bằng URL.

- Không ghi API key vào log.

## AD. Quy tắc thay đổi dependency

- Chỉ nâng dependency khi có lý do.

- Đọc changelog Wrangler trước khi nâng.

- Kiểm tra compatibility date.

- Cập nhật lockfile bằng npm.

- Không xóa package-lock tùy tiện.

- Chạy deploy preview sau nâng.

- Kiểm tra Pages Functions bundle.

- Kiểm tra static asset upload.

- Kiểm tra OAuth start.

- Kiểm tra session route.

- Kiểm tra project status.

- Ghi lại version mới.

- Không nâng package trong thay đổi logo.

- Không thêm framework UI cho patch nhỏ.

- Không thêm runtime dependency không cần.

## AE. Quy tắc bảo trì frontend

- Giữ id contract của button.

- Giữ route string ổn định.

- Giữ fallback avatar.

- Giữ aria-live trên kết quả.

- Giữ alt text logo.

- Giữ type button.

- Không inline secret.

- Không inline token.

- Không lưu credential localStorage.

- Không lưu session token sessionStorage.

- Không thêm analytics chứa email.

- Không gửi profile không cần thiết.

- Không thay đổi OAuth flow vì styling.

- Kiểm tra CSS mobile sau sửa.

- Kiểm tra favicon sau sửa asset.

## AF. Quy tắc bảo trì Functions

- Giữ export `onRequestGet`.

- Giữ export `onRequestPost`.

- Giữ route file name.

- Giữ helper import path.

- Giữ JSON content type.

- Giữ no-store cho session.

- Giữ cookie security attributes.

- Giữ expiry validation.

- Giữ state validation.

- Giữ redirect URI validation upstream.

- Giữ secret server-side.

- Giữ error status có ý nghĩa.

- Không catch rồi trả success giả.

- Không trả raw upstream secret.

- Không log request header Authorization.

## AG. Mẫu kiểm tra source nhanh

- `Get-ChildItem public` liệt kê asset.

- `Get-ChildItem functions` liệt kê routes.

- `Get-Content package.json` kiểm tra scripts.

- `Get-Content wrangler.toml` kiểm tra vars.

- `Select-String logoweb` kiểm tra logo.

- `Select-String APIUYTIN` kiểm tra endpoint.

- `Select-String client_secret` kiểm tra backend.

- `Select-String CHUOTAPI` kiểm tra boundary.

- `Select-String SESSION_SECRET` kiểm tra secret usage.

- `Get-FileHash public/logoweb.png` kiểm tra asset.

- Không dùng `Get-Content .env` để in credential.

- Chỉ in danh sách key env.

- Không gửi output credential vào chat.

- Không commit output tạm.

## AH. Quy trình review thay đổi

- Xem danh sách file thay đổi.

- Xác nhận chỉ file trong scope.

- Đọc diff đầy đủ.

- Tìm thay đổi route.

- Tìm thay đổi URL.

- Tìm thay đổi secret name.

- Tìm thay đổi cookie name.

- Tìm thay đổi expiry.

- Tìm thay đổi HTML ids.

- Tìm thay đổi script source.

- Tìm thay đổi asset path.

- Kiểm tra no accidental credential.

- Kiểm tra no generated temp files.

- Kiểm tra no node_modules changes.

- Chỉ deploy sau review.

## AI. Checklist thay đổi tài liệu

- Giữ nguyên các phần cũ.

- Chỉ nối nội dung ở cuối.

- Không chèn credential thật.

- Không chèn access token.

- Không chèn session cookie.

- Không chèn authorization code.

- Dùng placeholder cho secret.

- Dùng URL production đã công khai.

- Ghi rõ giả định khi cần.

- Ghi rõ route thực tế.

- Ghi rõ file thực tế.

- Ghi rõ giới hạn tài liệu.

- Không biến ví dụ thành lệnh nguy hiểm.

- Không hướng dẫn log secret.

- Không hướng dẫn commit env.

## AJ. Kiểm tra tính toàn vẹn file

- Hash source trước thay đổi tài liệu.

- Hash Functions sau thay đổi tài liệu.

- Hash public JS sau thay đổi tài liệu.

- Hash public HTML sau thay đổi tài liệu.

- Hash CSS sau thay đổi tài liệu.

- Hash logo sau thay đổi tài liệu.

- Hash manifest sau thay đổi tài liệu.

- Chỉ hash construction được thay đổi.

- Không cần rebuild code cho Markdown.

- Không cần redeploy cho nội dung local.

- Nếu tài liệu được deploy, kiểm tra encoding.

- Giữ UTF-8 cho tiếng Việt.

- Tránh ký tự điều khiển.

- Giữ code fence đóng mở cân bằng.

- Giữ bảng Markdown có header.

## AK. Bảng phụ thuộc runtime

| Thành phần | Phụ thuộc | Vai trò |
|---|---|---|
| Browser | HTTPS | Tải giao diện |
| Browser | Static assets | Render UI |
| Browser | `/auth/google` | Bắt đầu OAuth |
| Pages Function | `PK` | Nhận diện project |
| Pages Function | `SK` | Đổi authorization code |
| Pages Function | `SESSION_SECRET` | Ký cookie |
| Pages Function | API base URL | Gọi nền tảng |
| APIUYTIN | Google OAuth | Xác thực user |
| APIUYTIN | Project registry | Kiểm tra callback |
| Cloudflare Pages | Wrangler | Triển khai |

## AL. Bảng phụ thuộc theo thời điểm

- Build time cần package manifest.

- Build time cần lockfile phù hợp.

- Deploy time cần account id.

- Deploy time cần API token.

- Deploy time cần project name.

- Runtime homepage cần static files.

- Runtime OAuth cần PK.

- Runtime OAuth cần SK.

- Runtime OAuth cần callback URL.

- Runtime session cần session secret.

- Runtime project status cần API key.

- Runtime callback cần upstream availability.

- Runtime profile cần token exchange success.

- Runtime avatar cần profile picture optional.

## AM. Các điểm không nên nhầm

- `PK` không phải API key.

- `SK` không phải session secret.

- `API_KEY` không phải publish key.

- `state` không phải access token.

- `code` không phải user profile.

- `user` không phải session cookie.

- `redirect_uri` không phải base URL.

- `APIUYTIN_BASE_URL` không phải callback URL.

- `/auth/google` không phải Google endpoint.

- `/auth/callback` không phải nền tảng callback.

- `/api/session` không phải token endpoint.

- `/api/project-status` không phải project mutation.

- `avatar-placeholder.svg` không phải brand logo.

- `logoweb.png` không phải secret asset.

## AN. Đảm bảo route callback tuyệt đối

- Host phải là Pages test.

- Scheme phải là HTTPS.

- Path phải là `/auth/callback`.

- Không có trailing slash.

- Không có port local.

- Không có query mặc định.

- Không có fragment.

- Giá trị manifest phải giống record.

- Giá trị token exchange phải giống manifest.

- Google callback vẫn là callback nền tảng.

- Browser callback không gọi Google token trực tiếp.

- Callback code chỉ dùng một lần.

## AO. Mô tả giao tiếp giữa các hệ thống

- Browser gửi GET tới test page.

- Browser nhận HTML từ Cloudflare.

- Browser tải CSS cùng origin.

- Browser tải JS cùng origin.

- Browser gọi function cùng origin.

- Function tạo redirect URL.

- Function gửi public project identity.

- APIUYTIN xác nhận project.

- APIUYTIN chuyển tiếp OAuth tới Google.

- Google trả authorization result.

- APIUYTIN xử lý Google result.

- APIUYTIN trả code về test callback.

- Function xác minh state.

- Function đổi code lấy token response.

- Function tạo session riêng.

- Browser đọc profile qua session route.

- Browser yêu cầu project status qua function.

- Function lọc project metadata.

- Browser render kết quả đã lọc.

## AP. Dòng đời một phiên login

- User mở trang.

- Startup gọi session route.

- Session route đọc cookie.

- Không có cookie thì user null.

- User bấm login.

- Frontend khóa button.

- Frontend hiển thị trạng thái.

- Browser gọi auth start.

- Function tạo state.

- Function ký state.

- Function ghi state cookie.

- Function redirect APIUYTIN.

- APIUYTIN kiểm tra project.

- APIUYTIN redirect Google.

- User chọn tài khoản.

- Google trả kết quả nền tảng.

- Nền tảng tạo one-time code.

- Nền tảng redirect callback test.

- Function đọc code và state.

- Function verify state.

- Function gửi token exchange.

- Nền tảng kiểm tra code.

- Nền tảng trả user.

- Function ký session.

- Function xóa state cookie.

- Function redirect homepage.

- Startup đọc session mới.

- UI hiển thị profile.

## AQ. Dòng đời logout

- User nhìn thấy account card.

- User bấm logout.

- Browser gửi POST.

- Function tạo expired cookie.

- Browser nhận JSON success.

- Browser ẩn account card.

- Browser hiển thị thông báo.

- Google session không bị revoke.

- APIUYTIN project không bị revoke.

- Credential project không bị xóa.

- Reload page gọi session route.

- Session route thấy cookie rỗng.

- UI trở về trạng thái chưa login.

## AR. Dòng đời project status

- User bấm check API.

- Browser disable button.

- Browser hiển thị loading text.

- Browser gửi GET nội bộ.

- Function nhận env secrets.

- Function gọi project-info upstream.

- Upstream xác thực Authorization.

- Upstream trả project document.

- Function chọn metadata an toàn.

- Function trả JSON.

- Browser parse response.

- Browser render pretty JSON.

- Browser enable button.

- Nếu lỗi, route trả 502.

- Nếu frontend không xử lý lỗi riêng,

- response lỗi vẫn được render JSON.

## AS. Kiểm soát dữ liệu nhạy cảm

- Credential local chỉ nằm trong env.

- Deploy token chỉ nằm trong process.

- Secret Pages chỉ nằm trong env runtime.

- Secret không nằm trong static bundle.

- Secret không nằm trong HTML.

- Secret không nằm trong CSS.

- Secret không nằm trong client JS.

- Secret không nằm trong URL.

- Secret không nằm trong response metadata.

- Secret không nằm trong tài liệu.

- Authorization code chỉ tồn tại ngắn hạn.

- Access token không cần lưu frontend.

- Session payload chỉ chứa profile tối thiểu.

- Error message được kiểm soát.

- Upstream response được lọc.

## AT. Xử lý lỗi frontend nên theo dõi

- Fetch session có thể network fail.

- Fetch logout có thể network fail.

- Fetch project status có thể network fail.

- JSON parse có thể fail.

- Element selector có thể null.

- Button có thể bị double click.

- Redirect có thể bị popup blocker.

- Avatar URL có thể lỗi.

- Avatar URL có thể trả sai content type.

- CSS có thể chưa tải.

- JS có thể bị cache cũ.

- Asset logo có thể 404.

- Function có thể trả 502.

- OAuth callback có thể trả 400.

- UI cần được kiểm tra bằng browser.

## AU. Nguyên tắc cải thiện lỗi không đổi contract

- Giữ nguyên route path.

- Giữ nguyên HTTP method.

- Giữ nguyên field response.

- Giữ nguyên cookie names.

- Giữ nguyên callback URI.

- Thêm error handling cục bộ.

- Hiển thị trạng thái rõ ràng.

- Không nuốt exception backend.

- Không expose stack trace.

- Không expose secret.

- Không đổi flow thành client secret.

- Không đổi session thành localStorage token.

- Không đổi API call thành query secret.

## AV. Lưu ý về cache

- Session response phải no-store.

- Project status nên tránh cache nhạy cảm.

- OAuth start không nên cache.

- OAuth callback không nên cache.

- Static CSS có thể cache.

- Static JS có thể cache theo deployment.

- Logo có thể cache theo immutable asset.

- Khi logo đổi cần cache bust nếu cần.

- Favicon có thể bị browser cache lâu.

- Hard reload chỉ dùng để kiểm tra.

- Không cache authorization code.

- Không cache token exchange response.

## AW. Lưu ý về CORS và origin

- Frontend gọi cùng origin.

- Các API nội bộ không cần CORS rộng.

- Không mở wildcard nếu không cần.

- APIUYTIN base URL là origin khác.

- Function gọi server-to-server.

- Browser không gọi project API trực tiếp.

- Browser không cần đọc upstream headers.

- Callback là browser navigation.

- Cookie thuộc test page origin.

- Không chia cookie cross-site tùy tiện.

- Nếu thêm origin phải ghi rõ mục đích.

## AX. Lưu ý về Google profile

- `profile.id` được dùng làm subject.

- `profile.email` dùng hiển thị.

- `profile.name` có thể rỗng.

- `profile.picture` có thể rỗng.

- Tên fallback về email.

- Picture fallback về SVG.

- Không lưu toàn bộ Google profile.

- Không gửi profile nhạy cảm frontend.

- Không coi email là secret.

- Vẫn bảo vệ dữ liệu cá nhân.

- Không log email nếu không cần.

## AY. Mẫu test contract JSON

### Session chưa xác thực

```json
{
  "user": null
}
```

### Session đã xác thực

```json
{
  "user": {
    "sub": "user-id",
    "email": "user@example.invalid",
    "name": "Example User",
    "picture": "https://example.invalid/avatar.png",
    "exp": 1999999999999
  }
}
```

### Project status thành công

```json
{
  "ok": true,
  "project": {
    "id": "project-id",
    "name": "test-project",
    "redirectUri": "https://example.invalid/auth/callback",
    "status": "active"
  }
}
```

### Project status lỗi

```json
{
  "ok": false,
  "error": "upstream project request failed"
}
```

## AZ. Quy tắc dùng placeholder trong tài liệu

- Dùng `pk_YOUR_PROJECT_PUBLISH_KEY`.

- Dùng `sk_YOUR_PROJECT_SECRET_KEY`.

- Dùng `CHUOTAPI_REDACTED`.

- Dùng `ONE_TIME_CODE`.

- Dùng `SESSION_SECRET_VALUE`.

- Dùng `example.invalid`.

- Không dùng credential từng xuất hiện.

- Không dùng token lấy từ env.

- Không dùng cookie từ browser.

- Không dùng user email thật.

- Không dùng avatar URL thật.

- Không dùng deployment token thật.

## BA. Các lệnh an toàn để đọc project

- `Get-ChildItem public`.

- `Get-ChildItem functions -Recurse`.

- `Get-Content package.json`.

- `Get-Content wrangler.toml`.

- `Select-String -Path public\* -Pattern logo`.

- `Select-String -Path functions\* -Pattern APIUYTIN`.

- `Get-FileHash public\logoweb.png`.

- `npm run deploy -- --help`.

- `npx wrangler pages project list`.

- `Invoke-WebRequest homepage`.

- Không dùng lệnh in `.env`.

- Không dùng lệnh ghi đè source.

- Không dùng lệnh reset workspace.

## BB. Kiểm tra encoding Markdown

- File nên được lưu UTF-8.

- Tiếng Việt cần giữ dấu.

- Code fence dùng backticks cân bằng.

- Bảng không chứa credential.

- URL ví dụ dùng HTTPS.

- Heading phản ánh đúng nội dung.

- Không tạo liên kết tới secret file.

- Không chèn binary vào Markdown.

- Không chèn log deploy có token.

- Không chèn output environment.

- Giữ newline cuối file.

## BC. Kế hoạch khôi phục khi deploy lỗi

- Giữ lại deployment đang hoạt động.

- Không xóa project ngay lập tức.

- Xác định lỗi static hay Function.

- Kiểm tra preview deployment.

- So sánh manifest.

- So sánh route file.

- So sánh public asset.

- Kiểm tra secret binding.

- Kiểm tra callback URL.

- Sửa một nguyên nhân mỗi lần.

- Redeploy preview trước production.

- Probe homepage sau mỗi deploy.

- Probe session không cookie.

- Probe project status phù hợp.

- Ghi lại kết quả.

## BD. Quy tắc phân biệt preview và production

- Preview URL có hash deployment.

- Production URL là custom Pages hostname.

- Preview dùng để kiểm tra asset.

- Production dùng callback đã đăng ký.

- Không đăng ký preview làm callback lâu dài.

- Callback manifest hiện trỏ production.

- OAuth test nên dùng production callback.

- Preview OAuth có thể fail do URI mismatch.

- Static UI có thể kiểm tra preview.

- Backend behavior cần đúng environment.

- Ghi rõ URL khi báo cáo.

## BE. Kiểm tra sau thay đổi logo

- Logo file tồn tại trong public.

- HTML tham chiếu `/logoweb.png`.

- Favicon tham chiếu `/logoweb.png`.

- Apple icon tham chiếu `/logoweb.png`.

- CSS có kích thước logo.

- Alt text là APIUYTIN logo.

- Brand link vẫn hoạt động.

- App JS không bị sửa.

- Functions không bị sửa.

- Wrangler không bị sửa.

- Logo response có content type PNG.

- Logo response có HTTP 200.

- Browser snapshot thấy image.

- Page title vẫn đúng.

- Button ids vẫn đúng.

## BF. Bảo trì tài liệu theo release

- Ghi ngày cập nhật tài liệu.

- Ghi phạm vi thay đổi.

- Ghi file code đã đọc.

- Ghi route bị ảnh hưởng.

- Ghi deploy URL nếu deploy.

- Không ghi credential.

- Ghi breaking change nếu có.

- Ghi migration callback nếu có.

- Ghi dependency change nếu có.

- Ghi kiểm thử đã chạy.

- Ghi kiểm thử chưa chạy.

- Ghi giới hạn môi trường local.

- Ghi giả định về upstream.

- Giữ lịch sử nội dung cũ.

- Không xóa hướng dẫn còn đúng.

## BG. Danh mục kiểm tra định kỳ

- Kiểm tra certificate HTTPS.

- Kiểm tra hostname production.

- Kiểm tra callback còn active.

- Kiểm tra project status active.

- Kiểm tra Pages deployment mới nhất.

- Kiểm tra Functions bundle.

- Kiểm tra static asset.

- Kiểm tra login flow.

- Kiểm tra logout flow.

- Kiểm tra session expiry.

- Kiểm tra project status.

- Kiểm tra Google scopes.

- Kiểm tra secret rotation plan.

- Kiểm tra token permissions.

- Kiểm tra dependency updates.

## BH. Secret rotation

- Rotation phải có kế hoạch.

- Chuẩn bị secret mới trước.

- Cập nhật Pages secret.

- Không đặt secret mới trong vars.

- Kiểm tra function binding.

- Kiểm tra session invalidation policy.

- Deploy hoặc activate binding.

- Kiểm tra login mới.

- Kiểm tra project status mới.

- Thu hồi secret cũ khi an toàn.

- Không ghi secret vào tài liệu.

- Không gửi secret qua chat.

- Không copy secret vào ticket.

- Ghi chỉ thời điểm rotation.

- Ghi owner chịu trách nhiệm.

## BI. Nguyên tắc quyền hạn

- Deploy token chỉ có quyền cần thiết.

- Account ID không phải secret chính.

- Access key không dùng cho Pages nếu không cần.

- Secret API không đặt trong browser.

- Production credentials tách khỏi test.

- Local credentials tách khỏi deploy.

- Google client secrets tách khỏi API keys.

- Session secret tách khỏi project secret.

- Người review không cần xem giá trị.

- Chỉ cần biết tên binding.

## BJ. Các anti-pattern cần tránh

- Đặt `SK` trong query string.

- Đặt API key trong HTML.

- Đặt token trong localStorage.

- Dùng `innerHTML` với profile chưa escape.

- Log toàn bộ request headers.

- Log toàn bộ env object.

- Gọi Google token bằng APIUYTIN code.

- Bỏ qua state validation.

- Bỏ qua redirect validation.

- Dùng callback có slash khác.

- Reuse state vô hạn.

- Dùng secret mặc định công khai.

- Trả upstream raw response.

- Cache session response.

- Deploy từ nhầm project.

## BK. Checklist trước khi bàn giao

- Source đã được đọc toàn bộ.

- Tài liệu không chứa secret.

- Endpoint map đã cập nhật.

- Callback URL đã ghi rõ.

- Secret boundary đã ghi rõ.

- Deploy instructions có project name.

- Local instructions có thư mục.

- Error cases có hướng xử lý.

- Security rules có state.

- Security rules có cookie.

- Frontend behavior có mô tả.

- Backend behavior có mô tả.

- Asset behavior có mô tả.

- Responsive behavior có mô tả.

- Kiểm tra diff chỉ gồm Markdown.

## BL. Ghi chú cuối phụ lục

- Đây là web test độc lập.

- Đây không phải API backend chính.

- Đây không phải Google OAuth server.

- Đây không lưu credential trong frontend.

- Đây dùng Cloudflare Pages Functions.

- Đây dùng APIUYTIN làm OAuth broker.

- Đây dùng cookie HttpOnly cho session.

- Đây dùng HMAC để xác thực cookie.

- Đây dùng state cho OAuth binding.

- Đây dùng callback production cố định.

- Đây dùng API key ở server-side.

- Đây có fallback avatar cục bộ.

- Đây có logo brand cục bộ.

- Đây có route metadata project.

- Đây cần giữ secret hygiene.

## BM. Danh sách kiểm kê theo đường dẫn

- `tích hợp thử nghiệm/package.json` là manifest.

- `tích hợp thử nghiệm/package-lock.json` là lockfile.

- `tích hợp thử nghiệm/wrangler.toml` là deploy config.

- `tích hợp thử nghiệm/.gitignore` là ignore policy.

- `tích hợp thử nghiệm/.env` là runtime local.

- `tích hợp thử nghiệm/.deploy.env` là deploy local.

- `tích hợp thử nghiệm/public/index.html` là document.

- `tích hợp thử nghiệm/public/app.js` là browser module.

- `tích hợp thử nghiệm/public/styles.css` là stylesheet.

- `tích hợp thử nghiệm/public/logoweb.png` là logo asset.

- `tích hợp thử nghiệm/public/avatar-placeholder.svg` là fallback.

- `tích hợp thử nghiệm/functions/auth/google.js` là start route.

- `tích hợp thử nghiệm/functions/auth/callback.js` là callback route.

- `tích hợp thử nghiệm/functions/api/session.js` là session route.

- `tích hợp thử nghiệm/functions/api/logout.js` là logout route.

- `tích hợp thử nghiệm/functions/api/project-status.js` là status route.

- `tích hợp thử nghiệm/functions/_lib/test-auth.js` là helper.

## BN. Liên kết giữa file và behavior

- HTML liên kết CSS bằng `/styles.css`.

- HTML liên kết JS bằng `/app.js`.

- HTML liên kết logo bằng `/logoweb.png`.

- HTML liên kết avatar fallback bằng SVG.

- JS liên kết login với `/auth/google`.

- JS liên kết logout với `/api/logout`.

- JS liên kết session với `/api/session`.

- JS liên kết status với `/api/project-status`.

- Google route liên kết APIUYTIN OAuth.

- Callback route liên kết token endpoint.

- Session route liên kết signed cookie.

- Logout route liên kết expired cookie.

- Status route liên kết project-info.

- Helper liên kết Web Crypto.

- Wrangler liên kết mọi route Pages.

## BO. Các giả định vận hành

- Production chạy HTTPS.

- Cloudflare Pages Functions được bật.

- Project APIUYTIN còn tồn tại.

- Project test còn active.

- Callback đã đăng ký chính xác.

- Google client configuration hợp lệ.

- Secret bindings đã được cấu hình.

- Browser cho phép cookie.

- Clock runtime là chính xác.

- Upstream API có thể truy cập.

- Static assets được deploy đầy đủ.

- Dependency install thành công.

- Account deploy có quyền phù hợp.

- Không có WAF rule chặn callback.

- Không có CSP rule chặn asset cùng origin.

## BP. Giới hạn hiện tại cần biết

- Không có automated test runner trong manifest.

- Không có build bundler frontend.

- CSS là file tĩnh.

- JS là module tĩnh.

- Session không phải database session.

- Session chỉ chứa profile tối thiểu.

- Logout không revoke upstream token.

- Project status phụ thuộc upstream.

- OAuth phụ thuộc Google.

- Callback phụ thuộc cookie browser.

- Error UI còn tối giản.

- Retry UI chưa được tách riêng.

- Observability không nằm trong frontend.

- Secret rotation cần thao tác Pages.

## BQ. Hướng mở rộng an toàn

- Thêm test contract cho session.

- Thêm test contract cho logout.

- Thêm test contract cho project status.

- Thêm browser test cho logo.

- Thêm browser test cho login button.

- Thêm browser test cho responsive.

- Thêm structured logging không chứa secret.

- Thêm request correlation id không nhạy cảm.

- Thêm timeout cho upstream fetch.

- Thêm retry có giới hạn cho upstream.

- Thêm error state rõ ràng cho frontend.

- Thêm CSP chặt hơn nếu phù hợp.

- Thêm monitoring endpoint status.

- Thêm secret rotation runbook.

- Thêm preview environment riêng.

## BR. Điều kiện chấp nhận thay đổi tương lai

- Không làm hỏng homepage.

- Không làm hỏng logo.

- Không làm hỏng login.

- Không làm hỏng callback.

- Không làm hỏng session.

- Không làm hỏng logout.

- Không làm hỏng project status.

- Không lộ credential.

- Không đổi callback ngoài chủ đích.

- Không đổi response ngoài contract.

- Không thêm dependency không cần.

- Không tăng quyền deploy token.

- Không bỏ qua kiểm tra browser.

- Không bỏ qua kiểm tra endpoint.

- Không bỏ qua review diff.

## BS. Mẫu báo cáo triển khai không nhạy cảm

- Project: `apitestingtichchopoauth`.

- Environment: integration test.

- Output: `public`.

- Functions: enabled.

- Callback: production callback đã đăng ký.

- Asset check: homepage 200.

- Asset check: logo 200.

- Function check: session response hợp lệ.

- Function check: logout response hợp lệ.

- Function check: project status theo upstream.

- Credential values: không ghi.

- Token values: không ghi.

- Cookie values: không ghi.

- Deployment URL: ghi riêng trong hệ thống phù hợp.

## BT. Chuỗi xác minh tối thiểu

- Kiểm tra manifest tồn tại.

- Kiểm tra public index tồn tại.

- Kiểm tra logo tồn tại.

- Kiểm tra Functions tồn tại.

- Kiểm tra vars không có secret.

- Kiểm tra env key names.

- Cài dependency nếu thiếu.

- Deploy đúng project.

- Probe homepage.

- Probe logo.

- Probe session.

- Probe logout.

- Probe project status.

- Kiểm tra browser snapshot.

- Kiểm tra diff sau cùng.

## BU. Kết luận vận hành

- Web test có ranh giới frontend rõ.

- Web test có ranh giới backend rõ.

- OAuth broker là APIUYTIN.

- Google là identity provider.

- Cloudflare là hosting runtime.

- Wrangler là công cụ triển khai.

- Cookie là session transport.

- HMAC là integrity mechanism.

- State là OAuth correlation mechanism.

- Callback là contract quan trọng.

- Secret binding là contract vận hành.

- Public asset không được chứa secret.

- Project status chỉ trả metadata.

- Tài liệu này không chứa credential thật.

- Mọi thay đổi code cần review theo runbook.
