Playbook: ConvertAPI token management

Autor: secret-manager agent
Wersja: 1.0 — 2026-07-07
Zastosowanie: Autonomiczna rotacja V42_CONVERT_API w pinbox24-w4.env.sops


Architektura tokenów ConvertAPI

ConvertAPI oferuje dwa typy tokenów używanych jako Bearer w nagłówku Authorization:

CONVERTAPI_API_TOKEN  ←  długowieczny, konto-poziomowy

        │  HS256 self-sign lub POST /token/jwt

  V42_CONVERT_API     ←  JWT, krótkoterminowy (90 dni), w pinbox24-w4.env.sops
  • API Token (CONVERTAPI_API_TOKEN) — stały klucz konta, rotowany ręcznie przez dashboard. Przechowywany w role-secret-manager.env.sops. Nigdy nie trafia do kontenerów aplikacyjnych.
  • KID (CONVERTAPI_KID) — UUID przypisany do API Token (widoczny w dashboardzie). Nie jest sekretem, ale wymagany do podpisywania JWT.
  • JWT (V42_CONVERT_API) — generowany z API Token (HS256), ważny 90 dni. Trafia do pinbox24-w4.env.sops i używa go kontener v42.

Backend v42 wysyła oba identycznie: Authorization: Bearer <token> — bez zmian w kodzie.


Gdzie używane

SystemKlucz SOPSSOPS plik
Pinbox24 v42 (bms-1 + bms-4)V42_CONVERT_APIsecrets/pinbox24-w4.env.sops
secrets-manager workerCONVERTAPI_API_TOKEN + CONVERTAPI_KIDsecrets/role-secret-manager.env.sops

Pinbox24 v32 (W3) nie używa ConvertAPI.


Rotacja JWT (Tier 1 — autonomiczna, secrets-manager)

Dwie metody — preferowana: self-signed lokalnie (bez HTTP call).

Metoda A — self-signed lokalnie (HS256, preferowana)

JWT podpisywany lokalnie przy użyciu API Token jako sekretu HMAC. Nie wymaga połączenia sieciowego z ConvertAPI — tylko SOPS access.

$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
 
# 1. Wczytaj API Token + KID z role-secret-manager
$smLines = sops --decrypt --input-type dotenv --output-type dotenv secrets\role-secret-manager.env.sops 2>$null
$env:CA_TOKEN = ($smLines | Where-Object { $_ -match "^CONVERTAPI_API_TOKEN=" }).Split("=",2)[1]
$env:CA_KID   = ($smLines | Where-Object { $_ -match "^CONVERTAPI_KID=" }).Split("=",2)[1]
$smLines = @()
if (-not $env:CA_TOKEN) { throw "CONVERTAPI_API_TOKEN missing" }
if (-not $env:CA_KID)   { throw "CONVERTAPI_KID missing" }
 
# 2. Buduj JWT payload (HS256 self-signed)
$now    = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$expiry = $now + 7776000  # 90 dni
 
$header  = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('{"alg":"HS256","typ":"JWT"}')) `
           -replace '\+','-' -replace '/','_' -replace '='
$payload = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(
    "{`"kid`":`"$env:CA_KID`",`"exp`":$expiry,`"iat`":$now,`"nbf`":$now}"
)) -replace '\+','-' -replace '/','_' -replace '='
 
