upload-with-credentials.ps1 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  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. [ValidatePattern('^[a-z0-9-]+$')]
  33. [string]$Bucket,
  34. [Parameter(ParameterSetName = 'Publish', Mandatory = $true)]
  35. [switch]$Upload
  36. )
  37. $ErrorActionPreference = 'Stop'
  38. Set-StrictMode -Version Latest
  39. $child = $null
  40. $started = $false
  41. $secretId = $null
  42. $secretKey = $null
  43. $credentials = $null
  44. $startInfo = $null
  45. $stage = 'decrypt-file'
  46. try {
  47. if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
  48. throw 'Windows DPAPI is required.'
  49. }
  50. try {
  51. $credentials = Import-Clixml -LiteralPath $CredentialFile
  52. foreach ($field in @('SecretId', 'SecretKey')) {
  53. if ($credentials.$field -isnot [Security.SecureString] -or $credentials.$field.Length -eq 0) {
  54. throw 'Expected non-empty encrypted fields.'
  55. }
  56. }
  57. } catch {
  58. throw 'Cannot read encrypted COS credentials. Check the file and use its original Windows user and machine.'
  59. }
  60. $stage = 'prepare-node'
  61. $startInfo = New-Object Diagnostics.ProcessStartInfo
  62. $startInfo.FileName = (Get-Command node -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
  63. $startInfo.WorkingDirectory = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../..'))
  64. $startInfo.UseShellExecute = $false
  65. $startInfo.CreateNoWindow = $true
  66. $startInfo.RedirectStandardOutput = $true
  67. $startInfo.RedirectStandardError = $true
  68. foreach ($name in @($startInfo.EnvironmentVariables.Keys)) {
  69. # Node preload hooks and unrelated release secrets must not reach this credential-bearing process.
  70. if ($name -match 'KEY|SECRET|TOKEN|PASSWORD|^NODE_OPTIONS$|^DSH_DESKTOP_WINDOWS_|^APPLE_|^CSC_') {
  71. $startInfo.EnvironmentVariables.Remove($name)
  72. }
  73. }
  74. $stage = 'prepare-credentials'
  75. $prefix = if ($Environment -eq 'production') { 'DOWNLOAD_PROD_COS' } else { 'DOWNLOAD_TEST_COS' }
  76. $secretId = [Net.NetworkCredential]::new('', $credentials.SecretId).Password
  77. $secretKey = [Net.NetworkCredential]::new('', $credentials.SecretKey).Password
  78. if ([string]::IsNullOrWhiteSpace($secretId) -or [string]::IsNullOrWhiteSpace($secretKey)) {
  79. throw 'COS credential fields must not be blank.'
  80. }
  81. $startInfo.EnvironmentVariables["${prefix}_SECRET_ID"] = $secretId
  82. $startInfo.EnvironmentVariables["${prefix}_SECRET_KEY"] = $secretKey
  83. $startInfo.EnvironmentVariables['DSH_DESKTOP_AUTO_UPDATE_ENV'] = $Environment
  84. if ($Upload) {
  85. $startInfo.EnvironmentVariables["${prefix}_BUCKET"] = $Bucket
  86. $startInfo.Arguments = "--import tsx/esm apps/desktop/scripts/upload-target.ts $Target --credential-launcher --environment $Environment --bucket $Bucket"
  87. Write-Output "desktop credentials: uploading $Target to $Environment; release validation runs before network writes."
  88. } else {
  89. $probe = "const id=process.env.${prefix}_SECRET_ID;const key=process.env.${prefix}_SECRET_KEY;process.exit(id?.trim()&&key?.trim()?0:1)"
  90. $startInfo.Arguments = "-e `"$probe`""
  91. }
  92. $stage = 'run-node'
  93. $child = New-Object Diagnostics.Process
  94. $child.StartInfo = $startInfo
  95. $started = $child.Start()
  96. $stderr = $child.StandardError.ReadToEndAsync()
  97. while ($null -ne ($line = $child.StandardOutput.ReadLine())) {
  98. Write-Output $line.Replace($secretId, '[REDACTED]').Replace($secretKey, '[REDACTED]')
  99. }
  100. $child.WaitForExit()
  101. # SDK exception objects can include signed request details; do not forward raw stderr.
  102. $null = $stderr.GetAwaiter().GetResult()
  103. if ($child.ExitCode -ne 0) {
  104. throw "Node upload/check failed (exit $($child.ExitCode)); private diagnostics suppressed."
  105. }
  106. if (-not $Upload) {
  107. Write-Output 'desktop credentials: encrypted fields loaded; child environment verified; no network request made.'
  108. }
  109. } catch {
  110. # Import and process exceptions are not safe credential diagnostics.
  111. Write-Output "desktop credentials: failed; stage=$stage; line=$($_.InvocationInfo.ScriptLineNumber). Verify the encrypted file, Windows account, Node, and release inputs. No secrets printed."
  112. exit 1
  113. } finally {
  114. if ($started -and -not $child.HasExited) {
  115. $child.Kill()
  116. $child.WaitForExit()
  117. }
  118. if ($null -ne $child) { $child.Dispose() }
  119. if ($null -ne $startInfo) { $startInfo.EnvironmentVariables.Clear() }
  120. if ($null -ne $credentials) {
  121. foreach ($field in @('SecretId', 'SecretKey')) {
  122. if ($credentials.PSObject.Properties[$field] -and $credentials.$field -is [Security.SecureString]) {
  123. $credentials.$field.Dispose()
  124. }
  125. }
  126. }
  127. $secretId = $null
  128. $secretKey = $null
  129. }