Migración AWS, mapa real, y fixes de KYC/CETES - #344
Open
ericmt-98 wants to merge 23 commits into
Open
Conversation
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.
/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.
Plan ejecutable para el repo nuevo (micopay-agents): sacar de aqui todo lo de agentes —AIGENTS/x402, Bazaar, ZK, API de protocolo, frontend de demos y contratos de swap— y terminar ahi la pata XRPL del atomic swap. Este repo queda para el APK y la app movil. Escrito recorriendo el monorepo, no desde el SUBMISSION. Lo que salio de ese recorrido y condiciona el plan: - Hay duplicados divergentes de casi todo, y copiar el equivocado compila igual. Dos APIs (apps/api de agentes, no desplegada; micopay/backend del APK, en produccion) y DOS escrows en Soroban con IDs distintos, donde micopay/contracts/TESTNET.md dice literalmente "do not mix them". El plan lista rutas exactas de que se mueve y que no. - El "cross-chain" de hoy son dos contratos en la MISMA cadena: ATOMIC_SWAP_CONTRACT_B esta documentado en .env.example como "Second instance for demo (chain B simulation)". Eso es lo que el puente reemplaza. Y la orquestacion de dos patas no existe todavia: swaps.ts solo expone lecturas y demo.ts bloquea USDC llamandolo "cross-chain collateral". Es entregable, no herencia. - apps/api arrastra 21 errores de tsc; 18 estan en los modulos retail que no se migran. Filtrar los arregla. Los 3 de reputation.ts si hay que arreglarlos. - El split no es limpio: reputation.ts sirve tiers a agentes tras x402 pero los calcula con datos de comercios que quedan del lado movil. Se decide en M3, con las tres opciones y su costo; la copia por cadaptador es el error que el commit 1811016 ya documento en este repo. Incluye las reglas duras de la traduccion entre ledgers (la Condition de XRPL se deriva del MISMO preimage; el invariante initiator > counterparty tiene que sobrevivir el paso de secuencia-de-ledger a Ripple epoch), los cinco tests obligatorios —incluido revelar al filo del CancelAfter, que es el que atrapa el invariante mal traducido— y los requisitos del relay: idempotente, reanudable y sin custodia. Y dos correcciones al SUBMISSION: el campo `chain` de AssetInfo no esta en el contrato sino en una interfaz TypeScript off-chain, y el MicopayEscrow que corre produccion es la copia que se QUEDA en este repo. Regla de oro del documento: nada se borra de aqui hasta que el repo nuevo este verde y corra el demo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
POST /users/register aceptaba cualquier stellar_address (solo validaba longitud 56) y devolvia un JWT de 24h: cualquiera podia registrar la direccion publica de otra persona antes que ella. Ver el finding "Registro sin prueba de posesion de llave" en AUDIT_MOBILE_MAINNET.md. - Extrae el challenge/response de auth.ts a challenge.service.ts (issueChallenge/verifyAndConsumeChallenge), ahora compartido por /auth/token y /users/register. - register exige challenge+signature y valida con StrKey. - El JWT de registro ahora lleva jti, asi que es revocable desde el primer momento (antes solo lo era tras el primer login). - Frontend: registerUser() hace el mismo baile challenge -> firma -> registro; se elimina generateFallbackAddress de registerUser y getAuthToken (fabricaba direcciones invalidas en vez de fallar). CAMBIO DE CONTRATO DE API: 2 campos nuevos requeridos. Backend y APK deben desplegarse juntos; un APK viejo contra este backend recibe 400 en el registro. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GET /defi/ramp/order/:orderId y regenerate_tx devolvian el status de la orden de cualquiera con solo tener el orderId. Ver el finding "/defi/ramp/order/:orderId sin check de pertenencia" en AUDIT_MOBILE_MAINNET.md. Nueva tabla ramp_orders (order_id, user_id) que registra al dueno al crear la orden y se valida en ambas rutas (403 si no coincide). Fail-open para ordenes anteriores a la migracion (sin fila de ownership se permite y se loguea warning) para no romper ordenes en curso. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getTradeHistory traia SELECT id, username FROM users completo y paginaba en memoria con filtered.slice(). Reescrito con JOIN users su/bu (por FK, nunca excluye filas) y LIMIT/OFFSET parametrizados; el filtro expired (derivado, nunca persistido) tambien se empujo al WHERE. Ademas elimina updateMerchantReputation y su llamada en completeTrade: escribia UPDATE merchants, una tabla que no existe en este schema, y solo logueaba un warning "non-critical" en cada trade completado. La reputacion ya se calcula on-read en GET /users/me desde trades. Nota: el store en memoria de los tests locales no soporta doble JOIN ni LIMIT parametrizado (limitacion pre-existente del mock); ahi cae a merchant_username 'Usuario Micopay'. Contra Postgres real resuelve bien. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
client-errors.ts existia pero nunca se registraba en index.ts, asi que reportClientError del ErrorBoundary del frontend posteaba a un 404. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
USDC esta anclado ~1:1 al dolar, pero las stablecoins se despegan de vez en cuando, asi que consulta precio spot en vez de asumir 1.0. Mismo patron de resiliencia multi-fuente que xlm-mxn: Coinbase spot x er-api, CoinGecko directo, y como ultimo recurso asumir el peg y convertir USD->MXN. Cache con el mismo TTL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
QRReveal mostraba un mensaje estatico ("Estamos en Av. Juarez 34...")
atribuido a la contraparte real, con foto de stock de un tercero. Es
enganoso: parecia un mensaje que la contraparte escribio. Se sustituye
por el nombre de la contraparte y los accesos directos; los mensajes de
verdad viven en la pantalla de chat.
TradeDetail: usa buildTxUrl() en vez de hardcodear la URL de testnet de
stellar.expert, que en un build de mainnet apuntaba a la red equivocada.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Render se esta apagando; los dos builds pasan a api.micopay.app. Aviso importante dejado en .env.mainnet: ese dominio corre hoy con STELLAR_NETWORK=TESTNET, asi que el build "mainnet" no mueve fondos reales todavia. Hasta que exista un backend en modo MAINNET, sigue siendo un build de prueba. VITE_ENABLE_DEFI_TRADING=true solo en testnet (finding B2: ese camino no mueve fondos reales aun); queda sin poner en mainnet/production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El contenedor es el artefacto que se despliega en AWS y hasta ahora nada lo verificaba. El job nuevo lo construye (bloqueante), comprueba que las migraciones SQL viajen dentro de la imagen (sin ellas runMigrations() falla en boot, solo loguea y el servicio arranca contra una BD sin esquema) y hace un smoke test de /health y assetlinks.json. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AUDIT_MOBILE_MAINNET.md: se marcan como FIXED los findings cerrados en los commits anteriores (registro sin prueba de llave, IDOR de ramp, getTradeHistory, updateMerchantReputation, client-errors, interfaces RampQuote/RampOrder duplicadas). PLAN_SPLIT_Y_PUENTE_XRPL: el repo destino ya existe y se llama Micopay/micopaybridge (publico). Se ajusta M0 en consecuencia: no hay que crearlo, su LICENSE es (c) Micopay y no debe sobrescribirse, y al ser publico hay que barrer secretos y decidir que hacer con SEC-13 antes del primer push. Entran ademas los planes y auditorias que estaban sueltos en el disco: migracion a AWS, cumplimiento/KYC, mapa real, rediseno visual, distribucion beta y el brief legal de Fase 4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El README abria vendiendo acceso privado para agentes de IA, y el producto —la app que convierte dolares digitales en pesos fisicos— quedaba en un "Also..." entre parentesis. El orden contaba la historia al reves de lo que es el proyecto. - El encabezado ahora abre con la app, y cierra con "the app is the product; the ecosystem is what the product makes possible", que dice explicitamente lo que antes se dejaba al orden de las secciones. - La seccion completa de ZK Agent Credentials se mueve del principio al final, antes de Team. Nada de su contenido cambia. - La referencia cruzada dentro del ecosistema decia "see top of this README" y "now leads this README": habria quedado apuntando al vacio. Reescrita para apuntar al final, y con un resumen de tres lineas para que se entienda sin saltar. Entra tambien CLAUDE.md, que estaba sin versionar: guia de trabajo con AWS para el repo (preferir MCP/IaC, y la regla de no leer secretos con get-secret-value sino resolverlos en runtime). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 CIverify-fullreal contra RDS con el CA bundle embebido en la imagen — descubierto necesario en vivo, no solo como endurecimiento diferidoMapa real (
f706c7f..1e7faf9, más 2 fixes defeat/map-realpost-merge)maplibre-gla v5 — la v6 rompe su Worker interno bajo el esquemahttps://localhostde Capacitor (verificado con DevTools remoto conectado al WebView)/merchants/available(privacidad — el endpoint era público sin límites)online: truehardcodeado) y limpia código muertoFixes 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 Etherfusefix(kyc): Etherfuse ahora exigeemail(antes opcional) y el flujo fallaba en silencio sin mostrar error — se agrega captura de email + manejo de errores realTest plan
npm run builden ambos)api.micopay.app): RDS conecta converify-full, migraciones corren, health check en verdePendiente (no bloqueante, documentado en docs/PLAN_MAPA_REAL_2026-07.md §7)
🤖 Generated with Claude Code