@kutrion/cli 1.2.9 → 1.2.11

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 (2) hide show
  1. package/dist/index.js +972 -733
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -42,8 +42,10 @@ var init_define_KT_HUB_FILES = __esm({
42
42
  # ensure-hub.sh compare A B | verify-file FILE SHA512_B64
43
43
  #
44
44
  # Exit: 0 present, installed or skipped by policy; 10 refused; 11 cannot install here; 12 fetch failed.
45
+ # Every run that gets past argument parsing ends with one "ensure-hub: finished exit=N" line, so a caller can wait on it.
45
46
  # Environment: KT_NO_HUB=1 skips; KT_HUB_FEED_BASE (https or file) replaces the feed;
46
- # KT_HUB_ALLOW_UNSIGNED=1 accepts a build with no publisher identity; KT_HUB_APPS_DIR replaces /Applications.
47
+ # KT_HUB_ALLOW_UNSIGNED=1 accepts a build with no publisher identity; KT_HUB_APPS_DIR replaces /Applications;
48
+ # KT_HUB_FETCH_BUDGET seconds bounds one download across its resumed attempts (default 1200).
47
49
  set -u
48
50
 
49
51
  SELF_DIR=$(cd "$(dirname "$0")" && pwd)
@@ -51,7 +53,7 @@ CONF=\${KT_HUB_CONF:-$SELF_DIR/hub.conf}
51
53
  APP_ID=com.kutrion.desktop
52
54
  PKG_NAME=kutrion
53
55
 
54
- log() { printf 'ensure-hub: %s\\n' "$*"; }
56
+ log() { printf 'ensure-hub: %s %s\\n' "$(date -u +%H:%M:%S)" "$*"; }
55
57
  refuse() { log "refused, nothing installed: $*"; exit 10; }
56
58
  cannot() { log "skipped: $*"; exit 11; }
57
59
 
@@ -99,15 +101,49 @@ verify_file() {
99
101
  [ "$got" = "$2" ] || return 10
100
102
  }
101
103
 
104
+ file_size() { wc -c < "$1" 2>/dev/null | tr -d ' ' || echo 0; }
105
+
106
+ # A download that stalls or drops resumes from the bytes already on disk instead of starting over; the whole
107
+ # download shares one budget so a dead link ends the run rather than holding the install lock.
102
108
  fetch() {
103
109
  case "$1" in
104
- file://*) command cp "\${1#file://}" "$2" 2>/dev/null ;;
105
- https://*)
106
- if command -v curl >/dev/null 2>&1; then curl -fsSL --proto '=https' --tlsv1.2 --retry 2 --connect-timeout 15 --max-time 900 -o "$2" "$1"
107
- elif command -v wget >/dev/null 2>&1; then wget -q --https-only -O "$2" "$1"
108
- else return 1; fi ;;
110
+ file://*) command cp "\${1#file://}" "$2" 2>/dev/null; return ;;
111
+ https://*) ;;
109
112
  *) return 1 ;;
110
113
  esac
114
+ if ! command -v curl >/dev/null 2>&1; then
115
+ command -v wget >/dev/null 2>&1 || return 1
116
+ wget -q --https-only -c -O "$2" "$1"; return
117
+ fi
118
+ budget=\${KT_HUB_FETCH_BUDGET:-1200}; case "$budget" in ''|*[!0-9]*) budget=1200 ;; esac
119
+ t0=$(date +%s); n=0
120
+ while :; do
121
+ left=$((budget - ($(date +%s) - t0)))
122
+ [ "$left" -gt 0 ] || { log "fetch budget of \${budget}s used up at $(file_size "$2") bytes"; return 1; }
123
+ n=$((n + 1))
124
+ curl -fsS --proto '=https' --tlsv1.2 -C - --connect-timeout 15 --max-time "$left" --speed-time 60 --speed-limit 1024 -o "$2" "$1" && return 0
125
+ rc=$?
126
+ log "fetch attempt $n stopped (curl exit $rc) at $(file_size "$2") bytes"
127
+ [ "$n" -lt 20 ] || return 1
128
+ sleep 5
129
+ done
130
+ }
131
+
132
+ # fetch with a begin line, a byte count every 30 s while it runs, and an end line.
133
+ fetch_logged() {
134
+ log "fetch begin $1"
135
+ t=$(date +%s)
136
+ case "$1" in
137
+ https://*)
138
+ parent=$$
139
+ ( while sleep 30; do kill -0 "$parent" 2>/dev/null || exit 0; log "fetching, $(file_size "$2") bytes so far"; done ) &
140
+ tick=$!
141
+ fetch "$1" "$2"; rc=$?
142
+ kill "$tick" 2>/dev/null; wait "$tick" 2>/dev/null ;;
143
+ *) fetch "$1" "$2"; rc=$? ;;
144
+ esac
145
+ log "fetch end rc=$rc bytes=$(file_size "$2") seconds=$(( $(date +%s) - t ))"
146
+ return $rc
111
147
  }
112
148
 
113
149
  # A feed file name is a bare name; anything with a path, scheme or whitespace could point off the feed origin.
@@ -194,7 +230,7 @@ retry() {
194
230
  while :; do
195
231
  "$@" && return 0
196
232
  [ "$DEFER" -eq 1 ] && [ "$n" -lt 36 ] || return 1
197
- n=$((n + 1)); sleep 5
233
+ n=$((n + 1)); log "install attempt $n failed, retrying in 5s"; sleep 5
198
234
  done
199
235
  }
200
236
 
@@ -266,6 +302,8 @@ verify_linux_package() {
266
302
  }
267
303
 
268
304
  cleanup() {
305
+ rc=$?
306
+ [ "$STARTED" -eq 1 ] && log "finished exit=$rc"
269
307
  [ "$LOCK_HELD" -eq 1 ] && rm -r -f "$LOCK"
270
308
  # A run staged by the app lives in its own temp directory; nothing else is ever removed.
271
309
  case "$(basename "$SELF_DIR")" in kt-hub-run-*) rm -r -f "$SELF_DIR" ;; esac
@@ -275,7 +313,7 @@ cleanup() {
275
313
  }
276
314
 
