# Backend Service — amn-backend ## 1. Overview **amn-backend** is the Express 5 / TypeScript API server powering the Amanat escrow marketplace (`dev.amn.gg`). It handles all buyer-seller escrow workflow logic, crypto payment processing across multiple chains and providers, real-time socket events, authentication, admin tooling, and the in-progress Mongo→PostgreSQL migration. | Field | Value | |---|---| | Current version | **2.10.5** | | Status | Active — production at `dev.amn.gg` | | Repo | `git@git.tbs.amn.gg:escrow/backend.git` | | Runtime port | 8083 (production Docker), 8080 (dev Docker), 5001 (dev default) | | Database | PostgreSQL (Drizzle ORM) — sole persistence layer as of v2.9.12 | | Node version | 22 (`.nvmrc`) | PostgreSQL is the sole active database. MongoDB references remain in some env-var config for the dual-write seam during migration, but no Mongo-backed stores remain active in normal operation (all 11 repository domains use Drizzle repos). --- ## 2. Tech Stack | Layer | Technology | |---|---| | Framework | Express 5 (TypeScript) | | Runtime | Node.js 22 | | Primary DB | PostgreSQL via Drizzle ORM (`drizzle-orm ^0.45.2`, `pg ^8.21.0`) | | Migrations | Drizzle Kit (`drizzle-kit ^0.31.1`) — 19 landed SQL migrations | | Session / Cache | Redis (`ioredis`) with Socket.IO pub-sub adapter | | Realtime | Socket.IO with Redis adapter (seller/buyer rooms) | | Auth | JWT (`jsonwebtoken`), Google OAuth, WebAuthn passkeys (`@simplewebauthn/server`), Telegram Mini App initData | | Crypto payments | Request Network, amn.scanner (in-house), DePay, SHKeeper | | Rate limiting | In-memory (express-rate-limit) — Redis adapter planned | | AI integration | OpenAI (listing descriptions, moderation) | | Email | Nodemailer via Resend SMTP | | Telegram | Bot webhook + Mini App session + identity linking | | Security | Helmet, CORS, Cloudflare Turnstile CAPTCHA, HMAC webhook verification | | Containerization | Docker (Dockerfile.prod, Dockerfile.dev) | | CI/CD | Woodpecker CI (4 pipelines) | --- ## 3. Directory Structure ``` backend/src/ ├── app.ts # Express bootstrap, middleware chain, route registration, graceful shutdown ├── cluster.ts # Node.js cluster mode entry point (multi-core) ├── controllers/ # HTTP request handlers — thin layer, delegate to services ├── db/ # Drizzle/Postgres layer │ ├── schema/ # Per-table Drizzle schema files + index.ts barrel │ ├── migrations/ # 19 numbered SQL migration files (0000–0018) │ └── repositories/ # Drizzle repos, factory.ts, backfill scripts, verify utilities ├── infrastructure/ │ └── socket/ # Socket.IO server init, room helpers, emit wrappers ├── models/ # Removed — replaced by Drizzle schemas in db/schema/ ├── routes/ # Express Router definitions (mounted in app.ts) │ ├── amnScannerWebhookRoutes.ts │ ├── blogRoutes.ts │ ├── disputeRoutes.ts │ └── pointsRoutes.ts ├── scripts/ # CLI utilities (seed:users, seed:categories, backfill, etc.) ├── seeds/ # Seed data fixtures (Postgres-capable, store-aware, idempotent) ├── services/ # Feature domain services (self-contained per domain) │ ├── address/ # Address management │ ├── admin/ # Admin-only operations, AML config, break-glass, data cleanup │ ├── ai/ # OpenAI integration (descriptions, moderation) │ ├── auth/ # JWT, OAuth, Passkey, Telegram, password reset │ ├── blockchain/ # Web3 read/verify helpers │ ├── blog/ # Posts, categories, comments │ ├── chat/ # Conversations, messages, attachments │ ├── config/ # Runtime config service │ ├── delivery/ # Delivery tracking │ ├── dispute/ # Dispute lifecycle, evidence, mediator assignment │ ├── email/ # Nodemailer transport + templates │ ├── file/ # Multer uploads, MIME validation │ ├── health/ # Health check endpoint logic │ ├── marketplace/ # PurchaseRequest, SellerOffer, Template, Shop │ ├── notification/ # Templates, delivery, mark-as-read │ ├── payment/ # Payment orchestration + provider adapters + ledger │ │ ├── adapters/ # Provider-neutral adapter interface + registry │ │ ├── amnScanner/ # amn.scanner in-house pay-in detection │ │ ├── ledger/ # Internal funds ledger (available / held / releasable) │ │ ├── migration/ # Legacy data backfill utilities │ │ ├── observability/ # Logging and incident controls │ │ ├── orchestration/ # High-level payment flow coordination │ │ ├── priceOracle/ # Chainlink + off-chain FX oracle, depeg protection │ │ ├── reconciliation/ # Webhook + status reconciliation per provider │ │ ├── request-network/# Request Network routes and webhook signature │ │ ├── requestNetwork/ # Request Network service logic │ │ ├── safety/ # Transaction Safety Provider + confirmation thresholds │ │ ├── tokens/ # On-chain token registry / decimals lookup │ │ └── wallets/ # Derived destination wallets + sweep orchestration │ ├── points/ # Loyalty points, levels, redemption │ ├── redis/ # Redis client, cache helpers, pub-sub │ ├── telegram/ # Bot webhook, Mini App session, identity linking, notifications │ ├── trezor/ # Trezor hardware-wallet signing for admin approvals │ └── user/ # Profile, preferences, addresses ├── shared/ │ ├── config/index.ts # Centralised typed env-var loader │ ├── middleware/ # authMiddleware, errorHandler, roleGuard, validators │ ├── types/ # Cross-cutting TypeScript types │ └── utils/response-handler.ts # Standard success/error response envelope └── utils/ # Pure utilities (logger, currencyUtils, etc.) ``` Each service folder is self-contained: `Service.ts`, `Controller.ts`, `Routes.ts`, `Validation.ts`. This design allows future extraction to microservices with minimal coupling. --- ## 4. Key Services / Modules | Module | Description | |---|---| | `services/auth/` | JWT issuance/refresh, Google OAuth, WebAuthn passkeys, Telegram initData verification, password reset | | `services/marketplace/` | Core escrow domain: PurchaseRequest, SellerOffer, Template, Shop lifecycle | | `services/payment/` | Payment orchestration, provider adapters, internal ledger, reconciliation | | `services/payment/ledger/` | Double-spend guard: tracks available / held / releasable balances per payment | | `services/payment/wallets/` | Derived destination address derivation (xpub) + sweep orchestration | | `services/payment/priceOracle/` | Chainlink + off-chain FX oracle for multi-currency pricing + stablecoin depeg protection | | `services/payment/safety/` | Transaction Safety Provider: confirmation thresholds, tx hash and transfer match enforcement | | `services/payment/amnScanner/` | In-house blockchain scanner webhook adapter (replaces Request Network for pay-in detection) | | `services/payment/requestNetwork/` | Request Network pay-in routes, webhook signature verification, invoice creation | | `infrastructure/socket/` | Socket.IO server init, buyer/seller room management, emit helpers | | `services/redis/` | Redis client wrapper, pub-sub channel helpers, session cache | | `services/chat/` | Conversations and message threading between buyer and seller | | `services/dispute/` | Dispute lifecycle: open, evidence, mediator, resolution | | `services/admin/` | Admin RBAC operations: AML config, break-glass, dispute management, data cleanup | | `services/telegram/` | Bot webhook handler, Mini App session auth, Telegram identity linking, push notifications | | `services/trezor/` | Trezor hardware-wallet approval gate for high-value admin actions (break-glass overrideable) | | `services/notification/` | In-app notification templates, delivery, mark-as-read | | `services/ai/` | OpenAI integration: AI-assisted listing descriptions and content moderation | | `services/email/` | Nodemailer transport via Resend SMTP, HTML email templates | | `services/points/` | Loyalty points engine, tier levels, redemption | | `services/blog/` | Blog posts, categories, comments | | `services/file/` | Multer-based file upload handler, MIME validation, upload path management | | `services/blockchain/` | Low-level Web3 read helpers: balance checks, tx confirmation polling | | `db/repositories/` | Drizzle ORM repository layer for all 11 domain entities | | `seeds/` | Idempotent Postgres seed fixtures for users, categories, shops, configs | | `scripts/` | CLI backfill, migration verify, seeding, and maintenance scripts | --- ## 5. API Surface Summary All routes are mounted under `/api/*`. See [[03 - API Reference/API Overview]] for the full endpoint reference. Key route groups: | Prefix | Domain | |---|---| | `/api/auth/*` | Registration, login, OAuth, passkeys, Telegram auth | | `/api/payment/*` | Payment CRUD, status polling, provider webhooks | | `/api/payment/request-network/*` | Request Network webhook + invoice endpoints | | `/api/amn-scanner/*` | amn.scanner webhook receiver | | `/api/marketplace/*` | Purchase requests, seller offers, templates | | `/api/chat/*` | Conversations, messages, attachments | | `/api/dispute/*` | Dispute lifecycle | | `/api/admin/*` | Admin operations (role-gated) | | `/api/notification/*` | In-app notifications | | `/api/blog/*` | Blog posts and comments | | `/api/points/*` | Loyalty points | | `/api/user/*` | User profile, preferences | | `/health` | Docker healthcheck + active store mode listing | **Rate limits (active):** | Scope | Limit | |---|---| | Auth endpoints | 10 req / 15 min | | Payment endpoints | 30 req / 15 min | | AI endpoints | 20 req / 15 min | | Global | 100 req / 15 min | | `GET /api/payment/:id` | Exempt (polling route) | | RN + Telegram webhooks | Exempt from global limiter | --- ## 6. Database ### PostgreSQL (primary — active) - **ORM:** Drizzle ORM (`drizzle-orm ^0.45.2`) - **Driver:** `pg ^8.21.0` - **Migrations:** 19 SQL files under `src/db/migrations/` (0000–0018), managed by `drizzle-kit` - **Schemas:** per-table files in `src/db/schema/`, exported via `index.ts` barrel - **Repositories:** `src/db/repositories/` — one Drizzle repo per domain; `factory.ts` provides DI - **Connection:** `PG_URL` env var (`postgres://user:pass@host:5432/db`) - **Migrations run:** `npx drizzle-kit migrate` (or via `drizzle.config.ts`) ### MongoDB (legacy — migration in progress) MongoDB and Mongoose were removed at the code level as of v2.9.12. The `MONGO_CONNECT_MODE` env var and `*_STORE` vars remain for the dual-write seam but all active domain stores use Drizzle exclusively. Remaining migration work: - Backfill execution for remaining legacy records - Per-domain read cutover verification - Chat domain normalization (current blocker) - Full runtime coupling severance See [[PRD - Mongo Retirement (Full Nuke).md]] and [[MIGRATION_TODO.md]] for status. --- ## 7. Auth Model | Method | Mechanism | |---|---| | Password | bcrypt hashed, JWT access + refresh token pair | | Google OAuth | OAuth 2.0 code flow via `google-auth-library` | | WebAuthn / Passkeys | `@simplewebauthn/server` — RP ID: `dev.amn.gg` | | Telegram Mini App | initData HMAC verification (bot token), replay window: 120 s, TTL: 24 h | | Telegram Bot | Webhook secret token header verification | | Sessions | Stateless JWT; refresh token stored in Redis | | CAPTCHA | Cloudflare Turnstile, triggered after 3 failed login attempts from same IP | **RBAC roles:** `admin`, `buyer`, `seller`, `resolver`, `guard` `roleGuard(role)` middleware is applied per-route after `authMiddleware`. The admin role unlocks break-glass, AML config, dispute management, and data cleanup endpoints. **Trezor safekeeping:** when `TREZOR_SAFEKEEPING_REQUIRED=true`, high-value admin actions (release, refund, payout) require a Trezor-signed approval message. Break-glass overrides this for 1 hour and fires a Telegram alarm. --- ## 8. Realtime (Socket.IO) - **Adapter:** Redis pub-sub (`@socket.io/redis-adapter`) — scales across multiple backend instances - **Init:** `infrastructure/socket/socketService.ts` — attaches to the HTTP server after Express bootstraps - **Room model:** - `buyer:` — buyer-facing events (payment status, offer updates, cart) - `seller:` — seller-facing events (new requests, offer accepted) - Admin rooms for dispute/notification broadcasts - **Auth:** Socket handshake verified with JWT before room join - **Known issue:** Global payment broadcasts previously wiped all users' carts (fixed in frontend v2.8.4 with a provider gate). Backend room-scoping is an open follow-up item. Key emitted events (non-exhaustive): | Event | Direction | Description | |---|---|---| | `payment:status` | Server → client | Payment state change (pending → confirmed → released) | | `offer:new` | Server → seller | New purchase request from buyer | | `offer:accepted` | Server → buyer | Seller accepted the offer | | `notification:new` | Server → client | In-app notification delivery | | `dispute:update` | Server → both | Dispute state change | | `chat:message` | Server → both | New chat message in conversation | --- ## 9. Payment Providers The payment layer uses a provider-neutral adapter interface (`services/payment/adapters/`). All providers register in the adapter registry. The ledger (`services/payment/ledger/`) enforces double-spend prevention across all providers. | Provider | Type | Chains | Status | |---|---|---|---| | **amn.scanner** | In-house blockchain scanner | ETH, BSC, Base, TON | Active — default for new payments when `AMN_SCANNER_DEFAULT=true` | | **Request Network** | Decentralized payment protocol | BSC (USDC/USDT) + ETH | Active — legacy in-flight payments; webhook-driven | | **DePay** | Widget-based crypto payments | Multi-chain | Available via adapter | | **SHKeeper** | Self-hosted crypto gateway | Bitcoin + EVM | Available via adapter | ### Payment flow 1. Buyer creates intent (`POST /api/payment`) → provider adapter creates invoice / watch address 2. Provider webhook arrives → HMAC-verified → reconciliation service updates ledger 3. Escrow holds funds → seller fulfills → admin/resolver releases or refunds 4. Ledger enforces: held → releasable → released (no double-spend) ### amn.scanner specifics - Webhook endpoint: `POST /api/amn-scanner/webhook` - HMAC verification via `AMN_SCANNER_WEBHOOK_SECRET` - Discriminator field: `payload.event` (not `eventType`) — always check this field - Provider scoped by `provider: "amn.scanner"` in payment records - Read token decimals on-chain, not from registry ### Request Network specifics - Webhook endpoint: `POST /api/payment/request-network/webhook` - Webhook secret: `REQUEST_NETWORK_WEBHOOK_SECRET` - Network: BSC mainnet, currency: USDC - Canonical proxy addresses differ per chain (ETH: `0x370DE2…`, Base: `0x189219…`) — probe before trusting ### Safety layer - `TRANSACTION_SAFETY_MIN_CONFIRMATIONS=12` (default) - Requires tx hash match and on-chain transfer match before releasing funds - AML screening: `none` (default), `ofac` (OFAC SDN list, local, free), or `chainalysis` ### Price oracle / depeg protection - Providers: Chainlink + off-chain FX (`OFFCHAIN_FX_URL`) - Chains: ETH (RPC via `CHAINLINK_RPC_1`), BSC (via `CHAINLINK_RPC_56`) - Depeg hard cap: `DEPEG_HARD_CAP_BPS` (default 500 bps = 5%) - Oracle max staleness: `ORACLE_MAX_STALENESS_S=120` - Currently disabled (`ORACLE_QUOTING_ENABLED=false`) — enable after FX feeds are configured --- ## 10. CI/CD (Woodpecker) Four pipelines in `backend/.woodpecker/`: ### `production.yml` — primary deploy pipeline Trigger: `push` to `main`/`master` · Platform: `linux/arm64` | Step | Description | |---|---| | `get-version` | Reads `package.json` version, writes `dev-` to `.tags` | | `typecheck` | `npm ci` + `npm run typecheck` — gates image build on clean TypeScript (cached npm on host) | | `build-and-deploy` | `docker build -t git.tbs.amn.gg/escrow/backend:dev` locally on the agent, then `docker compose up -d --no-deps --pull never backend` — no registry push, image stays local | | `notify` | Posts plain-text result to Telegram via `scripts/ci/tg-notify.cjs` (no parse_mode) | > No registry push on production pipeline — agent is co-located with the stack; pushing large images over Tailscale times out. ### `development.yml` — parked Trigger: `event: cron` (no cron configured — effectively disabled). Targets legacy `git.manko.yoga` registry and retired Arcane deploy. Use `manual.yml` for manual playground builds. ### `manual.yml` — manual build playground Trigger: manual. Builds and pushes to `git.tbs.amn.gg/escrow/backend`. Used for testing the pipeline independently. ### `cleanup.yml` — image cleanup Trigger: scheduled/manual. Removes old image tags from the registry. **Important CI notes:** - Always bump `package.json` version before pushing a CI-triggering commit — otherwise the build tag doesn't change and the deployed image may be stale. - CI green does not guarantee the image was pushed — verify `git.tbs.amn.gg` has the `dev-` tag before trusting the deploy. - Woodpecker eats `${VAR}` in commands — use `$VAR` or `$$VAR`; prefer plugins over raw curl for notifications. --- ## 11. Local Development Quick-Start ```bash # Clone and install git clone git@git.tbs.amn.gg:escrow/backend.git cd backend npm install # Copy environment file cp .env.example .env.local # Edit .env.local — set PG_URL, REDIS_URI, JWT_SECRET at minimum # Start dependencies (Postgres + Redis) docker compose -f docker-compose.local.yml up -d # Run DB migrations npx drizzle-kit migrate # Start dev server (hot-reload) npm run dev # → listens on http://localhost:5001 # OR run in dev Docker docker compose -f docker-compose.dev.yml up # → listens on http://localhost:8080 # Seed database npm run seed:users npm run seed:categories ``` **Typecheck (required before push):** ```bash npm run typecheck ``` A pre-push git hook blocks the push on tsc errors. If a parallel agent's mid-refactor tree has errors, use explicit `git add ` — never `git add -A`. **Run tests:** ```bash npm test ``` Test files live in `__tests__/`. --- ## 12. Environment Variables | Variable | Description | |---|---| | `NODE_ENV` | `production` / `development` / `test` | | `PORT` | HTTP listen port (default 5001) | | `TRUST_PROXY_HOPS` | Number of reverse-proxy hops in front of app | | `FRONTEND_URL` | Allowed CORS origin for frontend | | `BACKEND_URL` | Public backend base URL | | `PG_URL` | PostgreSQL connection string | | `POSTGRES_USER` | Postgres username (Docker init) | | `POSTGRES_PASSWORD` | Postgres password (Docker init) | | `POSTGRES_DB` | Postgres database name (Docker init) | | `MONGO_CONNECT_MODE` | `always` / `never` / `optional` — Mongo connection behavior (legacy) | | `REDIS_URI` | Redis connection URI | | `JWT_SECRET` | HS256 signing secret for access tokens | | `JWT_EXPIRES_IN` | Access token TTL (e.g. `7d`) | | `REFRESH_TOKEN_EXPIRES_IN` | Refresh token TTL (e.g. `30d`) | | `ADMIN_EMAIL` | Bootstrap admin account email | | `ADMIN_PASSWORD` | Bootstrap admin account password | | `SEED_USERS` | `true` to auto-seed users on dev boot | | `SEED_PASSWORD_ADMIN` | Admin seed account password | | `SEED_PASSWORD_SUPPORT` | Support seed account password | | `SEED_PASSWORD_BUYER` | Buyer seed account password | | `SEED_PASSWORD_SELLER` | Seller seed account password | | `GOOGLE_CLIENT_ID` | Google OAuth client ID | | `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | | `WEBAUTHN_RP_ID` | WebAuthn relying party ID (e.g. `dev.amn.gg`) | | `WEBAUTHN_RP_NAME` | WebAuthn relying party display name | | `WEBAUTHN_RP_ORIGIN` | WebAuthn allowed origin | | `SMTP_HOST` | SMTP server host | | `SMTP_PORT` | SMTP server port | | `SMTP_SECURE` | `true` for TLS | | `SMTP_USER` | SMTP username | | `SMTP_PASS` | SMTP password | | `SMTP_FROM` | From address for outgoing email | | `RESEND_WEBHOOK_SECRET` | Resend inbound webhook signing secret (`whsec_…`) | | `TURNSTILE_SECRET_KEY` | Cloudflare Turnstile server-side secret (empty = CAPTCHA disabled) | | `RATE_LIMIT_WINDOW_MS` | Rate limit window in milliseconds | | `RATE_LIMIT_MAX_REQUESTS` | Max requests per window (global) | | `MAX_FILE_SIZE` | Upload max file size in bytes | | `UPLOAD_PATH` | Server-side upload directory | | `PAYMENT_PROVIDER_MODE` | `live` / `test` | | `PAYMENT_LEDGER_ENFORCEMENT` | `true` to enforce double-spend ledger guard | | `ESCROW_WALLET_ADDRESS` | Platform escrow wallet address | | `RECEIVER_WALLET_ADDRESS` | Platform receiver wallet address | | `REQUEST_NETWORK_ENABLED` | Enable Request Network provider | | `REQUEST_NETWORK_API_KEY` | Request Network API key | | `REQUEST_NETWORK_NETWORK` | Target chain (`bsc`, `eth`, etc.) | | `REQUEST_NETWORK_WEBHOOK_SECRET` | HMAC secret for RN webhook verification | | `AMN_SCANNER_URL` | amn.scanner service base URL | | `AMN_SCANNER_WEBHOOK_SECRET` | HMAC secret for scanner webhook verification | | `AMN_SCANNER_DEFAULT` | `true` to make amn.scanner the default provider | | `ORACLE_QUOTING_ENABLED` | Enable on-chain oracle pricing + depeg protection | | `PRICE_ORACLE_PROVIDERS` | Comma-separated oracle providers (`chainlink,offchain_fx`) | | `ORACLE_MAX_STALENESS_S` | Max oracle data age in seconds | | `DEPEG_HARD_CAP_BPS` | Stablecoin depeg hard cap in basis points | | `OFFCHAIN_FX_URL` | Off-chain FX rate source URL (required for IRR/TRY) | | `CHAINLINK_RPC_1` | Private RPC override for Chainlink on ETH mainnet | | `CHAINLINK_RPC_56` | Private RPC override for Chainlink on BSC | | `DERIVED_DESTINATION_XPUB` | xPub for derived payment address derivation | | `DERIVED_DESTINATION_SWEEP_SIGNER` | Sweep signing mode: `build-only` / `hot-key` / `kms` / `trezor` | | `DERIVED_DESTINATION_SWEEP_INTERVAL_MS` | Sweep cron interval in ms (0 = disabled) | | `SWEEP_MASTER_PRIVKEY` | Master sweep wallet private key (gas funder) | | `TREZOR_SAFEKEEPING_REQUIRED` | `true` to require Trezor approval for admin actions | | `TRANSACTION_SAFETY_ENABLED` | Enable transaction safety layer | | `TRANSACTION_SAFETY_MIN_CONFIRMATIONS` | Minimum on-chain confirmations before release | | `TRANSACTION_SAFETY_AML_PROVIDER` | AML provider: `none` / `ofac` / `chainalysis` | | `CHAINALYSIS_API_KEY` | Chainalysis API key (when AML provider = chainalysis) | | `TELEGRAM_BOT_TOKEN` | Telegram bot token | | `TELEGRAM_WEBHOOK_SECRET_TOKEN` | Telegram webhook secret token header value | | `TELEGRAM_INITDATA_MAX_AGE_SEC` | Max age for Telegram initData (default 86400 s) | | `TG_NOTIFY_CHATS` | Comma-separated Telegram chat IDs for CI/admin notifications | --- ## 13. Known Issues / Open Items | Issue | Status | Notes | |---|---|---| | Mongo→PG migration incomplete | In progress | Chat normalization is the current blocker; read cutover and backfill exec pending | | Backend room-scoping for socket events | Open | Frontend provider gate is in place (v2.8.4); backend should scope payment events to `seller:` rooms to prevent cross-user leakage | | Rate limit counters are in-memory | Open | Not shared across instances; Redis adapter planned for distributed deployments | | Oracle quoting disabled | Open | `ORACLE_QUOTING_ENABLED=false`; requires FX feed configuration before enabling | | amn.scanner multi-seller + multi-chain gap | Open | Current scanner watches one chain; multi-seller and multi-chain support not yet verified | | Woodpecker development.yml parked | Known | Targets legacy registry; needs repointing to `git.tbs.amn.gg` and new Arcane deploy before re-enabling | | Trezor safekeeping off by default | By design | `TREZOR_SAFEKEEPING_REQUIRED=false`; must be enabled explicitly in production once admin xpub is registered | | Request Network canonical proxy addresses | Known | RN's CREATE2 canonical-address claim is false for ETH and Base — probe actual address before trusting | | JSON assets not copied to dist/ | Fixed (requires postbuild) | `tsc` does not copy `.json` files; explicit `postbuild` copy step required for any `fs.readFileSync` on JSON assets | | Parallel agent push conflicts | Operational | mojtaba agent pushes to same branches; always `git fetch --rebase` before pushing; expect version-bump conflicts |