Skip to content

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ớpKiểm gìChạy khi nào
A — KiểuMọ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ệuMọi binding/var có trong trang này (AS-03.4.5)luôn
C — ThậtHỏ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.

BindingLoạiTài nguyênDùng cho
DBD1nemo12-platform (f4275a70-…)Toàn bộ dữ liệu quan hệ — 76 bảng
CONFIGKVf08c2394…Cấu hình runtime đọc nhiều, ghi hiếm
CONTENTR2nemo12-contentMedia, tài sản nội dung (SDD-009), và snapshot context AI (ai-context/…, AS-10.3.4)
AIWorkers AItài nguyên cấp tài khoảnLLM — luôn qua Gateway nemo12
EVENTSQueue producernemo12-eventsDomain events
FOUNDRYServicenemo12-foundryXưở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_STAGEDurable Objectlớ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 deploy fail. Worker lên xanh, rồi request đầu tiên chạm c.env.DB mớ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:

VarGiá trị productionÝ nghĩa và hệ quả nếu sai
ENVIRONMENTproductionPhân biệt môi trường trong log và thông điệp lỗi
COOKIE_DOMAIN.nemo12.comDomain cookie phiên. Sai → đăng nhập ở learn. không nhận ở marlins.
ALLOWED_ORIGIN_SUFFIXnemo12.comCORS + 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_IDnemo12Gateway 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_IDff302e77…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_IDee728643-…Id app RealtimeKit nemo12-speak-rooms (SRC-1168). Công khai
RTK_WEBHOOK_PUBLIC_KEYkhoá PEMKhoá 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_PROVIDERrealtimekitNhà 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_CAP2Trầ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_ID856179425546-…Audience để verify id_token. Client ID là public theo thiết kế của OAuth, không phải secret
GOOGLE_MOBILE_CLIENT_IDSchưa đặtClient 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_IDcom.nemo12.ieltsaud 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_DOMAINdac2205.cloudflareaccess.comTeam 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_AUD261f5250…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_DOLPHIN5b5df250…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_FROMNEMO <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_TOdac2205@gmail.comBả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_TOdac2205@gmail.comNgườ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_TOdangtuyethong2324@gmail.comBCC 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_ENABLED1Thư 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_ENABLED1Thư 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_ENABLED1Thư 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_ON2026-10-10#4Ngà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_ON2026-10-11#1Ngà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_TOdac2207@gmail.comHộ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_ENABLED1Cô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_TOdac2205@gmail.comThư 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_MODEbcccc = 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_DAY3Trầ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_ENABLED1Cô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_ORIGINhttps://api.nemo12.comGố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.

BindingLoạiTài nguyênDùng cho
DBD1nemo12-platform (f4275a70-…)Sổ lượt sinh: content_runs, content_run_trials
CONTENTR2nemo12-contentBản THÔ của mỗi lượt gọi model (foundry/<run>/<model>.json)
AIWorkers AItài nguyên cấp tài khoảnMọi model, kể cả model bên thứ ba, luôn qua Gateway nemo12
LESSON_FORGEWorkflownemo12-lesson-forgeWorkflow sinh một Lesson và chấm bằng nhiều model
VarGiá trịÝ nghĩa
AI_GATEWAY_IDnemo12Cùng gateway với api để log và hạn mức nằm chung một chỗ
FOUNDRY_DAILY_USD_CAP5Trầ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_RUNS8Bao 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_MIN15Cù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_TARGET3Thử 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_BATCH10Trần cho một cú bấm "lấy việc" và cho mỗi lượt cron kéo
FOUNDRY_AUTO_DRAIN11 = 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).

BindingLoạiTài nguyênDùng cho
DBD1nemo12-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)
VarGiá trịÝ nghĩa
TOOL_PLANE_ENVIRONMENTproductionTầ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 / VarGiá trị productionÝ nghĩa và hệ quả nếu sai
ASSETSbinding tới ./distPhục vụ file tĩnh của app. run_worker_first: ["/auth/*"] để SPA fallback không nuốt mất /auth/access-session
API_ORIGINhttps://api.nemo12.comNơ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ứỞ đâuVì sao không phải var
POLICY_VERSION = 2026-08-15shared/privacy.tsPhiê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.tsHạ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.tsHạ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):

ScopeHạnCửa sổĐặt ở đâuĐếm theo
login205 phútPOST /v1/auth/googleIP
ai-generate301 giờPOST /v1/coral/generate-items, POST /v1/coral/blueprints/{id}/generateuser
crawl601 giờdành cho thu thập nguồn ngoài — chưa gắn route nàouser/IP
submit1201 phútdành cho gửi bài — chưa gắn route nàouser
privacy-write3010 phút3 route ghi của /v1/consents + /v1/privacy/deletion-requestsuser

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:

