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 wrole-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 dopinbox24-w4.env.sopsi używa go kontener v42.
Backend v42 wysyła oba identycznie: Authorization: Bearer <token> — bez zmian w kodzie.
Gdzie używane
| System | Klucz SOPS | SOPS plik |
|---|---|---|
| Pinbox24 v42 (bms-1 + bms-4) | V42_CONVERT_API | secrets/pinbox24-w4.env.sops |
| secrets-manager worker | CONVERTAPI_API_TOKEN + CONVERTAPI_KID | secrets/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.
- Zaloguj się na convertapi.com przez Gmail OAuth (konto ecotrans.automation@gmail.com)
- Przejdź do: My Account → Authentication → API Tokens
- Utwórz nowy token — zapisz wartość tokena oraz jego KID (UUID widoczny w polu “API TOKEN’S KID”)
- Zapisz token do pliku w Downloads (np.
convertapi-token.txt), nigdy przez chat - 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- Usuń stary token z dashboardu ConvertAPI
- 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/jwtpowyż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 (rotacjaV42_CONVERT_API) nie unieważnia starego — stary token pozostaje w pełni ważny aż do swojegoexp(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):
- Zaloguj się na dashboard ConvertAPI i odśwież Master Token (procedura Tier 3 powyżej, kroki 1-4)
— zapisz nową wartość
CONVERTAPI_API_TOKENoraz nowyCONVERTAPI_KIDlokalnie, nigdy przez chat. - Natychmiast, w tej samej sesji, wygeneruj nowy JWT pod nowym
kid— procedura Tier 1 powyżej (Metoda A lub B), używając już zaktualizowanegoCONVERTAPI_API_TOKEN. - Zaktualizuj
secrets/role-secret-manager.env.sops(CONVERTAPI_API_TOKEN,CONVERTAPI_KID) isecrets/pinbox24-w4.env.sops(V42_CONVERT_API) w tym samym PR/commicie — nigdy osobno, bo stary JWT jest już martwy od momentu kroku 1. - Redystrybuuj przez
secrets-sync.yml, wymuś recreatev42-prodna bms-1. - Zweryfikuj brak błędów autoryzacji ConvertAPI w logach
v42-prod(patrz “Weryfikacja po rotacji” poniżej — nie używaj/userdo 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
/userjest BŁĘDNY — nie używaj go. Potwierdzone empirycznie: nawet stary, bezsprzecznie działający, aktualnie wdrożony JWT zwraca401 {"Code":4011,"Message":"Unauthorized. Invalid or missing API credentials."}na/user. Endpoint/userakceptuje tylkoCONVERTAPI_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łychPoprawna weryfikacja — jedna z dwóch:
- Strukturalna (natychmiastowa, offline) — sprawdź że JWT ma 3 segmenty base64url,
kidzgadza się zCONVERTAPI_KID,exp~90 dni w przyszłości. Nie dowodzi że token faktycznie działa u ConvertAPI, ale wyłapuje błędy generowania. - 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
| Akcja | Częstotliwość | Tier | Wykonuje |
|---|---|---|---|
Rotacja JWT (V42_CONVERT_API) | co 90 dni | 1 | secrets-manager (bms-4) |
| Rotacja bazowego API Token | co 365 dni lub przy incydencie | 3 | developer + 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— przechowujeCONVERTAPI_API_TOKEN+CONVERTAPI_KIDsecrets/pinbox24-w4.env.sops— przechowujeV42_CONVERT_API(JWT)docs/playbooks/sops-reset-pinbox24.md(planowany) — pełny reset W4docs/sops-templates/role-secret-manager.keys— tier dokumentacja- ConvertAPI Auth docs