277
315
  main() {
278
- FIRST_RUN=0; DEFER=0; FORCE=0; VERIFY_ONLY=0; OS_ARG=""; WORK=""; MOUNT=""; LOCK=""; LOCK_HELD=0
316
+ FIRST_RUN=0; DEFER=0; FORCE=0; VERIFY_ONLY=0; OS_ARG=""; WORK=""; MOUNT=""; LOCK=""; LOCK_HELD=0; STARTED=0
279
317
  trap cleanup EXIT
280
318
  while [ $# -gt 0 ]; do
281
319
  case "$1" in
@@ -291,6 +329,7 @@ main() {
291
329
  shift
292
330
  done
293
331
 
332
+ STARTED=1
294
333
  [ "\${KT_NO_HUB:-}" = 1 ] && { log "skipped: KT_NO_HUB=1"; exit 0; }
295
334
  MIN=$(conf_get MIN_HUB_VERSION)
296
335
  [ -n "$MIN" ] || refuse "no MIN_HUB_VERSION in $CONF"
@@ -337,6 +376,7 @@ main() {
337
376
  WORK=$(mktemp -d "\${TMPDIR:-/tmp}/kt-hub.XXXXXX") || cannot "no temporary directory"
338
377
 
339
378
  case "$OS" in mac) chan=latest-mac.yml ;; *) chan=latest-linux.yml ;; esac
379
+ log "start, minimum $MIN, kind $KIND"
340
380
  fetch "$FEED/$chan" "$WORK/feed.yml" || { log "could not fetch $FEED/$chan"; exit 12; }
341
381
  fv=$(feed_version "$WORK/feed.yml")
342
382
  c=$(ver_cmp "\${fv:-0}" "$MIN"); [ "$c" = 0 ] || [ "$c" = 1 ] || refuse "the feed offers \${fv:-no version}, below the minimum $MIN"
@@ -344,7 +384,8 @@ main() {
344
384
  [ -n "$pair" ] || refuse "the feed lists no $KIND package for $(uname -m)"
345
385
  name=\${pair% *}; want=\${pair##* }
346
386
  bare_name "$name" || refuse "the feed names '$name', which is not a plain file name"
347
- fetch "$FEED/$name" "$WORK/$name" || { log "could not fetch $FEED/$name"; exit 12; }
387
+ fetch_logged "$FEED/$name" "$WORK/$name" || { log "could not fetch $FEED/$name"; exit 12; }
388
+ log "verify sha512"
348
389
  verify_file "$WORK/$name" "$want" || refuse "sha512 of $name does not match the feed"
349
390
  log "sha512 of $name matches the feed"
350
391
 
@@ -369,6 +410,7 @@ main() {
369
410
  [ "$VERIFY_ONLY" -eq 1 ] && { log "verified $name, not installing (--verify-only)"; exit 0; }
370
411
  # The package manager's sandbox user must be able to read the file.
371
412
  chmod 644 "$WORK/$name"; chmod 755 "$WORK"
413
+ log "install $KIND"
372
414
  if [ "$KIND" = deb ]; then retry install_deb "$WORK/$name"; else retry install_rpm "$WORK/$name"; fi || { log "the package manager failed"; exit 12; }
373
415
  log "installed Kutrion $fv" ;;
374
416
  esac
@@ -382,8 +424,11 @@ main "$@"
382
424
  #
383
425
  # ensure-hub.ps1 [-FirstRun] [-Force] [-VerifyOnly]
384
426
  # Exit: 0 present, installed or skipped by policy; 10 refused; 11 cannot install here; 12 fetch or install failed.
427
+ # Every run that gets past the dot-source guard ends with one "ensure-hub: HH:mm:ss finished exit=N" line, written to the output and
428
+ # to the log file (%LOCALAPPDATA%\\Kutrion\\ensure-hub.log, or KT_HUB_LOG), so a caller that started it detached can wait on it.
385
429
  # Environment: KT_NO_HUB=1 skips; KT_HUB_FEED_BASE (https or file) replaces the feed;
386
- # KT_HUB_ALLOW_UNSIGNED=1 accepts a build with no publisher identity.
430
+ # KT_HUB_ALLOW_UNSIGNED=1 accepts a build with no publisher identity; KT_HUB_FETCH_BUDGET seconds bounds one
431
+ # download across its resumed attempts (default 1200).
387
432
  param(
388
433
  [switch]$FirstRun,
389
434
  [switch]$Force,
@@ -395,9 +440,23 @@ $ErrorActionPreference = 'Stop'
395
440
  $ProgressPreference = 'SilentlyContinue'
396
441
  $AppId = 'com.kutrion.desktop'
397
442
 
398
- function Write-Hub([string]$m) { Write-Output "ensure-hub: $m" }
399
- function Stop-Refused([string]$m) { Write-Hub "refused, nothing installed: $m"; exit 10 }
400
- function Stop-Cannot([string]$m) { Write-Hub "skipped: $m"; exit 11 }
443
+ $script:ExitCode = 1
444
+ $script:LogFile = if ($env:KT_HUB_LOG) { $env:KT_HUB_LOG } elseif ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA 'Kutrion\\ensure-hub.log' } else { '' }
445
+
446
+ # Host and file, not the pipeline: functions that return a value must not carry log lines with it.
447
+ function Write-Hub([string]$m) {
448
+ $line = 'ensure-hub: {0} {1}' -f (Get-Date -Format 'HH:mm:ss'), $m
449
+ Write-Host $line
450
+ if ($script:LogFile) {
451
+ try {
452
+ New-Item -ItemType Directory -Force -Path (Split-Path -Parent $script:LogFile) | Out-Null
453
+ Add-Content -LiteralPath $script:LogFile -Value $line -Encoding ASCII
454
+ } catch { $script:LogFile = '' }
455
+ }
456
+ }
457
+ function Exit-Hub([int]$Code) { $script:ExitCode = $Code; exit $Code }
458
+ function Stop-Refused([string]$m) { Write-Hub "refused, nothing installed: $m"; Exit-Hub 10 }
459
+ function Stop-Cannot([string]$m) { Write-Hub "skipped: $m"; Exit-Hub 11 }
401
460
 
402
461
  function Get-ConfValue([string]$Name) {
403
462
  if (-not (Test-Path -LiteralPath $Conf)) { return '' }
@@ -482,17 +541,94 @@ function Get-InstalledHub {
482
541
  return $null
483
542
  }
484
543
 
544
+ function Get-FileLength([string]$Path) { if (Test-Path -LiteralPath $Path) { return (Get-Item -LiteralPath $Path).Length } else { return 0L } }
545
+
546
+ # One HTTP attempt. Asks for the bytes after what is on disk; a server that answers 200 anyway sends the whole file, so the
547
+ # file is rewritten from the start. Stops when no byte arrives for 60 s or the shared deadline passes.
548
+ function Receive-Range([string]$Url, [string]$Dest, [datetime]$Deadline) {
549
+ $have = Get-FileLength $Dest
550
+ $req = [Net.HttpWebRequest]::Create($Url)
551
+ $req.Timeout = 30000
552
+ $req.ReadWriteTimeout = 60000
553
+ if ($have -gt 0) { $req.AddRange([long]$have) }
554
+ try { $resp = $req.GetResponse() }
555
+ catch [Net.WebException] {
556
+ # 416: the bytes on disk are not a prefix of what the server holds; start over rather than loop on it.
557
+ if ($_.Exception.Response -and [int]$_.Exception.Response.StatusCode -eq 416 -and $have -gt 0) { Remove-Item -LiteralPath $Dest -Force; $_.Exception.Response.Close(); return $false }
558
+ throw
559
+ }
560
+ try {
561
+ if ($resp.ResponseUri.Scheme -ne 'https') { throw "the feed redirected to a non-https address" }
562
+ $partial = ([int]$resp.StatusCode -eq 206)
563
+ $total = -1L
564
+ if ($partial) { if ($resp.Headers['Content-Range'] -match '/(\\d+)$') { $total = [long]$Matches[1] } }
565
+ elseif ($resp.ContentLength -ge 0) { $total = $resp.ContentLength }
566
+ $mode = if ($partial) { [IO.FileMode]::Append } else { [IO.FileMode]::Create }
567
+ $in = $resp.GetResponseStream()
568
+ $out = New-Object IO.FileStream($Dest, $mode, [IO.FileAccess]::Write)
569
+ try {
570
+ $buf = New-Object byte[] 65536
571
+ $clock = [Diagnostics.Stopwatch]::StartNew()
572
+ $lastTick = 0; $lastData = 0
573
+ while ($true) {
574
+ $task = $in.ReadAsync($buf, 0, $buf.Length)
575
+ while (-not $task.Wait(1000)) {
576
+ if ($clock.Elapsed.TotalSeconds - $lastData -gt 60) { throw 'no data for 60 s' }
577
+ if ((Get-Date) -gt $Deadline) { throw 'the time budget ended' }
578
+ if ($clock.Elapsed.TotalSeconds - $lastTick -ge 30) { $lastTick = $clock.Elapsed.TotalSeconds; Write-Hub "fetching, $(Get-FileLength $Dest) bytes so far" }
579
+ }
580
+ $n = $task.Result
581
+ if ($n -le 0) { break }
582
+ $out.Write($buf, 0, $n)
583
+ $lastData = $clock.Elapsed.TotalSeconds
584
+ if ((Get-Date) -gt $Deadline) { throw 'the time budget ended' }
585
+ if ($lastData - $lastTick -ge 30) { $lastTick = $lastData; $out.Flush(); Write-Hub "fetching, $(Get-FileLength $Dest) bytes so far" }
586
+ }
587
+ } finally { $out.Dispose(); $in.Dispose() }
588
+ $now = Get-FileLength $Dest
589
+ if ($total -ge 0 -and $now -ne $total) { throw "the download stopped at $now of $total bytes" }
590
+ return $true
591
+ } finally { $resp.Close() }
592
+ }
593
+
594
+ # A download that stalls or drops resumes from the bytes already on disk instead of starting over; the whole
595
+ # download shares one budget so a dead link ends the run rather than holding the install lock.
485
596
  function Get-Fetched([string]$Url, [string]$Dest) {
486
597
  if ($Url -like 'file://*') { Copy-Item -LiteralPath ([Uri]$Url).LocalPath -Destination $Dest -Force; return }
487
598
  if ($Url -notlike 'https://*') { throw "only https and file feeds are used: $Url" }
488
599
  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
489
- Invoke-WebRequest -UseBasicParsing -Uri $Url -OutFile $Dest -TimeoutSec 900
600
+ $budget = 1200
601
+ [void][int]::TryParse($env:KT_HUB_FETCH_BUDGET, [ref]$budget)
602
+ if ($budget -le 0) { $budget = 1200 }
603
+ $deadline = (Get-Date).AddSeconds($budget)
604
+ $attempt = 0
605
+ while ($true) {
606
+ $attempt++
607
+ try { if (Receive-Range $Url $Dest $deadline) { return } }
608
+ catch {
609
+ Write-Hub "fetch attempt $attempt stopped ($($_.Exception.Message)) at $(Get-FileLength $Dest) bytes"
610
+ if ((Get-Date) -gt $deadline) { throw "fetch budget of \${budget}s used up" }
611
+ if ($attempt -ge 20) { throw 'too many failed attempts' }
612
+ Start-Sleep -Seconds 5
613
+ }
614
+ }
615
+ }
616
+
617
+ # Get-Fetched with a begin line and an end line; the byte counts every 30 s come from the transfer itself.
618
+ function Get-FetchedLogged([string]$Url, [string]$Dest) {
619
+ Write-Hub "fetch begin $Url"
620
+ $t = Get-Date; $ok = $false
621
+ try { Get-Fetched $Url $Dest; $ok = $true } finally {
622
+ Write-Hub ('fetch end rc={0} bytes={1} seconds={2}' -f $(if ($ok) { 0 } else { 1 }), (Get-FileLength $Dest), [int]((Get-Date) - $t).TotalSeconds)
623
+ }
490
624
  }
491
625
 
492
626
  # Dot-sourcing loads the functions above and runs nothing, so the checks can be driven one by one.
493
627
  if ($MyInvocation.InvocationName -eq '.') { return }
494
628
 
495
- if ($env:KT_NO_HUB -eq '1') { Write-Hub 'skipped: KT_NO_HUB=1'; exit 0 }
629
+ try {
630
+ Write-Hub 'start'
631
+ if ($env:KT_NO_HUB -eq '1') { Write-Hub 'skipped: KT_NO_HUB=1'; Exit-Hub 0 }
496
632
 
497
633
  $min = Get-ConfValue 'MIN_HUB_VERSION'
498
634
  if (-not $min) { Stop-Refused "no MIN_HUB_VERSION in $Conf" }
@@ -500,14 +636,15 @@ $feed = if ($env:KT_HUB_FEED_BASE) { $env:KT_HUB_FEED_BASE } else { Get-ConfValu
500
636
  if (-not $feed) { Stop-Refused 'no feed configured' }
501
637
  $publisher = Get-ConfValue 'HUB_WIN_PUBLISHER'
502
638
  $allowUnsigned = ($env:KT_HUB_ALLOW_UNSIGNED -eq '1') -or ((Get-ConfValue 'HUB_ALLOW_UNSIGNED') -eq '1')
639
+ Write-Hub "minimum $min"
503
640
 
504
641
  if (-not $Force) {
505
642
  $cur = Get-InstalledHub
506
643
  if ($null -ne $cur) {
507
- if ($cur -eq 'unknown') { Write-Hub 'present, version unreadable, left alone'; exit 0 }
644
+ if ($cur -eq 'unknown') { Write-Hub 'present, version unreadable, left alone'; Exit-Hub 0 }
508
645
  $c = Compare-HubVersion $cur $min
509
- if ($null -eq $c) { Write-Hub "present, version '$cur' not comparable, left alone"; exit 0 }
510
- if ($c -ge 0) { Write-Hub "present at $cur (minimum $min), nothing to do"; exit 0 }
646
+ if ($null -eq $c) { Write-Hub "present, version '$cur' not comparable, left alone"; Exit-Hub 0 }
647
+ if ($c -ge 0) { Write-Hub "present at $cur (minimum $min), nothing to do"; Exit-Hub 0 }
511
648
  Write-Hub "present at $cur, below $min"
512
649
  } else {
513
650
  Write-Hub 'absent'
@@ -531,13 +668,13 @@ $mutex = New-Object Threading.Mutex($false, 'Local\\KutrionEnsureHub')
531
668
  $held = $false
532
669
  if (-not $VerifyOnly) {
533
670
  try { $held = $mutex.WaitOne(0) } catch [Threading.AbandonedMutexException] { $held = $true }
534
- if (-not $held) { Write-Hub 'another install is running'; exit 0 }
671
+ if (-not $held) { Write-Hub 'another install is running'; Exit-Hub 0 }
535
672
  }
536
673
 
537
674
  $work = Join-Path ([IO.Path]::GetTempPath()) ('kt-hub-' + [guid]::NewGuid().ToString('N'))
538
675
  New-Item -ItemType Directory -Force -Path $work | Out-Null
539
676
  try {
540
- try { Get-Fetched "$feed/latest.yml" (Join-Path $work 'feed.yml') } catch { Write-Hub "could not fetch $feed/latest.yml"; exit 12 }
677
+ try { Get-FetchedLogged "$feed/latest.yml" (Join-Path $work 'feed.yml') } catch { Write-Hub "could not fetch $feed/latest.yml"; Exit-Hub 12 }
541
678
  $text = Get-Content -LiteralPath (Join-Path $work 'feed.yml') -Raw
542
679
  $fv = Get-FeedVersion $text
543
680
  $c = if ($fv) { Compare-HubVersion $fv $min } else { $null }
@@ -546,7 +683,8 @@ try {
546
683
  if (-not $entry) { Stop-Refused 'the feed lists no installer' }
547
684
  if (-not (Test-BareName $entry.Name)) { Stop-Refused "the feed names '$($entry.Name)', which is not a plain file name" }
548
685
  $exe = Join-Path $work $entry.Name
549
- try { Get-Fetched "$feed/$($entry.Name)" $exe } catch { Write-Hub "could not fetch $feed/$($entry.Name)"; exit 12 }
686
+ try { Get-FetchedLogged "$feed/$($entry.Name)" $exe } catch { Write-Hub "could not fetch $feed/$($entry.Name)"; Exit-Hub 12 }
687
+ Write-Hub 'verify sha512'
550
688
  if (-not (Test-FileHash $exe $entry.Sha512)) { Stop-Refused "sha512 of $($entry.Name) does not match the feed" }
551
689
  Write-Hub "sha512 of $($entry.Name) matches the feed"
552
690
 
@@ -561,23 +699,28 @@ try {
561
699
  Stop-Refused 'no publisher identity is configured and unsigned builds are not allowed'
562
700
  }
563
701
 
564
- if ($VerifyOnly) { Write-Hub "verified $($entry.Name), not installing (-VerifyOnly)"; exit 0 }
702
+ if ($VerifyOnly) { Write-Hub "verified $($entry.Name), not installing (-VerifyOnly)"; Exit-Hub 0 }
565
703
 
566
704
  # Waits on this pid only: Start-Process -Wait in Windows PowerShell 5.1 waits on the whole process tree, so an
567
705
  # installer that starts the app never returns.
706
+ Write-Hub 'install'
568
707
  $p = [Diagnostics.Process]::Start((New-Object Diagnostics.ProcessStartInfo -Property @{ FileName = $exe; Arguments = '/S /currentuser'; UseShellExecute = $false }))
569
- if (-not $p.WaitForExit(600000)) { & taskkill.exe /PID $p.Id /T /F | Out-Null; Write-Hub 'the installer did not finish in 10 minutes'; exit 12 }
570
- if ($p.ExitCode -ne 0) { Write-Hub "the installer exited $($p.ExitCode)"; exit 12 }
708
+ if (-not $p.WaitForExit(600000)) { & taskkill.exe /PID $p.Id /T /F | Out-Null; Write-Hub 'the installer did not finish in 10 minutes'; Exit-Hub 12 }
709
+ if ($p.ExitCode -ne 0) { Write-Hub "the installer exited $($p.ExitCode)"; Exit-Hub 12 }
571
710
  $after = Get-InstalledHub
572
- if ($null -eq $after) { Write-Hub 'the installer finished but no Hub is registered'; exit 12 }
711
+ if ($null -eq $after) { Write-Hub 'the installer finished but no Hub is registered'; Exit-Hub 12 }
573
712
  Write-Hub "installed Kutrion $after"
574
- exit 0
713
+ Exit-Hub 0
575
714
  } finally {
576
715
  Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue
577
716
  if ($held) { $mutex.ReleaseMutex() }
578
717
  # A run staged by the app lives in its own temp directory; nothing else is ever removed.
579
718
  if ((Split-Path -Leaf $PSScriptRoot) -like 'kt-hub-run-*') { Remove-Item -LiteralPath $PSScriptRoot -Recurse -Force -ErrorAction SilentlyContinue }
580
719
  }
720
+ } finally {
721
+ Write-Hub "finished exit=$($script:ExitCode)"
722
+ }
723
+ exit $script:ExitCode
581
724
  `, "hub.conf": "# Read by ensure-hub.sh and ensure-hub.ps1, and shipped beside them in every app build.\nMIN_HUB_VERSION=1.2.9\nFEED_BASE=https://kutrion.com/updates\n# Authenticode subject common name the Hub installer must carry. Empty until a certificate exists (#973).\nHUB_WIN_PUBLISHER=\n# Apple Team ID the Hub bundle must be signed with. Empty until the Developer ID exists (#973).\nHUB_MAC_TEAM_ID=\n# 0 installs nothing that cannot be tied to a publisher identity above. 1 until code signing lands (#973, #3295). On Windows this equals the NSIS updater trust: sha512 over TLS from our own HTTPS feed. On macOS and Linux it is sha512 over TLS only, weaker than the macOS updater. Owner accepted this on #3283. KT_HUB_ALLOW_UNSIGNED=1 overrides per run.\nHUB_ALLOW_UNSIGNED=1\n" };
582
725
  }
583
726
  });
@@ -2840,7 +2983,7 @@ var require_websocket = __commonJS({
2840
2983
  var http = require("http");
2841
2984
  var net = require("net");
2842
2985
  var tls = require("tls");
2843
- var { randomBytes: randomBytes7, createHash: createHash3 } = require("crypto");
2986
+ var { randomBytes: randomBytes6, createHash: createHash2 } = require("crypto");
2844
2987
  var { Duplex, Readable } = require("stream");
2845
2988
  var { URL: URL2 } = require("url");
2846
2989
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -3378,7 +3521,7 @@ var require_websocket = __commonJS({
3378
3521
  }
3379
3522
  }
3380
3523
  const defaultPort = isSecure ? 443 : 80;
3381
- const key = randomBytes7(16).toString("base64");
3524
+ const key = randomBytes6(16).toString("base64");
3382
3525
  const request = isSecure ? https.request : http.request;
3383
3526
  const protocolSet = /* @__PURE__ */ new Set();
3384
3527
  let perMessageDeflate;
@@ -3508,7 +3651,7 @@ var require_websocket = __commonJS({
3508
3651
  abortHandshake(websocket, socket, "Invalid Upgrade header");
3509
3652
  return;
3510
3653
  }
3511
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
3654
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
3512
3655
  if (res.headers["sec-websocket-accept"] !== digest) {
3513
3656
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
3514
3657
  return;
@@ -3880,7 +4023,7 @@ var require_websocket_server = __commonJS({
3880
4023
  var EventEmitter = require("events");
3881
4024
  var http = require("http");
3882
4025
  var { Duplex } = require("stream");
3883
- var { createHash: createHash3 } = require("crypto");
4026
+ var { createHash: createHash2 } = require("crypto");
3884
4027
  var extension2 = require_extension();
3885
4028
  var PerMessageDeflate2 = require_permessage_deflate();
3886
4029
  var subprotocol2 = require_subprotocol();
@@ -4187,7 +4330,7 @@ var require_websocket_server = __commonJS({
4187
4330
  );
4188
4331
  }
4189
4332
  if (this._state > RUNNING) return abortHandshake(socket, 503);
4190
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
4333
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
4191
4334
  const headers = [
4192
4335
  "HTTP/1.1 101 Switching Protocols",
4193
4336
  "Upgrade: websocket",
@@ -8777,25 +8920,25 @@ var require_data_url = __commonJS({
8777
8920
  function isHTTPWhiteSpace(char) {
8778
8921
  return char === 13 || char === 10 || char === 9 || char === 32;
8779
8922
  }
8780
- function removeHTTPWhitespace(str6, leading = true, trailing = true) {
8781
- return removeChars(str6, leading, trailing, isHTTPWhiteSpace);
8923
+ function removeHTTPWhitespace(str8, leading = true, trailing = true) {
8924
+ return removeChars(str8, leading, trailing, isHTTPWhiteSpace);
8782
8925
  }
8783
8926
  function isASCIIWhitespace(char) {
8784
8927
  return char === 13 || char === 10 || char === 9 || char === 12 || char === 32;
8785
8928
  }
8786
- function removeASCIIWhitespace(str6, leading = true, trailing = true) {
8787
- return removeChars(str6, leading, trailing, isASCIIWhitespace);
8929
+ function removeASCIIWhitespace(str8, leading = true, trailing = true) {
8930
+ return removeChars(str8, leading, trailing, isASCIIWhitespace);
8788
8931
  }
8789
- function removeChars(str6, leading, trailing, predicate) {
8932
+ function removeChars(str8, leading, trailing, predicate) {
8790
8933
  let lead = 0;
8791
- let trail = str6.length - 1;
8934
+ let trail = str8.length - 1;
8792
8935
  if (leading) {
8793
- while (lead < str6.length && predicate(str6.charCodeAt(lead))) lead++;
8936
+ while (lead < str8.length && predicate(str8.charCodeAt(lead))) lead++;
8794
8937
  }
8795
8938
  if (trailing) {
8796
- while (trail > 0 && predicate(str6.charCodeAt(trail))) trail--;
8939
+ while (trail > 0 && predicate(str8.charCodeAt(trail))) trail--;
8797
8940
  }
8798
- return lead === 0 && trail === str6.length - 1 ? str6 : str6.slice(lead, trail + 1);
8941
+ return lead === 0 && trail === str8.length - 1 ? str8 : str8.slice(lead, trail + 1);
8799
8942
  }
8800
8943
  function isomorphicDecode(input) {
8801
8944
  const length = input.length;
@@ -10714,7 +10857,7 @@ var require_body = __commonJS({
10714
10857
  const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, "0")}`;
10715
10858
  const prefix = `--${boundary}\r
10716
10859
  Content-Disposition: form-data`;
10717
- const escape = (str6) => str6.replace(/\n/g, "%0A").replace(/\r/g, "%0D").replace(/"/g, "%22");
10860
+ const escape = (str8) => str8.replace(/\n/g, "%0A").replace(/\r/g, "%0D").replace(/"/g, "%22");
10718
10861
  const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, "\r\n");
10719
10862
  const blobParts = [];
10720
10863
  const rn = new Uint8Array([13, 10]);
@@ -17294,13 +17437,13 @@ var require_stringifyComment = __commonJS({
17294
17437
  "node_modules/yaml/dist/stringify/stringifyComment.js"(exports2) {
17295
17438
  "use strict";
17296
17439
  init_define_KT_HUB_FILES();
17297
- var stringifyComment = (str6) => str6.replace(/^(?!$)(?: $)?/gm, "#");
17440
+ var stringifyComment = (str8) => str8.replace(/^(?!$)(?: $)?/gm, "#");
17298
17441
  function indentComment(comment, indent) {
17299
17442
  if (/^\n+$/.test(comment))
17300
17443
  return comment.substring(1);
17301
17444
  return indent ? comment.replace(/^(?! *$)/gm, indent) : comment;
17302
17445
  }
17303
- var lineComment = (str6, indent, comment) => str6.endsWith("\n") ? indentComment(comment, indent) : comment.includes("\n") ? "\n" + indentComment(comment, indent) : (str6.endsWith(" ") ? "" : " ") + comment;
17446
+ var lineComment = (str8, indent, comment) => str8.endsWith("\n") ? indentComment(comment, indent) : comment.includes("\n") ? "\n" + indentComment(comment, indent) : (str8.endsWith(" ") ? "" : " ") + comment;
17304
17447
  exports2.indentComment = indentComment;
17305
17448
  exports2.lineComment = lineComment;
17306
17449
  exports2.stringifyComment = stringifyComment;
@@ -17456,16 +17599,16 @@ var require_stringifyString = __commonJS({
17456
17599
  lineWidth: ctx.options.lineWidth,
17457
17600
  minContentWidth: ctx.options.minContentWidth
17458
17601
  });
17459
- var containsDocumentMarker = (str6) => /^(%|---|\.\.\.)/m.test(str6);
17460
- function lineLengthOverLimit(str6, lineWidth, indentLength) {
17602
+ var containsDocumentMarker = (str8) => /^(%|---|\.\.\.)/m.test(str8);
17603
+ function lineLengthOverLimit(str8, lineWidth, indentLength) {
17461
17604
  if (!lineWidth || lineWidth < 0)
17462
17605
  return false;
17463
17606
  const limit = lineWidth - indentLength;
17464
- const strLen = str6.length;
17607
+ const strLen = str8.length;
17465
17608
  if (strLen <= limit)
17466
17609
  return false;
17467
17610
  for (let i = 0, start = 0; i < strLen; ++i) {
17468
- if (str6[i] === "\n") {
17611
+ if (str8[i] === "\n") {
17469
17612
  if (i - start > limit)
17470
17613
  return true;
17471
17614
  start = i + 1;
@@ -17482,11 +17625,11 @@ var require_stringifyString = __commonJS({
17482
17625
  const { implicitKey } = ctx;
17483
17626
  const minMultiLineLength = ctx.options.doubleQuotedMinMultiLineLength;
17484
17627
  const indent = ctx.indent || (containsDocumentMarker(value) ? " " : "");
17485
- let str6 = "";
17628
+ let str8 = "";
17486
17629
  let start = 0;
17487
17630
  for (let i = 0, ch = json[i]; ch; ch = json[++i]) {
17488
17631
  if (ch === " " && json[i + 1] === "\\" && json[i + 2] === "n") {
17489
- str6 += json.slice(start, i) + "\\ ";
17632
+ str8 += json.slice(start, i) + "\\ ";
17490
17633
  i += 1;
17491
17634
  start = i;
17492
17635
  ch = "\\";
@@ -17495,38 +17638,38 @@ var require_stringifyString = __commonJS({
17495
17638
  switch (json[i + 1]) {
17496
17639
  case "u":
17497
17640
  {
17498
- str6 += json.slice(start, i);
17641
+ str8 += json.slice(start, i);
17499
17642
  const code = json.substr(i + 2, 4);
17500
17643
  switch (code) {
17501
17644
  case "0000":
17502
- str6 += "\\0";
17645
+ str8 += "\\0";
17503
17646
  break;
17504
17647
  case "0007":
17505
- str6 += "\\a";
17648
+ str8 += "\\a";
17506
17649
  break;
17507
17650
  case "000b":
17508
- str6 += "\\v";
17651
+ str8 += "\\v";
17509
17652
  break;
17510
17653
  case "001b":
17511
- str6 += "\\e";
17654
+ str8 += "\\e";
17512
17655
  break;
17513
17656
  case "0085":
17514
- str6 += "\\N";
17657
+ str8 += "\\N";
17515
17658
  break;
17516
17659
  case "00a0":
17517
- str6 += "\\_";
17660
+ str8 += "\\_";
17518
17661
  break;
17519
17662
  case "2028":
17520
- str6 += "\\L";
17663
+ str8 += "\\L";
17521
17664
  break;
17522
17665
  case "2029":
17523
- str6 += "\\P";
17666
+ str8 += "\\P";
17524
17667
  break;
17525
17668
  default:
17526
17669
  if (code.substr(0, 2) === "00")
17527
- str6 += "\\x" + code.substr(2);
17670
+ str8 += "\\x" + code.substr(2);
17528
17671
  else
17529
- str6 += json.substr(i, 6);
17672
+ str8 += json.substr(i, 6);
17530
17673
  }
17531
17674
  i += 5;
17532
17675
  start = i + 1;
@@ -17536,14 +17679,14 @@ var require_stringifyString = __commonJS({
17536
17679
  if (implicitKey || json[i + 2] === '"' || json.length < minMultiLineLength) {
17537
17680
  i += 1;
17538
17681
  } else {
17539
- str6 += json.slice(start, i) + "\n\n";
17682
+ str8 += json.slice(start, i) + "\n\n";
17540
17683
  while (json[i + 2] === "\\" && json[i + 3] === "n" && json[i + 4] !== '"') {
17541
- str6 += "\n";
17684
+ str8 += "\n";
17542
17685
  i += 2;
17543
17686
  }
17544
- str6 += indent;
17687
+ str8 += indent;
17545
17688
  if (json[i + 2] === " ")
17546
- str6 += "\\";
17689
+ str8 += "\\";
17547
17690
  i += 1;
17548
17691
  start = i + 1;
17549
17692
  }
@@ -17552,8 +17695,8 @@ var require_stringifyString = __commonJS({
17552
17695
  i += 1;
17553
17696
  }
17554
17697
  }
17555
- str6 = start ? str6 + json.slice(start) : json;
17556
- return implicitKey ? str6 : foldFlowLines.foldFlowLines(str6, indent, foldFlowLines.FOLD_QUOTED, getFoldOptions(ctx, false));
17698
+ str8 = start ? str8 + json.slice(start) : json;
17699
+ return implicitKey ? str8 : foldFlowLines.foldFlowLines(str8, indent, foldFlowLines.FOLD_QUOTED, getFoldOptions(ctx, false));
17557
17700
  }
17558
17701
  function singleQuotedString(value, ctx) {
17559
17702
  if (ctx.options.singleQuote === false || ctx.implicitKey && value.includes("\n") || /[ \t]\n|\n[ \t]/.test(value))
@@ -17681,15 +17824,15 @@ ${indent}${start}${value}${end}`;
17681
17824
  return quotedString(value, ctx);
17682
17825
  }
17683
17826
  }
17684
- const str6 = value.replace(/\n+/g, `$&
17827
+ const str8 = value.replace(/\n+/g, `$&
17685
17828
  ${indent}`);
17686
17829
  if (actualString) {
17687
- const test = (tag) => tag.default && tag.tag !== "tag:yaml.org,2002:str" && tag.test?.test(str6);
17830
+ const test = (tag) => tag.default && tag.tag !== "tag:yaml.org,2002:str" && tag.test?.test(str8);
17688
17831
  const { compat, tags } = ctx.doc.schema;
17689
17832
  if (tags.some(test) || compat?.some(test))
17690
17833
  return quotedString(value, ctx);
17691
17834
  }
17692
- return implicitKey ? str6 : foldFlowLines.foldFlowLines(str6, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false));
17835
+ return implicitKey ? str8 : foldFlowLines.foldFlowLines(str8, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false));
17693
17836
  }
17694
17837
  function stringifyString(item, ctx, onComment, onChompKeep) {
17695
17838
  const { implicitKey, inFlow } = ctx;
@@ -17842,11 +17985,11 @@ var require_stringify = __commonJS({
17842
17985
  const props = stringifyProps(node, tagObj, ctx);
17843
17986
  if (props.length > 0)
17844
17987
  ctx.indentAtStart = (ctx.indentAtStart ?? 0) + props.length + 1;
17845
- const str6 = typeof tagObj.stringify === "function" ? tagObj.stringify(node, ctx, onComment, onChompKeep) : identity.isScalar(node) ? stringifyString.stringifyString(node, ctx, onComment, onChompKeep) : node.toString(ctx, onComment, onChompKeep);
17988
+ const str8 = typeof tagObj.stringify === "function" ? tagObj.stringify(node, ctx, onComment, onChompKeep) : identity.isScalar(node) ? stringifyString.stringifyString(node, ctx, onComment, onChompKeep) : node.toString(ctx, onComment, onChompKeep);
17846
17989
  if (!props)
17847
- return str6;
17848
- return identity.isScalar(node) || str6[0] === "{" || str6[0] === "[" ? `${props} ${str6}` : `${props}
17849
- ${ctx.indent}${str6}`;
17990
+ return str8;
17991
+ return identity.isScalar(node) || str8[0] === "{" || str8[0] === "[" ? `${props} ${str8}` : `${props}
17992
+ ${ctx.indent}${str8}`;
17850
17993
  }
17851
17994
  exports2.createStringifyContext = createStringifyContext;
17852
17995
  exports2.stringify = stringify;
@@ -17882,8 +18025,8 @@ var require_stringifyPair = __commonJS({
17882
18025
  });
17883
18026
  let keyCommentDone = false;
17884
18027
  let chompKeep = false;
17885
- let str6 = stringify.stringify(key, ctx, () => keyCommentDone = true, () => chompKeep = true);
17886
- if (!explicitKey && !ctx.inFlow && str6.length > 1024) {
18028
+ let str8 = stringify.stringify(key, ctx, () => keyCommentDone = true, () => chompKeep = true);
18029
+ if (!explicitKey && !ctx.inFlow && str8.length > 1024) {
17887
18030
  if (simpleKeys)
17888
18031
  throw new Error("With simple keys, single line scalar must not span more than 1024 characters");
17889
18032
  explicitKey = true;
@@ -17892,27 +18035,27 @@ var require_stringifyPair = __commonJS({
17892
18035
  if (allNullValues || value == null) {
17893
18036
  if (keyCommentDone && onComment)
17894
18037
  onComment();
17895
- return str6 === "" ? "?" : explicitKey ? `? ${str6}` : str6;
18038
+ return str8 === "" ? "?" : explicitKey ? `? ${str8}` : str8;
17896
18039
  }
17897
18040
  } else if (allNullValues && !simpleKeys || value == null && explicitKey) {
17898
- str6 = `? ${str6}`;
18041
+ str8 = `? ${str8}`;
17899
18042
  if (keyComment && !keyCommentDone) {
17900
- str6 += stringifyComment.lineComment(str6, ctx.indent, commentString(keyComment));
18043
+ str8 += stringifyComment.lineComment(str8, ctx.indent, commentString(keyComment));
17901
18044
  } else if (chompKeep && onChompKeep)
17902
18045
  onChompKeep();
17903
- return str6;
18046
+ return str8;
17904
18047
  }
17905
18048
  if (keyCommentDone)
17906
18049
  keyComment = null;
17907
18050
  if (explicitKey) {
17908
18051
  if (keyComment)
17909
- str6 += stringifyComment.lineComment(str6, ctx.indent, commentString(keyComment));
17910
- str6 = `? ${str6}
18052
+ str8 += stringifyComment.lineComment(str8, ctx.indent, commentString(keyComment));
18053
+ str8 = `? ${str8}
17911
18054
  ${indent}:`;
17912
18055
  } else {
17913
- str6 = `${str6}:`;
18056
+ str8 = `${str8}:`;
17914
18057
  if (keyComment)
17915
- str6 += stringifyComment.lineComment(str6, ctx.indent, commentString(keyComment));
18058
+ str8 += stringifyComment.lineComment(str8, ctx.indent, commentString(keyComment));
17916
18059
  }
17917
18060
  let vsb, vcb, valueComment;
17918
18061
  if (identity.isNode(value)) {
@@ -17928,7 +18071,7 @@ ${indent}:`;
17928
18071
  }
17929
18072
  ctx.implicitKey = false;
17930
18073
  if (!explicitKey && !keyComment && identity.isScalar(value))
17931
- ctx.indentAtStart = str6.length + 1;
18074
+ ctx.indentAtStart = str8.length + 1;
17932
18075
  chompKeep = false;
17933
18076
  if (!indentSeq && indentStep.length >= 2 && !ctx.inFlow && !explicitKey && identity.isSeq(value) && !value.flow && !value.tag && !value.anchor) {
17934
18077
  ctx.indent = ctx.indent.substring(2);
@@ -17972,16 +18115,16 @@ ${ctx.indent}`;
17972
18115
  } else if (valueStr === "" || valueStr[0] === "\n") {
17973
18116
  ws = "";
17974
18117
  }
17975
- str6 += ws + valueStr;
18118
+ str8 += ws + valueStr;
17976
18119
  if (ctx.inFlow) {
17977
18120
  if (valueCommentDone && onComment)
17978
18121
  onComment();
17979
18122
  } else if (valueComment && !valueCommentDone) {
17980
- str6 += stringifyComment.lineComment(str6, ctx.indent, commentString(valueComment));
18123
+ str8 += stringifyComment.lineComment(str8, ctx.indent, commentString(valueComment));
17981
18124
  } else if (chompKeep && onChompKeep) {
17982
18125
  onChompKeep();
17983
18126
  }
17984
- return str6;
18127
+ return str8;
17985
18128
  }
17986
18129
  exports2.stringifyPair = stringifyPair;
17987
18130
  }
@@ -18213,31 +18356,31 @@ var require_stringifyCollection = __commonJS({
18213
18356
  }
18214
18357
  }
18215
18358
  chompKeep = false;
18216
- let str7 = stringify.stringify(item, itemCtx, () => comment2 = null, () => chompKeep = true);
18359
+ let str9 = stringify.stringify(item, itemCtx, () => comment2 = null, () => chompKeep = true);
18217
18360
  if (comment2)
18218
- str7 += stringifyComment.lineComment(str7, itemIndent, commentString(comment2));
18361
+ str9 += stringifyComment.lineComment(str9, itemIndent, commentString(comment2));
18219
18362
  if (chompKeep && comment2)
18220
18363
  chompKeep = false;
18221
- lines.push(blockItemPrefix + str7);
18364
+ lines.push(blockItemPrefix + str9);
18222
18365
  }
18223
- let str6;
18366
+ let str8;
18224
18367
  if (lines.length === 0) {
18225
- str6 = flowChars.start + flowChars.end;
18368
+ str8 = flowChars.start + flowChars.end;
18226
18369
  } else {
18227
- str6 = lines[0];
18370
+ str8 = lines[0];
18228
18371
  for (let i = 1; i < lines.length; ++i) {
18229
18372
  const line = lines[i];
18230
- str6 += line ? `
18373
+ str8 += line ? `
18231
18374
  ${indent}${line}` : "\n";
18232
18375
  }
18233
18376
  }
18234
18377
  if (comment) {
18235
- str6 += "\n" + stringifyComment.indentComment(commentString(comment), indent);
18378
+ str8 += "\n" + stringifyComment.indentComment(commentString(comment), indent);
18236
18379
  if (onComment)
18237
18380
  onComment();
18238
18381
  } else if (chompKeep && onChompKeep)
18239
18382
  onChompKeep();
18240
- return str6;
18383
+ return str8;
18241
18384
  }
18242
18385
  function stringifyFlowCollection({ items }, ctx, { flowChars, itemIndent }) {
18243
18386
  const { indent, indentStep, flowCollectionPadding: fcPadding, options: { commentString } } = ctx;
@@ -18280,21 +18423,21 @@ ${indent}${line}` : "\n";
18280
18423
  }
18281
18424
  if (comment)
18282
18425
  reqNewline = true;
18283
- let str6 = stringify.stringify(item, itemCtx, () => comment = null);
18284
- reqNewline || (reqNewline = lines.length > linesAtValue || str6.includes("\n"));
18426
+ let str8 = stringify.stringify(item, itemCtx, () => comment = null);
18427
+ reqNewline || (reqNewline = lines.length > linesAtValue || str8.includes("\n"));
18285
18428
  if (i < items.length - 1) {
18286
- str6 += ",";
18429
+ str8 += ",";
18287
18430
  } else if (ctx.options.trailingComma) {
18288
18431
  if (ctx.options.lineWidth > 0) {
18289
- reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) + (str6.length + 2) > ctx.options.lineWidth);
18432
+ reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) + (str8.length + 2) > ctx.options.lineWidth);
18290
18433
  }
18291
18434
  if (reqNewline) {
18292
- str6 += ",";
18435
+ str8 += ",";
18293
18436
  }
18294
18437
  }
18295
18438
  if (comment)
18296
- str6 += stringifyComment.lineComment(str6, itemIndent, commentString(comment));
18297
- lines.push(str6);
18439
+ str8 += stringifyComment.lineComment(str8, itemIndent, commentString(comment));
18440
+ lines.push(str8);
18298
18441
  linesAtValue = lines.length;
18299
18442
  }
18300
18443
  const { start, end } = flowChars;
@@ -18306,11 +18449,11 @@ ${indent}${line}` : "\n";
18306
18449
  reqNewline = ctx.options.lineWidth > 0 && len > ctx.options.lineWidth;
18307
18450
  }
18308
18451
  if (reqNewline) {
18309
- let str6 = start;
18452
+ let str8 = start;
18310
18453
  for (const line of lines)
18311
- str6 += line ? `
18454
+ str8 += line ? `
18312
18455
  ${indentStep}${indent}${line}` : "\n";
18313
- return `${str6}
18456
+ return `${str8}
18314
18457
  ${indent}${end}`;
18315
18458
  } else {
18316
18459
  return `${start}${fcPadding}${lines.join(" ")}${fcPadding}${end}`;
@@ -18647,7 +18790,7 @@ var require_string = __commonJS({
18647
18790
  identify: (value) => typeof value === "string",
18648
18791
  default: true,
18649
18792
  tag: "tag:yaml.org,2002:str",
18650
- resolve: (str6) => str6,
18793
+ resolve: (str8) => str8,
18651
18794
  stringify(item, ctx, onComment, onChompKeep) {
18652
18795
  ctx = Object.assign({ actualString: true }, ctx);
18653
18796
  return stringifyString.stringifyString(item, ctx, onComment, onChompKeep);
@@ -18687,7 +18830,7 @@ var require_bool = __commonJS({
18687
18830
  default: true,
18688
18831
  tag: "tag:yaml.org,2002:bool",
18689
18832
  test: /^(?:[Tt]rue|TRUE|[Ff]alse|FALSE)$/,
18690
- resolve: (str6) => new Scalar.Scalar(str6[0] === "t" || str6[0] === "T"),
18833
+ resolve: (str8) => new Scalar.Scalar(str8[0] === "t" || str8[0] === "T"),
18691
18834
  stringify({ source, value }, ctx) {
18692
18835
  if (source && boolTag.test.test(source)) {
18693
18836
  const sv = source[0] === "t" || source[0] === "T";
@@ -18741,7 +18884,7 @@ var require_float = __commonJS({
18741
18884
  default: true,
18742
18885
  tag: "tag:yaml.org,2002:float",
18743
18886
  test: /^(?:[-+]?\.(?:inf|Inf|INF)|\.nan|\.NaN|\.NAN)$/,
18744
- resolve: (str6) => str6.slice(-3).toLowerCase() === "nan" ? NaN : str6[0] === "-" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY,
18887
+ resolve: (str8) => str8.slice(-3).toLowerCase() === "nan" ? NaN : str8[0] === "-" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY,
18745
18888
  stringify: stringifyNumber.stringifyNumber
18746
18889
  };
18747
18890
  var floatExp = {
@@ -18750,7 +18893,7 @@ var require_float = __commonJS({
18750
18893
  tag: "tag:yaml.org,2002:float",
18751
18894
  format: "EXP",
18752
18895
  test: /^[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)[eE][-+]?[0-9]+$/,
18753
- resolve: (str6) => parseFloat(str6),
18896
+ resolve: (str8) => parseFloat(str8),
18754
18897
  stringify(node) {
18755
18898
  const num2 = Number(node.value);
18756
18899
  return isFinite(num2) ? num2.toExponential() : stringifyNumber.stringifyNumber(node);
@@ -18761,11 +18904,11 @@ var require_float = __commonJS({
18761
18904
  default: true,
18762
18905
  tag: "tag:yaml.org,2002:float",
18763
18906
  test: /^[-+]?(?:\.[0-9]+|[0-9]+\.[0-9]*)$/,
18764
- resolve(str6) {
18765
- const node = new Scalar.Scalar(parseFloat(str6));
18766
- const dot = str6.indexOf(".");
18767
- if (dot !== -1 && str6[str6.length - 1] === "0")
18768
- node.minFractionDigits = str6.length - dot - 1;
18907
+ resolve(str8) {
18908
+ const node = new Scalar.Scalar(parseFloat(str8));
18909
+ const dot = str8.indexOf(".");
18910
+ if (dot !== -1 && str8[str8.length - 1] === "0")
18911
+ node.minFractionDigits = str8.length - dot - 1;
18769
18912
  return node;
18770
18913
  },
18771
18914
  stringify: stringifyNumber.stringifyNumber
@@ -18783,7 +18926,7 @@ var require_int = __commonJS({
18783
18926
  init_define_KT_HUB_FILES();
18784
18927
  var stringifyNumber = require_stringifyNumber();
18785
18928
  var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value);
18786
- var intResolve = (str6, offset, radix, { intAsBigInt }) => intAsBigInt ? BigInt(str6) : parseInt(str6.substring(offset), radix);
18929
+ var intResolve = (str8, offset, radix, { intAsBigInt }) => intAsBigInt ? BigInt(str8) : parseInt(str8.substring(offset), radix);
18787
18930
  function intStringify(node, radix, prefix) {
18788
18931
  const { value } = node;
18789
18932
  if (intIdentify(value) && value >= 0)
@@ -18796,7 +18939,7 @@ var require_int = __commonJS({
18796
18939
  tag: "tag:yaml.org,2002:int",
18797
18940
  format: "OCT",
18798
18941
  test: /^0o[0-7]+$/,
18799
- resolve: (str6, _onError, opt) => intResolve(str6, 2, 8, opt),
18942
+ resolve: (str8, _onError, opt) => intResolve(str8, 2, 8, opt),
18800
18943
  stringify: (node) => intStringify(node, 8, "0o")
18801
18944
  };
18802
18945
  var int = {
@@ -18804,7 +18947,7 @@ var require_int = __commonJS({
18804
18947
  default: true,
18805
18948
  tag: "tag:yaml.org,2002:int",
18806
18949
  test: /^[-+]?[0-9]+$/,
18807
- resolve: (str6, _onError, opt) => intResolve(str6, 0, 10, opt),
18950
+ resolve: (str8, _onError, opt) => intResolve(str8, 0, 10, opt),
18808
18951
  stringify: stringifyNumber.stringifyNumber
18809
18952
  };
18810
18953
  var intHex = {
@@ -18813,7 +18956,7 @@ var require_int = __commonJS({
18813
18956
  tag: "tag:yaml.org,2002:int",
18814
18957
  format: "HEX",
18815
18958
  test: /^0x[0-9a-fA-F]+$/,
18816
- resolve: (str6, _onError, opt) => intResolve(str6, 2, 16, opt),
18959
+ resolve: (str8, _onError, opt) => intResolve(str8, 2, 16, opt),
18817
18960
  stringify: (node) => intStringify(node, 16, "0x")
18818
18961
  };
18819
18962
  exports2.int = int;
@@ -18868,7 +19011,7 @@ var require_schema2 = __commonJS({
18868
19011
  identify: (value) => typeof value === "string",
18869
19012
  default: true,
18870
19013
  tag: "tag:yaml.org,2002:str",
18871
- resolve: (str6) => str6,
19014
+ resolve: (str8) => str8,
18872
19015
  stringify: stringifyJSON
18873
19016
  },
18874
19017
  {
@@ -18885,7 +19028,7 @@ var require_schema2 = __commonJS({
18885
19028
  default: true,
18886
19029
  tag: "tag:yaml.org,2002:bool",
18887
19030
  test: /^true$|^false$/,
18888
- resolve: (str6) => str6 === "true",
19031
+ resolve: (str8) => str8 === "true",
18889
19032
  stringify: stringifyJSON
18890
19033
  },
18891
19034
  {
@@ -18893,7 +19036,7 @@ var require_schema2 = __commonJS({
18893
19036
  default: true,
18894
19037
  tag: "tag:yaml.org,2002:int",
18895
19038
  test: /^-?(?:0|[1-9][0-9]*)$/,
18896
- resolve: (str6, _onError, { intAsBigInt }) => intAsBigInt ? BigInt(str6) : parseInt(str6, 10),
19039
+ resolve: (str8, _onError, { intAsBigInt }) => intAsBigInt ? BigInt(str8) : parseInt(str8, 10),
18897
19040
  stringify: ({ value }) => intIdentify(value) ? value.toString() : JSON.stringify(value)
18898
19041
  },
18899
19042
  {
@@ -18901,7 +19044,7 @@ var require_schema2 = __commonJS({
18901
19044
  default: true,
18902
19045
  tag: "tag:yaml.org,2002:float",
18903
19046
  test: /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][-+]?[0-9]+)?$/,
18904
- resolve: (str6) => parseFloat(str6),
19047
+ resolve: (str8) => parseFloat(str8),
18905
19048
  stringify: stringifyJSON
18906
19049
  }
18907
19050
  ];
@@ -18909,9 +19052,9 @@ var require_schema2 = __commonJS({
18909
19052
  default: true,
18910
19053
  tag: "",
18911
19054
  test: /^/,
18912
- resolve(str6, onError) {
18913
- onError(`Unresolved plain scalar ${JSON.stringify(str6)}`);
18914
- return str6;
19055
+ resolve(str8, onError) {
19056
+ onError(`Unresolved plain scalar ${JSON.stringify(str8)}`);
19057
+ return str8;
18915
19058
  }
18916
19059
  };
18917
19060
  var schema = [map.map, seq.seq].concat(jsonScalars, jsonError);
@@ -18944,10 +19087,10 @@ var require_binary = __commonJS({
18944
19087
  if (typeof node_buffer.Buffer === "function") {
18945
19088
  return node_buffer.Buffer.from(src, "base64");
18946
19089
  } else if (typeof atob === "function") {
18947
- const str6 = atob(src.replace(/[\n\r]/g, ""));
18948
- const buffer = new Uint8Array(str6.length);
18949
- for (let i = 0; i < str6.length; ++i)
18950
- buffer[i] = str6.charCodeAt(i);
19090
+ const str8 = atob(src.replace(/[\n\r]/g, ""));
19091
+ const buffer = new Uint8Array(str8.length);
19092
+ for (let i = 0; i < str8.length; ++i)
19093
+ buffer[i] = str8.charCodeAt(i);
18951
19094
  return buffer;
18952
19095
  } else {
18953
19096
  onError("This environment does not support reading binary tags; either Buffer or atob is required");
@@ -18958,28 +19101,28 @@ var require_binary = __commonJS({
18958
19101
  if (!value)
18959
19102
  return "";
18960
19103
  const buf = value;
18961
- let str6;
19104
+ let str8;
18962
19105
  if (typeof node_buffer.Buffer === "function") {
18963
- str6 = buf instanceof node_buffer.Buffer ? buf.toString("base64") : node_buffer.Buffer.from(buf.buffer).toString("base64");
19106
+ str8 = buf instanceof node_buffer.Buffer ? buf.toString("base64") : node_buffer.Buffer.from(buf.buffer).toString("base64");
18964
19107
  } else if (typeof btoa === "function") {
18965
19108
  let s = "";
18966
19109
  for (let i = 0; i < buf.length; ++i)
18967
19110
  s += String.fromCharCode(buf[i]);
18968
- str6 = btoa(s);
19111
+ str8 = btoa(s);
18969
19112
  } else {
18970
19113
  throw new Error("This environment does not support writing binary tags; either Buffer or btoa is required");
18971
19114
  }
18972
19115
  type ?? (type = Scalar.Scalar.BLOCK_LITERAL);
18973
19116
  if (type !== Scalar.Scalar.QUOTE_DOUBLE) {
18974
19117
  const lineWidth = Math.max(ctx.options.lineWidth - ctx.indent.length, ctx.options.minContentWidth);
18975
- const n = Math.ceil(str6.length / lineWidth);
19118
+ const n = Math.ceil(str8.length / lineWidth);
18976
19119
  const lines = new Array(n);
18977
19120
  for (let i = 0, o = 0; i < n; ++i, o += lineWidth) {
18978
- lines[i] = str6.substr(o, lineWidth);
19121
+ lines[i] = str8.substr(o, lineWidth);
18979
19122
  }
18980
- str6 = lines.join(type === Scalar.Scalar.BLOCK_LITERAL ? "\n" : " ");
19123
+ str8 = lines.join(type === Scalar.Scalar.BLOCK_LITERAL ? "\n" : " ");
18981
19124
  }
18982
- return stringifyString.stringifyString({ comment, type, value: str6 }, ctx, onComment, onChompKeep);
19125
+ return stringifyString.stringifyString({ comment, type, value: str8 }, ctx, onComment, onChompKeep);
18983
19126
  }
18984
19127
  };
18985
19128
  exports2.binary = binary;
@@ -19189,7 +19332,7 @@ var require_float2 = __commonJS({
19189
19332
  default: true,
19190
19333
  tag: "tag:yaml.org,2002:float",
19191
19334
  test: /^(?:[-+]?\.(?:inf|Inf|INF)|\.nan|\.NaN|\.NAN)$/,
19192
- resolve: (str6) => str6.slice(-3).toLowerCase() === "nan" ? NaN : str6[0] === "-" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY,
19335
+ resolve: (str8) => str8.slice(-3).toLowerCase() === "nan" ? NaN : str8[0] === "-" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY,
19193
19336
  stringify: stringifyNumber.stringifyNumber
19194
19337
  };
19195
19338
  var floatExp = {
@@ -19198,7 +19341,7 @@ var require_float2 = __commonJS({
19198
19341
  tag: "tag:yaml.org,2002:float",
19199
19342
  format: "EXP",
19200
19343
  test: /^[-+]?(?:[0-9][0-9_]*)?(?:\.[0-9_]*)?[eE][-+]?[0-9]+$/,
19201
- resolve: (str6) => parseFloat(str6.replace(/_/g, "")),
19344
+ resolve: (str8) => parseFloat(str8.replace(/_/g, "")),
19202
19345
  stringify(node) {
19203
19346
  const num2 = Number(node.value);
19204
19347
  return isFinite(num2) ? num2.toExponential() : stringifyNumber.stringifyNumber(node);
@@ -19209,11 +19352,11 @@ var require_float2 = __commonJS({
19209
19352
  default: true,
19210
19353
  tag: "tag:yaml.org,2002:float",
19211
19354
  test: /^[-+]?(?:[0-9][0-9_]*)?\.[0-9_]*$/,
19212
- resolve(str6) {
19213
- const node = new Scalar.Scalar(parseFloat(str6.replace(/_/g, "")));
19214
- const dot = str6.indexOf(".");
19355
+ resolve(str8) {
19356
+ const node = new Scalar.Scalar(parseFloat(str8.replace(/_/g, "")));
19357
+ const dot = str8.indexOf(".");
19215
19358
  if (dot !== -1) {
19216
- const f = str6.substring(dot + 1).replace(/_/g, "");
19359
+ const f = str8.substring(dot + 1).replace(/_/g, "");
19217
19360
  if (f[f.length - 1] === "0")
19218
19361
  node.minFractionDigits = f.length;
19219
19362
  }
@@ -19234,34 +19377,34 @@ var require_int2 = __commonJS({
19234
19377
  init_define_KT_HUB_FILES();
19235
19378
  var stringifyNumber = require_stringifyNumber();
19236
19379
  var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value);
19237
- function intResolve(str6, offset, radix, { intAsBigInt }) {
19238
- const sign3 = str6[0];
19239
- if (sign3 === "-" || sign3 === "+")
19380
+ function intResolve(str8, offset, radix, { intAsBigInt }) {
19381
+ const sign2 = str8[0];
19382
+ if (sign2 === "-" || sign2 === "+")
19240
19383
  offset += 1;
19241
- str6 = str6.substring(offset).replace(/_/g, "");
19384
+ str8 = str8.substring(offset).replace(/_/g, "");
19242
19385
  if (intAsBigInt) {
19243
19386
  switch (radix) {
19244
19387
  case 2:
19245
- str6 = `0b${str6}`;
19388
+ str8 = `0b${str8}`;
19246
19389
  break;
19247
19390
  case 8:
19248
- str6 = `0o${str6}`;
19391
+ str8 = `0o${str8}`;
19249
19392
  break;
19250
19393
  case 16:
19251
- str6 = `0x${str6}`;
19394
+ str8 = `0x${str8}`;
19252
19395
  break;
19253
19396
  }
19254
- const n2 = BigInt(str6);
19255
- return sign3 === "-" ? BigInt(-1) * n2 : n2;
19397
+ const n2 = BigInt(str8);
19398
+ return sign2 === "-" ? BigInt(-1) * n2 : n2;
19256
19399
  }
19257
- const n = parseInt(str6, radix);
19258
- return sign3 === "-" ? -1 * n : n;
19400
+ const n = parseInt(str8, radix);
19401
+ return sign2 === "-" ? -1 * n : n;
19259
19402
  }
19260
19403
  function intStringify(node, radix, prefix) {
19261
19404
  const { value } = node;
19262
19405
  if (intIdentify(value)) {
19263
- const str6 = value.toString(radix);
19264
- return value < 0 ? "-" + prefix + str6.substr(1) : prefix + str6;
19406
+ const str8 = value.toString(radix);
19407
+ return value < 0 ? "-" + prefix + str8.substr(1) : prefix + str8;
19265
19408
  }
19266
19409
  return stringifyNumber.stringifyNumber(node);
19267
19410
  }
@@ -19271,7 +19414,7 @@ var require_int2 = __commonJS({
19271
19414
  tag: "tag:yaml.org,2002:int",
19272
19415
  format: "BIN",
19273
19416
  test: /^[-+]?0b[0-1_]+$/,
19274
- resolve: (str6, _onError, opt) => intResolve(str6, 2, 2, opt),
19417
+ resolve: (str8, _onError, opt) => intResolve(str8, 2, 2, opt),
19275
19418
  stringify: (node) => intStringify(node, 2, "0b")
19276
19419
  };
19277
19420
  var intOct = {
@@ -19280,7 +19423,7 @@ var require_int2 = __commonJS({
19280
19423
  tag: "tag:yaml.org,2002:int",
19281
19424
  format: "OCT",
19282
19425
  test: /^[-+]?0[0-7_]+$/,
19283
- resolve: (str6, _onError, opt) => intResolve(str6, 1, 8, opt),
19426
+ resolve: (str8, _onError, opt) => intResolve(str8, 1, 8, opt),
19284
19427
  stringify: (node) => intStringify(node, 8, "0")
19285
19428
  };
19286
19429
  var int = {
@@ -19288,7 +19431,7 @@ var require_int2 = __commonJS({
19288
19431
  default: true,
19289
19432
  tag: "tag:yaml.org,2002:int",
19290
19433
  test: /^[-+]?[0-9][0-9_]*$/,
19291
- resolve: (str6, _onError, opt) => intResolve(str6, 0, 10, opt),
19434
+ resolve: (str8, _onError, opt) => intResolve(str8, 0, 10, opt),
19292
19435
  stringify: stringifyNumber.stringifyNumber
19293
19436
  };
19294
19437
  var intHex = {
@@ -19297,7 +19440,7 @@ var require_int2 = __commonJS({
19297
19440
  tag: "tag:yaml.org,2002:int",
19298
19441
  format: "HEX",
19299
19442
  test: /^[-+]?0x[0-9a-fA-F_]+$/,
19300
- resolve: (str6, _onError, opt) => intResolve(str6, 2, 16, opt),
19443
+ resolve: (str8, _onError, opt) => intResolve(str8, 2, 16, opt),
19301
19444
  stringify: (node) => intStringify(node, 16, "0x")
19302
19445
  };
19303
19446
  exports2.int = int;
@@ -19403,12 +19546,12 @@ var require_timestamp = __commonJS({
19403
19546
  "use strict";
19404
19547
  init_define_KT_HUB_FILES();
19405
19548
  var stringifyNumber = require_stringifyNumber();
19406
- function parseSexagesimal(str6, asBigInt) {
19407
- const sign3 = str6[0];
19408
- const parts = sign3 === "-" || sign3 === "+" ? str6.substring(1) : str6;
19549
+ function parseSexagesimal(str8, asBigInt) {
19550
+ const sign2 = str8[0];
19551
+ const parts = sign2 === "-" || sign2 === "+" ? str8.substring(1) : str8;
19409
19552
  const num2 = (n) => asBigInt ? BigInt(n) : Number(n);
19410
19553
  const res = parts.replace(/_/g, "").split(":").reduce((res2, p) => res2 * num2(60) + num2(p), num2(0));
19411
- return sign3 === "-" ? num2(-1) * res : res;
19554
+ return sign2 === "-" ? num2(-1) * res : res;
19412
19555
  }
19413
19556
  function stringifySexagesimal(node) {
19414
19557
  let { value } = node;
@@ -19417,9 +19560,9 @@ var require_timestamp = __commonJS({
19417
19560
  num2 = (n) => BigInt(n);
19418
19561
  else if (isNaN(value) || !isFinite(value))
19419
19562
  return stringifyNumber.stringifyNumber(node);
19420
- let sign3 = "";
19563
+ let sign2 = "";
19421
19564
  if (value < 0) {
19422
- sign3 = "-";
19565
+ sign2 = "-";
19423
19566
  value *= num2(-1);
19424
19567
  }
19425
19568
  const _60 = num2(60);
@@ -19434,7 +19577,7 @@ var require_timestamp = __commonJS({
19434
19577
  parts.unshift(value);
19435
19578
  }
19436
19579
  }
19437
- return sign3 + parts.map((n) => String(n).padStart(2, "0")).join(":").replace(/000000\d*$/, "");
19580
+ return sign2 + parts.map((n) => String(n).padStart(2, "0")).join(":").replace(/000000\d*$/, "");
19438
19581
  }
19439
19582
  var intTime = {
19440
19583
  identify: (value) => typeof value === "bigint" || Number.isInteger(value),
@@ -19442,7 +19585,7 @@ var require_timestamp = __commonJS({
19442
19585
  tag: "tag:yaml.org,2002:int",
19443
19586
  format: "TIME",
19444
19587
  test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+$/,
19445
- resolve: (str6, _onError, { intAsBigInt }) => parseSexagesimal(str6, intAsBigInt),
19588
+ resolve: (str8, _onError, { intAsBigInt }) => parseSexagesimal(str8, intAsBigInt),
19446
19589
  stringify: stringifySexagesimal
19447
19590
  };
19448
19591
  var floatTime = {
@@ -19451,7 +19594,7 @@ var require_timestamp = __commonJS({
19451
19594
  tag: "tag:yaml.org,2002:float",
19452
19595
  format: "TIME",
19453
19596
  test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*$/,
19454
- resolve: (str6) => parseSexagesimal(str6, false),
19597
+ resolve: (str8) => parseSexagesimal(str8, false),
19455
19598
  stringify: stringifySexagesimal
19456
19599
  };
19457
19600
  var timestamp = {
@@ -19462,8 +19605,8 @@ var require_timestamp = __commonJS({
19462
19605
  // may be omitted altogether, resulting in a date format. In such a case, the time part is
19463
19606
  // assumed to be 00:00:00Z (start of day, UTC).
19464
19607
  test: RegExp("^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})(?:(?:t|T|[ \\t]+)([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}(\\.[0-9]+)?)(?:[ \\t]*(Z|[-+][012]?[0-9](?::[0-9]{2})?))?)?$"),
19465
- resolve(str6) {
19466
- const match = str6.match(timestamp.test);
19608
+ resolve(str8) {
19609
+ const match = str8.match(timestamp.test);
19467
19610
  if (!match)
19468
19611
  throw new Error("!!timestamp expects a date, starting with yyyy-mm-dd");
19469
19612
  const [, year, month, day, hour, minute, second] = match.map(Number);
@@ -24060,10 +24203,10 @@ function hexToBytes(hex) {
24060
24203
  }
24061
24204
  var nextTick = async () => {
24062
24205
  };
24063
- function utf8ToBytes(str6) {
24064
- if (typeof str6 !== "string")
24206
+ function utf8ToBytes(str8) {
24207
+ if (typeof str8 !== "string")
24065
24208
  throw new TypeError("string expected");
24066
- return new Uint8Array(new TextEncoder().encode(str6));
24209
+ return new Uint8Array(new TextEncoder().encode(str8));
24067
24210
  }
24068
24211
  function kdfInputToBytes(data, errorTitle = "") {
24069
24212
  if (typeof data === "string")
@@ -25676,7 +25819,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
25676
25819
  bits2int_modN: "function"
25677
25820
  });
25678
25821
  ecdsaOpts = Object.assign({}, ecdsaOpts);
25679
- const randomBytes7 = ecdsaOpts.randomBytes === void 0 ? randomBytes2 : ecdsaOpts.randomBytes;
25822
+ const randomBytes6 = ecdsaOpts.randomBytes === void 0 ? randomBytes2 : ecdsaOpts.randomBytes;
25680
25823
  const hmac2 = ecdsaOpts.hmac === void 0 ? (key, msg2) => hmac(hash_, key, msg2) : ecdsaOpts.hmac;
25681
25824
  const { Fp, Fn } = Point;
25682
25825
  const { ORDER: CURVE_ORDER, BITS: fnBits } = Fn;
@@ -25822,7 +25965,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
25822
25965
  throw new Error("invalid private key");
25823
25966
  const seedArgs = [int2octets(d), int2octets(h1int)];
25824
25967
  if (extraEntropy != null && extraEntropy !== false) {
25825
- const e = extraEntropy === true ? randomBytes7(lengths.secretKey) : extraEntropy;
25968
+ const e = extraEntropy === true ? randomBytes6(lengths.secretKey) : extraEntropy;
25826
25969
  seedArgs.push(abytes2(e, void 0, "extraEntropy"));
25827
25970
  }
25828
25971
  const seed = concatBytes2(...seedArgs);
@@ -25849,7 +25992,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
25849
25992
  }
25850
25993
  return { seed, k2sig };
25851
25994
  }
25852
- function sign3(message, secretKey, opts2 = {}) {
25995
+ function sign2(message, secretKey, opts2 = {}) {
25853
25996
  const { seed, k2sig } = prepSig(message, secretKey, opts2);
25854
25997
  const drbg = createHmacDrbg(hash_.outputLen, Fn.BYTES, hmac2);
25855
25998
  const sig = drbg(seed, k2sig);
@@ -25895,7 +26038,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
25895
26038
  utils,
25896
26039
  lengths,
25897
26040
  Point,
25898
- sign: sign3,
26041
+ sign: sign2,
25899
26042
  verify,
25900
26043
  recoverPublicKey,
25901
26044
  Signature,
@@ -28317,7 +28460,15 @@ var KutrionClient = class {
28317
28460
  }
28318
28461
  // Rotate tokens explicitly (req() also does this transparently on 401).
28319
28462
  async refreshTokens(refreshToken) {
28320
- return this.req("POST", "/auth/refresh", await this.signedRefreshBody(refreshToken), false);
28463
+ const { body, signerThrew } = await this.signedRefreshBody(refreshToken);
28464
+ try {
28465
+ return await this.req("POST", "/auth/refresh", body, false);
28466
+ } catch (e) {
28467
+ if (signerThrew && !body.signature && e instanceof ApiError && e.status === 401 && isRefreshSignatureRefusal(e.body)) {
28468
+ throw new ApiError(0, "signer_unavailable");
28469
+ }
28470
+ throw e;
28471
+ }
28321
28472
  }
28322
28473
  refreshBody(refreshToken) {
28323
28474
  return this.refreshClaim ? { refresh_token: refreshToken, client: this.refreshClaim } : { refresh_token: refreshToken };
@@ -28462,8 +28613,8 @@ var KutrionClient = class {
28462
28613
  // --- step-up (MT-3 #117): lift a social-login session's vault gate by proving a 2nd factor.
28463
28614
  // Returns a fresh, unlocked token (swapped in via onToken). Passkey holders can instead complete
28464
28615
  // a passkey login, which also mints an unlocked token.
28465
- async stepUpTotp(code) {
28466
- const r = await this.req("POST", "/auth/step-up/totp", { code });
28616
+ async stepUpTotp(code, approvalId) {
28617
+ const r = await this.req("POST", "/auth/step-up/totp", approvalId ? { code, approval_id: approvalId } : { code });
28467
28618
  this.onToken?.(r.token);
28468
28619
  return r;
28469
28620
  }
@@ -28471,11 +28622,23 @@ var KutrionClient = class {
28471
28622
  webauthnStepUpOptions() {
28472
28623
  return this.req("POST", "/auth/webauthn/step-up/options");
28473
28624
  }
28474
- async webauthnStepUpVerify(response) {
28475
- const r = await this.req("POST", "/auth/webauthn/step-up/verify", { response });
28625
+ async webauthnStepUpVerify(response, approvalId) {
28626
+ const r = await this.req("POST", "/auth/webauthn/step-up/verify", approvalId ? { response, approval_id: approvalId } : { response });
28476
28627
  this.onToken?.(r.token);
28477
28628
  return r;
28478
28629
  }
28630
+ // #3301: the new-Hub approval page (server/src/modules/hub-keys.ts). The read returns only what the
28631
+ // approval's own session may see; an unknown, spent or foreign id is a 404 or `{status:"expired"}`.
28632
+ hubApprovalRead(id) {
28633
+ return this.req("GET", `/auth/hub-channel-keys/approvals/${encodeURIComponent(id)}`);
28634
+ }
28635
+ hubApprovalPassword(id, password) {
28636
+ return this.req("POST", `/auth/hub-channel-keys/approvals/${encodeURIComponent(id)}/password`, { password });
28637
+ }
28638
+ // Idempotent for a confirmed approval of the same session, so the page calls it straight after confirm.
28639
+ hubApprovalComplete(id) {
28640
+ return this.req("POST", `/auth/hub-channel-keys/approvals/${encodeURIComponent(id)}/complete`);
28641
+ }
28479
28642
  // #1723: the browser half of the desktop step-up handoff. The ceremony above has already run and
28480
28643
  // recorded the proof; this hands the request id back and gets the app's custom-scheme return URL.
28481
28644
  // The proof must be newer than the request, so it is only ever called after a verify.
@@ -28708,6 +28871,9 @@ var KutrionClient = class {
28708
28871
  credentialHealth() {
28709
28872
  return this.req("GET", "/vault/health");
28710
28873
  }
28874
+ credentialHealthSummary() {
28875
+ return this.req("GET", "/vault/health/summary");
28876
+ }
28711
28877
  // #745: duplicate credentials, found server-side across every vault the caller can reach. No secret
28712
28878
  // is returned — members carry an opaque secretGroup label so a client can show which ones agree.
28713
28879
  credentialDuplicates() {
@@ -28801,7 +28967,7 @@ var KutrionClient = class {
28801
28967
  }
28802
28968
  /** enroll: SSH rows with a username and no password are created with no credential, for a bulk enrollment (#2635). */
28803
28969
  commitConnImport(vaultId, connections, opts2 = {}) {
28804
- return this.req("POST", "/connection-import/commit", { vault_id: vaultId, connections, ...opts2.enroll ? { enroll: true } : {} });
28970
+ return this.req("POST", "/connection-import/commit", { vault_id: vaultId, connections, ...opts2.enroll ? { enroll: true } : {}, ...opts2.reach_via ? { reach_via: opts2.reach_via } : {} });
28805
28971
  }
28806
28972
  // Raw text download of a provider's sample/template file (carries the auth token).
28807
28973
  async importTemplate(provider) {
@@ -29492,6 +29658,54 @@ var KutrionClient = class {
29492
29658
  { scope, resource_id: resourceId }
29493
29659
  );
29494
29660
  }
29661
+ // --- #3264: port-forward allow-list. Owners and admins only. Opening the policy and adding a target
29662
+ // answer 403 step_up_required until a fresh check; 409 port_forward_target_exists / port_forward_target_limit. ---
29663
+ getPortForwardPolicy() {
29664
+ return this.req("GET", "/org/port-forward-policy");
29665
+ }
29666
+ setPortForwardPolicy(policy) {
29667
+ return this.req("PUT", "/org/port-forward-policy", { policy });
29668
+ }
29669
+ listPortForwardTargets() {
29670
+ return this.req("GET", "/org/port-forward-targets");
29671
+ }
29672
+ addPortForwardTarget(input) {
29673
+ return this.req("POST", "/org/port-forward-targets", input);
29674
+ }
29675
+ removePortForwardTarget(id) {
29676
+ return this.req("DELETE", `/org/port-forward-targets/${encodeURIComponent(id)}`);
29677
+ }
29678
+ // --- #3311: connectors. Org owners and admins only; every route is a 404 where the deployment has the
29679
+ // tunnel off, a 402 `feature: "connectors"` at the plan's cap. ---
29680
+ listConnectors() {
29681
+ return this.req("GET", "/connectors");
29682
+ }
29683
+ getConnector(id) {
29684
+ return this.req("GET", `/connectors/${encodeURIComponent(id)}`);
29685
+ }
29686
+ createConnector(name) {
29687
+ return this.req("POST", "/connectors", { name });
29688
+ }
29689
+ /** 403 step_up_required until a fresh check; 402 when reviving a revoked connector would pass the cap. */
29690
+ issueConnectorEnrolmentToken(id) {
29691
+ return this.req("POST", `/connectors/${encodeURIComponent(id)}/enrolment-token`);
29692
+ }
29693
+ revokeConnector(id) {
29694
+ return this.req("POST", `/connectors/${encodeURIComponent(id)}/revoke`);
29695
+ }
29696
+ /** 400 invalid_route lists problems by index; 409 wide_cidr_acknowledgement_required lists `wide` (resend with acknowledge_wide); 409 connector_revoked. */
29697
+ getConnectorRoutes(id) {
29698
+ return this.req("GET", `/connectors/${encodeURIComponent(id)}/routes`);
29699
+ }
29700
+ /** Replaces the whole set. */
29701
+ setConnectorRoutes(id, routes, acknowledgeWide = false) {
29702
+ return this.req("PUT", `/connectors/${encodeURIComponent(id)}/routes`, { routes, acknowledge_wide: acknowledgeWide });
29703
+ }
29704
+ /** Dry run by default. `confirm: true` writes the pins and needs a fresh step-up (403 step_up_required).
29705
+ * Fingerprints come only from the caller's input; nothing observed through the connector is returned. */
29706
+ pinConnectorHostKeys(id, input) {
29707
+ return this.req("POST", `/connectors/${encodeURIComponent(id)}/host-keys`, input);
29708
+ }
29495
29709
  // --- SSH host-key pins (#951 records them, #959 makes them answerable) ---
29496
29710
  // Read and discard only, org admin/owner. There is deliberately no create or update: nothing in the
29497
29711
  // product accepts a fingerprint from a human, because a pin means "this is what the host actually
@@ -29504,8 +29718,9 @@ var KutrionClient = class {
29504
29718
  return this.req("DELETE", `/ssh/host-keys/${id}`);
29505
29719
  }
29506
29720
  /**
29507
- * #2743: pins the key a host_key_unconfirmed refusal showed this caller. The server accepts only the
29508
- * fingerprint it observed for this caller, so this is not a way to type one in. 403 step_up_required
29721
+ * #2743: pins the key a host_key_unconfirmed refusal showed this caller. For a first-use refusal the server accepts only the
29722
+ * fingerprint it observed for this caller; for a host behind a jump chain (#3336) it holds no observed
29723
+ * key, so an organization admin passes the fingerprint read from the host itself and the server checks it. 403 step_up_required
29509
29724
  * or host_key_confirm_forbidden; 409 host_key_not_observed, host_key_fingerprint_mismatch or
29510
29725
  * host_key_mismatch.
29511
29726
  */
@@ -29934,7 +30149,7 @@ var KutrionClient = class {
29934
30149
  // #1687: the streaming counterparts of cloudDownload/cloudUpload, so a cloud node can be one side of
29935
30150
  // a brokered transfer without the bytes being materialised. Same helpers as the sftp/file pairs.
29936
30151
  //
29937
- // GET /cloud/connections/:id/download (server/src/modules/cloud.ts:438) takes the same `?redact=1`
30152
+ // GET /cloud/connections/:id/download (server/src/modules/cloud.ts:446) takes the same `?redact=1`
29938
30153
  // and the same open-ended `Range: bytes=<start>-` as the file route, and answers 206 with
29939
30154
  // Content-Range when it resumes. A BOUNDED range is refused outright by refuseUnsupportedRange
29940
30155
  // (cloud.ts:453) — downloadStream only ever sends the open-ended form, so that refusal is
@@ -30917,6 +31132,8 @@ var SSH_REFUSAL_CLASSIFICATION = {
30917
31132
  unsupported_credential: { family: "refused", retry: "pointless" },
30918
31133
  // The connection's port is not one the broker will dial.
30919
31134
  bad_port: { family: "refused", retry: "pointless" },
31135
+ // The org's port-forward allow-list does not list the target; it stays unlisted until an admin adds it.
31136
+ forward_target_refused: { family: "refused", retry: "pointless" },
30920
31137
  // The client presented none; reconnecting presents none again.
30921
31138
  missing_join_token: { family: "refused", retry: "pointless" },
30922
31139
  // The same token is presented again and rejected again.
@@ -30932,6 +31149,8 @@ var SSH_REFUSAL_CLASSIFICATION = {
30932
31149
  connection_unavailable: { family: "refused", retry: "pointless" },
30933
31150
  // Same as not_found, for the credential the connection names.
30934
31151
  credential_not_found: { family: "refused", retry: "pointless" },
31152
+ // #3386: a temporary credential past its expiry; the same attempt earns the same refusal.
31153
+ credential_expired: { family: "refused", retry: "pointless" },
30935
31154
  // #1949: this app does not own the surface and the user holds no live grant for it. "pointless" —
30936
31155
  // the same token from the same app earns the identical refusal, and the remedy is elsewhere: open
30937
31156
  // the app that owns the surface, or have an administrator grant access from this one.
@@ -31040,10 +31259,48 @@ var HOST_KEY_CONFIRM_COPY = {
31040
31259
  forbidden: "Only an organization admin, or an editor of the vault that holds this connection, can trust a new host key. Ask one of them to confirm it; they should check the fingerprint above against the host itself first.",
31041
31260
  notThisConnection: "This key is not for the host this connection opens. It belongs to a jump host on the way, or Kutrion could not tell which host it belongs to, so it cannot be trusted from this terminal. Open that host's own connection and confirm its key there; that needs an organization admin, or an editor of the vault that holds that connection."
31042
31261
  };
31262
+ var str3 = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
31043
31263
  var pinHost = (h) => h.trim().toLowerCase().replace(/\.$/, "");
31044
31264
  function isOwnHost(pending, own) {
31045
31265
  return !!own?.host && own.port !== void 0 && pending.port !== void 0 && pending.port !== null && pending.port === Number(own.port) && pinHost(pending.host) === pinHost(own.host);
31046
31266
  }
31267
+ var HOST_KEY_CHAINED_COPY = {
31268
+ heading: "Confirm this host's identity before connecting",
31269
+ body: "This host is reached through a jump host, and its key has not been confirmed yet, so Kutrion paused before signing in and did not send your credential. Confirming records this key for the whole organization; every later connection must match it.",
31270
+ help: "Read the fingerprint on the host itself, not through the jump host. Run this command on the host (it shows the Ed25519 key; use the matching file if the host uses another key type):",
31271
+ command: "ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub",
31272
+ fingerprintLabel: "Fingerprint from the host",
31273
+ fingerprintHint: "Enter the value that starts with SHA256:",
31274
+ invalid: "That is not a fingerprint. A fingerprint starts with SHA256: followed by 43 characters.",
31275
+ confirm: "Confirm and trust this host",
31276
+ confirming: "Confirming",
31277
+ nonAdmin: "Only an organization admin can confirm this host's key. Ask one to read the fingerprint on the host and confirm it.",
31278
+ mismatch: "That fingerprint does not match. Read it again on the host itself and try again.",
31279
+ expired: "The time to confirm has run out. Connect again, then confirm.",
31280
+ failed: "Could not confirm this host key.",
31281
+ checkingRole: "Checking your role\u2026",
31282
+ notThisConnection: "Open this host's own connection to confirm its key.",
31283
+ banner: "Connection paused: this host's key has not been confirmed."
31284
+ };
31285
+ var HOST_KEY_FINGERPRINT_PATTERN = /SHA256:[A-Za-z0-9+/]{43}(?![A-Za-z0-9+/=])/;
31286
+ var HOST_KEY_FINGERPRINT_EXACT = /^SHA256:[A-Za-z0-9+/]{43}$/;
31287
+ function extractHostKeyFingerprint(input) {
31288
+ return input.match(HOST_KEY_FINGERPRINT_PATTERN)?.[0] ?? null;
31289
+ }
31290
+ function hostKeyChainedFromFrame(frame) {
31291
+ if (frame?.code !== "host_key_unconfirmed" || !frame.detail || typeof frame.detail !== "object") return null;
31292
+ const d = frame.detail;
31293
+ const host2 = str3(d.host);
31294
+ if (!host2 || str3(d.presented_fingerprint) || str3(d.connector_id)) return null;
31295
+ const port = typeof d.port === "number" ? d.port : void 0;
31296
+ return { target: port === void 0 ? host2 : `${host2}:${port}`, host: host2, port };
31297
+ }
31298
+ function hostKeyConfirmFailure(code) {
31299
+ if (code === "host_key_confirm_forbidden" || code === "forbidden") return "forbidden";
31300
+ if (code === "host_key_fingerprint_mismatch" || code === "host_key_mismatch") return "mismatch";
31301
+ if (code === "host_key_not_observed") return "expired";
31302
+ return "other";
31303
+ }
31047
31304
 
31048
31305
  // ../packages/shared/src/conn-import-plan.ts
31049
31306
  init_define_KT_HUB_FILES();
@@ -31051,6 +31308,18 @@ function connImportNeedsCredential(row) {
31051
31308
  return !row.username || !row.password && !row.credential_id && !(row.enroll && row.protocol === "SFTP");
31052
31309
  }
31053
31310
  var CONN_IMPORT_NO_CREDENTIAL_HINT = "Will not import: this export carries no password. Enter a username and password to import it.";
31311
+ var CONN_IMPORT_BINDING_COPY = {
31312
+ title: "Import result",
31313
+ boundHeading: "Credentials now limited to these hosts",
31314
+ bound: (credential, n) => `${credential} is now limited to these ${n} host${n === 1 ? "" : "s"}:`,
31315
+ needsHeading: "Needs a credential binding",
31316
+ needsIntro: (n) => `${n} host${n === 1 ? "" : "s"} will be refused until a vault editor adds ${n === 1 ? "it" : "them"} to the credential.`,
31317
+ needsRow: (name, dest) => `${name} (${dest}) will be refused until a vault editor adds this host to the credential.`,
31318
+ needsWhere: "A vault editor can add the host to the credential's destinations in the Kutrion web app.",
31319
+ done: "Done",
31320
+ next: "Continue"
31321
+ };
31322
+ var connImportDestLabel = (d) => `${d.protocol} ${d.host}:${d.port}`;
31054
31323
 
31055
31324
  // ../packages/shared/src/jump-refusal.ts
31056
31325
  init_define_KT_HUB_FILES();
@@ -31067,11 +31336,11 @@ function isJumpRefusalCode(code) {
31067
31336
  return !!code && JUMP_REFUSAL_CODES.includes(code);
31068
31337
  }
31069
31338
  var MAX_JUMP_HOPS = 4;
31070
- var str3 = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
31339
+ var str4 = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
31071
31340
  function parseJumpRefusal(body) {
31072
31341
  if (!body || typeof body !== "object") return null;
31073
31342
  const b = body;
31074
- const code = str3(b.code) ?? str3(b.error);
31343
+ const code = str4(b.code) ?? str4(b.error);
31075
31344
  if (!isJumpRefusalCode(code)) return null;
31076
31345
  const nested = b.detail && typeof b.detail === "object" ? b.detail : {};
31077
31346
  const pick2 = (k) => b[k] !== void 0 ? b[k] : nested[k];
@@ -31079,14 +31348,14 @@ function parseJumpRefusal(body) {
31079
31348
  const allowed = pick2("allowed_destinations");
31080
31349
  return {
31081
31350
  code,
31082
- hopConnectionId: str3(pick2("hop_connection_id")),
31351
+ hopConnectionId: str4(pick2("hop_connection_id")),
31083
31352
  hopIndex: typeof idx === "number" && Number.isFinite(idx) ? idx : null,
31084
- detail: typeof b.detail === "string" ? str3(b.detail) : str3(nested.detail) ?? str3(b.error_description),
31353
+ detail: typeof b.detail === "string" ? str4(b.detail) : str4(nested.detail) ?? str4(b.error_description),
31085
31354
  allowedDestinations: Array.isArray(allowed) ? allowed.filter((x) => typeof x === "string" && x.trim() !== "") : null
31086
31355
  };
31087
31356
  }
31088
31357
  function hopName(r, names) {
31089
- return r.hopConnectionId ? str3(names(r.hopConnectionId)) : null;
31358
+ return r.hopConnectionId ? str4(names(r.hopConnectionId)) : null;
31090
31359
  }
31091
31360
  function jumpHopLabel(r, names) {
31092
31361
  const name = hopName(r, names);
@@ -31098,7 +31367,7 @@ function jumpHopPossessive(r, names) {
31098
31367
  }
31099
31368
  function jumpRefusalSummary(r, names, openedName) {
31100
31369
  const name = hopName(r, names);
31101
- const opened = str3(openedName);
31370
+ const opened = str4(openedName);
31102
31371
  const target = opened ? `\u201C${opened}\u201D` : "this connection";
31103
31372
  const where = r.hopIndex !== null && r.hopIndex > 0 ? ` at jump host ${r.hopIndex + 1} of the chain,` : "";
31104
31373
  const opening = `Kutrion did not reach ${target}. It was refused${where} one hop earlier,`;
@@ -31130,14 +31399,14 @@ function jumpAllowedDestinationsLabel(r, names) {
31130
31399
  function parseJumpHostInUse(body) {
31131
31400
  if (!body || typeof body !== "object") return null;
31132
31401
  const b = body;
31133
- if (str3(b.error) !== "jump_host_in_use") return null;
31402
+ if (str4(b.error) !== "jump_host_in_use") return null;
31134
31403
  const rows = Array.isArray(b.used_by) ? b.used_by : [];
31135
- const usedBy = rows.filter((x) => !!x && typeof x === "object").map((x) => ({ id: str3(x.id) ?? "", name: str3(x.name) ?? "" })).filter((x) => x.id && x.name);
31404
+ const usedBy = rows.filter((x) => !!x && typeof x === "object").map((x) => ({ id: str4(x.id) ?? "", name: str4(x.name) ?? "" })).filter((x) => x.id && x.name);
31136
31405
  return { usedBy };
31137
31406
  }
31138
31407
  var JUMP_HOST_IN_USE_TITLE = "Other connections are reached through this one";
31139
31408
  function jumpHostInUseSummary(r, deletingName) {
31140
- const name = str3(deletingName);
31409
+ const name = str4(deletingName);
31141
31410
  const subject2 = name ? `\u201C${name}\u201D` : "this connection";
31142
31411
  const n = r.usedBy.length;
31143
31412
  if (!n) {
@@ -33163,11 +33432,9 @@ function osKeychainAvailable() {
33163
33432
  }
33164
33433
  }
33165
33434
 
33166
- // src/app-signin.ts
33435
+ // src/hub-seed-login.ts
33167
33436
  init_define_KT_HUB_FILES();
33168
- var import_node_crypto4 = require("node:crypto");
33169
- var import_node_child_process = require("node:child_process");
33170
- var import_node_http = require("node:http");
33437
+ var import_os3 = require("os");
33171
33438
 
33172
33439
  // src/device-identity.ts
33173
33440
  init_define_KT_HUB_FILES();
@@ -33206,90 +33473,6 @@ async function bindRefreshKey(deviceId, devicePkcs8B64, certMessage, transport)
33206
33473
  return transport.unlock({ device_id: deviceId, signature, refresh_key: { spki, certification, hardware: false } });
33207
33474
  }
33208
33475
 
33209
- // src/hub/borrow-confirm.ts
33210
- init_define_KT_HUB_FILES();
33211
- var import_node_url = require("node:url");
33212
- var text = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
33213
- var SCRIPTS = [
33214
- ["latin", new RegExp("\\p{Script=Latin}", "u")],
33215
- ["cyrillic", new RegExp("\\p{Script=Cyrillic}", "u")],
33216
- ["greek", new RegExp("\\p{Script=Greek}", "u")],
33217
- ["armenian", new RegExp("\\p{Script=Armenian}", "u")],
33218
- ["cjk", /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u],
33219
- ["hangul", new RegExp("\\p{Script=Hangul}", "u")],
33220
- ["arabic", new RegExp("\\p{Script=Arabic}", "u")],
33221
- ["hebrew", new RegExp("\\p{Script=Hebrew}", "u")]
33222
- ];
33223
- function mixesScripts(s) {
33224
- const seen = /* @__PURE__ */ new Set();
33225
- for (const ch of s) {
33226
- if (!new RegExp("\\p{L}", "u").test(ch)) continue;
33227
- seen.add(SCRIPTS.find(([, re]) => re.test(ch))?.[0] ?? "other");
33228
- }
33229
- return seen.size > 1;
33230
- }
33231
- function joinedAccount(user, lastAccount) {
33232
- const at = user.email.lastIndexOf("@");
33233
- const localPart = at >= 0 ? user.email.slice(0, at) : user.email;
33234
- const raw = at >= 0 ? user.email.slice(at + 1) : "";
33235
- const ascii = raw ? (0, import_node_url.domainToASCII)(raw.toLowerCase()) || raw : "";
33236
- return {
33237
- email: user.email,
33238
- localPart,
33239
- domain: ascii,
33240
- internationalDomain: ascii !== raw.toLowerCase(),
33241
- mixedScript: mixesScripts(localPart),
33242
- displayName: text(user.display_name),
33243
- orgName: text(user.org_name),
33244
- accountChanged: lastAccount !== null && lastAccount !== user.id
33245
- };
33246
- }
33247
- function createBorrowConfirm(deps) {
33248
- let account = null;
33249
- let heldId = null;
33250
- let heldName = null;
33251
- const remember = (user) => {
33252
- if (deps.lastAccount.get() !== user.id) deps.lastAccount.set(user.id);
33253
- };
33254
- const reject = async () => {
33255
- account = null;
33256
- heldId = null;
33257
- heldName = null;
33258
- const held = deps.borrow.discard();
33259
- if (!held) return;
33260
- deps.pin.clear();
33261
- await deps.revoke(held.deviceId, held.bearer).catch(() => void 0);
33262
- };
33263
- return {
33264
- hold(user, deviceId, deviceName) {
33265
- account = user;
33266
- heldId = deviceId;
33267
- heldName = deviceName ?? null;
33268
- },
33269
- pending: () => account ? { ...joinedAccount(account, deps.lastAccount.get()), deviceName: heldName } : null,
33270
- seen(user) {
33271
- if (!account && user) remember(user);
33272
- },
33273
- async confirm() {
33274
- const user = account;
33275
- const id = heldId;
33276
- if (!user || !id) return false;
33277
- if (!deps.borrow.commit(id)) {
33278
- await reject();
33279
- return false;
33280
- }
33281
- account = null;
33282
- heldId = null;
33283
- remember(user);
33284
- return true;
33285
- },
33286
- reject,
33287
- forgetHub() {
33288
- deps.pin.clear();
33289
- }
33290
- };
33291
- }
33292
-
33293
33476
  // src/hub-login.ts
33294
33477
  init_define_KT_HUB_FILES();
33295
33478
  var import_os2 = require("os");
@@ -33772,16 +33955,19 @@ async function redeemEnrolment(p) {
33772
33955
  headers: { "Content-Type": "application/json", Accept: "application/json" },
33773
33956
  // #2975: this build asks the user to confirm a first-contact account, and says so, so a server
33774
33957
  // that refuses builds without that step can tell the two apart.
33775
- body: JSON.stringify({ code: p.code, signature, audience, capabilities: ["i1"] }),
33958
+ body: JSON.stringify({ code: p.code, signature, audience, capabilities: ["i1"], hub_pk: p.hubPk }),
33776
33959
  signal: AbortSignal.timeout(15e3)
33777
33960
  });
33961
+ p.latch?.observe(audience, res.headers);
33962
+ const parsed = await res.json().catch(() => null);
33778
33963
  if (!res.ok) {
33779
33964
  if (res.status !== 403) return null;
33780
- const refusal = await res.json().catch(() => null);
33781
- return refusal?.error === "device_clients_disabled" || refusal?.error === "satellite_update_required" ? refusal.error : null;
33965
+ const refusal = parsed;
33966
+ return refusal?.error === "device_clients_disabled" || refusal?.error === "satellite_update_required" || refusal?.error === "hub_not_bound" ? refusal.error : null;
33782
33967
  }
33783
- const body = await res.json();
33968
+ const body = parsed ?? {};
33784
33969
  if (!body.device?.id || !body.access_token || !body.refresh_token) return null;
33970
+ p.latch?.tokenIssued(audience, res, body);
33785
33971
  return {
33786
33972
  deviceId: body.device.id,
33787
33973
  deviceName: typeof body.device.name === "string" ? body.device.name : null,
@@ -33836,9 +34022,11 @@ async function borrowAndEnrol(deps) {
33836
34022
  apiBase: deps.apiBase,
33837
34023
  identity: deps.identity,
33838
34024
  code: borrowed.code,
34025
+ hubPk: borrowed.hubPublicKey,
34026
+ latch: deps.latch,
33839
34027
  fetchImpl: deps.fetchImpl
33840
34028
  });
33841
- if (enrolment === "device_clients_disabled" || enrolment === "satellite_update_required") return { ok: false, reason: enrolment };
34029
+ if (enrolment === "device_clients_disabled" || enrolment === "satellite_update_required" || enrolment === "hub_not_bound") return { ok: false, reason: enrolment };
33842
34030
  return enrolment ? { ok: true, enrolment, hubPublicKey: borrowed.hubPublicKey } : { ok: false, reason: "enrol_refused" };
33843
34031
  }
33844
34032
  function createBorrowSession(store, client, resolveDeps) {
@@ -33907,6 +34095,191 @@ function createBorrowSession(store, client, resolveDeps) {
33907
34095
  };
33908
34096
  }
33909
34097
 
34098
+ // src/hub/hub-key.ts
34099
+ init_define_KT_HUB_FILES();
34100
+ var FEATURES_HEADER = "kutrion-features";
34101
+ var HUB_KEY_FEATURE = "hub-key-check";
34102
+ var HUB_KEY_COPY = {
34103
+ notVouched: "Kutrion could not be confirmed for your account. Sign in to the Kutrion app directly.",
34104
+ notBound: "Sign in to the Kutrion app again."
34105
+ };
34106
+ function advertisesHubKeyCheck(headers) {
34107
+ let raw;
34108
+ try {
34109
+ raw = headers?.get(FEATURES_HEADER);
34110
+ } catch {
34111
+ return false;
34112
+ }
34113
+ return !!raw && raw.split(",").some((v) => v.trim().toLowerCase() === HUB_KEY_FEATURE);
34114
+ }
34115
+ function isApiNotFound(status, body) {
34116
+ return status === 404 && body?.error === "not_found";
34117
+ }
34118
+ var str5 = (v) => typeof v === "string" && v.length > 0;
34119
+ function issuesTokens(body) {
34120
+ const o = body && typeof body === "object" ? body : null;
34121
+ const device = o?.device && typeof o.device === "object" ? o.device : null;
34122
+ return !!o && str5(device?.id) && str5(o.access_token) && str5(o.refresh_token);
34123
+ }
34124
+ function createFeatureLatch(store, log3) {
34125
+ const read2 = () => {
34126
+ try {
34127
+ return store.read();
34128
+ } catch {
34129
+ return null;
34130
+ }
34131
+ };
34132
+ const write2 = (origin) => {
34133
+ try {
34134
+ store.write(origin);
34135
+ } catch {
34136
+ log3("[hub-key] could not persist the feature latch");
34137
+ }
34138
+ };
34139
+ const observe = (origin, headers) => {
34140
+ if (advertisesHubKeyCheck(headers) && read2() !== origin) write2(origin);
34141
+ };
34142
+ return {
34143
+ // An unreadable store reads as latched: not knowing is never a reason to accept a legacy answer.
34144
+ latched(origin) {
34145
+ try {
34146
+ return store.read() === origin;
34147
+ } catch {
34148
+ return true;
34149
+ }
34150
+ },
34151
+ observe,
34152
+ verdictSeen(origin) {
34153
+ if (read2() !== origin) write2(origin);
34154
+ },
34155
+ tokenIssued(origin, res, body) {
34156
+ if (res.status < 200 || res.status > 299 || res.redirected !== false) return;
34157
+ let type = "";
34158
+ try {
34159
+ type = res.headers?.get("content-type") ?? "";
34160
+ } catch {
34161
+ return;
34162
+ }
34163
+ if (!type.toLowerCase().includes("application/json") || !issuesTokens(body)) return;
34164
+ if (advertisesHubKeyCheck(res.headers)) {
34165
+ observe(origin, res.headers);
34166
+ return;
34167
+ }
34168
+ if (read2() !== origin) return;
34169
+ write2(null);
34170
+ log3(`[hub-key] unlatched ${origin}: its token-issuing response carried no ${FEATURES_HEADER}`);
34171
+ }
34172
+ };
34173
+ }
34174
+ async function checkHubKey(p) {
34175
+ const origin = apiOrigin(p.apiBase);
34176
+ if (!origin || !p.bearer) return "unavailable";
34177
+ let res;
34178
+ try {
34179
+ res = await (p.fetchImpl ?? fetch)(`${p.apiBase}/auth/hub-channel-keys/check`, {
34180
+ method: "POST",
34181
+ headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${p.bearer}`, ...p.headers },
34182
+ body: JSON.stringify({ channel_key: p.hubPk }),
34183
+ redirect: "error",
34184
+ signal: AbortSignal.timeout(p.timeoutMs ?? 1e4)
34185
+ });
34186
+ } catch {
34187
+ return "unavailable";
34188
+ }
34189
+ p.latch.observe(origin, res.headers);
34190
+ const body = await res.json().catch(() => null);
34191
+ if (isApiNotFound(res.status, body) && !p.latch.latched(origin)) return "legacy";
34192
+ if (!res.ok) return "unavailable";
34193
+ const v = body?.verdict;
34194
+ if (v !== "vouched" && v !== "first_contact" && v !== "new_hub" && v !== "refused") return "unavailable";
34195
+ p.latch.verdictSeen(origin);
34196
+ return v;
34197
+ }
34198
+
34199
+ // src/hub/borrow-confirm.ts
34200
+ init_define_KT_HUB_FILES();
34201
+ var import_node_url = require("node:url");
34202
+ var text = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
34203
+ var SCRIPTS = [
34204
+ ["latin", new RegExp("\\p{Script=Latin}", "u")],
34205
+ ["cyrillic", new RegExp("\\p{Script=Cyrillic}", "u")],
34206
+ ["greek", new RegExp("\\p{Script=Greek}", "u")],
34207
+ ["armenian", new RegExp("\\p{Script=Armenian}", "u")],
34208
+ ["cjk", /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u],
34209
+ ["hangul", new RegExp("\\p{Script=Hangul}", "u")],
34210
+ ["arabic", new RegExp("\\p{Script=Arabic}", "u")],
34211
+ ["hebrew", new RegExp("\\p{Script=Hebrew}", "u")]
34212
+ ];
34213
+ function mixesScripts(s) {
34214
+ const seen = /* @__PURE__ */ new Set();
34215
+ for (const ch of s) {
34216
+ if (!new RegExp("\\p{L}", "u").test(ch)) continue;
34217
+ seen.add(SCRIPTS.find(([, re]) => re.test(ch))?.[0] ?? "other");
34218
+ }
34219
+ return seen.size > 1;
34220
+ }
34221
+ function joinedAccount(user, lastAccount) {
34222
+ const at = user.email.lastIndexOf("@");
34223
+ const localPart = at >= 0 ? user.email.slice(0, at) : user.email;
34224
+ const raw = at >= 0 ? user.email.slice(at + 1) : "";
34225
+ const ascii = raw ? (0, import_node_url.domainToASCII)(raw.toLowerCase()) || raw : "";
34226
+ return {
34227
+ email: user.email,
34228
+ localPart,
34229
+ domain: ascii,
34230
+ internationalDomain: ascii !== raw.toLowerCase(),
34231
+ mixedScript: mixesScripts(localPart),
34232
+ displayName: text(user.display_name),
34233
+ orgName: text(user.org_name),
34234
+ accountChanged: lastAccount !== null && lastAccount !== user.id
34235
+ };
34236
+ }
34237
+ function createBorrowConfirm(deps) {
34238
+ let account = null;
34239
+ let heldId = null;
34240
+ let heldName = null;
34241
+ const remember = (user) => {
34242
+ if (deps.lastAccount.get() !== user.id) deps.lastAccount.set(user.id);
34243
+ };
34244
+ const reject = async () => {
34245
+ account = null;
34246
+ heldId = null;
34247
+ heldName = null;
34248
+ const held = deps.borrow.discard();
34249
+ if (!held) return;
34250
+ deps.pin.clear();
34251
+ await deps.revoke(held.deviceId, held.bearer).catch(() => void 0);
34252
+ };
34253
+ return {
34254
+ hold(user, deviceId, deviceName) {
34255
+ account = user;
34256
+ heldId = deviceId;
34257
+ heldName = deviceName ?? null;
34258
+ },
34259
+ pending: () => account ? { ...joinedAccount(account, deps.lastAccount.get()), deviceName: heldName } : null,
34260
+ seen(user) {
34261
+ if (!account && user) remember(user);
34262
+ },
34263
+ async confirm() {
34264
+ const user = account;
34265
+ const id = heldId;
34266
+ if (!user || !id) return false;
34267
+ if (!deps.borrow.commit(id)) {
34268
+ await reject();
34269
+ return false;
34270
+ }
34271
+ account = null;
34272
+ heldId = null;
34273
+ remember(user);
34274
+ return true;
34275
+ },
34276
+ reject,
34277
+ forgetHub() {
34278
+ deps.pin.clear();
34279
+ }
34280
+ };
34281
+ }
34282
+
33910
34283
  // src/hub/last-account.ts
33911
34284
  init_define_KT_HUB_FILES();
33912
34285
  var import_node_fs2 = require("node:fs");
@@ -34004,6 +34377,8 @@ var slot = {
34004
34377
  refresh: (base) => `${base}#refresh`,
34005
34378
  device: (base) => `${base}#device`,
34006
34379
  hubPin: (base) => `${base}#hub-pin`,
34380
+ // #3301: the API origin that has advertised the Hub key check; a fact about the server, kept across sign-out.
34381
+ featuresLatch: (base) => `${base}#hub-key-latch`,
34007
34382
  lastAccount: (base) => `${base}#last-account`,
34008
34383
  // #3051: the device id whose refresh-key bind is settled (bound, or refused for good), so it runs once.
34009
34384
  refreshBound: (base) => `${base}#refresh-bound`
@@ -34079,13 +34454,20 @@ function cliHubStores(base, kc, keychainAvailable) {
34079
34454
  drop(kc, slot.hubPin(base));
34080
34455
  }
34081
34456
  };
34457
+ const latch = {
34458
+ read: () => kc.get(slot.featuresLatch(base)) ?? null,
34459
+ write: (origin) => {
34460
+ if (origin === null) kc.clear(slot.featuresLatch(base));
34461
+ else kc.set(slot.featuresLatch(base), origin);
34462
+ }
34463
+ };
34082
34464
  const lastAccount = {
34083
34465
  get: () => read(kc, slot.lastAccount(base)),
34084
34466
  set: (userId) => {
34085
34467
  write(kc, slot.lastAccount(base), userId);
34086
34468
  }
34087
34469
  };
34088
- return { store, pin, lastAccount, failedWrite: () => failed2 ? { deviceId, bearer } : null };
34470
+ return { store, pin, latch, lastAccount, failedWrite: () => failed2 ? { deviceId, bearer } : null };
34089
34471
  }
34090
34472
  function borrowedDeviceKey(base, kc) {
34091
34473
  return readDevice(kc, base).key;
@@ -34147,6 +34529,7 @@ var HUB_COPY = {
34147
34529
  storeFailed: "The sign-in from the Kutrion app could not be saved in this computer's keychain, so that device was signed out and removed. Nothing was kept.",
34148
34530
  failure: {
34149
34531
  hub_not_running: "The Kutrion app is not running.",
34532
+ hub_timed_out: "The Kutrion app, or you, did not answer in time. Run the login again to retry.",
34150
34533
  hub_identity_changed: "The program answering for the Kutrion app is not the one this CLI paired with, so it was not used.",
34151
34534
  grant_not_from_hub: "The answer did not come from the paired Kutrion app, so it was not used.",
34152
34535
  protocol: "The Kutrion app answered in a way this CLI does not understand.",
@@ -34160,7 +34543,8 @@ var HUB_COPY = {
34160
34543
  unavailable: "The Kutrion app could not get a sign-in code from the server.",
34161
34544
  enrol_refused: "The server did not accept the sign-in from the Kutrion app.",
34162
34545
  device_clients_disabled: "Signing in through the Kutrion app is turned off on this server.",
34163
- satellite_update_required: "The server needs a newer CLI to sign in through the Kutrion app. Update the CLI, then start a new sign-in from the Kutrion app."
34546
+ satellite_update_required: "The server needs a newer CLI to sign in through the Kutrion app. Update the CLI, then start a new sign-in from the Kutrion app.",
34547
+ hub_not_bound: "Sign in to the Kutrion app again."
34164
34548
  }
34165
34549
  };
34166
34550
  async function presentJoined(confirm, out, ask, lead = HUB_COPY.joinedLead) {
@@ -34184,13 +34568,14 @@ var hubAbsent = (reason) => reason !== void 0 && ["hub_not_running", "hub_signed
34184
34568
  async function hubLogin(deps) {
34185
34569
  if (!deps.keychainAvailable()) return { kind: "fallback", note: null, reason: "no_keychain" };
34186
34570
  if (locationFault(deps.location)) return { kind: "fallback", note: null, reason: "hub_not_running" };
34187
- const { store, pin, lastAccount, failedWrite } = cliHubStores(deps.base, deps.kc, deps.keychainAvailable);
34571
+ const { store, pin, latch, lastAccount, failedWrite } = cliHubStores(deps.base, deps.kc, deps.keychainAvailable);
34188
34572
  const revoke = (deviceId, bearer) => endServerSession(deps.call, deviceId, bearer);
34189
34573
  const session = createBorrowSession(store, "cli", () => ({
34190
34574
  location: deps.location,
34191
34575
  apiBase: deps.base,
34192
34576
  pin,
34193
- revokeReplaced: revoke
34577
+ revokeReplaced: revoke,
34578
+ latch: createFeatureLatch(latch, deps.log ?? ((line) => console.error(line)))
34194
34579
  }));
34195
34580
  const confirm = createBorrowConfirm({
34196
34581
  borrow: { commit: (id) => session.commit(id), discard: () => session.discard() },
@@ -34241,315 +34626,16 @@ async function hubLogin(deps) {
34241
34626
  return { kind: "signed_in", user, firstContact: true };
34242
34627
  }
34243
34628
 
34244
- // src/app-signin.ts
34245
- var SIGNIN_FLOW_DEADLINE_MS = 3e5;
34246
- var REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:";
34247
- var ID_RE = /^[A-Za-z0-9_-]{43}$/;
34248
- var HANDLE_RE = /^[A-Za-z0-9_-]{43}$/;
34249
- var MAX_CALLBACK_BYTES = 2048;
34250
- var BROWSER_SIGNIN_COPY = {
34251
- lead: "Opening your web browser to sign in with Kutrion.",
34252
- waiting: "Waiting for you to approve this CLI in the browser. This request expires in a few minutes.",
34253
- openIt: (url) => `If the page did not open, go to this address in a browser on this computer:
34254
- ${url}`,
34255
- signedIn: "Sign in with Kutrion approved.",
34256
- notYou: "Not kept: that device was signed out and removed.",
34257
- notKept: HUB_COPY.notKept,
34258
- unconfirmed: "This CLI could not check which account the sign-in belongs to, so nothing from it was kept. Try again.",
34259
- storeFailed: "The sign-in could not be saved in this computer's keychain, so that device was signed out and removed. Nothing was kept.",
34260
- failure: {
34261
- unavailable: "This Kutrion server does not offer sign-in through the browser.",
34262
- already_enrolled: "This CLI is already registered on this computer. Run kt logout first.",
34263
- no_keychain: "There is no OS keychain to keep this sign-in in.",
34264
- rate_limited: "Too many sign-in attempts. Wait a few minutes and try again.",
34265
- unreachable: "Kutrion could not be reached. Check the connection and try again.",
34266
- request_failed: "Kutrion did not accept the sign-in request. Try again.",
34267
- no_approval_url: "The server did not return an approval page to open.",
34268
- approval_url_refused: "The approval page the server named is not a secure web address, so it was not used.",
34269
- listener_failed: "This CLI could not listen on this computer for the approval.",
34270
- timeout: "The approval did not arrive in time. Start again when you are ready.",
34271
- access_denied: "The sign-in was denied in the browser. Nothing was kept.",
34272
- origin_mismatch: "The approval came from a different network than this CLI, so it was refused. Sign in with your password instead.",
34273
- device_limit: "Your account has reached its device limit. Remove a device you no longer use in the web portal, then try again.",
34274
- redeem_failed: "The approval could not be completed on this computer. Start again."
34275
- }
34276
- };
34277
- function postJson(base, fetchImpl) {
34278
- return async (path, body) => {
34279
- try {
34280
- const res = await fetchImpl(base + path, {
34281
- method: "POST",
34282
- headers: { "Content-Type": "application/json", Accept: "application/json" },
34283
- body: JSON.stringify(body),
34284
- signal: AbortSignal.timeout(15e3)
34285
- });
34286
- const parsed = await res.json().catch(() => null);
34287
- return res.ok ? { ok: true, data: parsed } : { ok: false, status: res.status, body: parsed };
34288
- } catch {
34289
- return { ok: false, status: null, body: null };
34290
- }
34291
- };
34292
- }
34293
- var errorOf = (body) => body && typeof body === "object" ? body.error : void 0;
34294
- function isUnavailable(status, body) {
34295
- const e = errorOf(body);
34296
- if (status === 403) return e === "kutrion_sign_in_disabled" || e === "device_clients_disabled";
34297
- if (status === 400) return e === "unknown_client";
34298
- return status === 404;
34299
- }
34300
- function requestIdOf(requestUri) {
34301
- if (typeof requestUri !== "string" || !requestUri.startsWith(REQUEST_URI_PREFIX)) return null;
34302
- const id = requestUri.slice(REQUEST_URI_PREFIX.length);
34303
- return ID_RE.test(id) ? id : null;
34304
- }
34305
- function approvalUrlRefusal(url, allowLoopbackHttp) {
34306
- if (typeof url !== "string") return null;
34307
- let u;
34308
- try {
34309
- u = new URL(url);
34310
- } catch {
34311
- return null;
34312
- }
34313
- const loopback = u.hostname === "localhost" || u.hostname === "127.0.0.1";
34314
- if (u.protocol !== "https:" && !(allowLoopbackHttp && loopback && u.protocol === "http:")) return null;
34315
- if (u.username || u.password) return null;
34316
- return u.href;
34317
- }
34318
- function parseCallbackForm(body) {
34319
- const p = new URLSearchParams(body);
34320
- const requestId = p.get("request_id");
34321
- if (!requestId || !ID_RE.test(requestId)) return null;
34322
- const handle = p.get("handle");
34323
- const error = p.get("error");
34324
- if (handle !== null && error === null && HANDLE_RE.test(handle)) return { requestId, handle };
34325
- if (error !== null && handle === null && (error === "access_denied" || error === "origin_mismatch")) return { requestId, error };
34326
- return null;
34327
- }
34328
- var PAGE = (title, line) => `<!doctype html><meta charset="utf-8"><title>${title}</title><body style="font-family:system-ui;margin:3rem"><h1>${title}</h1><p>${line}</p></body>`;
34329
- function listenForCallback() {
34330
- return new Promise((resolve2) => {
34331
- let expected = null;
34332
- let origin = null;
34333
- const queue = [];
34334
- let waiter = null;
34335
- const server = (0, import_node_http.createServer)((req, res) => {
34336
- const headers = { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" };
34337
- if (req.method !== "POST" || req.url !== "/signin/callback") {
34338
- res.writeHead(404, headers).end();
34339
- return;
34340
- }
34341
- const sent = req.headers.origin;
34342
- if (origin && sent !== void 0 && sent !== "null" && sent !== origin) {
34343
- res.writeHead(403, headers).end();
34344
- return;
34345
- }
34346
- const chunks = [];
34347
- let size = 0;
34348
- req.on("data", (c) => {
34349
- size += c.length;
34350
- if (size > MAX_CALLBACK_BYTES) {
34351
- res.writeHead(413, headers).end();
34352
- req.destroy();
34353
- return;
34354
- }
34355
- chunks.push(c);
34356
- });
34357
- req.on("end", () => {
34358
- if (size > MAX_CALLBACK_BYTES) return;
34359
- const cb = parseCallbackForm(Buffer.concat(chunks).toString("utf8"));
34360
- if (!cb || cb.requestId !== expected || queue.length >= 8) {
34361
- res.writeHead(400, headers).end(PAGE("Not recognized", "This sign-in request is not the one this CLI is waiting for."));
34362
- return;
34363
- }
34364
- res.writeHead(200, headers).end(PAGE("Return to your terminal", "You can close this tab. The CLI is finishing the sign-in."));
34365
- const w = waiter;
34366
- waiter = null;
34367
- if (w) w(cb);
34368
- else queue.push(cb);
34369
- });
34370
- });
34371
- server.once("error", () => resolve2(null));
34372
- server.listen(0, "127.0.0.1", () => {
34373
- const port = server.address().port;
34374
- resolve2({
34375
- port,
34376
- expect: (requestId, approvalOrigin) => {
34377
- expected = requestId;
34378
- origin = approvalOrigin ?? null;
34379
- },
34380
- next: (until) => new Promise((done) => {
34381
- const queued = queue.shift();
34382
- if (queued) {
34383
- done(queued);
34384
- return;
34385
- }
34386
- const timer = setTimeout(() => {
34387
- waiter = null;
34388
- done("timeout");
34389
- }, Math.max(0, until - Date.now()));
34390
- waiter = (c) => {
34391
- clearTimeout(timer);
34392
- done(c);
34393
- };
34394
- }),
34395
- close: () => {
34396
- waiter = null;
34397
- server.close();
34398
- server.closeAllConnections();
34399
- }
34400
- });
34401
- });
34402
- });
34403
- }
34404
- function openInBrowser(url, platform3 = process.platform, env = process.env) {
34405
- if (platform3 !== "darwin" && platform3 !== "win32" && !env.DISPLAY && !env.WAYLAND_DISPLAY) return Promise.resolve(false);
34406
- const [cmd, args] = platform3 === "darwin" ? ["open", [url]] : platform3 === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
34407
- return new Promise((resolve2) => {
34408
- try {
34409
- const child = (0, import_node_child_process.spawn)(cmd, args, { stdio: "ignore", detached: true });
34410
- child.once("error", () => resolve2(false));
34411
- child.once("spawn", () => {
34412
- child.unref();
34413
- resolve2(true);
34414
- });
34415
- } catch {
34416
- resolve2(false);
34417
- }
34418
- });
34419
- }
34420
- async function redeem(post, handle, verifier, key) {
34421
- const priv = (0, import_node_crypto4.createPrivateKey)({ key: Buffer.from(key.privatePkcs8B64, "base64"), format: "der", type: "pkcs8" });
34422
- const signature = (0, import_node_crypto4.sign)(null, Buffer.from(handle, "utf8"), priv).toString("base64");
34423
- const res = await post(
34424
- "/auth/app-signin/redeem",
34425
- { handle, code_verifier: verifier, signature }
34426
- );
34427
- if (!res.ok) {
34428
- if (res.status === 400 && errorOf(res.body) === "invalid_grant") return "invalid_grant";
34429
- if (res.status === 409 && errorOf(res.body) === "device_limit") return "device_limit";
34430
- return "failed";
34431
- }
34432
- const { device, access_token: access, refresh_token: refresh } = res.data;
34433
- if (typeof device?.id !== "string" || typeof access !== "string" || typeof refresh !== "string") return "failed";
34434
- return { deviceId: device.id, privatePkcs8B64: key.privatePkcs8B64, accessToken: access, refreshToken: refresh };
34435
- }
34436
- async function runBrowserSignin(deps) {
34437
- const fail = (reason) => ({ ok: false, reason });
34438
- if (deps.store.deviceId()) return fail("already_enrolled");
34439
- if (!deps.store.keychainAvailable()) return fail("no_keychain");
34440
- const listener = await (deps.listen ?? listenForCallback)();
34441
- if (!listener) return fail("listener_failed");
34442
- try {
34443
- const key = resolveDeviceKey(deps.store, deps.generateKey);
34444
- const verifier = (0, import_node_crypto4.randomBytes)(32).toString("base64url");
34445
- const res = await deps.post("/auth/app-signin/request", {
34446
- client_id: "cli",
34447
- public_key: key.publicSpkiB64,
34448
- code_challenge: (0, import_node_crypto4.createHash)("sha256").update(verifier).digest("base64url"),
34449
- code_challenge_method: "S256",
34450
- device_name: deps.deviceName().slice(0, 100),
34451
- loopback_port: listener.port
34452
- });
34453
- if (!res.ok) {
34454
- if (isUnavailable(res.status, res.body)) return fail("unavailable");
34455
- return fail(res.status === 429 ? "rate_limited" : res.status === null ? "unreachable" : "request_failed");
34456
- }
34457
- const requestId = requestIdOf(res.data.request_uri);
34458
- if (!requestId) return fail("request_failed");
34459
- if (typeof res.data.approval_url !== "string" || !res.data.approval_url) return fail("no_approval_url");
34460
- const href = approvalUrlRefusal(res.data.approval_url, deps.allowLoopbackHttp);
34461
- if (!href) return fail("approval_url_refused");
34462
- const until = Date.now() + (deps.deadlineMs ?? SIGNIN_FLOW_DEADLINE_MS);
34463
- deps.out(BROWSER_SIGNIN_COPY.lead);
34464
- listener.expect(requestId, new URL(href).origin);
34465
- let waited = listener.next(until);
34466
- await deps.open(href);
34467
- deps.out(BROWSER_SIGNIN_COPY.openIt(href));
34468
- deps.out(BROWSER_SIGNIN_COPY.waiting);
34469
- for (; ; ) {
34470
- const cb = await waited;
34471
- if (cb === "timeout") return fail("timeout");
34472
- if ("error" in cb) return fail(cb.error);
34473
- const out = await redeem(deps.post, cb.handle, verifier, key);
34474
- if (out === "invalid_grant") {
34475
- if (Date.now() >= until) return fail("timeout");
34476
- waited = listener.next(until);
34477
- continue;
34478
- }
34479
- if (out === "device_limit") return fail("device_limit");
34480
- if (out === "failed") return fail("redeem_failed");
34481
- return { ok: true, held: out };
34482
- }
34483
- } finally {
34484
- listener.close();
34485
- }
34486
- }
34487
- async function browserLogin(deps) {
34488
- const { store, lastAccount, failedWrite } = cliHubStores(deps.base, deps.kc, deps.keychainAvailable);
34489
- const outcome = await runBrowserSignin({ ...deps, store });
34490
- if (!outcome.ok) return { kind: "failed", note: BROWSER_SIGNIN_COPY.failure[outcome.reason], reason: outcome.reason };
34491
- const held = outcome.held;
34492
- deps.out(BROWSER_SIGNIN_COPY.signedIn);
34493
- let current = held;
34494
- const revoke = (deviceId, bearer) => endServerSession(deps.call, deviceId, bearer);
34495
- const confirm = createBorrowConfirm({
34496
- borrow: {
34497
- commit: (id) => {
34498
- const h = current;
34499
- if (!h || h.deviceId !== id || store.deviceId()) return false;
34500
- current = null;
34501
- store.setRefresh(h.refreshToken);
34502
- store.setDevice(h.deviceId, h.privatePkcs8B64);
34503
- store.setAccess(h.accessToken);
34504
- return true;
34505
- },
34506
- discard: () => {
34507
- const h = current;
34508
- current = null;
34509
- return h ? { deviceId: h.deviceId, bearer: h.accessToken } : null;
34510
- }
34511
- },
34512
- pin: { hubPublicKey: null, pin() {
34513
- }, clear() {
34514
- } },
34515
- lastAccount,
34516
- revoke
34517
- });
34518
- const me = await deps.call("GET", "/auth/me", void 0, held.accessToken).catch(() => null);
34519
- const user = me?.ok && me.data?.user ? me.data.user : null;
34520
- if (!user) {
34521
- await confirm.reject();
34522
- return { kind: "failed", note: BROWSER_SIGNIN_COPY.unconfirmed };
34523
- }
34524
- confirm.hold(user, held.deviceId);
34525
- const verdict = await presentJoined(confirm, deps.out, deps.ask, "Sign in with Kutrion approved this CLI for this account:");
34526
- if (verdict === "not_you") return { kind: "failed", note: BROWSER_SIGNIN_COPY.notYou };
34527
- if (verdict === "not_kept" || !await confirm.confirm()) return { kind: "failed", note: BROWSER_SIGNIN_COPY.notKept };
34528
- const f = failedWrite();
34529
- if (f) {
34530
- await endServerSession(deps.call, f.deviceId, f.bearer);
34531
- dropBorrowed(deps.base, deps.kc);
34532
- try {
34533
- if (f.bearer && deps.kc.get(deps.base) === f.bearer) deps.kc.clear(deps.base);
34534
- } catch {
34535
- }
34536
- return { kind: "store_failed", note: BROWSER_SIGNIN_COPY.storeFailed };
34537
- }
34538
- return { kind: "signed_in", user };
34539
- }
34540
-
34541
- // src/hub-seed-login.ts
34542
- init_define_KT_HUB_FILES();
34543
- var import_os3 = require("os");
34544
-
34545
34629
  // src/hub/hub-seed.ts
34546
34630
  init_define_KT_HUB_FILES();
34547
34631
  var import_node_net2 = require("node:net");
34548
- var import_node_child_process2 = require("node:child_process");
34632
+ var import_node_child_process = require("node:child_process");
34549
34633
  var import_node_fs3 = require("node:fs");
34550
34634
  var import_node_os2 = require("node:os");
34551
34635
  var import_node_path3 = require("node:path");
34552
34636
  var REPLY_DEADLINE_MS2 = 13e4;
34637
+ var CONFIRM_DEADLINE_MS = 12e4;
34638
+ var SHOW_CODES_AFTER_MS = 1500;
34553
34639
  var CODE_RE2 = /^[A-Za-z0-9_-]{43}$/;
34554
34640
  var CLOSED = Symbol("closed");
34555
34641
  var BAD = Symbol("bad");
@@ -34599,35 +34685,80 @@ function lineReader(sock) {
34599
34685
  };
34600
34686
  }
34601
34687
  var failed = (reason) => ({ ok: false, reason });
34602
- function seedOnce(deps) {
34603
- const resolved = resolveEndpoint(deps.location, { platform: deps.platform });
34604
- if (resolved === null) return Promise.resolve(failed("hub_not_running"));
34605
- const origin = apiOrigin(deps.apiBase);
34606
- if (origin === null) return Promise.resolve(failed("protocol"));
34607
- const { identity } = deps;
34608
- const sock = (deps.connectTo ?? ((e) => (0, import_node_net2.connect)(e)))(resolved.endpoint);
34688
+ function openConn(deps, endpoint) {
34689
+ const sock = (deps.connectTo ?? ((e) => (0, import_node_net2.connect)(e)))(endpoint);
34609
34690
  const rd = lineReader(sock);
34610
34691
  const timer = setTimeout(() => sock.destroy(), REPLY_DEADLINE_MS2);
34611
34692
  timer.unref?.();
34612
- const send = (frame) => sock.write(`${JSON.stringify(frame)}
34613
- `);
34614
- return (async () => {
34615
- const cnonce = newNonce();
34616
- send({ v: CHANNEL_VERSION, t: "hello-seed", client: identity.client, pk: identity.publicSpkiB64, cnonce, origin });
34617
- const first = await rd.next();
34618
- if (first === CLOSED) return failed("hub_not_running");
34619
- if (first === BAD) return failed("protocol");
34620
- const refusedFirst = parseRefuse(first);
34621
- if (refusedFirst) return failed(refusedFirst.reason);
34622
- const offer = parseOfferSeed(first);
34623
- if (!offer) return failed("protocol");
34624
- const pinned = deps.pin.hubPublicKey;
34625
- if (pinned && !sameKey(pinned, offer.hub_pk)) return failed("hub_identity_changed");
34626
- if (resolved.expectedHubPk && !sameKey(resolved.expectedHubPk, offer.hub_pk)) return failed("hub_identity_changed");
34627
- if (!pinned) {
34628
- if (!await deps.confirmHub(keyFingerprint(identity.client, offer.hub_pk))) return failed("hub_unconfirmed");
34629
- deps.pin.pin(offer.hub_pk);
34693
+ return {
34694
+ sock,
34695
+ rd,
34696
+ send: (frame) => sock.write(`${JSON.stringify(frame)}
34697
+ `),
34698
+ close: () => {
34699
+ clearTimeout(timer);
34700
+ sock.destroy();
34630
34701
  }
34702
+ };
34703
+ }
34704
+ async function helloFor(conn, identity, origin) {
34705
+ const cnonce = newNonce();
34706
+ conn.send({ v: CHANNEL_VERSION, t: "hello-seed", client: identity.client, pk: identity.publicSpkiB64, cnonce, origin });
34707
+ const first = await conn.rd.next();
34708
+ if (first === CLOSED) return failed("hub_not_running");
34709
+ if (first === BAD) return failed("protocol");
34710
+ const refused = parseRefuse(first);
34711
+ if (refused) return failed(refused.reason);
34712
+ const offer = parseOfferSeed(first);
34713
+ return offer ? { cnonce, offer } : failed("protocol");
34714
+ }
34715
+ async function confirmWithin(deps, codes) {
34716
+ const ac = new AbortController();
34717
+ const t = setTimeout(() => ac.abort(), deps.timing?.confirmMs ?? CONFIRM_DEADLINE_MS);
34718
+ t.unref?.();
34719
+ try {
34720
+ const yes = await deps.confirmHub(codes, ac.signal);
34721
+ return ac.signal.aborted ? "timed_out" : yes;
34722
+ } finally {
34723
+ clearTimeout(t);
34724
+ }
34725
+ }
34726
+ async function seedOnce(deps) {
34727
+ const resolved = resolveEndpoint(deps.location, { platform: deps.platform });
34728
+ if (resolved === null) return failed("hub_not_running");
34729
+ const origin = apiOrigin(deps.apiBase);
34730
+ if (origin === null) return failed("protocol");
34731
+ const { identity } = deps;
34732
+ const appCode = keyFingerprint(identity.client, identity.publicSpkiB64);
34733
+ const wrongHub = (hubPk2, expect) => expect !== null && !sameKey(expect, hubPk2) || resolved.expectedHubPk != null && !sameKey(resolved.expectedHubPk, hubPk2);
34734
+ const pinned = deps.pin.hubPublicKey ?? null;
34735
+ const probe = openConn(deps, resolved.endpoint);
34736
+ let met;
34737
+ try {
34738
+ met = await helloFor(probe, identity, origin);
34739
+ } finally {
34740
+ probe.close();
34741
+ }
34742
+ if (!("offer" in met)) return met;
34743
+ const hubPk = met.offer.hub_pk;
34744
+ if (wrongHub(hubPk, pinned)) return failed("hub_identity_changed");
34745
+ const verdict = await deps.check(hubPk);
34746
+ if (verdict === "unavailable") return failed("hub_check_unavailable");
34747
+ if (verdict === "refused" || verdict === "new_hub") return failed("hub_not_vouched");
34748
+ if (pinned === null || verdict === "first_contact") {
34749
+ const answer = await confirmWithin(deps, { app: appCode, hub: keyFingerprint(identity.client, hubPk) });
34750
+ if (answer === "timed_out") return failed("hub_timed_out");
34751
+ if (!answer) return failed("hub_unconfirmed");
34752
+ if (pinned === null) deps.pin.pin(hubPk);
34753
+ }
34754
+ const expectHub = hubPk;
34755
+ const conn = openConn(deps, resolved.endpoint);
34756
+ const ui = new AbortController();
34757
+ try {
34758
+ const met2 = await helloFor(conn, identity, origin);
34759
+ if (!("offer" in met2)) return met2;
34760
+ const { cnonce, offer } = met2;
34761
+ if (wrongHub(offer.hub_pk, expectHub)) return failed("hub_identity_changed");
34631
34762
  const parts = {
34632
34763
  client: identity.client,
34633
34764
  satPk: identity.publicSpkiB64,
@@ -34637,48 +34768,67 @@ function seedOnce(deps) {
34637
34768
  snonce: offer.snonce,
34638
34769
  origin
34639
34770
  };
34640
- send({ v: CHANNEL_VERSION, t: "prove", sig: signTranscript(identity.privatePkcs8B64, seedRequestTranscript(parts)) });
34641
- const second = await rd.next();
34642
- if (second === CLOSED) return failed("hub_not_running");
34771
+ conn.send({ v: CHANNEL_VERSION, t: "prove", sig: signTranscript(identity.privatePkcs8B64, seedRequestTranscript(parts)) });
34772
+ let stopped = false;
34773
+ const show = deps.showCodes;
34774
+ const showTimer = show ? setTimeout(() => {
34775
+ void show({ app: appCode, hub: keyFingerprint(identity.client, offer.hub_pk) }, ui.signal).then((stop) => {
34776
+ if (stop && !ui.signal.aborted) {
34777
+ stopped = true;
34778
+ conn.sock.destroy();
34779
+ }
34780
+ }, () => void 0);
34781
+ }, deps.timing?.showCodesAfterMs ?? SHOW_CODES_AFTER_MS) : null;
34782
+ const second = await conn.rd.next();
34783
+ if (showTimer) clearTimeout(showTimer);
34784
+ ui.abort();
34785
+ if (stopped) return failed("hub_unconfirmed");
34786
+ if (second === CLOSED) return failed("hub_timed_out");
34643
34787
  if (second === BAD) return failed("protocol");
34644
34788
  const refusedSecond = parseRefuse(second);
34645
34789
  if (refusedSecond) return failed(refusedSecond.reason);
34646
34790
  const accept = parseAcceptSeed(second);
34647
34791
  if (!accept) return failed("protocol");
34648
34792
  if (!verifyTranscript(offer.hub_pk, seedAcceptTranscript(parts), accept.sig)) return failed("hub_not_from_hub");
34649
- const issued = await deps.issue(offer.device_pk);
34793
+ const issued = await deps.issue(offer.device_pk, offer.hub_pk);
34650
34794
  if (!issued.ok) return failed(issued.reason);
34651
- send({
34795
+ conn.send({
34652
34796
  v: CHANNEL_VERSION,
34653
34797
  t: "seed",
34654
34798
  code: issued.code,
34655
34799
  sig: signTranscript(identity.privatePkcs8B64, seedTranscript({ ...parts, code: issued.code }))
34656
34800
  });
34657
- const third = await rd.next();
34658
- if (third === CLOSED) return failed("hub_not_running");
34801
+ const third = await conn.rd.next();
34802
+ if (third === CLOSED) return failed("hub_timed_out");
34659
34803
  if (third === BAD) return failed("protocol");
34660
34804
  const refusedThird = parseRefuse(third);
34661
34805
  if (refusedThird) return failed(refusedThird.reason);
34662
34806
  const seeded = parseSeeded(third);
34663
34807
  return seeded ? { ok: true, result: seeded.result, hubPublicKey: offer.hub_pk } : failed("protocol");
34664
- })().finally(() => {
34665
- clearTimeout(timer);
34666
- sock.destroy();
34667
- });
34808
+ } finally {
34809
+ ui.abort();
34810
+ conn.close();
34811
+ }
34668
34812
  }
34669
34813
  function hubCodeIssuer(p) {
34670
- return async (devicePk) => {
34814
+ return async (devicePk, hubPk, approvalId) => {
34671
34815
  const bearer = p.bearer();
34672
34816
  const deviceId = p.deviceId();
34673
34817
  if (!bearer || !deviceId) return { ok: false, reason: "issue_refused" };
34674
34818
  const res = await p.post(
34675
34819
  "/auth/handoff/issue",
34676
- { purpose: "enrolment", device_id: deviceId, client: { id: "hub", name: p.hubName, platform: "desktop", public_key: devicePk } },
34820
+ {
34821
+ purpose: "enrolment",
34822
+ device_id: deviceId,
34823
+ client: { id: "hub", name: p.hubName, platform: "desktop", public_key: devicePk, channel_key: hubPk },
34824
+ ...approvalId ? { approval_id: approvalId } : {}
34825
+ },
34677
34826
  bearer
34678
34827
  );
34679
34828
  if (!res.ok) {
34680
34829
  const err = res.body?.error;
34681
- return { ok: false, reason: res.status === 403 && err === "step_up_required" ? "step_up_required" : "issue_refused" };
34830
+ if (res.status === 403 && err === "step_up_required") return { ok: false, reason: "step_up_required" };
34831
+ return { ok: false, reason: res.status === 403 && err === "hub_not_vouched" ? "hub_not_vouched" : "issue_refused" };
34682
34832
  }
34683
34833
  const code = res.data?.code;
34684
34834
  return typeof code === "string" && CODE_RE2.test(code) ? { ok: true, code } : { ok: false, reason: "issue_refused" };
@@ -34707,11 +34857,11 @@ function hubLauncher(env = {}) {
34707
34857
  const home = env.home ?? (0, import_node_os2.homedir)();
34708
34858
  const exists = env.exists ?? import_node_fs3.existsSync;
34709
34859
  const exec2 = env.exec ?? ((file, args) => new Promise((resolve2, reject) => {
34710
- (0, import_node_child_process2.execFile)(file, args, { timeout: 5e3, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }, (e, out) => e ? reject(e) : resolve2(out));
34860
+ (0, import_node_child_process.execFile)(file, args, { timeout: 5e3, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }, (e, out) => e ? reject(e) : resolve2(out));
34711
34861
  }));
34712
34862
  const startDetached2 = env.startDetached ?? ((file, args) => {
34713
34863
  try {
34714
- const child = (0, import_node_child_process2.spawn)(file, args, { detached: true, stdio: "ignore", windowsHide: true });
34864
+ const child = (0, import_node_child_process.spawn)(file, args, { detached: true, stdio: "ignore", windowsHide: true });
34715
34865
  child.on("error", () => void 0);
34716
34866
  child.unref();
34717
34867
  return true;
@@ -34785,14 +34935,16 @@ async function seedHubAfterSignIn(deps) {
34785
34935
  if (!reachable()) return { kind: "retry" };
34786
34936
  }
34787
34937
  const launcher = deps.launcher;
34788
- const confirmHub = launcher?.foreground ? async (fp) => {
34938
+ const confirmHub = launcher?.foreground ? async (codes, signal) => {
34789
34939
  await Promise.resolve(launcher.foreground?.()).catch(() => void 0);
34790
- return deps.confirmHub(fp);
34940
+ return deps.confirmHub(codes, signal);
34791
34941
  } : deps.confirmHub;
34792
34942
  const out = await seedOnce({ ...deps, confirmHub });
34793
34943
  if (out.ok) return { kind: "done", result: out.result };
34794
34944
  if (out.reason === "hub_other_account") return { kind: "other_account" };
34795
34945
  if (out.reason === "hub_not_running") return { kind: "retry" };
34946
+ if (out.reason === "hub_timed_out") return { kind: "timed_out" };
34947
+ if (out.reason === "hub_check_unavailable") return { kind: "retry" };
34796
34948
  return { kind: "stopped", reason: out.reason };
34797
34949
  }
34798
34950
 
@@ -34804,13 +34956,17 @@ var SEED_COPY = {
34804
34956
  failedNotRemoved: (name) => `This CLI could not register itself as a device, and the device it created could not be removed. You are still logged in. Remove "${name}" from Active devices in the web portal.`,
34805
34957
  joined: "The Kutrion app on this computer is now signed in to this account.",
34806
34958
  otherAccount: "The Kutrion app on this computer is signed in to a different account. Sign it out there first. This CLI keeps its own sign-in.",
34959
+ retryLater: "The Kutrion app was not signed in because the account service could not be reached to check it. It will try again at the next sign-in.",
34807
34960
  declined: "Not shared: the Kutrion app was not signed in.",
34808
34961
  stopped: (reason) => `The Kutrion app was not signed in (${printable(reason)}).`,
34809
- firstContact: (fp) => [
34810
- `The Kutrion app on this computer answered. Its code is ${fp}.`,
34962
+ firstContact: (codes) => [
34963
+ `The Kutrion app on this computer answered. Its code is ${codes.hub}.`,
34964
+ `The Kutrion app will then ask you to allow this CLI, and show this CLI's code: ${codes.app}`,
34811
34965
  "If you did not just open the Kutrion app yourself, answer no.",
34812
34966
  "Sharing gives the Kutrion app its own sign-in to your account. Your password is not sent, and you can sign it out again from Active devices."
34813
34967
  ],
34968
+ allowCodes: (codes) => `The Kutrion app is asking whether to allow this CLI. Check it shows these codes before you allow it: this CLI ${codes.app}, Kutrion ${codes.hub}. If they differ, choose Don't allow in the Kutrion app.`,
34969
+ timedOut: "Sharing your sign-in with the Kutrion app timed out, so it was not signed in. You are still logged in. Run kt login again to retry.",
34814
34970
  ask: "\x1B[0mSign the Kutrion app in too? Type yes to share, or anything else to skip: "
34815
34971
  };
34816
34972
  function seedSkipReason(env, platform3, tty) {
@@ -34888,7 +35044,8 @@ async function seedHubFromLogin(deps) {
34888
35044
  if (skip) return { kind: "skipped", reason: skip };
34889
35045
  if (!deps.location || !deps.keychainAvailable()) return { kind: "skipped", reason: "no_channel" };
34890
35046
  try {
34891
- const { store, pin } = cliHubStores(deps.base, deps.kc, deps.keychainAvailable);
35047
+ const { store, pin, latch: latchStore } = cliHubStores(deps.base, deps.kc, deps.keychainAvailable);
35048
+ const latch = createFeatureLatch(latchStore, deps.log ?? ((line) => console.error(line)));
34892
35049
  const deviceId = store.deviceId();
34893
35050
  const keyB64 = store.devicePrivateKeyPkcs8();
34894
35051
  const bearer = () => {
@@ -34904,18 +35061,29 @@ async function seedHubFromLogin(deps) {
34904
35061
  if (!mode?.ok || mode.data?.vault_mode !== "audited") return { kind: "skipped", reason: "private_vault" };
34905
35062
  const key = resolveDeviceKey(store);
34906
35063
  const issuer = hubCodeIssuer({ post: deps.post, bearer, deviceId: () => store.deviceId(), hubName: `Kutrion on ${host()}`.slice(0, 255) });
35064
+ let checkUnavailable = false;
34907
35065
  const status = await seedHubAfterSignIn({
34908
35066
  location: deps.location,
34909
35067
  identity: { client: "cli", ...key },
34910
35068
  pin,
34911
35069
  apiBase: deps.base,
34912
- confirmHub: async (fingerprint) => {
34913
- for (const line of SEED_COPY.firstContact(fingerprint)) deps.out(line);
35070
+ confirmHub: async (codes) => {
35071
+ for (const line of SEED_COPY.firstContact(codes)) deps.out(line);
34914
35072
  return (await deps.ask(SEED_COPY.ask).catch(() => "")).trim().toLowerCase() === "yes";
34915
35073
  },
34916
- issue: async (devicePk) => {
35074
+ // Printed once; a terminal cannot retract a line, so the prompt never asks to stop.
35075
+ showCodes: async (codes) => {
35076
+ deps.out(SEED_COPY.allowCodes(codes));
35077
+ return false;
35078
+ },
35079
+ check: async (hubPk) => {
35080
+ const verdict = await checkHubKey({ apiBase: deps.base, bearer: bearer(), hubPk, latch, fetchImpl: deps.fetchImpl });
35081
+ if (verdict === "unavailable") checkUnavailable = true;
35082
+ return verdict;
35083
+ },
35084
+ issue: async (devicePk, hubPk) => {
34917
35085
  await deps.touch().catch(() => void 0);
34918
- return issuer(devicePk);
35086
+ return issuer(devicePk, hubPk);
34919
35087
  },
34920
35088
  launcher: withForeground(deps.launcher ?? hubLauncher({ platform: platform3, localAppData: env.LOCALAPPDATA, programFiles: env.ProgramFiles }), tty),
34921
35089
  env,
@@ -34923,7 +35091,10 @@ async function seedHubFromLogin(deps) {
34923
35091
  });
34924
35092
  if (status.kind === "done" && status.result === "joined") deps.out(SEED_COPY.joined);
34925
35093
  else if (status.kind === "other_account") deps.out(SEED_COPY.otherAccount);
35094
+ else if (status.kind === "timed_out") deps.out(SEED_COPY.timedOut);
35095
+ else if (status.kind === "retry" && checkUnavailable) deps.out(SEED_COPY.retryLater);
34926
35096
  else if (status.kind === "stopped" && status.reason === "hub_unconfirmed") deps.out(SEED_COPY.declined);
35097
+ else if (status.kind === "stopped" && status.reason === "hub_not_vouched") deps.out(HUB_KEY_COPY.notVouched);
34927
35098
  else if (status.kind === "stopped" && status.reason !== "disabled") deps.out(SEED_COPY.stopped(status.reason));
34928
35099
  return status;
34929
35100
  } catch {
@@ -34933,7 +35104,7 @@ async function seedHubFromLogin(deps) {
34933
35104
 
34934
35105
  // src/hub/ensure-hub.ts
34935
35106
  init_define_KT_HUB_FILES();
34936
- var import_node_child_process3 = require("node:child_process");
35107
+ var import_node_child_process2 = require("node:child_process");
34937
35108
  var import_node_fs4 = require("node:fs");
34938
35109
  var import_node_os3 = require("node:os");
34939
35110
  var import_node_path4 = require("node:path");
@@ -34956,7 +35127,7 @@ function stageFiles(files) {
34956
35127
  return dir;
34957
35128
  }
34958
35129
  function startDetached(command, args) {
34959
- const child = (0, import_node_child_process3.spawn)(command, args, { detached: true, stdio: "ignore", windowsHide: true });
35130
+ const child = (0, import_node_child_process2.spawn)(command, args, { detached: true, stdio: "ignore", windowsHide: true });
34960
35131
  child.on("error", () => void 0);
34961
35132
  child.unref();
34962
35133
  }
@@ -35215,7 +35386,7 @@ var HOST_KEY_CONFIRM_COPY2 = {
35215
35386
  notThisConnection: "This key is not for the host this connection opens. It belongs to a jump host on the way, or Kutrion could not tell which host it belongs to, so it cannot be trusted from here. Open that host's own connection and confirm its key there; that needs an organization admin, or an editor of the vault that holds that connection.",
35216
35387
  connectAgain: "Connect again:"
35217
35388
  };
35218
- var HOST_KEY_FINGERPRINT = /^SHA256:[A-Za-z0-9+/]{43}$/;
35389
+ var HOST_KEY_FINGERPRINT = HOST_KEY_FINGERPRINT_EXACT;
35219
35390
  function parseHostKeyUnconfirmed(d) {
35220
35391
  if (!d || typeof d !== "object") return null;
35221
35392
  const o = d;
@@ -35269,6 +35440,74 @@ function hostKeyConfirmErrorLines(e, connectionId, indent = " ") {
35269
35440
  }
35270
35441
  return null;
35271
35442
  }
35443
+ var CHAINED_PROMPT = "Fingerprint from the host: ";
35444
+ function parseHostKeyChained(d) {
35445
+ return hostKeyChainedFromFrame({ code: "host_key_unconfirmed", detail: d && typeof d === "object" ? d : void 0 });
35446
+ }
35447
+ function hostKeyChainedFromError(e) {
35448
+ if (!(e instanceof ApiError) || e.status !== 409) return null;
35449
+ const b = e.body;
35450
+ return b?.error === "host_key_unconfirmed" ? parseHostKeyChained(b.host_key) : null;
35451
+ }
35452
+ function hostKeyChainedLines(k, own, indent = " ") {
35453
+ const lines = [`${HOST_KEY_CHAINED_COPY.heading} (host_key_unconfirmed)`, "", HOST_KEY_CHAINED_COPY.body, "", `${indent}host: ${k.target}`, ""];
35454
+ if (own && isOwnHost(k, own)) {
35455
+ lines.push(
35456
+ HOST_KEY_CHAINED_COPY.help,
35457
+ `${indent}${HOST_KEY_CHAINED_COPY.command}`,
35458
+ "",
35459
+ "An organization admin then trusts it with:",
35460
+ `${indent}kt ssh trust ${own.id} --fingerprint <fingerprint from the host>`,
35461
+ "",
35462
+ HOST_KEY_CHAINED_COPY.nonAdmin
35463
+ );
35464
+ } else {
35465
+ lines.push(HOST_KEY_CHAINED_COPY.notThisConnection);
35466
+ }
35467
+ return lines;
35468
+ }
35469
+ async function confirmChainedHostKey(deps, connId, k, own) {
35470
+ deps.out("");
35471
+ deps.out(`${HOST_KEY_CHAINED_COPY.heading} (host_key_unconfirmed)`);
35472
+ deps.out("");
35473
+ deps.out(HOST_KEY_CHAINED_COPY.body);
35474
+ deps.out("");
35475
+ deps.out(` host: ${k.target}`);
35476
+ deps.out("");
35477
+ if (!own || !isOwnHost(k, own)) {
35478
+ deps.out(HOST_KEY_CHAINED_COPY.notThisConnection);
35479
+ return false;
35480
+ }
35481
+ if (!await deps.isAdmin()) {
35482
+ deps.out(HOST_KEY_CHAINED_COPY.nonAdmin);
35483
+ return false;
35484
+ }
35485
+ deps.out(HOST_KEY_CHAINED_COPY.help);
35486
+ deps.out(` ${HOST_KEY_CHAINED_COPY.command}`);
35487
+ let fingerprint = null;
35488
+ for (let attempt = 0; attempt < 3 && !fingerprint; attempt++) {
35489
+ const answer = await deps.ask(CHAINED_PROMPT);
35490
+ if (!answer) return false;
35491
+ fingerprint = extractHostKeyFingerprint(answer);
35492
+ if (!fingerprint) deps.out(HOST_KEY_CHAINED_COPY.invalid);
35493
+ }
35494
+ if (!fingerprint) return false;
35495
+ try {
35496
+ const r = await deps.confirm(connId, fingerprint);
35497
+ deps.out(`${r.created ? "trusted" : "already trusted"}: ${r.pinned.host}:${r.pinned.port} ${r.pinned.key_type} ${r.pinned.fingerprint}`);
35498
+ deps.out(`connect with: kt ssh ${connId}`);
35499
+ return true;
35500
+ } catch (e) {
35501
+ if (!(e instanceof ApiError)) throw e;
35502
+ const code = e.body?.error;
35503
+ const failure = hostKeyConfirmFailure(typeof code === "string" ? code : null);
35504
+ if (failure === "forbidden") deps.out(HOST_KEY_CHAINED_COPY.nonAdmin);
35505
+ else if (failure === "mismatch") deps.out(HOST_KEY_CHAINED_COPY.mismatch);
35506
+ else if (failure === "expired") deps.out(`${HOST_KEY_CHAINED_COPY.expired} kt ssh ${connId}`);
35507
+ else throw e;
35508
+ return false;
35509
+ }
35510
+ }
35272
35511
 
35273
35512
  // src/refusal.ts
35274
35513
  var REFUSALS = {
@@ -35293,7 +35532,7 @@ var REFUSALS = {
35293
35532
  action: "Run `kt conns` and pass an id from the first column to --jump."
35294
35533
  },
35295
35534
  // auth-guard.ts:109 (social-session gate) and :167 (adaptive), plus rotation.ts:159, jit.ts:226,
35296
- // fleet.ts:190, agents.ts:146, modules/ssh.ts:727 and :1201. The remedy is mechanical and worth
35535
+ // fleet.ts:190, agents.ts:146, modules/ssh.ts:727 and :1222. The remedy is mechanical and worth
35297
35536
  // stating exactly, because it is not obvious that it can be satisfied anywhere but here: step-up
35298
35537
  // freshness is a per-USER Redis proof with a ~5 minute TTL (server/src/lib/step-up.ts:16), not a
35299
35538
  // claim baked into this CLI's token. Stepping up in the web app on the same account therefore
@@ -35403,7 +35642,7 @@ var REFUSALS = {
35403
35642
  why: "Promoting now would leave those hosts refusing every session, because they do not yet accept a certificate from the next CA.",
35404
35643
  action: "Install or verify each host listed by `kt broker-ca status`, or revoke its enrollment, then promote again."
35405
35644
  },
35406
- // authz.ts:69, connection-import.ts:116. Overage freeze after a downgrade — read stays, write stops.
35645
+ // authz.ts:69, connection-import.ts:142. Overage freeze after a downgrade — read stays, write stops.
35407
35646
  vault_frozen: {
35408
35647
  title: "Vault is read-only",
35409
35648
  why: "This vault is frozen for writes because the plan's vault limit was exceeded after a downgrade.",
@@ -35493,7 +35732,7 @@ var REFUSALS = {
35493
35732
  why: "Kutrion does not display, copy, export or share this credential's value. It is still used to authenticate connections and integrations.",
35494
35733
  action: "Use the credential rather than reading it \u2014 `kt ssh <connId>` or `kt exec <connId> <command>` broker a session with it without the value passing through this terminal."
35495
35734
  },
35496
- // modules/ssh.ts:462, modules/rdp.ts:151.
35735
+ // modules/ssh.ts:470, modules/rdp.ts:151.
35497
35736
  inject_only_requires_write: {
35498
35737
  title: "Write access required for a No-reveal credential",
35499
35738
  why: "Pointing a connection at a No-reveal credential needs write access to the vault holding it, because the person doing it decides where the credential gets presented.",
@@ -35625,15 +35864,15 @@ var REFUSALS = {
35625
35864
  action: "Ask whoever created it for a new one. `kt share <credId>` mints a fresh one-time link if the credential is yours."
35626
35865
  }
35627
35866
  };
35628
- var str4 = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
35867
+ var str6 = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
35629
35868
  var asRecord = (body) => body && typeof body === "object" ? body : null;
35630
35869
  function serverText(body) {
35631
35870
  const b = asRecord(body);
35632
35871
  if (!b) return null;
35633
- return str4(b.hint) ?? str4(b.detail) ?? str4(b.remedy) ?? str4(b.message) ?? str4(b.reason);
35872
+ return str6(b.hint) ?? str6(b.detail) ?? str6(b.remedy) ?? str6(b.message) ?? str6(b.reason);
35634
35873
  }
35635
35874
  function refusalCode(e) {
35636
- return str4(asRecord(e.body)?.error) ?? str4(e.message) ?? "unknown";
35875
+ return str6(asRecord(e.body)?.error) ?? str6(e.message) ?? "unknown";
35637
35876
  }
35638
35877
  var CONTEXT_FIELDS = [
35639
35878
  "feature",
@@ -35758,7 +35997,7 @@ function jumpRefusalReport(frame, code, head, ctx, indent) {
35758
35997
  if (!jump) return null;
35759
35998
  const names = ctx.names ?? noNames;
35760
35999
  const lines = [code ? `${head} (${code})` : head, "", jumpRefusalSummary(jump, names, ctx.openedName)];
35761
- const message = str4(frame.error);
36000
+ const message = str6(frame.error);
35762
36001
  if (jump.detail && jump.detail !== message) lines.push("", jump.detail);
35763
36002
  if (jump.allowedDestinations?.length) {
35764
36003
  lines.push("", jumpAllowedDestinationsLabel(jump, names), ...jump.allowedDestinations.map((a) => `${indent}${a}`));
@@ -35767,14 +36006,16 @@ function jumpRefusalReport(frame, code, head, ctx, indent) {
35767
36006
  return lines;
35768
36007
  }
35769
36008
  function wsRefusalLines(frame, indent = " ", jumpCtx = {}) {
35770
- const code = str4(frame.code);
36009
+ const code = str6(frame.code);
35771
36010
  const d = frame.detail ?? {};
35772
- const host2 = str4(d.host);
36011
+ const host2 = str6(d.host);
35773
36012
  const port = typeof d.port === "number" ? d.port : null;
35774
36013
  const allowed = Array.isArray(d.allowed_destinations) ? d.allowed_destinations.filter((x) => typeof x === "string" && x.trim() !== "") : [];
35775
36014
  if (code === "host_key_unconfirmed") {
35776
36015
  const k = parseHostKeyUnconfirmed(frame.detail);
35777
36016
  if (k) return hostKeyUnconfirmedLines(k, jumpCtx.own, indent);
36017
+ const chained = parseHostKeyChained(frame.detail);
36018
+ if (chained) return hostKeyChainedLines(chained, jumpCtx.own, indent);
35778
36019
  }
35779
36020
  const family = refusalFamily(code);
35780
36021
  const dest = parseDestinationRefusal({ error: code, reason: d.reason, allowed_destinations: allowed }) ?? { credential: null, destination: "", allowedDestinations: allowed, detail: null, side: null, reason: null };
@@ -35790,20 +36031,20 @@ function wsRefusalLines(frame, indent = " ", jumpCtx = {}) {
35790
36031
  const jump = jumpRefusalReport(frame, code, head, jumpCtx, indent);
35791
36032
  if (jump) return jump;
35792
36033
  const lines = [code && !ourFault ? `${head} (${code})` : head, ""];
35793
- lines.push(str4(frame.error) ?? "The server sent no explanation with this refusal.");
36034
+ lines.push(str6(frame.error) ?? "The server sent no explanation with this refusal.");
35794
36035
  if (host2) lines.push("", `${indent}endpoint: ${port === null ? host2 : `${host2}:${port}`}`);
35795
36036
  if (allowed.length) {
35796
36037
  lines.push("", destinationRefusalAllowedLabel(dest));
35797
36038
  lines.push(...allowed.map((a) => `${indent}${a}`));
35798
36039
  }
35799
- const remedy = !isDest ? str4(d.remedy) : ourFault ? destinationRefusalAction(dest) : `${destinationRefusalAction(dest)} ${DESTINATION_REFUSAL_NOT_A_REMEDY} ${DESTINATION_REFUSAL_CLI_ROUTE}`;
36040
+ const remedy = !isDest ? str6(d.remedy) : ourFault ? destinationRefusalAction(dest) : `${destinationRefusalAction(dest)} ${DESTINATION_REFUSAL_NOT_A_REMEDY} ${DESTINATION_REFUSAL_CLI_ROUTE}`;
35800
36041
  if (remedy) lines.push("", remedy);
35801
36042
  else if (!code) lines.push("", "The server sent no code and no remedy with this refusal.");
35802
36043
  return lines;
35803
36044
  }
35804
36045
 
35805
36046
  // src/shell.ts
35806
- function runShell(base, token, connId, onExit, jumpCtx = {}, afterSession) {
36047
+ function runShell(base, token, connId, onExit, jumpCtx = {}, afterSession, onChainedHostKey) {
35807
36048
  const wsUrl = shellUrl(base, connId);
35808
36049
  const ws = new wrapper_default(wsUrl, { headers: { Authorization: `Bearer ${token}` }, agent: wsAgentFor(wsUrl) });
35809
36050
  const stdin = process.stdin;
@@ -35861,6 +36102,14 @@ function runShell(base, token, connId, onExit, jumpCtx = {}, afterSession) {
35861
36102
  const frame = parseWsRefusal(text2);
35862
36103
  if (frame) {
35863
36104
  refused = true;
36105
+ const chained = frame.code === "host_key_unconfirmed" ? parseHostKeyChained(frame.detail) : null;
36106
+ if (chained && onChainedHostKey && isTty) {
36107
+ if (stdin.isTTY) stdin.setRawMode(false);
36108
+ stdin.removeAllListeners("data");
36109
+ void onChainedHostKey(chained).catch(() => {
36110
+ }).then(() => cleanup(1));
36111
+ return;
36112
+ }
35864
36113
  process.stderr.write(`\r
35865
36114
  ${wsRefusalLines(frame, " ", jumpCtx).join("\r\n")}\r
35866
36115
  `);
@@ -36459,16 +36708,16 @@ function rejectLiteralSecrets(node, path) {
36459
36708
  }
36460
36709
  }
36461
36710
  function execCommand(v, where) {
36462
- const command = str5(v, where);
36711
+ const command = str7(v, where);
36463
36712
  rejectVaultReferenceInCommand(command, where);
36464
36713
  return command;
36465
36714
  }
36466
- function str5(v, where) {
36715
+ function str7(v, where) {
36467
36716
  if (typeof v !== "string" || v.trim() === "") throw new JobFileError(`${where} must be a non-empty string`);
36468
36717
  return v;
36469
36718
  }
36470
36719
  function connectionAlias(v, where, declared) {
36471
- const alias = str5(v, where);
36720
+ const alias = str7(v, where);
36472
36721
  if (!(alias in declared)) throw new JobFileError(`${where}: undeclared connection "${alias}"`);
36473
36722
  return alias;
36474
36723
  }
@@ -36485,10 +36734,10 @@ function parseExpectations(raw, where) {
36485
36734
  case "stdout_contains":
36486
36735
  case "stderr_contains":
36487
36736
  case "stdout_not_contains":
36488
- out[k] = str5(v, `${where}.${k}`);
36737
+ out[k] = str7(v, `${where}.${k}`);
36489
36738
  break;
36490
36739
  case "stdout_matches": {
36491
- const pattern = str5(v, `${where}.stdout_matches`);
36740
+ const pattern = str7(v, `${where}.stdout_matches`);
36492
36741
  try {
36493
36742
  new RegExp(pattern);
36494
36743
  } catch {
@@ -36505,7 +36754,7 @@ function parseExpectations(raw, where) {
36505
36754
  }
36506
36755
  function parseSyncSide(raw, where, declared) {
36507
36756
  if (!isRecord(raw)) throw new JobFileError(`${where} must be a mapping with connection and path`);
36508
- return { connection: connectionAlias(raw.connection, `${where}.connection`, declared), path: str5(raw.path, `${where}.path`) };
36757
+ return { connection: connectionAlias(raw.connection, `${where}.connection`, declared), path: str7(raw.path, `${where}.path`) };
36509
36758
  }
36510
36759
  function parseStep(raw, index, declared) {
36511
36760
  const where = `steps[${index}]`;
@@ -36543,16 +36792,16 @@ function parseStep(raw, index, declared) {
36543
36792
  ...common,
36544
36793
  action,
36545
36794
  connection: connectionAlias(body.connection, `${where}.get.connection`, declared),
36546
- remote: str5(body.remote, `${where}.get.remote`),
36547
- local: str5(body.local, `${where}.get.local`)
36795
+ remote: str7(body.remote, `${where}.get.remote`),
36796
+ local: str7(body.local, `${where}.get.local`)
36548
36797
  };
36549
36798
  case "put":
36550
36799
  return {
36551
36800
  ...common,
36552
36801
  action,
36553
36802
  connection: connectionAlias(body.connection, `${where}.put.connection`, declared),
36554
- local: str5(body.local, `${where}.put.local`),
36555
- remote: str5(body.remote, `${where}.put.remote`)
36803
+ local: str7(body.local, `${where}.put.local`),
36804
+ remote: str7(body.remote, `${where}.put.remote`)
36556
36805
  };
36557
36806
  case "sync":
36558
36807
  return {
@@ -36579,7 +36828,7 @@ function parseJobFile(source) {
36579
36828
  if (doc.connections !== void 0 && doc.connections !== null) {
36580
36829
  if (!isRecord(doc.connections)) throw new JobFileError("connections must be a mapping of alias to connection name or id");
36581
36830
  for (const [alias, target] of Object.entries(doc.connections)) {
36582
- connections[alias] = str5(target, `connections.${alias}`);
36831
+ connections[alias] = str7(target, `connections.${alias}`);
36583
36832
  }
36584
36833
  }
36585
36834
  if (!Array.isArray(doc.steps) || doc.steps.length === 0) {
@@ -37899,13 +38148,13 @@ var ROLES = ["OWNER", "ADMIN", "EDITOR", "VIEWER"];
37899
38148
  var TOKEN_SCOPES = ["scim", "events"];
37900
38149
  var ORG_ROLES = ["owner", "admin", "member"];
37901
38150
  var ROTATION_SERVICES = ["linux_password", "postgres_password", "mysql_password"];
37902
- var VERSION = true ? "1.2.9" : "dev";
38151
+ var VERSION = true ? "1.2.11" : "dev";
37903
38152
  var HELP = `kt \u2014 Kutrion CLI
37904
38153
  kt login [--email <e>] [--password-stdin] [--code <2fa>] prompts for the password, or reads it from stdin;
37905
38154
  at a terminal with no --email, first asks the Kutrion desktop app,
37906
- then signs in through your web browser if that app is not running;
38155
+ then falls back to your email and password;
37907
38156
  at the emailed-code prompt, a blank entry or r sends a new code
37908
- kt login --browser sign in with Kutrion: approve this CLI in your web browser
38157
+ kt login --browser not available; exits with an error
37909
38158
  kt whoami
37910
38159
  kt logout
37911
38160
 
@@ -38107,7 +38356,7 @@ var HELP = `kt \u2014 Kutrion CLI
38107
38356
  kt integrations [catalog] alert/SIEM integrations (or the event catalog)
38108
38357
  kt integration test <id> send a test delivery
38109
38358
  kt hcv [sync <connId>] HashiCorp Vault connections (or sync one)
38110
- kt compliance [report] SOC2 controls (or an evidence report)
38359
+ kt compliance [report] control mapping to SOC 2 and PCI-DSS (or a printable report)
38111
38360
  kt analytics usage overview
38112
38361
  kt emergency emergency-access contacts
38113
38362
 
@@ -38424,66 +38673,29 @@ run \`kt --help\` for the flags each command takes`);
38424
38673
  const fromStdin = flags["password-stdin"] === true;
38425
38674
  if (fromStdin && typeof flags.email !== "string") die("--password-stdin needs --email, because stdin carries the password");
38426
38675
  const accountRoot = appDataDir();
38427
- const viaBrowser = flags.browser === true;
38428
- if (viaBrowser && (flags.email !== void 0 || fromStdin || flags.code !== void 0)) {
38429
- die("--browser signs in through the web browser; it cannot be combined with --email, --password-stdin or --code");
38430
- }
38431
- if (viaBrowser && !process.stdin.isTTY) {
38432
- console.error("kt login --browser needs an interactive terminal to confirm the account. In a script, use `kt login --email <email> --password-stdin`.");
38676
+ if (flags.browser === true) {
38677
+ console.error("kt login --browser is not available: browser sign-in is switched off. Sign in with the Kutrion app (`kt login`), or use `kt login --email <email> --password-stdin`.");
38433
38678
  process.exit(EXIT_ERROR);
38434
38679
  }
38435
- const browserSignIn = async () => {
38436
- const hint = otherAppsHint(accountRoot);
38437
- if (hint) console.error(hint);
38438
- const r = await browserLogin({
38680
+ if (process.stdin.isTTY && flags.email === void 0 && !fromStdin && flags.code === void 0 && accountRoot) {
38681
+ const hub = await hubLogin({
38439
38682
  base: cfg.base,
38683
+ location: hubLocation(accountRoot),
38440
38684
  kc,
38441
38685
  keychainAvailable: osKeychainAvailable,
38442
38686
  call: hubCall,
38443
- post: postJson(cfg.base, refuseBodyRedirects()),
38444
- open: openInBrowser,
38445
38687
  out: (l) => console.error(l),
38446
- ask: (q) => promptLine(q),
38447
- deviceName: () => (0, import_os5.hostname)() || "Kutrion CLI",
38448
- allowLoopbackHttp: isLoopback(u.hostname)
38688
+ ask: (q) => promptLine(q)
38449
38689
  });
38450
- if (r.kind === "store_failed") die(r.note);
38451
- if (r.kind === "failed") {
38452
- console.error(r.note);
38453
- if (viaBrowser) process.exit(EXIT_ERROR);
38454
- return false;
38455
- }
38456
- recordSignIn(accountRoot, r.user);
38457
- console.log(`logged in with Kutrion as ${printable(r.user.email)} (token stored in ${backendName()})`);
38458
- if (rememberBase(cfg.base)) console.log(`server saved: ${cfg.base} (later commands need no --base)`);
38459
- return true;
38460
- };
38461
- if (viaBrowser) {
38462
- if (await browserSignIn()) break;
38463
- }
38464
- if (process.stdin.isTTY && flags.email === void 0 && !fromStdin && flags.code === void 0) {
38465
- if (!accountRoot) {
38466
- if (await browserSignIn()) break;
38467
- } else {
38468
- const hub = await hubLogin({
38469
- base: cfg.base,
38470
- location: hubLocation(accountRoot),
38471
- kc,
38472
- keychainAvailable: osKeychainAvailable,
38473
- call: hubCall,
38474
- out: (l) => console.error(l),
38475
- ask: (q) => promptLine(q)
38476
- });
38477
- if (hub.kind === "store_failed") die(hub.note);
38478
- if (hub.kind === "signed_in") {
38479
- recordSignIn(accountRoot, hub.user);
38480
- console.log(`logged in with the Kutrion app${hub.user ? ` as ${printable(hub.user.email)}` : ""} (token stored in ${backendName()})`);
38481
- if (rememberBase(cfg.base)) console.log(`server saved: ${cfg.base} (later commands need no --base)`);
38482
- break;
38483
- }
38484
- if (hub.note) console.error(hub.note);
38485
- if (hubAbsent(hub.reason) && await browserSignIn()) break;
38690
+ if (hub.kind === "store_failed") die(hub.note);
38691
+ if (hub.kind === "signed_in") {
38692
+ recordSignIn(accountRoot, hub.user);
38693
+ console.log(`logged in with the Kutrion app${hub.user ? ` as ${printable(hub.user.email)}` : ""} (token stored in ${backendName()})`);
38694
+ if (rememberBase(cfg.base)) console.log(`server saved: ${cfg.base} (later commands need no --base)`);
38695
+ break;
38486
38696
  }
38697
+ if (hub.note) console.error(hub.note);
38698
+ if (hubAbsent(hub.reason)) console.error("Installing the Kutrion app... Run `kt login` again once it is open, or sign in with your email and password below.");
38487
38699
  }
38488
38700
  if (flags.email === void 0 && !fromStdin) {
38489
38701
  const hint = otherAppsHint(accountRoot);
@@ -38714,7 +38926,16 @@ run \`kt --help\` for the flags each command takes`);
38714
38926
  break;
38715
38927
  }
38716
38928
  if (!positionals[0]) return die("usage: kt ssh <connId>");
38717
- runShell(cfg.base, token, positionals[0], void 0, await connectionContext(positionals[0]));
38929
+ const sshCtx = await connectionContext(positionals[0]);
38930
+ const sshId = positionals[0];
38931
+ runShell(cfg.base, token, sshId, void 0, sshCtx, void 0, async (k) => {
38932
+ await confirmChainedHostKey({
38933
+ isAdmin: () => client.me().then((r) => r.user.org_role === "owner" || r.user.org_role === "admin", () => false),
38934
+ ask: (q) => promptLine(q),
38935
+ confirm: (id, fp) => withStepUp(client, () => client.confirmSshHostKey(id, fp)),
38936
+ out: (l) => console.error(l)
38937
+ }, sshId, k, sshCtx.own);
38938
+ });
38718
38939
  return;
38719
38940
  }
38720
38941
  case "exec": {
@@ -38722,8 +38943,10 @@ run \`kt --help\` for the flags each command takes`);
38722
38943
  if (!positionals[0] || !positionals[1]) return die("usage: kt exec <connId> <command...>");
38723
38944
  const r = await client.exec(positionals[0], positionals.slice(1).join(" ")).catch(async (e) => {
38724
38945
  const k = hostKeyUnconfirmedFromError(e);
38725
- if (!k) throw e;
38726
- for (const line of hostKeyUnconfirmedLines(k, (await connectionContext(positionals[0])).own)) console.error(line);
38946
+ const chained = k ? null : hostKeyChainedFromError(e);
38947
+ if (!k && !chained) throw e;
38948
+ const own = (await connectionContext(positionals[0])).own;
38949
+ for (const line of k ? hostKeyUnconfirmedLines(k, own) : hostKeyChainedLines(chained, own)) console.error(line);
38727
38950
  return process.exit(1);
38728
38951
  });
38729
38952
  if (asJson) console.log(JSON.stringify(r, null, 2));
@@ -38811,8 +39034,10 @@ run \`kt --help\` for the flags each command takes`);
38811
39034
  }
38812
39035
  } catch (e) {
38813
39036
  const k = hostKeyUnconfirmedFromError(e);
38814
- if (!k) throw e;
38815
- for (const line of hostKeyUnconfirmedLines(k, (await connectionContext(connId)).own)) console.error(line);
39037
+ const chained = k ? null : hostKeyChainedFromError(e);
39038
+ if (!k && !chained) throw e;
39039
+ const own = (await connectionContext(connId)).own;
39040
+ for (const line of k ? hostKeyUnconfirmedLines(k, own) : hostKeyChainedLines(chained, own)) console.error(line);
38816
39041
  process.exit(1);
38817
39042
  }
38818
39043
  break;
@@ -39628,14 +39853,19 @@ Re-run with --fill to type a username and password for each, or create them with
39628
39853
  }
39629
39854
  let imported = 0, updated = 0;
39630
39855
  const skipped = [];
39856
+ const needsBinding = [];
39857
+ const boundByCred = /* @__PURE__ */ new Map();
39631
39858
  for (let i = 0; i < ready.length; i += 200) {
39632
39859
  const r = await client.commitConnImport(vaultId, ready.slice(i, i + 200));
39633
39860
  imported += r.imported;
39634
39861
  updated += r.updated;
39635
39862
  skipped.push(...r.skipped);
39863
+ needsBinding.push(...r.needs_binding ?? []);
39864
+ for (const b of r.bound_destinations ?? []) boundByCred.set(b.credential_id, b);
39636
39865
  }
39866
+ const bound = [...boundByCred.values()];
39637
39867
  if (asJson) {
39638
- show({ provider: parsed.provider, imported, updated, skipped, held_back: blocked.length });
39868
+ show({ provider: parsed.provider, imported, updated, skipped, held_back: blocked.length, needs_binding: needsBinding, bound_destinations: bound });
39639
39869
  break;
39640
39870
  }
39641
39871
  console.log(`imported ${imported} from ${parsed.provider}${updated ? `, updated ${updated}` : ""}`);
@@ -39644,6 +39874,15 @@ Re-run with --fill to type a username and password for each, or create them with
39644
39874
  for (const [reason, names] of byReason) {
39645
39875
  console.log(`skipped ${names.length} \u2014 ${CONN_SKIP_REASON[reason] ?? reason}: ${names.join(", ")}`);
39646
39876
  }
39877
+ for (const b of bound) {
39878
+ console.log(CONN_IMPORT_BINDING_COPY.bound(b.credential, b.destinations.length));
39879
+ for (const d of b.destinations) console.log(` ${connImportDestLabel(d)}`);
39880
+ }
39881
+ if (needsBinding.length) {
39882
+ console.log(`${CONN_IMPORT_BINDING_COPY.needsHeading}: ${CONN_IMPORT_BINDING_COPY.needsIntro(needsBinding.length)}`);
39883
+ for (const n of needsBinding) console.log(` ${n.name} (${connImportDestLabel(n)})`);
39884
+ console.log(CONN_IMPORT_BINDING_COPY.needsWhere);
39885
+ }
39647
39886
  } else die("usage: kt conn add --name <n> --host <h> --username <u> --cred <credId> | kt conn set <connId> [--name --host --port --username --cred] | kt conn rm <connId> | kt conn providers | kt conn import <vaultId> <file>");
39648
39887
  break;
39649
39888
  }