All checks were successful
Build and deploy / Validate source (push) Successful in 9s
Build and deploy / Integration suite on a real stack (push) Successful in 2m49s
Build and deploy / Secret scan and release gate (push) Successful in 9s
Build and deploy / Publish images and notify Portainer (push) Has been skipped
Tiny v3 replaces the v2 token adapter. An operator connects Tiny once from the Kanban; the callback is authorised by a single-use state, because Tiny's cross-site redirect does not carry the SameSite=Strict operator cookie. Tokens are kept in provider_tokens, the refresh token rotates under a row lock, and the worker keeps the connection alive while order creation is off. Orders find or create the customer's contact by CNPJ, then POST /pedidos with product ids from TINY_PRODUCT_TEXTIL_FOLHA, _TEXTIL_AVULSA, _UV_FOLHA and _UV_AVULSA and numeroOrdemCompra DTF-<number>; a retry searches the customer's recent orders for that number first. The product settings avoid a _FILE suffix, which the secrets loader reads as a secret file path. Production passes the application credentials through but keeps TINY_ADAPTER fake: Tiny has no sandbox, so creating real orders waits for a supervised test. compose.providers.yaml gives the local API and worker an internet route for provider testing; the default local stack still has none. Verified with the full CI integration sequence locally, including the new tiny_oauth_test against the real database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
823 lines
48 KiB
Markdown
823 lines
48 KiB
Markdown
# DTF System — Working Roadmap
|
||
|
||
> Internal engineering tracker. Not a client document, not a promise sheet.
|
||
> The client-facing narrative lives in `docs/roadmap-cliente.pdf` and in the weekly
|
||
> report, which is a deliverable and is not versioned here.
|
||
>
|
||
> Update the **Current step** line and the item status every time something moves.
|
||
> Add new findings at the bottom of the relevant block rather than rewriting history.
|
||
|
||
**Current step (2026-09-24, Week 2):** Client inputs for Mercado Pago and
|
||
freight were requested on 2026-09-24; Tiny access is already with the client.
|
||
Built without them: server-side print-file generation (1.4), the delivery
|
||
address (3.8), the Kanban's payment-issue and print-file views (1.5), and
|
||
Mercado Pago and Tiny adapters written from the public API documentation and
|
||
tested against fake transports (1.1, 1.3). None of the provider work is a
|
||
verified integration. The complete CI integration sequence passed locally on
|
||
2026-09-24 (all API suites including the new `print_file_test`, adapter and
|
||
generator unit suites, retention, runtime security, and both browser suites),
|
||
run with Docker Engine in WSL against a fresh build; pending a Gitea runner run.
|
||
|
||
**Previous step (2026-09-23):** Payment safety fixes 2.13 and 2.14 and
|
||
the local order-correctness work in 3.6/3.9 have passed integration checks.
|
||
Production specification v2 now records each copy's film coordinates and is
|
||
kept through the approved order; 4.6 now pages pending and approved unpaid
|
||
quotes, including a tested 101st pending quote. Operational entrypoints in
|
||
5.12 are repaired and locally exercised. Image decoding, mixed-sheet grading,
|
||
rotation-sensitive DPI, and PDF page geometry are corrected in 3.9/4.4.
|
||
`main` pushes now validate without publishing; manual release requires a passing
|
||
source preflight. The containerized browser gate passes locally, pending a Gitea
|
||
runner run.
|
||
Next address the upload/scanner safety gate and unsupported PDF image evidence.
|
||
The customer/API upload admission now stops above the scanner's effective limit
|
||
before transfer; the 5 GiB large-file product path still needs agreement and
|
||
implementation.
|
||
Unfinished upload reservations now expire after one hour or can be cancelled
|
||
explicitly; anonymous admission and browser resource bounds stay open.
|
||
Generated print output, lifecycle/recovery, and provider work remain open.
|
||
Obtain decisions for unattended pricing, print-file acceptance and large files,
|
||
plus sandbox inputs for freight and Mercado Pago. Week 2 delivery items 1.1–1.5
|
||
remain open; 1.6 is complete.
|
||
|
||
> Paths in closed items are written as they were when the finding was made.
|
||
> The repository was laid out by role on 2026-09-21 (`local/` became `app/`,
|
||
> with `tests/`, `ops/`, `infra/` and `web/` beside it); the history is left
|
||
> as recorded rather than rewritten.
|
||
|
||
**Last audit:** 2026-09-18, full read of the then-current tree. The 2026-09-21
|
||
full review is `docs/REVIEW-2026-09-21.md`; the 2026-09-22 review and payment
|
||
probes added new findings to Blocks 2–5 below. Historical paths in closed items
|
||
remain as recorded.
|
||
|
||
**Full remediation register:** `docs/REMEDIATION-2026-09-22.md` maps every one
|
||
of the 37 review findings to an action and a release gate. Use it alongside
|
||
this Week 2 tracker; a green milestone here does not close the production gate.
|
||
|
||
| Status | Meaning |
|
||
|---|---|
|
||
| `[ ]` | not started |
|
||
| `[~]` | in progress |
|
||
| `[x]` | done and verified |
|
||
| `[?]` | blocked on a decision (product or client), not on code |
|
||
|
||
---
|
||
|
||
## Week 2 execution sequence
|
||
|
||
This sequence keeps the client commitments in Block 1 visible while correcting
|
||
defects that would make those commitments unsafe or impossible to operate.
|
||
Do not mark a provider item complete from a fake-adapter test or a healthy page.
|
||
|
||
| Order | Work | Exit evidence |
|
||
|---|---|---|
|
||
| 1. Immediate safety — done 2026-09-22 | Close 2.13 and 2.14; cover foreign quote IDs, missing/invalid amounts, duplicates and valid approvals. | Local integration checks pass and no other customer's order is returned. |
|
||
| 2. Order correctness | Fix 3.6 and 4.6: one current cart/quote snapshot, versioned per-file production instructions, correction/final revision binding, visible actionable quotes. | The approved quote, order and final file can be traced back to the same reviewed layout; edits cannot buy an old cart. |
|
||
| 3. Resolve product contracts | Decide 3.1–3.3: which quotes may auto-approve, what generates the print file, and which sizes the upload and scanner can release. | Written acceptance rules and representative artwork/large-file cases before enabling unattended payment. |
|
||
| 4. Week 2 integrations | Add destination data and real freight first, then Mercado Pago payment intents/webhooks/reconciliation, then Tiny/Olist order creation. Keep the four agreed WhatsApp events in the same delivery contract. | Sandbox flows and failure/retry cases pass; no fake provider is presented as production ready. |
|
||
| 5. Operability and release | Repair 5.12–5.14, signatures, proxy trust and backup/restore; gate browser tests and the exact deployed images. | Fresh install, upgrade, recovery and deployed release checks pass with alert ownership recorded. |
|
||
|
||
Client inputs needed for steps 3–4 are listed in `docs/PRODUCTION_INPUTS.md`.
|
||
Engineering can complete steps 1–2 and repair local operational commands while
|
||
those inputs are gathered. The full disposition of architecture, security,
|
||
quality, operational, and delivery findings is in
|
||
`docs/REMEDIATION-2026-09-22.md`; all release gates there must be met before
|
||
accepting real customer work.
|
||
|
||
---
|
||
|
||
## Block 0 · Broken right now
|
||
|
||
Nothing in this block is optional. Until it is closed, the system cannot be
|
||
demonstrated, and the week-1 claims cannot be defended.
|
||
|
||
### `[x]` 0.1 — API returns 500 on every session, login and registration `(F1)`
|
||
|
||
`local/auth.py:46` calls `os.environ` and the module never imports `os`.
|
||
Reproduced: `NameError: name 'os' is not defined`. `/api/session` calls
|
||
`new_session()` whenever there is no cookie, so the Site checkout bridge, cart
|
||
recovery and the customer portal all fail on first visit. Introduced in `e3e37f6`.
|
||
|
||
- Add `import os` to `local/auth.py`.
|
||
- Move the `COOKIE_SECURE` read to a module constant so it is evaluated once.
|
||
- **Accept:** a fresh browser hits the Site and `/api/session` returns 200 with a
|
||
`cart_scope`; `local/smoke_test.py` and `local/workflow_test.py` pass.
|
||
|
||
### `[x]` 0.2 — "Arquivo por metro" is unreachable in practice
|
||
|
||
Two independent causes, both from 2026-09-18 commits. Verified in a browser.
|
||
|
||
**a. Every entry point hard-routes to loose artwork** (`483a083`)
|
||
|
||
| Control | Currently opens |
|
||
|---|---|
|
||
| Nav "Impressão DTF" | `avulsa` |
|
||
| "DTF Têxtil · 57 cm" | `avulsa` |
|
||
| "DTF UV · 28,5 cm" | `uv` |
|
||
| Hero "Enviar minha arte" | `avulsa` |
|
||
|
||
Only the price card reaches `file`. Revert the three `data-modo-cta` attributes
|
||
so navigation links land on the product chooser, not on a product.
|
||
|
||
**b. Dropping a PNG/JPG in `file` mode silently switches the order** `(F25)`
|
||
|
||
`dtf-site.html` `sel()` — from `abrir('file')`, dropping `imagem-teste.jpg` gives
|
||
`modo: "avulsa"`, header "Artes avulsas", price `R$ 29,90/m`. Meanwhile the same
|
||
screen says *"Arraste suas folhas montadas · PNG, JPG ou PDF"*, marks
|
||
*"PNG, JPG ou PDF · a partir de R$ 14,90"* as the recommended path, and sets
|
||
`input.accept=".png,.jpg,.jpeg,.pdf"`. The page invites the drop and then
|
||
reprices the order 50% higher.
|
||
|
||
The escape hatch `#imagemComoFolha` sits above the drop zone as small text inside
|
||
an informational notice, defaults to unchecked, and must be ticked *before* the
|
||
drop. After the switch fires, `pintaModo()` sets `trocarParaAvulsa.hidden = true`,
|
||
so the checkbox disappears and there is no way back in place.
|
||
|
||
Net effect: the only formats that survive `file` mode are PDF/TIFF/PSD/AI/CDR —
|
||
the path the UI itself marks as the worse option. A mixed drop (JPG + PDF) is
|
||
rejected and accepts nothing.
|
||
|
||
- Make the ready-sheet choice an explicit two-option control **inside** the drop
|
||
area, styled like the existing `.cam` selector — not a checkbox in a notice.
|
||
- Stop advertising PNG/JPG in the by-metre drop zone while rejecting them.
|
||
- When a switch does happen, show the price change and offer one-click undo.
|
||
- **Accept:** a customer can complete a by-metre order with a PNG from any entry
|
||
point, and no product/price change ever happens without a visible confirmation.
|
||
|
||
### `[x]` 0.3 — Ready-sheet declaration is unverified and worth money `(F22 related)`
|
||
|
||
Ticking `#imagemComoFolha` is an honour-system claim that moves the price from
|
||
R$ 29,90/m to R$ 19,90/m (R$ 14,90 with a good grade). `medirFolha` then derives
|
||
sheet height purely from aspect ratio × 57 cm — the original bug, now opt-in.
|
||
|
||
Measured with `imagem-teste.jpg` (466 × 659 px):
|
||
|
||
| Route | System behaviour | Billed |
|
||
|---|---|---|
|
||
| Ticked | treated as a 57 × 80,6 cm mounted sheet | 1 m × R$ 19,90 = **R$ 19,90** |
|
||
| Not ticked | 20 cm wide, packs to 28,3 cm of film | 1 m × R$ 29,90 = **R$ 29,90** |
|
||
|
||
- Validate the claim: declared width must be ≈ film width (57 / 28,5 cm) at a
|
||
plausible DPI before the sheet model is accepted.
|
||
- Re-check server-side in `/api/operator/quotes/{id}/approve` before pricing.
|
||
- **Accept:** a small single artwork declared as a ready sheet is rejected with a
|
||
clear message; a genuine 57 cm sheet passes; the operator sees the verdict.
|
||
|
||
### `[x]` 0.4 — Documented local startup fails `(F2)`
|
||
|
||
`LOCAL_SETUP.md` says `docker compose up --build` with no `.env`.
|
||
`docker compose --env-file .env.example config` exits 1: `R2_ENDPOINT`,
|
||
`R2_ACCESS_KEY_ID`, `SITE_DOMAIN`, `KANBAN_DOMAIN` missing. `docker-compose.yml`
|
||
became a production/R2 stack in `e3e37f6`; `.env.example` is still the MinIO one
|
||
and there is no MinIO service left.
|
||
|
||
- Decide: keep one production compose and add `compose.local.yaml` with MinIO, or
|
||
restore a local default. Recommend the former.
|
||
- **Accept:** a clean clone reaches a working Site + Kanban with the documented
|
||
command, and `LOCAL_SETUP.md` matches what actually runs.
|
||
|
||
### `[x]` 0.5 — App DB role shares the admin password `(F3)`
|
||
|
||
`docker-compose.yml:65` sets `APP_DB_PASSWORD: ${POSTGRES_PASSWORD}` — the same
|
||
value as `dtf_admin`. `bootstrap.py` grants the app role DML-only and then hands
|
||
it a credential that also logs in as the owner. Anyone reading the API container
|
||
env has admin on the database.
|
||
|
||
- **Accept:** distinct secrets; connecting as `dtf_app` with the admin password fails.
|
||
|
||
### `[x]` 0.6 — A paid order showed the customer nothing (found while closing Block 0)
|
||
|
||
`#checkoutStatus` and `#checkoutActions` lived inside `#carr`, and the success path
|
||
in `checkout.js` clears the cart (`pedido=[]; limpaPaineis()`) before writing the
|
||
confirmation — `.carr{display:none}` then hid the panel holding it. Present since
|
||
the first commit; it only surfaced once 0.1 made a payment reachable at all.
|
||
Both elements now sit in their own always-visible `.checkout` container.
|
||
|
||
### How Block 0 was verified
|
||
|
||
A live stack (`compose.local.yaml`), then the full suite:
|
||
|
||
| Check | Result |
|
||
|---|---|
|
||
| `/api/session` on a cold browser | 200 with `cart_scope` + session cookie |
|
||
| smoke · workflow · security · scanning | pass |
|
||
| retention · runtime security (in-container) | pass |
|
||
| `artwork_browser_test.mjs` | pass, updated to the new declared-product behaviour |
|
||
| `browser_test.mjs` end-to-end | pass — upload → quote → operator approval → paid order → all Kanban states |
|
||
| 4 CI unit tests | pass |
|
||
|
||
Ports 8090/8091/8010 were used; 8080 was held by an unrelated preview server.
|
||
|
||
---
|
||
|
||
## Block 1 · Week 2 — committed to the client
|
||
|
||
From the report already sent. These are dated promises, not backlog.
|
||
|
||
- `[~]` 1.1 — Mercado Pago transparent checkout, signed and idempotent webhooks.
|
||
**Current foundation (2026-09-22):** a fake signer exercises signature rejection,
|
||
event-ID deduplication, amount comparison and transactional order creation.
|
||
This is not a Mercado Pago integration. Complete a durable payment intent,
|
||
provider payment ID and currency binding, real verification and status lookup,
|
||
delayed/duplicate event handling, refund/cancellation rules and reconciliation.
|
||
A refused paid event must be visible for operator resolution rather than silently
|
||
treated as finished. See 2.14 and 3.7. Requires sandbox access, webhook
|
||
administration, event mapping and an approved refund policy.
|
||
**Groundwork (2026-09-24):** `app/mercadopago.py` creates PIX or card-token
|
||
payments with the quote as idempotency key, verifies `x-signature` as
|
||
documented (HMAC-SHA256 over `id;request-id;ts`, 30-minute replay window),
|
||
and treats the notification as a pointer: the payment is fetched from the
|
||
API and only a BRL amount in whole centavos is compared. `payment_intents`
|
||
binds each provider payment to its quote; `/api/payments/intent` starts a
|
||
PIX and the Site shows its QR code. Refused paid events and refunds on
|
||
existing orders now stay on the Kanban until an operator records a
|
||
resolution. Unit-tested against a fake transport only; card form (needs the
|
||
public key), sandbox run and refund policy remain.
|
||
- `[ ]` 1.2 — Real freight quotation. **Blocked on client inputs** (see
|
||
`PRODUCTION_INPUTS.md`): source platform, credentials, origin CEP, services,
|
||
packaging weight/dimensions per length, subsidy policy.
|
||
- `[~]` 1.3 — Idempotent Tiny/Olist order creation with order-number traceability.
|
||
Confirm endpoints, tag behaviour and rate limits first.
|
||
**Groundwork (2026-09-24), API v3 by decision:** OAuth2 against Tiny's
|
||
Keycloak. An operator starts the connection from the Kanban; the callback is
|
||
authorised by a single-use state (the operator cookie is SameSite=Strict and
|
||
does not survive Tiny's cross-site redirect). Tokens live in
|
||
`provider_tokens`; the refresh token rotates under a row lock and the worker
|
||
keeps the connection alive. Orders: contact found by CNPJ or created, then
|
||
`POST /pedidos` with product ids from `TINY_PRODUCT_TEXTIL_FOLHA` / `_TEXTIL_AVULSA` / `_UV_FOLHA`
|
||
/ `_UV_AVULSA` (not `_<MODE>`: a name ending in `_FILE` is read as a secret
|
||
file path by `app/core/secrets.py`) and
|
||
`numeroOrdemCompra = DTF-<number>`; a retry searches the customer's last
|
||
seven days of orders for that number first. Production passes the app
|
||
credentials through but keeps `TINY_ADAPTER: fake`. Tested against fake
|
||
transports (`tests.test_tiny`) and, for OAuth, the real database
|
||
(`tests.tiny_oauth_test`). Tiny has no sandbox: the first real test creates
|
||
real orders. Still to confirm on the client's account: plan (Construa+),
|
||
product ids, token lifetimes, whether pickup needs a transportador, and
|
||
rate limits.
|
||
- `[~]` 1.4 — Final print-file generation (see 3.2 and 3.6: production instructions
|
||
must survive checkout before an output engine can reproduce the approved job).
|
||
**Built (2026-09-24):** each paid item gets a PDF the width of the film and
|
||
the length of the approved layout, with every copy at its reviewed position,
|
||
rotation and mirror (`app/printfile.py`, rendered by the worker from
|
||
`app/printjobs.py`). Sources are embedded once at original resolution; JPEG
|
||
bytes pass through, PNG alpha becomes a soft mask, EXIF orientation is
|
||
honoured. A file whose proportions differ from the quote, a layout longer than
|
||
billed, or PDF/PSD/AI/CDR artwork goes to hand preparation with the reason.
|
||
The operator approves the generated PDF as the final file through the
|
||
existing review. Unit tests include a raster check of every rotation and
|
||
mirror, and `tests.print_file_test` passes on the running stack (generate,
|
||
download, approve as final, queue; hand-preparation routing and retry).
|
||
**Still open:** a FlexiPRINT import of real
|
||
generated files (including one longer than 5 m, which uses `UserUnit`), and
|
||
PDF artwork, which this generator does not compose.
|
||
- `[~]` 1.5 — Main Kanban production states consolidated. The six states and
|
||
their transitions are unchanged; cards now show the delivery address, the
|
||
print-file status per item, and a panel lists payments that need a person
|
||
(money without an order, refunds after an order) until resolved. Confirm
|
||
with the operation that these are the main states before closing.
|
||
- `[x]` 1.6 — **Block 0.2 + 0.3** were completed and verified on 2026-09-18.
|
||
|
||
`[!]` The production compose currently blocks `dev_paid` (`ENVIRONMENT != 'local'`)
|
||
and ships only fake adapters, so the deployed system cannot take an order at all.
|
||
Real freight, payment initiation and verified provider events are required to
|
||
unblock it; the fake webhook alone does not.
|
||
|
||
---
|
||
|
||
## Block 2 · Security — before any public exposure
|
||
|
||
### `[x]` 2.1 — Rate limiting and audit logs are blind to the client `(F5)`
|
||
|
||
uvicorn runs without trusted proxy headers (`forwarded_allow_ips` defaults to
|
||
`127.0.0.1`; nginx is a different container IP), so `request.client.host` is nginx
|
||
for every request. `rate_limit('auth-source', ...)` at 60/15min becomes a single
|
||
global bucket — **60 failed logins lock out every customer** — and every `audit()`
|
||
record has no attacker IP.
|
||
|
||
- Set `--proxy-headers` with `FORWARDED_ALLOW_IPS` scoped to the nginx service, or
|
||
read `X-Forwarded-For` explicitly at the edge.
|
||
- **Accept:** two clients on different IPs have independent buckets; audit rows
|
||
carry the real IP.
|
||
|
||
### `[x]` 2.2 — The public site throttles itself `(F6)`
|
||
|
||
`local/app.py:115` — `rate_limit('guest-sessions', ENVIRONMENT, 120, 900)` is keyed
|
||
on the environment name: 120 new visitors per 15 minutes **site-wide** (~8/min).
|
||
Normal traffic 429s. Key per source IP (after 2.1) and raise the ceiling.
|
||
|
||
### `[x]` 2.3 — `deploy/stack.yaml` cannot boot `(F7)`
|
||
|
||
It passes `DATABASE_URL_FILE`, `AWS_ACCESS_KEY_ID_FILE`, `OPERATOR_PASSWORD_FILE`,
|
||
`OPERATOR_USER`. The code reads `DATABASE_URL`, `AWS_ACCESS_KEY_ID`,
|
||
`OPERATOR_PASSWORD`, `OPERATOR_EMAIL`, and no `_FILE` loader exists.
|
||
`app.py:58` does `os.environ['OPERATOR_PASSWORD']` → `KeyError` → 500 instead of 503.
|
||
|
||
- Implement `local/secrets.py` (the preflight already expects it) reading `*_FILE`
|
||
with env fallback. Reconcile `OPERATOR_USER` vs `OPERATOR_EMAIL`.
|
||
- **Accept:** the stack renders and boots against Swarm secrets; missing operator
|
||
config yields 503, not 500.
|
||
|
||
### `[x]` 2.4 — The documented release gate does not exist `(F8)`
|
||
|
||
`PORTAINER.md` and `SECURITY_REPORT.md` claim the workflow runs the full isolated
|
||
suite, Trivy HIGH/CRITICAL image gates, secret scanning and the source preflight
|
||
before calling Portainer. `.gitea/workflows/deploy.yml` runs `py_compile` plus four
|
||
unit tests, then builds, pushes `latest` and calls the webhook **unconditionally**.
|
||
`deploy/production_preflight.py` is never invoked — only its unit test runs.
|
||
|
||
- Either implement the gate or correct both documents. Do not leave the gap.
|
||
|
||
### `[x]` 2.5 — The preflight has silently decayed `(F9)`
|
||
|
||
It blocks by string-matching source. **4 of 6 markers are dead** after the R2
|
||
refactor: `'This runtime only supports APP_ENV=local'`,
|
||
`'Only local S3 storage is supported'`, `"allowed_hosts=['localhost', '127.0.0.1']"`,
|
||
`"'environment': 'local'"`. String gates weaken without failing.
|
||
|
||
- Replace marker matching with behavioural assertions (import the module, assert
|
||
the adapter classes in use).
|
||
|
||
### `[x]` 2.12 — Two divergent stack definitions; the docs named the wrong one
|
||
|
||
Found 2026-09-21 by asking which file Portainer deploys. `PORTAINER.md` called
|
||
`deploy/stack.yaml` "the production stack"; the deployed file is the repository's
|
||
`docker-compose.yml`. `deploy/stack.yaml` came from the first commit and was never
|
||
deployed — it supplied credentials as Docker secrets where the deployed file uses
|
||
plain environment variables.
|
||
|
||
**Decision (2026-09-21): keep `docker-compose.yml`, delete `deploy/stack.yaml`.**
|
||
|
||
The gain from Docker secrets here is narrower than it sounds. It keeps values out
|
||
of `docker inspect` and the Portainer UI, but `local/secrets.py` loads them into
|
||
the process environment anyway, and anyone who can read `docker inspect` is
|
||
already root or in the docker group and could read the secret files directly. The
|
||
operator is the only Portainer user, so the main benefit — limiting what a
|
||
lower-privileged console user can see — does not apply. Maintaining two
|
||
definitions that drift was the larger real cost.
|
||
|
||
`local/secrets.py` stays. It is inert against the deployed file and costs nothing,
|
||
and it means a stack can switch to Docker secrets later without a code change.
|
||
|
||
Still open: **rotate the R2 secret key.** Not because of Portainer, but because it
|
||
grants read and write over every customer's artwork and has been readable from the
|
||
stack environment for some time. The operator password is worth rotating with it.
|
||
|
||
### `[x]` 2.6 — Base images are not pinned `(F10)`
|
||
|
||
Dockerfiles default to mutable `python:3.12-slim` / `nginx:1.28-alpine`, the
|
||
workflow passes no digest build-args, and `--pull` makes builds non-reproducible —
|
||
while `PORTAINER.md` documents digest-pinned immutable bases.
|
||
|
||
### `[x]` 2.7 — pdf.js loaded from CDN without integrity `(F11)`
|
||
|
||
Vendored rather than integrity-pinned, so the Site no longer depends on a third
|
||
party being reachable and honest when a customer opens it. Both files are served
|
||
from this origin and their provenance is recorded in `local/static/vendor/README.md`,
|
||
verified against the SRI digests cdnjs publishes for 3.11.174.
|
||
|
||
`cdnjs.cloudflare.com` is gone from `script-src`, `worker-src` and `connect-src` in
|
||
both gateway templates: scripts and workers are now `'self'` plus `blob:` for the
|
||
worker the Site builds itself.
|
||
|
||
Verified in a browser against the running stack: pdf.js loads from `/vendor/`,
|
||
the blob worker starts, and a real 7-page PDF parses with no CSP violation. Both
|
||
browser suites and the full integration suite pass.
|
||
|
||
**Still open: the version.** 3.11.174 is old. GHSA-wgrm-67xf-hhpq is mitigated —
|
||
`dtf-site.html` already passes `isEvalSupported: false`, which is the documented
|
||
workaround — but staying on it indefinitely is not a posture. Upgrading is an API
|
||
change rather than a file swap and needs its own browser testing, so it is
|
||
deliberately not bundled here.
|
||
|
||
### `[x]` 2.8 — Single shared operator credential `(F12)`
|
||
|
||
Accounts now live in `dtf_local.operators`, one per person, with `movements.operator`
|
||
and `order_files.created_by` recording who actually acted. Administered from the API
|
||
container with `python3 -m app.operators` (list, add, password, disable, enable);
|
||
passwords are read from the terminal so they never reach shell history or the
|
||
process list, and disabling revokes open sessions immediately rather than leaving
|
||
them valid for the rest of the eight-hour window.
|
||
|
||
Migration was the risk, since getting it wrong locks the factory out of the Kanban.
|
||
`OPERATOR_EMAIL`/`OPERATOR_PASSWORD` seed the first account, once: a password
|
||
changed through the CLI is never reverted by a stale environment variable on the
|
||
next deploy. The first attempt did not work — `db-init` was not given those
|
||
variables in either compose file, so no account would have been created and login
|
||
would have failed closed with 503. Both files now pass them to the migration job.
|
||
Verified end to end: the unchanged credential still logs in, a second operator
|
||
authenticates separately, wrong passwords and unknown accounts are rejected, and
|
||
disabling ends access at once.
|
||
|
||
**Roles are deliberately not included.** The meeting described separation of duties
|
||
for rework authorisation (Mayana classifies, Thales or Alexandre authorise), but the
|
||
rework feature does not exist in this system, so there is nothing for a role to
|
||
gate. Building an authorisation model with no consumer would be guesswork. Add roles
|
||
with the feature that needs them.
|
||
|
||
### `[~]` 2.9 — TLS is terminated outside the repository `(F13)`
|
||
|
||
Downgraded 2026-09-21. The stack publishes plain HTTP on 18080/18081 while
|
||
`COOKIE_SECURE: "true"`, and nothing in the repo provisions certificates — but
|
||
`nginx-proxy-manager` on the host owns 80/443 and terminates TLS in front of it,
|
||
so cookies are not being dropped in practice. This is undocumented operational
|
||
knowledge rather than a live defect.
|
||
|
||
What remains: record the proxy in `PORTAINER.md` as part of the deployment
|
||
contract, so nobody moves the stack to a host without one and silently breaks
|
||
every session cookie. `TAREFAS.md` A2 still lists the certificate as pending;
|
||
confirm it is actually issued for the DTF subdomain.
|
||
|
||
### `[ ]` 2.10 — No email verification, no password recovery `(F14)`
|
||
|
||
A locked-out customer has no path back, and registration accepts any CNPJ without
|
||
proving control of the e-mail. Needs a transactional mail provider — **client input**.
|
||
|
||
### `[ ]` 2.11 — LGPD `(F15)`
|
||
|
||
CNPJ, phone and e-mail are kept indefinitely in `accounts.profile` and
|
||
`orders.snapshot`. Artwork has a 30-day policy; personal data has no defined
|
||
retention, export or deletion path. The Site links an external privacy notice;
|
||
confirm that it covers this processing and define the required records and
|
||
customer rights flow before production activation.
|
||
|
||
### `[x]` 2.13 — A foreign paid quote ID exposes an order (2026-09-22 review)
|
||
|
||
The `dev-paid` refusal fallback fetched `orders` by `quote_id` without `owner`.
|
||
A separate local customer session received the full paid order when supplied
|
||
another customer's quote ID. `app/api/orders.py` now includes the owner in the
|
||
fallback query. The local payment integration test confirms a foreign ID returns
|
||
404 while the owner can still retrieve the already-paid order.
|
||
|
||
### `[x]` 2.14 — A signed approval without a paid amount creates an order
|
||
|
||
`app/payments.py` compared amounts only when the event contained one. A local
|
||
signed `approved` event without `amount_cents` created an order. The service now
|
||
requires an actual integer amount equal to the approved total; local integration
|
||
tests cover missing, non-integer, underpaid and correct values. Currency and
|
||
provider payment identity belong to the wider contract in 3.7; this gate does
|
||
not complete 1.1.
|
||
|
||
### `[~]` 2.15 — Public intake controls need operational proof
|
||
|
||
Unfinished reservations now have a one-hour lease and owner-scoped cancellation;
|
||
anonymous admission capacity still needs a firm bound. Keep ClamAV signatures
|
||
current and alert on stale data; scope reverse-proxy IP trust
|
||
to the actual hop and verify it through Swarm ingress. These are separate
|
||
controls, but all must work before public large-file intake is considered safe.
|
||
The production topology has not been verified by the repository review.
|
||
|
||
---
|
||
|
||
## Block 3 · Architecture — needs a decision before code
|
||
|
||
### `[?]` 3.1 — Manual quote approval contradicts the 24h business case `(F16)`
|
||
|
||
Payment requires `quotes.approved`, set only by an authenticated operator. The
|
||
meeting's premise was that the 17h30 order waiting until 5am is what costs the
|
||
money. As built, a 2am order still waits for a person. `CONTEXT.md` frames this as a
|
||
temporary development trust boundary — the risk is that it silently becomes the
|
||
delivered model.
|
||
|
||
**Decide:** what makes a quote auto-approvable (mode, metre range, grade floor,
|
||
returning customer), and what still routes to a human.
|
||
|
||
### `[?]` 3.2 — Billable metres are computed in the customer's browser `(F17, F18)`
|
||
|
||
For loose artwork, `metros` comes from `desenhaMontagem`/`encaixar` — a canvas
|
||
alpha-mask packer running client-side. The server never recomputes it.
|
||
`passoDe()`/`CELULAS_MAX` coarsen the grid for large sheets and image decoding
|
||
differs by browser, so **the same cart can price differently on different devices**.
|
||
The code comments reference "o motor do servidor"; that engine does not exist.
|
||
|
||
Worse, the layout the customer is quoted on is never produced — operators upload
|
||
final files by hand, so billed metres ≠ printed metres and a designer redoes work
|
||
the site already did.
|
||
|
||
**Decide:** port the packer to the server as the pricing authority and the print-file
|
||
generator, with the browser as preview only. This is the single largest gap between
|
||
what was promised in the meeting and what exists.
|
||
|
||
### `[?]` 3.3 — The 5 GB problem is unsolved `(F19)`
|
||
|
||
Transport accepts 5 GiB; `SCAN_MAX_BYTES` / ClamAV `StreamMaxLength` release only
|
||
≤ 128 MiB. As of 2026-09-23, customer selection and API reservation reject files
|
||
above the effective scan limit before transfer, and the Site displays the current
|
||
limit. This prevents a doomed upload; it does not deliver the promised 5 GiB path.
|
||
This is exactly the risk Jorge raised in the meeting.
|
||
|
||
**Decide:** raise the scan ceiling with a resource/timeout design, or define an
|
||
explicit large-file path (staged scan, sampled scan, operator override with audit).
|
||
|
||
### `[ ]` 3.4 — Upload throughput `(F20)`
|
||
|
||
8 MiB parts, strictly sequential in `local/static/upload.js:21`, one presign
|
||
round-trip per part → ~640 sequential API calls for a 5 GB file, through an nginx
|
||
`limit_req` of 20r/s. Add parallelism (4–6 in flight) and batch presigning.
|
||
|
||
### `[~]` 3.5 — Payment ordering `(F27)`
|
||
|
||
The local fake `pay` call now runs inside the order transaction, and the inbound
|
||
webhook records and applies a delivery transactionally. This does not make an
|
||
external charge atomic with PostgreSQL: a provider can succeed while the database
|
||
write fails, or deliver the approval later. Add a durable payment intent,
|
||
provider idempotency key and reconciliation as part of 1.1 and 3.7.
|
||
|
||
### `[~]` 3.6 — Preserve and bind the order the customer actually reviewed
|
||
|
||
The browser's width, copies, rotation, mirroring and repetitions are absent from
|
||
the API item, so the factory cannot reproduce the priced layout. Removing an
|
||
artwork can leave a stale cart item; editing after quote creation can leave the
|
||
old quote payable; a new correction can leave an obsolete final active. Persist
|
||
a versioned per-file production specification, tie the displayed cart to its
|
||
immutable quote, and tie final approval to the latest correction revision.
|
||
Cover the real editor-to-quote-to-final journey, not only the pricing table.
|
||
|
||
**Local progress 2026-09-23:** The Site includes per-upload width, length,
|
||
copies, rotation, mirroring, measurement source, and the exact placement of
|
||
each copy in production specification v2. The API checks coverage, dimensions,
|
||
film bounds, and quote height; commercial review cannot replace the layout.
|
||
The order snapshot and downloadable Kanban manifest retain it. Browser quote actions are disabled when
|
||
the cart differs, including same-price changes. Editor changes invalidate the
|
||
current cart item immediately; a new customer correction deactivates prior
|
||
finals. Browser and local API regressions pass. **Still open:** generate and
|
||
validate the final print file from the approved source revision, and
|
||
make quote/cart continuity work across devices through a server-authoritative
|
||
confirmation flow. Current quote binding is a browser guard.
|
||
|
||
### `[?]` 3.7 — Complete payment state and reconciliation rules
|
||
|
||
Event-ID deduplication does not establish which provider payment settled which
|
||
quote. Define intent creation, provider transaction ID, currency, paid-at time,
|
||
pending/rejected/refunded/cancelled states, late approval after quote expiry,
|
||
overpayment and provider success followed by database failure. Record refused
|
||
paid events for resolution. Decide who reconciles them and when production must
|
||
stop or refund. Implement with 1.1 after the checkout/refund policy is approved.
|
||
|
||
### `[~]` 3.8 — Collect a deliverable destination before charging freight
|
||
|
||
The quote has a shipping service and CEP but no recipient, street, number,
|
||
city/state or delivery snapshot. Add and validate these fields with 1.2, then
|
||
bind the chosen service and final freight amount to the payment intent.
|
||
|
||
**Local progress 2026-09-24:** the Site collects recipient, street, number,
|
||
complement, district, city and UF for delivery; the API requires them for any
|
||
non-pickup quote, requires the CEP to be the one freight was quoted for, and
|
||
refuses an address on pickup. The address is part of the reviewed quote, the
|
||
order snapshot, the Kanban card and the Tiny payload. Changing it after a quote
|
||
invalidates that quote in the browser like any other cart change. Binding the
|
||
chosen freight service to the payment intent waits on 1.2.
|
||
|
||
### `[~]` 3.9 — Make artwork quality and geometry evidence explicit
|
||
|
||
Reject or route for review when PDF page count/geometry, image decoding or DPI
|
||
cannot be established. Do not infer pixels from compressed file size, grade a
|
||
mixed item from only the readable files, silently shrink oversized artwork, or
|
||
allow a displayed DPI rejection to proceed through checkout. Use representative
|
||
real artwork in acceptance checks.
|
||
|
||
**Local progress 2026-09-22:** DPI refusal and warning acknowledgement now gate
|
||
the cart and quote API records the acknowledgement. Oversized loose-art width
|
||
is rejected in the UI, API production contract, and packer. On 2026-09-23,
|
||
unreadable loose images are blocked, mixed analyzed/manual sheets receive no
|
||
automatic grade or discount, and rotated DPI uses the pixel dimension that
|
||
corresponds to printed width. PDF dimensions now come from the parsed page
|
||
model, with page count, crop, rotation, and UserUnit checks; multi-page and
|
||
malformed PDFs block quoting. Isolated browser checks cover those cases and
|
||
same-origin PDF rendering. Unsupported PDF image operators and representative
|
||
print-file evidence still need correction before this item can close.
|
||
|
||
---
|
||
|
||
## Block 4 · Scale and performance
|
||
|
||
- `[x]` 4.1 — Ten indexes added, each matched to a query the application issues,
|
||
and no more: every extra index is paid for on each write. The outbox and live
|
||
uploads use partial indexes so they stay the size of the backlog rather than of
|
||
all history. Verified against the running database — the planner chooses
|
||
`outbox_pending` and `orders_owner` for the queries they exist for.
|
||
- `[x]` 4.2 — The board returns every order still in progress, however old, plus a
|
||
window of recent finished ones (`BOARD_FINISHED_LIMIT`, default 50) and the true
|
||
finished total. An operator can never lose a card they could act on; only terminal
|
||
ones are trimmed. The Kanban column reads "Finalizado · 50 de 213" when truncated,
|
||
so the count is not mistaken for an all-time total. Pending quotes now have
|
||
a paginated view; older completed orders still need search in 4.6.
|
||
- `[ ]` 4.3 — Scan throughput `(F21)`: one `scan_loop` thread, `worker` at
|
||
`replicas: 1`, ClamAV `MaxThreads 2`, browser gives up after 150s.
|
||
- `[x]` 4.4 — Failed image decoding no longer infers pixels from compressed
|
||
file size. Unreadable loose images cannot enter the cart or receive a grade;
|
||
an isolated browser regression covers the failure path (2026-09-23).
|
||
- `[x]` 4.5 — Dead config `(F28)`: resolved by deleting `deploy/stack.yaml` in 2.12.
|
||
`CLAMD_HOST` no longer appears anywhere; `scanning.py` reaching `'scanner'`
|
||
directly is now simply how it works, not a contradiction.
|
||
- `[~]` 4.6 — Older unpaid quotes can disappear behind the board limit.
|
||
On 2026-09-23 the board began showing newest pending and approved unpaid
|
||
quotes separately, with cursor pagination and counts; a local regression
|
||
retrieved all 105 pending and 22 approved fixture quotes and cleaned them up.
|
||
**Still open:** an explicit terminal state for abandoned/expired quotes and
|
||
search/history for older completed orders.
|
||
|
||
---
|
||
|
||
## Block 5 · Hygiene and maintenance
|
||
|
||
- `[x]` 5.1 — CI coverage `(F33)`. An `integration` job now builds the localhost
|
||
stack and runs smoke, workflow, security, scanning, retention, runtime security
|
||
and both browser suites; `publish-and-deploy` depends on it. Verified by
|
||
reintroducing the 0.1 defect: `py_compile` and the unit tests still passed while
|
||
`smoke_test` failed on `/session`, blocking the release. Browser tests skip with
|
||
a warning when the runner has no Chrome — **install `google-chrome-stable` on the
|
||
runner (or set `CHROME_BIN`) to make them gate as well.**
|
||
- `[x]` 5.2 — `portal/`, `kanban/`, `agente/`, the root `schema.sql` and
|
||
`.env.exemplo` removed: 2,283 lines implementing a model this system abandoned,
|
||
referenced by nothing, with several endpoints taking the acting user from the
|
||
request body. The documents describing them are archived under `docs/historico/`
|
||
with a header saying they are background, not instructions.
|
||
- `[x]` 5.3 — Root `requirements.txt` deleted. It pinned by wildcard, listed
|
||
packages the system does not use, and sat next to the hash-locked
|
||
`infra/requirements.lock` inviting the wrong one to be installed. Only historical
|
||
documentation referred to it.
|
||
- `[x]` 5.4 — `pip` removed from `infra/requirements.txt` and from the lock. Nothing
|
||
depended on it; it was pinned only because it was listed directly, and installing
|
||
it put a package manager inside the read-only runtime image. The base image's own
|
||
pip performs the hash-enforced install. Verified: the image builds under
|
||
`--require-hashes` and reports the base pip, 25.0.1.
|
||
- `[ ]` 5.5 — Doc drift `(F34)`. `README.md`, `CONTEXT.md`, `LOCAL_SETUP.md` and
|
||
`SECURITY_REPORT.md` describe a MinIO localhost stack, an API with "no external
|
||
network route", a `operator` / `local-operator-only` login the email-validated
|
||
model rejects, and a release gate — none match the current tree.
|
||
- `[x]` 5.6 — The Site's 1,575-line inline script is now nine files under
|
||
`local/static/`, cut at the author's own section boundaries so no function was
|
||
split: config, modes, upload, sheet analysis, PDF, quality, packing, cart, flow.
|
||
`dtf-site.html` is 1,394 lines of markup and style. The extraction was verified
|
||
byte-identical before the tags were swapped in, and they load as classic scripts
|
||
in the original order, so evaluation semantics are unchanged.
|
||
|
||
A consequence worth having: with no inline script anywhere, the policy needs no
|
||
hash allowlist and is now simply `script-src 'self'`. `site-packing.js` is also
|
||
where a server-side packer (3.2) has to agree, which was the point of splitting.
|
||
- `[ ]` 5.7 — Commercial rules duplicated between `FAIXAS` (JS) and `TIERS` (Python)
|
||
`(F26)`. `test_pricing` guards parity; generate one from the other instead.
|
||
- `[ ]` 5.11 — Nothing tests the schema against an empty database. The 4.1 indexes
|
||
were added next to the existing one, which sits before the tables they name, so
|
||
`CREATE INDEX ... ON dtf_local.order_files` ran before that table existed. Every
|
||
local run passed because the volume already had the tables; CI caught it on its
|
||
clean volume. A migration is only really exercised from nothing, so the
|
||
integration job should run `down -v` before `up` — or a dedicated step should
|
||
apply `schema.sql` twice to a fresh database, proving both a first install and
|
||
a re-run.
|
||
- `[ ]` 5.10 — The browser suites were moved into a Chrome container on the
|
||
Compose network and made required in CI. Confirm the complete checkout journey
|
||
passes in that topology and on the actual Gitea runner before closing this item.
|
||
- `[ ]` 5.9 — `local/browser_test.mjs` failed once and passed on an immediate
|
||
re-run, with no code change in between (2026-09-21). It is a deploy gate when the
|
||
runner has Chrome, so an intermittent failure there blocks releases for no reason.
|
||
Suspect Chrome startup timing or a race against stack readiness. Watch it, and if
|
||
it recurs add an explicit readiness wait rather than a retry.
|
||
- `[x]` 5.8 — The Site claimed 90-day file storage and 12-month history in two
|
||
places, and invited customers to reorder "sem subir de novo". Files are kept 30
|
||
days. The copy now states 30 days, says a later order needs the file again, and
|
||
keeps only the true part: order history remains in the account. Policy unchanged;
|
||
the promise was corrected to match it.
|
||
- `[x]` 5.12 — Repair operational entrypoints after the `local/` split.
|
||
On 2026-09-23, staging and both API images package `ops/`; staging calls
|
||
`ops.staging_readiness`; local backup calls `ops.storage_backup` through
|
||
`compose.local.yaml`; documented security and backup commands use the real
|
||
modules. Verified a network-disabled staging pass with non-secret fixture
|
||
data, a production API image import, local security status, and a local
|
||
backup/restore of the database plus 78 clean objects. Production offsite
|
||
recovery and signature freshness remain separate open items.
|
||
- `[x]` 5.15 — MinIO stopped publishing public images: by 2026-09-24 both
|
||
Docker Hub and quay.io answered anonymous pulls with 401, so a runner or
|
||
machine without a cached image could not start the stack. The local/CI
|
||
storage and storage-init now use Chainguard's MinIO build (ships `sh` and
|
||
`mc`, non-root), pinned by digest. Verified with a fresh local build and the
|
||
full integration sequence. Production uses R2 and is unaffected.
|
||
- `[ ]` 5.16 — The operator login limit (10 per account per 15 minutes) counts
|
||
successful logins too, and every test client signs in separately. The CI
|
||
sequence sits close to that limit: one extra login made the final browser
|
||
test's sign-in fail with 429 until `print_file_test` was changed to reuse
|
||
one session. Either count only failures toward the account bucket or give
|
||
the suites a shared operator session, so adding a suite cannot break
|
||
another.
|
||
- `[ ]` 5.13 — Define production recovery: scheduled encrypted offsite database
|
||
and object backups, a consistent snapshot boundary, Swarm data placement and
|
||
a restore rehearsal that opens every required live order file.
|
||
- `[ ]` 5.14 — Promote and verify one immutable release. Normal `main` pushes
|
||
now run checks only; manual dispatch requires source preflight and a configured
|
||
webhook, and scans images before publishing. Still make the full preflight
|
||
validate the active stack, deploy the tested immutable image references, test
|
||
clean install and upgrade, check application readiness after Portainer
|
||
redeploys, and isolate concurrent CI stacks.
|
||
|
||
---
|
||
|
||
## Done
|
||
|
||
### Week 1 — infrastructure, uploads, service base
|
||
|
||
- `[x]` Compose stack: Site, Kanban, API, PostgreSQL, worker, ClamAV, R2/MinIO storage.
|
||
- `[x]` Gitea + Portainer publication path; web service startup and API rollout fixed.
|
||
- `[x]` Database passwords with special characters handled via discrete libpq fields.
|
||
- `[x]` Kanban e-mail/password login; missing operator config no longer breaks stack boot.
|
||
- `[x]` Direct resumable multipart browser → private object storage.
|
||
- `[x]` Quarantine + ClamAV gate: only `clean` files can be quoted, paid, downloaded or queued.
|
||
- `[x]` Retention worker: incomplete 1d, rejected 3d, originals 7d after approval, finals 30d.
|
||
- `[x]` Server-side pricing authority with parity test against the Site JavaScript (4,444 cases).
|
||
- `[x]` Loose-artwork packing flow: per-file width, copies, rotate, mirror, live 57 cm preview,
|
||
5 mm gap, ruler and watermark preserved; metres follow packed height.
|
||
- `[x]` Rotation/mirror applied to packing masks; stale renders no longer overwrite a newer preview.
|
||
- `[x]` Hash-locked Python dependencies; Trivy reports in `output/security/`.
|
||
|
||
### Block 0 — 2026-09-18
|
||
|
||
- `[x]` `import os` restored in `local/auth.py`; `COOKIE_SECURE` is now a single
|
||
module constant shared with `local/app.py`.
|
||
- `[x]` Navigation links no longer preselect a product (`data-modo-cta` removed).
|
||
- `[x]` Ready sheet vs loose artwork is an explicit, priced, reversible selector
|
||
(`#tipoEnvio`), shown in all four modes and locked once a file is attached.
|
||
The product is the declaration; `sel()` no longer switches anything silently.
|
||
- `[x]` `medirFolha` returns `dpiFolha`; an image that cannot span the film width
|
||
at `DPI_RECUSA` is refused as a sheet, with one click to send it as loose artwork.
|
||
- `[x]` `pintaCaminhos` scoped to `#caminhos .cam` — its global `.cam` selector was
|
||
clobbering the new control.
|
||
- `[x]` `compose.local.yaml` restored (MinIO, fake providers, builds from source).
|
||
- `[x]` `APP_DB_PASSWORD` separated from `POSTGRES_PASSWORD`, with a `bootstrap.py`
|
||
guard that refuses identical credentials in both configuration forms.
|
||
- `[x]` Checkout confirmation moved out of the panel the success path hides.
|
||
|
||
### Block 2 and CI — 2026-09-18
|
||
|
||
- `[x]` The web gateway overwrites `X-Forwarded-For` with the peer address instead
|
||
of appending to it, and `client_ip()` resolves the requester for rate-limit
|
||
buckets and security events. Verified: a forged `203.0.113.99` never reaches the
|
||
audit trail.
|
||
- `[x]` Guest sessions are limited per source. The first attempt used 30/IP, which
|
||
the new regression caught as too tight for shared NAT — recreating the original
|
||
fault in a narrower form — so the ceiling is 240 per 15 minutes, overridable with
|
||
`GUEST_SESSION_LIMIT`.
|
||
- `[x]` Security events now carry the source address (`operator_login_failed`,
|
||
`customer_login_failed`, `cross_origin_rejected`, `http_security_event`).
|
||
- `[x]` CI runs the integration suites against a real stack before publishing.
|
||
|
||
### 2026-09-21
|
||
|
||
- `[x]` 2.3 — `local/secrets.py` resolves every `<NAME>_FILE` into `<NAME>` from the
|
||
API, worker and bootstrap entrypoints, failing closed on an unreadable or empty
|
||
secret and on a value supplied both ways. Verified by booting the API, the worker
|
||
and bootstrap with credentials supplied only as mounted files, including a
|
||
password containing `:/?#[]&=+$ ,%`. `OPERATOR_USER` in the stack became
|
||
`OPERATOR_EMAIL`, which is what the runtime reads.
|
||
- `[x]` 2.5 — The gate now loads `local/secrets.py` and makes it resolve every
|
||
secret `deploy/stack.yaml` declares, plus asserts it fails closed. Verified
|
||
against a no-op loader (11 blockers) and one that swallows a missing file
|
||
(1 blocker); only the real implementation passes. The four marker strings that
|
||
stopped matching when R2 support landed were removed; the two describing real
|
||
blockers stay, so the gate still refuses a release while the payment and
|
||
messaging adapters are fake.
|
||
|
||
- `[x]` 2.4 — A blocking Trivy secret scan was added and verified both ways: a
|
||
planted AWS key pair, GitHub token and private key block the job; the repository
|
||
passes clean. Worth knowing: Trivy allowlists documented example credentials, so
|
||
my first probe passed with AWS's own sample keys — the gate is a backstop, not
|
||
permission to commit secrets. The source preflight now runs and always prints its
|
||
verdict, enforcing only when `ENFORCE_PRODUCTION_PREFLIGHT` is `true`; enforcing
|
||
it today would block every deploy, since it refuses a release while the adapters
|
||
are fake. Image vulnerabilities are reported after each build, not enforced —
|
||
56 HIGH and 3 CRITICAL, only 15 with an upstream fix. `PORTAINER.md` and
|
||
`SECURITY_REPORT.md` now carry a table of what gates and what does not, replacing
|
||
descriptions of checks that never ran.
|
||
|
||
- `[x]` 2.6 — Both bases pinned by digest, OS packages upgraded in the production
|
||
images, and the web image moved off the nginx 1.28 line.
|
||
|
||
| Image | Before | After |
|
||
|---|---|---|
|
||
| API | 56 HIGH, 3 CRITICAL (15 fixable) | 46 HIGH, 0 CRITICAL |
|
||
| Web | 5 HIGH, all unfixable in place | 0 HIGH, 0 CRITICAL |
|
||
|
||
The 1.28 nginx pins `nginx=1.28.3-r1` in `/etc/apk/world`, so `apk upgrade`
|
||
cannot patch it even though Alpine ships `-r7`; `nginx:alpine` (1.31.6) is clean
|
||
while `1.29-alpine` scans worse at 37 HIGH. The two remaining "fixable" API
|
||
findings are `msgpack` and `setuptools`, which I confirmed are absent from the
|
||
built image rather than trusting the earlier report. Local images now share the
|
||
pinned bases, so the integration suite exercises what ships; full suite passes on
|
||
nginx 1.31.6. With both images at zero CRITICAL, the image scan now **gates on
|
||
CRITICAL** and reports HIGH.
|
||
|
||
### 2026-09-21 — from the runner host inventory
|
||
|
||
- `[x]` Fixed a regression in 2.1: the production gateway sits behind
|
||
`nginx-proxy-manager`, so `$remote_addr` there is the proxy, not the customer.
|
||
Overwriting `X-Forwarded-For` with it would have recorded the proxy's address for
|
||
every request in production — the same bug 2.1 set out to fix. The gateway now
|
||
uses `real_ip` to recover the customer's address from the proxy's header, trusting
|
||
only private networks, so a request arriving directly at the published port
|
||
cannot spoof it. Validated with `nginx -t` against the rendered config.
|
||
- `[~]` 2.9 downgraded: TLS is terminated by that proxy, not missing.
|
||
|
||
### Reporting
|
||
|
||
- `[x]` Week-1 client report (`Relatorio-Semana-1-DTF.docx`), corrected 2026-09-18 to
|
||
remove the inaccurate "Arquivo por metro permanece separado, com seleção explícita"
|
||
claim and the internal commit reference.
|