@rover-studio/answer-me 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/bin/answerme-toolkit.mjs +6 -0
  2. package/distribution/npm/migrations.json +605 -0
  3. package/distribution/npm/package-manifest.json +192 -0
  4. package/distribution/npm/skills/answerme/SKILL.md +94 -0
  5. package/distribution/npm/skills/answerme/agents/openai.yaml +4 -0
  6. package/distribution/npm/skills/answerme/references/api.md +135 -0
  7. package/distribution/npm/skills/answerme/references/creator-credential-deployment.md +19 -0
  8. package/distribution/npm/skills/answerme/references/creator-credential-recovery.md +24 -0
  9. package/distribution/npm/skills/answerme/references/errors.md +44 -0
  10. package/distribution/npm/skills/answerme/references/handoff.md +46 -0
  11. package/distribution/npm/skills/answerme/references/install-self-test.md +28 -0
  12. package/distribution/npm/skills/answerme/references/result-token-store.md +50 -0
  13. package/distribution/npm/skills/answerme/references/templates.md +139 -0
  14. package/distribution/npm/skills/answerme/scripts/answerme-api-base-url.ps1 +40 -0
  15. package/distribution/npm/skills/answerme/scripts/create-answerme.ps1 +1892 -0
  16. package/distribution/npm/skills/answerme/scripts/creator-credential-store.windows.ps1 +503 -0
  17. package/distribution/npm/skills/answerme/scripts/deploy-answerme-creator-credential.ps1 +447 -0
  18. package/distribution/npm/skills/answerme/scripts/enroll-answerme-creator.ps1 +764 -0
  19. package/distribution/npm/skills/answerme/scripts/open-answerme-page.windows.ps1 +272 -0
  20. package/distribution/npm/skills/answerme/scripts/remove-answerme-result-token.ps1 +63 -0
  21. package/distribution/npm/skills/answerme/scripts/result-token-store.windows.ps1 +261 -0
  22. package/distribution/npm/skills/answerme/scripts/test-answerme-installation.ps1 +498 -0
  23. package/distribution/npm/skills/answerme/scripts/wait-answerme-result.ps1 +908 -0
  24. package/distribution/npm/skills/answerme/scripts/windows-crypto.ps1 +57 -0
  25. package/distribution/npm/skills/answerme/scripts/windows-http.ps1 +45 -0
  26. package/distribution/npm/skills/answerme/scripts/windows-process-start-info.ps1 +76 -0
  27. package/distribution/npm/skills/ask-when-needed/SKILL.md +164 -0
  28. package/distribution/npm/skills/ask-when-needed/agents/openai.yaml +4 -0
  29. package/distribution/npm/skills/ask-when-needed/references/interview-strategies.md +43 -0
  30. package/lib/npm-cli/commands.mjs +247 -0
  31. package/lib/npm-cli/constants.mjs +51 -0
  32. package/lib/npm-cli/errors.mjs +15 -0
  33. package/lib/npm-cli/filesystem.mjs +193 -0
  34. package/lib/npm-cli/host-discovery.mjs +404 -0
  35. package/lib/npm-cli/main.mjs +42 -0
  36. package/lib/npm-cli/package-integrity.mjs +212 -0
  37. package/lib/npm-cli/transaction.mjs +375 -0
  38. package/lib/npm-cli/usage-validation.mjs +349 -0
  39. package/package.json +17 -0
