docs(delivery): add a step-by-step quick start and a full pre-release section

1. "0. Quick start — run these in order" at the top of DELIVERY.md: five steps
   from an empty machine to a verified running stack, copy-pasteable, with no
   decisions to make. Covers which env vars must have a value (and how to
   generate the two JWT secrets), `pnpm build:release` BEFORE
   `docker compose build`, what the boot log should say, which banner is
   expected and which means a step was missed, and how to run the end-to-end
   check. Every command in it was executed verbatim to confirm it works.

2. "Before the real release" replaces the old three-item list:
   - A. blocking config: real JWT_SECRET and a SEPARATE JWT_CUSTOMER_SECRET,
     remove OTP_MASTER_CODE, configure the SMS credentials
   - B. credentials the reference leaked and that must be rotated (SEC-12,
     SEC-06) — same live accounts, never copied into this repo
   - C. parity work not finished: admin (financial documents, users and
     permissions incl. the still-open PERM-01, reports) and apps (MON-08 first,
     then STUB-05 SMS recipients, STUB-04, STUB-03, analytics), pointing at
     ISSUES.md for the rest

Also removed the obsolete `version:` key from docker-compose.yml, which printed
a deprecation warning on every compose command and made the checklist's output
noisy. Config re-validated.

Verified after these changes: container bundle is byte-identical to the local
release build (sha256 match), and that container was re-tested end to end:
compose e2e 32/32, complete-profile 38/38, report 40/40, expert login 36/36.
Locally: jest 54/54, build 5/5, typecheck clean on all four apps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-09-23 02:38:00 +03:30
co-authored by Claude Opus 5
parent 24b7a4c395
commit 2de90c6b35
2 changed files with 153 additions and 20 deletions
+151 -18
View File
@@ -8,25 +8,158 @@ Both merged with **no conflicts** (`phase-1-admin` already contained
---
## ⚠ Before the real release — do these three things
## 0. Quick start — run these in order
1. **Set `JWT_CUSTOMER_SECRET` to its own random value.** This one is a real
security boundary, not a default. If it is unset, the API signs customer and
sales-expert **app** tokens with `JWT_SECRET` — *the same key admin tokens
use* — so the two token realms stop being cryptographically separate. The
only thing still keeping an app token off the admin routes is the `typ`
check in `jwt.strategy.ts`, and a sales expert's app token carries their
**AdminUser id** in `sub`. Two independent defences become one. The API
prints a loud banner at boot while it is missing. Tracked as **REL-03
(Critical)**.
2. **Remove `OTP_MASTER_CODE` from the environment.** While it is set, **any
active customer can sign in with that one fixed code**. Unset or empty
disables it completely, and the API prints a warning banner at boot for as
long as it is armed. Tracked as **REL-01 (Critical)** in
`docs/parity/ISSUES.md`.
3. **Configure the SMS credentials** (`SMS_TREZ_*`). Until then no customer can
receive a real login code — messages are written to the log instead of sent.
Tracked as **REL-02**.
Copy-paste, top to bottom. Nothing here needs a decision.
### Step 1 — fill in the environment
Create `.env` in the repo root. These **must** have a value:
```dotenv
# Database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=<choose a strong password>
POSTGRES_DB=goldbank
DATABASE_URL=postgresql://postgres:<the same password>@db:5432/goldbank?schema=public
# Auth — two DIFFERENT random values. Generate each with:
# node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
JWT_SECRET=<random value 1>
JWT_CUSTOMER_SECRET=<random value 2, NOT the same as above>
```
Inside Docker the database host is `db`, not `localhost`.
Everything else is optional and may be left blank. Two you may want now:
```dotenv
# Delivery only — a fixed OTP that signs in ANY active customer.
# Exactly 4 digits, or it cannot be typed into the apps. REMOVE before the
# real release (see "Before the real release" below).
OTP_MASTER_CODE=1379
# Leave blank and SMS is logged, not sent. The server still starts.
SMS_TREZ_USERNAME=
SMS_TREZ_PASSWORD=
SMS_TREZ_SENDER_LINE=
```
### Step 2 — build
```bash
pnpm install
pnpm build:release
```
**Do not skip `pnpm build:release`, and never run `docker compose build`
without it first.** The API image copies a bundle built here on the machine; it
does not build inside the image. Skipping this step produces an image from
**old code** and still reports success. This is real — it happened during this
delivery's own test run, with a bundle four days stale. (DEPLOY-03)
### Step 3 — start
```bash
docker compose up -d --build
```
Database migrations and seeding run **automatically inside the API container**
at boot. There is no manual migration step. Never run `prisma migrate reset`.
### Step 4 — check it came up
```bash
docker compose ps # db healthy, api up
docker compose logs api | tail -40
```
In the log you should see `Nest application successfully started`, and
`Applied migrations: …` (first run) or `No pending migrations` (later runs).
An `OTP_MASTER_CODE IS ACTIVE` banner is **expected** while that variable is
set — it is a reminder, not an error, and it disappears once you remove the
variable before the real release.
A `JWT_CUSTOMER_SECRET IS NOT SET` banner means **Step 1 was not done**. That
one is a real security problem, not a warning to ignore — go back and set it.
### Step 5 — prove the whole flow works
```bash
# Publishes the API on host port 3100 so the test can reach it.
docker compose -f docker-compose.yml -f docker-compose.delivery-test.yml up -d api
node apps/api/scripts/test-compose-e2e.cjs http://localhost:3100/api <OTP_MASTER_CODE>
```
Expect **32 passed, 0 failed**. It signs a sales expert and an ordinary
customer into the app, has both place an order, confirms both orders appear in
the admin panel, and re-checks the token boundaries — all against the real
container.
`docker-compose.delivery-test.yml` exists **only** for that check. The normal
deployment uses `docker-compose.yml` alone, where the API has no host port and
Dokploy/Traefik routes to it by domain.
---
## ⚠ Before the real release
Tonight's build is a **delivery build**. These are the things that must change
before it is a production release. The first three are blocking.
### A. Configuration — blocking
| # | Do this | Why it matters |
|---|---|---|
| **1** | **Set `JWT_CUSTOMER_SECRET` to its own random value**, different from `JWT_SECRET`. | Not a cosmetic default. Unset, the API signs customer and sales-expert **app** tokens with `JWT_SECRET` — *the same key admin tokens use* — and the two token realms stop being cryptographically separate. The only remaining barrier is the `typ` check in `jwt.strategy.ts`, and a sales expert's app token carries their **AdminUser id** in `sub`. Two independent defences collapse into one. A loud boot banner fires while it is missing. **REL-03 (Critical).** |
| **2** | **Remove `OTP_MASTER_CODE` from the environment.** | While set, **any active customer signs in with that one fixed code**. Unset or empty disables it completely; the API prints a banner at boot for as long as it is armed. **REL-01 (Critical).** |
| **3** | **Set real `JWT_SECRET`.** | Without it the API falls back to a well-known development string, so anyone can forge an admin token. |
| **4** | **Configure the SMS credentials** (`SMS_TREZ_USERNAME`, `SMS_TREZ_PASSWORD`, `SMS_TREZ_SENDER_LINE`). | Until then **no customer can receive a real login code** — messages are written to the log instead of sent. The server still boots (D20). **REL-02.** |
After changing any of these: `docker compose up -d --build` and confirm the
`OTP_MASTER_CODE` banner is **gone** from `docker compose logs api`.
### B. Credentials to rotate — the reference leaked them
These are committed in plaintext in `henza_server` and must be assumed
compromised by anyone who has ever had repo access. None were copied into this
repository, but they are **the same live accounts**.
- **SMS panel** username + password + sender line (`app/Models/Sms.php:73-77`) — **SEC-12 (Critical)**
- PayPing bearer token, Tahesab bearer token + IP allowlist, wallet hash pepper — **SEC-06**
### C. Parity work not finished
Phase 1 stopped after the Persons section when delivery mode began (D19). The
**Phase 1 gate still stands** as the next milestone.
**Admin — not yet audited field-by-field.** Each has had only the breakage scan
(`scan-admin-b2b.cjs`, 34 endpoints, 0 broken), not a parity audit:
- **Financial documents** (اسناد) — the money path; highest priority of the three
- **Users & permissions** (کاربران و دسترسی‌ها) — includes **PERM-01**, still
open: the database holds **178** distinct permission ids against the UI's
**174**. Unresolved because fixing it means **deleting rows**, which needs
your approval.
- **Reports** (گزارش‌ها)
- Also untouched: products/categories/variants, SMS templates, promotions,
cities, branches, workshops.
**Apps — known gaps.**
| ID | What |
|---|---|
| **MON-08** | `wagePercent`, `discountPercent` and `flatRialCharge` are taken from the **client** on order creation, so a caller can set their own making charge. `rashWeightGrams` is correctly read from the variant. The reference is *worse* (it trusts client `rash_weight` too), so this is already a partial improvement — but it is still a money path trusting client input. **Fix this first in Phase 2.** |
| **STUB-05** | Admin SMS recipients are hardcoded mobile numbers scattered across six places in the reference. Port as `SMS_ADMIN_RECIPIENTS` (comma-separated), the way `PROFILE_ADMIN_MOBILES` already works — not by copying the literals. Staff changes should not need a deploy. |
| **STUB-04** | Live gold price is **pull-on-mount only** — no socket, no polling. `quotedAt` renders as a Jalali **date**, so a stale rate looks current. |
| **STUB-03** | All list filtering and paging is **client-side** over one flat `GET /catalog/products`; no server `page`/`limit`. |
| **Analytics** | The reference PWA calls `useCustomAnalytics` on every route change (`henza_pwa/src/router/index.ts:249`); neither target app has any analytics. Deliberate — it needs a provider decision from you, not a port. |
**`docs/parity/ISSUES.md`** holds the full list with evidence and file
references — currently **40+ entries**, including everything above plus the
reference bugs deliberately not ported (SEC-14, SEC-16, APP-03, and the two
D21 items). Phase 3 is the pass over that file.
---
+2 -2
View File
@@ -1,5 +1,5 @@
version: '3.8'
# No `version:` key — it is obsolete in Compose v2 and only prints a warning on
# every command, which made the delivery checklist's output noisy.
services:
db:
image: docker.arvancloud.ir/postgres:15-alpine