This repository was archived by the owner on Aug 11, 2026. It is now read-only.
fix(admin): stop pool-saturation 500s (withTenant Response leaked as SSR data) - #323
Merged
Merged
Conversation
…SSR data) `withTenant<T>()`'s pool-gate fallback returns a 503 DATABASE_BUSY `Response` cast to the generic `T`. That is correct for API routes (T=Response, returned to the client) but SSR renders and `resolveSsrContext` use `T` = a plain data object. Under pool saturation / circuit-open those callers received a truthy `Response` instead of their data — their `try/catch` did not catch it, and the template then crashed (`context.permissions.has(...)` / `Response.types.map(...)`), 500ing every `/admin/*` page. Reproduced in prod: 4 `context.permissions.has` errors interleaved 1:1 with 4 `database.pool.saturated` events. - Add opt-in `withTenant` option `unavailableBehavior: "throw"` (default `"response"` preserves API-route behavior) + exported `DatabaseUnavailableError` (carries `retryAfterSeconds`); the three pool-gate fallbacks throw instead of returning a Response when set. - `resolveSsrContext` surfaces the leaked Response; middleware serves it as a proper 503 (Retry-After) rather than storing it as `ssrContext` or bouncing a valid session to /login. - All 14 SSR render call sites (AdminLayout + 10 admin pages) pass `unavailableBehavior: "throw"`, so their existing try/catch degrades gracefully (fallback nav / loadError notice) under saturation. - Regression tests: all three fallback paths throw `DatabaseUnavailableError` for non-Response callers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This was referenced Jul 23, 2026
ahliweb
added a commit
that referenced
this pull request
Jul 23, 2026
…323/#324) (#325) - Register DATABASE_IDLE_IN_TXN_TIMEOUT_MS (src/lib/config/registry.ts + .env.example + doc 18) so the config-docs three-way drift gate stays in sync. - doc 16 + database-pooling.md §9: document "never Promise.all multiple queries on one withTenant tx" (idle-in-transaction leak → pool saturation) and the unavailableBehavior: "throw" requirement for non-Response callers. - database-capacity-runbook.md: add the idle-in-transaction leak diagnosis + pg_terminate_backend mitigation to the saturation incident SOP. - Agents (coder/reviewer) + skills (new-endpoint, performance): add the tx-concurrency anti-pattern to their checklists. Co-authored-by: AWCMS-Micro Security <security@awcms-micro> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ahliweb
added a commit
that referenced
this pull request
Jul 25, 2026
…ubah (#359) (#360) Menutup penundaan yang dicatat ADR-0037 §Alternatif. Sebelumnya kesegaran konten publik hanya dibatasi TTL (default 60 detik), dan invalidasi segera mengandalkan operator mengingat menjalankan `bun run edge-cache:purge`. Terhubung pada permukaan tempat konten publik benar-benar berubah: `publish`, `archive`, `restore`, `PATCH`, dan `DELETE` artikel blog, plus job publikasi terjadwal. Tiga aturan mengikat, masing-masing karena mode gagal yang pernah dialami repositori ini: 1. **Selalu di luar transaksi database** (ADR-0030 §4). Menahan koneksi pool selama panggilan HTTP ke cache adalah bentuk yang menjenuhkan pool pada Issue #324. `withPublicCacheInvalidation` membungkus hasil handler dan hanya bekerja setelah `withTenant` selesai — juga hanya ketika handler benar-benar sukses, sehingga `404`/`422` dan fallback `503` milik `withTenant` sendiri (yang berupa Response, bukan exception — PR #323) tidak memicu apa pun. 2. **Fail-open mutlak.** Publikasi yang sudah commit tidak boleh berubah menjadi gagal karena cache tak terjangkau. Setiap jalur resolve; tidak ada yang throw. TTL tetap jaring pengaman. 3. **Nol pekerjaan database bila cache tidak dikonfigurasi.** Pemeriksaan konfigurasi mendahului pencarian hostname — deployment tanpa edge cache tidak membayar satu query pun per publikasi. Ini aturan yang paling mudah rusak diam-diam, jadi diuji langsung, bukan disimpulkan. Yang di-purge adalah seluruh host, bukan hanya URL yang berubah: satu artikel terbit juga mengubah daftar, halaman tag/kategori, sitemap, dan feed. Mendaftarkannya presisi berarti menyalin ulang aturan routing tiap modul ke satu tempat dan diam-diam melewatkan modul yang ditambahkan kemudian. Job terjadwal hanya meng-invalidasi ketika benar-benar ada yang terbit — job berjalan pada timer, jadi purge tanpa syarat akan mengosongkan cache mengikuti jadwal, bukan mengikuti perubahan. Refs #359 Co-authored-by: AWCMS-Micro Security <security@awcms-micro> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ahliweb
pushed a commit
that referenced
this pull request
Jul 26, 2026
Reviewer + security auditor berjalan atas PR ini. Nol CRITICAL, nol BLOCKER. Yang diperbaiki di sini adalah temuan yang berada di berkas yang memang sudah dibuka PR ini. XSS laten — `ClientJsonData.astro` (muncul di KEDUA review). `JSON.stringify` tidak meng-escape `<`, sehingga payload berisi `</script>` menutup data island dan sisanya diparse sebagai HTML hidup. Nol call site eksploitabel hari ini (semuanya string i18n milik repo), tapi komponen ini mengiklankan diri menerima "payload JSON apa pun" dan 35 halaman admin adalah kandidat pemakainya. Serializer dipindah ke `src/lib/ui/client-json-data.ts` agar bisa diuji + 3 unit test dengan payload `</script>` sungguhan. Kelas #323 di rute PUBLIK — tiga pemanggil non-`Response` di modul `theming` memanggil `withTenant` tanpa `unavailableBehavior: "throw"`. Pada `/theming/tokens.css` pool jenuh berarti `[object Response]` tersaji sebagai CSS di setiap page load. Kini degradasi ke tema default / preview `null`. Pra-ada, tapi PR ini menyapu kelas yang sama di `admin/tenant/domains` dan melewatkan tiga ini di berkas yang ia sentuh sendiri. Pengerasan gerbang, semuanya kelas "hijau tapi mati" yang PR ini justru melawan: - `validate-module-graph.ts` — `src/lib` tak terbaca kini MELEMPAR, bukan melaporkan pohon kosong yang lolos; lookup tabel pakai `Object.hasOwn`. - `static-analysis.ts` — laporan tmp pakai `mkdtemp` (path lama bisa ditebak dan jadi target symlink lewat `-o` ESLint); pembersihan dipindah ke sebelum `fail()` karena `process.exit()` melewati `finally`. - `api:tenant-route:check` jadi langkah CI eksplisit — CI menjalankan gerbang satu per satu, jadi memasangnya di `bun run check` saja tidak cukup. - Daftar pengecualian ESLint dikosongkan; entrinya sudah basi di PR yang sama. - `link-check.ts` — filter skema jadi ALLOW-list (deny-list lama melewatkan `file:`), userinfo dibuang dari laporan JSON. Koreksi ADR-0038. Klaim "nol impor relatif lintas-modul di seluruh src/modules" SALAH dan saya yang menulisnya. Pengukuran ulang menemukan ~80 impor semacam itu yang sudah ada jauh sebelum ADR ini. Yang benar: tidak ada SIKLUS antar modul, dan yang dijaga hari ini adalah dependensi yang DIDEKLARASIKAN — bukan impor tingkat berkas. Lubang itu kini tercatat sebagai pekerjaan lanjutan, bukan diklaim selesai. Refs #369 #370 #371 #372 #374
ahliweb
added a commit
that referenced
this pull request
Jul 26, 2026
…369/#370/#371/#372 + bagian in-repo #296) (#374) * refactor(lib): tetapkan batas src/lib dan pindahkan kode presentasi modul ke presentation/ `src/lib/` tumbuh menjadi sistem modul kedua yang tidak dijaga gerbang mana pun: lima namespace (`comments`, `newsletter`, `theming`, `seo`, `search`) menyandang nama modul yang sudah ada dan berisi kode milik modul itu, dengan `seo_distribution` bahkan merujuk ke ATAS ke `src/lib/seo/` lewat jalur yang validator DAG tidak bisa lihat. Penyebabnya: kontrak modul tidak punya tempat bagi kode presentasi/pengiriman, sehingga `src/lib/<nama-modul>/` menjadi satu-satunya rumah yang tersedia. ADR-0038 memutuskan definisinya: `src/lib` hanya infrastruktur teknis yang tidak menyandang nama domain; composition root rute, glue middleware, dan skrip klien browser milik modul tinggal di `src/modules/<m>/presentation/`. Lapisan tidak dienumerasi di kode mana pun (tiga lapisan lain juga tidak), jadi tidak ada mekanisme baru yang ditambahkan ke `module-contract.ts` — yang ditegakkan mesin adalah gerbangnya, bukan penamaan lapisannya. - Sepuluh berkas dipindah dengan `git mv` (riwayat terjaga). Pemindahan MURNI: tidak ada perubahan perilaku, API, migration, event, permission, atau registry (tetap 22 modul; `MODULE_CONTRACT_VERSION` tidak berubah). - `modules:dag:check` diperluas: GAGAL bila sebuah namespace `src/lib/<x>/` bertabrakan nama dengan `moduleKey` — persis, atau lewat alias domain terdaftar (`seo` -> `seo_distribution`, `search` -> `site_search`), tanpanya dua dari lima kasus historis akan lolos. Pesan gagalnya menyebut modul pemilik + tujuan pindah. - `tests/unit/lib-namespace-ownership.test.ts` MENYUNTIKKAN pelanggaran untuk membuktikan gerbangnya benar-benar menolak (termasuk memanggil `main()` skrip di atas pohon fixture dan memeriksa exit code 1) — bukan sekadar hijau setelah diperbaiki. Satu pengecualian tercatat: `src/lib/logging/` (primitif logger bebas database, dipakai ~139 berkas); tesnya membuktikan `logging` TERDETEKSI dan hanya disenyapkan oleh tabel pengecualian, bukan titik buta deteksi. - `src/lib/files/` dan `src/lib/errors/` yang kosong (hanya `.gitkeep`) dihapus. Refs #371 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(api): satu factory defineTenantRoute untuk pembukaan auth/tenant rute API Pembukaan auth/tenant yang disalin ke 201 dari 260 rute (resolveAuthInputs → cek tenant/token → getDatabaseClient → hashSessionToken → withTenant → authorizeInTransaction → short-circuit auth.denied) kini hidup satu kali di src/modules/_shared/tenant-route.ts. - `workClass` WAJIB di tipe factory (tanpa default): 221 rute memakai "interactive" karena tidak ada yang meneruskan argumennya, bukan karena ada yang memutuskannya. Menghilangkannya sekarang gagal kompilasi. - `unavailableBehavior` di-hardcode "response" dan sengaja tidak bisa dioverride — rute adalah pemanggil Response menurut konstruksi; "throw" hanya untuk pemanggil non-Response (#323), dan di sini justru akan mengubah 503 terkendali jadi 500. - Larangan Promise.all atas satu `tx` (#324) didokumentasikan di tipe `tx` yang diterima handler. - `prepare` menampung parsing body/query, cek Idempotency-Key, dan request hashing — tetap sebelum koneksi diambil. Modul data_lifecycle dimigrasi penuh (5 file rute, 6 handler) tanpa perubahan perilaku; tes integrasi lamanya tetap hijau tanpa disunting. Gerbang baru `bun run api:tenant-route:check` menolak rute BARU yang memanggil withTenant langsung, dengan daftar NOT_YET_MIGRATED berisi 235 rute lama yang hanya boleh menyusut (entri basi juga menggagalkan gerbang). Generator work-class kini mengenali defineTenantRoute — tanpa itu rute yang dimigrasi justru HILANG dari registry, dan menolak menulis "default" untuk rute factory tanpa literal. Refs #370 * test(website-platform): axe atas daftar/pencarian/404 publik + pemeriksa tautan operator Menutup dua celah #296 yang masih terbuka di dalam repo. 1. `tests/e2e/public-discovery-a11y.e2e.ts` — axe-core (WCAG 2.2 A/AA, gagal pada critical/serious) atas permukaan di ANTARA beranda dan artikel: halaman daftar `/news` + `/blog/{tenantCode}`, halaman pencarian blog (kosong dan berisi), keempat cabang render `/search` (belum ada query / terlalu pendek / tanpa hasil / ada hasil), dan dokumen 404 publik bersama — dalam EN dan ID, pada viewport desktop dan ponsel, ditambah pemeriksaan pencarian yang dioperasikan hanya dengan keyboard (fokus terlihat + Enter mengirim form). Tiga mekanisme locale berbeda dipakai di permukaan publik; spec ini memakai `default_locale` tenant (tenant kedua ber-locale `id`) dan `?locale=` pada `/search`, bukan cookie yang dipakai spec lain. Status HTTP di-assert SEBELUM axe agar route yang diam-diam 404 tidak dilaporkan bebas pelanggaran. Kedua gate dibuktikan bisa MERAH lewat kontrol negatif (img tanpa alt; outline/box-shadow none). Spec ini menulis ulang singleton `awcms_micro_setup_state`, jadi ia memegang advisory lock lintas-berkas `setup-state-ownership` selama hidupnya; seed unik per-run dan dibersihkan di `afterAll` agar aman terhadap retry Playwright. 2. `scripts/link-check.ts` + `bun run link:check -- --url=<url>` — pemeriksa tautan yang bisa dijalankan operator terhadap URL mana pun. Merayapi graf halaman yang dirender dari URL awal, ditambah direktif `Sitemap:` di `robots.txt` dan sitemap index/anak, lalu memverifikasi setiap anchor internal, `rel=canonical`, `rel=alternate hreflang`, tautan feed, dan pagination benar-benar terselesaikan (<400 setelah redirect). Laporan JSON ke stdout mengikuti idiom `scripts/edge-cache-verify.ts`; exit 0 bersih / 1 tautan rusak / 2 usage error atau URL awal tak terjangkau — crawl yang tidak menjangkau apa pun tidak pernah dilaporkan hijau. Internal-vs-eksternal ditentukan oleh HOST, bukan origin penuh: terukur di app ini `<loc>` sitemap memakai `https://` sementara permalink di halaman yang sama memakai `http://`, sehingga perbandingan origin-ketat akan melewati separuh tautan situs sendiri lalu melaporkan hijau. `--site-origin=` untuk memeriksa deployment lewat alamat lain (host staging, IP internal di balik CDN). Ekstraksi tautan memakai pemindai satu-lintasan yang melewati komentar/CDATA/`<script>`/`<style>` (bukan rantai strip-lalu-rescan yang memicu CodeQL js/incomplete-multi-character-sanitization), dan entitas `&` didekode TERAKHIR (js/double-escaping). Refs #296 * refactor(pages): pecah empat halaman admin raksasa jadi markup + lapisan presentation 51 halaman `.astro` berbanding 16 komponen berarti praktis tidak ada lapisan komponen: tiap halaman membangun ulang tabel, dialog, form, dan penanganan errornya sendiri sebagai teks di dalam satu berkas. Gelombang pertama memecah empat halaman yang sudah punya jaring pengaman Playwright, dengan tiga gerakan mekanis per halaman — bukan penulisan ulang. - Skrip inline `<script>` (1.263 baris) pindah ke `src/modules/<m>/presentation/*-client.ts` (ADR-0038) dan diimpor kembali lewat `<script>` agar tetap di-bundle Astro; `bun run build` membuktikan keempat bundle klien terbentuk. Begitu jadi `.ts` ia masuk `tsc` dan bisa di-unit-test. - Frontmatter pindah ke `presentation/<page>-page-data.ts`: permission gating, `withTenant` read, builder daftar opsi, dan builder blob `clientStrings` — yang terakhir diketik dengan interface milik modul kliennya sendiri, jadi `tsc` gagal kalau kedua sisi melenceng. - Stylesheet pindah ke `presentation/<page>.css`. CSS itu kini global, bukan scoped Astro: tiga selector element-level di `tenant/domains` dijangkarkan ulang ke `.domain-manager` agar tidak merembes ke topbar `AdminLayout`. Komponen diekstrak hanya untuk pola yang DIUKUR muncul di lebih dari satu halaman: `ClientJsonData` (35/51 halaman), `LoadErrorNotice` (38/51), `TextField` (27/51), `SelectField` (31/51), `FieldHint` (18/51), `CheckboxField`/`CheckboxGroup` (16/51). Ketiganya sengaja tanpa CSS — halaman-halaman itu tidak sepakat soal spacing, jadi komponen memiliki markup + nama kelas dan stylesheet halaman tetap memiliki tampilannya. `DataTable` dapat prop `columns`/`dataRole`; `ConfirmDialog` membuat label opsional, yang sekaligus memperbaiki `admin/registrations.astro` — satu-satunya pemanggil yang tidak mengirim keduanya dan merender dua tombol tanpa teks (`bun run typecheck` = `tsc --noEmit`, yang tidak pernah membaca `.astro`). Hasil terukur: access-users 1005->397, analytics 1123->380, security 1069->399, tenant/domains 1045->371. Keempat spesifikasi E2E-nya lulus TANPA disunting (6 spec dijalankan nyata terhadap server lokal + Postgres, plus `admin-a11y-smoke`). 48 unit test baru menutup cabang penanganan error yang selama ini tidak pernah dieksekusi tes apa pun. Dua temuan ikutan saat memindahkan kode: `tenant/domains` memanggil `withTenant` tanpa `unavailableBehavior: "throw"` (kelas insiden PR #323), dan bentuk closure-assignment di frontmatter `security` menyempitkan `policy` jadi `never` bagi setiap referensi template. Gerbang anggaran baris sengaja TIDAK ditambahkan di sini (menyentuh `scripts/` + `package.json`); dicatat sebagai langkah lanjutan di doc 14. Refs #372 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(quality): pasang gerbang analisis statis untuk lapisan .astro Sebelum ini lapisan .astro (51 halaman + 16 komponen, ~19.2k baris skrip browser inline) tidak diperiksa alat apa pun: `tsc` melewati ekstensi tak dikenal secara diam-diam, `bun run lint` hanya `prettier --check`, dan `@astrojs/check` belum terpasang. Yang ditambahkan: - `bun run typecheck:astro` = `astro check --minimumSeverity error` (satu-satunya gerbang yang mengetik-periksa frontmatter .astro DAN isi `<script>` inline). Masuk `bun run check` + langkah CI tersendiri. - ESLint flat config (`eslint.config.mjs`) di dalam `bun run lint`: `no-floating-promises` + `no-misused-promises` (type-aware, .ts), `no-floating-promises` untuk frontmatter .astro, dan aturan sintaks (`js.configs.recommended` + `@typescript-eslint/no-unused-vars`) di dalam blok `<script>` yang diekstrak processor. - `"DOM"` + `"DOM.Iterable"` di `compilerOptions.lib`. Angka terukur pada 0a8c3ba (v1.1.0): `astro check` 34 error (7 berkas) -> 0; ESLint 49 error (48 no-floating-promises + 1 no-misused-promises) -> 0 dengan SATU pengecualian per-berkas eksplisit. Tidak ada `--max-warnings` longgar, tidak ada `@ts-ignore`/`any`, tidak ada aturan yang dimatikan global. `no-unnecessary-condition` sengaja BELUM dinyalakan: 87 temuan di ~40 berkas (termasuk area yang sedang dikerjakan paralel) — didokumentasikan sebagai hutang terukur di doc 07, bukan dilonggarkan diam-diam. Bug NYATA yang ditemukan gerbang baru ini: 1. /admin/registrations mengirim NAMA peran, bukan UUID-nya. `role.id` tidak ada pada `RoleWithPermissions` (yang benar `role.roleId`); `<option>` tanpa atribut `value` membuat `select.value` mengembalikan teks opsi, sehingga approve-dengan-peran selalu mengirim roleIds yang tidak valid. 2. /admin/media merender badge "null B" untuk objek tanpa ukuran (`formatSize(bytes: number)` diberi `number | null`; `null < 1024` bernilai true). 3. /admin/reporting/projections memanggil `showBanner(..., "warning")` padahal union-nya hanya "success" | "error" DAN tidak ada CSS `[data-variant="warning"]` di mana pun — peringatan ketidakcocokan rekonsiliasi tampil tanpa gaya, tak terbedakan dari pesan netral. 4. Dua `<ConfirmDialog>` dirender tanpa prop wajib `confirmLabel`/ `cancelLabel` (kosmetik: label ditimpa saat dialog dibuka). 5. 48 promise `Bun.serve(...).stop(true)` tanpa `await` di teardown test (kandidat penyebab flakiness port/timing). Selain itu 26 dari 34 error `astro check` berasal dari satu pola: `let x: T | null = null` yang ditulisi DARI DALAM callback `withTenant` — analisis alur TypeScript tidak melihat penulisan itu sehingga `x` menyempit jadi `never` di seluruh template. Diperbaiki dengan mengembalikan nilai sebagai return value callback, yang otomatis menuntut `{ unavailableBehavior: "throw" }` (AGENTS.md #8, kelas bug Perubahan devDependency: `typescript` diturunkan `^7.0.2` -> pin eksak `6.0.3`. TypeScript 7 adalah kompiler native yang tidak lagi mengekspor API programatik, sehingga `astro check` menolak jalan dan `typescript-eslint` (peer `<6.1.0`) tidak bisa dipakai sama sekali. `tsc --noEmit` tetap nol error, hanya 0,8 s -> 6,9 s. Refs #369 * fix(quality): karantina toolchain TS 6 agar root tetap TypeScript 7 Revisi atas commit sebelumnya: menurunkan `typescript` root ke 6.0.3 adalah arah yang salah. `tsc --noEmit` memeriksa 100% pohon `.ts` (seluruh src/lib, src/modules, scripts, tests), sementara `astro check` hanya menyentuh `.astro` — melemahkan gerbang yang lebih luas demi tooling yang lebih sempit tidak sepadan. Root dikembalikan ke `typescript ^7.0.2` (terbukti: `tsc --version` => 7.0.2, `bun run typecheck` 0,8 s, 0 error). Toolchain yang masih terikat API programatik TypeScript dikarantina di `tools/static-analysis/` dengan package.json + lockfile + node_modules sendiri berisi `typescript` 6.0.3 (pin eksak), `@astrojs/check`, `eslint`, `typescript-eslint`, `eslint-plugin-astro`, `eslint-plugin-jsx-a11y`. Bukan anggota workspace Bun, jadi TS 6 tidak ter-hoist ke root (terbukti: `bun run static-analysis:versions` => root 7.0.2, quarantine 6.0.3). `scripts/static-analysis.ts` menjalankan biner karantina dengan cwd = root repo, sehingga masing-masing me-resolve TypeScript dari pohonnya sendiri: - `bun run typecheck:astro` -> astro-check (1370 berkas, 0 error) - `bun run lint:eslint` -> eslint (1372 berkas dipindai, 0 masalah) - `bun run static-analysis:quarantine:check` -> GAGAL begitu `peerDependencies.typescript` toolchain yang terpasang menerima 7.x, jadi tidak ada yang perlu mengingat kapan workaround ini dicabut. Offline, deterministik, ikut `bun run check` + CI. Asersi cakupan, bukan sekadar "scanned > 0". Varian `basePath` yang disarankan literatur diukur me-lint TEPAT 1 berkas lalu exit 0 tanpa pesan apa pun — "> 0" akan lolos. Runner-nya menghitung berkas sumber nyata (1365) dan menolak hasil di bawah 90% darinya. Dibuktikan dengan mutation test: config lumpuh -> "hanya memeriksa 1 berkas, padahal pohon sumber punya 1365 (minimum 1228). Gerbang MATI, bukan hijau", exit 1. Konsekuensi karantina yang jujur dicatat: type-aware ESLint pada frontmatter `.astro` menjadi mustahil, karena `astro-eslint-parser` me-resolve `typescript` dari `process.cwd()` (= root = TS 7) saat membangun program tipe. Nol temuan hilang (aturan itu memang 0 temuan), dan `astro check` tetap mengetik-periksa frontmatter sepenuhnya. Blok `<script>` tetap di-lint (aturan sintaks) — dibuktikan ulang dengan mutation test setelah pemindahan. Dua jebakan tambahan yang ditemukan saat memindahkan konfigurasi: `src/**/*.ts` juga cocok dengan path virtual `x.astro/1_1.ts` sehingga aturan type-aware ikut menempel di sana (butuh `ignores`), dan `parserOptions` flat-config di-merge dangkal sehingga `project: null` saja tidak mematikan `projectService: true` warisan blok sebelumnya. Semua pekerjaan lain dipertahankan apa adanya: 5 bug nyata, daftar pengecualian per-berkas, keputusan tidak mengaktifkan `no-unnecessary-condition`, dan langkah CI. Refs #369 * chore(quality): tegakkan gerbang tenant-route + sinkronkan skill dengan arsitektur baru Gerbang `api:tenant-route:check` (Issue #370) sebelumnya bisa dijalankan tapi tidak ditegakkan — agent yang membuatnya tidak boleh menyunting baris `check` bersama. Sekarang terpasang, sehingga daftar `NOT_YET_MIGRATED` benar-benar hanya bisa menyusut. Tiga skill disinkronkan: `awcms-micro-new-endpoint` (defineTenantRoute wajib untuk rute tenant-scoped baru), `awcms-micro-new-module` (lapisan `presentation/` + batas `src/lib` per ADR-0038), `awcms-micro-ui-screen` (pola halaman `.astro` tipis, angka nyata hasil #372). Sekalian membuang dua klaim basi di `awcms-micro-new-endpoint`: topologi "LAN-first" (dihapus ADR-0034/0036) dan pernyataan bahwa rute publik tenant-scoped belum punya implementasi contoh. Skill tidak tercakup `bun run check`, jadi keduanya bertahan sejak refactor lama. Refs #369 #370 #371 #372 * fix(quality): perbaiki gerbang ESLint yang gagal karena ukuran laporan, lalu tangani 18 temuan yang tersembunyi di baliknya Gerbang `lint:eslint` dari #369 tidak pernah benar-benar melaporkan isinya di pohon terintegrasi. Dua sebab, keduanya membuatnya merah karena alasan yang tidak ada hubungannya dengan kode: 1. `ignores` tidak memuat `.claude/**`, sehingga ESLint memindai seluruh worktree agent paralel BESERTA keluaran `dist/`-nya. Pola flat-config tanpa awalan `**/` bersifat root-anchored, jadi `dist/**` tidak menangkap `dist` bersarang. CI tidak punya worktree, jadi cacat ini hanya menggigit secara lokal — justru karena itu ia harus dipatok. 2. `Bun.spawnSync` MEMOTONG pipa stdout untuk laporan JSON sebesar ini (~350 KB), sehingga parse-nya meledak di tengah token dan gerbang melaporkan "JSON tidak valid". Laporan kini lewat `-o` ke berkas, sehingga ukuran laporan tidak bisa lagi menentukan vonis. Setelah gerbangnya jujur, ia menemukan 18 `no-misused-promises` — semuanya di berkas `presentation/` BARU, yaitu kode yang baru terlihat oleh ESLint justru karena #372 memindahkannya keluar dari `.astro`. Tesis epik #373 membuktikan dirinya sendiri. Semua 18 adalah `addEventListener("submit", async …)`: DOM menerima promise yang tidak pernah dilihat siapa pun, jadi kegagalan menjadi unhandled rejection dan pengguna tidak melihat apa-apa — kelas kegagalan senyap yang sama dengan #359/#361. Ditangani lewat satu chokepoint `asyncHandler()` di `admin-form-client.ts`; island komentar PUBLIK memakai kembarannya secara lokal agar bundel publik tidak menyeret klien admin. `i18n/messages.pot` diregenerasi (referensi baris bergeser karena kode pindah berkas) dan `api:tenant-route:check` kini bagian dari `bun run check`. Refs #369 #370 #371 #372 * fix(security): tangani temuan ronde review PR #374 Reviewer + security auditor berjalan atas PR ini. Nol CRITICAL, nol BLOCKER. Yang diperbaiki di sini adalah temuan yang berada di berkas yang memang sudah dibuka PR ini. XSS laten — `ClientJsonData.astro` (muncul di KEDUA review). `JSON.stringify` tidak meng-escape `<`, sehingga payload berisi `</script>` menutup data island dan sisanya diparse sebagai HTML hidup. Nol call site eksploitabel hari ini (semuanya string i18n milik repo), tapi komponen ini mengiklankan diri menerima "payload JSON apa pun" dan 35 halaman admin adalah kandidat pemakainya. Serializer dipindah ke `src/lib/ui/client-json-data.ts` agar bisa diuji + 3 unit test dengan payload `</script>` sungguhan. Kelas #323 di rute PUBLIK — tiga pemanggil non-`Response` di modul `theming` memanggil `withTenant` tanpa `unavailableBehavior: "throw"`. Pada `/theming/tokens.css` pool jenuh berarti `[object Response]` tersaji sebagai CSS di setiap page load. Kini degradasi ke tema default / preview `null`. Pra-ada, tapi PR ini menyapu kelas yang sama di `admin/tenant/domains` dan melewatkan tiga ini di berkas yang ia sentuh sendiri. Pengerasan gerbang, semuanya kelas "hijau tapi mati" yang PR ini justru melawan: - `validate-module-graph.ts` — `src/lib` tak terbaca kini MELEMPAR, bukan melaporkan pohon kosong yang lolos; lookup tabel pakai `Object.hasOwn`. - `static-analysis.ts` — laporan tmp pakai `mkdtemp` (path lama bisa ditebak dan jadi target symlink lewat `-o` ESLint); pembersihan dipindah ke sebelum `fail()` karena `process.exit()` melewati `finally`. - `api:tenant-route:check` jadi langkah CI eksplisit — CI menjalankan gerbang satu per satu, jadi memasangnya di `bun run check` saja tidak cukup. - Daftar pengecualian ESLint dikosongkan; entrinya sudah basi di PR yang sama. - `link-check.ts` — filter skema jadi ALLOW-list (deny-list lama melewatkan `file:`), userinfo dibuang dari laporan JSON. Koreksi ADR-0038. Klaim "nol impor relatif lintas-modul di seluruh src/modules" SALAH dan saya yang menulisnya. Pengukuran ulang menemukan ~80 impor semacam itu yang sudah ada jauh sebelum ADR ini. Yang benar: tidak ada SIKLUS antar modul, dan yang dijaga hari ini adalah dependensi yang DIDEKLARASIKAN — bukan impor tingkat berkas. Lubang itu kini tercatat sebagai pekerjaan lanjutan, bukan diklaim selesai. Refs #369 #370 #371 #372 #374 --------- Co-authored-by: AWCMS-Micro Security <security@awcms-micro> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
/admin/sidebar-menu(and other admin pages) returned HTTP 500 in production. Container logs showed:Astro.locals.ssrContextwas truthy but had nopermissions. The 4 occurrences were interleaved 1:1 with 4database.pool.saturatedwarnings in a single 90-second window — the crash only happens during pool saturation.Root cause
withTenant<T>()'s pool-gate fallback returns a503 DATABASE_BUSYResponsecast to the genericT(its docblock even notes "in practice every real call site usesT = Response"). That's correct for API routes that return it to the client — but:resolveSsrContextcallswithTenantwithT = SsrContext(a plain object)T= their data objectUnder saturation / circuit-open, those callers get a
Responseinstead of their data. It's truthy, soif (!ctx)/ theirtry/catchdon't catch it, and the template then dereferences it:context.permissions.has(...)→undefined.has→ 500; likewiseResponse.types.map(...)inAdminLayout, which runs on every admin page.Fix
withTenant: new opt-in optionunavailableBehavior: "throw"(default"response"— API routes unchanged) makes the three pool-gate fallbacks throw the new exportedDatabaseUnavailableError(carriesretryAfterSeconds) instead of returning aResponse.resolveSsrContext+ middleware: the leakedResponseis surfaced and served as a proper 503 withRetry-After, not stored asssrContextand not a misleading/loginbounce (the session may be valid; the DB is just busy).unavailableBehavior: "throw", so their existingtry/catchdegrades gracefully (fallback nav /loadErrornotice) under saturation instead of 500ing.DatabaseUnavailableErrorfor non-Response callers (tests/unit/tenant-context-circuit-breaker.test.ts, DB-free).Verification
bun test tests/unit/tenant-context-circuit-breaker.test.ts→ 14 pass (+3 new)bun run typecheck,bun run lint,bun run format,astro build→ cleansrc/pages/api/**route touched (they keep the Response-returning default)🤖 Generated with Claude Code