#!/usr/bin/env pwsh # check-tokens.ps1 — Verify that the credentials in the environment actually work. # # Reads .env (or a supplied file) and validates access against live services: # 1. Gitea container registry (CI_REGISTRY_TOKEN / CI_REGISTRY_USER). # 2. Cloudflare API token(s) (CLOUDFLARE_API_TOKEN, and any R2 admin token). # 3. R2/S3 bucket access (R2_ACCESS_KEY_ID + R2_SECRET_ACCESS_KEY) — lists buckets. # 4. Linear Void admin login (ADMIN_USERNAME / ADMIN_PASSWORD) against the app origin. # # NOTE: Runs the checks through the target Docker host (default 192.168.88.40) so # network + TLS work regardless of this machine's proxy/firewall. Uses the # curlimages/curl and amazon/aws-cli images, which are pulled on demand. # # Usage: # ./scripts/check-tokens.ps1 # ./scripts/check-tokens.ps1 -EnvFile .env # ./scripts/check-tokens.ps1 -Target 192.168.88.40 # # Exit code 0 = all credentials valid; 1 = at least one check failed. param( [string]$EnvFile = '', [string]$Target = '192.168.88.40', [int]$DockerPort = 2375, [int]$TimeoutSec = 20 ) $ErrorActionPreference = 'Stop' $target = $Target $dockerHost = "tcp://${target}:${DockerPort}" $env:DOCKER_HOST = $dockerHost $failed = $false function Fail([string]$msg) { Write-Host " [FAIL] $msg" -ForegroundColor Red; $script:failed = $true } function Ok([string]$msg) { Write-Host " [OK] $msg" -ForegroundColor Green } function Warn([string]$msg) { Write-Host " [WARN] $msg" -ForegroundColor Yellow } # Resolve env file: explicit, or $PWD/.env, or repo root. if (-not $EnvFile) { foreach ($cand in @((Join-Path $PWD '.env'), (Join-Path (Split-Path $PWD) '.env'))) { if (Test-Path $cand) { $EnvFile = $cand; break } } } if (-not $EnvFile -or -not (Test-Path $EnvFile)) { Write-Host "No .env file found. Pass one with -EnvFile." -ForegroundColor Yellow exit 1 } # Parse KEY=VALUE lines into a hashtable. $envs = @{} Get-Content $EnvFile | ForEach-Object { if ($_ -match '^\s*([A-Za-z0-9_]+)\s*=\s*(.*?)\s*$') { $envs[$Matches[1]] = $Matches[2] } } function Get-Val([string]$key) { if ($envs.ContainsKey($key)) { return $envs[$key] } return $null } # Pull helper images once. Write-Host "Preparing helper images (curl, aws-cli) on $target..." -ForegroundColor Cyan docker pull curlimages/curl:latest 2>&1 | Out-Null docker pull amazon/aws-cli:latest 2>&1 | Out-Null function Curl([string]$url, [string]$bearer, [string]$tokenHeader) { $a = @('run', '--rm', 'curlimages/curl:latest', '-sS', '--max-time', "$TimeoutSec") if ($bearer) { $a += @('-H', "Authorization: Bearer $bearer") } if ($tokenHeader) { $a += @('-H', "Authorization: token $tokenHeader") } $a += $url return (& docker @a 2>&1 | Out-String).Trim() } # --------------------------------------------------------------------------- Write-Host "== Linear Void credential / token check ==" -ForegroundColor Cyan Write-Host "Env file : $EnvFile" Write-Host "Docker host: $dockerHost" # --- 1. Gitea registry token --- Write-Host "`n-- Gitea container registry --" -ForegroundColor Cyan $regUser = Get-Val 'CI_REGISTRY_USER' $regToken = Get-Val 'CI_REGISTRY_TOKEN' if ($regUser -and $regToken) { # Non-sensitive probe: /api/v1/user returns a scope message for token-based creds. $out = Curl 'https://git.allucanget.biz/api/v1/user' $null $regToken if ($out -match 'token does not have at least one of required scope' -or $out -match '"login"' -or $out -match 'invalid_grant') { if ($out -match 'invalid_grant|Unauthorized|invalid') { Fail "Gitea registry token rejected ($regUser): $out" } else { Ok "Gitea registry token valid for '$regUser' (scope: write:package/write:repository)" } } else { Warn "Gitea registry token response unexpected: $out" } } else { Warn "CI_REGISTRY_USER / CI_REGISTRY_TOKEN not set; skipping Gitea registry check" } # --- 2. Cloudflare API token --- Write-Host "`n-- Cloudflare API tokens --" -ForegroundColor Cyan $cfToken = Get-Val 'CLOUDFLARE_API_TOKEN' if ($cfToken) { $out = Curl 'https://api.cloudflare.com/client/v4/user/tokens/verify' $cfToken $null if ($out -match '"success":true') { if ($out -match '"status":"active"') { Ok 'Cloudflare API token valid and ACTIVE' } else { Warn "Cloudflare API token valid but status: $out" } } else { Fail "Cloudflare API token rejected: $out" } } else { Warn "CLOUDFLARE_API_TOKEN not set; skipping Cloudflare check" } # --- 3. Cloudflare R2 / S3 bucket access --- Write-Host "`n-- Cloudflare R2 (S3-compatible) --" -ForegroundColor Cyan $r2Key = Get-Val 'R2_ACCESS_KEY_ID' $r2Secret = Get-Val 'R2_SECRET_ACCESS_KEY' $r2Ep = Get-Val 'R2_ENDPOINT' $bucket = Get-Val 'R2_BUCKET_NAME' # Use the admin key pair (which has ListBuckets) when present, else the regular key. $listKey = if ((Get-Val 'R2_ADMIN_ACCESS_KEY_ID') -and (Get-Val 'R2_ADMIN_SECRET_ACCESS_KEY')) { @{ k = Get-Val 'R2_ADMIN_ACCESS_KEY_ID'; s = Get-Val 'R2_ADMIN_SECRET_ACCESS_KEY' } } else { @{ k = $r2Key; s = $r2Secret } } if ($listKey.k -and $listKey.s -and $r2Ep) { $script = @" export AWS_ACCESS_KEY_ID='$($listKey.k)' export AWS_SECRET_ACCESS_KEY='$($listKey.s)' aws s3 ls --endpoint-url '$r2Ep' --region auto 2>&1 "@ $out = (& docker run --rm --entrypoint sh amazon/aws-cli:latest -c $script 2>&1 | Out-String).Trim() if ($out -match $bucket) { Ok "R2 bucket listing OK, '$bucket' present" } elseif ($out -match 'AccessDenied') { Fail "R2 listing denied (no ListBuckets permission): $out" } elseif ($out -match '^\S+\s+') { Warn "R2 listing returned (unexpected): $out" } else { Fail "R2 access failed: $out" } } else { Warn "R2 credentials / endpoint not fully set; skipping R2 check" } # Verify the specific application bucket is reachable with the primary key (read). if ($r2Key -and $r2Secret -and $r2Ep -and $bucket) { $script = @" export AWS_ACCESS_KEY_ID='$r2Key' export AWS_SECRET_ACCESS_KEY='$r2Secret' aws s3 ls "s3://$bucket/" --endpoint-url '$r2Ep' --region auto --max-keys 5 2>&1 | head -5 "@ $out = (& docker run --rm --entrypoint sh amazon/aws-cli:latest -c $script 2>&1 | Out-String).Trim() if ($out -match 'AccessDenied') { Fail "Bucket '$bucket' read denied with primary key" } elseif ($out -eq '') { Ok "Bucket '$bucket' reachable (empty listing, expected)" } elseif ($out -match 'NoSuchBucket') { Fail "Bucket '$bucket' NOT FOUND" } else { Ok "Bucket '$bucket' reachable" } } elseif (-not $bucket) { Warn "R2_BUCKET_NAME not set; skipping bucket-level check" } # --- 4. Linear Void admin login --- Write-Host "`n-- Linear Void admin login --" -ForegroundColor Cyan $adminUser = Get-Val 'ADMIN_USERNAME' $adminPass = Get-Val 'ADMIN_PASSWORD' if ($adminUser -and $adminPass) { # Hit the chat backend directly (public origin is caught by nginx SPA fallback). $url = "http://${target}:3000/api/admin/login" $body = ('{"username":"' + $adminUser + '","password":"' + $adminPass + '"}') $wrongBody = ('{"username":"' + $adminUser + '","password":"__definitely_wrong__"}') $a = @('run', '--rm', 'curlimages/curl:latest', '-sS', '--max-time', "$TimeoutSec", '-o', '/dev/null', '-w', '%{http_code}', '-X', 'POST', '-H', 'Content-Type: application/json', '-d', $body, $url) $okCode = (& docker @a 2>&1 | Out-String).Trim() $b = @('run', '--rm', 'curlimages/curl:latest', '-sS', '--max-time', "$TimeoutSec", '-o', '/dev/null', '-w', '%{http_code}', '-X', 'POST', '-H', 'Content-Type: application/json', '-d', $wrongBody, $url) $wrongCode = (& docker @b 2>&1 | Out-String).Trim() if ($okCode -eq '200' -and $wrongCode -eq '401') { Ok "Admin credentials valid (correct=200, wrong=401)" } else { Fail "Admin login unexpected (correct=$okCode, wrong=$wrongCode)" } } else { Warn "ADMIN_USERNAME / ADMIN_PASSWORD not set; skipping admin check" } # --------------------------------------------------------------------------- Write-Host "" if ($failed) { Write-Host "RESULT: FAILED" -ForegroundColor Red; exit 1 } Write-Host "RESULT: OK (no failing checks)" -ForegroundColor Green; exit 0