@trillboards/edge-sdk 0.2.26 → 0.2.37

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.
@@ -462,6 +462,16 @@ function Invoke-Commit {
462
462
  $before = Get-TaskShape $TaskName
463
463
  Log "task shape before: $(Format-TaskShape $before)"
464
464
 
465
+ # The commit phase is only reached by an already-installed connector, so its
466
+ # scheduled task must exist and be visible to the caller. Windows can return
467
+ # no task here when an unelevated account cannot inspect a headless task owned
468
+ # by another user. Treating that ambiguous result as a fresh install skips the
469
+ # shape probe and can silently replace the headless task with a logon-only
470
+ # registration. Refuse before touching the live tree instead.
471
+ if ($null -eq $before) {
472
+ Fail 'rejected' 'the scheduled task is absent or inaccessible; refusing an update that could re-register it logon-only'
473
+ }
474
+
465
475
  # The predictive half. Only a machine that HAS a headless shape has one to
466
476
  # lose, so a logon-only install skips the probe entirely and updates freely.
467
477
  if ($null -ne $before -and $before.Headless) {
@@ -57,6 +57,12 @@
57
57
  signed (by Azure Trusted Signing) and are proved
58
58
  as-is, with the unsigned control derived by stripping
59
59
  the signature block from a copy.
60
+ .PARAMETER RetainSelfSignedFixturePath
61
+ In hermetic mode, retain the exact proven root until
62
+ a downstream proof cleans it and write only its
63
+ subject/thumbprint metadata to this path. The owning
64
+ harness emits this directly; callers must not scan
65
+ the certificate provider after the proof.
60
66
 
61
67
  .NOTES
62
68
  .cmd/.bat have no Authenticode SIP and cmd.exe has no execution policy, so a
@@ -71,7 +77,8 @@ param(
71
77
  [string]$CanaryPs1 = "",
72
78
  [string]$CanaryVbs = "",
73
79
  [switch]$SelfSign,
74
- [switch]$AlreadySigned
80
+ [switch]$AlreadySigned,
81
+ [string]$RetainSelfSignedFixturePath = ""
75
82
  )
76
83
 
77
84
  $ErrorActionPreference = 'Stop'
@@ -81,12 +88,17 @@ if (-not ($SelfSign -xor $AlreadySigned)) {
81
88
  Write-Error 'Choose exactly one of -SelfSign or -AlreadySigned.'
82
89
  exit 2
83
90
  }
91
+ if ($RetainSelfSignedFixturePath -and -not $SelfSign) {
92
+ Write-Error '-RetainSelfSignedFixturePath is valid only with -SelfSign.'
93
+ exit 2
94
+ }
84
95
 
85
96
  $PolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell'
86
97
  $WshKey = 'HKLM:\SOFTWARE\Microsoft\Windows Script Host\Settings'
87
98
  $Work = Join-Path ([IO.Path]::GetTempPath()) ("tb-allsigned-" + [Guid]::NewGuid().ToString('N').Substring(0, 8))
88
99
  $failures = New-Object System.Collections.Generic.List[string]
89
100
  $addedThumbprints = New-Object System.Collections.Generic.List[string]
101
+ $retainedRootThumbprint = $null
90
102
 
91
103
  function Say($m) { Write-Host "[allsigned] $m" }
92
104
  function Fail($m) { $script:failures.Add($m); Write-Host "[allsigned] FAIL: $m" -ForegroundColor Red }
@@ -288,6 +300,7 @@ try {
288
300
  # reason.
289
301
  Add-CertToStore -Cert $pub -StoreName 'Root'
290
302
  $addedThumbprints.Add('Root|' + $pub.Thumbprint)
303
+ if ($RetainSelfSignedFixturePath) { $retainedRootThumbprint = $pub.Thumbprint }
291
304
  foreach ($f in @(Get-ChildItem -LiteralPath $signedDir -File)) {
292
305
  $r = Set-AuthenticodeSignature -FilePath $f.FullName -Certificate $cert -HashAlgorithm SHA256
293
306
  if ($r.Status -ne 'Valid') { throw "self-signing produced $($r.Status) for $($f.Name)" }
@@ -589,6 +602,7 @@ try {
589
602
  elseif (Test-Path $WshKey) { Remove-ItemProperty -Path $WshKey -Name 'UseWINSAFER' -ErrorAction SilentlyContinue }
590
603
  foreach ($entry in $addedThumbprints) {
591
604
  $parts = $entry.Split('|')
605
+ if ($retainedRootThumbprint -and $parts[0] -eq 'Root' -and $parts[1] -eq $retainedRootThumbprint) { continue }
592
606
  Remove-CertFromStore -Thumbprint $parts[1] -StoreName $parts[0]
593
607
  }
594
608
  } catch {
@@ -604,5 +618,14 @@ if ($failures.Count -gt 0) {
604
618
  exit 1
605
619
  }
606
620
 
621
+ if ($RetainSelfSignedFixturePath) {
622
+ if (-not $cert -or -not $cert.HasPrivateKey -or $cert.Thumbprint -ne $retainedRootThumbprint) {
623
+ Write-Error 'the retained self-signed fixture is not the exact proven certificate'
624
+ exit 1
625
+ }
626
+ @([pscustomobject]@{ subject = $cert.Subject; thumbprint = $cert.Thumbprint }) |
627
+ ConvertTo-Json -Depth 3 | Set-Content -LiteralPath $RetainSelfSignedFixturePath -Encoding UTF8
628
+ }
629
+
607
630
  Say 'PASSED - unsigned is blocked, untrusted-publisher is blocked, tampered is blocked, and every shipped script is admitted.'
608
631
  exit 0
@@ -51,7 +51,25 @@
51
51
  [CmdletBinding()]
52
52
  param(
53
53
  # Arm K needs to create a local account. Off-switch for a host that forbids it.
54
- [switch]$SkipUnelevatedArm
54
+ [switch]$SkipUnelevatedArm,
55
+ # The production proof owns eight minutes. Seconds are injectable only so the
56
+ # Windows deadline probe can prove a blocked certificate child is killed.
57
+ [ValidateRange(1, 480)]
58
+ [int]$DeadlineSeconds = 480,
59
+ [ValidateRange(0, 60)]
60
+ [int]$CertificateMintDelaySeconds = 0,
61
+ [switch]$CertificateDeadlineProbe,
62
+ # Forces Process.WaitForExit to throw on an unstarted process. CI uses this
63
+ # to prove the wait observer fails closed rather than calling a thrown wait a
64
+ # successful exit and falling into a later unbounded wait.
65
+ [switch]$BoundedWaitFailureProbe,
66
+ # The full CI proof receives the one real certificate already minted, signed,
67
+ # and proven by the preceding AllSigned harness. `powershell.exe` children
68
+ # repeatedly wedged while minting on windows-latest even though the same
69
+ # cmdlet succeeds in the workflow host. Keep the killable child below only for
70
+ # the deliberate deadline mutant; do not use that unreliable process shape
71
+ # as the happy-path fixture factory.
72
+ [string]$CertificateFixturePath
55
73
  )
56
74
 
57
75
  $ErrorActionPreference = 'Stop'
@@ -65,6 +83,7 @@ $Root = Join-Path $env:RUNNER_TEMP 'tb-update-applier'
65
83
  if (-not $env:RUNNER_TEMP) { $Root = Join-Path $env:TEMP 'tb-update-applier' }
66
84
  $Www = Join-Path $Root 'www'
67
85
  $Certs = New-Object System.Collections.Generic.List[string]
86
+ $CertSubjects = New-Object System.Collections.Generic.List[string]
68
87
  $LocalUser = 'tbupd'
69
88
  $Failures = 0
70
89
  $StartedAt = Get-Date
@@ -81,7 +100,7 @@ $StartedAt = Get-Date
81
100
  time, because the reason that first failure was undiagnosable was that there
82
101
  was no output between "origin listening" and the first arm.
83
102
  #>
84
- $DeadlineMinutes = 8
103
+ $script:DeadlineSeconds = $DeadlineSeconds
85
104
 
86
105
  function Elapsed { ((Get-Date) - $script:StartedAt).TotalSeconds }
87
106
  function Say([string]$m) { Write-Host ("[update-applier {0,6:N1}s] {1}" -f (Elapsed), $m) }
@@ -89,7 +108,7 @@ function Say([string]$m) { Write-Host ("[update-applier {0,6:N1}s] {1}" -f (Elap
89
108
  $script:CurrentPhase = 'startup'
90
109
 
91
110
  function Fail-Deadline([string]$Phase, [string]$When) {
92
- Write-Host "FAIL - the harness exceeded its $($script:DeadlineMinutes)-minute deadline $When`: $Phase"
111
+ Write-Host "FAIL - the harness exceeded its $($script:DeadlineSeconds)-second deadline $When`: $Phase"
93
112
  Write-Host ' HARNESS BUDGET failure, not a product failure. The arms above ran;'
94
113
  Write-Host ' the ones below never started. Read the per-phase timings to find'
95
114
  Write-Host ' what got slow before assuming a defect in apply-update.ps1.'
@@ -99,12 +118,12 @@ function Fail-Deadline([string]$Phase, [string]$When) {
99
118
 
100
119
  function Assert-WithinDeadline([string]$Phase) {
101
120
  $script:CurrentPhase = $Phase
102
- if (((Get-Date) - $script:StartedAt).TotalMinutes -lt $script:DeadlineMinutes) { return }
121
+ if ((Elapsed) -lt $script:DeadlineSeconds) { return }
103
122
  Fail-Deadline $Phase 'before starting'
104
123
  }
105
124
 
106
125
  function Remaining-DeadlineSeconds {
107
- [Math]::Max(0, ($script:DeadlineMinutes * 60) - (Elapsed))
126
+ [Math]::Max(0, $script:DeadlineSeconds - (Elapsed))
108
127
  }
109
128
 
110
129
  <#
@@ -132,14 +151,25 @@ function Wait-Bounded {
132
151
  try { Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue } catch {}
133
152
  Fail-Deadline $Phase 'before starting'
134
153
  }
154
+ $timeoutMilliseconds = [int]([Math]::Min(2147483, $budget) * 1000)
135
155
  $exited = $false
136
- try { $exited = $Process.WaitForExit([int]([Math]::Min(2147483, $budget) * 1000)) } catch { $exited = $true }
156
+ try {
157
+ $exited = $Process.WaitForExit($timeoutMilliseconds)
158
+ } catch {
159
+ $waitException = $_.Exception
160
+ if ($null -ne $waitException.InnerException) { $waitException = $waitException.InnerException }
161
+ $waitErrorType = $waitException.GetType().FullName
162
+ $waitErrorMessage = $waitException.Message
163
+ try { Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue } catch {}
164
+ try { $Process.WaitForExit(5000) | Out-Null } catch {}
165
+ try { $Process.Close() } catch {}
166
+ Die "$Phase bounded process wait failed ($waitErrorType): $waitErrorMessage"
167
+ }
137
168
  if (-not $exited) {
138
169
  try { Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue } catch {}
139
170
  try { $Process.WaitForExit(5000) | Out-Null } catch {}
140
171
  Fail-Deadline $Phase 'while running'
141
172
  }
142
- try { $Process.Refresh() } catch {}
143
173
  return $Process
144
174
  }
145
175
  function Pass([string]$m) { Write-Host "PASS - $m" }
@@ -199,17 +229,111 @@ function Stop-Origin {
199
229
  # Certificates
200
230
  # ---------------------------------------------------------------------------
201
231
  function New-HarnessCert([string]$Subject) {
202
- $cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject "CN=$Subject" `
203
- -CertStoreLocation 'Cert:\CurrentUser\My' -NotAfter (Get-Date).AddDays(2)
232
+ # New-SelfSignedCertificate blocked for longer than the GitHub step budget on
233
+ # 2026-08-15. A synchronous cmdlet cannot be interrupted by Wait-Bounded, so
234
+ # mint in a disposable child process. The child receives the SAME remaining
235
+ # whole-harness budget as every applier invocation and is forcibly killed on
236
+ # expiry; a phase-specific failure is emitted before the job timeout can turn
237
+ # the result into an unattributable cancellation.
238
+ $phase = "minting code-signing certificate '$Subject'"
239
+ $tag = [guid]::NewGuid().ToString('N')
240
+ $thumbprintPath = Join-Path $Root "cert-$tag.thumbprint"
241
+ $stderrPath = Join-Path $Root "cert-$tag.stderr"
242
+ $subjectLiteral = $Subject.Replace("'", "''")
243
+ $thumbprintLiteral = $thumbprintPath.Replace("'", "''")
244
+ $stderrLiteral = $stderrPath.Replace("'", "''")
245
+ $script:CertSubjects.Add("CN=$Subject")
246
+ $mintSource = @"
247
+ `$ErrorActionPreference = 'Stop'
248
+ `$ProgressPreference = 'SilentlyContinue'
249
+ try {
250
+ if ($CertificateMintDelaySeconds -gt 0) { Start-Sleep -Seconds $CertificateMintDelaySeconds }
251
+ `$cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject 'CN=$subjectLiteral' -CertStoreLocation 'Cert:\CurrentUser\My' -NotAfter (Get-Date).AddDays(2)
252
+ [System.IO.File]::WriteAllText('$thumbprintLiteral', `$cert.Thumbprint)
253
+ } catch {
254
+ [System.IO.File]::WriteAllText('$stderrLiteral', (`$_ | Out-String))
255
+ exit 1
256
+ }
257
+ "@
258
+ $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($mintSource))
259
+ $powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
260
+ $startInfo = [Diagnostics.ProcessStartInfo]::new()
261
+ $startInfo.FileName = $powershell
262
+ $startInfo.Arguments = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded"
263
+ $startInfo.UseShellExecute = $false
264
+ $startInfo.CreateNoWindow = $true
265
+ $p = [Diagnostics.Process]::new()
266
+ $p.StartInfo = $startInfo
267
+ $null = $p.Start()
268
+ $null = Wait-Bounded -Process $p -Phase $phase
269
+ $exitCode = $p.ExitCode
270
+ $p.Close()
271
+
272
+ $childError = ''
273
+ if (Test-Path -LiteralPath $stderrPath) { $childError = (Get-Content -LiteralPath $stderrPath -Raw).Trim() }
274
+ if ($exitCode -ne 0) { Die "$phase failed (exit $exitCode): $childError" }
275
+ if (-not (Test-Path -LiteralPath $thumbprintPath)) { Die "$phase returned no thumbprint" }
276
+ $thumbprint = (Get-Content -LiteralPath $thumbprintPath -Raw).Trim().ToUpperInvariant()
277
+ if ($thumbprint -notmatch '^[0-9A-F]{40,128}$') { Die "$phase returned malformed thumbprint '$thumbprint'" }
278
+ $script:Certs.Add($thumbprint)
279
+ $cert = Get-Item -LiteralPath "Cert:\CurrentUser\My\$thumbprint" -ErrorAction Stop
280
+ if ((-not $cert.HasPrivateKey) -or ($cert.Subject -ne "CN=$Subject")) {
281
+ Die "$phase returned a certificate without the expected subject and private key"
282
+ }
283
+ Remove-Item -LiteralPath $thumbprintPath, $stderrPath -Force -ErrorAction SilentlyContinue
204
284
  # Into Root, or the chain does not build and Status is UnknownError - which
205
285
  # would make arm E fire for the wrong reason entirely.
206
286
  $pub = New-Object Security.Cryptography.X509Certificates.X509Certificate2(, $cert.RawData)
207
287
  $store = New-Object Security.Cryptography.X509Certificates.X509Store('Root', 'CurrentUser')
208
288
  $store.Open('ReadWrite'); $store.Add($pub); $store.Close()
209
- $Certs.Add($cert.Thumbprint)
210
289
  return $cert
211
290
  }
212
291
 
292
+ function Read-HarnessCertificateFixtures([string]$Path) {
293
+ if (-not $Path) { Die 'CertificateFixturePath is required for the full harness' }
294
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
295
+ Die "CertificateFixturePath does not exist: $Path"
296
+ }
297
+ try {
298
+ $rows = @(Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json)
299
+ } catch {
300
+ Die "CertificateFixturePath is not valid JSON: $($_.Exception.Message)"
301
+ }
302
+ if ($rows.Count -ne 1) { Die "CertificateFixturePath must contain exactly one certificate (got $($rows.Count))" }
303
+
304
+ $expectedSubjects = @('CN=Trillboards CI AllSigned Harness')
305
+ $bySubject = @{}
306
+ foreach ($row in $rows) {
307
+ $keys = @($row.PSObject.Properties.Name | Sort-Object)
308
+ if (($keys.Count -ne 2) -or (($keys -join ',') -ne 'subject,thumbprint')) {
309
+ Die 'CertificateFixturePath rows must contain exactly subject and thumbprint'
310
+ }
311
+ $subject = "$($row.subject)"
312
+ $thumbprint = "$($row.thumbprint)".Trim().ToUpperInvariant()
313
+ if ($expectedSubjects -notcontains $subject) { Die "CertificateFixturePath contains unexpected subject '$subject'" }
314
+ if ($bySubject.ContainsKey($subject)) { Die "CertificateFixturePath repeats subject '$subject'" }
315
+ if ($thumbprint -notmatch '^[0-9A-F]{40,128}$') { Die "CertificateFixturePath contains malformed thumbprint '$thumbprint'" }
316
+
317
+ $cert = Get-Item -LiteralPath "Cert:\CurrentUser\My\$thumbprint" -ErrorAction SilentlyContinue
318
+ if ((-not $cert) -or (-not $cert.HasPrivateKey) -or ($cert.Subject -ne $subject)) {
319
+ Die "CertificateFixturePath certificate '$subject' is absent or lacks its expected private key"
320
+ }
321
+ $root = Get-Item -LiteralPath "Cert:\LocalMachine\Root\$thumbprint" -ErrorAction SilentlyContinue
322
+ if ((-not $root) -or ($root.Subject -ne $subject) -or ($root.Thumbprint -ne $cert.Thumbprint)) {
323
+ Die "CertificateFixturePath certificate '$subject' is not bound to the matching trusted root"
324
+ }
325
+ $script:Certs.Add($thumbprint)
326
+ $script:CertSubjects.Add($subject)
327
+ $bySubject[$subject] = $cert
328
+ }
329
+ foreach ($subject in $expectedSubjects) {
330
+ if (-not $bySubject.ContainsKey($subject)) { Die "CertificateFixturePath is missing subject '$subject'" }
331
+ }
332
+ return [pscustomobject]@{
333
+ Good = $bySubject['CN=Trillboards CI AllSigned Harness']
334
+ }
335
+ }
336
+
213
337
  function Remove-HarnessCerts {
214
338
  foreach ($tp in $Certs) {
215
339
  foreach ($name in @('My', 'Root')) {
@@ -221,6 +345,18 @@ function Remove-HarnessCerts {
221
345
  } catch {}
222
346
  }
223
347
  }
348
+ # If the child was killed after committing the store write but before writing
349
+ # its thumbprint marker, the parent never saw the thumbprint. Exact harness
350
+ # subjects are therefore a second cleanup key; they are intentionally unique
351
+ # to this ephemeral CI proof.
352
+ foreach ($name in @('My', 'Root')) {
353
+ try {
354
+ $store = New-Object Security.Cryptography.X509Certificates.X509Store($name, 'CurrentUser')
355
+ $store.Open('ReadWrite')
356
+ foreach ($c in @($store.Certificates | Where-Object { $script:CertSubjects.Contains($_.Subject) })) { $store.Remove($c) }
357
+ $store.Close()
358
+ } catch {}
359
+ }
224
360
  }
225
361
 
226
362
  function Sign-File([string]$Path, $Cert) {
@@ -361,11 +497,49 @@ function New-Catalog([string]$Path, [string]$Version, [string]$Sha, [long]$Size)
361
497
  plus whether staged.json exists, which together are the only observable the
362
498
  connector ever acts on.
363
499
  #>
364
- function Invoke-Applier([string]$InstallDir, [string[]]$Args) {
500
+ function Invoke-Applier([string]$InstallDir, [System.Collections.IDictionary]$ApplierParameters) {
365
501
  $ps = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
366
- $all = @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $Applier, '-InstallDir', $InstallDir) + $Args
367
- $p = Start-Process -FilePath $ps -ArgumentList $all -NoNewWindow -PassThru
502
+ # Start-Process flattens -ArgumentList into one command-line string without
503
+ # preserving element boundaries. A value such as the real signer
504
+ # "Trillboards CI AllSigned Harness" therefore arrives at apply-update.ps1
505
+ # as four positional arguments. An array splat is also positional, so it
506
+ # cannot reconstruct names such as -ExpectedVersion after deserialization.
507
+ # Carry a named parameter map as JSON inside an encoded child program instead:
508
+ # the only native-process argument is base64, and the child rebuilds a
509
+ # hashtable that PowerShell binds by parameter name without reparsing values.
510
+ $namedParameters = [ordered]@{ InstallDir = [string]$InstallDir }
511
+ foreach ($name in $ApplierParameters.Keys) {
512
+ if ([string]$name -eq 'InstallDir') { Die 'Invoke-Applier parameters must not override InstallDir' }
513
+ $namedParameters[[string]$name] = $ApplierParameters[$name]
514
+ }
515
+ $invocationJson = [ordered]@{
516
+ applier = [string]$Applier
517
+ parameters = $namedParameters
518
+ } | ConvertTo-Json -Compress
519
+ $invocationBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($invocationJson))
520
+ $childSource = @"
521
+ `$ErrorActionPreference = 'Stop'
522
+ `$decodedInvocation = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('$invocationBase64'))
523
+ `$invocation = `$decodedInvocation | ConvertFrom-Json
524
+ `$childApplier = [string]`$invocation.applier
525
+ `$childParameters = @{}
526
+ foreach (`$property in @(`$invocation.parameters.PSObject.Properties)) {
527
+ `$childParameters[`$property.Name] = `$property.Value
528
+ }
529
+ & `$childApplier @childParameters
530
+ exit `$LASTEXITCODE
531
+ "@
532
+ $encodedChild = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($childSource))
533
+ $startInfo = [Diagnostics.ProcessStartInfo]::new()
534
+ $startInfo.FileName = $ps
535
+ $startInfo.Arguments = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedChild"
536
+ $startInfo.UseShellExecute = $false
537
+ $p = [Diagnostics.Process]::new()
538
+ $p.StartInfo = $startInfo
539
+ $null = $p.Start()
368
540
  $null = Wait-Bounded -Process $p
541
+ $exitCode = $p.ExitCode
542
+ $p.Close()
369
543
  $updateDir = Join-Path $InstallDir '.auto-update'
370
544
  $outcome = $null
371
545
  $outcomePath = Join-Path $updateDir 'outcome.json'
@@ -382,7 +556,7 @@ function Invoke-Applier([string]$InstallDir, [string[]]$Args) {
382
556
  if ($null -ne $outcome.detail) { $detail = [string]$outcome.detail }
383
557
  }
384
558
  return [pscustomobject]@{
385
- ExitCode = $p.ExitCode
559
+ ExitCode = $exitCode
386
560
  Outcome = $outcome
387
561
  Result = $result
388
562
  Detail = $detail
@@ -442,15 +616,31 @@ try {
442
616
  if (-not (Test-Path -LiteralPath $Applier)) { Die "apply-update.ps1 not found at $Applier" }
443
617
  if (Test-Path -LiteralPath $Root) { Remove-Item -LiteralPath $Root -Recurse -Force }
444
618
  New-Item -ItemType Directory -Force -Path $Root, $Www | Out-Null
445
- Start-Origin
446
-
447
- Say 'minting two throwaway code-signing certificates'
448
- $goodCert = New-HarnessCert 'Trillboards CI Update Harness'
449
- $otherCert = New-HarnessCert 'Definitely Not Trillboards'
450
- $signer = 'Trillboards CI Update Harness'
619
+ if (($CertificateMintDelaySeconds -gt 0) -and (-not $CertificateDeadlineProbe)) {
620
+ Die 'CertificateMintDelaySeconds is a deadline-probe input, not a full-harness option'
621
+ }
622
+ if ($BoundedWaitFailureProbe) {
623
+ $unstarted = [Diagnostics.Process]::new()
624
+ $null = Wait-Bounded -Process $unstarted -Phase 'bounded wait observer failure probe'
625
+ Die 'bounded wait observer failure probe was reclassified as a process exit'
626
+ }
627
+ if ($CertificateDeadlineProbe) {
628
+ if ($CertificateMintDelaySeconds -le $DeadlineSeconds) {
629
+ Die 'certificate deadline probe requires a mint delay longer than its deadline'
630
+ }
631
+ Say 'deadline probe: starting a deliberately blocked certificate mint'
632
+ $null = New-HarnessCert 'Trillboards CI Deadline Probe'
633
+ Die 'certificate deadline probe was not stopped by the harness deadline'
634
+ }
635
+ Say 'loading the real certificate retained from the AllSigned proof'
636
+ $certificateFixtures = Read-HarnessCertificateFixtures $CertificateFixturePath
637
+ $goodCert = $certificateFixtures.Good
638
+ $signer = 'Trillboards CI AllSigned Harness'
451
639
  $version = '9.9.9'
452
640
  Say 'certificates ready'
453
641
 
642
+ Start-Origin
643
+
454
644
  $payloadTree = Join-Path $Root 'payload-src'
455
645
  New-PayloadTree -Dest $payloadTree -SignCert $goodCert
456
646
  $good = Publish-Payload $payloadTree 'payload.zip'
@@ -468,8 +658,10 @@ try {
468
658
  Assert-WithinDeadline 'arm A: happy path (the shared control)'
469
659
  $installA = Join-Path $Root 'install-a'
470
660
  New-InstallTree $installA
471
- $a = Invoke-Applier $installA @('-Stage', '-UpdateUrl', $urlZip, '-CatalogUrl', "$BaseUrl/catalog-good.ps1",
472
- '-ExpectedVersion', $version, '-CurrentVersion', '9.9.8', '-ExpectedSigner', $signer)
661
+ $a = Invoke-Applier $installA ([ordered]@{
662
+ Stage = $true; UpdateUrl = $urlZip; CatalogUrl = "$BaseUrl/catalog-good.ps1"
663
+ ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
664
+ })
473
665
  if (-not $a.Staged) {
474
666
  Die "arm A did not stage (exit $($a.ExitCode), result '$($a.Result)', detail '$($a.Detail)') - every refusal below would be VACUOUS"
475
667
  }
@@ -487,8 +679,10 @@ try {
487
679
  [System.IO.File]::WriteAllBytes($tampered, $bytes)
488
680
  $installB = Join-Path $Root 'install-b'
489
681
  New-InstallTree $installB
490
- $b = Invoke-Applier $installB @('-Stage', '-UpdateUrl', "$BaseUrl/payload-tampered.zip", '-CatalogUrl', "$BaseUrl/catalog-good.ps1",
491
- '-ExpectedVersion', $version, '-CurrentVersion', '9.9.8', '-ExpectedSigner', $signer)
682
+ $b = Invoke-Applier $installB ([ordered]@{
683
+ Stage = $true; UpdateUrl = "$BaseUrl/payload-tampered.zip"; CatalogUrl = "$BaseUrl/catalog-good.ps1"
684
+ ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
685
+ })
492
686
  if ($b.Staged) { Bad 'arm B staged a payload whose SHA-256 did not match the signed catalog' }
493
687
  elseif ($b.Result -ne 'rejected') { Bad "arm B recorded '$($b.Result)', expected 'rejected'" }
494
688
  elseif ($b.Detail -notmatch 'SHA-256 mismatch') { Bad "arm B rejected for the wrong reason: $($b.Detail)" }
@@ -508,8 +702,10 @@ try {
508
702
  [System.IO.File]::WriteAllBytes($short, $half)
509
703
  $installC = Join-Path $Root 'install-c'
510
704
  New-InstallTree $installC
511
- $c = Invoke-Applier $installC @('-Stage', '-UpdateUrl', "$BaseUrl/payload-short.zip", '-CatalogUrl', "$BaseUrl/catalog-good.ps1",
512
- '-ExpectedVersion', $version, '-CurrentVersion', '9.9.8', '-ExpectedSigner', $signer)
705
+ $c = Invoke-Applier $installC ([ordered]@{
706
+ Stage = $true; UpdateUrl = "$BaseUrl/payload-short.zip"; CatalogUrl = "$BaseUrl/catalog-good.ps1"
707
+ ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
708
+ })
513
709
  if ($c.Staged) { Bad 'arm C staged a truncated payload' }
514
710
  elseif ($c.Detail -notmatch 'size mismatch') { Bad "arm C rejected for the wrong reason: $($c.Detail)" }
515
711
  elseif ($c.Archive) { Bad 'arm C left the truncated archive on disk' }
@@ -525,8 +721,10 @@ try {
525
721
  }
526
722
  $installD = Join-Path $Root 'install-d'
527
723
  New-InstallTree $installD
528
- $d = Invoke-Applier $installD @('-Stage', '-UpdateUrl', $urlZip, '-CatalogUrl', "$BaseUrl/catalog-unsigned.ps1",
529
- '-ExpectedVersion', $version, '-CurrentVersion', '9.9.8', '-ExpectedSigner', $signer)
724
+ $d = Invoke-Applier $installD ([ordered]@{
725
+ Stage = $true; UpdateUrl = $urlZip; CatalogUrl = "$BaseUrl/catalog-unsigned.ps1"
726
+ ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
727
+ })
530
728
  if ($d.Staged) { Bad 'arm D staged against an unsigned catalog' }
531
729
  elseif ($d.Detail -notmatch 'not Valid') { Bad "arm D rejected for the wrong reason: $($d.Detail)" }
532
730
  else { Pass 'an unsigned catalog is rejected' }
@@ -536,15 +734,17 @@ try {
536
734
  Assert-WithinDeadline 'arm E: the catalog is VALIDLY signed, by somebody else'
537
735
  $catalogOther = Join-Path $Www 'catalog-other.ps1'
538
736
  New-Catalog $catalogOther $version $good.Sha $good.Bytes
539
- Sign-File $catalogOther $otherCert
737
+ Sign-File $catalogOther $goodCert
540
738
  $otherStatus = (Get-AuthenticodeSignature -LiteralPath $catalogOther).Status
541
739
  if ($otherStatus -ne 'Valid') {
542
740
  Die "arm E is VACUOUS: the other publisher's signature is '$otherStatus', not Valid - a status check alone would reject it and prove nothing about the subject check"
543
741
  }
544
742
  $installE = Join-Path $Root 'install-e'
545
743
  New-InstallTree $installE
546
- $e = Invoke-Applier $installE @('-Stage', '-UpdateUrl', $urlZip, '-CatalogUrl', "$BaseUrl/catalog-other.ps1",
547
- '-ExpectedVersion', $version, '-CurrentVersion', '9.9.8', '-ExpectedSigner', $signer)
744
+ $e = Invoke-Applier $installE ([ordered]@{
745
+ Stage = $true; UpdateUrl = $urlZip; CatalogUrl = "$BaseUrl/catalog-other.ps1"
746
+ ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = 'Definitely Not Trillboards'
747
+ })
548
748
  if ($e.Staged) { Bad 'arm E staged a catalog signed by an unexpected publisher' }
549
749
  elseif ($e.Detail -notmatch 'unexpected publisher') { Bad "arm E rejected for the wrong reason: $($e.Detail)" }
550
750
  else { Pass 'a VALID signature from the wrong publisher is rejected on the subject' }
@@ -559,8 +759,10 @@ try {
559
759
  if ($editedStatus -eq 'Valid') { Die 'arm F is VACUOUS: the edited catalog still reports Valid' }
560
760
  $installF = Join-Path $Root 'install-f'
561
761
  New-InstallTree $installF
562
- $f = Invoke-Applier $installF @('-Stage', '-UpdateUrl', $urlZip, '-CatalogUrl', "$BaseUrl/catalog-edited.ps1",
563
- '-ExpectedVersion', $version, '-CurrentVersion', '9.9.8', '-ExpectedSigner', $signer)
762
+ $f = Invoke-Applier $installF ([ordered]@{
763
+ Stage = $true; UpdateUrl = $urlZip; CatalogUrl = "$BaseUrl/catalog-edited.ps1"
764
+ ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
765
+ })
564
766
  if ($f.Staged) { Bad 'arm F staged a catalog that was edited after signing' }
565
767
  else { Pass "a catalog edited after signing is rejected ($editedStatus)" }
566
768
 
@@ -572,8 +774,10 @@ try {
572
774
  Sign-File $huge $goodCert
573
775
  $installG = Join-Path $Root 'install-g'
574
776
  New-InstallTree $installG
575
- $g = Invoke-Applier $installG @('-Stage', '-UpdateUrl', $urlZip, '-CatalogUrl', "$BaseUrl/catalog-huge.ps1",
576
- '-ExpectedVersion', $version, '-CurrentVersion', '9.9.8', '-ExpectedSigner', $signer)
777
+ $g = Invoke-Applier $installG ([ordered]@{
778
+ Stage = $true; UpdateUrl = $urlZip; CatalogUrl = "$BaseUrl/catalog-huge.ps1"
779
+ ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
780
+ })
577
781
  if ($g.Staged) { Bad 'arm G staged a payload that cannot fit' }
578
782
  elseif ($g.Detail -notmatch 'free disk space') { Bad "arm G rejected for the wrong reason: $($g.Detail)" }
579
783
  elseif ($g.Archive) { Bad 'arm G downloaded before checking that the payload would fit' }
@@ -590,8 +794,10 @@ try {
590
794
  Sign-File $catalogRogue $goodCert
591
795
  $installH = Join-Path $Root 'install-h'
592
796
  New-InstallTree $installH
593
- $h = Invoke-Applier $installH @('-Stage', '-UpdateUrl', "$BaseUrl/payload-rogue.zip", '-CatalogUrl', "$BaseUrl/catalog-rogue.ps1",
594
- '-ExpectedVersion', $version, '-CurrentVersion', '9.9.8', '-ExpectedSigner', $signer)
797
+ $h = Invoke-Applier $installH ([ordered]@{
798
+ Stage = $true; UpdateUrl = "$BaseUrl/payload-rogue.zip"; CatalogUrl = "$BaseUrl/catalog-rogue.ps1"
799
+ ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
800
+ })
595
801
  if ($h.Staged) { Bad 'arm H staged a payload containing an unsigned script' }
596
802
  elseif ($h.Detail -notmatch 'unsigned or invalid') { Bad "arm H rejected for the wrong reason: $($h.Detail)" }
597
803
  else { Pass 'a payload carrying an unsigned script is rejected' }
@@ -613,9 +819,11 @@ try {
613
819
  New-Catalog $catDown $version $down.Sha $down.Bytes
614
820
  Sign-File $catDown $goodCert
615
821
 
616
- $staged = Invoke-Applier $installJ @('-Stage', '-UpdateUrl', "$BaseUrl/payload-down-$($arm.Name).zip",
617
- '-CatalogUrl', "$BaseUrl/catalog-down-$($arm.Name).ps1", '-ExpectedVersion', $version,
618
- '-CurrentVersion', '9.9.8', '-ExpectedSigner', $signer)
822
+ $staged = Invoke-Applier $installJ ([ordered]@{
823
+ Stage = $true; UpdateUrl = "$BaseUrl/payload-down-$($arm.Name).zip"
824
+ CatalogUrl = "$BaseUrl/catalog-down-$($arm.Name).ps1"; ExpectedVersion = $version
825
+ CurrentVersion = '9.9.8'; ExpectedSigner = $signer
826
+ })
619
827
  if (-not $staged.Staged) { Die "arm $($arm.Name) could not stage: $($staged.Detail)" }
620
828
 
621
829
  Register-HeadlessTask $installJ
@@ -633,9 +841,9 @@ try {
633
841
  Die "arm $($arm.Name) is VACUOUS: node.exe was not locked while the connector was running, so a successful swap proves nothing"
634
842
  }
635
843
 
636
- $args = @('-Commit', '-ExpectedVersion', $version, '-CurrentVersion', '9.9.8')
637
- if ($arm.Skip) { $args += '-SkipTaskShapeRestore' }
638
- $j = Invoke-Applier $installJ $args
844
+ $commitParameters = [ordered]@{ Commit = $true; ExpectedVersion = $version; CurrentVersion = '9.9.8' }
845
+ if ($arm.Skip) { $commitParameters['SkipTaskShapeRestore'] = $true }
846
+ $j = Invoke-Applier $installJ $commitParameters
639
847
  try { Stop-Process -Id $running.Id -Force -ErrorAction SilentlyContinue } catch {}
640
848
 
641
849
  $after = Get-Shape $TaskName
@@ -670,8 +878,10 @@ try {
670
878
  $catK = Join-Path $Www 'catalog-k.ps1'
671
879
  New-Catalog $catK $version $down.Sha $down.Bytes
672
880
  Sign-File $catK $goodCert
673
- $stagedK = Invoke-Applier $installK @('-Stage', '-UpdateUrl', "$BaseUrl/payload-k.zip", '-CatalogUrl', "$BaseUrl/catalog-k.ps1",
674
- '-ExpectedVersion', $version, '-CurrentVersion', '9.9.8', '-ExpectedSigner', $signer)
881
+ $stagedK = Invoke-Applier $installK ([ordered]@{
882
+ Stage = $true; UpdateUrl = "$BaseUrl/payload-k.zip"; CatalogUrl = "$BaseUrl/catalog-k.ps1"
883
+ ExpectedVersion = $version; CurrentVersion = '9.9.8'; ExpectedSigner = $signer
884
+ })
675
885
  if (-not $stagedK.Staged) { Die "arm K could not stage: $($stagedK.Detail)" }
676
886
  Register-HeadlessTask $installK
677
887
  $beforeK = Get-Shape $TaskName
package/dist/EdgeAgent.js CHANGED
@@ -44,7 +44,7 @@ const edge_ads_1 = require("@trillboards/edge-ads");
44
44
  const CommandRouter_1 = require("./CommandRouter");
45
45
  const config_1 = require("./config");
46
46
  const dataDir_1 = require("./dataDir");
47
- const SDK_VERSION = '0.2.26';
47
+ const SDK_VERSION = '0.2.37';
48
48
  class EdgeAgent extends events_1.EventEmitter {
49
49
  constructor(config, deps) {
50
50
  super();
@@ -31,7 +31,7 @@ export interface DiagnosticReport {
31
31
  skipped: number;
32
32
  };
33
33
  }
34
- declare const SDK_VERSION = "0.2.26";
34
+ declare const SDK_VERSION = "0.2.37";
35
35
  declare function checkNodeVersion(): CheckResult;
36
36
  declare function checkPlatform(): CheckResult;
37
37
  declare function checkMemory(): CheckResult;
package/dist/diagnose.js CHANGED
@@ -63,7 +63,7 @@ const localNetworkAccess_1 = require("./localNetworkAccess");
63
63
  // ---------------------------------------------------------------------------
64
64
  // SDK version — mirrors package.json; kept in sync by CI
65
65
  // ---------------------------------------------------------------------------
66
- const SDK_VERSION = '0.2.26';
66
+ const SDK_VERSION = '0.2.37';
67
67
  exports._SDK_VERSION = SDK_VERSION;
68
68
  // ---------------------------------------------------------------------------
69
69
  // ANSI helpers (no external deps)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trillboards/edge-sdk",
3
- "version": "0.2.26",
3
+ "version": "0.2.37",
4
4
  "description": "Composable cross-platform Edge AI SDK for audience sensing on DOOH/CTV devices",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",