npm package: @ncs191655320/live
Description: Agents CLI (Playwright-based) – chạy không cần .env theo mặc định; global bin: agents
Latest version: 0.4.4

# AGENT-JS

**Version**: 0.2.6  
**Package**: [@ncs191655320/live](https://www.npmjs.com/package/@ncs191655320/live)  
**CLI Command**: `agents` hoặc `agents -mkt`

AGENT-JS: Trình điều phối tự động hoá trình duyệt bằng **Playwright** (khung E2E – điều khiển Chromium/Firefox/WebKit) và kiến trúc mở để cắm **Fingerprint Suite** (bộ tạo & bộ tiêm fingerprint – hồ sơ trình duyệt).

## Yêu cầu hệ thống
- Node.js LTS
- Windows (đường dẫn kiểu `D:\...`)
- Quyền ghi thư mục dự án

## Cài đặt
```powershell
npm install
npm run pw:install
```

## Chạy chương trình

### Luồng chính (ephemeral profile)
```powershell
npm start
# hoặc sau khi cài global
agents
```

### Luồng MKT Login (profile manager)
```powershell
npm run start:mkt
# hoặc sau khi cài global
agents -mkt
```

Chương trình sẽ hỏi đường dẫn thư mục chứa nhiều `cookie.txt` (định dạng **Netscape cookie file** – định dạng lưu cookie truyền thống). Ví dụ: `D:\AZ-REFACTOR\LOGIN_LIVE\COOKIES_BULK`.

**Lưu ý luồng MKT**:
- Yêu cầu MKT Login API đang chạy tại `http://127.0.0.1:4980`
- Tự động chọn profile đầu tiên từ danh sách
- Không cần nhập workspaceId (sử dụng workspace mặc định)

## Luồng xử lý
1) Tạo BrowserContext bền vững mới với `userDataDir` tạm (tương đương “Delete browsing data – all time”).
2) Sinh fingerprint profile → áp dụng Context Options + `addInitScript`.
3) Warm-up Google (`waitUntil: networkidle`, scroll nhẹ).
4) Tiêm cookie theo domain (`context.addCookies`) và xác nhận trong jar.
5) Verify UI (login.live.com) + retry reload; Verify storage (portal.azure.com) qua `localStorage`/`sessionStorage`.
6) Phân loại strict/normal, lưu cookie.txt vào `outputs/`.

**Lưu ý**: Đây là luồng chính (ephemeral). Xem thêm **Luồng MKT Login** bên dưới.

### Luồng MKT Login (v0.2.6+)
1) Kết nối MKT Login API (`http://127.0.0.1:4980`).
2) **Tự động phát hiện profileId**: Lấy danh sách profiles và chọn profile đầu tiên.
3) Mở profile và kết nối qua CDP (Chrome DevTools Protocol).
4) **Tiêm fingerprint Apify**: Sinh fingerprint deterministic (seed từ SHA1 của tên file cookie) và inject vào page-level.
5) Warm-up → Tiêm cookies → Verify Live → Verify Azure.
6) Phân loại và di chuyển file cookie như luồng chính.
7) Đóng profile sau khi hoàn tất.

### Quy trình phân loại & di chuyển tệp cookie

1) Đánh giá kết quả
   - Chế độ `strict` (chuẩn duy nhất): khi cả hai kiểm tra đều vượt qua: `uiPassed === true` và `apiPassed === true` (UI + API). Lưu ý quan trọng: mọi PASS UI bắt buộc phải thu thập được email tài khoản (capturedEmail). UI pass ở `verifyLive.ts` (strict chain: account → email → portal), API pass ở `verifyAzure.ts`.
   - Chế độ `normal`: mọi trường hợp còn lại (UI fail hoặc API fail), bao gồm cả trường hợp thiếu email (EMAIL_MISSING), OTP/MFA bắt buộc, hoặc các lỗi URL.

