651 Commits

Author SHA1 Message Date
sdarbinyan
c2a56571af feat(bootstrap): fall back to built-in placeholder when marketplace unpublished
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
Adds published: boolean to the bootstrap wire contract. ConfigService
swaps to a new DEFAULT_BOOTSTRAP constant (all feature flags on, generic
branding/theme/pages) whenever the backend reports published: false, so
an unpublished marketplace renders a working demo instead of a blank or
broken page. Missing published field stays backward compatible (treated
as true). Documents the brand bootstrap wire shape for backend/ops use.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 22:02:42 +04:00
sdarbinyan
55634b3b57 Merge improvements/fork-harvest into main
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
Fork-harvest brings: the ip-api.com geo fix, credential bundle scan,
mock gateways out of production, JIT compiler dropped (1.55->1.04 MB),
host hardening, provider-agnostic identity + VK/Yandex + account linking,
and the backend contracts consolidated into one BACKEND-INTEGRATION.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

# Conflicts:
#	docs/backend/BACKEND-HANDOFF.md
#	docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md
2026-08-22 16:19:55 +04:00
sdarbinyan
d44565fae9 docs(backend): consolidate all backend contracts into one file
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
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>
2026-08-22 16:15:58 +04:00
sdarbinyan
846004e6d8 ci(deploy): make API-domain reconciliation opt-in, document the real host layout
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
The Reconcile tenant API domains step ran on every push to main. On the
production host that is actively harmful: api.gorbushka.market already has
a hand-written vhost, and configure-api-domain.sh writes its own file per
domain - so the step would hand nginx a second server block for a
server_name that already has one and re-run certbot against a live API,
once per deploy. Shipping frontend files needs none of it. Gate it behind
a workflow_dispatch input, off by default, for standing up a NEW base
domain.

This also shrinks the secrets a normal deploy requires to four
(DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY, DEPLOY_KNOWN_HOSTS);
STOREFRONT_DOMAINS, CERTBOT_EMAIL and BACKEND_UPSTREAM are now read only
on the opt-in path.

Document the production host as it actually is: provisioned by hand before
server-setup.sh existed, per-domain vhosts rooted at
/var/www/dexarmarket/browser, which is now a symlink to
/srv/marketplaces/current/frontend. Before 2026-08-22 it pointed straight
at a pinned release with no `current` in between, so releases 14d46ce and
98c39f6 uploaded successfully and were never served.
2026-08-22 16:08:13 +04:00
sdarbinyan
52fb52888f docs(backend): consolidated harvest requirements as one buildable file
HARVEST-BACKEND-REQUIREMENTS.md - single index the backend builds the
fork harvest from, so the FH-* mechanisms are not scattered across nine
phase contracts:

- the nine release invariants (the acceptance gate)
- every FH-* requirement with its exact mechanism and phase-contract
  reference, grouped by area (correctness core, sessions/access,
  tenancy/publish/content, identity, operations)
- 15 acceptance tests mapped to the invariant each guards
- build order, and what the frontend already delivered so the backend
  builds to a known target rather than guessing

Linked from the backend README index.

No implementation changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 12:09:38 +04:00
sdarbinyan
d4959bd4da docs(e2e): clear the stale known-issue markers, root cause found
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
checkout-request-shape.spec.ts and checkout-idempotent-click.spec.ts were
flagged known-failing pending investigation; dda0a3d found and fixed the
actual cause (circular DI in apiHeadersInterceptor). Update the comments
and README so they no longer point at an unresolved mystery.
2026-08-21 22:45:47 +04:00
sdarbinyan
dda0a3d2df fix(auth): break circular DI that logged every returning session out
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
apiHeadersInterceptor injected @marketplaces/auth's AuthService to attach
a WebSessionID header. AuthService's own constructor makes a synchronous
GET /users/sessions/:id call to verify a persisted session, which runs
through this exact interceptor - Angular throws NG0200 (circular
dependency) mid-construction, silently swallowed by the package's
catchError(() => of(null)), read as "session invalid," and the cookie
gets cleared on every single page load. This is what was gating the
architecture-governance e2e job on Gitea (3 checkout tests failing on a
disabled QR button). Session-check requests are the identity mechanism
itself and never needed that header - skip AuthService injection for
them instead.

