diff --git a/.gitignore b/.gitignore index ebe7d42..fef03d0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ __pycache__/ node_modules/ *.log .DS_Store +!vaults/vault.yaml diff --git a/CAPABILITIES_TODO.md b/CAPABILITIES_TODO.md index d018951..ce74207 100644 --- a/CAPABILITIES_TODO.md +++ b/CAPABILITIES_TODO.md @@ -2,7 +2,7 @@ title: Capacidades de navegador (CDP) + construcción del MCP full-CDP artefacto: project · projects/web_scraping created: 06/06/2026 00:00 -updated: 06/06/2026 10:00 +updated: 10/06/2026 18:00 status: in_progress related_issues: [] related_flows: [] @@ -307,6 +307,17 @@ Prioridad BAJA (formularios compuestos, emulación device, performance, drag): `apply_chromium_extension_policy` + `install_chromium_proxy_extension` (distribuir extensiones), y subcomando `launch` de `script_navegador`. - enlace: functions/browser/chrome_launch.go, bash/functions/browser/ +- [x] **Ciclo de vida de Chromium por perfil** (MCP `browser_mcp` v0.7.0, `tools_lifecycle.go`) + - resultado: tres tools del MCP que gestionan los Chromium del usuario POR PERFIL (distintas del + Chrome de automatización aislado de `browser_launch`): + `browser_list` (enumera procesos master leyendo `/proc/*/cmdline`: `--user-data-dir` presente, + `--type=` ausente → pid/profile/user_data_dir/cdp_port/has_cdp), + `browser_launch_profile` (lanza un perfil concreto con el binario REAL + `/usr/lib/chromium/chromium` saltando el wrapper; sin CDP por defecto para que Google mantenga + la sesión de perfiles humanos; detecta DISPLAY/XAUTHORITY de XFCE y lanza desacoplado con + setsid; reenvío al master existente documentado en `note`), + `browser_close` (localiza el master por profile/cdp_port/pid → SIGTERM 10s → SIGKILL). + - enlace: projects/web_scraping/apps/browser_mcp/tools_lifecycle.go - [x] **Pestañas — crear / listar / navegar** - resultado: `cdp_new_tab`, `cdp_open_url_and_wait` (crear+navegar+esperar load), `cdp_list_tabs`, `cdp_navigate`. (Falta cerrar/activar una pestaña concreta → Pendiente #2.) diff --git a/CHROMIUM_SYSTEM.md b/CHROMIUM_SYSTEM.md index a8dd766..21334db 100644 --- a/CHROMIUM_SYSTEM.md +++ b/CHROMIUM_SYSTEM.md @@ -98,6 +98,95 @@ Dos caminos (regla 9 de `CONVENTIONS.md`): Dos procesos chromium **no** pueden compartir el mismo `--remote-debugging-port`. El `--port` en cmdline sobreescribe al del fragmento global. +## Navegador gestionado por systemd — sesión persistente (cierre limpio) + +**Problema resuelto (2026-06-10):** el navegador diario perdía la sesión de Google (y de cualquier +sitio) cada vez que se cerraba y reabría — aparecía deslogueado al volver a abrirlo. + +### Causa raíz: el cierre sucio, NO el CDP + +Cuando el proceso **master** de Chromium se mata con `SIGKILL` (en vez de `SIGTERM`), no llega a +vaciar a disco las cookies de sesión recién escritas, marca `profile.exit_type = "Crashed"` y al +reabrir la sesión está perdida. Esto pasaba porque el navegador, lanzado por rofi/wrapper, no estaba +atado a nada que le mandara `SIGTERM` con margen: al logout/apagado de XFCE o al morir el proceso +lanzador recibía `SIGKILL` directo, sin tiempo de flush. + +Descartado tras experimento controlado (reusando la sesión real del usuario, sin re-login): + +- Con CDP activo (`--remote-debugging-port=9222 --remote-allow-origins=*`) **y** cliente CDP + conectado, `myaccount.google.com` seguía **logueado**. No hay deslogueo por tener CDP. +- `navigator.webdriver` es **`false`** con CDP → no hay detección de automatización por JS ni + server-side. El flag `--disable-blink-features=AutomationControlled` no cambia nada aquí. +- Tras un cierre **limpio** (`SIGTERM`, `exit_type=SessionEnded`), las cookies de auth + (`SID`, `SAPISID`, `__Secure-1PSID`, `__Secure-3PSID`) **persisten** a disco con CDP puesto. + +Callejones descartados (no eran la causa, no repetir): un supuesto "borrar cookies al cerrar" (no +existía), `restore_on_startup` (se aplicó policy `RestoreOnStartup:1`, no resolvió), y el cifrado +OSCrypt fallando en XFCE (`os_crypt.portal.prev_init_success:false`; se aplicó +`--password-store=basic` vía `/etc/chromium.d/password-store`, no resolvió — inofensivo, se deja). + +### Solución: systemd user service `chromium-personal` + +`~/.config/systemd/user/chromium-personal.service` mantiene el navegador como servicio gestionado, +de modo que **cada cierre es SIGTERM con margen de flush**. Claves del unit: + +| Directiva | Valor | Por qué | +|---|---|---| +| `ExecStart` | `/usr/lib/chromium/chromium --user-data-dir=%h/.config/chromium-cdp --profile-directory=Personal --remote-debugging-port=9222 --remote-debugging-address=127.0.0.1 --remote-allow-origins=* --no-first-run --no-default-browser-check` | Binario **real** (controlamos los flags). CDP en **loopback** explícito. | +| `KillSignal=SIGTERM` + `KillMode=mixed` | — | SIGTERM solo al master; él apaga sus hijos y vacía cookies. | +| `TimeoutStopSec=30` | 30 s | Margen para flushear antes de cualquier SIGKILL. | +| `PartOf=graphical-session.target` | — | systemd lo para ordenado en el **logout**, antes de que XFCE mate la sesión. | +| `Restart=always` + `RestartSec=3` | — | Siempre activo; se recupera de crashes. | +| `[Install] WantedBy=default.target` (`enable`) | — | Arranca al iniciar sesión. | + +`DISPLAY=:0` y `XAUTHORITY=%h/.Xauthority` van en `Environment=` (sin ellos no conecta al X server). + +**Seguridad:** el CDP escucha **solo en `127.0.0.1:9222`** (verificado con `ss`: `127.0.0.1:9222`, +nunca `0.0.0.0`). El `--remote-allow-origins=*` se mantiene porque lo necesita el cliente MCP local +para el websocket; no expone nada a la red porque el bind es loopback. + +### Todos los perfiles heredan el cierre limpio + +Chromium usa **un único master por `user-data-dir`**. Como el service mantiene ese master +(perfil inicial `Personal`), cualquier otro perfil que se abra (rofi, menú, selector de Chromium) se +**engancha al mismo master**. Por eso `systemctl --user stop` (o el logout) hace un shutdown global +que vacía las cookies de **todos** los perfiles abiertos. No hace falta un service por perfil (de +hecho no funcionaría: no pueden coexistir dos masters sobre el mismo `user-data-dir`). + +### rofi y escritorio usan el service + +- Script `~/.local/bin/chromium-launch`: hace `systemctl --user start chromium-personal`, espera a + que el CDP loopback responda y reenvía al master con el binario real + (`/usr/lib/chromium/chromium --user-data-dir=$HOME/.config/chromium-cdp "$@"`). Así nunca se crea + un master no gestionado. +- Override `~/.local/share/applications/chromium.desktop` (precedencia sobre + `/usr/share/applications/`): su `Exec` apunta a ese script. rofi y el menú XFCE lo usan + automáticamente. Trae `Actions` para nueva ventana / incógnito. + +### Uso + +```bash +systemctl --user start chromium-personal # abrir (o desde rofi/menú "Chromium") +systemctl --user stop chromium-personal # cerrar de verdad (no dispara Restart) +systemctl --user status chromium-personal # estado +``` + +Cerrar la ventana con la `X` o `Ctrl+Q` también es limpio (SIGTERM normal de Chromium). **Gotcha +UX de `Restart=always`:** si se cierran TODAS las ventanas, el master sale y systemd lo relanza en +~3 s (reabre `Personal`); para apagarlo del todo usar `stop`. Si molesta, cambiar a +`Restart=on-failure`. + +### Control por el MCP browser (lifecycle por perfil) + +El MCP `browser` (`apps/browser_mcp`) tiene tres tools para gestionar instancias por perfil sin +comandos a mano (añadidas 2026-06-10, rama `quick/browser-lifecycle`): + +- `browser_list` — lista los masters chromium corriendo: `{pid, profile, user_data_dir, cdp_port, has_cdp}`. +- `browser_launch_profile` — lanza por perfil; `cdp:false` usa el binario real **sin** remote-debugging + (navegador "humano" que Google no toca), `cdp:true` añade el puerto. Desacopla con `setsid` para + sobrevivir a la muerte del MCP. +- `browser_close` — cierra limpio por `profile`/`cdp_port`/`pid` (SIGTERM; SIGKILL solo como último recurso). + ## Proxy / captura mitm (web_proxy) - App: `projects/web_scraping/apps/web_proxy` (sub-repo Gitea `dataforge/web_proxy`). Compone diff --git a/hoppscotch/README.md b/hoppscotch/README.md new file mode 100644 index 0000000..574e718 --- /dev/null +++ b/hoppscotch/README.md @@ -0,0 +1,145 @@ +# Hoppscotch — consola de APIs + colecciones versionadas + +GUI para explorar/probar APIs a mano (Hoppscotch Desktop) con las consultas guardadas +como JSON versionado en este sub-repo, y un puente bidireccional con el motor de replay +del registry para **lanzar y automatizar** esas mismas peticiones desde terminal o scripts. + +Grupo de capacidad: `hoppscotch`. Página madre: `docs/capabilities/hoppscotch.md`. + +## El ciclo + +``` +GUI Hoppscotch ──export .json──▶ parse_hoppscotch_collection ──call specs──▶ run_hoppscotch_collection ──▶ http_replay_sequence + ▲ (automatiza, headless) + └──── build_hoppscotch_collection ◀── call specs (de HAR destilado, o a mano) ────────────────────────────┘ +``` + +- **GUI → automatización**: editas/creas requests en la app, exportas la colección a + `collections/.json`, y `run_hoppscotch_collection` las ejecuta sin tocar la GUI. +- **Automatización → GUI**: tienes call specs (p.ej. de un flujo grabado con HAR y destilado + con `har_extract_calls`), `build_hoppscotch_collection` te da un `.json` importable en la app + para inspeccionarlas visualmente. + +## Estructura + +``` +hoppscotch/ + collections/ # Colecciones .json VERSIONADAS (sin secretos) + registry_api.json # ejemplo: 4 endpoints GET del registry_api + environments/ # Variables de entorno Hoppscotch + .gitignore # ignora *.json (secretos) salvo *.example.json + registry_api.example.json # plantilla VERSIONADA (valores placeholder) + registry_api.json # real, GITIGNORED (puede llevar tokens) + README.md +``` + +Regla de secretos: las **colecciones** se versionan; los **environments reales** no (llevan +tokens/basicAuth). Solo la plantilla `*.example.json` viaja en git. En las requests, los valores +sensibles van como variable de environment con la sintaxis Hoppscotch `<>`, nunca +hardcodeados. + +## Lanzar la app Desktop + +Binario instalado por el `.deb`: `/usr/bin/hoppscotch-desktop` (menú de aplicaciones: "Hoppscotch"). +Lanzado desde una sesión gráfica normal o, de forma aislada: + +```bash +systemd-run --user --unit=hoppscotch-gui --setenv=DISPLAY=:0 /usr/bin/hoppscotch-desktop +``` + +### Importar una colección en la GUI + +1. Panel **Collections** (izquierda) → menú **Import / Export**. +2. **Import** → **Hoppscotch** → **Import from File**. +3. Selecciona `collections/registry_api.json`. +4. (Opcional) Importa también el environment: panel **Environments** → Import → + `environments/registry_api.example.json` y rellena los valores reales (no se versionan). +5. Selecciona el environment activo arriba a la derecha y pulsa **Send** en cualquier request. + +### Exportar / ver el JSON que genera la GUI + +Collections → menú → **Export** → **Hoppscotch** → guarda en `collections/`. Así se ve el +esquema nativo de la versión instalada (la app migra al esquema más reciente al importar/exportar). +`parse_hoppscotch_collection` lee cualquier versión v1..v12 por campos estables. + +## Automatizar desde el registry + +Lanzar toda una colección o un subconjunto, headless, con sustitución de variables: + +```python +import sys, os +sys.path.insert(0, os.path.join(os.path.expanduser("~/fn_registry"), "python", "functions")) +from infra.run_hoppscotch_collection import run_hoppscotch_collection + +res = run_hoppscotch_collection( + "projects/web_scraping/hoppscotch/collections/registry_api.json", + environment_path="projects/web_scraping/hoppscotch/environments/registry_api.json", + params={"baseURL": "https://registry.organic-machine.com"}, # pisa el environment + only=["status", "locations"], # filtra por nombre de request; None = todas +) +print(res["status"]) +for s in res["steps"]: + print(s["method"], s["url"], "->", s["status_code"], "ok" if s["ok"] else "FAIL") +``` + +`run_hoppscotch_collection` convierte la sintaxis Hoppscotch `<>` a la del motor de replay +`{{var}}`, mergea environment + params (params del caller ganan), filtra por `only`, y reproduce +las peticiones sobre una sesión HTTP compartida (cookie jar entre pasos). + +## Añadir peticiones rápido + +Sin editar el `.json` a mano, con `add_hoppscotch_request` (reusa el mapeo de `build`, no +destruye el archivo si el JSON es inválido): + +```python +from infra.add_hoppscotch_request import add_hoppscotch_request +r = add_hoppscotch_request( + "projects/web_scraping/hoppscotch/collections/registry_api.json", + "GET", "<>/api/search?q=metabase&kind=function", + name="search metabase", headers={"Accept": "application/json"}) +print(r["status"], r["total_requests"]) # ok 8 +``` + +Tras añadir, re-importa la colección en la app Desktop (no recarga el `.json` en caliente) o +córrela headless con `run_hoppscotch_collection` / `hopp test`. + +## Validar la colección + +Dos motores independientes, ambos verde sobre `registry_api.json` (7 requests → 7×200): + +```bash +# Motor nativo Hoppscotch (corre también preRequestScript/testScript JS): +hopp test collections/registry_api.json -e environments/registry_api.cli.json + +# Motor del registry (integrado, telemetría, params/extract): +python3 -c 'from infra.run_hoppscotch_collection import run_hoppscotch_collection as r; \ + print(r("collections/registry_api.json", environment_path="environments/registry_api.json")["status"])' +``` + +## Funciones del registry + +| ID | Qué hace | +|---|---| +| `build_hoppscotch_collection_py_infra` | call specs → dict de colección Hoppscotch (importable en la GUI) | +| `parse_hoppscotch_collection_py_infra` | dict de colección Hoppscotch (v1..v12) → call specs | +| `run_hoppscotch_collection_py_infra` | lee colección + environment .json y ejecuta vía `http_replay_sequence` | +| `add_hoppscotch_request_py_infra` | añade una petición a una colección `.json` existente (reusa `build`, no destructivo) | +| `http_replay_sequence_py_infra` | motor de replay HTTP (secuencia con sesión, `{{param}}`, extract) | +| `har_extract_calls_py_cybersecurity` | HAR destilado → call specs (origen del lado izquierdo del ciclo) | + +## Gotchas + +- **Sintaxis de variables**: la GUI usa `<>`; el motor de replay usa `{{var}}`. + `run_hoppscotch_collection` traduce automáticamente. Si compones a mano con + `http_replay_sequence`, traduce tú (`<>` → `{{x}}`). +- **Endpoints con efecto** (POST/DELETE destructivos): filtra con `only` para no ejecutar a + ciegas toda la colección. El replay reproduce lo que haya; la responsabilidad de no disparar + acciones irreversibles es del caller. +- **Esquema de versión**: `build_*` genera el formato canónico estable v1/v2 que la app migra al + importar. `parse_*` lee cualquier versión por campos. No fijamos v12/v17 a mano para no romper + con cambios de esquema upstream. +- **multipart/form-data**: `parse_*` lo marca `body_type="raw"` y no reconstruye el cuerpo binario. +- **Formato dual de environment**: la GUI Desktop importa el array `[{v,name,variables:[{key,value,secret}]}]` + (`registry_api.json` / `.example.json`); el CLI `hopp` v0.31.2 quiere un **objeto plano** + `{name,variables:[{key,value}]}` SIN `v` ni `secret` (`registry_api.cli.json`). Pasar el array a + `hopp test -e` da `MALFORMED_ENV_FILE`. `run_hoppscotch_collection` (registry) usa el array. diff --git a/hoppscotch/collections/licenseplatedata.json b/hoppscotch/collections/licenseplatedata.json new file mode 100644 index 0000000..a8bb5b0 --- /dev/null +++ b/hoppscotch/collections/licenseplatedata.json @@ -0,0 +1,97 @@ +{ + "v": 1, + "name": "licenseplatedata", + "folders": [], + "requests": [ + { + "v": "2", + "endpoint": "<>/plate-to-vin?plate=<>&state=<>", + "name": "plate-to-vin", + "params": [], + "headers": [ + { + "key": "X-API-Key", + "value": "<>", + "active": true + }, + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + }, + { + "v": "2", + "endpoint": "<>/vin/<>", + "name": "decode VIN", + "params": [], + "headers": [ + { + "key": "X-API-Key", + "value": "<>", + "active": true + }, + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + }, + { + "v": "2", + "endpoint": "<>/vehicle-images?vin=<>", + "name": "vehicle-images", + "params": [], + "headers": [ + { + "key": "X-API-Key", + "value": "<>", + "active": true + }, + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + } + ] +} diff --git a/hoppscotch/collections/plate_to_vin.json b/hoppscotch/collections/plate_to_vin.json new file mode 100644 index 0000000..42c41f1 --- /dev/null +++ b/hoppscotch/collections/plate_to_vin.json @@ -0,0 +1,112 @@ +{ + "v": 1, + "name": "Plate to VIN (free APIs)", + "folders": [], + "requests": [ + { + "v": "2", + "endpoint": "https://api.auto.dev/plate/<>/<>", + "name": "autodev_plate_to_vin", + "params": [], + "headers": [ + { + "key": "Accept", + "value": "application/json", + "active": true + }, + { + "key": "Authorization", + "value": "Bearer <>", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + }, + { + "v": "2", + "endpoint": "https://api.carsxe.com/v2/platedecoder?key=<>&plate=<>&state=<>", + "name": "carsxe_plate_decoder", + "params": [], + "headers": [ + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + }, + { + "v": "2", + "endpoint": "https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVinValues/<>?format=json", + "name": "nhtsa_decode_vin_values", + "params": [], + "headers": [ + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + }, + { + "v": "2", + "endpoint": "https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVin/<>?format=json", + "name": "nhtsa_decode_vin", + "params": [], + "headers": [ + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + } + ] +} diff --git a/hoppscotch/collections/registry_api.json b/hoppscotch/collections/registry_api.json new file mode 100644 index 0000000..543e64e --- /dev/null +++ b/hoppscotch/collections/registry_api.json @@ -0,0 +1,182 @@ +{ + "v": 1, + "name": "registry_api", + "folders": [], + "requests": [ + { + "v": "2", + "endpoint": "<>/api/status", + "name": "status", + "params": [], + "headers": [ + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + }, + { + "v": "2", + "endpoint": "<>/api/search?q=slice", + "name": "search basico", + "params": [], + "headers": [ + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + }, + { + "v": "2", + "endpoint": "<>/api/search?q=http&lang=py&kind=function", + "name": "search filtrado (py/function)", + "params": [], + "headers": [ + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + }, + { + "v": "2", + "endpoint": "<>/api/locations", + "name": "locations (todos)", + "params": [], + "headers": [ + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + }, + { + "v": "2", + "endpoint": "<>/api/locations/<>", + "name": "locations por PC", + "params": [], + "headers": [ + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + }, + { + "v": "2", + "endpoint": "<>/api/functions/filter_slice_go_core", + "name": "show funcion", + "params": [], + "headers": [ + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + }, + { + "v": "2", + "endpoint": "<>/api/search?q=metabase&kind=function", + "name": "search metabase", + "params": [], + "headers": [ + { + "key": "Accept", + "value": "application/json", + "active": true + } + ], + "method": "GET", + "auth": { + "authType": "none", + "authActive": true + }, + "preRequestScript": "", + "testScript": "", + "body": { + "contentType": null, + "body": null + }, + "requestVariables": [] + } + ] +} diff --git a/hoppscotch/environments/.gitignore b/hoppscotch/environments/.gitignore new file mode 100644 index 0000000..cd4cb97 --- /dev/null +++ b/hoppscotch/environments/.gitignore @@ -0,0 +1,4 @@ +# Environments de Hoppscotch llevan secretos (tokens, basicAuth). NUNCA se versionan. +# Solo se versiona la plantilla *.example.json (valores con <>). +*.json +!*.example.json diff --git a/hoppscotch/environments/registry_api.example.json b/hoppscotch/environments/registry_api.example.json new file mode 100644 index 0000000..eb49e2b --- /dev/null +++ b/hoppscotch/environments/registry_api.example.json @@ -0,0 +1,22 @@ +[ + { + "v": 1, + "name": "registry_api", + "variables": [ + { + "key": "baseURL", + "value": "https://registry.organic-machine.com", + "secret": false + }, + { + "key": "pc", + "value": "lucas-linux", + "secret": false + }, + { + "key": "registryToken", + "secret": true + } + ] + } +] diff --git a/hoppscotch/selfhost/.env.example b/hoppscotch/selfhost/.env.example new file mode 100644 index 0000000..1e30621 --- /dev/null +++ b/hoppscotch/selfhost/.env.example @@ -0,0 +1,14 @@ +# Generado 2026-06-10T20:27:47Z. Secretos reales — NO versionar (ver .gitignore). +DATABASE_URL=postgresql://postgres:@db:5432/hoppscotch?connect_timeout=300 +POSTGRES_PASSWORD= +DATA_ENCRYPTION_KEY=<32-chars-openssl-rand-hex-16> +WHITELISTED_ORIGINS=http://localhost:3170,http://localhost:3009,http://localhost:3100,app://localhost_3200,app://hoppscotch +PROXY_APP_URL=https://proxy.hoppscotch.io +TRUST_PROXY=false +VITE_BASE_URL=http://localhost:3009 +VITE_SHORTCODE_BASE_URL=http://localhost:3009 +VITE_ADMIN_URL=http://localhost:3100 +VITE_BACKEND_GQL_URL=http://localhost:3170/graphql +VITE_BACKEND_WS_URL=ws://localhost:3170/graphql +VITE_BACKEND_API_URL=http://localhost:3170/v1 +ENABLE_SUBPATH_BASED_ACCESS=false diff --git a/hoppscotch/selfhost/.gitignore b/hoppscotch/selfhost/.gitignore new file mode 100644 index 0000000..ca2e69c --- /dev/null +++ b/hoppscotch/selfhost/.gitignore @@ -0,0 +1,7 @@ +# El .env lleva secretos (DATA_ENCRYPTION_KEY, POSTGRES_PASSWORD) → nunca se versiona. +# Solo viaja la plantilla .env.example. +.env + +# Archivos oficiales descargados como referencia (no son nuestra config). +.env.example.upstream +docker-compose.upstream.yml diff --git a/hoppscotch/selfhost/docker-compose.yml b/hoppscotch/selfhost/docker-compose.yml new file mode 100644 index 0000000..7e1e82f --- /dev/null +++ b/hoppscotch/selfhost/docker-compose.yml @@ -0,0 +1,60 @@ +# Hoppscotch self-hosted (Community Edition) — stack local para trabajar en conjunto. +# +# Arquitectura: imagen all-in-one pre-built (app 3000 + admin 3100 + backend GraphQL 3170 +# + bundle 3200) + Postgres 15 + Mailpit (captura el magic link del login, sin OAuth externo +# ni SMTP real). Todo escucha solo en 127.0.0.1 (no se expone a la red). +# +# El puerto 3000 del host está ocupado por otro proceso, así que la app se publica en 3009. +# Las VITE_* del .env quedan coherentes con ese remapeo (el AIO las inyecta en runtime). +# +# Levantar: docker compose up -d +# Logs: docker compose logs -f hopp_aio +# Parar: docker compose down (los datos persisten en el volumen hopp_pgdata) +# Reset total: docker compose down -v (BORRA la base de datos) + +services: + db: + image: postgres:15 + container_name: hopp_db + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: hoppscotch + volumes: + - hopp_pgdata:/var/lib/postgresql/data + ports: + - "127.0.0.1:5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d hoppscotch"] + interval: 5s + timeout: 5s + retries: 20 + + # Mailpit: servidor SMTP de pruebas. Recibe el correo del magic link en el SMTP interno + # (mail:1025) y lo expone en una API/UI web (127.0.0.1:8025) desde donde se lee el enlace. + mail: + image: axllent/mailpit:latest + container_name: hopp_mail + restart: unless-stopped + ports: + - "127.0.0.1:8025:8025" # UI + API REST de Mailpit + # SMTP en el puerto interno 1025, accesible por la red de docker como mail:1025 + + aio: + image: hoppscotch/hoppscotch:2026.5.0 + container_name: hopp_aio + restart: unless-stopped + env_file: + - ./.env + depends_on: + db: + condition: service_healthy + ports: + - "127.0.0.1:3009:3000" # app web (3000 del host está ocupado) + - "127.0.0.1:3100:3100" # admin dashboard + - "127.0.0.1:3170:3170" # backend: GraphQL /graphql, REST /v1 + - "127.0.0.1:3200:3200" # bundle server + +volumes: + hopp_pgdata: