@testsmith/api-spector 0.6.0 → 0.6.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.
- package/docs/ai/api-spector-scripting-reference.md +149 -0
- package/docs/ai/collection-file-format.md +110 -0
- package/docs/ai/functional-testing-guide.md +145 -0
- package/docs/ai/security-testing-guide.md +253 -0
- package/out/main/agents.js +2 -1
- package/out/main/chunks/{snapshots-Y33M1SEy.js → broker-client-WEymdqAK.js} +119 -10
- package/out/main/chunks/{cli-common-BKYgsbGB.js → cli-common-vASoDfQH.js} +3 -2
- package/out/main/chunks/data-files-Br2h3Dt2.js +85 -0
- package/out/main/chunks/escape-BwPM4Tf6.js +9 -0
- package/out/main/chunks/{handle-BFaeFA0A.js → handle-CL-nWtYA.js} +11 -2
- package/out/main/chunks/{import-D9LH0isB.js → import-DdXhO56X.js} +2 -2
- package/out/main/chunks/{request-collection-BQIo1765.js → request-collection-Hqh3I3rs.js} +75 -15
- package/out/main/chunks/{request-exec-5_3279w1.js → request-exec-DAFA6Fap.js} +19 -1
- package/out/main/chunks/{soap-handler-CWn3o6Py.js → soap-handler-BAPCALkN.js} +2 -2
- package/out/main/compare.js +2 -1
- package/out/main/contract.js +50 -147
- package/out/main/coverage.js +2 -1
- package/out/main/generate-tests.js +2 -1
- package/out/main/index.js +131 -36
- package/out/main/lib.js +2 -2
- package/out/main/mock.js +2 -1
- package/out/main/record.js +2 -1
- package/out/main/runner.js +95 -13
- package/out/main/wsdl.js +5 -4
- package/out/preload/index.js +21 -3
- package/out/renderer/assets/{GraphQLEditor-DITvPhtG.js → GraphQLEditor-DtppmZJO.js} +3 -1
- package/out/renderer/assets/{SoapEditor-Bf-oE-rS.js → SoapEditor-BwSm12wH.js} +4 -2
- package/out/renderer/assets/ajv-2MAg6kGF.js +6153 -0
- package/out/renderer/assets/codemirror-F6m3Hm1M.js +26552 -0
- package/out/renderer/assets/index-B3mtLdU3.css +2 -0
- package/out/renderer/assets/{index-MMIHKSKx.js → index-D6-zmXen.js} +17493 -49301
- package/out/renderer/index.html +4 -2
- package/package.json +4 -2
- package/src/cli/agent-templates/aider/conventions.md +10 -0
- package/src/cli/agent-templates/claude/skills/api-spector-api-audit.md +135 -0
- package/src/cli/agent-templates/claude/skills/api-spector-functional-tests.md +73 -0
- package/src/cli/agent-templates/claude/skills/api-spector-generate-mocks.md +182 -0
- package/src/cli/agent-templates/claude/skills/api-spector-security-tests.md +73 -0
- package/src/cli/agent-templates/copilot/copilot-instructions.md +38 -0
- package/src/cli/agent-templates/cursor/rules/api-spector.mdc +21 -0
- package/src/cli/agent-templates/windsurf/windsurfrules +19 -0
- package/out/renderer/assets/index-CkHujZU3.css +0 -2
|
@@ -3,10 +3,11 @@ 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-DAFA6Fap.js");
|
|
7
7
|
const Ajv = require("ajv");
|
|
8
8
|
const jsYaml = require("js-yaml");
|
|
9
9
|
require("http");
|
|
10
|
+
const escape = require("./escape-BwPM4Tf6.js");
|
|
10
11
|
const MATCH_KEY = "__match";
|
|
11
12
|
function isMatcher$1(node) {
|
|
12
13
|
return typeof node === "object" && node !== null && !Array.isArray(node) && typeof node[MATCH_KEY] === "string";
|
|
@@ -873,7 +874,7 @@ function findOperation(spec, method, reqUrl, requestBaseUrl) {
|
|
|
873
874
|
function validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl) {
|
|
874
875
|
const violations = [];
|
|
875
876
|
const vars = envVars;
|
|
876
|
-
const url = req.url
|
|
877
|
+
const url = requestExec.interpolate(req.url, vars);
|
|
877
878
|
const match = findOperation(spec, req.method, url, requestBaseUrl);
|
|
878
879
|
if (!match) {
|
|
879
880
|
violations.push({
|
|
@@ -932,7 +933,7 @@ async function runProviderVerification(requests, envVars, collectionVars = {}, s
|
|
|
932
933
|
const activeRequests = requests.filter((r) => !r.disabled);
|
|
933
934
|
const results = activeRequests.map((req) => {
|
|
934
935
|
const violations = validateRequestAgainstSpec(spec, req, vars, requestBaseUrl);
|
|
935
|
-
const url = req.url
|
|
936
|
+
const url = requestExec.interpolate(req.url, vars);
|
|
936
937
|
return {
|
|
937
938
|
requestId: req.id,
|
|
938
939
|
requestName: req.name,
|
|
@@ -1121,7 +1122,7 @@ function checkSchemaCompatibility(consumerSchema, providerSchema, path2 = "") {
|
|
|
1121
1122
|
return violations;
|
|
1122
1123
|
}
|
|
1123
1124
|
function getProviderResponseSchema(spec, req, envVars, statusCode, requestBaseUrl) {
|
|
1124
|
-
const url = req.url
|
|
1125
|
+
const url = requestExec.interpolate(req.url, envVars);
|
|
1125
1126
|
const match = findOperation(spec, req.method, url, requestBaseUrl);
|
|
1126
1127
|
if (!match) return null;
|
|
1127
1128
|
const responses = match.operation["responses"] ?? {};
|
|
@@ -1280,7 +1281,7 @@ async function listResults(dir) {
|
|
|
1280
1281
|
}
|
|
1281
1282
|
return out;
|
|
1282
1283
|
}
|
|
1283
|
-
async function canIDeploy(dir, pacticipant, version, env) {
|
|
1284
|
+
async function canIDeploy$1(dir, pacticipant, version, env) {
|
|
1284
1285
|
const file = resultPath(dir, pacticipant, version);
|
|
1285
1286
|
let currentlyDeployed;
|
|
1286
1287
|
if (env) {
|
|
@@ -1323,7 +1324,7 @@ async function loadEnvironment(dir, env) {
|
|
|
1323
1324
|
return null;
|
|
1324
1325
|
}
|
|
1325
1326
|
}
|
|
1326
|
-
async function recordDeployment(dir, env, pacticipant, version, now) {
|
|
1327
|
+
async function recordDeployment$1(dir, env, pacticipant, version, now) {
|
|
1327
1328
|
const state = await loadEnvironment(dir, env) ?? { name: env, deployed: {} };
|
|
1328
1329
|
const previous = state.deployed[pacticipant];
|
|
1329
1330
|
state.deployed[pacticipant] = { version, recordedAt: now };
|
|
@@ -1756,7 +1757,7 @@ const ajv = new Ajv({ allErrors: true, strict: false });
|
|
|
1756
1757
|
const WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1757
1758
|
function specContextFor(spec, req, vars, requestBaseUrl) {
|
|
1758
1759
|
if (!spec) return null;
|
|
1759
|
-
const url = req.url
|
|
1760
|
+
const url = requestExec.interpolate(req.url, vars);
|
|
1760
1761
|
const match = findOperation(spec, req.method, url, requestBaseUrl);
|
|
1761
1762
|
if (!match) return null;
|
|
1762
1763
|
const op = match.operation;
|
|
@@ -1982,9 +1983,7 @@ async function runFuzz(opts) {
|
|
|
1982
1983
|
skippedNoBody
|
|
1983
1984
|
};
|
|
1984
1985
|
}
|
|
1985
|
-
|
|
1986
|
-
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1987
|
-
}
|
|
1986
|
+
const esc = escape.escapeHtml;
|
|
1988
1987
|
function modeLabel(mode) {
|
|
1989
1988
|
if (mode === "bidirectional") return "Bi-directional";
|
|
1990
1989
|
if (mode === "provider-live") return "Provider (live)";
|
|
@@ -2299,12 +2298,117 @@ async function deleteSnapshot(workspaceDir, relPath) {
|
|
|
2299
2298
|
} catch {
|
|
2300
2299
|
}
|
|
2301
2300
|
}
|
|
2301
|
+
const DEFAULT_CLOUD_ENDPOINT = "https://api-spector.dev";
|
|
2302
|
+
function resolveCloudEndpoint() {
|
|
2303
|
+
return (process.env["API_SPECTOR_CLOUD_ENDPOINT"] || DEFAULT_CLOUD_ENDPOINT).replace(/\/+$/, "");
|
|
2304
|
+
}
|
|
2305
|
+
function brokerConfigFromEnv() {
|
|
2306
|
+
return {
|
|
2307
|
+
endpoint: resolveCloudEndpoint(),
|
|
2308
|
+
token: process.env["API_SPECTOR_TOKEN"] || ""
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
async function brokerFetch(cfg, path2, method, body) {
|
|
2312
|
+
if (!cfg.token) throw new Error("No API token. Set API_SPECTOR_TOKEN (create one in the cloud dashboard under Tokens).");
|
|
2313
|
+
const url = cfg.endpoint + path2;
|
|
2314
|
+
let res;
|
|
2315
|
+
try {
|
|
2316
|
+
res = await undici.fetch(url, {
|
|
2317
|
+
method,
|
|
2318
|
+
headers: {
|
|
2319
|
+
"Content-Type": "application/json",
|
|
2320
|
+
Accept: "application/json",
|
|
2321
|
+
Authorization: `Bearer ${cfg.token}`
|
|
2322
|
+
},
|
|
2323
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
2324
|
+
});
|
|
2325
|
+
} catch (err) {
|
|
2326
|
+
throw new Error(`Could not reach ${url}: ${err.message}`);
|
|
2327
|
+
}
|
|
2328
|
+
const text = await res.text();
|
|
2329
|
+
let json;
|
|
2330
|
+
try {
|
|
2331
|
+
json = text ? JSON.parse(text) : {};
|
|
2332
|
+
} catch {
|
|
2333
|
+
json = { raw: text };
|
|
2334
|
+
}
|
|
2335
|
+
return { status: res.status, ok: res.ok, json };
|
|
2336
|
+
}
|
|
2337
|
+
function fail(r, what) {
|
|
2338
|
+
const detail = r.json?.error || r.json?.message || (r.status === 401 ? "Unauthorized (check API_SPECTOR_TOKEN)" : `HTTP ${r.status}`);
|
|
2339
|
+
throw new Error(`${what}: ${detail}`);
|
|
2340
|
+
}
|
|
2341
|
+
async function publishPact(cfg, input) {
|
|
2342
|
+
const r = await brokerFetch(cfg, "/api/contracts", "PUT", {
|
|
2343
|
+
consumer: input.consumer,
|
|
2344
|
+
consumerVersion: input.consumerVersion,
|
|
2345
|
+
provider: input.provider,
|
|
2346
|
+
content: input.pact
|
|
2347
|
+
});
|
|
2348
|
+
if (!r.ok) fail(r, "Publish pact failed");
|
|
2349
|
+
}
|
|
2350
|
+
async function publishSpec(cfg, input) {
|
|
2351
|
+
const r = await brokerFetch(cfg, "/api/provider-contracts", "PUT", {
|
|
2352
|
+
pacticipant: input.pacticipant,
|
|
2353
|
+
version: input.version,
|
|
2354
|
+
spec: input.spec,
|
|
2355
|
+
// Optional provider self-verification (its own tests run against this spec).
|
|
2356
|
+
...input.results ? { results: input.results } : {}
|
|
2357
|
+
});
|
|
2358
|
+
if (!r.ok) fail(r, "Publish spec failed");
|
|
2359
|
+
}
|
|
2360
|
+
async function canIDeploy(cfg, input) {
|
|
2361
|
+
const q = new URLSearchParams({ pacticipant: input.pacticipant, version: input.version, environment: input.environment }).toString();
|
|
2362
|
+
const r = await brokerFetch(cfg, "/can-i-deploy?" + q, "GET");
|
|
2363
|
+
if (r.status !== 200 && r.status !== 409) fail(r, "can-i-deploy failed");
|
|
2364
|
+
return { deployable: r.status === 200, reason: r.json?.summary?.reason || "" };
|
|
2365
|
+
}
|
|
2366
|
+
async function deployPreview(cfg, input) {
|
|
2367
|
+
const q = new URLSearchParams({ pacticipant: input.pacticipant, version: input.version, environment: input.environment }).toString();
|
|
2368
|
+
const r = await brokerFetch(cfg, "/api/deploy-preview?" + q, "GET");
|
|
2369
|
+
if (!r.ok) fail(r, "deploy-preview failed");
|
|
2370
|
+
return r.json;
|
|
2371
|
+
}
|
|
2372
|
+
async function recordDeployment(cfg, input) {
|
|
2373
|
+
const path2 = `/pacticipants/${encodeURIComponent(input.pacticipant)}/versions/${encodeURIComponent(input.version)}/deployed-versions/environment/${encodeURIComponent(input.environment)}`;
|
|
2374
|
+
const r = await brokerFetch(cfg, path2, "POST", {});
|
|
2375
|
+
if (!r.ok) fail(r, "record-deployment failed");
|
|
2376
|
+
}
|
|
2377
|
+
async function checkCompatibility(cfg, input) {
|
|
2378
|
+
const q = new URLSearchParams({ consumer: input.consumer, consumerVersion: input.consumerVersion, provider: input.provider, providerVersion: input.providerVersion }).toString();
|
|
2379
|
+
const r = await brokerFetch(cfg, "/api/compatibility?" + q, "GET");
|
|
2380
|
+
if (r.status !== 200 && r.status !== 409) fail(r, "Compatibility check failed");
|
|
2381
|
+
return { compatible: r.json?.compatible === true, checks: r.json?.checks ?? [] };
|
|
2382
|
+
}
|
|
2383
|
+
async function fetchContracts(cfg, input = {}) {
|
|
2384
|
+
const q = new URLSearchParams();
|
|
2385
|
+
if (input.consumer) q.set("consumer", input.consumer);
|
|
2386
|
+
if (input.provider) q.set("provider", input.provider);
|
|
2387
|
+
const suffix = q.toString() ? "?" + q.toString() : "";
|
|
2388
|
+
const r = await brokerFetch(cfg, "/api/contracts" + suffix, "GET");
|
|
2389
|
+
if (!r.ok) fail(r, "Fetch contracts failed");
|
|
2390
|
+
return r.json?.contracts ?? [];
|
|
2391
|
+
}
|
|
2392
|
+
async function publishVerification(cfg, input) {
|
|
2393
|
+
const r = await brokerFetch(cfg, "/api/verifications", "POST", {
|
|
2394
|
+
contractId: input.contractId,
|
|
2395
|
+
providerVersion: input.providerVersion,
|
|
2396
|
+
success: input.success,
|
|
2397
|
+
...input.buildUrl ? { buildUrl: input.buildUrl } : {}
|
|
2398
|
+
});
|
|
2399
|
+
if (!r.ok) fail(r, "Publish verification failed");
|
|
2400
|
+
}
|
|
2401
|
+
exports.brokerConfigFromEnv = brokerConfigFromEnv;
|
|
2302
2402
|
exports.canIDeploy = canIDeploy;
|
|
2403
|
+
exports.canIDeploy$1 = canIDeploy$1;
|
|
2303
2404
|
exports.captureSnapshot = captureSnapshot;
|
|
2405
|
+
exports.checkCompatibility = checkCompatibility;
|
|
2304
2406
|
exports.dashboardToHtml = dashboardToHtml;
|
|
2305
2407
|
exports.deleteSnapshot = deleteSnapshot;
|
|
2408
|
+
exports.deployPreview = deployPreview;
|
|
2306
2409
|
exports.designContractToPact = designContractToPact;
|
|
2307
2410
|
exports.exportPact = exportPact;
|
|
2411
|
+
exports.fetchContracts = fetchContracts;
|
|
2308
2412
|
exports.fuzzReportToHtml = fuzzReportToHtml;
|
|
2309
2413
|
exports.hasContract = hasContract;
|
|
2310
2414
|
exports.importPact = importPact;
|
|
@@ -2314,10 +2418,15 @@ exports.listSnapshots = listSnapshots;
|
|
|
2314
2418
|
exports.loadDesignContractRequests = loadDesignContractRequests;
|
|
2315
2419
|
exports.loadSnapshot = loadSnapshot;
|
|
2316
2420
|
exports.pactToCollection = pactToCollection;
|
|
2421
|
+
exports.publishPact = publishPact;
|
|
2422
|
+
exports.publishSpec = publishSpec;
|
|
2423
|
+
exports.publishVerification = publishVerification;
|
|
2317
2424
|
exports.recordDeployment = recordDeployment;
|
|
2425
|
+
exports.recordDeployment$1 = recordDeployment$1;
|
|
2318
2426
|
exports.recordResult = recordResult;
|
|
2319
2427
|
exports.relPathOf = relPathOf;
|
|
2320
2428
|
exports.reportToHtml = reportToHtml;
|
|
2429
|
+
exports.resolveCloudEndpoint = resolveCloudEndpoint;
|
|
2321
2430
|
exports.runBidirectional = runBidirectional;
|
|
2322
2431
|
exports.runConsumerContracts = runConsumerContracts;
|
|
2323
2432
|
exports.runFuzz = runFuzz;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const promises = require("fs/promises");
|
|
3
3
|
const path = require("path");
|
|
4
|
+
const dataFiles = require("./data-files-Br2h3Dt2.js");
|
|
4
5
|
require("crypto");
|
|
5
6
|
const C = {
|
|
6
7
|
reset: "\x1B[0m",
|
|
@@ -57,8 +58,8 @@ async function loadCollections(workspace, dir, opts = {}) {
|
|
|
57
58
|
const cols = [];
|
|
58
59
|
for (const relPath of workspace.collections) {
|
|
59
60
|
try {
|
|
60
|
-
const
|
|
61
|
-
const col = JSON.parse(
|
|
61
|
+
const fullPath = path.join(dir, relPath);
|
|
62
|
+
const col = await dataFiles.inlineDataSets(JSON.parse(await promises.readFile(fullPath, "utf8")), path.dirname(fullPath));
|
|
62
63
|
if (!opts.filterName || col.name === opts.filterName) cols.push(col);
|
|
63
64
|
} catch {
|
|
64
65
|
opts.onError?.(relPath);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const promises = require("fs/promises");
|
|
4
|
+
function toCSV(ds) {
|
|
5
|
+
const escape = (s) => s.includes(",") || s.includes('"') || s.includes("\n") ? `"${s.replace(/"/g, '""')}"` : s;
|
|
6
|
+
return [ds.columns, ...ds.rows].map((row) => row.map(escape).join(",")).join("\n");
|
|
7
|
+
}
|
|
8
|
+
function parseCSV(text) {
|
|
9
|
+
const lines = text.trim().split(/\r?\n/).filter(Boolean);
|
|
10
|
+
if (lines.length === 0) return { columns: [], rows: [] };
|
|
11
|
+
function splitRow(line) {
|
|
12
|
+
const cells = [];
|
|
13
|
+
let cur = "";
|
|
14
|
+
let inQuote = false;
|
|
15
|
+
for (let i = 0; i < line.length; i++) {
|
|
16
|
+
const ch = line[i];
|
|
17
|
+
if (ch === '"') {
|
|
18
|
+
if (inQuote && line[i + 1] === '"') {
|
|
19
|
+
cur += '"';
|
|
20
|
+
i++;
|
|
21
|
+
} else inQuote = !inQuote;
|
|
22
|
+
} else if (ch === "," && !inQuote) {
|
|
23
|
+
cells.push(cur.trim());
|
|
24
|
+
cur = "";
|
|
25
|
+
} else {
|
|
26
|
+
cur += ch;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
cells.push(cur.trim());
|
|
30
|
+
return cells;
|
|
31
|
+
}
|
|
32
|
+
const columns = splitRow(lines[0]);
|
|
33
|
+
const rows = lines.slice(1).map(splitRow);
|
|
34
|
+
return { columns, rows };
|
|
35
|
+
}
|
|
36
|
+
const DATA_DIR = "data";
|
|
37
|
+
const hasRows = (ds) => !!ds && ds.columns.length > 0;
|
|
38
|
+
const refFor = (id) => `${DATA_DIR}/${id}.csv`;
|
|
39
|
+
async function externalizeDataSets(col, colDir) {
|
|
40
|
+
const clone = JSON.parse(JSON.stringify(col));
|
|
41
|
+
const writes = [];
|
|
42
|
+
const removals = [];
|
|
43
|
+
const handle = (h) => {
|
|
44
|
+
if (hasRows(h.dataSet)) {
|
|
45
|
+
const ref = refFor(h.id);
|
|
46
|
+
writes.push({ path: path.join(colDir, ref), content: toCSV(h.dataSet) });
|
|
47
|
+
h.dataSetRef = ref;
|
|
48
|
+
} else if (h.dataSet || h.dataSetRef) {
|
|
49
|
+
removals.push(path.join(colDir, refFor(h.id)));
|
|
50
|
+
delete h.dataSetRef;
|
|
51
|
+
}
|
|
52
|
+
delete h.dataSet;
|
|
53
|
+
};
|
|
54
|
+
handle(clone);
|
|
55
|
+
const walk = (f) => {
|
|
56
|
+
handle(f);
|
|
57
|
+
f.folders.forEach(walk);
|
|
58
|
+
};
|
|
59
|
+
walk(clone.rootFolder);
|
|
60
|
+
if (writes.length) await promises.mkdir(path.join(colDir, DATA_DIR), { recursive: true });
|
|
61
|
+
await Promise.all(writes.map((w) => promises.writeFile(w.path, w.content, "utf8")));
|
|
62
|
+
await Promise.all(removals.map((p) => promises.unlink(p).catch(() => {
|
|
63
|
+
})));
|
|
64
|
+
return clone;
|
|
65
|
+
}
|
|
66
|
+
async function inlineDataSets(col, colDir) {
|
|
67
|
+
const load = async (h) => {
|
|
68
|
+
if (!h.dataSetRef) return;
|
|
69
|
+
try {
|
|
70
|
+
const text = await promises.readFile(path.join(colDir, h.dataSetRef), "utf8");
|
|
71
|
+
h.dataSet = parseCSV(text);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
delete h.dataSetRef;
|
|
75
|
+
};
|
|
76
|
+
await load(col);
|
|
77
|
+
const walk = async (f) => {
|
|
78
|
+
await load(f);
|
|
79
|
+
await Promise.all(f.folders.map(walk));
|
|
80
|
+
};
|
|
81
|
+
await walk(col.rootFolder);
|
|
82
|
+
return col;
|
|
83
|
+
}
|
|
84
|
+
exports.externalizeDataSets = externalizeDataSets;
|
|
85
|
+
exports.inlineDataSets = inlineDataSets;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
function escapeHtml(s) {
|
|
3
|
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4
|
+
}
|
|
5
|
+
function escapeXml(s) {
|
|
6
|
+
return escapeHtml(s).replace(/'/g, "'");
|
|
7
|
+
}
|
|
8
|
+
exports.escapeHtml = escapeHtml;
|
|
9
|
+
exports.escapeXml = escapeXml;
|
|
@@ -39,7 +39,13 @@ const IPC = {
|
|
|
39
39
|
secret: {
|
|
40
40
|
checkMasterKey: "secret:checkMasterKey",
|
|
41
41
|
setMasterKey: "secret:setMasterKey",
|
|
42
|
-
set: "secret:set"
|
|
42
|
+
set: "secret:set",
|
|
43
|
+
has: "secret:has",
|
|
44
|
+
delete: "secret:delete"
|
|
45
|
+
},
|
|
46
|
+
// ─── AI (optional; key lives only in the OS keychain) ────────────────────────
|
|
47
|
+
ai: {
|
|
48
|
+
generateDocs: "ai:generateDocs"
|
|
43
49
|
},
|
|
44
50
|
// ─── Global variables ──────────────────────────────────────────────────────
|
|
45
51
|
globals: {
|
|
@@ -163,6 +169,7 @@ const IPC = {
|
|
|
163
169
|
branches: "git:branches",
|
|
164
170
|
checkout: "git:checkout",
|
|
165
171
|
deleteBranch: "git:deleteBranch",
|
|
172
|
+
fetch: "git:fetch",
|
|
166
173
|
pull: "git:pull",
|
|
167
174
|
push: "git:push",
|
|
168
175
|
remotes: "git:remotes",
|
|
@@ -172,7 +179,9 @@ const IPC = {
|
|
|
172
179
|
writeCiFile: "git:writeCiFile",
|
|
173
180
|
resolveOurs: "git:resolveOurs",
|
|
174
181
|
resolveTheirs: "git:resolveTheirs",
|
|
175
|
-
markResolved: "git:markResolved"
|
|
182
|
+
markResolved: "git:markResolved",
|
|
183
|
+
readConflict: "git:readConflict",
|
|
184
|
+
writeResolved: "git:writeResolved"
|
|
176
185
|
},
|
|
177
186
|
// ─── Recorder ──────────────────────────────────────────────────────────────
|
|
178
187
|
record: {
|
|
@@ -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-
|
|
5
|
-
require("./handle-
|
|
4
|
+
const soapHandler = require("./soap-handler-BAPCALkN.js");
|
|
5
|
+
require("./handle-CL-nWtYA.js");
|
|
6
6
|
require("https");
|
|
7
7
|
require("http");
|
|
8
8
|
require("@xmldom/xmldom");
|
|
@@ -25,17 +25,49 @@ function findFolder(root, id) {
|
|
|
25
25
|
}
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
|
+
function flattenFolderRequests(folder, requests) {
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const child of orderedChildren(folder)) {
|
|
31
|
+
if (child.type === "request") {
|
|
32
|
+
const r = requests[child.id];
|
|
33
|
+
if (r && !r.disabled) out.push(r);
|
|
34
|
+
} else {
|
|
35
|
+
const sub = folder.folders.find((f) => f.id === child.id);
|
|
36
|
+
if (sub && !sub.disabled) out.push(...flattenFolderRequests(sub, requests));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
28
41
|
function makeHook(req, collectionVars, hookType, scopeId, scopeAncestors, scopePath, mainRequestId) {
|
|
29
42
|
return { request: req, collectionVars, isHook: true, hookType, scopeId, scopeAncestors, scopePath, mainRequestId };
|
|
30
43
|
}
|
|
31
44
|
function buildFolderPlan(folder, requests, collectionVars, filterTags, scopeId, ancestorIds, parentPath, wrappers, isRoot) {
|
|
32
45
|
const result = [];
|
|
33
46
|
const scopePath = isRoot ? [] : [...parentPath, folder.name];
|
|
34
|
-
const folderReqs = folder.
|
|
35
|
-
const beforeAllHooks =
|
|
36
|
-
const beforeHooks =
|
|
37
|
-
const afterHooks =
|
|
38
|
-
const afterAllHooks =
|
|
47
|
+
const folderReqs = orderedChildren(folder).filter((c) => c.type === "request").map((c) => requests[c.id]).filter((r) => r && !r.disabled);
|
|
48
|
+
const beforeAllHooks = [];
|
|
49
|
+
const beforeHooks = [];
|
|
50
|
+
const afterHooks = [];
|
|
51
|
+
const afterAllHooks = [];
|
|
52
|
+
const hookFolderIds = /* @__PURE__ */ new Set();
|
|
53
|
+
const pushHook = (ht, reqs) => {
|
|
54
|
+
if (ht === "beforeAll") beforeAllHooks.push(...reqs);
|
|
55
|
+
else if (ht === "before") beforeHooks.push(...reqs);
|
|
56
|
+
else if (ht === "after") afterHooks.push(...reqs);
|
|
57
|
+
else if (ht === "afterAll") afterAllHooks.push(...reqs);
|
|
58
|
+
};
|
|
59
|
+
for (const child of orderedChildren(folder)) {
|
|
60
|
+
if (child.type === "request") {
|
|
61
|
+
const r = requests[child.id];
|
|
62
|
+
if (r && !r.disabled && r.hookType) pushHook(r.hookType, [r]);
|
|
63
|
+
} else {
|
|
64
|
+
const sub = folder.folders.find((f) => f.id === child.id);
|
|
65
|
+
if (sub && !sub.disabled && sub.hookType) {
|
|
66
|
+
hookFolderIds.add(sub.id);
|
|
67
|
+
pushHook(sub.hookType, flattenFolderRequests(sub, requests));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
39
71
|
const regularReqs = folderReqs.filter((r) => !r.hookType);
|
|
40
72
|
const myWrapper = { scopeId, ancestors: ancestorIds, scopePath, before: beforeHooks, after: afterHooks };
|
|
41
73
|
const allWrappers = [...wrappers, myWrapper];
|
|
@@ -63,7 +95,8 @@ function buildFolderPlan(folder, requests, collectionVars, filterTags, scopeId,
|
|
|
63
95
|
}
|
|
64
96
|
} else {
|
|
65
97
|
const sub = subById.get(child.id);
|
|
66
|
-
if (!sub) continue;
|
|
98
|
+
if (!sub || sub.disabled) continue;
|
|
99
|
+
if (hookFolderIds.has(sub.id)) continue;
|
|
67
100
|
const folderTags = sub.tags ?? [];
|
|
68
101
|
const effectiveFilter = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
|
|
69
102
|
result.push(...buildFolderPlan(
|
|
@@ -168,12 +201,36 @@ function buildRunPlan(collection, folderId, filterTags) {
|
|
|
168
201
|
function expandRunPlanWithData(items, ds) {
|
|
169
202
|
if (!ds || ds.rows.length === 0) return items;
|
|
170
203
|
return ds.rows.flatMap((row, ri) => {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
204
|
+
const parsed = parseDataRow(ds.columns, row);
|
|
205
|
+
return items.map((item) => ({
|
|
206
|
+
...item,
|
|
207
|
+
dataRow: { ...item.dataRow, ...parsed.dataRow },
|
|
208
|
+
iterationLabel: `${ri + 1}/${ds.rows.length}`,
|
|
209
|
+
...parsed.expectStatus ? { expectStatus: parsed.expectStatus } : {},
|
|
210
|
+
...parsed.owaspTag ? { owaspTag: parsed.owaspTag } : {}
|
|
211
|
+
}));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const RESERVED_STATUS = /* @__PURE__ */ new Set(["expectstatus", "expect_status", "expectedstatus", "expected_status"]);
|
|
215
|
+
const RESERVED_OWASP = /* @__PURE__ */ new Set(["owasp", "owasp_tag", "category"]);
|
|
216
|
+
function parseDataRow(columns, row) {
|
|
217
|
+
const dataRow = {};
|
|
218
|
+
let expectStatus;
|
|
219
|
+
let owaspTag;
|
|
220
|
+
columns.forEach((col, ci) => {
|
|
221
|
+
if (!col) return;
|
|
222
|
+
const key = col.toLowerCase();
|
|
223
|
+
const val = row[ci] ?? "";
|
|
224
|
+
if (RESERVED_STATUS.has(key)) {
|
|
225
|
+
const nums = val.split(/[\s,|]+/).map((x) => parseInt(x, 10)).filter((n) => Number.isFinite(n));
|
|
226
|
+
if (nums.length) expectStatus = nums;
|
|
227
|
+
} else if (RESERVED_OWASP.has(key)) {
|
|
228
|
+
if (val.trim()) owaspTag = val.trim().toUpperCase();
|
|
229
|
+
} else {
|
|
230
|
+
dataRow[col] = val;
|
|
231
|
+
}
|
|
176
232
|
});
|
|
233
|
+
return { dataRow, expectStatus, owaspTag };
|
|
177
234
|
}
|
|
178
235
|
function nearestFolderDataSet(item, collection) {
|
|
179
236
|
const chain = [item.scopeId, ...item.scopeAncestors ?? []].filter(Boolean);
|
|
@@ -192,11 +249,14 @@ function expandFolderDataSets(items, collection) {
|
|
|
192
249
|
continue;
|
|
193
250
|
}
|
|
194
251
|
ds.rows.forEach((row, ri) => {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
252
|
+
const parsed = parseDataRow(ds.columns, row);
|
|
253
|
+
out.push({
|
|
254
|
+
...item,
|
|
255
|
+
dataRow: { ...item.dataRow, ...parsed.dataRow },
|
|
256
|
+
iterationLabel: `${ri + 1}/${ds.rows.length}`,
|
|
257
|
+
...parsed.expectStatus ? { expectStatus: parsed.expectStatus } : {},
|
|
258
|
+
...parsed.owaspTag ? { owaspTag: parsed.owaspTag } : {}
|
|
198
259
|
});
|
|
199
|
-
out.push({ ...item, dataRow: { ...item.dataRow, ...rowVars }, iterationLabel: `${ri + 1}/${ds.rows.length}` });
|
|
200
260
|
});
|
|
201
261
|
}
|
|
202
262
|
return out;
|
|
@@ -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-
|
|
27
|
+
const handle = require("./handle-CL-nWtYA.js");
|
|
28
28
|
const fs = require("node:fs");
|
|
29
29
|
const os = require("node:os");
|
|
30
30
|
const path$1 = require("node:path");
|
|
@@ -582,6 +582,15 @@ function registerSecretHandlers(ipc) {
|
|
|
582
582
|
secretStore[ref] = ss.encryptString(value).toString("base64");
|
|
583
583
|
await persistSecretStore();
|
|
584
584
|
});
|
|
585
|
+
handle.handleIpc(ipc, handle.IPC.secret.has, async (_e, ref) => {
|
|
586
|
+
return { has: await getSecret(ref) != null };
|
|
587
|
+
});
|
|
588
|
+
handle.handleIpc(ipc, handle.IPC.secret.delete, async (_e, ref) => {
|
|
589
|
+
if (ref in secretStore) {
|
|
590
|
+
delete secretStore[ref];
|
|
591
|
+
await persistSecretStore();
|
|
592
|
+
}
|
|
593
|
+
});
|
|
585
594
|
}
|
|
586
595
|
function decryptSecret(encrypted, salt, iv, password) {
|
|
587
596
|
const saltBuf = Buffer.from(salt, "base64");
|
|
@@ -2159,6 +2168,15 @@ async function executeRunnerRequest(opts) {
|
|
|
2159
2168
|
await persistGlobals();
|
|
2160
2169
|
onScriptOutput?.("post", r.consoleOutput, r.error);
|
|
2161
2170
|
}
|
|
2171
|
+
if (opts.expectStatus && opts.expectStatus.length > 0) {
|
|
2172
|
+
const tag = opts.owaspTag ? `[${opts.owaspTag}] ` : "";
|
|
2173
|
+
const passed = opts.expectStatus.includes(exchange.status);
|
|
2174
|
+
testResults = [...testResults, {
|
|
2175
|
+
name: `${tag}expected status ${opts.expectStatus.join(" or ")}`,
|
|
2176
|
+
passed,
|
|
2177
|
+
error: passed ? void 0 : `got ${exchange.status} ${exchange.statusText}`
|
|
2178
|
+
}];
|
|
2179
|
+
}
|
|
2162
2180
|
const status = deriveRunStatus(postScriptError, testResults, exchange.status);
|
|
2163
2181
|
if (exchange.status >= 400 && testResults.length === 0) {
|
|
2164
2182
|
testResults = [...testResults, syntheticHttpFailure(exchange.status, exchange.statusText)];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const handle = require("./handle-
|
|
2
|
+
const handle = require("./handle-CL-nWtYA.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-sqUFqRmE.js"));
|
|
317
317
|
validateWsdlImport(opts);
|
|
318
|
-
const { importWsdl } = await Promise.resolve().then(() => require("./import-
|
|
318
|
+
const { importWsdl } = await Promise.resolve().then(() => require("./import-DdXhO56X.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/compare.js
CHANGED
|
@@ -4,8 +4,9 @@ const promises = require("fs/promises");
|
|
|
4
4
|
const jsYaml = require("js-yaml");
|
|
5
5
|
const undici = require("undici");
|
|
6
6
|
const coverage = require("./chunks/coverage-C54mZEh_.js");
|
|
7
|
-
const cliCommon = require("./chunks/cli-common-
|
|
7
|
+
const cliCommon = require("./chunks/cli-common-vASoDfQH.js");
|
|
8
8
|
require("path");
|
|
9
|
+
require("./chunks/data-files-Br2h3Dt2.js");
|
|
9
10
|
require("crypto");
|
|
10
11
|
const HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
|
|
11
12
|
function resolveRef(spec, ref) {
|