Summary
Setting LUMIBASE_REQUIRE_SETUP_TOKEN=true makes an instance impossible to set up: /setup/complete demands a token, but the CMS never prints one. There is no way out except turning the flag off and recreating the container.
The mint-and-print helper exists and is unit-tested. It is simply never called at startup.
Found while implementing #332 (PR #468): the starter initially enabled this flag believing it was the safer choice.
Reproduce
docker run -e LUMIBASE_REQUIRE_SETUP_TOKEN=true -e DATABASE_URL=... \
ghcr.io/khuepm/lumibase-cms
docker logs <container> | grep SETUP_TOKEN
# (nothing)
curl -s -H 'x-lumi-site: __default__' http://localhost:1989/api/v1/setup/state
# {"state":"uninitialized","requiresSetupToken":true}
curl -s -X POST -H 'content-type: application/json' -H 'x-lumi-site: __default__' \
-d '{"account":{...},"adminPath":"admin-a7f3c1"}' \
http://localhost:1989/api/v1/setup/complete
# {"errors":[{"code":"SETUP_TOKEN_REQUIRED"}]}
A closed loop: requiresSetupToken: true, complete refused, and no way to obtain the token.
Cause
printSetupTokenIfRequired (apps/cms/src/modules/setup/setup-token.ts:148) does the right thing: generates a token, stores its sha256 in system_state.setup_token_hash, prints one [lumibase-cms] SETUP_TOKEN=<token> line (:199), and is idempotent within a process.
But grepping the repo returns three hits, all inside that same file — two doc-comment mentions and the declaration:
$ git grep -n "printSetupTokenIfRequired" -- '*.ts' | grep -v __tests__
apps/cms/src/modules/setup/setup-token.ts:16: * - {@link printSetupTokenIfRequired} encapsulates the startup-side
apps/cms/src/modules/setup/setup-token.ts:115: * if `printSetupTokenIfRequired` is called multiple times (e.g. test
apps/cms/src/modules/setup/setup-token.ts:148:export async function printSetupTokenIfRequired(
Neither serve.ts nor index.ts calls it. The reading side — verifySetupToken, the requiresSetupToken branch of /setup/state, the SETUP_TOKEN_REQUIRED error — is fully wired. So the system checks a token that nothing produces.
How it slipped through
The helper's own doc comment cites Req 2.6, and the requirement is explicit (.kiro/specs/admin-setup-wizard/requirements.md:63):
WHERE biến môi trường LUMIBASE_REQUIRE_SETUP_TOKEN=true được set, THE CMS SHALL sinh Setup_Token ngẫu nhiên … lúc startup khi System_State là uninitialized và in token vào stdout đúng một lần.
(Where LUMIBASE_REQUIRE_SETUP_TOKEN=true is set, the CMS shall generate a random Setup_Token … at startup while System_State is uninitialized, and print it to stdout exactly once.)
The unit tests call the helper directly, so they stay green; nothing asserts that startup calls it. This is the classic gap between "the function is correct" and "the function is wired in".
Impact
- The flag is a trap: an operator enables it to be safer and locks themselves out instead.
- The broken state only shows up after the container is running and someone tries to set up.
- In practice an exposed instance therefore cannot be protected the way the docs describe: the only way to complete setup is to leave the flag off, which means whoever reaches the port first claims the admin account.
Suggested fix (not implemented)
- Call
printSetupTokenIfRequired from apps/cms/src/serve.ts during startup (after the DB is available, before serving requests).
- Decide the Cloudflare story: Workers has no "startup" in this sense, so either mint lazily on the first
/setup/* touch, or document the flag as Node/Docker-only.
- A regression test should go through the startup path rather than calling the helper directly — that is exactly where this slipped.
Verified on
main @ 6a20441a, and on the published image ghcr.io/khuepm/lumibase-cms@sha256:3f125caa… (revision 683a0270).
Related: the #332 starter deliberately does not enable this flag and says why in its README; revisit that once this closes.
🇻🇳 Tóm tắt tiếng Việt
Vấn đề. Đặt LUMIBASE_REQUIRE_SETUP_TOKEN=true làm instance không thể setup được nữa: /setup/complete đòi token, nhưng CMS không bao giờ in ra token nào. Không có đường phục hồi ngoài tắt cờ rồi tạo lại container.
Nguyên nhân. printSetupTokenIfRequired (setup-token.ts:148) làm đúng việc — sinh token, lưu hash, in một dòng [lumibase-cms] SETUP_TOKEN=<token> (:199). Nhưng grep toàn repo chỉ ra 3 kết quả, đều trong chính file đó: hai dòng doc comment và một dòng khai báo. serve.ts và index.ts đều không gọi. Trong khi phía đọc token (verifySetupToken, nhánh requiresSetupToken, mã lỗi SETUP_TOKEN_REQUIRED) đã nối đầy đủ — nên hệ thống kiểm tra một token mà không có gì sinh ra nó.
Vì sao lọt. Requirement ghi tường minh (admin-setup-wizard/requirements.md:63): sinh token lúc startup và in đúng một lần. Unit test gọi thẳng hàm nên vẫn xanh; không có test nào khẳng định startup gọi nó — khoảng trống điển hình giữa "hàm đúng" và "hàm được nối vào".
Hệ quả. Cờ này là bẫy: bật để an toàn hơn thì tự khoá mình ra ngoài. Thực tế khiến một instance expose không thể được bảo vệ theo cách tài liệu mô tả — muốn setup được thì phải để cờ tắt, tức ai tới cổng trước thì chiếm tài khoản admin.
Hướng sửa (gợi ý): (1) gọi printSetupTokenIfRequired trong serve.ts lúc khởi động; (2) chốt cách xử lý cho Cloudflare (Workers không có "startup" — hoặc mint lazy ở lần chạm /setup/* đầu, hoặc ghi rõ cờ chỉ dành cho Node/Docker); (3) test hồi quy phải đi qua đường khởi động, không gọi thẳng hàm.
Kiểm chứng trên main @ 6a20441a và image đã phát hành sha256:3f125caa….
Summary
Setting
LUMIBASE_REQUIRE_SETUP_TOKEN=truemakes an instance impossible to set up:/setup/completedemands a token, but the CMS never prints one. There is no way out except turning the flag off and recreating the container.The mint-and-print helper exists and is unit-tested. It is simply never called at startup.
Found while implementing #332 (PR #468): the starter initially enabled this flag believing it was the safer choice.
Reproduce
A closed loop:
requiresSetupToken: true,completerefused, and no way to obtain the token.Cause
printSetupTokenIfRequired(apps/cms/src/modules/setup/setup-token.ts:148) does the right thing: generates a token, stores itssha256insystem_state.setup_token_hash, prints one[lumibase-cms] SETUP_TOKEN=<token>line (:199), and is idempotent within a process.But grepping the repo returns three hits, all inside that same file — two doc-comment mentions and the declaration:
Neither
serve.tsnorindex.tscalls it. The reading side —verifySetupToken, therequiresSetupTokenbranch of/setup/state, theSETUP_TOKEN_REQUIREDerror — is fully wired. So the system checks a token that nothing produces.How it slipped through
The helper's own doc comment cites Req 2.6, and the requirement is explicit (
.kiro/specs/admin-setup-wizard/requirements.md:63):The unit tests call the helper directly, so they stay green; nothing asserts that startup calls it. This is the classic gap between "the function is correct" and "the function is wired in".
Impact
Suggested fix (not implemented)
printSetupTokenIfRequiredfromapps/cms/src/serve.tsduring startup (after the DB is available, before serving requests)./setup/*touch, or document the flag as Node/Docker-only.Verified on
main@6a20441a, and on the published imageghcr.io/khuepm/lumibase-cms@sha256:3f125caa…(revision683a0270).Related: the #332 starter deliberately does not enable this flag and says why in its README; revisit that once this closes.
🇻🇳 Tóm tắt tiếng Việt
Vấn đề. Đặt
LUMIBASE_REQUIRE_SETUP_TOKEN=truelàm instance không thể setup được nữa:/setup/completeđòi token, nhưng CMS không bao giờ in ra token nào. Không có đường phục hồi ngoài tắt cờ rồi tạo lại container.Nguyên nhân.
printSetupTokenIfRequired(setup-token.ts:148) làm đúng việc — sinh token, lưu hash, in một dòng[lumibase-cms] SETUP_TOKEN=<token>(:199). Nhưng grep toàn repo chỉ ra 3 kết quả, đều trong chính file đó: hai dòng doc comment và một dòng khai báo.serve.tsvàindex.tsđều không gọi. Trong khi phía đọc token (verifySetupToken, nhánhrequiresSetupToken, mã lỗiSETUP_TOKEN_REQUIRED) đã nối đầy đủ — nên hệ thống kiểm tra một token mà không có gì sinh ra nó.Vì sao lọt. Requirement ghi tường minh (
admin-setup-wizard/requirements.md:63): sinh token lúc startup và in đúng một lần. Unit test gọi thẳng hàm nên vẫn xanh; không có test nào khẳng định startup gọi nó — khoảng trống điển hình giữa "hàm đúng" và "hàm được nối vào".Hệ quả. Cờ này là bẫy: bật để an toàn hơn thì tự khoá mình ra ngoài. Thực tế khiến một instance expose không thể được bảo vệ theo cách tài liệu mô tả — muốn setup được thì phải để cờ tắt, tức ai tới cổng trước thì chiếm tài khoản admin.
Hướng sửa (gợi ý): (1) gọi
printSetupTokenIfRequiredtrongserve.tslúc khởi động; (2) chốt cách xử lý cho Cloudflare (Workers không có "startup" — hoặc mint lazy ở lần chạm/setup/*đầu, hoặc ghi rõ cờ chỉ dành cho Node/Docker); (3) test hồi quy phải đi qua đường khởi động, không gọi thẳng hàm.Kiểm chứng trên
main@6a20441avà image đã phát hànhsha256:3f125caa….