Repository navigation
Releases
v0.16.3
AW Business Platform Wave A documents and owner decisions
The owner answered decisions O1 to O12 for epic #280 on 10 October 2026. They also chose an upstream Booking v1 scope change for nightly stays (W0, ahliweb/awcms#931). The awcms-one Wave A documents were written from these decisions:
docs/aw-business-platform-prd.md: platform blueprint and PRD for the awcms-one-owned parts (A6, #336).docs/aw-business-platform-metrics.md: metric contracts for revenue, occupancy, utilization, retention and productivity (A7, #337).docs/aw-business-platform-threat-model.md: adapter threat model, privacy analysis and regulatory applicability notes. It makes no compliance claim (A8, #338).docs/aw-business-platform-dor.md: records the answers and the A9 cross-spec review (#339).- The
awcms-one-commerceskill now says that commerce domain-event consumers are declared in the module descriptor (ADR-0134 inapps/cms/docs/adr/), not in upstream'sconsumer-registry.ts.
This change is documentation only: it adds no code, module, migration or OpenAPI path (ADR-0040 D7).
Sync apps/cms to AWCMS v10.7.0 and adopt descriptor-declared domain-event consumers
Subtree sync of apps/cms from upstream 66c74273 (AWCMS v10.7.0, 7 commits; issue #347). No new migration.
- Upstream ADR-0134 (ahliweb/awcms#926): consumers are declared in
ModuleDescriptor.domainEventConsumers(module contract 4.2.0) and the runtime builds its registry from the composed module list; the newdomain-events:consumers:checkgate is in theapps/cmscheck chain. - This repo's standing divergence in
domain-event-runtime/infrastructure/consumer-registry.tsis retired: upstream's file is taken unchanged and the fourcommerceconsumers (commerce.order_paid_entitlement_grantor,commerce.order_paid_loyalty_earner,commerce.order_cancelled_loyalty_reverser,commerce.inventory_stock_cache_projector) are declared in the commerce module descriptor with unchanged names. Thedomain_event_runtime -> commercemodule-boundary exception is gone. - Wave A design packs for booking, hr_payroll and delivery plus provisional cross-domain AsyncAPI events arrive as documentation.
AGENTS.md/AGENTS.id.md"Last sync" row and divergence list, anddocs/aw-business-platform-dor.mdDoR item 9, are updated.
v0.16.2
ADR-0040 accepted by the owner
Owner decision O0 on issue #335 accepted ADR-0040 (AW Business Platform capability ownership and boundaries) as written: D1, D2 and D4 stand unamended, so Booking, the hr_payroll family and generic delivery stay upstream-first in ahliweb/awcms (#915 to #918).
- ADR-0040 and its Indonesian mirror are set to Accepted with a decision record; the ADR index row follows.
docs/aw-business-platform-dor.mddrops the answered O0 row and marks Wave A item A1 done. O1 to O12 remain open, and A6 to A8 stay blocked on them.- Documentation only: no code, module, migration or OpenAPI path is added (ADR-0040 D7).
Sync apps/cms to upstream AWCMS v10.6.0
apps/cms now embeds ahliweb/awcms main at 61507bb7 (AWCMS v10.6.0), two commits past the previous sync (a0dabcd9).
idn-admin-regionsdataset rollback (ahliweb/awcms#914, #922):now()is the transaction start, so the active dataset could be stamped equal to or earlier than the one it superseded, and the strictactivated_at <filter then found nothing to roll back to. Rollback now takes the latest superseded dataset by(activated_at, id)with no comparison. Covered by a new integration test.- Version bump to 10.6.0 with upstream's
CHANGELOG.mdandPROJECT_STATEupdates. No new migrations, no dependency changes. - No standing local divergence was resolved or added. The only conflict was in
apps/cms/docs/PROJECT_STATE.mdand its Indonesian mirror (upstream's version and release-note rows against this embed's module and migration counts); both lineages were kept and the mirror restamped.
v0.16.1
AW Business Platform: capability ownership ADR and DoR tracker (epic #280, Wave A)
Documentation only; no code, migration or module is added. ADR-0040 (Proposed) re-validates the epic's reuse/extend/new matrix against the current tree and places each capability: upstream ahliweb/awcms first (Booking, hr_payroll, generic delivery), awcms-one adapters after the upstream sync, consumer repositories for vertical semantics. It also records that ADR-0029 already settles the invoice boundary.
- New:
docs/adr/0040-aw-business-platform-capability-ownership-and-boundaries.mdand its Indonesian mirror; ADR index rows added. - New:
docs/aw-business-platform-dor.md(and mirror), the ten DoR artifacts with owner repository, status and owner decisions, plus the proposed Wave A child issues. docs/status.md"Not here yet" anddocs/README.mdpoint to the tracker.
Commerce orphan detection reads balances through the inventory port
listLedgerOrphans (the orphans section of the stock reconciliation) no longer SELECTs awcms_inventory_balances directly. It pages InventoryLedgerPort.listBalances (synced from upstream awcms#913) and classifies each page against commerce's own tables in one query, so commerce no longer reads any inventory table. The report is unchanged: same reasons, same ordering, same ORPHAN_LIMIT cap.
- The walk is bounded at
ORPHAN_SCAN_MAX_PAGES(100 pages of 500 balances); stopping at that cap setstruncated: true, which the old unbounded query never needed. - The
sql/947workerSELECTgrant onawcms_inventory_balancesis now unused by commerce and is deliberately left in place. - ADR-0038's "Orphan detection" paragraph and the commerce README are updated.
apps/cms synced to ahliweb/awcms a0dabcd
apps/cms now carries upstream main at a0dabcd9: the sharp, shell-quote and source-map-js advisory fixes (ahliweb/awcms#920) and InventoryLedgerPort.listBalances, a paged, read-only listing of ledger balances (ahliweb/awcms#913). No new migration, no conflict beyond the regenerated apps/cms/docs/awcms/repo-inventory.md.
v0.16.0
Commerce bundles: item kits stocked through their components (issue #290, ADR-0036)
A shop can now sell a set - "two coffees and a syrup at one price" - without a stock counter on the set that somebody keeps in step by hand. A bundle is a product with kind = 'bundle': no variants, no stock of its own, not a service product, not flash-sale eligible, and made of 1-20 component lines (a product, a variant when that product has live variants, and a quantity per bundle). Nesting is refused by trigger, so a cycle cannot exist. Decision and rejected alternatives: docs/adr/0036-bundles-are-component-stocked-products-sold-as-one-line.md.
- New:
sql/953tosql/955underapps/cms-kind/bundle_pricing/bundle_discount_percentonawcms_commerce_products,awcms_commerce_bundle_components(composite tenant FKs, FORCE RLS, the nesting/variant/flash-sale triggers), the append-onlyawcms_commerce_order_item_componentssnapshot, and theawcms_workergrant the expiry restock needs. Existing tenants need no backfill; every product isstandard. - Pricing is
fixed(the product's own price) orderived(the components' list prices x quantity, less a percent, half-up to the cent). Availability ismin floor(component stock / quantity)and is reported as the bundle'sstock, so the storefront, POS search and barcode scan need no new field. - A bundle sells as one order line with an immutable component snapshot (units, text as sold, the line value split across components to the cent). The components - never the bundle - move stock: a counter decrement after locking and re-quoting in
countermode, asaleper component with source line<orderItemId>:c<position>through the inventory adapter inledgermode. Cancel, expiry and a whole-bundle return put exactly those components back from the snapshot; editing the bundle afterwards changes nothing about an old order. Tax is one line by the bundle's own category. - API:
POST/PATCH /api/v1/commerce/productsacceptkind,bundlePricing,bundleDiscountPercentandbundleComponents(ids or SKUs); product reads gainkind,bundlePricing,bundleDiscountPercentandbundle; a cart quote line for a bundle carriesbundle.components. No new permission (commerce.products.*). - Admin: a Bundle toggle, pricing strategy, discount and a
SKU x quantitycontents textarea on the product form. Storefront: an "Isi paket" list on the product page. - Not here (recorded in the ADR): per-component tax classes, partial component return, a bundle report, nested bundles, a cart-level combined component check in
countermode. - Backward compatible. Nothing changes for an existing product or order.
Commerce stock on the upstream inventory ledger (issue #282, ADR-0038)
A tenant's commerce stock was one integer on each product and variant. Upstream AWCMS now ships a multi-location stock ledger (module inventory, embedded by the v10.5.0 sync), and a store can move onto it without a second source of truth: in the new ledger mode the ledger is authoritative and stock becomes a write-through cache of it, so the storefront, cart quote, POS, barcodes and admin lists read exactly what they read before. The default, counter, is today's behaviour byte for byte. Decision and rejected alternatives: docs/adr/0038-commerce-stock-is-a-write-through-cache-of-the-inventory-ledger.md; operator runbook in docs/deployment.md.
- New:
apps/cms/sql/947_awcms_commerce_inventory_adapter.sql—inventory_mode/inventory_location_id(composite FK to the inventory location) /inventory_mode_changed_atonawcms_commerce_store_settings, twocommerce.inventory.{read,configure}permissions, and theawcms_workerinventory grants the expiry job and the cache projector need (a column-levelUPDATE (updated_at)on locations, never a table-level one). Existing tenants runbun run identity-access:permissions:backfillto receive the permissions. - New:
bun run commerce:inventory:cutover --tenant <id> --location <id> [--commit]— dry-run by default; one transaction posts anopeningper stock unit, flips the mode and verifies, behind an exclusive per-tenant mode lock. A second run is refused. - In
ledgermode an order or POS sale postssale, a cancel/expiry restock and a return's restock postsale_return, through the ledger port inside the caller's transaction, sorted against deadlocks and in a savepoint: an out-of-stock cart is still409 CART_CHANGED/PosCartChangedErrorwith nothing written, and eight parallel orders for the last unit sell exactly one. Any other ledger refusal is409 INVENTORY_UNAVAILABLE. - A movement made by something else (a receipt, an adjustment, a transfer) reaches the cache through the new
commerce.inventory_stock_cache_projectorconsumer, which re-reads the ledger and never trusts the payload. This is a recorded local divergence inconsumer-registry.ts(seeAGENTS.md). - A product/variant edit, a create with stock, or a CSV stock change that would change a count is
409 STOCK_MANAGED_BY_INVENTORYinledgermode (the unchanged value is accepted). - New endpoints:
GET /api/v1/commerce/inventory,GET …/inventory/reconciliation,POST …/inventory/resync,POST …/inventory/rollback(commerce.inventory.read/.configure).commercenow declares a dependency oninventory. - Low stock is upstream's
inventory.low_stockprojection, not a commerce report (supersedes the ADR-0035 D1 note for #282). - Backward compatible, opt-in per tenant. Nothing changes until an operator runs the cut-over;
POST …/inventory/rollbackreturns a tenant to the counter.
Procurement feeds commerce stock (issue #283, ADR-0038 addendum)
Upstream awcms's procurement module (suppliers, receiving, supplier returns, requisitions, transfers; upstream ADR-0128) already posts to the inventory ledger. This change makes it work with commerce stock on a tenant in ledger mode, with no new table or migration:
- Convention. A procurement line stocking commerce goods uses
itemTypecommerce.variant(variant uuid) orcommerce.product(uuid of a product with no live variant), unitunit, at the sales location (or another location and then atransfer). NewGET /api/v1/commerce/inventory/items?q=<sku or name>(commerce.inventory.read, keyset-paged, at most 50) resolves a SKU or name to{ itemType, itemRef, sku, name, variantName, unitCode }. - Orphan detection.
GET /api/v1/commerce/inventory/reconciliationnow also returns, on its first page,orphans: non-zerocommerce.*ledger balances at the sales location naming no live unit (not_found,product_has_variants,wrong_unit,unknown_item_type), capped at 100 withtruncated. - Counter mode. A procurement receipt on a
countertenant stays in the ledger and does not change commerce stock - cut over toledgerfirst (runbook indocs/deployment.md; existing tenants runidentity-access:permissions:backfillforprocurement.*). - Reporting. Upstream's
procurement.receiving/procurement.suppliersprojections are the receiving reports (supersedes the ADR-0035 D1 note for #283). - Backward compatible. The
orphansfield is additive; nothing changes for a tenant that never uses procurement.
Commerce tax moves onto the tax module, per tenant (issue #293, ADR-0039)
Until now a store's tax was one number, payment.tax.percent, applied once to subtotal - voucher discount. Upstream AWCMS's generic tax module (awcms ADR-0127, arrived with the v10.5.0 sync) can express what that number cannot — a rate that changes on a date, exempt and zero-rated categories, prices that already include tax, and a refund that takes back exactly the tax that was charged. This change is the adapter, behind a per-tenant mode so nobody moves until an operator moves them: flat (the default) is the existing arithmetic byte for byte, engine asks the module.
- New:
awcms_commerce_store_settings.tax_mode/tax_profile_code,awcms_commerce_products.tax_category_codeandawcms_commerce_orders.tax_snapshot_id(a composite FK to the tax snapshot ledger), all inapps/cms/sql/948_awcms_commerce_tax_adapter.sql— nullable or defaulted, nothing backfilled.commercenow declarestaxin its module dependencies. - In
enginemode the cart, POS and quotation quote use the module's calculator (the store'sAsia/Jakartabusiness date picks the effective-dated version); a voucher is allocated across the lines in cents so, with the same percentage, the engine equals the flat figure exactly — proven by a seeded property test. A storefront or POS order finalises exactly one tax snapshot in its own transaction and stores its id; a return reverses the returned units' tax, and a cancelled or expired order the rest, from the ORIGINAL snapshot, never from today's rule. A tax the engine cannot answer blocks checkout instead of selling untaxed. - New ops-only
bun run commerce:tax:cutover(dry-run by default;--commit,--tenant,--sample,--since,--rollback): derives thestore-defaultversion from the current settings, re-prices the tenant's recent orders in shadow and refuses to flip unless every one matches to the cent. The flip and its rollback are audited (tax_mode.update). Runbook:docs/deployment.md, includingbun run identity-access:permissions:backfillso existing tenants' owners hold thetax.*permissions. - Additive API shapes: the cart quote's
taxgainsmode,inclusive,errorandengine; admin store settingsGETgains the read-onlytaxMode/taxProfileCode; product create/update/admin read gaintaxCategoryCode. The product form has a Tax category field; store settings shows the mode. awcms_workergainsINSERT, UPDATEonawcms_tax_snapshotsso the expiry job can reverse an expired engine order's tax (the append-only trigger still refuses real updates); a setting...
v0.15.0
Close the devalue/fast-uri advisories; accept the unfixable http-cache-semantics one with a date
New advisories against transitive dependencies turned main red on
local-ci/check-toko (bun audit) and local-ci/check-cms
(deps:audit:check) without any change in this repository — every open PR
inherited the failure.
devalue5.9.2 → 5.9.3 andfast-uri3.1.7 → 3.1.8, both inside every
dependent's declared range, sobun audit fixclosed them inbun.lock
alone.http-cache-semantics(GHSA-ch52-4w7c-c8xp) has no fixed release at all.
Its only consumer isastro's build-time remote-image cache, never a shared
request-time cache, so it is accepted as a dated, reasoned exception (review
by 2026-11-03) inapps/cms'sdeps:audit:checklist and in a new root
tools/ci/dependency-audit-exceptions.json, whichlocal-ci/check-tokoand
local-ci/security(the two legs that runbun audit) now pass to
bun audit --ignore, and which fails either leg once an entry's review date
has passed.
Typed catalog attributes, safe attribute filtering, validated CSV import/export
Issue #291 (epic #281, OSPOS parity). The catalog gains tenant-defined, typed custom attributes, attribute filtering that cannot be turned into SQL injection, and a CSV import/export that validates before it writes. Everything is inside the existing commerce module (ADR-0008) and takes migrations 960–964 of the reserved range (ADR-0015). The decisions and the measured query plans are in ADR-0027.
- Attributes are typed columns, not a JSON blob. Definitions (
key, labels, a closed per-type constraint schema, searchable/filterable/visibility flags, product/variant applicability) and values (text, whole number, decimal, boolean, date, choice list) with a strict ASCII grammar:1234.5is a number,1,5is a 400 — a persisted number never depends on a locale. - Filtering is allowlisted by construction.
attr=<key>:<op>:<value>resolves the key to a definition id in the database, parses the operand with the same typed grammar, and maps each (type, operator) pair to one literal SQL template with bound parameters. There is no code path from a request to a SQL identifier or expression, and a test throws injection payloads at keys, operators and values. - The storefront API only ever sees public attributes. Additive
attributes[]on products and variants,filterable && visible_publicfiltering, andqsearch over searchable attributes of the same audience; existing fields are untouched. - Import is dry-run → report → apply, all-or-nothing and replay-safe. Same planner for both, one savepoint,
Idempotency-Key,expectedSha256to bind apply to the reviewed file, matched onsku, no media column, no network. Export neutralises spreadsheet formulas. - A new
importaccess action (high-risk, besideexport), permissionscommerce.attributes.read|manageandcommerce.products.import|export. - Operators: run
bun run db:migrate(adds five migrations, no data change). Grant the new permissions to the roles that manage the catalog; existing tenants do not gain them automatically. - Review hardening. The admin
qsearch and the CSV export only reach non-public attributes for a caller who holdscommerce.attributes.read; the import accepts exactlytext/csv(no CORS-safelistedtext/plain); an apply claims its Idempotency-Key before writing, so concurrent applies replay or 409 instead of failing with a 500.
Held sales, quotations, work orders, and numbered receipts/invoices (issue #286, ADR-0029)
The sale had no documents around it: no way to park a cart, quote a price, track a service job, or print a numbered receipt or invoice, and the OSPOS way of getting those (one overloaded sale row) was ruled out. They now exist as separate records around the unchanged order, with seven tables (awcms_commerce_document_sequences, …_held_sales, …_quotations, …_quotation_versions, …_work_orders, …_work_order_events, …_documents — migrations 980–982 under apps/cms/sql/, starting at apps/cms/sql/980_awcms_commerce_documents_schema.sql). The "why", the invoice-versus-accounts-receivable boundary and the options weighed are in ADR-0029.
- New: park a POS cart and resume it (lines only — no price, no stock reserved, owned by the cashier, expires after 24 hours by default, single-use resume); create a priced, versioned quotation with a validity period, accept it (which pins the version shown) and convert it into an order through the ordinary POS path (created at today's price with the balance due; a changed total is refused unless accepted; exactly one order per quotation, race-safe and idempotent); run work orders through a status machine with an append-only history, optionally from an accepted quotation; issue a numbered, immutable receipt or invoice for a finalized order (
INV-2026-000042— gapless per tenant, type and UTC year, allocated in the same transaction as the row) and render it as JSON, a 42-column plain-text receipt, or a print-ready, script-free HTML page under a locked-down CSP. All mutations take anIdempotency-Key; eventsawcms.commerce.quotation.{accepted,converted},awcms.commerce.work_order.status_changed,awcms.commerce.document.issued; admin screen/admin/commerce-documentsand hold/resume controls on/admin/commerce-pos. - No second monetary authority: a document's money is a copy a database trigger verifies against the order, and a quotation never writes an order itself. A commercial document is not an accounts-receivable invoice — that stays a future, separate record that will reference a document.
- Thirteen new permissions, none implied by
commerce.pos.createor by each other:commerce.held_sales.{read,create,update,approve},commerce.quotations.{read,create,update},commerce.quotation_conversions.create(a conversion also needscommerce.pos_due.create),commerce.work_orders.{read,create,update},commerce.documents.{read,create}. Existing tenants do not gain them retroactively. - Backward compatible, opt-in. The whole surface is behind the new
documentsfeature flag (commerce settings → Features), which defaults OFF: with it off every new route answers409 FEATURE_DISABLED, the sidebar entry is hidden, and checkout, POS and the order APIs behave exactly as before (no existing table gained a column). - Hardening:
awcms_applosesDELETEon all seven tables (andUPDATEon versions, events and documents); composite tenant-safe FKs; guard triggers freeze identity/provenance and allow only legal status moves;security-readiness.tsasserts the exact privilege sets. - Not here yet (ADR-0029 Deferred): an accounts-receivable invoice and credit notes, voiding or replacing a document, a receipt per payment, e-mail/WhatsApp/PDF delivery, a tenant-timezone numbering year, stock reservation by a quotation.
Loyalty points: an append-only ledger with earn, redeem, expiry, reversal and reconciliation (#289)
commerce gains a loyalty/rewards capability designed as an auditable points ledger, not a mutable points column (ADR-0026). The "why": the epic's rules (#281) are exact arithmetic, no destructive edits, payments and points as separate ledgers, idempotency on every high-risk write — so a balance is the sum of immutable facts, kept in the same transaction as each insert under a row lock, and every correction is a compensating row.
- Migrations
sql/950–952:awcms_commerce_loyalty_programs(versioned, effective-dated earn rules),_accounts(projected balance) and_ledger(append-only —awcms_appis revokedUPDATE/DELETE, a trigger rejects everyUPDATE; per-tenant unique idempotency key; composite tenant-safe foreign keys), FORCE RLS, worker grants, four permissions. - Earn and reversal are driven by the
order.paid/order.cancelleddomain events (two newdomain_event_runtimeconsumers), so no order, POS, pricing or payment-webhook code changed. Exactly once per order, from the order row'ssubtotal - discountin integer cents with FLOOR rounding, under the program version effective atpaid_at. Walk-in customers never earn. commerce:loyalty:expire(hourly, append-only, idempotent) and a read-onlycommerce:loyalty:reconcile(daily); an audited repair that rewrites only the projection.- Owner API (programs, accounts, ledger, redeem, adjust, summary, reconcile), a bearer-secured customer balance/history endpoint, and the
/admin/commerce-loyaltyscreen. OpenAPI and AsyncAPI updated. - Behaviour change to know about: a sixth feature flag,
features.loyalty, defaults to off — nothing changes for a tenant until it opts in. - Permissions are
commerce.loyalty.{read,manage},commerce.loyalty_adjustments.createandcommerce.loyalty_redemptions.create, notloyalty.adjust|redeem, so the upstream-ownedAccessActionunion is not widened (a new subtree divergence). awcms_workergainsSELECTonawcms_module_settings(the earn consumer reads the feature flag).- Deferred, with reasons in the ADR: redemption into checkout pricing/POS tender (needs #285), tiers, per-line return compensation (#287), campaign eligibility (#280), a storefront balance page.
- Review hardening. Reconcile repair now rewrites only complete ledger histories (an account whose early rows were purged is reported
unrepairable_history_purged), and an earn whose key belongs to another account is a recordedskipped_conflictinstead of an endless retry.
Commerce payments become an append-only allocation ledger (issue #285, ADR-0025)
An order's payment was one column (payment_method) plus a `payment_status...
v0.14.0
Shared translated label maps for commerce admin enums/statuses (issue #243)
A full sweep of every apps/cms/src/pages/admin/commerce*.astro screen plus
commerce-orders/[id].astro — every <option> text, table cell, status/tone
badge, detail field, and filter-tab label that shows an enum value — found
the same defect repeated across sixteen screens: a raw English/snake_case
value (order.status, session.provider, run.status, …) rendered straight
from the database instead of a translated label, each screen that needed one
either redefining its own ad hoc map or skipping it entirely, so the same
field (order status, e.g.) could read differently depending which of up to
four screens showed it. This change adds the one shared module the follow-up
screen changes adopt, so a status has exactly one Indonesian label everywhere
it appears.
- New
apps/cms/src/lib/ui/commerce-admin-labels.ts:createCommerceLabels(t)
returns one object of 23 label maps — order status, payment status, order
channel, product status, product type, voucher type and status, campaign
status and channel, review status, affiliate status, commission status,
conversation status, customer status, WhatsApp message status, flash-sale
status, popup frequency, webhook-endpoint provider, payment-gateway session
status/provider, payment-event outcome, and the sharedreportingmodule's
projection run/freshness status — eachsatisfies Record<TheEnumType, string>against the domain layer's own exported union, so a future new
enum value is a compile error here until labelled.commerceLabel(map, raw)is the per-row lookup a screen calls; it never throws and falls back
to the raw value. Six tone maps (orderStatusTone,affiliateStatusTone,
commissionStatusTone,reviewStatusTone,reportFreshnessTone,
reportRunStatusTone) are extracted from theSTATUS_TONE/
FRESHNESS_VARIANTmaps that already exist, redundantly, on
commerce-dashboard.astro,commerce-orders/[id].astro,
commerce-affiliates.astroandcommerce-reports.astro— no tone map for
an enum no screen already colour-codes. - Nine of the 23 unions had no exported TS type anywhere:
OrderChannel,
PaymentGatewaySessionProviderandPaymentEventOutcomeare derived from
the owning column's own databaseCHECKconstraint (sql/931,sql/926
twice);ConversationStatus/AffiliateStatus/CommissionStatus/
CustomerStatusare derived via indexed access into an already-exported
record type (AdminConversationRecord["status"], etc.) rather than
redeclared by hand. locales/en.po/locales/id.pogain 26 new msgids under a
# commerce admin labels (#243)comment. The 23 label maps use 54
distinct msgids in total; the other 28 already existed — reused verbatim
where an existing screen orapps/storefront's own
order/affiliate/review/conversation labels already established the exact
wording (e.g.commerce-affiliates.astro's own commission-status filter
already spells "Pending"/"Approved"/"Paid"/"Void";commerce-vouchers.astro,
commerce-customers.astroandcommerce-popup.astro's own<select>
options already spell out every value their respective enum needs).- This issue ships no screen change — the sixteen screens the sweep
found still render their own raw values today. Issues #245/#246/#247 adopt
this module screen by screen; the adopting issues' own PR descriptions are
the place to enumerate exactly whichlabels.<map>key replaces which
file:line.
An accessible, fully-translated confirm dialog replaces window.confirm() on ten commerce admin screens
Part of the commerce admin v2 epic (LK admin v2 parity, issue #242). Eleven
window.confirm() calls across ten apps/cms commerce admin screens —
categories, campaigns (send + cancel), affiliates (approve/pay/void
commission), popup, products, reviews, sliders, testimonials, vouchers,
flash sales — are replaced by one shared, commerce-owned accessible dialog,
with every string an operator reads translated through t().
- New
apps/cms/src/components/CommerceConfirmDialog.astro: a native
<dialog role="alertdialog">, rendered once per page, with
aria-labelledby/aria-describedbywired to its own title/message
regions, an optional (hidden-by-default) note<textarea>region for a
future note-taking confirm, and Cancel/Confirm buttons reusing the
existing.btn-secondary/.btn-primary/.btn-danger/.btn-danger--solid
classes — no new button chrome invented. - New
apps/cms/src/lib/ui/commerce-confirm-dialog-client.ts:
confirmCommerceAction/confirmCommerceActionWithNotedrive the dialog
viashowModal()(focus trap, Escape-to-cancel, backdrop, focus
restoration to the opener all free), explicitly focusing the safe Cancel
button first for adangeraction. Falls back towindow.confirmonly
whenHTMLDialogElement/showModal()is unsupported or the dialog is
missing from the page — the one remaining reference, documented as such.
Pure option-normalization/note-trimming helpers are exported separately
and unit-tested without a DOM. - No English literal ever reaches a screen's
<script>. Every trigger
button (delete/send/cancel/approve/pay/void) carries its own
t()-translateddata-confirm-title/-message/-label/-danger
attributes, rendered server-side with any per-row value (a SKU, a name, a
code) already interpolated via the catalogue's{placeholder}syntax. The
newconfirmFromTrigger(el)reads them back — the ONLY thing a screen's
<script>calls; the purereadConfirmOptions(dataset)half is
unit-tested without a DOM. Natural, professional Indonesian copy for all
thirteen trigger actions is inid.po(e.g. "Hapus produk?" / "Produk
{sku} akan dipindahkan ke sampah dan dapat dipulihkan nanti." / "Hapus
produk"; "Kirim kampanye" is not danger, "Batalkan kampanye" is). - Affiliate commission approve/pay gained a confirmation they never had
before ("Setujui komisi"/"Tandai dibayar", both non-danger) alongside
void ("Batalkan komisi",danger) — a small UX addition, not a backend
change, made possible by the same trigger now existing on all three
buttons. - A campaign send stays non-
danger(styled as the ordinary primary
action); a campaign cancel, every affiliate void, and every delete action
aredanger(styled destructive, Cancel focused first). - No backend change: none of the endpoints these ten screens call accept or
record a reason, so the note variant ships unused here — issue #246
(order-status change) is the first real consumer. apps/cms/scripts/client-asset-budget.ts'sAPP_BUDGET_BYTESraised
259,000 B → 265,000 B for the new component/client module. Moving the
confirm copy server-side ultimately measured LIGHTER than the first,
literal-based pass (262,861 B vs. an interim 264,289 B), since every
screen's own script shrank to a one-lineconfirmFromTrigger(button)
call.- New test:
apps/cms/tests/commerce-confirm-dialog.test.ts— pure helper
coverage (includingreadConfirmOptions) plus a static contract over all
ten screens: nowindow.confirm(or hand-built
confirmCommerceAction({ title, message })literal left, the component
imported and rendered exactly once each, every trigger'sdata-confirm-*
attributes present (checked per occurrence, sincecommission-void-btn
renders twice), no inlinestyle=in the component.
Commerce admin: translate stacked-table column labels
Issue #253. apps/cms/src/styles/admin.css renders each cell's column name
on stacked tables at 767px and below through content: attr(data-label).
Every commerce admin table but commerce-pos.astro (Issue #171's own screen)
set data-label="Status"/"Customer"/"Created"/etc. as bare English
literals, so a phone-width operator on an Indonesian admin saw untranslated
column names even though the table's own <th> header was correctly
translated with t().
- 18 commerce admin screens (
commerce.astro,commerce-affiliates.astro,
commerce-campaigns.astro,commerce-categories.astro,
commerce-customers.astro,commerce-dashboard.astro,
commerce-flash-sales.astro,commerce-inbox.astro,
commerce-orders.astro,commerce-orders/[id].astro,
commerce-popup.astro,commerce-reports.astro,
commerce-reviews.astro,commerce-settings.astro,
commerce-sliders.astro,commerce-testimonials.astro,
commerce-vouchers.astro,commerce-whatsapp.astro) now write
data-label={t("…")}on every table cell, reusing the exact msgid the
matching<th>in the same table already callst()with — the stacked
label always equals the column header now.commerce-pos.astroneeded no
change. - Two cells had no header text to copy directly:
commerce.astro's row-checkbox column (header has no visible text) now
reuses the existing"Select"msgid;commerce-affiliates.astro's
commission table's first data column (header is a status-filter
<select>, not a plain label) gets a new"Affiliate"msgid.
commerce-affiliates.astro's affiliate-rate cell was also corrected from
"Rate"to the header's own"Rate (%)"msgid, which it had silently
drifted from. - New msgid:
"Affiliate"(English + Indonesian"Afiliasi"). apps/cms/scripts/client-asset-budget.ts: no change to
APP_BUDGET_BYTES— every cell already renders a translated string via
the existingt()catalogue, so this adds no new script weight. The
constant's docblock does record that this issue was checked against it.- Tests: new
apps/cms/tests/commerce-data-label-i18n-253.test.ts— a
repo-wide regression guard forbidding a literaldata-label="…"on any
commerce admin screen (present or future), plus targeted assertions for
the"Select"/"Affiliate"/"Rate (%)"cases above.
apps/cms/tests/commerce-settings-save-bar.test.ts(Issue #244) updated
to expect t...
v0.13.1
Apply Debian security updates in the storefront runtime image
apps/storefront/Dockerfile's runtime stage is FROM oven/bun:${BUN_VERSION}-slim, a
Debian base that only carries the package versions current the day that tag
was cut. A trivy scan of oven/bun:1.4.2-slim (27 Sep 2026, --severity CRITICAL) found three CRITICAL CVEs — CVE-2026-13221, CVE-2026-42496,
CVE-2026-8376 — all in perl-base 5.40.1-6, all fixed by Debian security in
5.40.1-6+deb13u1.
- The
runtimestage now runsapt-get update && apt-get upgrade -y --no-install-recommends && rm -rf /var/lib/apt/lists/*beforeUSER bun,
closing these (and future Debian-security fixes) on every rebuild rather
than waiting onoven/bunto re-cut the base tag. - Only the
runtimestage changes;depsandbuildare untouched. docs/deployment.md/.id.mddescribe the updated runtime stage.- New test:
tests/storefront-dockerfile-security-updates.test.mjsasserts
the upgrade-and-cleanup RUN precedesUSER bun.
v0.13.0
The primary nav is now actually visible above 720px
Header.astro (and NavBerita.astro on a berita-profile page) put the
primary navigation inside a <details>/<summary> disclosure and relied on
author CSS to show it as a plain horizontal nav above the mobile breakpoint.
That never worked: a closed <details> hides everything but its
<summary> through the UA stylesheet's own ::details-content rule, which
no CSS on the hidden content's children can override — so the primary nav
was invisible above 720px, in every browser, in all three SITE_PROFILEs
(issue #230).
The fix renders the same nav array twice — an always-open desktop
.primary-nav and the pre-existing mobile <details> copy — and lets
global.css show exactly one of the two by viewport width, display: none-ing the other out of the accessibility tree so a screen reader still
meets exactly one "Navigasi utama" landmark. No JavaScript is involved,
keeping issue #24's no-JS mobile-nav design intact.
- A reader on any deployment of this template can now reach every primary
nav link at desktop width — previously, only the mobile disclosure ever
worked. apps/storefront/tests/e2e/navigasi-utama.e2e.tsis new regression
coverage: at 1440px it asserts the desktop nav is visible and
keyboard-focusable, and that exactly one "Navigasi utama" landmark is
accessible at a time; at 360px it asserts the desktop copy is hidden and
the disclosure toggle still opens the mobile nav. Run for all three
build profiles.
Local/server CI runner with exact-SHA commit statuses (#225 part 1)
The owner decided to move this workspace's CI off GitHub Actions entirely. This is part 1: tools/ci/ reproduces every leg .github/workflows/{ci,template-init-smoke,e2e,codeql}.yml runs today, one-to-one, as twelve local-ci/* commit-status contexts reported against the exact SHA each leg ran at — see ADR-0021.
bun run ciruns all twelve legs against HEAD inside a disposablegit worktree, never mutating the developer's own checkout;bun run ci:pr -- <n>does the same for a pull request, refusing fork PRs unless--allow-forkis given explicitly.bun run ci:watchpolls open PRs and skips any (repo, PR, head SHA, CI-definition version) already recorded;tools/ci/systemd/documents the systemd user timer that runs it every 10 minutes (not installed by this change).- Every leg fails closed if the running Bun does not match the root
package.json's pin; every log/evidence file is redacted for tokens and DSN passwords before it touches disk. - The
local-ci/securityleg runs the CodeQL CLI, gitleaks (pinned by digest), andbun audit, checked against a committedtools/ci/security-baseline.jsonseeded with this repository's nine already-dismissed GitHub code-scanning alerts (quoting each one's own dismissal reason) plus one further entry for a genuine new-code finding this leg's own first run surfaced intools/ci/lib/lock.ts. .github/workflows/*.ymlare not removed by this change — see the ADR's "Migration sequence" for why branch protection moves tolocal-ci/*first, in a follow-up PR.
Local CI runs every leg with CI=true, as GitHub Actions did
GitHub Actions set CI=true for every step, and both Playwright configs key
their CI behaviour off it. Without it, the local e2e legs silently ran
without forbidOnly (a stray test.only would have skipped the rest of the
suite and still passed), without the one retry for a flaky
Page.captureScreenshot protocol error, and without the HTML report the leg
keeps as evidence. tools/ci/lib/orchestrate.ts now sets it once, after the
Bun pin check, for every leg.
Build, sign, and publish release images and GitHub Releases from a trusted release host
Issue #225 part 2 (zero-GitHub-Actions policy). tools/release/images.ts and tools/release/publish.ts (bun run release:images, bun run release:publish) replace what .github/workflows/images.yml and .github/workflows/release.yml do — building and pushing apps/cms's runtime/jobs images to GHCR, signing each pushed digest with cosign, scanning with trivy (fail-closed on CRITICAL), exporting SBOMs, and publishing the matching GitHub Release — from a release host awcms-one itself controls, rather than a GitHub-hosted runner.
Neither workflow file is removed by this change; both keep running until a later change disables GitHub Actions at the repository level and migrates the required-status-check contexts (issue #225's own remaining steps).
- ADR-0023 documents the new cosign-key trust root, why it supersedes ADR-0020's GitHub-native attestation for images published from here on, and the honest assurance-level trade-off.
docs/rilis.mdis the full release runbook, including how to rehearse the whole publish path against a throwaway local registry without touching GHCR or a production key.- New root env vars:
GHCR_USER,GHCR_TOKEN,COSIGN_KEY,COSIGN_PASSWORD,COSIGN_PUBLIC_KEY,RELEASE_EVIDENCE_DIR,RELEASE_DANGEROUSLY_SKIP_ANCESTOR_CHECK,RELEASE_GITHUB_TOKEN— all documented in root.env.example. - An owner running the first real
--publishneeds aCOSIGN_KEY/COSIGN_PUBLIC_KEYpair provisioned on the release host and aGHCR_TOKENscoped towrite:packagesonly — neither exists yet as a repository secret, by design (this tool never generates a signing key on the fly).
Remove GitHub Actions; required contexts are now local-ci/* (#225 part 3, closes #225)
The owner's zero-GitHub-Actions decision (ADR-0021), started in part 1 (tools/ci/) and part 2 (tools/release/, ADR-0023), is now complete: every root workflow is deleted and GitHub Actions no longer executes anything in this repository.
- Removed
.github/workflows/{ci,codeql,e2e,images,release,template-init-smoke}.ymland.github/dependabot.yml(its only ecosystem,github-actions, is now meaningless). - Added
tests/tanpa-github-actions.test.mjs— a root gate test that fails if a workflow file ever exists again under root.github/workflows/, and explicitly excludesapps/cms/.github/**(upstream's own subtree files, inert here since GitHub only reads a repository's own root.github/workflows/). Subsumes issue #224's workflow-guardrail requirement. - Fixed every test/tool that read a now-deleted workflow file:
tests/versi-toolchain.test.mjsnow checkstools/ci/lib/orchestrate.ts's Bun-pin enforcement instead of abun-version:line; the test that guarded.github/dependabot.yml's own shape is removed with the config file it guarded; several tools/tests updated their comments to point at thetools/ci//tools/release/successors instead of claiming current GitHub Actions behaviour (artifact uploads, automatic tag-push publication). docs/template.mdconfirms and documents thattools/template-init/**never generates or references workflows — a derived repository starts with zero root GitHub Actions workflows too, and runsbun run ci/ci:watchlocally or attaches its own approved CI by its own governance decision.- Full documentation sweep (EN + ID mirrors):
AGENTS.md(new explicit rule that GitHub Actions is not an accepted implementation path; the gates table now lists the twelvelocal-ci/*legs; the Bun-pin/Dependabot paragraphs rewritten; Definition of Done requiresbun run ci:pr -- <n>),README.md,SECURITY.md,docs/alur-kerja-pengembangan.md(five old per-workflow CI sections folded into one "Local CI: the twelve legs" section, keeping each leg's promotion history for the record),docs/pengujian.md,docs/deployment.md,docs/status.md,docs/template.md,docs/aksesibilitas.md,docs/responsif.md,docs/routing.md. ADR-0018 and ADR-0020 keep their original decisions; each gets a short "Superseded in part by ADR-0021/0023" note (their own CI/publish mechanism moved, not the decision itself), and the ADR index status column reflects it. - The twelve required status checks on
mainare nowlocal-ci/*(posted bybun run ci:pr/ci:watch) instead of the old GitHub Actions contexts — branch protection was switched over in a companion change before this PR merged, so no Actions checks ever needed to run on it.
Server-side, explicit production deployment (tools/deploy/*.sh, ADR-0022)
GitHub Actions could, until now, plausibly grow into the production control
plane by accident — nothing in this repository forbade a future workflow
from SSHing into production or calling a Coolify deploy API with
production credentials in its secrets, and a validated derived repository
already has exactly that shape today. This closes that gap: the ONE
canonical way to mutate production is now tools/deploy/deploy-production.sh
(plus its thin deploy-remote.sh SSH wrapper, healthcheck-production.sh,
and rollback-production.sh), invoked explicitly by an operator or agent,
never by CI.
- New root scripts:
bun run deploy:production|remote|health|rollback.
Fail-closed transaction (lock, exact-release resolution, clean-checkout
check, preflight, pre-migration backup, build-or-pull, migrate, runtime
least-privilege verification, activate, health, smoke, append-only audit
record); a migration having run in the failed attempt blocks any automatic
code rollback and points at the documented DB recovery runbook instead. - New
packages/gerbang/lib/redact.mjs+tools/deploy/redact-log.mjs:
every line these scripts print, and the audit JSONL itself, is redacted
before it reaches a terminal or disk. tests/deploy-production.test.mjs— a hermetic scenario suite; no real
docker/git/curl/ssh/cosign is ever invoked.docs/adr/0022-production-deployment-is-server-side-and-explicit.md+
.id.md;docs/deployment.md/.id.mdrewritten to separate CI checks,
art...
v0.12.0
Align the root-owned Bun pin to 1.4.2
The #210 subtree sync left a root-owned toolchain drift: root package.json
still pinned packageManager: "bun@1.4.0"/engines.bun: ">=1.3.0" while the
embedded apps/cms/package.json (a leftover from before the subtree embed,
not part of this pin) already read bun@1.4.2, and every root workflow that
installs Bun (ci.yml, template-init-smoke.yml, e2e.yml, images.yml,
release.yml) still requested 1.4.0. Two Bun patch versions coexisting in
one monorepo makes local/CI reproduction less deterministic.
- Raised the authoritative root pin to
bun@1.4.2:package.json's
packageManager, andbun-version: "1.4.2"in every job of every root
workflow above. engines.bunmoved from>=1.3.0to>=1.4.2— its meaning stays a floor,
not an exact pin (it also gatesapps/storefront's ownbun installand
any contributor's local Bun, where "at least this new enough" is the
actual requirement), only its value moved up alongside the rest.- Corrected AGENTS.md's (and its Indonesian mirror's) "Configuration and
toolchain" bullet, which had stated the pin as living in "three places"
naming onlyci.yml— already an undercount before this change, since
release.yml's ownbun-versionjob existed and was not named. The rule
now names the actual, current set of root workflows that install Bun
instead of a fixed count, and a workflow that starts installing Bun in the
future is expected to join that list in the same change that adds its
bun-versionline. - No lockfile content changed: this repository's local Bun runtime is
already 1.4.2, sobun installandbun run check:lockfilewere exercised
under that same version andbun.lockcame back byte-identical.
Astro/Playwright/Changesets updates that arrived through #210 remain that
subtree sync's own scope and are untouched here; this change is root-owned
toolchain alignment only (issue #211).
Encrypted backup assurance and restore drills, integrated into the production topology
compose.production.yaml gains three profile-gated services — backup, restore-drill, offsite-copy — that reuse upstream apps/cms/deploy/backup/*.sh (issue #210, ADR-0123) through this repository's own distinct awcms_setup/awcms_app/awcms_worker identities, rather than forking or reimplementing that tooling. docker/backup/Dockerfile supplies only the binaries (age, rsync, openssh-client) those scripts' own README already says a runner image needs, on top of the same postgres:18.4 the postgres service runs. ops/run-backup-compose.sh and ops/awcms-one-backup.crontab give the host-cron scheduling path.
- Backups are
age-encrypted with an HMAC-SHA256-authenticated manifest; a local-only backup is never reported as a successful off-site copy. restore-drillhas a hard-coded command with no--targetcode path anywhere in it — structurally incapable of targeting a production database, not merely documented as a drill. A real disaster-recovery restore stays a manual, confirmed runbook step, never a compose service.- Validated end to end against a disposable PostgreSQL with synthetic data: create → backup → encrypt → manifest → off-site copy (a disposable SSH test target) → restore-drill into an isolated scratch database → schema/RLS integrity verification → measured RTO/RPO, plus five fail-closed paths (wrong/missing key material, tampered manifest, corrupted backup, unavailable off-site destination, unsafe restore target).
docs/deployment.md's new "Backup assurance" section (and its Indonesian mirror) is the operator runbook;docs/status.mdremoves its "at-rest backup encryption" gap now that the capability is documented and validated.
Test awcms-one's deployment/migration boundary around the synchronized omes_control module
Issue #210 pulled upstream's omes_control domain module into apps/cms
(sql/154-sql/158, awcms's own ADR-0122). Upstream already tests that module's own unit
and application semantics; issue #212 adds the AWCMS-One-specific integration
boundary around it — deployment/migration shape this repo owns, not a re-test
of upstream's own coverage, and no OMES/Hermes runtime-execution behaviour
(issue #146 moved that ownership to ahliweb/omes on purpose).
apps/cms/tests/integration/omes-control-deployment-boundary.integration.test.ts
(12 tests, real PostgreSQL):154-158and the full commerce901-934
range apply to one clean database with no numeric-prefix collision and a
migration ledger that matches the files on disk (ADR-0015's reserved-range
rule);awcms_setup/awcms_app/awcms_workerare three distinct roles on
a migrated cluster, matchingcompose.production.yaml's three DSNs; all
eightomes_controltables carry RLS bothENABLEd andFORCEd;
cross-tenant reads/writes onomes_controldata fail closed; a freshly
migrated tenant holds zeroomes_controlrole-permission grants
(default-deny survives deployment); existing commerce RLS/tenant isolation
stays green alongside the new module; and, as the required negative
test, deliberately re-grantingawcms_workerthe exactINSERT/UPDATE
privilegessql/156revoked is caught by the least-privilege assertion
(with an over-grant message naming the table and both verbs), then reverted
and re-verified passing.apps/cms/tests/omes-control-execution-boundary.test.ts(DB-free, 2 tests):
no source file undersrc/modules/omes-control/or
src/pages/api/v1/omes/importschild_process/ssh2, calls
Bun.spawn, or calls a rawexec/execSync— the module stays a
tenant-scoped control-plane record store, never a shell/SSH executor.- Both suites run in the existing DB-backed
check-cmsCI leg (apps/cms's
ownbun test tests/integration/step and its plainbun test
respectively) — no second migration/test pipeline was added, and root
bun testremains PostgreSQL-free. docs/pengujian.md/.id.mddocument the new coverage undercheck-cms's
section.
Promote CodeQL to a required pull-request status check
CodeQL (issue #184) ran on every push and PR for a full triage cycle
(issue #206) with a green track record and no un-triaged findings — the
probation condition set when it was introduced by PR #194. That
prerequisite is now satisfied, so it is promoted from advisory to a
required merge gate on main (issue #214).
main's branch protection now requiresAnalyze (javascript-typescript)
— the CodeQL workflow's own job-status check (app: GitHub Actions) —
additively, alongside the eleven contexts issue #215 had already
established. All eleven survive unchanged;enforce_adminsandstrict
are untouched.- Not the sibling
CodeQLcheck (app: GitHub Advanced Security, the
code-scanning-results check posted only once a SARIF upload succeeds):
verified from a real PR's check-runs that the job-status check is the
one that fails closed on a workflow/analyzer failure, which is what a
required security gate needs — the results check cannot represent an
analyzer crash that never got far enough to upload a SARIF at all. security-extended,apps/cms/**in scope, the weekly schedule,
SHA-pinned actions, and least-privilege workflow permissions are all
unchanged.security-and-qualityis deliberately not made
merge-blocking — a separate signal/risk decision, out of scope here.- Documentation and workflow comments that described CodeQL as "not
required" are updated:AGENTS.md/AGENTS.id.md,SECURITY.md/
SECURITY.id.md,README.md/README.id.md,docs/status.md/
docs/status.id.md,docs/alur-kerja-pengembangan.md/.id.md, and
.github/workflows/codeql.yml's own comments.
Promote the Playwright e2e matrix to required status checks
.github/workflows/e2e.yml (issue #183) was introduced on a deliberate
probation period: a real-browser suite carries a different flake risk than
a type-check or unit test, so it started as advisory only, pending a
proven, deterministic run history.
That bar is now met. Issue #215 reviewed the post-introduction run history
(gh run list --workflow e2e.yml) and found at least seven consecutive
green runs across #197/#202/#203/#204/#207/#208/#209, with no observed e2e
failure in any run since the workflow was introduced. The exact
e2e (toko)/e2e (berita)/e2e (landing) contexts were verified against
a real PR's checks (gh pr checks, gh api .../check-runs) rather than
inferred from the workflow file, and all three are now added to main's
branch protection additively — every previously required check
(check-cms, the three Check (*) legs, and the four
template-init-smoke legs) is preserved, and enforce_admins stays on.
main's required status checks go from eight contexts to eleven.- A pending or failing
e2e (toko)/e2e (berita)/e2e (landing)leg now
blocks a PR merge, the same as any other required check. - No test scope, assertion, or screenshot behaviour changed — this is a
branch-protection and documentation change only. .github/workflows/e2e.yml's own comment,AGENTS.md,README.md,
docs/status.md, anddocs/alur-kerja-pengembangan.md/docs/pengujian.md
(plus their Indonesian mirrors) no longer describe this workflow as
optional.
Sync the apps/cms subtree from upstream 8c64528d to 2d29a446
apps/cms is ahliweb/awcms embedded via git subtree (see AGENTS.md's "The
subtree embed"). This pulls the 14 upstream commits that landed since the last
sync (issue #170), full-history, with a merge commit (issue #210).
- The
omes_controldomain module (26th registered module) — host fleet
lifecycle, worker enrollments, desired-vs-observed deployments, an
allowlisted safe-operation surface, a worker job dispatch queue, health/
backup/audit projections — arrives in the embedded tree via migrations
sql/154-sql/158. Its worker grant ...
v0.11.1
First CodeQL triage: dismiss the false positives, record the standing rules
Thirteen of the fifteen CodeQL alerts open on main after issue #184 were false
positives, won't-fix, or test-only findings, each verified against the actual
flagged line before being dismissed via the code-scanning API with a reason —
not accepted or rejected on the tool's word alone.
- Dismissed:
js/insufficient-password-hash×3 (a SHA-256 hash over a
randomBytes(32)token is not a password hash;computePkceChallengeS256is
RFC 7636 §4.2, which mandates SHA-256),js/file-access-to-http×4 and one
js/clear-text-logging(tools/seed-cms.ts's own purpose — reading local
seed data and printing a one-time generated password for its operator),
js/file-system-race×3 (single-user local tooling, or test-only), and one
each ofjs/reflected-xssandjs/incomplete-url-substring-sanitization
(both test-only, not production code paths). - Left open, on purpose: two
js/clear-text-loggingalerts on
apps/cms/scripts/commerce-deploy-preflight.ts— real findings, fixed as
code by a sibling change rather than dismissed here. SECURITY.mdgains a "CodeQL triage" section naming where alerts appear, why
security-extendedstays the query suite, how anapps/cms/**alert is
triaged (upstream's tree vs. this repo's owncommercemodule), the
dismissed rule IDs, and the two standing rules above so a future reader does
not re-litigate them.docs/alur-kerja-pengembangan.md'scodeqlworkflow
section now cross-links it instead of stating a since-outdated "no triage
playbook yet".
Both changed governance documents ship their Indonesian mirror in this same
change (bun run docs:i18n:stamp).
commerce:deploy:preflight redacts credential-shaped values before printing
CodeQL's first triage after the production-preflight work (#184) flagged two
js/clear-text-logging alerts on apps/cms/scripts/commerce-deploy-preflight.ts:
the two paths that echo a delegated script's captured child-process stderr
print a value CodeQL taints back to process.env. Neither path was actually
printing a credential today, but the property was only true by hand-checked
convention in a script whose output runs through deploy pipelines with
retained logs.
- A single exported
redact()helper now masks apostgres://user:pass@host
DSN's password, a bearer/API-token-shaped value, and the whole value of any
env variable whose NAME matches/(PASSWORD|SECRET|TOKEN|KEY|DSN|DATABASE_URL)/i. - Applied to every string the script actually prints —
printResult, the
main()startup banner, and both captured child-process stderr/stdout
tails — so nothing bypasses it. A benign value (a provider name, a role
name, a plain URL) passes through unchanged, unit-tested alongside the
three masked shapes inapps/cms/tests/commerce-deploy-preflight.test.ts. docs/deployment.md(+ its Indonesian mirror) now says what the preflight
prints and what it redacts.