claude-mode.ps1 was 2,407 lines. It is now 153: the help block, parameters, paths, the managed-key list, a loader, and the dispatch. The rest moved, verbatim, into eleven files under lib/ - providers, output, files, core, vault, switch, guards, health, catalogue, commands, menu - dot-sourced into the script's scope in their original order, with the same check as the bash split that every original line landed in exactly one file. Inside a module $PSScriptRoot is lib\, so the one path beside the main script (providers.json) now goes through $script:Here. install.ps1 ships lib\, clearing old modules first. The Windows suite parses every module and install.ps1 (36 checks, all green on Windows PowerShell 5.1); install.ps1 is parsed but never run, since it edits the real profile and User PATH. linux/bootstrap.sh and scripts/build-package.ps1 existed only to build and serve packages for the Arkylx Index. Both installers fetch the repository's own archive, so a push to master is the release; the two scripts, the dist/ ignore and their mentions in the docs are gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
86 lines
3.6 KiB
PowerShell
86 lines
3.6 KiB
PowerShell
# lib/vault.ps1 - the DPAPI key vault, and a preset's auth.
|
|
#
|
|
# Part of claude-mode.ps1, which dot-sources it into its own script scope after
|
|
# the settings at its top. Not meant to run on its own. ASCII only: Windows
|
|
# PowerShell 5.1 reads a .ps1 without a BOM as ANSI. $PSScriptRoot here would be
|
|
# lib\, so paths beside the main script go through $script:Here.
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Vault (DPAPI: CurrentUser scope)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Get-VaultPath { param([string] $Ref) return (Join-Path $script:VaultDir "$Ref.cred") }
|
|
|
|
function ConvertFrom-SecureStringPlain {
|
|
param([System.Security.SecureString] $Secure)
|
|
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secure)
|
|
try { return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) }
|
|
finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
|
|
}
|
|
|
|
function Set-VaultKey {
|
|
param([string] $Ref, [string] $Key = '')
|
|
Initialize-Root
|
|
if ($Key) {
|
|
Write-Warn2 'the key was given on the command line, so it is in this shell history - the hidden prompt leaves no trace'
|
|
$plain = $Key
|
|
# The vault is written from a SecureString (DPAPI), so the inline form
|
|
# has to produce one too.
|
|
$secure = ConvertTo-SecureString -String $Key -AsPlainText -Force
|
|
} else {
|
|
Write-Host "Paste the API key for ref '$Ref' (input hidden):"
|
|
$secure = Read-Host -AsSecureString
|
|
$plain = ConvertFrom-SecureStringPlain $secure
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($plain)) { throw 'empty key, aborted' }
|
|
|
|
# A hidden prompt will happily swallow a mis-paste. Guard the two shapes that
|
|
# are never a real key, because the failure is otherwise invisible until the
|
|
# provider answers 401 and the UI just spins.
|
|
if ($plain -match '\s') {
|
|
throw "that value contains whitespace, so it is not an API key (a pasted command line?). Nothing was stored."
|
|
}
|
|
if ($plain -like 'claude-mode*') {
|
|
throw "that value is a claude-mode command, not an API key. Nothing was stored."
|
|
}
|
|
if ($plain.Length -lt 16) {
|
|
$unit = if ($plain.Length -eq 1) { 'character' } else { 'characters' }
|
|
Write-Warn2 "that key is only $($plain.Length) $unit - unusually short. Storing anyway."
|
|
}
|
|
if ($Ref -eq 'openrouter' -and $plain -notlike 'sk-or-*') {
|
|
Write-Warn2 "key does not start with 'sk-or-' - storing anyway"
|
|
}
|
|
|
|
$path = Get-VaultPath $Ref
|
|
# ConvertFrom-SecureString with no -Key uses DPAPI, CurrentUser scope.
|
|
ConvertFrom-SecureString -SecureString $secure |
|
|
Set-Content -LiteralPath $path -Encoding ASCII -NoNewline
|
|
Protect-FileAcl $path
|
|
Write-Ok "stored DPAPI-encrypted key at $path"
|
|
}
|
|
|
|
function Get-VaultKey {
|
|
param([string] $Ref)
|
|
$path = Get-VaultPath $Ref
|
|
if (-not (Test-Path -LiteralPath $path)) { return $null }
|
|
$blob = (Get-Content -LiteralPath $path -Raw).Trim()
|
|
if ([string]::IsNullOrWhiteSpace($blob)) { return $null }
|
|
try { return ConvertFrom-SecureStringPlain (ConvertTo-SecureString $blob) }
|
|
catch { return $null }
|
|
}
|
|
|
|
function Format-KeyMask {
|
|
param([string] $Key)
|
|
if ([string]::IsNullOrEmpty($Key)) { return '(none)' }
|
|
if ($Key.Length -le 12) { return '****' }
|
|
return ($Key.Substring(0, 8) + '...' + $Key.Substring($Key.Length - 4))
|
|
}
|
|
|
|
function Get-PresetAuth {
|
|
param($Preset)
|
|
$auth = $Preset['auth']
|
|
if ($null -eq $auth) { $auth = [ordered]@{ mode = 'vault'; keyRef = 'openrouter' } }
|
|
if (-not $auth.Contains('mode')) { $auth['mode'] = 'vault' }
|
|
return $auth
|
|
}
|