@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,908 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [Parameter(Mandatory = $true)]
4
+ [string]$InteractionId,
5
+ [string]$ApiBaseUrl,
6
+ [switch]$AllowInsecureLoopbackHttpForTest,
7
+ [string]$CredentialPath = $env:ANSWERME_CREATOR_CREDENTIAL_PATH,
8
+ [string]$ResultTokenEnvironmentVariable = 'ANSWERME_RESULT_TOKEN',
9
+ [string]$ResultTokenStoreRoot = $env:ANSWERME_RESULT_TOKEN_STORE_ROOT,
10
+ [switch]$AllowNonDefaultResultTokenStore,
11
+ [ValidateRange(1, 60)]
12
+ [int]$PollSeconds = 5,
13
+ [double]$TimeoutSeconds = 300,
14
+ [switch]$ValidateOnly
15
+ )
16
+
17
+ $ErrorActionPreference = 'Stop'
18
+ [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
19
+
20
+ $httpSupport = Join-Path $PSScriptRoot 'windows-http.ps1'
21
+ if (-not (Test-Path -LiteralPath $httpSupport -PathType Leaf)) {
22
+ throw 'The AnswerMe Windows HTTP helper is missing.'
23
+ }
24
+ . $httpSupport
25
+ $AnswerMeProtocolVersion = '1.1'
26
+ $AnswerMeSkillVersion = '0.3.0'
27
+
28
+ function Write-AnswerMeWaitFailure {
29
+ param(
30
+ [Parameter(Mandatory = $true)][string]$Code,
31
+ [Parameter(Mandatory = $true)][string]$Message,
32
+ [Parameter(Mandatory = $true)][int]$ExitCode,
33
+ [bool]$NetworkCalled = $false
34
+ )
35
+
36
+ [PSCustomObject]@{
37
+ ok = $false
38
+ outcome = 'failed'
39
+ status = 'unknown'
40
+ code = $Code
41
+ message = $Message
42
+ interactionId = $InteractionId
43
+ networkCalled = $NetworkCalled
44
+ } | ConvertTo-Json -Depth 5
45
+ exit $ExitCode
46
+ }
47
+
48
+ function Get-DefaultAnswerMeCredentialPath {
49
+ if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
50
+ return $null
51
+ }
52
+ $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
53
+ if ([string]::IsNullOrWhiteSpace($localAppData)) {
54
+ return $null
55
+ }
56
+ return Join-Path $localAppData 'AnswerMe\creator-credential.clixml'
57
+ }
58
+
59
+ $apiBaseUrlPolicyPath = Join-Path $PSScriptRoot 'answerme-api-base-url.ps1'
60
+ if (-not (Test-Path -LiteralPath $apiBaseUrlPolicyPath -PathType Leaf)) {
61
+ Write-AnswerMeWaitFailure -Code 'api-base-url-policy-missing' -Message 'The AnswerMe API base URL policy is required.' -ExitCode 3
62
+ }
63
+ try {
64
+ . $apiBaseUrlPolicyPath
65
+ }
66
+ catch {
67
+ Write-AnswerMeWaitFailure -Code 'api-base-url-policy-unavailable' -Message 'The AnswerMe API base URL policy could not be loaded.' -ExitCode 3
68
+ }
69
+
70
+ if ([string]::IsNullOrWhiteSpace($InteractionId)) {
71
+ Write-AnswerMeWaitFailure -Code 'interaction-id-missing' -Message 'InteractionId is required.' -ExitCode 2
72
+ }
73
+
74
+ $timeoutIsInteger = -not [double]::IsNaN($TimeoutSeconds) -and
75
+ -not [double]::IsInfinity($TimeoutSeconds) -and
76
+ [Math]::Truncate($TimeoutSeconds) -eq $TimeoutSeconds
77
+ $effectiveTimeoutSeconds = if ($timeoutIsInteger -and
78
+ ($TimeoutSeconds -eq 0 -or ($TimeoutSeconds -ge 30 -and $TimeoutSeconds -le 600))) {
79
+ [int]$TimeoutSeconds
80
+ }
81
+ else {
82
+ 0
83
+ }
84
+ $timeoutDecisionReason = if ($effectiveTimeoutSeconds -eq 0 -and $TimeoutSeconds -ne 0) {
85
+ 'timeout_out_of_range'
86
+ }
87
+ else {
88
+ $null
89
+ }
90
+
91
+ $validatedApiBaseUrl = $null
92
+ if (-not [string]::IsNullOrWhiteSpace($ApiBaseUrl)) {
93
+ try {
94
+ $validatedApiBaseUrl = Resolve-AnswerMeApiBaseUrl `
95
+ -ApiBaseUrl $ApiBaseUrl `
96
+ -AllowInsecureLoopbackHttpForTest:$AllowInsecureLoopbackHttpForTest
97
+ }
98
+ catch {
99
+ Write-AnswerMeWaitFailure -Code 'api-base-url-invalid' -Message $_.Exception.Message -ExitCode 2
100
+ }
101
+ }
102
+
103
+ if ($ValidateOnly) {
104
+ [PSCustomObject]@{
105
+ ok = $true
106
+ status = 'validated'
107
+ interactionId = $InteractionId
108
+ pollSeconds = $PollSeconds
109
+ requestedTimeoutSeconds = $TimeoutSeconds
110
+ effectiveTimeoutSeconds = $effectiveTimeoutSeconds
111
+ reason = $timeoutDecisionReason
112
+ protocolVersion = $AnswerMeProtocolVersion
113
+ skillVersion = $AnswerMeSkillVersion
114
+ capabilityDiscovery = $true
115
+ networkCalled = $false
116
+ } | ConvertTo-Json -Depth 4
117
+ exit 0
118
+ }
119
+
120
+ if ($effectiveTimeoutSeconds -eq 0) {
121
+ [PSCustomObject]@{
122
+ ok = $false
123
+ outcome = 'paused'
124
+ interactionId = $InteractionId
125
+ status = 'pending'
126
+ code = if ($null -ne $timeoutDecisionReason) { $timeoutDecisionReason } else { 'wait_not_requested' }
127
+ requestedTimeoutSeconds = $TimeoutSeconds
128
+ effectiveTimeoutSeconds = 0
129
+ reason = $timeoutDecisionReason
130
+ preserveResultToken = $true
131
+ recoveryReference = 'references/errors.md'
132
+ networkCalled = $false
133
+ } | ConvertTo-Json -Depth 6
134
+ exit 5
135
+ }
136
+
137
+ if ([string]::IsNullOrWhiteSpace($ApiBaseUrl)) {
138
+ if ([string]::IsNullOrWhiteSpace($CredentialPath)) {
139
+ $CredentialPath = Get-DefaultAnswerMeCredentialPath
140
+ }
141
+ if (-not [string]::IsNullOrWhiteSpace($CredentialPath) -and (Test-Path -LiteralPath $CredentialPath -PathType Leaf)) {
142
+ try {
143
+ $encryptedCredential = Import-Clixml -LiteralPath $CredentialPath
144
+ if ($encryptedCredential -isnot [PSCredential]) {
145
+ throw 'Encrypted credential file does not contain a PSCredential.'
146
+ }
147
+ $ApiBaseUrl = $encryptedCredential.UserName
148
+ $encryptedCredential = $null
149
+ }
150
+ catch {
151
+ Write-AnswerMeWaitFailure -Code 'credential-file-invalid' -Message 'The encrypted AnswerMe configuration cannot be read by this Windows account.' -ExitCode 3
152
+ }
153
+ }
154
+ }
155
+
156
+ if ([string]::IsNullOrWhiteSpace($ApiBaseUrl)) {
157
+ Write-AnswerMeWaitFailure -Code 'api-base-url-missing' -Message 'A trusted AnswerMe API base URL is required.' -ExitCode 2
158
+ }
159
+ $parsedBaseUrl = $validatedApiBaseUrl
160
+ try {
161
+ $parsedBaseUrl = Resolve-AnswerMeApiBaseUrl `
162
+ -ApiBaseUrl $ApiBaseUrl `
163
+ -AllowInsecureLoopbackHttpForTest:$AllowInsecureLoopbackHttpForTest
164
+ }
165
+ catch {
166
+ Write-AnswerMeWaitFailure -Code 'api-base-url-invalid' -Message $_.Exception.Message -ExitCode 2
167
+ }
168
+
169
+ $resultTokenStoreAdapter = Join-Path $PSScriptRoot 'result-token-store.windows.ps1'
170
+ if (-not (Test-Path -LiteralPath $resultTokenStoreAdapter -PathType Leaf)) {
171
+ Write-AnswerMeWaitFailure -Code 'result-token-store-adapter-missing' -Message 'A protected Result Token Store adapter is required.' -ExitCode 3
172
+ }
173
+ try {
174
+ . $resultTokenStoreAdapter
175
+ $resultToken = Get-AnswerMeResultToken `
176
+ -InteractionId $InteractionId `
177
+ -StoreRoot $ResultTokenStoreRoot `
178
+ -AllowNonDefaultStoreRoot:$AllowNonDefaultResultTokenStore
179
+ $workingToken = [Environment]::GetEnvironmentVariable($ResultTokenEnvironmentVariable, 'Process')
180
+ if (-not [string]::IsNullOrWhiteSpace($workingToken) -and $workingToken -cne $resultToken) {
181
+ throw 'The process Result Token does not match the protected credential.'
182
+ }
183
+ $resultTokenSource = 'protected-store'
184
+ }
185
+ catch {
186
+ Write-AnswerMeWaitFailure -Code 'result-token-store-unavailable' -Message $_.Exception.Message -ExitCode 3
187
+ }
188
+ $knownResponseSecrets = @(
189
+ $resultToken,
190
+ [Environment]::GetEnvironmentVariable('ANSWERME_CREATOR_KEY', 'Process'),
191
+ [Environment]::GetEnvironmentVariable('ANSWERME_TEST_CREATOR_KEY', 'Process')
192
+ ) | Where-Object { -not [string]::IsNullOrEmpty([string]$_) }
193
+
194
+ function ConvertFrom-AnswerMeJsonResponse {
195
+ param([Parameter(Mandatory = $true)][object]$Response)
196
+ if ([string]::IsNullOrWhiteSpace([string]$Response.Content)) {
197
+ return $null
198
+ }
199
+ try {
200
+ if ((Get-Command ConvertFrom-Json).Parameters.ContainsKey('DateKind')) {
201
+ return $Response.Content | ConvertFrom-Json -DateKind String
202
+ }
203
+ return $Response.Content | ConvertFrom-Json
204
+ }
205
+ catch {
206
+ return $null
207
+ }
208
+ }
209
+
210
+ function Test-AnswerMeWaitResponseFields {
211
+ param(
212
+ [AllowNull()][object]$Body,
213
+ [Parameter(Mandatory = $true)][string[]]$ExpectedFields
214
+ )
215
+
216
+ if ($Body -isnot [PSCustomObject]) { return $false }
217
+ $actual = @($Body.PSObject.Properties.Name | Sort-Object)
218
+ $expected = @($ExpectedFields | Sort-Object)
219
+ return $actual.Count -eq $expected.Count -and
220
+ -not (Compare-Object -ReferenceObject $expected -DifferenceObject $actual -CaseSensitive)
221
+ }
222
+
223
+ function Test-AnswerMeWaitNoStoreResponse {
224
+ param([Parameter(Mandatory = $true)][object]$Response)
225
+
226
+ return ([string]$Response.Headers['Cache-Control']).Trim() -ceq 'no-store'
227
+ }
228
+
229
+ function Test-AnswerMeWaitCapabilitiesResponse {
230
+ param([AllowNull()][object]$Body)
231
+
232
+ if (-not (Test-AnswerMeWaitResponseFields -Body $Body -ExpectedFields @(
233
+ 'protocolVersion', 'schemaVersions', 'clientProtocol', 'capabilities', 'requestId')) -or
234
+ $Body.protocolVersion -isnot [string] -or
235
+ $Body.protocolVersion -cne $AnswerMeProtocolVersion -or
236
+ $Body.requestId -isnot [string] -or
237
+ [string]::IsNullOrWhiteSpace($Body.requestId) -or
238
+ $Body.schemaVersions -isnot [Array] -or
239
+ @($Body.schemaVersions).Count -ne 1 -or
240
+ ($Body.schemaVersions[0] -isnot [int] -and $Body.schemaVersions[0] -isnot [long]) -or
241
+ $Body.schemaVersions[0] -ne 1) { return $false }
242
+
243
+ $client = $Body.clientProtocol
244
+ if (-not (Test-AnswerMeWaitResponseFields -Body $client -ExpectedFields @(
245
+ 'supportedVersions', 'recommendedVersion', 'minimumCompatibleSkillVersion')) -or
246
+ $client.recommendedVersion -isnot [string] -or
247
+ $client.recommendedVersion -cne $AnswerMeProtocolVersion -or
248
+ $client.minimumCompatibleSkillVersion -isnot [string] -or
249
+ [string]::IsNullOrWhiteSpace($client.minimumCompatibleSkillVersion) -or
250
+ $client.supportedVersions -isnot [Array]) { return $false }
251
+
252
+ $versions = @($client.supportedVersions)
253
+ if ($versions.Count -eq 0 -or
254
+ @($versions | Where-Object { $_ -isnot [string] -or [string]::IsNullOrWhiteSpace($_) }).Count -gt 0) {
255
+ return $false
256
+ }
257
+ # Compatible responses follow the same frozen v1 contract as creation/enrollment.
258
+ # A fully typed response excluding this client may still report incompatibility.
259
+ if ($versions -ccontains $AnswerMeProtocolVersion -and
260
+ ($versions.Count -ne 2 -or $versions -cnotcontains '1.0' -or
261
+ $client.minimumCompatibleSkillVersion -cne '0.1.0')) { return $false }
262
+
263
+ $capabilities = $Body.capabilities
264
+ if (-not (Test-AnswerMeWaitResponseFields -Body $capabilities -ExpectedFields @(
265
+ 'createInteraction', 'resultQuery', 'resultWait')) -or
266
+ $capabilities.createInteraction -isnot [bool] -or
267
+ $capabilities.resultQuery -isnot [bool]) { return $false }
268
+
269
+ $wait = $capabilities.resultWait
270
+ return (Test-AnswerMeWaitResponseFields -Body $wait -ExpectedFields @(
271
+ 'enabled', 'defaultWaitSeconds', 'maxWaitSeconds')) -and
272
+ $wait.enabled -is [bool] -and
273
+ ($wait.defaultWaitSeconds -is [int] -or $wait.defaultWaitSeconds -is [long]) -and
274
+ $wait.defaultWaitSeconds -eq 55 -and
275
+ ($wait.maxWaitSeconds -is [int] -or $wait.maxWaitSeconds -is [long]) -and
276
+ $wait.maxWaitSeconds -eq 600
277
+ }
278
+
279
+ function Test-AnswerMeWaitDateTime {
280
+ param([AllowNull()][object]$Value)
281
+
282
+ if ($Value -isnot [string] -or
283
+ [string]$Value -cnotmatch '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$') {
284
+ return $false
285
+ }
286
+ $parsed = [DateTimeOffset]::MinValue
287
+ return [DateTimeOffset]::TryParse(
288
+ [string]$Value,
289
+ [Globalization.CultureInfo]::InvariantCulture,
290
+ [Globalization.DateTimeStyles]::RoundtripKind,
291
+ [ref]$parsed
292
+ )
293
+ }
294
+
295
+ function Test-AnswerMeWaitBodyContainsSecret {
296
+ param(
297
+ [AllowNull()][object]$Value,
298
+ [Parameter(Mandatory = $true)][string[]]$Secrets
299
+ )
300
+
301
+ if ($null -eq $Value) { return $false }
302
+ if ($Value -is [string]) {
303
+ foreach ($secret in $Secrets) {
304
+ if (-not [string]::IsNullOrEmpty($secret) -and
305
+ ([string]$Value).IndexOf($secret, [StringComparison]::Ordinal) -ge 0) {
306
+ return $true
307
+ }
308
+ }
309
+ return $false
310
+ }
311
+ if ($Value -is [PSCustomObject]) {
312
+ foreach ($property in $Value.PSObject.Properties) {
313
+ if (Test-AnswerMeWaitBodyContainsSecret -Value $property.Value -Secrets $Secrets) {
314
+ return $true
315
+ }
316
+ }
317
+ return $false
318
+ }
319
+ if ($Value -is [Array]) {
320
+ foreach ($item in $Value) {
321
+ if (Test-AnswerMeWaitBodyContainsSecret -Value $item -Secrets $Secrets) {
322
+ return $true
323
+ }
324
+ }
325
+ }
326
+ return $false
327
+ }
328
+
329
+ function Test-AnswerMeAnswerLength {
330
+ param([string]$Value)
331
+ $count = 0
332
+ for ($index = 0; $index -lt $Value.Length; $index += 1) {
333
+ if ([char]::IsHighSurrogate($Value[$index])) {
334
+ if ($index + 1 -ge $Value.Length -or -not [char]::IsLowSurrogate($Value[$index + 1])) { return $false }
335
+ $index += 1
336
+ }
337
+ elseif ([char]::IsLowSurrogate($Value[$index])) { return $false }
338
+ $count += 1
339
+ if ($count -gt 2000) { return $false }
340
+ }
341
+ return $true
342
+ }
343
+
344
+ function ConvertTo-AnswerMeSafeQuestionnaireResult {
345
+ param([AllowNull()][object]$Result)
346
+
347
+ if ($Result -isnot [PSCustomObject]) { return $null }
348
+ $resultFields = @($Result.PSObject.Properties.Name)
349
+ $expectedResultFields = if ($resultFields -ccontains 'additionalContext') {
350
+ @('type', 'answers', 'additionalContext')
351
+ }
352
+ else {
353
+ @('type', 'answers')
354
+ }
355
+ if (-not (Test-AnswerMeWaitResponseFields -Body $Result -ExpectedFields $expectedResultFields) -or
356
+ $Result.type -isnot [string] -or
357
+ [string]$Result.type -cne 'questionnaire' -or
358
+ $Result.answers -isnot [Array] -or
359
+ ($expectedResultFields -ccontains 'additionalContext' -and
360
+ ($Result.additionalContext -isnot [string] -or
361
+ [string]::IsNullOrWhiteSpace([string]$Result.additionalContext) -or
362
+ ([string]$Result.additionalContext).Length -gt 2000))) {
363
+ return $null
364
+ }
365
+
366
+ $safeAnswers = [Collections.Generic.List[object]]::new()
367
+ foreach ($answer in @($Result.answers)) {
368
+ if ($answer -isnot [PSCustomObject] -or
369
+ $answer.questionId -isnot [string] -or
370
+ [string]::IsNullOrWhiteSpace([string]$answer.questionId)) {
371
+ return $null
372
+ }
373
+ $answerFields = @($answer.PSObject.Properties.Name)
374
+ $valueFields = @($answerFields | Where-Object { $_ -cin @('optionId', 'optionIds', 'customAnswer') })
375
+ if ($valueFields.Count -ne 1 -or
376
+ -not (Test-AnswerMeWaitResponseFields `
377
+ -Body $answer `
378
+ -ExpectedFields @('questionId', $valueFields[0]))) {
379
+ return $null
380
+ }
381
+ $safeAnswer = [ordered]@{ questionId = [string]$answer.questionId }
382
+ switch -CaseSensitive ($valueFields[0]) {
383
+ 'optionId' {
384
+ if ($answer.optionId -isnot [string] -or
385
+ [string]::IsNullOrWhiteSpace([string]$answer.optionId)) { return $null }
386
+ $safeAnswer.optionId = [string]$answer.optionId
387
+ }
388
+ 'customAnswer' {
389
+ if ($answer.customAnswer -isnot [string] -or
390
+ [string]::IsNullOrWhiteSpace([string]$answer.customAnswer) -or
391
+ -not (Test-AnswerMeAnswerLength -Value $answer.customAnswer)) { return $null }
392
+ $safeAnswer.customAnswer = [string]$answer.customAnswer
393
+ }
394
+ 'optionIds' {
395
+ if ($answer.optionIds -isnot [Array] -or
396
+ @($answer.optionIds).Count -lt 1 -or
397
+ @($answer.optionIds).Count -gt 20) { return $null }
398
+ $seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
399
+ $safeOptionIds = [Collections.Generic.List[string]]::new()
400
+ foreach ($optionId in @($answer.optionIds)) {
401
+ if ($optionId -isnot [string] -or
402
+ [string]::IsNullOrWhiteSpace([string]$optionId) -or
403
+ -not $seen.Add([string]$optionId)) { return $null }
404
+ [void]$safeOptionIds.Add([string]$optionId)
405
+ }
406
+ $safeAnswer.optionIds = @($safeOptionIds)
407
+ }
408
+ default { return $null }
409
+ }
410
+ [void]$safeAnswers.Add([PSCustomObject]$safeAnswer)
411
+ }
412
+
413
+ $safeResult = [ordered]@{
414
+ type = 'questionnaire'
415
+ answers = @($safeAnswers)
416
+ }
417
+ if ($expectedResultFields -ccontains 'additionalContext') {
418
+ $safeResult.additionalContext = [string]$Result.additionalContext
419
+ }
420
+ return [PSCustomObject]$safeResult
421
+ }
422
+
423
+ function ConvertTo-AnswerMeSafeResultResponse {
424
+ param(
425
+ [AllowNull()][object]$Body,
426
+ [Parameter(Mandatory = $true)][string]$Endpoint
427
+ )
428
+
429
+ if (-not (Test-AnswerMeWaitResponseFields `
430
+ -Body $Body `
431
+ -ExpectedFields @(
432
+ 'interactionId', 'status', 'topic', 'createdAt', 'expiresAt',
433
+ 'completedAt', 'purgeAt', 'result', 'requestId'
434
+ )) -or
435
+ $Body.interactionId -isnot [string] -or
436
+ [string]$Body.interactionId -cne $InteractionId -or
437
+ $Body.status -isnot [string] -or
438
+ [string]$Body.status -cnotin @('pending', 'submitted', 'cancelled', 'expired', 'revoked') -or
439
+ ($Endpoint -ceq 'wait' -and [string]$Body.status -ceq 'pending') -or
440
+ $Body.topic -isnot [string] -or
441
+ -not (Test-AnswerMeWaitDateTime -Value $Body.createdAt) -or
442
+ -not (Test-AnswerMeWaitDateTime -Value $Body.expiresAt) -or
443
+ ($null -ne $Body.completedAt -and -not (Test-AnswerMeWaitDateTime -Value $Body.completedAt)) -or
444
+ ($null -ne $Body.purgeAt -and -not (Test-AnswerMeWaitDateTime -Value $Body.purgeAt)) -or
445
+ $Body.requestId -isnot [string] -or
446
+ [string]::IsNullOrWhiteSpace([string]$Body.requestId)) {
447
+ return $null
448
+ }
449
+
450
+ $safeResult = $null
451
+ if ($null -ne $Body.result) {
452
+ $safeResult = ConvertTo-AnswerMeSafeQuestionnaireResult -Result $Body.result
453
+ if ($null -eq $safeResult) { return $null }
454
+ }
455
+ return [PSCustomObject][ordered]@{
456
+ interactionId = [string]$Body.interactionId
457
+ status = [string]$Body.status
458
+ topic = [string]$Body.topic
459
+ createdAt = [string]$Body.createdAt
460
+ expiresAt = [string]$Body.expiresAt
461
+ completedAt = if ($null -eq $Body.completedAt) { $null } else { [string]$Body.completedAt }
462
+ purgeAt = if ($null -eq $Body.purgeAt) { $null } else { [string]$Body.purgeAt }
463
+ result = $safeResult
464
+ requestId = [string]$Body.requestId
465
+ }
466
+ }
467
+
468
+ function Test-AnswerMeWaitRetryAfter {
469
+ param(
470
+ [Parameter(Mandatory = $true)][object]$Response,
471
+ [switch]$Required,
472
+ [int]$Maximum = [int]::MaxValue
473
+ )
474
+
475
+ $rawValue = [string]$Response.Headers['Retry-After']
476
+ if ([string]::IsNullOrWhiteSpace($rawValue)) { return -not $Required }
477
+ $parsedValue = 0
478
+ return [int]::TryParse($rawValue, [ref]$parsedValue) -and
479
+ $parsedValue -ge 1 -and
480
+ $parsedValue -le $Maximum
481
+ }
482
+
483
+ function ConvertTo-AnswerMeSafeErrorResponse {
484
+ param(
485
+ [Parameter(Mandatory = $true)][int]$HttpStatus,
486
+ [AllowNull()][object]$Body,
487
+ [Parameter(Mandatory = $true)][ValidateSet('query', 'wait')][string]$Endpoint,
488
+ [Parameter(Mandatory = $true)][object]$Response
489
+ )
490
+
491
+ if (-not (Test-AnswerMeWaitNoStoreResponse -Response $Response) -or
492
+ $Body -isnot [PSCustomObject] -or
493
+ $Body.requestId -isnot [string] -or
494
+ [string]::IsNullOrWhiteSpace([string]$Body.requestId)) {
495
+ return $null
496
+ }
497
+
498
+ $reason = $null
499
+ switch ($HttpStatus) {
500
+ 400 {
501
+ $allowedReasons = if ($Endpoint -ceq 'wait') {
502
+ @('invalid_query', 'invalid_interaction_id', 'request_body_not_allowed', 'invalid_timeout_seconds')
503
+ }
504
+ else { @() }
505
+ if ($allowedReasons.Count -eq 0) {
506
+ if (-not (Test-AnswerMeWaitResponseFields -Body $Body -ExpectedFields @('code', 'requestId'))) {
507
+ return $null
508
+ }
509
+ }
510
+ else {
511
+ if (-not (Test-AnswerMeWaitResponseFields -Body $Body -ExpectedFields @('code', 'requestId', 'reason')) -or
512
+ $Body.reason -isnot [string] -or
513
+ [string]$Body.reason -cnotin $allowedReasons) { return $null }
514
+ $reason = [string]$Body.reason
515
+ }
516
+ if ($Body.code -isnot [string] -or [string]$Body.code -cne 'invalid_request') { return $null }
517
+ }
518
+ 404 {
519
+ if (-not (Test-AnswerMeWaitResponseFields -Body $Body -ExpectedFields @('code', 'requestId')) -or
520
+ $Body.code -isnot [string] -or
521
+ [string]$Body.code -cne 'interaction_not_found') { return $null }
522
+ }
523
+ 409 {
524
+ if (-not (Test-AnswerMeWaitResponseFields `
525
+ -Body $Body `
526
+ -ExpectedFields @(
527
+ 'code', 'requestId', 'reason', 'receivedProtocolVersion',
528
+ 'supportedProtocolVersions', 'minimumCompatibleSkillVersion'
529
+ )) -or
530
+ $Body.code -isnot [string] -or
531
+ [string]$Body.code -cne 'client_protocol_unsupported' -or
532
+ $Body.reason -isnot [string] -or
533
+ [string]$Body.reason -cne 'answerme_client_protocol_version_is_not_supported' -or
534
+ $Body.receivedProtocolVersion -isnot [string] -or
535
+ [string]::IsNullOrWhiteSpace([string]$Body.receivedProtocolVersion) -or
536
+ $Body.supportedProtocolVersions -isnot [Array] -or
537
+ @($Body.supportedProtocolVersions).Count -lt 1 -or
538
+ @($Body.supportedProtocolVersions | Where-Object {
539
+ $_ -isnot [string] -or [string]::IsNullOrWhiteSpace([string]$_)
540
+ }).Count -gt 0 -or
541
+ $Body.minimumCompatibleSkillVersion -isnot [string] -or
542
+ [string]::IsNullOrWhiteSpace([string]$Body.minimumCompatibleSkillVersion)) { return $null }
543
+ $reason = [string]$Body.reason
544
+ }
545
+ 429 {
546
+ if ($Endpoint -cne 'wait' -or
547
+ -not (Test-AnswerMeWaitResponseFields -Body $Body -ExpectedFields @('code', 'requestId', 'reason')) -or
548
+ $Body.code -isnot [string] -or
549
+ [string]$Body.code -cne 'rate_limited' -or
550
+ $Body.reason -isnot [string] -or
551
+ [string]$Body.reason -cnotin @(
552
+ 'result_wait_safety_global_rate_limited',
553
+ 'result_wait_safety_ip_rate_limited',
554
+ 'result_wait_interaction_capacity'
555
+ ) -or
556
+ -not (Test-AnswerMeWaitRetryAfter -Response $Response -Required -Maximum 60)) { return $null }
557
+ $reason = [string]$Body.reason
558
+ }
559
+ 503 {
560
+ if ($Endpoint -ceq 'wait') {
561
+ if (-not (Test-AnswerMeWaitRetryAfter -Response $Response -Required)) { return $null }
562
+ $hasReason = @($Body.PSObject.Properties.Name) -ccontains 'reason'
563
+ $expectedFields = if ($hasReason) { @('code', 'requestId', 'reason') } else { @('code', 'requestId') }
564
+ if (-not (Test-AnswerMeWaitResponseFields -Body $Body -ExpectedFields $expectedFields) -or
565
+ $Body.code -isnot [string] -or
566
+ [string]$Body.code -cnotin @('result_wait_disabled', 'service_unavailable')) { return $null }
567
+ if ($hasReason) {
568
+ if ($Body.reason -isnot [string]) { return $null }
569
+ $allowedReasons = if ([string]$Body.code -ceq 'result_wait_disabled') {
570
+ @('result_wait_disabled', 'result_wait_not_configured')
571
+ }
572
+ else {
573
+ @(
574
+ 'capacity_pressure', 'result_wait_auth_queue_full',
575
+ 'result_wait_auth_queue_timeout', 'result_wait_global_capacity',
576
+ 'result_wait_dependency_failure'
577
+ )
578
+ }
579
+ if ([string]$Body.reason -cnotin $allowedReasons) { return $null }
580
+ $reason = [string]$Body.reason
581
+ }
582
+ }
583
+ else {
584
+ $hasReason = @($Body.PSObject.Properties.Name) -ccontains 'reason'
585
+ $expectedFields = if ($hasReason) { @('code', 'requestId', 'reason') } else { @('code', 'requestId') }
586
+ if (-not (Test-AnswerMeWaitResponseFields -Body $Body -ExpectedFields $expectedFields) -or
587
+ $Body.code -isnot [string] -or
588
+ [string]$Body.code -cne 'service_unavailable') { return $null }
589
+ if ($hasReason) {
590
+ if ($Body.reason -isnot [string] -or
591
+ [string]$Body.reason -cnotin @('writer_admission_full', 'writer_admission_timeout')) {
592
+ return $null
593
+ }
594
+ $reason = [string]$Body.reason
595
+ }
596
+ if (-not (Test-AnswerMeWaitRetryAfter -Response $Response)) { return $null }
597
+ }
598
+ }
599
+ default { return $null }
600
+ }
601
+
602
+ return [PSCustomObject]@{
603
+ code = [string]$Body.code
604
+ reason = $reason
605
+ receivedProtocolVersion = if ($HttpStatus -eq 409) { [string]$Body.receivedProtocolVersion } else { $null }
606
+ supportedProtocolVersions = if ($HttpStatus -eq 409) { @($Body.supportedProtocolVersions) } else { @() }
607
+ minimumCompatibleSkillVersion = if ($HttpStatus -eq 409) { [string]$Body.minimumCompatibleSkillVersion } else { $null }
608
+ }
609
+ }
610
+
611
+ function Get-AnswerMeRetryAfterSeconds {
612
+ param([Parameter(Mandatory = $true)][object]$Response)
613
+ $rawValue = [string]$Response.Headers['Retry-After']
614
+ $parsedValue = 0
615
+ if ([int]::TryParse($rawValue, [ref]$parsedValue) -and $parsedValue -ge 0) {
616
+ return $parsedValue
617
+ }
618
+ return 0
619
+ }
620
+
621
+ function Start-AnswerMeBoundedSleep {
622
+ param(
623
+ [Parameter(Mandatory = $true)][DateTimeOffset]$Deadline,
624
+ [Parameter(Mandatory = $true)][double]$Seconds
625
+ )
626
+
627
+ $remainingMilliseconds = [Math]::Max(
628
+ 0,
629
+ [Math]::Floor(($Deadline - [DateTimeOffset]::UtcNow).TotalMilliseconds)
630
+ )
631
+ $requestedMilliseconds = [Math]::Max(0, $Seconds * 1000)
632
+ $sleepMilliseconds = [int][Math]::Min(
633
+ [int]::MaxValue,
634
+ [Math]::Min($remainingMilliseconds, $requestedMilliseconds)
635
+ )
636
+ if ($sleepMilliseconds -gt 0) {
637
+ Start-Sleep -Milliseconds $sleepMilliseconds
638
+ }
639
+ }
640
+
641
+ function Get-AnswerMeBoundedRequestTimeoutSeconds {
642
+ param(
643
+ [Parameter(Mandatory = $true)][DateTimeOffset]$Deadline,
644
+ [Parameter(Mandatory = $true)][ValidateRange(1, 300)][int]$MaximumSeconds
645
+ )
646
+
647
+ $remainingSeconds = [int][Math]::Ceiling(
648
+ ($Deadline - [DateTimeOffset]::UtcNow).TotalSeconds
649
+ )
650
+ if ($remainingSeconds -le 0) {
651
+ return 0
652
+ }
653
+ return [Math]::Min($MaximumSeconds, $remainingSeconds)
654
+ }
655
+
656
+ function Write-AnswerMeWaitTimeout {
657
+ param(
658
+ [Parameter(Mandatory = $true)][string]$Status,
659
+ [AllowNull()][string]$LastError,
660
+ [Parameter(Mandatory = $true)][string]$Mode
661
+ )
662
+
663
+ [PSCustomObject]@{
664
+ ok = $false
665
+ outcome = 'timeout'
666
+ interactionId = $InteractionId
667
+ status = $Status
668
+ code = 'wait-timeout'
669
+ requestedTimeoutSeconds = $TimeoutSeconds
670
+ effectiveTimeoutSeconds = $effectiveTimeoutSeconds
671
+ lastError = $LastError
672
+ waitMode = $Mode
673
+ preserveResultToken = $true
674
+ recoveryReference = 'references/errors.md'
675
+ resultTokenSource = $resultTokenSource
676
+ networkCalled = $true
677
+ recovery = 'Keep the original interaction and rerun this poll while the Result Token remains available.'
678
+ } | ConvertTo-Json -Depth 6
679
+ exit 5
680
+ }
681
+
682
+ function Write-AnswerMeTerminalResult {
683
+ param(
684
+ [Parameter(Mandatory = $true)][object]$Body,
685
+ [Parameter(Mandatory = $true)][string]$Mode
686
+ )
687
+ [PSCustomObject]@{
688
+ ok = $true
689
+ outcome = 'terminal'
690
+ interactionId = $InteractionId
691
+ status = [string]$Body.status
692
+ response = $Body
693
+ waitMode = $Mode
694
+ resultTokenSource = $resultTokenSource
695
+ networkCalled = $true
696
+ } | ConvertTo-Json -Depth 32
697
+ exit 0
698
+ }
699
+
700
+ function Write-AnswerMeRemoteFailure {
701
+ param(
702
+ [Parameter(Mandatory = $true)][int]$HttpStatus,
703
+ [Parameter(Mandatory = $true)][object]$SafeError,
704
+ [Parameter(Mandatory = $true)][int]$ExitCode,
705
+ [int]$RetryAfterSeconds = 0
706
+ )
707
+ $remoteCode = [string]$SafeError.code
708
+ $outcome = if ($HttpStatus -eq 404) { 'not-found' } elseif ($remoteCode -eq 'client_protocol_unsupported') { 'protocol-incompatible' } else { 'failed' }
709
+ [object[]]$supportedProtocolVersions = @()
710
+ $supportedProtocolVersions = @($SafeError.supportedProtocolVersions)
711
+ [PSCustomObject]@{
712
+ ok = $false
713
+ outcome = $outcome
714
+ interactionId = $InteractionId
715
+ status = 'unknown'
716
+ code = $remoteCode
717
+ httpStatus = $HttpStatus
718
+ reason = $SafeError.reason
719
+ retryAfterSeconds = $RetryAfterSeconds
720
+ receivedProtocolVersion = $SafeError.receivedProtocolVersion
721
+ supportedProtocolVersions = $supportedProtocolVersions
722
+ minimumCompatibleSkillVersion = $SafeError.minimumCompatibleSkillVersion
723
+ preserveResultToken = $true
724
+ recoveryReference = 'references/errors.md'
725
+ resultTokenSource = $resultTokenSource
726
+ networkCalled = $true
727
+ } | ConvertTo-Json -Depth 10
728
+ exit $ExitCode
729
+ }
730
+
731
+ function Write-AnswerMeInvalidRemoteResponse {
732
+ param([Parameter(Mandatory = $true)][int]$HttpStatus)
733
+
734
+ [PSCustomObject]@{
735
+ ok = $false
736
+ outcome = 'failed'
737
+ interactionId = $InteractionId
738
+ status = 'unknown'
739
+ code = 'result-response-invalid'
740
+ httpStatus = $HttpStatus
741
+ reason = $null
742
+ retryAfterSeconds = 0
743
+ receivedProtocolVersion = $null
744
+ supportedProtocolVersions = @()
745
+ minimumCompatibleSkillVersion = $null
746
+ preserveResultToken = $true
747
+ recoveryReference = 'references/errors.md'
748
+ resultTokenSource = $resultTokenSource
749
+ networkCalled = $true
750
+ } | ConvertTo-Json -Depth 10
751
+ exit 4
752
+ }
753
+
754
+ $escapedId = [Uri]::EscapeDataString($InteractionId)
755
+ $resultUri = ([Uri]::new($parsedBaseUrl, "/api/v1/interactions/$escapedId")).AbsoluteUri
756
+ $capabilitiesUri = ([Uri]::new($parsedBaseUrl, '/api/v1/capabilities')).AbsoluteUri
757
+ $capabilitiesHeaders = @{
758
+ Accept = 'application/json'
759
+ 'X-AnswerMe-Protocol-Version' = $AnswerMeProtocolVersion
760
+ 'X-AnswerMe-Skill-Version' = $AnswerMeSkillVersion
761
+ }
762
+ $requestHeaders = @{
763
+ Authorization = "Bearer $resultToken"
764
+ Accept = 'application/json'
765
+ 'X-AnswerMe-Protocol-Version' = $AnswerMeProtocolVersion
766
+ 'X-AnswerMe-Skill-Version' = $AnswerMeSkillVersion
767
+ }
768
+
769
+ $deadline = [DateTimeOffset]::UtcNow.AddSeconds($effectiveTimeoutSeconds)
770
+ $capabilitiesTimeout = Get-AnswerMeBoundedRequestTimeoutSeconds -Deadline $deadline -MaximumSeconds 15
771
+ if ($capabilitiesTimeout -le 0) {
772
+ Write-AnswerMeWaitTimeout -Status 'unknown' -LastError $null -Mode 'capability-discovery'
773
+ }
774
+ try {
775
+ $capabilitiesResponse = Invoke-AnswerMeWebRequest -Uri $capabilitiesUri -Method Get -Headers $capabilitiesHeaders -TimeoutSec $capabilitiesTimeout
776
+ }
777
+ catch {
778
+ if ([DateTimeOffset]::UtcNow -ge $deadline) {
779
+ Write-AnswerMeWaitTimeout -Status 'unknown' -LastError $_.Exception.Message -Mode 'capability-discovery'
780
+ }
781
+ Write-AnswerMeWaitFailure -Code 'capabilities-unavailable' -Message $_.Exception.Message -ExitCode 4 -NetworkCalled $true
782
+ }
783
+ $capabilitiesBody = ConvertFrom-AnswerMeJsonResponse -Response $capabilitiesResponse
784
+ if ([int]$capabilitiesResponse.StatusCode -eq 200 -and
785
+ (-not (Test-AnswerMeWaitNoStoreResponse -Response $capabilitiesResponse) -or
786
+ (Test-AnswerMeWaitBodyContainsSecret -Value $capabilitiesBody -Secrets $knownResponseSecrets) -or
787
+ -not (Test-AnswerMeWaitCapabilitiesResponse -Body $capabilitiesBody))) {
788
+ Write-AnswerMeWaitFailure -Code 'capabilities-invalid' -Message 'AnswerMe capabilities could not be verified.' -ExitCode 4 -NetworkCalled $true
789
+ }
790
+ $supportedProtocolVersions = @($capabilitiesBody.clientProtocol.supportedVersions)
791
+ if ([int]$capabilitiesResponse.StatusCode -eq 200 -and
792
+ $supportedProtocolVersions -notcontains $AnswerMeProtocolVersion) {
793
+ $protocolCompatibilityError = [PSCustomObject]@{
794
+ code = 'client_protocol_unsupported'
795
+ reason = 'answerme_client_protocol_version_is_not_supported'
796
+ receivedProtocolVersion = $AnswerMeProtocolVersion
797
+ supportedProtocolVersions = $supportedProtocolVersions
798
+ minimumCompatibleSkillVersion = [string]$capabilitiesBody.clientProtocol.minimumCompatibleSkillVersion
799
+ }
800
+ Write-AnswerMeRemoteFailure -HttpStatus 200 -SafeError $protocolCompatibilityError -ExitCode 6
801
+ }
802
+ if ([int]$capabilitiesResponse.StatusCode -ne 200 -or
803
+ $capabilitiesBody.capabilities.resultQuery -ne $true) {
804
+ if ([int]$capabilitiesResponse.StatusCode -eq 409) {
805
+ $safeCapabilityError = ConvertTo-AnswerMeSafeErrorResponse `
806
+ -HttpStatus 409 `
807
+ -Body $capabilitiesBody `
808
+ -Endpoint query `
809
+ -Response $capabilitiesResponse
810
+ if ($null -ne $safeCapabilityError -and
811
+ -not (Test-AnswerMeWaitBodyContainsSecret -Value $capabilitiesBody -Secrets $knownResponseSecrets)) {
812
+ Write-AnswerMeRemoteFailure -HttpStatus 409 -SafeError $safeCapabilityError -ExitCode 6
813
+ }
814
+ }
815
+ Write-AnswerMeWaitFailure -Code 'capabilities-invalid' -Message 'AnswerMe capabilities could not be verified.' -ExitCode 4 -NetworkCalled $true
816
+ }
817
+
818
+ $useActiveWait = $capabilitiesBody.capabilities.resultWait.enabled -eq $true
819
+ $lastStatus = 'unknown'
820
+ $lastError = $null
821
+
822
+ while ([DateTimeOffset]::UtcNow -lt $deadline) {
823
+ $remainingSeconds = [Math]::Max(1, [int][Math]::Ceiling(($deadline - [DateTimeOffset]::UtcNow).TotalSeconds))
824
+ if ($useActiveWait) {
825
+ $windowSeconds = [Math]::Min($effectiveTimeoutSeconds, $remainingSeconds)
826
+ if ($windowSeconds -lt 30) {
827
+ Write-AnswerMeWaitTimeout -Status $lastStatus -LastError $lastError -Mode 'capability-discovery'
828
+ }
829
+ $requestUri = "$resultUri/wait?timeoutSeconds=$windowSeconds"
830
+ $requestTimeout = $windowSeconds + 20
831
+ $mode = 'active-wait'
832
+ }
833
+ else {
834
+ $requestUri = $resultUri
835
+ $requestTimeout = [Math]::Min([Math]::Max(15, $PollSeconds + 10), $remainingSeconds)
836
+ $mode = 'polling-compatibility'
837
+ }
838
+
839
+ try {
840
+ $response = Invoke-AnswerMeWebRequest -Uri $requestUri -Method Get -Headers $requestHeaders -TimeoutSec $requestTimeout
841
+ }
842
+ catch {
843
+ $lastError = $_.Exception.Message
844
+ Write-AnswerMeWaitFailure -Code 'result-connection-unknown' -Message $_.Exception.Message -ExitCode 4 -NetworkCalled $true
845
+ }
846
+
847
+ $statusCode = [int]$response.StatusCode
848
+ $body = ConvertFrom-AnswerMeJsonResponse -Response $response
849
+ $endpoint = if ($useActiveWait) { 'wait' } else { 'query' }
850
+ if (Test-AnswerMeWaitBodyContainsSecret -Value $body -Secrets $knownResponseSecrets) {
851
+ Write-AnswerMeInvalidRemoteResponse -HttpStatus $statusCode
852
+ }
853
+ if ($statusCode -eq 200) {
854
+ if (-not (Test-AnswerMeWaitNoStoreResponse -Response $response)) {
855
+ Write-AnswerMeInvalidRemoteResponse -HttpStatus 200
856
+ }
857
+ $safeResult = ConvertTo-AnswerMeSafeResultResponse -Body $body -Endpoint $endpoint
858
+ if ($null -eq $safeResult) {
859
+ Write-AnswerMeInvalidRemoteResponse -HttpStatus 200
860
+ }
861
+ $lastStatus = [string]$safeResult.status
862
+ $lastError = $null
863
+ if ($lastStatus -in @('submitted', 'cancelled', 'expired', 'revoked')) {
864
+ Write-AnswerMeTerminalResult -Body $safeResult -Mode $mode
865
+ }
866
+ if ($useActiveWait) {
867
+ Write-AnswerMeInvalidRemoteResponse -HttpStatus 200
868
+ }
869
+ }
870
+ elseif ($statusCode -eq 204 -and $useActiveWait) {
871
+ $waitReason = [string]$response.Headers['X-AnswerMe-Result-Wait-Reason']
872
+ if ($null -ne $body -or
873
+ -not (Test-AnswerMeWaitNoStoreResponse -Response $response) -or
874
+ $waitReason -cne 'timeout') {
875
+ Write-AnswerMeInvalidRemoteResponse -HttpStatus 204
876
+ }
877
+ Write-AnswerMeWaitTimeout -Status 'pending' -LastError $null -Mode 'active-wait'
878
+ }
879
+ elseif ($statusCode -in @(400, 404, 409, 429, 503)) {
880
+ if (($statusCode -eq 429 -and -not $useActiveWait)) {
881
+ Write-AnswerMeInvalidRemoteResponse -HttpStatus $statusCode
882
+ }
883
+ $safeError = ConvertTo-AnswerMeSafeErrorResponse `
884
+ -HttpStatus $statusCode `
885
+ -Body $body `
886
+ -Endpoint $endpoint `
887
+ -Response $response
888
+ if ($null -eq $safeError) {
889
+ Write-AnswerMeInvalidRemoteResponse -HttpStatus $statusCode
890
+ }
891
+ $retryAfter = Get-AnswerMeRetryAfterSeconds -Response $response
892
+ Write-AnswerMeRemoteFailure `
893
+ -HttpStatus $statusCode `
894
+ -SafeError $safeError `
895
+ -ExitCode $(if ($statusCode -eq 409) { 6 } else { 4 }) `
896
+ -RetryAfterSeconds $retryAfter
897
+ }
898
+ else {
899
+ Write-AnswerMeInvalidRemoteResponse -HttpStatus $statusCode
900
+ }
901
+
902
+ if (-not $useActiveWait) {
903
+ Start-AnswerMeBoundedSleep -Deadline $deadline -Seconds $PollSeconds
904
+ }
905
+ }
906
+
907
+ $timeoutMode = if ($useActiveWait) { 'active-wait' } else { 'polling-compatibility' }
908
+ Write-AnswerMeWaitTimeout -Status $lastStatus -LastError $lastError -Mode $timeoutMode