Skip to content
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
ahliweb merged 1 commit into
mainfrom
fix/withtenant-response-leak-admin-500
Jul 23, 2026
Merged

ahliweb merged 1 commit into
mainfrom
fix/withtenant-response-leak-admin-500

Conversation

@ahliweb

@ahliweb ahliweb commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Problem

/admin/sidebar-menu (and other admin pages) returned HTTP 500 in production. Container logs showed:

TypeError: undefined is not an object (evaluating 'context.permissions.has')
  at sidebar-menu ...:28
  at modules ...:26

Astro.locals.ssrContext was truthy but had no permissions. The 4 occurrences were interleaved 1:1 with 4 database.pool.saturated warnings in a single 90-second window — the crash only happens during pool saturation.

Root cause

withTenant<T>()'s pool-gate fallback returns a 503 DATABASE_BUSY Response cast to the generic T (its docblock even notes "in practice every real call site uses T = Response"). That's correct for API routes that return it to the client — but:

  • resolveSsrContext calls withTenant with T = SsrContext (a plain object)
  • SSR page/layout renders call it with T = their data object

Under saturation / circuit-open, those callers get a Response instead of their data. It's truthy, so if (!ctx) / their try/catch don't catch it, and the template then dereferences it: context.permissions.has(...) → undefined.has → 500; likewise Response.types.map(...) in AdminLayout, which runs on every admin page.

Fix

  • withTenant: new opt-in option unavailableBehavior: "throw" (default "response" — API routes unchanged) makes the three pool-gate fallbacks throw the new exported DatabaseUnavailableError (carries retryAfterSeconds) instead of returning a Response.
  • resolveSsrContext + middleware: the leaked Response is surfaced and served as a proper 503 with Retry-After, not stored as ssrContext and not a misleading /login bounce (the session may be valid; the DB is just busy).
  • 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 instead of 500ing.
  • Regression tests cover all three fallback paths throwing DatabaseUnavailableError for 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 → clean
  • Confirmed no src/pages/api/** route touched (they keep the Response-returning default)

🤖 Generated with Claude Code

…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>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@ahliweb
ahliweb merged commit 69bd35c into main Jul 23, 2026
8 checks passed
@ahliweb
ahliweb deleted the fix/withtenant-response-leak-admin-500 branch July 23, 2026 16:40
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
`&amp;` 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>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant