# Takumi Guard token provisioning # Usage: $env:TG_BOT_API_KEY="..." ; .\takumi-guard-setup-{VERSION}.ps1 [SCOPES] # # Mints a new org user token and configures package managers to use Takumi Guard. # Designed to be called by an MDM deployment script. # # Supported: # npm ecosystem -- npm, pnpm, yarn v2+, bun # PyPI ecosystem -- pip, uv, poetry # # Arguments: # BOT_ID Required. Bot ID from Shisho Cloud console. # USER_IDENTIFIER Required. Unique device/user identifier. # Allowed characters: a-z, A-Z, 0-9, hyphen, underscore, dot. # Length: 4-255 characters. # SCOPES Optional. Comma-separated ecosystems to configure (default: npm,pypi). # # Environment: # TG_BOT_API_KEY Required. Bot API key from Shisho Cloud console. # Passed via env var to avoid shell history / process table exposure. # USER_HOME Optional. Override user home directory (for MDM wrapper scripts). # # Idempotency: # This script is safe to run multiple times. If a tg_org_* token is already # present in any config file, the script reuses it without minting a new token. # Config files for the specified scopes are still updated if needed. # # Backup and rollback: # Before modifying an existing config file, a timestamped backup is created # next to the original (e.g. .npmrc-backup-20260408-162351). These backups # are preserved even if the script succeeds, so you can manually restore the # previous state at any time by copying the backup file back. # # If the script fails midway through, all changes made so far are automatically # rolled back to the pre-execution state. No manual intervention is needed. # # Config file handling: # - If a config file already exists, it is updated regardless of whether the # corresponding tool is installed. Package managers do not remove their config # files on uninstall, and pre-placing config ensures Guard is active as soon # as the tool is (re)installed. # - If a config file does not exist, it is created only if the tool is installed. # - Non-Guard settings in existing config files are preserved. # # Prerequisites: # PowerShell 5.1 or later. param( [Parameter(Mandatory=$true, Position=0)][string]$BotId, [Parameter(Mandatory=$true, Position=1)][string]$UserIdentifier, [string]$Scopes = "npm,pypi" ) $ErrorActionPreference = "Stop" # --------------------------------------------------------------------------- # Argument validation # --------------------------------------------------------------------------- $ApiKey = $env:TG_BOT_API_KEY if ($env:USER_HOME) { $UserHome = $env:USER_HOME } else { $UserHome = $env:USERPROFILE } if (-not $ApiKey) { throw "Set TG_BOT_API_KEY environment variable" } if (-not $UserHome) { throw "Could not determine user home directory" } function Test-SafeString { param([string]$Label, [string]$Value) if ($Value -notmatch '^[0-9a-zA-Z._-]+$') { throw "[Error] $Label contains invalid characters" } } function Test-UserIdentifier { param([string]$Id) if ($Id.Length -lt 4 -or $Id.Length -gt 255) { throw "[Error] USER_IDENTIFIER must be 4-255 characters (got $($Id.Length))" } Test-SafeString "USER_IDENTIFIER" $Id } Test-UserIdentifier $UserIdentifier Test-SafeString "BOT_ID" $BotId Test-SafeString "TG_BOT_API_KEY" $ApiKey function Has-Scope { param([string]$Name) return (",$Scopes," -like "*,$Name,*") } # --------------------------------------------------------------------------- # Backup and rollback # --------------------------------------------------------------------------- $Timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $TmpBackupDir = Join-Path ([System.IO.Path]::GetTempPath()) "guard-backup-$Timestamp" New-Item -ItemType Directory -Path $TmpBackupDir -Force | Out-Null $TmpBackupFiles = @() $CreatedFiles = @() function Backup-File { param([string]$Path) if (Test-Path $Path) { # Persistent backup for manual rollback $dir = Split-Path $Path -Parent $name = Split-Path $Path -Leaf $backupPath = Join-Path $dir "$name-backup-$Timestamp" Copy-Item $Path $backupPath Write-Output "[Backup] Created $backupPath" # Temporary backup for auto-rollback Copy-Item $Path (Join-Path $TmpBackupDir $name) $script:TmpBackupFiles += $Path } } function Track-CreatedFile { param([string]$Path) $script:CreatedFiles += $Path } function Invoke-Rollback { Write-Output "[Error] setup.ps1 failed. Rolling back changes..." foreach ($src in $script:TmpBackupFiles) { $name = Split-Path $src -Leaf $backupFile = Join-Path $TmpBackupDir $name if (Test-Path $backupFile) { Copy-Item $backupFile $src -Force Write-Output "[Rollback] Restored $src" } } foreach ($src in $script:CreatedFiles) { if (Test-Path $src) { Remove-Item $src -Force Write-Output "[Rollback] Removed $src" } } Remove-Item $TmpBackupDir -Recurse -Force -ErrorAction SilentlyContinue } function Invoke-Cleanup { Remove-Item $TmpBackupDir -Recurse -Force -ErrorAction SilentlyContinue } try { # --------------------------------------------------------------------------- # Check for existing tg_org_* token # --------------------------------------------------------------------------- $Token = $null if ($env:XDG_CONFIG_HOME) { $xdgPnpmRc = Join-Path $env:XDG_CONFIG_HOME "pnpm\rc" } else { $xdgPnpmRc = Join-Path $UserHome "AppData\Local\pnpm\config\rc" } $checkFiles = @( (Join-Path $UserHome ".npmrc"), $xdgPnpmRc, (Join-Path $UserHome ".yarnrc.yml"), (Join-Path $UserHome ".bunfig.toml"), (Join-Path $UserHome "AppData\Roaming\pip\pip.ini"), (Join-Path $UserHome "AppData\Roaming\uv\uv.toml"), (Join-Path $UserHome "AppData\Roaming\pypoetry\auth.toml") ) foreach ($file in $checkFiles) { if (-not $Token -and (Test-Path $file)) { $match = Select-String -Path $file -Pattern 'tg_org_[A-Za-z0-9_-]+' -AllMatches | Select-Object -First 1 if ($match) { $Token = $match.Matches[0].Value } } } # --------------------------------------------------------------------------- # Pre-check: ensure at least one configurable tool or config file exists # --------------------------------------------------------------------------- # Without this check, a clean environment (no tools, no config files) would # mint a token on every run but never write it anywhere -- wasting tokens. $HasTarget = $false if (Has-Scope "npm") { if ((Test-Path (Join-Path $UserHome ".npmrc")) -or (Get-Command npm -ErrorAction SilentlyContinue)) { $HasTarget = $true } if (Get-Command pnpm -ErrorAction SilentlyContinue) { $HasTarget = $true } if (Test-Path (Join-Path $UserHome ".yarnrc.yml")) { $HasTarget = $true } if (Get-Command yarn -ErrorAction SilentlyContinue) { $HasTarget = $true } if (Test-Path (Join-Path $UserHome ".bunfig.toml")) { $HasTarget = $true } if (Get-Command bun -ErrorAction SilentlyContinue) { $HasTarget = $true } } if (Has-Scope "pypi") { if (Test-Path (Join-Path $UserHome "AppData\Roaming\pip\pip.ini")) { $HasTarget = $true } if ((Get-Command pip3 -ErrorAction SilentlyContinue) -or (Get-Command pip -ErrorAction SilentlyContinue)) { $HasTarget = $true } if ((Get-Command py -ErrorAction SilentlyContinue) -and (& py -m pip --version 2>$null)) { $HasTarget = $true } if (Test-Path (Join-Path $UserHome "AppData\Roaming\uv\uv.toml")) { $HasTarget = $true } if (Get-Command uv -ErrorAction SilentlyContinue) { $HasTarget = $true } if (Get-Command poetry -ErrorAction SilentlyContinue) { $HasTarget = $true } } if (-not $HasTarget) { Write-Output "[Done] No configurable tools found for scopes: $Scopes. Skipping token mint." Invoke-Cleanup exit 0 } if ($Token) { Write-Output "[Skip] Existing org token found, reusing" # Continue to configure scopes (don't exit -- allows incremental scope addition) } elseif ($env:TG_PREMINTED_TOKEN) { $Token = $env:TG_PREMINTED_TOKEN if ($Token -notmatch '^tg_org_[A-Za-z0-9_-]{20,}$') { throw "[Error] TG_PREMINTED_TOKEN format unexpected" } Write-Output "[OK] Using pre-minted token" } else { # --------------------------------------------------------------------------- # Mint a new org user token # --------------------------------------------------------------------------- $ApiUrl = "https://apiv2.cloud.shisho.dev/v1/guard/tokens/org-user" $body = @{ bot_id = $BotId api_key = $ApiKey user_identifier = $UserIdentifier } | ConvertTo-Json try { $response = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $body -ContentType "application/json" $Token = $response.token } catch { $statusCode = $_.Exception.Response.StatusCode.value__ Write-Output "[Error] Token API returned HTTP $statusCode" Write-Output $_.ErrorDetails.Message throw } if (-not $Token) { throw "[Error] Failed to extract token from API response" } # Validate token format if ($Token -notmatch '^tg_org_[A-Za-z0-9_-]{20,}$') { throw "[Error] Token format unexpected" } Write-Output "[OK] Token minted" } # --------------------------------------------------------------------------- # npm ecosystem # --------------------------------------------------------------------------- if (Has-Scope "npm") { # --- npm / pnpm (.npmrc) --- $npmrc = Join-Path $UserHome ".npmrc" if ((Test-Path $npmrc) -or (Get-Command npm -ErrorAction SilentlyContinue) -or (Get-Command pnpm -ErrorAction SilentlyContinue)) { if ((Test-Path $npmrc) -and (Select-String -Path $npmrc -Pattern 'tg_org_' -Quiet)) { Write-Output "[OK] npm already configured" } else { if (-not (Test-Path $npmrc)) { Track-CreatedFile $npmrc } Backup-File $npmrc if (Get-Command npm -ErrorAction SilentlyContinue) { $existingRegistry = & npm config get registry --location=user 2>$null if ($existingRegistry -and $existingRegistry -ne "https://npm.flatt.tech/" -and $existingRegistry -ne "https://registry.npmjs.org/" -and $existingRegistry -ne "undefined") { Write-Output "[WARN] Existing npm registry will be overwritten: $existingRegistry" } & npm config set "//npm.flatt.tech/:_authToken" $Token --userconfig $npmrc & npm config set registry "https://npm.flatt.tech/" --userconfig $npmrc } else { # npm not installed but .npmrc exists -- edit file directly if ((Test-Path $npmrc) -and (Select-String -Path $npmrc -Pattern "npm.flatt.tech" -Quiet)) { (Get-Content $npmrc) -replace '//npm.flatt.tech/:_authToken=.*', "//npm.flatt.tech/:_authToken=$Token" | Set-Content $npmrc } else { Add-Content -Path $npmrc -Value "registry=https://npm.flatt.tech/" Add-Content -Path $npmrc -Value "//npm.flatt.tech/:_authToken=$Token" } } Write-Output "[OK] npm configured" } # end tg_org_ check } else { Write-Output "[SKIP] npm not available" } # --- pnpm (global rc) --- if ($env:XDG_CONFIG_HOME) { $pnpmRcDir = Join-Path $env:XDG_CONFIG_HOME "pnpm" } else { $pnpmRcDir = Join-Path $UserHome "AppData\Local\pnpm\config" } $pnpmRc = Join-Path $pnpmRcDir "rc" if (Test-Path $pnpmRc) { if (Select-String -Path $pnpmRc -Pattern 'tg_org_' -Quiet) { Write-Output "[OK] pnpm already configured" } else { Backup-File $pnpmRc if (Select-String -Path $pnpmRc -Pattern "npm.flatt.tech" -Quiet) { (Get-Content $pnpmRc) -replace '//npm.flatt.tech/:_authToken=.*', "//npm.flatt.tech/:_authToken=$Token" | Set-Content $pnpmRc } else { Add-Content -Path $pnpmRc -Value "registry=https://npm.flatt.tech/" Add-Content -Path $pnpmRc -Value "//npm.flatt.tech/:_authToken=$Token" } Write-Output "[OK] pnpm configured" } } elseif (Get-Command pnpm -ErrorAction SilentlyContinue) { if (-not (Test-Path $pnpmRcDir)) { New-Item -ItemType Directory -Path $pnpmRcDir -Force | Out-Null } Set-Content -Path $pnpmRc -Value "registry=https://npm.flatt.tech/" Add-Content -Path $pnpmRc -Value "//npm.flatt.tech/:_authToken=$Token" Track-CreatedFile $pnpmRc Write-Output "[OK] pnpm configured" } else { Write-Output "[SKIP] pnpm not available" } # --- yarn v2+ (.yarnrc.yml) --- $yarnrc = Join-Path $UserHome ".yarnrc.yml" if (Test-Path $yarnrc) { if (Select-String -Path $yarnrc -Pattern 'tg_org_' -Quiet) { Write-Output "[OK] yarn already configured" } else { Backup-File $yarnrc if (Select-String -Path $yarnrc -Pattern "npm.flatt.tech" -Quiet) { if (Select-String -Path $yarnrc -Pattern "npmAuthToken" -Quiet) { (Get-Content $yarnrc) -replace 'npmAuthToken:.*', "npmAuthToken: `"$Token`"" | Set-Content $yarnrc } else { $content = Get-Content $yarnrc $newContent = @() foreach ($line in $content) { $newContent += $line if ($line -match 'npmRegistryServer:') { $newContent += "npmAuthToken: `"$Token`"" } } $newContent | Set-Content $yarnrc } } else { if (Select-String -Path $yarnrc -Pattern "npmRegistryServer" -Quiet) { (Get-Content $yarnrc) -replace 'npmRegistryServer:.*', "npmRegistryServer: `"https://npm.flatt.tech/`"" | Set-Content $yarnrc $content = Get-Content $yarnrc $newContent = @() foreach ($line in $content) { $newContent += $line if ($line -match 'npmRegistryServer:') { $newContent += "npmAuthToken: `"$Token`"" } } $newContent | Set-Content $yarnrc } else { Add-Content -Path $yarnrc -Value "" Add-Content -Path $yarnrc -Value "npmRegistryServer: `"https://npm.flatt.tech/`"" Add-Content -Path $yarnrc -Value "npmAuthToken: `"$Token`"" } } Write-Output "[OK] yarn configured" } # end tg_org_ check } elseif (Get-Command yarn -ErrorAction SilentlyContinue) { Set-Content -Path $yarnrc -Value "npmRegistryServer: `"https://npm.flatt.tech/`"" Add-Content -Path $yarnrc -Value "npmAuthToken: `"$Token`"" Track-CreatedFile $yarnrc Write-Output "[OK] yarn configured" } else { Write-Output "[SKIP] yarn not available" } # --- bun (.bunfig.toml) --- $bunfig = Join-Path $UserHome ".bunfig.toml" if (Test-Path $bunfig) { if (Select-String -Path $bunfig -Pattern 'tg_org_' -Quiet) { Write-Output "[OK] bun already configured" } else { Backup-File $bunfig if (Select-String -Path $bunfig -Pattern "npm.flatt.tech" -Quiet) { (Get-Content $bunfig) -replace '(npm\.flatt\.tech.*?token\s*=\s*)"[^"]*"', "`$1`"$Token`"" | Set-Content $bunfig } else { if (Select-String -Path $bunfig -Pattern '^\[install\]' -Quiet) { $content = Get-Content $bunfig $newContent = @() foreach ($line in $content) { $newContent += $line if ($line -match '^\[install\]') { $newContent += "registry = { url = `"https://npm.flatt.tech/`", token = `"$Token`" }" } } $newContent | Set-Content $bunfig } else { Add-Content -Path $bunfig -Value "" Add-Content -Path $bunfig -Value "[install]" Add-Content -Path $bunfig -Value "registry = { url = `"https://npm.flatt.tech/`", token = `"$Token`" }" } } Write-Output "[OK] bun configured" } # end tg_org_ check } elseif (Get-Command bun -ErrorAction SilentlyContinue) { Set-Content -Path $bunfig -Value "[install]" Add-Content -Path $bunfig -Value "registry = { url = `"https://npm.flatt.tech/`", token = `"$Token`" }" Track-CreatedFile $bunfig Write-Output "[OK] bun configured" } else { Write-Output "[SKIP] bun not available" } } # Has-Scope npm # --------------------------------------------------------------------------- # PyPI ecosystem # --------------------------------------------------------------------------- if (Has-Scope "pypi") { # --- pip (pip.ini on Windows) --- $pipDir = Join-Path $UserHome "AppData\Roaming\pip" $pipConf = Join-Path $pipDir "pip.ini" if (Test-Path $pipConf) { if (Select-String -Path $pipConf -Pattern 'tg_org_' -Quiet) { Write-Output "[OK] pip already configured" } else { Backup-File $pipConf if (Select-String -Path $pipConf -Pattern "pypi.flatt.tech" -Quiet) { (Get-Content $pipConf) -replace 'index-url = .*pypi\.flatt\.tech.*', "index-url = https://token:$Token@pypi.flatt.tech/simple/" | Set-Content $pipConf } else { if (Select-String -Path $pipConf -Pattern '^\[global\]' -Quiet) { $content = Get-Content $pipConf $newContent = @() foreach ($line in $content) { $newContent += $line if ($line -match '^\[global\]') { $newContent += "index-url = https://token:$Token@pypi.flatt.tech/simple/" } } $newContent | Set-Content $pipConf } else { Add-Content -Path $pipConf -Value "" Add-Content -Path $pipConf -Value "[global]" Add-Content -Path $pipConf -Value "index-url = https://token:$Token@pypi.flatt.tech/simple/" } } Write-Output "[OK] pip configured" } # end tg_org_ check } elseif ((Get-Command pip3 -ErrorAction SilentlyContinue) -or (Get-Command pip -ErrorAction SilentlyContinue) -or ((Get-Command py -ErrorAction SilentlyContinue) -and (& py -m pip --version 2>$null))) { if (-not (Test-Path $pipDir)) { New-Item -ItemType Directory -Path $pipDir -Force | Out-Null } Set-Content -Path $pipConf -Value "[global]" Add-Content -Path $pipConf -Value "index-url = https://token:$Token@pypi.flatt.tech/simple/" Track-CreatedFile $pipConf Write-Output "[OK] pip configured" } else { Write-Output "[SKIP] pip not available" } # --- uv --- $uvDir = Join-Path $UserHome "AppData\Roaming\uv" $uvConf = Join-Path $uvDir "uv.toml" if (Test-Path $uvConf) { if (Select-String -Path $uvConf -Pattern 'tg_org_' -Quiet) { Write-Output "[OK] uv already configured" } else { Backup-File $uvConf if (Select-String -Path $uvConf -Pattern "pypi.flatt.tech" -Quiet) { (Get-Content $uvConf) -replace 'url = ".*pypi\.flatt\.tech.*"', "url = `"https://token:$Token@pypi.flatt.tech/simple/`"" | Set-Content $uvConf } else { if (Select-String -Path $uvConf -Pattern 'default = true' -Quiet) { (Get-Content $uvConf) -replace 'default = true', 'default = false' | Set-Content $uvConf } Add-Content -Path $uvConf -Value "" Add-Content -Path $uvConf -Value "[[index]]" Add-Content -Path $uvConf -Value "url = `"https://token:$Token@pypi.flatt.tech/simple/`"" Add-Content -Path $uvConf -Value "default = true" } Write-Output "[OK] uv configured" } # end tg_org_ check } elseif (Get-Command uv -ErrorAction SilentlyContinue) { if (-not (Test-Path $uvDir)) { New-Item -ItemType Directory -Path $uvDir -Force | Out-Null } Set-Content -Path $uvConf -Value "[[index]]" Add-Content -Path $uvConf -Value "url = `"https://token:$Token@pypi.flatt.tech/simple/`"" Add-Content -Path $uvConf -Value "default = true" Track-CreatedFile $uvConf Write-Output "[OK] uv configured" } else { Write-Output "[SKIP] uv not available" } # --- poetry --- if (Get-Command poetry -ErrorAction SilentlyContinue) { $poetryAuth = Join-Path $UserHome "AppData\Roaming\pypoetry\auth.toml" if ((Test-Path $poetryAuth) -and (Select-String -Path $poetryAuth -Pattern 'tg_org_' -Quiet)) { Write-Output "[OK] poetry already configured" } else { # Disable keyring to prevent interactive prompts and errors in non-GUI environments $env:PYTHON_KEYRING_BACKEND = "keyring.backends.null.Keyring" & poetry config repositories.takumi-guard https://pypi.flatt.tech/simple/ & poetry config http-basic.takumi-guard token $Token Remove-Item Env:\PYTHON_KEYRING_BACKEND -ErrorAction SilentlyContinue Write-Output "[OK] poetry configured" } } else { Write-Output "[SKIP] poetry not available" } } # Has-Scope pypi Write-Output "[Done] Takumi Guard setup complete" } catch { Invoke-Rollback throw } finally { Invoke-Cleanup }