$sigInput = "$header.$payload"
$hmac     = [System.Security.Cryptography.HMACSHA256]::new(
    [Text.Encoding]::UTF8.GetBytes($env:CA_TOKEN)
)
$sig = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($sigInput))) `
       -replace '\+','-' -replace '/','_' -replace '='
 
$env:CA_TOKEN = ""  # wyczyść natychmiast
$newJwt = "$sigInput.$sig"
 
# 3. Zaktualizuj pinbox24-w4.env.sops
# (Update-SopsKey z sops-edit-operations.md)
Update-SopsKey "secrets\pinbox24-w4.env.sops" "V42_CONVERT_API" $newJwt
$newJwt = ""; $env:CA_KID = ""
 
Write-Host "V42_CONVERT_API rotated — self-signed JWT valid 90 days"
Write-Host "Next rotation due: $((Get-Date).AddDays(90).ToString('yyyy-MM-dd'))"

Metoda B — via ConvertAPI JWT endpoint (fallback)

Używać gdy self-signing nie działa (np. zmiana algorytmu po stronie ConvertAPI).

$env:CA_TOKEN = ...  # z role-secret-manager jak wyżej
$env:CA_KID   = ...
 
$body = @{ Kid = $env:CA_KID; ExpiresInSec = 7776000 } | ConvertTo-Json
$response = Invoke-RestMethod `
    -Uri "https://v2.convertapi.com/token/jwt" `
    -Method POST `
    -Headers @{ Authorization = "Bearer $env:CA_TOKEN"; "Content-Type" = "application/json" } `
    -Body $body
$env:CA_TOKEN = ""
$newJwt = $response.Token
if (-not $newJwt) { throw "JWT generation failed" }
Update-SopsKey "secrets\pinbox24-w4.env.sops" "V42_CONVERT_API" $newJwt
$newJwt = ""

Rotacja bazowego API Token (Tier 3 — human, rzadko)

Wykonywana tylko przy podejrzeniu kompromitacji lub po upływie >1 roku.

  1. Zaloguj się na convertapi.com przez Gmail OAuth (konto ecotrans.automation@gmail.com)
  2. Przejdź do: My Account → Authentication → API Tokens
  3. Utwórz nowy token — zapisz wartość tokena oraz jego KID (UUID widoczny w polu “API TOKEN’S KID”)
  4. Zapisz token do pliku w Downloads (np. convertapi-token.txt), nigdy przez chat
  5. W sesji secret-manager:
$env:NEW_CA_TOKEN = (Get-Content "D:\downloads\convertapi-token.txt" -Raw).Trim()
Update-SopsKey "secrets\role-secret-manager.env.sops" "CONVERTAPI_API_TOKEN" $env:NEW_CA_TOKEN
$env:NEW_CA_TOKEN = ""
# Zaktualizuj też CONVERTAPI_KID jeśli nowy token ma inny KID
  1. Usuń stary token z dashboardu ConvertAPI
  2. Wygeneruj nowy JWT dla v42 (procedura Tier 1 powyżej)

Rewokacja JWT

Brak endpointu do rewokacji pojedynczego JWT. Potwierdzone zarówno w tym playbooku (Metoda A self-signed HS256 i Metoda B /token/jwt powyżej — obie tylko wystawiają nowy JWT, żadna nie unieważnia poprzedniego), jak i w dokumentacji autoryzacji ConvertAPI (sprawdzone 2026-08-01): ConvertAPI nie udostępnia żadnego API do unieważnienia konkretnego, już wystawionego tokena JWT. Wygenerowanie nowego JWT (rotacja V42_CONVERT_API) nie unieważnia starego — stary token pozostaje w pełni ważny aż do swojego exp (do 90 dni od wystawienia).

Jedyny istniejący mechanizm unieważnienia to odświeżenie tokena bazowego konta (CONVERTAPI_API_TOKEN / CONVERTAPI_KID) — patrz sekcja “Rotacja bazowego API Token (Tier 3 — human, rzadko)” powyżej. Odświeżenie Master Tokena natychmiast unieważnia wszystkie JWT podpisane pod starym kid — nie tylko jeden konkretny (np. wyciekniony) token, ale każdy JWT wystawiony z tego API Tokena, łącznie z tym aktualnie używanym produkcyjnie na v42-prod.

Z tego wynika:

  • Nie istnieje ścieżka “punktowej” rewokacji pojedynczego skompromitowanego/wyciekniętego JWT bez jednoczesnego unieważnienia wszystkich innych aktywnych JWT podpisanych pod tym samym kid.
  • Decyzja o odświeżeniu Master Tokena wyłącznie po to, by unieważnić jeden konkretny JWT, jest Tier 3 — wyłącznie human (dashboard ConvertAPI, login przez Gmail OAuth) i wymaga świadomej kalkulacji ryzyka: czy pozostawienie starego JWT żywego aż do naturalnego wygaśnięcia jest akceptowalne, czy uzasadnia wymuszoną rotację tokena bazowego (z wynikającym z niej ryzykiem przestoju — patrz sekwencja poniżej). “Accept and expire naturally” jest poprawnym wynikiem tej kalkulacji, jeśli świadomie udokumentowanym, a nie domyślnym przez brak decyzji.

Sekwencja unikająca przestoju v42-prod

Jeśli padnie decyzja o odświeżeniu Master Tokena (np. po potwierdzonej kompromitacji, nie tylko podejrzeniu), kolejność jest krytyczna — odświeżenie unieważnia natychmiast wszystkie JWT pod starym kid, więc zastępczy JWT musi zostać wygenerowany pod nowym kid i wdrożony w tym samym oknie czasowym, żeby uniknąć przestoju konwersji dokumentów na v42-prod (bms-1):

  1. Zaloguj się na dashboard ConvertAPI i odśwież Master Token (procedura Tier 3 powyżej, kroki 1-4) — zapisz nową wartość CONVERTAPI_API_TOKEN oraz nowy CONVERTAPI_KID lokalnie, nigdy przez chat.
  2. Natychmiast, w tej samej sesji, wygeneruj nowy JWT pod nowym kid — procedura Tier 1 powyżej (Metoda A lub B), używając już zaktualizowanego CONVERTAPI_API_TOKEN.
  3. Zaktualizuj secrets/role-secret-manager.env.sops (CONVERTAPI_API_TOKEN, CONVERTAPI_KID) i secrets/pinbox24-w4.env.sops (V42_CONVERT_API) w tym samym PR/commicie — nigdy osobno, bo stary JWT jest już martwy od momentu kroku 1.
  4. Redystrybuuj przez secrets-sync.yml, wymuś recreate v42-prod na bms-1.
  5. Zweryfikuj brak błędów autoryzacji ConvertAPI w logach v42-prod (patrz “Weryfikacja po rotacji” poniżej — nie używaj /user do weryfikacji, ten endpoint zawsze zwraca 401 dla JWT, niezależnie od tego czy token jest poprawny).

Odwleczenie kroków 2-4 po odświeżeniu Master Tokena w kroku 1 = gwarantowany przestój konwersji dokumentów na v42-prod (wszystkie JWT, także ten aktualnie używany, przestają działać natychmiast po odświeżeniu).


Weryfikacja po rotacji

⚠️ UPDATE 2026-08-01 (#4966/#4969): poniższy check przez /user jest BŁĘDNY — nie używaj go. Potwierdzone empirycznie: nawet stary, bezsprzecznie działający, aktualnie wdrożony JWT zwraca 401 {"Code":4011,"Message":"Unauthorized. Invalid or missing API credentials."} na /user. Endpoint /user akceptuje tylko CONVERTAPI_API_TOKEN (konto-poziomowy), NIGDY JWT z /token/jwt — JWT są scope’owane wyłącznie do endpointów konwersji, nie do account-info. Ten check zawsze zwróci 401 niezależnie od tego czy nowy JWT jest poprawny czy nie — nie traktuj 401 tutaj jako dowodu, że rotacja się nie powiodła.

# BŁĘDNE — zostawione dla kontekstu historycznego, nie używać:
# $testResult = Invoke-RestMethod -Uri "https://v2.convertapi.com/user" `
#     -Headers @{ Authorization = "Bearer $env:NEW_JWT" }
# $testResult.SecondsLeft -gt 0 i $testResult.Active -eq $true -- to ZAWSZE 401 dla JWT, nie tylko złych

