ci: ratchet the bundle budget and scan builds for credentials (FH-3.3, FH-3.5)

Bundle budget was warning-only: initial warning 700 kB, error 1.8 MB.
Measured today the initial bundle is 1.55 MB raw / 324.58 kB transfer -
up from the 1.15 MB measured on 11 August, so it had been growing with
nothing to stop it.

Lowers maximumError to 1.6 MB. That is a ratchet, not a target: just
above today's size so the bundle cannot grow, with the 700 kB warning
left in place as the goal. Lower it each time the number comes down.

Adds scripts/ci/scan-bundle.sh (npm run scan:bundle), run in CI after
the build. Seven patterns: both provider auth headers, the partner ID
shape, client_secret, private key blocks, AWS keys, Telegram bot
tokens. The legacy payment code that put credentials in the browser is
already deleted; this is what stops it coming back. Verified in both
directions - clean against the real dist, exit 1 against a planted
credential.

Measurement also corrected two assumptions recorded in the harvest
TODO: admin and editor code is already lazy-loaded, so the initial
bundle is main alone rather than a deployable-split problem; and mock
gateway fixtures do reach production chunks, which is now filed as
FH-E.6 with the cause identified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-21 10:36:37 +04:00
parent 04272ae673
commit 6e47d01c32
5 changed files with 84 additions and 10 deletions

View File

@@ -40,9 +40,19 @@ jobs:
CHROME_BIN: ${{ steps.setup-chrome.outputs.chrome-path }}
run: npm run test:coverage
# The production build is what enforces the bundle budget. The initial
# bundle sits at ~1.55 MB raw against a 700 kB target, so the error
# threshold is a ratchet, not the goal: it is set just above today's
# size so the bundle cannot grow while we work it back down. Lower the
# ratchet in angular.json every time it comes down.
- name: Build
run: npm run build
# Stops payment credentials returning to the browser bundle. See
# scripts/ci/scan-bundle.sh for what it looks for and why.
- name: Scan bundle for credentials
run: npm run scan:bundle
- name: E2E
run: |
npx playwright install --with-deps chromium

View File

