docs: fork analysis + improvement harvest spec and todo
Deep analysis of marketplaces-main.zip (hub.numus.cc/numus/marketplaces). Findings: - Not a fork of us. Separate platform monorepo (NestJS + Postgres + 2 Angular apps + infra) sharing an older dexarmarket ancestor. - Our repo is vendored inside it as reference/parallel-frontend/, SHA-256 pinned, dated 11 Aug 2026, classified "reference, not production". - Zero VK/Yandex/OAuth code anywhere in their source. Their only customer login is Telegram, proxied to an external service. - They lead on backend truth and ops; we lead on frontend depth, tests, e2e, and framework currency. Three of their audit findings are still live in our code and are defects, not just posture: plaintext ip-api.com call from an HTTPS origin (mixed content, region detect silently dead), unvalidated bypassSecurityTrustResourceUrl on a bank URL rendered in an iframe, and provider credentials plus a partner ID literal in the bundle. Adds: - docs/FORK-ANALYSIS-2026-08-21.md - full comparison, their audit of us assessed line by line, and a VK ID + Yandex ID design. - docs/superpowers/specs/2026-08-21-fork-harvest-design.md - working brief, five lanes, four waves, scope and rejection rules. - docs/FORK-HARVEST-TODO.md - 42 items with effort, dependencies and acceptance criteria. Improvements only; nothing regresses our Angular version, test count, or architecture governance. No implementation changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
392
docs/FORK-ANALYSIS-2026-08-21.md
Normal file
392
docs/FORK-ANALYSIS-2026-08-21.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# Fork Analysis — `marketplaces-main.zip` (hub.numus.cc/numus/marketplaces)
|
||||
|
||||
**Date:** 2026-08-21
|
||||
**Artifact analysed:** `C:\Users\darbi\Downloads\marketplaces-main.zip` (4.48 MB, 13 MB extracted)
|
||||
**Analysed against:** this repo, branch `B2B`, HEAD `92f1c88`
|
||||
**Canonical repo of the archive:** `ssh://git@hub.numus.cc:2222/numus/marketplaces.git`, tag `handoff-baseline-2026-08-11`
|
||||
|
||||
---
|
||||
|
||||
## 0. Verdict in five lines
|
||||
|
||||
1. This is **not a fork of our repo**. It is a **separate monorepo** — NestJS backend + PostgreSQL + two Angular apps + real infrastructure — that shares an *older* common ancestor with us (`dexarmarket`, Angular 21.2.18).
|
||||
2. They **received our code on 11 Aug 2026**, audited it, and parked it verbatim under `reference/parallel-frontend/` with a SHA-256 fingerprint. They explicitly ruled it **not production**, and wrote a document listing what they will and will not take from us.
|
||||
3. They are ahead of us in exactly one dimension, and it is the decisive one: **they have a backend, a database, RBAC, payments, tenancy by Host, publish/rollback revisions, and a deploy runbook that exists.** We have contracts describing all of that and 22 mock gateways.
|
||||
4. We are ahead of them in exactly one dimension, and they admit it in writing: **frontend depth and editor UX** (530 `.ts` vs 189, 158 components, 30 spec files, boundary checker, Angular 22). Their backoffice is 45 files and still ships `mock-data.service.ts`.
|
||||
5. **VK and Yandex login do not exist in their code.** Zero references, backend and frontend. Section 8 covers what they actually have and gives the design to add VK ID + Yandex ID on our side.
|
||||
|
||||
---
|
||||
|
||||
## 1. What the archive actually contains
|
||||
|
||||
```
|
||||
marketplaces/
|
||||
├── platform-api/ NestJS 11 + Fastify + Prisma + PostgreSQL 17 (44 .ts, ~4 800 LOC)
|
||||
├── backoffice/ Angular 21.2.18 admin + order-manager portal (45 .ts)
|
||||
├── marketplaces/ Angular 21.2.18 runtime storefront (189 .ts)
|
||||
├── infra/ Docker Compose, Nginx, backup, domain automation (21 files)
|
||||
├── docs/ 11 canonical documents, ~1 090 lines, Russian
|
||||
└── reference/
|
||||
└── parallel-frontend/ ← OUR REPO, verbatim, 791 files
|
||||
```
|
||||
|
||||
### 1.1 Lineage — read this carefully
|
||||
|
||||
- `marketplaces/package.json` is named **`dexarmarket`**, Angular **21.2.18**, with brand configs `dexar` / `novo` / `lavero`.
|
||||
- Our `package.json` is named **`dexarmarket`**, Angular **22.0.8**.
|
||||
- `reference/parallel-frontend/package.json` is **our current code**, Angular 22.0.8, with our `arch:check` scripts.
|
||||
|
||||
Common ancestor. They branched earlier and went backend-first; we stayed frontend and went deep. `reference/parallel-frontend/SOURCE_MANIFEST.md` records:
|
||||
|
||||
> Source: `marketplaces-main.zip`, received 11 August 2026. SHA-256 `4d2d990416f573791b09df9ca8c02266432b5ddf213b9ed38c0bced1f19d854f`. 745 files in `src/`, 26 in `public/`, 1 in `tools/`.
|
||||
|
||||
They stripped our sprint reports and internal task docs and replaced them with their own summary. Our source they copied unchanged.
|
||||
|
||||
### 1.2 They are not "overtaking us" — they were handed the platform mandate
|
||||
|
||||
`docs/DEVELOPER_HANDOFF.md` is a **handover-to-a-new-developer document**, with a priority ladder that puts our work last:
|
||||
|
||||
> 1. Security, tenant isolation and financial correctness.
|
||||
> 2. `docs/PRODUCT_SPECIFICATION.md`.
|
||||
> 3. Real models and invariants of `platform-api`.
|
||||
> 4. Existing confirmed production scenarios.
|
||||
> 5. **UI/UX patterns of the parallel implementation.**
|
||||
|
||||
That is the political read: our repo has been reclassified from "the product" to "the design reference". Everything below assumes we want that reversed or renegotiated.
|
||||
|
||||
---
|
||||
|
||||
## 2. Inventory comparison
|
||||
|
||||
| | **Them (archive)** | **Us (`B2B` @ 92f1c88)** |
|
||||
|---|---|---|
|
||||
| Backend | NestJS 11 / Fastify, 44 files, running | None. 17 contract docs in `docs/backend/` |
|
||||
| Database | PostgreSQL 17, Prisma, 36 models, 3 migrations | None |
|
||||
| Storefront | Angular 21.2.18, 189 `.ts` | Angular 22.0.8, 530 `.ts`, 158 components |
|
||||
| Backoffice | Angular 21.2.18, 45 `.ts`, still mock-backed | 14 admin modules, 22 local + 22 API gateway pairs |
|
||||
| Auth (admin) | Email + Argon2id + mandatory TOTP, HttpOnly cookie, server sessions | `admin-auth.guard.ts` + dev bypass |
|
||||
| Auth (customer) | Telegram QR via external `USERAUTH_API_URL`, server session, HttpOnly cookie | Telegram, client-side |
|
||||
| RBAC | 5 roles enforced server-side + per-marketplace membership | Client-side permission model |
|
||||
| Payments | Vitanova + NUMUS adapters, encrypted per-tenant credentials, HMAC webhooks, idempotency, poll fallback | FX/pricing gateways, payment contracts, no server |
|
||||
| Multi-tenancy | Host → verified `MarketplaceDomain` → tenant, 30 s cache, 404 on unknown | Bootstrap-driven runtime config |
|
||||
| Publish | Immutable `MarketplaceRevision` snapshots, atomic publish, rollback-as-new-revision | Draft/publish UI, local persistence |
|
||||
| Infra | Compose (internal + egress networks), Nginx templates, certbot, WAL archiving, backup timers, restore check, fail2ban, sysctl/ssh hardening | `scripts/deploy/*.sh`, GH Actions deploy, wildcard TLS |
|
||||
| Tests | 9 spec files, 25 tests total, **no e2e at all** | 30 spec files, Playwright e2e, coverage floor in CI |
|
||||
| Arch governance | None | `check-boundaries.mjs`, madge cycles, `architecture-governance.yml` |
|
||||
| Bundle | storefront 648 kB (48 kB over) | 1.15 MB (452 kB over a 700 kB budget) — *their measurement of us* |
|
||||
|
||||
---
|
||||
|
||||
## 3. Their backend, in detail — the part worth studying
|
||||
|
||||
### 3.1 Tenant resolution (`common/tenant.service.ts`)
|
||||
|
||||
- `normalizeHost()` lowercases, strips trailing dot, strips port.
|
||||
- Looks up `MarketplaceDomain` by `hostname` **unique index**, requires `verifiedAt != null`, requires `status = ACTIVE`.
|
||||
- 30-second in-process cache keyed by hostname, with `invalidate(hostname?)`.
|
||||
- Unknown host → `404`, never a fallback tenant.
|
||||
- **Preview:** HMAC-signed token `base64url(payload).base64url(hmac)` carrying `{marketplaceId, expiresAt, nonce}`, 15 min TTL, delivered as a `storefront_preview` cookie. A global Fastify `onRequest` hook returns `404 Preview mode is read-only` for any non-GET on `/api/v1/*` while that cookie is present. Cheap, clean, and something we do not have.
|
||||
|
||||
### 3.2 Admin auth (`auth/admin-auth.service.ts`)
|
||||
|
||||
- Argon2id (`memoryCost 65536, timeCost 3, parallelism 1`), TOTP **mandatory** — first login without TOTP returns a signed 10-minute `setupToken` + `otpauth://` URI and refuses to issue a session until TOTP is confirmed.
|
||||
- Sessions are random 32 bytes, stored as **SHA-256 hash only**, 12 h TTL, with `ipAddress` + `userAgent`.
|
||||
- `authenticate()` rejects if `revokedAt`, expired, user inactive, **or TOTP not enabled**.
|
||||
- Password change requires ≥16 chars and revokes every live session in the same transaction.
|
||||
- Role weights: `ORDER_MANAGER 0 < VIEWER 1 < CONTENT_MANAGER 2 < ADMIN 3 < OWNER 4`; `hasAccess()` checks weight **and** marketplace scope.
|
||||
- Manager portal uses a **separate cookie** (`manager_session`) and a separate guard, pinned to one marketplace slug.
|
||||
|
||||
### 3.3 CSRF / origin control (`main.ts`, `auth/admin-origins.ts`)
|
||||
|
||||
A global hook rejects any non-GET on `/api/admin/*` or `/api/manager/*` whose `Origin` header is not in `ADMIN_ORIGIN` + `ADMIN_ORIGINS`. CORS `origin` is the same allowlist with `credentials: true`. Rate limit 120/min per IP; multipart capped at 1 file / 10 MB / 4 fields; body limit 12 MB; `trustProxy: true`; global `ValidationPipe({ whitelist, forbidNonWhitelisted, transform })`.
|
||||
|
||||
### 3.4 Checkout (`checkout/checkout.service.ts`) — the atomicity pattern
|
||||
|
||||
```sql
|
||||
UPDATE "MarketplaceInventory"
|
||||
SET "reserved" = "reserved" + $qty, "updatedAt" = NOW()
|
||||
WHERE "marketplaceId" = $mp AND "variantId" = $variant
|
||||
AND ("onHand" - "reserved") >= $qty
|
||||
RETURNING "id"
|
||||
```
|
||||
|
||||
Empty result → `409 Insufficient stock`. This single conditional UPDATE inside a Prisma transaction is the whole oversell defence: no read-then-write race, no advisory locks. Then a `StockReservation` (15 min), one `InventoryMovement` per line with `reason: 'checkout_reservation'`, and an `Order` carrying a full `productSnapshot` per item. Price comes only from the server-side snapshot; the browser's price is never read. `publicToken` is `randomBytes(24).base64url` — no sequential IDs leak. Digital codes are decrypted into the response **only** when order status is `PAID`/`PROCESSING`/`FULFILLED`.
|
||||
|
||||
### 3.5 Payments (`payments/payment.service.ts`)
|
||||
|
||||
- `Payment.idempotencyKey` is a **unique column**; a repeat POST with the same key returns the existing payment, and a key reused across a different order/marketplace → `409`.
|
||||
- Provider credentials live in `PaymentCredential.encryptedConfig`, AES-256-GCM (`v1.iv.tag.ciphertext`, base64url) with a 32-byte `FIELD_ENCRYPTION_KEY`. Decrypted only inside the service, never serialized into a response.
|
||||
- NUMUS webhook: requires `eventId`/`eventType`/`timestamp`/`signature` headers, validates the envelope (`schemaVersion === 1`), HMAC-verifies the **raw body**, then inserts into `PaymentWebhookEvent` with `@@unique([provider, eventKey])`. A Prisma `P2002` collision returns `{accepted: true, duplicate: true}` — replay is a no-op by construction, not by an `if`.
|
||||
- Vitanova webhook: same shape, per-marketplace `webhookSecret` overriding the global one, `eventKey` falling back to `sha256(rawBody)`.
|
||||
- `pollPending(50)` is the reconciliation fallback for the last 24 h; failures are swallowed so the next tick retries.
|
||||
- `checkoutUrl` passes through `safeHttpsUrl()` before it is ever returned to a browser.
|
||||
|
||||
### 3.6 Publish / revisions (`admin/admin.service.ts`)
|
||||
|
||||
`publish()` materializes the draft into a full snapshot, computes `version = max(version) + 1`, writes an immutable `MarketplaceRevision`, and flips `publishedRevision` in the same transaction. Clone copies theme/categories/offers/variants/category links and **forces inventory to zero**, drops domains/customers/orders/secrets, and creates an OWNER membership for the actor. Category cloning is a topological walk that throws `Category tree contains a cycle`.
|
||||
|
||||
### 3.7 Config validation (`common/storefront-config.ts`)
|
||||
|
||||
The section schema is validated **server-side**, per section type, with hard clamps: max 40 sections; id regex; per-type height ranges (`categoryRail 98–360`, `productRail 390–760`, hero fallback 312); colour must match `#rrggbb` or fall back; URLs accepted only if local `/path` or `https://`; product/category ID lists deduped and capped at 24 UUIDs. This is exactly the "validation engine" they praised in our editor — except theirs runs where it actually binds.
|
||||
|
||||
### 3.8 Infrastructure (`infra/`)
|
||||
|
||||
- Compose with **two networks**: `platform` (`internal: true`, no egress — Postgres lives here) and `egress` (API + worker + migrate only).
|
||||
- API bound to `127.0.0.1:3000` only. `no-new-privileges` on every service.
|
||||
- Postgres 17.7 with `wal_level=replica`, `archive_mode=on`, `archive_timeout=300`, archive command copying WAL into a backup volume.
|
||||
- `migrate` is a separate one-shot service; `api` and `worker` both `depends_on: migrate: service_completed_successfully`.
|
||||
- Healthchecks: the API container hits its own `/health/ready`.
|
||||
- systemd timers: `marketplaces-backup`, `marketplaces-domain-sync`, `marketplaces-thumbnails` (path-triggered), plus `*-healthcheck` timers per app.
|
||||
- `provision-domain.sh` refuses to run unless the domain's A record already resolves to the server IP, then certbot webroot, then a **manual** review step before Nginx reload.
|
||||
- Hardening set we do not have: `fail2ban/jail.local`, `sshd` hardening drop-in, `sysctl` hardening, `docker/daemon.json`, scoped sudoers per deploy role, `restore-check.sh`.
|
||||
|
||||
---
|
||||
|
||||
## 4. What they wrote about us (`docs/PARALLEL_IMPLEMENTATION_AUDIT.md`)
|
||||
|
||||
Their measurements of our code, 11 Aug 2026:
|
||||
|
||||
- Production build passes on Node 24.18.1.
|
||||
- **Initial bundle ~1.15 MB against a 700 kB budget — 452 kB over.**
|
||||
- Boundary + cycle checks pass.
|
||||
- 57 unit tests pass; **5 spec files total**; "checkout, RBAC, publishing, orders and admin CRUD flows are not meaningfully covered".
|
||||
- `npm audit --omit=dev`: **0 production vulnerabilities** — better than all three of their own packages.
|
||||
|
||||
Their blocking objections:
|
||||
|
||||
| Their objection | Is it fair? |
|
||||
|---|---|
|
||||
| Mock/localStorage repositories as production implementation | **Fair.** 22 local gateways, 19 files touching `localStorage`. |
|
||||
| Publishing config through localStorage | **Fair** for the modules that still do it. |
|
||||
| Admin JWT / refresh token in localStorage | Fair as of the snapshot. |
|
||||
| Admin session cookie set by JS and readable via `document.cookie` | **Fair and serious.** |
|
||||
| Shared Telegram session for customer *and* admin | **Fair and serious.** |
|
||||
| Client-side `authorization-key`, `userid-value`, partner ID | **Fair and serious** — payment credentials in the browser. |
|
||||
| Direct call to `http://ip-api.com` from an HTTPS storefront | Fair — mixed content plus a third-party geo leak. |
|
||||
| Unconditional `bypassSecurityTrustResourceUrl` on a bank URL | **Fair.** Redirect targets must be backend-allowlisted. |
|
||||
| Storefront + editor + backoffice in one deployable bundle | Fair, and it is also why our bundle is 452 kB over. |
|
||||
| Hardcoded fallback regions / provider URLs / brands in components | Fair. |
|
||||
|
||||
What they said they **want** from us (their P1 list, their order): editor information architecture; the section-editor schema; the validation engine with blockers/warnings/notices; undo/redo + dirty state + change summary; device preview and per-device media; searchable product/category pickers with SKU/price/stock; the media-library interaction model; semantic design tokens; the admin IA; and **our boundary checker and ADR tooling**.
|
||||
|
||||
That list is our leverage. It is also a precise statement of which of our modules are worth hardening first.
|
||||
|
||||
---
|
||||
|
||||
## 5. Differences that matter, ranked by consequence
|
||||
|
||||
1. **Truth ownership.** Their price, stock, tenant and payment truth is server-side and provably so. Ours is a contract document. Every argument about "who is ahead" reduces to this one.
|
||||
2. **Session model.** They: server-stored, hashed, HttpOnly, revocable, TOTP-gated, separate cookies per contour. Us: client-held.
|
||||
3. **Idempotency.** They: unique constraints doing the work (`Payment.idempotencyKey`, `PaymentWebhookEvent(provider,eventKey)`). Us: zero `idempot*` anywhere in the codebase.
|
||||
4. **Deployability.** They: Compose + migrations + healthchecks + WAL + restore check + domain automation. Us: shell scripts and GH Actions, with no database to migrate.
|
||||
5. **Frontend depth.** Us: 2.8× their storefront file count, 158 components, dynamic renderer, widget system, theme system, i18n, 30 spec files, Playwright e2e, boundary governance. Them: a 45-file backoffice with `mock-data.service.ts` still in it.
|
||||
6. **Test posture.** They have **no e2e whatsoever** and 25 unit tests across the whole platform. Their own `VERIFICATION.md` says the count "is insufficient to conclude production readiness". We have e2e plus a CI coverage floor. This is a real gap on their side and worth naming out loud.
|
||||
7. **Framework currency.** We are on Angular 22 / TS 6.0.3; both of their apps are on 21.2.18 with 7–8 fixable high findings in production dependencies.
|
||||
|
||||
---
|
||||
|
||||
## 6. What we should take — concrete, ordered
|
||||
|
||||
### P0 — take these regardless of how the org question resolves
|
||||
|
||||
1. **Conditional-UPDATE stock reservation.** Adopt the pattern verbatim in `docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md`: reserve via `WHERE (onHand - reserved) >= qty RETURNING id`, empty result = 409. It removes a whole class of race conditions and it is one line of SQL.
|
||||
2. **Idempotency as a unique constraint, not application logic.** `Payment.idempotencyKey UNIQUE`, `PaymentWebhookEvent @@unique([provider, eventKey])`, P2002 → `{duplicate: true}`. Push this into `PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md` as a schema requirement, not a behavioural note.
|
||||
3. **Move every credential out of the browser.** Their audit is right about `authorization-key` / `userid-value` / partner ID. Mirror `PaymentCredential.encryptedConfig` (AES-256-GCM, versioned `v1.iv.tag.ct`) in our contract and delete the client-side header path.
|
||||
4. **HttpOnly server sessions, separate cookie per contour** (`bo_session`, `manager_session`, `marketplace_session`). Kill the JS-set cookie and the shared Telegram session for admin + customer. This is our single worst finding in their audit.
|
||||
5. **Origin allowlist hook for all admin mutations.** Twelve lines in `main.ts`; kills CSRF for cookie-authenticated mutations. Mirror in `TRACK-S-SECURITY-RBAC-CONTRACT.md`.
|
||||
6. **Backend-side config validation.** Our validation engine is better than theirs, but it runs in the browser. The server must re-run it. Their clamp-and-fallback ergonomics are right: never reject a colour, clamp it; never accept a non-https URL, blank it.
|
||||
|
||||
### P1 — take into our own architecture
|
||||
|
||||
7. **Signed preview token + read-only preview enforcement.** HMAC token, 15 min, `storefront_preview` cookie, global hook rejecting non-GET. We have preview UI and no preview safety.
|
||||
8. **Immutable revisions with `version = max+1`, rollback-as-new-revision.** Never rewrite history; `publishedRevision` is an integer pointer flipped in-transaction.
|
||||
9. **Clone semantics.** Copy design + catalog assignments, force inventory to 0, never copy domains/customers/orders/secrets. Their topological category walk with cycle detection is worth copying line for line.
|
||||
10. **`MarketplaceAuthCredential` table.** They have it and do not use it. It is exactly the right home for per-tenant VK/Yandex OAuth app credentials — see §8.
|
||||
11. **Two-network Compose split** (`internal: true` for the data network) and API bound to loopback. Makes "the database is not reachable from the internet" structural rather than a firewall promise.
|
||||
12. **WAL archiving + `restore-check.sh` + a scheduled restore drill.** We have deploy automation and no proven restore.
|
||||
|
||||
### P2 — process, not code
|
||||
|
||||
13. Their **`DEVELOPER_HANDOFF.md` §7 "inviolable invariants"** list is a better acceptance gate than anything currently in our delivery plan. Nine lines, each falsifiable. Adopt it as the header of our own handoff doc.
|
||||
14. Their **PR policy**: one functional area per PR; mandatory purpose, screenshots, API changes, migrations, test evidence, security impact, rollback plan; never change payment/inventory/order state machines inside a redesign PR.
|
||||
15. Their **status discipline**: "a local build or the existence of a UI does not mean production readiness". Every release records version, migration, healthcheck, smoke, audit, rollback.
|
||||
|
||||
---
|
||||
|
||||
## 7. Ideas worth stealing (product-level)
|
||||
|
||||
- **`ORDER_MANAGER` as a fully separate contour** — separate URL, separate shell, separate cookie, separate login, pinned to one marketplace, cannot see catalog/design/domains/payment settings. Genuinely good product thinking: the people who touch orders all day are not admins, and giving them their own small app removes an entire permissions surface.
|
||||
- **`FulfillmentMode: MANUAL | CODE_POOL` + a `DigitalCode` pool** with `AVAILABLE/RESERVED/ASSIGNED/REVOKED`, encrypted values, `valueHash` unique per `(marketplace, variant)`, and codes revealed only after payment. We have no digital-goods story at all; this is a complete one in one table.
|
||||
- **Marketplace status machine** `DRAFT → DOMAIN_PENDING → READY → ACTIVE → SUSPENDED`, with `DOMAIN_PENDING` as a real state rather than an error condition.
|
||||
- **Per-tenant delivery options priced in minor units on the offer**, validated at checkout ("select a delivery option for each physical product").
|
||||
- **`InventoryMovement` as an append-only journal** with `reason`, `referenceType`, `referenceId`, `actorId` — every stock change explainable after the fact. This directly answers the "we do not trust your numbers" complaint in the v3.1 plan.
|
||||
- **CSV marketplace import** (`POST /marketplaces/import`, `dryRun` default true) — bulk tenant creation as a first-class operation.
|
||||
- **Their §22 acceptance scenarios** (15 of them) are a ready-made e2e suite. Scenario 3 (two concurrent purchases of the last unit) and scenario 10 (replayed webhook) are the two tests that would catch the most expensive possible bugs. Write those two this sprint regardless of anything else in this document.
|
||||
|
||||
---
|
||||
|
||||
## 8. VK ID and Yandex login — what is actually there, and how we add it
|
||||
|
||||
### 8.1 Finding: they do not have it
|
||||
|
||||
Exhaustive search of the archive (`*.ts`, `*.html`, `*.md`, `*.json`, `*.prisma`, `*.sql`, `*.yml`, `*.conf`, env examples), excluding `node_modules` and excluding our own code under `reference/`:
|
||||
|
||||
- `vk` / `vkontakte` / `vkid` — **0 hits** in source. The only matches anywhere are inside `package-lock.json` integrity hashes and two Armenian/English FAQ content pages.
|
||||
- `yandex` — **0 hits** in source; the same two content pages only.
|
||||
- `oauth` — **0 hits** in their code. The single `oauth`-adjacent file in the whole archive is **ours**: `reference/parallel-frontend/src/app/core/auth/services/auth.service.ts`.
|
||||
- The Prisma schema has **no** `ExternalIdentity`, no `provider` column on `Customer`, and no social tables. Customer identity is `@@unique([marketplaceId, telegramUserId])` — Telegram only.
|
||||
|
||||
**Their only customer login is Telegram**, and it is not even self-hosted: `CustomerAuthService` proxies to an external service at `USERAUTH_API_URL` (`https://users.vitanova.network:456`), creates a web session, polls `/users/sessions/{id}` until `status` is confirmed, then upserts a `Customer` and issues its own 30-day session cookie.
|
||||
|
||||
So there is nothing to copy from them here. But their **session-issuing half is the right shape**, and it is what VK/Yandex should terminate into.
|
||||
|
||||
### 8.2 What we already have
|
||||
|
||||
| File | State |
|
||||
|---|---|
|
||||
| [vk-id-gateway.interface.ts](src/app/core/identity/services/vk-id-gateway.interface.ts) | `getAuthorizeUrl()`, `completeCallback(code, codeVerifier)` |
|
||||
| [vk-id-api.gateway.ts](src/app/core/identity/services/vk-id-api.gateway.ts) | Real HTTP client → `/api/identity/v1/vk/authorize`, `/vk/callback` |
|
||||
| [vk-id-local.gateway.ts](src/app/core/identity/services/vk-id-local.gateway.ts) | Mock |
|
||||
| [vk-id-gateway.token.ts](src/app/core/identity/services/vk-id-gateway.token.ts) | DI seam via `environment.useMockData` |
|
||||
| [vk-id-login.component.ts](src/app/components/vk-id-login/vk-id-login.component.ts) | Button component |
|
||||
| [customer-identity.model.ts](src/app/core/identity/models/customer-identity.model.ts) | `ExternalIdentityProvider = 'vk_id' \| 'telegram' \| 'max'` |
|
||||
| [PHASE-8-IDENTITY-MESSAGING-CONTRACT.md](docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md) | §2 defines the VK ID contract |
|
||||
|
||||
We have the scaffolding and the contract. Yandex is absent everywhere except one mention of *Yandex Market* as a possible marketplace connector in the gap analysis — a different thing entirely.
|
||||
|
||||
### 8.3 Design — one provider-agnostic social login, VK ID and Yandex ID as instances
|
||||
|
||||
**Principle (already in our contract — keep it):** the OAuth code exchange happens entirely backend-side. No client secret, no access token, and no `code_verifier` ever reaches the browser.
|
||||
|
||||
**Change to make:** our current interface passes `codeVerifier` from the client, which forces the browser to generate and store the PKCE verifier. We are a confidential client — the backend should own the verifier. Recommended surface:
|
||||
|
||||
```
|
||||
GET /api/identity/v1/{provider}/authorize
|
||||
-> 302 to the provider, OR { url } for the client to navigate to.
|
||||
Backend generates state + code_verifier and stores both in a
|
||||
short-lived HttpOnly cookie (or server-side, keyed by state),
|
||||
10 min TTL, single use.
|
||||
|
||||
GET /api/identity/v1/{provider}/callback?code=…&state=…[&device_id=…]
|
||||
-> backend validates state, exchanges code + stored verifier,
|
||||
fetches the profile, resolves/links Customer, issues the
|
||||
marketplace session cookie, 302 back into the storefront.
|
||||
|
||||
POST /api/identity/v1/{provider}/unlink (authenticated)
|
||||
GET /api/identity/v1/me/identities (authenticated) -> linked providers
|
||||
```
|
||||
|
||||
`{provider}` ∈ `vk` | `yandex` (later `telegram`, `max`). One controller, one service, a per-provider strategy object. The frontend keeps exactly one gateway interface:
|
||||
|
||||
```ts
|
||||
export type SocialProvider = 'vk' | 'yandex';
|
||||
|
||||
export interface SocialIdentityGateway {
|
||||
getAuthorizeUrl(provider: SocialProvider, returnTo?: string): Observable<string>;
|
||||
listIdentities(): Observable<ExternalIdentity[]>;
|
||||
unlink(provider: SocialProvider): Observable<void>;
|
||||
}
|
||||
```
|
||||
|
||||
`VkIdGateway` collapses into it, `completeCallback()` disappears from the frontend entirely (the backend handles the callback and redirects), and `vk-id-login.component` becomes `social-login-button` with a provider input. Add `'yandex_id'` to `ExternalIdentityProvider` in `customer-identity.model.ts`.
|
||||
|
||||
**Provider specifics** — confirm exact parameter and scope names against the live provider docs before implementing; both providers have revised their flows recently.
|
||||
|
||||
*VK ID* — OAuth 2.1, **PKCE mandatory**, S256.
|
||||
- Authorize: `https://id.vk.com/authorize` — `client_id`, `redirect_uri`, `response_type=code`, `code_challenge`, `code_challenge_method=S256`, `state`, `scope` (typically `vkid.personal_info email phone`).
|
||||
- The callback returns a **`device_id` alongside `code`**, and it is required for the token exchange. Missing it makes every exchange fail; this is the single most common VK ID integration bug.
|
||||
- Token: `POST https://id.vk.com/oauth2/auth` — `grant_type=authorization_code`, `code`, `code_verifier`, `device_id`, `client_id`, `redirect_uri`.
|
||||
- Profile: `POST https://id.vk.com/oauth2/user_info` with the access token → stable `user_id`, name, optional email/phone.
|
||||
- Logout: `https://id.vk.com/oauth2/logout` — call it on unlink so the provider session is not left dangling.
|
||||
|
||||
*Yandex ID* — OAuth 2.0, PKCE supported; use it.
|
||||
- Authorize: `https://oauth.yandex.ru/authorize` — `response_type=code`, `client_id`, `redirect_uri`, `state`, `code_challenge`, `code_challenge_method=S256`.
|
||||
- Token: `POST https://oauth.yandex.ru/token` — `grant_type=authorization_code`, `code`, `code_verifier`, HTTP Basic auth with `client_id:client_secret`.
|
||||
- Profile: `GET https://login.yandex.ru/info?format=json` with header `Authorization: OAuth <access_token>` → `id` (stable), `login`, `default_email`, `default_phone`, `psuid`, avatar id.
|
||||
- Yandex returns an email in most cases; VK often will not. Do not make email a required field on `Customer`.
|
||||
|
||||
**Data model** — add to whatever schema we land on. Their `MarketplaceAuthCredential` is the right precedent for the credentials half.
|
||||
|
||||
```prisma
|
||||
model ExternalIdentity {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
customerId String @db.Uuid
|
||||
provider String // 'vk_id' | 'yandex_id' | 'telegram' | 'max'
|
||||
providerUserId String
|
||||
email String?
|
||||
phone String?
|
||||
displayName String?
|
||||
verifiedAt DateTime @default(now())
|
||||
lastUsedAt DateTime @default(now())
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerUserId]) // one provider account -> one customer
|
||||
@@index([customerId])
|
||||
}
|
||||
```
|
||||
|
||||
Plus, per tenant, an encrypted OAuth app config using the same envelope as their `FieldEncryptionService`:
|
||||
|
||||
```
|
||||
MarketplaceAuthCredential { marketplaceId, provider, encryptedConfig, active }
|
||||
encryptedConfig = { clientId, clientSecret, scopes[], redirectUri }
|
||||
```
|
||||
|
||||
**Five rules that decide whether this ships correctly:**
|
||||
|
||||
1. **Redirect URI vs. multi-tenant domains.** VK and Yandex both validate `redirect_uri` against an exact registered list. With N tenant domains you cannot register N URIs per app, and you cannot let tenants supply their own. Use **one central identity host** (e.g. `id.<platform-domain>`) as the only registered callback, carry the origin tenant inside the signed `state`, and 302 back to the tenant domain with a short-lived signed one-time handoff token that the tenant's API exchanges for the session cookie. Decide this before writing any code — retrofitting it is expensive.
|
||||
2. **`@@unique([provider, providerUserId])`, plus a decision on per-tenant customer separation.** Their platform isolates `Customer` per marketplace even for the same Telegram ID. Decide explicitly whether one VK account across two of our storefronts is one customer or two. Their answer is *two*; that is the safer default for data protection and the one our `Customer.marketplaceId` already implies.
|
||||
3. **Identity conflict is not an upsert.** Our PHASE-8 §2 already says this: if `providerUserId` is already bound to a different `Customer`, route to controlled resolution — never silently rebind. Enforce it with the unique index so the database refuses, rather than trusting the service layer.
|
||||
4. **`state` is single-use and bound to the browser.** Store `{ state, codeVerifier, marketplaceId, returnTo, expiresAt }` server-side or in a signed HttpOnly cookie; delete on first use. Reject unknown/expired/replayed `state` with a generic error.
|
||||
5. **The session that comes out is our normal session.** VK/Yandex end where Telegram ends: a random 32-byte token, stored as SHA-256, HttpOnly + Secure + SameSite=Lax, per-marketplace, revocable. Social login is an *entry path*, not a session format.
|
||||
|
||||
**Build order:** provider-agnostic backend endpoints + `ExternalIdentity` table → VK ID (v3.1 names it the primary social login) → Yandex ID (a second instance of the same strategy, roughly a day once VK works) → migrate Telegram onto `ExternalIdentity` so it becomes one provider among several rather than the schema's only key → account-linking UI (`/me/identities`, link/unlink) → email/phone OTP as recovery.
|
||||
|
||||
---
|
||||
|
||||
## 9. What we must not copy from them
|
||||
|
||||
- **Angular 21.2.18** with 7–8 open high findings in production dependencies, in both apps. We are on 22.0.8 with a clean production audit. Do not regress.
|
||||
- **`mock-data.service.ts` in the backoffice** — they still ship one while telling us mocks are disqualifying.
|
||||
- **25 unit tests and zero e2e.** Their own verification doc concedes this is not sufficient.
|
||||
- **`ORDER_MANAGER_MARKETPLACE_SLUG` pinned by environment variable** (default `'dexar'`, `'novo'` in the example env). Manager scope should come from membership rows, not an env string.
|
||||
- **Server IP hardcoded in `provision-domain.sh`** (`109.120.134.244`), and the `sslip.io` staging hosts baked into the committed env example.
|
||||
- Their **section schema** is narrower than ours (5 section types vs our widget system). Take their *server-side validation discipline*, not their schema.
|
||||
|
||||
---
|
||||
|
||||
## 10. Recommended next actions
|
||||
|
||||
| # | Action | Why now |
|
||||
|---|---|---|
|
||||
| 1 | Write the two e2e tests from their §22: concurrent purchase of the last unit, and a replayed webhook | Highest bug-cost coverage per hour, and they have neither |
|
||||
| 2 | Remove client-held payment credentials and JS-set admin cookies | Their audit's most serious finding, and it is correct |
|
||||
| 3 | Fold their invariant list (§7 of their handoff) into our own handoff doc as a signed acceptance gate | Turns their strongest document into our shared standard |
|
||||
| 4 | Decide the central-identity-host question in §8.3 rule 1 | Blocks VK ID, and it is a one-way door |
|
||||
| 5 | Implement provider-agnostic social identity, then VK ID, then Yandex ID | v3.1 §14 names VK ID the primary social login; nobody has it yet, including them |
|
||||
| 6 | Cut the storefront bundle below budget by splitting storefront / editor / backoffice deployables | 452 kB over, and it is the one performance criticism that is objectively measured |
|
||||
| 7 | Take the position explicitly that the two codebases merge as *their backend + our frontend* | Their handoff doc already ranks our work fifth; unchallenged, that becomes the plan of record |
|
||||
|
||||
---
|
||||
|
||||
## Appendix — where things live in the archive
|
||||
|
||||
| Concern | Path |
|
||||
|---|---|
|
||||
| Tenant by Host, preview tokens | `platform-api/src/common/tenant.service.ts` |
|
||||
| Admin auth, TOTP, RBAC weights | `platform-api/src/auth/admin-auth.service.ts` |
|
||||
| Origin allowlist / CSRF hook | `platform-api/src/main.ts`, `src/auth/admin-origins.ts` |
|
||||
| Stock reservation SQL | `platform-api/src/checkout/checkout.service.ts` |
|
||||
| Idempotency, webhooks, polling | `platform-api/src/payments/payment.service.ts` |
|
||||
| Field encryption (AES-256-GCM) | `platform-api/src/common/field-encryption.service.ts` |
|
||||
| Server-side section validation | `platform-api/src/common/storefront-config.ts` |
|
||||
| Publish / rollback / clone | `platform-api/src/admin/admin.service.ts` |
|
||||
| Customer (Telegram) sessions | `platform-api/src/storefront/customer-auth.service.ts` |
|
||||
| Data model, 36 entities | `platform-api/prisma/schema.prisma` |
|
||||
| Compose, networks, WAL | `infra/compose.yml` |
|
||||
| Domain provisioning, backup, restore check | `infra/scripts/` |
|
||||
| Host hardening (fail2ban, sshd, sysctl) | `marketplaces/infra/server/` |
|
||||
| Their audit of our code | `docs/PARALLEL_IMPLEMENTATION_AUDIT.md` |
|
||||
| Their target spec (479 lines) | `docs/PRODUCT_SPECIFICATION.md` |
|
||||
| Their handoff + invariants | `docs/DEVELOPER_HANDOFF.md` |
|
||||
| Our code, verbatim | `reference/parallel-frontend/` |
|
||||
231
docs/FORK-HARVEST-TODO.md
Normal file
231
docs/FORK-HARVEST-TODO.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Fork Harvest — TODO
|
||||
|
||||
**Branch:** `improvements/fork-harvest` (from `B2B` @ `92f1c88`)
|
||||
**Design:** [2026-08-21-fork-harvest-design.md](superpowers/specs/2026-08-21-fork-harvest-design.md)
|
||||
**Source analysis:** [FORK-ANALYSIS-2026-08-21.md](FORK-ANALYSIS-2026-08-21.md)
|
||||
|
||||
Improvements only. Nothing here regresses our Angular version, test count, or architecture governance.
|
||||
|
||||
**Effort:** S ≤ half a day · M ≤ 2 days · L > 2 days
|
||||
**Lane:** A frontend · B backend contract · C `@marketplaces/auth` package · D infra/ops · E process
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 — Decide first (blocks Wave 4)
|
||||
|
||||
- [ ] **FH-0.1 — Decide the central identity host** · L · Lane C · *blocker*
|
||||
VK ID and Yandex ID both validate `redirect_uri` against an exact registered list. We cannot register one per tenant domain, and we cannot let tenants supply their own.
|
||||
**Decision needed:** single central callback host (e.g. `id.<platform-domain>`) as the only registered URI, tenant carried inside signed `state`, 302 back to the tenant domain with a short-lived signed handoff token the tenant API exchanges for a session cookie.
|
||||
**Also decide:** is one VK account across two of our storefronts one `Customer` or two? Their platform says two; our `Customer.marketplaceId` already implies two.
|
||||
**Done when:** an ADR exists in `docs/context/adrs/` and both questions have a recorded answer.
|
||||
|
||||
- [ ] **FH-0.2 — Confirm the server-priced checkout path covers every live flow** · S · Lane A · *blocks FH-1.3*
|
||||
`api.service.ts` already has a server-priced checkout session method. Confirm no production flow still depends on `createPayment(payload, headers)` before deleting the header path.
|
||||
**Done when:** every caller of the legacy header path is enumerated and has a replacement.
|
||||
|
||||
---
|
||||
|
||||
## Wave 1 — Live defects with a security benefit (Lane A, this sprint)
|
||||
|
||||
- [ ] **FH-1.1 — Kill the plaintext third-party geo call** · S · Lane A
|
||||
`src/app/services/location.service.ts:75` calls `http://ip-api.com/json/?fields=…` from an HTTPS origin. Browsers block mixed active content, so `detectLocation()` has been silently taking its error branch in production — region auto-detect is dead, not just insecure. It is also a third-party geo leak on every session.
|
||||
**Do:** remove the direct call. Resolve region server-side (`GET /api/v1/geo/resolve`, backend reads the client IP) or drop auto-detect and keep the manual region picker.
|
||||
**Done when:** zero `http://` literals in `src/`; a unit test asserts `detectLocation()` issues no cross-origin request to a non-allowlisted host.
|
||||
|
||||
- [ ] **FH-1.2 — Stop blindly trusting the bank redirect URL** · M · Lane A
|
||||
`src/app/pages/cart/cart.component.ts:485` — `bypassSecurityTrustResourceUrl(bankUrl)` with no validation, rendered into a popup iframe. Most acquirer 3-D Secure pages send `X-Frame-Options: DENY`, so the popup is blank for those banks. Their spec: card checkout navigates the current tab, no intermediate popup.
|
||||
**Do:** accept only an `https:` URL whose origin the backend returned in the payment response (backend allowlist, per their `safeHttpsUrl()`); navigate the current tab instead of framing.
|
||||
**Done when:** a non-https or non-allowlisted URL is refused with a visible payment error; a test covers both the accepted and the refused case.
|
||||
|
||||
- [ ] **FH-1.3 — Remove provider credentials from the browser** · M · Lane A · *depends on FH-0.2*
|
||||
`src/app/services/api.service.ts:675` sets `authorization-key` and `userid-value` headers client-side. `api.service.ts:143` ships a partner ID literal (`'web-97ec-9c57-4dde-9037-3a68f7f83750'`) in the bundle. Their audit's most serious finding, and it is correct.
|
||||
**Do:** delete both header paths and the literal; the browser gets a checkout URL or a status endpoint, never a credential.
|
||||
**Done when:** `grep -ri "authorization-key\|userid-value" src/` returns nothing; no partner ID literal in `dist/`; a CI check greps the built bundle for both patterns.
|
||||
|
||||
- [ ] **FH-1.4 — Send `Idempotency-Key` on payment creation** · S · Lane A
|
||||
Zero `idempot*` anywhere in our codebase. Their API requires the header and rejects a key reused across a different order.
|
||||
**Do:** generate one key per checkout attempt, stable across retries and across a double-click, sent on payment creation.
|
||||
**Done when:** the existing `checkout-idempotent-click.spec.ts` asserts both requests carry the *same* key.
|
||||
|
||||
---
|
||||
|
||||
## Wave 2 — Contract hardening (Lane B, parallel with Wave 1)
|
||||
|
||||
Each item is normative text plus an acceptance scenario in `docs/backend/BACKEND-HANDOFF.md`, so it becomes a delivery gate rather than a wish.
|
||||
|
||||
- [ ] **FH-2.1 — Conditional-UPDATE stock reservation** · S · `PHASE-6-CART-CHECKOUT-CONTRACT.md`
|
||||
`UPDATE … SET reserved = reserved + $qty WHERE (onHand - reserved) >= $qty RETURNING id`; empty result → `409`. Reservation TTL 15 min. Price read only from the server-side snapshot, never from the request.
|
||||
**Acceptance:** two concurrent purchases of the last unit produce exactly one payable order.
|
||||
|
||||
- [ ] **FH-2.2 — Idempotency as unique constraints** · S · `PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md`
|
||||
`Payment.idempotencyKey UNIQUE`; a key reused against a different order/marketplace → `409`. `PaymentWebhookEvent @@unique([provider, eventKey])`; duplicate insert → `{accepted: true, duplicate: true}`. `eventKey` falls back to `sha256(rawBody)`. Signature verified against the **raw** body. Status poll as a 24-hour reconciliation fallback.
|
||||
**Acceptance:** a replayed webhook neither completes the order twice nor moves stock twice.
|
||||
|
||||
- [ ] **FH-2.3 — Session and credential model** · M · `TRACK-S-SECURITY-RBAC-CONTRACT.md`
|
||||
Server-stored sessions; random 32 bytes; **stored as SHA-256 hash only**; HttpOnly + Secure + SameSite; revocable; a distinct cookie per contour (`bo_session` / `manager_session` / `marketplace_session`). Argon2id `memoryCost 65536, timeCost 3, parallelism 1`. TOTP mandatory, gated by a signed 10-minute setup token. Password change ≥16 chars and revokes every live session in the same transaction. Role weights `ORDER_MANAGER 0 < VIEWER 1 < CONTENT_MANAGER 2 < ADMIN 3 < OWNER 4`, checked together with marketplace scope.
|
||||
**Acceptance:** a CONTENT_MANAGER cannot read an unassigned marketplace through a direct API call.
|
||||
|
||||
- [ ] **FH-2.4 — Origin allowlist for admin mutations** · S · `TRACK-S-SECURITY-RBAC-CONTRACT.md`
|
||||
Global hook: any non-GET on an admin/manager path whose `Origin` is not in the configured allowlist → `403`. CORS uses the same allowlist with `credentials: true`.
|
||||
**Acceptance:** a cross-origin POST with a valid session cookie is refused.
|
||||
|
||||
- [ ] **FH-2.5 — Tenant by verified Host only** · S · `PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md`
|
||||
Normalize host (lowercase, strip trailing dot, strip port) → unique `hostname` row → require `verifiedAt` and `ACTIVE`. Short cache with explicit invalidation. Unknown host → `404`, never a fallback tenant. The public API never accepts a `marketplaceId` from the browser.
|
||||
**Acceptance:** an unknown Host returns 404 and leaks no other tenant's data.
|
||||
|
||||
- [ ] **FH-2.6 — Signed preview token, read-only preview** · S · `PHASE-9-…`
|
||||
HMAC-signed token carrying `{marketplaceId, expiresAt, nonce}`, 15-minute TTL, `storefront_preview` cookie. Global hook returns `404 Preview mode is read-only` for any non-GET while that cookie is present. Preview is not indexable.
|
||||
**Acceptance:** a mutation attempted in preview mode is refused.
|
||||
|
||||
- [ ] **FH-2.7 — Immutable revisions, rollback, clone** · M · new section, `PHASE-9-…`
|
||||
`version = max(version) + 1`, immutable snapshot row, `publishedRevision` pointer flipped in the same transaction. Rollback creates a new revision; history is never rewritten. Clone copies design + catalog assignments, **forces inventory to 0**, never copies domains/customers/orders/secrets, and walks the category tree topologically with explicit cycle detection.
|
||||
**Acceptance:** rollback restores the chosen revision and leaves live inventory untouched.
|
||||
|
||||
- [ ] **FH-2.8 — Append-only inventory journal** · S · `PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md`
|
||||
Every stock change writes `reason`, `referenceType`, `referenceId`, `actorId`, resulting balance. Direct answer to the v3.1 "we cannot explain your numbers" complaint.
|
||||
**Acceptance:** any current quantity is reconstructible from the journal alone.
|
||||
|
||||
- [ ] **FH-2.9 — Per-tenant encrypted credentials** · S · `PHASE-1` / `PHASE-7`
|
||||
AES-256-GCM, versioned envelope `v1.iv.tag.ciphertext` (base64url), 32-byte key from the environment. Decrypted only inside the service; never serialized into any response. Redirect/callback URLs built backend-side and allowlisted.
|
||||
**Acceptance:** no credential appears in any API response, JS bundle, or browser storage.
|
||||
|
||||
- [ ] **FH-2.10 — Server-side storefront config validation** · M · `PHASE-10-CONTENT-MODULES-CONTRACT.md`
|
||||
The server re-runs our editor's validation. Clamp-and-fallback ergonomics: clamp out-of-range numbers rather than rejecting; blank a URL that is not local `/path` or `https://` rather than erroring; fall back an invalid colour. Cap sections per page and IDs per list.
|
||||
**Acceptance:** a hand-crafted API call cannot store a config the editor would have refused.
|
||||
|
||||
- [ ] **FH-2.11 — Digital goods** · M · `PHASE-3-…`
|
||||
`FulfillmentMode: MANUAL | CODE_POOL`. `DigitalCode` pool with `AVAILABLE/RESERVED/ASSIGNED/REVOKED`, encrypted value, `valueHash` unique per `(marketplace, variant)`. Codes revealed only when the order is `PAID`/`PROCESSING`/`FULFILLED`.
|
||||
**Acceptance:** an unpaid order never returns a code.
|
||||
|
||||
- [ ] **FH-2.12 — Marketplace status machine** · S · `PHASE-9-…`
|
||||
`DRAFT → DOMAIN_PENDING → READY → ACTIVE → SUSPENDED`, with `DOMAIN_PENDING` as a real state rather than an error condition.
|
||||
|
||||
- [ ] **FH-2.13 — Order public token, not sequential IDs** · S · `PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md`
|
||||
Orders are addressed publicly by a random `base64url` token. Order line items carry an immutable snapshot of name, SKU, price, currency, delivery, and contact data at purchase time.
|
||||
|
||||
- [ ] **FH-2.14 — Order-manager as a separate contour** · M · `TRACK-S-…`
|
||||
Separate URL, shell, cookie, and login; scoped to assigned marketplaces via **membership rows, not an environment variable** (their env-pinned slug is the one part not to copy). No visibility into catalog, design, domains, payment settings, or platform users. PII masked in lists, revealed in detail only with permission, and both export and reveal are logged.
|
||||
|
||||
- [ ] **FH-2.15 — CSV marketplace import with `dryRun` default true** · S · `PARTNER-PROVISIONING-API-CONTRACT.md`
|
||||
Bulk tenant creation as a first-class operation: validate fully without writing, report create/update/skip/error per row, then confirm.
|
||||
|
||||
---
|
||||
|
||||
## Wave 3 — Proof (Lane A)
|
||||
|
||||
- [ ] **FH-3.1 — E2E: concurrent purchase of the last unit** · M · `e2e/`
|
||||
Their §22 scenario 3. Two sessions race for the final unit; exactly one payable order results, the other gets a clean out-of-stock state.
|
||||
|
||||
- [ ] **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.
|
||||
|
||||
- [ ] **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.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.
|
||||
|
||||
---
|
||||
|
||||
## Wave 4 — Identity: VK ID + Yandex ID (Lane C, `@marketplaces/auth`)
|
||||
|
||||
Blocked on **FH-0.1**. Nothing to copy from the archive — it has zero VK/Yandex/OAuth code. We take the session-issuing shape of their Telegram flow and terminate both providers into it.
|
||||
|
||||
- [ ] **FH-4.1 — Provider-agnostic social identity surface** · M
|
||||
Collapse `VkIdGateway` into `SocialIdentityGateway`:
|
||||
```ts
|
||||
export type SocialProvider = 'vk' | 'yandex';
|
||||
export interface SocialIdentityGateway {
|
||||
getAuthorizeUrl(provider: SocialProvider, returnTo?: string): Observable<string>;
|
||||
listIdentities(): Observable<ExternalIdentity[]>;
|
||||
unlink(provider: SocialProvider): Observable<void>;
|
||||
}
|
||||
```
|
||||
Touches: `src/app/core/identity/services/vk-id-gateway.interface.ts`, `vk-id-api.gateway.ts`, `vk-id-local.gateway.ts`, `vk-id-gateway.token.ts`, `src/app/components/vk-id-login/` → `social-login-button`. Add `'yandex_id'` to `ExternalIdentityProvider` in `core/identity/models/customer-identity.model.ts`.
|
||||
|
||||
- [ ] **FH-4.2 — Move PKCE ownership to the backend** · S · Lane B + C
|
||||
Today `completeCallback(code, codeVerifier)` forces the browser to generate and hold the verifier. We are a confidential client. Backend generates `state` + `code_verifier`, stores them single-use for 10 minutes, handles the callback, and redirects. `completeCallback()` leaves the frontend entirely.
|
||||
Contract endpoints: `GET /api/identity/v1/{provider}/authorize`, `GET /api/identity/v1/{provider}/callback`, `POST /{provider}/unlink`, `GET /me/identities`. Update `PHASE-8-IDENTITY-MESSAGING-CONTRACT.md` §2.
|
||||
|
||||
- [ ] **FH-4.3 — `ExternalIdentity` model** · S · Lane B
|
||||
```prisma
|
||||
@@unique([provider, providerUserId]) // one provider account -> one customer
|
||||
```
|
||||
Conflict is **not** an upsert: a `providerUserId` already bound to a different `Customer` routes to controlled resolution. The unique index makes the database refuse a silent rebind. Per-tenant OAuth app config stored encrypted (same envelope as FH-2.9): `{ clientId, clientSecret, scopes[], redirectUri }`.
|
||||
|
||||
- [ ] **FH-4.4 — VK ID** · M
|
||||
OAuth 2.1, PKCE mandatory (S256). Authorize `https://id.vk.com/authorize`; token `POST https://id.vk.com/oauth2/auth`; profile `POST https://id.vk.com/oauth2/user_info`; logout `https://id.vk.com/oauth2/logout` on unlink.
|
||||
**Trap to write into the contract:** the callback returns `device_id` alongside `code`, and the token exchange fails without it. This is the most common VK ID integration bug.
|
||||
VK often does not return an email — email must stay optional on `Customer`.
|
||||
|
||||
- [ ] **FH-4.5 — Yandex ID** · S · *after FH-4.4*
|
||||
OAuth 2.0 with PKCE. Authorize `https://oauth.yandex.ru/authorize`; token `POST https://oauth.yandex.ru/token` with HTTP Basic `client_id:client_secret`; profile `GET https://login.yandex.ru/info?format=json` with header `Authorization: OAuth <token>` → `id`, `login`, `default_email`, `default_phone`, `psuid`.
|
||||
A second strategy object against the same surface — roughly a day once VK works.
|
||||
*Confirm exact parameter and scope names against live provider docs; both providers revised their flows recently.*
|
||||
|
||||
- [ ] **FH-4.6 — Migrate Telegram onto `ExternalIdentity`** · M · Lane B + C
|
||||
Telegram becomes one provider among several rather than the schema's only key. Ends the shared customer/admin Telegram session their audit flagged.
|
||||
|
||||
- [ ] **FH-4.7 — Account linking UI** · M
|
||||
`/me/identities` — show linked providers, link, unlink, and surface the conflict-resolution path from FH-4.3.
|
||||
|
||||
- [ ] **FH-4.8 — Email/phone OTP repositioned as recovery** · S · Lane E
|
||||
Our approved [email/phone login spec](superpowers/specs/2026-08-15-email-phone-login-design.md) stays valid but drops below VK ID and becomes the fallback when a messenger channel is unavailable, per v3.1 §14.
|
||||
|
||||
---
|
||||
|
||||
## Continuous — Ops (Lane D)
|
||||
|
||||
- [ ] **FH-D.1 — Proven restore drill** · M
|
||||
We have deploy automation and no proven restore. Add a restore-check script and schedule it. Their `restore-check.sh` + WAL archiving (`wal_level=replica`, `archive_mode=on`, `archive_timeout=300`) is the model.
|
||||
**Done when:** a restore into a clean environment has been executed and its result recorded.
|
||||
|
||||
- [ ] **FH-D.2 — Database unreachable from the internet, structurally** · S · Lane B/D
|
||||
Data network `internal: true`; API bound to loopback only; `no-new-privileges` on every service. Makes it a property of the topology rather than a firewall promise.
|
||||
|
||||
- [ ] **FH-D.3 — Host hardening we lack** · M
|
||||
fail2ban jail, sshd hardening drop-in, sysctl hardening, scoped sudoers per deploy role. Add to `scripts/deploy/server-setup.sh`.
|
||||
*Keep ours where ours is better:* `add-domain.sh` already pre-checks the DNS A record and runs `nginx -t` before and after; `server-setup.sh` already configures ufw. Do **not** copy their hardcoded server IP.
|
||||
|
||||
---
|
||||
|
||||
## Continuous — Process (Lane E)
|
||||
|
||||
- [ ] **FH-E.1 — Adopt the nine invariants as a signed acceptance gate** · S
|
||||
From their handoff §7: tenant by Host not by a browser-supplied ID; order price computed backend; stock and reservation atomic; payment webhook idempotent; credentials never leave the backend; published revision immutable; design rollback does not roll back live inventory; no user reads an unassigned marketplace through UI or API; every admin mutation leaves an audit trail.
|
||||
Put them at the head of `docs/backend/BACKEND-HANDOFF.md` as gates, not aspirations.
|
||||
|
||||
- [ ] **FH-E.2 — PR policy** · S
|
||||
One functional area per PR. Mandatory: purpose, screenshots, API changes, migrations, test evidence, security impact, rollback plan. Never change payment/inventory/order state machines inside a redesign PR.
|
||||
|
||||
- [ ] **FH-E.3 — Release discipline** · S
|
||||
"A local build or the existence of a UI does not mean production readiness." Every release records version, migrations, healthcheck, smoke result, dependency audit, and a rollback path.
|
||||
|
||||
- [ ] **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.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.
|
||||
**Done when:** no admin or editor write path can publish without a server round-trip.
|
||||
|
||||
---
|
||||
|
||||
## Scoreboard
|
||||
|
||||
| Wave | Items | Lane | Blocked by |
|
||||
|---|---:|---|---|
|
||||
| 0 — Decide | 2 | C, A | — |
|
||||
| 1 — Live defects | 4 | A | FH-0.2 (one item) |
|
||||
| 2 — Contracts | 15 | B | — |
|
||||
| 3 — Proof | 5 | A | — |
|
||||
| 4 — Identity | 8 | C | FH-0.1 |
|
||||
| Ops | 3 | D | — |
|
||||
| Process | 5 | E | — |
|
||||
| **Total** | **42** | | |
|
||||
|
||||
**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.
|
||||
201
docs/superpowers/specs/2026-08-21-fork-harvest-design.md
Normal file
201
docs/superpowers/specs/2026-08-21-fork-harvest-design.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# Fork Harvest — Design / Working Description
|
||||
|
||||
**Branch:** `improvements/fork-harvest` (cut from `B2B` @ `92f1c88`)
|
||||
**Date:** 2026-08-21
|
||||
**Input:** [FORK-ANALYSIS-2026-08-21.md](../../FORK-ANALYSIS-2026-08-21.md)
|
||||
**Companion:** [FORK-HARVEST-TODO.md](../../FORK-HARVEST-TODO.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Take **only the improvements** from the `hub.numus.cc/numus/marketplaces` archive. Nothing else. No architecture adoption, no code copying, no rewrite, no framework regression.
|
||||
|
||||
The archive is a competing platform monorepo, not a fork of us. It contains our repo verbatim as `reference/parallel-frontend/`. It is ahead of us on backend truth and operations, behind us on frontend depth, testing, and framework currency.
|
||||
|
||||
This document is the working brief for whoever executes the harvest — including a future session of me. It states what is true today in this repo, what changes, and how each item is proven done.
|
||||
|
||||
---
|
||||
|
||||
## 2. Constraint that shapes everything
|
||||
|
||||
**This repo has no backend.** 530 `.ts` files, Angular 22, zero server code. Our backend exists only as 17 contract documents in `docs/backend/`, implemented by another team.
|
||||
|
||||
That splits every harvested improvement into one of five lanes:
|
||||
|
||||
| Lane | Meaning | Where it lands |
|
||||
|---|---|---|
|
||||
| **A — Frontend** | We write the code, this sprint | `src/`, `e2e/`, `angular.json`, CI |
|
||||
| **B — Contracts** | We write the requirement; backend implements | `docs/backend/*.md` |
|
||||
| **C — Packages** | Ships in `@marketplaces/auth` (external repo `vitanovaPackages`) | package repo + DI wiring here |
|
||||
| **D — Infra/Ops** | Deploy scripts and runbook | `scripts/deploy/`, `docs/DEPLOYMENT.md` |
|
||||
| **E — Process** | Governance, gates, policy | `docs/backend/BACKEND-HANDOFF.md`, ADRs |
|
||||
|
||||
Anything that would require us to stand up Prisma, Postgres, or NestJS in *this* repo is out of scope. It becomes a Lane B contract line instead.
|
||||
|
||||
---
|
||||
|
||||
## 3. What we verified in our own code (2026-08-21, current HEAD)
|
||||
|
||||
Their audit was written against our 11 Aug snapshot. Re-checked against today's code:
|
||||
|
||||
| Their finding | Status now | Evidence |
|
||||
|---|---|---|
|
||||
| Admin session cookie set by JS, readable via `document.cookie` | **Already fixed** | zero `document.cookie` hits in `src/` |
|
||||
| Admin JWT / refresh token in `localStorage` | **Already fixed** | no `setItem(*token*)` anywhere |
|
||||
| `http://ip-api.com` from an HTTPS storefront | **STILL LIVE** | `src/app/services/location.service.ts:75` |
|
||||
| Unconditional `bypassSecurityTrustResourceUrl` on a bank URL | **STILL LIVE** | `src/app/pages/cart/cart.component.ts:485` |
|
||||
| Client-side `authorization-key` / `userid-value` headers | **STILL LIVE** | `src/app/services/api.service.ts:675` |
|
||||
| Hardcoded partner ID in the bundle | **STILL LIVE** | `src/app/services/api.service.ts:143` — `'web-97ec-9c57-4dde-9037-3a68f7f83750'` |
|
||||
| `localStorage` as persistence | **19 files**, mostly admin facades + editor draft storage | see TODO FH-A7 |
|
||||
| Bundle 452 kB over a 700 kB budget | **Still true** | their measured build |
|
||||
| Zero `idempot*` in the codebase | **Still true** | no Idempotency-Key sent on payment creation |
|
||||
|
||||
Two of those are live bugs, not just security posture:
|
||||
|
||||
- **`http://ip-api.com`** — browsers block mixed active content on an HTTPS origin. `detectLocation()` therefore always takes its error branch in production. Region auto-detect has been silently dead.
|
||||
- **Bank URL in an iframe** — most acquirer 3-D Secure pages send `X-Frame-Options: DENY` / frame-ancestors CSP. The popup renders blank for those banks. Their spec calls this out explicitly: card checkout should navigate the current tab, not open an intermediate popup.
|
||||
|
||||
That reframes three of the "security" items as **defect fixes with a security benefit**, which is a much easier sell and a much better use of the sprint.
|
||||
|
||||
---
|
||||
|
||||
## 4. Selection rule — what counts as "an improvement"
|
||||
|
||||
An item is harvested only if it passes all four:
|
||||
|
||||
1. **It is better than what we have**, not merely different.
|
||||
2. **It survives without their backend.** Either we can build it, or it is a contract line the backend team can implement against.
|
||||
3. **It does not regress us.** Nothing that drops us to Angular 21, reintroduces mocks, or lowers our test bar.
|
||||
4. **It is falsifiable.** There is a test, a check, or an observable state that proves it done.
|
||||
|
||||
Explicitly rejected by this rule (from the analysis §9): their Angular version, their `mock-data.service.ts`, their 25-test/zero-e2e posture, their env-pinned `ORDER_MANAGER_MARKETPLACE_SLUG`, their hardcoded server IP, their narrower 5-type section schema.
|
||||
|
||||
---
|
||||
|
||||
## 5. The harvest, by theme
|
||||
|
||||
### 5.1 Correctness primitives (the highest-value cluster)
|
||||
|
||||
Three patterns from their backend that are worth more than everything else combined, because each replaces application logic with a database guarantee:
|
||||
|
||||
**Conditional-UPDATE reservation.** One statement is their entire oversell defence:
|
||||
|
||||
```sql
|
||||
UPDATE "MarketplaceInventory"
|
||||
SET "reserved" = "reserved" + $qty
|
||||
WHERE "marketplaceId" = $mp AND "variantId" = $variant
|
||||
AND ("onHand" - "reserved") >= $qty
|
||||
RETURNING "id"
|
||||
```
|
||||
|
||||
Empty result set → `409`. No read-then-write window, no advisory lock, no retry loop. Goes into `PHASE-6-CART-CHECKOUT-CONTRACT.md` as a normative requirement, not a suggestion.
|
||||
|
||||
**Idempotency as a unique constraint.** `Payment.idempotencyKey UNIQUE` and `PaymentWebhookEvent @@unique([provider, eventKey])`. A duplicate insert throws, and the catch returns `{accepted: true, duplicate: true}`. Replay protection becomes structurally impossible to forget, versus an `if` somebody eventually deletes. Goes into `PHASE-7`.
|
||||
|
||||
**Append-only inventory journal.** Every stock change writes `reason`, `referenceType`, `referenceId`, `actorId`, resulting balance. This is the direct answer to the v3.1 plan's "we cannot explain your numbers" complaint — it makes every quantity reconstructible after the fact.
|
||||
|
||||
None of these are hard. All three are cheap to specify and expensive to retrofit.
|
||||
|
||||
### 5.2 Session and credential hygiene
|
||||
|
||||
Their model, which we adopt as the contract target: server-stored sessions, random 32 bytes, **stored as SHA-256 hash only**, HttpOnly + Secure + SameSite, revocable, one distinct cookie per contour (`bo_session` / `manager_session` / `marketplace_session`), Argon2id `memoryCost 65536 / timeCost 3 / parallelism 1`, mandatory TOTP with a signed 10-minute setup token, and password change revoking every live session in the same transaction.
|
||||
|
||||
Plus the twelve-line CSRF defence we do not have: a global `onRequest` hook rejecting any non-GET on an admin/manager path whose `Origin` is not in the configured allowlist.
|
||||
|
||||
Our side of this is subtractive: stop sending provider credentials from the browser, stop shipping a partner ID literal in the bundle.
|
||||
|
||||
### 5.3 Tenant and preview safety
|
||||
|
||||
Host → verified domain row → tenant, 30-second cache with explicit invalidation, `404` on unknown host with no fallback tenant. We have bootstrap-driven runtime config and no equivalent guarantee written down.
|
||||
|
||||
Their preview mechanism is the piece worth copying outright: an HMAC-signed token carrying `{marketplaceId, expiresAt, nonce}`, 15-minute TTL, delivered as a `storefront_preview` cookie, plus a global hook that returns `404 Preview mode is read-only` for any non-GET while that cookie is present. We have preview UI and no preview safety at all.
|
||||
|
||||
### 5.4 Publish, revisions, clone
|
||||
|
||||
`version = max(version) + 1`, immutable snapshot row, `publishedRevision` pointer flipped in the same transaction, rollback creates a *new* revision rather than rewriting history. Clone copies design and catalog assignments, **forces inventory to zero**, and never copies domains, customers, orders, or secrets; its category walk is topological with explicit cycle detection.
|
||||
|
||||
### 5.5 Product ideas worth taking
|
||||
|
||||
- **Order-manager as a fully separate contour** — separate URL, shell, cookie, login, scoped to one marketplace, with no visibility into catalog, design, domains, or payment settings. Removes an entire permissions surface rather than guarding it.
|
||||
- **Digital goods in one table** — `FulfillmentMode: MANUAL | CODE_POOL`, a `DigitalCode` pool with `AVAILABLE/RESERVED/ASSIGNED/REVOKED`, encrypted values, `valueHash` unique per `(marketplace, variant)`, codes revealed only once the order is `PAID`. We have no digital-goods story; this is a complete one.
|
||||
- **`DOMAIN_PENDING` as a real marketplace state**, not an error condition.
|
||||
- **CSV marketplace import with `dryRun` default true** — bulk tenant creation as a first-class operation.
|
||||
- **Their §22 acceptance list** as a ready-made e2e suite. Two of the fifteen are worth writing immediately: concurrent purchase of the last unit, and a replayed webhook.
|
||||
|
||||
### 5.6 Social identity — VK ID and Yandex ID
|
||||
|
||||
The archive has **zero** VK/Yandex/OAuth code; there is nothing to copy. What we take is the *session-issuing shape* of their Telegram flow and terminate VK/Yandex into it.
|
||||
|
||||
Design decisions, all of which belong in `@marketplaces/auth`:
|
||||
|
||||
1. **Provider-agnostic surface.** `SocialIdentityGateway` with a `SocialProvider` union, replacing today's VK-specific `VkIdGateway`. One controller pattern backend-side, one strategy object per provider.
|
||||
2. **Backend-owned PKCE.** Our current interface passes `codeVerifier` from the client, which forces the browser to generate and hold the verifier. We are a confidential client. The backend generates `state` + `code_verifier`, stores them single-use for 10 minutes, and the browser only ever gets redirected. `completeCallback()` disappears from the frontend entirely.
|
||||
3. **VK ID gotcha:** the callback returns `device_id` next to `code`, and the token exchange fails without it. This is the single most common VK ID integration bug and it must be in the contract text.
|
||||
4. **Multi-tenant `redirect_uri` is a one-way door.** Both providers validate `redirect_uri` against an exact registered list; we cannot register one per tenant domain. Resolution: a single central identity host as the only registered callback, tenant carried inside the signed `state`, then a 302 back to the tenant domain with a short-lived signed handoff token the tenant API exchanges for its session cookie. **This must be decided before any code is written.**
|
||||
5. **Identity conflict is not an upsert.** `@@unique([provider, providerUserId])` so the database refuses a silent rebind; conflicts route to controlled resolution.
|
||||
|
||||
Build order: provider-agnostic surface → VK ID → Yandex ID (a second strategy, roughly a day) → migrate Telegram onto `ExternalIdentity` → linking UI → email/phone OTP demoted to recovery.
|
||||
|
||||
### 5.7 Operations
|
||||
|
||||
Adopt: WAL archiving plus a *scheduled, proven* restore drill; a data network that is `internal: true` so "the database is not reachable from the internet" is structural rather than a firewall promise; host hardening we lack (fail2ban, sshd drop-in, sysctl).
|
||||
|
||||
Already better on our side, keep as-is: our `add-domain.sh` already pre-checks the DNS A record and runs `nginx -t` before and after; `server-setup.sh` already configures ufw. Their `provision-domain.sh` hardcodes the server IP — do not copy that shape.
|
||||
|
||||
### 5.8 Process
|
||||
|
||||
Their `DEVELOPER_HANDOFF.md` §7 is nine falsifiable invariants and is a better acceptance gate than anything currently in our delivery plan. Their PR policy (one functional area per PR; mandatory security impact and rollback plan; never touch payment/inventory/order state machines inside a redesign PR) and their release discipline ("a local build or the existence of a UI does not mean production readiness") are both worth adopting verbatim.
|
||||
|
||||
---
|
||||
|
||||
## 6. Sequencing
|
||||
|
||||
Four waves. Each wave is independently shippable; nothing in a later wave blocks an earlier one.
|
||||
|
||||
**Wave 1 — Defect fixes with a security benefit (this sprint, Lane A).**
|
||||
The three live bugs: geo over HTTP, bank URL in an iframe, provider credentials and partner ID in the bundle. Plus `Idempotency-Key` on payment creation. All frontend, all provable, all things their audit will otherwise keep pointing at.
|
||||
|
||||
**Wave 2 — Contract hardening (Lane B, parallel with Wave 1).**
|
||||
Write the correctness primitives, session model, tenant/preview rules, revision semantics, and inventory journal into `docs/backend/`. Costs no engineering capacity from the frontend team and immediately raises the bar the backend is built to.
|
||||
|
||||
**Wave 3 — Proof (Lane A).**
|
||||
The two acceptance e2e tests, bundle budget as a blocking CI check, and the deployable split that gets us under budget.
|
||||
|
||||
**Wave 4 — Identity (Lane C).**
|
||||
Blocked on the central-identity-host decision. Provider-agnostic surface, VK ID, Yandex ID, Telegram migration, linking UI.
|
||||
|
||||
Ops (Lane D) and process (Lane E) run continuously alongside.
|
||||
|
||||
---
|
||||
|
||||
## 7. Explicitly out of scope
|
||||
|
||||
- Merging the two codebases, in either direction.
|
||||
- Reimplementing their backend here.
|
||||
- Adopting their section schema, their template list, or their backoffice.
|
||||
- Any dependency downgrade.
|
||||
- Removing our boundary checker, cycle check, or coverage floor to match their looser governance.
|
||||
|
||||
---
|
||||
|
||||
## 8. Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| Contract lines in `docs/backend/` are written and never implemented | Pair each with an acceptance scenario in the handoff doc so it is a delivery gate, not a wish |
|
||||
| The central-identity-host decision slips and blocks all of Wave 4 | It is the first item in the TODO; escalate on day one |
|
||||
| Removing the client-side payment credential path breaks checkout before the server side exists | Confirm the server-priced checkout session path (already in `api.service.ts`) covers every live flow before deleting the legacy header path |
|
||||
| The deployable split is larger than estimated | Wave 3 item, not a blocker for Waves 1–2; can ship the bundle-budget CI check first and let it fail loudly |
|
||||
| Harvest is read as "they were right about everything" | The analysis records where they are behind us — tests, e2e, framework currency, frontend depth — and the TODO carries no item that regresses those |
|
||||
|
||||
---
|
||||
|
||||
## 9. Done means
|
||||
|
||||
- Every Wave 1 item has a test or an observable check proving it.
|
||||
- Every Lane B item exists as normative text in `docs/backend/` with an acceptance scenario attached.
|
||||
- The two §22 acceptance tests run in CI.
|
||||
- Bundle budget is a blocking check and the storefront is under it.
|
||||
- VK ID and Yandex ID both log a customer in through `@marketplaces/auth`, with the client never holding a secret, a token, or a code verifier.
|
||||
- No item in this harvest lowered our Angular version, our test count, or our architecture governance.
|
||||
Reference in New Issue
Block a user