checkly 8.14.1 → 8.15.0

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 (148) hide show
  1. package/dist/ai-context/context.d.ts +18 -0
  2. package/dist/ai-context/context.js +89 -0
  3. package/dist/ai-context/context.js.map +1 -1
  4. package/dist/ai-context/skills-command/references/configure-grpc-monitors.md +37 -0
  5. package/dist/ai-context/skills-command/references/configure-playwright-checks.md +5 -5
  6. package/dist/ai-context/skills-command/references/configure-ssl-monitors.md +39 -0
  7. package/dist/ai-context/skills-command/references/configure-traceroute-monitors.md +36 -0
  8. package/dist/ai-context/skills-command/references/configure.md +9 -0
  9. package/dist/commands/checks/stats.js +1 -0
  10. package/dist/commands/checks/stats.js.map +1 -1
  11. package/dist/commands/deploy.js +25 -16
  12. package/dist/commands/deploy.js.map +1 -1
  13. package/dist/constants.d.ts +4 -1
  14. package/dist/constants.js +3 -0
  15. package/dist/constants.js.map +1 -1
  16. package/dist/constructs/api-assertion-codegen.js +2 -2
  17. package/dist/constructs/api-assertion-codegen.js.map +1 -1
  18. package/dist/constructs/api-assertion.d.ts +3 -3
  19. package/dist/constructs/check-codegen.d.ts +6 -0
  20. package/dist/constructs/check-codegen.js +24 -0
  21. package/dist/constructs/check-codegen.js.map +1 -1
  22. package/dist/constructs/dns-assertion-codegen.js +2 -2
  23. package/dist/constructs/dns-assertion-codegen.js.map +1 -1
  24. package/dist/constructs/dns-assertion.d.ts +3 -3
  25. package/dist/constructs/dns-request.d.ts +2 -2
  26. package/dist/constructs/grpc-assertion-codegen.d.ts +3 -0
  27. package/dist/constructs/grpc-assertion-codegen.js +33 -0
  28. package/dist/constructs/grpc-assertion-codegen.js.map +1 -0
  29. package/dist/constructs/grpc-assertion-validation.d.ts +13 -0
  30. package/dist/constructs/grpc-assertion-validation.js +53 -0
  31. package/dist/constructs/grpc-assertion-validation.js.map +1 -0
  32. package/dist/constructs/grpc-assertion.d.ts +61 -0
  33. package/dist/constructs/grpc-assertion.js +71 -0
  34. package/dist/constructs/grpc-assertion.js.map +1 -0
  35. package/dist/constructs/grpc-monitor-codegen.d.ts +13 -0
  36. package/dist/constructs/grpc-monitor-codegen.js +34 -0
  37. package/dist/constructs/grpc-monitor-codegen.js.map +1 -0
  38. package/dist/constructs/grpc-monitor.d.ts +83 -0
  39. package/dist/constructs/grpc-monitor.js +56 -0
  40. package/dist/constructs/grpc-monitor.js.map +1 -0
  41. package/dist/constructs/grpc-request-codegen.d.ts +4 -0
  42. package/dist/constructs/grpc-request-codegen.js +68 -0
  43. package/dist/constructs/grpc-request-codegen.js.map +1 -0
  44. package/dist/constructs/grpc-request.d.ts +137 -0
  45. package/dist/constructs/grpc-request.js +2 -0
  46. package/dist/constructs/grpc-request.js.map +1 -0
  47. package/dist/constructs/icmp-assertion-codegen.js +2 -2
  48. package/dist/constructs/icmp-assertion-codegen.js.map +1 -1
  49. package/dist/constructs/icmp-assertion.d.ts +1 -1
  50. package/dist/constructs/icmp-request.d.ts +2 -2
  51. package/dist/constructs/index.d.ts +9 -0
  52. package/dist/constructs/index.js +9 -0
  53. package/dist/constructs/index.js.map +1 -1
  54. package/dist/constructs/internal/assertion-codegen.d.ts +11 -0
  55. package/dist/constructs/internal/assertion-codegen.js +39 -0
  56. package/dist/constructs/internal/assertion-codegen.js.map +1 -1
  57. package/dist/constructs/internal/assertion-validation.d.ts +3 -0
  58. package/dist/constructs/internal/assertion-validation.js +17 -0
  59. package/dist/constructs/internal/assertion-validation.js.map +1 -0
  60. package/dist/constructs/internal/assertion.d.ts +17 -11
  61. package/dist/constructs/internal/assertion.js +39 -38
  62. package/dist/constructs/internal/assertion.js.map +1 -1
  63. package/dist/constructs/internal/common-diagnostics.d.ts +1 -0
  64. package/dist/constructs/internal/common-diagnostics.js +14 -0
  65. package/dist/constructs/internal/common-diagnostics.js.map +1 -1
  66. package/dist/constructs/ssl-assertion-codegen.d.ts +3 -0
  67. package/dist/constructs/ssl-assertion-codegen.js +36 -0
  68. package/dist/constructs/ssl-assertion-codegen.js.map +1 -0
  69. package/dist/constructs/ssl-assertion-validation.d.ts +13 -0
  70. package/dist/constructs/ssl-assertion-validation.js +49 -0
  71. package/dist/constructs/ssl-assertion-validation.js.map +1 -0
  72. package/dist/constructs/ssl-assertion.d.ts +197 -0
  73. package/dist/constructs/ssl-assertion.js +267 -0
  74. package/dist/constructs/ssl-assertion.js.map +1 -0
  75. package/dist/constructs/ssl-monitor-codegen.d.ts +36 -0
  76. package/dist/constructs/ssl-monitor-codegen.js +51 -0
  77. package/dist/constructs/ssl-monitor-codegen.js.map +1 -0
  78. package/dist/constructs/ssl-monitor.d.ts +89 -0
  79. package/dist/constructs/ssl-monitor.js +73 -0
  80. package/dist/constructs/ssl-monitor.js.map +1 -0
  81. package/dist/constructs/ssl-request-codegen.d.ts +4 -0
  82. package/dist/constructs/ssl-request-codegen.js +48 -0
  83. package/dist/constructs/ssl-request-codegen.js.map +1 -0
  84. package/dist/constructs/ssl-request.d.ts +143 -0
  85. package/dist/constructs/ssl-request.js +2 -0
  86. package/dist/constructs/ssl-request.js.map +1 -0
  87. package/dist/constructs/tcp-monitor-codegen.js +2 -2
  88. package/dist/constructs/tcp-monitor-codegen.js.map +1 -1
  89. package/dist/constructs/tcp-monitor.d.ts +1 -1
  90. package/dist/constructs/traceroute-assertion-codegen.d.ts +3 -0
  91. package/dist/constructs/traceroute-assertion-codegen.js +17 -0
  92. package/dist/constructs/traceroute-assertion-codegen.js.map +1 -0
  93. package/dist/constructs/traceroute-assertion-validation.d.ts +11 -0
  94. package/dist/constructs/traceroute-assertion-validation.js +77 -0
  95. package/dist/constructs/traceroute-assertion-validation.js.map +1 -0
  96. package/dist/constructs/traceroute-assertion.d.ts +67 -0
  97. package/dist/constructs/traceroute-assertion.js +96 -0
  98. package/dist/constructs/traceroute-assertion.js.map +1 -0
  99. package/dist/constructs/traceroute-monitor-codegen.d.ts +13 -0
  100. package/dist/constructs/traceroute-monitor-codegen.js +34 -0
  101. package/dist/constructs/traceroute-monitor-codegen.js.map +1 -0
  102. package/dist/constructs/traceroute-monitor.d.ts +83 -0
  103. package/dist/constructs/traceroute-monitor.js +54 -0
  104. package/dist/constructs/traceroute-monitor.js.map +1 -0
  105. package/dist/constructs/traceroute-request-codegen.d.ts +4 -0
  106. package/dist/constructs/traceroute-request-codegen.js +41 -0
  107. package/dist/constructs/traceroute-request-codegen.js.map +1 -0
  108. package/dist/constructs/traceroute-request.d.ts +81 -0
  109. package/dist/constructs/traceroute-request.js +2 -0
  110. package/dist/constructs/traceroute-request.js.map +1 -0
  111. package/dist/constructs/url-assertion-codegen.js +2 -2
  112. package/dist/constructs/url-assertion-codegen.js.map +1 -1
  113. package/dist/formatters/assertion-line.d.ts +27 -0
  114. package/dist/formatters/assertion-line.js +126 -0
  115. package/dist/formatters/assertion-line.js.map +1 -0
  116. package/dist/formatters/batch-stats.js +20 -1
  117. package/dist/formatters/batch-stats.js.map +1 -1
  118. package/dist/formatters/check-result-detail.js +604 -1
  119. package/dist/formatters/check-result-detail.js.map +1 -1
  120. package/dist/helpers/cli-mode.d.ts +2 -2
  121. package/dist/helpers/cli-mode.js +103 -11
  122. package/dist/helpers/cli-mode.js.map +1 -1
  123. package/dist/reporters/json.d.ts +1 -0
  124. package/dist/reporters/json.js +58 -0
  125. package/dist/reporters/json.js.map +1 -1
  126. package/dist/reporters/util.js +192 -89
  127. package/dist/reporters/util.js.map +1 -1
  128. package/dist/rest/analytics.js +6 -0
  129. package/dist/rest/analytics.js.map +1 -1
  130. package/dist/rest/batch-analytics.d.ts +1 -0
  131. package/dist/rest/batch-analytics.js.map +1 -1
  132. package/dist/rest/check-results.d.ts +75 -1
  133. package/dist/rest/check-results.js.map +1 -1
  134. package/dist/rest/projects.d.ts +1 -1
  135. package/dist/services/check-parser/bundler.d.ts +6 -0
  136. package/dist/services/check-parser/bundler.js +8 -0
  137. package/dist/services/check-parser/bundler.js.map +1 -1
  138. package/dist/services/check-parser/cache-hash.d.ts +24 -2
  139. package/dist/services/check-parser/cache-hash.js +45 -3
  140. package/dist/services/check-parser/cache-hash.js.map +1 -1
  141. package/dist/services/check-parser/package-files/resolver.d.ts +6 -1
  142. package/dist/services/check-parser/package-files/resolver.js +19 -0
  143. package/dist/services/check-parser/package-files/resolver.js.map +1 -1
  144. package/dist/services/util.d.ts +4 -0
  145. package/dist/services/util.js +4 -0
  146. package/dist/services/util.js.map +1 -1
  147. package/oclif.manifest.json +9 -3
  148. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { formatMs, formatDate, resolveResultStatus, heading, escapeMdCell, renderDetailFields, renderCommandHints, renderAdaptiveTable, truncateSingleLine, } from './render.js';
