@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,1892 @@
|
|
|
1
|
+
[CmdletBinding()]
|
|
2
|
+
param(
|
|
3
|
+
[string]$TemplateJson,
|
|
4
|
+
[string]$CompactQuestionnaireJson,
|
|
5
|
+
[string]$ApiBaseUrl,
|
|
6
|
+
[switch]$AllowInsecureLoopbackHttpForTest,
|
|
7
|
+
[string]$IdempotencyKey,
|
|
8
|
+
[string]$CreatorKeyEnvironmentVariable = 'ANSWERME_CREATOR_KEY',
|
|
9
|
+
[string]$CredentialPath = $env:ANSWERME_CREATOR_CREDENTIAL_PATH,
|
|
10
|
+
[string]$ResultTokenStoreRoot = $env:ANSWERME_RESULT_TOKEN_STORE_ROOT,
|
|
11
|
+
[switch]$AllowNonDefaultResultTokenStore,
|
|
12
|
+
[object]$WaitTimeoutSeconds = 300,
|
|
13
|
+
[object]$EnrollmentWaitTimeoutSeconds = 300,
|
|
14
|
+
[switch]$SkipPopup,
|
|
15
|
+
[switch]$ValidateOnly,
|
|
16
|
+
[switch]$ValidateCredentialOnly
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
$ErrorActionPreference = 'Stop'
|
|
20
|
+
|
|
21
|
+
$processStartInfoSupport = Join-Path $PSScriptRoot 'windows-process-start-info.ps1'
|
|
22
|
+
if (-not (Test-Path -LiteralPath $processStartInfoSupport -PathType Leaf)) {
|
|
23
|
+
throw 'The AnswerMe Windows process argument helper is missing.'
|
|
24
|
+
}
|
|
25
|
+
. $processStartInfoSupport
|
|
26
|
+
|
|
27
|
+
$cryptoSupport = Join-Path $PSScriptRoot 'windows-crypto.ps1'
|
|
28
|
+
if (-not (Test-Path -LiteralPath $cryptoSupport -PathType Leaf)) {
|
|
29
|
+
throw 'The AnswerMe Windows cryptography helper is missing.'
|
|
30
|
+
}
|
|
31
|
+
. $cryptoSupport
|
|
32
|
+
|
|
33
|
+
$httpSupport = Join-Path $PSScriptRoot 'windows-http.ps1'
|
|
34
|
+
if (-not (Test-Path -LiteralPath $httpSupport -PathType Leaf)) {
|
|
35
|
+
throw 'The AnswerMe Windows HTTP helper is missing.'
|
|
36
|
+
}
|
|
37
|
+
. $httpSupport
|
|
38
|
+
|
|
39
|
+
$AnswerMeProtocolVersion = '1.1'
|
|
40
|
+
$AnswerMeSkillVersion = '0.3.0'
|
|
41
|
+
$AnswerMeOperationStopwatch = [Diagnostics.Stopwatch]::StartNew()
|
|
42
|
+
$AnswerMeAutomaticEnrollmentAttempted = $false
|
|
43
|
+
$AnswerMeOriginalRequestRetried = $false
|
|
44
|
+
$AnswerMeTimings = [ordered]@{
|
|
45
|
+
inputValidationMilliseconds = 0L
|
|
46
|
+
configurationMilliseconds = 0L
|
|
47
|
+
capabilitiesMilliseconds = 0L
|
|
48
|
+
createRequestMilliseconds = 0L
|
|
49
|
+
responseValidationMilliseconds = 0L
|
|
50
|
+
tokenPersistenceMilliseconds = 0L
|
|
51
|
+
popupMilliseconds = 0L
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function Get-AnswerMeTimings {
|
|
55
|
+
$snapshot = [ordered]@{}
|
|
56
|
+
foreach ($entry in $AnswerMeTimings.GetEnumerator()) {
|
|
57
|
+
$snapshot[$entry.Key] = [long]$entry.Value
|
|
58
|
+
}
|
|
59
|
+
$snapshot.totalMilliseconds = [long]$AnswerMeOperationStopwatch.ElapsedMilliseconds
|
|
60
|
+
return [PSCustomObject]$snapshot
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Write-AnswerMeFailure {
|
|
64
|
+
param(
|
|
65
|
+
[Parameter(Mandatory = $true)][string]$Code,
|
|
66
|
+
[Parameter(Mandatory = $true)][string]$Message,
|
|
67
|
+
[Parameter(Mandatory = $true)][int]$ExitCode,
|
|
68
|
+
[bool]$NetworkCalled = $false,
|
|
69
|
+
[AllowNull()][string]$Reason,
|
|
70
|
+
[int]$RetryAfterSeconds = 0,
|
|
71
|
+
[AllowNull()][object]$RecoveryAction,
|
|
72
|
+
[AllowNull()][object]$RecoveryReference,
|
|
73
|
+
[AllowNull()][string]$IdempotencyKey
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
$failure = [ordered]@{
|
|
77
|
+
ok = $false
|
|
78
|
+
status = 'failed'
|
|
79
|
+
stage = 'create'
|
|
80
|
+
code = $Code
|
|
81
|
+
message = $Message
|
|
82
|
+
reason = $Reason
|
|
83
|
+
retryAfterSeconds = $RetryAfterSeconds
|
|
84
|
+
recoveryAction = $RecoveryAction
|
|
85
|
+
recoveryReference = $RecoveryReference
|
|
86
|
+
timings = Get-AnswerMeTimings
|
|
87
|
+
networkCalled = $NetworkCalled
|
|
88
|
+
automaticEnrollmentAttempted = $AnswerMeAutomaticEnrollmentAttempted
|
|
89
|
+
originalRequestRetried = $AnswerMeOriginalRequestRetried
|
|
90
|
+
}
|
|
91
|
+
if (-not [string]::IsNullOrWhiteSpace($IdempotencyKey)) {
|
|
92
|
+
$failure.idempotencyKey = $IdempotencyKey
|
|
93
|
+
}
|
|
94
|
+
[PSCustomObject]$failure | ConvertTo-Json -Depth 4
|
|
95
|
+
exit $ExitCode
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function Get-AnswerMeWaitDecision {
|
|
99
|
+
param([Parameter(Mandatory = $true)][object]$RequestedValue)
|
|
100
|
+
|
|
101
|
+
$text = [string]$RequestedValue
|
|
102
|
+
$integerValue = 0L
|
|
103
|
+
$numericValue = 0.0
|
|
104
|
+
if ([long]::TryParse(
|
|
105
|
+
$text,
|
|
106
|
+
[Globalization.NumberStyles]::Integer,
|
|
107
|
+
[Globalization.CultureInfo]::InvariantCulture,
|
|
108
|
+
[ref]$integerValue
|
|
109
|
+
)) {
|
|
110
|
+
$requested = $integerValue
|
|
111
|
+
$valid = $integerValue -eq 0 -or ($integerValue -ge 30 -and $integerValue -le 600)
|
|
112
|
+
}
|
|
113
|
+
elseif ([double]::TryParse(
|
|
114
|
+
$text,
|
|
115
|
+
[Globalization.NumberStyles]::Float,
|
|
116
|
+
[Globalization.CultureInfo]::InvariantCulture,
|
|
117
|
+
[ref]$numericValue
|
|
118
|
+
)) {
|
|
119
|
+
$requested = $numericValue
|
|
120
|
+
$valid = $false
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
$requested = $text
|
|
124
|
+
$valid = $false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return [PSCustomObject]@{
|
|
128
|
+
requestedTimeoutSeconds = $requested
|
|
129
|
+
effectiveTimeoutSeconds = if ($valid) { [int]$integerValue } else { 0 }
|
|
130
|
+
reason = if ($valid) { $null } else { 'timeout_out_of_range' }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ConvertTo-AnswerMeCanonicalJsonScalar {
|
|
135
|
+
param([AllowNull()][object]$Value)
|
|
136
|
+
|
|
137
|
+
return ConvertTo-Json -InputObject $Value -Compress -Depth 4
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function Get-DefaultAnswerMeCredentialPath {
|
|
141
|
+
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
|
|
142
|
+
return $null
|
|
143
|
+
}
|
|
144
|
+
$localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
|
|
145
|
+
if ([string]::IsNullOrWhiteSpace($localAppData)) {
|
|
146
|
+
return $null
|
|
147
|
+
}
|
|
148
|
+
return Join-Path $localAppData 'AnswerMe\creator-credential.clixml'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function Assert-AnswerMeAllowedProperties {
|
|
152
|
+
param(
|
|
153
|
+
[Parameter(Mandatory = $true)][object]$Value,
|
|
154
|
+
[Parameter(Mandatory = $true)][string[]]$Allowed,
|
|
155
|
+
[Parameter(Mandatory = $true)][string]$Context
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
$unsupported = @($Value.PSObject.Properties.Name | Where-Object { $Allowed -cnotcontains $_ })
|
|
159
|
+
if ($unsupported.Count -gt 0) {
|
|
160
|
+
throw "$Context contains unsupported field '$($unsupported[0])'."
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function Assert-AnswerMeJsonObject {
|
|
165
|
+
param(
|
|
166
|
+
[AllowNull()][object]$Value,
|
|
167
|
+
[Parameter(Mandatory = $true)][string]$Context
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if ($Value -isnot [PSCustomObject]) {
|
|
171
|
+
throw "$Context must be a JSON object."
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function Assert-AnswerMeJsonUnicodeWellFormed {
|
|
176
|
+
param([Parameter(Mandatory = $true)][string]$Json)
|
|
177
|
+
|
|
178
|
+
$insideString = $false
|
|
179
|
+
for ($index = 0; $index -lt $Json.Length; $index += 1) {
|
|
180
|
+
$current = $Json[$index]
|
|
181
|
+
if ([char]::IsHighSurrogate($current)) {
|
|
182
|
+
if ($index + 1 -ge $Json.Length -or -not [char]::IsLowSurrogate($Json[$index + 1])) {
|
|
183
|
+
throw 'JSON contains an unpaired Unicode surrogate.'
|
|
184
|
+
}
|
|
185
|
+
$index += 1
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
if ([char]::IsLowSurrogate($current)) {
|
|
189
|
+
throw 'JSON contains an unpaired Unicode surrogate.'
|
|
190
|
+
}
|
|
191
|
+
if (-not $insideString) {
|
|
192
|
+
if ($current -ceq [char]'"') { $insideString = $true }
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
if ($current -ceq [char]'"') {
|
|
196
|
+
$insideString = $false
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
if ($current -cne [char]'\') { continue }
|
|
200
|
+
if ($index + 1 -ge $Json.Length) { return }
|
|
201
|
+
|
|
202
|
+
$escape = $Json[$index + 1]
|
|
203
|
+
if ($escape -cne [char]'u') {
|
|
204
|
+
$index += 1
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
if ($index + 5 -ge $Json.Length) { return }
|
|
208
|
+
|
|
209
|
+
$hex = $Json.Substring($index + 2, 4)
|
|
210
|
+
$codeUnit = 0
|
|
211
|
+
if (-not [int]::TryParse(
|
|
212
|
+
$hex,
|
|
213
|
+
[Globalization.NumberStyles]::AllowHexSpecifier,
|
|
214
|
+
[Globalization.CultureInfo]::InvariantCulture,
|
|
215
|
+
[ref]$codeUnit
|
|
216
|
+
)) { return }
|
|
217
|
+
if ($codeUnit -ge 0xD800 -and $codeUnit -le 0xDBFF) {
|
|
218
|
+
if ($index + 11 -ge $Json.Length -or
|
|
219
|
+
$Json[$index + 6] -cne [char]'\' -or
|
|
220
|
+
$Json[$index + 7] -cne [char]'u') {
|
|
221
|
+
throw 'JSON contains an unpaired Unicode surrogate escape.'
|
|
222
|
+
}
|
|
223
|
+
$lowHex = $Json.Substring($index + 8, 4)
|
|
224
|
+
$lowCodeUnit = 0
|
|
225
|
+
if (-not [int]::TryParse(
|
|
226
|
+
$lowHex,
|
|
227
|
+
[Globalization.NumberStyles]::AllowHexSpecifier,
|
|
228
|
+
[Globalization.CultureInfo]::InvariantCulture,
|
|
229
|
+
[ref]$lowCodeUnit
|
|
230
|
+
) -or $lowCodeUnit -lt 0xDC00 -or $lowCodeUnit -gt 0xDFFF) {
|
|
231
|
+
throw 'JSON contains an unpaired Unicode surrogate escape.'
|
|
232
|
+
}
|
|
233
|
+
$index += 11
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
if ($codeUnit -ge 0xDC00 -and $codeUnit -le 0xDFFF) {
|
|
237
|
+
throw 'JSON contains an unpaired Unicode surrogate escape.'
|
|
238
|
+
}
|
|
239
|
+
$index += 5
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function Get-AnswerMeProperty {
|
|
244
|
+
param(
|
|
245
|
+
[Parameter(Mandatory = $true)][object]$Value,
|
|
246
|
+
[Parameter(Mandatory = $true)][string]$Name,
|
|
247
|
+
[Parameter(Mandatory = $true)][string]$Context,
|
|
248
|
+
[switch]$Required
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
$properties = @($Value.PSObject.Properties | Where-Object { $_.Name -ceq $Name })
|
|
252
|
+
if ($properties.Count -gt 1) {
|
|
253
|
+
throw "$Context contains duplicate field '$Name'."
|
|
254
|
+
}
|
|
255
|
+
if ($properties.Count -eq 0) {
|
|
256
|
+
if ($Required) {
|
|
257
|
+
throw "$Context is missing required field '$Name'."
|
|
258
|
+
}
|
|
259
|
+
return $null
|
|
260
|
+
}
|
|
261
|
+
return $properties[0]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function Get-AnswerMeJsonArray {
|
|
265
|
+
param(
|
|
266
|
+
[AllowNull()][object]$Value,
|
|
267
|
+
[Parameter(Mandatory = $true)][string]$Context
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if ($null -eq $Value -or $Value -isnot [Array]) {
|
|
271
|
+
throw "$Context must be a JSON array."
|
|
272
|
+
}
|
|
273
|
+
return [object[]]$Value
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function Get-AnswerMeInteger {
|
|
277
|
+
param(
|
|
278
|
+
[AllowNull()][object]$Value,
|
|
279
|
+
[Parameter(Mandatory = $true)][string]$Context
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if ($Value -isnot [int] -and $Value -isnot [long]) {
|
|
283
|
+
throw "$Context must be an integer."
|
|
284
|
+
}
|
|
285
|
+
return [long]$Value
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function Get-AnswerMeUnicodeCodePointCount {
|
|
289
|
+
param(
|
|
290
|
+
[Parameter(Mandatory = $true)][string]$Value,
|
|
291
|
+
[Parameter(Mandatory = $true)][string]$Context
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
$count = 0
|
|
295
|
+
for ($index = 0; $index -lt $Value.Length; $index += 1) {
|
|
296
|
+
$current = $Value[$index]
|
|
297
|
+
if ([char]::IsHighSurrogate($current)) {
|
|
298
|
+
if ($index + 1 -ge $Value.Length -or -not [char]::IsLowSurrogate($Value[$index + 1])) {
|
|
299
|
+
throw "$Context contains an unpaired Unicode surrogate."
|
|
300
|
+
}
|
|
301
|
+
$index += 1
|
|
302
|
+
}
|
|
303
|
+
elseif ([char]::IsLowSurrogate($current)) {
|
|
304
|
+
throw "$Context contains an unpaired Unicode surrogate."
|
|
305
|
+
}
|
|
306
|
+
$count += 1
|
|
307
|
+
}
|
|
308
|
+
return $count
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function Get-AnswerMeNormalizedString {
|
|
312
|
+
param(
|
|
313
|
+
[AllowNull()][object]$Value,
|
|
314
|
+
[Parameter(Mandatory = $true)][string]$Context,
|
|
315
|
+
[int]$MinimumCodePoints = 0,
|
|
316
|
+
[int]$MaximumCodePoints = [int]::MaxValue
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if ($Value -isnot [string]) {
|
|
320
|
+
throw "$Context must be a string."
|
|
321
|
+
}
|
|
322
|
+
$trimmed = $Value.Trim()
|
|
323
|
+
$codePointCount = Get-AnswerMeUnicodeCodePointCount -Value $trimmed -Context $Context
|
|
324
|
+
if ($codePointCount -lt $MinimumCodePoints -or $codePointCount -gt $MaximumCodePoints) {
|
|
325
|
+
throw "$Context must contain between $MinimumCodePoints and $MaximumCodePoints Unicode code points."
|
|
326
|
+
}
|
|
327
|
+
return $trimmed
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function Get-AnswerMeSubstringByCodePoints {
|
|
331
|
+
param(
|
|
332
|
+
[Parameter(Mandatory = $true)][string]$Value,
|
|
333
|
+
[Parameter(Mandatory = $true)][ValidateRange(1, [int]::MaxValue)][int]$MaximumCodePoints
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
$endIndex = 0
|
|
337
|
+
$codePointCount = 0
|
|
338
|
+
while ($endIndex -lt $Value.Length -and $codePointCount -lt $MaximumCodePoints) {
|
|
339
|
+
$current = $Value[$endIndex]
|
|
340
|
+
if ([char]::IsHighSurrogate($current)) {
|
|
341
|
+
if ($endIndex + 1 -ge $Value.Length -or -not [char]::IsLowSurrogate($Value[$endIndex + 1])) {
|
|
342
|
+
throw 'String contains an unpaired Unicode surrogate.'
|
|
343
|
+
}
|
|
344
|
+
$endIndex += 2
|
|
345
|
+
}
|
|
346
|
+
elseif ([char]::IsLowSurrogate($current)) {
|
|
347
|
+
throw 'String contains an unpaired Unicode surrogate.'
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
$endIndex += 1
|
|
351
|
+
}
|
|
352
|
+
$codePointCount += 1
|
|
353
|
+
}
|
|
354
|
+
return $Value.Substring(0, $endIndex)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function Get-AnswerMeCompactShortLabel {
|
|
358
|
+
param([Parameter(Mandatory = $true)][string]$Title)
|
|
359
|
+
|
|
360
|
+
return Get-AnswerMeSubstringByCodePoints -Value $Title -MaximumCodePoints 20
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function ConvertFrom-AnswerMeCompactQuestionnaire {
|
|
364
|
+
param([Parameter(Mandatory = $true)][object]$Compact)
|
|
365
|
+
|
|
366
|
+
Assert-AnswerMeJsonObject -Value $Compact -Context 'Compact questionnaire'
|
|
367
|
+
Assert-AnswerMeAllowedProperties `
|
|
368
|
+
-Value $Compact `
|
|
369
|
+
-Allowed @('topic', 'intro', 'questions', 'allowAdditionalContext', 'expiresInSeconds') `
|
|
370
|
+
-Context 'Compact questionnaire'
|
|
371
|
+
|
|
372
|
+
$topicProperty = Get-AnswerMeProperty -Value $Compact -Name 'topic' -Context 'Compact questionnaire' -Required
|
|
373
|
+
$topic = Get-AnswerMeNormalizedString -Value $topicProperty.Value -Context 'Compact questionnaire topic' -MinimumCodePoints 1 -MaximumCodePoints 100
|
|
374
|
+
$questionsProperty = Get-AnswerMeProperty -Value $Compact -Name 'questions' -Context 'Compact questionnaire' -Required
|
|
375
|
+
$compactQuestions = @(Get-AnswerMeJsonArray -Value $questionsProperty.Value -Context 'Compact questionnaire questions')
|
|
376
|
+
if ($compactQuestions.Count -lt 1 -or $compactQuestions.Count -gt 20) {
|
|
377
|
+
throw 'Compact questionnaire must contain between 1 and 20 questions.'
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
$intro = $null
|
|
381
|
+
$introProperty = Get-AnswerMeProperty -Value $Compact -Name 'intro' -Context 'Compact questionnaire'
|
|
382
|
+
if ($null -ne $introProperty) {
|
|
383
|
+
$intro = Get-AnswerMeNormalizedString -Value $introProperty.Value -Context 'Compact questionnaire intro' -MaximumCodePoints 1000
|
|
384
|
+
}
|
|
385
|
+
$allowAdditionalContext = $false
|
|
386
|
+
$additionalContextProperty = Get-AnswerMeProperty -Value $Compact -Name 'allowAdditionalContext' -Context 'Compact questionnaire'
|
|
387
|
+
if ($null -ne $additionalContextProperty) {
|
|
388
|
+
if ($additionalContextProperty.Value -isnot [bool]) {
|
|
389
|
+
throw 'Compact questionnaire allowAdditionalContext must be a boolean.'
|
|
390
|
+
}
|
|
391
|
+
$allowAdditionalContext = [bool]$additionalContextProperty.Value
|
|
392
|
+
}
|
|
393
|
+
$expiresInSeconds = 3600
|
|
394
|
+
$expiresProperty = Get-AnswerMeProperty -Value $Compact -Name 'expiresInSeconds' -Context 'Compact questionnaire'
|
|
395
|
+
if ($null -ne $expiresProperty) {
|
|
396
|
+
$expiresInSeconds = Get-AnswerMeInteger -Value $expiresProperty.Value -Context 'Compact questionnaire expiresInSeconds'
|
|
397
|
+
if ($expiresInSeconds -lt 1 -or $expiresInSeconds -gt 604800) {
|
|
398
|
+
throw 'Compact questionnaire expiresInSeconds must be between 1 and 604800.'
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
$expandedQuestions = @()
|
|
403
|
+
$schemaVersion = 1
|
|
404
|
+
for ($questionIndex = 0; $questionIndex -lt $compactQuestions.Count; $questionIndex += 1) {
|
|
405
|
+
$question = $compactQuestions[$questionIndex]
|
|
406
|
+
$questionNumber = $questionIndex + 1
|
|
407
|
+
$context = "Compact question $questionNumber"
|
|
408
|
+
Assert-AnswerMeJsonObject -Value $question -Context $context
|
|
409
|
+
Assert-AnswerMeAllowedProperties `
|
|
410
|
+
-Value $question `
|
|
411
|
+
-Allowed @('title', 'type', 'body', 'maxLength', 'options', 'minSelections', 'maxSelections') `
|
|
412
|
+
-Context $context
|
|
413
|
+
|
|
414
|
+
$titleProperty = Get-AnswerMeProperty -Value $question -Name 'title' -Context $context -Required
|
|
415
|
+
$title = Get-AnswerMeNormalizedString -Value $titleProperty.Value -Context "$context title" -MinimumCodePoints 1 -MaximumCodePoints 1000
|
|
416
|
+
$typeProperty = Get-AnswerMeProperty -Value $question -Name 'type' -Context $context -Required
|
|
417
|
+
$questionType = Get-AnswerMeNormalizedString -Value $typeProperty.Value -Context "$context type" -MinimumCodePoints 1 -MaximumCodePoints 32
|
|
418
|
+
if ($questionType -cnotin @('single-choice', 'multiple-choice', 'text')) {
|
|
419
|
+
throw "$context must use type=single-choice, type=multiple-choice or type=text."
|
|
420
|
+
}
|
|
421
|
+
$bodyProperty = Get-AnswerMeProperty -Value $question -Name 'body' -Context $context
|
|
422
|
+
if ($null -ne $bodyProperty -or $questionType -ceq 'text') { $schemaVersion = 2 }
|
|
423
|
+
if ($questionType -ceq 'text') {
|
|
424
|
+
Assert-AnswerMeAllowedProperties -Value $question -Allowed @('title', 'type', 'body', 'maxLength') -Context $context
|
|
425
|
+
$expandedText = [ordered]@{
|
|
426
|
+
id = "q$questionNumber"
|
|
427
|
+
type = 'text'
|
|
428
|
+
title = $title
|
|
429
|
+
shortLabel = Get-AnswerMeCompactShortLabel -Title $title
|
|
430
|
+
required = $true
|
|
431
|
+
}
|
|
432
|
+
if ($null -ne $bodyProperty) { $expandedText.body = $bodyProperty.Value }
|
|
433
|
+
$lengthProperty = Get-AnswerMeProperty -Value $question -Name 'maxLength' -Context $context
|
|
434
|
+
if ($null -ne $lengthProperty) { $expandedText.maxLength = $lengthProperty.Value }
|
|
435
|
+
$expandedQuestions += [PSCustomObject]$expandedText
|
|
436
|
+
continue
|
|
437
|
+
}
|
|
438
|
+
if ($null -ne (Get-AnswerMeProperty -Value $question -Name 'maxLength' -Context $context)) {
|
|
439
|
+
throw "$context choice questions forbid maxLength."
|
|
440
|
+
}
|
|
441
|
+
$optionsProperty = Get-AnswerMeProperty -Value $question -Name 'options' -Context $context -Required
|
|
442
|
+
$compactOptions = @(Get-AnswerMeJsonArray -Value $optionsProperty.Value -Context "$context options")
|
|
443
|
+
if ($compactOptions.Count -lt 2 -or $compactOptions.Count -gt 20) {
|
|
444
|
+
throw "$context must contain between 2 and 20 option labels."
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
$questionId = "q$questionNumber"
|
|
448
|
+
$expandedOptions = @()
|
|
449
|
+
for ($optionIndex = 0; $optionIndex -lt $compactOptions.Count; $optionIndex += 1) {
|
|
450
|
+
$optionLabel = Get-AnswerMeNormalizedString `
|
|
451
|
+
-Value $compactOptions[$optionIndex] `
|
|
452
|
+
-Context "$context option $($optionIndex + 1)" `
|
|
453
|
+
-MinimumCodePoints 1 `
|
|
454
|
+
-MaximumCodePoints 100
|
|
455
|
+
$expandedOptions += [PSCustomObject][ordered]@{
|
|
456
|
+
id = "$questionId-o$($optionIndex + 1)"
|
|
457
|
+
label = $optionLabel
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if ($questionType -ceq 'single-choice') {
|
|
462
|
+
if ($null -ne (Get-AnswerMeProperty -Value $question -Name 'minSelections' -Context $context) -or
|
|
463
|
+
$null -ne (Get-AnswerMeProperty -Value $question -Name 'maxSelections' -Context $context)) {
|
|
464
|
+
throw "$context single-choice selection range is fixed by the adapter."
|
|
465
|
+
}
|
|
466
|
+
$minSelections = 1
|
|
467
|
+
$maxSelections = 1
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
$minSelections = 1
|
|
471
|
+
$maxSelections = $compactOptions.Count
|
|
472
|
+
$minimumProperty = Get-AnswerMeProperty -Value $question -Name 'minSelections' -Context $context
|
|
473
|
+
if ($null -ne $minimumProperty) {
|
|
474
|
+
$minSelections = Get-AnswerMeInteger -Value $minimumProperty.Value -Context "$context minSelections"
|
|
475
|
+
}
|
|
476
|
+
$maximumProperty = Get-AnswerMeProperty -Value $question -Name 'maxSelections' -Context $context
|
|
477
|
+
if ($null -ne $maximumProperty) {
|
|
478
|
+
$maxSelections = Get-AnswerMeInteger -Value $maximumProperty.Value -Context "$context maxSelections"
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
$expandedQuestions += [PSCustomObject][ordered]@{
|
|
483
|
+
id = $questionId
|
|
484
|
+
type = $questionType
|
|
485
|
+
title = $title
|
|
486
|
+
shortLabel = Get-AnswerMeCompactShortLabel -Title $title
|
|
487
|
+
required = $true
|
|
488
|
+
minSelections = $minSelections
|
|
489
|
+
maxSelections = $maxSelections
|
|
490
|
+
options = $expandedOptions
|
|
491
|
+
allowCustomAnswer = $false
|
|
492
|
+
}
|
|
493
|
+
if ($null -ne $bodyProperty) { $expandedQuestions[-1] | Add-Member -NotePropertyName body -NotePropertyValue $bodyProperty.Value }
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
$expanded = [ordered]@{
|
|
497
|
+
schemaVersion = $schemaVersion
|
|
498
|
+
type = 'questionnaire'
|
|
499
|
+
topic = $topic
|
|
500
|
+
questions = $expandedQuestions
|
|
501
|
+
allowAdditionalContext = $allowAdditionalContext
|
|
502
|
+
expiresInSeconds = $expiresInSeconds
|
|
503
|
+
visual = [PSCustomObject]@{ theme = 'auto' }
|
|
504
|
+
}
|
|
505
|
+
if ($null -ne $intro -and $intro.Length -gt 0) {
|
|
506
|
+
$expanded.intro = $intro
|
|
507
|
+
}
|
|
508
|
+
return [PSCustomObject]$expanded
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function Assert-AnswerMeTemplate {
|
|
512
|
+
param([Parameter(Mandatory = $true)][object]$Template)
|
|
513
|
+
|
|
514
|
+
Assert-AnswerMeJsonObject -Value $Template -Context 'Template'
|
|
515
|
+
Assert-AnswerMeAllowedProperties `
|
|
516
|
+
-Value $Template `
|
|
517
|
+
-Allowed @('schemaVersion', 'type', 'topic', 'intro', 'questions', 'allowAdditionalContext', 'expiresInSeconds', 'visual') `
|
|
518
|
+
-Context 'Template'
|
|
519
|
+
|
|
520
|
+
$schemaVersionProperty = Get-AnswerMeProperty -Value $Template -Name 'schemaVersion' -Context 'Template' -Required
|
|
521
|
+
$schemaVersion = Get-AnswerMeInteger -Value $schemaVersionProperty.Value -Context 'Template schemaVersion'
|
|
522
|
+
if ($schemaVersion -notin @(1, 2)) {
|
|
523
|
+
throw 'Template must use schemaVersion=1 or schemaVersion=2.'
|
|
524
|
+
}
|
|
525
|
+
$typeProperty = Get-AnswerMeProperty -Value $Template -Name 'type' -Context 'Template' -Required
|
|
526
|
+
$templateType = Get-AnswerMeNormalizedString -Value $typeProperty.Value -Context 'Template type' -MinimumCodePoints 1 -MaximumCodePoints 32
|
|
527
|
+
if ($templateType -cne 'questionnaire') {
|
|
528
|
+
throw 'Template must use type=questionnaire.'
|
|
529
|
+
}
|
|
530
|
+
$typeProperty.Value = $templateType
|
|
531
|
+
$topicProperty = Get-AnswerMeProperty -Value $Template -Name 'topic' -Context 'Template' -Required
|
|
532
|
+
$topicProperty.Value = Get-AnswerMeNormalizedString -Value $topicProperty.Value -Context 'Template topic' -MinimumCodePoints 1 -MaximumCodePoints 100
|
|
533
|
+
|
|
534
|
+
$introProperty = Get-AnswerMeProperty -Value $Template -Name 'intro' -Context 'Template'
|
|
535
|
+
if ($null -ne $introProperty) {
|
|
536
|
+
$introProperty.Value = Get-AnswerMeNormalizedString -Value $introProperty.Value -Context 'Template intro' -MaximumCodePoints 1000
|
|
537
|
+
}
|
|
538
|
+
$additionalContextProperty = Get-AnswerMeProperty -Value $Template -Name 'allowAdditionalContext' -Context 'Template'
|
|
539
|
+
if ($null -ne $additionalContextProperty -and $additionalContextProperty.Value -isnot [bool]) {
|
|
540
|
+
throw 'Template allowAdditionalContext must be a boolean.'
|
|
541
|
+
}
|
|
542
|
+
$expiresProperty = Get-AnswerMeProperty -Value $Template -Name 'expiresInSeconds' -Context 'Template'
|
|
543
|
+
if ($null -ne $expiresProperty) {
|
|
544
|
+
$expiresInSeconds = Get-AnswerMeInteger -Value $expiresProperty.Value -Context 'Template expiresInSeconds'
|
|
545
|
+
if ($expiresInSeconds -lt 1 -or $expiresInSeconds -gt 604800) {
|
|
546
|
+
throw 'Template expiresInSeconds must be between 1 and 604800.'
|
|
547
|
+
}
|
|
548
|
+
$expiresProperty.Value = $expiresInSeconds
|
|
549
|
+
}
|
|
550
|
+
$visualProperty = Get-AnswerMeProperty -Value $Template -Name 'visual' -Context 'Template' -Required
|
|
551
|
+
$visual = $visualProperty.Value
|
|
552
|
+
Assert-AnswerMeJsonObject -Value $visual -Context 'Template visual'
|
|
553
|
+
Assert-AnswerMeAllowedProperties -Value $visual -Allowed @('theme') -Context 'Template visual'
|
|
554
|
+
$themeProperty = Get-AnswerMeProperty -Value $visual -Name 'theme' -Context 'Template visual' -Required
|
|
555
|
+
$theme = Get-AnswerMeNormalizedString -Value $themeProperty.Value -Context 'Template visual theme' -MinimumCodePoints 1 -MaximumCodePoints 16
|
|
556
|
+
if ($theme -cne 'auto') {
|
|
557
|
+
throw 'Template visual theme must be auto.'
|
|
558
|
+
}
|
|
559
|
+
$themeProperty.Value = $theme
|
|
560
|
+
|
|
561
|
+
$questionsProperty = Get-AnswerMeProperty -Value $Template -Name 'questions' -Context 'Template' -Required
|
|
562
|
+
$questions = @(Get-AnswerMeJsonArray -Value $questionsProperty.Value -Context 'Template questions')
|
|
563
|
+
if ($questions.Count -lt 1 -or $questions.Count -gt 20) {
|
|
564
|
+
throw 'Template must contain between 1 and 20 questions.'
|
|
565
|
+
}
|
|
566
|
+
$questionIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
567
|
+
for ($questionIndex = 0; $questionIndex -lt $questions.Count; $questionIndex += 1) {
|
|
568
|
+
$question = $questions[$questionIndex]
|
|
569
|
+
$questionContext = "Question $($questionIndex + 1)"
|
|
570
|
+
Assert-AnswerMeJsonObject -Value $question -Context $questionContext
|
|
571
|
+
Assert-AnswerMeAllowedProperties `
|
|
572
|
+
-Value $question `
|
|
573
|
+
-Allowed @('id', 'type', 'title', 'shortLabel', 'required', 'body', 'maxLength', 'minSelections', 'maxSelections', 'options', 'allowCustomAnswer', 'customAnswerMaxLength') `
|
|
574
|
+
-Context $questionContext
|
|
575
|
+
|
|
576
|
+
$questionIdProperty = Get-AnswerMeProperty -Value $question -Name 'id' -Context $questionContext -Required
|
|
577
|
+
$questionId = Get-AnswerMeNormalizedString -Value $questionIdProperty.Value -Context "$questionContext id" -MinimumCodePoints 1
|
|
578
|
+
$questionIdProperty.Value = $questionId
|
|
579
|
+
if (-not $questionIds.Add($questionId)) {
|
|
580
|
+
throw "Duplicate question id '$questionId'."
|
|
581
|
+
}
|
|
582
|
+
$questionTypeProperty = Get-AnswerMeProperty -Value $question -Name 'type' -Context $questionContext -Required
|
|
583
|
+
$questionType = Get-AnswerMeNormalizedString -Value $questionTypeProperty.Value -Context "$questionContext type" -MinimumCodePoints 1 -MaximumCodePoints 32
|
|
584
|
+
if ($questionType -cnotin @('single-choice', 'multiple-choice', 'text')) {
|
|
585
|
+
throw "$questionContext must use type=single-choice, type=multiple-choice or type=text."
|
|
586
|
+
}
|
|
587
|
+
$bodyProperty = Get-AnswerMeProperty -Value $question -Name 'body' -Context $questionContext
|
|
588
|
+
$lengthProperty = Get-AnswerMeProperty -Value $question -Name 'maxLength' -Context $questionContext
|
|
589
|
+
if ($schemaVersion -eq 1 -and ($null -ne $bodyProperty -or $null -ne $lengthProperty -or $questionType -ceq 'text')) {
|
|
590
|
+
throw "$questionContext body, maxLength and text require schemaVersion=2."
|
|
591
|
+
}
|
|
592
|
+
if ($null -ne $bodyProperty) {
|
|
593
|
+
if ($bodyProperty.Value -isnot [string]) { throw "$questionContext body must be a string." }
|
|
594
|
+
if ($bodyProperty.Value.Length -gt 0 -and (Get-AnswerMeUnicodeCodePointCount -Value $bodyProperty.Value -Context "$questionContext body") -gt 10000) {
|
|
595
|
+
throw "$questionContext body must contain at most 10000 Unicode code points."
|
|
596
|
+
}
|
|
597
|
+
if ([string]::IsNullOrWhiteSpace($bodyProperty.Value)) { $question.PSObject.Properties.Remove('body') }
|
|
598
|
+
}
|
|
599
|
+
$questionTypeProperty.Value = $questionType
|
|
600
|
+
$titleProperty = Get-AnswerMeProperty -Value $question -Name 'title' -Context $questionContext -Required
|
|
601
|
+
$titleProperty.Value = Get-AnswerMeNormalizedString -Value $titleProperty.Value -Context "$questionContext title" -MinimumCodePoints 1 -MaximumCodePoints 1000
|
|
602
|
+
$shortLabelProperty = Get-AnswerMeProperty -Value $question -Name 'shortLabel' -Context $questionContext -Required
|
|
603
|
+
$shortLabelProperty.Value = Get-AnswerMeNormalizedString -Value $shortLabelProperty.Value -Context "$questionContext shortLabel" -MinimumCodePoints 1 -MaximumCodePoints 20
|
|
604
|
+
$requiredProperty = Get-AnswerMeProperty -Value $question -Name 'required' -Context $questionContext -Required
|
|
605
|
+
if ($requiredProperty.Value -isnot [bool]) {
|
|
606
|
+
throw "$questionContext required must be a boolean."
|
|
607
|
+
}
|
|
608
|
+
if ($questionType -ceq 'text') {
|
|
609
|
+
Assert-AnswerMeAllowedProperties -Value $question -Allowed @('id', 'type', 'title', 'shortLabel', 'required', 'body', 'maxLength') -Context $questionContext
|
|
610
|
+
$maxLength = 2000
|
|
611
|
+
if ($null -ne $lengthProperty) {
|
|
612
|
+
$maxLength = Get-AnswerMeInteger -Value $lengthProperty.Value -Context "$questionContext maxLength"
|
|
613
|
+
if ($maxLength -lt 1 -or $maxLength -gt 2000) { throw "$questionContext maxLength must be between 1 and 2000." }
|
|
614
|
+
$lengthProperty.Value = $maxLength
|
|
615
|
+
}
|
|
616
|
+
else { $question | Add-Member -NotePropertyName maxLength -NotePropertyValue $maxLength }
|
|
617
|
+
continue
|
|
618
|
+
}
|
|
619
|
+
if ($null -ne $lengthProperty) { throw "$questionContext choice questions forbid maxLength." }
|
|
620
|
+
$minimumProperty = Get-AnswerMeProperty -Value $question -Name 'minSelections' -Context $questionContext -Required
|
|
621
|
+
$minSelections = Get-AnswerMeInteger -Value $minimumProperty.Value -Context "$questionContext minSelections"
|
|
622
|
+
$minimumProperty.Value = $minSelections
|
|
623
|
+
$maximumProperty = Get-AnswerMeProperty -Value $question -Name 'maxSelections' -Context $questionContext -Required
|
|
624
|
+
$maxSelections = Get-AnswerMeInteger -Value $maximumProperty.Value -Context "$questionContext maxSelections"
|
|
625
|
+
$maximumProperty.Value = $maxSelections
|
|
626
|
+
$allowCustomAnswerProperty = Get-AnswerMeProperty -Value $question -Name 'allowCustomAnswer' -Context $questionContext -Required
|
|
627
|
+
if ($allowCustomAnswerProperty.Value -isnot [bool]) {
|
|
628
|
+
throw "$questionContext allowCustomAnswer must be a boolean."
|
|
629
|
+
}
|
|
630
|
+
$allowCustomAnswer = [bool]$allowCustomAnswerProperty.Value
|
|
631
|
+
$optionsProperty = Get-AnswerMeProperty -Value $question -Name 'options' -Context $questionContext -Required
|
|
632
|
+
$options = @(Get-AnswerMeJsonArray -Value $optionsProperty.Value -Context "$questionContext options")
|
|
633
|
+
if ($options.Count -lt 2 -or $options.Count -gt 20) {
|
|
634
|
+
throw "Question '$questionId' must contain between 2 and 20 options."
|
|
635
|
+
}
|
|
636
|
+
if ($questionType -ceq 'single-choice') {
|
|
637
|
+
if ($minSelections -ne 1 -or $maxSelections -ne 1) {
|
|
638
|
+
throw "Question '$questionId' must use minSelections=1 and maxSelections=1 for single-choice."
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
$availableSelections = $options.Count + $(if ($allowCustomAnswer) { 1 } else { 0 })
|
|
643
|
+
if ($minSelections -lt 1 -or
|
|
644
|
+
$maxSelections -lt 2 -or
|
|
645
|
+
$minSelections -gt $maxSelections -or
|
|
646
|
+
$maxSelections -gt 20 -or
|
|
647
|
+
$maxSelections -gt $availableSelections) {
|
|
648
|
+
throw "Question '$questionId' has an invalid multiple-choice selection range."
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
$customAnswerLengthProperty = Get-AnswerMeProperty -Value $question -Name 'customAnswerMaxLength' -Context $questionContext
|
|
652
|
+
if ($null -ne $customAnswerLengthProperty) {
|
|
653
|
+
$customAnswerMaxLength = Get-AnswerMeInteger -Value $customAnswerLengthProperty.Value -Context "$questionContext customAnswerMaxLength"
|
|
654
|
+
if (-not $allowCustomAnswer -or $customAnswerMaxLength -lt 1 -or $customAnswerMaxLength -gt 2000) {
|
|
655
|
+
throw "Question '$questionId' has an invalid customAnswerMaxLength."
|
|
656
|
+
}
|
|
657
|
+
$customAnswerLengthProperty.Value = $customAnswerMaxLength
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
$optionIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
661
|
+
for ($optionIndex = 0; $optionIndex -lt $options.Count; $optionIndex += 1) {
|
|
662
|
+
$option = $options[$optionIndex]
|
|
663
|
+
$optionContext = "Question '$questionId' option $($optionIndex + 1)"
|
|
664
|
+
Assert-AnswerMeJsonObject -Value $option -Context $optionContext
|
|
665
|
+
Assert-AnswerMeAllowedProperties -Value $option -Allowed @('id', 'label', 'description') -Context $optionContext
|
|
666
|
+
$optionIdProperty = Get-AnswerMeProperty -Value $option -Name 'id' -Context $optionContext -Required
|
|
667
|
+
$optionId = Get-AnswerMeNormalizedString -Value $optionIdProperty.Value -Context "$optionContext id" -MinimumCodePoints 1
|
|
668
|
+
$optionIdProperty.Value = $optionId
|
|
669
|
+
if (-not $optionIds.Add($optionId)) {
|
|
670
|
+
throw "Duplicate option id '$optionId' in question '$questionId'."
|
|
671
|
+
}
|
|
672
|
+
$labelProperty = Get-AnswerMeProperty -Value $option -Name 'label' -Context $optionContext -Required
|
|
673
|
+
$labelProperty.Value = Get-AnswerMeNormalizedString -Value $labelProperty.Value -Context "$optionContext label" -MinimumCodePoints 1 -MaximumCodePoints 100
|
|
674
|
+
$descriptionProperty = Get-AnswerMeProperty -Value $option -Name 'description' -Context $optionContext
|
|
675
|
+
if ($null -ne $descriptionProperty) {
|
|
676
|
+
$descriptionProperty.Value = Get-AnswerMeNormalizedString -Value $descriptionProperty.Value -Context "$optionContext description" -MaximumCodePoints 500
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function Get-AnswerMeIdempotencyKey {
|
|
683
|
+
param(
|
|
684
|
+
[AllowNull()][string]$Value,
|
|
685
|
+
[switch]$Generate
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
if (-not $Generate) {
|
|
689
|
+
if ($Value -cnotmatch '^[0-9a-f]{32}$') {
|
|
690
|
+
throw 'IdempotencyKey must be exactly 32 lowercase hexadecimal characters.'
|
|
691
|
+
}
|
|
692
|
+
return $Value
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
$randomBytes = New-AnswerMeCryptographicRandomBytes -Length 16
|
|
696
|
+
try {
|
|
697
|
+
return ConvertTo-AnswerMeLowerHex -Bytes $randomBytes
|
|
698
|
+
}
|
|
699
|
+
finally {
|
|
700
|
+
Clear-AnswerMeSensitiveByteArray -Bytes $randomBytes
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function Get-AnswerMeRetryAfterSeconds {
|
|
705
|
+
param([Parameter(Mandatory = $true)][object]$Response)
|
|
706
|
+
|
|
707
|
+
$rawValue = [string]$Response.Headers['Retry-After']
|
|
708
|
+
$parsedValue = 0
|
|
709
|
+
if ([int]::TryParse($rawValue, [ref]$parsedValue) -and $parsedValue -ge 0) {
|
|
710
|
+
return $parsedValue
|
|
711
|
+
}
|
|
712
|
+
return 0
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function Test-AnswerMeWriterAdmissionResponse {
|
|
716
|
+
param(
|
|
717
|
+
[Parameter(Mandatory = $true)][object]$Response,
|
|
718
|
+
[AllowNull()][object]$Body
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
return (Test-AnswerMeCreateErrorResponse -StatusCode ([int]$Response.StatusCode) -Body $Body) -and
|
|
722
|
+
$Body.code -ceq 'service_unavailable' -and
|
|
723
|
+
$Body.reason -cin @('writer_admission_full', 'writer_admission_timeout')
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function ConvertFrom-AnswerMeResponseBody {
|
|
727
|
+
param([Parameter(Mandatory = $true)][object]$Response)
|
|
728
|
+
|
|
729
|
+
if ([string]::IsNullOrWhiteSpace([string]$Response.Content)) {
|
|
730
|
+
return $null
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
if ((Get-Command ConvertFrom-Json).Parameters.ContainsKey('DateKind')) {
|
|
734
|
+
return $Response.Content | ConvertFrom-Json -DateKind String
|
|
735
|
+
}
|
|
736
|
+
return $Response.Content | ConvertFrom-Json
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
return $null
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function Test-AnswerMeNoStoreResponse {
|
|
744
|
+
param([Parameter(Mandatory = $true)][object]$Response)
|
|
745
|
+
|
|
746
|
+
return ([string]$Response.Headers['Cache-Control']).Trim() -ceq 'no-store'
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function Test-AnswerMeResponseFields {
|
|
750
|
+
param(
|
|
751
|
+
[AllowNull()][object]$Body,
|
|
752
|
+
[Parameter(Mandatory = $true)][string[]]$ExpectedFields
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
if ($null -eq $Body) { return $false }
|
|
756
|
+
$actual = @($Body.PSObject.Properties.Name | Sort-Object)
|
|
757
|
+
$expected = @($ExpectedFields | Sort-Object)
|
|
758
|
+
return $actual.Count -eq $expected.Count -and
|
|
759
|
+
-not (Compare-Object -ReferenceObject $expected -DifferenceObject $actual -CaseSensitive)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function Test-AnswerMeCreateErrorResponse {
|
|
763
|
+
param(
|
|
764
|
+
[Parameter(Mandatory = $true)][int]$StatusCode,
|
|
765
|
+
[AllowNull()][object]$Body
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
if ($Body -isnot [PSCustomObject]) { return $false }
|
|
769
|
+
$allowedFields = @('code', 'requestId', 'path', 'reason')
|
|
770
|
+
$actualFields = @($Body.PSObject.Properties.Name)
|
|
771
|
+
if ($actualFields.Count -lt 2 -or
|
|
772
|
+
@($actualFields | Where-Object { $allowedFields -cnotcontains $_ }).Count -gt 0 -or
|
|
773
|
+
@($actualFields | Where-Object { $_ -ceq 'code' }).Count -ne 1 -or
|
|
774
|
+
@($actualFields | Where-Object { $_ -ceq 'requestId' }).Count -ne 1 -or
|
|
775
|
+
$Body.code -isnot [string] -or
|
|
776
|
+
$Body.requestId -isnot [string] -or
|
|
777
|
+
[string]::IsNullOrWhiteSpace([string]$Body.requestId)) {
|
|
778
|
+
return $false
|
|
779
|
+
}
|
|
780
|
+
if ($null -ne $Body.PSObject.Properties['path'] -and
|
|
781
|
+
($Body.path -isnot [string] -or [string]$Body.path -cne '/api/v1/interactions')) {
|
|
782
|
+
return $false
|
|
783
|
+
}
|
|
784
|
+
if ($null -ne $Body.PSObject.Properties['reason'] -and $Body.reason -isnot [string]) {
|
|
785
|
+
return $false
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
$reason = if ($null -ne $Body.PSObject.Properties['reason']) { [string]$Body.reason } else { $null }
|
|
789
|
+
switch ($StatusCode) {
|
|
790
|
+
400 { return $Body.code -ceq 'invalid_request' -and $null -eq $reason }
|
|
791
|
+
401 {
|
|
792
|
+
return $Body.code -ceq 'invalid_creator_credential' -and
|
|
793
|
+
$reason -cin @('invalid', 'idle_expired', 'max_lifetime_expired', 'abuse_revoked')
|
|
794
|
+
}
|
|
795
|
+
404 { return $Body.code -ceq 'interaction_not_found' -and $null -eq $reason }
|
|
796
|
+
405 { return $Body.code -ceq 'invalid_request' -and $null -eq $reason }
|
|
797
|
+
409 { return $Body.code -ceq 'idempotency_conflict' -and $null -eq $reason }
|
|
798
|
+
413 { return $Body.code -ceq 'invalid_request' -and $null -eq $reason }
|
|
799
|
+
422 { return $Body.code -ceq 'definition_invalid' -and $null -eq $reason }
|
|
800
|
+
429 {
|
|
801
|
+
return $Body.code -ceq 'rate_limited' -and
|
|
802
|
+
($null -eq $reason -or $reason -cin @('creator_rolling_create_limit', 'temporary_credential_daily_limit'))
|
|
803
|
+
}
|
|
804
|
+
503 {
|
|
805
|
+
return $Body.code -cin @('service_unavailable', 'creation_unavailable') -and
|
|
806
|
+
($null -eq $reason -or $reason -cin @('writer_admission_full', 'writer_admission_timeout'))
|
|
807
|
+
}
|
|
808
|
+
default { return $false }
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function Test-AnswerMeCreateResponse {
|
|
813
|
+
param(
|
|
814
|
+
[Parameter(Mandatory = $true)][object]$Response,
|
|
815
|
+
[AllowNull()][object]$Body
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
if (-not (Test-AnswerMeNoStoreResponse -Response $Response) -or $Body -isnot [PSCustomObject]) {
|
|
819
|
+
return $false
|
|
820
|
+
}
|
|
821
|
+
$requiredFields = @('interactionId', 'answerUrl', 'resultToken', 'status', 'expiresAt', 'requestId')
|
|
822
|
+
$allowedFields = @($requiredFields + @('idempotentReplay', 'waitDecision'))
|
|
823
|
+
$actualFields = @($Body.PSObject.Properties.Name)
|
|
824
|
+
if (@($actualFields | Where-Object { $allowedFields -cnotcontains $_ }).Count -gt 0) { return $false }
|
|
825
|
+
foreach ($requiredField in $requiredFields) {
|
|
826
|
+
if (@($actualFields | Where-Object { $_ -ceq $requiredField }).Count -ne 1) { return $false }
|
|
827
|
+
}
|
|
828
|
+
foreach ($textField in @('interactionId', 'answerUrl', 'resultToken', 'status', 'expiresAt', 'requestId')) {
|
|
829
|
+
if ($Body.$textField -isnot [string] -or [string]::IsNullOrWhiteSpace([string]$Body.$textField)) {
|
|
830
|
+
return $false
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if ([string]$Body.status -cne 'pending') { return $false }
|
|
834
|
+
if ($null -ne $Body.PSObject.Properties['idempotentReplay'] -and
|
|
835
|
+
($Body.idempotentReplay -isnot [bool] -or $Body.idempotentReplay -ne $true)) {
|
|
836
|
+
return $false
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
$expiresAt = [string]$Body.expiresAt
|
|
840
|
+
$parsedExpiresAt = [DateTimeOffset]::MinValue
|
|
841
|
+
if ($expiresAt -cnotmatch '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$' -or
|
|
842
|
+
-not [DateTimeOffset]::TryParse(
|
|
843
|
+
$expiresAt,
|
|
844
|
+
[Globalization.CultureInfo]::InvariantCulture,
|
|
845
|
+
[Globalization.DateTimeStyles]::RoundtripKind,
|
|
846
|
+
[ref]$parsedExpiresAt
|
|
847
|
+
)) {
|
|
848
|
+
return $false
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
$answerUri = $null
|
|
852
|
+
if (-not [Uri]::TryCreate([string]$Body.answerUrl, [UriKind]::Absolute, [ref]$answerUri) -or
|
|
853
|
+
-not [string]::IsNullOrEmpty($answerUri.UserInfo)) {
|
|
854
|
+
return $false
|
|
855
|
+
}
|
|
856
|
+
if ($answerUri.Scheme -ceq 'https') { return $true }
|
|
857
|
+
if (-not $AllowInsecureLoopbackHttpForTest -or $answerUri.Scheme -cne 'http') { return $false }
|
|
858
|
+
return $answerUri.DnsSafeHost -cin @('127.0.0.1', 'localhost', '::1')
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function Test-AnswerMeCapabilitiesResponse {
|
|
862
|
+
param([AllowNull()][object]$Body, [int]$SchemaVersion = 1)
|
|
863
|
+
|
|
864
|
+
if ($Body -isnot [PSCustomObject] -or
|
|
865
|
+
-not (Test-AnswerMeResponseFields `
|
|
866
|
+
-Body $Body `
|
|
867
|
+
-ExpectedFields @('protocolVersion', 'schemaVersions', 'clientProtocol', 'capabilities', 'requestId'))) {
|
|
868
|
+
return $false
|
|
869
|
+
}
|
|
870
|
+
if ($Body.protocolVersion -isnot [string] -or
|
|
871
|
+
[string]$Body.protocolVersion -cne $AnswerMeProtocolVersion -or
|
|
872
|
+
$Body.requestId -isnot [string] -or
|
|
873
|
+
[string]::IsNullOrWhiteSpace([string]$Body.requestId) -or
|
|
874
|
+
$Body.schemaVersions -isnot [Array] -or
|
|
875
|
+
@($Body.schemaVersions).Count -ne $SchemaVersion -or
|
|
876
|
+
@($Body.schemaVersions | Where-Object { ($_ -isnot [int] -and $_ -isnot [long]) -or $_ -notin @(1, 2) }).Count -gt 0 -or
|
|
877
|
+
$Body.schemaVersions -notcontains 1 -or
|
|
878
|
+
($SchemaVersion -eq 2 -and $Body.schemaVersions -notcontains 2)) {
|
|
879
|
+
return $false
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
$clientProtocol = $Body.clientProtocol
|
|
883
|
+
if ($clientProtocol -isnot [PSCustomObject] -or
|
|
884
|
+
-not (Test-AnswerMeResponseFields `
|
|
885
|
+
-Body $clientProtocol `
|
|
886
|
+
-ExpectedFields @('supportedVersions', 'recommendedVersion', 'minimumCompatibleSkillVersion')) -or
|
|
887
|
+
$clientProtocol.recommendedVersion -isnot [string] -or
|
|
888
|
+
[string]$clientProtocol.recommendedVersion -cne $AnswerMeProtocolVersion -or
|
|
889
|
+
$clientProtocol.minimumCompatibleSkillVersion -isnot [string] -or
|
|
890
|
+
[string]$clientProtocol.minimumCompatibleSkillVersion -cne '0.1.0' -or
|
|
891
|
+
$clientProtocol.supportedVersions -isnot [Array]) {
|
|
892
|
+
return $false
|
|
893
|
+
}
|
|
894
|
+
$supportedVersions = @($clientProtocol.supportedVersions)
|
|
895
|
+
if ($supportedVersions.Count -ne 2 -or
|
|
896
|
+
$supportedVersions[0] -isnot [string] -or
|
|
897
|
+
$supportedVersions[1] -isnot [string] -or
|
|
898
|
+
$supportedVersions -cnotcontains '1.0' -or
|
|
899
|
+
$supportedVersions -cnotcontains $AnswerMeProtocolVersion) {
|
|
900
|
+
return $false
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
$capabilities = $Body.capabilities
|
|
904
|
+
if ($capabilities -isnot [PSCustomObject] -or
|
|
905
|
+
-not (Test-AnswerMeResponseFields `
|
|
906
|
+
-Body $capabilities `
|
|
907
|
+
-ExpectedFields @('createInteraction', 'resultQuery', 'resultWait')) -or
|
|
908
|
+
$capabilities.createInteraction -isnot [bool] -or
|
|
909
|
+
$capabilities.createInteraction -ne $true -or
|
|
910
|
+
$capabilities.resultQuery -isnot [bool]) {
|
|
911
|
+
return $false
|
|
912
|
+
}
|
|
913
|
+
$resultWait = $capabilities.resultWait
|
|
914
|
+
return $resultWait -is [PSCustomObject] -and
|
|
915
|
+
(Test-AnswerMeResponseFields `
|
|
916
|
+
-Body $resultWait `
|
|
917
|
+
-ExpectedFields @('enabled', 'defaultWaitSeconds', 'maxWaitSeconds')) -and
|
|
918
|
+
$resultWait.enabled -is [bool] -and
|
|
919
|
+
($resultWait.defaultWaitSeconds -is [int] -or $resultWait.defaultWaitSeconds -is [long]) -and
|
|
920
|
+
[long]$resultWait.defaultWaitSeconds -eq 55 -and
|
|
921
|
+
($resultWait.maxWaitSeconds -is [int] -or $resultWait.maxWaitSeconds -is [long]) -and
|
|
922
|
+
[long]$resultWait.maxWaitSeconds -eq 600
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function Test-AnswerMeIncompatibleCapabilitiesResponse {
|
|
926
|
+
param([AllowNull()][object]$Body)
|
|
927
|
+
|
|
928
|
+
if ($Body -isnot [PSCustomObject] -or
|
|
929
|
+
-not (Test-AnswerMeResponseFields `
|
|
930
|
+
-Body $Body `
|
|
931
|
+
-ExpectedFields @('protocolVersion', 'schemaVersions', 'clientProtocol', 'capabilities', 'requestId')) -or
|
|
932
|
+
$Body.protocolVersion -isnot [string] -or
|
|
933
|
+
[string]$Body.protocolVersion -cne $AnswerMeProtocolVersion -or
|
|
934
|
+
$Body.requestId -isnot [string] -or
|
|
935
|
+
[string]::IsNullOrWhiteSpace([string]$Body.requestId) -or
|
|
936
|
+
$Body.schemaVersions -isnot [Array] -or
|
|
937
|
+
@($Body.schemaVersions).Count -ne 1 -or
|
|
938
|
+
($Body.schemaVersions[0] -isnot [int] -and $Body.schemaVersions[0] -isnot [long]) -or
|
|
939
|
+
[long]$Body.schemaVersions[0] -ne 1) {
|
|
940
|
+
return $false
|
|
941
|
+
}
|
|
942
|
+
$clientProtocol = $Body.clientProtocol
|
|
943
|
+
if ($clientProtocol -isnot [PSCustomObject] -or
|
|
944
|
+
-not (Test-AnswerMeResponseFields `
|
|
945
|
+
-Body $clientProtocol `
|
|
946
|
+
-ExpectedFields @('supportedVersions', 'recommendedVersion', 'minimumCompatibleSkillVersion')) -or
|
|
947
|
+
$clientProtocol.recommendedVersion -isnot [string] -or
|
|
948
|
+
[string]$clientProtocol.recommendedVersion -cne $AnswerMeProtocolVersion -or
|
|
949
|
+
$clientProtocol.minimumCompatibleSkillVersion -isnot [string] -or
|
|
950
|
+
[string]::IsNullOrWhiteSpace([string]$clientProtocol.minimumCompatibleSkillVersion) -or
|
|
951
|
+
$clientProtocol.supportedVersions -isnot [Array]) {
|
|
952
|
+
return $false
|
|
953
|
+
}
|
|
954
|
+
$supportedVersions = @($clientProtocol.supportedVersions)
|
|
955
|
+
if ($supportedVersions.Count -lt 1 -or
|
|
956
|
+
@($supportedVersions | Where-Object { $_ -isnot [string] -or [string]::IsNullOrWhiteSpace([string]$_) }).Count -gt 0 -or
|
|
957
|
+
$supportedVersions -ccontains $AnswerMeProtocolVersion) {
|
|
958
|
+
return $false
|
|
959
|
+
}
|
|
960
|
+
$capabilities = $Body.capabilities
|
|
961
|
+
if ($capabilities -isnot [PSCustomObject] -or
|
|
962
|
+
-not (Test-AnswerMeResponseFields `
|
|
963
|
+
-Body $capabilities `
|
|
964
|
+
-ExpectedFields @('createInteraction', 'resultQuery', 'resultWait')) -or
|
|
965
|
+
$capabilities.createInteraction -isnot [bool] -or
|
|
966
|
+
$capabilities.resultQuery -isnot [bool]) {
|
|
967
|
+
return $false
|
|
968
|
+
}
|
|
969
|
+
$resultWait = $capabilities.resultWait
|
|
970
|
+
return $resultWait -is [PSCustomObject] -and
|
|
971
|
+
(Test-AnswerMeResponseFields `
|
|
972
|
+
-Body $resultWait `
|
|
973
|
+
-ExpectedFields @('enabled', 'defaultWaitSeconds', 'maxWaitSeconds')) -and
|
|
974
|
+
$resultWait.enabled -is [bool] -and
|
|
975
|
+
($resultWait.defaultWaitSeconds -is [int] -or $resultWait.defaultWaitSeconds -is [long]) -and
|
|
976
|
+
[long]$resultWait.defaultWaitSeconds -eq 55 -and
|
|
977
|
+
($resultWait.maxWaitSeconds -is [int] -or $resultWait.maxWaitSeconds -is [long]) -and
|
|
978
|
+
[long]$resultWait.maxWaitSeconds -eq 600
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function Test-AnswerMeProtocolUnsupportedErrorResponse {
|
|
982
|
+
param([AllowNull()][object]$Body)
|
|
983
|
+
|
|
984
|
+
if ($Body -isnot [PSCustomObject] -or
|
|
985
|
+
-not (Test-AnswerMeResponseFields `
|
|
986
|
+
-Body $Body `
|
|
987
|
+
-ExpectedFields @(
|
|
988
|
+
'code',
|
|
989
|
+
'requestId',
|
|
990
|
+
'reason',
|
|
991
|
+
'receivedProtocolVersion',
|
|
992
|
+
'supportedProtocolVersions',
|
|
993
|
+
'minimumCompatibleSkillVersion'
|
|
994
|
+
)) -or
|
|
995
|
+
$Body.code -isnot [string] -or
|
|
996
|
+
[string]$Body.code -cne 'client_protocol_unsupported' -or
|
|
997
|
+
$Body.reason -isnot [string] -or
|
|
998
|
+
[string]$Body.reason -cne 'answerme_client_protocol_version_is_not_supported' -or
|
|
999
|
+
$Body.requestId -isnot [string] -or
|
|
1000
|
+
[string]::IsNullOrWhiteSpace([string]$Body.requestId) -or
|
|
1001
|
+
$Body.receivedProtocolVersion -isnot [string] -or
|
|
1002
|
+
[string]::IsNullOrWhiteSpace([string]$Body.receivedProtocolVersion) -or
|
|
1003
|
+
$Body.minimumCompatibleSkillVersion -isnot [string] -or
|
|
1004
|
+
[string]::IsNullOrWhiteSpace([string]$Body.minimumCompatibleSkillVersion) -or
|
|
1005
|
+
$Body.supportedProtocolVersions -isnot [Array]) {
|
|
1006
|
+
return $false
|
|
1007
|
+
}
|
|
1008
|
+
$supportedVersions = @($Body.supportedProtocolVersions)
|
|
1009
|
+
return $supportedVersions.Count -gt 0 -and
|
|
1010
|
+
@($supportedVersions | Where-Object { $_ -isnot [string] -or [string]::IsNullOrWhiteSpace([string]$_) }).Count -eq 0
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function Test-AnswerMeEnrollmentProgressEvent {
|
|
1014
|
+
param(
|
|
1015
|
+
[Parameter(Mandatory = $true)][string]$Line,
|
|
1016
|
+
[Parameter(Mandatory = $true)][Uri]$ResolvedApiBaseUrl
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
try {
|
|
1020
|
+
$eventBody = $Line | ConvertFrom-Json
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
return $false
|
|
1024
|
+
}
|
|
1025
|
+
if (-not (Test-AnswerMeResponseFields `
|
|
1026
|
+
-Body $eventBody `
|
|
1027
|
+
-ExpectedFields @('event', 'stage', 'verificationUrl')) -or
|
|
1028
|
+
$eventBody.event -isnot [string] -or
|
|
1029
|
+
[string]$eventBody.event -cne 'answerme.creator-enrollment.verification-url' -or
|
|
1030
|
+
$eventBody.stage -isnot [string] -or
|
|
1031
|
+
[string]$eventBody.stage -cne 'verification-handoff' -or
|
|
1032
|
+
$eventBody.verificationUrl -isnot [string]) {
|
|
1033
|
+
return $false
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
$eventUri = $null
|
|
1037
|
+
return [Uri]::TryCreate([string]$eventBody.verificationUrl, [UriKind]::Absolute, [ref]$eventUri) -and
|
|
1038
|
+
$eventUri.Scheme.Equals($ResolvedApiBaseUrl.Scheme, [StringComparison]::OrdinalIgnoreCase) -and
|
|
1039
|
+
$eventUri.DnsSafeHost.Equals($ResolvedApiBaseUrl.DnsSafeHost, [StringComparison]::OrdinalIgnoreCase) -and
|
|
1040
|
+
$eventUri.Port -eq $ResolvedApiBaseUrl.Port -and
|
|
1041
|
+
[string]::IsNullOrEmpty($eventUri.UserInfo) -and
|
|
1042
|
+
[string]::IsNullOrEmpty($eventUri.Fragment)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function Invoke-AnswerMeEnrollmentChildProcess {
|
|
1046
|
+
param(
|
|
1047
|
+
[Parameter(Mandatory = $true)][string]$PowerShellPath,
|
|
1048
|
+
[Parameter(Mandatory = $true)][string[]]$Arguments,
|
|
1049
|
+
[Parameter(Mandatory = $true)][Uri]$ResolvedApiBaseUrl
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
$startInfo = [Diagnostics.ProcessStartInfo]::new()
|
|
1053
|
+
$startInfo.FileName = $PowerShellPath
|
|
1054
|
+
$startInfo.UseShellExecute = $false
|
|
1055
|
+
$startInfo.CreateNoWindow = $true
|
|
1056
|
+
$startInfo.RedirectStandardOutput = $true
|
|
1057
|
+
$startInfo.RedirectStandardError = $true
|
|
1058
|
+
$startInfo.RedirectStandardInput = $true
|
|
1059
|
+
$acknowledgementToken = [Guid]::NewGuid().ToString('N')
|
|
1060
|
+
Set-AnswerMeProcessEnvironmentVariable `
|
|
1061
|
+
-StartInfo $startInfo `
|
|
1062
|
+
-Name 'ANSWERME_INTERNAL_VERIFICATION_EVENT_ACK' `
|
|
1063
|
+
-Value $acknowledgementToken
|
|
1064
|
+
Set-AnswerMeProcessArguments -StartInfo $startInfo -Arguments $Arguments
|
|
1065
|
+
|
|
1066
|
+
$process = [Diagnostics.Process]::new()
|
|
1067
|
+
$process.StartInfo = $startInfo
|
|
1068
|
+
try {
|
|
1069
|
+
if (-not $process.Start()) { throw 'Enrollment client process could not be started.' }
|
|
1070
|
+
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
|
|
1071
|
+
$invalidStderr = $false
|
|
1072
|
+
$progressEventCount = 0
|
|
1073
|
+
while ($null -ne ($line = $process.StandardError.ReadLine())) {
|
|
1074
|
+
if (Test-AnswerMeEnrollmentProgressEvent -Line $line -ResolvedApiBaseUrl $ResolvedApiBaseUrl) {
|
|
1075
|
+
$progressEventCount += 1
|
|
1076
|
+
[Console]::Error.WriteLine($line)
|
|
1077
|
+
[Console]::Error.Flush()
|
|
1078
|
+
$selfTestAcknowledgementToken = [Environment]::GetEnvironmentVariable(
|
|
1079
|
+
'ANSWERME_INTERNAL_SELFTEST_CREATE_EVENT_ACK',
|
|
1080
|
+
'Process'
|
|
1081
|
+
)
|
|
1082
|
+
if (-not [string]::IsNullOrWhiteSpace($selfTestAcknowledgementToken)) {
|
|
1083
|
+
$selfTestAcknowledgement = [Console]::In.ReadLine()
|
|
1084
|
+
if ($selfTestAcknowledgement -cne $selfTestAcknowledgementToken) {
|
|
1085
|
+
$invalidStderr = $true
|
|
1086
|
+
$process.StandardInput.WriteLine('invalid-event')
|
|
1087
|
+
$process.StandardInput.Flush()
|
|
1088
|
+
continue
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
$process.StandardInput.WriteLine($acknowledgementToken)
|
|
1092
|
+
$process.StandardInput.Flush()
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
$invalidStderr = $true
|
|
1096
|
+
$process.StandardInput.WriteLine('invalid-event')
|
|
1097
|
+
$process.StandardInput.Flush()
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
$process.WaitForExit()
|
|
1101
|
+
return [PSCustomObject]@{
|
|
1102
|
+
stdout = $stdoutTask.GetAwaiter().GetResult()
|
|
1103
|
+
exitCode = $process.ExitCode
|
|
1104
|
+
invalidStderr = $invalidStderr
|
|
1105
|
+
progressEventCount = $progressEventCount
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
finally {
|
|
1109
|
+
$process.Dispose()
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function Test-AnswerMeRecoverableCredentialReason {
|
|
1114
|
+
param([AllowNull()][string]$Reason)
|
|
1115
|
+
|
|
1116
|
+
return $Reason -cin @('invalid', 'idle_expired', 'max_lifetime_expired', 'abuse_revoked')
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function Invoke-AnswerMeAutomaticEnrollment {
|
|
1120
|
+
param(
|
|
1121
|
+
[Parameter(Mandatory = $true)][Uri]$ResolvedApiBaseUrl,
|
|
1122
|
+
[Parameter(Mandatory = $true)][string]$ResolvedCredentialPath
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
if ($AnswerMeAutomaticEnrollmentAttempted) {
|
|
1126
|
+
Write-AnswerMeFailure `
|
|
1127
|
+
-Code 'automatic-enrollment-exhausted' `
|
|
1128
|
+
-Message 'This operation has already attempted its one automatic Creator enrollment.' `
|
|
1129
|
+
-ExitCode 4 `
|
|
1130
|
+
-NetworkCalled $true
|
|
1131
|
+
}
|
|
1132
|
+
$script:AnswerMeAutomaticEnrollmentAttempted = $true
|
|
1133
|
+
|
|
1134
|
+
$enrollmentScript = Join-Path $PSScriptRoot 'enroll-answerme-creator.ps1'
|
|
1135
|
+
if (-not (Test-Path -LiteralPath $enrollmentScript -PathType Leaf)) {
|
|
1136
|
+
Write-AnswerMeFailure `
|
|
1137
|
+
-Code 'enrollment-client-missing' `
|
|
1138
|
+
-Message 'The AnswerMe Creator enrollment client is required for automatic recovery.' `
|
|
1139
|
+
-ExitCode 3
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
$arguments = @(
|
|
1143
|
+
'-NoLogo',
|
|
1144
|
+
'-NoProfile',
|
|
1145
|
+
'-File', $enrollmentScript,
|
|
1146
|
+
'-ApiBaseUrl', $ResolvedApiBaseUrl.AbsoluteUri.TrimEnd('/'),
|
|
1147
|
+
'-CredentialPath', $ResolvedCredentialPath,
|
|
1148
|
+
'-WaitTimeoutSeconds', [string]$localEnrollmentWaitDecision.effectiveTimeoutSeconds
|
|
1149
|
+
)
|
|
1150
|
+
if ($AllowInsecureLoopbackHttpForTest) { $arguments += '-AllowInsecureLoopbackHttpForTest' }
|
|
1151
|
+
if ($SkipPopup) { $arguments += '-SkipPopup' }
|
|
1152
|
+
|
|
1153
|
+
$pwshPath = (Get-Process -Id $PID).Path
|
|
1154
|
+
try {
|
|
1155
|
+
$childResult = Invoke-AnswerMeEnrollmentChildProcess `
|
|
1156
|
+
-PowerShellPath $pwshPath `
|
|
1157
|
+
-Arguments $arguments `
|
|
1158
|
+
-ResolvedApiBaseUrl $ResolvedApiBaseUrl
|
|
1159
|
+
}
|
|
1160
|
+
catch {
|
|
1161
|
+
Write-AnswerMeFailure `
|
|
1162
|
+
-Code 'automatic-enrollment-process-failed' `
|
|
1163
|
+
-Message 'The automatic Creator enrollment process could not be executed.' `
|
|
1164
|
+
-ExitCode 4 `
|
|
1165
|
+
-NetworkCalled $true
|
|
1166
|
+
}
|
|
1167
|
+
if ($childResult.invalidStderr -or $childResult.progressEventCount -gt 1) {
|
|
1168
|
+
Write-AnswerMeFailure `
|
|
1169
|
+
-Code 'automatic-enrollment-event-invalid' `
|
|
1170
|
+
-Message 'Automatic Creator enrollment emitted an invalid progress event.' `
|
|
1171
|
+
-ExitCode 4 `
|
|
1172
|
+
-NetworkCalled $true
|
|
1173
|
+
}
|
|
1174
|
+
$enrollmentExitCode = [int]$childResult.exitCode
|
|
1175
|
+
$enrollmentText = ([string]$childResult.stdout).Trim()
|
|
1176
|
+
$enrollment = $null
|
|
1177
|
+
if (-not [string]::IsNullOrWhiteSpace($enrollmentText)) {
|
|
1178
|
+
try {
|
|
1179
|
+
$enrollment = $enrollmentText | ConvertFrom-Json
|
|
1180
|
+
}
|
|
1181
|
+
catch {
|
|
1182
|
+
$enrollment = $null
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if ($enrollmentExitCode -ne 0 -or $null -eq $enrollment -or $enrollment.ok -ne $true) {
|
|
1186
|
+
Write-AnswerMeFailure `
|
|
1187
|
+
-Code $(if ($null -ne $enrollment -and $enrollment.code) { [string]$enrollment.code } else { 'automatic-enrollment-failed' }) `
|
|
1188
|
+
-Message 'Automatic Creator enrollment did not configure a protected credential.' `
|
|
1189
|
+
-ExitCode $(if ($enrollmentExitCode -gt 0) { [int]$enrollmentExitCode } else { 4 }) `
|
|
1190
|
+
-NetworkCalled $(if ($null -ne $enrollment) { $enrollment.networkCalled -eq $true } else { $true }) `
|
|
1191
|
+
-Reason $(if ($null -ne $enrollment) { [string]$enrollment.reason } else { $null }) `
|
|
1192
|
+
-RetryAfterSeconds $(if ($null -ne $enrollment) { [int]$enrollment.retryAfterSeconds } else { 0 })
|
|
1193
|
+
}
|
|
1194
|
+
$foregroundVerified = $null -ne $enrollment.verificationDelivery -and
|
|
1195
|
+
$enrollment.verificationDelivery.foregroundVerified -eq $true
|
|
1196
|
+
$expectedEventCount = if ($foregroundVerified) { 0 } else { 1 }
|
|
1197
|
+
if ($childResult.progressEventCount -ne $expectedEventCount) {
|
|
1198
|
+
Write-AnswerMeFailure `
|
|
1199
|
+
-Code 'automatic-enrollment-event-missing' `
|
|
1200
|
+
-Message 'Automatic Creator enrollment did not deliver its required verification URL event.' `
|
|
1201
|
+
-ExitCode 4 `
|
|
1202
|
+
-NetworkCalled $true
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
try {
|
|
1206
|
+
$recoveredCredential = Import-Clixml -LiteralPath $ResolvedCredentialPath
|
|
1207
|
+
if ($recoveredCredential -isnot [PSCredential]) {
|
|
1208
|
+
throw 'Protected Creator credential did not contain a PSCredential.'
|
|
1209
|
+
}
|
|
1210
|
+
$recoveredBaseUrl = Resolve-AnswerMeApiBaseUrl `
|
|
1211
|
+
-ApiBaseUrl $recoveredCredential.UserName `
|
|
1212
|
+
-AllowInsecureLoopbackHttpForTest:$AllowInsecureLoopbackHttpForTest
|
|
1213
|
+
if ($recoveredBaseUrl.AbsoluteUri.TrimEnd('/') -cne $ResolvedApiBaseUrl.AbsoluteUri.TrimEnd('/')) {
|
|
1214
|
+
throw 'Protected Creator credential does not match the requested API origin.'
|
|
1215
|
+
}
|
|
1216
|
+
$recoveredKey = $recoveredCredential.GetNetworkCredential().Password
|
|
1217
|
+
if ([string]::IsNullOrWhiteSpace($recoveredKey)) {
|
|
1218
|
+
throw 'Protected Creator credential did not contain a Creator Key.'
|
|
1219
|
+
}
|
|
1220
|
+
return [PSCustomObject]@{
|
|
1221
|
+
creatorKey = $recoveredKey
|
|
1222
|
+
credentialSource = 'encrypted-file-recovered'
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
catch {
|
|
1226
|
+
Write-AnswerMeFailure `
|
|
1227
|
+
-Code 'credential-recovery-readback-failed' `
|
|
1228
|
+
-Message 'The enrolled Creator credential could not be read back from protected storage.' `
|
|
1229
|
+
-ExitCode 5 `
|
|
1230
|
+
-NetworkCalled $true
|
|
1231
|
+
}
|
|
1232
|
+
finally {
|
|
1233
|
+
$recoveredCredential = $null
|
|
1234
|
+
$recoveredKey = $null
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function Get-AnswerMeCredentialVerification {
|
|
1239
|
+
param(
|
|
1240
|
+
[Parameter(Mandatory = $true)][Uri]$ResolvedApiBaseUrl,
|
|
1241
|
+
[Parameter(Mandatory = $true)][string]$CreatorKey
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
$verificationUri = ([Uri]::new($ResolvedApiBaseUrl, '/api/v1/creator-credentials/verify')).AbsoluteUri
|
|
1245
|
+
$verificationHeaders = @{
|
|
1246
|
+
Accept = 'application/json'
|
|
1247
|
+
Authorization = "Bearer $CreatorKey"
|
|
1248
|
+
'X-AnswerMe-Protocol-Version' = $AnswerMeProtocolVersion
|
|
1249
|
+
'X-AnswerMe-Skill-Version' = $AnswerMeSkillVersion
|
|
1250
|
+
}
|
|
1251
|
+
try {
|
|
1252
|
+
$verificationResponse = Invoke-AnswerMeWebRequest `
|
|
1253
|
+
-Uri $verificationUri `
|
|
1254
|
+
-Method Get `
|
|
1255
|
+
-Headers $verificationHeaders `
|
|
1256
|
+
-TimeoutSec 15
|
|
1257
|
+
}
|
|
1258
|
+
catch {
|
|
1259
|
+
Write-AnswerMeFailure `
|
|
1260
|
+
-Code 'credential-verification-unavailable' `
|
|
1261
|
+
-Message 'The Creator credential could not be verified safely.' `
|
|
1262
|
+
-ExitCode 4 `
|
|
1263
|
+
-NetworkCalled $true
|
|
1264
|
+
}
|
|
1265
|
+
$verificationBody = ConvertFrom-AnswerMeResponseBody -Response $verificationResponse
|
|
1266
|
+
if ([int]$verificationResponse.StatusCode -ne 200) {
|
|
1267
|
+
if ([int]$verificationResponse.StatusCode -eq 503 -and
|
|
1268
|
+
(Test-AnswerMeNoStoreResponse -Response $verificationResponse) -and
|
|
1269
|
+
(Test-AnswerMeResponseFields -Body $verificationBody -ExpectedFields @('code', 'requestId')) -and
|
|
1270
|
+
$verificationBody.code -is [string] -and
|
|
1271
|
+
[string]$verificationBody.code -ceq 'service_unavailable' -and
|
|
1272
|
+
$verificationBody.requestId -is [string] -and
|
|
1273
|
+
-not [string]::IsNullOrWhiteSpace([string]$verificationBody.requestId)) {
|
|
1274
|
+
Write-AnswerMeFailure `
|
|
1275
|
+
-Code 'service_unavailable' `
|
|
1276
|
+
-Message 'The Creator credential verification service is unavailable.' `
|
|
1277
|
+
-ExitCode 4 `
|
|
1278
|
+
-NetworkCalled $true
|
|
1279
|
+
}
|
|
1280
|
+
Write-AnswerMeFailure `
|
|
1281
|
+
-Code 'credential-verification-response-invalid' `
|
|
1282
|
+
-Message 'The Creator credential verification service returned an invalid failure response.' `
|
|
1283
|
+
-ExitCode 4 `
|
|
1284
|
+
-NetworkCalled $true
|
|
1285
|
+
}
|
|
1286
|
+
if (-not (Test-AnswerMeNoStoreResponse -Response $verificationResponse) -or $null -eq $verificationBody) {
|
|
1287
|
+
Write-AnswerMeFailure `
|
|
1288
|
+
-Code 'credential-verification-response-invalid' `
|
|
1289
|
+
-Message 'Creator credential verification omitted its no-store contract or response body.' `
|
|
1290
|
+
-ExitCode 4 `
|
|
1291
|
+
-NetworkCalled $true
|
|
1292
|
+
}
|
|
1293
|
+
if ($verificationBody.valid -is [bool] -and
|
|
1294
|
+
$verificationBody.valid -eq $true -and
|
|
1295
|
+
(Test-AnswerMeResponseFields -Body $verificationBody -ExpectedFields @('valid', 'creatorId', 'reason', 'requestId')) -and
|
|
1296
|
+
$verificationBody.creatorId -is [string] -and
|
|
1297
|
+
$verificationBody.reason -is [string] -and
|
|
1298
|
+
$verificationBody.requestId -is [string] -and
|
|
1299
|
+
$verificationBody.reason -ceq 'active' -and
|
|
1300
|
+
-not [string]::IsNullOrWhiteSpace($verificationBody.creatorId) -and
|
|
1301
|
+
-not [string]::IsNullOrWhiteSpace($verificationBody.requestId)) {
|
|
1302
|
+
return [PSCustomObject]@{ valid = $true; reason = 'active' }
|
|
1303
|
+
}
|
|
1304
|
+
if ($verificationBody.valid -is [bool] -and
|
|
1305
|
+
$verificationBody.valid -eq $false -and
|
|
1306
|
+
(Test-AnswerMeResponseFields -Body $verificationBody -ExpectedFields @('valid', 'reason', 'requestId')) -and
|
|
1307
|
+
$verificationBody.reason -is [string] -and
|
|
1308
|
+
$verificationBody.requestId -is [string] -and
|
|
1309
|
+
(Test-AnswerMeRecoverableCredentialReason -Reason $verificationBody.reason) -and
|
|
1310
|
+
-not [string]::IsNullOrWhiteSpace($verificationBody.requestId)) {
|
|
1311
|
+
return [PSCustomObject]@{ valid = $false; reason = $verificationBody.reason }
|
|
1312
|
+
}
|
|
1313
|
+
Write-AnswerMeFailure `
|
|
1314
|
+
-Code 'credential-verification-response-invalid' `
|
|
1315
|
+
-Message 'Creator credential verification returned an unknown state.' `
|
|
1316
|
+
-ExitCode 4 `
|
|
1317
|
+
-NetworkCalled $true
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
$apiBaseUrlPolicyPath = Join-Path $PSScriptRoot 'answerme-api-base-url.ps1'
|
|
1321
|
+
if (-not (Test-Path -LiteralPath $apiBaseUrlPolicyPath -PathType Leaf)) {
|
|
1322
|
+
Write-AnswerMeFailure -Code 'api-base-url-policy-missing' -Message 'The AnswerMe API base URL policy is required.' -ExitCode 3
|
|
1323
|
+
}
|
|
1324
|
+
try {
|
|
1325
|
+
. $apiBaseUrlPolicyPath
|
|
1326
|
+
}
|
|
1327
|
+
catch {
|
|
1328
|
+
Write-AnswerMeFailure -Code 'api-base-url-policy-unavailable' -Message 'The AnswerMe API base URL policy could not be loaded.' -ExitCode 3
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
$inputStopwatch = [Diagnostics.Stopwatch]::StartNew()
|
|
1332
|
+
try {
|
|
1333
|
+
$hasTemplateJson = -not [string]::IsNullOrWhiteSpace($TemplateJson)
|
|
1334
|
+
$hasCompactJson = -not [string]::IsNullOrWhiteSpace($CompactQuestionnaireJson)
|
|
1335
|
+
if ($hasTemplateJson -eq $hasCompactJson) {
|
|
1336
|
+
throw 'Specify exactly one of TemplateJson or CompactQuestionnaireJson.'
|
|
1337
|
+
}
|
|
1338
|
+
if ($hasCompactJson) {
|
|
1339
|
+
Assert-AnswerMeJsonUnicodeWellFormed -Json $CompactQuestionnaireJson
|
|
1340
|
+
$compactQuestionnaire = $CompactQuestionnaireJson | ConvertFrom-Json
|
|
1341
|
+
$template = ConvertFrom-AnswerMeCompactQuestionnaire -Compact $compactQuestionnaire
|
|
1342
|
+
$inputMode = 'compact-questionnaire-json-argument'
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
Assert-AnswerMeJsonUnicodeWellFormed -Json $TemplateJson
|
|
1346
|
+
$template = $TemplateJson | ConvertFrom-Json
|
|
1347
|
+
$inputMode = 'json-argument'
|
|
1348
|
+
}
|
|
1349
|
+
Assert-AnswerMeTemplate -Template $template
|
|
1350
|
+
$localWaitDecision = Get-AnswerMeWaitDecision -RequestedValue $WaitTimeoutSeconds
|
|
1351
|
+
$localEnrollmentWaitDecision = Get-AnswerMeWaitDecision -RequestedValue $EnrollmentWaitTimeoutSeconds
|
|
1352
|
+
$template | Add-Member -NotePropertyName 'waitPreference' -NotePropertyValue ([PSCustomObject]@{
|
|
1353
|
+
timeoutSeconds = $localWaitDecision.requestedTimeoutSeconds
|
|
1354
|
+
}) -Force
|
|
1355
|
+
$requestJson = $template | ConvertTo-Json -Depth 32 -Compress
|
|
1356
|
+
}
|
|
1357
|
+
catch {
|
|
1358
|
+
$AnswerMeTimings.inputValidationMilliseconds = [long]$inputStopwatch.ElapsedMilliseconds
|
|
1359
|
+
Write-AnswerMeFailure `
|
|
1360
|
+
-Code 'invalid-template' `
|
|
1361
|
+
-Message $_.Exception.Message `
|
|
1362
|
+
-ExitCode 2 `
|
|
1363
|
+
-RecoveryAction 'correct-input-and-retry-once'
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
if ($localEnrollmentWaitDecision.effectiveTimeoutSeconds -lt 30) {
|
|
1367
|
+
$AnswerMeTimings.inputValidationMilliseconds = [long]$inputStopwatch.ElapsedMilliseconds
|
|
1368
|
+
Write-AnswerMeFailure `
|
|
1369
|
+
-Code 'enrollment-wait-timeout-invalid' `
|
|
1370
|
+
-Message 'EnrollmentWaitTimeoutSeconds must be an integer from 30 through 600.' `
|
|
1371
|
+
-ExitCode 2
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
$validatedApiBaseUrl = $null
|
|
1375
|
+
if (-not [string]::IsNullOrWhiteSpace($ApiBaseUrl)) {
|
|
1376
|
+
try {
|
|
1377
|
+
$validatedApiBaseUrl = Resolve-AnswerMeApiBaseUrl `
|
|
1378
|
+
-ApiBaseUrl $ApiBaseUrl `
|
|
1379
|
+
-AllowInsecureLoopbackHttpForTest:$AllowInsecureLoopbackHttpForTest
|
|
1380
|
+
}
|
|
1381
|
+
catch {
|
|
1382
|
+
$AnswerMeTimings.inputValidationMilliseconds = [long]$inputStopwatch.ElapsedMilliseconds
|
|
1383
|
+
Write-AnswerMeFailure -Code 'api-base-url-invalid' -Message $_.Exception.Message -ExitCode 2
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
$resolvedIdempotencyKey = $null
|
|
1387
|
+
if ($PSBoundParameters.ContainsKey('IdempotencyKey')) {
|
|
1388
|
+
try {
|
|
1389
|
+
$resolvedIdempotencyKey = Get-AnswerMeIdempotencyKey -Value $IdempotencyKey
|
|
1390
|
+
}
|
|
1391
|
+
catch {
|
|
1392
|
+
$AnswerMeTimings.inputValidationMilliseconds = [long]$inputStopwatch.ElapsedMilliseconds
|
|
1393
|
+
Write-AnswerMeFailure -Code 'idempotency-key-invalid' -Message $_.Exception.Message -ExitCode 2
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
$AnswerMeTimings.inputValidationMilliseconds = [long]$inputStopwatch.ElapsedMilliseconds
|
|
1397
|
+
|
|
1398
|
+
if ($ValidateOnly) {
|
|
1399
|
+
[PSCustomObject]@{
|
|
1400
|
+
ok = $true
|
|
1401
|
+
status = 'validated'
|
|
1402
|
+
inputMode = $inputMode
|
|
1403
|
+
questionCount = @($template.questions).Count
|
|
1404
|
+
waitDecision = $localWaitDecision
|
|
1405
|
+
enrollmentWaitDecision = $localEnrollmentWaitDecision
|
|
1406
|
+
protocolVersion = $AnswerMeProtocolVersion
|
|
1407
|
+
skillVersion = $AnswerMeSkillVersion
|
|
1408
|
+
timings = Get-AnswerMeTimings
|
|
1409
|
+
networkCalled = $false
|
|
1410
|
+
automaticEnrollmentAttempted = $false
|
|
1411
|
+
originalRequestRetried = $false
|
|
1412
|
+
} | ConvertTo-Json -Depth 6
|
|
1413
|
+
exit 0
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
$configurationStopwatch = [Diagnostics.Stopwatch]::StartNew()
|
|
1417
|
+
$creatorKey = [Environment]::GetEnvironmentVariable($CreatorKeyEnvironmentVariable, 'Process')
|
|
1418
|
+
$credentialSource = 'environment'
|
|
1419
|
+
if ([string]::IsNullOrWhiteSpace($creatorKey)) {
|
|
1420
|
+
$credentialSource = 'missing'
|
|
1421
|
+
}
|
|
1422
|
+
if ([string]::IsNullOrWhiteSpace($creatorKey) -or [string]::IsNullOrWhiteSpace($ApiBaseUrl)) {
|
|
1423
|
+
if ([string]::IsNullOrWhiteSpace($CredentialPath)) {
|
|
1424
|
+
$CredentialPath = Get-DefaultAnswerMeCredentialPath
|
|
1425
|
+
}
|
|
1426
|
+
if (-not [string]::IsNullOrWhiteSpace($CredentialPath) -and (Test-Path -LiteralPath $CredentialPath -PathType Leaf)) {
|
|
1427
|
+
try {
|
|
1428
|
+
$encryptedCredential = Import-Clixml -LiteralPath $CredentialPath
|
|
1429
|
+
if ($encryptedCredential -isnot [PSCredential]) {
|
|
1430
|
+
throw 'Encrypted credential file does not contain a PSCredential.'
|
|
1431
|
+
}
|
|
1432
|
+
if ([string]::IsNullOrWhiteSpace($ApiBaseUrl)) {
|
|
1433
|
+
$ApiBaseUrl = $encryptedCredential.UserName
|
|
1434
|
+
}
|
|
1435
|
+
if ([string]::IsNullOrWhiteSpace($creatorKey)) {
|
|
1436
|
+
$creatorKey = $encryptedCredential.GetNetworkCredential().Password
|
|
1437
|
+
$credentialSource = 'encrypted-file'
|
|
1438
|
+
}
|
|
1439
|
+
$encryptedCredential = $null
|
|
1440
|
+
}
|
|
1441
|
+
catch {
|
|
1442
|
+
Write-AnswerMeFailure -Code 'credential-file-invalid' -Message 'The encrypted Creator credential cannot be read by this Windows account.' -ExitCode 3
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if ([string]::IsNullOrWhiteSpace($CredentialPath)) {
|
|
1447
|
+
$CredentialPath = Get-DefaultAnswerMeCredentialPath
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if ([string]::IsNullOrWhiteSpace($ApiBaseUrl)) {
|
|
1451
|
+
Write-AnswerMeFailure -Code 'api-base-url-missing' -Message 'A trusted AnswerMe API base URL is required.' -ExitCode 2
|
|
1452
|
+
}
|
|
1453
|
+
$parsedBaseUrl = $validatedApiBaseUrl
|
|
1454
|
+
try {
|
|
1455
|
+
$parsedBaseUrl = Resolve-AnswerMeApiBaseUrl `
|
|
1456
|
+
-ApiBaseUrl $ApiBaseUrl `
|
|
1457
|
+
-AllowInsecureLoopbackHttpForTest:$AllowInsecureLoopbackHttpForTest
|
|
1458
|
+
}
|
|
1459
|
+
catch {
|
|
1460
|
+
Write-AnswerMeFailure -Code 'api-base-url-invalid' -Message $_.Exception.Message -ExitCode 2
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
if ($ValidateCredentialOnly) {
|
|
1464
|
+
if ([string]::IsNullOrWhiteSpace($creatorKey)) {
|
|
1465
|
+
Write-AnswerMeFailure -Code 'creator-key-missing' -Message 'A Creator Key is missing from the configured protected credential.' -ExitCode 3
|
|
1466
|
+
}
|
|
1467
|
+
$AnswerMeTimings.configurationMilliseconds = [long]$configurationStopwatch.ElapsedMilliseconds
|
|
1468
|
+
[PSCustomObject]@{
|
|
1469
|
+
ok = $true
|
|
1470
|
+
status = 'credential-validated'
|
|
1471
|
+
apiBaseUrl = $parsedBaseUrl.AbsoluteUri.TrimEnd('/')
|
|
1472
|
+
credentialSource = $credentialSource
|
|
1473
|
+
timings = Get-AnswerMeTimings
|
|
1474
|
+
networkCalled = $false
|
|
1475
|
+
automaticEnrollmentAttempted = $false
|
|
1476
|
+
originalRequestRetried = $false
|
|
1477
|
+
} | ConvertTo-Json -Depth 6
|
|
1478
|
+
$creatorKey = $null
|
|
1479
|
+
exit 0
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
$resultTokenStoreAdapter = Join-Path $PSScriptRoot 'result-token-store.windows.ps1'
|
|
1483
|
+
if (-not (Test-Path -LiteralPath $resultTokenStoreAdapter -PathType Leaf)) {
|
|
1484
|
+
Write-AnswerMeFailure -Code 'result-token-store-adapter-missing' -Message 'A protected Result Token Store adapter is required.' -ExitCode 3
|
|
1485
|
+
}
|
|
1486
|
+
try {
|
|
1487
|
+
. $resultTokenStoreAdapter
|
|
1488
|
+
$resultTokenStore = Assert-AnswerMeResultTokenStore `
|
|
1489
|
+
-StoreRoot $ResultTokenStoreRoot `
|
|
1490
|
+
-AllowNonDefaultStoreRoot:$AllowNonDefaultResultTokenStore
|
|
1491
|
+
}
|
|
1492
|
+
catch {
|
|
1493
|
+
Write-AnswerMeFailure -Code 'result-token-store-unavailable' -Message $_.Exception.Message -ExitCode 3
|
|
1494
|
+
}
|
|
1495
|
+
$AnswerMeTimings.configurationMilliseconds = [long]$configurationStopwatch.ElapsedMilliseconds
|
|
1496
|
+
|
|
1497
|
+
$capabilitiesUri = ([Uri]::new($parsedBaseUrl, '/api/v1/capabilities')).AbsoluteUri
|
|
1498
|
+
if ($template.schemaVersion -eq 2) { $capabilitiesUri += '?schemaVersion=2' }
|
|
1499
|
+
$capabilitiesHeaders = @{
|
|
1500
|
+
Accept = 'application/json'
|
|
1501
|
+
'X-AnswerMe-Protocol-Version' = $AnswerMeProtocolVersion
|
|
1502
|
+
'X-AnswerMe-Skill-Version' = $AnswerMeSkillVersion
|
|
1503
|
+
}
|
|
1504
|
+
$capabilitiesStopwatch = [Diagnostics.Stopwatch]::StartNew()
|
|
1505
|
+
try {
|
|
1506
|
+
$capabilitiesResponse = Invoke-AnswerMeWebRequest -Uri $capabilitiesUri -Method Get -Headers $capabilitiesHeaders -TimeoutSec 15
|
|
1507
|
+
}
|
|
1508
|
+
catch {
|
|
1509
|
+
$AnswerMeTimings.capabilitiesMilliseconds = [long]$capabilitiesStopwatch.ElapsedMilliseconds
|
|
1510
|
+
Write-AnswerMeFailure -Code 'capabilities-unavailable' -Message $_.Exception.Message -ExitCode 4 -NetworkCalled $true
|
|
1511
|
+
}
|
|
1512
|
+
$AnswerMeTimings.capabilitiesMilliseconds = [long]$capabilitiesStopwatch.ElapsedMilliseconds
|
|
1513
|
+
$capabilitiesBody = $null
|
|
1514
|
+
try {
|
|
1515
|
+
if (-not [string]::IsNullOrWhiteSpace([string]$capabilitiesResponse.Content)) {
|
|
1516
|
+
$capabilitiesBody = $capabilitiesResponse.Content | ConvertFrom-Json
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
catch {
|
|
1520
|
+
$capabilitiesBody = $null
|
|
1521
|
+
}
|
|
1522
|
+
if ([int]$capabilitiesResponse.StatusCode -eq 409 -and
|
|
1523
|
+
(Test-AnswerMeProtocolUnsupportedErrorResponse -Body $capabilitiesBody)) {
|
|
1524
|
+
[PSCustomObject]@{
|
|
1525
|
+
ok = $false
|
|
1526
|
+
status = 'failed'
|
|
1527
|
+
stage = 'create'
|
|
1528
|
+
code = 'client_protocol_unsupported'
|
|
1529
|
+
message = 'The installed AnswerMe client protocol is not supported by the service.'
|
|
1530
|
+
httpStatus = 409
|
|
1531
|
+
reason = [string]$capabilitiesBody.reason
|
|
1532
|
+
receivedProtocolVersion = [string]$capabilitiesBody.receivedProtocolVersion
|
|
1533
|
+
supportedProtocolVersions = @($capabilitiesBody.supportedProtocolVersions)
|
|
1534
|
+
minimumCompatibleSkillVersion = [string]$capabilitiesBody.minimumCompatibleSkillVersion
|
|
1535
|
+
recoveryReference = 'references/errors.md'
|
|
1536
|
+
networkCalled = $true
|
|
1537
|
+
automaticEnrollmentAttempted = $AnswerMeAutomaticEnrollmentAttempted
|
|
1538
|
+
originalRequestRetried = $AnswerMeOriginalRequestRetried
|
|
1539
|
+
} | ConvertTo-Json -Depth 8
|
|
1540
|
+
exit 6
|
|
1541
|
+
}
|
|
1542
|
+
$supportedProtocolVersions = @($capabilitiesBody.clientProtocol.supportedVersions)
|
|
1543
|
+
if ([int]$capabilitiesResponse.StatusCode -eq 200 -and
|
|
1544
|
+
(Test-AnswerMeIncompatibleCapabilitiesResponse -Body $capabilitiesBody)) {
|
|
1545
|
+
[PSCustomObject]@{
|
|
1546
|
+
ok = $false
|
|
1547
|
+
status = 'failed'
|
|
1548
|
+
stage = 'create'
|
|
1549
|
+
code = 'client_protocol_unsupported'
|
|
1550
|
+
message = 'The installed AnswerMe client protocol is not supported by the service.'
|
|
1551
|
+
httpStatus = 200
|
|
1552
|
+
reason = 'answerme_client_protocol_version_is_not_supported'
|
|
1553
|
+
receivedProtocolVersion = $AnswerMeProtocolVersion
|
|
1554
|
+
supportedProtocolVersions = $supportedProtocolVersions
|
|
1555
|
+
minimumCompatibleSkillVersion = [string]$capabilitiesBody.clientProtocol.minimumCompatibleSkillVersion
|
|
1556
|
+
recoveryReference = 'references/errors.md'
|
|
1557
|
+
networkCalled = $true
|
|
1558
|
+
automaticEnrollmentAttempted = $AnswerMeAutomaticEnrollmentAttempted
|
|
1559
|
+
originalRequestRetried = $AnswerMeOriginalRequestRetried
|
|
1560
|
+
} | ConvertTo-Json -Depth 8
|
|
1561
|
+
exit 6
|
|
1562
|
+
}
|
|
1563
|
+
if ($template.schemaVersion -eq 2 -and
|
|
1564
|
+
([int]$capabilitiesResponse.StatusCode -eq 400 -or
|
|
1565
|
+
([int]$capabilitiesResponse.StatusCode -eq 200 -and
|
|
1566
|
+
@($capabilitiesBody.schemaVersions | Where-Object { ($_ -is [int] -or $_ -is [long]) -and $_ -eq 2 }).Count -eq 0))) {
|
|
1567
|
+
Write-AnswerMeFailure -Code 'unsupported-schema' -Message 'The service did not declare support for schemaVersion=2; creation was not attempted.' -ExitCode 6 -NetworkCalled $true
|
|
1568
|
+
}
|
|
1569
|
+
if ([int]$capabilitiesResponse.StatusCode -ne 200 -or
|
|
1570
|
+
-not (Test-AnswerMeCapabilitiesResponse -Body $capabilitiesBody -SchemaVersion $template.schemaVersion)) {
|
|
1571
|
+
Write-AnswerMeFailure -Code 'capabilities-invalid' -Message 'AnswerMe creation capabilities could not be verified.' -ExitCode 4 -NetworkCalled $true
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if ([string]::IsNullOrWhiteSpace($creatorKey)) {
|
|
1575
|
+
if ([string]::IsNullOrWhiteSpace($CredentialPath)) {
|
|
1576
|
+
Write-AnswerMeFailure `
|
|
1577
|
+
-Code 'credential-path-unavailable' `
|
|
1578
|
+
-Message 'A protected Creator credential target is required for automatic enrollment.' `
|
|
1579
|
+
-ExitCode 3 `
|
|
1580
|
+
-NetworkCalled $true
|
|
1581
|
+
}
|
|
1582
|
+
$recovered = Invoke-AnswerMeAutomaticEnrollment `
|
|
1583
|
+
-ResolvedApiBaseUrl $parsedBaseUrl `
|
|
1584
|
+
-ResolvedCredentialPath $CredentialPath
|
|
1585
|
+
$creatorKey = [string]$recovered.creatorKey
|
|
1586
|
+
$credentialSource = [string]$recovered.credentialSource
|
|
1587
|
+
$recovered = $null
|
|
1588
|
+
}
|
|
1589
|
+
elseif ($credentialSource -ceq 'encrypted-file') {
|
|
1590
|
+
$credentialVerification = Get-AnswerMeCredentialVerification `
|
|
1591
|
+
-ResolvedApiBaseUrl $parsedBaseUrl `
|
|
1592
|
+
-CreatorKey $creatorKey
|
|
1593
|
+
if ($credentialVerification.valid -ne $true) {
|
|
1594
|
+
$recovered = Invoke-AnswerMeAutomaticEnrollment `
|
|
1595
|
+
-ResolvedApiBaseUrl $parsedBaseUrl `
|
|
1596
|
+
-ResolvedCredentialPath $CredentialPath
|
|
1597
|
+
$creatorKey = [string]$recovered.creatorKey
|
|
1598
|
+
$credentialSource = [string]$recovered.credentialSource
|
|
1599
|
+
$recovered = $null
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
$resolvedIdempotencyKey = if ($null -ne $resolvedIdempotencyKey) {
|
|
1604
|
+
$resolvedIdempotencyKey
|
|
1605
|
+
}
|
|
1606
|
+
else {
|
|
1607
|
+
Get-AnswerMeIdempotencyKey -Generate
|
|
1608
|
+
}
|
|
1609
|
+
$uri = ([Uri]::new($parsedBaseUrl, '/api/v1/interactions')).AbsoluteUri
|
|
1610
|
+
|
|
1611
|
+
try {
|
|
1612
|
+
$createRequestStopwatch = [Diagnostics.Stopwatch]::StartNew()
|
|
1613
|
+
try {
|
|
1614
|
+
$headers = @{
|
|
1615
|
+
Authorization = "Bearer $creatorKey"
|
|
1616
|
+
'Idempotency-Key' = $resolvedIdempotencyKey
|
|
1617
|
+
'X-AnswerMe-Protocol-Version' = $AnswerMeProtocolVersion
|
|
1618
|
+
'X-AnswerMe-Skill-Version' = $AnswerMeSkillVersion
|
|
1619
|
+
}
|
|
1620
|
+
$requestBytes = [Text.Encoding]::UTF8.GetBytes($requestJson)
|
|
1621
|
+
$response = Invoke-AnswerMeWebRequest -Uri $uri -Method Post -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $requestBytes
|
|
1622
|
+
$errorBody = $null
|
|
1623
|
+
if ([int]$response.StatusCode -ne 201) {
|
|
1624
|
+
$errorBody = ConvertFrom-AnswerMeResponseBody -Response $response
|
|
1625
|
+
}
|
|
1626
|
+
if (Test-AnswerMeWriterAdmissionResponse -Response $response -Body $errorBody) {
|
|
1627
|
+
$retryAfter = Get-AnswerMeRetryAfterSeconds -Response $response
|
|
1628
|
+
if ($retryAfter -gt 0) {
|
|
1629
|
+
Start-Sleep -Seconds $retryAfter
|
|
1630
|
+
}
|
|
1631
|
+
$script:AnswerMeOriginalRequestRetried = $true
|
|
1632
|
+
$response = Invoke-AnswerMeWebRequest -Uri $uri -Method Post -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $requestBytes
|
|
1633
|
+
$errorBody = $null
|
|
1634
|
+
if ([int]$response.StatusCode -ne 201) {
|
|
1635
|
+
$errorBody = ConvertFrom-AnswerMeResponseBody -Response $response
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
if ([int]$response.StatusCode -eq 401 -and
|
|
1639
|
+
(Test-AnswerMeCreateErrorResponse -StatusCode 401 -Body $errorBody) -and
|
|
1640
|
+
[string]$errorBody.code -ceq 'invalid_creator_credential' -and
|
|
1641
|
+
(Test-AnswerMeRecoverableCredentialReason -Reason ([string]$errorBody.reason)) -and
|
|
1642
|
+
-not $AnswerMeAutomaticEnrollmentAttempted -and
|
|
1643
|
+
-not $AnswerMeOriginalRequestRetried) {
|
|
1644
|
+
$recovered = Invoke-AnswerMeAutomaticEnrollment `
|
|
1645
|
+
-ResolvedApiBaseUrl $parsedBaseUrl `
|
|
1646
|
+
-ResolvedCredentialPath $CredentialPath
|
|
1647
|
+
$creatorKey = [string]$recovered.creatorKey
|
|
1648
|
+
$credentialSource = [string]$recovered.credentialSource
|
|
1649
|
+
$recovered = $null
|
|
1650
|
+
$script:AnswerMeOriginalRequestRetried = $true
|
|
1651
|
+
$headers.Authorization = "Bearer $creatorKey"
|
|
1652
|
+
$response = Invoke-AnswerMeWebRequest `
|
|
1653
|
+
-Uri $uri `
|
|
1654
|
+
-Method Post `
|
|
1655
|
+
-Headers $headers `
|
|
1656
|
+
-ContentType 'application/json; charset=utf-8' `
|
|
1657
|
+
-Body $requestBytes
|
|
1658
|
+
$errorBody = $null
|
|
1659
|
+
if ([int]$response.StatusCode -ne 201) {
|
|
1660
|
+
$errorBody = ConvertFrom-AnswerMeResponseBody -Response $response
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
finally {
|
|
1665
|
+
$AnswerMeTimings.createRequestMilliseconds = [long]$createRequestStopwatch.ElapsedMilliseconds
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
$responseValidationStopwatch = [Diagnostics.Stopwatch]::StartNew()
|
|
1669
|
+
if ([int]$response.StatusCode -ne 201) {
|
|
1670
|
+
if ([int]$response.StatusCode -eq 409 -and
|
|
1671
|
+
(Test-AnswerMeProtocolUnsupportedErrorResponse -Body $errorBody)) {
|
|
1672
|
+
[PSCustomObject]@{
|
|
1673
|
+
ok = $false
|
|
1674
|
+
status = 'failed'
|
|
1675
|
+
stage = 'create'
|
|
1676
|
+
code = 'client_protocol_unsupported'
|
|
1677
|
+
message = 'The installed AnswerMe client protocol is not supported by the service.'
|
|
1678
|
+
httpStatus = 409
|
|
1679
|
+
reason = [string]$errorBody.reason
|
|
1680
|
+
receivedProtocolVersion = [string]$errorBody.receivedProtocolVersion
|
|
1681
|
+
supportedProtocolVersions = @($errorBody.supportedProtocolVersions)
|
|
1682
|
+
minimumCompatibleSkillVersion = [string]$errorBody.minimumCompatibleSkillVersion
|
|
1683
|
+
recoveryReference = 'references/errors.md'
|
|
1684
|
+
idempotencyKey = $resolvedIdempotencyKey
|
|
1685
|
+
networkCalled = $true
|
|
1686
|
+
automaticEnrollmentAttempted = $AnswerMeAutomaticEnrollmentAttempted
|
|
1687
|
+
originalRequestRetried = $AnswerMeOriginalRequestRetried
|
|
1688
|
+
} | ConvertTo-Json -Depth 8
|
|
1689
|
+
exit 6
|
|
1690
|
+
}
|
|
1691
|
+
if ([int]$response.StatusCode -eq 429 -and
|
|
1692
|
+
(Test-AnswerMeCreateErrorResponse -StatusCode 429 -Body $errorBody) -and
|
|
1693
|
+
$errorBody.reason -ceq 'creator_rolling_create_limit') {
|
|
1694
|
+
$retryAfter = Get-AnswerMeRetryAfterSeconds -Response $response
|
|
1695
|
+
Write-AnswerMeFailure `
|
|
1696
|
+
-Code 'rate_limited' `
|
|
1697
|
+
-Message 'Create rate limit reached; combine related questions and retry later.' `
|
|
1698
|
+
-ExitCode 4 `
|
|
1699
|
+
-NetworkCalled $true `
|
|
1700
|
+
-Reason 'creator_rolling_create_limit' `
|
|
1701
|
+
-RetryAfterSeconds $retryAfter `
|
|
1702
|
+
-IdempotencyKey $resolvedIdempotencyKey
|
|
1703
|
+
}
|
|
1704
|
+
if (Test-AnswerMeWriterAdmissionResponse -Response $response -Body $errorBody) {
|
|
1705
|
+
$retryAfter = Get-AnswerMeRetryAfterSeconds -Response $response
|
|
1706
|
+
Write-AnswerMeFailure `
|
|
1707
|
+
-Code 'writer-admission-unavailable' `
|
|
1708
|
+
-Message 'AnswerMe writer admission did not accept this creation request.' `
|
|
1709
|
+
-ExitCode 4 `
|
|
1710
|
+
-NetworkCalled $true `
|
|
1711
|
+
-Reason ([string]$errorBody.reason) `
|
|
1712
|
+
-RetryAfterSeconds $retryAfter `
|
|
1713
|
+
-RecoveryAction 'retry-create-with-same-idempotency-key' `
|
|
1714
|
+
-RecoveryReference 'references/errors.md' `
|
|
1715
|
+
-IdempotencyKey $resolvedIdempotencyKey
|
|
1716
|
+
}
|
|
1717
|
+
$trustedErrorBody = Test-AnswerMeCreateErrorResponse `
|
|
1718
|
+
-StatusCode ([int]$response.StatusCode) `
|
|
1719
|
+
-Body $errorBody
|
|
1720
|
+
Write-AnswerMeFailure `
|
|
1721
|
+
-Code $(if ($trustedErrorBody) { [string]$errorBody.code } else { 'create-http-status' }) `
|
|
1722
|
+
-Message "AnswerMe create returned HTTP $($response.StatusCode)." `
|
|
1723
|
+
-ExitCode 4 `
|
|
1724
|
+
-NetworkCalled $true `
|
|
1725
|
+
-Reason $(if ($trustedErrorBody -and $null -ne $errorBody.PSObject.Properties['reason']) { [string]$errorBody.reason } else { $null }) `
|
|
1726
|
+
-RetryAfterSeconds (Get-AnswerMeRetryAfterSeconds -Response $response) `
|
|
1727
|
+
-IdempotencyKey $resolvedIdempotencyKey
|
|
1728
|
+
}
|
|
1729
|
+
$body = ConvertFrom-AnswerMeResponseBody -Response $response
|
|
1730
|
+
if (-not (Test-AnswerMeCreateResponse -Response $response -Body $body)) {
|
|
1731
|
+
Write-AnswerMeFailure `
|
|
1732
|
+
-Code 'create-response-invalid' `
|
|
1733
|
+
-Message 'Create response did not match the required no-store handoff contract.' `
|
|
1734
|
+
-ExitCode 4 `
|
|
1735
|
+
-NetworkCalled $true `
|
|
1736
|
+
-IdempotencyKey $resolvedIdempotencyKey
|
|
1737
|
+
}
|
|
1738
|
+
$waitDecisionMatches = $null -eq $body.PSObject.Properties['waitDecision']
|
|
1739
|
+
if ($null -ne $body.waitDecision) {
|
|
1740
|
+
$actualWaitDecisionFields = @($body.waitDecision.PSObject.Properties.Name | Sort-Object)
|
|
1741
|
+
$expectedWaitDecisionFields = @('effectiveTimeoutSeconds', 'requestedTimeoutSeconds')
|
|
1742
|
+
if ($null -ne $localWaitDecision.reason) {
|
|
1743
|
+
$expectedWaitDecisionFields += 'reason'
|
|
1744
|
+
}
|
|
1745
|
+
$expectedWaitDecisionFields = @($expectedWaitDecisionFields | Sort-Object)
|
|
1746
|
+
$shapeMatches = $actualWaitDecisionFields.Count -eq $expectedWaitDecisionFields.Count -and
|
|
1747
|
+
-not (Compare-Object -ReferenceObject $expectedWaitDecisionFields -DifferenceObject $actualWaitDecisionFields)
|
|
1748
|
+
$requestedMatches = $null -ne $body.waitDecision.PSObject.Properties['requestedTimeoutSeconds'] -and
|
|
1749
|
+
(ConvertTo-AnswerMeCanonicalJsonScalar $body.waitDecision.requestedTimeoutSeconds) -ceq
|
|
1750
|
+
(ConvertTo-AnswerMeCanonicalJsonScalar $localWaitDecision.requestedTimeoutSeconds)
|
|
1751
|
+
$effectiveMatches = $null -ne $body.waitDecision.PSObject.Properties['effectiveTimeoutSeconds'] -and
|
|
1752
|
+
(ConvertTo-AnswerMeCanonicalJsonScalar $body.waitDecision.effectiveTimeoutSeconds) -ceq
|
|
1753
|
+
(ConvertTo-AnswerMeCanonicalJsonScalar $localWaitDecision.effectiveTimeoutSeconds)
|
|
1754
|
+
$reasonMatches = if ($null -eq $localWaitDecision.reason) {
|
|
1755
|
+
$null -eq $body.waitDecision.PSObject.Properties['reason']
|
|
1756
|
+
}
|
|
1757
|
+
else {
|
|
1758
|
+
$null -ne $body.waitDecision.PSObject.Properties['reason'] -and
|
|
1759
|
+
(ConvertTo-AnswerMeCanonicalJsonScalar $body.waitDecision.reason) -ceq
|
|
1760
|
+
(ConvertTo-AnswerMeCanonicalJsonScalar $localWaitDecision.reason)
|
|
1761
|
+
}
|
|
1762
|
+
$waitDecisionMatches = $shapeMatches -and $requestedMatches -and $effectiveMatches -and $reasonMatches
|
|
1763
|
+
}
|
|
1764
|
+
if (-not $waitDecisionMatches) {
|
|
1765
|
+
Write-AnswerMeFailure `
|
|
1766
|
+
-Code 'create-wait-decision-invalid' `
|
|
1767
|
+
-Message 'Create response omitted or changed the requested wait decision.' `
|
|
1768
|
+
-ExitCode 4 `
|
|
1769
|
+
-NetworkCalled $true `
|
|
1770
|
+
-IdempotencyKey $resolvedIdempotencyKey
|
|
1771
|
+
}
|
|
1772
|
+
$AnswerMeTimings.responseValidationMilliseconds = [long]$responseValidationStopwatch.ElapsedMilliseconds
|
|
1773
|
+
|
|
1774
|
+
$tokenPersistenceStopwatch = [Diagnostics.Stopwatch]::StartNew()
|
|
1775
|
+
try {
|
|
1776
|
+
$resultToken = [string]$body.resultToken
|
|
1777
|
+
$persistedToken = Save-AnswerMeResultToken `
|
|
1778
|
+
-InteractionId ([string]$body.interactionId) `
|
|
1779
|
+
-ResultToken $resultToken `
|
|
1780
|
+
-StoreRoot $ResultTokenStoreRoot `
|
|
1781
|
+
-AllowNonDefaultStoreRoot:$AllowNonDefaultResultTokenStore
|
|
1782
|
+
if (-not $persistedToken.persisted) {
|
|
1783
|
+
throw 'Result Token Store did not confirm persistence.'
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
catch {
|
|
1787
|
+
Write-AnswerMeFailure `
|
|
1788
|
+
-Code 'result-token-persistence-failed' `
|
|
1789
|
+
-Message 'The Result Token could not be verified in protected storage.' `
|
|
1790
|
+
-ExitCode 5 `
|
|
1791
|
+
-NetworkCalled $true `
|
|
1792
|
+
-IdempotencyKey $resolvedIdempotencyKey
|
|
1793
|
+
}
|
|
1794
|
+
finally {
|
|
1795
|
+
$resultToken = $null
|
|
1796
|
+
$body.resultToken = $null
|
|
1797
|
+
}
|
|
1798
|
+
$AnswerMeTimings.tokenPersistenceMilliseconds = [long]$tokenPersistenceStopwatch.ElapsedMilliseconds
|
|
1799
|
+
|
|
1800
|
+
$popupStopwatch = [Diagnostics.Stopwatch]::StartNew()
|
|
1801
|
+
$delivery = [PSCustomObject]@{
|
|
1802
|
+
ok = $false
|
|
1803
|
+
status = 'link-only'
|
|
1804
|
+
code = 'popup-skipped'
|
|
1805
|
+
browserFamily = $null
|
|
1806
|
+
targetTitle = $null
|
|
1807
|
+
computerControlPerformed = $false
|
|
1808
|
+
elapsedMilliseconds = 0
|
|
1809
|
+
}
|
|
1810
|
+
if (-not $SkipPopup) {
|
|
1811
|
+
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
|
|
1812
|
+
$delivery = [PSCustomObject]@{
|
|
1813
|
+
ok = $false
|
|
1814
|
+
status = 'link-only'
|
|
1815
|
+
code = 'desktop-adapter-unavailable'
|
|
1816
|
+
browserFamily = $null
|
|
1817
|
+
targetTitle = $null
|
|
1818
|
+
computerControlPerformed = $false
|
|
1819
|
+
elapsedMilliseconds = 0
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
else {
|
|
1823
|
+
$popupAdapter = Join-Path $PSScriptRoot 'open-answerme-page.windows.ps1'
|
|
1824
|
+
if (-not (Test-Path -LiteralPath $popupAdapter -PathType Leaf)) {
|
|
1825
|
+
$delivery = [PSCustomObject]@{
|
|
1826
|
+
ok = $false
|
|
1827
|
+
status = 'failed'
|
|
1828
|
+
code = 'popup-adapter-missing'
|
|
1829
|
+
browserFamily = $null
|
|
1830
|
+
targetTitle = $null
|
|
1831
|
+
computerControlPerformed = $false
|
|
1832
|
+
elapsedMilliseconds = 0
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
else {
|
|
1836
|
+
try {
|
|
1837
|
+
$popupRaw = & $popupAdapter -Url ([string]$body.answerUrl)
|
|
1838
|
+
$popup = ($popupRaw | Out-String) | ConvertFrom-Json
|
|
1839
|
+
$delivery = [PSCustomObject]@{
|
|
1840
|
+
ok = $popup.ok -eq $true
|
|
1841
|
+
status = [string]$popup.status
|
|
1842
|
+
code = [string]$popup.code
|
|
1843
|
+
browserFamily = [string]$popup.browserFamily
|
|
1844
|
+
targetTitle = [string]$popup.targetTitle
|
|
1845
|
+
computerControlPerformed = $popup.computerControlPerformed -eq $true
|
|
1846
|
+
elapsedMilliseconds = [long]$popup.elapsedMilliseconds
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
catch {
|
|
1850
|
+
$delivery = [PSCustomObject]@{
|
|
1851
|
+
ok = $false
|
|
1852
|
+
status = 'failed'
|
|
1853
|
+
code = 'popup-adapter-invalid-result'
|
|
1854
|
+
browserFamily = $null
|
|
1855
|
+
targetTitle = $null
|
|
1856
|
+
computerControlPerformed = $false
|
|
1857
|
+
elapsedMilliseconds = 0
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
$AnswerMeTimings.popupMilliseconds = [long]$popupStopwatch.ElapsedMilliseconds
|
|
1864
|
+
|
|
1865
|
+
[PSCustomObject]@{
|
|
1866
|
+
ok = $true
|
|
1867
|
+
status = [string]$body.status
|
|
1868
|
+
inputMode = $inputMode
|
|
1869
|
+
interactionId = $body.interactionId
|
|
1870
|
+
answerUrl = $body.answerUrl
|
|
1871
|
+
resultTokenPersisted = $true
|
|
1872
|
+
resultTokenProvider = $persistedToken.provider
|
|
1873
|
+
expiresAt = $body.expiresAt
|
|
1874
|
+
requestId = $body.requestId
|
|
1875
|
+
questionCount = @($template.questions).Count
|
|
1876
|
+
waitDecision = $body.waitDecision
|
|
1877
|
+
delivery = $delivery
|
|
1878
|
+
timings = Get-AnswerMeTimings
|
|
1879
|
+
networkCalled = $true
|
|
1880
|
+
automaticEnrollmentAttempted = $AnswerMeAutomaticEnrollmentAttempted
|
|
1881
|
+
originalRequestRetried = $AnswerMeOriginalRequestRetried
|
|
1882
|
+
} | ConvertTo-Json -Depth 8
|
|
1883
|
+
exit 0
|
|
1884
|
+
}
|
|
1885
|
+
catch {
|
|
1886
|
+
Write-AnswerMeFailure `
|
|
1887
|
+
-Code 'create-request-failed' `
|
|
1888
|
+
-Message 'AnswerMe creation failed before a verified response could be returned.' `
|
|
1889
|
+
-ExitCode 4 `
|
|
1890
|
+
-NetworkCalled $true `
|
|
1891
|
+
-IdempotencyKey $resolvedIdempotencyKey
|
|
1892
|
+
}
|