@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
package/out/main/index.js
CHANGED
|
@@ -22,18 +22,19 @@ 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-CL-nWtYA.js");
|
|
26
26
|
const path = require("path");
|
|
27
27
|
const fs$1 = require("fs");
|
|
28
28
|
const promises = require("fs/promises");
|
|
29
29
|
const crypto = require("crypto");
|
|
30
30
|
const JSZip = require("jszip");
|
|
31
|
-
const
|
|
31
|
+
const dataFiles = require("./chunks/data-files-Br2h3Dt2.js");
|
|
32
|
+
const requestExec = require("./chunks/request-exec-DAFA6Fap.js");
|
|
32
33
|
const ipcValidate = require("./chunks/ipc-validate-sqUFqRmE.js");
|
|
33
34
|
const uuid = require("uuid");
|
|
34
35
|
const jsYaml = require("js-yaml");
|
|
35
36
|
const undici = require("undici");
|
|
36
|
-
const requestCollection = require("./chunks/request-collection-
|
|
37
|
+
const requestCollection = require("./chunks/request-collection-Hqh3I3rs.js");
|
|
37
38
|
const mockServer = require("./chunks/mock-server-BxiXhP1m.js");
|
|
38
39
|
const http = require("http");
|
|
39
40
|
const WebSocket = require("ws");
|
|
@@ -42,10 +43,11 @@ const os = require("node:os");
|
|
|
42
43
|
const path$1 = require("node:path");
|
|
43
44
|
const grpc = require("@grpc/grpc-js");
|
|
44
45
|
const protoLoader = require("@grpc/proto-loader");
|
|
45
|
-
const soapHandler = require("./chunks/soap-handler-
|
|
46
|
+
const soapHandler = require("./chunks/soap-handler-BAPCALkN.js");
|
|
47
|
+
const escape = require("./chunks/escape-BwPM4Tf6.js");
|
|
46
48
|
const promises$1 = require("node:fs/promises");
|
|
47
49
|
const os$1 = require("os");
|
|
48
|
-
const
|
|
50
|
+
const brokerClient = require("./chunks/broker-client-WEymdqAK.js");
|
|
49
51
|
const simpleGit = require("simple-git");
|
|
50
52
|
const recorder = require("./chunks/recorder-0Ij921El.js");
|
|
51
53
|
require("tls");
|
|
@@ -332,14 +334,16 @@ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The
|
|
|
332
334
|
});
|
|
333
335
|
handle.handleIpc(ipc, handle.IPC.file.loadCollection, async (_e, relPath) => {
|
|
334
336
|
if (!workspaceDir) throw new Error("No workspace open");
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
+
const fullPath = path.resolve(workspaceDir, relPath);
|
|
338
|
+
const raw = await promises.readFile(fullPath, "utf8");
|
|
339
|
+
return dataFiles.inlineDataSets(JSON.parse(raw), path.dirname(fullPath));
|
|
337
340
|
});
|
|
338
341
|
handle.handleIpc(ipc, handle.IPC.file.saveCollection, async (_e, relPath, col) => {
|
|
339
342
|
if (!workspaceDir) throw new Error("No workspace open");
|
|
340
343
|
const fullPath = path.resolve(workspaceDir, relPath);
|
|
341
344
|
await promises.mkdir(path.dirname(fullPath), { recursive: true });
|
|
342
|
-
await
|
|
345
|
+
const toWrite = await dataFiles.externalizeDataSets(col, path.dirname(fullPath));
|
|
346
|
+
await atomicWrite(fullPath, JSON.stringify(toWrite, null, 2));
|
|
343
347
|
});
|
|
344
348
|
handle.handleIpc(ipc, handle.IPC.file.loadEnvironment, async (_e, relPath) => {
|
|
345
349
|
if (!workspaceDir) throw new Error("No workspace open");
|
|
@@ -942,6 +946,77 @@ function registerRequestHandler(ipc) {
|
|
|
942
946
|
};
|
|
943
947
|
});
|
|
944
948
|
}
|
|
949
|
+
const AI_OPENAI_TOKEN_REF = "ai:openai:token";
|
|
950
|
+
const DEFAULT_AI_MODEL = "gpt-4o-mini";
|
|
951
|
+
const OPENAI_URL = "https://api.openai.com/v1/chat/completions";
|
|
952
|
+
function systemPrompt(level) {
|
|
953
|
+
return [
|
|
954
|
+
"You write clear, concise API documentation in GitHub-flavored Markdown.",
|
|
955
|
+
`You are documenting an API ${level}.`,
|
|
956
|
+
"Return ONLY markdown — no preamble, no code fences around the whole answer.",
|
|
957
|
+
"Use short paragraphs and headings. Describe purpose, key parameters, request/response shape,",
|
|
958
|
+
"auth if relevant, and any gotchas. Do not invent endpoints or fields that are not in the context.",
|
|
959
|
+
"Keep it practical for a developer who will call this API."
|
|
960
|
+
].join(" ");
|
|
961
|
+
}
|
|
962
|
+
function registerAiHandlers(ipc) {
|
|
963
|
+
handle.handleIpc(ipc, handle.IPC.ai.generateDocs, async (_e, input) => {
|
|
964
|
+
const token = await requestExec.getSecret(AI_OPENAI_TOKEN_REF);
|
|
965
|
+
if (!token) {
|
|
966
|
+
throw new Error("No OpenAI API key configured. Add one in Settings → AI.");
|
|
967
|
+
}
|
|
968
|
+
const model = input.model?.trim() || DEFAULT_AI_MODEL;
|
|
969
|
+
const userParts = [
|
|
970
|
+
`Name: ${input.name}`,
|
|
971
|
+
"",
|
|
972
|
+
input.context
|
|
973
|
+
];
|
|
974
|
+
if (input.existing?.trim()) {
|
|
975
|
+
userParts.push("", "Existing documentation to refine and expand (keep what is still accurate):", input.existing.trim());
|
|
976
|
+
}
|
|
977
|
+
const controller = new AbortController();
|
|
978
|
+
const timeout = setTimeout(() => controller.abort(), 6e4);
|
|
979
|
+
try {
|
|
980
|
+
const res = await fetch(OPENAI_URL, {
|
|
981
|
+
method: "POST",
|
|
982
|
+
headers: {
|
|
983
|
+
"content-type": "application/json",
|
|
984
|
+
authorization: `Bearer ${token}`
|
|
985
|
+
},
|
|
986
|
+
body: JSON.stringify({
|
|
987
|
+
model,
|
|
988
|
+
temperature: 0.3,
|
|
989
|
+
messages: [
|
|
990
|
+
{ role: "system", content: systemPrompt(input.level) },
|
|
991
|
+
{ role: "user", content: userParts.join("\n") }
|
|
992
|
+
]
|
|
993
|
+
}),
|
|
994
|
+
signal: controller.signal,
|
|
995
|
+
// @ts-expect-error undici accepts a dispatcher for proxy/TLS support
|
|
996
|
+
dispatcher: await requestExec.buildDispatcher(void 0, void 0)
|
|
997
|
+
});
|
|
998
|
+
if (!res.ok) {
|
|
999
|
+
let detail = `HTTP ${res.status}`;
|
|
1000
|
+
try {
|
|
1001
|
+
const body = await res.json();
|
|
1002
|
+
if (body?.error?.message) detail = body.error.message;
|
|
1003
|
+
} catch {
|
|
1004
|
+
}
|
|
1005
|
+
if (res.status === 401) detail = "OpenAI rejected the API key (401). Check the key in Settings → AI.";
|
|
1006
|
+
throw new Error(`Doc generation failed: ${detail}`);
|
|
1007
|
+
}
|
|
1008
|
+
const data = await res.json();
|
|
1009
|
+
const content = data.choices?.[0]?.message?.content?.trim();
|
|
1010
|
+
if (!content) throw new Error("The model returned an empty response.");
|
|
1011
|
+
return content;
|
|
1012
|
+
} catch (e) {
|
|
1013
|
+
if (e instanceof Error && e.name === "AbortError") throw new Error("Doc generation timed out after 60s.");
|
|
1014
|
+
throw e;
|
|
1015
|
+
} finally {
|
|
1016
|
+
clearTimeout(timeout);
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
945
1020
|
const POSTMAN_RULES = [
|
|
946
1021
|
// Variables — environment
|
|
947
1022
|
[/\bpm\.environment\.get\(/g, "sp.environment.get("],
|
|
@@ -1433,6 +1508,10 @@ function buildCollection(spec) {
|
|
|
1433
1508
|
const globalSecurity = spec.security ?? [];
|
|
1434
1509
|
const requests = {};
|
|
1435
1510
|
const foldersByTag = {};
|
|
1511
|
+
const tagDescriptions = {};
|
|
1512
|
+
for (const tg of spec.tags ?? []) {
|
|
1513
|
+
if (tg?.name && tg.description) tagDescriptions[tg.name] = tg.description;
|
|
1514
|
+
}
|
|
1436
1515
|
for (const [pathStr, pathItem] of Object.entries(spec.paths ?? {})) {
|
|
1437
1516
|
const resolved = resolve(spec, pathItem);
|
|
1438
1517
|
const pathLevelParams = resolved.parameters ?? [];
|
|
@@ -1462,14 +1541,14 @@ function buildCollection(spec) {
|
|
|
1462
1541
|
params,
|
|
1463
1542
|
auth: buildAuth(security, securitySchemes),
|
|
1464
1543
|
body,
|
|
1465
|
-
description: operation.description
|
|
1544
|
+
description: operation.description || operation.summary || "",
|
|
1466
1545
|
meta: { tags },
|
|
1467
1546
|
...responseSchema ? { schema: responseSchema } : {},
|
|
1468
1547
|
...examples ? { examples } : {}
|
|
1469
1548
|
};
|
|
1470
1549
|
requests[req.id] = req;
|
|
1471
1550
|
if (!foldersByTag[tag]) {
|
|
1472
|
-
foldersByTag[tag] = { id: uuid.v4(), name: tag, description: "", folders: [], requestIds: [] };
|
|
1551
|
+
foldersByTag[tag] = { id: uuid.v4(), name: tag, description: tagDescriptions[tag] ?? "", folders: [], requestIds: [] };
|
|
1473
1552
|
}
|
|
1474
1553
|
foldersByTag[tag].requestIds.push(req.id);
|
|
1475
1554
|
}
|
|
@@ -1744,7 +1823,8 @@ function parseBruFile(content, fileName) {
|
|
|
1744
1823
|
params,
|
|
1745
1824
|
auth,
|
|
1746
1825
|
body,
|
|
1747
|
-
|
|
1826
|
+
// Bruno keeps request documentation in a `docs { ... }` block (markdown).
|
|
1827
|
+
description: extractBlock(content, "docs")?.trim() ?? "",
|
|
1748
1828
|
preRequestScript: extractBlock(content, "script:pre-request") ? translateScript(extractBlock(content, "script:pre-request").trim(), "bruno") || void 0 : void 0,
|
|
1749
1829
|
postRequestScript: extractBlock(content, "script:post-response") ? translateScript(extractBlock(content, "script:post-response").trim(), "bruno") || void 0 : void 0,
|
|
1750
1830
|
meta: { seq: meta["seq"] !== void 0 ? Number(meta["seq"]) : 0 }
|
|
@@ -4631,7 +4711,9 @@ function registerRunnerHandler(ipc) {
|
|
|
4631
4711
|
dispatcher,
|
|
4632
4712
|
piiMaskPatterns,
|
|
4633
4713
|
proxy,
|
|
4634
|
-
tls
|
|
4714
|
+
tls,
|
|
4715
|
+
expectStatus: item.expectStatus,
|
|
4716
|
+
owaspTag: item.owaspTag
|
|
4635
4717
|
});
|
|
4636
4718
|
runEnvVars = updatedEnvVars;
|
|
4637
4719
|
runCollectionVars = updatedCollectionVars;
|
|
@@ -5251,9 +5333,7 @@ function generateMarkdown(payload) {
|
|
|
5251
5333
|
}
|
|
5252
5334
|
return lines.join("\n");
|
|
5253
5335
|
}
|
|
5254
|
-
|
|
5255
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5256
|
-
}
|
|
5336
|
+
const escHtml = escape.escapeHtml;
|
|
5257
5337
|
const METHOD_COLORS = {
|
|
5258
5338
|
GET: "#34d399",
|
|
5259
5339
|
POST: "#60a5fa",
|
|
@@ -5443,7 +5523,7 @@ function inferSchemaFromJson(json) {
|
|
|
5443
5523
|
async function resolveSnapshotSpec(relPath) {
|
|
5444
5524
|
const dir = getWorkspaceDir();
|
|
5445
5525
|
if (!dir) throw new Error("No workspace open - cannot resolve snapshot.");
|
|
5446
|
-
const snap = await
|
|
5526
|
+
const snap = await brokerClient.loadSnapshot(dir, relPath);
|
|
5447
5527
|
const tmp = path.join(os$1.tmpdir(), `api-spector-${crypto.randomUUID()}.${snap.format === "yaml" ? "yaml" : "json"}`);
|
|
5448
5528
|
await promises.writeFile(tmp, snap.spec, "utf8");
|
|
5449
5529
|
return { specPath: tmp };
|
|
@@ -5455,7 +5535,7 @@ function registerContractHandlers(ipc) {
|
|
|
5455
5535
|
let requests = payload.requests;
|
|
5456
5536
|
let { specUrl, specPath } = payload;
|
|
5457
5537
|
if (mode !== "provider") {
|
|
5458
|
-
const designReqs = await
|
|
5538
|
+
const designReqs = await brokerClient.loadDesignContractRequests(
|
|
5459
5539
|
{ designContracts: payload.designContracts },
|
|
5460
5540
|
getWorkspaceDir() ?? void 0
|
|
5461
5541
|
);
|
|
@@ -5476,13 +5556,13 @@ function registerContractHandlers(ipc) {
|
|
|
5476
5556
|
}
|
|
5477
5557
|
switch (mode) {
|
|
5478
5558
|
case "consumer":
|
|
5479
|
-
return
|
|
5559
|
+
return brokerClient.runConsumerContracts(requests, envVars, collectionVars, providerBaseUrl);
|
|
5480
5560
|
case "provider":
|
|
5481
|
-
return
|
|
5561
|
+
return brokerClient.runProviderVerification(requests, envVars, collectionVars, specUrl, specPath, requestBaseUrl);
|
|
5482
5562
|
case "provider-live":
|
|
5483
|
-
return
|
|
5563
|
+
return brokerClient.runLiveProviderVerification(requests, envVars, collectionVars, providerBaseUrl, stateHandlerUrl);
|
|
5484
5564
|
case "bidirectional":
|
|
5485
|
-
return
|
|
5565
|
+
return brokerClient.runBidirectional(requests, envVars, collectionVars, specUrl, specPath, requestBaseUrl);
|
|
5486
5566
|
}
|
|
5487
5567
|
});
|
|
5488
5568
|
handle.handleIpc(ipc, handle.IPC.contract.inferSchema, (_e, jsonBody) => {
|
|
@@ -5496,7 +5576,7 @@ function registerContractHandlers(ipc) {
|
|
|
5496
5576
|
filters: [{ name: "HTML", extensions: ["html"] }]
|
|
5497
5577
|
});
|
|
5498
5578
|
if (canceled || !filePath) return false;
|
|
5499
|
-
await promises.writeFile(filePath,
|
|
5579
|
+
await promises.writeFile(filePath, brokerClient.reportToHtml(report, { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...meta }), "utf8");
|
|
5500
5580
|
return true;
|
|
5501
5581
|
});
|
|
5502
5582
|
handle.handleIpc(ipc, handle.IPC.contract.exportDesignPact, async (_e, contract) => {
|
|
@@ -5506,31 +5586,31 @@ function registerContractHandlers(ipc) {
|
|
|
5506
5586
|
const relPath = path.join("pacts", `${safe(contract.consumer)}-${safe(contract.provider)}.pact.json`);
|
|
5507
5587
|
const fullPath = path.join(dir, relPath);
|
|
5508
5588
|
await promises.mkdir(path.join(dir, "pacts"), { recursive: true });
|
|
5509
|
-
await promises.writeFile(fullPath, JSON.stringify(
|
|
5589
|
+
await promises.writeFile(fullPath, JSON.stringify(brokerClient.designContractToPact(contract), null, 2), "utf8");
|
|
5510
5590
|
return relPath;
|
|
5511
5591
|
});
|
|
5512
5592
|
handle.handleIpc(ipc, handle.IPC.contract.captureSnapshot, async (_e, opts) => {
|
|
5513
5593
|
const dir = getWorkspaceDir();
|
|
5514
5594
|
if (!dir) throw new Error("No workspace open - cannot capture snapshot.");
|
|
5515
|
-
const snapshot = await
|
|
5516
|
-
const relPath =
|
|
5595
|
+
const snapshot = await brokerClient.captureSnapshot(dir, opts);
|
|
5596
|
+
const relPath = brokerClient.relPathOf(snapshot);
|
|
5517
5597
|
if (!relPath) throw new Error("Snapshot created but relPath was not attached.");
|
|
5518
5598
|
return { relPath, snapshot };
|
|
5519
5599
|
});
|
|
5520
5600
|
handle.handleIpc(ipc, handle.IPC.contract.listSnapshots, async (_e, registered = []) => {
|
|
5521
5601
|
const dir = getWorkspaceDir();
|
|
5522
5602
|
if (!dir) return [];
|
|
5523
|
-
return
|
|
5603
|
+
return brokerClient.listSnapshots(dir, registered);
|
|
5524
5604
|
});
|
|
5525
5605
|
handle.handleIpc(ipc, handle.IPC.contract.loadSnapshot, async (_e, relPath) => {
|
|
5526
5606
|
const dir = getWorkspaceDir();
|
|
5527
5607
|
if (!dir) throw new Error("No workspace open.");
|
|
5528
|
-
return
|
|
5608
|
+
return brokerClient.loadSnapshot(dir, relPath);
|
|
5529
5609
|
});
|
|
5530
5610
|
handle.handleIpc(ipc, handle.IPC.contract.deleteSnapshot, async (_e, relPath) => {
|
|
5531
5611
|
const dir = getWorkspaceDir();
|
|
5532
5612
|
if (!dir) return;
|
|
5533
|
-
await
|
|
5613
|
+
await brokerClient.deleteSnapshot(dir, relPath);
|
|
5534
5614
|
});
|
|
5535
5615
|
handle.handleIpc(ipc, handle.IPC.contract.fuzz, async (_e, payload) => {
|
|
5536
5616
|
let { specUrl, specPath } = payload;
|
|
@@ -5539,7 +5619,7 @@ function registerContractHandlers(ipc) {
|
|
|
5539
5619
|
specPath = resolved.specPath;
|
|
5540
5620
|
specUrl = void 0;
|
|
5541
5621
|
}
|
|
5542
|
-
return
|
|
5622
|
+
return brokerClient.runFuzz({ ...payload, specUrl, specPath });
|
|
5543
5623
|
});
|
|
5544
5624
|
handle.handleIpc(ipc, handle.IPC.contract.recordResult, async (_e, opts) => {
|
|
5545
5625
|
const dir = getWorkspaceDir();
|
|
@@ -5547,7 +5627,7 @@ function registerContractHandlers(ipc) {
|
|
|
5547
5627
|
const pacticipant = opts.pacticipant?.trim();
|
|
5548
5628
|
const version = opts.version?.trim();
|
|
5549
5629
|
if (!pacticipant || !version) throw new Error("Pacticipant and version are required.");
|
|
5550
|
-
const file = await
|
|
5630
|
+
const file = await brokerClient.recordResult(dir, pacticipant, version, opts.report, (/* @__PURE__ */ new Date()).toISOString());
|
|
5551
5631
|
return { file };
|
|
5552
5632
|
});
|
|
5553
5633
|
}
|
|
@@ -5598,6 +5678,17 @@ function registerGitHandlers(ipc) {
|
|
|
5598
5678
|
handle.handleIpc(ipc, handle.IPC.git.markResolved, async (_e, filePath) => {
|
|
5599
5679
|
await git().add([filePath]);
|
|
5600
5680
|
});
|
|
5681
|
+
handle.handleIpc(ipc, handle.IPC.git.readConflict, async (_e, filePath) => {
|
|
5682
|
+
const dir = getWorkspaceDir();
|
|
5683
|
+
if (!dir) throw new Error("No workspace open");
|
|
5684
|
+
return promises.readFile(path.join(dir, filePath), "utf8");
|
|
5685
|
+
});
|
|
5686
|
+
handle.handleIpc(ipc, handle.IPC.git.writeResolved, async (_e, filePath, content) => {
|
|
5687
|
+
const dir = getWorkspaceDir();
|
|
5688
|
+
if (!dir) throw new Error("No workspace open");
|
|
5689
|
+
await promises.writeFile(path.join(dir, filePath), content, "utf8");
|
|
5690
|
+
await git().add([filePath]);
|
|
5691
|
+
});
|
|
5601
5692
|
handle.handleIpc(ipc, handle.IPC.git.diff, async (_e, filePath) => {
|
|
5602
5693
|
if (filePath) return git().diff(["--", filePath]);
|
|
5603
5694
|
return git().diff();
|
|
@@ -5683,8 +5774,11 @@ function registerGitHandlers(ipc) {
|
|
|
5683
5774
|
handle.handleIpc(ipc, handle.IPC.git.deleteBranch, async (_e, name, force = false) => {
|
|
5684
5775
|
await git().deleteLocalBranch(name, force);
|
|
5685
5776
|
});
|
|
5686
|
-
handle.handleIpc(ipc, handle.IPC.git.
|
|
5687
|
-
await git().
|
|
5777
|
+
handle.handleIpc(ipc, handle.IPC.git.fetch, async () => {
|
|
5778
|
+
await git().fetch(["--prune"]);
|
|
5779
|
+
});
|
|
5780
|
+
handle.handleIpc(ipc, handle.IPC.git.pull, async (_e, rebase = false) => {
|
|
5781
|
+
await git().raw(["pull", ...rebase ? ["--rebase"] : []]);
|
|
5688
5782
|
});
|
|
5689
5783
|
handle.handleIpc(ipc, handle.IPC.git.push, async (_e, setUpstream) => {
|
|
5690
5784
|
if (setUpstream) {
|
|
@@ -5744,7 +5838,7 @@ function registerRecordHandlers(ipc, getWebContents) {
|
|
|
5744
5838
|
});
|
|
5745
5839
|
}
|
|
5746
5840
|
const CLOUD_TOKEN_REF = "cloud:token";
|
|
5747
|
-
const CLOUD_ENDPOINT =
|
|
5841
|
+
const CLOUD_ENDPOINT = brokerClient.resolveCloudEndpoint();
|
|
5748
5842
|
async function cloudFetch(path2, method, body) {
|
|
5749
5843
|
const token = await requestExec.getSecret(CLOUD_TOKEN_REF);
|
|
5750
5844
|
if (!token) throw new Error("No cloud API token set. Add one in Settings → Cloud.");
|
|
@@ -5871,7 +5965,7 @@ function registerCloudHandlers(ipc) {
|
|
|
5871
5965
|
return cloudFetch("/api/mocks/" + encodeURIComponent(slugify(name)), "GET");
|
|
5872
5966
|
});
|
|
5873
5967
|
handle.handleIpc(ipc, handle.IPC.cloud.pushPact, async (_e, input) => {
|
|
5874
|
-
const pact =
|
|
5968
|
+
const pact = brokerClient.exportPact(input.consumer, input.provider, input.requests ?? []);
|
|
5875
5969
|
return cloudFetch("/api/contracts", "PUT", {
|
|
5876
5970
|
consumer: input.consumer,
|
|
5877
5971
|
consumerVersion: input.consumerVersion,
|
|
@@ -5880,7 +5974,7 @@ function registerCloudHandlers(ipc) {
|
|
|
5880
5974
|
});
|
|
5881
5975
|
});
|
|
5882
5976
|
handle.handleIpc(ipc, handle.IPC.cloud.pushDesignContract, async (_e, input) => {
|
|
5883
|
-
const pact =
|
|
5977
|
+
const pact = brokerClient.designContractToPact(input.contract);
|
|
5884
5978
|
return cloudFetch("/api/contracts", "PUT", {
|
|
5885
5979
|
consumer: input.contract.consumer,
|
|
5886
5980
|
consumerVersion: input.consumerVersion,
|
|
@@ -5945,7 +6039,7 @@ function isNewer(a, b) {
|
|
|
5945
6039
|
return a2 > b2;
|
|
5946
6040
|
}
|
|
5947
6041
|
async function checkForUpdate() {
|
|
5948
|
-
const current = "0.6.
|
|
6042
|
+
const current = "0.6.2";
|
|
5949
6043
|
try {
|
|
5950
6044
|
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(4e3) });
|
|
5951
6045
|
if (!res.ok) return null;
|
|
@@ -6124,6 +6218,7 @@ electron.app.whenReady().then(async () => {
|
|
|
6124
6218
|
registerFileHandlers(electron.ipcMain);
|
|
6125
6219
|
registerRequestHandler(electron.ipcMain);
|
|
6126
6220
|
requestExec.registerSecretHandlers(electron.ipcMain);
|
|
6221
|
+
registerAiHandlers(electron.ipcMain);
|
|
6127
6222
|
registerImportHandlers(electron.ipcMain);
|
|
6128
6223
|
registerGenerateHandlers(electron.ipcMain);
|
|
6129
6224
|
registerRunnerHandler(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-DAFA6Fap.js");
|
|
5
5
|
require("http");
|
|
6
6
|
require("crypto");
|
|
7
7
|
require("vm");
|
|
@@ -10,7 +10,7 @@ require("@xmldom/xmldom");
|
|
|
10
10
|
require("undici");
|
|
11
11
|
require("fs/promises");
|
|
12
12
|
require("tls");
|
|
13
|
-
require("./chunks/handle-
|
|
13
|
+
require("./chunks/handle-CL-nWtYA.js");
|
|
14
14
|
require("node:fs");
|
|
15
15
|
require("node:os");
|
|
16
16
|
require("node:path");
|
package/out/main/mock.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
const mockServer = require("./chunks/mock-server-BxiXhP1m.js");
|
|
4
|
-
const cliCommon = require("./chunks/cli-common-
|
|
4
|
+
const cliCommon = require("./chunks/cli-common-vASoDfQH.js");
|
|
5
5
|
require("http");
|
|
6
6
|
require("crypto");
|
|
7
7
|
require("vm");
|
|
@@ -9,6 +9,7 @@ require("dayjs");
|
|
|
9
9
|
require("@xmldom/xmldom");
|
|
10
10
|
require("fs/promises");
|
|
11
11
|
require("path");
|
|
12
|
+
require("./chunks/data-files-Br2h3Dt2.js");
|
|
12
13
|
async function main() {
|
|
13
14
|
const args = cliCommon.parseArgs(process.argv.slice(2), ["name"]);
|
|
14
15
|
if (args.help) {
|
package/out/main/record.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
const promises = require("fs/promises");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const recorder = require("./chunks/recorder-0Ij921El.js");
|
|
6
|
-
const cliCommon = require("./chunks/cli-common-
|
|
6
|
+
const cliCommon = require("./chunks/cli-common-vASoDfQH.js");
|
|
7
7
|
require("http");
|
|
8
8
|
require("crypto");
|
|
9
9
|
require("undici");
|
|
10
|
+
require("./chunks/data-files-Br2h3Dt2.js");
|
|
10
11
|
function methodBadge(method) {
|
|
11
12
|
const m = method.toUpperCase();
|
|
12
13
|
const c = m === "GET" ? cliCommon.C.green : m === "POST" ? cliCommon.C.cyan : m === "PUT" || m === "PATCH" ? cliCommon.C.yellow : m === "DELETE" ? cliCommon.C.red : cliCommon.C.white;
|
package/out/main/runner.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
const promises = require("fs/promises");
|
|
4
4
|
const path = require("path");
|
|
5
|
-
const requestExec = require("./chunks/request-exec-
|
|
6
|
-
const
|
|
5
|
+
const requestExec = require("./chunks/request-exec-DAFA6Fap.js");
|
|
6
|
+
const escape = require("./chunks/escape-BwPM4Tf6.js");
|
|
7
|
+
const requestCollection = require("./chunks/request-collection-Hqh3I3rs.js");
|
|
7
8
|
const environments = require("./chunks/environments-iM3SUM-4.js");
|
|
8
|
-
const cliCommon = require("./chunks/cli-common-
|
|
9
|
+
const cliCommon = require("./chunks/cli-common-vASoDfQH.js");
|
|
9
10
|
require("undici");
|
|
10
11
|
require("tls");
|
|
11
|
-
require("./chunks/handle-
|
|
12
|
+
require("./chunks/handle-CL-nWtYA.js");
|
|
12
13
|
require("node:fs");
|
|
13
14
|
require("node:os");
|
|
14
15
|
require("node:path");
|
|
@@ -22,6 +23,7 @@ require("@xmldom/xmldom");
|
|
|
22
23
|
require("http");
|
|
23
24
|
require("ajv");
|
|
24
25
|
require("uuid");
|
|
26
|
+
require("./chunks/data-files-Br2h3Dt2.js");
|
|
25
27
|
function buildJsonReport(results, summary, meta = {}) {
|
|
26
28
|
return JSON.stringify({
|
|
27
29
|
timestamp: meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -50,7 +52,7 @@ function buildJsonReport(results, summary, meta = {}) {
|
|
|
50
52
|
}, null, 2);
|
|
51
53
|
}
|
|
52
54
|
function buildHtmlReport(results, summary, meta = {}) {
|
|
53
|
-
const esc =
|
|
55
|
+
const esc = escape.escapeHtml;
|
|
54
56
|
function prettyJson(s) {
|
|
55
57
|
try {
|
|
56
58
|
return esc(JSON.stringify(JSON.parse(s), null, 2));
|
|
@@ -295,7 +297,7 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
295
297
|
`;
|
|
296
298
|
}
|
|
297
299
|
function buildJUnitReport(results, summary, meta = {}) {
|
|
298
|
-
const esc =
|
|
300
|
+
const esc = escape.escapeHtml;
|
|
299
301
|
const suiteName = esc(meta.collection ?? "API Tests");
|
|
300
302
|
const totalSec = (summary.durationMs / 1e3).toFixed(3);
|
|
301
303
|
const ts = meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -345,6 +347,70 @@ ${failures.join("\n")}
|
|
|
345
347
|
];
|
|
346
348
|
return lines.join("\n") + "\n";
|
|
347
349
|
}
|
|
350
|
+
const OWASP_BY_TAG = {
|
|
351
|
+
BOLA: { id: "API1:2023", name: "Broken Object Level Authorization", ref: "https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/" },
|
|
352
|
+
AUTH: { id: "API2:2023", name: "Broken Authentication", ref: "https://owasp.org/API-Security/editions/2023/en/0xa2-broken-authentication/" },
|
|
353
|
+
"MASS-ASSIGN": { id: "API3:2023", name: "Broken Object Property Level Authorization", ref: "https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/" },
|
|
354
|
+
RESOURCE: { id: "API4:2023", name: "Unrestricted Resource Consumption", ref: "https://owasp.org/API-Security/editions/2023/en/0xa4-unrestricted-resource-consumption/" },
|
|
355
|
+
"FUNC-AUTH": { id: "API5:2023", name: "Broken Function Level Authorization", ref: "https://owasp.org/API-Security/editions/2023/en/0xa5-broken-function-level-authorization/" },
|
|
356
|
+
"BIZ-FLOW": { id: "API6:2023", name: "Unrestricted Access to Sensitive Business Flows", ref: "https://owasp.org/API-Security/editions/2023/en/0xa6-unrestricted-access-to-sensitive-business-flows/" },
|
|
357
|
+
SSRF: { id: "API7:2023", name: "Server Side Request Forgery", ref: "https://owasp.org/API-Security/editions/2023/en/0xa7-server-side-request-forgery/" },
|
|
358
|
+
CONFIG: { id: "API8:2023", name: "Security Misconfiguration", ref: "https://owasp.org/API-Security/editions/2023/en/0xa8-security-misconfiguration/" },
|
|
359
|
+
INVENTORY: { id: "API9:2023", name: "Improper Inventory Management", ref: "https://owasp.org/API-Security/editions/2023/en/0xa9-improper-inventory-management/" },
|
|
360
|
+
INJECTION: { id: "API10:2023", name: "Unsafe Consumption of APIs", ref: "https://owasp.org/API-Security/editions/2023/en/0xaa-unsafe-consumption-of-apis/" }
|
|
361
|
+
};
|
|
362
|
+
function owaspRuleForName(name) {
|
|
363
|
+
const m = /^\s*\[([A-Za-z-]+)\]/.exec(name ?? "");
|
|
364
|
+
return m ? OWASP_BY_TAG[m[1].toUpperCase()] ?? null : null;
|
|
365
|
+
}
|
|
366
|
+
function buildSarifReport(results, _summary, meta = {}) {
|
|
367
|
+
const rules = /* @__PURE__ */ new Map();
|
|
368
|
+
const addRule = (id, name, helpUri) => {
|
|
369
|
+
if (!rules.has(id)) rules.set(id, { id, name, ...helpUri ? { helpUri } : {} });
|
|
370
|
+
};
|
|
371
|
+
const sarifResults = [];
|
|
372
|
+
const emit = (rule, fallbackId, fallbackName, r, message) => {
|
|
373
|
+
const id = rule ? rule.id : fallbackId;
|
|
374
|
+
const name = rule ? rule.name : fallbackName;
|
|
375
|
+
addRule(id, name, rule?.ref);
|
|
376
|
+
sarifResults.push({
|
|
377
|
+
ruleId: id,
|
|
378
|
+
level: "error",
|
|
379
|
+
message: { text: `${r.method} ${r.resolvedUrl}${r.iterationLabel ? ` #${r.iterationLabel}` : ""} — ${message}` },
|
|
380
|
+
locations: [{ physicalLocation: { artifactLocation: { uri: r.resolvedUrl || r.name } } }]
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
for (const r of results) {
|
|
384
|
+
if (r.status === "error" && r.error) emit(owaspRuleForName(r.name), "request-error", "Request error", r, r.error);
|
|
385
|
+
if (r.preScriptError) emit(null, "pre-script-error", "Pre-request script error", r, r.preScriptError);
|
|
386
|
+
if (r.postScriptError) emit(null, "post-script-error", "Post-request script error", r, r.postScriptError);
|
|
387
|
+
for (const t of r.testResults ?? []) {
|
|
388
|
+
if (t.passed) continue;
|
|
389
|
+
const rule = owaspRuleForName(t.name) ?? owaspRuleForName(r.name);
|
|
390
|
+
emit(rule, "assertion-failed", "Assertion failed", r, `${t.name}${t.error ? `: ${t.error}` : ""}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return JSON.stringify({
|
|
394
|
+
version: "2.1.0",
|
|
395
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
396
|
+
runs: [{
|
|
397
|
+
tool: {
|
|
398
|
+
driver: {
|
|
399
|
+
name: "API Spector",
|
|
400
|
+
informationUri: "https://github.com/testsmith-io/api-spector",
|
|
401
|
+
rules: [...rules.values()].map((r) => ({
|
|
402
|
+
id: r.id,
|
|
403
|
+
name: r.name,
|
|
404
|
+
shortDescription: { text: r.name },
|
|
405
|
+
...r.helpUri ? { helpUri: r.helpUri } : {}
|
|
406
|
+
}))
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
...meta.timestamp ? { invocations: [{ executionSuccessful: sarifResults.length === 0, endTimeUtc: meta.timestamp }] } : {},
|
|
410
|
+
results: sarifResults
|
|
411
|
+
}]
|
|
412
|
+
}, null, 2);
|
|
413
|
+
}
|
|
348
414
|
function printResult(r, verbose) {
|
|
349
415
|
const icon = r.status === "passed" ? cliCommon.color("✓", cliCommon.C.green, cliCommon.C.bold) : r.status === "failed" ? cliCommon.color("✗", cliCommon.C.red, cliCommon.C.bold) : r.status === "skipped" ? cliCommon.color("○", cliCommon.C.gray, cliCommon.C.bold) : cliCommon.color("⚠", cliCommon.C.yellow, cliCommon.C.bold);
|
|
350
416
|
const http = r.httpStatus ? cliCommon.color(` ${r.httpStatus}`, r.httpStatus < 400 ? cliCommon.C.green : cliCommon.C.red) : "";
|
|
@@ -365,7 +431,7 @@ async function main() {
|
|
|
365
431
|
const args = cliCommon.parseArgs(process.argv.slice(2));
|
|
366
432
|
if (args.help) {
|
|
367
433
|
console.log(
|
|
368
|
-
"\nUsage:\n api-spector run --workspace <path> [--environment <name>] [--tags <a,b>]\n [--collection <name>] [--output <path>] [--format json|junit]\n [--verbose] [--bail]\n"
|
|
434
|
+
"\nUsage:\n api-spector run --workspace <path> [--environment <name>] [--tags <a,b>]\n [--collection <name>] [--output <path>] [--format json|junit|html|sarif]\n [--verbose] [--bail]\n"
|
|
369
435
|
);
|
|
370
436
|
process.exit(0);
|
|
371
437
|
}
|
|
@@ -380,9 +446,9 @@ async function main() {
|
|
|
380
446
|
const verbose = Boolean(args.verbose);
|
|
381
447
|
const bail = Boolean(args.bail);
|
|
382
448
|
const outputPath = args.output;
|
|
383
|
-
const inferredFormat = outputPath ? path.extname(outputPath).toLowerCase() === ".xml" ? "junit" : path.extname(outputPath).toLowerCase() === ".html" ? "html" : "json" : "json";
|
|
449
|
+
const inferredFormat = outputPath ? path.extname(outputPath).toLowerCase() === ".xml" ? "junit" : path.extname(outputPath).toLowerCase() === ".html" ? "html" : path.extname(outputPath).toLowerCase() === ".sarif" ? "sarif" : "json" : "json";
|
|
384
450
|
const explicitFormat = args.format?.toLowerCase();
|
|
385
|
-
const outputFormat = explicitFormat === "junit" || explicitFormat === "html" ? explicitFormat : inferredFormat;
|
|
451
|
+
const outputFormat = explicitFormat === "junit" || explicitFormat === "html" || explicitFormat === "sarif" ? explicitFormat : inferredFormat;
|
|
386
452
|
let workspace, wsDir;
|
|
387
453
|
try {
|
|
388
454
|
;
|
|
@@ -403,7 +469,7 @@ async function main() {
|
|
|
403
469
|
} else if (!envName && workspace.settings?.defaultEnvironment && !env) {
|
|
404
470
|
console.warn(cliCommon.color(`Warning: default environment "${workspace.settings.defaultEnvironment}" not found. Running without environment.`, cliCommon.C.yellow));
|
|
405
471
|
}
|
|
406
|
-
const version = `v${"0.6.
|
|
472
|
+
const version = `v${"0.6.2"}`;
|
|
407
473
|
console.log("");
|
|
408
474
|
console.log(cliCommon.color(" API Test Runner" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
|
|
409
475
|
console.log(cliCommon.color(` Workspace: ${wsPath}`, cliCommon.C.gray));
|
|
@@ -442,7 +508,21 @@ async function main() {
|
|
|
442
508
|
let firstColName;
|
|
443
509
|
for (const col of collections) {
|
|
444
510
|
if (colName && col.name.toLowerCase() !== colName.toLowerCase()) continue;
|
|
445
|
-
if (col.disabled)
|
|
511
|
+
if (col.disabled) {
|
|
512
|
+
console.log(cliCommon.color(` ┌ ${col.name} (disabled)`, cliCommon.C.gray));
|
|
513
|
+
allResults.push({
|
|
514
|
+
requestId: col.id,
|
|
515
|
+
name: col.name,
|
|
516
|
+
method: "",
|
|
517
|
+
resolvedUrl: "",
|
|
518
|
+
status: "skipped",
|
|
519
|
+
error: "Collection disabled",
|
|
520
|
+
scopePath: []
|
|
521
|
+
});
|
|
522
|
+
summary.total++;
|
|
523
|
+
summary.skipped++;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
446
526
|
let items = requestCollection.buildRunPlan(col, null, filterTags);
|
|
447
527
|
if (items.length === 0) continue;
|
|
448
528
|
if (!firstColName) firstColName = col.name;
|
|
@@ -502,7 +582,9 @@ async function main() {
|
|
|
502
582
|
dispatcher,
|
|
503
583
|
piiMaskPatterns: piiPatterns,
|
|
504
584
|
tls: effectiveTls,
|
|
505
|
-
onScriptOutput
|
|
585
|
+
onScriptOutput,
|
|
586
|
+
expectStatus: item.expectStatus,
|
|
587
|
+
owaspTag: item.owaspTag
|
|
506
588
|
});
|
|
507
589
|
result = out.result;
|
|
508
590
|
runEnvVars = out.updatedEnvVars;
|
|
@@ -548,7 +630,7 @@ async function main() {
|
|
|
548
630
|
if (outputPath) {
|
|
549
631
|
const meta = { workspace: wsPath, environment: env?.name ?? null, collection: firstColName, timestamp };
|
|
550
632
|
const maskedResults = allResults.map(maskResult);
|
|
551
|
-
const report = outputFormat === "junit" ? buildJUnitReport(maskedResults, summary, meta) : outputFormat === "html" ? buildHtmlReport(maskedResults, summary, meta) : buildJsonReport(maskedResults, summary, meta);
|
|
633
|
+
const report = outputFormat === "junit" ? buildJUnitReport(maskedResults, summary, meta) : outputFormat === "html" ? buildHtmlReport(maskedResults, summary, meta) : outputFormat === "sarif" ? buildSarifReport(maskedResults, summary, meta) : buildJsonReport(maskedResults, summary, meta);
|
|
552
634
|
await promises.writeFile(path.resolve(outputPath), report, "utf8");
|
|
553
635
|
console.log(cliCommon.color(` Report written: ${outputPath} (${outputFormat})
|
|
554
636
|
`, cliCommon.C.gray));
|
package/out/main/wsdl.js
CHANGED
|
@@ -4,12 +4,13 @@ const promises = require("fs/promises");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const https = require("https");
|
|
6
6
|
const http = require("http");
|
|
7
|
-
const soapHandler = require("./chunks/soap-handler-
|
|
8
|
-
const _import = require("./chunks/import-
|
|
9
|
-
const cliCommon = require("./chunks/cli-common-
|
|
10
|
-
require("./chunks/handle-
|
|
7
|
+
const soapHandler = require("./chunks/soap-handler-BAPCALkN.js");
|
|
8
|
+
const _import = require("./chunks/import-DdXhO56X.js");
|
|
9
|
+
const cliCommon = require("./chunks/cli-common-vASoDfQH.js");
|
|
10
|
+
require("./chunks/handle-CL-nWtYA.js");
|
|
11
11
|
require("@xmldom/xmldom");
|
|
12
12
|
require("uuid");
|
|
13
|
+
require("./chunks/data-files-Br2h3Dt2.js");
|
|
13
14
|
require("crypto");
|
|
14
15
|
function fetchUrl(url) {
|
|
15
16
|
return new Promise((resolveP, rejectP) => {
|