Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
968ed68
docs(spec): propose the Next.js starter contract for #332
khuepm Sep 12, 2026
91b6da3
feat(create-lumibase): add a Next.js starter with CMS, Studio and see…
khuepm Sep 12, 2026
59c78dd
feat(create-lumibase): offer the Next.js template and reject unknown …
khuepm Sep 12, 2026
3e5e9cf
docs(spec): record what running the starter actually proved
khuepm Sep 12, 2026
e96dc90
test(create-lumibase): check the draft by id, and pin argv forwarding
khuepm Sep 12, 2026
1a22d72
docs(spec): answer the review, and retract the Cloudflare claim
khuepm Sep 12, 2026
febcd5d
docs: point the starter's workarounds at their tracking issues
khuepm Sep 12, 2026
e3e97d0
docs(spec): write the starter contract in English
khuepm Sep 12, 2026
dc7fd5b
fix(create-lumibase): bind the starter's stack to loopback
khuepm Sep 12, 2026
00564db
fix(create-lumibase): stop cms:verify passing against a broken server
khuepm Sep 12, 2026
e62ef13
fix(create-lumibase): make bootstrap and seed actually re-runnable
khuepm Sep 12, 2026
3f62f46
fix(create-lumibase): stop the setup screen asking for a token that i…
khuepm Sep 12, 2026
c43ac32
test(create-lumibase): pin the five properties review round 2 found m…
khuepm Sep 12, 2026
837319e
fix(create-lumibase): make bootstrap produce a usable, owned, editabl…
khuepm Sep 12, 2026
a07b4e3
fix(create-lumibase): require the response envelope before believing …
khuepm Sep 12, 2026
2d86588
test(create-lumibase): run the starter's scripts, not just read them
khuepm Sep 12, 2026
efb92fe
fix(create-lumibase): confirm COLLECTION_EXISTS instead of any 409
khuepm Sep 12, 2026
2888159
feat(create-lumibase): test tenant isolation against a site that exists
khuepm Sep 12, 2026
e802e06
docs(create-lumibase): document connecting to a CMS you already run
khuepm Sep 12, 2026
c4fff75
test(create-lumibase): cover the error-code, isolation and connect-pa…
khuepm Sep 12, 2026
455b576
docs: record the starter in the changelog and both CMS bugs in the ba…
khuepm Sep 12, 2026
02615ce
docs(spec): prove `lumibase init` against a local registry
khuepm Sep 12, 2026
b6298b2
fix(create-lumibase): a missing collection is not proof of tenant iso…
khuepm Sep 13, 2026
1db8545
test(create-lumibase): stop the behavioural tests writing to the repo…
khuepm Sep 13, 2026
fe1b584
docs(spec): record round 4, and correct the typecheck claim
khuepm Sep 13, 2026
71a719a
test(create-lumibase): compare .env before and after, don't demand it…
khuepm Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .kiro/specs/admin-setup-wizard/setup-impact.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ Trạng thái: `pending` (chưa làm) · `in-progress` · `done` (setup + backfi

| 119 | (CMS serves the Studio SPA in Docker mode: #332 option B) | v1.0.0-rc.2 | Đã rà soát: `apps/cms/src/serve-studio.ts` mount `apps/studio/dist` từ chính process CMS ở Node/Docker (Workers vẫn dùng Pages, không có filesystem); `docker/Dockerfile` build Studio và copy vào `/app/studio`; `/assets/` + `/sw.js` thêm vào `NEVER_STUDIO_SCOPE_PREFIXES` của `adminPathGuard`. (1) KHÔNG seed. (2) KHÔNG settings key DB — điều khiển bằng env, không phải per-site. (3) KHÔNG policy/grant DB. (4) KHÔNG bước setup wizard mới — nhưng đây là lần đầu wizard **truy cập được** từ một Docker deployment: `/setup` trả shell của SPA thay vì 404. (5) KHÔNG capability `/setup/capabilities` mới. (6) KHÔNG migration/backfill | n/a | — | Rà soát 2026-09-07. **Hai env var mới, đều tuỳ chọn**: `LUMIBASE_STUDIO_DIST` (mặc định `./studio` tương đối CWD) và `LUMIBASE_SERVE_STUDIO=false` để tắt — nên tắt khi Studio cũng deploy trên Pages đứng trước CMS này, vì hai bản sao trôi lệch phiên bản thì người dùng gặp bản nào tuỳ hostname họ gõ. Vắng bundle → log rồi chạy API-only, tức **degrade an toàn**, đúng hành vi trước thay đổi này; `LUMIBASE_STUDIO_DIST` trỏ sai đường thì warn tường minh chứ không im lặng. **Ảnh hưởng Req 5.x (Hide-Login)**: bypass thêm hai prefix nghĩa là ai có tên file content-hashed xác nhận được "host này có Studio" — nhưng KHÔNG suy ra được admin path (build từ chối nhúng, `assertNoAdminPathEnv`), và `/`, `/admin`, `/studio`, admin-path-lệch-một-ký-tự vẫn trả canonical 404. Property 7 (`404-indistinguishable.test.ts`) đã chạy lại: pass |

| 123 | (create-lumibase: Next.js starter with CMS + Studio + seed: #332) | v1.0.0-rc.2 | Reviewed: a third template `templates/nextjs` in `create-lumibase` (Next.js app + `docker-compose.yml` pulling the CMS image **by digest** + `bootstrap`/`seed`/`verify` scripts), a widened `Template` union + prompt + `isNextjs`, `--template` validation, and safety-invariant tests. (1) NO CMS-side seed — the seed lives in the **user's generated project**, runs with their own admin token, and is idempotent by `slug`. (2) NO new CMS settings key / env var — the `NEXT_PUBLIC_LUMIBASE_*` and `LUMIBASE_ADMIN_*` variables belong to the user's project. (3) NO new policy/grant in this repo — the bootstrap script **calls** existing APIs (`POST /access/grants/public/enable` then `POST /access/grants/public` with `publishedOnly`), i.e. it creates grants on the user's instance rather than adding definitions to LumiBase. (4) NO new setup-wizard step — the script calls the existing `POST /setup/complete`. (5) NO new `/setup/capabilities` capability. (6) NO migration/backfill | n/a | nextjs-starter-contract/design.md | Reviewed 2026-09-12. Touches the scaffolder plus one `packages/cli` test only; does **not** touch `apps/cms`/schema/routes. **Operational notes (NOT setup-wizard concerns)**: (a) compose pins the image **by digest** rather than by tag because **no semver tag contains Studio** — the commit adding Studio (`2bd5b0ab`, 2026-09-07) postdates the `1.0.0-rc.1` build (2026-09-03); `edge` has Studio but is a moving tag. Verified by running `ls /app/studio` inside the images, not by reading today's Dockerfile. (b) The template deliberately does **not** enable `LUMIBASE_REQUIRE_SETUP_TOKEN`: `printSetupTokenIfRequired` is unit-tested but **called from nowhere** at startup, so enabling the flag locks setup out permanently — CMS bug **#470**, outside #332's scope. (c) `cms:verify` keeps its cross-tenant probe behind an opt-in flag because sending a non-existent `X-Lumi-Site` **kills the process** (the audit row is written under a site id that does not exist → foreign-key violation, rethrown inside a fire-and-forget flush) — CMS bug **#469**, also out of scope. (d) `lumibase init --template nextjs` only works **after** `create-lumibase` is published again: `init` resolves the scaffolder from the registry, and the published `1.0.0-rc.1` does not ship this template |

## Lưu ý backfill

Các gap #1–#3 ảnh hưởng cả instance **đã setup** — fix không chỉ nằm trong setup wizard mà cần kèm migration/backfill idempotent (`onConflictDoNothing`) hoặc giữ lazy-init làm fallback song song.
Expand Down
663 changes: 663 additions & 0 deletions .kiro/specs/nextjs-starter-contract/design.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions .kiro/steering/out-of-scope-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,7 @@
| B55 | 2026-09-07 · deployment-integrations task 12.3 (webhook signature) | bug (docs) | `docs/{en,vi}/api/hono-api-spec.md` | Task 16.1 của spec `deployment-integrations` được tick `[x]` với nội dung *"section 7b endpoints deployment"*, nhưng **section đó không tồn tại**: `grep -n "deployment"` trên `docs/en/api/hono-api-spec.md` không trúng một endpoint nào (chỉ trúng chữ "deployment" trong mô tả rate-limit/encryption), `grep "7b"` trả rỗng, và bản VI cũng vậy — mục lục nhảy từ `## 7. Flows / Automation` sang `## 8. AI Copilot`. Nghĩa là toàn bộ surface `/api/v1/deployments/*` (targets CRUD, trigger, list/detail/logs/refresh, webhook vào) **không có trong API spec** dù spec đó là artifact được `v1-release-criteria.md` §2 xếp vào "API surface freeze". Feature doc `docs/{en,vi}/features/deployment-integrations.md` §5 có bảng REST đầy đủ nên thông tin không mất, nhưng nó nằm sai chỗ so với cái checklist DoD §4 đang trỏ tới. Cùng class "checkbox tasks.md không phải bằng chứng" mà chính `v1-scope-classification.md` cảnh báo | medium | `open` | Thêm `## 7c. Deployments` vào `docs/en/api/hono-api-spec.md` + bản VI cùng PR (DoD §4a) rồi re-stamp pair; ghi rõ header chữ ký bắt buộc của endpoint webhook (`x-vercel-signature` / `x-webhook-signature`) + `401 INVALID_SIGNATURE`. Không làm trong task 12.3 vì đó là một section API spec mới cho cả feature, không phải một dòng thuộc scope verify chữ ký |
| B54 | 2026-09-07 · deployment-integrations task 6.2 (rate-limit) | task (docs debt) | `docs/{en,vi}/security/anti-abuse.md` | Cặp EN/VI của `anti-abuse.md` lệch cấu trúc sẵn trên `main`: `stamp-pair.mjs` từ chối stamp vì EN có 4 in-page anchor link còn VI có 0, và link tới `../api/graphql-api-spec.md#abuse-guards` ở VI trỏ anchor đã dịch (`#chống-lạm-dụng`). Phần anchor đã dịch là **false positive** (heading khác locale thì slug phải khác — cùng loại với ca (d) của B37), nhưng 4 anchor nội trang thiếu ở VI là thiếu thật: bản VI viết "xem phần Khoảng trống & khuyến nghị bên dưới" bằng chữ thay vì link, nên người đọc VI không nhảy được tới mục đó. `docs:i18n:detect` xếp cặp này `up-to-date` vì hash còn khớp — đúng cái điểm mù B37 đã nêu. Ngoài scope task 6.2 (chỉ thêm một bullet registry vào §1) | low | `fixed` | Đóng 2026-09-10 khi tích hợp #458 vào #457: thêm đủ 4 liên kết nội trang ở bản VI; `check-parity.mjs` so đường dẫn của liên kết Markdown tương đối nhưng cho phép fragment heading đã dịch khác nhau, còn fragment URL ngoài vẫn được giữ nguyên. Hai test khóa cả hai hành vi; cặp được verify 72 code reference, stamp lại và vượt parity không cần waiver cấu trúc. |
| B59 | 2026-09-09 · review PR #458 (parity gate) | bug (CI guard không fire) | `.github/workflows/docs-i18n-sync.yml` · `scripts/docs-i18n/gate-changed-parity.mjs` | **Gate parity bỏ qua đúng ca nó cần bắt.** Workflow lấy danh sách file bằng `git diff --name-only --diff-filter=d`, mà `--diff-filter=d` **loại file bị xoá**. Một PR xoá riêng `docs/vi/x.md` để bản EN mồ côi nhưng **không cung cấp file doc nào** cho gate → danh sách rỗng → gate exit 0 và PR xanh. Rename cũng cùng lỗ, qua đường path cũ đã biến mất. Bản thân script **đã có** xử lý orphan từ đầu — nó chỉ không bao giờ nhận được path đó, nên đây là class "guard có mà không fire" giống B31/B36/B53/B56: đọc code script thì thấy đúng, đọc workflow mới thấy sai | medium | `fixed` | Bỏ `--diff-filter=d` và dùng `--no-renames` để luôn nhận cả path cũ lẫn mới; gate tự phân loại theo số locale còn sống: cả hai còn → so parity, còn một → **đỏ** (orphan), không còn bên nào → **xanh** (cho doc về hưu, không cần override). Tách `relsFromChangedFiles` + `classifyPairs` thành hàm thuần và thêm `scripts/__tests__/gate-changed-parity.test.mjs` (11 ca `node --test`: add một bên, edit hai bên, xoá một bên mỗi phía, xoá cả hai, rename hai bên, rename một bên, cộng hai ca dựng **repo git thật** để chứng minh workflow giữ deletion và path cũ của rename). Đã kiểm âm: coi half-deleted là hợp lệ → 5 ca đỏ. `scripts:test` đã nằm trong `check:all` của CI |
| B64 | 2026-09-13 · A-02 review PR #468 (#332) | vuln (availability) | `apps/cms/src/middleware/tenant.ts` · `middleware/auth.ts:93` · `modules/audit/worker.ts:139-141` | **DoS không cần xác thực, một request là đủ.** `withTenant` chỉ shape-check `X-Lumi-Site`, không kiểm tra site tồn tại; khi từ chối api key, `auditApiKeyUseDenied` ghi audit dưới chính site id client gửi → vi phạm FK `lumibase_audit_log_site_id_lumibase_sites_id_fk`. `AuditLogger.write` **có** bắt lỗi cho insert đồng bộ (`logger.ts:485-490`), nhưng đường queue thì batcher bắt rồi **`throw err`** trong flush fire-and-forget (`void this.scheduleFlush()`) ⇒ unhandled rejection ⇒ chết process. Batch còn gộp nhiều site trong một insert nên một hàng hỏng **mất luôn audit của site hợp lệ** (mất dấu vết bảo mật, không chỉ downtime). Chỉ site id **không tồn tại** mới gây crash: probe với site thật trả 401 và server vẫn khỏe | high | `tracked` | Issue **#469** (có bước tái hiện + hướng sửa 3 lớp). Starter #332 né bằng cách để probe non-existent-id sau cờ `LUMIBASE_VERIFY_CROSS_TENANT=1`; probe isolation thật (`LUMIBASE_VERIFY_OTHER_SITE`) chạy mặc định. KHÔNG sửa trong #332 — `apps/cms` ngoài scope |
| B65 | 2026-09-13 · A-02 review PR #468 (#332) | bug | `apps/cms/src/modules/setup/setup-token.ts:148` · `apps/cms/src/serve.ts` | **`LUMIBASE_REQUIRE_SETUP_TOKEN=true` khoá chết instance.** `printSetupTokenIfRequired` sinh token, lưu hash và in một dòng `[lumibase-cms] SETUP_TOKEN=…` — nhưng **không được gọi từ đâu** lúc khởi động (grep toàn repo: 3 kết quả, đều trong chính file đó). Phía đọc đã nối đủ (`verifySetupToken`, nhánh `requiresSetupToken`, mã `SETUP_TOKEN_REQUIRED`), nên hệ thống *kiểm tra* một token mà không có gì *sinh* ra nó ⇒ `/setup/complete` trả `SETUP_TOKEN_REQUIRED` vĩnh viễn, không có đường phục hồi ngoài tắt cờ + tạo lại container. Requirement ghi rõ phải sinh **lúc startup** (`admin-setup-wizard/requirements.md:63`); unit test gọi thẳng hàm nên vẫn xanh — khoảng trống giữa "hàm đúng" và "hàm được nối vào". Hệ quả: instance expose **không thể** bảo vệ theo cách tài liệu mô tả | medium | `tracked` | Issue **#470**. Fix = gọi trong `serve.ts` sau khi có DB + chốt cách xử lý Cloudflare (Workers không có "startup") + test đi qua **đường khởi động**. Starter #332 cố ý không bật cờ, bind loopback thay thế |
| B61 | 2026-08-31 · rà workflow trên main sau batch dependabot (rebase 2026-09-06) | vuln (availability) | `packages/runtime/src/index.ts` · `apps/cms/src/middleware/runtime.ts` · `apps/cms/wrangler.toml` | **Mọi deploy Cloudflare bị Cloudflare từ chối** kể từ khi bullmq 6 vào main (#421, 2026-08-30): `Uncaught Error: Could not determine sql-loader directory path`, validation error 10021. Nguyên nhân: `packages/runtime/src/index.ts` re-export **cả** `./adapters/docker` (và `./factory` = `createRuntime`, và `./leader-lock` import `ioredis` như **value**), còn `middleware/runtime.ts` import `createRuntime` từ barrel → bundle Worker kéo theo cả cây docker gồm `bullmq`. BullMQ 6 thêm backend Postgres, và `dist/esm/postgres/sql-loader.js` gọi `getDirname()` ở **top level module**, throw khi không có `__dirname` lẫn frame `file:///` — đúng môi trường Worker đã bundle. Worker throw lúc khởi tạo ⇒ script bị từ chối. **Vì sao mọi gate đều xanh:** `pnpm build` chạy `wrangler deploy --dry-run`, nó *bundle mà không instantiate*; job `build` của CI vì thế không thể thấy class lỗi "throw ở top level". Đây cũng là chỗ tôi kết luận sai khi rà #421 — coi "build pass + round-trip Redis thật" là đủ, trong khi chưa có gì boot Worker. Chỉ `deploy-cms.yml` (dev/staging) đỏ; production do `release.yml` sở hữu (chỉ chạy khi tag) nên chưa vỡ nhưng sẽ vỡ ở lần cắt release tới | critical | `fixed` | Tách entry point: root barrel chỉ còn thứ an toàn cho Worker; `@lumibase/runtime/docker` (adapter docker) và `@lumibase/runtime/node` (`createRuntime` + leader lock) là subpath riêng. `middleware/runtime.ts` static-import **chỉ** `createCloudflareRuntime`, nhánh docker đi qua `await import()`. Nhưng dynamic import **vẫn là static edge** với bundler (esbuild inline target — đo được: bundle vẫn chứa bullmq), nên thêm `[alias]` trong `wrangler.toml` map subpath docker sang stub `runtime-docker-unavailable.ts` (throw kèm mô tả nếu nhánh bất khả thi kia chạy). Kết quả đo: upload Worker **8848.98 KiB → 6085.28 KiB** (số trước lấy từ log deploy đỏ cuối cùng, run 33953796948 ngày 2026-09-05; số sau là build của PR này), 0 dấu vết bullmq/ioredis/aws-sdk/sql-loader ở cả 3 env. Gate mới: `pnpm verify:worker-bundle` (assert thành phần bundle — **đây** mới là hàng rào thật, đã test âm: thêm lại re-export docker → đỏ đúng) + `pnpm verify:worker-startup` (boot Worker bằng workerd). **Lưu ý đã đo:** startup gate **KHÔNG** bắt được ca này — với docker cố tình bundle lại, `wrangler dev` vẫn boot và trả `/health` 500, vì fallback quét stack `file:///` của bullmq thành công ở local mà thất bại ở Worker deploy; nên đừng đọc "startup xanh" thành "deploy được". Cả hai chạy trong CI job `worker-bundle`. Bonus: `serve.ts` trước đây inject runtime bằng middleware thứ hai đặt **sau** `withRuntime`, nên Docker mode dựng **hai** runtime mỗi process (hai kết nối Redis, hai pg pool) rồi bỏ một — nay dùng `setRuntimeFactory`, còn một Issue #459. |
| B60 | 2026-09-09 · review PR #456 (§4a docs) | task (docs debt) | `docs/{en,vi}/features/agent-harness-layer.md` | Bản VI của `agent-harness-layer.md` **không phải bản dịch** của bản EN mà là một tài liệu khác: một đề xuất 8 mục có số, 10 heading so với 25 của EN, dài **25%** bản EN, có bảng và code fence mà EN không có và thiếu 253 inline-code identifier EN có. Front matter vẫn ghi `syncStatus: human-translated`, và `docs:i18n:detect` xếp cặp này up-to-date vì hash còn khớp — đúng điểm mù B37/B54/B58 nhưng ở quy mô cả file. PR #456 thêm phần approval claim/quarantine/reopen vào **cả hai** bên trong cùng commit nên không làm tệ thêm, và stamp bằng `--allow-structure-drift` kèm lý do | medium | `open` | Dịch lại bản VI từ bản EN hiện tại (hoặc quyết định bản VI là một tài liệu roadmap riêng và **đổi tên** nó, bỏ `translatedFrom`/`syncStatus` để nó không còn giả vờ là một nửa của cặp). Việc dịch thuần, không đụng code. **Cập nhật 2026-09-10:** parity gate (#458) đã merge và chặn PR #456 vì cặp này; đã thêm waiver `<!-- check-parity: allow headings code-fences inline-code links tables bulk -->` vào đầu bản VI để gate không chặn thay đổi không liên quan. Waiver ghi nhận hiện trạng chứ không chấp nhận nó — **gỡ waiver khi làm xong mục này**, lúc đó cặp phải qua parity mà không cần miễn trừ |
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,41 @@ Source: [github.com/khuepm/lumibase](https://github.com/khuepm/lumibase) · Webs

### Added

- **`npm create lumibase --template nextjs` scaffolds a working website, not a
starting point.** The two existing templates give you a server to build on;
this one gives you a site that already works: a Next.js front end, a CMS with
Studio in one container, a `posts` collection with real fields, and seeded
content you can edit and publish. The point of #332 was to make that loop
provable end to end, so the generated project ships `cms:bootstrap`,
`cms:seed` and `cms:verify` and all three are re-runnable.

The browser never holds an admin credential, and two independent things keep
that true. It gets a *publishable* key (`lbk_pub_…`), origin-locked and bound
to one site, while the admin token stays in a variable with no `NEXT_PUBLIC_`
prefix so Next.js cannot inline it. And the public read grant carries
`publishedOnly`, which matters more than it sounds: `GET /api/v1/items`
applies no published-only filter of its own, 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 it checks the list, the item by
its own id, an explicit `status=draft` query, a write attempt, and — given a
second site id — cross-tenant isolation.

A refusal only counts when the server actually refused. An earlier version
treated any failed request as proof of a working guard, so an HTTP 500 read as
"denied" and a broken server produced a green run; it now requires 401/403
(plus 404 where hiding a row *is* the refusal) and a well-formed response
envelope, and reports checks it could not run as skipped rather than passed.

The CMS image is pinned by digest rather than tag, because no semver tag
contains Studio yet — the commit adding it postdates the last release build —
and `edge`, which does, is rebuilt on every push to main. Every published port
binds `127.0.0.1`: the stack ships development secrets, and the setup-token
gate is deliberately left off because enabling it locks an instance out
([#470](https://github.com/khuepm/LumiBase/issues/470)).

`lumibase init --template nextjs` resolves the scaffolder from the registry,
so it reaches this template only after `create-lumibase` is published again.

- **Deploy triggers are now rate-limited per target.** `POST /api/v1/deployments/targets/:id/deploy`
was gated on "site admin" and "target is active" — who may trigger, never how
often — so anything holding admin credentials (a script, a stuck flow, an
Expand Down
Loading