Also fixes mock-data.interceptor's session-check mock, which required
3 polls before reporting an id active with no way to represent a
returning session with an already-valid cookie - not the actual trigger
for this bug (useMockData is false in the dev config CI uses), but a
real gap in the mock's fidelity worth closing while in this file.
2026-08-21 22:44:04 +04:00
sdarbinyan
b4772d10c7 docs(editor): mark editor publish as backend-blocked local-only (FH-E.5)
Audited all 21 localStorage users against the "localStorage as source of
truth" objection. It was already false almost everywhere:

- Every admin facade (products, categories, orders, moderation,
  dashboard) uses localStorage only for view preferences - viewMode,
  density, visibleColumns, expandedIds, sort. Entity CRUD goes through
  the API gateways.
- currency-rates.service already removed its localStorage-typed rates.
- language, location region, search history, the anonymous session id,
  admin preferences - all legitimate cache/preference.
- The editor already shows an "unsaved local draft restored" banner
  (draftRestored -> save bar), which is the recovery-cache indicator this
  item called for.

One real gap remains and it is backend-blocked: project-editor
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
in publish() with the required behaviour (await the server, only then
mark published) and cross-referenced to the contract.

Nothing to rip out: the codebase was already at the target state
everywhere the backend exists to support it.

262 tests pass. Boundaries green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 22:41:14 +04:00
sdarbinyan
1c87a53f02 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
sdarbinyan
771dce9e29 perf(build): drop the JIT compiler from production, 1.55 MB -> 1.04 MB (FH-3.4)
Built with --stats-json and read the esbuild metafile instead of guessing.
@angular/compiler was 495,831 bytes of a 1.5 MB main chunk - the JIT
compiler, in an AOT production build, 33% of everything an anonymous
visitor downloads.

src/main.ts imported it deliberately, with a comment explaining that
@marketplaces/auth shipped plain tsc output 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. Nothing needs JIT.

Verified against the production bundle served statically, not just a
green build: the failure mode this guarded was a runtime throw, so a
successful compile proves nothing. Angular 22.0.8 bootstrapped, the
router resolved /ru, and the app rendered its own "server unavailable"
screen - meaning DI, HttpClient and the full interceptor chain ran. That
chain injects AuthService from @marketplaces/auth, the exact class the
old comment named. Zero JIT or compiler errors; the only console output
was the expected 404s from having no backend behind a static server.

Initial bundle 1.55 MB -> 1.04 MB raw, 323.58 kB -> 215.45 kB transfer.
Budget ratchet lowered 1.6MB -> 1.1MB, which is now also what stops the
import being re-added.

Next lever, deliberately not taken here: i18n/ru.ts is 290 kB, eager,
while en/hy are already lazy. TranslateService has the loader plumbing
and languageGuard already awaits a preload, so it is mechanically small -
but it adds a round-trip before first paint for the majority language,
which is a product tradeoff, not a cleanup. Roughly a 750 kB bundle once
someone decides.

256 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 16:10:34 +04:00
sdarbinyan
a35953d90f refactor(di): keep mock gateways out of production builds (FH-E.6)
21 DI tokens selected their implementation like this:

  factory: () => (environment.useMockData ? inject(XLocal) : inject(XApi))

That reads as a toggle and is not one. Naming both classes in the factory
keeps both reachable, so every mock shipped regardless of the flag - and
`useMockData` is false in both environment files, so none of them were
ever the selected implementation in the first place. Verified: a fixture
string from partner-hierarchy-local.gateway.ts was present in a
production bundle.

Token factories now inject the API gateway unconditionally. Mock
overrides move to src/app/mock-gateway.providers.ts, swapped for a
production copy that imports nothing, via the same fileReplacements
mechanism mock-data.interceptor.production.ts already uses. Dev behaviour
is unchanged - flip useMockData in environment.ts exactly as before.

