@testsmith/api-spector 0.4.5 → 0.4.7

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.
@@ -2,7 +2,8 @@
2
2
  "use strict";
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
- const cliCommon = require("./chunks/cli-common-CDQY1erJ.js");
5
+ const cliCommon = require("./chunks/cli-common-BKYgsbGB.js");
6
+ require("crypto");
6
7
  const AGENTS = {
7
8
  claude: {
8
9
  name: "Claude Code",
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  const promises = require("fs/promises");
3
3
  const path = require("path");
4
+ require("crypto");
4
5
  const C = {
5
6
  reset: "\x1B[0m",
6
7
  bold: "\x1B[1m",
@@ -25,7 +25,11 @@ const IPC = {
25
25
  },
26
26
  // ─── HTTP execution ────────────────────────────────────────────────────────
27
27
  request: {
28
- send: "request:send"
28
+ send: "request:send",
29
+ /** Event (main→renderer): a batch of streamed response frames. */
30
+ streamEvent: "request:stream-event",
31
+ /** Renderer→main: abort an in-flight streamed read by streamId. */
32
+ stopStream: "request:stop-stream"
29
33
  },
30
34
  // ─── Secrets ───────────────────────────────────────────────────────────────
31
35
  secret: {
@@ -105,6 +109,8 @@ const IPC = {
105
109
  run: "contract:run",
106
110
  inferSchema: "contract:inferSchema",
107
111
  exportReportHtml: "contract:exportReportHtml",
112
+ /** Compile a design-first contract to a Pact file on disk (save dialog). */
113
+ exportDesignPact: "contract:exportDesignPact",
108
114
  captureSnapshot: "contract:captureSnapshot",
109
115
  listSnapshots: "contract:listSnapshots",
110
116
  loadSnapshot: "contract:loadSnapshot",
@@ -159,6 +165,25 @@ const IPC = {
159
165
  // ─── App ───────────────────────────────────────────────────────────────────
160
166
  app: {
161
167
  checkUpdate: "app:checkUpdate"
168
+ },
169
+ // ─── Cloud (API Spector Cloud integration) ──────────────────────────────────
170
+ cloud: {
171
+ /** Verify the endpoint + token (GET /api/me). */
172
+ test: "cloud:test",
173
+ /** Push a mock server definition (POST /api/mocks). */
174
+ pushMock: "cloud:pushMock",
175
+ /** Look up an existing cloud mock's routes (GET /api/mocks/{slug}). */
176
+ getMock: "cloud:getMock",
177
+ /** Push a request as a monitor, URL resolved (POST /api/monitors). */
178
+ pushMonitor: "cloud:pushMonitor",
179
+ /** Publish a consumer pact built from requests (PUT /api/contracts). */
180
+ pushPact: "cloud:pushPact",
181
+ /** Publish a design-first consumer contract as a pact (PUT /api/contracts). */
182
+ pushDesignContract: "cloud:pushDesignContract",
183
+ /** Publish a provider OpenAPI spec (PUT /api/provider-contracts). */
184
+ pushSpec: "cloud:pushSpec",
185
+ /** Open the cloud deployment matrix in the browser. */
186
+ openMatrix: "cloud:openMatrix"
162
187
  }
163
188
  };
164
189
  function handleIpc(ipc, channel, fn) {
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const uuid = require("uuid");
4
- const soapHandler = require("./soap-handler-CAyUjzxB.js");
5
- require("./handle-BGYDylL2.js");
4
+ const soapHandler = require("./soap-handler-BXx842MN.js");
5
+ require("./handle-rimXXdJH.js");
6
6
  require("https");
7
7
  require("http");
8
8
  require("@xmldom/xmldom");
@@ -292,7 +292,9 @@ function getRunningIds() {
292
292
  async function stopAll() {
293
293
  await Promise.all([...running.keys()].map(stopMock));
294
294
  }
295
+ exports.findRoute = findRoute;
295
296
  exports.getRunningIds = getRunningIds;
297
+ exports.handleRequest = handleRequest;
296
298
  exports.isRunning = isRunning;
297
299
  exports.setHitCallback = setHitCallback;
298
300
  exports.startMock = startMock;
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  const undici = require("undici");
25
25
  const promises = require("fs/promises");
26
26
  const nodeTls = require("tls");
27
- const handle = require("./handle-BGYDylL2.js");
27
+ const handle = require("./handle-rimXXdJH.js");
28
28
  const crypto = require("crypto");
29
29
  const path = require("path");
30
30
  const dayjs = require("dayjs");
@@ -637,6 +637,178 @@ function normalizeProxyInput(input) {
637
637
  function ensureScheme(value) {
638
638
  return /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `http://${value}`;
639
639
  }
640
+ function detectStreamKind(contentType, forceStream = false) {
641
+ const ct = (contentType || "").toLowerCase();
642
+ if (ct.includes("text/event-stream")) return "sse";
643
+ if (ct.includes("application/x-ndjson") || ct.includes("application/ndjson") || ct.includes("application/jsonl") || ct.includes("application/x-jsonlines") || ct.includes("application/stream+json") || ct.includes("application/json-seq")) return "ndjson";
644
+ return forceStream ? "chunk" : null;
645
+ }
646
+ class StreamFramer {
647
+ constructor(mode) {
648
+ this.mode = mode;
649
+ }
650
+ carry = "";
651
+ /** Feed a decoded text chunk; returns the frames that completed. */
652
+ push(text) {
653
+ this.carry += text.replace(/\r\n?/g, "\n");
654
+ return this.mode === "sse" ? this.drainSse() : this.drainNdjson();
655
+ }
656
+ /** Emit whatever remains once the stream closes (a last line/block with no
657
+ * trailing delimiter). */
658
+ flush() {
659
+ const rest = this.carry;
660
+ this.carry = "";
661
+ if (this.mode === "ndjson") {
662
+ const f2 = this.ndjsonLine(rest);
663
+ return f2 ? [f2] : [];
664
+ }
665
+ const f = this.parseSseBlock(rest);
666
+ return f ? [f] : [];
667
+ }
668
+ drainSse() {
669
+ const out = [];
670
+ let idx;
671
+ while ((idx = this.carry.indexOf("\n\n")) !== -1) {
672
+ const block = this.carry.slice(0, idx);
673
+ this.carry = this.carry.slice(idx + 2);
674
+ const f = this.parseSseBlock(block);
675
+ if (f) out.push(f);
676
+ }
677
+ return out;
678
+ }
679
+ parseSseBlock(block) {
680
+ let name;
681
+ let id;
682
+ const dataLines = [];
683
+ for (const line of block.split("\n")) {
684
+ if (line === "" || line.startsWith(":")) continue;
685
+ const colon = line.indexOf(":");
686
+ const field = colon === -1 ? line : line.slice(0, colon);
687
+ let value = colon === -1 ? "" : line.slice(colon + 1);
688
+ if (value.startsWith(" ")) value = value.slice(1);
689
+ if (field === "data") dataLines.push(value);
690
+ else if (field === "event") name = value;
691
+ else if (field === "id") id = value;
692
+ }
693
+ if (dataLines.length === 0) return null;
694
+ return { name: name ?? "message", id, data: dataLines.join("\n") };
695
+ }
696
+ drainNdjson() {
697
+ const out = [];
698
+ let idx;
699
+ while ((idx = this.carry.indexOf("\n")) !== -1) {
700
+ const line = this.carry.slice(0, idx);
701
+ this.carry = this.carry.slice(idx + 1);
702
+ const f = this.ndjsonLine(line);
703
+ if (f) out.push(f);
704
+ }
705
+ return out;
706
+ }
707
+ ndjsonLine(line) {
708
+ const body = line.charCodeAt(0) === 30 ? line.slice(1) : line;
709
+ const s = body.trim();
710
+ return s ? { data: s } : null;
711
+ }
712
+ }
713
+ function toEvent(frame, kind, seq, tMs) {
714
+ let json;
715
+ const trimmed = frame.data.trim();
716
+ if (trimmed && (trimmed[0] === "{" || trimmed[0] === "[")) {
717
+ try {
718
+ json = JSON.parse(trimmed);
719
+ } catch {
720
+ }
721
+ }
722
+ const ev = { seq, tMs, kind, data: frame.data };
723
+ if (frame.name !== void 0) ev.name = frame.name;
724
+ if (frame.id !== void 0) ev.id = frame.id;
725
+ if (json !== void 0) ev.json = json;
726
+ return ev;
727
+ }
728
+ async function readStream(body, kind, startMs, opts = {}) {
729
+ const now = opts.now ?? Date.now;
730
+ const maxEvents = opts.maxEvents ?? 5e3;
731
+ const maxMs = opts.maxMs ?? 3e5;
732
+ const idleMs = opts.idleMs ?? 6e4;
733
+ let lastActivity = now();
734
+ const framer = kind === "chunk" ? null : new StreamFramer(kind === "sse" ? "sse" : "ndjson");
735
+ const decoder = new TextDecoder("utf-8", { fatal: false });
736
+ const events = [];
737
+ let text = "";
738
+ let firstEventMs = -1;
739
+ let close = "complete";
740
+ if (!body) return { events, text, close, firstEventMs: 0 };
741
+ const emit = (frames) => {
742
+ for (const frame of frames) {
743
+ const tMs = now() - startMs;
744
+ const ev = toEvent(frame, kind, events.length, tMs);
745
+ if (firstEventMs < 0) firstEventMs = tMs;
746
+ events.push(ev);
747
+ opts.onEvent?.(ev);
748
+ if (events.length >= maxEvents) return false;
749
+ }
750
+ return true;
751
+ };
752
+ const reader = body.getReader();
753
+ try {
754
+ for (; ; ) {
755
+ if (opts.signal?.aborted) {
756
+ close = "stopped";
757
+ break;
758
+ }
759
+ const totalRem = maxMs > 0 ? maxMs - (now() - startMs) : Infinity;
760
+ const idleRem = idleMs > 0 ? idleMs - (now() - lastActivity) : Infinity;
761
+ if (totalRem <= 0 || idleRem <= 0) {
762
+ close = "timeout";
763
+ break;
764
+ }
765
+ const wait = Math.min(totalRem, idleRem);
766
+ let step;
767
+ if (wait === Infinity) {
768
+ step = await reader.read();
769
+ } else {
770
+ let timer;
771
+ const timeout = new Promise((res) => {
772
+ timer = setTimeout(() => res("timeout"), wait);
773
+ });
774
+ step = await Promise.race([reader.read(), timeout]);
775
+ if (timer) clearTimeout(timer);
776
+ }
777
+ if (step === "timeout") {
778
+ close = "timeout";
779
+ break;
780
+ }
781
+ const { value, done } = step;
782
+ if (done) break;
783
+ if (!value) continue;
784
+ lastActivity = now();
785
+ const chunkText = decoder.decode(value, { stream: true });
786
+ text += chunkText;
787
+ const keepGoing = framer ? emit(framer.push(chunkText)) : emit([{ data: chunkText }]);
788
+ if (!keepGoing) {
789
+ close = "stopped";
790
+ break;
791
+ }
792
+ }
793
+ if (close === "complete") {
794
+ const tail = decoder.decode();
795
+ if (tail) {
796
+ text += tail;
797
+ if (framer) emit(framer.push(tail));
798
+ else emit([{ data: tail }]);
799
+ }
800
+ if (framer) emit(framer.flush());
801
+ }
802
+ } catch {
803
+ close = "error";
804
+ } finally {
805
+ try {
806
+ await reader.cancel();
807
+ } catch {
808
+ }
809
+ }
810
+ return { events, text, close, firstEventMs: firstEventMs < 0 ? 0 : firstEventMs };
811
+ }
640
812
  const SIGNATURE = Buffer.from("NTLMSSP\0", "latin1");
641
813
  const F_UNICODE = 1;
642
814
  const F_OEM = 2;
@@ -1181,6 +1353,11 @@ function applyRequestDefaults(req) {
1181
1353
  if (!req.body) req.body = { mode: "none" };
1182
1354
  if (!req.auth) req.auth = { type: "none" };
1183
1355
  }
1356
+ let _defaultAgent;
1357
+ function defaultAgent() {
1358
+ if (!_defaultAgent) _defaultAgent = new undici.Agent({ allowH2: true });
1359
+ return _defaultAgent;
1360
+ }
1184
1361
  async function buildDispatcher(proxy, tls) {
1185
1362
  if (proxy?.url?.trim()) trustSystemCertificateStore();
1186
1363
  const connectOpts = {};
@@ -1213,13 +1390,14 @@ async function buildDispatcher(proxy, tls) {
1213
1390
  return new undici.ProxyAgent({
1214
1391
  uri: buildProxyUri({ url: proxy.url, auth: proxy.auth }),
1215
1392
  requestTls: hasTls ? connectOpts : void 0,
1216
- proxyTls: hasTls ? connectOpts : void 0
1393
+ proxyTls: hasTls ? connectOpts : void 0,
1394
+ allowH2: true
1217
1395
  });
1218
1396
  }
1219
1397
  if (hasTls) {
1220
- return new undici.Agent({ connect: connectOpts });
1398
+ return new undici.Agent({ connect: connectOpts, allowH2: true });
1221
1399
  }
1222
- return void 0;
1400
+ return defaultAgent();
1223
1401
  }
1224
1402
  function buildBodyAndApplyHeaders(req, vars, headers) {
1225
1403
  let body;
@@ -1323,11 +1501,27 @@ async function performHttpExchange(opts) {
1323
1501
  sentHeaders = captureSent(headers);
1324
1502
  fetchResp = await doFetch(headers);
1325
1503
  }
1326
- const responseBody = await fetchResp.text();
1327
1504
  const rawHeaders = {};
1328
1505
  fetchResp.headers.forEach((value, key) => {
1329
1506
  rawHeaders[key] = value;
1330
1507
  });
1508
+ const kind = detectStreamKind(rawHeaders["content-type"], opts.forceStream);
1509
+ const bodyStream = fetchResp.body;
1510
+ const canStream = kind !== null && bodyStream !== null && typeof bodyStream.getReader === "function";
1511
+ let responseBody;
1512
+ let streamFields = {};
1513
+ if (canStream) {
1514
+ const r = await readStream(bodyStream, kind, start, {
1515
+ signal: opts.signal,
1516
+ onEvent: opts.onStreamEvent,
1517
+ idleMs: req.stream?.idleMs,
1518
+ maxMs: req.stream?.maxMs
1519
+ });
1520
+ responseBody = r.text;
1521
+ streamFields = { streamed: true, events: r.events, streamClose: r.close, firstEventMs: r.firstEventMs };
1522
+ } else {
1523
+ responseBody = await fetchResp.text();
1524
+ }
1331
1525
  return {
1332
1526
  status: fetchResp.status,
1333
1527
  statusText: fetchResp.statusText,
@@ -1336,7 +1530,8 @@ async function performHttpExchange(opts) {
1336
1530
  durationMs: Date.now() - start,
1337
1531
  sentHeaders,
1338
1532
  sentBody: effectiveBody,
1339
- finalUrl
1533
+ finalUrl,
1534
+ ...streamFields
1340
1535
  };
1341
1536
  }
1342
1537
  function deriveRunStatus(postScriptError, testResults, httpStatus) {
@@ -1522,9 +1717,11 @@ exports.buildAuthHeaders = buildAuthHeaders;
1522
1717
  exports.buildDispatcher = buildDispatcher;
1523
1718
  exports.buildDynamicVars = buildDynamicVars;
1524
1719
  exports.buildEnvVars = buildEnvVars;
1720
+ exports.buildProtocolFaultTests = buildProtocolFaultTests;
1525
1721
  exports.buildProxyUri = buildProxyUri;
1526
1722
  exports.buildSchemaTestResults = buildSchemaTestResults;
1527
1723
  exports.buildUrl = buildUrl;
1724
+ exports.deriveRunStatus = deriveRunStatus;
1528
1725
  exports.executeRunnerRequest = executeRunnerRequest;
1529
1726
  exports.getGlobals = getGlobals;
1530
1727
  exports.getSecret = getSecret;
@@ -1540,3 +1737,4 @@ exports.persistGlobals = persistGlobals;
1540
1737
  exports.registerSecretHandlers = registerSecretHandlers;
1541
1738
  exports.runScript = runScript;
1542
1739
  exports.setGlobals = setGlobals;
1740
+ exports.syntheticHttpFailure = syntheticHttpFailure;