diff --git a/CLAUDE.md b/CLAUDE.md index 8e206a0..16d1cd7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,21 +27,38 @@ Extensión de VS Code de un único fichero fuente (`src/extension.ts`). TypeScri - `getEditorAndSelection()` obtiene editor activo + selección (helper compartido). - `replaceSelection()` / `replaceWholeDocument()` aplican ediciones vía `editor.edit()`. +Los comandos Base64 y `decodeJwt` requieren selección. `fixJson` opera sobre la selección si la hay o sobre el documento completo en caso contrario. + +Los cuatro comandos aparecen en el menú contextual del editor; Base64 y JWT solo cuando hay selección (`when: editorHasSelection`). + ### Comando `fixJson` — pipeline de limpieza -La función `cleanAndParseJson()` prueba estrategias en orden hasta que `JSON.parse()` tenga éxito: +La función `cleanAndParseJson()` prueba estrategias en orden hasta que `JSON.parse()` tenga éxito. Si ninguna funciona, reemplaza el contenido con el último intento para que el usuario pueda inspeccionarlo. + +| # | Estrategia | +|---|------------| +| 1 | Texto tal cual | +| 2 | Trim | +| 3 | `tryUnwrapString` — desenvuelve capa exterior `"…"` vía `JSON.parse` | +| 4 | `wrapAndUnescape` — envuelve en comillas para que el motor desescapee `\"` en un solo paso; falla si el texto contiene comillas sin escapar | +| 5 | Eliminación de barras antes de comillas (sin contexto URL) + `\"` → `"` | +| 6 | Eliminación de barras simétricas `/"key/"` en ambos lados | +| 7 | `grafanaDeepClean` — BOM, CRLF, desescapado multi-nivel, trailing commas, `unquoteJsonStringValues` | +| 8 | `grafanaDeepClean` + `tryUnwrapString` combinados | -1. Texto tal cual -2. Trim -3. `tryUnwrapString` — desenvuelve capa exterior `"…"` vía `JSON.parse` -4. `wrapAndUnescape` — envuelve en comillas para que el motor desescapee `\"` -5. Eliminación de barras antes de comillas (sin contexto URL) -6. Eliminación de barras simétricas `/"key/"` en ambos lados -7. `grafanaDeepClean` — BOM, CRLF, desescapado multi-nivel, trailing commas, `unquoteJsonStringValues` -8. `grafanaDeepClean` + `tryUnwrapString` combinados +#### Helpers clave del pipeline -El formateado final usa `formatJsonText()`, un formateador a nivel de texto que **no pasa por `JSON.parse` → `JSON.stringify`**, preservando así representaciones numéricas como `150.0`. +- **`tryUnwrapString`** — si el string empieza y termina con `"`, usa `JSON.parse` para desescapar; hace fallback a recorte manual. +- **`grafanaDeepClean`** — limpiador multi-paso: BOM → CRLF → unwrap (dos niveles) → barras simétricas → barras asimétricas → desescapado iterativo (máx. 5 pasadas) → segunda pasada de barras → trailing slash en valores → `unquoteJsonStringValues` → contenedores vacíos como string → dobles comillas exteriores → trailing commas. +- **`unquoteJsonStringValues`** — elimina las comillas que envuelven valores que son objetos/arrays JSON sin escapar (p. ej. `"Content":"{"k":"v"}"` → `"Content":{"k":"v"}`). Itera hasta estabilización. + +#### Formateador de texto `formatJsonText` + +Formatea JSON con 2 espacios de indentación **trabajando a nivel de texto**, sin pasar por `JSON.parse` → `JSON.stringify`. Esto preserva representaciones numéricas como `150.0`. Los contenedores vacíos (`{}`, `[]`) se emiten en una sola línea. ## Release -Empujar un tag `vX.Y.Z` dispara el workflow `.github/workflows/build.yml`, que sincroniza la versión en `package.json`, empaqueta el `.vsix` y lo adjunta al GitHub Release (lo crea si no existe). +El workflow `.github/workflows/build.yml` se dispara en dos situaciones: + +- **Push a `main`**: compila y sube el `.vsix` como artefacto de CI (90 días de retención), pero no crea release. +- **Push de tag `vX.Y.Z`**: además sincroniza la versión en `package.json` y adjunta el `.vsix` al GitHub Release (lo crea si no existe). diff --git a/package.json b/package.json index 3d20336..947b5be 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,10 @@ { "command": "wrenchit.fixJson", "title": "WrenchIT: Limpiar y formatear JSON (Grafana)" + }, + { + "command": "wrenchit.decodeJwt", + "title": "WrenchIT: Decodificar JWT" } ], "menus": { @@ -43,6 +47,11 @@ { "command": "wrenchit.fixJson", "group": "wrenchit@3" + }, + { + "command": "wrenchit.decodeJwt", + "group": "wrenchit@4", + "when": "editorHasSelection" } ] } diff --git a/src/extension.ts b/src/extension.ts index 46db31e..b3b8697 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,7 +4,8 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('wrenchit.encodeBase64', () => encodeBase64()), vscode.commands.registerCommand('wrenchit.decodeBase64', () => decodeBase64()), - vscode.commands.registerCommand('wrenchit.fixJson', () => fixJson()) + vscode.commands.registerCommand('wrenchit.fixJson', () => fixJson()), + vscode.commands.registerCommand('wrenchit.decodeJwt', () => decodeJwt()) ); } @@ -371,6 +372,49 @@ function grafanaDeepClean(s: string): string { return result; } +// --------------------------------------------------------------------------- +// Decode JWT +// --------------------------------------------------------------------------- + +function decodeJwt() { + const ctx = getEditorAndSelection(); + if (!ctx) { return; } + const { editor, selection, text } = ctx; + + if (!text.trim()) { + vscode.window.showWarningMessage('WrenchIT: Selecciona el token JWT que quieres decodificar.'); + return; + } + + const parts = text.trim().split('.'); + if (parts.length < 2) { + vscode.window.showErrorMessage('WrenchIT: El texto seleccionado no tiene formato JWT (se esperan al menos 2 partes separadas por ".").'); + return; + } + + try { + const decoded = decodeJwtParts(parts[0], parts[1]); + replaceSelection(editor, selection, decoded); + } catch { + vscode.window.showErrorMessage('WrenchIT: No se pudo decodificar el JWT. Comprueba que el token es válido.'); + } +} + +function decodeJwtParts(headerB64: string, payloadB64: string): string { + const decodeSegment = (seg: string): unknown => { + // Base64url → Base64 standard → Buffer → UTF-8 → JSON + const base64 = seg.replace(/-/g, '+').replace(/_/g, '/'); + const json = Buffer.from(base64, 'base64').toString('utf8'); + return JSON.parse(json); + }; + + const header = decodeSegment(headerB64); + const payload = decodeSegment(payloadB64); + + const combined = JSON.stringify({ header, payload }); + return formatJsonText(combined); +} + // --------------------------------------------------------------------------- // Text-level JSON formatter (preserves original numeric representations) // ---------------------------------------------------------------------------