Config & Bindings
Danh sách đóng. Mọi biến cấu hình đang chạy phải có mặt ở trang này (AS-03.4.5). Đây không phải quy ước lịch sự — scripts/verify-bindings.mjs (chạy trong CI) đọc chính trang này và fail nếu một binding hoặc một var trong wrangler.jsonc không được nhắc tới ở đây. Thêm cấu hình mà quên tài liệu = CI đỏ, không phải nợ âm thầm.
Ba lớp kiểm của verify:bindings:
| Lớp | Kiểm gì | Chạy khi nào |
|---|---|---|
| A — Kiểu | Mọi binding/var của nemo12-api được khai kiểu trong workers/api/src/env.ts (AS-03.4.3) | luôn |
| B — Tài liệu | Mọi binding/var có trong trang này (AS-03.4.5) | luôn |
| C — Thật | Hỏi thẳng Cloudflare xem tài nguyên có tồn tại (RISK-026) | khi có CLOUDFLARE_API_TOKEN |
1. Bindings của nemo12-api
Nguồn: workers/api/wrangler.jsonc. Kiểu: workers/api/src/env.ts.
| Binding | Loại | Tài nguyên | Dùng cho |
|---|---|---|---|
DB | D1 | nemo12-platform (f4275a70-…) | Toàn bộ dữ liệu quan hệ — 76 bảng |
CONFIG | KV | f08c2394… | Cấu hình runtime đọc nhiều, ghi hiếm |
CONTENT | R2 | nemo12-content | Media, tài sản nội dung (SDD-009), và snapshot context AI (ai-context/…, AS-10.3.4) |
AI | Workers AI | tài nguyên cấp tài khoản | LLM — luôn qua Gateway nemo12 |
EVENTS | Queue producer | nemo12-events | Domain events |
FOUNDRY | Service | nemo12-foundry | Xưởng sinh course content (SRC-632). Worker ấy không có route công khai, nên binding này là đường vào duy nhất — và nó nằm sau cổng admin của api |
EVENT_STAGE | Durable Object | lớp EventStage (migration DO v1, new_sqlite_classes) | Live Stage của buổi sự kiện (SRC-711). Durable Object đầu tiên của hệ thống. Một instance cho mỗi buổi (idFromName(event_id)): giữ các WebSocket đang mở và phát lại ảnh chụp đọc từ D1. Nó KHÔNG giữ trạng thái nghiệp vụ — DO bị thu hồi giữa buổi thì không mất phiếu nào |
Consumer nemo12-events: max_batch_size 25, max_retries 5, DLQ nemo12-events-dlq. Message chết được ghi bền vào bảng queue_dead_letters để replay được — xem queues.
RISK-026: binding sai không làm
wrangler deployfail. Worker lên xanh, rồi request đầu tiên chạmc.env.DBmới nổ — trên production, trước mặt phụ huynh. Đó là lý do lớp C tồn tại.
2. Vars của nemo12-api (công khai, nằm trong wrangler.jsonc)
Đủ 13 var, khớp Env trong workers/api/src/env.ts:
| Var | Giá trị production | Ý nghĩa và hệ quả nếu sai |
|---|---|---|
ENVIRONMENT | production | Phân biệt môi trường trong log và thông điệp lỗi |
COOKIE_DOMAIN | .nemo12.com | Domain cookie phiên. Sai → đăng nhập ở learn. không nhận ở marlins. |
ALLOWED_ORIGIN_SUFFIX | nemo12.com | CORS + CSRF: request đổi dữ liệu phải có Origin thuộc hậu tố này. Không hardcode domain trong code (REQ-SEC-06) |
AI_GATEWAY_ID | nemo12 | Gateway bắt buộc cho mọi lời gọi AI (QG-010). Sai → lời gọi đi ngoài gateway, mất cả log lẫn hạn mức |
CF_ACCOUNT_ID | ff302e77… | Id tài khoản Cloudflare, dùng gọi API RealtimeKit (SRC-1168). Công khai, không phải bí mật |
RTK_APP_ID | ee728643-… | Id app RealtimeKit nemo12-speak-rooms (SRC-1168). Công khai |
RTK_WEBHOOK_PUBLIC_KEY | khoá PEM | Khoá công khai của RealtimeKit để kiểm chữ ký webhook recording.statusUpdate (SRC-1168). Nguồn: api.realtime.cloudflare.com/.well-known/webhooks.json |
SPEAK_ROOMS_PROVIDER | realtimekit | Nhà cung cấp phòng nói Speak rooms (SRC-1162, SDD-052 §4): off | mock | realtimekit. off thì tạo phòng trả lỗi, không gọi mạng; đổi sang realtimekit chỉ sau khi chủ dự án đã đặt các secret RTK_* và CF_ACCOUNT_ID (xem .claude/memory/viec-dang-cho.md) |
AI_DAILY_USD_CAP | 2 | Trần USD/ngày (ƯỚC theo đơn giá token công bố; hoá đơn thật tính theo neuron — neo: tháng 8/2026 = 7,84M neuron = $86,28) cho cron AI */5 (xưởng curriculum + IELTS + dịch b21), cộng dồn ở KV ai:usage:YYYY-MM-DD mỗi nhịp, log cron_ai_tick_done ghi usd_tick/usd_today. Chạm trần là nhịp không gọi model (cron_ai_daily_cap_reached); 0 = tắt trần. Trần MỀM (đọc-rồi-ghi KV); trần cứng là công tắc KV ai_cron_paused. Foundry có trần riêng bên dưới. Mức 2 (~$60/tháng) là trần CHẶN do audit đặt, chủ dự án chỉnh theo nhu cầu sinh nội dung (Audit #013, P-1) |
GOOGLE_CLIENT_ID | 856179425546-… | Audience để verify id_token. Client ID là public theo thiết kế của OAuth, không phải secret |
GOOGLE_MOBILE_CLIENT_IDS | chưa đặt | Client id Google của app NEMO IELTS (iOS, Android), cách nhau dấu phẩy (SRC-1100, SDD-055 §4). Chỉ được nhận khi POST /v1/auth/google gửi client: "mobile"; đăng nhập web vẫn chỉ nhận GOOGLE_CLIENT_ID. Public như mọi client id. Chưa đặt thì app chỉ đăng nhập được bằng token có aud là client web |
APPLE_BUNDLE_ID | com.nemo12.ielts | aud bắt buộc khi verify identity token của Sign in with Apple (SRC-1100, SDD-055 §4). Thiếu hoặc rỗng thì POST /v1/auth/apple trả 503, không bao giờ xác thực. Public, không phải secret |
ACCESS_TEAM_DOMAIN | dac2205.cloudflareaccess.com | Team Zero Trust phát Access JWT. Dùng làm issuer + nguồn JWKS khi verify. Sai → nhận token của team khác (SRC-129) |
ACCESS_AUD | 261f5250… | AUD tag của app Nemo12 Admin. Là audience bắt buộc khi verify. Thiếu/sai → JWT của bất kỳ app nào khác cùng team (Coral, Dolphin…) cũng mở được admin |
ACCESS_AUD_DOLPHIN | 5b5df250… | AUD tag của app Nemo12 Dolphin (SRC-1262). Dùng khi POST /v1/auth/mentor-grant verify JWT để cấp vai mentor cho ai qua Access của dolphin. Phải KHÁC ACCESS_AUD: JWT của admin không được lấy vai mentor, JWT của dolphin không được mở admin. Thiếu → endpoint trả lỗi cấu hình, không cấp gì |
DIGEST_FROM | NEMO <support@nemo12.com> | Địa chỉ gửi thư qua Email Service (SRC-602/604). Đổi được không cần deploy code. Sai domain → E_SENDER_DOMAIN_NOT_CONFIGURED, thư ghi failed |
DIGEST_TEST_RECIPIENT | "" (đang TẮT — gửi thật) | Chốt xem trước: còn đặt thì mọi thư của cả 10 loại đi về đây thay vì tới phụ huynh thật, tiêu đề mang [THỬ → ai-đáng-lẽ-nhận]. Đặt "" là bật gửi thật |
EMAIL_COPY_TO | dac2205@gmail.com | Bản đối chứng mọi thư gửi phụ huynh, để chủ dự án đọc được đúng thứ họ nhận. Để trống thì không gắn bản sao. Đây là địa chỉ thật của một người, đổi người nhận là đổi ai đọc được thư gửi cho gia đình |
IELTS_REPORT_TO | dac2205@gmail.com | Người nhận báo cáo tiến độ IELTS hằng ngày (SRC-928), gửi trong lượt cron 12:00 UTC. Để trống thì báo cáo về đúng EMAIL_COPY_TO, tức là vẫn tới chủ dự án. Thư vận hành: miễn trần ngày, và gửi cả vào ngày không ai học |
EMAIL_BCC_TO | dangtuyethong2324@gmail.com | BCC cố định cho MỌI lá thư (SRC-1311). Phụ huynh không thấy địa chỉ này; trang minh bạch dữ liệu khai nó. Đổi người nhận là đổi ai đọc được thư gửi cho gia đình. Rỗng = tắt |
PARENT_REPORTS_ENABLED | 1 | Thư báo cáo phụ huynh sau buổi học + thư tuần cho mọi phụ huynh (SRC-1311). 0 = thư sau buổi chạy khô, thư tuần cũ giữ nguyên. 1 = gửi thật, thư tuần mới thay weekly-report |
PLAN_MAILS_ENABLED | 1 | Thư Learning Plan IELTS và SAT (SRC-1327): tuần (tối Thứ Năm, cho 7 ngày từ Thứ Sáu) và tháng (ngày 25, cho tháng sau), bản phụ huynh và bản học sinh. 0 = chạy khô, 1 = gửi thật |
WHATS_NEW_ENABLED | 1 | Thư Có gì mới hằng tuần (SRC-1328): bản nháp tối Thứ Năm luôn về EMAIL_COPY_TO; lá thật tối Chủ nhật cho phụ huynh và học sinh chỉ khi bằng 1 và tuần có tin published |
PARENT_REPORT_TEST_ON | 2026-10-10#4 | Ngày (giờ VN) chạy MỘT lượt gửi thử thư báo cáo phụ huynh: 3 học sinh mỗi chương trình, dữ liệu thật, chỉ gửi về EMAIL_COPY_TO (và BCC). Ngày khác thì không chạy |
SAMPLE_SEND_ON | 2026-10-11#1 | Ngày (giờ VN) gửi TOÀN BỘ thư mẫu một lần tới EMAIL_SAMPLE_TO, qua cron mỗi giờ, không cần ai đăng nhập admin. Ngày khác thì không chạy; chạy lại cùng ngày không gửi lần hai. Dạng YYYY-MM-DD#n: gửi lại trong cùng ngày thì tăng n |
EMAIL_SAMPLE_TO | dac2207@gmail.com | Hộp thư thử của chủ dự án: nút "Gửi thư mẫu" ở admin.nemo12.com/#emails gửi về đây (07.10.2026), để xem thư như một người nhận thật. Rỗng thì về EMAIL_COPY_TO. Endpoint vẫn không nhận địa chỉ tuỳ ý: đổi người nhận thư mẫu là đổi cấu hình |
MAIL_SERIES_ENABLED | 1 | Công tắc chuỗi thư dài hạn của sub-brand (SRC-1289, ba lá mỗi tuần). 0 = lượt cron chạy KHÔ: tính đủ ai sẽ nhận lá nào, ghi log, không gửi, không giành hàng chống trùng. 1 = gửi thật tới learner. Chỉ bật sau khi chủ dự án đã đọc bản mẫu |
INBOUND_FORWARD_TO | dac2205@gmail.com | Thư tới mọi hộp thư sub-brand (ielts@, sat@, grammar@..., SRC-1282) được Email Worker chuyển tiếp về đây. Phải là Destination address đã xác minh trong Email Routing, nếu không thư bị trả về người gửi. Để trống thì về EMAIL_COPY_TO. Đổi địa chỉ này là đổi ai đọc thư người ngoài gửi cho Nemo12 |
EMAIL_COPY_MODE | bcc | cc = phụ huynh NHÌN THẤY địa chỉ đối chứng và có thể Trả lời tất cả vào đó (chỉ đạo 2026-09-07); bcc = đối chứng vô hình. Đổi được mà không cần deploy |
EMAIL_MAX_PER_DAY | 3 | Trần số thư một người nhận trong một ngày lịch Việt Nam (chỉ đạo 2026-09-07). Thư theo lịch vượt trần bị hoãn sang hôm sau chứ không mất; thư trả lời hành động vừa xảy ra (vé sự kiện, chào mừng, kết quả bài đầu vào) không bị trần chặn |
OUTREACH_SEND_ENABLED | 1 | Công tắc thư mời trường tháng 10.2026 từ hello@nemo12.com (SRC-1110). Chỉ đúng 1 mới gửi thật; giá trị khác thì "Gửi lô đã duyệt" bị từ chối, chỉ xem trước được. Bật sau khi hello@nemo12.com nhận được thư trả lời (Email Routing), xem SDD-049 §5 |
PUBLIC_API_ORIGIN | https://api.nemo12.com | Gốc URL dựng địa chỉ ảnh 1×1 theo dõi lượt mở thư (SRC-681). Sai gốc thì ảnh không tải được và mọi thư trông như chưa ai mở |
1b. Binding của nemo12-foundry (SRC-632)
Nguồn: workers/foundry/wrangler.jsonc. Kiểu: workers/foundry/src/env.ts.
| Binding | Loại | Tài nguyên | Dùng cho |
|---|---|---|---|
DB | D1 | nemo12-platform (f4275a70-…) | Sổ lượt sinh: content_runs, content_run_trials |
CONTENT | R2 | nemo12-content | Bản THÔ của mỗi lượt gọi model (foundry/<run>/<model>.json) |
AI | Workers AI | tài nguyên cấp tài khoản | Mọi model, kể cả model bên thứ ba, luôn qua Gateway nemo12 |
LESSON_FORGE | Workflow | nemo12-lesson-forge | Workflow sinh một Lesson và chấm bằng nhiều model |
| Var | Giá trị | Ý nghĩa |
|---|---|---|
AI_GATEWAY_ID | nemo12 | Cùng gateway với api để log và hạn mức nằm chung một chỗ |
FOUNDRY_DAILY_USD_CAP | 5 | Trần chi phí (USD niêm yết ước) cộng dồn 24h. Chạm trần là mọi lượt mới bị từ chối; 0 = tắt chốt |
FOUNDRY_MAX_CONCURRENT_RUNS | 8 | Bao nhiêu lượt chạy cùng lúc. Nâng 2→8 (2026-08-28) sau khi ĐO được chi phí thật: $0,003/bài học, $0,017/bài kho câu hỏi — trần song song chỉ chặn tốc độ, trần chi phí ngày mới chặn tiền |
FOUNDRY_TARGET_COOLDOWN_MIN | 15 | Cùng một bài phải cách bao nhiêu phút mới được chạy lại |
FOUNDRY_MAX_ATTEMPTS_PER_TARGET | 3 | Thử quá số này mà vẫn thiếu thì dừng và ghi sổ việc tắc (SRC-636), chờ người chỉnh thuật toán |
FOUNDRY_MAX_BATCH | 10 | Trần cho một cú bấm "lấy việc" và cho mỗi lượt cron kéo |
FOUNDRY_AUTO_DRAIN | 1 | 1 = cron mỗi 15 phút tự kéo việc (vẫn qua đủ năm chốt). Khác 1 = tắt. Đang bật cho đợt 1 (3 khoá Toán + 3 khoá Ngữ văn); xong đợt thì đổi về 0 |
Năm trần trên là luật chống "chạy loạn workflow" (SRC-634). Chúng ở vars chứ không ở D1 là cố ý: trần sửa được từ giao diện thì không còn là trần — nới phải qua một lần deploy.
Worker này khai workers_dev: false và không có routes: nó chỉ nhận request qua service binding FOUNDRY của api. Một endpoint đốt tiền model mà phơi ra Internet thì sớm muộn cũng có người bấm hộ.
1c. Binding của nemo12-tool-plane (SRC-667)
Nguồn: workers/tool-plane/wrangler.jsonc. Kiểu: workers/tool-plane/src/env.ts. Route công khai mcp.nemo12.com (custom domain) — cố ý, vì Claude Managed Agent chạy ở Anthropic nên không có service binding nào tới được; không token của agent là 401 trước khi chạm bảng nào (SDD-030 §5).
| Binding | Loại | Tài nguyên | Dùng cho |
|---|---|---|---|
DB | D1 | nemo12-platform (f4275a70-…) | Đọc learner data qua binding trực tiếp; bảng riêng tp_agents, tp_applications, tp_tools, tp_agent_tools, tp_policies, tp_audit_logs (migration 0188), tp_agent_tokens (0189) |
| Var | Giá trị | Ý nghĩa |
|---|---|---|
TOOL_PLANE_ENVIRONMENT | production | Tầng môi trường mà grant environment_scope phải phủ; khai sai thì code coi là production (sai về phía chặt) |
Không có secret nào ở worker: token của agent chỉ lưu SHA-256 (tp_agent_tokens, hoặc tp_agents.token_hash cho token không trói user). Cấp token bằng node scripts/tool-plane/issue-agent-token.mjs <agent_id> [--user <id>] (in token một lần + SQL nạp hash). Managed Agents chỉ gửi được Authorization, nên user nằm trong token (SDD-030 §6).
2b. Vars của nemo12-admin (worker phục vụ admin.nemo12.com)
apps/admin không còn là app tĩnh thuần: nó có main (worker.ts) để nhận danh tính Cloudflare Access và đổi lấy session (SRC-129). Kiểu Env khai ngay trong apps/admin/worker.ts.
| Binding / Var | Giá trị production | Ý nghĩa và hệ quả nếu sai |
|---|---|---|
ASSETS | binding tới ./dist | Phục vụ file tĩnh của app. run_worker_first: ["/auth/*"] để SPA fallback không nuốt mất /auth/access-session |
API_ORIGIN | https://api.nemo12.com | Nơi worker chuyển tiếp Access assertion. Sai → admin không đăng nhập được (không còn đường đăng nhập nào khác) |
3. Biến runtime không nằm trong vars
Ba thứ hành xử như cấu hình nhưng không đi qua wrangler.jsonc — kê ra để danh sách thật sự đóng:
| Thứ | Ở đâu | Vì sao không phải var |
|---|---|---|
POLICY_VERSION = 2026-08-15 | shared/privacy.ts | Phiên bản văn bản chính sách. Đổi nó là một thay đổi có review, phải đi qua PR và commit — không phải nút vặn lúc runtime |
RATE_LIMITS.* (5 quy tắc) | shared/ratelimit.ts | Hạn mức là hợp đồng với người dùng; đổi lén qua env sẽ khiến không ai biết vì sao hôm nay bị chặn. Xem §4 |
TIMEOUT_MS (fetch 8s, ai 30s) | shared/http.ts | Hạn giờ gắn với hành vi fallback trong code, đọc cùng chỗ với chỗ dùng |
AI_MODELS (shared/prompts.ts) cũng vậy: model id khai tường minh trong code để grep ra được đang chạy model nào (AS-10.1.3), không đọc từ env — env đọc được nghĩa là đổi được model production mà không ai thấy trong git.
4. Hạn mức đang áp
Khai tập trung tại shared/ratelimit.ts, đếm bằng D1 (rate_limit_buckets):
| Scope | Hạn | Cửa sổ | Đặt ở đâu | Đếm theo |
|---|---|---|---|---|
login | 20 | 5 phút | POST /v1/auth/google | IP |
ai-generate | 30 | 1 giờ | POST /v1/coral/generate-items, POST /v1/coral/blueprints/{id}/generate | user |
crawl | 60 | 1 giờ | dành cho thu thập nguồn ngoài — chưa gắn route nào | user/IP |
submit | 120 | 1 phút | dành cho gửi bài — chưa gắn route nào | user |
privacy-write | 30 | 10 phút | 3 route ghi của /v1/consents + /v1/privacy/deletion-requests | user |
Hai dòng "chưa gắn route" được ghi ra thay vì giấu: quy tắc đã có, chỗ dùng thì chưa. Chạm hạn mức → RATE_LIMITED (429) kèm Retry-After. Bộ đếm hỏng thì fail-open có chủ đích (chặn nhầm phụ huynh đang dùng thật tệ hơn là lọt vài lượt gọi) và ghi RATE_LIMIT_CHECK_FAILED vào log.
5. Secrets
Không có secret nào trong repo, trong vars, hay trong git history (AS-07.5.1 PASS ở audit #001 — đừng làm hỏng). Đặt qua wrangler secret put / GitHub Secrets:
| Secret | Nơi dùng | Quyền cần |
|---|---|---|
CLOUDFLARE_API_TOKEN | GitHub Actions: deploy, rollback, verify:bindings lớp C | Edit Workers, D1, KV, R2, Queues |
CLOUDFLARE_ACCOUNT_ID | như trên | — |
Luật: cấm hardcode secret, domain, audience (QG-008). Client secret của Google không cần cho luồng ID-token hiện tại — đó là lý do nó không có trong bảng.
6. Cấu hình từng worker (9 worker)
Mọi worker: compatibility_date 2026-08-01, observability.enabled = true (AS-08.3.1).
Worker (name) | Config | Domain | Kiểu | Binding | Cron |
|---|---|---|---|---|---|
nemo12-api | workers/api/wrangler.jsonc | api.nemo12.com | code (src/index.ts) | 5 (§1) | 0 21 * * * |
nemo12-web | apps/web/wrangler.jsonc | nemo12.com, www.nemo12.com | assets (SPA) | — | — |
nemo12-learn | apps/learn/wrangler.jsonc | learn.nemo12.com | assets (SPA) | — | — |
nemo12-marlins | apps/marlins/wrangler.jsonc | marlins.nemo12.com | assets (SPA) | — | — |
nemo12-dolphin | apps/mentors/wrangler.jsonc | dolphin.nemo12.com | assets (SPA) | — | — |
nemo12-admin | apps/admin/wrangler.jsonc | admin.nemo12.com | assets (SPA) | — | — |
nemo12-coral | apps/coral/wrangler.jsonc | coral.nemo12.com | assets (SPA) | — | — |
nemo12-ielts | apps/ielts/wrangler.jsonc | ielts.nemo12.com | assets (SPA) | — | — |
nemo12-docs | apps/docs/wrangler.jsonc | docs.nemo12.com | assets (VitePress) | — | — |
nemo12-pearl | apps/pearl/wrangler.jsonc | pearl.nemo12.com | assets (VitePress) | — | — |
Hai điều dễ vấp, ghi thẳng ra:
- Thư mục
apps/mentors/deploy ra worker tênnemo12-dolphin. Tên thư mục theo persona kỹ thuật, tên worker theo cách gọi của sản phẩm. Mọi lệnhwrangler … --namephải dùngnemo12-dolphin. data.nemo12.comxuất hiện trong tài liệu cũ như một surface nội bộ nhưng không cówrangler.jsoncnào trong repo — không phải worker của monorepo này.
Cron duy nhất: 0 21 * * * (04:00 giờ Việt Nam — giờ yên, learner không đang học) chạy WF-17 Retention Refresh, bọc withLock TTL 6 giờ để không chạy chồng (AS-08.2.5). Xem schedules.
Surface nội bộ (admin, coral, docs) được bảo vệ thêm bằng Cloudflare Access ở tầng hạ tầng — cấu hình ở dashboard Cloudflare, không nằm trong repo.
7. CI/CD
| Workflow | Kích hoạt | Việc |
|---|---|---|
.github/workflows/ci.yml | mọi push, mọi PR, dispatch | Cổng chất lượng (xem §8) và, trên main, deploy mọi app bị đụng — migration trước, code sau (AS-08.4.3 🔴) |
.github/workflows/deploy-app.yml | workflow_dispatch, chọn một app | Deploy lại một app hoặc quay về commit cũ. Đọc kết quả ci trên đúng commit ấy qua wait-for-ci.sh nên không lách được cổng |
.github/workflows/rollback.yml | workflow_dispatch thủ công | Quay ngược một worker về version trước — xem SDD-006 §17 |
.github/workflows/seed-data.yml | workflow_dispatch, một file scripts/*.sql | Nạp dữ liệu vào D1 production |
.github/workflows/check-production.yml | 23:00 UTC hằng ngày, dispatch | Đối chiếu commit đang chạy trên production với main |
App nào gồm đường dẫn nào, build bằng workspace nào, có cần áp migration không — tất cả khai ở .github/app-paths.json, và cả ci.yml, deploy-app.yml, scope.sh lẫn cổng đối chiếu production đều đọc từ đó. Trước 22.09.2026 câu trả lời ấy nằm chép tay ở 13 file deploy-*.yml cộng 13 bước trong ci.yml (SRC-948).
8. Cổng CI — 12 cổng, không cổng nào được || true
Tất cả nằm trong một job của ci.yml, và mỗi cổng chỉ chạy khi push ĐỤNG tới thứ nó gác (.github/scripts/scope.sh quyết định). Cột "chạy khi" là cờ phạm vi.
| # | Cổng | Lệnh | Chạy khi |
|---|---|---|---|
| 1 | typecheck | npm run typecheck (cả monorepo) | code |
| 2 | lint | npm run lint | code |
| 3 | test | npm test của workspace bị đụng | code |
| 4 | smoke giao diện | npm run test:e2e trong apps/learn (AS-05.3.5) | e2e |
| 5 | docs integrity | npm run check:docs — 11 cổng con (QG-001) | docs |
| 6 | cổng kiểm code | npm run check:code — 27 cổng con: em dash, ngày tháng, shadcn/token, gói cấm, định danh tiếng Anh, tương phản WCAG… | code |
| 7 | sổ chung không bị ghi đè | scripts/check-no-clobber.mjs (SRC-457) | docs |
| 8 | reference khớp source | scripts/gen-reference.mjs --check (AS-01.4.1 🔴) | reference |
| 9 | hợp đồng /v1 | npm run contract:diff (AS-03.2.4 🔴) | api |
| 10 | số migration liên tục | scripts/check-migration-numbers.mjs (AS-04.1.1 🔴) | migrations |
| 11 | migration dry-run từ D1 rỗng | npm run migrate:dryrun (AS-04.1.4 🔴) | migrations |
| 12 | binding tồn tại thật | npm run verify:bindings (AS-03.4.4) | bindings |
Cổng 5 và 6 mỗi cái là một NHÓM chạy qua scripts/run-checks.mjs: nhóm chạy hết rồi in bảng tổng kết, thay vì dừng ở cổng đỏ đầu tiên. Một lượt chạy chỉ trả về một lỗi là một lượt chạy gần như không có thông tin (SRC-948).
Cổng 8 dùng --check nên chỉ ĐỌC và so, không sửa cây làm việc — nhờ vậy chạy được cả ở máy (npm run check:docs gọi nó) lẫn trong CI. Trước đó nó sinh ra rồi git diff, và vì thế là cổng chặn deploy duy nhất không ai chạy được trước khi push.
Nguyên tắc: một cổng không bao giờ đỏ là một cổng không tồn tại. Đường workflow_dispatch (deploy-app.yml, rollback.yml) không chạy lại bộ cổng mà ĐỌC kết quả ci trên đúng commit đang deploy qua .github/scripts/wait-for-ci.sh, và tự chạy cổng tại chỗ nếu commit đó chưa từng qua ci. Trước 23.08.2026 mỗi workflow deploy chạy lại typecheck/lint/test — đúng thứ vừa xanh trên cùng commit, và GitHub tính tiền theo SỐ JOB làm tròn lên phút.
Hai cổng cố ý đứng NGOÀI ci vì chúng phụ thuộc mạng: npm run check:ui-latest (độ mới của bộ ba UI, DS-001 §0) và check-production.yml (đối chiếu production với main, chạy 23:00 UTC). Một cổng phụ thuộc mạng đứng chắn mọi push thì đến ngày mạng nấc là mọi phiên đứng.
8b. Nâng phụ thuộc
Dependabot mở PR theo lịch (.github/dependabot.yml): npm hằng tuần sáng thứ Hai, GitHub Actions hằng tháng. Bản minor và patch gom làm một PR; bản major đứng riêng, vì một major cần đọc changelog chứ không nên trôi lẫn trong một PR hai mươi gói.
Nâng nhiều gói thì phải thử CHUNG, không thử riêng (SRC-951). Ba PR đầu tiên của dependabot ngày 22.09.2026 cho thấy vì sao: mỗi PR xanh trên CI của chính nó, nhưng chúng không gộp được rời nhau — wrangler 4.136 đòi @cloudflare/workers-types ^5, nên PR nâng wrangler KHÔNG cài được nếu thiếu PR nâng workers-types. Gộp lần lượt thì lượt gộp thứ nhất làm main đỏ.
Hai lớp hỏng hay gặp khi nâng, và cách nhận ra:
| Dấu hiệu trong log | Nghĩa là | Cách chữa |
|---|---|---|
Two different types with this name exist, but they are unrelated | Hai bản cùng một gói type trong cây. Xảy ra khi workspace khai bản mới còn một gói trung gian xin * và npm giữ bản cũ ở gốc | Khai gói type ấy làm devDependencies của package.json gốc để chỉ một bản được hoist |
Handler<...> is not assignable to Context<...> hàng loạt ở workers/api | Một gói trong mạch Hono/zod đổi kiểu | Cô lập bằng cách nâng RIÊNG từng gói trong mạch, rồi ignore trong dependabot.yml |
Chặn theo RANH GIỚI, đừng chặn theo bản đã đo. Chuỗi sai lầm ngày 22.09.2026 với @hono/zod-openapi đáng chép lại nguyên vẹn:
- Đo thấy
1.6.3đỏ →ignore: ["1.6.3"]. Dependabot đề xuất 1.6.2 → cũng đỏ. - Kết luận "cả dòng 1.6" →
ignore: ["1.6.x"]. Dependabot đề xuất 1.5.3 → cũng đỏ. - Thật ra chỗ đổi kiểu nằm ở 1.5.3, và bản cuối cùng còn lành là 1.5.2 →
ignore: [">1.5.2"].
Sai lầm ở bước 1 và 2 giống nhau: chặn đúng những bản MÌNH ĐÃ ĐO, rồi suy ra phần còn lại an toàn. Bước 2 còn suy sai một lần nữa — thấy 1.5.2 lành thì tưởng cả 1.5.x lành. Dependabot sẽ tìm ra đúng cái bản mình chưa đo, mỗi tuần một lần, cho tới khi chặn đúng ranh giới.
Và nói cho rõ — @hono/zod-openapi không hỏng: nó đổi kiểu handler có chủ đích ở 1.5.3. Muốn lên bất kỳ bản nào cao hơn thì phải sửa 8 module route của workers/api cho khớp, và đó là một việc CODE có commit và có test, không phải một lượt bấm gộp. Lúc làm việc ấy thì gỡ dòng ignore trong .github/dependabot.yml.
@types/react và @types/react-dom khai ở package.json gốc chính vì lớp thứ nhất: chúng không được dùng trực tiếp ở gốc, chúng nằm đó để cả monorepo chỉ có MỘT bản.
8c. Lỗ hổng phụ thuộc còn tồn (rà 22.09.2026)
npm audit còn 3 lỗ hổng và cả ba chưa có bản vá thượng nguồn — ghi ở đây để lần rà sau không phải điều tra lại từ đầu:
| Gói | Mức | Vì sao chưa vá được |
|---|---|---|
vite (lồng trong vitepress) | cao | vitepress 1.6.4 đã là bản mới nhất và nó ghim một bản vite cũ. Không có đường nâng. |
esbuild (lồng trong vite ấy) | vừa | cùng lý do |
vitepress | vừa | cùng lý do |
Cả ba đều là lỗ của DEV SERVER: đọc phản hồi từ vite dev, path traversal trong .map của optimized deps, server.fs.deny bị lách trên Windows. Bốn site VitePress (docs · pearl · compass · playbooks) build tĩnh rồi deploy làm asset — dev server không chạy trên production, nên bề mặt tấn công nằm ở máy người phát triển, không ở người dùng.
js-yaml (mức cao) đã vá ngày 22.09.2026 bằng npm audit fix: ba dòng trong package-lock.json, không đụng semver trong package.json.
Rà lại khi vitepress ra bản mới. npm audit KHÔNG nằm trong CI, cùng lý do với check:ui-latest: nó hỏi registry, mà một cổng phụ thuộc mạng đứng chắn mọi push thì đến ngày mạng nấc là mọi phiên đứng.
9. Lệnh hay dùng
npm run typecheck # toàn monorepo
npm run lint # eslint — chỉ rule bắt lỗi thật, không bắt phong cách
npm test # unit + behavior tests
npm run check:docs # QG-001 — frontmatter, trace, link gãy
npm run gen:reference # sinh lại data-dictionary / api / events
npm run contract:diff # so hợp đồng /v1 với snapshot
npm run verify:bindings # binding có kiểu, có tài liệu, có thật
npm run migrate:dryrun # áp toàn bộ migration lên D1 local rỗng10. Bắt buộc khi thêm cấu hình mới
- Thêm binding/var vào
wrangler.jsonc. - Khai kiểu trong
workers/api/src/env.ts(lớp A sẽ bắt nếu quên). - Thêm dòng vào §1 hoặc §2 của trang này kèm tên tài nguyên trong dấu backtick (lớp B đọc đúng dấu backtick).
- Tạo tài nguyên thật trên Cloudflare (lớp C sẽ bắt nếu quên).
- Là secret thì không đưa vào
vars— dùngwrangler secret put, và thêm dòng vào §5.