Poprawna weryfikacja — jedna z dwóch:

  1. Strukturalna (natychmiastowa, offline) — sprawdź że JWT ma 3 segmenty base64url, kid zgadza się z CONVERTAPI_KID, exp ~90 dni w przyszłości. Nie dowodzi że token faktycznie działa u ConvertAPI, ale wyłapuje błędy generowania.
  2. Rzeczywista (po restarcie v42-prod, zalecana) — sprawdź logi kontenera pod kątem błędów autoryzacji ConvertAPI (brak błędów = token działa):
    ssh root@94.23.26.113 "docker logs --since 3m v42-prod 2>&1 | grep -iE 'convert' | grep -iE 'error|fail|401|unauthor'"
    # pusty wynik = OK, żadnych błędów autoryzacji ConvertAPI

Harmonogram rotacji

AkcjaCzęstotliwośćTierWykonuje
Rotacja JWT (V42_CONVERT_API)co 90 dni1secrets-manager (bms-4)
Rotacja bazowego API Tokenco 365 dni lub przy incydencie3developer + secrets-manager

Zalecane: dodać do docs/priorities.md wpis z datą kolejnej rotacji JWT po pierwszym wdrożeniu.


Powiązane

  • secrets/role-secret-manager.env.sops — przechowuje CONVERTAPI_API_TOKEN + CONVERTAPI_KID
  • secrets/pinbox24-w4.env.sops — przechowuje V42_CONVERT_API (JWT)
  • docs/playbooks/sops-reset-pinbox24.md (planowany) — pełny reset W4
  • docs/sops-templates/role-secret-manager.keys — tier dokumentacja
  • ConvertAPI Auth docs