2) Xử lý tệp cookie
   - Đánh dấu đã xử lý: kết quả của mỗi tệp được push vào mảng `outcomes` để ghi ra báo cáo JSON cuối cùng.
   - Tổ chức lưu trữ đích:
     - Thư mục gốc và các nhánh con được đảm bảo tồn tại khi khởi động: `artifacts/`, `outputs/`, `outputs/strict/`, `outputs/normal/` (khởi tạo tại `src/config.ts`, hàm `ensureDirectories()` – xem `config.ts:237-242`).
     - Chọn thư mục đích theo kết quả:
       - `strict` nếu `uiPassed && apiPassed`.
       - `normal` nếu không thỏa điều kiện trên (bao gồm UI fail hoặc Azure fail, hoặc bị skip Azure do Live ở trang đăng nhập).
     - Di chuyển/sao chép file: chương trình sao chép `cookie.txt` vào thư mục tương ứng và lưu lại đường dẫn đích (`dest`) trong kết quả (thực hiện tại `src/orchestrator/main.ts` – đoạn phân loại & copy, xem `main.ts:299-307`).
   - Ghi báo cáo: cuối quá trình, toàn bộ `outcomes` được ghi ra file JSON trong `artifacts/` (xem `main.ts:314-316`).

## Scripts
- `npm run build` — biên dịch TypeScript.
- `npm start` — build + chạy orchestrator (luồng chính).
- `npm run start:mkt` — build + chạy orchestrator (luồng MKT Login).
- `npm run pw:install` — cài đặt trình duyệt Playwright Chromium.
- `npm run test:e2e` — chạy kiểm thử E2E cơ bản (Playwright Test).
- `npm publish` — publish gói lên npm registry.

## Phase 1 – Củng cố nền tảng

- Đã dựng bộ chuẩn **ESLint** (trình phân tích tĩnh – enforce rule TypeScript/bảo mật) + **Prettier** (trình định dạng code) với script:
  - `npm run lint` / `npm run lint:fix`
  - `npm run format` / `npm run format:fix`
- Cung cấp hàm `validateEnv()` tại `src/config.ts` để kiểm tra biến môi trường bằng **Zod** (thư viện validate schema); mọi module có thể gọi lại để đọc cấu hình sạch.
- Chuẩn hóa logging với wrapper **Pino** (logger hiệu năng cao) tại `src/logger.ts`; các module sử dụng `logger.info/warn/error` (không dùng `console.log`).
- Bật các flag TypeScript nghiêm ngặt: `noImplicitAny`, `exactOptionalPropertyTypes`, `noUnusedParameters`; đã xử lý cảnh báo trong toàn bộ codebase.
- Cập nhật `tsconfig.eslint.json` phục vụ ESLint với Project References.
- Bổ sung tài liệu chi tiết trong `docs/de-xuat-toi-uu.md` mô tả roadmap tối ưu theo phase.

## Phase 2 – Ổn định trình duyệt và DOM

- Thêm bộ helper DOM tại `src/utils/dom.ts` với `getStableLocator()` (locator ổn định + timeout động) và `ensureDomStable()` (snapshot DOM + retry, log `{ step, retryCount, message: "DOM khong on dinh" }`).
- Các flow (`authFlows.ts`, `verifyLive.ts`) chuyển sang dùng helper thay vì truy cập selector trực tiếp; mọi thao tác quan trọng đều đo ổn định DOM.
- Giới thiệu `ContextPool` (`src/browser/contextPool.ts`) giới hạn đồng thời, tái sử dụng `BrowserContext`, tự động `context.clearCookies()` giữa job và đóng context incognito khi cần.
- Đảm bảo `ephemeralProfile` và profile tạm đều cleanup trong `finally`.
- Bổ sung biến môi trường điều khiển pool: `CONTEXT_MAX_CONCURRENCY`, `CONTEXT_REUSE`, `CONTEXT_CLEAR_COOKIES`.
- Ghi nhận metric DOM: trung bình thời gian xử lý, số lần retry → phục vụ báo cáo Phase 2.

## Phase 3 – Tối ưu API & Dữ liệu

- `mktlogin-client/core/request.ts` dùng **p-retry** (retry + backoff jitter) và `AbortController`; log structured `{ event: "api_retry", attempt, status }`, timeout mặc định 15s (tùy chỉnh qua biến môi trường).
- `ApiError` mapping giữ nguyên nhưng response được phân loại rõ ràng hơn qua logging chính xác tên lỗi, trạng thái.
- `DataMover` refactor sang streaming (`fs.createReadStream`/`createWriteStream`), kiểm tra checksum SHA-1 sau copy trước khi xoá nguồn.
- Biến môi trường hỗ trợ Phase 3: `API_RETRY_ATTEMPTS`, `API_RETRY_MIN_TIMEOUT_MS`, `API_REQUEST_TIMEOUT_MS`.
- Tài liệu Phase 3 cập nhật tại `docs/de-xuat-toi-uu.md`; cần chạy thử để thu thập metric retry và mức RAM.

## Phase 4 – Quan sát & Kiểm thử