SecretNơi dùngQuyền cần
CLOUDFLARE_API_TOKENGitHub Actions: deploy, rollback, verify:bindings lớp CEdit Workers, D1, KV, R2, Queues
CLOUDFLARE_ACCOUNT_IDnhư 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)ConfigDomainKiểuBindingCron
nemo12-apiworkers/api/wrangler.jsoncapi.nemo12.comcode (src/index.ts)5 (§1)0 21 * * *
nemo12-webapps/web/wrangler.jsoncnemo12.com, www.nemo12.comassets (SPA)——
nemo12-learnapps/learn/wrangler.jsonclearn.nemo12.comassets (SPA)——
nemo12-marlinsapps/marlins/wrangler.jsoncmarlins.nemo12.comassets (SPA)——
nemo12-dolphinapps/mentors/wrangler.jsoncdolphin.nemo12.comassets (SPA)——
nemo12-adminapps/admin/wrangler.jsoncadmin.nemo12.comassets (SPA)——
nemo12-coralapps/coral/wrangler.jsonccoral.nemo12.comassets (SPA)——
nemo12-ieltsapps/ielts/wrangler.jsoncielts.nemo12.comassets (SPA)——
nemo12-docsapps/docs/wrangler.jsoncdocs.nemo12.comassets (VitePress)——
nemo12-pearlapps/pearl/wrangler.jsoncpearl.nemo12.comassets (VitePress)——

Hai điều dễ vấp, ghi thẳng ra:

  • Thư mục apps/mentors/ deploy ra worker tên nemo12-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ệnh wrangler … --name phải dùng nemo12-dolphin.
  • data.nemo12.com xuất hiện trong tài liệu cũ như một surface nội bộ nhưng không có wrangler.jsonc nà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 ​

WorkflowKích hoạtViệc
.github/workflows/ci.ymlmọi push, mọi PR, dispatchCổ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.ymlworkflow_dispatch, chọn một appDeploy 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.ymlworkflow_dispatch thủ côngQuay ngược một worker về version trước — xem SDD-006 §17
.github/workflows/seed-data.ymlworkflow_dispatch, một file scripts/*.sqlNạp dữ liệu vào D1 production
.github/workflows/check-production.yml23: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ổngLệnhChạy khi
1typechecknpm run typecheck (cả monorepo)code
2lintnpm run lintcode
3testnpm test của workspace bị đụngcode
4smoke giao diệnnpm run test:e2e trong apps/learn (AS-05.3.5)e2e
5docs integritynpm run check:docs — 11 cổng con (QG-001)docs
6cổng kiểm codenpm 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
7sổ chung không bị ghi đèscripts/check-no-clobber.mjs (SRC-457)docs
8reference khớp sourcescripts/gen-reference.mjs --check (AS-01.4.1 🔴)reference
9hợp đồng /v1npm run contract:diff (AS-03.2.4 🔴)api
10số migration liên tụcscripts/check-migration-numbers.mjs (AS-04.1.1 🔴)migrations
11migration dry-run từ D1 rỗngnpm run migrate:dryrun (AS-04.1.4 🔴)migrations
12binding tồn tại thậtnpm 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 logNghĩa làCách chữa
Two different types with this name exist, but they are unrelatedHai 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ốcKhai 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/apiMột gói trong mạch Hono/zod đổi kiểuCô 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:

  1. Đo thấy 1.6.3 đỏ → ignore: ["1.6.3"]. Dependabot đề xuất 1.6.2 → cũng đỏ.
  2. Kết luận "cả dòng 1.6" → ignore: ["1.6.x"]. Dependabot đề xuất 1.5.3 → cũng đỏ.
  3. 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óiMứcVì sao chưa vá được
vite (lồng trong vitepress)caovitepress 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ừacùng lý do
vitepressvừacù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 ​

bash
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ỗng

10. Bắt buộc khi thêm cấu hình mới ​

  1. Thêm binding/var vào wrangler.jsonc.
  2. Khai kiểu trong workers/api/src/env.ts (lớp A sẽ bắt nếu quên).
  3. 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).
  4. Tạo tài nguyên thật trên Cloudflare (lớp C sẽ bắt nếu quên).
  5. Là secret thì không đưa vào vars — dùng wrangler secret put, và thêm dòng vào §5.

Trace ​

  • REQ-PLT-01/02 (Cloudflare-first), REQ-SEC-06 (CORS/CSRF), REQ-NFR-*.
  • Rủi ro: RISK-022 (cấm probe-fallback AI), RISK-026 (binding verify), RISK-025 (observability).
  • Thiết kế: SDD-001, SDD-006, SDD-009.
  • Trang anh em: Queues, Schedules, AI Registry.
  • Kiểm chứng: QG-008, QG-009, QG-010.