useExisting rather than useClass: the local gateways are already
providedIn: 'root' singletons, and an app-level provider for the token
wins over its tree-shakable default.

scan-bundle.sh gains two patterns so this cannot come back: any
*LocalGateway class name, and known fixture literals. Verified in both
directions - clean against the real dist, exit 1 against a planted
OfferLocalGateway.

Result: zero LocalGateway classes and zero fixtures in the production
bundle, down from 21 classes and 75 kB of source. Initial bundle is
unchanged at 1.55 MB because these all sat in lazy chunks; the win is
that production can no longer serve seeded fixtures as real data, not
bytes off the critical path.

Not addressed here: MediaRepository is still bound to MockMediaRepository
unconditionally in app.config.ts. That one cannot be deleted - no real
implementation exists yet - so it is a missing API gateway, not dead
weight. Tracked separately.

256 tests pass. Build green, boundaries and cycles green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 15:59:11 +04:00
sdarbinyan
885f4d1299 Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
2026-08-21 13:33:45 +04:00
sdarbinyan
a3808842c1 feat(deploy): host hardening on the frontend server (FH-D.3)
server-setup.sh configured ufw and stopped there, which leaves SSH open
to unlimited password guessing and the kernel on defaults that are wrong
for an internet-facing host.

Adds three drop-in files, so a re-run replaces its own config and never
edits a distro file in place:

  /etc/ssh/sshd_config.d/10-marketplaces-hardening.conf
      password and keyboard-interactive auth off, root key-only,
      no agent/X11 forwarding, MaxAuthTries 3, 30s login grace
  /etc/fail2ban/jail.d/marketplaces.local
      sshd, nginx-http-auth, nginx-bad-request; 5 in 10m, 1h ban
  /etc/sysctl.d/99-marketplaces-hardening.conf
      no redirects or source routing, rp_filter, SYN cookies,
      forwarding off, restricted kernel pointers and dmesg

Both accounts on the host are key-only by construction - the deploy user
is created with no password at all - so disabling password auth cannot
lock anyone out. It only closes guessing against a credential nobody
intended to exist.

The sshd block runs `sshd -t` first and removes its own drop-in if the
test fails. A bad sshd config that takes effect on a remote box is how
people lock themselves out permanently.

DEPLOYMENT.md §3.2 documents all three plus the post-provision checks.

Not copied from the reference implementation: its hardcoded server IP.
Kept as-is because ours is already better: add-domain.sh pre-checks the
DNS A record and runs nginx -t before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:17:02 +04:00
sdarbinyan
cf17b0b6c6 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
sdarbinyan
f9e09b1757 docs(backend): harvest platform mechanisms into the contracts (Wave 2, FH-E.1-E.4)
Writes the 14 harvested mechanisms from FORK-ANALYSIS-2026-08-21.md into
the backend contracts. Each section is dated 2026-08-21 and tagged FH-*
so any wording traces back to why it is worded that way.

The through-line: several contracts stated correctness as behaviour
("the webhook must be idempotent"). Behaviour written as an if-statement
gets deleted by a refactor and the failure mode is a double charge. These
sections restate it as schema and mechanism.

PHASE-3  3.1 conditional-write reservation, 409 on zero rows, cart-wide
             rollback, 15 min TTL
         3.2 InventoryMovement append-only journal with resultingAvailable
         6   bulk import idempotent by SKU, rollback while unsold
         6a  digital code pools, revealed only when paid
PHASE-7  5   unique constraints for payment idempotency and webhook
             replay, insert-first handling, signature over raw body,
             24h poll as reconciliation not primary
TRACK-S  2.1 session model - 32 bytes stored as SHA-256 only, HttpOnly,
             one cookie per contour, Argon2id params, mandatory TOTP
         2.2 origin allowlist ahead of routing on every cookie mutation
         4.2 AES-256-GCM envelope for stored secrets, HMAC fingerprints
         8a  order manager as a separate contour, scoped by membership
             rows rather than by configuration
