upload-with-credentials.ps1 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. #requires -Version 5.1
  2. <#
  3. .SYNOPSIS
  4. Checks Windows DPAPI credentials, or explicitly uploads one completed Desktop target.
  5. .DESCRIPTION
  6. Imports encrypted SecretId and SecretKey fields from an external CLIXML file.
  7. Only the Node child receives plaintext COS credentials. The default check starts
  8. a keyless-code probe without contacting COS; it does not verify cloud permissions.
  9. .PARAMETER CredentialFile
  10. Path to the CLIXML file created by the current Windows user on this machine.
  11. .PARAMETER Environment
  12. Deployment that owns the credential pair; never inferred from the filename.
  13. .PARAMETER Target
  14. Completed Desktop target to upload when Upload is explicitly selected.
  15. .PARAMETER Bucket
  16. COS bucket for an explicit upload. Not needed for the local credential check.
  17. .PARAMETER Upload
  18. Authorize the existing target upload entry. Omit to check credentials locally.
  19. #>
  20. [CmdletBinding(DefaultParameterSetName = 'Check')]
  21. param(
  22. [Parameter(Mandatory = $true)]
  23. [string]$CredentialFile,
  24. [Parameter(Mandatory = $true)]
  25. [ValidateSet('test', 'production')]
  26. [string]$Environment,
  27. [Parameter(ParameterSetName = 'Publish', Mandatory = $true)]
  28. [ValidateSet('win-x64', 'mac-x64', 'mac-arm64')]
  29. [string]$Target,
  30. [Parameter(ParameterSetName = 'Publish', Mandatory = $true)]
  31. [ValidateNotNullOrEmpty()]
  32. [string]$Bucket,
  33. [Parameter(ParameterSetName = 'Publish', Mandatory = $true)]
  34. [switch]$Upload
  35. )
  36. $ErrorActionPreference = 'Stop'
  37. Set-StrictMode -Version Latest
  38. $child = $null
  39. $started = $false
  40. $secretId = $null
  41. $secretKey = $null
  42. $credentials = $null
  43. $startInfo = $null
  44. $stage = 'decrypt-file'
  45. try {
  46. if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
  47. throw 'Windows DPAPI is required.'
  48. }
  49. try {
  50. $credentials = Import-Clixml -LiteralPath $CredentialFile
  51. foreach ($field in @('SecretId', 'SecretKey')) {
  52. if ($credentials.$field -isnot [Security.SecureString] -or $credentials.$field.Length -eq 0) {
  53. throw 'Expected non-empty encrypted fields.'
  54. }
  55. }
  56. } catch {
  57. throw 'Cannot read encrypted COS credentials. Check the file and use its original Windows user and machine.'
  58. }
  59. $stage = 'prepare-node'
  60. $startInfo = New-Object Diagnostics.ProcessStartInfo
  61. $startInfo.FileName = (Get-Command node -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
  62. $startInfo.WorkingDirectory = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../..'))
  63. $startInfo.UseShellExecute = $false
  64. $startInfo.CreateNoWindow = $true
  65. $startInfo.RedirectStandardOutput = $true
  66. $startInfo.RedirectStandardError = $true
  67. foreach ($name in @($startInfo.EnvironmentVariables.Keys)) {
  68. # Node preload hooks and unrelated release secrets must not reach this credential-bearing process.
  69. if ($name -match 'KEY|SECRET|TOKEN|PASSWORD|^NODE_OPTIONS$|^DSH_DESKTOP_WINDOWS_|^APPLE_|^CSC_') {
  70. $startInfo.EnvironmentVariables.Remove($name)
  71. }
  72. }
  73. $stage = 'prepare-credentials'
  74. $prefix = if ($Environment -eq 'production') { 'DOWNLOAD_PROD_COS' } else { 'DOWNLOAD_TEST_COS' }
  75. $secretId = [Net.NetworkCredential]::new('', $credentials.SecretId).Password
  76. $secretKey = [Net.NetworkCredential]::new('', $credentials.SecretKey).Password
  77. if ([string]::IsNullOrWhiteSpace($secretId) -or [string]::IsNullOrWhiteSpace($secretKey)) {
  78. throw 'COS credential fields must not be blank.'
  79. }
  80. $startInfo.EnvironmentVariables["${prefix}_SECRET_ID"] = $secretId
  81. $startInfo.EnvironmentVariables["${prefix}_SECRET_KEY"] = $secretKey
  82. $startInfo.EnvironmentVariables['DSH_DESKTOP_AUTO_UPDATE_ENV'] = $Environment
  83. if ($Upload) {
  84. $startInfo.EnvironmentVariables["${prefix}_BUCKET"] = $Bucket
  85. $startInfo.Arguments = "--import tsx/esm apps/desktop/scripts/upload-target.ts $Target"
  86. Write-Output "desktop credentials: uploading $Target to $Environment; release validation runs before network writes."
  87. } else {
  88. $probe = "const id=process.env.${prefix}_SECRET_ID;const key=process.env.${prefix}_SECRET_KEY;process.exit(id?.trim()&&key?.trim()?0:1)"
  89. $startInfo.Arguments = "-e `"$probe`""
  90. }
  91. $stage = 'run-node'
  92. $child = New-Object Diagnostics.Process
  93. $child.StartInfo = $startInfo
  94. $started = $child.Start()
  95. $stderr = $child.StandardError.ReadToEndAsync()
  96. while ($null -ne ($line = $child.StandardOutput.ReadLine())) {
  97. Write-Output $line.Replace($secretId, '[REDACTED]').Replace($secretKey, '[REDACTED]')
  98. }
  99. $child.WaitForExit()
  100. # SDK exception objects can include signed request details; do not forward raw stderr.
  101. $null = $stderr.GetAwaiter().GetResult()
  102. if ($child.ExitCode -ne 0) {
  103. throw "Node upload/check failed (exit $($child.ExitCode)); private diagnostics suppressed."
  104. }
  105. if (-not $Upload) {
  106. Write-Output 'desktop credentials: encrypted fields loaded; child environment verified; no network request made.'
  107. }
  108. } catch {
  109. # Import and process exceptions are not safe credential diagnostics.
  110. Write-Output "desktop credentials: failed; stage=$stage; line=$($_.InvocationInfo.ScriptLineNumber). Verify the encrypted file, Windows account, Node, and release inputs. No secrets printed."
  111. exit 1
  112. } finally {
  113. if ($started -and -not $child.HasExited) {
  114. $child.Kill()
  115. $child.WaitForExit()
  116. }
  117. if ($null -ne $child) { $child.Dispose() }
  118. if ($null -ne $startInfo) { $startInfo.EnvironmentVariables.Clear() }
  119. if ($null -ne $credentials) {
  120. foreach ($field in @('SecretId', 'SecretKey')) {
  121. if ($credentials.PSObject.Properties[$field] -and $credentials.$field -is [Security.SecureString]) {
  122. $credentials.$field.Dispose()
  123. }
  124. }
  125. }
  126. $secretId = $null
  127. $secretKey = $null
  128. }