@testsmith/api-spector 0.4.7 → 0.4.9
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.
- package/out/main/chunks/{handle-rimXXdJH.js → handle-CF2LjTGV.js} +4 -0
- package/out/main/chunks/{import-DcenB5Q_.js → import-ChCkdHOB.js} +2 -2
- package/out/main/chunks/{request-exec-BH-M3KqZ.js → request-exec-DeXgxps8.js} +544 -9
- package/out/main/chunks/{snapshots-CtYxkSLz.js → snapshots-8wLrfQcq.js} +23 -21
- package/out/main/chunks/{soap-handler-BXx842MN.js → soap-handler-pOrJ625E.js} +2 -2
- package/out/main/contract.js +73 -149
- package/out/main/index.js +89 -6
- package/out/main/lib.js +11 -2
- package/out/main/runner.js +9 -4
- package/out/main/wsdl.js +3 -3
- package/out/preload/index.js +6 -0
- package/out/renderer/assets/index-CFvibipL.css +2 -0
- package/out/renderer/assets/{index-B-OQJk1G.js → index-b63nUtxA.js} +255 -35
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/readme.md +20 -0
- package/out/renderer/assets/index-B_xyaTDo.css +0 -2
|
@@ -3,9 +3,10 @@ const promises = require("fs/promises");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const crypto = require("crypto");
|
|
5
5
|
const undici = require("undici");
|
|
6
|
-
const requestExec = require("./request-exec-
|
|
6
|
+
const requestExec = require("./request-exec-DeXgxps8.js");
|
|
7
7
|
const Ajv = require("ajv");
|
|
8
8
|
const jsYaml = require("js-yaml");
|
|
9
|
+
require("http");
|
|
9
10
|
const MATCH_KEY = "__match";
|
|
10
11
|
function isMatcher$1(node) {
|
|
11
12
|
return typeof node === "object" && node !== null && !Array.isArray(node) && typeof node[MATCH_KEY] === "string";
|
|
@@ -706,9 +707,23 @@ function validateBody(schema, bodyText) {
|
|
|
706
707
|
}
|
|
707
708
|
return violations;
|
|
708
709
|
}
|
|
709
|
-
async function executeContract(req, vars) {
|
|
710
|
-
const url = requestExec.buildUrl(req.url, req.params, vars);
|
|
710
|
+
async function executeContract(req, vars, providerBaseUrl) {
|
|
711
|
+
const url = requestExec.rebaseUrl(requestExec.buildUrl(req.url, req.params, vars), providerBaseUrl);
|
|
711
712
|
const start = Date.now();
|
|
713
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) {
|
|
714
|
+
return {
|
|
715
|
+
requestId: req.id,
|
|
716
|
+
requestName: req.name,
|
|
717
|
+
method: req.method,
|
|
718
|
+
url,
|
|
719
|
+
passed: false,
|
|
720
|
+
violations: [{
|
|
721
|
+
type: "status_mismatch",
|
|
722
|
+
message: `Request URL "${url}" has no host. Set a base URL to send it against, or set a {{baseUrl}} variable in the active environment.`
|
|
723
|
+
}],
|
|
724
|
+
durationMs: Date.now() - start
|
|
725
|
+
};
|
|
726
|
+
}
|
|
712
727
|
try {
|
|
713
728
|
const headers = new undici.Headers();
|
|
714
729
|
for (const h of req.headers) {
|
|
@@ -759,11 +774,11 @@ async function executeContract(req, vars) {
|
|
|
759
774
|
};
|
|
760
775
|
}
|
|
761
776
|
}
|
|
762
|
-
async function runConsumerContracts(requests, envVars, collectionVars = {}) {
|
|
777
|
+
async function runConsumerContracts(requests, envVars, collectionVars = {}, providerBaseUrl) {
|
|
763
778
|
const vars = { ...envVars, ...collectionVars };
|
|
764
779
|
const contractRequests = requests.filter((r) => !r.disabled && hasContract(r.contract));
|
|
765
780
|
const start = Date.now();
|
|
766
|
-
const results = await Promise.all(contractRequests.map((r) => executeContract(r, vars)));
|
|
781
|
+
const results = await Promise.all(contractRequests.map((r) => executeContract(r, vars, providerBaseUrl)));
|
|
767
782
|
const passed = results.filter((r) => r.passed).length;
|
|
768
783
|
return {
|
|
769
784
|
mode: "consumer",
|
|
@@ -937,19 +952,6 @@ async function runProviderVerification(requests, envVars, collectionVars = {}, s
|
|
|
937
952
|
durationMs: Date.now() - start
|
|
938
953
|
};
|
|
939
954
|
}
|
|
940
|
-
function rebaseUrl(fullUrl, providerBaseUrl) {
|
|
941
|
-
if (!providerBaseUrl) return fullUrl;
|
|
942
|
-
try {
|
|
943
|
-
const orig = new URL(fullUrl, "http://placeholder.invalid");
|
|
944
|
-
const base = new URL(providerBaseUrl);
|
|
945
|
-
const basePath = base.pathname.replace(/\/$/, "");
|
|
946
|
-
base.pathname = (basePath + orig.pathname).replace(/\/{2,}/g, "/");
|
|
947
|
-
base.search = orig.search;
|
|
948
|
-
return base.toString();
|
|
949
|
-
} catch {
|
|
950
|
-
return fullUrl;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
955
|
async function setupState(stateHandlerUrl, state, action) {
|
|
954
956
|
if (!stateHandlerUrl) {
|
|
955
957
|
return {
|
|
@@ -982,7 +984,7 @@ async function setupState(stateHandlerUrl, state, action) {
|
|
|
982
984
|
}
|
|
983
985
|
}
|
|
984
986
|
async function verifyInteraction(req, vars, providerBaseUrl, stateHandlerUrl) {
|
|
985
|
-
const url = rebaseUrl(requestExec.buildUrl(req.url, req.params, vars), providerBaseUrl);
|
|
987
|
+
const url = requestExec.rebaseUrl(requestExec.buildUrl(req.url, req.params, vars), providerBaseUrl);
|
|
986
988
|
const start = Date.now();
|
|
987
989
|
const violations = [];
|
|
988
990
|
const states = req.contract?.providerStates ?? [];
|
|
@@ -1788,7 +1790,7 @@ async function runFuzz(opts) {
|
|
|
1788
1790
|
params: c.params,
|
|
1789
1791
|
body: c.bodyJson !== void 0 ? { mode: "json", json: c.bodyJson } : req.body
|
|
1790
1792
|
};
|
|
1791
|
-
const resolvedUrl = rebaseUrl(requestExec.buildUrl(fuzzReq.url, fuzzReq.params, vars), opts.providerBaseUrl);
|
|
1793
|
+
const resolvedUrl = requestExec.rebaseUrl(requestExec.buildUrl(fuzzReq.url, fuzzReq.params, vars), opts.providerBaseUrl);
|
|
1792
1794
|
let sentSnapshot = { method: req.method, url: resolvedUrl, headers: {} };
|
|
1793
1795
|
try {
|
|
1794
1796
|
const ex = await requestExec.performHttpExchange({
|
|
@@ -1846,7 +1848,7 @@ async function runFuzz(opts) {
|
|
|
1846
1848
|
requestId: req.id,
|
|
1847
1849
|
requestName: req.name,
|
|
1848
1850
|
method: req.method,
|
|
1849
|
-
url: rebaseUrl(requestExec.buildUrl(req.url, req.params, vars), opts.providerBaseUrl),
|
|
1851
|
+
url: requestExec.rebaseUrl(requestExec.buildUrl(req.url, req.params, vars), opts.providerBaseUrl),
|
|
1850
1852
|
cases: cases.length,
|
|
1851
1853
|
findings,
|
|
1852
1854
|
trace
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const handle = require("./handle-
|
|
2
|
+
const handle = require("./handle-CF2LjTGV.js");
|
|
3
3
|
const https = require("https");
|
|
4
4
|
const http = require("http");
|
|
5
5
|
const xmldom = require("@xmldom/xmldom");
|
|
@@ -315,7 +315,7 @@ function registerSoapHandlers(ipc) {
|
|
|
315
315
|
handle.handleIpc(ipc, handle.IPC.wsdl.import, async (_event, opts) => {
|
|
316
316
|
const { validateWsdlImport } = await Promise.resolve().then(() => require("./ipc-validate-k6KI8adf.js"));
|
|
317
317
|
validateWsdlImport(opts);
|
|
318
|
-
const { importWsdl } = await Promise.resolve().then(() => require("./import-
|
|
318
|
+
const { importWsdl } = await Promise.resolve().then(() => require("./import-ChCkdHOB.js"));
|
|
319
319
|
const wsdlText = opts.xml ?? (opts.url ? await fetchUrl(opts.url) : "");
|
|
320
320
|
if (!wsdlText) throw new Error("wsdl:import requires either `url` or `xml`");
|
|
321
321
|
return importWsdl(wsdlText, { name: opts.name, existingMockPorts: opts.existingMockPorts });
|
package/out/main/contract.js
CHANGED
|
@@ -1,30 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
-
for (let key of __getOwnPropNames(from))
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
-
}
|
|
15
|
-
return to;
|
|
16
|
-
};
|
|
17
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
-
mod
|
|
24
|
-
));
|
|
25
3
|
const promises = require("fs/promises");
|
|
26
4
|
const path = require("path");
|
|
27
|
-
const snapshots = require("./chunks/snapshots-
|
|
5
|
+
const snapshots = require("./chunks/snapshots-8wLrfQcq.js");
|
|
28
6
|
const crypto = require("crypto");
|
|
29
7
|
const undici = require("undici");
|
|
30
8
|
const os = require("os");
|
|
@@ -32,9 +10,13 @@ const cliCommon = require("./chunks/cli-common-BKYgsbGB.js");
|
|
|
32
10
|
const child_process = require("child_process");
|
|
33
11
|
const jsYaml = require("js-yaml");
|
|
34
12
|
const environments = require("./chunks/environments-iM3SUM-4.js");
|
|
35
|
-
require("./chunks/request-exec-
|
|
13
|
+
require("./chunks/request-exec-DeXgxps8.js");
|
|
36
14
|
require("tls");
|
|
37
|
-
require("./chunks/handle-
|
|
15
|
+
require("./chunks/handle-CF2LjTGV.js");
|
|
16
|
+
require("node:fs");
|
|
17
|
+
require("node:os");
|
|
18
|
+
require("node:path");
|
|
19
|
+
require("node:crypto");
|
|
38
20
|
require("dayjs");
|
|
39
21
|
require("vm");
|
|
40
22
|
require("tv4");
|
|
@@ -151,71 +133,6 @@ async function fireWebhooks(hooks, payload, env = process.env, log = console.log
|
|
|
151
133
|
}
|
|
152
134
|
}));
|
|
153
135
|
}
|
|
154
|
-
function snapshotState(results, envs) {
|
|
155
|
-
const state = { results: {}, deployments: {} };
|
|
156
|
-
for (const r of results) state.results[`${r.pacticipant}@@${r.version}`] = r.recordedAt;
|
|
157
|
-
for (const e of envs) {
|
|
158
|
-
for (const [p, d] of Object.entries(e.deployed)) {
|
|
159
|
-
state.deployments[`${e.name}@@${p}`] = `${d.version}@@${d.recordedAt}`;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return state;
|
|
163
|
-
}
|
|
164
|
-
function diffState(prev, next, results) {
|
|
165
|
-
const events = [];
|
|
166
|
-
for (const [key, recordedAt] of Object.entries(next.results)) {
|
|
167
|
-
if (prev.results[key] === recordedAt) continue;
|
|
168
|
-
const [pacticipant, version] = key.split("@@");
|
|
169
|
-
const rec = results.find((r) => r.pacticipant === pacticipant && r.version === version);
|
|
170
|
-
events.push({
|
|
171
|
-
event: "result-recorded",
|
|
172
|
-
pacticipant,
|
|
173
|
-
version,
|
|
174
|
-
passed: rec?.passed,
|
|
175
|
-
recordedAt
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
for (const [key, value] of Object.entries(next.deployments)) {
|
|
179
|
-
if (prev.deployments[key] === value) continue;
|
|
180
|
-
const [environment, pacticipant] = key.split("@@");
|
|
181
|
-
const [version, recordedAt] = value.split("@@");
|
|
182
|
-
events.push({
|
|
183
|
-
event: "deployment-recorded",
|
|
184
|
-
pacticipant,
|
|
185
|
-
version,
|
|
186
|
-
environment,
|
|
187
|
-
recordedAt
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
return events;
|
|
191
|
-
}
|
|
192
|
-
function watchContractEvents(dir, hooks, intervalMs = 1e4, log = console.log) {
|
|
193
|
-
let prev = null;
|
|
194
|
-
let running = false;
|
|
195
|
-
const tick = async () => {
|
|
196
|
-
if (running) return;
|
|
197
|
-
running = true;
|
|
198
|
-
try {
|
|
199
|
-
const [results, envs] = await Promise.all([snapshots.listResults(dir), snapshots.listEnvironments(dir)]);
|
|
200
|
-
const next = snapshotState(results, envs);
|
|
201
|
-
if (prev !== null) {
|
|
202
|
-
for (const payload of diffState(prev, next, results)) {
|
|
203
|
-
await fireWebhooks(hooks, payload, process.env, log);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
prev = next;
|
|
207
|
-
} catch (e) {
|
|
208
|
-
log(` [webhook] scan failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
209
|
-
} finally {
|
|
210
|
-
running = false;
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
void tick();
|
|
214
|
-
const timer = setInterval(() => {
|
|
215
|
-
void tick();
|
|
216
|
-
}, intervalMs);
|
|
217
|
-
return () => clearInterval(timer);
|
|
218
|
-
}
|
|
219
136
|
function brokerConfigFromEnv() {
|
|
220
137
|
return {
|
|
221
138
|
endpoint: (process.env["API_SPECTOR_CLOUD_ENDPOINT"] || "https://api-spector.dev").replace(/\/+$/, ""),
|
|
@@ -288,6 +205,30 @@ async function recordDeployment(cfg, input) {
|
|
|
288
205
|
const r = await brokerFetch(cfg, path2, "POST", {});
|
|
289
206
|
if (!r.ok) fail(r, "record-deployment failed");
|
|
290
207
|
}
|
|
208
|
+
async function checkCompatibility(cfg, input) {
|
|
209
|
+
const q = new URLSearchParams({ consumer: input.consumer, consumerVersion: input.consumerVersion, provider: input.provider, providerVersion: input.providerVersion }).toString();
|
|
210
|
+
const r = await brokerFetch(cfg, "/api/compatibility?" + q, "GET");
|
|
211
|
+
if (r.status !== 200 && r.status !== 409) fail(r, "Compatibility check failed");
|
|
212
|
+
return { compatible: r.json?.compatible === true, checks: r.json?.checks ?? [] };
|
|
213
|
+
}
|
|
214
|
+
async function fetchContracts(cfg, input = {}) {
|
|
215
|
+
const q = new URLSearchParams();
|
|
216
|
+
if (input.consumer) q.set("consumer", input.consumer);
|
|
217
|
+
if (input.provider) q.set("provider", input.provider);
|
|
218
|
+
const suffix = q.toString() ? "?" + q.toString() : "";
|
|
219
|
+
const r = await brokerFetch(cfg, "/api/contracts" + suffix, "GET");
|
|
220
|
+
if (!r.ok) fail(r, "Fetch contracts failed");
|
|
221
|
+
return r.json?.contracts ?? [];
|
|
222
|
+
}
|
|
223
|
+
async function publishVerification(cfg, input) {
|
|
224
|
+
const r = await brokerFetch(cfg, "/api/verifications", "POST", {
|
|
225
|
+
contractId: input.contractId,
|
|
226
|
+
providerVersion: input.providerVersion,
|
|
227
|
+
success: input.success,
|
|
228
|
+
...input.buildUrl ? { buildUrl: input.buildUrl } : {}
|
|
229
|
+
});
|
|
230
|
+
if (!r.ok) fail(r, "Publish verification failed");
|
|
231
|
+
}
|
|
291
232
|
function resolveVersion(override) {
|
|
292
233
|
if (override) return override;
|
|
293
234
|
const env = process.env["GITHUB_SHA"] || process.env["CI_COMMIT_SHA"] || process.env["GIT_COMMIT"] || process.env["CIRCLE_SHA1"] || process.env["BUILD_SOURCEVERSION"] || process.env["BITBUCKET_COMMIT"];
|
|
@@ -467,7 +408,7 @@ async function cmdRun(args) {
|
|
|
467
408
|
const modeValue = mode;
|
|
468
409
|
switch (modeValue) {
|
|
469
410
|
case "consumer":
|
|
470
|
-
report = await snapshots.runConsumerContracts(contractRequests, envVars, collectionVars);
|
|
411
|
+
report = await snapshots.runConsumerContracts(contractRequests, envVars, collectionVars, providerBaseUrl);
|
|
471
412
|
break;
|
|
472
413
|
case "provider":
|
|
473
414
|
report = await snapshots.runProviderVerification(allRequests, envVars, collectionVars, specUrl, specPath, requestBaseUrl);
|
|
@@ -740,8 +681,8 @@ async function cmdWebhooks(args) {
|
|
|
740
681
|
console.log(" ]");
|
|
741
682
|
console.log(" }");
|
|
742
683
|
console.log("");
|
|
743
|
-
console.log(" $NAME tokens are replaced from the
|
|
744
|
-
console.log("
|
|
684
|
+
console.log(" $NAME tokens are replaced from the process environment.");
|
|
685
|
+
console.log(" Run `contract webhooks --test` to send a sample event to each configured URL.");
|
|
745
686
|
return;
|
|
746
687
|
}
|
|
747
688
|
console.log("");
|
|
@@ -769,60 +710,6 @@ async function cmdReport(args) {
|
|
|
769
710
|
process.exit(2);
|
|
770
711
|
}
|
|
771
712
|
const { dir } = await cliCommon.loadWorkspace(wsArg);
|
|
772
|
-
if (args["serve"]) {
|
|
773
|
-
const port = typeof args["port"] === "string" ? Number(args["port"]) : 8080;
|
|
774
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
775
|
-
console.error(" [error] --port must be a number between 1 and 65535");
|
|
776
|
-
process.exit(2);
|
|
777
|
-
}
|
|
778
|
-
const { createServer } = await import("http");
|
|
779
|
-
const server = createServer(async (req, res) => {
|
|
780
|
-
try {
|
|
781
|
-
const url = req.url ?? "/";
|
|
782
|
-
if (url === "/healthz") {
|
|
783
|
-
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
784
|
-
res.end("ok");
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
const records2 = await snapshots.listResults(dir);
|
|
788
|
-
const runMatch = /^\/run\/([^/]+)\/([^/]+)$/.exec(url);
|
|
789
|
-
if (runMatch) {
|
|
790
|
-
const pacticipant = decodeURIComponent(runMatch[1]);
|
|
791
|
-
const version = decodeURIComponent(runMatch[2]);
|
|
792
|
-
const rec = records2.find((r) => r.pacticipant === pacticipant && r.version === version);
|
|
793
|
-
if (!rec) {
|
|
794
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
795
|
-
res.end("No recorded result for that pacticipant/version");
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
799
|
-
res.end(snapshots.reportToHtml(rec.report, {
|
|
800
|
-
title: `${pacticipant} @ ${version}`,
|
|
801
|
-
generatedAt: rec.recordedAt
|
|
802
|
-
}));
|
|
803
|
-
return;
|
|
804
|
-
}
|
|
805
|
-
const environments22 = await snapshots.listEnvironments(dir);
|
|
806
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
807
|
-
res.end(snapshots.dashboardToHtml(records2, (/* @__PURE__ */ new Date()).toISOString(), { runLinkBase: "/run", environments: environments22 }));
|
|
808
|
-
} catch (e) {
|
|
809
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
810
|
-
res.end(e instanceof Error ? e.message : String(e));
|
|
811
|
-
}
|
|
812
|
-
});
|
|
813
|
-
server.listen(port, () => {
|
|
814
|
-
console.log(` Contract dashboard serving at http://localhost:${port}`);
|
|
815
|
-
console.log(` Workspace: ${wsArg} (results re-read on every request)`);
|
|
816
|
-
console.log(" Read-only: record new results via `contract run --record`, then refresh.");
|
|
817
|
-
});
|
|
818
|
-
const hooks = await loadWebhookConfig(dir);
|
|
819
|
-
if (hooks.length > 0) {
|
|
820
|
-
const intervalMs = typeof args["webhook-interval"] === "string" ? Math.max(2, Number(args["webhook-interval"])) * 1e3 : 1e4;
|
|
821
|
-
watchContractEvents(dir, hooks, intervalMs);
|
|
822
|
-
console.log(` Webhooks: ${hooks.length} configured (polling every ${intervalMs / 1e3}s)`);
|
|
823
|
-
}
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
713
|
const out = typeof args["html"] === "string" ? args["html"] : "contract-dashboard.html";
|
|
827
714
|
const records = await snapshots.listResults(dir);
|
|
828
715
|
const environments2 = await snapshots.listEnvironments(dir);
|
|
@@ -981,6 +868,40 @@ async function cmdCloudRecordDeployment(args) {
|
|
|
981
868
|
await recordDeployment(brokerConfigFromEnv(), { pacticipant, version, environment });
|
|
982
869
|
console.log(` Recorded ${pacticipant}@${version.slice(0, 7)} deployed to ${environment}`);
|
|
983
870
|
}
|
|
871
|
+
async function cmdCheck(args) {
|
|
872
|
+
const consumer = str(args["consumer"]) ?? die("--consumer <name> is required");
|
|
873
|
+
const consumerVersion = str(args["consumer-version"]) ?? die("--consumer-version <ver> is required");
|
|
874
|
+
const provider = str(args["provider"]) ?? die("--provider <name> is required");
|
|
875
|
+
const providerVersion = str(args["provider-version"]) ?? die("--provider-version <ver> is required");
|
|
876
|
+
const { compatible, checks } = await checkCompatibility(brokerConfigFromEnv(), { consumer, consumerVersion, provider, providerVersion });
|
|
877
|
+
if (compatible) {
|
|
878
|
+
console.log(` ✓ ${consumer}@${consumerVersion} is compatible with ${provider}@${providerVersion}`);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
console.error(` ✗ ${consumer}@${consumerVersion} is INCOMPATIBLE with ${provider}@${providerVersion}`);
|
|
882
|
+
for (const c of checks.filter((c2) => !c2.passed)) {
|
|
883
|
+
console.error(` ${c.interaction}`);
|
|
884
|
+
for (const m of c.mismatches ?? []) console.error(` ${m.location}: consumer requires ${m.consumer}, provider ${m.provider}`);
|
|
885
|
+
if ((!c.mismatches || !c.mismatches.length) && c.error) console.error(` ${c.error}`);
|
|
886
|
+
}
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
async function cmdPublishVerification(args) {
|
|
890
|
+
const consumer = str(args["consumer"]) ?? die("--consumer <name> is required");
|
|
891
|
+
const provider = str(args["provider"]) ?? die("--provider <name> is required");
|
|
892
|
+
const providerVersion = resolveVersion(str(args["provider-version"]) ?? str(args["version"]));
|
|
893
|
+
const successArg = str(args["success"]);
|
|
894
|
+
if (successArg === void 0) die("--success <true|false> is required");
|
|
895
|
+
const success = successArg === "true" || successArg === "1";
|
|
896
|
+
const buildUrl = str(args["build-url"]);
|
|
897
|
+
const consumerVersion = str(args["consumer-version"]);
|
|
898
|
+
const cfg = brokerConfigFromEnv();
|
|
899
|
+
const contracts = await fetchContracts(cfg, { consumer, provider });
|
|
900
|
+
const match = consumerVersion ? contracts.find((c) => c.consumerVersion === consumerVersion) : contracts[contracts.length - 1];
|
|
901
|
+
if (!match) die(`No published contract for ${consumer} -> ${provider}${consumerVersion ? "@" + consumerVersion : ""}.`);
|
|
902
|
+
await publishVerification(cfg, { contractId: match.id, providerVersion, success, buildUrl });
|
|
903
|
+
console.log(` Published verification: ${provider}@${providerVersion.slice(0, 7)} -> ${consumer}@${match.consumerVersion} = ${success ? "passed" : "FAILED"}`);
|
|
904
|
+
}
|
|
984
905
|
async function main() {
|
|
985
906
|
const [, , sub, ...rest] = process.argv;
|
|
986
907
|
const args = cliCommon.parseArgs(rest);
|
|
@@ -991,6 +912,8 @@ async function main() {
|
|
|
991
912
|
if (sub === "publish-spec") return wantsCloud(args) ? cmdPublishSpec(args) : cmdPublishSpecLocal(args);
|
|
992
913
|
if (sub === "preview") return cmdPreview(args);
|
|
993
914
|
if (sub === "deploy-check" || sub === "can-i-deploy") return wantsCloud(args) ? cmdCloudCanIDeploy(args) : cmdCanIDeploy(args);
|
|
915
|
+
if (sub === "check") return cmdCheck(args);
|
|
916
|
+
if (sub === "publish-verification") return cmdPublishVerification(args);
|
|
994
917
|
if (sub === "record-deployment") return wantsCloud(args) ? cmdCloudRecordDeployment(args) : cmdRecordDeployment(args);
|
|
995
918
|
if (sub === "environments") return cmdEnvironments(args);
|
|
996
919
|
if (sub === "webhooks") return cmdWebhooks(args);
|
|
@@ -1003,7 +926,7 @@ async function main() {
|
|
|
1003
926
|
api-spector contract list --workspace <path>
|
|
1004
927
|
api-spector contract pin --workspace <path> --spec-url <url> | --spec-path <file> [--name <label>]
|
|
1005
928
|
api-spector contract run --workspace <path> --mode <consumer|provider|provider-live|bidirectional> [options]
|
|
1006
|
-
api-spector contract report --workspace <path> [--html <path>]
|
|
929
|
+
api-spector contract report --workspace <path> [--html <path>]
|
|
1007
930
|
api-spector contract deploy-check --workspace <path> --pacticipant <name> --app-version <ver> [--to <env>]
|
|
1008
931
|
api-spector contract record-deployment --workspace <path> --pacticipant <name> --app-version <ver> --env <name>
|
|
1009
932
|
api-spector contract environments --workspace <path>
|
|
@@ -1033,7 +956,8 @@ async function main() {
|
|
|
1033
956
|
--collection <name> Filter to one collection
|
|
1034
957
|
--environment <name> Environment for {{var}} resolution
|
|
1035
958
|
--request-base-url <url> Strip this host before matching spec paths
|
|
1036
|
-
--provider-base-url <url> (provider-live) rebase requests onto
|
|
959
|
+
--provider-base-url <url> (provider-live, consumer) rebase requests onto
|
|
960
|
+
this origin (lets host-less design contracts run)
|
|
1037
961
|
--states-url <url> (provider-live) provider state handler endpoint
|
|
1038
962
|
--output <path> Write ContractReport JSON here
|
|
1039
963
|
--junit <path> Write JUnit XML here (for CI test reporters)
|
package/out/main/index.js
CHANGED
|
@@ -22,12 +22,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
mod
|
|
23
23
|
));
|
|
24
24
|
const electron = require("electron");
|
|
25
|
-
const handle = require("./chunks/handle-
|
|
25
|
+
const handle = require("./chunks/handle-CF2LjTGV.js");
|
|
26
26
|
const path = require("path");
|
|
27
27
|
const fs = require("fs");
|
|
28
28
|
const promises = require("fs/promises");
|
|
29
29
|
const crypto = require("crypto");
|
|
30
|
-
const requestExec = require("./chunks/request-exec-
|
|
30
|
+
const requestExec = require("./chunks/request-exec-DeXgxps8.js");
|
|
31
31
|
const ipcValidate = require("./chunks/ipc-validate-k6KI8adf.js");
|
|
32
32
|
const uuid = require("uuid");
|
|
33
33
|
const jsYaml = require("js-yaml");
|
|
@@ -37,12 +37,16 @@ const requestCollection = require("./chunks/request-collection-CJoXpvOw.js");
|
|
|
37
37
|
const mockServer = require("./chunks/mock-server-BxiXhP1m.js");
|
|
38
38
|
const http = require("http");
|
|
39
39
|
const WebSocket = require("ws");
|
|
40
|
-
const soapHandler = require("./chunks/soap-handler-
|
|
40
|
+
const soapHandler = require("./chunks/soap-handler-pOrJ625E.js");
|
|
41
41
|
const os = require("os");
|
|
42
|
-
const snapshots = require("./chunks/snapshots-
|
|
42
|
+
const snapshots = require("./chunks/snapshots-8wLrfQcq.js");
|
|
43
43
|
const simpleGit = require("simple-git");
|
|
44
44
|
const recorder = require("./chunks/recorder-0Ij921El.js");
|
|
45
45
|
require("tls");
|
|
46
|
+
require("node:fs");
|
|
47
|
+
require("node:os");
|
|
48
|
+
require("node:path");
|
|
49
|
+
require("node:crypto");
|
|
46
50
|
require("dayjs");
|
|
47
51
|
require("vm");
|
|
48
52
|
require("tv4");
|
|
@@ -266,6 +270,7 @@ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The
|
|
|
266
270
|
workspaceFile = wsPath;
|
|
267
271
|
await requestExec.loadGlobals(workspaceDir);
|
|
268
272
|
await saveLastWorkspacePath(wsPath);
|
|
273
|
+
requestExec.setSecretsConfig(parsed.settings?.secrets);
|
|
269
274
|
return { workspace: parsed, workspacePath: wsPath };
|
|
270
275
|
});
|
|
271
276
|
handle.handleIpc(ipc, handle.IPC.file.newWorkspace, async () => {
|
|
@@ -297,6 +302,7 @@ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The
|
|
|
297
302
|
});
|
|
298
303
|
handle.handleIpc(ipc, handle.IPC.file.saveWorkspace, async (_e, ws) => {
|
|
299
304
|
if (!workspaceFile) return;
|
|
305
|
+
requestExec.setSecretsConfig(ws.settings?.secrets);
|
|
300
306
|
await atomicWrite(workspaceFile, JSON.stringify(ws, null, 2));
|
|
301
307
|
if (workspaceDir) await ensureVscodeFileAssociation(workspaceDir);
|
|
302
308
|
});
|
|
@@ -397,6 +403,7 @@ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The
|
|
|
397
403
|
workspaceDir = path.dirname(wsPath);
|
|
398
404
|
workspaceFile = wsPath;
|
|
399
405
|
await requestExec.loadGlobals(workspaceDir);
|
|
406
|
+
requestExec.setSecretsConfig(workspace.settings?.secrets);
|
|
400
407
|
return { workspace, workspacePath: wsPath };
|
|
401
408
|
} catch {
|
|
402
409
|
return null;
|
|
@@ -427,6 +434,7 @@ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The
|
|
|
427
434
|
workspaceFile = wsPath;
|
|
428
435
|
await requestExec.loadGlobals(workspaceDir);
|
|
429
436
|
await saveLastWorkspacePath(wsPath);
|
|
437
|
+
requestExec.setSecretsConfig(parsed.settings?.secrets);
|
|
430
438
|
return { workspace: parsed, workspacePath: wsPath };
|
|
431
439
|
});
|
|
432
440
|
}
|
|
@@ -446,6 +454,7 @@ async function tryOpenWorkspaceInDir(dir) {
|
|
|
446
454
|
workspaceDir = dir;
|
|
447
455
|
workspaceFile = wsPath;
|
|
448
456
|
await requestExec.loadGlobals(workspaceDir);
|
|
457
|
+
requestExec.setSecretsConfig(workspace.settings?.secrets);
|
|
449
458
|
return { workspace, workspacePath: wsPath };
|
|
450
459
|
} catch {
|
|
451
460
|
return null;
|
|
@@ -4637,6 +4646,79 @@ function registerOAuth2Handlers(ipc) {
|
|
|
4637
4646
|
};
|
|
4638
4647
|
});
|
|
4639
4648
|
}
|
|
4649
|
+
const CALLBACK_PORT = 8250;
|
|
4650
|
+
const CALLBACK_PATH = "/oidc/callback";
|
|
4651
|
+
function registerVaultHandlers(ipc) {
|
|
4652
|
+
handle.handleIpc(ipc, handle.IPC.vault.oidcLogin, async (_e, opts) => {
|
|
4653
|
+
const address = (opts.address ?? "").replace(/\/+$/, "");
|
|
4654
|
+
if (!address) throw new Error("Vault OIDC: address is required.");
|
|
4655
|
+
const mount = opts.mount?.trim() || "oidc";
|
|
4656
|
+
const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
4657
|
+
const clientNonce = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
4658
|
+
const { fetch: nodeFetch, Agent } = await import("undici");
|
|
4659
|
+
const dispatcher = opts.skipVerify ? new Agent({ connect: { rejectUnauthorized: false } }) : void 0;
|
|
4660
|
+
const nsHeaders = opts.namespace ? { "X-Vault-Namespace": opts.namespace } : {};
|
|
4661
|
+
const vault = async (path2, init = {}) => {
|
|
4662
|
+
const res = await nodeFetch(`${address}/v1/${path2}`, {
|
|
4663
|
+
...init,
|
|
4664
|
+
headers: { ...nsHeaders, ...init.headers },
|
|
4665
|
+
...dispatcher ? { dispatcher } : {}
|
|
4666
|
+
});
|
|
4667
|
+
if (!res.ok) {
|
|
4668
|
+
const body = await res.text().catch(() => "");
|
|
4669
|
+
throw new Error(`Vault ${res.status} on /v1/${path2}: ${body.slice(0, 300)}`);
|
|
4670
|
+
}
|
|
4671
|
+
return res.json();
|
|
4672
|
+
};
|
|
4673
|
+
const authUrlResp = await vault(`auth/${mount}/oidc/auth_url`, {
|
|
4674
|
+
method: "POST",
|
|
4675
|
+
headers: { "content-type": "application/json" },
|
|
4676
|
+
body: JSON.stringify({ redirect_uri: redirectUri, role: opts.role || void 0, client_nonce: clientNonce })
|
|
4677
|
+
});
|
|
4678
|
+
const authUrl = authUrlResp?.data?.auth_url;
|
|
4679
|
+
if (!authUrl) throw new Error("Vault OIDC: no auth_url returned (check the mount, role, and allowed redirect URIs).");
|
|
4680
|
+
const params = await new Promise((resolve2, reject) => {
|
|
4681
|
+
const server = http.createServer((req, res) => {
|
|
4682
|
+
const reqUrl = new URL(req.url ?? "/", `http://localhost:${CALLBACK_PORT}`);
|
|
4683
|
+
if (!reqUrl.pathname.startsWith(CALLBACK_PATH)) {
|
|
4684
|
+
res.writeHead(404);
|
|
4685
|
+
res.end();
|
|
4686
|
+
return;
|
|
4687
|
+
}
|
|
4688
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4689
|
+
res.end("<html><body><p>Vault sign-in complete. You may close this tab.</p></body></html>");
|
|
4690
|
+
server.close();
|
|
4691
|
+
const error = reqUrl.searchParams.get("error");
|
|
4692
|
+
if (error) {
|
|
4693
|
+
reject(new Error(`Vault OIDC error: ${error}`));
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4696
|
+
resolve2(reqUrl.searchParams);
|
|
4697
|
+
});
|
|
4698
|
+
server.on("error", reject);
|
|
4699
|
+
server.listen(CALLBACK_PORT, "127.0.0.1", () => {
|
|
4700
|
+
electron.shell.openExternal(authUrl).catch(reject);
|
|
4701
|
+
});
|
|
4702
|
+
setTimeout(() => {
|
|
4703
|
+
server.close();
|
|
4704
|
+
reject(new Error("Vault OIDC sign-in timed out (5 min)."));
|
|
4705
|
+
}, 5 * 60 * 1e3);
|
|
4706
|
+
});
|
|
4707
|
+
const code = params.get("code");
|
|
4708
|
+
const state = params.get("state");
|
|
4709
|
+
if (!code || !state) throw new Error("Vault OIDC: callback missing code/state.");
|
|
4710
|
+
const cb = new URLSearchParams({ state, code, client_nonce: clientNonce });
|
|
4711
|
+
const login = await vault(`auth/${mount}/oidc/callback?${cb.toString()}`);
|
|
4712
|
+
const token = login?.auth?.client_token;
|
|
4713
|
+
if (!token) throw new Error("Vault OIDC: callback did not return a client token.");
|
|
4714
|
+
process.env.VAULT_TOKEN = token;
|
|
4715
|
+
return {
|
|
4716
|
+
ok: true,
|
|
4717
|
+
expiresInSeconds: Number(login?.auth?.lease_duration ?? 0),
|
|
4718
|
+
entityId: login?.auth?.entity_id ? String(login.auth.entity_id) : void 0
|
|
4719
|
+
};
|
|
4720
|
+
});
|
|
4721
|
+
}
|
|
4640
4722
|
const connections = /* @__PURE__ */ new Map();
|
|
4641
4723
|
function closeAllWsConnections() {
|
|
4642
4724
|
for (const [, ws] of connections) {
|
|
@@ -5051,7 +5133,7 @@ function registerContractHandlers(ipc) {
|
|
|
5051
5133
|
}
|
|
5052
5134
|
switch (mode) {
|
|
5053
5135
|
case "consumer":
|
|
5054
|
-
return snapshots.runConsumerContracts(requests, envVars, collectionVars);
|
|
5136
|
+
return snapshots.runConsumerContracts(requests, envVars, collectionVars, providerBaseUrl);
|
|
5055
5137
|
case "provider":
|
|
5056
5138
|
return snapshots.runProviderVerification(requests, envVars, collectionVars, specUrl, specPath, requestBaseUrl);
|
|
5057
5139
|
case "provider-live":
|
|
@@ -5520,7 +5602,7 @@ function isNewer(a, b) {
|
|
|
5520
5602
|
return a2 > b2;
|
|
5521
5603
|
}
|
|
5522
5604
|
async function checkForUpdate() {
|
|
5523
|
-
const current = "0.4.
|
|
5605
|
+
const current = "0.4.9";
|
|
5524
5606
|
try {
|
|
5525
5607
|
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(4e3) });
|
|
5526
5608
|
if (!res.ok) return null;
|
|
@@ -5634,6 +5716,7 @@ electron.app.whenReady().then(async () => {
|
|
|
5634
5716
|
registerRunnerHandler(electron.ipcMain);
|
|
5635
5717
|
registerMockHandlers(electron.ipcMain);
|
|
5636
5718
|
registerOAuth2Handlers(electron.ipcMain);
|
|
5719
|
+
registerVaultHandlers(electron.ipcMain);
|
|
5637
5720
|
registerWsHandlers(electron.ipcMain);
|
|
5638
5721
|
soapHandler.registerSoapHandlers(electron.ipcMain);
|
|
5639
5722
|
registerDocsHandlers(electron.ipcMain);
|
package/out/main/lib.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const mockServer = require("./chunks/mock-server-BxiXhP1m.js");
|
|
4
|
-
const requestExec = require("./chunks/request-exec-
|
|
4
|
+
const requestExec = require("./chunks/request-exec-DeXgxps8.js");
|
|
5
5
|
require("http");
|
|
6
6
|
require("crypto");
|
|
7
7
|
require("vm");
|
|
@@ -10,7 +10,11 @@ require("@xmldom/xmldom");
|
|
|
10
10
|
require("undici");
|
|
11
11
|
require("fs/promises");
|
|
12
12
|
require("tls");
|
|
13
|
-
require("./chunks/handle-
|
|
13
|
+
require("./chunks/handle-CF2LjTGV.js");
|
|
14
|
+
require("node:fs");
|
|
15
|
+
require("node:os");
|
|
16
|
+
require("node:path");
|
|
17
|
+
require("node:crypto");
|
|
14
18
|
require("path");
|
|
15
19
|
require("tv4");
|
|
16
20
|
require("jsonpath-plus");
|
|
@@ -31,7 +35,12 @@ exports.buildProtocolFaultTests = requestExec.buildProtocolFaultTests;
|
|
|
31
35
|
exports.buildSchemaTestResults = requestExec.buildSchemaTestResults;
|
|
32
36
|
exports.deriveRunStatus = requestExec.deriveRunStatus;
|
|
33
37
|
exports.executeRunnerRequest = requestExec.executeRunnerRequest;
|
|
38
|
+
exports.hasSecretScheme = requestExec.hasSecretScheme;
|
|
34
39
|
exports.maskHeaders = requestExec.maskHeaders;
|
|
35
40
|
exports.maskPii = requestExec.maskPii;
|
|
36
41
|
exports.performHttpExchange = requestExec.performHttpExchange;
|
|
42
|
+
exports.registerSecretProvider = requestExec.registerSecretProvider;
|
|
43
|
+
exports.registeredSchemes = requestExec.registeredSchemes;
|
|
44
|
+
exports.resolveExternalSecret = requestExec.resolveExternalSecret;
|
|
45
|
+
exports.setSecretsConfig = requestExec.setSecretsConfig;
|
|
37
46
|
exports.syntheticHttpFailure = requestExec.syntheticHttpFailure;
|
package/out/main/runner.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
const promises = require("fs/promises");
|
|
4
4
|
const path = require("path");
|
|
5
|
-
const requestExec = require("./chunks/request-exec-
|
|
5
|
+
const requestExec = require("./chunks/request-exec-DeXgxps8.js");
|
|
6
6
|
const requestCollection = require("./chunks/request-collection-CJoXpvOw.js");
|
|
7
7
|
const environments = require("./chunks/environments-iM3SUM-4.js");
|
|
8
8
|
const cliCommon = require("./chunks/cli-common-BKYgsbGB.js");
|
|
9
9
|
require("undici");
|
|
10
10
|
require("tls");
|
|
11
|
-
require("./chunks/handle-
|
|
11
|
+
require("./chunks/handle-CF2LjTGV.js");
|
|
12
|
+
require("node:fs");
|
|
13
|
+
require("node:os");
|
|
14
|
+
require("node:path");
|
|
15
|
+
require("node:crypto");
|
|
12
16
|
require("crypto");
|
|
13
17
|
require("dayjs");
|
|
14
18
|
require("vm");
|
|
@@ -387,6 +391,7 @@ async function main() {
|
|
|
387
391
|
process.exit(1);
|
|
388
392
|
}
|
|
389
393
|
await requestExec.loadGlobals(wsDir);
|
|
394
|
+
requestExec.setSecretsConfig(workspace.settings?.secrets);
|
|
390
395
|
const collections = await cliCommon.loadCollections(workspace, wsDir, {
|
|
391
396
|
onError: (relPath) => console.error(cliCommon.color(` [warn] Could not load collection: ${relPath}`, cliCommon.C.yellow))
|
|
392
397
|
});
|
|
@@ -397,7 +402,7 @@ async function main() {
|
|
|
397
402
|
} else if (!envName && workspace.settings?.defaultEnvironment && !env) {
|
|
398
403
|
console.warn(cliCommon.color(`Warning: default environment "${workspace.settings.defaultEnvironment}" not found. Running without environment.`, cliCommon.C.yellow));
|
|
399
404
|
}
|
|
400
|
-
const version = `v${"0.4.
|
|
405
|
+
const version = `v${"0.4.9"}`;
|
|
401
406
|
console.log("");
|
|
402
407
|
console.log(cliCommon.color(" API Test Runner" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
|
|
403
408
|
console.log(cliCommon.color(` Workspace: ${wsPath}`, cliCommon.C.gray));
|
|
@@ -405,7 +410,7 @@ async function main() {
|
|
|
405
410
|
if (filterTags.length) console.log(cliCommon.color(` Tags: ${filterTags.join(", ")}`, cliCommon.C.gray));
|
|
406
411
|
console.log("");
|
|
407
412
|
const envVarsSnapshot = await requestExec.buildEnvVars(env);
|
|
408
|
-
const secretValuesToMask = (env?.variables ?? []).filter((v) => v.secret && v.enabled).map((v) => envVarsSnapshot[v.key]).filter((v) => typeof v === "string" && v.length > 0);
|
|
413
|
+
const secretValuesToMask = (env?.variables ?? []).filter((v) => (v.secret || v.secretRef) && v.enabled).map((v) => envVarsSnapshot[v.key]).filter((v) => typeof v === "string" && v.length > 0);
|
|
409
414
|
function redact(s) {
|
|
410
415
|
let out = s;
|
|
411
416
|
for (const secret of secretValuesToMask) out = out.split(secret).join("***");
|