- Logger hỗ trợ `LOG_TRANSPORT_TARGET` + `LOG_TRANSPORT_OPTIONS` để chuyển log sang hệ thống quan sát (ví dụ Elastic APM) thông qua `pino.transport`.
- `src/utils/dom.ts` phát hành metric DOM (`dom_metric_snapshot`) giúp dashboard hoá retry/delay.
- Thêm `npm run bench` (Playwright) lưu kết quả vào `outputs/benchmarks/`; CI chạy benchmark nhẹ.
- CI GitHub Actions (`.github/workflows/ci.yml`) chạy lint → test → bench tự động.
- Test bổ sung `tests/domHelpers.spec.ts` đảm bảo helper DOM hoạt động ổn định.
- Lưu ý: SEO lint tạm thời ở chế độ "warning only" cho các thư mục legacy (`src/browser`, `src/services`, `src/utils`) trong khi dọn dẹp dần; xem runbook để biết kế hoạch làm sạch.

## Cấu trúc thư mục
```
AGENT-JS/
  package.json
  tsconfig.json
  .gitignore
  README.md
  src/
    config.ts
    logger.ts
    utils/
      cookies.ts
    browser/
      fingerprint.ts
      launch.ts
      context.ts
    services/
      warmup.ts
      injectCookies.ts
      verifyLive.ts
      verifyAzure.ts
    orchestrator/
      main.ts
  tests/
    e2e.spec.ts
  artifacts/
  outputs/
    strict/
    normal/
```

## Vận hành an toàn
- Không sử dụng vào mục đích bất chính.
- `HEADLESS` có thể bật/tắt qua biến môi trường.
- Thời gian chờ/Retry có thể tinh chỉnh qua `src/config.ts`.

## Citations (Bằng chứng tài liệu chính thức)
- Playwright — BrowserContext.addCookies  
  https://playwright.dev/docs/api/class-browsercontext#browser-context-add-cookies  
  Tóm tắt: Thêm nhiều cookie vào BrowserContext; cookie gồm name, value, domain, path, expires, httpOnly, secure, sameSite.

- Playwright — launchPersistentContext (Chromium)  
  https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context  
  Tóm tắt: Khởi chạy context bền vững dùng `userDataDir`; tạo thư mục mới tương đương hồ sơ sạch (xoá dữ liệu cũ).

- Playwright — page.goto (waitUntil: 'networkidle')  
  https://playwright.dev/docs/api/class-page#page-goto  
  Tóm tắt: `waitUntil` có thể là `networkidle` (không còn kết nối mạng ≥ 500 ms), phù hợp để chờ trang ổn định trước thao tác.

- Playwright — addInitScript  
  https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script  
  Tóm tắt: Tiêm script chạy trước mọi script của trang; có thể tinh chỉnh `navigator.*` ở mức JS.

- cURL — Netscape cookie file format  
  https://curl.se/docs/http-cookies.html  
  Tóm tắt: Định dạng “Netscape HTTP Cookie File”: domain, flag, path, secure, expiration, name, value; dòng `#` là comment; có tiền tố `#HttpOnly_`.

## Assumptions & Gaps (Giả định & Khoảng trống cần đối chiếu)
- Fingerprint Suite (generator + injector): CHƯA xác minh tên gói và API chính thức trên npm. Cần chỉ định vendor + tài liệu để viết adapter thực thi thật (thay thế `generateFingerprint()` và mở rộng `injectFingerprint()`).
- Heuristics verify login.live.com và portal.azure.com: cần bằng chứng runtime để tối ưu (URL/selector ổn định, key storage phổ biến). Hiện dùng chiến lược phòng thủ, có retry và bằng chứng ảnh.
- Môi trường Python gốc: cần cây thư mục và file nguồn (main.py, nơi xử lý cookie, trình duyệt) để so khớp 1-1.
- Chính sách bảo mật/tốc độ: có thể cần điều chỉnh timeout và `MIN_LS_KEYS` theo mạng/tài khoản thực tế.

## Kiểm thử
```powershell
npm run test:e2e
```

## Góp ý & mở rộng
- Có thể mở rộng module fingerprint để cắm vendor chính thức khi có tài liệu.
- Bổ sung lưu HAR và thêm báo cáo chi tiết nếu cần.

## Cấu hình Azure (verifyAzure)