PHASE-9  5.1 revision immutability, version = max+1, pointer flipped
             in-transaction, operational state does not travel
         5.2 clone carry / no-carry list, inventory to zero
         5.3 signed read-only preview, non-GET 404s while previewing
         6   host normalization, verifiedAt required, cache invalidation
PHASE-10 3a  server re-runs the editor's validation, clamp-and-fallback
PHASE-2  3.1 order publicToken, snapshot completeness, never updated

FH-2.12 rejected on the merits: our marketplace lifecycle state machine
is richer than theirs, adopting it would be a downgrade. Recorded in the
TODO so it is not raised again.

Also adds BACKEND-HANDOFF.md sections 0 and 0a - nine falsifiable
invariants as a release gate, each cross-referenced to the contract that
specifies it, plus PR and release discipline. And ADR-0006 recording what
we take, what we reject, what we keep because ours is better, and the
organizational question it deliberately does not settle.

No implementation changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:12:05 +04:00
sdarbinyan
e8fc8480fe docs: trim stale instructions from completed harvest items
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:37:10 +04:00
sdarbinyan
6e47d01c32 ci: ratchet the bundle budget and scan builds for credentials (FH-3.3, FH-3.5)
Bundle budget was warning-only: initial warning 700 kB, error 1.8 MB.
Measured today the initial bundle is 1.55 MB raw / 324.58 kB transfer -
up from the 1.15 MB measured on 11 August, so it had been growing with
nothing to stop it.

Lowers maximumError to 1.6 MB. That is a ratchet, not a target: just
above today's size so the bundle cannot grow, with the 700 kB warning
left in place as the goal. Lower it each time the number comes down.

Adds scripts/ci/scan-bundle.sh (npm run scan:bundle), run in CI after
the build. Seven patterns: both provider auth headers, the partner ID
shape, client_secret, private key blocks, AWS keys, Telegram bot
tokens. The legacy payment code that put credentials in the browser is
already deleted; this is what stops it coming back. Verified in both
directions - clean against the real dist, exit 1 against a planted
credential.

Measurement also corrected two assumptions recorded in the harvest
TODO: admin and editor code is already lazy-loaded, so the initial
bundle is main alone rather than a deployable-split problem; and mock
gateway fixtures do reach production chunks, which is now filed as
FH-E.6 with the cause identified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:36:37 +04:00
sdarbinyan
04272ae673 fix(geo): stop calling ip-api.com from the browser (FH-1.1)
detectLocation() fetched http://ip-api.com over plaintext from an HTTPS
storefront. Browsers block mixed active content, so the request never
completed and auto-detect only ever took its error branch - region
detection has been dead in production, not merely insecure. The attempt
also handed every visitor's IP to a third party from the page itself.

Geo now resolves through the tenant API at {baseUrl}/geo/resolve, the
same base /regions already uses. The server reads the client IP; the
browser sends nothing and receives no third-party payload.

The endpoint is specified in BACKEND-API-REFERENCE.md and is not built
yet. Until it ships the client falls back to the manual region picker -
identical to the behaviour production already had.

Adds location.service.spec.ts: geo goes to the tenant API, no request
leaves that origin or uses http://, failure degrades to the manual
picker, and detection is not retried once attempted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:30:27 +04:00
sdarbinyan
bc5f7c7a64 refactor: delete dead legacy payment code from ApiService
Verified zero remaining callers for each before deleting (grepped
src/app for every method name individually), not assumed from the
earlier commit's dead-code note.

Deleted from api.service.ts:
- createPayment() - legacy direct QR creation (POST {qrBaseUrl}/qr)
- createCartPayment() - legacy /cart payment creation, client-sent amount
- createPaymentIntent() - superseded by @marketplaces/payment's gateway
- checkCartPaymentStatus(), checkCartCardPaymentStatus(), checkPaymentStatus()
  - legacy QR/card status polls, superseded by the same gateway
