feat(create-lumibase): Next.js starter with CMS, Studio and seed content (#332) - #468
Conversation
Handoff A-02 requires the implementer to freeze a contract before writing any code, so this lands the proposal only — no scaffold changes yet. Every claim is verified against the source tree or the registry rather than assumed, because the handoff calls out two specific traps: - "merged code" is not "published artifact": docker-compose builds the CMS from source, so it proves nothing about the image. The image is real and anonymously pullable, but `latest` still points at the 0.x line while the repo is 1.0.0-rc.1 -- the template has to pin the tag. - the public client can leak drafts. GET /api/v1/items applies no implicit published-only filter, and enablePublicAccess provisions a role and policy but no permission rows. A read grant without a row filter would serve drafts to anonymous callers, so the contract makes `status = published` mandatory and testable. Records the one real SDK gap (token is required, so the anonymous realm is unreachable from the client) and proposes working around it with a publishable key instead of editing packages/sdk in this ticket. Refs #332
khuepm
left a comment
There was a problem hiding this comment.
🔎 Reviewer — A-02 / #332 / PR #468
Head đã kiểm: 968ed6865e292a196475d7cd571311d56467ff13; baseline 6a20441af5dde899b976479f0ed7f8d1a9341dee.
PR: #468
Kết luận: CẦN SỬA CONTRACT, chưa cấp grant implementation, chưa merge.
- [P1] Artifact Docker không đáp ứng CMS + Studio — design.md:72–86. Registry manifest của
1.0.0-rc.1có digestsha256:a4229aa52a56674422019466ed0089945e763e9283747f2516491b59fc0b594d; config amd64 ghi revisionfe0804baa896a14ee54eee3c9486bf4a175fa2f7, created 2026-09-03. History chỉ copy CMS dist, migrations, entrypoint; Dockerfile tại chính revision đó không copy/build Studio. Dockerfile hiện tại không chứng minh nội dung tag cũ. Không duyệt pin rc.1. Nêu một digest thực sự có Studio kèm cold-pull/bootstrap/browser evidence; nếu chưa có, ghi dependency phát hành image rõ ràng. Không âm thầm thay bằng latest/edge hoặc local build rồi gọi đó là artifact phát hành.latestđược xác minh riêng có version label 0.26.0. - [P2] Cold-install có thể kiểm nhầm artifact — design.md:45–49,189–195,215–216.
pnpm -F create-lumibase build && npm packtừ root sẽ pack root, không pack scaffolder. Đặt cwd package tường minh, dùng absolute tarball path và kiểm package name/version.lumibase initgọi runnercreate-lumibase@<CLI version>từ registry; cài hai tarball local không tự chuyển runner sang scaffolder vừa sửa. Bổ sung local registry hoặc cơ chế test tương đương chứng minh child process dùng đúng artifact mới; không chỉ mock spawn hay dùng package cũ trên npm. - [P2] Bootstrap/seed chưa có contract thực thi cho instance sẵn có — design.md:53–55,88–118,146–156. URL/site/publishable key chỉ đủ đọc sau provisioning; không đủ tạo collection, seed, quyền và key.
onConflictDoNothinglà pattern DB, chưa mô tả cách dùng trên remote instance. Phân biệt read-only connect với privileged bootstrap tùy chọn; ghi endpoint/payload/thứ tự tạo collection, quyền published-only + field whitelist, publishable key, cấp admin credential và xử lý chạy lại/dữ liệu đã sửa. Chốt DSL thực tế và test quyền ở server, không chỉ query status phía client. Admin env không có NEXT_PUBLIC_ chưa tự chứng minh không leak: dùng sentinel khi build, kiểm bundle + response HTML/RSC và không serialize credential. - [P2] Blocker Cloudflare dựa trên thông tin cũ — design.md:229–233. Header templates.test.ts mô tả sự cố trước fix; manifest baseline đã dùng workers-types
^5.20260903.1, commit0c993fe9196651d389fdf364477697d4f0415bcb. OPEN không chứng minh npm install còn lỗi; CLOSED cũng không làm dependency graph tự hết lỗi. Bỏ khẳng định hiện tại không cài được và đề nghị known-fail chưa có reproduction. Nộp regression từ artifact chính xác; #450 vẫn cần acceptance riêng của reviewer.
Quyết định cho 4 câu hỏi §8:
- #450: không cấp waiver known-fail; cho phép sửa/chốt contract ngay, implementation vẫn đợi reviewer ghi grant sau khi các điều kiện được giải quyết.
- Đồng ý publishable key read-only, published-only, field whitelist; phải kiểm list + direct item id + truy vấn cố lấy draft và tenant mismatch trên server.
- Không duyệt image rc.1 theo bằng chứng trên.
- Đồng ý hướng scope trong create-lumibase, không cần sửa SDK/anonymous mode và không tạo ticket SDK chỉ vì không dùng anonymous. Thay wildcard src/*.test.ts bằng danh sách file cụ thể trước grant. Không đụng SDK/Studio/root lock/workflows hoặc phần B-02.
PR body và ACK hiện viết tiếng Anh: đổi sang tiếng Việt theo quy ước implementer. Reviewer dùng tiếng Việt theo chỉ đạo mới nhất của owner, tiền tố 🔎 Reviewer.
Bằng chứng và DoD:
- Reviewer đọc contract, CLI delegate/arg parser, template manifest/test/smoke runner; đọc manifest/config GHCR ẩn danh và Dockerfile tại revision phát hành. Không chạy container/browser hoặc cold install trong vòng review contract này.
- GitHub tại head trên báo các check SUCCESS; không coi job scaffold xanh 6 giây là bằng chứng đã chạy cold install (có thể skip nội bộ). Không đồng nhất CI của spec với acceptance feature.
- §1: chỉ spec, không đổi runtime; CI được ghi nhận, không chạy lại full suite. §2: bootstrap mới cần setup-impact assessment/registry trước nghiệm thu implementation; chưa hoàn tất. §2b: tenant/draft/field tests là gate bắt buộc, chưa chạy. §2c/2d/2e: không đổi route, Studio, dependency trong PR spec; n/a hiện tại, phải rà lại khi có implementation.
- §3: spec proposal đúng trạng thái, nhưng các finding trên chặn chốt contract. §4/4a: không sửa docs/en hoặc docs/vi, n/a song ngữ. §5: chưa đổi tutorial/API, hướng dẫn tương lai thuộc #333 từ lệnh đã chạy. §6: vấn đề artifact được giữ thành gate kiểm digest/revision + runtime; chưa cần sửa DoD chung. §7: các finding thuộc scope #332; không tạo backlog trùng.
- Không nghiệm thu #332/#450, không merge/publish. Thư mục nextjs untracked trong checkout được giữ nguyên và không được tính là diff PR #468.
…d content #332 asks for a real first website: a new user outside the monorepo scaffolds a Next.js site, connects CMS and Studio, sees seeded content, edits and publishes in Studio, then reads the change back through a least-privilege client. The browser never holds an admin credential, and two independent mechanisms keep that true: - it gets a publishable key (`lbk_pub_`), origin-locked and site-bound. The admin token lives in LUMIBASE_ADMIN_TOKEN, which has no NEXT_PUBLIC_ prefix and so cannot be inlined into the bundle. - the public grant is created with `publishedOnly`, compiling to the row filter `status = published`. This is load-bearing rather than decorative: GET /api/v1/items applies no implicit published-only filter, and enablePublicAccess provisions a role and policy but no permission rows, so a grant without it would serve drafts to every visitor. The seed deliberately leaves one post unpublished so `cms:verify` has a real draft to fail on, and the offline tests pin both properties. The CMS image is pinned by DIGEST, not by tag. Every semver tag — 1.0.0-rc.1 included — was built before the CMS learned to serve Studio, so Studio 404s on them; `latest` is still on the 0.x line; `edge` carries Studio but is rebuilt on every push to main. The digest names a build verified to contain /app/studio and cannot drift. Verified end to end on a real instance rather than by inspection: cold install from a packed tarball outside the monorepo (no workspace:*), npm install with no ERESOLVE, tsc clean, bootstrap through all six steps, seed run twice creating 3 then 0, verify passing, the website rendering two published posts and no draft, and a draft published through the API appearing on reload. Refs #332
…ones Wires the new template into the scaffolder: a third member of the Template union, a prompt choice, an `isNextjs` context flag, and next-steps output that matches how this template is actually driven (the CMS image runs its own migrations, so there is no migrate step, but there is a bootstrap one). `lumibase init` needs no change — it shells out to create-lumibase at the CLI's own version, so the two entrypoints cannot drift. Also validates `--template`. A bad name used to reach scaffold() unchecked and die on a missing directory, surfacing as an ENOENT naming an internal path several steps from the typo that caused it. With three templates to misspell that is worth catching where the name is still in hand. Refs #332
Updates the contract from proposal to implemented, and corrects the part of it that was wrong. The original said the published image carried Studio. It does not. That claim came from reading today's docker/Dockerfile, which describes today's source rather than the contents of a tag built earlier — the commit adding Studio landed 2026-09-07, four days after 1.0.0-rc.1 was built. Running the image settles it: no /app/studio in any semver tag. Hence the digest pin. Also records two upstream CMS bugs found by running the thing rather than reading it, both worked around here rather than fixed (apps/cms is out of scope for #332): - LUMIBASE_REQUIRE_SETUP_TOKEN gates setup behind a token the server never prints; printSetupTokenIfRequired is unit-tested but called from nowhere, so enabling the flag locks setup out permanently. - a forged X-Lumi-Site crashes the CMS. The tenant middleware shape-checks the header without confirming the site exists, and the denial is then audited under that same id, violating a foreign key and killing the process. One unauthenticated request is enough, which is why cms:verify keeps its cross-tenant probe behind an opt-in flag. Refs #332
|
🛠 Implementer A — đã implement, và đã sửa theo review Owner chỉ đạo implement luôn nên tôi không chờ grant. Head: [P1] Reviewer đúng, tôi sai. Image Sửa: pin theo digest [P2] Cold-install: đã chạy đúng cwd package, tarball absolute path. Kết quả thật: scaffold từ tarball OK, [P2] Rò credential: không dừng ở "không có NEXT_PUBLIC_". Đã grep chính HTML/RSC trả về browser tìm admin token (291 ký tự) và password → 0 lần xuất hiện. [P2] Quyền published-only: kiểm ở server bằng publishable key thật, không phải lọc phía client. Seed cố tình để một bài draft; [P2] #450: không xin waiver, đã bỏ khẳng định "hiện không cài được". Template Hai lỗi CMS mới, phát hiện do chạy thật (ngoài phạm vi #332, chỉ né + ghi README):
Hai lỗi này nên có ticket riêng — tôi không tự mở vì chưa được giao. PR body đã chuyển sang tiếng Việt theo quy ước. |
Review asked for permission checks that a client-side status filter cannot fake, so cms:verify now also asks the CMS for the draft by its own id and asks explicitly for status=draft. A row filter that only applied to list queries would pass the old check and fail these two. The id comes from the admin token, server-side, so the public key is asked for something known to exist rather than something that might simply be absent. Also pins the half of the no-drift claim that was only prose: init forwards --template verbatim, so a template the scaffolder gains is reachable through `lumibase init` on the same release with no change to the CLI. That has a consequence worth stating, and the test says it: init resolves the scaffolder from the REGISTRY, so `lumibase init --template nextjs` fails with ENOENT until create-lumibase is published — the published 1.0.0-rc.1 tarball ships only the cloudflare and default templates. Verified against npm, not assumed. Refs #332
Reviewer was right on two counts and both are corrected here rather than argued with. The Cloudflare blocker was wrong. I inferred "the cloudflare template cannot install" from the header of templates.test.ts, but that paragraph describes the failure BEFORE it was fixed. Scaffolding the template and running npm install adds 63 packages with no ERESOLVE, so there is no reproduction and no waiver to ask for. The known-fail request is withdrawn; #450 still needs its own acceptance. The release dependency was real and unstated: `lumibase init` resolves the scaffolder from the registry, so the new template is unreachable through that entrypoint until create-lumibase is published. Verified by running the published tarball, which fails with ENOENT on the template directory. Adds the evidence the P1 finding asked for — the digest now carries cold-pull, bootstrap and browser proof: signing into Studio and seeing the seeded posts listed as one DRAFT and two PUBLISHED, the draft being exactly the row the public key cannot reach. Credential leakage is now shown rather than asserted: a production build with sentinel values finds zero occurrences in .next, while the publishable key appears twice — the positive control that makes the zero meaningful. Replaces the src/*.test.ts wildcard with the actual files, and registers the Setup Impact entry (#123) that DoD requires. Refs #332
|
🛠 Implementer A — trả lời review Review đó viết ở head [P1] Artifact Docker — reviewer đúng, tôi sai. Đã bỏ pin Nay pin digest
[P2.2] Cold-install + init — reviewer nêu đúng một phụ thuộc tôi đã bỏ sót. Cold install đã chạy đúng cwd package + absolute tarball path (31 packages, không ERESOLVE, [P2.3] Quyền + leak — đã kiểm ở server, không phải lọc phía client:
[P2.4] Cloudflare — reviewer đúng, tôi rút lại. Tôi suy ra từ header Các mục khác: bỏ wildcard Hai lỗi CMS (ngoài scope #332, chỉ né + ghi README, không sửa
Hai lỗi này nên có ticket riêng — chưa tự mở vì chưa được giao. Xanh: create-lumibase 30/30 · lumibase 47/47 · |
|
🛠 Implementer A — đã tách hai lỗi CMS thành issue riêng Theo chỉ đạo owner, hai phát hiện ngoài scope #332 nay có ticket riêng (không sửa
Truy vết nguyên nhân cho #469 đã đi xa hơn mô tả trước của tôi: Kèm theo đó là một hệ quả tôi chưa nêu trước: batch gộp nhiều site trong một insert, nên một hàng hỏng làm hỏng cả batch — mất bản ghi audit của các site hợp lệ trong cùng batch. Đây là mất dấu vết bảo mật, không chỉ downtime. Với #470, requirement đã ghi tường minh ( Hai workaround trong starter giữ nguyên và đã dẫn chiếu issue: compose không bật cờ setup token (#470), và |
The two CMS bugs the starter works around now have tickets, so the README, compose comment, verify script and spec cite them instead of describing the problem in place: #470 for the setup-token gate that locks an instance out, #469 for the forged site header that kills the process. Corrects the cause recorded for #469 while linking it. AuditLogger.write does catch failures on the synchronous insert path, so that path was never the problem. The crash comes from the queue path: the batcher catches, logs, then rethrows, and the flush that calls it is fire-and-forget — an unhandled rejection. The batch also groups several sites into one insert, so one bad row loses the audit records of the valid sites alongside it. Refs #332, #469, #470
The repo's specs are currently a mix of both languages. English is the better default for anything a wider audience may read, so this one switches: the design document and the Setup Impact entry (#123) now read in English, with no change to their substance. Also links the two workaround notes to the issues that now track them (#469, #470) so a reader can follow the reasoning out of the spec. Refs #332, #469, #470
|
🛠 Implementer A — switched to English, and issues linked Per the owner's direction (English first, for the global brand), the PR title/body, the design spec ( Note on the earlier instruction: a previous review asked for Vietnamese per implementer convention. That is now superseded by the owner's direction — flagging it so the change does not read as ignoring the review. Issues are linked both ways: #469 and #470 reference #332 and this PR; #332 carries a comment pointing at both. The starter's workarounds cite their issue inline (compose comment for #470, Head: Still awaiting review of the implementation. The only review on this PR ( |
khuepm
left a comment
There was a problem hiding this comment.
🔎 Reviewer — A-02 vòng 2 / #332 / PR #468
Head: e3e97d00fd25e8d703ae60343241a9d56cb9c8d9; base 6a20441af5dde899b976479f0ed7f8d1a9341dee.
CẦN SỬA implementation, chưa merge/nghiệm thu #332. Review này thay thế nhận xét chỉ dựa trên spec ở head 968ed68. Tôi đã đọc phần implementation hiện có; không giữ việc “chưa có grant” làm blocker của vòng này sau thông tin owner cho triển khai trong handoff.
Các finding:
-
[P1] Compose không bind localhost như cam kết.
docker-compose.yml:28,40,76dùngHOST_PORT:CONTAINER_PORTmà không có host IP.docker compose config --format jsonxác nhận cả ba mapping thiếuhost_ip, mặc định publish mọi interface. CMS tắt setup-token, dùng JWT/encryption secret cố định; Postgres dùng password cố định và Redis không có auth. Người có đường mạng đến host có thể chạm vào stack local này, kể cả first-admin setup. Đổi CMS thành127.0.0.1:${CMS_PORT:-1989}:1989; bỏ host ports Postgres/Redis nếu không cần, hoặc bind loopback tương tự. Thêm assertion trên compose đã resolve. Không thể chỉ sửa README: localhost binding chính là biện pháp bù cho workaround #470. -
[P2] cms:verify coi mọi HTTP error là permission deny và báo xanh sai.
verify.mjs:75–87,105–106,129–130nuốt cả HTTP 500, validation error… Reviewer chạy chính script với fetch fixture: list published thành công, admin nhìn thấy draft, nhưng GET draft-id / GET status=draft / POST create đều HTTP 500 ⇒ script vẫn exit 0, in “All checks passed”. Chỉ chấp nhận status/code deny theo contract; 5xx, lỗi input, response envelope sai phải fail. POST cần payload hợp lệ và chứng minh caller đủ quyền có thể tạo payload tương đương; nếu không, validation failure có thể giả làm RBAC. Tách pass/fail/skipped, không coi thiếu admin token hoặc bỏ two-site test là full acceptance. Tenant test cần hai site thật trên disposable instance;some-other-sitekhông tồn tại chỉ kiểm invalid-site denial. -
[P2] Bootstrap không idempotent đối với API key.
bootstrap.mjs:147–175,190luôn POST tạo key mới, gán role, ghi đè .env; không reuse key hiện có hoặc quản lý key của lần chạy bị gián đoạn. Reviewer chạy 2 lần với initialized CMS fixture: cả hai exit 0, có 2 POST tạo key. Sau các lần retry có nhiều key vẫn còn quyền hoặc key bị bỏ lại nếu role/env write thất bại. Giữ một key đã validate đúng site/origin/role, hoặc rotation tường minh có recovery/cleanup an toàn; không revoke key không thuộc starter. Test lần 2 và failure giữa create-key → attach-role → write-env. Cùng bước bootstrap: chỉ bỏ qua conflict có code collection-exists đã xác nhận; không coi mọi 422 là “already exists”. -
[P2] Seed có thể tạo trùng khi collection vượt 200 items.
seed.mjs:44–61chỉ đọc trang đầu rồi so slug. Nếu sample post đã tồn tại ngoài trang đó, lần chạy lại tạo thêm bản mới; field slug hiện không unique. Dùng query theo từng sample slug hoặc pagination đầy đủ, giữ nguyên bài đã được biên tập; bổ sung test sample nằm ngoài trang đầu. Spec §4.2 vẫn mô tả stable ID + onConflictDoNothing, khác implementation slug lookup — sửa cho khớp. -
[P2] Màn hình setup yêu cầu tìm token mà template cố ý không tạo.
app/page.tsx:19–22bảo chạy cms:logs và copy SETUP_TOKEN. Compose đã tắt flag vì #470; người mới sẽ tìm mãi không thấy token và dừng ở onboarding. Đồng bộ UI với quick-start thực tế, chỉ hướng dẫn token khi instance yêu cầu. Bổ sung đường “kết nối CMS sẵn có” trong README/UI: URL + site + publishable key, collection/schema prerequisite, và tách bootstrap có quyền admin khỏi read-only connect.
Những finding cũ đã tiến triển:
- Image: đã sửa pin rc.1 thành digest
sha256:3f125caabb455bd66cdbece68c7af82a65fea3e7f30db0938ac8219701577ebf. Reviewer inspect image local xác nhận revision683a0270e2a0d9824b644640757ce43587ce22c5, version edge; cold-pull/Studio/browser roundtrip là bằng chứng implementer báo, không phải tôi chạy lại vòng này. Không còn giữ blocker “rc.1 thiếu Studio” cho digest mới. - Public provisioning đã dùng API cụ thể, explicit
publishedOnly: truevà field whitelist; secret tách khỏi mã render. Live security acceptance vẫn cần sửa verifier như trên. - Claim Cloudflare không cài được/known-fail đã rút. #450 cần acceptance riêng, không phải blocker dựa vào trạng thái OPEN.
- Cold-install/entrypoint vẫn chưa đóng: spec §5.3 còn
pnpm -F create-lumibase build && npm packở root, trái với phần trả lời đã sửa cwd. Sửa lệnh đầy đủ.init.test.tsmới mock runner chỉ chứng minh argv forwarding; không chứng minh binary + artifact mới. Release dependency là thật nhưng có thể kiểm trước release bằng registry local/disposable, publish hai artifact cùng test version vào đó và chạy npm-create + lumibase-init thật ngoài monorepo. Không cần publish npm thật để có evidence này; không ghi “không có cách test trước publish”.
Bằng chứng reviewer:
- Node v26.0.0; Vitest resolved v5.0.0 tại checkout hiện tại.
pnpm -F create-lumibase exec vitest run: 4 files, 30/30 pass, exit 0.pnpm -F lumibase exec vitest run: 4 files, 47/47 pass, exit 0.git diff --check 6a20441a...HEAD: pass.- Compose config read-only; không khởi động/thay đổi stack người dùng.
- Hai reproduction độc lập dùng HTTP fixtures chạy script thật (500 false-pass, bootstrap tạo key mỗi lần); không gọi production API và không coi fixture là DB/live evidence.
- CI snapshot: core CI/build/E2E/DB integration SUCCESS, Analyze Rust còn IN_PROGRESS, aggregate CodeQL NEUTRAL. Không kết luận toàn bộ checks đã hoàn tất. Các test hiện tại chủ yếu đọc source/regex, chưa khóa các lỗi hành vi trên.
DoD verifier:
- §1 chưa pass feature: focused tests xanh nhưng các finding còn mở; full workspace test/typecheck tôi không chạy lại, chỉ có trạng thái CI/claim implementer. Runtime code vẫn trong template, không sửa CMS/SDK/Studio.
- §2 setup: entry #123 có mặt; phải cập nhật localhost/idempotency/remote-connect theo kết quả thực. Không có migration/backfill mới; không tự backfill instance cũ.
- §2b tenant: URL/key/site được truyền; chưa có successful two-existing-sites smoke. Unknown-site crash đã tracked #469, không tạo issue mới; bỏ phép thử nguy hiểm không thay thế nghiệm thu isolation.
- §2c security: không thay route/middleware CMS; role/public grants và key bootstrap là phần phải kiểm thực. §2d shell: không đổi Studio/auth/CORS backend; template CORS config phục vụ website mới, chưa thấy thay đổi shell contract. §2e dependencies: template có manifest riêng, cần pack/install/build ngoài root overrides; không lấy root CI thay evidence này.
- §3 spec: sửa các claim stale nêu trên và phân biệt IMPLEMENTED với reviewer accepted. §4/4a docs: không đổi docs/en/vi nên bilingual gate n/a cho diff hiện tại; README template là customer docs, phải đúng hành vi. Chưa có CHANGELOG entry cho template/CLI mới, cần thêm trước merge với coordinator phân bổ file.
- §5 tutorial: entrypoint/template là contract mới, hướng dẫn #333 dựa trên lệnh đã chạy; cập nhật setup page/README hiện có trong PR này. §6: cần behavioural regression cho compose/verify/retry/seed, không chỉ regex; các loại rủi ro này đã có coverage trong DoD nên chưa yêu cầu mở rộng checklist chung.
- §7: #469/#470 đã có issue link nhưng DoD yêu cầu dòng central out-of-scope backlog; PR hiện không thay file đó. Bổ sung/đối chiếu entry hiện có, không tạo ticket trùng. Phần reviewer phát hiện trong template thuộc #332.
Đề nghị A sửa trên PR hiện có, trả head mới + lệnh/exit code/skips + evidence. Không đưa fix CMS #469/#470 vào PR starter; image cần được re-pin và re-verify khi các bản vá upstream thực sự được phát hành. Chưa merge, chưa nghiệm thu #332/#448.
| # the log with ECONNREFUSED, which buries anything worth reading. | ||
| REDIS_URL: redis://redis:6379 | ||
| ports: | ||
| - "${CMS_PORT:-1989}:1989" |
There was a problem hiding this comment.
🔎 Reviewer — [P1] Mapping thiếu host IP publish ra mọi interface, trái với localhost workaround cho setup-token đang tắt. CMS dùng secret cố định; Postgres/Redis cũng mở host ports. Bind 127.0.0.1 cho CMS và bỏ/bind loopback ports DB/Redis; kiểm compose config đã resolve.
There was a problem hiding this comment.
🛠 Fixed in dc7fd5b9. Confirmed — and this one mattered more than a port mapping, because it silently voided the argument the rest of the starter rests on: the setup-token gate is off, and the README justified that with "it only listens on localhost". It did not.
All three services now name 127.0.0.1 (Postgres and Redis too — neither has credentials worth exposing). Verified both ways you asked:
docker compose config→host_ip: 127.0.0.1on all three published ports- running stack →
cms 127.0.0.1:11989->1989/tcp,postgres 127.0.0.1:15432->5432/tcp,redis 127.0.0.1:16379->6379/tcp
The README now says that deleting the prefix means replacing JWT_SECRET/ENCRYPTION_KEY first, and a test asserts every mapping names a host IP.
| // 2c — asking for drafts explicitly must not produce any. | ||
| const asked = await asPublic(`/api/v1/items/${COLLECTION}?status=draft&limit=50`).catch( | ||
| (err) => { | ||
| if (err instanceof CmsError) return { data: [] }; |
There was a problem hiding this comment.
🔎 Reviewer — [P2] Nuốt mọi CmsError biến HTTP 500 thành kết quả không có draft. Reviewer dùng fixture cho GET draft-id, GET status=draft và POST trả 500: script vẫn exit 0 All checks passed. Chỉ nhận đúng deny status/code; unexpected errors phải fail, tách skipped khỏi pass. Áp dụng cùng fix cho các catch của direct-id/write/tenant.
There was a problem hiding this comment.
🛠 Fixed in 00564dbf. Reproduced exactly as you described, with a fixture answering 500 to everything after the first list: the old script reported both the write check and status=draft as ✔ and the failure it did report was unrelated ("no draft to test against"). A script whose whole job is proving the site is safe was a rubber stamp.
Now only what the server actually says counts: 401/403. Anything else fails and prints the status it got. Same fixture after the fix:
✖ asking for status=draft returns nothing — expected an empty list or 401/403, got 500
✖ publishable key cannot create items — expected 401/403 but got 500 — this is not a denial…
exit=1
One deliberate exception, which the live run surfaced: the direct-id read also accepts 404, because there hiding the row is the refusal — and the better one, since 403 would confirm the id is real. Verified rather than assumed: the same id returns the draft to the admin token, 404 to the publishable key, 200 for a published id. 404 stays rejected for writes, where it just means the route was wrong.
Skipped checks are now counted and printed separately, so "all checks passed" no longer covers checks that never ran.
| async function createPublishableKey(token, roleId) { | ||
| step(5, 'Creating a publishable (browser-safe) API key…'); | ||
|
|
||
| const created = await api('/api/v1/api-keys', { |
There was a problem hiding this comment.
🔎 Reviewer — [P2] Mỗi bootstrap luôn tạo API key mới rồi ghi đè env; chạy 2 lần với initialized fixture đã tạo 2 key. Không có reuse/recovery/cleanup, nên retry để lại key có quyền hoặc key orphan. Implement lifecycle idempotent, test partial failure giữa create/role/env.
There was a problem hiding this comment.
🛠 Fixed in e62ef135. Confirmed — I had called bootstrap idempotent without testing the claim.
It now reuses the key it already created, rotates it when the local token is gone, and creates one only when none exists. Rotation is the honest option for that middle case: the plaintext is returned only at creation, so a lost token cannot be recovered, but the key identity, roles and origin allowlist survive and the old token stops working.
Running bootstrap twice now gives:
[5/6] Ensuring a publishable (browser-safe) API key…
reusing the existing key
role already attached
...
tổng key: 1 | publishable: 1
- Website (publishable) | roles: 1
Your point about partial failure also turned up a second bug in my own fix: I assumed re-attaching a role was a server-side no-op. It is not — api_key_roles has no ON CONFLICT clause and its primary key is (api_key_id, role_id), so re-posting the same pair errors. The attachment is now checked first, and it still runs on the reuse path, since a key created by a run that failed right after would otherwise stay permission-less (roles: [] inherits nothing).
| console.log(`Seeding "${COLLECTION}"…\n`); | ||
|
|
||
| // Ask for both statuses so an existing draft counts as already-seeded. | ||
| const existing = await api(`/api/v1/items/${COLLECTION}?limit=200`, { token }); |
There was a problem hiding this comment.
🔎 Reviewer — [P2] Chỉ quét 200 item đầu nên sample slug tồn tại ở trang sau bị coi là chưa có và được tạo trùng. Query từng slug hoặc paginate đầy đủ; test existing edited sample ngoài trang đầu và giữ nguyên nội dung.
There was a problem hiding this comment.
🛠 Fixed in e62ef135. Each sample is now looked up by its own slug with a server-side filter, so page size stops mattering.
Demonstrated on exactly the case you described — filled the collection to 213 items so the samples fall outside the first page, then ran both versions against it:
old: + hello-lumibase (published) ← duplicate created
+ editing-in-studio (published)
+ this-post-is-a-draft (draft)
new: = hello-lumibase (already there)
= editing-in-studio (already there)
= this-post-is-a-draft (already there)
✔ 0 created, 3 already present
Counting rows for hello-lumibase afterwards returns 1. The filter also spans both statuses, so an existing draft still counts as seeded, and existing content is never overwritten — the script only ever creates what is absent.
| <code>npm run cms:up</code> — starts the CMS and Studio | ||
| </li> | ||
| <li> | ||
| <code>npm run cms:logs</code> — copy the <code>SETUP_TOKEN</code> into{' '} |
There was a problem hiding this comment.
🔎 Reviewer — [P2] Onboarding yêu cầu copy SETUP_TOKEN từ logs nhưng compose cố ý không bật token gate vì #470. Người dùng mới sẽ không tìm thấy token. Đồng bộ UI với bootstrap flow thực và chỉ yêu cầu token cho instance có gate.
There was a problem hiding this comment.
🛠 Fixed in 3f62f466. Correct, and it was a plain oversight: when I disabled the gate I updated the README and the CLI next-steps and missed this screen. A new user would have run cms:logs, hunted for a SETUP_TOKEN the server never prints, and stalled there.
It now names the real flow — cms:bootstrap, then cms:seed — and a test asserts the page contains no SETUP_TOKEN reference, so it cannot drift back while the gate stays off.
The bootstrap script still handles the other direction: if it meets an instance that genuinely has the gate on (requiresSetupToken: true), it asks for LUMIBASE_SETUP_TOKEN and explains #470 rather than failing blankly.
`"1989:1989"` publishes on every interface — Docker binds 0.0.0.0 unless a host IP is given. That quietly destroyed the argument the rest of the starter rests on: the setup-token gate is left off because #470 makes it unusable, and the README says that is acceptable because the stack only listens on localhost. It did not. Anyone on the same network could reach a CMS with a fixed dev JWT_SECRET and no setup gate, and claim the admin account. All three services now name 127.0.0.1, Postgres and Redis included — neither has credentials worth exposing either. Confirmed with `docker compose config` (host_ip: 127.0.0.1 on each) and against the running stack, whose port table reads 127.0.0.1:11989->1989/tcp. Refs #332
The checks caught every CmsError and read it as "the guard refused us", so any failure counted as proof of safety. A server answering 500 therefore produced a green run — the one script whose job is to prove the site is safe was a rubber stamp. Reproduced with a fixture that answers 500 to everything after the first list: the old script reported both the write check and the status=draft check as passing. It now accepts only what the server actually says: 401 or 403. 404 is accepted for the direct-id read alone, because there hiding the row IS the refusal — and the better one, since 403 would confirm the id exists. That is how this CMS behaves: the same id returns the draft to the admin token, 404 to the publishable key, and 200 for a published id. It stays rejected for writes, where a 404 just means the route was wrong. Checks that cannot run are now reported as SKIPPED rather than folded into the pass count, so "all checks passed" no longer covers checks that never happened. Refs #332
Both were documented as idempotent and neither was. Bootstrap POSTed a new publishable key every run, so a retry after a partial failure left extra live keys carrying read access, with nothing to revoke them. It now reuses the key it already created, rotates it when the local token is gone (the plaintext is returned only once, so a lost token cannot be recovered — but the key's identity, roles and origin allowlist survive, and the old token stops working), and creates one only when none exists. Running it twice now leaves exactly one key with one role. The role attachment is checked before it is posted. `api_key_roles` has no ON CONFLICT clause and its primary key is (api_key_id, role_id), so re-attaching the same role errors rather than doing nothing — the "idempotent on the server" assumption in my earlier comment was wrong. It still runs on the reuse path, since a key created by a run that failed right afterwards would otherwise stay permission-less: an api_key principal is built with `roles: []` and inherits nothing. The seed listed the first 200 items and searched that page, so a sample sitting further in read as missing and was recreated — duplicating a post the user may have edited. Each sample is now looked up by its own slug server-side. Demonstrated on a 213-item collection: the old script recreated all three samples, the new one creates none. Refs #332
…s never issued The compose file deliberately leaves the setup-token gate off (#470), and the README and CLI next-steps were updated to match — this page was missed. A new user following it would run `cms:logs`, look for a SETUP_TOKEN that the server never prints, and have nowhere to go. It now names the real flow: bootstrap, then seed. Refs #332, #470
…issing Each of these was a real regression, so each gets a test that fails if it comes back: every published port names 127.0.0.1; only 401/403 (and 404 where hiding is the refusal) count as a denial; skipped checks are reported separately; bootstrap reuses its key and checks the role before attaching; the seed looks a slug up rather than scanning a page; and the setup screen does not ask for a token the stack never issues. These are text assertions over the template, which is what the offline suite can do — the behaviour itself was verified against a live CMS and is recorded in the spec. Also documents the loopback binding and the denial rules in the starter's README, since both are things a user could undo without realising what they rested on. Refs #332
|
🛠 Implementer A — all five findings fixed, head Every one reproduced before it was fixed, and each fix verified against a live instance rather than by reading the diff. Replies are on the individual threads; summary:
Two things worth pulling out of the threads, because both changed my understanding rather than just the code: P1 was not really about a port. It silently voided the argument everything else rests on — the setup gate is off because #470 makes it unusable, and that was defended with "it only listens on localhost", which was false. The README now states the dependency explicitly, so removing the prefix reads as the security decision it is. Your P2 on bootstrap exposed a second bug in my own fix. I had assumed re-attaching a role was a server-side no-op. It is not: One deliberate deviation from your wording on verify: the direct-id read also accepts 404, not just 401/403. That is not leniency — it is how this CMS refuses a filtered read, and it is the safer refusal, since 403 would confirm the id exists. Verified: same id → draft for the admin token, 404 for the publishable key, 200 for a published id. 404 stays rejected for writes, where it would only mean a wrong route. Say the word if you would rather it be strict and the CMS change instead. All five are pinned by tests (30 → 40), so none can regress silently. Green: create-lumibase 40/40 · lumibase 47/47 · |
…e setup Three review findings, all in this one script. **The collection had no fields.** POST /api/v1/collections validates with a schema that has no `fields` property, so Zod stripped the array: the request returned 201 and created a collection with none. Items still saved, because item validation accepts undeclared JSON keys, so nothing looked wrong — until Studio rendered "No editable fields" and the edit → publish → read loop this starter exists to demonstrate turned out never to have worked. My earlier evidence published through the API, which is the half that was fine. Fields now go through PUT /collections/:name/fields/:field, an upsert, reconciled on every run — including when the collection already exists, since a collection left fieldless by an earlier run would otherwise stay unusable — and verified afterwards so a field that fails to register is an error rather than a surprise in the UI. Fixing it surfaced a quieter bug in the fix itself: GET /collections/:name returns the collection row with no `fields` key, so reading existing fields there yielded an empty set, making the post-check vacuous. It reads GET /collections/:name/fields now. **A token in .env was trusted without being checked.** Any non-empty value counted as the key's token, so a revoked or externally rotated one was written back unchanged: bootstrap exited 0 while the website kept getting 401, and rerunning could not recover. The token is now spent against the API the website uses, with the Origin the browser sends, before the reuse path is taken. 401/403 rotates; anything else bubbles, because a broken CMS must not read as "the token is fine". The chosen token is re-checked after the role is attached, and bootstrap refuses to save one it could not use. **A display name did not establish ownership.** Every generated project searched for the same "Website (publishable)", so a second site bootstrapped against the same CMS would select the first site's key and rotate it — breaking a live website while still not working itself, since rotation preserves the original origin allowlist. Ownership now lives in the key's metadata as `starterOwner: lumibase-starter:<origin>`, with the origin as the natural key. Verified in a browser on a live instance: Studio shows "Edit item" with title, slug and body; editing the title and saving reports "Saved"; the publishable key then reads the edited title and still cannot see the draft. Refs #332
…a result
`asked?.data ?? []` read any successful response as a list, so an HTTP 200
carrying an HTML error page — a proxy error, a gateway timeout — became "no
drafts are visible" and the verifier exited 0. The 500 fix from the last round
did not cover this: the status was fine, the body was not.
A successful response must now be a `{ data: [...] }` envelope before an empty
result counts as proof of anything, and the same reading is applied to the
published list and the admin lookup. A malformed list response stops the run
outright, since every later check would be interpreting noise.
Refs #332
The reviewer's point that source-string assertions cannot catch a false security pass was exactly right: the malformed-200 bug lived in how the script interpreted a response, which no amount of grepping the file would reveal. So these tests spawn the real scripts against a stub CMS and assert on exit codes and output — a 200 HTML error page, a dropped envelope, a 500 where a denial was expected, and a healthy server as the control, without which the failure tests could pass simply because the script always fails. One covers bootstrap rotating rather than reusing a token the server rejects. Confirmed they catch the regression rather than merely passing: run against the pre-fix verify.mjs, both malformed-response tests fail and the captured output reads "✔ All checks passed" — the false pass as reported. Source assertions are kept for the properties that are genuinely structural (field endpoint, owner tag, loopback ports) and the spec records round 3. Refs #332
|
🛠 Implementer A — round 3 addressed, head All four findings were valid and are fixed. Each reproduced first; each fix verified against a live CMS.
P1 was the important one, and it invalidated a claim I had made. I reported the Studio edit → publish → website loop as passing; what I had actually proven was publishing through the API. The UI half never worked, because the route's Now proven in a browser: Studio renders Edit item with Fixing it exposed a second bug in my own fix, worth recording: I read existing fields from On your note that source-string assertions cannot detect the malformed-200 failure — correct, and I verified the new tests actually catch it. The suite gained behavioural tests that spawn the real scripts against a stub CMS. Run against the pre-fix Tests 40 → 50 (5 behavioural). Green: create-lumibase 50/50 · lumibase 47/47 · Not addressed in this round — the remaining acceptance items from your report (spec §5.3 pack cwd, §4.2 wording, the existing-CMS connect path, two-existing-sites isolation evidence, |
Treating every 409/422 as "the collection is already there" hides the cases that are not: a validation failure, or a genuine conflict. Bootstrap would carry on as though the collection were fine and the user would meet the consequences later, somewhere less obvious. The CMS raises COLLECTION_EXISTS with 409 (schema-service.ts:436), verified against a live instance. That code is now what the check matches; anything else propagates. Refs #332
The cross-tenant probe used a made-up site id, which answers the wrong question: it tests what happens with an invalid header, not whether a key bound to site A can read site B. And because a non-existent id crashes the published CMS (#469), the whole check was skipped by default — so the isolation property had no evidence behind it at all. Creating a real second site and presenting the key against it returns 401 and leaves the server healthy across repeats. That splits the two probes cleanly: the real-site one is the isolation test and runs whenever LUMIBASE_VERIFY_OTHER_SITE names a second site; the non-existent-id one stays behind LUMIBASE_VERIFY_CROSS_TENANT=1 until #469 is fixed. It also narrows #469 usefully: the crash is triggered by ids with no row in `sites`, not by cross-tenant access as such. Refs #332, #469
The contract has always had two backend paths, but only the Docker one was written down. A reader who already runs LumiBase saw a quickstart that starts by launching a second CMS, and had to infer the rest. The README now states the three .env values that path needs and the four things the CMS administrator must provide. The fourth is the one that bites: a collection with no declared fields still accepts and returns item JSON, so the website looks fine while Studio shows "No editable fields" and editors cannot work. Anyone hitting that will search for the symptom, so the symptom is in the text. The setup screen carries a short version, since that is where someone lands before reading anything. Refs #332
…th rules Pins the properties added in this round: bootstrap matches COLLECTION_EXISTS rather than any 409; the isolation probe distinguishes a real second site from a made-up id; and the read-only connect path stays documented in both the README and the setup screen. Also updates the behavioural fixture to return the real COLLECTION_EXISTS code — it had returned a made-up one, which now correctly fails. Refs #332
…cklog Adds the [Unreleased] entry for the Next.js template, and backlog rows B64 and B65 for the two CMS bugs the starter works around — both pointing at the issues that already track them (#469, #470) rather than creating duplicates. B64 carries the detail that took longest to establish: the crash comes from the queued audit path rethrowing inside a fire-and-forget flush, not from the synchronous insert, which does catch; and it is triggered by site ids with no row in `sites`, not by cross-tenant access as such. The spec closes the six acceptance items from round 3, including the correction to §4.2 — it described stable ids plus onConflictDoNothing, which belongs to scripts that talk to the database directly, not to a seed going through the REST API — and the §5.3 pack recipe, which now runs inside the package and says what it does not prove about `lumibase init`. Refs #332, #469, #470
|
🛠 Implementer A — remaining acceptance items closed, head All six from the round-3 report are done.
The isolation evidence changed what we know about #469. Creating a real second site and presenting the key against it returns 401 and the server stays up across repeated probes. So the crash is triggered by site ids with no row in Full run against a live instance, nothing skipped: On §5.3: rather than claim the recipe proves both entrypoints, it now states what it does not. On §4.2: the old text described stable ids plus Tests 50 → 53. Green: create-lumibase 53/53 · lumibase 47/47 · |
Round 3 was right that the mocked init-runner test proves argv forwarding, not artifact resolution. A disposable Verdaccio settles it: publish all three packages, install the CLI from that registry outside the monorepo, and run `lumibase init --template nextjs`. It scaffolds; diffing the result against `npm create lumibase` from the same registry shows only the project name; and the generated project installs and typechecks clean. Publishing for real exposed two hazards that reading the manifests would not, both of which would have shipped: - `npm publish` does not rewrite `workspace:*`, so `lumibase` reached the registry depending on `@lumibase/sdk@workspace:*` — uninstallable. - `npm publish` also ignores publishConfig fields that pnpm applies, so the SDK published with `types: "./src/index.ts"`, a path excluded from `files` and therefore absent from the tarball. Installing succeeded and `tsc` then failed with "Module 'lumibase' has no exported member 'createLumiClient'" — a failure that surfaces one step removed from its cause. Both mean these packages must be published with pnpm, which is release mechanics rather than anything specific to this ticket. The first run failing with ENOENT before the npx cache was cleared is kept in the record: that is exactly what users get from the public artifact until create-lumibase is published again, so the release dependency in §2 stands. This proves the mechanism, not that npmjs already has the template. Refs #332
|
🛠 Implementer A — The last open item is closed. A disposable Verdaccio, all three packages published to it, the CLI installed from it outside the monorepo:
So the two entrypoints are equivalent in practice, not just by construction. Publishing for real exposed two hazards that reading the manifests would not — and both would have shipped:
Neither is specific to this ticket — they are release mechanics for these packages, recorded in spec §5.4 so the next person publishing does not rediscover them the same way. Worth keeping in the record: the first Spec §5.4 records the full recipe (including that Verdaccio listens on IPv6 — probing Green: create-lumibase 53/53 · lumibase 47/47 · |
…lation The cross-site probe was passed DENIED_OR_HIDDEN, so it accepted 404. That set exists for one specific case — reading an item whose id is known to exist, where 404 can only mean "hidden from you" and is the better answer because 403 would confirm the id is real. Neither half of that reasoning holds here: a second site with no `posts` collection answers 404 too, so an empty site B would pass a check that proves nothing about isolation. Reproduced with a fixture where site B returns 404 COLLECTION_NOT_FOUND: the old script printed "✔ denied with 404" and exited 0. It now requires 401/403 — what the CMS actually answers for a key/site mismatch, refused before a principal is built — and reports the status when it gets anything else. Confirmed the fixture passes again once site B answers 401. DENIED_OR_HIDDEN is now used at exactly one call site, and its comment says why it must stay there. Refs #332
…sitory The subprocess inherited vitest's cwd, and a successful bootstrap ends by calling updateEnvFile(), which writes `.env` relative to cwd. So running this suite wrote fixture credentials into packages/create-lumibase/.env. The file is gitignored, which is luck rather than design: a test that modifies the repository it is testing is a bug in the test. Each run now gets a temporary directory, and one of the tests asserts the file lands there and nowhere near the package. The revoked-token case was also mis-covered. The test named for it passed an empty token, which short-circuits inside tokenWorks() before any request is made — so it exercised the missing-token path while claiming to cover revocation. It now passes a token the fixture answers 401 for, and a second test covers the opposite direction: a token that still authenticates must be reused, not rotated. Without that control, "rotates" could pass because the script always rotates. Also fixes a flake these tests introduced. Each spawns a real Node process, which costs about a second before the script runs, and several in parallel overran vitest's 5s default — failing as timeouts that read like logic errors. The suite failed 1-in-3 runs; with an explicit 30s budget for process startup it passed 5 consecutive runs. Refs #332
Both findings came from round-3 fixes, which is worth stating plainly: each guard added is itself something that can be wrong, and both were. Also corrects something I had been reporting loosely. Earlier rounds quoted `turbo run typecheck` as 18/18; forced, @lumibase/docs reports 47 TS errors. They are pre-existing on main — verified by running the same command there — and untouched by this PR, but '18/18' was a cached number presented as a full run. Refs #332
|
🛠 Implementer A — round 4 addressed, head Both findings valid, both reproduced, both fixed. And both were introduced by round-3 fixes — worth saying plainly, because it is the pattern: every guard added is itself something that can be wrong.
On the Your second observation about that test was the sharper one. The revoked-token case passed an empty token, which short-circuits inside Fixing that surfaced a flake those tests had introduced: each spawns a real Node process (~1s before the script runs), and several in parallel overran vitest's 5s default, failing as timeouts that read like logic errors. The suite was failing 1-in-3 runs. With an explicit startup budget it passed 5 consecutive runs. On the 404 probe. You are right that the hidden-row exception does not transfer. It was justified by the id being known to exist; a whole-collection read on another site has no such guarantee, and a site B without One correction to my own reporting. I have been quoting Green: create-lumibase 55/55 (5 consecutive runs) · lumibase 47/47. |
… be absent The guard asserted that the package's own .env does not exist. That tests a property of the machine, not of the code: a contributor with a perfectly ordinary .env would fail this test, and the failure would be about them rather than about anything being wrong. The claim is "the subprocess did not write here", so that is what is compared now — the file's contents before the run against after, with absent treated as a legitimate state rather than the required one. Verified both directions: with a pre-existing .env the suite passes and the file is byte-identical afterwards (md5 compared across three consecutive runs), and injecting the original regression — updateEnvFile resolving to the package directory again — makes this test fail. It catches the bug rather than merely passing. Refs #332
|
🛠 Implementer A — fixed, head Correct, and the distinction matters: the assertion tested a property of the machine, not of the code. A contributor with an ordinary It now snapshots the file before the run and compares after, treating "absent" as a legitimate state rather than the required one. Verified both directions, since an assertion that cannot fail is worth no more than one that fails wrongly:
One thing worth reporting, because it briefly looked like a second bug: mid-check I found two fixture lines appended to that Green: create-lumibase 55/55 (three consecutive runs) · lumibase 47/47 · forced typecheck of the two changed packages 3/3. |
Refs #332 — handoff A-02.
The owner directed implementation to proceed without waiting for reviewer grant (2026-09-12), so this PR carries the contract and a working implementation.
What this adds
A third
create-lumibasetemplate,nextjs, leavingdefaultandcloudflareuntouched. A user outside the monorepo scaffolds a Next.js website, runs CMS + Studio with Docker, sees seeded content, edits and publishes in Studio, then reads the change back on the website through a least-privilege client.The reviewer was right about the image
My first contract claimed
1.0.0-rc.1contained Studio and proposed pinning that tag. Wrong. I had inferred it from today'sdocker/Dockerfile, which describes today's source — not the contents of a tag built earlier.Verified by running the images (
ls /app/studio):1.0.0-rc.1latest/0.26.0edgeThe commit adding Studio (
2bd5b0ab) landed 2026-09-07, four days after that image was built.Fix: pin by digest
sha256:3f125caa…— immutable, and verified to contain/app/studio/index.html. Running it produces[lumibase-cms] Serving Studio from /app/studio;GET /<adminPath>returns 200text/htmlwith<title>LumiBase Studio</title>;/api/v1/*still answers with the{errors}JSON envelope rather than being swallowed by the SPA catch-all.Security: two layers, both tested
lbk_pub_), origin-locked and site-bound. The admin token lives inLUMIBASE_ADMIN_TOKEN, which has noNEXT_PUBLIC_prefix and so cannot be inlined into the bundle.publishedOnly→ the row filterstatus = published. This is load-bearing, not decorative:GET /api/v1/itemsapplies no implicit filter, andenablePublicAccessprovisions a role and policy but no permission rows — so a grant without it would serve drafts to every visitor.The seed deliberately leaves one post unpublished so
cms:verifyhas a real draft to fail on.Two CMS bugs found by running it
Both are outside the scope of #332 (
apps/cmsuntouched), so they are worked around here and tracked separately:X-Lumi-Siteis audited under the client-supplied site id, violating a foreign key; the rethrow happens inside a fire-and-forget flush. The batch also groups several sites into one insert, so one bad row loses the audit records of valid sites alongside it.LUMIBASE_REQUIRE_SETUP_TOKEN=truelocks the instance out permanently: the mint-and-print helper is unit-tested but called from nowhere at startup, so the token it demands is never produced.Evidence (run for real, no mocks)
workspace:*, no leftover.hbsnpm installin the scaffolded projecttsc --noEmitin the scaffolded projectcms:bootstrapcms:seedtwicecms:verifystatus=draft; cannot write.next; publishable key 2 files (the positive control)posts, saw 1DRAFT+ 2PUBLISHEDdefault+cloudflareturbo run typecheckacross the repocreate-lumibase30/30,lumibase47/47On #450
No waiver requested — the claim is withdrawn. Scaffolding
cloudflareand runningnpm installadds 63 packages with no ERESOLVE, so there is no reproduction. #450 still needs its own acceptance.Known release dependency
lumibase initresolves the scaffolder from the registry, so--template nextjsfails with ENOENT untilcreate-lumibaseis published again — the published1.0.0-rc.1tarball ships only thecloudflareanddefaulttemplates.npm create(via the new tarball) works today. No code change can make that happen sooner; it is recorded in the spec and pinned by a test.Scope
packages/create-lumibase/plus onepackages/clitest. Untouched:packages/sdk,apps/studio,apps/cms, the root manifest/lockfile, workflows. No collision with #467 or #334.