- Biến môi trường quan trọng (xem ánh xạ trong `src/config.ts`):
  - `AZURE_REQUIRE_USERID_STRICT` (mặc định `true`) — siết chặt: bắt buộc có `userId` hợp lệ kể cả khi có `github` (tham chiếu `src/config.ts:74-76`, `src/config.ts:243-245`).
  - `STORAGE_RETRIES` (mặc định `2`) — số lần thử lại khi kiểm tra storage (tham chiếu `src/config.ts:240-241`, dùng trong `verifyAzure.ts:51-56`).
  - `STORAGE_BACKOFF_MS` (mặc định `2000`) — thời gian chờ giữa các lần thử (tham chiếu `src/config.ts:240-241`, dùng trong `verifyAzure.ts:51-56`).
  - `STORAGE_REQUIRE_LOCAL_ONLY` (mặc định `true`) — gate readiness ưu tiên `localStorage` (tham chiếu `src/config.ts:239-241`, dùng trong `verifyAzure.ts:51-76`).
  - `STORAGE_WAIT_PATTERNS` — danh sách pattern chờ key xuất hiện (ví dụ: `sessionTracking_,msal.account.keys`) (tham chiếu `src/config.ts:219-224`, dùng trong `verifyAzure.ts:51-76`).
  - `STORAGE_VERIFY_WRITE` — probe ghi thử (`none` | `sessionOnly` | `localPreferred`) (tham chiếu `src/config.ts:226-231`, dùng trong `verifyAzure.ts:51-76`).
  - `STORAGE_MIN_KEYS` — ngưỡng tối thiểu key để pass về mặt số lượng (tham chiếu `src/config.ts:225-226`, dùng trong `verifyAzure.ts` qua tham số `minLocalStorageKeys`).

## Quy trình verifyAzure (tóm tắt kỹ thuật)

1) Mở `https://portal.azure.com` và ổn định trang: `stabilizePage(...)` (tham chiếu `src/services/verifyAzure.ts:48-49`).
2) Vòng lặp kiểm tra readiness + refresh/backoff theo config (tham chiếu `src/services/verifyAzure.ts:51-93`):
   - `checkStorageReady()` gate trước khi đọc chi tiết (tham chiếu `src/utils/checkStorageReady.ts:10-21,34-44,146-183`).
   - Trước lượt cuối: `page.reload()` để phục hồi nếu cần (tham chiếu `src/services/verifyAzure.ts:83-88`).
3) Đọc bằng chứng storage: `readAzureStorage()` (tham chiếu `src/utils/storage_scripts.ts`) và đánh giá tiêu chí pass/fail (tham chiếu `src/services/verifyAzure.ts:95-123`).
4) Ghi ảnh `azure-*.png` và artifact chi tiết `azure-detail-*.json` (tham chiếu `src/services/verifyAzure.ts:126-142`).

Tiêu chí pass (điều kiện đầy đủ):
- Số key `localStorage` ≥ `STORAGE_MIN_KEYS` VÀ
- Có đầy đủ nhóm key bắt buộc `userId`, `displayName`, `domainName`, `email` HOẶC có key `github` VÀ
- `userId` hợp lệ theo cấu hình siết chặt (khi `AZURE_REQUIRE_USERID_STRICT=true`, luôn bắt buộc `userId` hợp lệ; tham chiếu `src/services/verifyAzure.ts:108-112`).

## Reason codes (Azure)

- `OK` — đạt tiêu chí pass (tham chiếu `src/types/resultCodes.ts:15`).
- `STORAGE_UNAVAILABLE` — không truy cập được Web Storage (gate readiness fail) (tham chiếu `src/types/resultCodes.ts:16`).
- `STORAGE_INSUFFICIENT` — đếm không đủ key hoặc thiếu các key bắt buộc (tham chiếu `src/types/resultCodes.ts:17`).
- `RETRY_EXCEEDED` — hết số lần thử mà vẫn không đạt (tham chiếu `src/types/resultCodes.ts:18`).
- `RETRIED_REFRESH_RECOVERED` — hồi phục sau khi refresh (tham chiếu `src/types/resultCodes.ts:19`).
- `INTERNAL_ERROR` — lỗi evaluate/ngoại lệ (tham chiếu `src/types/resultCodes.ts:20`).
- `UNKNOWN` — dự phòng (tham chiếu `src/types/resultCodes.ts:21`).

## Artifact chi tiết (azure-detail-*.json)

- Ghi tại thư mục `artifacts/` mỗi lần verify Azure (tham chiếu `src/services/verifyAzure.ts:130-142`).
- Trường dữ liệu chính:
  - `url`, `attempts`, `refreshed`, `ready`, `reasonCode`, `apiPassed`.
  - `evidence`: snapshot storage (đã ẩn giá trị PII — xem `sample` trong `src/utils/storage_scripts.ts`).





 
