src/Jetons.ps1
|
# --------------------------------------------------------------------------- # ctx doctor -Live -- does this token work, and does it open the RIGHT account? # --------------------------------------------------------------------------- # # Without the network, a diagnostic can only say a token is LOADED. That is the # cheap half of the question. The expensive half -- is it still valid, does it # belong to the account this folder expects, does it carry the rights the work # needs -- has until now been answered by a command failing mid-task, or worse, # by succeeding against the wrong account. # # Three rules hold everywhere below. # # 1. OPT-IN. A diagnostic that silently calls the network is a diagnostic people # stop running. -Live is explicit, and every call is a read-only identity # probe: whoami, list projects. Nothing is created, changed or deleted. # # 2. THE TOKEN NEVER COMES BACK OUT. Not in a verdict, not in an error, not in # a URL. Every message crosses Protect-CtxMessage on its way to the report, # because a report is pasted into chats and committed into logs. # # 3. FAILING IS AN ANSWER, NOT A CRASH. An unreachable network is INFO -- it # says nothing about the token. A 401 is a PROBLEM -- it says the token is # dead. Confusing the two would make the tool cry wolf on a train. $script:JetonTimeout = 8 # --------------------------------------------------------------------------- # Redaction # --------------------------------------------------------------------------- function Protect-CtxMessage { <# Removes anything that looks like a credential from a string on its way into a report. This is a last line, not the first: nothing here is supposed to put a token in a message. It exists because "supposed to" is what leaked the Vercel bypass token into a browser URL on 24 Apr 2026, and because an HTTP library is free to quote a request line we never inspected. #> param([AllowNull()][AllowEmptyString()][string]$Message) if ([string]::IsNullOrEmpty($Message)) { return $Message } $motifs = @( # --- par emetteur connu --- 'sbp_[A-Za-z0-9_\-]+', 'sbs_[A-Za-z0-9_\-]+', 'sb_secret_[A-Za-z0-9_\-]+', 'gh[pousr]_[A-Za-z0-9_\-]+', 'github_pat_[A-Za-z0-9_\-]+', 'glpat-[A-Za-z0-9_\-]+', 'sntrys_[A-Za-z0-9_\-\.]+', 'sk-[A-Za-z0-9_\-]{16,}', 'xox[baprs]-[A-Za-z0-9\-]+', 'ya29\.[A-Za-z0-9_\-]+', 'AIza[A-Za-z0-9_\-]{20,}', '(AKIA|ASIA)[A-Z0-9]{12,}', # --- par forme, la ou la liste d'emetteurs sera toujours en retard --- # # Un JWT : c'est la forme des cles anon et service_role de Supabase, et # la service_role contourne toute RLS. 'eyJ[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]+', # La portion mot de passe d'une URL de connexion. `--db-url` en porte # une, et l'audit du 15 aout 2026 a montre qu'elle traversait intacte. '(?i)(postgres(ql)?|mysql|mongodb(\+srv)?|redis|amqp)://[^:@/\s]+:[^@\s]+@', # Mot-cle suivi d'une valeur. « password » manquait, alors que # SUPABASE_DB_PASSWORD est un secret que ce module gere lui-meme. '(?i)(bearer|token|access[_-]?token|api[_-]?key|password|passwd|pwd|secret)\s*[:=]?\s*[A-Za-z0-9_\-\.!@#$%^&*]{8,}' ) $sortie = $Message foreach ($m in $motifs) { $sortie = [regex]::Replace($sortie, $m, '<REDACTED>') } $sortie } # --------------------------------------------------------------------------- # Pure decisions # --------------------------------------------------------------------------- function Test-CtxDoctorJetonGitHub { <# The interesting failure is not "the token is dead". It is "the token is alive and belongs to someone else" -- a working credential for the wrong account is exactly what this whole module exists to prevent, and it is the one case a token check that only asks "is it valid" would bless. #> param( [AllowNull()][string]$LoginAttendu, [AllowNull()][string]$LoginReel, [AllowNull()][string]$Portees, [int]$Code = 0, [AllowNull()][string]$Erreur ) if ($Code -eq 401) { return New-CtxCheck -Domaine 'gh' -Sujet 'jeton' -Verdict 'PROBLEME' ` -Detail 'jeton refuse (401) : expire ou revoque' ` -Correctif 'gh auth login (dans un terminal ou work a ete execute)' } if (-not $LoginReel) { return New-CtxCheck -Domaine 'gh' -Sujet 'jeton' -Verdict 'INFO' ` -Detail ('non verifie : ' + (Protect-CtxMessage $Erreur)) } if ($LoginAttendu -and $LoginReel -ne $LoginAttendu) { return New-CtxCheck -Domaine 'gh' -Sujet 'jeton' -Verdict 'PROBLEME' ` -Detail "le jeton ouvre le compte '$LoginReel', ce dossier attend '$LoginAttendu'" ` -Correctif 'GH_CONFIG_DIR pointe sur le mauvais contexte, ou ce contexte est connecte au mauvais compte' } $detail = "valide — $LoginReel" if ($Portees) { $detail += " (portees : $Portees)" } New-CtxCheck -Domaine 'gh' -Sujet 'jeton' -Verdict 'OK' -Detail $detail } function Test-CtxDoctorJetonSupabase { <# Listing the projects a token can see answers three questions at once: the token is valid, it belongs to a given account, and that account owns the project THIS folder is linked to. A whoami would only answer the first. #> param( [AllowNull()][string]$RefAttendu, [object[]]$Projets = @(), [int]$Code = 0, [AllowNull()][string]$Erreur ) if ($Code -eq 401 -or $Code -eq 403) { return New-CtxCheck -Domaine 'supabase' -Sujet 'jeton' -Verdict 'PROBLEME' ` -Detail "jeton refuse ($Code) : expire, revoque, ou sans droit sur l organisation" ` -Correctif 'regenerer le jeton sur supabase.com/dashboard/account/tokens, puis le poser dans le coffre' } if ($Code -eq 0 -and $Erreur) { return New-CtxCheck -Domaine 'supabase' -Sujet 'jeton' -Verdict 'INFO' ` -Detail ('non verifie : ' + (Protect-CtxMessage $Erreur)) } if (-not $RefAttendu) { return New-CtxCheck -Domaine 'supabase' -Sujet 'jeton' -Verdict 'OK' ` -Detail "valide — $($Projets.Count) projet(s) visible(s)" } $vise = @($Projets | Where-Object { (Get-CtxProp $_ 'id') -eq $RefAttendu }) | Select-Object -First 1 if (-not $vise) { # Ce cas EST le scenario redoute, dans l'autre sens : le jeton marche, # mais pas sur le projet de ce dossier. Une commande partirait alors sur # un compte reel, avec un message d'erreur qui ne dit pas pourquoi. return New-CtxCheck -Domaine 'supabase' -Sujet 'jeton' -Verdict 'PROBLEME' ` -Detail "jeton valide, mais le projet lie a ce dossier n y est pas visible" ` -Correctif 'sb-index — puis work <contexte> pour recharger la bonne cle' } New-CtxCheck -Domaine 'supabase' -Sujet 'jeton' -Verdict 'OK' ` -Detail "valide — acces confirme a $(Get-CtxProp $vise 'name' $RefAttendu)" } function Test-CtxDoctorJetonVercel { param( [AllowNull()][string]$Utilisateur, [int]$Code = 0, [AllowNull()][string]$Erreur ) if ($Code -eq 401 -or $Code -eq 403) { return New-CtxCheck -Domaine 'vercel' -Sujet 'jeton' -Verdict 'PROBLEME' ` -Detail "jeton refuse ($Code)" ` -Correctif 'vercel login (dans un terminal ou work a ete execute)' } if (-not $Utilisateur) { return New-CtxCheck -Domaine 'vercel' -Sujet 'jeton' -Verdict 'INFO' ` -Detail ('non verifie : ' + (Protect-CtxMessage $Erreur)) } New-CtxCheck -Domaine 'vercel' -Sujet 'jeton' -Verdict 'OK' -Detail "valide — $Utilisateur" } # --------------------------------------------------------------------------- # Network access # --------------------------------------------------------------------------- function Invoke-CtxApi { <# One read-only GET, with a timeout, returning a verdict-shaped object instead of throwing. The status code is what the decisions branch on, so it is carried separately from the message: 401 means the token is dead, a timeout means only that a train went into a tunnel. Treating both as failure would make the tool untrustworthy in exactly the situation where trust matters. #> param( [Parameter(Mandatory)][string]$Uri, [hashtable]$Headers = @{}, [int]$TimeoutSec = $script:JetonTimeout ) try { $data = Invoke-RestMethod -Uri $Uri -Headers $Headers -TimeoutSec $TimeoutSec ` -Method Get -ErrorAction Stop [pscustomobject]@{ Ok = $true; Data = $data; Code = 200; Erreur = $null } } catch { $code = 0 $reponse = $_.Exception.PSObject.Properties['Response'] if ($reponse -and $_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode } [pscustomobject]@{ Ok = $false; Data = $null; Code = $code Erreur = (Protect-CtxMessage $_.Exception.Message) } } } function Get-CtxJetonChecks { <# Probes every token loaded in this shell. Absent tokens are skipped in silence: the non-live pass has already said they are missing, and saying it twice trains the reader to skim. #> param( [AllowNull()]$Manifeste, [AllowNull()][string]$Ref ) $checks = [System.Collections.Generic.List[object]]::new() # --- GitHub ------------------------------------------------------------ # Via la CLI et non l'API brute : elle lit GH_CONFIG_DIR, donc elle prouve # ce qui compte vraiment — l'identite que `gh` utilisera reellement ici. # # LE BINAIRE REEL, JAMAIS L'ALIAS DU MODULE. Depuis la 1.4.0, `gh` designe # Invoke-DevGh dans toute session ayant importe le module -- et ce wrapper # CORRIGE GH_CONFIG_DIR avant d'appeler la CLI. Un diagnostic qui passerait # par lui mesurerait donc l'identite APRES correction : il repondrait « le # bon compte » sur une machine ou git-bash, lui, part toujours sur le # mauvais. Un diagnostic doit observer l'etat, jamais le reparer en chemin. $ghExe = try { Get-CtxGhExe } catch { $null } if ($ghExe) { $login = $null; $portees = $null; $code = 0; $erreur = $null $brut = & $ghExe api user --jq '.login' 2>&1 if ($LASTEXITCODE -eq 0) { $login = ([string]$brut).Trim() $entetes = & $ghExe api -i user 2>&1 | Select-String -Pattern '^X-OAuth-Scopes:' | Select-Object -First 1 if ($entetes) { $portees = ($entetes.Line -replace '^X-OAuth-Scopes:\s*', '').Trim() } } else { $erreur = Protect-CtxMessage (($brut | Out-String).Trim()) if ($erreur -match 'HTTP 401|Bad credentials') { $code = 401 } } $checks.Add((Test-CtxDoctorJetonGitHub ` -LoginAttendu (Get-CtxProp $Manifeste 'github.login') ` -LoginReel $login -Portees $portees -Code $code -Erreur $erreur)) } # --- Supabase ---------------------------------------------------------- if ($env:SUPABASE_ACCESS_TOKEN) { $r = Invoke-CtxApi -Uri 'https://api.supabase.com/v1/projects' ` -Headers @{ Authorization = "Bearer $env:SUPABASE_ACCESS_TOKEN" } $checks.Add((Test-CtxDoctorJetonSupabase -RefAttendu $Ref ` -Projets @($r.Data) -Code $r.Code -Erreur $r.Erreur)) } # --- Vercel ------------------------------------------------------------ if ($env:VERCEL_TOKEN) { $r = Invoke-CtxApi -Uri 'https://api.vercel.com/v2/user' ` -Headers @{ Authorization = "Bearer $env:VERCEL_TOKEN" } $utilisateur = if ($r.Ok) { Get-CtxProp $r.Data 'user.username' } else { $null } $checks.Add((Test-CtxDoctorJetonVercel -Utilisateur $utilisateur -Code $r.Code -Erreur $r.Erreur)) } $checks } |