- resolvePaymentQrId/resolvePaymentQrUrl/resolvePaymentLink/
  resolveBankPaymentUrl - QrCreateResponse field-normalization helpers,
  no longer had a caller once the methods above were gone
- Types: QrCreateRequest, QrCreateResponse, CartPaymentRequest,
  PaymentIntentRequest, QrDynamicStatusResponse
- Fields: qrBaseUrl, cartPaymentPartnerId - no longer read by anything
- Imports: HttpHeaders, environment - no longer used in this file

Did NOT touch createOrder() or createCheckoutSession() - both still have
live callers in cart.component.ts, confirmed before deciding what to keep.

cart.component.ts: corrected the createPaymentIntent() comment, which
referenced the deleted method/helper names, to name what actually got
deleted instead of what was merely "dead as of that commit."

docs/backend/FRONTEND-API-SURFACE-COMPLETE.md: moved the 4 now-deleted
QR/card endpoints out of the "still live" legacy table into a dated removal
note - the doc's own premise is "every endpoint this codebase currently
calls," so it was wrong to leave them listed as called once they weren't.
Legacy-undocumented count corrected 15->11, total 97->93.

Verified: production build succeeds (no dangling references), 247/247 unit
tests, arch:check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:27:59 +04:00
sdarbinyan
d27c10dd17 feat: start @marketplaces/payment implementation
package.json already had @marketplaces/payment added (uncommitted) when this
started. Wired it in.

- app.config.ts: provideMarketplacesPayment(). apiUrl is environment.qrApiUrl
  with its trailing /api stripped - found and fixed a real bug while wiring
  this: qrApiUrl already ends in /api, and the package's default
  paymentsPath is '/api/v1/payments', so passing qrApiUrl unchanged would
  have silently doubled the path to .../api/api/v1/payments. Confirmed by
  reading the package's baseUrl() concatenation directly, not guessed.
  marketplaceDomain is a plain closure (not TenantResolverService) since
  provideMarketplacesPayment runs outside the injector.

