Skip to content

Migración AWS, mapa real, y fixes de KYC/CETES - #344

Open
ericmt-98 wants to merge 12 commits into
mainfrom
feat/map-real
Open

Migración AWS, mapa real, y fixes de KYC/CETES#344
ericmt-98 wants to merge 12 commits into
mainfrom
feat/map-real

Conversation

@ericmt-98

Copy link
Copy Markdown
Collaborator

Resumen

Tres frentes de trabajo, en commits atómicos separados:

Migración a AWS (1db550a, 02232b4)

  • trustProxy: 1 (evita evasión de rate limits vía X-Forwarded-For) y Node 22 en CI
  • TLS verify-full real contra RDS con el CA bundle embebido en la imagen — descubierto necesario en vivo, no solo como endurecimiento diferido

Mapa real (f706c7f..1e7faf9, más 2 fixes de feat/map-real post-merge)

  • Reemplaza el mapa PNG simulado por MapLibre GL con tiles reales (OpenFreeMap)
  • Fija maplibre-gl a v5 — la v6 rompe su Worker interno bajo el esquema https://localhost de Capacitor (verificado con DevTools remoto conectado al WebView)
  • Ubicación real de comercios: nuevo flujo en Ajustes con pin arrastrable, conectado al endpoint que ya existía sin usarse
  • Rate limit + redondeo de coordenadas en /merchants/available (privacidad — el endpoint era público sin límites)
  • Quita señales falsas (online: true hardcodeado) y limpia código muerto

Fixes de producto encontrados probando en dispositivo real

  • fix(cetes): el botón "¿Sin cripto?" mandaba a la red de agentes P2P en vez del ramp real de Etherfuse
  • fix(kyc): Etherfuse ahora exige email (antes opcional) y el flujo fallaba en silencio sin mostrar error — se agrega captura de email + manejo de errores real

Test plan

  • Backend y frontend compilan limpio (npm run build en ambos)
  • Verificado en AWS real (api.micopay.app): RDS conecta con verify-full, migraciones corren, health check en verde
  • APK instalado en dispositivo físico: mapa real renderiza con tiles y ubicación GPS correcta, agentes demo visibles
  • Flujo de KYC probado end-to-end contra el sandbox real de Etherfuse

Pendiente (no bloqueante, documentado en docs/PLAN_MAPA_REAL_2026-07.md §7)

  • Key de MapTiler (opcional, ya hay fallback funcional con OpenFreeMap)
  • Re-registrar webhooks de Etherfuse contra las URLs de AWS

🤖 Generated with Claude Code

ericmt-98 added 12 commits July 24, 2026 13:40
trustProxy: true trusted the leftmost X-Forwarded-For entry, which a
client behind the ALB can spoof to evade per-IP rate limits. Pin to a
single proxy hop instead.

Node 20 is EOL; CI now builds with Node 22 to match the production
container runtime.
Testing the ECS infra directly against RDS revealed pg-connection-string
now treats sslmode=require as an alias for verify-full, so the app
rejected RDS's cert as self-signed and looped through its 5 connect
retries before exiting. The plan had this as a deferred hardening step
(A5/Fase 9) but it's required now for the app to boot at all.

