@testsmith/api-spector 0.3.1 → 0.3.2

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.
@@ -1,12 +1,77 @@
1
1
  "use strict";
2
- const promises = require("fs/promises");
3
2
  const undici = require("undici");
3
+ const authBuilder = require("./auth-builder-CUs9yzOF.js");
4
+ const promises = require("fs/promises");
4
5
  const jsYaml = require("js-yaml");
5
6
  const Ajv = require("ajv");
6
- const authBuilder = require("./auth-builder-CRQayp8x.js");
7
7
  const path = require("path");
8
8
  const crypto = require("crypto");
9
+ const MATCH_KEY = "__match";
10
+ function isMatcher(node) {
11
+ return typeof node === "object" && node !== null && !Array.isArray(node) && typeof node[MATCH_KEY] === "string";
12
+ }
13
+ function compileMatcher(m) {
14
+ switch (m[MATCH_KEY]) {
15
+ case "type":
16
+ return compileMatcherExample(m.value, false);
17
+ case "integer":
18
+ return { type: "integer" };
19
+ case "decimal":
20
+ case "number":
21
+ return { type: "number" };
22
+ case "boolean":
23
+ return { type: "boolean" };
24
+ case "string":
25
+ return { type: "string" };
26
+ case "null":
27
+ return { type: "null" };
28
+ case "regex":
29
+ return { type: "string", pattern: m.regex ?? ".*" };
30
+ case "datetime":
31
+ case "timestamp":
32
+ return { type: "string", format: "date-time" };
33
+ case "date":
34
+ return { type: "string", format: "date" };
35
+ case "time":
36
+ return { type: "string", format: "time" };
37
+ case "eachLike": {
38
+ const schema = { type: "array", items: compileMatcherExample(m.value, false) };
39
+ schema["minItems"] = typeof m.min === "number" ? m.min : 1;
40
+ return schema;
41
+ }
42
+ default:
43
+ return {};
44
+ }
45
+ }
46
+ function compileMatcherExample(node, exact = true) {
47
+ if (isMatcher(node)) return compileMatcher(node);
48
+ if (node === null) return { type: "null" };
49
+ const t = typeof node;
50
+ if (t === "boolean") return exact ? { const: node } : { type: "boolean" };
51
+ if (t === "number") return exact ? { const: node } : Number.isInteger(node) ? { type: "integer" } : { type: "number" };
52
+ if (t === "string") return exact ? { const: node } : { type: "string" };
53
+ if (Array.isArray(node)) {
54
+ if (node.length === 0) return { type: "array" };
55
+ const schema = { type: "array", items: node.map((n) => compileMatcherExample(n, exact)) };
56
+ if (exact) schema["minItems"] = node.length;
57
+ return schema;
58
+ }
59
+ if (t === "object") {
60
+ const obj = node;
61
+ const properties = {};
62
+ const required = [];
63
+ for (const [key, value] of Object.entries(obj)) {
64
+ properties[key] = compileMatcherExample(value, exact);
65
+ required.push(key);
66
+ }
67
+ return { type: "object", properties, required, additionalProperties: true };
68
+ }
69
+ return {};
70
+ }
9
71
  const ajv$1 = new Ajv({ allErrors: true, strict: false });
72
+ function hasContract(contract) {
73
+ return !!contract && (contract.statusCode !== void 0 || !!contract.bodySchema || !!contract.bodyMatcher || !!contract.headers?.length);
74
+ }
10
75
  function validateConsumerResponse(contract, actualStatus, actualHeaders, bodyText) {
11
76
  const violations = [];
12
77
  if (contract.statusCode !== void 0 && actualStatus !== contract.statusCode) {
@@ -47,33 +112,51 @@ function validateConsumerResponse(contract, actualStatus, actualHeaders, bodyTex
47
112
  });
48
113
  return violations;
49
114
  }
50
- let data;
115
+ violations.push(...validateBody(schema, bodyText));
116
+ }
117
+ if (contract.bodyMatcher?.trim()) {
118
+ let example;
51
119
  try {
52
- data = JSON.parse(bodyText);
120
+ example = JSON.parse(contract.bodyMatcher);
53
121
  } catch {
54
122
  violations.push({
55
123
  type: "schema_violation",
56
- message: "Response body is not valid JSON — cannot validate against schema"
124
+ message: "Contract bodyMatcher is not valid JSON"
57
125
  });
58
126
  return violations;
59
127
  }
60
- try {
61
- const validate = ajv$1.compile(schema);
62
- if (!validate(data)) {
63
- for (const err of validate.errors ?? []) {
64
- violations.push({
65
- type: "schema_violation",
66
- message: err.message ?? "Schema violation",
67
- path: err.instancePath || "/"
68
- });
69
- }
128
+ violations.push(...validateBody(compileMatcherExample(example), bodyText));
129
+ }
130
+ return violations;
131
+ }
132
+ function validateBody(schema, bodyText) {
133
+ const violations = [];
134
+ let data;
135
+ try {
136
+ data = JSON.parse(bodyText);
137
+ } catch {
138
+ violations.push({
139
+ type: "schema_violation",
140
+ message: "Response body is not valid JSON — cannot validate against schema"
141
+ });
142
+ return violations;
143
+ }
144
+ try {
145
+ const validate = ajv$1.compile(schema);
146
+ if (!validate(data)) {
147
+ for (const err of validate.errors ?? []) {
148
+ violations.push({
149
+ type: "schema_violation",
150
+ message: err.message ?? "Schema violation",
151
+ path: err.instancePath || "/"
152
+ });
70
153
  }
71
- } catch (e) {
72
- violations.push({
73
- type: "schema_violation",
74
- message: `Schema compile error: ${e instanceof Error ? e.message : String(e)}`
75
- });
76
154
  }
155
+ } catch (e) {
156
+ violations.push({
157
+ type: "schema_violation",
158
+ message: `Schema compile error: ${e instanceof Error ? e.message : String(e)}`
159
+ });
77
160
  }
78
161
  return violations;
79
162
  }
@@ -132,9 +215,7 @@ async function executeContract(req, vars) {
132
215
  }
133
216
  async function runConsumerContracts(requests, envVars, collectionVars = {}) {
134
217
  const vars = { ...envVars, ...collectionVars };
135
- const contractRequests = requests.filter(
136
- (r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
137
- );
218
+ const contractRequests = requests.filter((r) => !r.disabled && hasContract(r.contract));
138
219
  const start = Date.now();
139
220
  const results = await Promise.all(contractRequests.map((r) => executeContract(r, vars)));
140
221
  const passed = results.filter((r) => r.passed).length;
@@ -309,6 +390,125 @@ async function runProviderVerification(requests, envVars, specUrl, specPath, req
309
390
  durationMs: Date.now() - start
310
391
  };
311
392
  }
393
+ function rebaseUrl(fullUrl, providerBaseUrl) {
394
+ if (!providerBaseUrl) return fullUrl;
395
+ try {
396
+ const orig = new URL(fullUrl, "http://placeholder.invalid");
397
+ const base = new URL(providerBaseUrl);
398
+ const basePath = base.pathname.replace(/\/$/, "");
399
+ base.pathname = (basePath + orig.pathname).replace(/\/{2,}/g, "/");
400
+ base.search = orig.search;
401
+ return base.toString();
402
+ } catch {
403
+ return fullUrl;
404
+ }
405
+ }
406
+ async function setupState(stateHandlerUrl, state, action) {
407
+ if (!stateHandlerUrl) {
408
+ return {
409
+ type: "provider_state_failed",
410
+ message: `Interaction requires provider state "${state}" but no --states-url / stateHandlerUrl was configured`,
411
+ expected: state
412
+ };
413
+ }
414
+ try {
415
+ const resp = await undici.fetch(stateHandlerUrl, {
416
+ method: "POST",
417
+ headers: { "Content-Type": "application/json" },
418
+ body: JSON.stringify({ state, action })
419
+ });
420
+ if (!resp.ok) {
421
+ return {
422
+ type: "provider_state_failed",
423
+ message: `State handler returned HTTP ${resp.status} setting up "${state}"`,
424
+ expected: state,
425
+ actual: String(resp.status)
426
+ };
427
+ }
428
+ return null;
429
+ } catch (err) {
430
+ return {
431
+ type: "provider_state_failed",
432
+ message: `State handler unreachable for "${state}": ${err instanceof Error ? err.message : String(err)}`,
433
+ expected: state
434
+ };
435
+ }
436
+ }
437
+ async function verifyInteraction(req, vars, providerBaseUrl, stateHandlerUrl) {
438
+ const url = rebaseUrl(authBuilder.buildUrl(req.url, req.params, vars), providerBaseUrl);
439
+ const start = Date.now();
440
+ const violations = [];
441
+ const states = req.contract?.providerStates ?? [];
442
+ for (const state of states) {
443
+ const v = await setupState(stateHandlerUrl, state, "setup");
444
+ if (v) violations.push(v);
445
+ }
446
+ if (violations.length > 0) {
447
+ return { requestId: req.id, requestName: req.name, method: req.method, url, passed: false, violations, durationMs: Date.now() - start };
448
+ }
449
+ try {
450
+ const headers = new undici.Headers();
451
+ for (const h of req.headers) {
452
+ if (h.enabled && h.key) headers.set(authBuilder.interpolate(h.key, vars), authBuilder.interpolate(h.value, vars));
453
+ }
454
+ const authHeaders = await authBuilder.buildAuthHeaders(req.auth, vars);
455
+ for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
456
+ let body;
457
+ if (req.body.mode === "json" && req.body.json) {
458
+ body = authBuilder.interpolate(req.body.json, vars);
459
+ if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
460
+ } else if (req.body.mode === "raw" && req.body.raw) {
461
+ body = authBuilder.interpolate(req.body.raw, vars);
462
+ }
463
+ const resp = await undici.fetch(url, {
464
+ method: req.method,
465
+ headers,
466
+ body: !["GET", "HEAD"].includes(req.method) ? body : void 0
467
+ });
468
+ const bodyText = await resp.text();
469
+ const rawHeaders = {};
470
+ resp.headers.forEach((v, k) => {
471
+ rawHeaders[k] = v;
472
+ });
473
+ violations.push(...validateConsumerResponse(req.contract, resp.status, rawHeaders, bodyText));
474
+ return {
475
+ requestId: req.id,
476
+ requestName: req.name,
477
+ method: req.method,
478
+ url,
479
+ passed: violations.length === 0,
480
+ violations,
481
+ durationMs: Date.now() - start,
482
+ actualStatus: resp.status
483
+ };
484
+ } catch (err) {
485
+ violations.push({
486
+ type: "status_mismatch",
487
+ message: `Request failed: ${err instanceof Error ? err.message : String(err)}`
488
+ });
489
+ return { requestId: req.id, requestName: req.name, method: req.method, url, passed: false, violations, durationMs: Date.now() - start };
490
+ } finally {
491
+ for (const state of states) await setupState(stateHandlerUrl, state, "teardown");
492
+ }
493
+ }
494
+ async function runLiveProviderVerification(requests, envVars, collectionVars = {}, providerBaseUrl, stateHandlerUrl) {
495
+ const vars = { ...envVars, ...collectionVars };
496
+ const start = Date.now();
497
+ const contractRequests = requests.filter((r) => !r.disabled && hasContract(r.contract));
498
+ const results = [];
499
+ for (const req of contractRequests) {
500
+ results.push(await verifyInteraction(req, vars, providerBaseUrl, stateHandlerUrl));
501
+ }
502
+ const passed = results.filter((r) => r.passed).length;
503
+ return {
504
+ mode: "provider-live",
505
+ total: results.length,
506
+ passed,
507
+ failed: results.length - passed,
508
+ results,
509
+ durationMs: Date.now() - start
510
+ };
511
+ }
312
512
  function checkSchemaCompatibility(consumerSchema, providerSchema, path2 = "") {
313
513
  const violations = [];
314
514
  if (!consumerSchema || !providerSchema) return violations;
@@ -417,9 +617,7 @@ async function runBidirectional(requests, envVars, collectionVars = {}, specUrl,
417
617
  const spec = await loadSpec(specUrl, specPath);
418
618
  const vars = { ...envVars, ...collectionVars };
419
619
  const start = Date.now();
420
- const contractRequests = requests.filter(
421
- (r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
422
- );
620
+ const contractRequests = requests.filter((r) => !r.disabled && hasContract(r.contract));
423
621
  const results = await Promise.all(contractRequests.map(async (req) => {
424
622
  const url = authBuilder.buildUrl(req.url, req.params, vars);
425
623
  const violations = [];
@@ -475,6 +673,143 @@ async function runBidirectional(requests, envVars, collectionVars = {}, specUrl,
475
673
  durationMs: Date.now() - start
476
674
  };
477
675
  }
676
+ function esc(s) {
677
+ return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
678
+ }
679
+ function modeLabel(mode) {
680
+ if (mode === "bidirectional") return "Bi-directional";
681
+ if (mode === "provider-live") return "Provider (live)";
682
+ return mode.charAt(0).toUpperCase() + mode.slice(1);
683
+ }
684
+ const STYLE = `
685
+ :root { color-scheme: dark; }
686
+ * { box-sizing: border-box; }
687
+ body { margin: 0; font: 14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
688
+ background: #0f1115; color: #e5e7eb; }
689
+ .wrap { max-width: 960px; margin: 0 auto; padding: 32px 24px 64px; }
690
+ h1 { font-size: 20px; margin: 0 0 4px; }
691
+ .sub { color: #8b929e; font-size: 13px; margin: 0 0 24px; }
692
+ .meta { display: flex; flex-wrap: wrap; gap: 8px 20px; color: #9aa1ad; font-size: 12px; margin-bottom: 12px; }
693
+ .meta b { color: #cbd2dc; font-weight: 600; }
694
+ .headline { font-size: 24px; font-weight: 700; }
695
+ .headline.ok { color: #34d399; } .headline.bad { color: #f87171; }
696
+ .bar { display: flex; height: 10px; border-radius: 6px; overflow: hidden; background: #1b1f27; margin: 14px 0 6px; }
697
+ .bar > i { display: block; height: 100%; }
698
+ .bar .ok { background: #34d399; } .bar .bad { background: #f87171; }
699
+ .cards { display: flex; flex-direction: column; gap: 10px; margin-top: 24px; }
700
+ .card { border: 1px solid #232834; border-radius: 10px; overflow: hidden; background: #141821; }
701
+ .card.fail { border-color: #7f1d1d; }
702
+ .row { display: flex; align-items: center; gap: 12px; padding: 12px 16px; }
703
+ .card.fail summary .row { background: rgba(127,29,29,.18); }
704
+ .pill { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 5px; flex-shrink: 0; letter-spacing: .03em; }
705
+ .pill.ok { background: rgba(16,84,55,.55); color: #34d399; }
706
+ .pill.bad { background: rgba(127,29,29,.55); color: #f87171; }
707
+ .method { font: 700 12px ui-monospace,SFMono-Regular,Menlo,monospace; width: 56px; flex-shrink: 0; color: #93c5fd; }
708
+ .name { flex: 1; color: #f3f4f6; font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
709
+ .url { color: #6b7280; font: 11px ui-monospace,monospace; max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
710
+ .dur { color: #6b7280; font-size: 11px; }
711
+ .code { font: 700 12px ui-monospace,monospace; }
712
+ .code.s2 { color: #34d399; } .code.s3 { color: #fbbf24; } .code.s4, .code.s5 { color: #f87171; }
713
+ .viol { padding: 14px 16px; border-top: 1px solid #1f2530; display: flex; flex-direction: column; gap: 10px; }
714
+ .v { border-left: 2px solid #dc2626; background: rgba(127,29,29,.12); border-radius: 0 6px 6px 0; padding: 8px 12px; }
715
+ .v .t { font: 700 10px ui-monospace,monospace; color: #f87171; text-transform: uppercase; letter-spacing: .04em; }
716
+ .v .path { font: 10px ui-monospace,monospace; color: #8b929e; background: #1b1f27; padding: 1px 6px; border-radius: 4px; margin-left: 8px; }
717
+ .v .m { color: #fecaca; font-size: 13px; margin: 4px 0 0; }
718
+ .v .ea { font: 11px ui-monospace,monospace; margin-top: 4px; display: flex; gap: 18px; }
719
+ .v .ea .lab { color: #6b7280; }
720
+ .v .ea .exp { color: #34d399; } .v .ea .act { color: #f87171; }
721
+ .pass-note { color: #34d399; font-size: 13px; padding: 12px 16px; border-top: 1px solid #1f2530; }
722
+ table { border-collapse: collapse; width: 100%; margin-top: 16px; font-size: 13px; }
723
+ th, td { border: 1px solid #232834; padding: 8px 12px; text-align: left; }
724
+ th { background: #141821; color: #cbd2dc; font-weight: 600; }
725
+ td.cell { text-align: center; }
726
+ .b { display: inline-block; min-width: 56px; padding: 2px 8px; border-radius: 5px; font-size: 11px; font-weight: 700; }
727
+ .b.ok { background: rgba(16,84,55,.55); color: #34d399; }
728
+ .b.bad { background: rgba(127,29,29,.55); color: #f87171; }
729
+ .b.na { background: #1b1f27; color: #6b7280; }
730
+ .foot { color: #6b7280; font-size: 11px; margin-top: 40px; text-align: center; }
731
+ summary { cursor: pointer; list-style: none; }
732
+ summary::-webkit-details-marker { display: none; }
733
+ `;
734
+ function page(title, body) {
735
+ return `<!doctype html>
736
+ <html lang="en"><head><meta charset="utf-8">
737
+ <meta name="viewport" content="width=device-width, initial-scale=1">
738
+ <title>${esc(title)}</title>
739
+ <style>${STYLE}</style>
740
+ </head><body><div class="wrap">${body}
741
+ <p class="foot">Generated by API Spector · contract reporting</p>
742
+ </div></body></html>`;
743
+ }
744
+ function statusClass(code) {
745
+ if (code === void 0) return "";
746
+ return "s" + String(code)[0];
747
+ }
748
+ function violationHtml(r) {
749
+ if (r.violations.length === 0) return `<div class="pass-note">All expectations met.</div>`;
750
+ const items = r.violations.map((v) => {
751
+ const path2 = v.path ? `<span class="path">${esc(v.path)}</span>` : "";
752
+ const ea = v.expected || v.actual ? `<div class="ea">${v.expected ? `<span><span class="lab">expected </span><span class="exp">${esc(v.expected)}</span></span>` : ""}${v.actual ? `<span><span class="lab">actual </span><span class="act">${esc(v.actual)}</span></span>` : ""}</div>` : "";
753
+ return `<div class="v"><div><span class="t">${esc(v.type.replace(/_/g, " "))}</span>${path2}</div><p class="m">${esc(v.message)}</p>${ea}</div>`;
754
+ }).join("");
755
+ return `<div class="viol">${items}</div>`;
756
+ }
757
+ function cardHtml(r) {
758
+ const pill = r.passed ? `<span class="pill ok">PASS</span>` : `<span class="pill bad">FAIL</span>`;
759
+ const status = r.actualStatus !== void 0 ? `<span class="code ${statusClass(r.actualStatus)}">${r.actualStatus}</span>` : "";
760
+ const dur = r.durationMs !== void 0 ? `<span class="dur">${r.durationMs}ms</span>` : "";
761
+ return `<details class="card ${r.passed ? "" : "fail"}" ${r.passed ? "" : "open"}>
762
+ <summary><div class="row">${pill}<span class="method">${esc(r.method)}</span><span class="name">${esc(r.requestName)}</span><span class="url">${esc(r.url)}</span>${status}${dur}</div></summary>
763
+ ${violationHtml(r)}
764
+ </details>`;
765
+ }
766
+ function reportToHtml(report, meta = {}) {
767
+ const okPct = report.total ? Math.round(report.passed / report.total * 100) : 100;
768
+ const badPct = 100 - okPct;
769
+ const headlineCls = report.failed === 0 ? "ok" : "bad";
770
+ const headline = report.failed === 0 ? "✓ All passed" : `✗ ${report.failed} failed`;
771
+ const metaRows = [
772
+ `<span><b>Mode</b> ${esc(modeLabel(report.mode))}</span>`,
773
+ meta.provider ? `<span><b>Provider</b> ${esc(meta.provider)}</span>` : "",
774
+ meta.consumer ? `<span><b>Consumer</b> ${esc(meta.consumer)}</span>` : "",
775
+ meta.spec ? `<span><b>Spec</b> ${esc(meta.spec)}</span>` : "",
776
+ `<span><b>Duration</b> ${report.durationMs}ms</span>`,
777
+ meta.generatedAt ? `<span><b>Generated</b> ${esc(meta.generatedAt)}</span>` : ""
778
+ ].filter(Boolean).join("");
779
+ const failed = report.results.filter((r) => !r.passed);
780
+ const passed = report.results.filter((r) => r.passed);
781
+ const cards = [...failed, ...passed].map(cardHtml).join("");
782
+ const body = `
783
+ <h1>${esc(meta.title ?? "Contract Verification Report")}</h1>
784
+ <p class="sub"><span class="headline ${headlineCls}">${esc(headline)}</span> &nbsp; ${report.passed} / ${report.total} interactions passed</p>
785
+ <div class="meta">${metaRows}</div>
786
+ <div class="bar"><i class="ok" style="width:${okPct}%"></i><i class="bad" style="width:${badPct}%"></i></div>
787
+ <div class="cards">${cards || '<p class="sub">No interactions ran.</p>'}</div>`;
788
+ return page(meta.title ?? "Contract Verification Report", body);
789
+ }
790
+ function dashboardToHtml(records, generatedAt) {
791
+ const pacticipants = [...new Set(records.map((r) => r.pacticipant))].sort();
792
+ const versions = [...new Set(records.map((r) => r.version))].sort();
793
+ const byKey = new Map(records.map((r) => [`${r.pacticipant}@@${r.version}`, r]));
794
+ const header = `<tr><th>Pacticipant \\ Version</th>${versions.map((v) => `<th>${esc(v)}</th>`).join("")}</tr>`;
795
+ const rows = pacticipants.map((p) => {
796
+ const cells = versions.map((v) => {
797
+ const rec = byKey.get(`${p}@@${v}`);
798
+ if (!rec) return `<td class="cell"><span class="b na">—</span></td>`;
799
+ const cls = rec.passed ? "ok" : "bad";
800
+ const label = rec.passed ? `${rec.report.total}/${rec.report.total}` : `${rec.report.passed}/${rec.report.total}`;
801
+ return `<td class="cell" title="${esc(rec.recordedAt)}"><span class="b ${cls}">${esc(label)}</span></td>`;
802
+ }).join("");
803
+ return `<tr><td><b>${esc(p)}</b></td>${cells}</tr>`;
804
+ }).join("");
805
+ const totalPass = records.filter((r) => r.passed).length;
806
+ const body = `
807
+ <h1>Contract Dashboard</h1>
808
+ <p class="sub">${records.length} recorded verification${records.length === 1 ? "" : "s"} · ${totalPass} passing</p>
809
+ <div class="meta">${generatedAt ? `<span><b>Generated</b> ${esc(generatedAt)}</span>` : ""}</div>
810
+ ${records.length ? `<table>${header}${rows}</table>` : '<p class="sub">No recorded results yet. Run a verification with <code>--record --app-version &lt;ver&gt;</code>.</p>'}`;
811
+ return page("Contract Dashboard", body);
812
+ }
478
813
  const SNAPSHOT_DIR = "contracts";
479
814
  function safeName(raw) {
480
815
  return raw.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "spec";
@@ -578,11 +913,16 @@ async function deleteSnapshot(workspaceDir, relPath) {
578
913
  } catch {
579
914
  }
580
915
  }
916
+ exports.MATCH_KEY = MATCH_KEY;
581
917
  exports.captureSnapshot = captureSnapshot;
918
+ exports.dashboardToHtml = dashboardToHtml;
582
919
  exports.deleteSnapshot = deleteSnapshot;
920
+ exports.hasContract = hasContract;
583
921
  exports.listSnapshots = listSnapshots;
584
922
  exports.loadSnapshot = loadSnapshot;
585
923
  exports.relPathOf = relPathOf;
924
+ exports.reportToHtml = reportToHtml;
586
925
  exports.runBidirectional = runBidirectional;
587
926
  exports.runConsumerContracts = runConsumerContracts;
927
+ exports.runLiveProviderVerification = runLiveProviderVerification;
588
928
  exports.runProviderVerification = runProviderVerification;
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ const handle = require("./handle-C0IQL-Vl.js");
2
3
  const https = require("https");
3
4
  const http = require("http");
4
5
  const xmldom = require("@xmldom/xmldom");
@@ -305,16 +306,16 @@ if (opName) {
305
306
  `;
306
307
  }
307
308
  function registerSoapHandlers(ipc) {
308
- ipc.handle("wsdl:fetch", async (_event, url, extraHeaders = {}) => {
309
- const { validateWsdlFetchUrl } = await Promise.resolve().then(() => require("./ipc-validate-CscN4HfG.js"));
309
+ handle.handleIpc(ipc, handle.IPC.wsdl.fetch, async (_event, url, extraHeaders = {}) => {
310
+ const { validateWsdlFetchUrl } = await Promise.resolve().then(() => require("./ipc-validate-k6KI8adf.js"));
310
311
  validateWsdlFetchUrl(url);
311
312
  const wsdlText = await fetchUrl(url, extraHeaders);
312
313
  return parseWsdl(wsdlText);
313
314
  });
314
- ipc.handle("wsdl:import", async (_event, opts) => {
315
- const { validateWsdlImport } = await Promise.resolve().then(() => require("./ipc-validate-CscN4HfG.js"));
315
+ handle.handleIpc(ipc, handle.IPC.wsdl.import, async (_event, opts) => {
316
+ const { validateWsdlImport } = await Promise.resolve().then(() => require("./ipc-validate-k6KI8adf.js"));
316
317
  validateWsdlImport(opts);
317
- const { importWsdl } = await Promise.resolve().then(() => require("./import-C9qdkBCH.js"));
318
+ const { importWsdl } = await Promise.resolve().then(() => require("./import-C5Ngq8hk.js"));
318
319
  const wsdlText = opts.xml ?? (opts.url ? await fetchUrl(opts.url) : "");
319
320
  if (!wsdlText) throw new Error("wsdl:import requires either `url` or `xml`");
320
321
  return importWsdl(wsdlText, { name: opts.name, existingMockPorts: opts.existingMockPorts });