docs: backend contracts for Phases 8-10 (identity/messaging, tenant registry, content modules)
- Phase 8: Customer/ExternalIdentity/ContactChannel, VK ID OAuth 2.1/PKCE
built first per Sprint 0.1 ("do all after vk"), then OTP, then MAX/
Telegram bot linking, then the Notification Orchestrator + Delivery
Conversation State Machine. Hard rule carried through: bots never touch
financial statuses, only delivery fields via a dedicated Delivery Service.
- Phase 9: Marketplace/MarketplaceDomain/MarketplaceFeatureSet/
MarketplaceRevision, full Hostinger DNS automation sequence (snapshot
before change, never touch MX/SPF/DKIM/DMARC/CAA), lifecycle state
machine that must expose its own blocker on every transition, publish
model with orders/payments/inventory explicitly excluded from revisions.
- Phase 10: Gorbushka-class content entities (Shop/Service/Floor/
SchemePin/RentListing/News/Lead/MallSettings), explicitly lowest
priority and gated on Commerce Core being real first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:05:39 +04:00
# Phase 8 Backend Contract — Customer Identity, VK ID, MAX/Telegram Messaging
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md ](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md ) Phase 8 (Sprints 8.1– 8.5). Covers plan §2.9, §3.4, and all of §14 (the v3.1-only addition).
**Status: ready to build. Sprint order fixed by Sprint 0.1 decision: VK ID first, then everything else** ("do all after vk"). Sequence below follows that: identity core → VK ID → email/phone OTP → MAX/Telegram → Notification Orchestrator.
---
## 1. Entities
```ts
interface Customer {
id: string;
marketplaceId: string; // or global identity strategy, tenant-configurable
name?: string;
email?: string;
phone?: string;
status: 'active' | 'suspended';
createdAt: string;
}
interface ExternalIdentity {
customerId: string;
feat(identity): provider-agnostic social login, VK ID + Yandex ID (FH-4.1, FH-4.2)
The VK-only scaffolding had a shape problem worth fixing before anything
was built on it: completeCallback(code, codeVerifier) took the PKCE
verifier from the client, which forces the browser to generate and hold
it. We are a confidential client - a browser-held verifier buys nothing
and adds a place to steal it from.
Replaces the four vk-id-* files with a provider-agnostic surface:
getAuthorizeUrl(provider, returnTo?)
listIdentities()
unlink(provider)
completeCallback is gone entirely. The backend mints and stores state and
code_verifier single-use for 10 minutes, handles the provider's callback
itself, issues the session cookie and redirects. VK and Yandex differ
only in a path segment, because everything that actually differs between
them - PKCE handling, VK's device_id, Yandex's Basic-auth exchange -
lives backend-side.
vk-id-login becomes social-login-button with a provider input; adding
Yandex to the UI is an input value, not new code. Adds yandex_id to
ExternalIdentityProvider, plus optional email/phone/displayName since VK
frequently returns no email.
social-identity-gateway.spec.ts (5 tests) asserts the requests carry no
code_verifier and no client_secret, so reintroducing a browser-held
verifier fails the build rather than passing review.
PHASE-8 §2 rewritten to match: the four endpoints, backend-owned state
and verifier, UNIQUE (provider, providerUserId) with conflict routed to
controlled resolution rather than a silent rebind, per-tenant OAuth app
config under the Track S §4.2 envelope, and both providers' full endpoint
sets. Two things recorded there because they are expensive to discover
later: VK's callback returns device_id alongside code and the token
exchange fails without it, and both providers validate redirect_uri
against an exact registered list - which a multi-tenant platform cannot
satisfy without a central identity host (FH-0.1, still undecided).
256 tests pass. Build green, boundaries and cycles green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:15:26 +04:00
provider: 'vk_id' | 'yandex_id' | 'telegram' | 'max'; // yandex_id added 2026-08-21, FH-4.5
docs: backend contracts for Phases 8-10 (identity/messaging, tenant registry, content modules)
- Phase 8: Customer/ExternalIdentity/ContactChannel, VK ID OAuth 2.1/PKCE
built first per Sprint 0.1 ("do all after vk"), then OTP, then MAX/
Telegram bot linking, then the Notification Orchestrator + Delivery
Conversation State Machine. Hard rule carried through: bots never touch
financial statuses, only delivery fields via a dedicated Delivery Service.
- Phase 9: Marketplace/MarketplaceDomain/MarketplaceFeatureSet/
MarketplaceRevision, full Hostinger DNS automation sequence (snapshot
before change, never touch MX/SPF/DKIM/DMARC/CAA), lifecycle state
machine that must expose its own blocker on every transition, publish
model with orders/payments/inventory explicitly excluded from revisions.
- Phase 10: Gorbushka-class content entities (Shop/Service/Floor/
SchemePin/RentListing/News/Lead/MallSettings), explicitly lowest
priority and gated on Commerce Core being real first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:05:39 +04:00
providerUserId: string;
feat(identity): provider-agnostic social login, VK ID + Yandex ID (FH-4.1, FH-4.2)
The VK-only scaffolding had a shape problem worth fixing before anything
was built on it: completeCallback(code, codeVerifier) took the PKCE
verifier from the client, which forces the browser to generate and hold
it. We are a confidential client - a browser-held verifier buys nothing
and adds a place to steal it from.
Replaces the four vk-id-* files with a provider-agnostic surface:
getAuthorizeUrl(provider, returnTo?)
listIdentities()
unlink(provider)
completeCallback is gone entirely. The backend mints and stores state and
code_verifier single-use for 10 minutes, handles the provider's callback
itself, issues the session cookie and redirects. VK and Yandex differ
only in a path segment, because everything that actually differs between
them - PKCE handling, VK's device_id, Yandex's Basic-auth exchange -
lives backend-side.
vk-id-login becomes social-login-button with a provider input; adding
Yandex to the UI is an input value, not new code. Adds yandex_id to
ExternalIdentityProvider, plus optional email/phone/displayName since VK
frequently returns no email.
social-identity-gateway.spec.ts (5 tests) asserts the requests carry no
code_verifier and no client_secret, so reintroducing a browser-held
verifier fails the build rather than passing review.
PHASE-8 §2 rewritten to match: the four endpoints, backend-owned state
and verifier, UNIQUE (provider, providerUserId) with conflict routed to
controlled resolution rather than a silent rebind, per-tenant OAuth app
config under the Track S §4.2 envelope, and both providers' full endpoint
sets. Two things recorded there because they are expensive to discover
later: VK's callback returns device_id alongside code and the token
exchange fails without it, and both providers validate redirect_uri
against an exact registered list - which a multi-tenant platform cannot
satisfy without a central identity host (FH-0.1, still undecided).
256 tests pass. Build green, boundaries and cycles green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:15:26 +04:00
email?: string; // VK frequently returns none - never require it
phone?: string;
displayName?: string;
docs: backend contracts for Phases 8-10 (identity/messaging, tenant registry, content modules)
- Phase 8: Customer/ExternalIdentity/ContactChannel, VK ID OAuth 2.1/PKCE
built first per Sprint 0.1 ("do all after vk"), then OTP, then MAX/
Telegram bot linking, then the Notification Orchestrator + Delivery
Conversation State Machine. Hard rule carried through: bots never touch
financial statuses, only delivery fields via a dedicated Delivery Service.
- Phase 9: Marketplace/MarketplaceDomain/MarketplaceFeatureSet/
MarketplaceRevision, full Hostinger DNS automation sequence (snapshot
before change, never touch MX/SPF/DKIM/DMARC/CAA), lifecycle state
machine that must expose its own blocker on every transition, publish
model with orders/payments/inventory explicitly excluded from revisions.
- Phase 10: Gorbushka-class content entities (Shop/Service/Floor/
SchemePin/RentListing/News/Lead/MallSettings), explicitly lowest
priority and gated on Commerce Core being real first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:05:39 +04:00
verifiedAt: string;
metadata: Record<string, unknown>;
lastUsedAt: string;
}
interface ContactMethod {
customerId: string;
type: 'email' | 'phone';
value: string;
verifiedAt?: string;
}
interface ContactChannel {
customerId: string;
provider: 'telegram' | 'vk' | 'max';
chatId: string;
verified: boolean;
notificationsEnabled: boolean;
deliveryEnabled: boolean;
}
interface MessagingConsent {
customerId: string;
channel: string;
purpose: 'marketing' | 'order_service_messages';
grantedAt?: string;
revokedAt?: string;
}
```
Telegram is demoted from sole identity to one `ExternalIdentity` provider among several — it must remain fully functional, just no longer the only path.
feat(identity): provider-agnostic social login, VK ID + Yandex ID (FH-4.1, FH-4.2)
The VK-only scaffolding had a shape problem worth fixing before anything
was built on it: completeCallback(code, codeVerifier) took the PKCE
verifier from the client, which forces the browser to generate and hold
it. We are a confidential client - a browser-held verifier buys nothing
and adds a place to steal it from.
Replaces the four vk-id-* files with a provider-agnostic surface:
getAuthorizeUrl(provider, returnTo?)
listIdentities()
unlink(provider)
completeCallback is gone entirely. The backend mints and stores state and
code_verifier single-use for 10 minutes, handles the provider's callback
itself, issues the session cookie and redirects. VK and Yandex differ
only in a path segment, because everything that actually differs between
them - PKCE handling, VK's device_id, Yandex's Basic-auth exchange -
lives backend-side.
vk-id-login becomes social-login-button with a provider input; adding
Yandex to the UI is an input value, not new code. Adds yandex_id to
ExternalIdentityProvider, plus optional email/phone/displayName since VK
frequently returns no email.
social-identity-gateway.spec.ts (5 tests) asserts the requests carry no
code_verifier and no client_secret, so reintroducing a browser-held
verifier fails the build rather than passing review.
PHASE-8 §2 rewritten to match: the four endpoints, backend-owned state
and verifier, UNIQUE (provider, providerUserId) with conflict routed to
controlled resolution rather than a silent rebind, per-tenant OAuth app
config under the Track S §4.2 envelope, and both providers' full endpoint
sets. Two things recorded there because they are expensive to discover
later: VK's callback returns device_id alongside code and the token
exchange fails without it, and both providers validate redirect_uri
against an exact registered list - which a multi-tenant platform cannot
satisfy without a central identity host (FH-0.1, still undecided).
256 tests pass. Build green, boundaries and cycles green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:15:26 +04:00
## 2. Sprint 8.2 — Social identity: VK ID first, Yandex ID second
Rewritten 2026-08-21 (FH-4.1– FH-4.5). Previously this section specified a VK-only pair of endpoints where the callback took `{ code, codeVerifier }` from the browser. Two changes: the surface is provider-agnostic, and the PKCE verifier stops travelling through the client.
### 2.1 Surface
```
GET /api/identity/v1/{provider}/authorize?returnTo= -> { url } (or 302)
GET /api/identity/v1/{provider}/callback?code=&state=[&device_id=]
POST /api/identity/v1/{provider}/unlink (authenticated)
GET /api/identity/v1/me/identities (authenticated) -> ExternalIdentity[]
```
`{provider}` is `vk` or `yandex` today; `telegram` and `max` join it when §4 migrates them onto `ExternalIdentity` . One controller, one strategy object per provider. The frontend gateway is a single interface (`src/app/core/identity/services/social-identity-gateway.interface.ts` ) — providers differ only in a path segment, because everything that actually differs between them is backend-side.
### 2.2 The backend owns state and the code verifier
`/authorize` generates `state` and `code_verifier` , stores `{ state, codeVerifier, marketplaceId, returnTo, expiresAt }` server-side or in a signed `HttpOnly` cookie, TTL 10 minutes, **single use — deleted on first presentation ** . It returns (or redirects to) the provider URL carrying `code_challenge` (S256) and `state` .
`/callback` validates `state` , exchanges the code using the stored verifier, fetches the profile, resolves or links the `ExternalIdentity` , issues the customer session cookie ([Track S §2.1 ](TRACK-S-SECURITY-RBAC-CONTRACT.md )), and redirects to `returnTo` .
- The client never sees a client secret, an access token, or a code verifier. We are a confidential client; a browser-held verifier buys nothing and adds a place to steal it from.
- An unknown, expired, or replayed `state` is a generic error. Do not distinguish the cases to the caller.
- `returnTo` is validated against the tenant's own verified origin. It is an open redirect otherwise.
### 2.3 Identity resolution
- A repeat login for the same `providerUserId` resolves to the same `Customer` , never a duplicate.
- `UNIQUE (provider, providerUserId)` . If a provider account is already bound to a * different * `Customer` , that is an identity conflict — route to controlled resolution, never silently rebind (plan §14.3). The unique index is what enforces this; a service-layer check is not sufficient.
- Whether one VK account across two of our storefronts is one `Customer` or two is a **product decision that must be made before implementation ** (FH-0.1). `Customer.marketplaceId` currently implies two, and two is the safer default for data protection.
- Email is optional on `Customer` . Yandex returns one in most cases; VK frequently does not.
### 2.4 Per-tenant OAuth applications
Client id and secret are per marketplace, stored with the [Track S §4.2 ](TRACK-S-SECURITY-RBAC-CONTRACT.md ) envelope: `{ clientId, clientSecret, scopes[], redirectUri }` . Never returned by any endpoint.
**The redirect_uri problem, which must be solved before any code is written.** Both providers validate `redirect_uri` against an exact registered list. We cannot register one per tenant domain and we cannot let tenants supply their own. Resolution: one **central identity host ** as the sole registered callback, the origin tenant carried inside the signed `state` , then a 302 back to the tenant domain with a short-lived signed one-time handoff token that the tenant API exchanges for its session cookie. This is a one-way door — retrofitting it after the first provider is live is expensive.
### 2.5 Provider notes
Confirm exact parameter and scope names against live provider documentation before implementing; both providers have revised their flows recently.
**VK ID** — OAuth 2.1, PKCE mandatory (S256).
```
authorize GET https://id.vk.com/authorize
client_id, redirect_uri, response_type=code,
code_challenge, code_challenge_method=S256, state, scope
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
logout https://id.vk.com/oauth2/logout (call on unlink)
```
**The callback returns `device_id` alongside `code` , and the token exchange fails without it.** This is the single most common VK ID integration bug; it is in this contract so it is not rediscovered at debugging time.
**Yandex ID** — OAuth 2.0, PKCE supported; use it.
docs: backend contracts for Phases 8-10 (identity/messaging, tenant registry, content modules)
- Phase 8: Customer/ExternalIdentity/ContactChannel, VK ID OAuth 2.1/PKCE
built first per Sprint 0.1 ("do all after vk"), then OTP, then MAX/
Telegram bot linking, then the Notification Orchestrator + Delivery
Conversation State Machine. Hard rule carried through: bots never touch
financial statuses, only delivery fields via a dedicated Delivery Service.
- Phase 9: Marketplace/MarketplaceDomain/MarketplaceFeatureSet/
MarketplaceRevision, full Hostinger DNS automation sequence (snapshot
before change, never touch MX/SPF/DKIM/DMARC/CAA), lifecycle state
machine that must expose its own blocker on every transition, publish
model with orders/payments/inventory explicitly excluded from revisions.
- Phase 10: Gorbushka-class content entities (Shop/Service/Floor/
SchemePin/RentListing/News/Lead/MallSettings), explicitly lowest
priority and gated on Commerce Core being real first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:05:39 +04:00
```
feat(identity): provider-agnostic social login, VK ID + Yandex ID (FH-4.1, FH-4.2)
The VK-only scaffolding had a shape problem worth fixing before anything
was built on it: completeCallback(code, codeVerifier) took the PKCE
verifier from the client, which forces the browser to generate and hold
it. We are a confidential client - a browser-held verifier buys nothing
and adds a place to steal it from.
Replaces the four vk-id-* files with a provider-agnostic surface:
getAuthorizeUrl(provider, returnTo?)
listIdentities()
unlink(provider)
completeCallback is gone entirely. The backend mints and stores state and
code_verifier single-use for 10 minutes, handles the provider's callback
itself, issues the session cookie and redirects. VK and Yandex differ
only in a path segment, because everything that actually differs between
them - PKCE handling, VK's device_id, Yandex's Basic-auth exchange -
lives backend-side.
vk-id-login becomes social-login-button with a provider input; adding
Yandex to the UI is an input value, not new code. Adds yandex_id to
ExternalIdentityProvider, plus optional email/phone/displayName since VK
frequently returns no email.
social-identity-gateway.spec.ts (5 tests) asserts the requests carry no
code_verifier and no client_secret, so reintroducing a browser-held
verifier fails the build rather than passing review.
PHASE-8 §2 rewritten to match: the four endpoints, backend-owned state
and verifier, UNIQUE (provider, providerUserId) with conflict routed to
controlled resolution rather than a silent rebind, per-tenant OAuth app
config under the Track S §4.2 envelope, and both providers' full endpoint
sets. Two things recorded there because they are expensive to discover
later: VK's callback returns device_id alongside code and the token
exchange fails without it, and both providers validate redirect_uri
against an exact registered list - which a multi-tenant platform cannot
satisfy without a central identity host (FH-0.1, still undecided).
256 tests pass. Build green, boundaries and cycles green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:15:26 +04:00
authorize GET 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: client_id:client_secret
profile GET https://login.yandex.ru/info?format=json
Authorization: OAuth <access_token>
-> id, login, default_email, default_phone, psuid
docs: backend contracts for Phases 8-10 (identity/messaging, tenant registry, content modules)
- Phase 8: Customer/ExternalIdentity/ContactChannel, VK ID OAuth 2.1/PKCE
built first per Sprint 0.1 ("do all after vk"), then OTP, then MAX/
Telegram bot linking, then the Notification Orchestrator + Delivery
Conversation State Machine. Hard rule carried through: bots never touch
financial statuses, only delivery fields via a dedicated Delivery Service.
- Phase 9: Marketplace/MarketplaceDomain/MarketplaceFeatureSet/
MarketplaceRevision, full Hostinger DNS automation sequence (snapshot
before change, never touch MX/SPF/DKIM/DMARC/CAA), lifecycle state
machine that must expose its own blocker on every transition, publish
model with orders/payments/inventory explicitly excluded from revisions.
- Phase 10: Gorbushka-class content entities (Shop/Service/Floor/
SchemePin/RentListing/News/Lead/MallSettings), explicitly lowest
priority and gated on Commerce Core being real first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:05:39 +04:00
```
feat(identity): provider-agnostic social login, VK ID + Yandex ID (FH-4.1, FH-4.2)
The VK-only scaffolding had a shape problem worth fixing before anything
was built on it: completeCallback(code, codeVerifier) took the PKCE
verifier from the client, which forces the browser to generate and hold
it. We are a confidential client - a browser-held verifier buys nothing
and adds a place to steal it from.
Replaces the four vk-id-* files with a provider-agnostic surface:
getAuthorizeUrl(provider, returnTo?)
listIdentities()
unlink(provider)
completeCallback is gone entirely. The backend mints and stores state and
code_verifier single-use for 10 minutes, handles the provider's callback
itself, issues the session cookie and redirects. VK and Yandex differ
only in a path segment, because everything that actually differs between
them - PKCE handling, VK's device_id, Yandex's Basic-auth exchange -
lives backend-side.
vk-id-login becomes social-login-button with a provider input; adding
Yandex to the UI is an input value, not new code. Adds yandex_id to
ExternalIdentityProvider, plus optional email/phone/displayName since VK
frequently returns no email.
social-identity-gateway.spec.ts (5 tests) asserts the requests carry no
code_verifier and no client_secret, so reintroducing a browser-held
verifier fails the build rather than passing review.
PHASE-8 §2 rewritten to match: the four endpoints, backend-owned state
and verifier, UNIQUE (provider, providerUserId) with conflict routed to
controlled resolution rather than a silent rebind, per-tenant OAuth app
config under the Track S §4.2 envelope, and both providers' full endpoint
sets. Two things recorded there because they are expensive to discover
later: VK's callback returns device_id alongside code and the token
exchange fails without it, and both providers validate redirect_uri
against an exact registered list - which a multi-tenant platform cannot
satisfy without a central identity host (FH-0.1, still undecided).
256 tests pass. Build green, boundaries and cycles green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:15:26 +04:00
Yandex is a second strategy object against the same surface, not a second integration. Build it after VK works.
docs: backend contracts for Phases 8-10 (identity/messaging, tenant registry, content modules)
- Phase 8: Customer/ExternalIdentity/ContactChannel, VK ID OAuth 2.1/PKCE
built first per Sprint 0.1 ("do all after vk"), then OTP, then MAX/
Telegram bot linking, then the Notification Orchestrator + Delivery
Conversation State Machine. Hard rule carried through: bots never touch
financial statuses, only delivery fields via a dedicated Delivery Service.
- Phase 9: Marketplace/MarketplaceDomain/MarketplaceFeatureSet/
MarketplaceRevision, full Hostinger DNS automation sequence (snapshot
before change, never touch MX/SPF/DKIM/DMARC/CAA), lifecycle state
machine that must expose its own blocker on every transition, publish
model with orders/payments/inventory explicitly excluded from revisions.
- Phase 10: Gorbushka-class content entities (Shop/Service/Floor/
SchemePin/RentListing/News/Lead/MallSettings), explicitly lowest
priority and gated on Commerce Core being real first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:05:39 +04:00
feat(identity): account-linking UI + Telegram-as-identity surface (FH-4.7, FH-4.6, FH-4.8)
FH-4.7 - AccountIdentitiesComponent under
features/website/account/identities/. Lists linked identities from
GET /me/identities, offers attach buttons only for OAuth providers not
already linked (reusing SocialLoginButtonComponent), detaches through
unlink(). Refuses to detach the last remaining identity - it is the only
way back in - with the control disabled and an explanatory title, matching
the backend's last-identity 409. Loading / error / ready states; a load
failure surfaces an error rather than rendering an empty account, and a
slot carries the identity-conflict message from PHASE-8 §2.3. 6 unit tests.
Not wired into a route: the storefront has no customer account area yet
and no live OAuth application to authorize against (FH-0.1). This is the
surface both depend on, buildable and tested now.
FH-4.6 (client + contract) - the gateway now separates the two provider
sets. SocialProvider (vk | yandex) is what has an OAuth authorize
redirect; ExternalIdentityProvider (adds telegram | max) is what can be
listed and unlinked. unlink() widened to the latter so Telegram detaches
through the same path as VK, with no second code path. The dev local
gateway seeds a Telegram identity so the linking screen is exercisable
before any real provider exists.
PHASE-8 §2.6 specifies the backend migration: a Telegram login writes an
ExternalIdentity row under the same uniqueness and identity-conflict rule
as VK, appears in /me/identities, is removable subject to the
last-identity 409, and keeps customer (marketplace_session) and admin
(bo_session) sessions as distinct cookies - closing the shared
customer/admin Telegram session the audit flagged. The identity row and
the messaging BotConversationBinding stay separate records.
FH-4.8 - PHASE-8 §3 now states email/phone OTP's position explicitly:
recovery when a linked messenger is unreachable and an addable second
factor, never the primary login, and one more identity on the same
customer rather than a parallel account.
262 tests pass. Build green, boundaries and cycles green, bundle scan clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 22:36:36 +04:00
### 2.6 Migrating Telegram onto `ExternalIdentity` (FH-4.6)
Added 2026-08-21. Telegram is the current app's only customer login and it does not go through §2's OAuth surface — it authenticates via a QR/bot flow owned by `@marketplaces/auth` . The migration makes it * one identity among several * without changing how it authenticates:
- On a successful Telegram login the backend writes (or updates) an `ExternalIdentity` with `provider: 'telegram'` and `providerUserId` = the Telegram user id, under the same `UNIQUE (provider, providerUserId)` and same identity-conflict rule as §2.3. Telegram stops being a special-cased column and becomes a row like any other provider.
- Telegram appears in `GET /me/identities` and is removable through `POST /api/identity/v1/telegram/unlink` , subject to the **last-identity rule ** : the backend refuses to unlink a customer's only remaining identity (`409` , a login they cannot get back). The frontend gateway's `unlink()` already accepts any `ExternalIdentityProvider` , not just the OAuth ones, so the linking UI (FH-4.7) drives this without a second code path.
- **The customer/admin session split their audit flagged is closed here.** A Telegram customer session (`marketplace_session` ) and an admin session (`bo_session` ) are different cookies checked by different guards ([Track S §2.1 ](TRACK-S-SECURITY-RBAC-CONTRACT.md )); the same Telegram account authenticating as a customer must never satisfy an admin guard, and vice versa. Migrating Telegram to `ExternalIdentity` on the customer side does not grant it any admin scope — admin membership is a separate axis.
- Telegram's messaging binding (§4 `BotConversationBinding` , delivery updates) is a * contact channel * , distinct from the * identity * row added here. One provider can be both; they are separate records so unlinking the identity does not silently kill an order's delivery conversation, and vice versa.
Client state today: `SocialProvider` (`vk` | `yandex` ) is the set with an OAuth authorize redirect; `ExternalIdentityProvider` (`vk_id` | `yandex_id` | `telegram` | `max` ) is the set that can be listed and unlinked. `unlink()` takes the wider type; `getAuthorizeUrl()` takes the narrower one. See `src/app/core/identity/services/social-identity-gateway.interface.ts` .
## 3. Sprint 8.3 — Email/phone OTP (after VK ID), positioned as recovery (FH-4.8)
docs: backend contracts for Phases 8-10 (identity/messaging, tenant registry, content modules)
- Phase 8: Customer/ExternalIdentity/ContactChannel, VK ID OAuth 2.1/PKCE
built first per Sprint 0.1 ("do all after vk"), then OTP, then MAX/
Telegram bot linking, then the Notification Orchestrator + Delivery
Conversation State Machine. Hard rule carried through: bots never touch
financial statuses, only delivery fields via a dedicated Delivery Service.
- Phase 9: Marketplace/MarketplaceDomain/MarketplaceFeatureSet/
MarketplaceRevision, full Hostinger DNS automation sequence (snapshot
before change, never touch MX/SPF/DKIM/DMARC/CAA), lifecycle state
machine that must expose its own blocker on every transition, publish
model with orders/payments/inventory explicitly excluded from revisions.
- Phase 10: Gorbushka-class content entities (Shop/Service/Floor/
SchemePin/RentListing/News/Lead/MallSettings), explicitly lowest
priority and gated on Commerce Core being real first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:05:39 +04:00
Implements the already-approved [email/phone login spec ](../superpowers/specs/2026-08-15-email-phone-login-design.md ). Per v3.1 §14, position this as **recovery/fallback ** when a messenger channel is unavailable — not the primary login path. No new contract beyond that spec; this section exists only to fix its place in the build order relative to VK ID.
feat(identity): account-linking UI + Telegram-as-identity surface (FH-4.7, FH-4.6, FH-4.8)
FH-4.7 - AccountIdentitiesComponent under
features/website/account/identities/. Lists linked identities from
GET /me/identities, offers attach buttons only for OAuth providers not
already linked (reusing SocialLoginButtonComponent), detaches through
unlink(). Refuses to detach the last remaining identity - it is the only
way back in - with the control disabled and an explanatory title, matching
the backend's last-identity 409. Loading / error / ready states; a load
failure surfaces an error rather than rendering an empty account, and a
slot carries the identity-conflict message from PHASE-8 §2.3. 6 unit tests.
Not wired into a route: the storefront has no customer account area yet
and no live OAuth application to authorize against (FH-0.1). This is the
surface both depend on, buildable and tested now.
FH-4.6 (client + contract) - the gateway now separates the two provider
sets. SocialProvider (vk | yandex) is what has an OAuth authorize
redirect; ExternalIdentityProvider (adds telegram | max) is what can be
listed and unlinked. unlink() widened to the latter so Telegram detaches
through the same path as VK, with no second code path. The dev local
gateway seeds a Telegram identity so the linking screen is exercisable
before any real provider exists.
PHASE-8 §2.6 specifies the backend migration: a Telegram login writes an
ExternalIdentity row under the same uniqueness and identity-conflict rule
as VK, appears in /me/identities, is removable subject to the
last-identity 409, and keeps customer (marketplace_session) and admin
(bo_session) sessions as distinct cookies - closing the shared
customer/admin Telegram session the audit flagged. The identity row and
the messaging BotConversationBinding stay separate records.
FH-4.8 - PHASE-8 §3 now states email/phone OTP's position explicitly:
recovery when a linked messenger is unreachable and an addable second
factor, never the primary login, and one more identity on the same
customer rather than a parallel account.
262 tests pass. Build green, boundaries and cycles green, bundle scan clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 22:36:36 +04:00
Concretely, "recovery/fallback" means: email/phone OTP is offered as a way back in when a customer's linked messenger identity is unreachable, and as a second factor a customer can add — never as the front-and-centre first option on the login surface, which is VK ID (and Telegram, where it is already the norm). It is one more `ExternalIdentity` /`ContactMethod` on the same customer, not a parallel account.
docs: backend contracts for Phases 8-10 (identity/messaging, tenant registry, content modules)
- Phase 8: Customer/ExternalIdentity/ContactChannel, VK ID OAuth 2.1/PKCE
built first per Sprint 0.1 ("do all after vk"), then OTP, then MAX/
Telegram bot linking, then the Notification Orchestrator + Delivery
Conversation State Machine. Hard rule carried through: bots never touch
financial statuses, only delivery fields via a dedicated Delivery Service.
- Phase 9: Marketplace/MarketplaceDomain/MarketplaceFeatureSet/
MarketplaceRevision, full Hostinger DNS automation sequence (snapshot
before change, never touch MX/SPF/DKIM/DMARC/CAA), lifecycle state
machine that must expose its own blocker on every transition, publish
model with orders/payments/inventory explicitly excluded from revisions.
- Phase 10: Gorbushka-class content entities (Shop/Service/Floor/
SchemePin/RentListing/News/Lead/MallSettings), explicitly lowest
priority and gated on Commerce Core being real first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:05:39 +04:00
## 4. Sprint 8.4 — MAX + Telegram bot channels
```ts
interface BotConversationBinding {
customerId: string;
marketplaceId: string;
provider: 'telegram' | 'max';
chatId: string;
state: string; // see §5 state machine
orderId?: string;
lastMessageAt: string;
}
```
MAX linking flow (bot-assisted, one-time code):
```
POST /api/identity/v1/max/link-code -> { code, expiresAt } (TTL, single-use, bound to marketplace + browser session)
```
User opens the MAX bot, sends the code; a confirmed bot update on the backend calls:
```
POST /api/providers/v1/max/bot-webhook -- idempotent; a repeated update must not create a duplicate binding
```
which links the pending `Customer` session to the MAX `chatId` .
All three providers' incoming bot updates (VK, MAX, Telegram) normalize into one shape:
```ts
interface MessagingEvent {
provider: 'telegram' | 'vk' | 'max';
chatId: string;
orderId?: string;
text?: string;
receivedAt: string;
}
```
Provider bot tokens/secrets never reach the frontend, ever — only the backend calls each provider's Bot API.
## 5. Sprint 8.5 — Notification Orchestrator + Delivery Conversation State Machine
On `order.paid` (Phase 2 event bus), the orchestrator picks the customer's chosen channel (captured at checkout, see [Phase 6 ](PHASE-6-CART-CHECKOUT-CONTRACT.md ) and `OrderContactSnapshot` in [Phase 2 ](PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md )) and drives:
```
not_started -> awaiting_customer -> details_received -> manager_assigned/auto_confirmed -> shipment_planned -> completed
```
```ts
interface DeliveryDetailsSnapshot {
orderId: string;
city?: string;
address?: string;
recipientName?: string;
phone?: string;
timeWindow?: string;
comment?: string;
receivedAt: string;
}
```
Hard rules:
- **The bot never changes financial statuses.** It can only write `DeliveryDetailsSnapshot` fields via a dedicated Delivery Service — no bot code path touches `Order.paymentStatus` /`orderStatus` .
- The backoffice `Notification` (Phase 2 §6) fires unconditionally on `order.paid` , independent of whether the customer's messenger channel is reachable.
- If the chosen channel is unavailable, log a `DeliveryAttempt` error (Phase 2 §6) and fall back per tenant-configured policy (e.g. email/SMS) — never block the order itself.
- Follow-up messages are rate-limited per tenant policy; after the configured attempt limit, hand off to a human manager instead of continuing to message.
```
POST /api/providers/v1/{provider}/bot-webhook -- generic entrypoint for all three providers
GET /api/admin/v2/orders/{orderId}/conversation -- message history + current state, for manager handoff
POST /api/admin/v2/orders/{orderId}/conversation/handoff
```
## 6. What the frontend will start doing once this ships
feat(identity): provider-agnostic social login, VK ID + Yandex ID (FH-4.1, FH-4.2)
The VK-only scaffolding had a shape problem worth fixing before anything
was built on it: completeCallback(code, codeVerifier) took the PKCE
verifier from the client, which forces the browser to generate and hold
it. We are a confidential client - a browser-held verifier buys nothing
and adds a place to steal it from.
Replaces the four vk-id-* files with a provider-agnostic surface:
getAuthorizeUrl(provider, returnTo?)
listIdentities()
unlink(provider)
completeCallback is gone entirely. The backend mints and stores state and
code_verifier single-use for 10 minutes, handles the provider's callback
itself, issues the session cookie and redirects. VK and Yandex differ
only in a path segment, because everything that actually differs between
them - PKCE handling, VK's device_id, Yandex's Basic-auth exchange -
lives backend-side.
vk-id-login becomes social-login-button with a provider input; adding
Yandex to the UI is an input value, not new code. Adds yandex_id to
ExternalIdentityProvider, plus optional email/phone/displayName since VK
frequently returns no email.
social-identity-gateway.spec.ts (5 tests) asserts the requests carry no
code_verifier and no client_secret, so reintroducing a browser-held
verifier fails the build rather than passing review.
PHASE-8 §2 rewritten to match: the four endpoints, backend-owned state
and verifier, UNIQUE (provider, providerUserId) with conflict routed to
controlled resolution rather than a silent rebind, per-tenant OAuth app
config under the Track S §4.2 envelope, and both providers' full endpoint
sets. Two things recorded there because they are expensive to discover
later: VK's callback returns device_id alongside code and the token
exchange fails without it, and both providers validate redirect_uri
against an exact registered list - which a multi-tenant platform cannot
satisfy without a central identity host (FH-0.1, still undecided).
256 tests pass. Build green, boundaries and cycles green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:15:26 +04:00
- VK ID and Yandex ID login buttons on the storefront. The client half already exists and is provider-agnostic: `SocialLoginButtonComponent` behind `SOCIAL_IDENTITY_GATEWAY` , with a real HTTP gateway waiting on §2.1's endpoints. Adding Yandex once VK works is a `provider` input, not new code.
- Account linking screen (`GET /me/identities` , link/unlink), including the identity-conflict resolution path from §2.3.
docs: backend contracts for Phases 8-10 (identity/messaging, tenant registry, content modules)
- Phase 8: Customer/ExternalIdentity/ContactChannel, VK ID OAuth 2.1/PKCE
built first per Sprint 0.1 ("do all after vk"), then OTP, then MAX/
Telegram bot linking, then the Notification Orchestrator + Delivery
Conversation State Machine. Hard rule carried through: bots never touch
financial statuses, only delivery fields via a dedicated Delivery Service.
- Phase 9: Marketplace/MarketplaceDomain/MarketplaceFeatureSet/
MarketplaceRevision, full Hostinger DNS automation sequence (snapshot
before change, never touch MX/SPF/DKIM/DMARC/CAA), lifecycle state
machine that must expose its own blocker on every transition, publish
model with orders/payments/inventory explicitly excluded from revisions.
- Phase 10: Gorbushka-class content entities (Shop/Service/Floor/
SchemePin/RentListing/News/Lead/MallSettings), explicitly lowest
priority and gated on Commerce Core being real first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:05:39 +04:00
- MAX/Telegram linking UI (one-time code flow).
- Checkout channel-choice step ("where should we send confirmation?") — VK / MAX / Telegram / email/SMS fallback.
- Manager-facing conversation view (message history, current delivery state, accept handoff).