Chuẩn xác thực tài khoản (strict only) – login.live.com → portal.azure.com

- Nguyên tắc bất di bất dịch: PASS UI chỉ hợp lệ khi đã thu thập được email tài khoản (capturedEmail) sau khi vào account.* và trước/đồng thời khi vào portal.
- Sau khi điều hướng đến `login.live.com` và ổn định lần đầu:
  - Nếu URL vẫn là trang đăng nhập (LOGIN_PAGE) → FAIL sớm.
  - Nếu phát hiện OTP/MFA → FAIL với `OTP_REQUIRED`/`MFA_REQUIRED`.
  - Nếu gặp passkey prompt → cố gắng bỏ qua (handlePasskeyInterrupt) và tiếp tục.
- Chuỗi strict:
  1) Vào `account.*` → thu thập email (qua `/me`, JWT cookies, URL param dev, hoặc Web Storage với `checkStorageReady` siết ổn định tên keys).
  2) Điều hướng/ở `portal.azure.com` → điền email + Next → safe-load → ổn định thích ứng.
  3) Kiểm tra OTP/MFA sau khi nhập email.
  4) Nếu có `capturedEmail` và không gặp OTP/MFA → PASS `OK_STRICT_CHAIN`.

## Tăng cường độ tin cậy thu thập email (Web Storage)

- `getEmailFromWebStorage(...)` chỉ chạy khi `checkStorageReady(...)` đạt sẵn sàng với ổn định tên keys (names stability):
  - Bật `requireStableNames=true` và yêu cầu `consistencyPasses ≥ 2` (mặc định 2) với `scanIntervalMs` hợp lý (mặc định 200 ms).
  - Nếu readiness không đạt, hàm dừng (return null), KHÔNG quét best‑effort.
- Điều này giảm false‑positive và đảm bảo email được lấy khi storage đã “ổn định”.

## Adaptive stabilization – Hướng dẫn đọc thống kê & checklist debug

Mục này giúp đọc nhanh thống kê ổn định thích ứng (AdaptiveStat) và xử lý khi `ready=false` mà không cần tăng timeout.

### AdaptiveStat gồm những gì

- label: nhãn của lần đo (ví dụ: `verifyLive-adaptive`, `verifyLive-portalNav-adaptive`, `verifyAzure-adaptive`). Gán nhãn tại `src/services/verifyLive.ts` quanh các lần push thống kê.
- scans: số vòng quét DOM/console trong nhịp; xấp xỉ `durationMs / scanIntervalMs` (tham số ở `src/config.ts` – runtime mapping `adaptiveScanIntervalMsLive/Azure`).
- durationMs: thời lượng của nhịp (ms); bị chặn bởi `maxDurationMs` (ví dụ Live đặt `Math.min(safeWaitMs, 4000)` tại `src/services/verifyLive.ts`).
- firstStableAtMs: thời điểm lần đầu đạt trạng thái ổn định trong nhịp; có thể rỗng nếu không ổn định.
- ready: true nếu điều kiện ổn định thỏa trong nhịp; false nếu hết thời lượng mà chưa đạt.
- phase: pha nhận diện được (ví dụ `PRE`, `POST`, hoặc `NONE`).
- passes: số lần liên tiếp thỏa điều kiện trong nhịp (chống nhiễu); trần bởi `consecutivePasses` (`src/config.ts` – runtime mapping `adaptiveConsecPassesLive/Azure`).

### Quy tắc quyết định từ SELECTORS (pre/post)

- Any-Of: trong mỗi tập `preSelectors` và `postSelectors`, chỉ cần tìm thấy 1 selector đáng tin là coi như “match” cho nhãn tương ứng; sau đó cần đạt liên tiếp `consecutivePasses` để kết luận “ready”.
- Nguồn cấu hình: selector nạp từ `config.livePreSelectors` và `config.livePostSelectors` (mặc định đã ghim trong `src/config.ts`). Ở runtime được dùng tại `src/services/verifyLive.ts:392-399`.
- Ưu tiên: tập POST nên ưu tiên selector shell ổn định của Azure Portal: `div.fxs-portal`, `#shell-header`, `#appBar`, `div.fxs-topbar`, `nav.fxs-sidebar`, `div.fxs-avatarmenu`, `div.fxs-blade-content`.

### Checklist khi `ready=false`

