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>
33 KiB
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
- 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). - 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. - 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.
- We are ahead of them in exactly one dimension, and they admit it in writing: frontend depth and editor UX (530
.tsvs 189, 158 components, 30 spec files, boundary checker, Angular 22). Their backoffice is 45 files and still shipsmock-data.service.ts. - 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.jsonis nameddexarmarket, Angular 21.2.18, with brand configsdexar/novo/lavero.- Our
package.jsonis nameddexarmarket, Angular 22.0.8. reference/parallel-frontend/package.jsonis our current code, Angular 22.0.8, with ourarch:checkscripts.
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-2564d2d990416f573791b09df9ca8c02266432b5ddf213b9ed38c0bced1f19d854f. 745 files insrc/, 26 inpublic/, 1 intools/.
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:
- Security, tenant isolation and financial correctness.
docs/PRODUCT_SPECIFICATION.md.- Real models and invariants of
platform-api.- Existing confirmed production scenarios.
- 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
MarketplaceDomainbyhostnameunique index, requiresverifiedAt != null, requiresstatus = 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 astorefront_previewcookie. A global FastifyonRequesthook returns404 Preview mode is read-onlyfor 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-minutesetupToken+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 ifrevokedAt, 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
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.idempotencyKeyis 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-byteFIELD_ENCRYPTION_KEY. Decrypted only inside the service, never serialized into a response. - NUMUS webhook: requires
eventId/eventType/timestamp/signatureheaders, validates the envelope (schemaVersion === 1), HMAC-verifies the raw body, then inserts intoPaymentWebhookEventwith@@unique([provider, eventKey]). A PrismaP2002collision returns{accepted: true, duplicate: true}— replay is a no-op by construction, not by anif. - Vitanova webhook: same shape, per-marketplace
webhookSecretoverriding the global one,eventKeyfalling back tosha256(rawBody). pollPending(50)is the reconciliation fallback for the last 24 h; failures are swallowed so the next tick retries.checkoutUrlpasses throughsafeHttpsUrl()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) andegress(API + worker + migrate only). - API bound to
127.0.0.1:3000only.no-new-privilegeson every service. - Postgres 17.7 with
wal_level=replica,archive_mode=on,archive_timeout=300, archive command copying WAL into a backup volume. migrateis a separate one-shot service;apiandworkerbothdepends_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*-healthchecktimers per app. provision-domain.shrefuses 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,sshdhardening drop-in,sysctlhardening,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
- 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.
- Session model. They: server-stored, hashed, HttpOnly, revocable, TOTP-gated, separate cookies per contour. Us: client-held.
- Idempotency. They: unique constraints doing the work (
Payment.idempotencyKey,PaymentWebhookEvent(provider,eventKey)). Us: zeroidempot*anywhere in the codebase. - Deployability. They: Compose + migrations + healthchecks + WAL + restore check + domain automation. Us: shell scripts and GH Actions, with no database to migrate.
- 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.tsstill in it. - Test posture. They have no e2e whatsoever and 25 unit tests across the whole platform. Their own
VERIFICATION.mdsays 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. - 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
- Conditional-UPDATE stock reservation. Adopt the pattern verbatim in
docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md: reserve viaWHERE (onHand - reserved) >= qty RETURNING id, empty result = 409. It removes a whole class of race conditions and it is one line of SQL. - Idempotency as a unique constraint, not application logic.
Payment.idempotencyKey UNIQUE,PaymentWebhookEvent @@unique([provider, eventKey]), P2002 →{duplicate: true}. Push this intoPHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.mdas a schema requirement, not a behavioural note. - Move every credential out of the browser. Their audit is right about
authorization-key/userid-value/ partner ID. MirrorPaymentCredential.encryptedConfig(AES-256-GCM, versionedv1.iv.tag.ct) in our contract and delete the client-side header path. - 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. - Origin allowlist hook for all admin mutations. Twelve lines in
main.ts; kills CSRF for cookie-authenticated mutations. Mirror inTRACK-S-SECURITY-RBAC-CONTRACT.md. - 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
- Signed preview token + read-only preview enforcement. HMAC token, 15 min,
storefront_previewcookie, global hook rejecting non-GET. We have preview UI and no preview safety. - Immutable revisions with
version = max+1, rollback-as-new-revision. Never rewrite history;publishedRevisionis an integer pointer flipped in-transaction. - 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.
MarketplaceAuthCredentialtable. They have it and do not use it. It is exactly the right home for per-tenant VK/Yandex OAuth app credentials — see §8.- Two-network Compose split (
internal: truefor the data network) and API bound to loopback. Makes "the database is not reachable from the internet" structural rather than a firewall promise. - WAL archiving +
restore-check.sh+ a scheduled restore drill. We have deploy automation and no proven restore.
P2 — process, not code
- 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. - 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.
- 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_MANAGERas 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+ aDigitalCodepool withAVAILABLE/RESERVED/ASSIGNED/REVOKED, encrypted values,valueHashunique 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, withDOMAIN_PENDINGas 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").
InventoryMovementas an append-only journal withreason,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,dryRundefault 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 insidepackage-lock.jsonintegrity 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 singleoauth-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, noprovidercolumn onCustomer, 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 | getAuthorizeUrl(), completeCallback(code, codeVerifier) |
| vk-id-api.gateway.ts | Real HTTP client → /api/identity/v1/vk/authorize, /vk/callback |
| vk-id-local.gateway.ts | Mock |
| vk-id-gateway.token.ts | DI seam via environment.useMockData |
| vk-id-login.component.ts | Button component |
| customer-identity.model.ts | ExternalIdentityProvider = 'vk_id' | 'telegram' | 'max' |
| 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:
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(typicallyvkid.personal_info email phone). - The callback returns a
device_idalongsidecode, 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_infowith the access token → stableuser_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 withclient_id:client_secret. - Profile:
GET https://login.yandex.ru/info?format=jsonwith headerAuthorization: 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.
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:
- Redirect URI vs. multi-tenant domains. VK and Yandex both validate
redirect_uriagainst 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 signedstate, 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. @@unique([provider, providerUserId]), plus a decision on per-tenant customer separation. Their platform isolatesCustomerper 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 ourCustomer.marketplaceIdalready implies.- Identity conflict is not an upsert. Our PHASE-8 §2 already says this: if
providerUserIdis already bound to a differentCustomer, route to controlled resolution — never silently rebind. Enforce it with the unique index so the database refuses, rather than trusting the service layer. stateis 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/replayedstatewith a generic error.- 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.tsin 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_SLUGpinned 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 thesslip.iostaging 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/ |