Collapses the entire docs/backend/ set - Phase 1-10, Track A/S, the partner API, the two handoffs, the frontend surface inventory, and the harvest requirements - into a single source of truth, docs/backend/BACKEND-INTEGRATION.md. Every contract's entities, endpoints, and invariants are preserved, reorganised by domain rather than by sprint. The nine release invariants, the FH-* harvest mechanisms, the RBAC/audit/secrets cross-cutting rules, the tenant-routing infra contract, the 15 acceptance tests, build order, dev setup, and open decisions are all in the one file, with a change log (§14) at the bottom. The file opens with the maintenance rule: any new backend need, contract change, or shipped item updates this file in the same change - the affected section and the change log. No new backend .md files. Inbound links from BACKEND-API-REFERENCE, the ADRs, the fork docs, DEPLOYMENT, PACKAGES-USAGE, the delivery plan, and e2e/README are repointed at the single doc (section anchors collapse to the file; the prose section refs remain as context). Also recorded the rule in the repo CLAUDE.md. 17 backend docs removed, 1 added. No implementation changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
34 KiB
Fork Harvest — TODO
Branch: improvements/fork-harvest (from B2B @ 92f1c88)
Design: 2026-08-21-fork-harvest-design.md
Source analysis: 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_uriagainst 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 signedstate, 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 oneCustomeror two? Their platform says two; ourCustomer.marketplaceIdalready implies two. Done when: an ADR exists indocs/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.tsalready has a server-priced checkout session method. Confirm no production flow still depends oncreatePayment(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 · done 2026-08-21 Now
GET {tenantApiBase}/geo/resolve, same base as/regions. Server reads the client IP; nothing leaves our infrastructure. Endpoint specified in BACKEND-API-REFERENCE.md §6 — not built yet, and until it is the client falls back to the manual picker, which is what production has effectively had all along. Covered bysrc/app/services/location.service.spec.ts(4 tests, one of which fails the build on any off-origin or plaintext request from this service). Was:location.service.ts:75calledhttp://ip-api.com/json/?fields=…from an HTTPS origin. Mixed active content is blocked, sodetectLocation()only ever took its error branch — auto-detect was dead in production, not merely insecure — and the attempt still leaked every visitor's IP to a third party. -
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 sendX-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 anhttps:URL whose origin the backend returned in the payment response (backend allowlist, per theirsafeHttpsUrl()); 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 · landed via the
@marketplaces/paymentmigration The legacy payment surface onApiServicewas deleted wholesale in that work.grep -ri "authorization-key\|userid-value\|web-97ec" src/now returns nothing. Keep FH-3.5 (bundle secret scan) to stop it coming back. Was:api.service.ts:675setauthorization-keyanduserid-valueheaders client-side, andapi.service.ts:143shipped a partner ID literal in the bundle. Their audit's most serious finding, and it was correct. -
FH-1.4 — Send
Idempotency-Keyon payment creation · S · Lane A Zeroidempot*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 existingcheckout-idempotent-click.spec.tsasserts 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 backend/BACKEND-INTEGRATION.md, so it becomes a delivery gate rather than a wish.
-
FH-2.1 — Conditional-UPDATE stock reservation · S ·
backend/BACKEND-INTEGRATION.mdWritten 2026-08-21: PHASE-3 §3.1 — the conditionalUPDATE … WHERE (available - reserved) >= qty RETURNING id, 409 on zero rows, whole-cart rollback, 15 min TTL.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 ·
backend/BACKEND-INTEGRATION.mdWritten 2026-08-21: PHASE-7 §5 — unique constraints onpayment.idempotency_keyand(provider, event_key), insert-first webhook handling,sha256(rawBody)fallback key, signature over the raw body, 24 h poll as reconciliation.Payment.idempotencyKey UNIQUE; a key reused against a different order/marketplace →409.PaymentWebhookEvent @@unique([provider, eventKey]); duplicate insert →{accepted: true, duplicate: true}.eventKeyfalls back tosha256(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 ·
backend/BACKEND-INTEGRATION.mdWritten 2026-08-21: TRACK-S §2.1 — 32 random bytes stored as SHA-256 only, HttpOnly/Secure/SameSite, one cookie per contour, Argon2id params, mandatory TOTP with a single-use enrolment token, password change revokes all sessions in-transaction. 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). Argon2idmemoryCost 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 weightsORDER_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 ·
backend/BACKEND-INTEGRATION.mdWritten 2026-08-21: TRACK-S §2.2 — origin allowlist ahead of routing on every admin/platform/manager mutation, same list for CORS. Global hook: any non-GET on an admin/manager path whoseOriginis not in the configured allowlist →403. CORS uses the same allowlist withcredentials: true. Acceptance: a cross-origin POST with a valid session cookie is refused. -
FH-2.5 — Tenant by verified Host only · S ·
backend/BACKEND-INTEGRATION.mdWritten 2026-08-21: PHASE-9 §6 — normalization specified,verifiedAtrequired, cache with explicit invalidation, proxy header trust, no public endpoint acceptsmarketplaceId. Normalize host (lowercase, strip trailing dot, strip port) → uniquehostnamerow → requireverifiedAtandACTIVE. Short cache with explicit invalidation. Unknown host →404, never a fallback tenant. The public API never accepts amarketplaceIdfrom 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-…Written 2026-08-21: PHASE-9 §5.3 — HMAC preview token, 15 min, HttpOnly cookie, every non-GET 404s while preview is active,noindex. HMAC-signed token carrying{marketplaceId, expiresAt, nonce}, 15-minute TTL,storefront_previewcookie. Global hook returns404 Preview mode is read-onlyfor 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-…Written 2026-08-21: PHASE-9 §5.1–5.2 —version = max+1unique per marketplace, materialized snapshot, pointer flipped in-transaction, rollback as a new revision, clone carry/no-carry list, inventory to zero, topological category walk.version = max(version) + 1, immutable snapshot row,publishedRevisionpointer 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 ·
backend/BACKEND-INTEGRATION.mdWritten 2026-08-21: PHASE-3 §3.2 —InventoryMovementappend-only with reason, reference, actor, andresultingAvailablewritten at the time. Every stock change writesreason,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-7Written 2026-08-21: TRACK-S §4.2 —v1.iv.tag.ciphertextAES-256-GCM envelope, per-value IV, decrypt only in-service, HMAC fingerprints for display, backend-built allowlisted redirect URLs. AES-256-GCM, versioned envelopev1.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 ·
backend/BACKEND-INTEGRATION.mdWritten 2026-08-21: PHASE-10 §3a — server re-runs the editor rules, clamp-and-fallback ergonomics, structural violations 400, limits published as one schema, referential checks as publish blockers. 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/pathorhttps://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-…Written 2026-08-21: PHASE-3 §6a —FulfillmentMode,DigitalCodestates,valueHashunique per (marketplace, offer), codes revealed only when paid.FulfillmentMode: MANUAL | CODE_POOL.DigitalCodepool withAVAILABLE/RESERVED/ASSIGNED/REVOKED, encrypted value,valueHashunique per(marketplace, variant). Codes revealed only when the order isPAID/PROCESSING/FULFILLED. Acceptance: an unpaid order never returns a code. -
[~] FH-2.12 — Marketplace status machine · rejected 2026-08-21 — ours is better Theirs is
DRAFT → DOMAIN_PENDING → READY → ACTIVE → SUSPENDED. PHASE-9 §2 already carriesdraft → configured → content_ready → domains_planned → staging_live → qa_passed → production_ready → live → paused/archived, plus a lifecycle endpoint that must name the specific blocker preventing the next transition. Adopting theirs would be a downgrade. Recorded so it does not get raised again. -
FH-2.13 — Order public token, not sequential IDs · S ·
backend/BACKEND-INTEGRATION.mdWritten 2026-08-21: PHASE-2 §3.1 —publicToken≥24 random bytes for every customer-facing route, tenant-scoped lookup, snapshot completeness, snapshots never updated in place. Orders are addressed publicly by a randombase64urltoken. 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-…Written 2026-08-21: TRACK-S §8a — separate URL, shell, login and cookie; scope from membership rows not configuration; endpoints refuse rather than hide; PII masking and audited reveal. 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 — Bulk import: idempotency and rollback · S · done 2026-08-21 The validate-then-apply half already existed — PHASE-3 §6 has the preview of validation errors and a separate apply step, which is equivalent to their
dryRun. What was missing and is now written: the import is idempotent by SKU/external key so re-running a file updates rather than duplicates, a row-level error never publishes a partial result, and an applied import is rollback-able only while none of its products have appeared on a paid order.
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 · done 2026-08-21
maximumErroron the initial bundle lowered1.8MB → 1.6MBinangular.json, and again to1.1MBonce FH-3.4 landed. Measured today: 1.55 MB raw / 324.58 kB transfer — worse than the 1.15 MB they measured on 11 Aug, so this had been growing unwatched. The threshold is a ratchet, not the target: set just above today's size so the bundle cannot grow, with the 700 kB warning left in place as the goal. Lower it every time the number comes down. CI now runs the production build (npm run buildalready defaults to production). -
[~] FH-3.4 — Get the initial bundle down · L · 1.55 MB → 1.04 MB on 2026-08-21; not yet at target Premise corrected after measuring. Admin, editor, catalog, cart and the
en/hylocales are already lazy chunks — nothing admin-shaped ships to an anonymous visitor. The whole initial bundle ismainalone, so this was never a "split the deployables" job.Built with
--stats-jsonand read the esbuild metafile rather than guessing. Composition of the 1.5 MBmain:bytes what 495,831 @angular/compiler290,589 src/app/i18n/ru.ts162,915 @angular/core82,209 @angular/router53,354 @angular/common@angular/compiler— 33% of the bundle — was the JIT compiler, in an AOT production build.src/main.tsimported it explicitly, with a comment explaining that@marketplaces/authshipped plaintscoutput carrying no Ivy metadata, so Angular JIT-compiled its classes at runtime and bootstrap threw without it.That comment was stale. The package is
0.2.0, built with ng-packagr,module: dist/fesm2022/marketplaces-auth.mjs— proper Angular Package Format, partial-compiled (ɵɵngDeclareInjectable), linked at consumer build time. No JIT needed.Removed the import. Verified against the production bundle served statically, not just a successful build — the failure mode it guarded was a runtime throw, so a green build proves nothing. Angular 22.0.8 bootstrapped, the router resolved
/ru, and the app rendered its own "server unavailable" screen, which means DI, HttpClient and the whole interceptor chain ran. That chain injectsAuthServicefrom@marketplaces/auth— the exact class named in the old comment. Zero JIT/compiler errors; the only console output was the expected 404s from having no backend.Result: 1.55 MB → 1.04 MB raw, 323.58 kB → 215.45 kB transfer. Ratchet lowered
1.6MB → 1.1MB, which is now also the guard against the import being re-added.Remaining path to the 700 kB target. The next lever is
i18n/ru.tsat 290 kB — the default locale, eager, whileen/hyare lazy.TranslateServicealready has the loader plumbing andlanguageGuardalready awaits a preload before route activation, so makingrulazy is mechanically small. It is deliberately not done here: it adds a round-trip before first paint for the majority language, which is a product tradeoff rather than a cleanup. Needs a decision, then it is roughly a 750 kB bundle. Also outstanding:qrcode(23.7 kB), pulled in by@marketplaces/auth, is not ESM and causes an optimizer bailout — a fix for the package repo. -
FH-3.5 — Bundle secret scan in CI · S · done 2026-08-21
scripts/ci/scan-bundle.sh, wired asnpm run scan:bundleand a CI step after Build. Seven patterns: both provider auth headers, the partner ID shape,client_secret, private key blocks, AWS keys, Telegram bot tokens. Verified in both directions — clean against the realdist/, and fails with exit 1 against a planted credential.
Wave 4 — Identity: VK ID + Yandex ID (Lane C, @marketplaces/auth)
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.
The client half and the contract are done (2026-08-21). What remains is backend implementation, and registering the OAuth applications — which is what FH-0.1 gates.
-
FH-4.1 — Provider-agnostic social identity surface · M · done 2026-08-21 Landed as
social-identity-gateway.interface.ts/-api.gateway.ts/-local.gateway.ts/-gateway.token.tsundersrc/app/core/identity/services/, with the fourvk-id-*files deleted andvk-id-loginreplaced bysocial-login-buttontaking aproviderinput.'yandex_id'added toExternalIdentityProvider. Covered bysocial-identity-gateway.spec.ts(5 tests), which asserts the request carries nocode_verifierorclient_secret— so re-adding a browser-held verifier fails the build rather than passing review. CollapseVkIdGatewayintoSocialIdentityGateway: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'toExternalIdentityProviderincore/identity/models/customer-identity.model.ts. -
FH-4.2 — Move PKCE ownership to the backend · S · done 2026-08-21
completeCallback()is gone from the frontend entirely. PHASE-8 §2.1–2.2 rewritten:/authorizemints and stores{state, codeVerifier, marketplaceId, returnTo, expiresAt}single-use for 10 minutes,/callbackis a backend GET that exchanges, links, issues the session cookie and redirects.returnTovalidated against the tenant's own origin. TodaycompleteCallback(code, codeVerifier)forces the browser to generate and hold the verifier. We are a confidential client. Backend generatesstate+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. Updatebackend/BACKEND-INTEGRATION.md§2. -
[~] FH-4.3 —
ExternalIdentitymodel · S · Lane B · contract written 2026-08-21, awaiting backend PHASE-8 §1 and §2.3:UNIQUE (provider, providerUserId), conflict routes to controlled resolution rather than rebinding, optional email/phone/displayName, per-tenant OAuth app config under the Track S §4.2 envelope.@@unique([provider, providerUserId]) // one provider account -> one customerConflict is not an upsert: a
providerUserIdalready bound to a differentCustomerroutes 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 · contract written 2026-08-21, awaiting backend PHASE-8 §2.5 carries the full endpoint set and the
device_idtrap. OAuth 2.1, PKCE mandatory (S256). Authorizehttps://id.vk.com/authorize; tokenPOST https://id.vk.com/oauth2/auth; profilePOST https://id.vk.com/oauth2/user_info; logouthttps://id.vk.com/oauth2/logouton unlink. Trap to write into the contract: the callback returnsdevice_idalongsidecode, 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 onCustomer. -
[~] FH-4.5 — Yandex ID · S · contract written 2026-08-21, awaiting backend PHASE-8 §2.5. On the client it is a
providerinput, not new code. OAuth 2.0 with PKCE. Authorizehttps://oauth.yandex.ru/authorize; tokenPOST https://oauth.yandex.ru/tokenwith HTTP Basicclient_id:client_secret; profileGET https://login.yandex.ru/info?format=jsonwith headerAuthorization: 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 · client + contract done 2026-08-21; backend write path pending Client: the gateway now separates the two provider sets —SocialProvider(vk/yandex, has an OAuth authorize) vsExternalIdentityProvider(addstelegram/max, listable and unlinkable).unlink()takes the wider type, so the linking UI detaches Telegram through the same path as VK. The dev local gateway seeds a Telegram identity so the surface is exercisable now. Contract: PHASE-8 §2.6 — Telegram login writes anExternalIdentityrow under the same uniqueness/conflict rule as VK, appears in/me/identities, is removable subject to the last-identity409, and — the audit finding — keeps customer (marketplace_session) and admin (bo_session) sessions as separate cookies so a Telegram customer never satisfies an admin guard. Identity row vs messagingBotConversationBindingkept distinct. Backend still owns: the actual write-on-login and the session split enforcement. Telegram login itself lives in@marketplaces/auth. -
FH-4.7 — Account linking UI · M · done 2026-08-21
AccountIdentitiesComponent(src/app/features/website/account/identities/): lists linked identities fromGET /me/identities, offers attach buttons only for OAuth providers not yet linked (reusingSocialLoginButtonComponent), detaches throughunlink(), disables the detach control on the last remaining identity with an explanatory title, and has a slot for the §2.3 conflict message. Loading / error / ready states, error surfaced rather than shown as an empty account. 6 unit tests. Not yet wired into a route — the storefront has no customer account area and no live OAuth app (FH-0.1) — but fully built and tested behind that. -
FH-4.8 — Email/phone OTP repositioned as recovery · S · done 2026-08-21 PHASE-8 §3 now states it explicitly: OTP is a way back in when a linked messenger is unreachable and a second factor a customer may add, never the front-and-centre first login option, and one more
ExternalIdentity/ContactMethodon the same customer rather than a parallel account. The email/phone spec stays valid; only its priority relative to VK ID moves.
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-privilegeson every service. Makes it a property of the topology rather than a firewall promise. -
FH-D.3 — Host hardening we lack · M · done 2026-08-21 Three drop-in files in
scripts/deploy/server-setup.sh, documented in DEPLOYMENT.md §3.2: sshd hardening (password and keyboard-interactive auth off, root key-only,MaxAuthTries 3, 30 s grace, no forwarding), fail2ban (sshd,nginx-http-auth,nginx-bad-request; 5 failures in 10 min, 1 h ban), and sysctl (no redirects or source routing, rp_filter, SYN cookies, forwarding off, restricted kernel pointers and dmesg). The script runssshd -tbefore reloading and removes its own drop-in if the test fails — a bad sshd config taking effect remotely is how people lock themselves out permanently. Scoped sudoers was already in place. Kept ours where ours is better:add-domain.shpre-checks the DNS A record and runsnginx -tbefore and after; ufw was already configured. Their hardcoded server IP deliberately not copied.
Continuous — Process (Lane E)
-
FH-E.1 — Adopt the nine invariants as an acceptance gate · S · done 2026-08-21 Now
backend/BACKEND-INTEGRATION.md§0, ahead of everything else, each one cross-referenced to the contract section that specifies it. Framed as a release gate: violate one and it does not ship, regardless of what else is finished. -
FH-E.2 — PR policy · S · done 2026-08-21
backend/BACKEND-INTEGRATION.md§0a, with the expand/contract migration rule alongside it. -
FH-E.3 — Release discipline · S · done 2026-08-21
backend/BACKEND-INTEGRATION.md§0a. A release records version, migrations, healthcheck, smoke, dependency audit, and the rollback path actually available. -
FH-E.4 — ADR for the harvest · S · done 2026-08-21 ADR-0006. Records what we take, what we reject, what we keep because ours is better, and the one organizational question it deliberately does not settle.
-
FH-E.6 — Keep mock gateways out of production builds · M · done 2026-08-21 21 token factories now
inject(XApiGateway)unconditionally; mock overrides moved tosrc/app/mock-gateway.providers.ts, swapped for a production copy that imports nothing viafileReplacements. Zero*LocalGatewayclasses and zero fixtures in the production bundle, down from 21 classes and 75 kB of source.scan-bundle.shgained two patterns so it cannot return, verified in both directions. Dev behaviour unchanged — flipuseMockDatainenvironment.tsas before. Worth noting for whoever picks up FH-E.5:useMockDataisfalsein both environment files, so none of these mocks were ever the selected implementation. They were pure weight. Not fixed here:MediaRepositoryis still bound toMockMediaRepositoryunconditionally inapp.config.ts. That one cannot simply be deleted — no real implementation exists — so it is a missing API gateway, not dead weight. Measured 2026-08-21: mock seed data reaches the production bundle.ptr_local, a fixture literal frompartner-hierarchy-local.gateway.ts, is present in a built lazy chunk. Cause: 21 DI tokens usefactory: () => (environment.useMockData ? inject(XLocalGateway) : inject(XApiGateway)), and referencing both branches keeps both classes reachable, so the optimizer cannot drop the mock. 75 kB of local-gateway source, plus its fixtures, ships to users. This is the concrete form of their strongest objection — "mock repositories as production implementation" — and it is mechanical to fix. The pattern to copy is already in this repo:mock-data.interceptor.production.tsswapped in viafileReplacements. Done when:scan-bundle.shcan gate on mock fixture markers and pass. -
FH-E.5 — Reduce
localStorageto cache, never truth · M · audited + gap marked 2026-08-21 Audited all 21localStorageusers. The premise — "localStorage is your source of truth" — turned out already false across the app:- Every admin facade (products, categories, orders, moderation, dashboard) uses
localStoragefor view preferences only —viewMode,density,visibleColumns,expandedIds,sort. Entity CRUD goes through the API gateways. That is cache, not truth. currency-rates.service.tsalready removed its localStorage-typed rates (its own comment records it).language,locationregion,search-history,api-headersanonymous session id,admin-preferences— all legitimate preference/cache.- The editor already surfaces an "unsaved local draft restored" banner (
draftRestored→ save bar), which is the recovery-cache indicator this item asked for.
One real gap, and it is backend-blocked: the project editor's
publish()applies config to the in-memory runtime and saves the draft to localStorage, then declares itself published — no server round-trip, because the PHASE-9 §5 revision API does not exist yet. Marked precisely inpublish()with the required behaviour (await the server, only then mark published) and cross-referenced to the contract. Cannot be finished on the frontend alone; the contract for the fix is already written.Net: nothing to rip out — the codebase was already at the target state everywhere the backend exists to support it.
- Every admin facade (products, categories, orders, moderation, dashboard) uses
Scoreboard
| Wave | Done | Contract written, awaiting backend | Open | Blocked by |
|---|---|---|---|---|
| 0 — Decide | 0 | — | 2 | needs a person, not a session |
| 1 — Live defects | 2 | — | 2 | FH-1.2 / FH-1.4 sit in files another session owns |
| 2 — Contracts | 14 | — | 0 | 1 rejected (FH-2.12) |
| 3 — Proof | 3 | 1 | 1 | see note below |
| 4 — Identity | 4 | 3 | 1 | OAuth apps, which FH-0.1 gates |
| Ops | 1 | — | 2 | — |
| Process | 6 | — | 0 | — |
| Total | 30 | 4 | 8 | 1 rejected |
Landed 2026-08-21
- Wave 1 — FH-1.1 (geo off
ip-api.com, 4 new tests), FH-1.3 (credentials out of the browser, via the@marketplaces/paymentmigration). - Wave 2 — all 14 remaining contract items written into
docs/backend/, taggedFH-*and dated so each traces back to the analysis. FH-2.12 rejected on the merits: our lifecycle state machine is richer than theirs. - Wave 3 — FH-3.3 (bundle budget ratcheted to a blocking error at 1.6 MB, measured 1.55 MB), FH-3.5 (
scripts/ci/scan-bundle.sh, in CI, verified in both directions). - Wave 4 — FH-4.1 and FH-4.2 complete on the client and in the contract; FH-4.3–4.5 specified and waiting on backend plus registered OAuth applications.
- Process — FH-E.1–E.4, including ADR-0006.
Test count over the session: 247 -> 256 (+4 geo, +5 social identity). Initial bundle 1.55 MB -> 1.04 MB. Build green, boundary and cycle checks green.
On FH-3.1 / FH-3.2 — reclassified, not skipped
Both are backend races: two transactions competing for the last unit, and the same provider event arriving twice. Playwright against mocked routes cannot prove either — a test that mocks both sides of a race proves only that the mock behaved. checkout-idempotent-click.spec.ts already says this in its own header and covers the genuinely frontend-testable half.
So the acceptance criteria now live where they bind, as normative text in PHASE-3 §3.1 and PHASE-7 §5, and the e2e work they imply is backend integration testing, not frontend e2e. What is worth doing on our side first: the existing checkout e2e specs have a known-failing session setup (documented in-file, dated 2026-08-21) — a green suite is the prerequisite for anything built on top of it.
Next
- FH-0.1 — the central identity host, and the one-customer-or-two question. One-way door, gates the remaining Wave 4 work, needs a decision from a person.
- FH-1.2 / FH-1.4 — bank URL validation and
Idempotency-Key. Both live incart.component.ts/ the payment package; pick up once that work settles. - FH-E.6 — mock fixtures reach production chunks. The fix is mechanical but touches 21 DI token files plus
app.config.ts, which another session currently owns — deliberately deferred rather than merged into a busy tree. Plan: move mock selection out of the token factories into one dev-only provider array swapped byfileReplacements, the same mechanismmock-data.interceptor.production.tsalready uses. - Fix the e2e session setup, then revisit what proof is worth adding.