- Selector – kiểm tính phù hợp
  - Xác nhận selector POST thực sự xuất hiện trên tenant: kiểm nhanh bằng `page.$(selector)` hoặc thêm log.
  - Điều chỉnh danh sách POST (giữ 5–7 selector “nòng cốt”) trong `src/config.ts` để tăng xác suất match sớm, không cần tăng timeout.
- URL guard – đúng đích portal
  - Điều hướng HTTPS trực tiếp `https://portal.azure.com` và ép `#home` khi cần (xem `src/services/verifyAzure.ts`).
  - Sau mỗi `goto()/reload()` có guard “chrome-error bounce” để phục hồi (xem `src/services/verifyAzure.ts` và `src/services/verifyLive.ts`).
- Service Worker/Network – giảm nhiễu
  - Bật CDP `Network.enable` + `Network.setBypassServiceWorker({ bypass: true })` ngay sau khi có `page` (xem `src/services/verifyLive.ts`, `src/services/verifyAzure.ts`).
- Storage readiness – Azure
  - Dùng `checkStorageReady()` để gate trước khi đọc chi tiết (gọi trong `src/services/verifyAzure.ts`).
  - Ngưỡng đếm và yêu cầu keys cấu hình ở `src/config.ts` (nhóm `STORAGE_*`).
- Consecutive passes – chống nhiễu
  - Nếu selector đúng nhưng vẫn nhiễu, có thể tăng nhẹ `ADAPTIVE_CONSEC_PASSES_*` (không cần tăng timeout) trong `src/config.ts`.
- Ảnh chứng cứ
  - Chức năng chụp ảnh đã vô hiệu hóa trong mã; hãy dựa vào log và artifact JSON.

### Đoạn kiểm thử nhanh selector (TypeScript)

```ts
// Chạy sau khi điều hướng portal để kiểm xem selector shell có xuất hiện hay không
const candidates = [
  'div.fxs-portal', '#shell-header', '#appBar',
  'div.fxs-topbar', 'nav.fxs-sidebar', 'div.fxs-avatarmenu',
  'div.fxs-blade-content'
];
for (const sel of candidates) {
  const ok = !!(await page.$(sel));
  console.log({ sel, ok });
}
```

Nếu nhiều selector phía trên trả về `ok=true`, hãy giữ chúng trong `LIVE_POST_SELECTORS` để adaptive “ready” sớm hơn mà không cần tăng thời gian chờ.


Quy tắc xử lý (STRICT ONLY – account → email → portal):
1) LOGIN_PAGE hoặc OTP/MFA → `uiPassed=false` (failCode: `LOGIN_PAGE` | `OTP_REQUIRED` | `MFA_REQUIRED` | `MFA_PUSH_NOTIFICATION_REQUIRED`).
   - `MFA_PUSH_NOTIFICATION_REQUIRED`: Màn hình "Get a code to sign in" + nút "Send notification" yêu cầu xác thực qua push notification đến thiết bị di động → không thể tự động bypass.

2) ERROR_URL → tiếp tục retry trong giới hạn; nếu vẫn lỗi → `uiPassed=false` (`ERROR_URL`).
   - Nếu gặp passkey: dùng `handlePasskeyInterrupt` để bỏ qua rồi tiếp tục quy trình.
   - Không dùng `OK_LOGGED_IN` để PASS; đây chỉ là trạng thái tham chiếu.

3) Ở portal.azure.com và đã nhập email:
   - Chỉ FAIL bởi OTP/MFA sau khi nhập email.
   - PASS chỉ khi đã thu thập được email (capturedEmail) và không gặp OTP/MFA → `reasonCode = OK_STRICT_CHAIN`, `uiPassed=true`.
   - Hậu kiểm: nếu `uiPassed=true` nhưng thiếu email → ép FAIL `EMAIL_MISSING`.


điều chỉnh thêm trường hợp Ở portal.azure.com và đã nhập email:
   - Chỉ FAIL bởi OTP/MFA sau khi nhập email.
   - ERROR_URL → sau khi nhập email tiếp tục retry trong giới hạn 2 lần; nếu vẫn lỗi → `uiPassed=false` (`ERROR_URL`).
   - PASS chỉ khi đã thu thập được email (capturedEmail) và không gặp OTP/MFA → `reasonCode = OK_STRICT_CHAIN`, `uiPassed=true`.
   - Hậu kiểm: nếu `uiPassed=true` nhưng thiếu email → ép FAIL `EMAIL_MISSING`.