All files / scripts/fetch-calendar/mcp errors.ts

100% Statements 9/9
100% Branches 6/6
100% Functions 3/3
100% Lines 9/9

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68                                41x             32x                                                 38x 38x 38x                     25x 25x 25x 25x      
/**
 * @module scripts/fetch-calendar/mcp/errors
 * @description Typed transport-error class and HTML-error detector for the
 * MCP calendar transport.
 *
 * Per `Threat_Modeling.md` (trust-boundary rule for external HTML), an HTML
 * response from a JSON-RPC endpoint is treated as a hostile / error response
 * — never parsed as JSON — and the orchestrator falls back to the web
 * scraper instead.
 *
 * @author Hack23 AB
 * @license Apache-2.0
 */
 
// HTML detection: common HTML document / fragment leading tags.
export const HTML_PREFIX_RE =
  /^\s*(?:<!doctype(?=[\s>])|<html(?=[\s>/])|<head(?=[\s>/])|<body(?=[\s>/])|<title(?=[\s>/])|<meta(?=[\s>/]))/i;
 
/**
 * Returns true when `text` looks like an HTML document rather than JSON.
 * Used to detect when the MCP endpoint returns an error page instead of JSON.
 */
export function isHtmlErrorResponse(text: string): boolean {
  return HTML_PREFIX_RE.test(text);
}
 
/**
 * Detect the riksdag-regering **degraded-kalender sentinel** payload.
 *
 * When the upstream `data.riksdagen.se/kalender/` endpoint serves an HTML
 * error page instead of JSON, the MCP server does not surface a JSON-RPC
 * error — it returns a *successful* tool result whose inner content is a
 * sentinel envelope such as:
 *
 * ```json
 * { "count": 0, "events": [], "rawHtml": "<script…",
 *   "error": "Riksdagens kalender-API returnerade HTML istället för JSON.",
 *   "notice": "API:et fungerar inte korrekt för närvarande.",
 *   "suggestions": [ … ] }
 * ```
 *
 * The empty `events: []` array would otherwise be read as a legitimate
 * zero-event window, masking the outage and suppressing the web-scraper
 * fallback. Treat the presence of a non-empty `error` string or a `rawHtml`
 * field as a degraded signal so the orchestrator falls straight back to the
 * public-page scraper.
 */
export function isDegradedKalenderSentinel(inner: Record<string, unknown>): boolean {
  const hasErrorString = typeof inner['error'] === 'string' && inner['error'].trim().length > 0;
  const hasRawHtml = typeof inner['rawHtml'] === 'string' && inner['rawHtml'].trim().length > 0;
  return hasErrorString || hasRawHtml;
}
 
/** Typed error for MCP transport / protocol failures. */
export class CalendarMcpError extends Error {
  /** Error category. */
  readonly kind: 'html' | 'http' | 'network' | 'json' | 'tool';
  /** Raw response body (only present for `html` / `http` kinds). */
  readonly responseText?: string;
 
  constructor(message: string, kind: CalendarMcpError['kind'], responseText?: string) {
    super(message);
    this.name = 'CalendarMcpError';
    this.kind = kind;
    this.responseText = responseText;
  }
}