@@ -59,7 +59,7 @@
{
"type": "initial",
"maximumWarning": "700kB",
"maximumError": "1.8MB"
"maximumError": "1.6MB"
},
{
"type": "anyComponentStyle",

View File

@@ -121,15 +121,17 @@ Each item is normative text plus an acceptance scenario in `docs/backend/BACKEND
- [ ] **FH-3.2 — E2E: replayed webhook** · M · `e2e/`
Their §22 scenario 10. The same provider event delivered twice does not complete the order twice or move stock twice.
- [ ] **FH-3.3 — Bundle budget as a blocking CI check** · S · `angular.json`, `.github/workflows/`
Currently 1.15 MB against a 700 kB budget — 452 kB over, and the only performance criticism of us that is objectively measured. Make the budget fail the build.
- [x] **FH-3.3 — Bundle budget as a blocking CI check** · S · **done 2026-08-21**
`maximumError` on the initial bundle lowered `1.8MB → 1.6MB` in `angular.json`. Measured today: **1.55 MB raw / 324.58 kB transfer** — worse than the 1.15 MB they measured on 11 Aug, so this had been growing unwatched. The threshold is a **ratchet**, not the target: set just above today's size so the bundle cannot grow, with the 700 kB warning left in place as the goal. Lower it every time the number comes down. CI now runs the production build (`npm run build` already defaults to production).
- [ ] **FH-3.4 — Split storefront / editor / backoffice deployables** · L · `angular.json`, routes
The single deployable bundle is both the bundle-size cause and a real separation-of-concerns problem: shipping editor and admin code to every anonymous storefront visitor. Separate bundles, separate budgets, separate CSP.
**Done when:** the storefront entry point is under budget and contains no admin or editor code.
- [ ] **FH-3.4 — Get the initial bundle down** · L · `angular.json`, `src/app/`
**Premise corrected after measuring.** Admin, editor, catalog, cart and the `en`/`hy` locales are *already* lazy chunks — nothing admin-shaped ships to an anonymous visitor. The entire 1.55 MB is `main` alone. So this is not a "split the deployables" job; it is a "find what is eager" job.
Known contributors: `src/app/i18n/ru.ts` (141 kB of source, default locale, eager while `en`/`hy` are lazy), the eagerly-provided core services and their DI tokens, `icon-registry.ts`.
Also: `qrcode`, pulled in by `@marketplaces/auth`, is not ESM and causes an optimizer bailout — worth fixing in the package.
**Done when:** initial is under the 700 kB warning, with the ratchet lowered in steps along the way.
- [ ] **FH-3.5 — Bundle secret scan in CI** · S · `.github/workflows/`
Grep the built output for credential patterns (`authorization-key`, `userid-value`, partner ID shapes, any `client_secret`). Fails the build on a hit. Cheap insurance against FH-1.3 regressing.
- [x] **FH-3.5 — Bundle secret scan in CI** · S · **done 2026-08-21**
`scripts/ci/scan-bundle.sh`, wired as `npm run scan:bundle` and a CI step after Build. Seven patterns: both provider auth headers, the partner ID shape, `client_secret`, private key blocks, AWS keys, Telegram bot tokens. Verified in both directions — clean against the real `dist/`, and fails with exit 1 against a planted credential.
---
@@ -210,6 +212,11 @@ Blocked on **FH-0.1**. Nothing to copy from the archive — it has zero VK/Yande
- [ ] **FH-E.4 — ADR for the harvest** · S
Record the decision: adopt these improvements, reject their architecture, keep our frontend and governance. Include the §9 rejection list so it does not get relitigated.
- [ ] **FH-E.6 — Keep mock gateways out of production builds** · M · Lane A
Measured 2026-08-21: mock seed data reaches the production bundle. `ptr_local`, a fixture literal from `partner-hierarchy-local.gateway.ts`, is present in a built lazy chunk. Cause: 21 DI tokens use `factory: () => (environment.useMockData ? inject(XLocalGateway) : inject(XApiGateway))`, and referencing both branches keeps both classes reachable, so the optimizer cannot drop the mock. 75 kB of local-gateway source, plus its fixtures, ships to users.
This is the concrete form of their strongest objection — "mock repositories as production implementation" — and it is mechanical to fix. The pattern to copy is already in this repo: `mock-data.interceptor.production.ts` swapped in via `fileReplacements`.
**Done when:** `scan-bundle.sh` can gate on mock fixture markers and pass.
- [ ] **FH-E.5 — Reduce `localStorage` to cache, never truth** · M · Lane A
19 files touch `localStorage`, mostly admin facades and `project-editor-draft-storage.service.ts`. Their disqualifying objection is not "you use localStorage" — it is "localStorage is your source of truth."
**Do:** keep local drafts as an offline convenience with an explicit "unsaved local draft" indicator and server-wins reconciliation; never let a local value be the published state.
@@ -227,7 +234,7 @@ Blocked on **FH-0.1**. Nothing to copy from the archive — it has zero VK/Yande
| 3 — Proof | 5 | A | — |
| 4 — Identity | 8 | C | FH-0.1 |
| Ops | 3 | D | — |
| Process | 5 | E | — |
| **Total** | **42** | | |
| Process | 6 | E | — |
| **Total** | **43** | | |
**Start here:** FH-0.1 (escalate today, it is a one-way door), then FH-1.1 and FH-1.4 — both are small, both are live defects, and both close findings their audit will otherwise keep raising.

View File

@@ -14,6 +14,7 @@
"arch:check:boundaries": "node tools/architecture/check-boundaries.mjs",
"arch:check:cycles": "npx --yes madge --circular --extensions ts src/app --ts-config tsconfig.app.json",
"arch:check": "npm run arch:check:boundaries ; npm run arch:check:cycles",
"scan:bundle": "bash scripts/ci/scan-bundle.sh",
"barry": "barry-cache",
"barry:validate": "barry-cache validate",
"barry:resume": "barry-cache resume",

56
scripts/ci/scan-bundle.sh Normal file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Fails the build if a production bundle contains anything that should only
# ever exist server-side.
#
# Why this exists: the storefront used to send provider payment credentials
# from the browser - an `authorization-key` header, a `userid-value` header,
# and a hardcoded partner ID literal compiled into the bundle. That code is
# gone (FH-1.3), and this check is what stops it coming back. A credential in
# a JS bundle is not a leak you can revoke quietly; it is published.
#
# Usage:
# npm run build && scripts/ci/scan-bundle.sh [dist-dir]
set -euo pipefail
DIST="${1:-dist}"
if [[ ! -d "$DIST" ]]; then
echo "scan-bundle: '$DIST' does not exist - build first" >&2
exit 2
fi
# Each entry is "label|extended-regex". Keep patterns specific: a pattern that
# fires on ordinary code trains people to ignore this check.
PATTERNS=(
"provider auth header|authorization-key"
"provider user header|userid-value"
"hardcoded partner id|web-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
"oauth client secret|client_secret[\"']?[[:space:]]*[:=]"
"private key block|BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY"
"aws access key|AKIA[0-9A-Z]{16}"
"telegram bot token|[0-9]{8,10}:AA[0-9A-Za-z_-]{33}"
)
failed=0
for entry in "${PATTERNS[@]}"; do
label="${entry%%|*}"
pattern="${entry#*|}"
if matches="$(grep -rIlE "$pattern" "$DIST" 2>/dev/null)"; then
if [[ -n "$matches" ]]; then
echo "FAIL: $label found in the built bundle" >&2
echo "$matches" | sed 's/^/ /' >&2
failed=1
fi
fi
done
if [[ $failed -ne 0 ]]; then
echo >&2
echo "A credential reached the browser bundle. Move it behind the API." >&2
exit 1
fi
echo "scan-bundle: clean ($DIST)"