@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.
Files changed (39) hide show
  1. package/bin/answerme-toolkit.mjs +6 -0
  2. package/distribution/npm/migrations.json +605 -0
  3. package/distribution/npm/package-manifest.json +192 -0
  4. package/distribution/npm/skills/answerme/SKILL.md +94 -0
  5. package/distribution/npm/skills/answerme/agents/openai.yaml +4 -0
  6. package/distribution/npm/skills/answerme/references/api.md +135 -0
  7. package/distribution/npm/skills/answerme/references/creator-credential-deployment.md +19 -0
  8. package/distribution/npm/skills/answerme/references/creator-credential-recovery.md +24 -0
  9. package/distribution/npm/skills/answerme/references/errors.md +44 -0
  10. package/distribution/npm/skills/answerme/references/handoff.md +46 -0
  11. package/distribution/npm/skills/answerme/references/install-self-test.md +28 -0
  12. package/distribution/npm/skills/answerme/references/result-token-store.md +50 -0
  13. package/distribution/npm/skills/answerme/references/templates.md +139 -0
  14. package/distribution/npm/skills/answerme/scripts/answerme-api-base-url.ps1 +40 -0
  15. package/distribution/npm/skills/answerme/scripts/create-answerme.ps1 +1892 -0
  16. package/distribution/npm/skills/answerme/scripts/creator-credential-store.windows.ps1 +503 -0
  17. package/distribution/npm/skills/answerme/scripts/deploy-answerme-creator-credential.ps1 +447 -0
  18. package/distribution/npm/skills/answerme/scripts/enroll-answerme-creator.ps1 +764 -0
  19. package/distribution/npm/skills/answerme/scripts/open-answerme-page.windows.ps1 +272 -0
  20. package/distribution/npm/skills/answerme/scripts/remove-answerme-result-token.ps1 +63 -0
  21. package/distribution/npm/skills/answerme/scripts/result-token-store.windows.ps1 +261 -0
  22. package/distribution/npm/skills/answerme/scripts/test-answerme-installation.ps1 +498 -0
  23. package/distribution/npm/skills/answerme/scripts/wait-answerme-result.ps1 +908 -0
  24. package/distribution/npm/skills/answerme/scripts/windows-crypto.ps1 +57 -0
  25. package/distribution/npm/skills/answerme/scripts/windows-http.ps1 +45 -0
  26. package/distribution/npm/skills/answerme/scripts/windows-process-start-info.ps1 +76 -0
  27. package/distribution/npm/skills/ask-when-needed/SKILL.md +164 -0
  28. package/distribution/npm/skills/ask-when-needed/agents/openai.yaml +4 -0
  29. package/distribution/npm/skills/ask-when-needed/references/interview-strategies.md +43 -0
  30. package/lib/npm-cli/commands.mjs +247 -0
  31. package/lib/npm-cli/constants.mjs +51 -0
  32. package/lib/npm-cli/errors.mjs +15 -0
  33. package/lib/npm-cli/filesystem.mjs +193 -0
  34. package/lib/npm-cli/host-discovery.mjs +404 -0
  35. package/lib/npm-cli/main.mjs +42 -0
  36. package/lib/npm-cli/package-integrity.mjs +212 -0
  37. package/lib/npm-cli/transaction.mjs +375 -0
  38. package/lib/npm-cli/usage-validation.mjs +349 -0
  39. 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