- cart.component.ts: createPaymentIntent() and startPolling() now go through
  MARKETPLACES_PAYMENT_GATEWAY instead of api.service.ts's
  createPaymentIntent/checkCartPaymentStatus/checkCartCardPaymentStatus (our
  own earlier inferred contract, now superseded by the package's real,
  published one - POST/GET {qrApiUrl}/api/v1/payments). Deliberately did NOT
  swap to the package's own <mp-payment> UI component - that has a different
  UX paradigm entirely (window.open for redirects instead of an iframe
  popup, client-side QR generation instead of an external image service) and
  replacing the existing, already-tested 769-line popup state machine
  wholesale is a separate, much larger change than "wire the new package
  in." Only the I/O layer moved; the surrounding state machine (paymentStatus,
  checkoutInFlight, timeout/success/error handling, bank-iframe UX) is
  untouched.

  Response shape differs from the legacy provider: the package's
  PaymentStatus is a fixed union (created/pending/authorized/paid/failed/
  cancelled/expired), not a free-form string+code pair - simplified the
  status-check conditionals accordingly and added 'authorized' as a second
  success state (PaymentResult's own status union), which the legacy check
  didn't have. The package also carries no TTL/expiry field on its response,
  unlike the legacy provider's qrTTL - polling duration now falls back to
  PAYMENT_MIN_POLL_SECONDS alone; flagged in a comment.

- api.service.ts's createPaymentIntent and its QrCreateResponse-based
  resolvePaymentQrId/resolvePaymentQrUrl/resolvePaymentLink/
  resolveBankPaymentUrl helpers are now dead code. Left in place rather than
  deleted in the same pass that adds a new external dependency, so a revert
  doesn't also need to resurrect deleted code.

Verified: production build succeeds, 247/247 unit tests, arch:check clean.

E2E: 2 of 7 tests currently fail
(checkout-request-shape.spec.ts, checkout-idempotent-click.spec.ts), and
this is disclosed honestly rather than hidden. Root cause, confirmed by
tracing real network requests: the customer-session cookie fake these tests
rely on stops working somewhere between the cookie being demonstrably
present in the browser (context.cookies(), and document.cookie read from a
plain page on the same origin) and Angular's own AuthService reading it -
the session-check request never fires at all. This reproduces with or
without this session's payment changes (checkout-idempotent-click.spec.ts
doesn't touch payment creation and fails the same way), so it is not a
regression introduced here, but it is unresolved. Tried switching
context.addCookies from {domain,path} to {url} form (the standard fix for
this class of Playwright cookie issue) - did not fix it, kept anyway as the
more correct form. Documented as a known, unresolved issue directly in both
spec files and e2e/README.md rather than deleting or silently marking the
tests skip - the request-shape assertions those tests make are still
correct, they are just currently unverifiable through this harness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:37:16 +04:00
sdarbinyan
fd3ca85929 docs: fork analysis + improvement harvest spec and todo
Deep analysis of marketplaces-main.zip (hub.numus.cc/numus/marketplaces).

Findings:
- Not a fork of us. Separate platform monorepo (NestJS + Postgres +
  2 Angular apps + infra) sharing an older dexarmarket ancestor.
- Our repo is vendored inside it as reference/parallel-frontend/,
  SHA-256 pinned, dated 11 Aug 2026, classified "reference, not production".
- Zero VK/Yandex/OAuth code anywhere in their source. Their only
  customer login is Telegram, proxied to an external service.
- They lead on backend truth and ops; we lead on frontend depth,
  tests, e2e, and framework currency.

Three of their audit findings are still live in our code and are
defects, not just posture: plaintext ip-api.com call from an HTTPS
origin (mixed content, region detect silently dead), unvalidated
bypassSecurityTrustResourceUrl on a bank URL rendered in an iframe,
and provider credentials plus a partner ID literal in the bundle.

Adds:
- docs/FORK-ANALYSIS-2026-08-21.md - full comparison, their audit of
  us assessed line by line, and a VK ID + Yandex ID design.
- docs/superpowers/specs/2026-08-21-fork-harvest-design.md - working
  brief, five lanes, four waves, scope and rejection rules.
- docs/FORK-HARVEST-TODO.md - 42 items with effort, dependencies and
  acceptance criteria. Improvements only; nothing regresses our
  Angular version, test count, or architecture governance.

No implementation changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:31:54 +04:00
98c39f6844 fix(auth): clarify admin login flow
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
Use admin-specific Telegram copy and define the missing credential API. Replace predictable bootstrap passwords with random one-time secrets.
2026-08-21 07:39:30 +04:00
14d46ceaa6 fix(admin): isolate login on admin host
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
Allow runtime request headers through API preflight and keep the storefront shell hidden while admin QR authentication gates backoffice.
2026-08-20 20:15:21 +04:00
2e41e216c0 Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
2026-08-20 16:29:22 +04:00
92f1c884c9 fix(proxy): strip upstream browser origin
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
nginx owns the validated CORS response; the live :445 backend rejects requests when the browser Origin is forwarded.
2026-08-20 16:29:05 +04:00
bc74fa77d9 Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
2026-08-20 16:12:57 +04:00
9cd56586fb fix(api): share base-domain API host
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Tenant subdomains route through api.<base-domain>; nginx forwards the exact storefront host derived from the validated browser origin.
2026-08-20 16:12:37 +04:00
3c53a6a33e Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Failing after 6m33s
Deploy Frontend / deploy (push) Failing after 2m53s
2026-08-20 15:05:57 +04:00
e5949c3967 fix(deploy): provision tenant API domains
Some checks failed
Architecture Governance / architecture (push) Failing after 6m16s
Reconcile TLS, exact CORS, and backend proxying before release activation so every storefront uses its derived API host.
2026-08-20 15:04:10 +04:00
8e58ee85f0 Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
2026-08-20 14:54:44 +04:00
66a0ccfdb8 ci: restore standard deploy runner
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
2026-08-20 14:54:33 +04:00
6d25172a13 Merge branch 'B2B'
Some checks failed
Deploy Frontend / deploy (push) Failing after 2m35s
Architecture Governance / architecture (push) Has been cancelled
2026-08-20 14:50:09 +04:00
20e03d4340 ci: run Angular 22 on Node 24
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
2026-08-20 14:49:58 +04:00
4288e5cd44 Merge branch 'B2B'
Some checks failed
Deploy Frontend / deploy (push) Failing after 1m45s
Architecture Governance / architecture (push) Has been cancelled
2026-08-20 14:46:01 +04:00
03c750cae7 ci: target emergency deploy runner
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
2026-08-20 14:45:40 +04:00
e8c48043ed Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
2026-08-20 14:43:02 +04:00
358996cbf2 ci: stream releases over ssh
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
2026-08-20 14:42:29 +04:00
2602d0c838 Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
2026-08-20 14:31:53 +04:00
640360d63c fix(api): derive host from storefront domain
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Every storefront, including nested subdomains, uses its matching api.<hostname> endpoint.
2026-08-20 14:31:22 +04:00
3f550de6b2 Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
2026-08-20 14:24:08 +04:00
f4ea4c7af8 fix(api): route tenants through origin gateway
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
2026-08-20 14:23:12 +04:00
sdarbinyan
217ab37496 Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
2026-08-18 22:12:24 +04:00
sdarbinyan
bbf12cad33 docs: add §0 - auth + payment authorization was missing entirely
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Direct question caught a real gap: "everything is there? payment auth?"
The census only grepped src/app/ - auth moved into the external
@marketplaces/auth package this session, and its HTTP calls were never
captured. Seven real endpoints were silently absent from a doc that called
itself "complete":

- 3 Telegram QR/session endpoints (POST/GET/DELETE .../users/sessions) -
  live today, customer and admin login share them, which is exactly why
  every admin endpoint must independently verify authorization server-side
- 4 ed25519 admin challenge/response endpoints - specified in
  BACKEND-HANDOFF.md §3 and the package's own auth-api.model.ts, but not
  built server-side. Client shows backend-unavailable until they exist.

Added §0.3 stating plainly what actually connects auth to payment: there is
no separate payment login. Checkout, order pricing (§20), and partner
credentials (§16) each ride on whichever of the two sessions above is
active, or on the partner API's own separate signed-request auth (§6 of
that contract - unrelated to Telegram/ed25519, already built, not a gap).
The real payment gap is §1 (QR/card creation and polling, undocumented
anywhere), not auth.