2
+ import { formatMs, formatDate, resolveResultStatus, heading, escapeMdCell, renderDetailFields, renderCommandHints, renderAdaptiveTable, truncateSingleLine, stripAnsi, } from './render.js';
3
+ import { assertionSymbols, formatAssertionLine } from './assertion-line.js';
3
4
  // --- Helpers ---
4
5
  function label(text, width = 16) {
5
6
  return chalk.dim(text.padEnd(width));
@@ -53,6 +54,16 @@ function firstErrorMessage(result) {
53
54
  .find(m => m.length > 0);
54
55
  if (agenticErr)
55
56
  return agenticErr;
57
+ const tracerouteErr = result.tracerouteCheckResult?.requestError;
58
+ if (tracerouteErr)
59
+ return tracerouteErr;
60
+ const grpcErr = result.grpcCheckResult?.requestError
61
+ ?? result.grpcCheckResult?.response?.grpcStatusMessage;
62
+ if (grpcErr)
63
+ return grpcErr;
64
+ const sslErr = result.sslCheckResult?.requestError ?? result.sslCheckResult?.failureCategory;
65
+ if (sslErr)
66
+ return sslErr;
56
67
  return '';
57
68
  }
58
69
  // Renders the retry table for a sequence; `attempts` must be oldest-first.
@@ -147,6 +158,24 @@ export function formatResultDetail(result, format) {
147
158
  : formatAgenticResultTerminal(result.agenticCheckResult);
148
159
  parts.push(subLines.join('\n'));
149
160
  }
161
+ if (result.tracerouteCheckResult) {
162
+ const subLines = format === 'md'
163
+ ? formatTracerouteResultMd(result.tracerouteCheckResult)
164
+ : formatTracerouteResultTerminal(result.tracerouteCheckResult);
165
+ parts.push(subLines.join('\n'));
166
+ }
167
+ if (result.grpcCheckResult) {
168
+ const subLines = format === 'md'
169
+ ? formatGrpcResultMd(result.grpcCheckResult)
170
+ : formatGrpcResultTerminal(result.grpcCheckResult);
171
+ parts.push(subLines.join('\n'));
172
+ }
173
+ if (result.sslCheckResult) {
174
+ const subLines = format === 'md'
175
+ ? formatSslResultMd(result.sslCheckResult)
176
+ : formatSslResultTerminal(result.sslCheckResult);
177
+ parts.push(subLines.join('\n'));
178
+ }
150
179
  return parts.join('\n\n');
151
180
  }
