@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,503 @@
1
+ $ErrorActionPreference = 'Stop'
2
+
3
+ $cryptoSupport = Join-Path $PSScriptRoot 'windows-crypto.ps1'
4
+ if (-not (Test-Path -LiteralPath $cryptoSupport -PathType Leaf)) {
5
+ throw 'The AnswerMe Windows cryptography helper is missing.'
6
+ }
7
+ . $cryptoSupport
8
+
9
+ function New-AnswerMeCreatorCredentialStoreException {
10
+ param(
11
+ [Parameter(Mandatory = $true)][string]$Code,
12
+ [Parameter(Mandatory = $true)][string]$Stage,
13
+ [Parameter(Mandatory = $true)][string]$Message,
14
+ [bool]$TargetChanged = $false,
15
+ [bool]$BackupCreated = $false,
16
+ [string]$RollbackStatus = 'not-required'
17
+ )
18
+
19
+ $exception = [InvalidOperationException]::new($Message)
20
+ $exception.Data['AnswerMeCode'] = $Code
21
+ $exception.Data['AnswerMeStage'] = $Stage
22
+ $exception.Data['AnswerMeTargetChanged'] = $TargetChanged
23
+ $exception.Data['AnswerMeBackupCreated'] = $BackupCreated
24
+ $exception.Data['AnswerMeRollbackStatus'] = $RollbackStatus
25
+ $exception.Data['AnswerMeNetworkCalled'] = $false
26
+ return $exception
27
+ }
28
+
29
+ function Test-AnswerMeCreatorCredentialLoopbackHost {
30
+ param([Parameter(Mandatory = $true)][Uri]$Uri)
31
+
32
+ return $Uri.DnsSafeHost.Equals('localhost', [StringComparison]::OrdinalIgnoreCase) -or
33
+ $Uri.DnsSafeHost.Equals('127.0.0.1', [StringComparison]::Ordinal) -or
34
+ $Uri.DnsSafeHost.Equals('::1', [StringComparison]::Ordinal)
35
+ }
36
+
37
+ function Resolve-AnswerMeCreatorCredentialApiBaseUrl {
38
+ param(
39
+ [AllowNull()][string]$ApiBaseUrl,
40
+ [switch]$AllowInsecureLoopbackHttpForTest
41
+ )
42
+
43
+ if ([string]::IsNullOrWhiteSpace($ApiBaseUrl)) {
44
+ throw 'A trusted API base URL is required.'
45
+ }
46
+
47
+ $parsed = $null
48
+ if (-not [Uri]::TryCreate($ApiBaseUrl, [UriKind]::Absolute, [ref]$parsed)) {
49
+ throw 'The API base URL is invalid.'
50
+ }
51
+ if (-not [string]::IsNullOrEmpty($parsed.UserInfo) -or
52
+ -not [string]::IsNullOrEmpty($parsed.Query) -or
53
+ -not [string]::IsNullOrEmpty($parsed.Fragment) -or
54
+ $parsed.AbsolutePath -notin @('', '/')) {
55
+ throw 'The API base URL must not include credentials, a path, a query, or a fragment.'
56
+ }
57
+ if ($parsed.Scheme -ne 'https' -and -not (
58
+ $parsed.Scheme -eq 'http' -and
59
+ $AllowInsecureLoopbackHttpForTest -and
60
+ (Test-AnswerMeCreatorCredentialLoopbackHost -Uri $parsed)
61
+ )) {
62
+ throw 'The API base URL must use HTTPS.'
63
+ }
64
+
65
+ return $parsed.AbsoluteUri.TrimEnd('/')
66
+ }
67
+
68
+ function Get-AnswerMeDefaultCreatorCredentialPath {
69
+ if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
70
+ throw 'The Windows Creator Credential Store requires Windows.'
71
+ }
72
+ $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
73
+ if ([string]::IsNullOrWhiteSpace($localAppData)) {
74
+ throw 'The current Windows account has no LocalApplicationData directory.'
75
+ }
76
+ return [IO.Path]::GetFullPath((Join-Path $localAppData 'AnswerMe\creator-credential.clixml'))
77
+ }
78
+
79
+ function Get-AnswerMeCreatorCredentialSecureBytes {
80
+ param([Parameter(Mandatory = $true)][Security.SecureString]$SecureValue)
81
+
82
+ $bstr = [IntPtr]::Zero
83
+ try {
84
+ $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureValue)
85
+ $byteLength = [Runtime.InteropServices.Marshal]::ReadInt32($bstr, -4)
86
+ $bytes = [byte[]]::new($byteLength)
87
+ if ($byteLength -gt 0) {
88
+ [Runtime.InteropServices.Marshal]::Copy($bstr, $bytes, 0, $byteLength)
89
+ }
90
+ return ,$bytes
91
+ }
92
+ finally {
93
+ if ($bstr -ne [IntPtr]::Zero) {
94
+ [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
95
+ }
96
+ }
97
+ }
98
+
99
+ function Test-AnswerMeCreatorCredentialSecureEqual {
100
+ param(
101
+ [Parameter(Mandatory = $true)][Security.SecureString]$Left,
102
+ [Parameter(Mandatory = $true)][Security.SecureString]$Right
103
+ )
104
+
105
+ [byte[]]$leftBytes = Get-AnswerMeCreatorCredentialSecureBytes -SecureValue $Left
106
+ [byte[]]$rightBytes = Get-AnswerMeCreatorCredentialSecureBytes -SecureValue $Right
107
+ $leftHash = $null
108
+ $rightHash = $null
109
+ try {
110
+ $leftHash = Get-AnswerMeSha256Bytes -Bytes $leftBytes
111
+ $rightHash = Get-AnswerMeSha256Bytes -Bytes $rightBytes
112
+ return Test-AnswerMeFixedTimeBytesEqual -Left $leftHash -Right $rightHash
113
+ }
114
+ finally {
115
+ if ($null -ne $leftBytes) {
116
+ Clear-AnswerMeSensitiveByteArray -Bytes $leftBytes
117
+ }
118
+ if ($null -ne $rightBytes) {
119
+ Clear-AnswerMeSensitiveByteArray -Bytes $rightBytes
120
+ }
121
+ if ($null -ne $leftHash) {
122
+ Clear-AnswerMeSensitiveByteArray -Bytes $leftHash
123
+ }
124
+ if ($null -ne $rightHash) {
125
+ Clear-AnswerMeSensitiveByteArray -Bytes $rightHash
126
+ }
127
+ }
128
+ }
129
+
130
+ function Get-AnswerMeCreatorCredentialFileHash {
131
+ param([Parameter(Mandatory = $true)][string]$LiteralPath)
132
+
133
+ $bytes = [IO.File]::ReadAllBytes($LiteralPath)
134
+ try {
135
+ return ,(Get-AnswerMeSha256Bytes -Bytes $bytes)
136
+ }
137
+ finally {
138
+ Clear-AnswerMeSensitiveByteArray -Bytes $bytes
139
+ }
140
+ }
141
+
142
+ function Test-AnswerMeCreatorCredentialBytesEqual {
143
+ param(
144
+ [Parameter(Mandatory = $true)][byte[]]$Left,
145
+ [Parameter(Mandatory = $true)][byte[]]$Right
146
+ )
147
+
148
+ return Test-AnswerMeFixedTimeBytesEqual -Left $Left -Right $Right
149
+ }
150
+
151
+ function Import-AnswerMeCreatorCredentialFile {
152
+ param([Parameter(Mandatory = $true)][string]$LiteralPath)
153
+
154
+ return Import-Clixml -LiteralPath $LiteralPath
155
+ }
156
+
157
+ function Export-AnswerMeCreatorCredentialFile {
158
+ param(
159
+ [Parameter(Mandatory = $true)][PSCredential]$Credential,
160
+ [Parameter(Mandatory = $true)][string]$LiteralPath
161
+ )
162
+
163
+ $Credential | Export-Clixml -LiteralPath $LiteralPath
164
+ }
165
+
166
+ function Invoke-AnswerMeCreatorCredentialAtomicReplace {
167
+ param(
168
+ [Parameter(Mandatory = $true)][string]$SourcePath,
169
+ [Parameter(Mandatory = $true)][string]$TargetPath,
170
+ [AllowNull()][string]$BackupPath,
171
+ [bool]$TargetExists
172
+ )
173
+
174
+ if ($TargetExists) {
175
+ [IO.File]::Replace($SourcePath, $TargetPath, $BackupPath, $true)
176
+ }
177
+ else {
178
+ [IO.File]::Move($SourcePath, $TargetPath)
179
+ }
180
+ }
181
+
182
+ function Restore-AnswerMeCreatorCredentialTarget {
183
+ param(
184
+ [Parameter(Mandatory = $true)][string]$TargetPath,
185
+ [AllowNull()][string]$BackupPath,
186
+ [AllowNull()][byte[]]$OriginalFileHash,
187
+ [bool]$TargetPreviouslyExisted,
188
+ [Parameter(Mandatory = $true)][string]$RollbackTemporaryPath
189
+ )
190
+
191
+ if ($TargetPreviouslyExisted) {
192
+ if (Test-Path -LiteralPath $TargetPath -PathType Leaf) {
193
+ [byte[]]$currentHash = Get-AnswerMeCreatorCredentialFileHash -LiteralPath $TargetPath
194
+ try {
195
+ if (Test-AnswerMeCreatorCredentialBytesEqual -Left $OriginalFileHash -Right $currentHash) {
196
+ return $true
197
+ }
198
+ }
199
+ finally {
200
+ Clear-AnswerMeSensitiveByteArray -Bytes $currentHash
201
+ }
202
+ }
203
+ if ([string]::IsNullOrWhiteSpace($BackupPath) -or
204
+ -not (Test-Path -LiteralPath $BackupPath -PathType Leaf)) {
205
+ return $false
206
+ }
207
+ [IO.File]::Copy($BackupPath, $RollbackTemporaryPath, $false)
208
+ $displacedPath = $RollbackTemporaryPath + '.displaced'
209
+ try {
210
+ if (Test-Path -LiteralPath $TargetPath -PathType Leaf) {
211
+ Invoke-AnswerMeCreatorCredentialAtomicReplace `
212
+ -SourcePath $RollbackTemporaryPath `
213
+ -TargetPath $TargetPath `
214
+ -BackupPath $displacedPath `
215
+ -TargetExists $true
216
+ }
217
+ else {
218
+ Invoke-AnswerMeCreatorCredentialAtomicReplace `
219
+ -SourcePath $RollbackTemporaryPath `
220
+ -TargetPath $TargetPath `
221
+ -BackupPath $null `
222
+ -TargetExists $false
223
+ }
224
+ }
225
+ finally {
226
+ if (Test-Path -LiteralPath $displacedPath -PathType Leaf) {
227
+ Remove-Item -LiteralPath $displacedPath -Force -ErrorAction SilentlyContinue
228
+ }
229
+ }
230
+ if (-not (Test-Path -LiteralPath $TargetPath -PathType Leaf)) {
231
+ return $false
232
+ }
233
+ [byte[]]$restoredHash = Get-AnswerMeCreatorCredentialFileHash -LiteralPath $TargetPath
234
+ try {
235
+ return Test-AnswerMeCreatorCredentialBytesEqual -Left $OriginalFileHash -Right $restoredHash
236
+ }
237
+ finally {
238
+ Clear-AnswerMeSensitiveByteArray -Bytes $restoredHash
239
+ }
240
+ }
241
+
242
+ if (Test-Path -LiteralPath $TargetPath -PathType Leaf) {
243
+ [IO.File]::Delete($TargetPath)
244
+ }
245
+ return -not (Test-Path -LiteralPath $TargetPath)
246
+ }
247
+
248
+ function Install-AnswerMeCreatorCredential {
249
+ [CmdletBinding()]
250
+ param(
251
+ [Parameter(Mandatory = $true)][Security.SecureString]$CreatorKey,
252
+ [Parameter(Mandatory = $true)][string]$ApiBaseUrl,
253
+ [Parameter(Mandatory = $true)][string]$CreatorId,
254
+ [string]$TargetCredentialPath,
255
+ [switch]$AllowInsecureLoopbackHttpForTest
256
+ )
257
+
258
+ $normalizedApiBaseUrl = $null
259
+ $resolvedTargetPath = $null
260
+ $targetParent = $null
261
+ $targetPreviouslyExisted = $false
262
+ $originalCredential = $null
263
+ $originalFileHash = $null
264
+ try {
265
+ if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
266
+ throw 'The Windows Creator Credential Store requires Windows.'
267
+ }
268
+ if ([string]::IsNullOrWhiteSpace($CreatorId)) {
269
+ throw 'Creator ID is required.'
270
+ }
271
+ if ($CreatorKey.Length -eq 0) {
272
+ throw 'Creator Key is empty.'
273
+ }
274
+ $normalizedApiBaseUrl = Resolve-AnswerMeCreatorCredentialApiBaseUrl `
275
+ -ApiBaseUrl $ApiBaseUrl `
276
+ -AllowInsecureLoopbackHttpForTest:$AllowInsecureLoopbackHttpForTest
277
+ if ([string]::IsNullOrWhiteSpace($TargetCredentialPath)) {
278
+ $TargetCredentialPath = Get-AnswerMeDefaultCreatorCredentialPath
279
+ }
280
+ $resolvedTargetPath = [IO.Path]::GetFullPath($TargetCredentialPath)
281
+ if ([IO.Path]::GetExtension($resolvedTargetPath) -cne '.clixml') {
282
+ throw 'The Creator credential target must be a .clixml file.'
283
+ }
284
+ if (Test-Path -LiteralPath $resolvedTargetPath -PathType Container) {
285
+ throw 'The Creator credential target is not a file.'
286
+ }
287
+ $targetParent = [IO.Path]::GetDirectoryName($resolvedTargetPath)
288
+ if ([string]::IsNullOrWhiteSpace($targetParent)) {
289
+ throw 'The Creator credential target has no parent directory.'
290
+ }
291
+
292
+ $targetPreviouslyExisted = Test-Path -LiteralPath $resolvedTargetPath -PathType Leaf
293
+ if ($targetPreviouslyExisted) {
294
+ $originalCredential = Import-AnswerMeCreatorCredentialFile -LiteralPath $resolvedTargetPath
295
+ if ($originalCredential -isnot [PSCredential] -or
296
+ [string]::IsNullOrWhiteSpace($originalCredential.UserName) -or
297
+ $originalCredential.Password.Length -eq 0) {
298
+ throw 'The existing Creator credential target has an unknown format.'
299
+ }
300
+ try {
301
+ $null = Resolve-AnswerMeCreatorCredentialApiBaseUrl `
302
+ -ApiBaseUrl $originalCredential.UserName `
303
+ -AllowInsecureLoopbackHttpForTest:$AllowInsecureLoopbackHttpForTest
304
+ }
305
+ catch {
306
+ throw 'The existing Creator credential target has an unknown format.'
307
+ }
308
+ [byte[]]$originalFileHash = Get-AnswerMeCreatorCredentialFileHash -LiteralPath $resolvedTargetPath
309
+
310
+ $sameKey = Test-AnswerMeCreatorCredentialSecureEqual `
311
+ -Left $originalCredential.Password `
312
+ -Right $CreatorKey
313
+ if ($originalCredential.UserName -ceq $normalizedApiBaseUrl -and $sameKey) {
314
+ return [PSCustomObject]@{
315
+ ok = $true
316
+ status = 'no-op'
317
+ creatorId = $CreatorId
318
+ targetChanged = $false
319
+ backupCreated = $false
320
+ rollbackStatus = 'not-required'
321
+ networkCalled = $false
322
+ }
323
+ }
324
+ }
325
+ }
326
+ catch {
327
+ if ($_.Exception.Data.Contains('AnswerMeCode')) {
328
+ throw $_.Exception
329
+ }
330
+ throw (New-AnswerMeCreatorCredentialStoreException `
331
+ -Code 'credential-store-preflight-failed' `
332
+ -Stage 'preflight' `
333
+ -Message 'Creator credential preflight failed.')
334
+ }
335
+
336
+ $createdParent = $false
337
+ $temporaryPath = Join-Path $targetParent ('.answerme-{0}.tmp.clixml' -f [Guid]::NewGuid().ToString('N'))
338
+ $backupPath = if ($targetPreviouslyExisted) {
339
+ Join-Path $targetParent ('.answerme-{0}.backup.clixml' -f [Guid]::NewGuid().ToString('N'))
340
+ }
341
+ else {
342
+ $null
343
+ }
344
+ $rollbackTemporaryPath = Join-Path $targetParent ('.answerme-{0}.rollback.clixml' -f [Guid]::NewGuid().ToString('N'))
345
+ $backupCreated = $false
346
+ $targetChanged = $false
347
+ $preserveBackup = $false
348
+ $newCredential = $null
349
+ $roundTrip = $null
350
+ try {
351
+ if (-not (Test-Path -LiteralPath $targetParent -PathType Container)) {
352
+ [IO.Directory]::CreateDirectory($targetParent) | Out-Null
353
+ $createdParent = $true
354
+ }
355
+
356
+ $newCredential = [PSCredential]::new($normalizedApiBaseUrl, $CreatorKey)
357
+ Export-AnswerMeCreatorCredentialFile -Credential $newCredential -LiteralPath $temporaryPath
358
+ $roundTrip = Import-AnswerMeCreatorCredentialFile -LiteralPath $temporaryPath
359
+ if ($roundTrip -isnot [PSCredential] -or
360
+ $roundTrip.UserName -cne $normalizedApiBaseUrl -or
361
+ -not (Test-AnswerMeCreatorCredentialSecureEqual -Left $roundTrip.Password -Right $CreatorKey)) {
362
+ throw (New-AnswerMeCreatorCredentialStoreException `
363
+ -Code 'credential-write-verification-failed' `
364
+ -Stage 'install' `
365
+ -Message 'The staged Creator credential failed protected readback verification.')
366
+ }
367
+ $roundTrip = $null
368
+
369
+ $targetChanged = $true
370
+ Invoke-AnswerMeCreatorCredentialAtomicReplace `
371
+ -SourcePath $temporaryPath `
372
+ -TargetPath $resolvedTargetPath `
373
+ -BackupPath $backupPath `
374
+ -TargetExists $targetPreviouslyExisted
375
+ $backupCreated = $targetPreviouslyExisted -and (Test-Path -LiteralPath $backupPath -PathType Leaf)
376
+
377
+ try {
378
+ $roundTrip = Import-AnswerMeCreatorCredentialFile -LiteralPath $resolvedTargetPath
379
+ if ($roundTrip -isnot [PSCredential] -or
380
+ $roundTrip.UserName -cne $normalizedApiBaseUrl -or
381
+ -not (Test-AnswerMeCreatorCredentialSecureEqual -Left $roundTrip.Password -Right $CreatorKey)) {
382
+ throw 'The installed credential did not match the requested credential.'
383
+ }
384
+ }
385
+ catch {
386
+ $rollbackVerified = $false
387
+ try {
388
+ $backupCreated = $backupCreated -or (
389
+ $targetPreviouslyExisted -and
390
+ (Test-Path -LiteralPath $backupPath -PathType Leaf)
391
+ )
392
+ $rollbackVerified = Restore-AnswerMeCreatorCredentialTarget `
393
+ -TargetPath $resolvedTargetPath `
394
+ -BackupPath $backupPath `
395
+ -OriginalFileHash $originalFileHash `
396
+ -TargetPreviouslyExisted $targetPreviouslyExisted `
397
+ -RollbackTemporaryPath $rollbackTemporaryPath
398
+ }
399
+ catch {
400
+ $rollbackVerified = $false
401
+ }
402
+ if (-not $rollbackVerified) {
403
+ $preserveBackup = $backupCreated
404
+ throw (New-AnswerMeCreatorCredentialStoreException `
405
+ -Code 'rollback-unverified' `
406
+ -Stage 'rollback' `
407
+ -Message 'Creator credential rollback could not be verified.' `
408
+ -TargetChanged $true `
409
+ -BackupCreated $backupCreated `
410
+ -RollbackStatus 'unverified')
411
+ }
412
+ throw (New-AnswerMeCreatorCredentialStoreException `
413
+ -Code 'credential-readback-failed' `
414
+ -Stage 'verify' `
415
+ -Message 'Installed Creator credential verification failed; the previous target was restored.' `
416
+ -TargetChanged $true `
417
+ -BackupCreated $backupCreated `
418
+ -RollbackStatus 'verified')
419
+ }
420
+
421
+ return [PSCustomObject]@{
422
+ ok = $true
423
+ status = 'installed'
424
+ creatorId = $CreatorId
425
+ targetChanged = $true
426
+ backupCreated = $backupCreated
427
+ rollbackStatus = 'not-required'
428
+ networkCalled = $false
429
+ }
430
+ }
431
+ catch {
432
+ if ($_.Exception.Data.Contains('AnswerMeCode')) {
433
+ throw $_.Exception
434
+ }
435
+
436
+ $backupCreated = $backupCreated -or (
437
+ $targetPreviouslyExisted -and
438
+ -not [string]::IsNullOrWhiteSpace($backupPath) -and
439
+ (Test-Path -LiteralPath $backupPath -PathType Leaf)
440
+ )
441
+ $rollbackVerified = $false
442
+ try {
443
+ if ($targetChanged) {
444
+ $rollbackVerified = Restore-AnswerMeCreatorCredentialTarget `
445
+ -TargetPath $resolvedTargetPath `
446
+ -BackupPath $backupPath `
447
+ -OriginalFileHash $originalFileHash `
448
+ -TargetPreviouslyExisted $targetPreviouslyExisted `
449
+ -RollbackTemporaryPath $rollbackTemporaryPath
450
+ }
451
+ else {
452
+ $rollbackVerified = $true
453
+ }
454
+ }
455
+ catch {
456
+ $rollbackVerified = $false
457
+ }
458
+ if (-not $rollbackVerified) {
459
+ $preserveBackup = $backupCreated
460
+ throw (New-AnswerMeCreatorCredentialStoreException `
461
+ -Code 'rollback-unverified' `
462
+ -Stage 'rollback' `
463
+ -Message 'Creator credential rollback could not be verified.' `
464
+ -TargetChanged $targetChanged `
465
+ -BackupCreated $backupCreated `
466
+ -RollbackStatus 'unverified')
467
+ }
468
+ throw (New-AnswerMeCreatorCredentialStoreException `
469
+ -Code 'credential-install-failed' `
470
+ -Stage 'install' `
471
+ -Message 'Creator credential installation failed.' `
472
+ -TargetChanged $targetChanged `
473
+ -BackupCreated $backupCreated `
474
+ -RollbackStatus $(if ($targetChanged) { 'verified' } else { 'not-required' }))
475
+ }
476
+ finally {
477
+ $newCredential = $null
478
+ $roundTrip = $null
479
+ $originalCredential = $null
480
+ if ($null -ne $originalFileHash) {
481
+ Clear-AnswerMeSensitiveByteArray -Bytes $originalFileHash
482
+ }
483
+ foreach ($cleanupPath in @($temporaryPath, $rollbackTemporaryPath)) {
484
+ if (-not [string]::IsNullOrWhiteSpace($cleanupPath) -and
485
+ (Test-Path -LiteralPath $cleanupPath -PathType Leaf)) {
486
+ Remove-Item -LiteralPath $cleanupPath -Force -ErrorAction SilentlyContinue
487
+ }
488
+ }
489
+ if (-not $preserveBackup -and
490
+ -not [string]::IsNullOrWhiteSpace($backupPath) -and
491
+ (Test-Path -LiteralPath $backupPath -PathType Leaf)) {
492
+ Remove-Item -LiteralPath $backupPath -Force -ErrorAction SilentlyContinue
493
+ }
494
+ if ($createdParent -and (Test-Path -LiteralPath $targetParent -PathType Container)) {
495
+ try {
496
+ [IO.Directory]::Delete($targetParent, $false)
497
+ }
498
+ catch {
499
+ # Keep a non-empty directory; never remove unknown assets during cleanup.
500
+ }
501
+ }
502
+ }
503
+ }