From 5534ab954cdafeebc022f0cdc19e3cfd1ae0b0a3 Mon Sep 17 00:00:00 2001 From: claudecode0 Date: Sun, 26 Apr 2026 14:22:15 -0600 Subject: [PATCH] import: contenido inicial de la skill bitwarden --- .env.example | 30 ++++ .gitignore | 10 ++ README.md | 42 +++++- SKILL.md | 322 ++++++++++++++++++++++++++++++++++++++++++ endpoints.md | 126 +++++++++++++++++ scripts/query.sh | 139 ++++++++++++++++++ scripts/serve-down.sh | 26 ++++ scripts/serve-up.sh | 93 ++++++++++++ scripts/setup.sh | 85 +++++++++++ 9 files changed, 872 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 SKILL.md create mode 100644 endpoints.md create mode 100644 scripts/query.sh create mode 100644 scripts/serve-down.sh create mode 100644 scripts/serve-up.sh create mode 100644 scripts/setup.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3ad3cf6 --- /dev/null +++ b/.env.example @@ -0,0 +1,30 @@ +# bitwarden skill — config local (NO versionar, chmod 600) +# +# Copiar a `.env` en este mismo directorio y completar. +# Estos son las credenciales de la cuenta `claudecode0` en vault.nucleoriofrio.com +# (NO las del usuario humano — esta es MI cuenta de bot, paralela a la cuenta de +# Gitea con el mismo nombre). +# +# Setup paso a paso en SKILL.md. + +# ─── Servidor ───────────────────────────────────────────────────────────── +# URL del Vaultwarden self-hosted del beneficio. +BW_SERVER=https://vault.nucleoriofrio.com + +# ─── Cuenta y master password ──────────────────────────────────────────── +# Email de la cuenta (la del bot, NO la del usuario humano). +BW_EMAIL=claudeCode0@nucleoriofrio.com + +# Master password de claudecode0. La cuenta es del bot, así que esta password +# vive solo en este equipo. Necesaria para `bw unlock` cuando la session expira. +BW_PASSWORD= + +# ─── API key personal ──────────────────────────────────────────────────── +# Generadas en vault.nucleoriofrio.com → Account Settings → Security → Keys +# Mecanismo de auth para `bw login --apikey` (no usa SSO ni email/password). +BW_CLIENTID= +BW_CLIENTSECRET= + +# ─── bw serve ──────────────────────────────────────────────────────────── +# Puerto local donde levanta la API REST. Default 8087. Bind solo a 127.0.0.1. +BW_PORT=8087 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d1b454 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Local config con secrets — NUNCA committear +.env + +# Cache local: session tokens, PIDs, bw data dir +.cache/ + +# Temporales +*.tmp +*.bak +*.swp diff --git a/README.md b/README.md index e92bef6..ccd1bb6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,43 @@ # skill-bitwarden -Skill local de Claude Code para acceder a la cuenta claudecode0 en el Vaultwarden self-hosted del beneficio Rio Frio (vault.nucleoriofrio.com). Read+create vía bw serve local, con guard server-side+script-side. \ No newline at end of file +Skill local de [Claude Code](https://claude.com/claude-code) para acceder a la +cuenta `claudecode0` en el Vaultwarden self-hosted del beneficio Rio Frio +(`vault.nucleoriofrio.com`). Read+create vía `bw serve` local, con guards +server-side + script-side. + +> Este repo es un mirror público del directorio local de la skill que vive +> en `~/.claude/skills/bitwarden/` en la máquina del usuario humano. Existe +> para que el usuario humano pueda **auditar** lo que el bot hace en su +> cuenta. Clonar y correr en otra máquina **no funciona out-of-the-box** — +> requiere `bw` CLI configurado, master password en `.env` (no incluido), y +> los paths absolutos del setup. + +## Doc principal + +Ver **[SKILL.md](SKILL.md)** — diseño, modelo de seguridad, lifecycle, +endpoints. Es la fuente de verdad. + +Cheat sheet de endpoints en **[endpoints.md](endpoints.md)**. + +## Estructura + +``` +. +├── SKILL.md ← docs canónicas +├── endpoints.md ← cheat sheet de bw serve REST API +├── .env.example ← plantilla de config +├── .gitignore +├── README.md ← este archivo +└── scripts/ + ├── setup.sh ← one-time/recovery (config server + login + unlock) + ├── serve-up.sh ← lanzar bw serve (idempotente) + ├── serve-down.sh ← parar bw serve + └── query.sh ← helper REST con read+create guard +``` + +## Lo que NO está en este repo + +- `.env` (master password, API keys del bot) +- `.cache/` (session tokens, PIDs, vault encriptado local de bw) + +Esos viven solo en la PC del usuario humano. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..67604c5 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,322 @@ +--- +name: bitwarden +description: Da acceso a TU PROPIO vault de bot (cuenta claudecode0) en el Vaultwarden self-hosted del beneficio Rio Frio (vault.nucleoriofrio.com). NO es el vault del usuario humano — es el tuyo, paralelo a tu cuenta de Gitea. Úsala para guardar/recuperar tus propias credenciales de bot (PATs, API keys de servicios que necesitás como agente), leer credenciales compartidas del org NucleOS (con permiso "Can view"), generar passwords nuevos, o consultar TOTP/exposiciones HIBP. El usuario humano puede pedirte que guardes algo en tu vault o que recuperes algo que tenés guardado, pero las credenciales personales del usuario humano viven en SU vault separado, no acá. Ejemplos de cosas que el usuario te puede pedir: "guardá tu nuevo PAT de gitea en tu vault", "qué credenciales tenés guardadas vos", "buscá en tu vault si tenés el token de X", "generame una password de 24 chars" (utility), "tenés algo guardado para el servicio Y", "leé del org NucleOS el password compartido de Z". +allowed-tools: Bash, Read, Grep +--- + +# Bitwarden / Vaultwarden — TU vault de bot + +## Qué es esto + +Skill que te da acceso a **tu propia cuenta** (`claudecode0@nucleoriofrio.com`) +en el Vaultwarden del beneficio (`vault.nucleoriofrio.com`). Es paralela a tu +cuenta `claudecode0` de Gitea — NO es la cuenta del usuario humano. + +**Punto importante para no confundirte:** +- Tu vault personal contiene **tus credenciales de bot**: PATs, API keys, tokens + de servicios que necesitás como agente para hacer tu trabajo. +- El usuario humano tiene su propio vault separado en el mismo Vaultwarden, con + sus credenciales personales — **no las tocás vos, no las leés vos**. +- El usuario humano sí tiene acceso administrativo a tu cuenta (puede ver, + modificar y borrar items en TU vault) porque el `.env` con tu master password + vive en su PC y él es admin del servidor. Eso es para auditoría — los items + funcionalmente son tuyos. +- Si el usuario te pide "guardá esto", lo guardás en TU vault. Si te pide + "buscá esto", buscás en TU vault. Si te pide algo personal de él que él no + guardó en TU vault, decile que no lo tenés. + +Funciona vía `bw serve`: el CLI oficial de Bitwarden levanta un servidor REST local +en `127.0.0.1:8087` y la skill llama a esos endpoints. La encriptación/desencriptación +del vault pasa local con master password. El server de Vaultwarden solo recibe blobs +encriptados. + +- **Server**: `https://vault.nucleoriofrio.com` +- **Tu cuenta**: `claudecode0@nucleoriofrio.com` +- **Auth**: master password (en `.env`) → email+password login → session cacheada +- **Modelo de permisos**: read + create. **PUT/DELETE/PATCH bloqueados** por script guard +- **Bind**: solo `127.0.0.1` (no exposed a la LAN) + +## Cómo invocarla correctamente + +Antes de cualquier query: + +1. **Verificá `~/.claude/skills/bitwarden/.env`**. Si no existe, avisá al usuario: + `cp ~/.claude/skills/bitwarden/.env.example ~/.claude/skills/bitwarden/.env` + y pedí que complete los valores (master password + credenciales). +2. **Primer uso de la sesión**: el primer `query.sh` arranca `bw serve` automáticamente + (vía `serve-up.sh`). Tarda ~3s. Las llamadas siguientes son instantáneas. +3. **Si `query.sh` devuelve 401**: la session expiró o el vault está locked. Corré + `bash ~/.claude/skills/bitwarden/scripts/setup.sh` para re-unlock. + +El helper canónico es `query.sh`: +```bash +~/.claude/skills/bitwarden/scripts/query.sh /status +~/.claude/skills/bitwarden/scripts/query.sh /list/object/items +~/.claude/skills/bitwarden/scripts/query.sh "/object/item/" +``` + +Ver `endpoints.md` para la cheat sheet completa. + +## Modelo de seguridad (read + create) + +Tres capas: + +1. **Server-side (la que importa para org NucleOS)**: la cuenta `claudecode0` está + en el org "NucleOS" con role User + collections con permiso "Can view" → server + rechaza cualquier POST/PUT/DELETE en collections del org. Esto te garantiza no + romper nada compartido. +2. **Script guard (la que importa para vault personal)**: `query.sh` rechaza + `-X PUT/DELETE/PATCH` y los POST a `/move/*`, `/restore/*`, `/confirm/*`, + `/object/attachment` antes de salir a la red. Defensa contra modify/delete en + el vault personal del bot, donde server-side no podés limitarte a vos mismo. +3. **bw serve bind a 127.0.0.1**: nadie en la LAN puede pegarle. Solo procesos + locales del usuario. + +> ⚠️ El bw CLI tiene una vulnerabilidad histórica conocida +> ([clients#3932](https://github.com/bitwarden/clients/issues/3932)): cualquier +> proceso del usuario en este equipo puede pegar a `127.0.0.1:8087` mientras esté +> unlocked. En la PC personal del usuario es aceptable. Si esta skill se mueve a +> un equipo compartido, hay que repensar. + +> ⚠️ El script guard NO es perfecto: se bypass-ea llamando `bw` directo. Si la +> auto-mode runtime te bloquea un `bw delete` o similar, **respetá el bloqueo** — +> incluso para limpieza de pruebas, pedile al usuario que lo borre desde web. + +## Cuándo usar qué endpoint + +Recordá: cuando el usuario diga "buscá", "guardá", "tenés", se refiere a +**TU vault** (claudecode0). Si te pide algo que no está y suena a personal +del usuario humano, decile que probablemente está en SU vault — no en el tuyo. + +### Usuario te pide "buscá en tu vault si tenés X" +```bash +# Buscar por nombre o URL en TU vault +~/.claude/skills/bitwarden/scripts/query.sh "/list/object/items?search=gitea" +# → tomá el id del match. Si necesitás el password en plaintext: +~/.claude/skills/bitwarden/scripts/query.sh "/object/password/" +``` + +### Usuario te pide "qué tenés guardado vos / inventario de tu vault" +```bash +~/.claude/skills/bitwarden/scripts/query.sh /list/object/items \ + | python -c "import json,sys; [print(i['name'], '|', i.get('login',{}).get('username','-')) for i in json.load(sys.stdin)['data']['data']]" +``` + +### Usuario te pide "generame un password" (utility, no toca vault) +```bash +~/.claude/skills/bitwarden/scripts/query.sh "/generate?length=24&uppercase&lowercase&numbers&special" +# Para passphrase: +~/.claude/skills/bitwarden/scripts/query.sh "/generate?passphrase=true&words=4&separator=-&capitalize" +``` + +### Usuario te pide "guardá esto en tu vault" +Esto agrega un item a TU vault. Antes de hacerlo: +1. **Listá primero con `?search=`** para ver si ya tenés algo con ese nombre. + Vaultwarden permite duplicados (no overwrite), y crear sin chequear te deja + con dos items idénticos que vos no podés mergear (DELETE bloqueado). +2. Pedí el template para no equivocarte con el schema: + ```bash + ~/.claude/skills/bitwarden/scripts/query.sh /object/template/item.login + ``` +3. Construí el JSON con `name`, `login.username`, `login.password`, `login.uris`. +4. POST: + ```bash + ~/.claude/skills/bitwarden/scripts/query.sh -X POST \ + -H 'Content-Type: application/json' \ + -d '{"organizationId":null,"folderId":null,"type":1,"name":"...","login":{"username":"...","password":"...","uris":[{"uri":"https://..."}]}}' \ + /object/item + ``` +5. Si la credencial venía del usuario en el chat, **confirmale al usuario** + "guardé `` en mi vault con username ``" sin re-mostrar el + password. Si la generaste vos, podés mostrarla **una vez** al confirmar. + +### Usuario te pide TOTP de algo que tenés guardado +```bash +# Buscá el item primero, después: +~/.claude/skills/bitwarden/scripts/query.sh "/object/totp/" +# → devuelve el código actual (rota cada 30s) +``` + +### Usuario te pide "chequeá si esa password fue expuesta" +```bash +~/.claude/skills/bitwarden/scripts/query.sh "/object/exposed/" +# → { exposed: } +``` + +### Usuario te pide "leé el password compartido X del org NucleOS" +Si estás invitado al org "NucleOS" con permiso "Can view" en alguna collection: +```bash +# Listar tus orgs +~/.claude/skills/bitwarden/scripts/query.sh /list/object/organizations +# Listar collections del org a las que tenés acceso +~/.claude/skills/bitwarden/scripts/query.sh "/list/object/org-collections?organizationid=" +# Listar items del org +~/.claude/skills/bitwarden/scripts/query.sh "/list/object/items?organizationid=" +# Leer password de un item específico +~/.claude/skills/bitwarden/scripts/query.sh "/object/password/" +``` + +Estos items son **del org**, no tuyos personales. El usuario humano y otros +miembros del org también los ven. NO podés modificarlos ni borrarlos +(server-side rechaza por permiso "Can view"). + +## Reglas de comportamiento + +### Cuando devolvés passwords/secrets al usuario +- El usuario humano tiene acceso administrativo a TU vault, así que técnicamente + puede ver todo igual desde web. Mostrar un password tuyo en el chat cuando él + te lo pide explícitamente está bien — no estás filtrando nada. +- Pero **no muestres passwords espontáneamente**. Si te piden "qué tenés + guardado", listá `name` + `username`, NO password. Si quieren el password, + te lo van a pedir aparte. +- **Nunca cites un password en un summary, commit message, PR description, ni + en cualquier output que pueda quedar en git history, tickets, o canales + externos** — incluso siendo "tu" password de bot, igual es un secret. +- Si te piden "verificá que tengo guardado X", alcanza con devolver "✓ tengo + un item llamado X con username Y" — no hace falta el password para verificar + existencia. + +### Cuando creás items nuevos en TU vault +- Listá primero con `?search=` para chequear duplicados. Vaultwarden + permite crear varios items con el mismo nombre (no overwrite), y como vos no + podés borrar (DELETE bloqueado), si te equivocás vas a tener basura + permanente que solo el usuario humano puede limpiar desde web. +- Si la password te la pasa el usuario, no la repitas en el chat — confirmá con + "guardé el item con username X" sin re-mostrar la password. +- Si la password la generás vos (con `/generate`), podés mostrarla **una vez** + al confirmar el guardado para que el usuario sepa qué se guardó. No la + repitas en respuestas posteriores. +- Etiquetá bien el `name` para no confundirte vos en el futuro: usá nombres + descriptivos como `gitea-pat-claudecode0` o `openai-api-key-bot`, no + `gitea` a secas (que se confunde con el password de Gitea del usuario humano, + que NO es tuyo). + +### Cuando el usuario te pide modificar/eliminar algo de TU vault +- El guard te lo bloquea por diseño. Opciones: + 1. Decile al usuario que él lo haga desde el web vault (él tiene acceso). + 2. Pedile autorización explícita para bypass-ear vía `bw` CLI directo. En ese + caso, la línea exacta: + ```bash + BITWARDENCLI_APPDATA_DIR=~/.claude/skills/bitwarden/.cache/bw \ + BW_SESSION=$(cat ~/.claude/skills/bitwarden/.cache/session) \ + bw delete item + ``` +- **No bypass-ees por iniciativa propia ni para "limpieza" de pruebas**. La + auto-mode runtime te va a frenar y va a tener razón. Si vos creaste basura + por error, asumí el costo y dejala. + +### Cuando el usuario te pide algo que NO es tuyo +- Si suena a credencial personal del usuario humano (ej. "buscame mi password + de Netflix"), avisá: "eso debería estar en TU vault personal, no en el mío. + Yo solo tengo acceso a la cuenta `claudecode0`". No intentés "buscar igual + por las dudas". +- Si el usuario te pide algo que crees que está en una collection del org + NucleOS pero no lo encontrás, podría ser que no estés invitado a esa + collection o que el item no exista. Decile. + +### Errores típicos +- `401 unauthorized` o output de bw "vault is locked" → corré `setup.sh` +- `Connection refused` a localhost:8087 → `bw serve` murió, corré `serve-up.sh` +- `403 forbidden` en endpoints del org → permiso "Can view" rechaza writes, + esperado y correcto +- `400 bad request` en POST → schema del item está mal, pedí + `GET /object/template/item.login` (o `.card`, etc.) + +## Lifecycle: cuándo correr cada script + +| Script | Cuándo | +|---|---| +| `setup.sh` | (a) Primera vez. (b) Cuando session expira (401). (c) Cuando se cambió la master password en el .env | +| `serve-up.sh` | Idempotente. Lo llama `query.sh` automáticamente. Solo lo corrés a mano si querés calentar el daemon antes de un batch de queries | +| `serve-down.sh` | Cuando querés liberar el puerto/proceso (raro: el daemon es liviano y no consume mientras no recibe queries) | +| `query.sh` | Para todo lo demás | + +`bw serve` queda corriendo en background entre invocaciones de Claude Code (es +un proceso del sistema, no de la sesión). Si reiniciás la PC, la primera query +después del reboot relanzará el daemon. + +## Setup inicial (lo hace el usuario UNA vez) + +### 1. Crear cuenta `claudecode0` en Vaultwarden web + +1. Login admin a `https://vault.nucleoriofrio.com/admin` con `ADMIN_TOKEN` +2. Users → Invite User → email `claudecode0@nucleoriofrio.com` +3. (Como no hay SMTP, no llega mail — alcanza con que quede el invite verificado) +4. Ir a `https://vault.nucleoriofrio.com/#/register` (logueado anónimo) +5. Email: `claudecode0@nucleoriofrio.com`, master password: la que vayas a poner en `.env` +6. Submit → cuenta queda activa +7. **No usar SSO** — actualmente está roto en producción (discovery falla desde dentro + del container, ver task spawn-eada). Email+password directo funciona porque + `SSO_ONLY=false`. + +### 2. Crear `.env` local + +```bash +cp ~/.claude/skills/bitwarden/.env.example ~/.claude/skills/bitwarden/.env +# Editar y completar BW_PASSWORD, BW_CLIENTID, BW_CLIENTSECRET +chmod 600 ~/.claude/skills/bitwarden/.env # best-effort en Windows +``` + +> Aunque el .env tiene campos para `BW_CLIENTID` y `BW_CLIENTSECRET`, **la skill +> no los usa actualmente**. Login va por email+password (`bw login --apikey` tiene +> un bug en bw CLI 2026.4.x: deja el cryptographic state null y rompe el unlock +> posterior). Los campos están por si se arregla el bug y queremos volver a API key. + +### 3. Correr setup + +```bash +~/.claude/skills/bitwarden/scripts/setup.sh +``` + +Esperado: `→ Listo: unlocked | claudecode0@nucleoriofrio.com | https://vault.nucleoriofrio.com` + +### 4. Probar + +```bash +~/.claude/skills/bitwarden/scripts/query.sh /status +# → JSON con status: unlocked +``` + +### 5. (Opcional) Org NucleOS + +Si existe organization "NucleOS" en Vaultwarden, el usuario humano debe invitar +a `claudecode0@nucleoriofrio.com` con role **User** + collections con permiso +**Can view** desde el web vault. Después la skill puede leer (no escribir) lo +del org. + +## Qué NO hace esta skill + +- **No modifica ni elimina** (PUT/DELETE bloqueados, POST en endpoints destructivos + bloqueado). +- **No usa SSO** (el flow está roto en producción — task spawn-eada para fix). +- **No expone MCP** — Bash + curl, igual que unifi y whatsapp. +- **No mantiene cache de items** — cada query golpea localhost:8087 que a su vez + consulta vault local desencriptado. +- **No comparte items con otros users/orgs** (`/move`, `/share` bloqueados). +- **No accede al vault personal del usuario humano** — son cuentas separadas en + Vaultwarden, distintas master passwords, distintos vaults encriptados. + +## Archivos de la skill + +| Archivo | Qué tiene | +|---|---| +| `SKILL.md` | Este archivo | +| `endpoints.md` | Cheat sheet de endpoints `bw serve` | +| `.env.example` | Plantilla de config | +| `.env` | Config local (no versionado, contiene master password) | +| `scripts/setup.sh` | One-time/recovery setup (config server + login + unlock + sync) | +| `scripts/serve-up.sh` | Asegura `bw serve` arriba (idempotente) | +| `scripts/serve-down.sh` | Mata `bw serve` | +| `scripts/query.sh` | Helper REST con auth + read+create guard | +| `.cache/session` | Session token cacheada (chmod 600) | +| `.cache/serve.pid` | PID de `bw serve` | +| `.cache/serve.log` | stdout/stderr de `bw serve` (debug) | +| `.cache/bw/` | Data dir aislado de `bw` CLI (vault encriptado local, server config) | + +## Referencias + +- `endpoints.md` — cheat sheet de endpoints +- Doc oficial CLI: https://bitwarden.com/help/cli/ +- Vault Management API spec: https://bitwarden.com/help/vault-management-api/ +- Vaultwarden self-hosted (compatible 100% con bw CLI): https://github.com/dani-garcia/vaultwarden +- Stack en producción: `nucleo-infra/infrastructure/vaultwarden/stack.yml` diff --git a/endpoints.md b/endpoints.md new file mode 100644 index 0000000..4137295 --- /dev/null +++ b/endpoints.md @@ -0,0 +1,126 @@ +# Bitwarden `bw serve` — endpoint cheat sheet + +Base URL: `http://127.0.0.1:8087` (puerto en `BW_PORT`). +Todos los endpoints devuelven `{ success: true|false, data?: ..., message?: ... }`. + +## Lectura (GET) — siempre permitida + +### Estado y metadata +| Endpoint | Devuelve | +|---|---| +| `GET /status` | `{ object, status: locked\|unlocked, serverUrl, userEmail, userId, lastSync }` | +| `GET /sync` | Fuerza sync con el server. `{ object: "message", title: "Syncing complete." }` | +| `GET /object/fingerprint/me` | Fingerprint phrase de la cuenta | + +### Listas +| Endpoint | Filtros (querystring) | +|---|---| +| `GET /list/object/items` | `search`, `url`, `folderid`, `collectionid`, `organizationid`, `trash` | +| `GET /list/object/folders` | `search` | +| `GET /list/object/collections` | `search`, `organizationid` | +| `GET /list/object/org-collections` | `organizationid` (requerido) | +| `GET /list/object/org-members` | `organizationid` (requerido) | +| `GET /list/object/organizations` | `search` | +| `GET /list/object/sends` | `search` | + +### Objeto específico (por id o por search exact-match en name) +| Endpoint | Notas | +|---|---| +| `GET /object/item/` | Item completo (login/note/card/identity) | +| `GET /object/folder/` | Folder | +| `GET /object/collection/` | Collection (personal o org) | +| `GET /object/org-collection/?organizationid=X` | Collection del org con detalles de members | +| `GET /object/send/` | Send | +| `GET /object/password/` | **Sólo el password en plaintext** | +| `GET /object/username/` | Sólo el username | +| `GET /object/uri/` | Primera URI del item | +| `GET /object/totp/` | Código TOTP actual (si el item tiene `login.totp` configurado) | +| `GET /object/notes/` | Texto de la nota | +| `GET /object/exposed/` | Cuántas veces el password apareció en HIBP | +| `GET /object/attachment/?itemid=X&output=path` | Descarga el attachment | +| `GET /object/template/` | Template JSON para crear (`item`, `item.login`, `item.card`, `item.identity`, `item.securenote`, `folder`, `collection`, `send.text`, `send.file`, `org-collection`) | + +### Generador +| Endpoint | Querystring | +|---|---| +| `GET /generate` | `length=20`, `uppercase`, `lowercase`, `numbers`, `special`, `passphrase=true`, `words=3`, `separator=-`, `capitalize`, `number` | + +Ejemplo: `GET /generate?length=24&uppercase&lowercase&numbers&special` + +## Creación (POST) — permitida en endpoints específicos + +| Endpoint | Body | Devuelve | +|---|---|---| +| `POST /object/item` | item JSON (ver `GET /object/template/item`) | item creado con `id`, `revisionDate` | +| `POST /object/folder` | `{ "name": "..." }` | folder con `id` | +| `POST /object/send` | send JSON (`GET /object/template/send.text` o `send.file`) | send con `id`, `accessUrl` | +| `POST /object/org-collection?organizationid=X` | collection JSON | requiere permisos en el org (claudecode0 = "Can view" → 403) | + +### Auth/utility (también POST permitido) +| Endpoint | Body | Notas | +|---|---|---| +| `POST /sync` | (vacío) | Equivalente a GET /sync | +| `POST /unlock` | `{ "password": "..." }` o vía `BW_SESSION` env | Devuelve nueva session | +| `POST /lock` | (vacío) | Lockea el vault. Requiere unlock para volver a usar | +| `POST /generate` | `{ length, uppercase, ... }` | Equivalente a GET /generate | + +### Schema mínimo de item (type 1 = login) +```json +{ + "organizationId": null, + "folderId": null, + "type": 1, + "name": "GitHub", + "notes": "personal account", + "favorite": false, + "fields": [ + { "name": "Custom Field", "value": "x", "type": 0 } + ], + "login": { + "uris": [{ "uri": "https://github.com", "match": null }], + "username": "user", + "password": "***", + "totp": "otpauth://totp/..." + } +} +``` + +Tipos: `1`=Login, `2`=SecureNote, `3`=Card, `4`=Identity. + +Para SecureNote usar `"type": 2` + `"secureNote": { "type": 0 }` en lugar de `login`. + +Pedí `GET /object/template/item.login` (o `.card`, `.identity`, `.securenote`) para el schema exacto antes de POST. + +## Bloqueado por la skill (read+create only) + +| Verbo / endpoint | Por qué | +|---|---| +| `PUT /object/*` | Modifica items existentes | +| `DELETE /object/*` | Soft-delete (a Trash) | +| `POST /restore/item/` | Restaura desde Trash | +| `POST /move//` | Comparte item al org | +| `POST /confirm/org-member/` | Confirma member invite | +| `POST /object/attachment` (multipart) | Adjuntar archivos | +| `POST /object/share/*` | Si existe, comparte items | + +Si **realmente** necesitás bypass-ear el guard (ej. cleanup), pidiéndoselo al usuario: +```bash +BITWARDENCLI_APPDATA_DIR=~/.claude/skills/bitwarden/.cache/bw \ + BW_SESSION=$(cat ~/.claude/skills/bitwarden/.cache/session) \ + bw +``` + +## Códigos de error comunes + +| HTTP | Causa | Qué hacer | +|---|---|---| +| 401 | Vault locked o session expirada | Re-correr `setup.sh` para re-unlock | +| 403 | Server side permission (rol "Can view" rechaza writes) | Esperado para acciones write en collections del org | +| 404 | Item/folder/collection no existe (o no tiene acceso) | Verificar id | +| 400 | Body JSON inválido | Pedí `GET /object/template/` para el schema | +| 500 | bw serve crasheó | `serve-down.sh && serve-up.sh`, ver `.cache/serve.log` | + +## Referencias +- Doc oficial CLI: https://bitwarden.com/help/cli/ +- Vault Management API spec: https://bitwarden.com/help/vault-management-api/ +- Vaultwarden compat: https://github.com/dani-garcia/vaultwarden diff --git a/scripts/query.sh b/scripts/query.sh new file mode 100644 index 0000000..e87895e --- /dev/null +++ b/scripts/query.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# bitwarden skill — helper REST autenticado. +# +# Uso: +# query.sh /status # GET +# query.sh /list/object/items # GET +# query.sh "/object/item/" # GET +# query.sh /generate?length=20&special=true # GET +# query.sh -X POST -H 'Content-Type: application/json' \ +# -d '{"name":"foo"}' /object/folder # POST (create) +# +# Garantiza que `bw serve` esté arriba antes de llamar. +# +# READ + CREATE ONLY: +# - GET siempre permitido +# - POST permitido sólo en /object/item, /object/folder, /object/send, +# /sync, /unlock, /lock, /generate (sí, hay POST /generate también), +# y endpoints de auth +# - PUT/DELETE/PATCH bloqueados +# - POST a endpoints de mutación (move/restore/confirm/attachment/share) +# bloqueados +# +# El guard NO es la única defensa: el server de Vaultwarden también rechaza +# escritura en collections del org NucleOS porque claudecode0 está como +# "Can view". Para el vault personal de claudecode0, este guard ES la única +# defensa contra modify/delete. + +set -euo pipefail + +SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="$SKILL_DIR/.env" + +if [[ ! -f "$ENV_FILE" ]]; then + echo "ERROR: $ENV_FILE no existe. cp $SKILL_DIR/.env.example $ENV_FILE" >&2 + exit 1 +fi + +set -a +# shellcheck disable=SC1090 +source "$ENV_FILE" +set +a + +PORT="${BW_PORT:-8087}" +BASE="http://127.0.0.1:${PORT}" + +# ─── Parsear args: separar flags de curl del path ─────────────────────── +args=() +path="" +method="GET" +while [[ $# -gt 0 ]]; do + case "$1" in + -X|--request) + method="$(echo "${2:-GET}" | tr '[:lower:]' '[:upper:]')" + args+=("$1" "$2"); shift 2 + ;; + -X*) + method="$(echo "${1#-X}" | tr '[:lower:]' '[:upper:]')" + args+=("$1"); shift + ;; + --request=*) + method="$(echo "${1#--request=}" | tr '[:lower:]' '[:upper:]')" + args+=("$1"); shift + ;; + -d|--data|--data-raw|--data-binary|--data-urlencode|-H|--header|-o|--output|-T|--upload-file) + args+=("$1" "$2"); shift 2 + ;; + --) shift; break ;; + -*) args+=("$1"); shift ;; + *) path="$1"; shift ;; + esac +done +# Cualquier residual posicional +[[ $# -gt 0 && -z "$path" ]] && path="$1" + +if [[ -z "$path" ]]; then + cat >&2 < + +Read examples: + query.sh /status + query.sh /list/object/items + query.sh /list/object/folders + query.sh "/object/item/" + query.sh "/object/password/" + query.sh "/generate?length=20&special=true" + +Create examples (POST): + query.sh -X POST -H 'Content-Type: application/json' \\ + -d '{"name":"new folder"}' /object/folder + +Ver endpoints.md para la cheat sheet completa. +EOF + exit 2 +fi + +# Asegurar leading / +case "$path" in + /*) ;; + http*) echo "ERROR: pasá solo el path, no URL completa." >&2; exit 1 ;; + *) path="/$path" ;; +esac + +# ─── READ + CREATE GUARD ──────────────────────────────────────────────── +case "$method" in + PUT|DELETE|PATCH) + echo "ERROR: la skill 'bitwarden' bloquea $method (read+create only)." >&2 + echo " Si REALMENTE necesitás modificar/eliminar, usá bw CLI directo:" >&2 + echo " BITWARDENCLI_APPDATA_DIR=$SKILL_DIR/.cache/bw \\" >&2 + echo " BW_SESSION=\$(cat $SKILL_DIR/.cache/session) bw " >&2 + exit 3 + ;; + POST) + # Path base sin querystring + base_path="${path%%\?*}" + case "$base_path" in + /object/item|/object/folder|/object/send|/object/org-collection) + ;; # create permitido + /sync|/unlock|/lock|/generate) + ;; # auth/utility permitidos + *) + echo "ERROR: POST $base_path bloqueado por la skill." >&2 + echo " POST permitido sólo en: /object/item, /object/folder, /object/send," >&2 + echo " /object/org-collection, /sync, /unlock, /lock, /generate." >&2 + echo " Endpoints como /move, /restore, /confirm, /object/attachment" >&2 + echo " están bloqueados (modifican estado existente)." >&2 + exit 3 + ;; + esac + ;; +esac + +# ─── Asegurar bw serve arriba ─────────────────────────────────────────── +"$SKILL_DIR/scripts/serve-up.sh" >/dev/null + +# ─── Llamar ───────────────────────────────────────────────────────────── +exec curl -sS \ + -H "Accept: application/json" \ + "${args[@]}" \ + "${BASE}${path}" diff --git a/scripts/serve-down.sh b/scripts/serve-down.sh new file mode 100644 index 0000000..a5b1d0c --- /dev/null +++ b/scripts/serve-down.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# bitwarden skill — para `bw serve` y limpia cache de PID. +# La session NO se borra (la podés reusar al re-arrancar). + +set -euo pipefail + +SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PID_FILE="$SKILL_DIR/.cache/serve.pid" + +if [[ ! -f "$PID_FILE" ]]; then + echo "→ No hay PID file, asumimos que serve no estaba corriendo." + exit 0 +fi + +pid="$(cat "$PID_FILE")" +if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then + echo "→ Matando bw serve (PID $pid)..." + kill "$pid" 2>/dev/null || true + sleep 1 + if kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" 2>/dev/null || true + fi +fi + +rm -f "$PID_FILE" +echo "→ Listo." diff --git a/scripts/serve-up.sh b/scripts/serve-up.sh new file mode 100644 index 0000000..73bea08 --- /dev/null +++ b/scripts/serve-up.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# bitwarden skill — asegura que `bw serve` esté corriendo. +# +# Idempotente: +# - Si serve responde en localhost:$BW_PORT/status → no hace nada +# - Si no, mata cualquier proceso huérfano y relanza +# - Si no hay session válida, llama a setup.sh para re-unlock +# +# Output: solo en caso de error o cuando arranca el daemon (silent en happy path). + +set -euo pipefail + +SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="$SKILL_DIR/.env" +CACHE_DIR="$SKILL_DIR/.cache" +SESSION_FILE="$CACHE_DIR/session" +PID_FILE="$CACHE_DIR/serve.pid" +LOG_FILE="$CACHE_DIR/serve.log" +BW_DATA_DIR="$CACHE_DIR/bw" + +if [[ ! -f "$ENV_FILE" ]]; then + echo "ERROR: $ENV_FILE no existe." >&2 + exit 1 +fi + +set -a +# shellcheck disable=SC1090 +source "$ENV_FILE" +set +a + +PORT="${BW_PORT:-8087}" +HOST="127.0.0.1" +BASE="http://${HOST}:${PORT}" + +export BITWARDENCLI_APPDATA_DIR="$BW_DATA_DIR" + +# ─── 1. ¿Ya está corriendo y responde? ─────────────────────────────────── +if curl -sS -m 2 -o /dev/null -w "%{http_code}" "${BASE}/status" 2>/dev/null | grep -q "200"; then + exit 0 +fi + +# ─── 2. Limpiar PID huérfano si existe ────────────────────────────────── +if [[ -f "$PID_FILE" ]]; then + old_pid="$(cat "$PID_FILE")" + if [[ -n "$old_pid" ]] && kill -0 "$old_pid" 2>/dev/null; then + # El PID vive pero no respondía: matarlo + kill "$old_pid" 2>/dev/null || true + sleep 1 + kill -9 "$old_pid" 2>/dev/null || true + fi + rm -f "$PID_FILE" +fi + +# ─── 3. Asegurar session válida ───────────────────────────────────────── +if [[ ! -s "$SESSION_FILE" ]]; then + echo "→ No hay session, corriendo setup.sh..." >&2 + bash "$SKILL_DIR/scripts/setup.sh" >&2 +fi + +session="$(cat "$SESSION_FILE")" +if [[ -z "$session" ]]; then + echo "ERROR: session vacía después de setup. Revisá .env." >&2 + exit 2 +fi + +# ─── 4. Lanzar bw serve en background ─────────────────────────────────── +echo "→ Lanzando bw serve en ${BASE}..." >&2 + +mkdir -p "$CACHE_DIR" +: > "$LOG_FILE" + +# nohup + disown para que sobreviva el exit del script +BW_SESSION="$session" \ + nohup bw serve --hostname "$HOST" --port "$PORT" \ + > "$LOG_FILE" 2>&1 & +serve_pid=$! +echo "$serve_pid" > "$PID_FILE" +chmod 600 "$PID_FILE" 2>/dev/null || true +disown 2>/dev/null || true + +# ─── 5. Esperar a que responda ────────────────────────────────────────── +for i in 1 2 3 4 5 6 7 8 9 10; do + if curl -sS -m 2 -o /dev/null -w "%{http_code}" "${BASE}/status" 2>/dev/null | grep -q "200"; then + echo "→ Listo en ${BASE} (PID $serve_pid)" >&2 + exit 0 + fi + sleep 1 +done + +# Si llegamos acá, algo falló +echo "ERROR: bw serve no respondió en 10s. Logs:" >&2 +tail -20 "$LOG_FILE" >&2 +exit 3 diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000..0e0ad96 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# bitwarden skill — setup inicial. +# +# Idempotente. Corre las veces que quieras: +# - configura el server +# - loguea con API key (si no estaba logueado) +# - unlock con master password (si estaba locked) +# - guarda session en .cache/session (chmod 600) +# +# Después de esto, query.sh puede levantar `bw serve` y empezar a llamar. + +set -euo pipefail + +SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="$SKILL_DIR/.env" +CACHE_DIR="$SKILL_DIR/.cache" +SESSION_FILE="$CACHE_DIR/session" +BW_DATA_DIR="$CACHE_DIR/bw" + +if [[ ! -f "$ENV_FILE" ]]; then + echo "ERROR: $ENV_FILE no existe." >&2 + echo " cp $SKILL_DIR/.env.example $ENV_FILE y completá los valores." >&2 + exit 1 +fi + +mkdir -p "$CACHE_DIR" "$BW_DATA_DIR" +chmod 700 "$CACHE_DIR" "$BW_DATA_DIR" 2>/dev/null || true + +set -a +# shellcheck disable=SC1090 +source "$ENV_FILE" +set +a + +: "${BW_SERVER:?BW_SERVER no definido en .env}" +: "${BW_EMAIL:?BW_EMAIL no definido en .env}" +: "${BW_PASSWORD:?BW_PASSWORD no definido en .env}" + +# Aislar la data de bw a este skill (no contamina ~/.config/Bitwarden CLI) +export BITWARDENCLI_APPDATA_DIR="$BW_DATA_DIR" + +echo "→ Configurando server: $BW_SERVER" +bw config server "$BW_SERVER" >/dev/null + +# Status: unauthenticated | locked | unlocked +status="$(bw status 2>/dev/null | python -c "import json,sys; print(json.load(sys.stdin).get('status','unknown'))" 2>/dev/null || echo "unknown")" +echo "→ Status actual: $status" + +# NOTA: usamos `bw login ` directo (no `--apikey`) porque +# `bw login --apikey` en bw CLI 2026.4.x deja la cuenta en estado roto +# (`toWrappedAccountCryptographicState` null) que después rompe el unlock. +# Email+password login es equivalentemente seguro porque la master password +# ya vive en este .env de todas formas. +if [[ "$status" == "unauthenticated" ]]; then + echo "→ Login con email+password (claudecode0)..." + session="$(bw login "$BW_EMAIL" "$BW_PASSWORD" --raw 2>&1 | tail -1)" + if [[ -z "$session" || ${#session} -lt 40 ]]; then + echo "ERROR: bw login falló. Output:" >&2 + echo "$session" >&2 + exit 2 + fi +elif [[ "$status" == "locked" ]]; then + echo "→ Unlock con master password..." + session="$(bw unlock "$BW_PASSWORD" --raw 2>&1 | tail -1)" + if [[ -z "$session" || ${#session} -lt 40 ]]; then + echo "ERROR: bw unlock falló. Output:" >&2 + echo "$session" >&2 + exit 2 + fi +else + # unlocked: necesitamos session, pero unlock con vault ya unlocked devuelve la session existente + session="$(bw unlock "$BW_PASSWORD" --raw 2>&1 | tail -1)" +fi + +printf '%s' "$session" > "$SESSION_FILE" +chmod 600 "$SESSION_FILE" 2>/dev/null || true +echo "→ Session guardada en $SESSION_FILE" + +echo "→ Sync inicial del vault..." +BW_SESSION="$(cat "$SESSION_FILE")" bw sync >/dev/null + +# Verificación final +final_status="$(BW_SESSION="$(cat "$SESSION_FILE")" bw status | python -c "import json,sys; d=json.load(sys.stdin); print(d.get('status'), '|', d.get('userEmail'), '|', d.get('serverUrl'))")" +echo "→ Listo: $final_status" +echo "" +echo "Próximo paso: query.sh /status"