Downloads the RDS global CA bundle at build time and chmods it readable
by the non-root `node` user. DATABASE_URL in SSM updated separately to
sslmode=verify-full&sslrootcert=/app/rds-global-bundle.pem. Verified
against the real micopay-prod RDS instance via a standalone ECS task.
Brings in #320 (SAT/UIF compliance reporting + alert SLA) and #321
(Didit provider for the #314 tiered KYC gate) on top of the AWS
migration fixes (trustProxy, Node 22, RDS verify-full TLS).
/merchants/available already filters by merchant_available=true, so
every merchant returned is available by definition. The online field
on Offer/OfferConfirmData and the agentStatus badge in TradeConfirmation
were an invented signal (audit G2) — remove them instead of deriving a
fake always-true value.
/merchants/available was public, unauthenticated, with no rate limit,
and returned exact lat/lng — letting anyone scrape the full census of
merchant locations. Add a 30 req/min per-IP rate limiter and round the
publicly returned latitude/longitude to 3 decimals (~110m); distance_km
still uses the exact stored coordinates via the existing SQL Haversine.
Exact coordinates remain available to a counterparty only inside an
accepted trade.

Note: micopay/backend/package.json also carries pre-existing unrelated
script additions (test:trade-auth, test:refund, test:challenge) from
outside this change set — verified harmless/compatible, included
because they share the same file/hunk as the new test:discovery script.
Replace the simulated PNG map (MapSim, bounding-box-normalized fake
pins, user always centered, hardcoded "CDMX · ZONA CENTRO" / "Agentes
reales cercanos") with MapReal: real tiles, real GPS-centered user
position, real merchant coordinates, pan/zoom via MapLibre GL.

- useMerchantsAvailable now exposes userPosition in its success state
  (previously resolved lat/lng then discarded them).
- ExploreMap and DepositMap swapped to MapReal; MapSim marked
  @deprecated but kept (referenced elsewhere, removal is WP5).
- VITE_MAP_STYLE_URL added to .env.testnet/.env.mainnet (empty —
  pending a MapTiler key from Eric); MapReal falls back to the public
  MapLibre demo style + a small "dev map" notice until it's set.
- npm i maplibre-gl.
Backend already exposed PATCH /merchants/me/location, validated and
authenticated, but the frontend never called it — so no real merchant
could ever appear on the map (audit §3.3), only the 4 seed-demo ones.

- api.ts: updateMerchantLocation() + MerchantLocation type; MerchantConfig
  now types the latitude/longitude/address_text fields the backend's
  GET /merchants/me/config already returns.
- MapReal: new pickerMode/pickerPosition/onPickerPositionChange props for
  a single draggable pin, additive only — existing merchant/camera effect
  bails out early when pickerMode is set, non-picker behavior unchanged.
- MerchantSettings: new "Mi ubicación" section — CTA using useGeolocation
  to get a GPS fix, MapReal picker to drag-adjust, optional address text,
  save via updateMerchantLocation. Location kept in separate state from
  `form` (PUT /merchants/me/config has additionalProperties:false, so
  merging would break the existing rate/limits save).
- MerchantAvailabilityToggle: optional hasLocation prop drives a
  non-blocking warning when a merchant activates availability without a
  fixed location (soft gate per plan — does not block activation).
- i18n: new merchantSettings.location.* keys (es/en).
…WP5)

- Delete src/components/MapSim.tsx and public/map_bg.png (superseded
  by MapReal since WP1; no remaining consumers).
- Delete micopay/backend/src/seed.ts (orphaned script, unreferenced by
  index.ts or package.json; the real seed is seedDemoMerchants() in
  index.ts, untouched).
- docs/AUDIT_APK_MAPA_2026-07.md: mark G1/G2/G3/G6 and §3 (simulated
  render + missing location-capture pipeline) as resolved, referencing
  WP1-WP5 on this branch.
demotiles.maplibre.org only carries country-level geometry, so at the
street zooms fitBounds produces in a town the map rendered as an empty
background — first real-device test in Huatusco showed no map at all.
OpenFreeMap's liberty style has full OSM street data, needs no API key,
and permits production use. Compact attribution control added (OSM
license requires visible credit); the "dev map" notice is gone since
the fallback is no longer a dev-only style.
Real-device testing in Huatusco showed the map rendering nothing: no
tiles, no roads, blank canvas, despite the network layer and WebGL
context both working fine.

Root cause, confirmed via remote DevTools attached to the WebView:
maplibre-gl v6.0.0 (a fresh major with no patch releases yet) changed
its internal tile-parsing worker to an ES module. Under Capacitor's
https://localhost custom scheme, that worker's relative imports never
resolve — no Worker target even showed up in DevTools, no thrown
exception, but map.isStyleLoaded() stayed false forever and 'load'/
'idle' never fired. A plain non-module Worker roundtrip worked fine in
the same WebView, isolating the failure to v6's module-worker bundling
specifically, not Workers in general.

Fix: pin to maplibre-gl@5.24.0, the last major before the ESM-worker
rewrite, which uses a classic importScripts worker with no such
resolution issue. After downgrading, a Worker target appeared in
DevTools and 'load'/'idle' fired normally; verified visually via a
captured canvas screenshot with real street tiles.

Also swaps the fallback style from demotiles.maplibre.org (country
borders only — renders blank at street zoom, a second, independent gap
found during the same session) to OpenFreeMap's `liberty` style (full
OSM street data, no API key, production-safe). VITE_MAP_STYLE_URL is
effectively no longer required — MapTiler handoff from the plan is
now optional, not blocking.
…the cash-agent network

The "¿Sin cripto? Conecta tu banco vía SPEI" button on CETESScreen sent
users to /deposit — the P2P cash-agent discovery flow (DepositMap,
farmacia_guadalupe, etc.) — instead of the real Etherfuse onramp that's
already built into this same screen (payMethod === 'spei', the
getRampQuote('onramp', ...) path). Two completely different products;
this CTA promises a bank connection and delivered a cash meetup.

The SPEI payment method tab requires canDepositSpei (approved KYC), so
the fix is conditional: if the user already has approved KYC, the
click now reveals the in-page SPEI tab directly (setTab('buy') +
setPayMethod('spei')); otherwise it navigates to /kyc — the actual
prerequisite for connecting a bank — instead of the unrelated agent
flow. The CTA also hides itself once the SPEI tab is already showing,
since it'd otherwise sit there redundantly pointing at itself.
Etherfuse's POST /ramp/onboarding-url now rejects requests without
userInfo.email (their docs had flagged it "optional, will become
required in a future release" — that release landed in sandbox
2026-07-25). MicoPay's Stellar-keypair auth never collected an email
from anyone, so every /defi/kyc/start call was failing with a 502
wrapping "Json deserialize error: missing field `email`".

Backend: adds a nullable users.email column (migration), accepts an
optional email in the POST body, persists it once set, and returns a
new EMAIL_REQUIRED error if neither the column nor the request has one.

Frontend: KYCScreen prompts for an email inline when EMAIL_REQUIRED
comes back, then retries startKYC with it. Also fixes a second, adjacent
bug found while diagnosing this: handleOpenHostedFlow had no catch
block at all, so any failed startKYC() (this one included) silently
opened nothing and left the user staring at an unresponsive button —
this is what was actually reported ("no abre nada en el navegador").
extractApiErrorPayload only read response.data.error, but the backend's
error handler sends `code` (see index.ts setErrorHandler) — fixed so
EMAIL_REQUIRED and every other error code the backend already sends are
actually reachable from the frontend, not just the message string.

Verified by reproducing the exact 502 via curl against the real
sandbox and reading the underlying Etherfuse error from CloudWatch
logs before writing the fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant