import: contenido inicial de la skill bitwarden

This commit is contained in:
2026-04-26 14:22:15 -06:00
parent 976454dc79
commit 5534ab954c
9 changed files with 872 additions and 1 deletions

139
scripts/query.sh Normal file
View File

@@ -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/<uuid>" # 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 <<EOF
Uso: query.sh [curl flags] <path>
Read examples:
query.sh /status
query.sh /list/object/items
query.sh /list/object/folders
query.sh "/object/item/<uuid>"
query.sh "/object/password/<uuid>"
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 <cmd>" >&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}"

26
scripts/serve-down.sh Normal file
View File

@@ -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."

93
scripts/serve-up.sh Normal file
View File

@@ -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

85
scripts/setup.sh Normal file
View File

@@ -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 <email> <password>` 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"