152
181
  // --- API check result (terminal) ---
@@ -490,6 +519,580 @@ function formatAgenticResultMd(agentic) {
490
519
  }
491
520
  return lines;
492
521
  }
522
+ // --- Traceroute check result ---
523
+ //
524
+ // The diagnostic objects (tracerouteCheckResult/grpcCheckResult/sslCheckResult)
525
+ // carry documented top-level scalars plus a richer `response` artifact. The
526
+ // renderers prefer the top-level scalar but fall back to the artifact so a
527
+ // metadata-only uptime result still surfaces something useful.
528
+ function num(...vals) {
529
+ for (const v of vals) {
530
+ if (typeof v === 'number' && !Number.isNaN(v))
531
+ return v;
532
+ }
533
+ return undefined;
534
+ }
535
+ function str(...vals) {
536
+ for (const v of vals) {
537
+ if (typeof v === 'string' && v.length > 0)
538
+ return v;
539
+ }
540
+ return undefined;
541
+ }
542
+ function boolFlag(...vals) {
543
+ for (const v of vals) {
544
+ if (typeof v === 'boolean')
545
+ return v;
546
+ }
547
+ return undefined;
548
+ }
549
+ function yesNo(value, { goodWhenTrue = true } = {}) {
550
+ if (value == null)
551
+ return chalk.dim('—');
552
+ const good = goodWhenTrue ? value : !value;
553
+ const text = value ? 'yes' : 'no';
554
+ return good ? chalk.green(text) : chalk.red(text);
555
+ }
556
+ // finalHopLatency / RTT objects come straight from the runner artifact, where
557
+ // keys are snake_case (`avg_ms`/`best_ms`/`worst_ms`); accept the camelCase
558
+ // variants too in case a consumer reshapes the payload.
559
+ function formatLatencyStats(obj) {
560
+ if (!obj || typeof obj !== 'object')
561
+ return undefined;
562
+ const o = obj;
563
+ const avg = num(o.avgMs, o.avg_ms, o.avg);
564
+ const best = num(o.bestMs, o.best_ms, o.best);
565
+ const worst = num(o.worstMs, o.worst_ms, o.worst);
566
+ const parts = [
567
+ avg != null ? `avg ${formatMs(avg)}` : null,
568
+ best != null ? `best ${formatMs(best)}` : null,
569
+ worst != null ? `worst ${formatMs(worst)}` : null,
570
+ ].filter(Boolean);
571
+ return parts.length > 0 ? parts.join(' / ') : undefined;
572
+ }
573
+ // A request that errored never evaluated its assertions, and the backend reports
574
+ // those unevaluated entries without an `error`, which renders as a green success
575
+ // mark. Suppress the whole section instead of claiming assertions passed.
576
+ function assertionsToRender(result) {
577
+ if (result.requestError)
578
+ return;
579
+ if (!result.assertions || result.assertions.length === 0)
580
+ return;
581
+ return result.assertions;
582
+ }
583
+ function appendAssertionsTerminal(lines, result) {
584
+ const assertions = assertionsToRender(result);
585
+ if (!assertions)
586
+ return;
587
+ lines.push('');
588
+ lines.push(heading('ASSERTIONS', 2, 'terminal'));
589
+ for (const assertion of assertions) {
590
+ lines.push(` ${formatAssertionLine(assertion)}`);
591
+ }
592
+ }
593
+ function appendAssertionsMd(lines, result) {
594
+ const assertions = assertionsToRender(result);
595
+ if (!assertions)
596
+ return;
597
+ lines.push('');
598
+ lines.push('## Assertions');
599
+ for (const assertion of assertions) {
600
+ lines.push(`- ${stripAnsi(formatAssertionLine(assertion))}`);
601
+ }
602
+ }
603
+ function formatTracerouteResultTerminal(tr) {
604
+ const lines = [];
605
+ const resp = tr.response ?? {};
606
+ lines.push(heading('TRACEROUTE RESULT', 2, 'terminal'));
607
+ const host = str(resp.hostname);
608
+ const ip = str(resp.resolvedIp);
609
+ if (host || ip) {
610
+ const dest = [host, ip ? `(${ip})` : null].filter(Boolean).join(' ');
611
+ lines.push(`${label('Destination:')}${dest}`);
612
+ }
613
+ const protocol = str(resp.protocol, resp.probeProtocol);
614
+ if (protocol)
615
+ lines.push(`${label('Protocol:')}${protocol}`);
616
+ const probeProtocol = str(resp.probeProtocol);
617
+ if (probeProtocol)
618
+ lines.push(`${label('Probe protocol:')}${probeProtocol}`);
619
+ const totalHops = num(tr.totalHops, resp.totalHops);
620
+ if (totalHops != null)
621
+ lines.push(`${label('Hops:')}${totalHops}`);
622
+ const reached = boolFlag(tr.destinationReached, resp.destinationReached);
623
+ lines.push(`${label('Reached:')}${yesNo(reached)}`);
624
+ const truncation = str(resp.truncationReason);
625
+ if (truncation)
626
+ lines.push(`${label('Truncated:')}${chalk.yellow(truncation)}`);
627
+ const finalHop = formatLatencyStats(tr.finalHopLatency ?? resp.finalHopLatency);
628
+ if (finalHop)
629
+ lines.push(`${label('Final hop:')}${finalHop}`);
630
+ const hops = Array.isArray(resp.hops) ? resp.hops : [];
631
+ if (hops.length > 0) {
632
+ lines.push('');
633
+ lines.push(heading('HOPS', 2, 'terminal'));
634
+ for (const hop of hops.slice(0, 40)) {
635
+ lines.push(` ${formatTracerouteHop(hop)}`);
636
+ }
637
+ if (hops.length > 40) {
638
+ lines.push(chalk.dim(` ... (${hops.length - 40} more hops)`));
639
+ }
640
+ }
641
+ const trDns = num(asObject(tr.timingPhases)?.dns);
642
+ if (trDns != null) {
643
+ lines.push('');
644
+ lines.push(heading('TIMING', 2, 'terminal'));
645
+ lines.push(`${label('DNS:')}${formatMs(trDns)}`);
646
+ }
647
+ appendAssertionsTerminal(lines, tr);
648
+ if (tr.requestError) {
649
+ lines.push('');
650
+ lines.push(heading('ERROR', 2, 'terminal'));
651
+ lines.push(chalk.red(` ${tr.requestError}`));
652
+ }
653
+ return lines;
654
+ }
655
+ function formatTracerouteHop(hop) {
656
+ if (!hop || typeof hop !== 'object')
657
+ return String(hop);
658
+ const h = hop;
659
+ const n = num(h.hop_number, h.hopNumber, h.number, h.hop);
660
+ const addr = str(h.main_ip, h.mainIp, h.ip, h.address) ?? '*';
661
+ const host = str(h.main_host, h.mainHost, h.host);
662
+ const loss = num(h.loss_percentage, h.lossPercentage, h.loss);
663
+ const rtt = formatLatencyStats(h.rtt);
664
+ const asn = num(h.asn);
665
+ return [
666
+ n != null ? chalk.dim(String(n).padStart(2)) : chalk.dim(' ·'),
667
+ host ? `${addr} (${host})` : addr,
668
+ loss != null ? `loss ${loss.toFixed(0)}%` : null,
669
+ rtt ? `rtt ${rtt}` : null,
670
+ asn ? chalk.dim(`AS${asn}`) : null,
671
+ ].filter(Boolean).join(' ');
672
+ }
673
+ function formatTracerouteResultMd(tr) {
674
+ const lines = ['## Traceroute Result'];
675
+ const resp = tr.response ?? {};
676
+ const host = str(resp.hostname);
677
+ const ip = str(resp.resolvedIp);
678
+ if (host || ip)
679
+ lines.push(`- **Destination:** ${[host, ip ? `(${ip})` : null].filter(Boolean).join(' ')}`);
680
+ const probeProtocol = str(resp.probeProtocol);
681
+ if (probeProtocol)
682
+ lines.push(`- **Probe protocol:** ${probeProtocol}`);
683
+ const totalHops = num(tr.totalHops, resp.totalHops);
684
+ if (totalHops != null)
685
+ lines.push(`- **Hops:** ${totalHops}`);
686
+ const reached = boolFlag(tr.destinationReached, resp.destinationReached);
687
+ if (reached != null)
688
+ lines.push(`- **Reached:** ${reached ? 'yes' : 'no'}`);
689
+ const truncation = str(resp.truncationReason);
690
+ if (truncation)
691
+ lines.push(`- **Truncated:** ${truncation}`);
692
+ const finalHop = formatLatencyStats(tr.finalHopLatency ?? resp.finalHopLatency);
693
+ if (finalHop)
694
+ lines.push(`- **Final hop:** ${finalHop}`);
695
+ const trDns = num(asObject(tr.timingPhases)?.dns);
696
+ if (trDns != null) {
697
+ lines.push('');
698
+ lines.push('## Timing');
699
+ lines.push(`- **DNS:** ${formatMs(trDns)}`);
700
+ }
701
+ appendAssertionsMd(lines, tr);
702
+ if (tr.requestError) {
703
+ lines.push('');
704
+ lines.push('## Error');
705
+ lines.push(`- ${tr.requestError}`);
706
+ }
707
+ return lines;
708
+ }
709
+ // --- gRPC check result ---
710
+ function formatGrpcResultTerminal(grpc) {
711
+ const lines = [];
712
+ const resp = grpc.response ?? {};
713
+ lines.push(heading('GRPC RESULT', 2, 'terminal'));
714
+ const host = str(resp.host);
715
+ const ip = str(resp.resolvedIp);
716
+ const port = num(resp.port);
717
+ if (host || ip || port != null) {
718
+ const target = [
719
+ host ?? ip,
720
+ port != null ? `:${port}` : '',
721
+ ].filter(Boolean).join('');
722
+ lines.push(`${label('Target:')}${target}`);
723
+ }
724
+ const method = str(resp.grpcMethod);
725
+ if (method)
726
+ lines.push(`${label('Method:')}${method}`);
727
+ const mode = str(resp.grpcMode);
728
+ if (mode)
729
+ lines.push(`${label('Mode:')}${mode}`);
730
+ const code = num(grpc.grpcStatusCode, resp.grpcStatusCode);
731
+ const statusMsg = str(resp.grpcStatusMessage);
732
+ if (code != null) {
733
+ const codeStr = code === 0 ? chalk.green(String(code)) : chalk.red(String(code));
734
+ lines.push(`${label('Status:')}${codeStr}${statusMsg ? ` ${statusMsg}` : ''}`);
735
+ }
736
+ const healthLabel = str(resp.healthStatusLabel);
737
+ const healthNum = num(grpc.healthStatus, resp.healthStatus);
738
+ if (healthLabel || healthNum != null) {
739
+ const display = healthLabel ?? String(healthNum);
740
+ const colored = healthLabel === 'SERVING' ? chalk.green(display) : chalk.red(display);
741
+ lines.push(`${label('Health:')}${colored}`);
742
+ }
743
+ const responseMessage = str(resp.responseMessage);
744
+ if (responseMessage)
745
+ lines.push(`${label('Response:')}${truncateSingleLine(responseMessage, 200)}`);
746
+ const methods = Array.isArray(resp.discoveredMethods) ? resp.discoveredMethods : [];
747
+ if (methods.length > 0) {
748
+ lines.push(`${label('Methods:')}${methods.slice(0, 20).join(', ')}${methods.length > 20 ? ', …' : ''}`);
749
+ }
750
+ const metadata = Array.isArray(resp.metadata) ? resp.metadata : [];
751
+ if (metadata.length > 0) {
752
+ lines.push(`${label('Metadata:')}`);
753
+ for (const entry of metadata.slice(0, 20)) {
754
+ lines.push(` ${formatKeyValueEntry(entry)}`);
755
+ }
756
+ }
757
+ // Timing breakdown. gRPC exposes dns/connect/total (not the HTTP phases the
758
+ // API TIMING bar renders), so surface those directly for parity with the API
759
+ // result's timing detail instead of only the top-level total response time.
760
+ const timing = (grpc.timingPhases ?? resp.timingPhases);
761
+ const tDns = num(timing?.dns), tConnect = num(timing?.connect), tTotal = num(timing?.total);
762
+ if (tDns != null || tConnect != null || tTotal != null) {
763
+ lines.push('');
764
+ lines.push(heading('TIMING', 2, 'terminal'));
765
+ if (tDns != null)
766
+ lines.push(`${label('DNS:')}${formatMs(tDns)}`);
767
+ if (tConnect != null)
768
+ lines.push(`${label('Connect:')}${formatMs(tConnect)}`);
769
+ if (tTotal != null)
770
+ lines.push(`${label('Total:')}${formatMs(tTotal)}`);
771
+ }
772
+ appendAssertionsTerminal(lines, grpc);
773
+ if (grpc.requestError) {
774
+ lines.push('');
775
+ lines.push(heading('ERROR', 2, 'terminal'));
776
+ lines.push(chalk.red(` ${grpc.requestError}`));
777
+ }
778
+ return lines;
779
+ }
780
+ function formatKeyValueEntry(entry) {
781
+ if (!entry || typeof entry !== 'object')
782
+ return String(entry);
783
+ const e = entry;
784
+ const key = str(e.key, e.name) ?? '(key)';
785
+ const value = e.value != null ? String(e.value) : '';
786
+ return `${chalk.dim(key + ':')} ${value}`;
787
+ }
788
+ function formatGrpcResultMd(grpc) {
789
+ const lines = ['## gRPC Result'];
790
+ const resp = grpc.response ?? {};
791
+ const code = num(grpc.grpcStatusCode, resp.grpcStatusCode);
792
+ const statusMsg = str(resp.grpcStatusMessage);
793
+ if (code != null)
794
+ lines.push(`- **Status:** ${code}${statusMsg ? ` ${statusMsg}` : ''}`);
795
+ const healthLabel = str(resp.healthStatusLabel);
796
+ if (healthLabel)
797
+ lines.push(`- **Health:** ${healthLabel}`);
798
+ const method = str(resp.grpcMethod);
799
+ if (method)
800
+ lines.push(`- **Method:** ${method}`);
801
+ const responseMessage = str(resp.responseMessage);
802
+ if (responseMessage)
803
+ lines.push(`- **Response:** \`${truncateSingleLine(responseMessage, 200)}\``);
804
+ const methods = Array.isArray(resp.discoveredMethods) ? resp.discoveredMethods : [];
805
+ if (methods.length > 0)
806
+ lines.push(`- **Discovered methods:** ${methods.join(', ')}`);
807
+ const timing = (grpc.timingPhases ?? resp.timingPhases);
808
+ const tDns = num(timing?.dns), tConnect = num(timing?.connect), tTotal = num(timing?.total);
809
+ if (tDns != null || tConnect != null || tTotal != null) {
810
+ lines.push('');
811
+ lines.push('## Timing');
812
+ lines.push('| Phase | Duration |');
813
+ lines.push('| --- | --- |');
814
+ if (tDns != null)
815
+ lines.push(`| DNS | ${formatMs(tDns)} |`);
816
+ if (tConnect != null)
817
+ lines.push(`| Connect | ${formatMs(tConnect)} |`);
818
+ if (tTotal != null)
819
+ lines.push(`| **Total** | **${formatMs(tTotal)}** |`);
820
+ }
821
+ appendAssertionsMd(lines, grpc);
822
+ if (grpc.requestError) {
823
+ lines.push('');
824
+ lines.push('## Error');
825
+ lines.push(`- ${grpc.requestError}`);
826
+ }
827
+ return lines;
828
+ }
829
+ // --- SSL check result ---
830
+ // SSL security-baseline rules, in display order, mapped to humanized labels.
831
+ // Each rule in `response.securityBaseline` is `{ violated, severity }`: a
832
+ // non-violated rule renders as a pass, a violated one as a fail, with the
833
+ // configured enforcement severity (fail / warn / ignore) shown alongside.
834
+ const SSL_BASELINE_RULES = [
835
+ ['minTLSVersion', 'min TLS version'],
836
+ ['minKeySizeBits', 'min key size'],
837
+ ['weakSignatureAlgorithm', 'weak signature algorithm'],
838
+ ['weakCipherSuite', 'weak cipher suite'],
839
+ ['knownBadCA', 'known bad CA'],
840
+ ['recommendedTLSVersion', 'recommended TLS version'],
841
+ ['recommendedKeySizeBits', 'recommended key size'],
842
+ ['ocspMustStapleRespected', 'OCSP must-staple respected'],
843
+ ['sctPresent', 'SCT present'],
844
+ ];
845
+ // Join SANs with a cap so a wildcard cert with dozens of names stays readable.
846
+ function formatSans(sans) {
847
+ if (!Array.isArray(sans))
848
+ return undefined;
849
+ const list = sans.filter((s) => typeof s === 'string' && s.length > 0);
850
+ if (list.length === 0)
851
+ return undefined;
852
+ const cap = 10;
853
+ if (list.length <= cap)
854
+ return list.join(', ');
855
+ return `${list.slice(0, cap).join(', ')}, +${list.length - cap} more`;
856
+ }
857
+ function asObject(value) {
858
+ return value && typeof value === 'object' ? value : undefined;
859
+ }
860
+ function appendSslCertificateTerminal(lines, resp) {
861
+ const cert = asObject(resp.certificate);
862
+ const ocspStapled = boolFlag(resp.ocspStapled);
863
+ const body = [];
864
+ if (cert) {
865
+ const subjectCN = str(cert.subjectCN);
866
+ if (subjectCN)
867
+ body.push(`${label('Subject CN:')}${subjectCN}`);
868
+ const issuerCN = str(cert.issuerCN);
869
+ if (issuerCN)
870
+ body.push(`${label('Issuer CN:')}${issuerCN}`);
871
+ const notBefore = str(cert.notBefore);
872
+ const notAfter = str(cert.notAfter);
873
+ if (notBefore || notAfter)
874
+ body.push(`${label('Valid:')}${[notBefore, notAfter].filter(Boolean).join(' → ')}`);
875
+ const keyAlgorithm = str(cert.keyAlgorithm);
876
+ const keySizeBits = num(cert.keySizeBits);
877
+ if (keyAlgorithm || keySizeBits != null) {
878
+ body.push(`${label('Key:')}${[keyAlgorithm, keySizeBits != null ? `${keySizeBits}-bit` : null].filter(Boolean).join(' ')}`);
879
+ }
880
+ const signatureAlgorithm = str(cert.signatureAlgorithm);
881
+ if (signatureAlgorithm)
882
+ body.push(`${label('Signature:')}${signatureAlgorithm}`);
883
+ const fingerprint = str(cert.fingerprintSha256);
884
+ if (fingerprint)
885
+ body.push(`${label('SHA-256:')}${fingerprint}`);
886
+ const sans = formatSans(cert.sans);
887
+ if (sans)
888
+ body.push(`${label('SANs:')}${sans}`);
889
+ const serial = str(cert.serialNumber);
890
+ if (serial)
891
+ body.push(`${label('Serial:')}${serial}`);
892
+ if (boolFlag(cert.selfSigned) === true)
893
+ body.push(`${label('Self-signed:')}${chalk.yellow('yes')}`);
894
+ if (boolFlag(cert.isCA) === true)
895
+ body.push(`${label('CA:')}yes`);
896
+ }
897
+ if (ocspStapled != null)
898
+ body.push(`${label('OCSP stapled:')}${yesNo(ocspStapled)}`);
899
+ if (body.length === 0)
900
+ return;
901
+ lines.push('');
902
+ lines.push(heading('CERTIFICATE', 2, 'terminal'));
903
+ lines.push(...body);
904
+ }
905
+ function appendSslCertificateMd(lines, resp) {
906
+ const cert = asObject(resp.certificate);
907
+ const ocspStapled = boolFlag(resp.ocspStapled);
908
+ const body = [];
909
+ if (cert) {
910
+ const subjectCN = str(cert.subjectCN);
911
+ if (subjectCN)
912
+ body.push(`- **Subject CN:** ${subjectCN}`);
913
+ const issuerCN = str(cert.issuerCN);
914
+ if (issuerCN)
915
+ body.push(`- **Issuer CN:** ${issuerCN}`);
916
+ const notBefore = str(cert.notBefore);
917
+ const notAfter = str(cert.notAfter);
918
+ if (notBefore || notAfter)
919
+ body.push(`- **Valid:** ${[notBefore, notAfter].filter(Boolean).join(' → ')}`);
920
+ const keyAlgorithm = str(cert.keyAlgorithm);
921
+ const keySizeBits = num(cert.keySizeBits);
922
+ if (keyAlgorithm || keySizeBits != null) {
923
+ body.push(`- **Key:** ${[keyAlgorithm, keySizeBits != null ? `${keySizeBits}-bit` : null].filter(Boolean).join(' ')}`);
924
+ }
925
+ const signatureAlgorithm = str(cert.signatureAlgorithm);
926
+ if (signatureAlgorithm)
927
+ body.push(`- **Signature:** ${signatureAlgorithm}`);
928
+ const fingerprint = str(cert.fingerprintSha256);
929
+ if (fingerprint)
930
+ body.push(`- **SHA-256:** \`${fingerprint}\``);
931
+ const sans = formatSans(cert.sans);
932
+ if (sans)
933
+ body.push(`- **SANs:** ${sans}`);
934
+ const serial = str(cert.serialNumber);
935
+ if (serial)
936
+ body.push(`- **Serial:** ${serial}`);
937
+ if (boolFlag(cert.selfSigned) === true)
938
+ body.push('- **Self-signed:** yes');
939
+ if (boolFlag(cert.isCA) === true)
940
+ body.push('- **CA:** yes');
941
+ }
942
+ if (ocspStapled != null)
943
+ body.push(`- **OCSP stapled:** ${ocspStapled ? 'yes' : 'no'}`);
944
+ if (body.length === 0)
945
+ return;
946
+ lines.push('');
947
+ lines.push('## Certificate');
948
+ lines.push(...body);
949
+ }
950
+ // Build the per-rule baseline breakdown as [symbol, humanLabel, severity, violated]
951
+ // tuples so terminal and markdown can render the same rules with format-specific
952
+ // styling. A rule that carries neither a `violated` flag nor a scalar value is
953
+ // skipped.
954
+ function sslBaselineRules(resp) {
955
+ const baseline = asObject(resp.securityBaseline);
956
+ if (!baseline)
957
+ return [];
958
+ const rules = [];
959
+ for (const [key, human] of SSL_BASELINE_RULES) {
960
+ const rule = baseline[key];
961
+ if (rule == null)
962
+ continue;
963
+ const obj = asObject(rule);
964
+ if (obj) {
965
+ const violated = boolFlag(obj.violated);
966
+ const severity = str(obj.severity);
967
+ if (violated == null && severity == null)
968
+ continue;
969
+ rules.push({ human, violated, severity });
970
+ }
971
+ else if (typeof rule === 'string' || typeof rule === 'number' || typeof rule === 'boolean') {
972
+ rules.push({ human, scalar: String(rule) });
973
+ }
974
+ }
975
+ return rules;
976
+ }
977
+ function appendSslBaselineTerminal(lines, resp) {
978
+ const rules = sslBaselineRules(resp);
979
+ if (rules.length === 0)
980
+ return;
981
+ lines.push('');
982
+ lines.push(heading('SECURITY BASELINE', 2, 'terminal'));
983
+ for (const rule of rules) {
984
+ if (rule.violated == null && rule.scalar == null) {
985
+ lines.push(` ${chalk.dim('·')} ${rule.human}${rule.severity ? chalk.dim(` (${rule.severity})`) : ''}`);
986
+ continue;
987
+ }
988
+ if (rule.scalar != null) {
989
+ lines.push(` ${chalk.dim('·')} ${rule.human}: ${rule.scalar}`);
990
+ continue;
991
+ }
992
+ const head = `${rule.violated ? assertionSymbols.error : assertionSymbols.success} ${rule.human}`;
993
+ const coloredHead = rule.violated ? chalk.red(head) : chalk.green(head);
994
+ lines.push(` ${coloredHead}${rule.severity ? chalk.dim(` (${rule.severity})`) : ''}`);
995
+ }
996
+ }
997
+ function appendSslBaselineMd(lines, resp) {
998
+ const rules = sslBaselineRules(resp);
999
+ if (rules.length === 0)
1000
+ return;
1001
+ lines.push('');
1002
+ lines.push('## Security Baseline');
1003
+ for (const rule of rules) {
1004
+ if (rule.scalar != null) {
1005
+ lines.push(`- ${rule.human}: ${rule.scalar}`);
1006
+ continue;
1007
+ }
1008
+ if (rule.violated == null) {
1009
+ lines.push(`- ${rule.human}${rule.severity ? ` (${rule.severity})` : ''}`);
1010
+ continue;
1011
+ }
1012
+ const sym = rule.violated ? assertionSymbols.error : assertionSymbols.success;
1013
+ lines.push(`- ${sym} ${rule.human}${rule.severity ? ` (${rule.severity})` : ''}`);
1014
+ }
1015
+ }
1016
+ function formatSslResultTerminal(ssl) {
1017
+ const lines = [];
1018
+ const resp = ssl.response ?? {};
1019
+ lines.push(heading('SSL RESULT', 2, 'terminal'));
1020
+ const ip = str(resp.resolvedIp);
1021
+ if (ip)
1022
+ lines.push(`${label('Resolved IP:')}${ip}`);
1023
+ const tls = str(ssl.tlsVersion, resp.protocol);
1024
+ const cipher = str(ssl.cipherSuite, resp.cipherSuite);
1025
+ if (tls || cipher)
1026
+ lines.push(`${label('TLS:')}${[tls, cipher].filter(Boolean).join(' / ')}`);
1027
+ const days = num(ssl.daysUntilExpiry, resp.daysUntilExpiry);
1028
+ if (days != null) {
1029
+ const text = days < 0 ? `expired ${-days} day(s) ago` : `${days} day(s)`;
1030
+ lines.push(`${label('Expires in:')}${days <= 0 ? chalk.red(text) : days < 14 ? chalk.yellow(text) : chalk.green(text)}`);
1031
+ }
1032
+ const handshake = num(ssl.handshakeTimeMs, resp.handshakeTimeMs);
1033
+ if (handshake != null)
1034
+ lines.push(`${label('Handshake:')}${formatMs(handshake)}`);
1035
+ const chainTrusted = boolFlag(ssl.chainTrusted, resp.chainTrusted);
1036
+ if (chainTrusted != null)
1037
+ lines.push(`${label('Chain trusted:')}${yesNo(chainTrusted)}`);
1038
+ const hostnameVerified = boolFlag(ssl.hostnameVerified, resp.hostnameVerified);
1039
+ if (hostnameVerified != null)
1040
+ lines.push(`${label('Hostname:')}${yesNo(hostnameVerified)}`);
1041
+ const verdict = str(ssl.baselineVerdict);
1042
+ const grade = str(ssl.baselineGrade);
1043
+ if (verdict || grade) {
1044
+ const verdictStr = verdict ? (verdict === 'PASS' ? chalk.green(verdict) : chalk.red(verdict)) : '';
1045
+ lines.push(`${label('Baseline:')}${[verdictStr, grade ? `grade ${grade}` : ''].filter(Boolean).join(' ')}`);
1046
+ }
1047
+ const failure = str(ssl.failureCategory);
1048
+ if (failure)
1049
+ lines.push(`${label('Failure:')}${chalk.red(failure)}`);
1050
+ appendSslCertificateTerminal(lines, resp);
1051
+ appendSslBaselineTerminal(lines, resp);
1052
+ appendAssertionsTerminal(lines, ssl);
1053
+ if (ssl.requestError) {
1054
+ lines.push('');
1055
+ lines.push(heading('ERROR', 2, 'terminal'));
1056
+ lines.push(chalk.red(` ${ssl.requestError}`));
1057
+ }
1058
+ return lines;
1059
+ }
1060
+ function formatSslResultMd(ssl) {
1061
+ const lines = ['## SSL Result'];
1062
+ const resp = ssl.response ?? {};
1063
+ const tls = str(ssl.tlsVersion, resp.protocol);
1064
+ const cipher = str(ssl.cipherSuite, resp.cipherSuite);
1065
+ if (tls || cipher)
1066
+ lines.push(`- **TLS:** ${[tls, cipher].filter(Boolean).join(' / ')}`);
1067
+ const days = num(ssl.daysUntilExpiry, resp.daysUntilExpiry);
1068
+ if (days != null)
1069
+ lines.push(`- **Expires in:** ${days < 0 ? `expired ${-days} day(s) ago` : `${days} day(s)`}`);
1070
+ const handshake = num(ssl.handshakeTimeMs, resp.handshakeTimeMs);
1071
+ if (handshake != null)
1072
+ lines.push(`- **Handshake:** ${formatMs(handshake)}`);
1073
+ const chainTrusted = boolFlag(ssl.chainTrusted, resp.chainTrusted);
1074
+ if (chainTrusted != null)
1075
+ lines.push(`- **Chain trusted:** ${chainTrusted ? 'yes' : 'no'}`);
1076
+ const hostnameVerified = boolFlag(ssl.hostnameVerified, resp.hostnameVerified);
1077
+ if (hostnameVerified != null)
1078
+ lines.push(`- **Hostname verified:** ${hostnameVerified ? 'yes' : 'no'}`);
1079
+ const verdict = str(ssl.baselineVerdict);
1080
+ const grade = str(ssl.baselineGrade);
1081
+ if (verdict || grade)
1082
+ lines.push(`- **Baseline:** ${[verdict, grade ? `grade ${grade}` : ''].filter(Boolean).join(' ')}`);
1083
+ const failure = str(ssl.failureCategory);
1084
+ if (failure)
1085
+ lines.push(`- **Failure:** ${failure}`);
1086
+ appendSslCertificateMd(lines, resp);
1087
+ appendSslBaselineMd(lines, resp);
1088
+ appendAssertionsMd(lines, ssl);
1089
+ if (ssl.requestError) {
1090
+ lines.push('');
1091
+ lines.push('## Error');
1092
+ lines.push(`- ${ssl.requestError}`);
1093
+ }
1094
+ return lines;
1095
+ }
493
1096
  // --- Shared internal helpers ---
494
1097
  function colorStatus(code) {
495
1098
  if (code >= 200 && code < 300)