@subhamxcod/apiguard 1.0.4 → 1.1.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.
@@ -16,7 +16,7 @@ var ignoreSchema = z.strictObject({
16
16
  "show-ignored": z.boolean().optional().default(false)
17
17
  });
18
18
  var outputSchema = z.strictObject({
19
- format: z.enum(["terminal", "json", "markdown"]).optional().default("terminal"),
19
+ format: z.enum(["terminal", "json", "markdown", "html", "sarif"]).optional().default("terminal"),
20
20
  "include-non-breaking": z.boolean().optional().default(false),
21
21
  color: z.enum(["auto", "always", "never"]).optional().default("auto")
22
22
  });
@@ -24,13 +24,17 @@ var versioningSchema = z.strictObject({
24
24
  suggest: z.boolean().optional().default(true)
25
25
  });
26
26
  var loaderSchema = z.strictObject({
27
- "allow-remote-refs": z.boolean().optional().default(false)
27
+ "allow-remote-refs": z.boolean().optional().default(false),
28
+ "allow-external-files": z.boolean().optional().default(false),
29
+ "allow-private-ips": z.boolean().optional().default(false),
30
+ headers: z.record(z.string(), z.string()).optional().default({})
28
31
  });
29
32
  var githubSchema = z.strictObject({
30
33
  comment: z.boolean().optional().default(true),
31
34
  "check-run": z.boolean().optional().default(true),
32
35
  "update-existing-comment": z.boolean().optional().default(true)
33
36
  });