Counts corrected: 51->54 specified, 90->97 total. Added item 0 to the
action list, ahead of everything else: the ed25519 endpoints are the single
most serious open issue named anywhere in docs/backend/, and every other
item on the list assumes a working admin session to authorize against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:12:22 +04:00
sdarbinyan
c06ae56d88 Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
2026-08-18 22:08:44 +04:00
sdarbinyan
2149e6435a docs: final backend handoff - revision endpoints, response-shape additions, close-out
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Updates FRONTEND-API-SURFACE-COMPLETE.md to cover everything built after it
was first generated - this is the final version for this pass, not a
snapshot mid-way through.

Added, none of which existed in the doc before:

- §18: the 4 marketplace revision endpoints (create draft/validate/
  publish/rollback) - built this session, missing from the census entirely.
  Flags the same draft/validated/preview/published ambiguity already
  documented in the model itself, so backend sees it without having to
  read source.
- §19: RoutingContext as an optional field addition on GET
  /api/admin/v2/orders/{id} - not a new endpoint, a response-shape ask.
- §20: order pricing-breakdown fields (unitPriceMinor, lineTotalMinor,
  priceSnapshotId, discountMinor, fxQuoteId, deliveryMinor) feeding the new
  total-formula panel. States plainly that the panel shows nothing rather
  than a wrong number while these are absent.
- §21: 8 optional dashboard-metrics fields (GMV, conversion, moderation
  queue, etc.) - explicitly labelled a genuine ask, not a confirmed
  contract, since no spec exists for this endpoint at all (§15).