@@ -0,0 +1,764 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [string]$ApiBaseUrl,
4
+ [switch]$AllowInsecureLoopbackHttpForTest,
5
+ [string]$CredentialPath = $env:ANSWERME_CREATOR_CREDENTIAL_PATH,
6
+ [object]$WaitTimeoutSeconds = 300,
7
+ [ValidateRange(1, 30)]
8
+ [int]$PollSeconds = 2,
9
+ [switch]$SkipPopup,
10
+ [switch]$ValidateOnly
11
+ )
12
+
13
+ $ErrorActionPreference = 'Stop'
14
+ $httpSupport = Join-Path $PSScriptRoot 'windows-http.ps1'
15
+ if (-not (Test-Path -LiteralPath $httpSupport -PathType Leaf)) {
16
+ throw 'The AnswerMe Windows HTTP helper is missing.'
17
+ }
18
+ . $httpSupport
19
+ $AnswerMeProtocolVersion = '1.1'
20
+ $AnswerMeSkillVersion = '0.3.0'
21
+ $AnswerMeNetworkCalled = $false
22
+ $AnswerMeVerificationDelivery = $null
23
+
24
+ function Write-AnswerMeEnrollmentFailure {
25
+ param(
26
+ [Parameter(Mandatory = $true)][string]$Stage,
27
+ [Parameter(Mandatory = $true)][string]$Code,
28
+ [Parameter(Mandatory = $true)][string]$Message,
29
+ [Parameter(Mandatory = $true)][int]$ExitCode,
30
+ [AllowNull()][object]$Reason,
31
+ [int]$RetryAfterSeconds = 0,
32
+ [AllowNull()][string]$CredentialStoreStage,
33
+ [bool]$TargetChanged = $false,
34
+ [bool]$BackupCreated = $false,
35
+ [AllowNull()][string]$RollbackStatus
36
+ )
37
+
38
+ [PSCustomObject]@{
39
+ ok = $false
40
+ status = 'failed'
41
+ stage = $Stage
42
+ code = $Code
43
+ message = $Message
44
+ reason = $Reason
45
+ retryAfterSeconds = $RetryAfterSeconds
46
+ verificationDelivery = $AnswerMeVerificationDelivery
47
+ credentialState = $null
48
+ credentialStoreStage = $CredentialStoreStage
49
+ targetChanged = $TargetChanged
50
+ backupCreated = $BackupCreated
51
+ rollbackStatus = $RollbackStatus
52
+ networkCalled = $AnswerMeNetworkCalled
53
+ } | ConvertTo-Json -Depth 8
54
+ exit $ExitCode
55
+ }
56
+
57
+ function Get-AnswerMeDefaultCreatorCredentialPath {
58
+ if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
59
+ return $null
60
+ }
61
+ $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
62
+ if ([string]::IsNullOrWhiteSpace($localAppData)) {
63
+ return $null
64
+ }
65
+ return Join-Path $localAppData 'AnswerMe\creator-credential.clixml'
66
+ }
67
+
68
+ function ConvertFrom-AnswerMeEnrollmentResponse {
69
+ param([Parameter(Mandatory = $true)][object]$Response)
70
+
71
+ if ([string]::IsNullOrWhiteSpace([string]$Response.Content)) {
72
+ return $null
73
+ }
74
+ try {
75
+ return $Response.Content | ConvertFrom-Json
76
+ }
77
+ catch {
78
+ return $null
79
+ }
80
+ }
81
+
82
+ function Test-AnswerMeEnrollmentNoStore {
83
+ param([Parameter(Mandatory = $true)][object]$Response)
84
+
85
+ return ([string]$Response.Headers['Cache-Control']).Trim() -ceq 'no-store'
86
+ }
87
+
88
+ function Test-AnswerMeEnrollmentResponseFields {
89
+ param(
90
+ [AllowNull()][object]$Body,
91
+ [Parameter(Mandatory = $true)][string[]]$ExpectedFields
92
+ )
93
+
94
+ if ($null -eq $Body) { return $false }
95
+ $actual = @($Body.PSObject.Properties.Name | Sort-Object)
96
+ $expected = @($ExpectedFields | Sort-Object)
97
+ return $actual.Count -eq $expected.Count -and
98
+ -not (Compare-Object -ReferenceObject $expected -DifferenceObject $actual)
99
+ }
100
+
101
+ function Test-AnswerMeEnrollmentJsonString {
102
+ param(
103
+ [AllowNull()][object]$Body,
104
+ [Parameter(Mandatory = $true)][string]$PropertyName,
105
+ [int]$MaximumLength = 0
106
+ )
107
+
108
+ if ($null -eq $Body) { return $false }
109
+ $property = $Body.PSObject.Properties[$PropertyName]
110
+ if ($null -eq $property -or $property.Value -isnot [string]) { return $false }
111
+ $value = [string]$property.Value
112
+ return -not [string]::IsNullOrWhiteSpace($value) -and
113
+ ($MaximumLength -le 0 -or $value.Length -le $MaximumLength)
114
+ }
115
+
116
+ function Test-AnswerMeEnrollmentJsonInt64 {
117
+ param(
118
+ [AllowNull()][object]$Body,
119
+ [Parameter(Mandatory = $true)][string]$PropertyName
120
+ )
121
+
122
+ if ($null -eq $Body) { return $false }
123
+ $property = $Body.PSObject.Properties[$PropertyName]
124
+ if ($null -eq $property) { return $false }
125
+ $value = $property.Value
126
+ return ($value -is [int] -or $value -is [long]) -and [long]$value -ge 0
127
+ }
128
+
129
+ function Test-AnswerMeEnrollmentCapabilitiesResponse {
130
+ param([AllowNull()][object]$Body)
131
+
132
+ if ($Body -isnot [PSCustomObject] -or
133
+ -not (Test-AnswerMeEnrollmentResponseFields `
134
+ -Body $Body `
135
+ -ExpectedFields @('protocolVersion', 'schemaVersions', 'clientProtocol', 'capabilities', 'requestId')) -or
136
+ -not (Test-AnswerMeEnrollmentJsonString -Body $Body -PropertyName 'protocolVersion') -or
137
+ [string]$Body.protocolVersion -cne $AnswerMeProtocolVersion -or
138
+ -not (Test-AnswerMeEnrollmentJsonString -Body $Body -PropertyName 'requestId')) {
139
+ return $false
140
+ }
141
+
142
+ if ($Body.schemaVersions -isnot [Array] -or
143
+ @($Body.schemaVersions).Count -ne 1 -or
144
+ ($Body.schemaVersions[0] -isnot [int] -and $Body.schemaVersions[0] -isnot [long]) -or
145
+ [long]$Body.schemaVersions[0] -ne 1) {
146
+ return $false
147
+ }
148
+
149
+ $clientProtocol = $Body.clientProtocol
150
+ if ($clientProtocol -isnot [PSCustomObject] -or
151
+ -not (Test-AnswerMeEnrollmentResponseFields `
152
+ -Body $clientProtocol `
153
+ -ExpectedFields @('supportedVersions', 'recommendedVersion', 'minimumCompatibleSkillVersion')) -or
154
+ -not (Test-AnswerMeEnrollmentJsonString -Body $clientProtocol -PropertyName 'recommendedVersion') -or
155
+ [string]$clientProtocol.recommendedVersion -cne $AnswerMeProtocolVersion -or
156
+ -not (Test-AnswerMeEnrollmentJsonString -Body $clientProtocol -PropertyName 'minimumCompatibleSkillVersion') -or
157
+ [string]$clientProtocol.minimumCompatibleSkillVersion -cne '0.1.0' -or
158
+ $clientProtocol.supportedVersions -isnot [Array]) {
159
+ return $false
160
+ }
161
+ $supportedVersions = @($clientProtocol.supportedVersions)
162
+ if ($supportedVersions.Count -ne 2 -or
163
+ $supportedVersions[0] -isnot [string] -or
164
+ $supportedVersions[1] -isnot [string] -or
165
+ $supportedVersions -cnotcontains '1.0' -or
166
+ $supportedVersions -cnotcontains $AnswerMeProtocolVersion) {
167
+ return $false
168
+ }
169
+
170
+ $capabilities = $Body.capabilities
171
+ if ($capabilities -isnot [PSCustomObject] -or
172
+ -not (Test-AnswerMeEnrollmentResponseFields `
173
+ -Body $capabilities `
174
+ -ExpectedFields @('createInteraction', 'resultQuery', 'resultWait')) -or
175
+ $capabilities.createInteraction -isnot [bool] -or
176
+ $capabilities.createInteraction -ne $true -or
177
+ $capabilities.resultQuery -isnot [bool]) {
178
+ return $false
179
+ }
180
+
181
+ $resultWait = $capabilities.resultWait
182
+ return $resultWait -is [PSCustomObject] -and
183
+ (Test-AnswerMeEnrollmentResponseFields `
184
+ -Body $resultWait `
185
+ -ExpectedFields @('enabled', 'defaultWaitSeconds', 'maxWaitSeconds')) -and
186
+ $resultWait.enabled -is [bool] -and
187
+ ($resultWait.defaultWaitSeconds -is [int] -or $resultWait.defaultWaitSeconds -is [long]) -and
188
+ [long]$resultWait.defaultWaitSeconds -eq 55 -and
189
+ ($resultWait.maxWaitSeconds -is [int] -or $resultWait.maxWaitSeconds -is [long]) -and
190
+ [long]$resultWait.maxWaitSeconds -eq 600
191
+ }
192
+
193
+ function Get-AnswerMeEnrollmentRetryAfterSeconds {
194
+ param([Parameter(Mandatory = $true)][object]$Response)
195
+
196
+ $parsed = 0
197
+ if ([int]::TryParse([string]$Response.Headers['Retry-After'], [ref]$parsed) -and $parsed -ge 0) {
198
+ return $parsed
199
+ }
200
+ return 0
201
+ }
202
+
203
+ function Get-AnswerMeEnrollmentAllowedRemoteFailure {
204
+ param(
205
+ [Parameter(Mandatory = $true)][ValidateSet('create', 'status', 'claim')][string]$Endpoint,
206
+ [Parameter(Mandatory = $true)][object]$Response,
207
+ [AllowNull()][object]$Body,
208
+ [Parameter(Mandatory = $true)][string]$FallbackCode
209
+ )
210
+
211
+ $generic = [PSCustomObject]@{ code = $FallbackCode; reason = $null; retryAfterSeconds = 0 }
212
+ if ($Body -isnot [PSCustomObject]) { return $generic }
213
+ $actualFields = @($Body.PSObject.Properties.Name)
214
+ if ($actualFields | Where-Object { $_ -cnotin @('code', 'requestId', 'path', 'reason') }) {
215
+ return $generic
216
+ }
217
+ if (-not (Test-AnswerMeEnrollmentJsonString -Body $Body -PropertyName 'code') -or
218
+ -not (Test-AnswerMeEnrollmentJsonString -Body $Body -PropertyName 'requestId')) {
219
+ return $generic
220
+ }
221
+ if ($null -ne $Body.PSObject.Properties['path'] -and $Body.path -isnot [string]) { return $generic }
222
+ if ($null -ne $Body.PSObject.Properties['reason'] -and $Body.reason -isnot [string]) { return $generic }
223
+
224
+ $status = [int]$Response.StatusCode
225
+ $code = [string]$Body.code
226
+ $reason = if ($null -ne $Body.PSObject.Properties['reason']) { [string]$Body.reason } else { $null }
227
+ $allowed = $false
228
+ switch ("$Endpoint`:$status") {
229
+ 'create:503' {
230
+ $allowed = $code -ceq 'enrollment_unavailable' -and
231
+ $reason -cin @('automatic_issuance_disabled', 'issuance_unavailable')
232
+ }
233
+ 'status:404' {
234
+ $allowed = $code -ceq 'enrollment_not_found' -and
235
+ ($null -eq $reason -or $reason -ceq 'enrollment_not_found')
236
+ }
237
+ 'status:503' {
238
+ $allowed = $code -ceq 'enrollment_unavailable' -and $reason -ceq 'claim_unavailable'
239
+ }
240
+ 'claim:401' {
241
+ $allowed = $code -ceq 'claim_invalid' -and $reason -ceq 'claim_invalid'
242
+ }
243
+ 'claim:404' {
244
+ $allowed = $code -ceq 'enrollment_not_found' -and $reason -ceq 'enrollment_not_found'
245
+ }
246
+ 'claim:409' {
247
+ $allowed = $code -ceq 'enrollment_conflict' -and
248
+ $reason -cin @(
249
+ 'enrollment_pending',
250
+ 'enrollment_failed',
251
+ 'enrollment_already_claimed',
252
+ 'enrollment_replay',
253
+ 'verification_token_replay'
254
+ )
255
+ }
256
+ 'claim:410' {
257
+ $allowed = $code -ceq 'enrollment_expired' -and
258
+ $reason -cin @('enrollment_expired', 'verification_token_expired')
259
+ }
260
+ 'claim:429' {
261
+ $allowed = $code -ceq 'claim_rate_limited' -and $reason -ceq 'claim_rate_limited'
262
+ }
263
+ 'claim:503' {
264
+ $allowed = $code -ceq 'enrollment_unavailable' -and $reason -ceq 'claim_unavailable'
265
+ }
266
+ }
267
+ if (-not $allowed) { return $generic }
268
+ return [PSCustomObject]@{
269
+ code = $code
270
+ reason = $reason
271
+ retryAfterSeconds = Get-AnswerMeEnrollmentRetryAfterSeconds -Response $Response
272
+ }
273
+ }
274
+
275
+ function Write-AnswerMeEnrollmentRemoteFailure {
276
+ param(
277
+ [Parameter(Mandatory = $true)][string]$Stage,
278
+ [Parameter(Mandatory = $true)][ValidateSet('create', 'status', 'claim')][string]$Endpoint,
279
+ [Parameter(Mandatory = $true)][object]$Response,
280
+ [AllowNull()][object]$Body,
281
+ [Parameter(Mandatory = $true)][string]$FallbackCode,
282
+ [int]$ExitCode = 4
283
+ )
284
+
285
+ $failure = Get-AnswerMeEnrollmentAllowedRemoteFailure `
286
+ -Endpoint $Endpoint `
287
+ -Response $Response `
288
+ -Body $Body `
289
+ -FallbackCode $FallbackCode
290
+ Write-AnswerMeEnrollmentFailure `
291
+ -Stage $Stage `
292
+ -Code $failure.code `
293
+ -Message "AnswerMe enrollment failed during $Stage." `
294
+ -ExitCode $ExitCode `
295
+ -Reason $failure.reason `
296
+ -RetryAfterSeconds $failure.retryAfterSeconds
297
+ }
298
+
299
+ function Write-AnswerMeEnrollmentVerificationUrlEvent {
300
+ param([Parameter(Mandatory = $true)][Uri]$VerificationUrl)
301
+
302
+ $event = [PSCustomObject][ordered]@{
303
+ event = 'answerme.creator-enrollment.verification-url'
304
+ stage = 'verification-handoff'
305
+ verificationUrl = $VerificationUrl.AbsoluteUri
306
+ }
307
+ [Console]::Error.WriteLine(($event | ConvertTo-Json -Compress))
308
+ [Console]::Error.Flush()
309
+ $acknowledgementToken = [Environment]::GetEnvironmentVariable(
310
+ 'ANSWERME_INTERNAL_VERIFICATION_EVENT_ACK',
311
+ 'Process'
312
+ )
313
+ if (-not [string]::IsNullOrWhiteSpace($acknowledgementToken)) {
314
+ $acknowledgement = [Console]::In.ReadLine()
315
+ if ($acknowledgement -cne $acknowledgementToken) {
316
+ Write-AnswerMeEnrollmentFailure `
317
+ -Stage 'verification-handoff' `
318
+ -Code 'verification-event-acknowledgement-invalid' `
319
+ -Message 'The verification URL event was not acknowledged by the parent process.' `
320
+ -ExitCode 4
321
+ }
322
+ }
323
+ }
324
+
325
+ function Test-AnswerMeEnrollmentSameOrigin {
326
+ param(
327
+ [Parameter(Mandatory = $true)][Uri]$Left,
328
+ [Parameter(Mandatory = $true)][Uri]$Right
329
+ )
330
+
331
+ return $Left.Scheme.Equals($Right.Scheme, [StringComparison]::OrdinalIgnoreCase) -and
332
+ $Left.DnsSafeHost.Equals($Right.DnsSafeHost, [StringComparison]::OrdinalIgnoreCase) -and
333
+ $Left.Port -eq $Right.Port
334
+ }
335
+
336
+ function Invoke-AnswerMeEnrollmentCredentialInstall {
337
+ param(
338
+ [Parameter(Mandatory = $true)][Security.SecureString]$CreatorKey,
339
+ [Parameter(Mandatory = $true)][Uri]$ResolvedApiBaseUrl,
340
+ [Parameter(Mandatory = $true)][string]$CreatorId,
341
+ [Parameter(Mandatory = $true)][string]$ResolvedCredentialPath
342
+ )
343
+
344
+ $adapterPath = Join-Path $PSScriptRoot 'creator-credential-store.windows.ps1'
345
+ if (-not (Test-Path -LiteralPath $adapterPath -PathType Leaf)) {
346
+ throw (New-AnswerMeEnrollmentDependencyException `
347
+ -Code 'credential-store-adapter-missing' `
348
+ -Message 'The protected Creator credential store adapter is required.')
349
+ }
350
+ try {
351
+ . $adapterPath
352
+ }
353
+ catch {
354
+ throw (New-AnswerMeEnrollmentDependencyException `
355
+ -Code 'credential-store-adapter-unavailable' `
356
+ -Message 'The protected Creator credential store adapter could not be loaded.')
357
+ }
358
+ return Install-AnswerMeCreatorCredential `
359
+ -CreatorKey $CreatorKey `
360
+ -ApiBaseUrl $ResolvedApiBaseUrl.AbsoluteUri.TrimEnd('/') `
361
+ -CreatorId $CreatorId `
362
+ -TargetCredentialPath $ResolvedCredentialPath `
363
+ -AllowInsecureLoopbackHttpForTest:$AllowInsecureLoopbackHttpForTest
364
+ }
365
+
366
+ function New-AnswerMeEnrollmentDependencyException {
367
+ param(
368
+ [Parameter(Mandatory = $true)][string]$Code,
369
+ [Parameter(Mandatory = $true)][string]$Message
370
+ )
371
+
372
+ $exception = [InvalidOperationException]::new($Message)
373
+ $exception.Data['AnswerMeCode'] = $Code
374
+ $exception.Data['AnswerMeStage'] = 'preflight'
375
+ $exception.Data['AnswerMeTargetChanged'] = $false
376
+ $exception.Data['AnswerMeBackupCreated'] = $false
377
+ $exception.Data['AnswerMeRollbackStatus'] = 'not-required'
378
+ $exception.Data['AnswerMeNetworkCalled'] = $false
379
+ return $exception
380
+ }
381
+
382
+ $apiPolicyPath = Join-Path $PSScriptRoot 'answerme-api-base-url.ps1'
383
+ if (-not (Test-Path -LiteralPath $apiPolicyPath -PathType Leaf)) {
384
+ Write-AnswerMeEnrollmentFailure `
385
+ -Stage 'preflight' `
386
+ -Code 'api-base-url-policy-missing' `
387
+ -Message 'The AnswerMe API base URL policy is required.' `
388
+ -ExitCode 3
389
+ }
390
+ try {
391
+ . $apiPolicyPath
392
+ }
393
+ catch {
394
+ Write-AnswerMeEnrollmentFailure `
395
+ -Stage 'preflight' `
396
+ -Code 'api-base-url-policy-unavailable' `
397
+ -Message 'The AnswerMe API base URL policy could not be loaded.' `
398
+ -ExitCode 3
399
+ }
400
+
401
+ try {
402
+ $parsedBaseUrl = Resolve-AnswerMeApiBaseUrl `
403
+ -ApiBaseUrl $ApiBaseUrl `
404
+ -AllowInsecureLoopbackHttpForTest:$AllowInsecureLoopbackHttpForTest
405
+ if ($null -eq $parsedBaseUrl) {
406
+ throw 'A trusted AnswerMe API base URL is required.'
407
+ }
408
+ }
409
+ catch {
410
+ Write-AnswerMeEnrollmentFailure `
411
+ -Stage 'preflight' `
412
+ -Code 'api-base-url-invalid' `
413
+ -Message $_.Exception.Message `
414
+ -ExitCode 2
415
+ }
416
+
417
+ $waitText = [string]$WaitTimeoutSeconds
418
+ $waitValue = 0
419
+ $waitValid = [int]::TryParse(
420
+ $waitText,
421
+ [Globalization.NumberStyles]::Integer,
422
+ [Globalization.CultureInfo]::InvariantCulture,
423
+ [ref]$waitValue
424
+ ) -and $waitValue -ge 30 -and $waitValue -le 600
425
+ if (-not $waitValid) {
426
+ Write-AnswerMeEnrollmentFailure `
427
+ -Stage 'preflight' `
428
+ -Code 'enrollment-wait-timeout-invalid' `
429
+ -Message 'WaitTimeoutSeconds must be an integer from 30 through 600.' `
430
+ -ExitCode 2
431
+ }
432
+
433
+ if ([string]::IsNullOrWhiteSpace($CredentialPath)) {
434
+ $CredentialPath = Get-AnswerMeDefaultCreatorCredentialPath
435
+ }
436
+ if ([string]::IsNullOrWhiteSpace($CredentialPath)) {
437
+ Write-AnswerMeEnrollmentFailure `
438
+ -Stage 'preflight' `
439
+ -Code 'credential-path-unavailable' `
440
+ -Message 'A protected Creator credential target is required.' `
441
+ -ExitCode 3
442
+ }
443
+ $resolvedCredentialPath = [IO.Path]::GetFullPath($CredentialPath)
444
+
445
+ if ($ValidateOnly) {
446
+ [PSCustomObject]@{
447
+ ok = $true
448
+ status = 'validated'
449
+ stage = 'preflight'
450
+ apiBaseUrl = $parsedBaseUrl.AbsoluteUri.TrimEnd('/')
451
+ waitTimeoutSeconds = $waitValue
452
+ pollSeconds = $PollSeconds
453
+ sameOriginVerificationRequired = $true
454
+ privateClaimRequired = $true
455
+ protectedCredentialInstallRequired = $true
456
+ networkCalled = $false
457
+ } | ConvertTo-Json -Depth 5
458
+ exit 0
459
+ }
460
+
461
+ $commonHeaders = @{
462
+ Accept = 'application/json'
463
+ 'X-AnswerMe-Protocol-Version' = $AnswerMeProtocolVersion
464
+ 'X-AnswerMe-Skill-Version' = $AnswerMeSkillVersion
465
+ }
466
+ $capabilitiesUri = ([Uri]::new($parsedBaseUrl, '/api/v1/capabilities')).AbsoluteUri
467
+ $AnswerMeNetworkCalled = $true
468
+ try {
469
+ $capabilitiesResponse = Invoke-AnswerMeWebRequest `
470
+ -Uri $capabilitiesUri `
471
+ -Method Get `
472
+ -Headers $commonHeaders `
473
+ -TimeoutSec 15
474
+ }
475
+ catch {
476
+ Write-AnswerMeEnrollmentFailure `
477
+ -Stage 'capabilities' `
478
+ -Code 'capabilities-unavailable' `
479
+ -Message 'AnswerMe capabilities could not be read.' `
480
+ -ExitCode 4
481
+ }
482
+ $capabilitiesBody = ConvertFrom-AnswerMeEnrollmentResponse -Response $capabilitiesResponse
483
+ if ([int]$capabilitiesResponse.StatusCode -ne 200 -or
484
+ -not (Test-AnswerMeEnrollmentCapabilitiesResponse -Body $capabilitiesBody)) {
485
+ Write-AnswerMeEnrollmentFailure `
486
+ -Stage 'capabilities' `
487
+ -Code 'capabilities-invalid' `
488
+ -Message 'AnswerMe capabilities did not match the complete enrollment protocol contract.' `
489
+ -ExitCode 4
490
+ }
491
+
492
+ $createUri = ([Uri]::new($parsedBaseUrl, '/api/v1/creator-enrollments')).AbsoluteUri
493
+ try {
494
+ $createResponse = Invoke-AnswerMeWebRequest `
495
+ -Uri $createUri `
496
+ -Method Post `
497
+ -Headers $commonHeaders `
498
+ -TimeoutSec 15
499
+ }
500
+ catch {
501
+ Write-AnswerMeEnrollmentFailure `
502
+ -Stage 'create-enrollment' `
503
+ -Code 'enrollment-connection-unknown' `
504
+ -Message 'Creator enrollment creation has an unknown outcome.' `
505
+ -ExitCode 4
506
+ }
507
+ $createBody = ConvertFrom-AnswerMeEnrollmentResponse -Response $createResponse
508
+ if ([int]$createResponse.StatusCode -ne 201) {
509
+ Write-AnswerMeEnrollmentRemoteFailure `
510
+ -Stage 'create-enrollment' `
511
+ -Endpoint 'create' `
512
+ -Response $createResponse `
513
+ -Body $createBody `
514
+ -FallbackCode 'enrollment-create-http-status'
515
+ }
516
+ if (-not (Test-AnswerMeEnrollmentNoStore -Response $createResponse) -or
517
+ -not (Test-AnswerMeEnrollmentResponseFields `
518
+ -Body $createBody `
519
+ -ExpectedFields @('enrollmentId', 'verificationUrl', 'claimCredential', 'verificationExpiresAt', 'requestId')) -or
520
+ -not (Test-AnswerMeEnrollmentJsonString -Body $createBody -PropertyName 'enrollmentId' -MaximumLength 128) -or
521
+ -not (Test-AnswerMeEnrollmentJsonString -Body $createBody -PropertyName 'verificationUrl') -or
522
+ -not (Test-AnswerMeEnrollmentJsonString -Body $createBody -PropertyName 'claimCredential' -MaximumLength 128) -or
523
+ -not (Test-AnswerMeEnrollmentJsonInt64 -Body $createBody -PropertyName 'verificationExpiresAt') -or
524
+ -not (Test-AnswerMeEnrollmentJsonString -Body $createBody -PropertyName 'requestId')) {
525
+ Write-AnswerMeEnrollmentFailure `
526
+ -Stage 'create-enrollment' `
527
+ -Code 'enrollment-create-response-invalid' `
528
+ -Message 'Creator enrollment creation omitted a required no-store handoff field.' `
529
+ -ExitCode 4
530
+ }
531
+
532
+ $enrollmentId = [string]$createBody.enrollmentId
533
+ $claimCredential = [string]$createBody.claimCredential
534
+ $verificationUrl = $null
535
+ if (-not [Uri]::TryCreate([string]$createBody.verificationUrl, [UriKind]::Absolute, [ref]$verificationUrl) -or
536
+ -not (Test-AnswerMeEnrollmentSameOrigin -Left $parsedBaseUrl -Right $verificationUrl) -or
537
+ -not [string]::IsNullOrEmpty($verificationUrl.UserInfo) -or
538
+ -not [string]::IsNullOrEmpty($verificationUrl.Fragment) -or
539
+ $verificationUrl.AbsoluteUri.IndexOf($claimCredential, [StringComparison]::Ordinal) -ge 0 -or
540
+ $verificationUrl.AbsoluteUri.IndexOf([Uri]::EscapeDataString($claimCredential), [StringComparison]::Ordinal) -ge 0) {
541
+ $claimCredential = $null
542
+ $createBody.claimCredential = $null
543
+ Write-AnswerMeEnrollmentFailure `
544
+ -Stage 'verification-handoff' `
545
+ -Code 'verification-url-invalid' `
546
+ -Message 'The enrollment verification URL did not satisfy the trusted same-origin contract.' `
547
+ -ExitCode 4
548
+ }
549
+ $createBody.claimCredential = $null
550
+
551
+ $AnswerMeVerificationDelivery = [PSCustomObject]@{
552
+ status = 'link-only'
553
+ code = 'popup-skipped'
554
+ foregroundVerified = $false
555
+ computerControlPerformed = $false
556
+ manualUrl = $verificationUrl.AbsoluteUri
557
+ }
558
+ if (-not $SkipPopup) {
559
+ $popupScript = Join-Path $PSScriptRoot 'open-answerme-page.windows.ps1'
560
+ if (-not (Test-Path -LiteralPath $popupScript -PathType Leaf)) {
561
+ Write-AnswerMeEnrollmentFailure `
562
+ -Stage 'verification-handoff' `
563
+ -Code 'popup-adapter-missing' `
564
+ -Message 'The enrollment verification page could not be opened.' `
565
+ -ExitCode 3
566
+ }
567
+ try {
568
+ $popupRaw = & $popupScript -Url $verificationUrl.AbsoluteUri
569
+ $popup = ($popupRaw | Out-String) | ConvertFrom-Json
570
+ $AnswerMeVerificationDelivery = [PSCustomObject]@{
571
+ status = [string]$popup.status
572
+ code = [string]$popup.code
573
+ foregroundVerified = $popup.ok -eq $true -and [string]$popup.status -ceq 'foreground-verified'
574
+ computerControlPerformed = $popup.computerControlPerformed -eq $true
575
+ manualUrl = $verificationUrl.AbsoluteUri
576
+ }
577
+ }
578
+ catch {
579
+ Write-AnswerMeEnrollmentFailure `
580
+ -Stage 'verification-handoff' `
581
+ -Code 'popup-adapter-invalid-result' `
582
+ -Message 'The enrollment verification page delivery result was invalid.' `
583
+ -ExitCode 4
584
+ }
585
+ }
586
+ if ($AnswerMeVerificationDelivery.foregroundVerified -ne $true) {
587
+ Write-AnswerMeEnrollmentVerificationUrlEvent -VerificationUrl $verificationUrl
588
+ }
589
+
590
+ $escapedEnrollmentId = [Uri]::EscapeDataString($enrollmentId)
591
+ $statusUri = ([Uri]::new($parsedBaseUrl, "/api/v1/creator-enrollments/$escapedEnrollmentId")).AbsoluteUri
592
+ $deadline = [DateTimeOffset]::UtcNow.AddSeconds($waitValue)
593
+ $verified = $false
594
+ while ([DateTimeOffset]::UtcNow -lt $deadline) {
595
+ try {
596
+ $statusResponse = Invoke-AnswerMeWebRequest `
597
+ -Uri $statusUri `
598
+ -Method Get `
599
+ -Headers $commonHeaders `
600
+ -TimeoutSec 15
601
+ }
602
+ catch {
603
+ $claimCredential = $null
604
+ Write-AnswerMeEnrollmentFailure `
605
+ -Stage 'wait-verification' `
606
+ -Code 'enrollment-status-connection-unknown' `
607
+ -Message 'Creator enrollment status has an unknown outcome.' `
608
+ -ExitCode 4
609
+ }
610
+ $statusBody = ConvertFrom-AnswerMeEnrollmentResponse -Response $statusResponse
611
+ if ([int]$statusResponse.StatusCode -ne 200) {
612
+ $claimCredential = $null
613
+ Write-AnswerMeEnrollmentRemoteFailure `
614
+ -Stage 'wait-verification' `
615
+ -Endpoint 'status' `
616
+ -Response $statusResponse `
617
+ -Body $statusBody `
618
+ -FallbackCode 'enrollment-status-http-status'
619
+ }
620
+ if (-not (Test-AnswerMeEnrollmentNoStore -Response $statusResponse) -or
621
+ -not (Test-AnswerMeEnrollmentResponseFields -Body $statusBody -ExpectedFields @('status', 'requestId')) -or
622
+ -not (Test-AnswerMeEnrollmentJsonString -Body $statusBody -PropertyName 'status') -or
623
+ -not (Test-AnswerMeEnrollmentJsonString -Body $statusBody -PropertyName 'requestId') -or
624
+ [string]$statusBody.status -cnotin @('pending', 'verified', 'complete', 'failed', 'expired')) {
625
+ $claimCredential = $null
626
+ Write-AnswerMeEnrollmentFailure `
627
+ -Stage 'wait-verification' `
628
+ -Code 'enrollment-status-response-invalid' `
629
+ -Message 'Creator enrollment status omitted its no-store state.' `
630
+ -ExitCode 4
631
+ }
632
+ $remoteStatus = [string]$statusBody.status
633
+ if ($remoteStatus -ceq 'verified') {
634
+ $verified = $true
635
+ break
636
+ }
637
+ if ($remoteStatus -ceq 'pending') {
638
+ $remainingMilliseconds = [Math]::Max(0, [Math]::Floor(($deadline - [DateTimeOffset]::UtcNow).TotalMilliseconds))
639
+ $sleepMilliseconds = [int][Math]::Min($remainingMilliseconds, $PollSeconds * 1000)
640
+ if ($sleepMilliseconds -gt 0) { Start-Sleep -Milliseconds $sleepMilliseconds }
641
+ continue
642
+ }
643
+ $claimCredential = $null
644
+ Write-AnswerMeEnrollmentFailure `
645
+ -Stage 'wait-verification' `
646
+ -Code $(if ($remoteStatus -ceq 'expired') { 'enrollment_expired' } else { 'enrollment_conflict' }) `
647
+ -Message 'Creator enrollment reached a terminal state before private claim.' `
648
+ -ExitCode 4 `
649
+ -Reason "enrollment_$remoteStatus"
650
+ }
651
+ if (-not $verified) {
652
+ $claimCredential = $null
653
+ Write-AnswerMeEnrollmentFailure `
654
+ -Stage 'wait-verification' `
655
+ -Code 'enrollment-pending' `
656
+ -Message 'Creator enrollment remained pending until the bounded deadline.' `
657
+ -ExitCode 5 `
658
+ -Reason 'enrollment_pending'
659
+ }
660
+
661
+ $claimUri = ([Uri]::new($parsedBaseUrl, "/api/v1/creator-enrollments/$escapedEnrollmentId/claim")).AbsoluteUri
662
+ $claimHeaders = @{
663
+ Accept = 'application/json'
664
+ Authorization = "Bearer $claimCredential"
665
+ 'X-AnswerMe-Protocol-Version' = $AnswerMeProtocolVersion
666
+ 'X-AnswerMe-Skill-Version' = $AnswerMeSkillVersion
667
+ }
668
+ try {
669
+ $claimResponse = Invoke-AnswerMeWebRequest `
670
+ -Uri $claimUri `
671
+ -Method Post `
672
+ -Headers $claimHeaders `
673
+ -TimeoutSec 15
674
+ }
675
+ catch {
676
+ $claimCredential = $null
677
+ Write-AnswerMeEnrollmentFailure `
678
+ -Stage 'claim' `
679
+ -Code 'claim-outcome-unknown' `
680
+ -Message 'The private Creator credential claim has an unknown outcome and will not be retried.' `
681
+ -ExitCode 4
682
+ }
683
+ $claimCredential = $null
684
+ $claimHeaders.Authorization = $null
685
+ $claimBody = ConvertFrom-AnswerMeEnrollmentResponse -Response $claimResponse
686
+ if ([int]$claimResponse.StatusCode -ne 200) {
687
+ Write-AnswerMeEnrollmentRemoteFailure `
688
+ -Stage 'claim' `
689
+ -Endpoint 'claim' `
690
+ -Response $claimResponse `
691
+ -Body $claimBody `
692
+ -FallbackCode 'claim-http-status'
693
+ }
694
+ if (-not (Test-AnswerMeEnrollmentNoStore -Response $claimResponse) -or
695
+ -not (Test-AnswerMeEnrollmentResponseFields `
696
+ -Body $claimBody `
697
+ -ExpectedFields @('creatorId', 'creatorApiKey', 'requestId')) -or
698
+ -not (Test-AnswerMeEnrollmentJsonString -Body $claimBody -PropertyName 'creatorId') -or
699
+ -not (Test-AnswerMeEnrollmentJsonString -Body $claimBody -PropertyName 'creatorApiKey') -or
700
+ -not (Test-AnswerMeEnrollmentJsonString -Body $claimBody -PropertyName 'requestId')) {
701
+ Write-AnswerMeEnrollmentFailure `
702
+ -Stage 'claim' `
703
+ -Code 'claim-response-invalid' `
704
+ -Message 'The private claim response omitted a required no-store credential field.' `
705
+ -ExitCode 4
706
+ }
707
+
708
+ $creatorApiKey = [string]$claimBody.creatorApiKey
709
+ $secureCreatorKey = $null
710
+ try {
711
+ $secureCreatorKey = ConvertTo-SecureString $creatorApiKey -AsPlainText -Force
712
+ $claimBody.creatorApiKey = $null
713
+ $creatorApiKey = $null
714
+ $install = Invoke-AnswerMeEnrollmentCredentialInstall `
715
+ -CreatorKey $secureCreatorKey `
716
+ -ResolvedApiBaseUrl $parsedBaseUrl `
717
+ -CreatorId ([string]$claimBody.creatorId) `
718
+ -ResolvedCredentialPath $resolvedCredentialPath
719
+ }
720
+ catch {
721
+ $installException = $_.Exception
722
+ Write-AnswerMeEnrollmentFailure `
723
+ -Stage 'credential-install' `
724
+ -Code $(if ($installException.Data.Contains('AnswerMeCode')) { [string]$installException.Data['AnswerMeCode'] } else { 'credential-install-failed' }) `
725
+ -Message 'The claimed Creator credential was not confirmed in protected storage.' `
726
+ -ExitCode 5 `
727
+ -CredentialStoreStage $(if ($installException.Data.Contains('AnswerMeStage')) { [string]$installException.Data['AnswerMeStage'] } else { $null }) `
728
+ -TargetChanged $(if ($installException.Data.Contains('AnswerMeTargetChanged')) { [bool]$installException.Data['AnswerMeTargetChanged'] } else { $false }) `
729
+ -BackupCreated $(if ($installException.Data.Contains('AnswerMeBackupCreated')) { [bool]$installException.Data['AnswerMeBackupCreated'] } else { $false }) `
730
+ -RollbackStatus $(if ($installException.Data.Contains('AnswerMeRollbackStatus')) { [string]$installException.Data['AnswerMeRollbackStatus'] } else { $null })
731
+ }
732
+ finally {
733
+ $claimBody.creatorApiKey = $null
734
+ $creatorApiKey = $null
735
+ $secureCreatorKey = $null
736
+ }
737
+
738
+ if ($null -eq $install -or
739
+ -not (Test-AnswerMeEnrollmentResponseFields `
740
+ -Body $install `
741
+ -ExpectedFields @('ok', 'status', 'creatorId', 'targetChanged', 'backupCreated', 'rollbackStatus', 'networkCalled')) -or
742
+ $install.ok -ne $true -or
743
+ [string]$install.status -cnotin @('installed', 'no-op') -or
744
+ [string]$install.creatorId -cne [string]$claimBody.creatorId -or
745
+ $install.networkCalled -ne $false) {
746
+ Write-AnswerMeEnrollmentFailure `
747
+ -Stage 'credential-install' `
748
+ -Code 'credential-install-result-invalid' `
749
+ -Message 'The protected credential store did not confirm a valid install result.' `
750
+ -ExitCode 5
751
+ }
752
+
753
+ [PSCustomObject]@{
754
+ ok = $true
755
+ status = 'credential-configured'
756
+ stage = 'complete'
757
+ credentialState = [string]$install.status
758
+ targetChanged = $install.targetChanged -eq $true
759
+ backupCreated = $install.backupCreated -eq $true
760
+ rollbackStatus = [string]$install.rollbackStatus
761
+ verificationDelivery = $AnswerMeVerificationDelivery
762
+ networkCalled = $true
763
+ } | ConvertTo-Json -Depth 8
764
+ exit 0