37
+ var profileSchema = z.enum(["default", "strict-sdk", "loose-web"]).default("default");
34
38
  var configSchema = z.strictObject({
35
39
  schemaVersion: z.literal(1, {
36
40
  error: "schemaVersion must be 1 (other versions are rejected; see docs/configuration.md)"
@@ -40,6 +44,7 @@ var configSchema = z.strictObject({
40
44
  output: outputSchema.optional(),
41
45
  "fail-on": z.enum(["error", "warning", "never"]).optional(),
42
46
  versioning: versioningSchema.optional(),
47
+ profile: profileSchema.optional(),
43
48
  loader: loaderSchema.optional(),
44
49
  github: githubSchema.optional()
45
50
  });
@@ -63,7 +68,13 @@ var DEFAULT_CONFIG = {
63
68
  failOn: "error",
64
69
  // §13 golden output shows the bump line "only with --suggest-version"; config may opt in.
65
70
  versioning: { suggest: false },
66
- loader: { allowRemoteRefs: false },
71
+ profile: "default",
72
+ loader: {
73
+ allowRemoteRefs: false,
74
+ allowExternalFiles: false,
75
+ allowPrivateIps: false,
76
+ headers: {}
77
+ },
67
78
  github: { comment: true, checkRun: true, updateExistingComment: true }
68
79
  };
69
80
 
@@ -156,7 +167,13 @@ function toResolved(parsed) {
156
167
  },
157
168
  failOn: parsed["fail-on"] ?? "error",
158
169
  versioning: { suggest: parsed.versioning?.suggest ?? true },
159
- loader: { allowRemoteRefs: parsed.loader?.["allow-remote-refs"] ?? false },
170
+ profile: parsed.profile ?? "default",
171
+ loader: {
172
+ allowRemoteRefs: parsed.loader?.["allow-remote-refs"] ?? false,
173
+ allowExternalFiles: parsed.loader?.["allow-external-files"] ?? false,
174
+ allowPrivateIps: parsed.loader?.["allow-private-ips"] ?? false,
175
+ headers: parsed.loader?.headers ?? {}
176
+ },
160
177
  github: {
161
178
  comment: parsed.github?.comment ?? true,
162
179
  checkRun: parsed.github?.["check-run"] ?? true,
@@ -236,13 +253,16 @@ function mergeWithFlags(base, flags) {
236
253
  failOn: flags.failOn ?? base.failOn,
237
254
  versioning: {
238
255
  suggest: flags.suggestVersion ?? base.versioning.suggest
239
- }
256
+ },
257
+ profile: flags.profile ?? base.profile
240
258
  };
241
259
  }
242
260
 
243
261
  // src/loaders/spec-loader.ts
244
262
  import fs2 from "fs";
245
263
  import path2 from "path";
264
+ import os from "os";
265
+ import { execFileSync } from "child_process";
246
266
  import SwaggerParser from "@apidevtools/swagger-parser";
247
267
  import { parse as parseYaml2 } from "yaml";
248
268
 
@@ -341,6 +361,87 @@ function toNormalizedSpec(loaded, normalized) {
341
361
  }
342
362
 
343
363
  // src/loaders/spec-loader.ts
364
+ function isPrivateHost(hostname) {
365
+ const h = hostname.toLowerCase().trim();
366
+ if (h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0" || h === "::") {
367
+ return true;
368
+ }
369
+ const ipv4Match = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
370
+ if (ipv4Match) {
371
+ const [b0, b1] = [Number(ipv4Match[1]), Number(ipv4Match[2])];
372
+ if (b0 === 127) return true;
373
+ if (b0 === 10) return true;
374
+ if (b0 === 172 && b1 >= 16 && b1 <= 31) return true;
375
+ if (b0 === 192 && b1 === 168) return true;
376
+ if (b0 === 169 && b1 === 254) return true;
377
+ if (b0 === 0) return true;
378
+ }
379
+ if (h.startsWith("fc") || h.startsWith("fd") || h.startsWith("fe80:")) {
380
+ return true;
381
+ }
382
+ return false;
383
+ }
384
+ async function fetchRemoteSpec(urlStr, options = {}) {
385
+ let url;
386
+ try {
387
+ url = new URL(urlStr);
388
+ } catch (e) {
389
+ throw new SpecLoadError(`${urlStr}: invalid URL: ${errorMessage(e)}`);
390
+ }
391
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
392
+ throw new SpecLoadError(
393
+ `${urlStr}: unsupported protocol "${url.protocol}" (expected http: or https:)`
394
+ );
395
+ }
396
+ if (!options.allowPrivateIps && isPrivateHost(url.hostname)) {
397
+ throw new SpecLoadError(
398
+ `${urlStr}: remote spec blocked: private or loopback host "${url.hostname}" denied by SSRF protection. Set loader.allow-private-ips: true in apiguard.yaml or CLI to opt in.`
399
+ );
400
+ }
401
+ let text;
402
+ try {
403
+ const res = await fetch(urlStr, {
404
+ headers: options.headers ?? {},
405
+ signal: AbortSignal.timeout(15e3)
406
+ });
407
+ if (!res.ok) {
408
+ throw new SpecLoadError(
409
+ `${urlStr}: failed to fetch spec (HTTP ${res.status} ${res.statusText})`
410
+ );
411
+ }
412
+ text = await res.text();
413
+ } catch (e) {
414
+ if (e instanceof SpecLoadError) throw e;
415
+ throw new SpecLoadError(`${urlStr}: network error fetching spec: ${errorMessage(e)}`);
416
+ }
417
+ const tmpDir = fs2.mkdtempSync(path2.join(os.tmpdir(), "apiguard-remote-"));
418
+ const ext = url.pathname.toLowerCase().endsWith(".json") ? ".json" : ".yaml";
419
+ const localPath = path2.join(tmpDir, "spec" + ext);
420
+ fs2.writeFileSync(localPath, text, "utf8");
421
+ return { localPath, text };
422
+ }
423
+ function fetchGitSpec(gitUri, cwd = process.cwd()) {
424
+ const stripped = gitUri.replace(/^git:\/\//i, "").replace(/^git:/i, "");
425
+ const colonIdx = stripped.indexOf(":");
426
+ if (colonIdx === -1) {
427
+ throw new SpecLoadError(`${gitUri}: invalid git spec URI (expected git://<rev>:<path>)`);
428
+ }
429
+ const rev = stripped.slice(0, colonIdx);
430
+ const relPath = stripped.slice(colonIdx + 1);
431
+ let text;
432
+ try {
433
+ text = execFileSync("git", ["show", `${rev}:${relPath}`], { cwd, encoding: "utf8" });
434
+ } catch (e) {
435
+ throw new SpecLoadError(
436
+ `${gitUri}: failed to retrieve spec from git (${rev}:${relPath}): ${errorMessage(e)}`
437
+ );
438
+ }
439
+ const tmpDir = fs2.mkdtempSync(path2.join(os.tmpdir(), "apiguard-git-"));
440
+ const ext = relPath.toLowerCase().endsWith(".json") ? ".json" : ".yaml";
441
+ const localPath = path2.join(tmpDir, path2.basename(relPath) || "spec" + ext);
442
+ fs2.writeFileSync(localPath, text, "utf8");
443
+ return { localPath, text };
444
+ }
344
445
  function parseSpecText(text, source) {
345
446
  let parsed;
346
447
  const isJsonByExt = source.trimEnd().toLowerCase().endsWith(".json");
@@ -418,12 +519,23 @@ function findExternalFileRefs(document, specDir) {
418
519
  return found;
419
520
  }
420
521
  async function loadSpec(filePath, options = {}) {
421
- const abs = path2.resolve(filePath);
522
+ let abs;
422
523
  let text;
423
- try {
424
- text = fs2.readFileSync(abs, "utf8");
425
- } catch (e) {
426
- throw new SpecLoadError(`${filePath}: cannot read file: ${errorMessage(e)}`);
524
+ if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
525
+ const fetched = await fetchRemoteSpec(filePath, options.loader);
526
+ abs = fetched.localPath;
527
+ text = fetched.text;
528
+ } else if (filePath.startsWith("git://") || filePath.startsWith("git:")) {
529
+ const fetched = fetchGitSpec(filePath, options.cwd);
530
+ abs = fetched.localPath;
531
+ text = fetched.text;
532
+ } else {
533
+ abs = path2.resolve(filePath);
534
+ try {
535
+ text = fs2.readFileSync(abs, "utf8");
536
+ } catch (e) {
537
+ throw new SpecLoadError(`${filePath}: cannot read file: ${errorMessage(e)}`);
538
+ }
427
539
  }
428
540
  if (text.trim() === "") {
429
541
  throw new SpecLoadError(`${filePath}: file is empty \u2014 a spec must contain an OpenAPI document`);
@@ -468,7 +580,8 @@ async function loadSpec(filePath, options = {}) {
468
580
  source: filePath,
469
581
  openapiVersion: openapi,
470
582
  title,
471
- sha256: ""
583
+ sha256: "",
584
+ rawText: text
472
585
  };
473
586
  return toNormalizedSpec(loaded, normalizeDocument(document));
474
587
  }
@@ -1509,6 +1622,9 @@ function getRule(id) {
1509
1622
  if (!rule) throw new Error(`internal error: unknown rule id "${id}"`);
1510
1623
  return rule;
1511
1624
  }
1625
+ function getAllRules() {
1626
+ return [...RULES];
1627
+ }
1512
1628
  function constraintText(c) {
1513
1629
  const name = nameOf(c);
1514
1630
  const oldV = c.oldValue === void 0 ? void 0 : JSON.stringify(c.oldValue);
@@ -1577,7 +1693,21 @@ function classify(ruleId, context, options, doctrine) {
1577
1693
  "request-body-removed",
1578
1694
  "removed-parameter"
1579
1695
  ]);
1580
- const severity = override ?? (DOCTRINE_RULES.has(ruleId) ? resolveContextSeverity(ruleId, context, doctrine ?? {}) : rule.defaultSeverity);
1696
+ let baseSeverity = DOCTRINE_RULES.has(ruleId) ? resolveContextSeverity(ruleId, context, doctrine ?? {}) : rule.defaultSeverity;
1697
+ if (override === void 0 && options.profile) {
1698
+ if (options.profile === "strict-sdk") {
1699
+ if (ruleId === "enum-value-added") {
1700
+ baseSeverity = "error";
1701
+ } else if (ruleId === "anyOf-oneOf-member-added") {
1702
+ baseSeverity = "warning";
1703
+ }
1704
+ } else if (options.profile === "loose-web") {
1705
+ if (ruleId === "enum-value-added") {
1706
+ baseSeverity = "info";
1707
+ }
1708
+ }
1709
+ }
1710
+ const severity = override ?? baseSeverity;
1581
1711
  return { severity, kind: rule.defaultKind };
1582
1712
  }
1583
1713
 
@@ -2303,7 +2433,7 @@ function emit(ctx, opts) {
2303
2433
  const resolved = classify(
2304
2434
  opts.ruleId,
2305
2435
  opts.location.context,
2306
- { rules: ctx.config.rules },
2436
+ { rules: ctx.config.rules, profile: ctx.config.profile },
2307
2437
  opts.doctrine
2308
2438
  );
2309
2439
  const rule = getRule(opts.ruleId);
@@ -3721,6 +3851,71 @@ function satisfiesAny(oldReq, newReqs) {
3721
3851
  });
3722
3852
  }
3723
3853
 
3854
+ // src/utils/source-map.ts
3855
+ import { parseDocument, LineCounter } from "yaml";
3856
+ var SpecSourceMap = class {
3857
+ doc;
3858
+ lineCounter;
3859
+ constructor(rawText) {
3860
+ this.lineCounter = new LineCounter();
3861
+ this.doc = parseDocument(rawText, { lineCounter: this.lineCounter });
3862
+ }
3863
+ /** Resolve 1-based line & column for a JSON pointer. */
3864
+ findPosition(pointer) {
3865
+ if (!pointer || pointer === "/") {
3866
+ return { line: 1, column: 1 };
3867
+ }
3868
+ const tokens = pointer.split("/").filter(Boolean).map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"));
3869
+ for (let i = tokens.length; i >= 1; i--) {
3870
+ const subTokens = tokens.slice(0, i);
3871
+ try {
3872
+ const node = this.doc.getIn(subTokens, true);
3873
+ if (node && Array.isArray(node.range) && typeof node.range[0] === "number") {
3874
+ const pos = this.lineCounter.linePos(node.range[0]);
3875
+ return { line: pos.line, column: pos.col };
3876
+ }
3877
+ } catch {
3878
+ }
3879
+ }
3880
+ return void 0;
3881
+ }
3882
+ };
3883
+ function attachSourceLocations(changes, baselineSpec, currentSpec) {
3884
+ let currentMap;
3885
+ let baselineMap;
3886
+ if (currentSpec.rawText) {
3887
+ try {
3888
+ currentMap = new SpecSourceMap(currentSpec.rawText);
3889
+ } catch {
3890
+ }
3891
+ }
3892
+ if (baselineSpec.rawText) {
3893
+ try {
3894
+ baselineMap = new SpecSourceMap(baselineSpec.rawText);
3895
+ } catch {
3896
+ }
3897
+ }
3898
+ for (const c of changes) {
3899
+ if (currentMap && c.location.pointerNew) {
3900
+ const pos = currentMap.findPosition(c.location.pointerNew);
3901
+ if (pos) {
3902
+ c.location.file = currentSpec.source;
3903
+ c.location.line = pos.line;
3904
+ c.location.column = pos.column;
3905
+ continue;
3906
+ }
3907
+ }
3908
+ if (baselineMap && c.location.pointerOld) {
3909
+ const pos = baselineMap.findPosition(c.location.pointerOld);
3910
+ if (pos) {
3911
+ c.location.file = baselineSpec.source;
3912
+ c.location.line = pos.line;
3913
+ c.location.column = pos.column;
3914
+ }
3915
+ }
3916
+ }
3917
+ }
3918
+
3724
3919
  // src/core/ignore/ignore.ts
3725
3920
  import micromatch from "micromatch";
3726
3921
  function changePath(change) {
@@ -3797,8 +3992,8 @@ async function analyze(baselinePath, currentPath, options = {}) {
3797
3992
  const loaded = options.config ? { config: options.config, sourcePath: "", warnings: [] } : discoverAndLoadConfig({ explicit: options.configPath });
3798
3993
  const config = loaded.config;
3799
3994
  const [baseline, current] = await Promise.all([
3800
- loadSpec(baselinePath, { loader: { allowRemoteRefs: config.loader.allowRemoteRefs } }),
3801
- loadSpec(currentPath, { loader: { allowRemoteRefs: config.loader.allowRemoteRefs } })
3995
+ loadSpec(baselinePath, { loader: config.loader }),
3996
+ loadSpec(currentPath, { loader: config.loader })
3802
3997
  ]);
3803
3998
  structuralDeref(baseline.normalized);
3804
3999
  structuralDeref(current.normalized);
@@ -3812,6 +4007,7 @@ async function analyze(baselinePath, currentPath, options = {}) {
3812
4007
  };
3813
4008
  runStructuralDiff(ctx);
3814
4009
  const deduped = dedupeChanges(sortChanges([...ctx.candidates]));
4010
+ attachSourceLocations(deduped, baseline, current);
3815
4011
  const { kept, ignored } = applyIgnores(deduped, config);
3816
4012
  const summary = summarize(kept, ignored.length);
3817
4013
  const semver = suggestVersion(kept);
@@ -3830,6 +4026,802 @@ async function analyze(baselinePath, currentPath, options = {}) {
3830
4026
  return { report, ignored };
3831
4027
  }
3832
4028
 
4029
+ // src/reporters/html.ts
4030
+ function esc(s) {
4031
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
4032
+ }
4033
+ function endpointOf(change) {
4034
+ const loc = change.location;
4035
+ if (loc.method && loc.path) return `${loc.method} ${loc.path}`;
4036
+ if (loc.path && loc.path !== "/") return loc.path;
4037
+ if (loc.context === "webhook") return `webhook ${loc.componentName ?? ""}`.trim();
4038
+ if (loc.componentName) return `component ${loc.componentName}`;
4039
+ return "API Root";
4040
+ }
4041
+ function valueRepr(v) {
4042
+ if (v === void 0) return "absent";
4043
+ if (v === null) return "null";
4044
+ if (typeof v === "string") return v;
4045
+ return JSON.stringify(v);
4046
+ }
4047
+ function renderHtml(report) {
4048
+ const breaking = report.summary.bySeverity.error;
4049
+ const warnings = report.summary.bySeverity.warning;
4050
+ const infos = report.summary.bySeverity.info;
4051
+ const total = report.summary.total;
4052
+ const isPass = breaking === 0;
4053
+ return `<!DOCTYPE html>
4054
+ <html lang="en">
4055
+ <head>
4056
+ <meta charset="utf-8">
4057
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4058
+ <title>API Guard Report - ${isPass ? "Passed" : "Breaking Changes Found"}</title>
4059
+ <style>
4060
+ /* Hallmark stamp: modern-minimal developer tool contract dashboard */
4061
+ :root {
4062
+ --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
4063
+ --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
4064
+ --bg-page: #f8fafc;
4065
+ --bg-card: #ffffff;
4066
+ --bg-input: #ffffff;
4067
+ --border-subtle: #e2e8f0;
4068
+ --border-strong: #cbd5e1;
4069
+ --text-main: #0f172a;
4070
+ --text-muted: #64748b;
4071
+ --color-pass: #059669;
4072
+ --color-pass-bg: #ecfdf5;
4073
+ --color-pass-border: #a7f3d0;
4074
+ --color-fail: #dc2626;
4075
+ --color-fail-bg: #fef2f2;
4076
+ --color-fail-border: #fecaca;
4077
+ --color-warn: #d97706;
4078
+ --color-warn-bg: #fffbeb;
4079
+ --color-warn-border: #fde68a;
4080
+ --color-info: #0284c7;
4081
+ --color-info-bg: #f0f9ff;
4082
+ --color-info-border: #bae6fd;
4083
+ --color-accent: #2563eb;
4084
+ --color-accent-bg: #eff6ff;
4085
+ --focus-ring: #3b82f6;
4086
+ }
4087
+
4088
+ @media (prefers-color-scheme: dark) {
4089
+ :root {
4090
+ --bg-page: #090d16;
4091
+ --bg-card: #131b2e;
4092
+ --bg-input: #1a243b;
4093
+ --border-subtle: #1e293b;
4094
+ --border-strong: #334155;
4095
+ --text-main: #f1f5f9;
4096
+ --text-muted: #94a3b8;
4097
+ --color-pass: #10b981;
4098
+ --color-pass-bg: #064e3b33;
4099
+ --color-pass-border: #065f46;
4100
+ --color-fail: #f87171;
4101
+ --color-fail-bg: #7f1d1d33;
4102
+ --color-fail-border: #991b1b;
4103
+ --color-warn: #fbbf24;
4104
+ --color-warn-bg: #78350f33;
4105
+ --color-warn-border: #92400e;
4106
+ --color-info: #38bdf8;
4107
+ --color-info-bg: #0c4a6e33;
4108
+ --color-info-border: #075985;
4109
+ --color-accent: #60a5fa;
4110
+ --color-accent-bg: #1e3a8a33;
4111
+ --focus-ring: #60a5fa;
4112
+ }
4113
+ }
4114
+
4115
+ html, body {
4116
+ margin: 0;
4117
+ padding: 0;
4118
+ overflow-x: clip;
4119
+ background: var(--bg-page);
4120
+ color: var(--text-main);
4121
+ font-family: var(--font-body);
4122
+ line-height: 1.5;
4123
+ font-size: 15px;
4124
+ }
4125
+
4126
+ * {
4127
+ box-sizing: border-box;
4128
+ }
4129
+
4130
+ *:focus-visible {
4131
+ outline: 2px solid var(--focus-ring);
4132
+ outline-offset: 2px;
4133
+ }
4134
+
4135
+ .container {
4136
+ max-width: 1200px;
4137
+ margin: 0 auto;
4138
+ padding: 2.5rem 1.5rem 5rem 1.5rem;
4139
+ }
4140
+
4141
+ .header {
4142
+ background: var(--bg-card);
4143
+ border: 1px solid var(--border-subtle);
4144
+ border-radius: 8px;
4145
+ padding: 1.75rem 2rem;
4146
+ margin-bottom: 2rem;
4147
+ }
4148
+
4149
+ .header-top {
4150
+ display: flex;
4151
+ justify-content: space-between;
4152
+ align-items: center;
4153
+ flex-wrap: wrap;
4154
+ gap: 1rem;
4155
+ margin-bottom: 1.25rem;
4156
+ }
4157
+
4158
+ .title-row {
4159
+ display: flex;
4160
+ align-items: center;
4161
+ gap: 1rem;
4162
+ }
4163
+
4164
+ .brand-title {
4165
+ font-size: 1.35rem;
4166
+ font-weight: 700;
4167
+ letter-spacing: -0.02em;
4168
+ margin: 0;
4169
+ }
4170
+
4171
+ .status-badge {
4172
+ display: inline-flex;
4173
+ align-items: center;
4174
+ font-size: 0.825rem;
4175
+ font-weight: 600;
4176
+ padding: 0.25rem 0.75rem;
4177
+ border-radius: 9999px;
4178
+ border: 1px solid;
4179
+ }
4180
+
4181
+ .status-badge.pass {
4182
+ background: var(--color-pass-bg);
4183
+ color: var(--color-pass);
4184
+ border-color: var(--color-pass-border);
4185
+ }
4186
+
4187
+ .status-badge.fail {
4188
+ background: var(--color-fail-bg);
4189
+ color: var(--color-fail);
4190
+ border-color: var(--color-fail-border);
4191
+ }
4192
+
4193
+ .spec-metadata {
4194
+ display: grid;
4195
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
4196
+ gap: 1.25rem;
4197
+ padding-top: 1.25rem;
4198
+ border-top: 1px solid var(--border-subtle);
4199
+ font-size: 0.875rem;
4200
+ color: var(--text-muted);
4201
+ }
4202
+
4203
+ .meta-box strong {
4204
+ color: var(--text-main);
4205
+ display: block;
4206
+ margin-bottom: 0.2rem;
4207
+ }
4208
+
4209
+ .meta-code {
4210
+ font-family: var(--font-mono);
4211
+ font-size: 0.8rem;
4212
+ }
4213
+
4214
+ .stats-row {
4215
+ display: grid;
4216
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
4217
+ gap: 1rem;
4218
+ margin-bottom: 2rem;
4219
+ }
4220
+
4221
+ .stat-card {
4222
+ background: var(--bg-card);
4223
+ border: 1px solid var(--border-subtle);
4224
+ border-radius: 8px;
4225
+ padding: 1.25rem 1.5rem;
4226
+ }
4227
+
4228
+ .stat-label {
4229
+ font-size: 0.8rem;
4230
+ color: var(--text-muted);
4231
+ margin-bottom: 0.35rem;
4232
+ }
4233
+
4234
+ .stat-value {
4235
+ font-size: 1.75rem;
4236
+ font-weight: 700;
4237
+ line-height: 1;
4238
+ }
4239
+
4240
+ .stat-card.error .stat-value { color: var(--color-fail); }
4241
+ .stat-card.warning .stat-value { color: var(--color-warn); }
4242
+ .stat-card.info .stat-value { color: var(--color-info); }
4243
+ .stat-card.total .stat-value { color: var(--text-main); }
4244
+
4245
+ .semver-banner {
4246
+ background: var(--color-accent-bg);
4247
+ border: 1px solid var(--border-subtle);
4248
+ border-left: 4px solid var(--color-accent);
4249
+ border-radius: 6px;
4250
+ padding: 1rem 1.25rem;
4251
+ margin-bottom: 2rem;
4252
+ font-size: 0.9rem;
4253
+ }
4254
+
4255
+ .semver-banner strong {
4256
+ color: var(--text-main);
4257
+ }
4258
+
4259
+ .controls-bar {
4260
+ display: flex;
4261
+ flex-wrap: wrap;
4262
+ gap: 1rem;
4263
+ align-items: center;
4264
+ justify-content: space-between;
4265
+ margin-bottom: 1.5rem;
4266
+ }
4267
+
4268
+ .search-input {
4269
+ flex: 1 1 260px;
4270
+ padding: 0.6rem 0.9rem;
4271
+ font-size: 0.9rem;
4272
+ border: 1px solid var(--border-strong);
4273
+ border-radius: 6px;
4274
+ background: var(--bg-input);
4275
+ color: var(--text-main);
4276
+ }
4277
+
4278
+ .filter-group {
4279
+ display: flex;
4280
+ flex-wrap: wrap;
4281
+ gap: 0.5rem;
4282
+ }
4283
+
4284
+ .filter-btn {
4285
+ padding: 0.45rem 0.85rem;
4286
+ font-size: 0.825rem;
4287
+ font-weight: 500;
4288
+ border: 1px solid var(--border-strong);
4289
+ border-radius: 6px;
4290
+ background: var(--bg-card);
4291
+ color: var(--text-muted);
4292
+ cursor: pointer;
4293
+ }
4294
+
4295
+ .filter-btn.active {
4296
+ background: var(--color-accent);
4297
+ color: #ffffff;
4298
+ border-color: var(--color-accent);
4299
+ }
4300
+
4301
+ .changes-list {
4302
+ display: flex;
4303
+ flex-direction: column;
4304
+ gap: 1rem;
4305
+ }
4306
+
4307
+ .change-card {
4308
+ background: var(--bg-card);
4309
+ border: 1px solid var(--border-subtle);
4310
+ border-radius: 8px;
4311
+ padding: 1.25rem 1.5rem;
4312
+ }
4313
+
4314
+ .change-card.error { border-left: 4px solid var(--color-fail); }
4315
+ .change-card.warning { border-left: 4px solid var(--color-warn); }
4316
+ .change-card.info { border-left: 4px solid var(--color-info); }
4317
+
4318
+ .change-header {
4319
+ display: flex;
4320
+ justify-content: space-between;
4321
+ align-items: baseline;
4322
+ flex-wrap: wrap;
4323
+ gap: 0.5rem;
4324
+ margin-bottom: 0.75rem;
4325
+ }
4326
+
4327
+ .change-title-group {
4328
+ display: flex;
4329
+ align-items: center;
4330
+ flex-wrap: wrap;
4331
+ gap: 0.6rem;
4332
+ }
4333
+
4334
+ .severity-tag {
4335
+ font-size: 0.725rem;
4336
+ font-weight: 700;
4337
+ padding: 0.15rem 0.5rem;
4338
+ border-radius: 4px;
4339
+ text-transform: uppercase;
4340
+ letter-spacing: 0.04em;
4341
+ }
4342
+
4343
+ .severity-tag.error { background: var(--color-fail-bg); color: var(--color-fail); border: 1px solid var(--color-fail-border); }
4344
+ .severity-tag.warning { background: var(--color-warn-bg); color: var(--color-warn); border: 1px solid var(--color-warn-border); }
4345
+ .severity-tag.info { background: var(--color-info-bg); color: var(--color-info); border: 1px solid var(--color-info-border); }
4346
+
4347
+ .rule-name {
4348
+ font-family: var(--font-mono);
4349
+ font-size: 0.85rem;
4350
+ font-weight: 600;
4351
+ color: var(--text-main);
4352
+ }
4353
+
4354
+ .endpoint-pill {
4355
+ font-family: var(--font-mono);
4356
+ font-size: 0.825rem;
4357
+ color: var(--text-muted);
4358
+ background: var(--bg-page);
4359
+ border: 1px solid var(--border-subtle);
4360
+ padding: 0.15rem 0.5rem;
4361
+ border-radius: 4px;
4362
+ }
4363
+
4364
+ .location-info {
4365
+ font-family: var(--font-mono);
4366
+ font-size: 0.775rem;
4367
+ color: var(--text-muted);
4368
+ }
4369
+
4370
+ .change-message {
4371
+ font-size: 0.95rem;
4372
+ margin: 0 0 0.75rem 0;
4373
+ color: var(--text-main);
4374
+ word-break: break-word;
4375
+ }
4376
+
4377
+ .suggestion-box {
4378
+ font-size: 0.85rem;
4379
+ color: var(--text-muted);
4380
+ background: var(--bg-page);
4381
+ border-radius: 6px;
4382
+ padding: 0.6rem 0.9rem;
4383
+ border: 1px solid var(--border-subtle);
4384
+ }
4385
+
4386
+ .suggestion-box strong {
4387
+ color: var(--text-main);
4388
+ }
4389
+
4390
+ .diff-values {
4391
+ margin-top: 0.75rem;
4392
+ display: flex;
4393
+ flex-wrap: wrap;
4394
+ gap: 1rem;
4395
+ font-family: var(--font-mono);
4396
+ font-size: 0.8rem;
4397
+ }
4398
+
4399
+ .val-block {
4400
+ padding: 0.35rem 0.65rem;
4401
+ border-radius: 4px;
4402
+ background: var(--bg-page);
4403
+ border: 1px solid var(--border-subtle);
4404
+ }
4405
+
4406
+ .empty-state {
4407
+ text-align: center;
4408
+ padding: 4rem 2rem;
4409
+ background: var(--bg-card);
4410
+ border: 1px solid var(--border-subtle);
4411
+ border-radius: 8px;
4412
+ color: var(--text-muted);
4413
+ }
4414
+
4415
+ @media (prefers-reduced-motion: reduce) {
4416
+ * {
4417
+ animation-duration: 0.01ms !important;
4418
+ transition-duration: 0.01ms !important;
4419
+ }
4420
+ }
4421
+ </style>
4422
+ </head>
4423
+ <body>
4424
+ <div class="container">
4425
+ <header class="header">
4426
+ <div class="header-top">
4427
+ <div class="title-row">
4428
+ <h1 class="brand-title">API Guard Diff Report</h1>
4429
+ <span class="status-badge ${isPass ? "pass" : "fail"}">
4430
+ ${isPass ? "PASS: No Breaking Changes" : "FAIL: Breaking Changes Detected"}
4431
+ </span>
4432
+ </div>
4433
+ <div style="font-size: 0.85rem; color: var(--text-muted);">
4434
+ Generated by apiguard ${esc(report.tool.version)}
4435
+ </div>
4436
+ </div>
4437
+ <div class="spec-metadata">
4438
+ <div class="meta-box">
4439
+ <strong>Baseline Specification</strong>
4440
+ <div>${esc(report.baseline.title || report.baseline.source)}</div>
4441
+ <div class="meta-code">SHA: ${esc(report.baseline.sha256.slice(0, 16))} (${esc(report.baseline.openapiVersion)})</div>
4442
+ </div>
4443
+ <div class="meta-box">
4444
+ <strong>Current Specification</strong>
4445
+ <div>${esc(report.current.title || report.current.source)}</div>
4446
+ <div class="meta-code">SHA: ${esc(report.current.sha256.slice(0, 16))} (${esc(report.current.openapiVersion)})</div>
4447
+ </div>
4448
+ </div>
4449
+ </header>
4450
+
4451
+ <section class="stats-row">
4452
+ <div class="stat-card error">
4453
+ <div class="stat-label">Breaking Errors</div>
4454
+ <div class="stat-value">${breaking}</div>
4455
+ </div>
4456
+ <div class="stat-card warning">
4457
+ <div class="stat-label">Warnings</div>
4458
+ <div class="stat-value">${warnings}</div>
4459
+ </div>
4460
+ <div class="stat-card info">
4461
+ <div class="stat-label">Informational</div>
4462
+ <div class="stat-value">${infos}</div>
4463
+ </div>
4464
+ <div class="stat-card total">
4465
+ <div class="stat-label">Total Changes</div>
4466
+ <div class="stat-value">${total}</div>
4467
+ </div>
4468
+ </section>
4469
+
4470
+ <div class="semver-banner">
4471
+ <strong>Suggested Version Bump:</strong> ${esc(report.semver.bump.toUpperCase())} &mdash; ${esc(report.semver.reason)}
4472
+ </div>
4473
+
4474
+ <section class="controls-bar">
4475
+ <input type="search" id="searchInput" class="search-input" placeholder="Search by rule, path, or message..." aria-label="Search changes">
4476
+ <div class="filter-group">
4477
+ <button class="filter-btn active" data-severity="all">All (${total})</button>
4478
+ <button class="filter-btn" data-severity="error">Errors (${breaking})</button>
4479
+ <button class="filter-btn" data-severity="warning">Warnings (${warnings})</button>
4480
+ <button class="filter-btn" data-severity="info">Info (${infos})</button>
4481
+ </div>
4482
+ </section>
4483
+
4484
+ <main class="changes-list" id="changesContainer">
4485
+ ${report.changes.length === 0 ? `<div class="empty-state"><h3>No contract changes detected</h3><p>The current specification is fully compatible with the baseline.</p></div>` : report.changes.map((c) => {
4486
+ const ep = endpointOf(c);
4487
+ const locStr = c.location.file && c.location.line ? `${c.location.file}:${c.location.line}:${c.location.column ?? 1}` : c.location.pointerNew || c.location.pointerOld;
4488
+ const hasDiff = c.oldValue !== void 0 || c.newValue !== void 0;
4489
+ return `
4490
+ <article class="change-card ${c.severity}" data-severity="${c.severity}" data-search="${esc(`${c.ruleId} ${ep} ${c.message} ${locStr}`).toLowerCase()}">
4491
+ <div class="change-header">
4492
+ <div class="change-title-group">
4493
+ <span class="severity-tag ${c.severity}">${c.severity}</span>
4494
+ <span class="rule-name">${esc(c.ruleId)}</span>
4495
+ <span class="endpoint-pill">${esc(ep)}</span>
4496
+ </div>
4497
+ <div class="location-info">${esc(locStr)}</div>
4498
+ </div>
4499
+ <p class="change-message">${esc(c.message)}</p>
4500
+ <div class="suggestion-box">
4501
+ <strong>Suggestion:</strong> ${esc(c.suggestion)}
4502
+ </div>
4503
+ ${hasDiff ? `<div class="diff-values">
4504
+ ${c.oldValue !== void 0 ? `<div class="val-block">Old: <code>${esc(valueRepr(c.oldValue))}</code></div>` : ""}
4505
+ ${c.newValue !== void 0 ? `<div class="val-block">New: <code>${esc(valueRepr(c.newValue))}</code></div>` : ""}
4506
+ </div>` : ""}
4507
+ </article>`;
4508
+ }).join("\n")}
4509
+ </main>
4510
+ </div>
4511
+
4512
+ <script>
4513
+ (function() {
4514
+ const searchInput = document.getElementById('searchInput');
4515
+ const filterBtns = document.querySelectorAll('.filter-btn');
4516
+ const cards = document.querySelectorAll('.change-card');
4517
+ let currentSeverity = 'all';
4518
+ let currentQuery = '';
4519
+
4520
+ function applyFilters() {
4521
+ cards.forEach(card => {
4522
+ const sev = card.getAttribute('data-severity');
4523
+ const searchData = card.getAttribute('data-search') || '';
4524
+ const matchesSev = (currentSeverity === 'all' || sev === currentSeverity);
4525
+ const matchesQuery = (!currentQuery || searchData.includes(currentQuery));
4526
+ card.style.display = (matchesSev && matchesQuery) ? '' : 'none';
4527
+ });
4528
+ }
4529
+
4530
+ if (searchInput) {
4531
+ searchInput.addEventListener('input', e => {
4532
+ currentQuery = e.target.value.trim().toLowerCase();
4533
+ applyFilters();
4534
+ });
4535
+ }
4536
+
4537
+ filterBtns.forEach(btn => {
4538
+ btn.addEventListener('click', () => {
4539
+ filterBtns.forEach(b => b.classList.remove('active'));
4540
+ btn.classList.add('active');
4541
+ currentSeverity = btn.getAttribute('data-severity') || 'all';
4542
+ applyFilters();
4543
+ });
4544
+ });
4545
+ })();
4546
+ </script>
4547
+ </body>
4548
+ </html>`;
4549
+ }
4550
+
4551
+ // src/reporters/sarif.ts
4552
+ function sarifLevel(severity) {
4553
+ switch (severity) {
4554
+ case "error":
4555
+ return "error";
4556
+ case "warning":
4557
+ return "warning";
4558
+ case "info":
4559
+ return "note";
4560
+ }
4561
+ }
4562
+ function renderSarif(report) {
4563
+ const rules = getAllRules().map((r) => ({
4564
+ id: r.id,
4565
+ shortDescription: { text: r.id },
4566
+ fullDescription: { text: r.trigger },
4567
+ defaultConfiguration: {
4568
+ level: sarifLevel(r.defaultSeverity)
4569
+ },
4570
+ help: {
4571
+ text: `${r.trigger}
4572
+ Suggestion: ${r.suggestion({ location: { path: "/", pointerOld: "", pointerNew: "", context: "api" } })}`,
4573
+ markdown: `**Trigger:** ${r.trigger}
4574
+
4575
+ [API Guard Rule Reference](https://github.com/subhamxaura/apiguard/blob/main/docs/rules.md#${r.id})`
4576
+ },
4577
+ properties: {
4578
+ tags: ["api-contract", "openapi", "breaking-change", ...r.contexts]
4579
+ }
4580
+ }));
4581
+ const results = report.changes.map((c) => {
4582
+ const fileUri = (c.location.file || report.current.source).replace(/\\/g, "/");
4583
+ const line = c.location.line && c.location.line > 0 ? c.location.line : 1;
4584
+ const col = c.location.column && c.location.column > 0 ? c.location.column : 1;
4585
+ return {
4586
+ ruleId: c.ruleId,
4587
+ level: sarifLevel(c.severity),
4588
+ message: {
4589
+ text: `${c.message}. Suggestion: ${c.suggestion}`
4590
+ },
4591
+ locations: [
4592
+ {
4593
+ physicalLocation: {
4594
+ artifactLocation: {
4595
+ uri: fileUri,
4596
+ uriBaseId: "%SRCROOT%"
4597
+ },
4598
+ region: {
4599
+ startLine: line,
4600
+ startColumn: col
4601
+ }
4602
+ }
4603
+ }
4604
+ ],
4605
+ properties: {
4606
+ kind: c.kind,
4607
+ breaking: c.breaking,
4608
+ context: c.location.context,
4609
+ path: c.location.path,
4610
+ method: c.location.method
4611
+ }
4612
+ };
4613
+ });
4614
+ const sarifLog = {
4615
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
4616
+ version: "2.1.0",
4617
+ runs: [
4618
+ {
4619
+ tool: {
4620
+ driver: {
4621
+ name: "apiguard",
4622
+ version: report.tool.version,
4623
+ informationUri: "https://github.com/subhamxaura/apiguard",
4624
+ rules
4625
+ }
4626
+ },
4627
+ results
4628
+ }
4629
+ ]
4630
+ };
4631
+ return JSON.stringify(sarifLog, null, 2);
4632
+ }
4633
+
4634
+ // src/reporters/changelog.ts
4635
+ function endpointOf2(change) {
4636
+ const loc = change.location;
4637
+ if (loc.method && loc.path) return `\`${loc.method} ${loc.path}\``;
4638
+ if (loc.path && loc.path !== "/") return `\`${loc.path}\``;
4639
+ if (loc.componentName) return `component \`${loc.componentName}\``;
4640
+ return "API";
4641
+ }
4642
+ function renderChangelog(report) {
4643
+ const lines = [];
4644
+ const version = report.semver.bump === "none" ? "Current Release (No version bump)" : `Next Release (Recommended bump: ${report.semver.bump.toUpperCase()})`;
4645
+ lines.push(`## ${version}`);
4646
+ lines.push("");
4647
+ const breaking = report.changes.filter((c) => c.severity === "error");
4648
+ const additions = report.changes.filter((c) => c.severity !== "error" && c.kind === "addition");
4649
+ const modifications = report.changes.filter(
4650
+ (c) => c.severity !== "error" && c.kind === "modification"
4651
+ );
4652
+ const relaxations = report.changes.filter(
4653
+ (c) => c.severity !== "error" && (c.kind === "relaxation" || c.kind === "documentation")
4654
+ );
4655
+ if (breaking.length > 0) {
4656
+ lines.push("### \u{1F6D1} Breaking Changes");
4657
+ lines.push("");
4658
+ for (const c of breaking) {
4659
+ lines.push(`- **${endpointOf2(c)}**: ${c.message} (\`${c.ruleId}\`)`);
4660
+ lines.push(` *Suggestion: ${c.suggestion}*`);
4661
+ }
4662
+ lines.push("");
4663
+ }
4664
+ if (additions.length > 0) {
4665
+ lines.push("### \u{1F680} Added");
4666
+ lines.push("");
4667
+ for (const c of additions) {
4668
+ lines.push(`- **${endpointOf2(c)}**: ${c.message}`);
4669
+ }
4670
+ lines.push("");
4671
+ }
4672
+ if (modifications.length > 0) {
4673
+ lines.push("### \u{1F504} Changed");
4674
+ lines.push("");
4675
+ for (const c of modifications) {
4676
+ lines.push(`- **${endpointOf2(c)}**: ${c.message}`);
4677
+ }
4678
+ lines.push("");
4679
+ }
4680
+ if (relaxations.length > 0) {
4681
+ lines.push("### \u2139\uFE0F Other Improvements");
4682
+ lines.push("");
4683
+ for (const c of relaxations) {
4684
+ lines.push(`- **${endpointOf2(c)}**: ${c.message}`);
4685
+ }
4686
+ lines.push("");
4687
+ }
4688
+ if (report.changes.length === 0) {
4689
+ lines.push("No functional contract changes detected between specifications.");
4690
+ }
4691
+ return lines.join("\n").trim();
4692
+ }
4693
+
4694
+ // src/codegen/shim.ts
4695
+ function generateExpressShim(report) {
4696
+ const breaking = report.changes.filter((c) => c.severity === "error");
4697
+ const pathRewrites = [];
4698
+ const paramDefaults = [];
4699
+ for (const c of breaking) {
4700
+ if (c.ruleId === "operation-removed" && c.location.path) {
4701
+ pathRewrites.push({ oldPath: c.location.path });
4702
+ } else if (c.ruleId === "parameter-added-required" || c.ruleId === "parameter-made-required") {
4703
+ const paramName = c.location.pointerNew.split("/").pop() || "param";
4704
+ paramDefaults.push({
4705
+ method: c.location.method,
4706
+ path: c.location.path,
4707
+ param: paramName,
4708
+ defaultVal: c.newValue ?? ""
4709
+ });
4710
+ }
4711
+ }
4712
+ return `// Generated by API Guard (apiguard codegen-shim --target express)
4713
+ // Backward compatibility shim for baseline @ ${report.baseline.sha256.slice(0, 12)}
4714
+ import type { Request, Response, NextFunction } from 'express';
4715
+
4716
+ export function apiGuardCompatibilityShim(req: Request, res: Response, next: NextFunction) {
4717
+ const method = req.method.toUpperCase();
4718
+ const path = req.path;
4719
+
4720
+ // 1. Parameter defaults for newly required parameters
4721
+ ${paramDefaults.map(
4722
+ (p) => ` if (${p.method ? `method === '${p.method}' && ` : ""}path.startsWith('${p.path.replace(/\{.*\}/, "")}')) {
4723
+ if (req.query['${p.param}'] === undefined && req.body?.['${p.param}'] === undefined) {
4724
+ req.query['${p.param}'] = '${String(p.defaultVal)}';
4725
+ }
4726
+ }`
4727
+ ).join("\n")}
4728
+
4729
+ // 2. Response adapter interception
4730
+ const originalJson = res.json;
4731
+ res.json = function(body: any) {
4732
+ // Intercept and maintain legacy schema structure
4733
+ if (body && typeof body === 'object') {
4734
+ // Compatibility mutations can be inserted here
4735
+ }
4736
+ return originalJson.call(this, body);
4737
+ };
4738
+
4739
+ next();
4740
+ }
4741
+ `;
4742
+ }
4743
+ function generateCloudflareWorkerShim(report) {
4744
+ const breaking = report.changes.filter((c) => c.severity === "error");
4745
+ return `// Generated by API Guard (apiguard codegen-shim --target cloudflare-worker)
4746
+ // Edge backward-compatibility worker
4747
+ export default {
4748
+ async fetch(request: Request, env: any, ctx: any): Promise<Response> {
4749
+ const url = new URL(request.url);
4750
+ const method = request.method.toUpperCase();
4751
+
4752
+ // Clone request headers for mutation
4753
+ const modifiedHeaders = new Headers(request.headers);
4754
+
4755
+ // Track downstream upstream response
4756
+ const response = await fetch(new Request(url.toString(), {
4757
+ method: request.method,
4758
+ headers: modifiedHeaders,
4759
+ body: request.body,
4760
+ redirect: 'manual'
4761
+ }));
4762
+
4763
+ // Intercept JSON responses for schema backward compatibility
4764
+ const contentType = response.headers.get('content-type') || '';
4765
+ if (contentType.includes('application/json')) {
4766
+ try {
4767
+ const data = await response.json();
4768
+ // ${breaking.length} breaking changes monitored
4769
+ return new Response(JSON.stringify(data), {
4770
+ status: response.status,
4771
+ headers: response.headers
4772
+ });
4773
+ } catch {
4774
+ // Pass through unmodified if not json
4775
+ }
4776
+ }
4777
+
4778
+ return response;
4779
+ }
4780
+ };
4781
+ `;
4782
+ }
4783
+ function generateEnvoyLuaShim(report) {
4784
+ return `-- Generated by API Guard (apiguard codegen-shim --target envoy-lua)
4785
+ -- Envoy Proxy Lua filter for API backward compatibility (baseline: ${report.baseline.sha256.slice(0, 12)})
4786
+
4787
+ function envoy_on_request(request_handle)
4788
+ local headers = request_handle:headers()
4789
+ local path = headers:get(":path")
4790
+ local method = headers:get(":method")
4791
+
4792
+ -- Check for API compatibility headers
4793
+ if not headers:get("x-apiguard-shimmed") then
4794
+ headers:add("x-apiguard-shimmed", "true")
4795
+ end
4796
+ end
4797
+
4798
+ function envoy_on_response(response_handle)
4799
+ local headers = response_handle:headers()
4800
+ headers:add("x-apiguard-adapter", "v1-compat")
4801
+ end
4802
+ `;
4803
+ }
4804
+ function generateGatewayShim(report, target = "express") {
4805
+ const breaking = report.changes.filter((c) => c.severity === "error");
4806
+ let code;
4807
+ switch (target) {
4808
+ case "express":
4809
+ code = generateExpressShim(report);
4810
+ break;
4811
+ case "cloudflare-worker":
4812
+ code = generateCloudflareWorkerShim(report);
4813
+ break;
4814
+ case "envoy-lua":
4815
+ code = generateEnvoyLuaShim(report);
4816
+ break;
4817
+ }
4818
+ return {
4819
+ target,
4820
+ code,
4821
+ handledChanges: breaking.length
4822
+ };
4823
+ }
4824
+
3833
4825
  export {
3834
4826
  ExitCode,
3835
4827
  ApiguardError,
@@ -3844,5 +4836,9 @@ export {
3844
4836
  validateSemantics,
3845
4837
  formatIssues,
3846
4838
  suggestVersion,
3847
- analyze
4839
+ analyze,
4840
+ renderHtml,
4841
+ renderSarif,
4842
+ renderChangelog,
4843
+ generateGatewayShim
3848
4844
  };