@rover-studio/answer-me 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/answerme-toolkit.mjs +6 -0
- package/distribution/npm/migrations.json +605 -0
- package/distribution/npm/package-manifest.json +192 -0
- package/distribution/npm/skills/answerme/SKILL.md +94 -0
- package/distribution/npm/skills/answerme/agents/openai.yaml +4 -0
- package/distribution/npm/skills/answerme/references/api.md +135 -0
- package/distribution/npm/skills/answerme/references/creator-credential-deployment.md +19 -0
- package/distribution/npm/skills/answerme/references/creator-credential-recovery.md +24 -0
- package/distribution/npm/skills/answerme/references/errors.md +44 -0
- package/distribution/npm/skills/answerme/references/handoff.md +46 -0
- package/distribution/npm/skills/answerme/references/install-self-test.md +28 -0
- package/distribution/npm/skills/answerme/references/result-token-store.md +50 -0
- package/distribution/npm/skills/answerme/references/templates.md +139 -0
- package/distribution/npm/skills/answerme/scripts/answerme-api-base-url.ps1 +40 -0
- package/distribution/npm/skills/answerme/scripts/create-answerme.ps1 +1892 -0
- package/distribution/npm/skills/answerme/scripts/creator-credential-store.windows.ps1 +503 -0
- package/distribution/npm/skills/answerme/scripts/deploy-answerme-creator-credential.ps1 +447 -0
- package/distribution/npm/skills/answerme/scripts/enroll-answerme-creator.ps1 +764 -0
- package/distribution/npm/skills/answerme/scripts/open-answerme-page.windows.ps1 +272 -0
- package/distribution/npm/skills/answerme/scripts/remove-answerme-result-token.ps1 +63 -0
- package/distribution/npm/skills/answerme/scripts/result-token-store.windows.ps1 +261 -0
- package/distribution/npm/skills/answerme/scripts/test-answerme-installation.ps1 +498 -0
- package/distribution/npm/skills/answerme/scripts/wait-answerme-result.ps1 +908 -0
- package/distribution/npm/skills/answerme/scripts/windows-crypto.ps1 +57 -0
- package/distribution/npm/skills/answerme/scripts/windows-http.ps1 +45 -0
- package/distribution/npm/skills/answerme/scripts/windows-process-start-info.ps1 +76 -0
- package/distribution/npm/skills/ask-when-needed/SKILL.md +164 -0
- package/distribution/npm/skills/ask-when-needed/agents/openai.yaml +4 -0
- package/distribution/npm/skills/ask-when-needed/references/interview-strategies.md +43 -0
- package/lib/npm-cli/commands.mjs +247 -0
- package/lib/npm-cli/constants.mjs +51 -0
- package/lib/npm-cli/errors.mjs +15 -0
- package/lib/npm-cli/filesystem.mjs +193 -0
- package/lib/npm-cli/host-discovery.mjs +404 -0
- package/lib/npm-cli/main.mjs +42 -0
- package/lib/npm-cli/package-integrity.mjs +212 -0
- package/lib/npm-cli/transaction.mjs +375 -0
- package/lib/npm-cli/usage-validation.mjs +349 -0
- package/package.json +17 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
[CmdletBinding()]
|
|
2
|
+
param(
|
|
3
|
+
[Parameter(Mandatory = $true)][string]$HandoffCredentialPath,
|
|
4
|
+
[Parameter(Mandatory = $true)][string]$CreatorId,
|
|
5
|
+
[Parameter(Mandatory = $true)][string]$ApiBaseUrl,
|
|
6
|
+
[string]$TargetCredentialPath,
|
|
7
|
+
[switch]$VerifyRemote,
|
|
8
|
+
[switch]$AllowInsecureLoopbackHttpForTest,
|
|
9
|
+
[string]$CredentialStoreAdapterPath = (Join-Path $PSScriptRoot 'creator-credential-store.windows.ps1'),
|
|
10
|
+
[switch]$AllowTestAdapter
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
$ErrorActionPreference = 'Stop'
|
|
14
|
+
$cryptoSupport = Join-Path $PSScriptRoot 'windows-crypto.ps1'
|
|
15
|
+
if (-not (Test-Path -LiteralPath $cryptoSupport -PathType Leaf)) {
|
|
16
|
+
throw 'The AnswerMe Windows cryptography helper is missing.'
|
|
17
|
+
}
|
|
18
|
+
. $cryptoSupport
|
|
19
|
+
$httpSupport = Join-Path $PSScriptRoot 'windows-http.ps1'
|
|
20
|
+
if (-not (Test-Path -LiteralPath $httpSupport -PathType Leaf)) {
|
|
21
|
+
throw 'The AnswerMe Windows HTTP helper is missing.'
|
|
22
|
+
}
|
|
23
|
+
. $httpSupport
|
|
24
|
+
$networkCalled = $false
|
|
25
|
+
$targetChanged = $false
|
|
26
|
+
$backupCreated = $false
|
|
27
|
+
$rollbackStatus = 'not-required'
|
|
28
|
+
$handoffRemoved = $false
|
|
29
|
+
$verifiedRemotely = $false
|
|
30
|
+
$remoteValid = $null
|
|
31
|
+
$remoteReason = $null
|
|
32
|
+
$safeCreatorId = $null
|
|
33
|
+
$handoffHash = $null
|
|
34
|
+
$handoffCredential = $null
|
|
35
|
+
$resolvedHandoffPath = $null
|
|
36
|
+
$resolvedTargetPath = $null
|
|
37
|
+
$cleanupQuarantinePath = $null
|
|
38
|
+
$handoffQuarantined = $false
|
|
39
|
+
|
|
40
|
+
function Write-AnswerMeCreatorCredentialDeployFailure {
|
|
41
|
+
param(
|
|
42
|
+
[Parameter(Mandatory = $true)][string]$Stage,
|
|
43
|
+
[Parameter(Mandatory = $true)][string]$Code,
|
|
44
|
+
[Parameter(Mandatory = $true)][string]$Message,
|
|
45
|
+
[Parameter(Mandatory = $true)][int]$ExitCode
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
[PSCustomObject]@{
|
|
49
|
+
ok = $false
|
|
50
|
+
status = 'failed'
|
|
51
|
+
stage = $Stage
|
|
52
|
+
code = $Code
|
|
53
|
+
message = $Message
|
|
54
|
+
creatorId = if ($VerifyRemote -and $remoteValid -ne $true) { $null } else { $safeCreatorId }
|
|
55
|
+
targetChanged = $targetChanged
|
|
56
|
+
backupCreated = $backupCreated
|
|
57
|
+
rollbackStatus = $rollbackStatus
|
|
58
|
+
handoffRemoved = $handoffRemoved
|
|
59
|
+
verifiedRemotely = $verifiedRemotely
|
|
60
|
+
valid = $remoteValid
|
|
61
|
+
reason = $remoteReason
|
|
62
|
+
networkCalled = $networkCalled
|
|
63
|
+
} | ConvertTo-Json -Compress
|
|
64
|
+
exit $ExitCode
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Get-AnswerMeCreatorCredentialDeployExitCode {
|
|
68
|
+
param([Parameter(Mandatory = $true)][string]$Stage)
|
|
69
|
+
|
|
70
|
+
switch ($Stage) {
|
|
71
|
+
'preflight' { return 2 }
|
|
72
|
+
'install' { return 3 }
|
|
73
|
+
'verify' { return 4 }
|
|
74
|
+
'cleanup' { return 5 }
|
|
75
|
+
'rollback' { return 6 }
|
|
76
|
+
default { return 3 }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function Get-AnswerMeCreatorCredentialDeployDataValue {
|
|
81
|
+
param(
|
|
82
|
+
[Parameter(Mandatory = $true)][Exception]$Exception,
|
|
83
|
+
[Parameter(Mandatory = $true)][string]$Name,
|
|
84
|
+
[AllowNull()][object]$Default
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if ($Exception.Data.Contains($Name)) {
|
|
88
|
+
return $Exception.Data[$Name]
|
|
89
|
+
}
|
|
90
|
+
return $Default
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function Test-AnswerMeCreatorCredentialDeployAllowedProperties {
|
|
94
|
+
param(
|
|
95
|
+
[Parameter(Mandatory = $true)][object]$Value,
|
|
96
|
+
[Parameter(Mandatory = $true)][string[]]$Allowed
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
$actual = @($Value.PSObject.Properties.Name)
|
|
100
|
+
return $actual.Count -eq $Allowed.Count -and
|
|
101
|
+
@($actual | Where-Object { $Allowed -cnotcontains $_ }).Count -eq 0
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function Invoke-AnswerMeCreatorCredentialRemoteVerification {
|
|
105
|
+
param(
|
|
106
|
+
[Parameter(Mandatory = $true)][string]$NormalizedApiBaseUrl,
|
|
107
|
+
[Parameter(Mandatory = $true)][string]$ExpectedCreatorId,
|
|
108
|
+
[Parameter(Mandatory = $true)][PSCredential]$Credential
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
$plainKey = $null
|
|
112
|
+
try {
|
|
113
|
+
$plainKey = $Credential.GetNetworkCredential().Password
|
|
114
|
+
if ([string]::IsNullOrEmpty($plainKey)) {
|
|
115
|
+
throw 'The handoff credential is empty.'
|
|
116
|
+
}
|
|
117
|
+
$script:networkCalled = $true
|
|
118
|
+
$response = Invoke-AnswerMeWebRequest `
|
|
119
|
+
-Uri ($NormalizedApiBaseUrl + '/api/v1/creator-credentials/verify') `
|
|
120
|
+
-Method Get `
|
|
121
|
+
-Headers @{
|
|
122
|
+
Accept = 'application/json'
|
|
123
|
+
Authorization = "Bearer $plainKey"
|
|
124
|
+
} `
|
|
125
|
+
-TimeoutSec 30
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
throw (New-AnswerMeCreatorCredentialStoreException `
|
|
129
|
+
-Code 'creator-credential-verification-unavailable' `
|
|
130
|
+
-Stage 'verify' `
|
|
131
|
+
-Message 'Remote Creator credential verification was unavailable.')
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
$plainKey = $null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
$cacheControl = ([string]$response.Headers['Cache-Control']).Trim()
|
|
138
|
+
try {
|
|
139
|
+
$body = $response.Content | ConvertFrom-Json
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
throw (New-AnswerMeCreatorCredentialStoreException `
|
|
143
|
+
-Code 'creator-credential-verification-invalid-response' `
|
|
144
|
+
-Stage 'verify' `
|
|
145
|
+
-Message 'Remote Creator credential verification returned an invalid response.')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if ([int]$response.StatusCode -eq 503) {
|
|
149
|
+
if ($cacheControl -cne 'no-store' -or
|
|
150
|
+
$body -isnot [PSCustomObject] -or
|
|
151
|
+
-not (Test-AnswerMeCreatorCredentialDeployAllowedProperties `
|
|
152
|
+
-Value $body `
|
|
153
|
+
-Allowed @('code', 'requestId')) -or
|
|
154
|
+
$body.code -isnot [string] -or
|
|
155
|
+
[string]$body.code -cne 'service_unavailable' -or
|
|
156
|
+
$body.requestId -isnot [string] -or
|
|
157
|
+
[string]::IsNullOrWhiteSpace([string]$body.requestId)) {
|
|
158
|
+
throw (New-AnswerMeCreatorCredentialStoreException `
|
|
159
|
+
-Code 'creator-credential-verification-invalid-response' `
|
|
160
|
+
-Stage 'verify' `
|
|
161
|
+
-Message 'Remote Creator credential verification returned an invalid response.')
|
|
162
|
+
}
|
|
163
|
+
throw (New-AnswerMeCreatorCredentialStoreException `
|
|
164
|
+
-Code 'creator-credential-verification-unavailable' `
|
|
165
|
+
-Stage 'verify' `
|
|
166
|
+
-Message 'Remote Creator credential verification was unavailable.')
|
|
167
|
+
}
|
|
168
|
+
if ([int]$response.StatusCode -ne 200) {
|
|
169
|
+
throw (New-AnswerMeCreatorCredentialStoreException `
|
|
170
|
+
-Code 'creator-credential-verification-unavailable' `
|
|
171
|
+
-Stage 'verify' `
|
|
172
|
+
-Message 'Remote Creator credential verification was unavailable.')
|
|
173
|
+
}
|
|
174
|
+
if ($cacheControl -cne 'no-store') {
|
|
175
|
+
throw (New-AnswerMeCreatorCredentialStoreException `
|
|
176
|
+
-Code 'creator-credential-verification-invalid-response' `
|
|
177
|
+
-Stage 'verify' `
|
|
178
|
+
-Message 'Remote Creator credential verification returned an invalid response.')
|
|
179
|
+
}
|
|
180
|
+
if ($body -isnot [PSCustomObject] -or
|
|
181
|
+
$body.valid -isnot [bool] -or
|
|
182
|
+
$body.requestId -isnot [string] -or
|
|
183
|
+
[string]::IsNullOrWhiteSpace($body.requestId)) {
|
|
184
|
+
throw (New-AnswerMeCreatorCredentialStoreException `
|
|
185
|
+
-Code 'creator-credential-verification-invalid-response' `
|
|
186
|
+
-Stage 'verify' `
|
|
187
|
+
-Message 'Remote Creator credential verification returned an invalid response.')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if ($body.valid) {
|
|
191
|
+
if (-not (Test-AnswerMeCreatorCredentialDeployAllowedProperties `
|
|
192
|
+
-Value $body `
|
|
193
|
+
-Allowed @('valid', 'creatorId', 'reason', 'requestId')) -or
|
|
194
|
+
$body.creatorId -isnot [string] -or
|
|
195
|
+
$body.reason -isnot [string] -or
|
|
196
|
+
[string]::IsNullOrWhiteSpace($body.creatorId) -or
|
|
197
|
+
$body.creatorId -cne $ExpectedCreatorId -or
|
|
198
|
+
$body.reason -cne 'active') {
|
|
199
|
+
throw (New-AnswerMeCreatorCredentialStoreException `
|
|
200
|
+
-Code 'creator-credential-verification-invalid-response' `
|
|
201
|
+
-Stage 'verify' `
|
|
202
|
+
-Message 'Remote Creator credential verification returned an invalid response.')
|
|
203
|
+
}
|
|
204
|
+
return [PSCustomObject]@{
|
|
205
|
+
creatorId = $body.creatorId
|
|
206
|
+
valid = $true
|
|
207
|
+
reason = 'active'
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (-not (Test-AnswerMeCreatorCredentialDeployAllowedProperties `
|
|
212
|
+
-Value $body `
|
|
213
|
+
-Allowed @('valid', 'reason', 'requestId')) -or
|
|
214
|
+
$body.reason -isnot [string] -or
|
|
215
|
+
$body.reason -cnotin @('invalid', 'idle_expired', 'max_lifetime_expired', 'abuse_revoked')) {
|
|
216
|
+
throw (New-AnswerMeCreatorCredentialStoreException `
|
|
217
|
+
-Code 'creator-credential-verification-invalid-response' `
|
|
218
|
+
-Stage 'verify' `
|
|
219
|
+
-Message 'Remote Creator credential verification returned an invalid response.')
|
|
220
|
+
}
|
|
221
|
+
return [PSCustomObject]@{
|
|
222
|
+
creatorId = $null
|
|
223
|
+
valid = $false
|
|
224
|
+
reason = $body.reason
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
|
|
230
|
+
throw 'The Creator credential deployment command requires Windows.'
|
|
231
|
+
}
|
|
232
|
+
$defaultAdapterPath = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot 'creator-credential-store.windows.ps1'))
|
|
233
|
+
$resolvedAdapterPath = [IO.Path]::GetFullPath($CredentialStoreAdapterPath)
|
|
234
|
+
if (-not $AllowTestAdapter -and
|
|
235
|
+
-not $resolvedAdapterPath.Equals($defaultAdapterPath, [StringComparison]::OrdinalIgnoreCase)) {
|
|
236
|
+
throw 'A non-default Credential Store adapter is allowed only for isolated tests.'
|
|
237
|
+
}
|
|
238
|
+
if (-not (Test-Path -LiteralPath $resolvedAdapterPath -PathType Leaf)) {
|
|
239
|
+
throw 'The Creator Credential Store adapter is unavailable.'
|
|
240
|
+
}
|
|
241
|
+
. $resolvedAdapterPath
|
|
242
|
+
foreach ($requiredFunction in @(
|
|
243
|
+
'Install-AnswerMeCreatorCredential',
|
|
244
|
+
'Resolve-AnswerMeCreatorCredentialApiBaseUrl',
|
|
245
|
+
'Get-AnswerMeDefaultCreatorCredentialPath',
|
|
246
|
+
'Get-AnswerMeCreatorCredentialFileHash',
|
|
247
|
+
'Test-AnswerMeCreatorCredentialBytesEqual',
|
|
248
|
+
'Test-AnswerMeCreatorCredentialSecureEqual',
|
|
249
|
+
'New-AnswerMeCreatorCredentialStoreException'
|
|
250
|
+
)) {
|
|
251
|
+
if (-not (Get-Command -Name $requiredFunction -CommandType Function -ErrorAction SilentlyContinue)) {
|
|
252
|
+
throw 'The Creator Credential Store adapter contract is incomplete.'
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
$safeCreatorId = $CreatorId.Trim()
|
|
257
|
+
if ([string]::IsNullOrWhiteSpace($safeCreatorId)) {
|
|
258
|
+
throw 'Creator ID is required.'
|
|
259
|
+
}
|
|
260
|
+
$normalizedApiBaseUrl = Resolve-AnswerMeCreatorCredentialApiBaseUrl `
|
|
261
|
+
-ApiBaseUrl $ApiBaseUrl `
|
|
262
|
+
-AllowInsecureLoopbackHttpForTest:$AllowInsecureLoopbackHttpForTest
|
|
263
|
+
|
|
264
|
+
$resolvedHandoffPath = [IO.Path]::GetFullPath($HandoffCredentialPath)
|
|
265
|
+
if ([IO.Path]::GetExtension($resolvedHandoffPath) -cne '.clixml' -or
|
|
266
|
+
-not (Test-Path -LiteralPath $resolvedHandoffPath -PathType Leaf)) {
|
|
267
|
+
throw 'The protected handoff credential is unavailable or has an unknown format.'
|
|
268
|
+
}
|
|
269
|
+
if ([string]::IsNullOrWhiteSpace($TargetCredentialPath)) {
|
|
270
|
+
$TargetCredentialPath = Get-AnswerMeDefaultCreatorCredentialPath
|
|
271
|
+
}
|
|
272
|
+
$resolvedTargetPath = [IO.Path]::GetFullPath($TargetCredentialPath)
|
|
273
|
+
if ([IO.Path]::GetExtension($resolvedTargetPath) -cne '.clixml') {
|
|
274
|
+
throw 'The Creator credential target must be a .clixml file.'
|
|
275
|
+
}
|
|
276
|
+
if ($resolvedHandoffPath.Equals($resolvedTargetPath, [StringComparison]::OrdinalIgnoreCase)) {
|
|
277
|
+
throw 'The handoff and target credential files must be different.'
|
|
278
|
+
}
|
|
279
|
+
$handoffRoot = [IO.Path]::GetPathRoot($resolvedHandoffPath)
|
|
280
|
+
$targetRoot = [IO.Path]::GetPathRoot($resolvedTargetPath)
|
|
281
|
+
if ([string]::IsNullOrWhiteSpace($handoffRoot) -or
|
|
282
|
+
[string]::IsNullOrWhiteSpace($targetRoot) -or
|
|
283
|
+
-not $handoffRoot.Equals($targetRoot, [StringComparison]::OrdinalIgnoreCase)) {
|
|
284
|
+
throw 'The handoff and target credential files must be on the same volume.'
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
$handoffCredential = Import-Clixml -LiteralPath $resolvedHandoffPath
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
throw 'The protected handoff credential cannot be read by this Windows account.'
|
|
292
|
+
}
|
|
293
|
+
if ($handoffCredential -isnot [PSCredential] -or
|
|
294
|
+
$handoffCredential.UserName -cne $safeCreatorId -or
|
|
295
|
+
$handoffCredential.Password.Length -eq 0) {
|
|
296
|
+
throw 'The protected handoff credential has an unknown format or account mismatch.'
|
|
297
|
+
}
|
|
298
|
+
[byte[]]$handoffHash = Get-AnswerMeCreatorCredentialFileHash -LiteralPath $resolvedHandoffPath
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
Write-AnswerMeCreatorCredentialDeployFailure `
|
|
302
|
+
-Stage 'preflight' `
|
|
303
|
+
-Code 'credential-deploy-preflight-failed' `
|
|
304
|
+
-Message 'Creator credential deployment preflight failed.' `
|
|
305
|
+
-ExitCode 2
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if ($VerifyRemote) {
|
|
309
|
+
try {
|
|
310
|
+
$remoteVerification = Invoke-AnswerMeCreatorCredentialRemoteVerification `
|
|
311
|
+
-NormalizedApiBaseUrl $normalizedApiBaseUrl `
|
|
312
|
+
-ExpectedCreatorId $safeCreatorId `
|
|
313
|
+
-Credential $handoffCredential
|
|
314
|
+
$verifiedRemotely = $true
|
|
315
|
+
$remoteValid = [bool]$remoteVerification.valid
|
|
316
|
+
$remoteReason = [string]$remoteVerification.reason
|
|
317
|
+
if (-not $remoteValid) {
|
|
318
|
+
Write-AnswerMeCreatorCredentialDeployFailure `
|
|
319
|
+
-Stage 'verify' `
|
|
320
|
+
-Code 'creator-credential-invalid' `
|
|
321
|
+
-Message 'Remote verification rejected the Creator credential.' `
|
|
322
|
+
-ExitCode 4
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
$stage = [string](Get-AnswerMeCreatorCredentialDeployDataValue `
|
|
327
|
+
-Exception $_.Exception -Name 'AnswerMeStage' -Default 'verify')
|
|
328
|
+
$code = [string](Get-AnswerMeCreatorCredentialDeployDataValue `
|
|
329
|
+
-Exception $_.Exception -Name 'AnswerMeCode' -Default 'creator-credential-verification-failed')
|
|
330
|
+
Write-AnswerMeCreatorCredentialDeployFailure `
|
|
331
|
+
-Stage $stage `
|
|
332
|
+
-Code $code `
|
|
333
|
+
-Message 'Remote Creator credential verification failed.' `
|
|
334
|
+
-ExitCode 4
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
$installResult = Install-AnswerMeCreatorCredential `
|
|
340
|
+
-CreatorKey $handoffCredential.Password `
|
|
341
|
+
-ApiBaseUrl $normalizedApiBaseUrl `
|
|
342
|
+
-CreatorId $safeCreatorId `
|
|
343
|
+
-TargetCredentialPath $resolvedTargetPath `
|
|
344
|
+
-AllowInsecureLoopbackHttpForTest:$AllowInsecureLoopbackHttpForTest
|
|
345
|
+
$targetChanged = [bool]$installResult.targetChanged
|
|
346
|
+
$backupCreated = [bool]$installResult.backupCreated
|
|
347
|
+
$rollbackStatus = [string]$installResult.rollbackStatus
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
$stage = [string](Get-AnswerMeCreatorCredentialDeployDataValue `
|
|
351
|
+
-Exception $_.Exception -Name 'AnswerMeStage' -Default 'install')
|
|
352
|
+
$code = [string](Get-AnswerMeCreatorCredentialDeployDataValue `
|
|
353
|
+
-Exception $_.Exception -Name 'AnswerMeCode' -Default 'credential-install-failed')
|
|
354
|
+
$targetChanged = [bool](Get-AnswerMeCreatorCredentialDeployDataValue `
|
|
355
|
+
-Exception $_.Exception -Name 'AnswerMeTargetChanged' -Default $false)
|
|
356
|
+
$backupCreated = [bool](Get-AnswerMeCreatorCredentialDeployDataValue `
|
|
357
|
+
-Exception $_.Exception -Name 'AnswerMeBackupCreated' -Default $false)
|
|
358
|
+
$rollbackStatus = [string](Get-AnswerMeCreatorCredentialDeployDataValue `
|
|
359
|
+
-Exception $_.Exception -Name 'AnswerMeRollbackStatus' -Default 'not-required')
|
|
360
|
+
Write-AnswerMeCreatorCredentialDeployFailure `
|
|
361
|
+
-Stage $stage `
|
|
362
|
+
-Code $code `
|
|
363
|
+
-Message 'Creator credential installation failed.' `
|
|
364
|
+
-ExitCode (Get-AnswerMeCreatorCredentialDeployExitCode -Stage $stage)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
if (-not (Test-Path -LiteralPath $resolvedHandoffPath -PathType Leaf)) {
|
|
369
|
+
throw 'The protected handoff credential changed before cleanup.'
|
|
370
|
+
}
|
|
371
|
+
$handoffParent = [IO.Path]::GetDirectoryName($resolvedHandoffPath)
|
|
372
|
+
$cleanupQuarantinePath = Join-Path $handoffParent ('.answerme-{0}.handoff-cleanup.clixml' -f [Guid]::NewGuid().ToString('N'))
|
|
373
|
+
[IO.File]::Move($resolvedHandoffPath, $cleanupQuarantinePath)
|
|
374
|
+
$handoffQuarantined = $true
|
|
375
|
+
|
|
376
|
+
[byte[]]$currentHandoffHash = Get-AnswerMeCreatorCredentialFileHash -LiteralPath $cleanupQuarantinePath
|
|
377
|
+
try {
|
|
378
|
+
if (-not (Test-AnswerMeCreatorCredentialBytesEqual -Left $handoffHash -Right $currentHandoffHash)) {
|
|
379
|
+
throw 'The protected handoff credential changed before cleanup.'
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
Clear-AnswerMeSensitiveByteArray -Bytes $currentHandoffHash
|
|
384
|
+
}
|
|
385
|
+
$cleanupCredential = Import-Clixml -LiteralPath $cleanupQuarantinePath
|
|
386
|
+
if ($cleanupCredential -isnot [PSCredential] -or
|
|
387
|
+
$cleanupCredential.UserName -cne $safeCreatorId -or
|
|
388
|
+
-not (Test-AnswerMeCreatorCredentialSecureEqual `
|
|
389
|
+
-Left $cleanupCredential.Password `
|
|
390
|
+
-Right $handoffCredential.Password)) {
|
|
391
|
+
throw 'The protected handoff credential changed before cleanup.'
|
|
392
|
+
}
|
|
393
|
+
$cleanupCredential = $null
|
|
394
|
+
[IO.File]::Delete($cleanupQuarantinePath)
|
|
395
|
+
if (Test-Path -LiteralPath $cleanupQuarantinePath) {
|
|
396
|
+
throw 'The protected handoff credential could not be removed.'
|
|
397
|
+
}
|
|
398
|
+
$handoffQuarantined = $false
|
|
399
|
+
$handoffRemoved = $true
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
if ($handoffQuarantined -and
|
|
403
|
+
-not [string]::IsNullOrWhiteSpace($cleanupQuarantinePath) -and
|
|
404
|
+
(Test-Path -LiteralPath $cleanupQuarantinePath -PathType Leaf) -and
|
|
405
|
+
-not (Test-Path -LiteralPath $resolvedHandoffPath)) {
|
|
406
|
+
try {
|
|
407
|
+
[IO.File]::Move($cleanupQuarantinePath, $resolvedHandoffPath)
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
# Preserve the quarantined asset when its original path cannot be restored safely.
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
Write-AnswerMeCreatorCredentialDeployFailure `
|
|
414
|
+
-Stage 'cleanup' `
|
|
415
|
+
-Code 'handoff-cleanup-failed' `
|
|
416
|
+
-Message 'The installed credential is protected, but exact handoff cleanup failed.' `
|
|
417
|
+
-ExitCode 5
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
$handoffCredential = $null
|
|
421
|
+
if ($null -ne $handoffHash) {
|
|
422
|
+
Clear-AnswerMeSensitiveByteArray -Bytes $handoffHash
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
[PSCustomObject]@{
|
|
427
|
+
ok = $true
|
|
428
|
+
status = [string]$installResult.status
|
|
429
|
+
stage = 'complete'
|
|
430
|
+
code = if ($installResult.status -ceq 'no-op') {
|
|
431
|
+
'credential-unchanged'
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
'credential-deployed'
|
|
435
|
+
}
|
|
436
|
+
message = 'Creator credential deployment completed.'
|
|
437
|
+
creatorId = $safeCreatorId
|
|
438
|
+
targetChanged = $targetChanged
|
|
439
|
+
backupCreated = $backupCreated
|
|
440
|
+
rollbackStatus = $rollbackStatus
|
|
441
|
+
handoffRemoved = $handoffRemoved
|
|
442
|
+
verifiedRemotely = $verifiedRemotely
|
|
443
|
+
valid = $remoteValid
|
|
444
|
+
reason = $remoteReason
|
|
445
|
+
networkCalled = $networkCalled
|
|
446
|
+
} | ConvertTo-Json -Compress
|
|
447
|
+
exit 0
|