- §22: states directly that the frontend's new double-click guard does not
  replace backend idempotency enforcement and was never meant to - closes
  one UI race, does nothing for a retried request or a duplicate webhook.

Counts updated (47->51 specified, 86->90 total; +3 response-shape asks that
aren't new endpoints). Closing section states the one item genuinely
blocked pending a live backend (F60, the full acceptance-path E2E) so
nobody mistakes "frontend backlog complete" for "nothing left to build."

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:08:41 +04:00
sdarbinyan
8cdafbe62a Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
2026-08-18 22:03:31 +04:00
sdarbinyan
de6bef8e9a ci: coverage floor + gate; CI never ran a single test before this (F64)
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Delivery plan Q9: "set a justified coverage floor and CI gate. Deliberately
unset today." Both parts of that were still true - karma.conf.js had no
check thresholds, and architecture-governance.yml built and checked
boundaries but never invoked `ng test` at all.

Floor set 5 points below the measured level right after this session's
facade-test pass (43.3%/29.0%/34.1%/43.7% statements/branches/functions/
lines): 40/25/30/40. A deliberate floor, not an aspiration - meant to be
ratcheted up as coverage grows, and to fail a PR that drops below it
rather than silently accept a lower number.

Verified the gate actually fails, not just logs a warning: set
statements to an impossible 99% locally, confirmed `npm run test:coverage`
exits 1 (my first attempt at this check was wrong - piping through `tail`
meant the $? I read back was tail's exit code, not npm's; fixed by
capturing it directly). Restored the real floor and confirmed a clean
exit 0 before committing.

CI changes: added the Setup Chrome + coverage-gated test step
karma.conf.js's CHROME_BIN needs (its fallback is a Windows path, useless
on ubuntu-latest), plus an E2E step. Structurally verified locally (no
tabs, step count, manual read-through) - a real GitHub Actions run on
Anthropic's infrastructure could not be executed from this session, so
this is not confirmed end-to-end the way the coverage gate itself was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:03:29 +04:00
sdarbinyan
9344f2702c Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
2026-08-18 21:57:12 +04:00
sdarbinyan
e5ed1c96e5 fix: checkout double-click created two sessions; F59/F62 E2E coverage
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
E2E found a real, pre-existing bug, not a test artifact: isCheckoutDisabled
only checked terms/auth/delivery-selection, never whether a checkout was
already in flight. A double-click (or any rapid repeat click) fired two
handler calls before showPaymentPopup's change detection had a chance to
cover the button, producing two separate POST /api/v2/storefront/checkout
requests for one click.

Fixed with checkoutInFlight, set synchronously at the top of checkout()
before anything async happens, checked in isCheckoutDisabled. Released in
both closePaymentPopup() (every retry/close path routes through it) and
setPaymentError() directly, since the popup can stay open to show an error
rather than closing - relying on only one of those would leave a failed
attempt unable to retry.

Track Q coverage (F59, F62):

- admin-dev-bypass.spec.ts - proves ?devBypassAdmin=true (already shipped
  in app.ts, gated by @marketplaces/auth's isDevMode() check at runtime)
  actually gets an E2E run into the admin shell without a Telegram login.
  This was the missing piece behind Q2's note that past "verified live"
  admin claims were code-inspection only.
- checkout-idempotent-click.spec.ts - the frontend-testable half of Q5
  ("repeat webhook and double-click create exactly one order"). The
  webhook-idempotency half is a backend contract
  (PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §6.3) this suite can't exercise
  without a live backend.

One own test bug fixed en route, not shipped: the idempotency test's first
draft waited on label[for="terms-checkbox"], which does not exist in the
markup (the checkbox and its text share a plain clickable wrapper, no
label/for). checkout-request-shape.spec.ts already had the correct fallback
(dispatchEvent('click') on the input directly) for exactly this reason -
this test just hadn't copied it.

Verified: 237/237 unit tests, arch:check clean, 7/7 E2E, production build
succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:57:09 +04:00
sdarbinyan
4247a7f83f Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
2026-08-18 21:44:10 +04:00