@testsmith/api-spector 0.0.1

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.
@@ -0,0 +1,4003 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ const electron = require("electron");
25
+ const path = require("path");
26
+ const fs = require("fs");
27
+ const promises = require("fs/promises");
28
+ const scriptRunner = require("./chunks/script-runner-Ci5t2-bo.js");
29
+ const undici = require("undici");
30
+ const crypto = require("crypto");
31
+ const uuid = require("uuid");
32
+ const jsYaml = require("js-yaml");
33
+ const JSZip = require("jszip");
34
+ const mockServer = require("./chunks/mock-server-ZB7zpXh9.js");
35
+ const http = require("http");
36
+ const WebSocket = require("ws");
37
+ const https = require("https");
38
+ const Ajv = require("ajv");
39
+ require("vm");
40
+ require("dayjs");
41
+ require("tv4");
42
+ const LAST_WS_FILE = path.join(electron.app.getPath("userData"), "last-workspace.json");
43
+ async function saveLastWorkspacePath(wsPath) {
44
+ await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: wsPath }), "utf8");
45
+ }
46
+ async function readLastWorkspacePath() {
47
+ try {
48
+ const raw = await promises.readFile(LAST_WS_FILE, "utf8");
49
+ return JSON.parse(raw).path ?? null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+ let workspaceDir = null;
55
+ let workspaceFile = null;
56
+ function atomicWrite(path2, data) {
57
+ return promises.writeFile(path2, data, "utf8");
58
+ }
59
+ function registerFileHandlers(ipc) {
60
+ ipc.handle("file:openWorkspace", async () => {
61
+ const result = await electron.dialog.showOpenDialog({
62
+ title: "Open Workspace",
63
+ filters: [{ name: "api Spector Workspace", extensions: ["spector", "json"] }],
64
+ properties: ["openFile"]
65
+ });
66
+ if (result.canceled || !result.filePaths[0]) return null;
67
+ const wsPath = result.filePaths[0];
68
+ workspaceDir = path.dirname(wsPath);
69
+ workspaceFile = wsPath;
70
+ await scriptRunner.loadGlobals(workspaceDir);
71
+ await saveLastWorkspacePath(wsPath);
72
+ const raw = await promises.readFile(wsPath, "utf8");
73
+ return { workspace: JSON.parse(raw), workspacePath: wsPath };
74
+ });
75
+ ipc.handle("file:newWorkspace", async () => {
76
+ const result = await electron.dialog.showSaveDialog({
77
+ title: "Create Workspace",
78
+ defaultPath: "my-workspace.spector",
79
+ filters: [{ name: "api Spector Workspace", extensions: ["spector", "json"] }]
80
+ });
81
+ if (result.canceled || !result.filePath) return null;
82
+ workspaceDir = path.dirname(result.filePath);
83
+ workspaceFile = result.filePath;
84
+ await scriptRunner.loadGlobals(workspaceDir);
85
+ await promises.mkdir(path.join(workspaceDir, "collections"), { recursive: true });
86
+ await promises.mkdir(path.join(workspaceDir, "environments"), { recursive: true });
87
+ const gitignore = "# api Spector — never commit secrets\n*.secrets\n.env.local\n";
88
+ await atomicWrite(path.join(workspaceDir, ".gitignore"), gitignore);
89
+ const ws = {
90
+ version: "1.0",
91
+ collections: [],
92
+ environments: [],
93
+ activeEnvironmentId: null
94
+ };
95
+ await atomicWrite(result.filePath, JSON.stringify(ws, null, 2));
96
+ await saveLastWorkspacePath(result.filePath);
97
+ return { workspace: ws, workspacePath: result.filePath };
98
+ });
99
+ ipc.handle("file:saveWorkspace", async (_e, ws) => {
100
+ if (!workspaceFile) return;
101
+ await atomicWrite(workspaceFile, JSON.stringify(ws, null, 2));
102
+ });
103
+ ipc.handle("file:loadCollection", async (_e, relPath) => {
104
+ if (!workspaceDir) throw new Error("No workspace open");
105
+ const raw = await promises.readFile(path.resolve(workspaceDir, relPath), "utf8");
106
+ return JSON.parse(raw);
107
+ });
108
+ ipc.handle("file:saveCollection", async (_e, relPath, col) => {
109
+ if (!workspaceDir) throw new Error("No workspace open");
110
+ const fullPath = path.resolve(workspaceDir, relPath);
111
+ await promises.mkdir(path.dirname(fullPath), { recursive: true });
112
+ await atomicWrite(fullPath, JSON.stringify(col, null, 2));
113
+ });
114
+ ipc.handle("file:loadEnvironment", async (_e, relPath) => {
115
+ if (!workspaceDir) throw new Error("No workspace open");
116
+ const raw = await promises.readFile(path.resolve(workspaceDir, relPath), "utf8");
117
+ return JSON.parse(raw);
118
+ });
119
+ ipc.handle("file:saveEnvironment", async (_e, relPath, env) => {
120
+ if (!workspaceDir) throw new Error("No workspace open");
121
+ const fullPath = path.resolve(workspaceDir, relPath);
122
+ await promises.mkdir(path.dirname(fullPath), { recursive: true });
123
+ await atomicWrite(fullPath, JSON.stringify(env, null, 2));
124
+ });
125
+ ipc.handle("dialog:pickDir", async () => {
126
+ const result = await electron.dialog.showOpenDialog({
127
+ title: "Select Output Directory",
128
+ properties: ["openDirectory", "createDirectory"]
129
+ });
130
+ return result.canceled ? null : result.filePaths[0];
131
+ });
132
+ ipc.handle("results:save", async (_e, content, defaultName) => {
133
+ const ext = defaultName.endsWith(".xml") ? "xml" : defaultName.endsWith(".html") ? "html" : "json";
134
+ const allFilters = [
135
+ { name: "JSON", extensions: ["json"] },
136
+ { name: "JUnit XML", extensions: ["xml"] },
137
+ { name: "HTML", extensions: ["html"] }
138
+ ];
139
+ const result = await electron.dialog.showSaveDialog({
140
+ title: "Save Test Results",
141
+ defaultPath: defaultName,
142
+ filters: ext === "xml" ? [allFilters[1], allFilters[0], allFilters[2]] : ext === "html" ? [allFilters[2], allFilters[0], allFilters[1]] : [allFilters[0], allFilters[1], allFilters[2]]
143
+ });
144
+ if (result.canceled || !result.filePath) return false;
145
+ await promises.writeFile(result.filePath, content, "utf8");
146
+ return true;
147
+ });
148
+ ipc.handle("globals:get", () => scriptRunner.getGlobals());
149
+ ipc.handle("globals:set", async (_e, patch) => {
150
+ scriptRunner.setGlobals(patch);
151
+ await scriptRunner.persistGlobals();
152
+ });
153
+ ipc.handle("file:closeWorkspace", async () => {
154
+ workspaceDir = null;
155
+ workspaceFile = null;
156
+ await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: null }), "utf8").catch(() => {
157
+ });
158
+ });
159
+ ipc.handle("file:getLastWorkspace", async () => {
160
+ const wsPath = await readLastWorkspacePath();
161
+ if (!wsPath) return null;
162
+ try {
163
+ workspaceDir = path.dirname(wsPath);
164
+ workspaceFile = wsPath;
165
+ await scriptRunner.loadGlobals(workspaceDir);
166
+ const raw = await promises.readFile(wsPath, "utf8");
167
+ return { workspace: JSON.parse(raw), workspacePath: wsPath };
168
+ } catch {
169
+ return null;
170
+ }
171
+ });
172
+ }
173
+ function getWorkspaceDir() {
174
+ return workspaceDir;
175
+ }
176
+ async function buildAuthHeaders(auth, vars) {
177
+ const headers = {};
178
+ if (auth.type === "bearer") {
179
+ let token = auth.token ?? "";
180
+ if (!token && auth.tokenSecretRef) token = await scriptRunner.getSecret(auth.tokenSecretRef) ?? "";
181
+ token = scriptRunner.interpolate(token, vars);
182
+ if (token) headers["Authorization"] = `Bearer ${token}`;
183
+ }
184
+ if (auth.type === "basic") {
185
+ let password = auth.password ?? "";
186
+ if (!password && auth.passwordSecretRef) password = await scriptRunner.getSecret(auth.passwordSecretRef) ?? "";
187
+ password = scriptRunner.interpolate(password, vars);
188
+ const username = scriptRunner.interpolate(auth.username ?? "", vars);
189
+ headers["Authorization"] = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
190
+ }
191
+ if (auth.type === "apikey" && auth.apiKeyIn === "header") {
192
+ let value = auth.apiKeyValue ?? "";
193
+ if (!value && auth.apiKeySecretRef) value = await scriptRunner.getSecret(auth.apiKeySecretRef) ?? "";
194
+ value = scriptRunner.interpolate(value, vars);
195
+ headers[auth.apiKeyName ?? "X-API-Key"] = value;
196
+ }
197
+ if (auth.type === "oauth2") {
198
+ const now = Date.now();
199
+ if (auth.oauth2CachedToken && auth.oauth2TokenExpiry && auth.oauth2TokenExpiry > now + 5e3) {
200
+ headers["Authorization"] = `Bearer ${auth.oauth2CachedToken}`;
201
+ }
202
+ }
203
+ return headers;
204
+ }
205
+ async function buildApiKeyParam(auth, vars) {
206
+ if (auth.type !== "apikey" || auth.apiKeyIn !== "query") return null;
207
+ let value = auth.apiKeyValue ?? "";
208
+ if (!value && auth.apiKeySecretRef) value = await scriptRunner.getSecret(auth.apiKeySecretRef) ?? "";
209
+ value = scriptRunner.interpolate(value, vars);
210
+ return { key: auth.apiKeyName ?? "apikey", value };
211
+ }
212
+ function parseDigestChallenge(wwwAuth) {
213
+ const extract = (key) => {
214
+ const m = new RegExp(`${key}="([^"]*)"`, "i").exec(wwwAuth);
215
+ return m ? m[1] : "";
216
+ };
217
+ const extractUnquoted = (key) => {
218
+ const m = new RegExp(`${key}=([^,\\s]+)`, "i").exec(wwwAuth);
219
+ return m ? m[1] : "";
220
+ };
221
+ return {
222
+ realm: extract("realm"),
223
+ nonce: extract("nonce"),
224
+ qop: extract("qop") || extractUnquoted("qop") || void 0,
225
+ algorithm: extract("algorithm") || extractUnquoted("algorithm") || "MD5",
226
+ opaque: extract("opaque") || void 0
227
+ };
228
+ }
229
+ function md5(s) {
230
+ return crypto.createHash("md5").update(s).digest("hex");
231
+ }
232
+ function buildDigestAuthHeader(challenge, username, password, method, uri) {
233
+ const { realm, nonce, qop, algorithm, opaque } = challenge;
234
+ const algo = (algorithm ?? "MD5").toUpperCase();
235
+ const ha1 = algo === "MD5-SESS" ? md5(`${md5(`${username}:${realm}:${password}`)}:${nonce}:`) : md5(`${username}:${realm}:${password}`);
236
+ const ha2 = md5(`${method}:${uri}`);
237
+ let response;
238
+ let nc;
239
+ let cnonce;
240
+ if (qop === "auth" || qop === "auth-int") {
241
+ nc = "00000001";
242
+ cnonce = crypto.randomBytes(8).toString("hex");
243
+ response = md5(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`);
244
+ } else {
245
+ response = md5(`${ha1}:${nonce}:${ha2}`);
246
+ }
247
+ let header = `Digest username="${username}", realm="${realm}", nonce="${nonce}", uri="${uri}", response="${response}"`;
248
+ if (qop) header += `, qop=${qop}`;
249
+ if (nc) header += `, nc=${nc}`;
250
+ if (cnonce) header += `, cnonce="${cnonce}"`;
251
+ if (opaque) header += `, opaque="${opaque}"`;
252
+ if (algo !== "MD5") header += `, algorithm=${algo}`;
253
+ return header;
254
+ }
255
+ async function performDigestAuth(url, method, auth, vars, fetchFn) {
256
+ const probeResp = await fetchFn(url, { method, headers: {} });
257
+ if (probeResp.status !== 401) return null;
258
+ const wwwAuth = probeResp.headers.get("www-authenticate") ?? "";
259
+ if (!wwwAuth.toLowerCase().startsWith("digest")) return null;
260
+ const challenge = parseDigestChallenge(wwwAuth);
261
+ let password = auth.password ?? "";
262
+ if (!password && auth.passwordSecretRef) password = await scriptRunner.getSecret(auth.passwordSecretRef) ?? "";
263
+ password = scriptRunner.interpolate(password, vars);
264
+ const username = scriptRunner.interpolate(auth.username ?? "", vars);
265
+ let uri = "/";
266
+ try {
267
+ uri = new URL(url).pathname + (new URL(url).search ?? "");
268
+ } catch {
269
+ }
270
+ return buildDigestAuthHeader(challenge, username, password, method, uri);
271
+ }
272
+ async function performNtlmRequest(_url, _method, _auth, _vars) {
273
+ throw new Error(
274
+ 'NTLM auth is not yet implemented. Add "httpntlm" to package.json dependencies and implement performNtlmRequest in auth-builder.ts.'
275
+ );
276
+ }
277
+ async function fetchOAuth2Token(auth, vars) {
278
+ const flow = auth.oauth2Flow ?? "client_credentials";
279
+ if (flow === "authorization_code") {
280
+ throw new Error("authorization_code flow requires the oauth2:startFlow IPC call from the renderer.");
281
+ }
282
+ if (flow === "implicit") {
283
+ throw new Error("implicit flow cannot be performed server-side — tokens must be obtained via the browser redirect.");
284
+ }
285
+ const tokenUrl = scriptRunner.interpolate(auth.oauth2TokenUrl ?? "", vars);
286
+ if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required.");
287
+ const clientId = scriptRunner.interpolate(auth.oauth2ClientId ?? "", vars);
288
+ let clientSecret = auth.oauth2ClientSecret ?? "";
289
+ if (!clientSecret && auth.oauth2ClientSecretRef) {
290
+ clientSecret = await scriptRunner.getSecret(auth.oauth2ClientSecretRef) ?? "";
291
+ }
292
+ clientSecret = scriptRunner.interpolate(clientSecret, vars);
293
+ const params = new URLSearchParams();
294
+ params.set("grant_type", flow === "password" ? "password" : "client_credentials");
295
+ params.set("client_id", clientId);
296
+ params.set("client_secret", clientSecret);
297
+ if (auth.oauth2Scopes) params.set("scope", auth.oauth2Scopes);
298
+ if (flow === "password") {
299
+ let password = auth.password ?? "";
300
+ if (!password && auth.passwordSecretRef) password = await scriptRunner.getSecret(auth.passwordSecretRef) ?? "";
301
+ password = scriptRunner.interpolate(password, vars);
302
+ params.set("username", scriptRunner.interpolate(auth.username ?? "", vars));
303
+ params.set("password", password);
304
+ }
305
+ const { fetch: nodeFetch } = await import("undici");
306
+ const resp = await nodeFetch(tokenUrl, {
307
+ method: "POST",
308
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
309
+ body: params.toString()
310
+ });
311
+ if (!resp.ok) {
312
+ const body = await resp.text();
313
+ throw new Error(`OAuth 2.0 token request failed (${resp.status}): ${body}`);
314
+ }
315
+ const json = await resp.json();
316
+ const accessToken = String(json["access_token"] ?? "");
317
+ if (!accessToken) throw new Error("OAuth 2.0: token response missing access_token.");
318
+ const expiresIn = Number(json["expires_in"] ?? 3600);
319
+ const expiresAt = Date.now() + expiresIn * 1e3;
320
+ return {
321
+ accessToken,
322
+ expiresAt,
323
+ refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
324
+ };
325
+ }
326
+ function maskPii(data, patterns) {
327
+ if (!patterns.length) return data;
328
+ try {
329
+ const obj = JSON.parse(data);
330
+ const masked = maskObject(obj, patterns);
331
+ return JSON.stringify(masked);
332
+ } catch {
333
+ return data;
334
+ }
335
+ }
336
+ function maskObject(obj, patterns) {
337
+ if (Array.isArray(obj)) return obj.map((item) => maskObject(item, patterns));
338
+ if (obj && typeof obj === "object") {
339
+ const result = {};
340
+ for (const [k, v] of Object.entries(obj)) {
341
+ if (patterns.some((p) => k.toLowerCase().includes(p.toLowerCase()))) {
342
+ result[k] = "[REDACTED]";
343
+ } else {
344
+ result[k] = maskObject(v, patterns);
345
+ }
346
+ }
347
+ return result;
348
+ }
349
+ return obj;
350
+ }
351
+ function maskHeaders(headers, patterns) {
352
+ if (!patterns.length) return headers;
353
+ const alwaysMask = ["authorization", "cookie", "set-cookie"];
354
+ const result = {};
355
+ for (const [k, v] of Object.entries(headers)) {
356
+ const lower = k.toLowerCase();
357
+ if (alwaysMask.includes(lower) || patterns.some((p) => lower.includes(p.toLowerCase()))) {
358
+ result[k] = "[REDACTED]";
359
+ } else {
360
+ result[k] = v;
361
+ }
362
+ }
363
+ return result;
364
+ }
365
+ async function buildDispatcher$1(proxy, tls) {
366
+ const connectOpts = {};
367
+ let hasTls = false;
368
+ if (tls) {
369
+ hasTls = true;
370
+ if (tls.rejectUnauthorized !== void 0) {
371
+ connectOpts["rejectUnauthorized"] = tls.rejectUnauthorized;
372
+ }
373
+ if (tls.caCertPath) {
374
+ try {
375
+ connectOpts["ca"] = await promises.readFile(tls.caCertPath);
376
+ } catch {
377
+ }
378
+ }
379
+ if (tls.clientCertPath) {
380
+ try {
381
+ connectOpts["cert"] = await promises.readFile(tls.clientCertPath);
382
+ } catch {
383
+ }
384
+ }
385
+ if (tls.clientKeyPath) {
386
+ try {
387
+ connectOpts["key"] = await promises.readFile(tls.clientKeyPath);
388
+ } catch {
389
+ }
390
+ }
391
+ }
392
+ if (proxy?.url) {
393
+ const proxyUri = proxy.auth ? proxy.url.replace("://", `://${encodeURIComponent(proxy.auth.username)}:${encodeURIComponent(proxy.auth.password)}@`) : proxy.url;
394
+ return new undici.ProxyAgent({
395
+ uri: proxyUri,
396
+ ...hasTls ? { connect: connectOpts } : {}
397
+ });
398
+ }
399
+ if (hasTls) {
400
+ return new undici.Agent({ connect: connectOpts });
401
+ }
402
+ return void 0;
403
+ }
404
+ function registerRequestHandler(ipc) {
405
+ ipc.handle("request:send", async (_e, payload) => {
406
+ const {
407
+ request: req,
408
+ environment,
409
+ collectionVars,
410
+ globals: payloadGlobals,
411
+ proxy,
412
+ tls,
413
+ piiMaskPatterns = []
414
+ } = payload;
415
+ const start = Date.now();
416
+ const liveGlobals = scriptRunner.getGlobals();
417
+ const mergedGlobals = { ...payloadGlobals, ...liveGlobals };
418
+ const envVars = await scriptRunner.buildEnvVars(environment);
419
+ let localVars = {};
420
+ const decryptionWarnings = [];
421
+ if (environment) {
422
+ const masterKeySet = Boolean(process.env["API_SPECTOR_MASTER_KEY"]);
423
+ for (const v of environment.variables) {
424
+ if (!v.enabled || !v.secret || !v.secretEncrypted) continue;
425
+ if (!masterKeySet) {
426
+ decryptionWarnings.push(`[warn] Secret "${v.key}" was not decrypted: API_SPECTOR_MASTER_KEY is not set. Use the master password modal or export the variable in your shell.`);
427
+ } else if (envVars[v.key] === void 0) {
428
+ decryptionWarnings.push(`[warn] Secret "${v.key}" could not be decrypted: wrong password or corrupted data.`);
429
+ }
430
+ }
431
+ }
432
+ let vars = scriptRunner.mergeVars(envVars, collectionVars, mergedGlobals, localVars);
433
+ let preScriptMeta = { consoleOutput: [] };
434
+ let updatedCollectionVars = { ...collectionVars };
435
+ let updatedEnvVars = { ...envVars };
436
+ let updatedGlobals = { ...mergedGlobals };
437
+ if (req.preRequestScript?.trim()) {
438
+ const result = await scriptRunner.runScript(req.preRequestScript, {
439
+ envVars: { ...envVars },
440
+ collectionVars: { ...collectionVars },
441
+ globals: { ...mergedGlobals },
442
+ localVars: {}
443
+ });
444
+ preScriptMeta = { error: result.error, consoleOutput: result.consoleOutput };
445
+ localVars = result.updatedLocalVars;
446
+ updatedEnvVars = result.updatedEnvVars;
447
+ updatedCollectionVars = result.updatedCollectionVars;
448
+ updatedGlobals = result.updatedGlobals;
449
+ scriptRunner.patchGlobals(result.updatedGlobals);
450
+ await scriptRunner.persistGlobals();
451
+ vars = scriptRunner.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars);
452
+ }
453
+ let response;
454
+ let sentRequest = { method: req.method, url: "", headers: {} };
455
+ const resolvedUrl = scriptRunner.buildUrl(req.url, req.params, vars);
456
+ const secretValues = /* @__PURE__ */ new Set();
457
+ if (environment) {
458
+ for (const v of environment.variables) {
459
+ if (!v.enabled) continue;
460
+ if ((v.secret || v.envRef) && envVars[v.key]) {
461
+ secretValues.add(envVars[v.key]);
462
+ }
463
+ }
464
+ }
465
+ function redactSecrets(s) {
466
+ if (!secretValues.size) return s;
467
+ let result = s;
468
+ for (const secret of secretValues) {
469
+ if (secret) result = result.split(secret).join("[*****]");
470
+ }
471
+ return result;
472
+ }
473
+ function redactSentRequest(sr) {
474
+ const headers = {};
475
+ for (const [k, v] of Object.entries(sr.headers)) {
476
+ headers[k] = redactSecrets(v);
477
+ }
478
+ return {
479
+ method: sr.method,
480
+ url: redactSecrets(sr.url),
481
+ headers,
482
+ body: sr.body !== void 0 ? redactSecrets(sr.body) : void 0
483
+ };
484
+ }
485
+ try {
486
+ const dispatcher = await buildDispatcher$1(proxy, tls);
487
+ if (req.auth.type === "oauth2") {
488
+ const now = Date.now();
489
+ const tokenMissing = !req.auth.oauth2CachedToken;
490
+ const tokenExpired = req.auth.oauth2TokenExpiry ? req.auth.oauth2TokenExpiry <= now + 5e3 : true;
491
+ if (tokenMissing || tokenExpired) {
492
+ const result = await fetchOAuth2Token(req.auth, vars);
493
+ req.auth.oauth2CachedToken = result.accessToken;
494
+ req.auth.oauth2TokenExpiry = result.expiresAt;
495
+ }
496
+ }
497
+ const authHeaders = await buildAuthHeaders(req.auth, vars);
498
+ const apiKeyParam = await buildApiKeyParam(req.auth, vars);
499
+ let finalUrl = resolvedUrl;
500
+ if (apiKeyParam) {
501
+ const sep = finalUrl.includes("?") ? "&" : "?";
502
+ finalUrl += `${sep}${encodeURIComponent(apiKeyParam.key)}=${encodeURIComponent(apiKeyParam.value)}`;
503
+ }
504
+ const buildHeaders2 = () => {
505
+ const h = new undici.Headers();
506
+ for (const header of req.headers) {
507
+ if (header.enabled && header.key) {
508
+ h.set(scriptRunner.interpolate(header.key, vars), scriptRunner.interpolate(header.value, vars));
509
+ }
510
+ }
511
+ for (const [k, v] of Object.entries(authHeaders)) h.set(k, v);
512
+ return h;
513
+ };
514
+ let body;
515
+ if (req.body.mode === "json" && req.body.json) {
516
+ body = scriptRunner.interpolate(req.body.json, vars);
517
+ } else if (req.body.mode === "form" && req.body.form) {
518
+ body = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${encodeURIComponent(scriptRunner.interpolate(p.key, vars))}=${encodeURIComponent(scriptRunner.interpolate(p.value, vars))}`).join("&");
519
+ } else if (req.body.mode === "raw" && req.body.raw) {
520
+ body = scriptRunner.interpolate(req.body.raw, vars);
521
+ } else if (req.body.mode === "graphql" && req.body.graphql) {
522
+ const gql = req.body.graphql;
523
+ const gqlBody = { query: scriptRunner.interpolate(gql.query, vars) };
524
+ const rawVars = gql.variables?.trim();
525
+ if (rawVars) {
526
+ try {
527
+ gqlBody.variables = JSON.parse(scriptRunner.interpolate(rawVars, vars));
528
+ } catch {
529
+ }
530
+ }
531
+ if (gql.operationName?.trim()) gqlBody.operationName = gql.operationName.trim();
532
+ body = JSON.stringify(gqlBody);
533
+ } else if (req.body.mode === "soap" && req.body.soap) {
534
+ const soap = req.body.soap;
535
+ body = scriptRunner.interpolate(soap.envelope, vars);
536
+ }
537
+ const methodHasBody = !["GET", "HEAD"].includes(req.method);
538
+ const doFetch = async (overrideHeaders) => {
539
+ const h = overrideHeaders ?? buildHeaders2();
540
+ if (body !== void 0) {
541
+ if (!h.has("content-type")) {
542
+ if (req.body.mode === "json" || req.body.mode === "graphql") h.set("Content-Type", "application/json");
543
+ else if (req.body.mode === "form") h.set("Content-Type", "application/x-www-form-urlencoded");
544
+ else if (req.body.mode === "raw") h.set("Content-Type", req.body.rawContentType ?? "text/plain");
545
+ else if (req.body.mode === "soap") h.set("Content-Type", "text/xml; charset=utf-8");
546
+ }
547
+ if (req.body.mode === "soap" && req.body.soap?.soapAction && !h.has("soapaction")) {
548
+ h.set("SOAPAction", req.body.soap.soapAction);
549
+ }
550
+ }
551
+ const capturedHeaders = {};
552
+ h.forEach((value, key) => {
553
+ capturedHeaders[key] = value;
554
+ });
555
+ sentRequest = { method: req.method, url: finalUrl, headers: capturedHeaders, body: methodHasBody ? body : void 0 };
556
+ return undici.fetch(finalUrl, {
557
+ method: req.method,
558
+ headers: h,
559
+ body: methodHasBody ? body : void 0,
560
+ dispatcher
561
+ });
562
+ };
563
+ let fetchResp;
564
+ if (req.auth.type === "ntlm") {
565
+ await performNtlmRequest(finalUrl, req.method, req.auth, vars);
566
+ fetchResp = await doFetch();
567
+ } else if (req.auth.type === "digest") {
568
+ const probeFetch = async (url, init) => {
569
+ return undici.fetch(url, {
570
+ ...init,
571
+ dispatcher
572
+ });
573
+ };
574
+ const digestHeader = await performDigestAuth(finalUrl, req.method, req.auth, vars, probeFetch);
575
+ const h = buildHeaders2();
576
+ if (digestHeader) h.set("Authorization", digestHeader);
577
+ fetchResp = await doFetch(h);
578
+ } else {
579
+ fetchResp = await doFetch();
580
+ }
581
+ const responseBody = await fetchResp.text();
582
+ const durationMs = Date.now() - start;
583
+ const rawResponseHeaders = {};
584
+ fetchResp.headers.forEach((value, key) => {
585
+ rawResponseHeaders[key] = value;
586
+ });
587
+ const maskedBody = maskPii(responseBody, piiMaskPatterns);
588
+ const maskedHeaders = maskHeaders(rawResponseHeaders, piiMaskPatterns);
589
+ response = {
590
+ status: fetchResp.status,
591
+ statusText: fetchResp.statusText,
592
+ headers: maskedHeaders,
593
+ body: maskedBody,
594
+ bodySize: Buffer.byteLength(responseBody, "utf8"),
595
+ durationMs
596
+ };
597
+ } catch (err) {
598
+ response = {
599
+ status: 0,
600
+ statusText: "Error",
601
+ headers: {},
602
+ body: "",
603
+ bodySize: 0,
604
+ durationMs: Date.now() - start,
605
+ error: err instanceof Error ? err.message : String(err)
606
+ };
607
+ }
608
+ let postTestResults = [];
609
+ let postConsole = [];
610
+ let postError;
611
+ if (req.postRequestScript?.trim() && !response.error) {
612
+ const result = await scriptRunner.runScript(req.postRequestScript, {
613
+ envVars: { ...updatedEnvVars },
614
+ collectionVars: { ...updatedCollectionVars },
615
+ globals: { ...updatedGlobals },
616
+ localVars: { ...localVars },
617
+ response
618
+ });
619
+ postTestResults = result.testResults;
620
+ postConsole = result.consoleOutput;
621
+ postError = result.error;
622
+ updatedEnvVars = result.updatedEnvVars;
623
+ updatedCollectionVars = result.updatedCollectionVars;
624
+ updatedGlobals = result.updatedGlobals;
625
+ scriptRunner.patchGlobals(result.updatedGlobals);
626
+ await scriptRunner.persistGlobals();
627
+ }
628
+ const scriptResult = {
629
+ testResults: postTestResults,
630
+ consoleOutput: [...decryptionWarnings, ...preScriptMeta.consoleOutput, ...postConsole],
631
+ updatedEnvVars,
632
+ updatedCollectionVars,
633
+ updatedGlobals,
634
+ resolvedUrl,
635
+ preScriptError: preScriptMeta.error,
636
+ postScriptError: postError
637
+ };
638
+ return { response, scriptResult, sentRequest: redactSentRequest(sentRequest) };
639
+ });
640
+ }
641
+ const POSTMAN_RULES = [
642
+ // Variables — environment
643
+ [/\bpm\.environment\.get\(/g, "sp.environment.get("],
644
+ [/\bpm\.environment\.set\(/g, "sp.environment.set("],
645
+ [/\bpm\.environment\.unset\(/g, "sp.environment.clear("],
646
+ [/\bpm\.environment\.has\(/g, "sp.environment.has("],
647
+ // Variables — collection
648
+ [/\bpm\.collectionVariables\.get\(/g, "sp.collectionVariables.get("],
649
+ [/\bpm\.collectionVariables\.set\(/g, "sp.collectionVariables.set("],
650
+ [/\bpm\.collectionVariables\.unset\(/g, "sp.collectionVariables.clear("],
651
+ [/\bpm\.collectionVariables\.has\(/g, "sp.collectionVariables.has("],
652
+ // Variables — globals
653
+ [/\bpm\.globals\.get\(/g, "sp.globals.get("],
654
+ [/\bpm\.globals\.set\(/g, "sp.globals.set("],
655
+ [/\bpm\.globals\.unset\(/g, "sp.globals.clear("],
656
+ [/\bpm\.globals\.has\(/g, "sp.globals.has("],
657
+ // Variables — local/request scope
658
+ [/\bpm\.variables\.get\(/g, "sp.variables.get("],
659
+ [/\bpm\.variables\.set\(/g, "sp.variables.set("],
660
+ // Tests & assertions
661
+ [/\bpm\.test\(/g, "sp.test("],
662
+ [/\bpm\.expect\(/g, "sp.expect("],
663
+ // Response — status
664
+ [/\bpm\.response\.to\.have\.status\(/g, "sp.response.to.have.status("],
665
+ [/\bpm\.response\.code\b/g, "sp.response.code"],
666
+ [/\bpm\.response\.status\b/g, "sp.response.status"],
667
+ [/\bpm\.response\.responseTime\b/g, "sp.response.responseTime"],
668
+ // Response — headers
669
+ [/\bpm\.response\.headers\.get\(/g, "sp.response.headers.get("],
670
+ // Response — body
671
+ [/\bpm\.response\.json\(\)/g, "sp.response.json()"],
672
+ [/\bpm\.response\.text\(\)/g, "sp.response.text()"]
673
+ ];
674
+ const BRUNO_RULES = [
675
+ // Variables — environment
676
+ [/\bbru\.getEnvVar\(/g, "sp.environment.get("],
677
+ [/\bbru\.setEnvVar\(/g, "sp.environment.set("],
678
+ [/\bbru\.deleteEnvVar\(/g, "sp.environment.clear("],
679
+ // Variables — collection
680
+ [/\bbru\.getCollectionVar\(/g, "sp.collectionVariables.get("],
681
+ [/\bbru\.setCollectionVar\(/g, "sp.collectionVariables.set("],
682
+ [/\bbru\.deleteCollectionVar\(/g, "sp.collectionVariables.clear("],
683
+ // Variables — local/request scope
684
+ [/\bbru\.getVar\(/g, "sp.variables.get("],
685
+ [/\bbru\.setVar\(/g, "sp.variables.set("],
686
+ [/\bbru\.deleteVar\(/g, "sp.variables.clear("],
687
+ // Response — body (order matters: specific first)
688
+ [/\bres\.getBody\(\)\.json\(\)/g, "sp.response.json()"],
689
+ [/\bres\.getBody\(\)\.text\(\)/g, "sp.response.text()"],
690
+ [/\bres\.getBody\(\)/g, "sp.response.json()"],
691
+ // Response — status / headers / time
692
+ [/\bres\.getStatus\(\)/g, "sp.response.code"],
693
+ [/\bres\.getHeader\(/g, "sp.response.headers.get("],
694
+ [/\bres\.getResponseTime\(\)/g, "sp.response.responseTime"],
695
+ // Tests & assertions (bare expect/test — Bruno exposes them as globals)
696
+ [/(?<!\.)(?<!\w)expect\(/g, "sp.expect("],
697
+ [/(?<!\.)(?<!\w)test\(/g, "sp.test("]
698
+ ];
699
+ const INSOMNIA_RULES = [
700
+ // Older SDK: insomnia.*
701
+ [/\binsomnia\.environment\.get\(/g, "sp.environment.get("],
702
+ [/\binsomnia\.environment\.set\(/g, "sp.environment.set("],
703
+ [/\binsomnia\.environment\.getItem\(/g, "sp.environment.get("],
704
+ [/\binsomnia\.environment\.setItem\(/g, "sp.environment.set("],
705
+ // Newer SDK: context.*
706
+ [/\bcontext\.environment\.get\(/g, "sp.environment.get("],
707
+ [/\bcontext\.environment\.set\(/g, "sp.environment.set("],
708
+ [/\bcontext\.environment\.getItem\(/g, "sp.environment.get("],
709
+ [/\bcontext\.environment\.setItem\(/g, "sp.environment.set("],
710
+ // Response
711
+ [/\bcontext\.response\.getBody\(\)\.json\(\)/g, "sp.response.json()"],
712
+ [/\bcontext\.response\.getBody\(\)/g, "sp.response.text()"],
713
+ [/\bcontext\.response\.getStatusCode\(\)/g, "sp.response.code"],
714
+ [/\bcontext\.response\.getHeader\(/g, "sp.response.headers.get("]
715
+ ];
716
+ function translateScript(source, format) {
717
+ const rules = format === "postman" ? POSTMAN_RULES : format === "bruno" ? BRUNO_RULES : INSOMNIA_RULES;
718
+ return rules.reduce((src, [pattern, replacement]) => src.replace(pattern, replacement), source);
719
+ }
720
+ function parseHeaders$1(raw) {
721
+ return (raw ?? []).map((h) => ({
722
+ key: h.key ?? "",
723
+ value: h.value ?? "",
724
+ enabled: !h.disabled,
725
+ description: h.description ?? ""
726
+ }));
727
+ }
728
+ function parseParams$1(urlObj) {
729
+ if (!urlObj || typeof urlObj === "string") return [];
730
+ return (urlObj.query ?? []).map((p) => ({
731
+ key: p.key ?? "",
732
+ value: p.value ?? "",
733
+ enabled: !p.disabled,
734
+ description: p.description ?? ""
735
+ }));
736
+ }
737
+ function parseUrl(urlObj) {
738
+ if (!urlObj) return "";
739
+ if (typeof urlObj === "string") return urlObj;
740
+ return urlObj.raw ?? "";
741
+ }
742
+ function parseBody$1(body) {
743
+ if (!body) return { mode: "none" };
744
+ const mode = body.mode ?? "none";
745
+ if (mode === "raw") {
746
+ const lang = body.options?.raw?.language ?? "text";
747
+ if (lang === "json") return { mode: "json", json: body.raw ?? "" };
748
+ return { mode: "raw", raw: body.raw ?? "", rawContentType: "text/plain" };
749
+ }
750
+ if (mode === "urlencoded") {
751
+ return {
752
+ mode: "form",
753
+ form: (body.urlencoded ?? []).map((p) => ({
754
+ key: p.key ?? "",
755
+ value: p.value ?? "",
756
+ enabled: !p.disabled
757
+ }))
758
+ };
759
+ }
760
+ return { mode: "none" };
761
+ }
762
+ function parseAuth$1(auth) {
763
+ if (!auth) return { type: "none" };
764
+ const type = auth.type ?? "noauth";
765
+ if (type === "noauth" || type === "none") return { type: "none" };
766
+ if (type === "bearer") {
767
+ const entries = Object.fromEntries((auth.bearer ?? []).map((e) => [e.key, e.value]));
768
+ return { type: "bearer", token: entries.token ?? "" };
769
+ }
770
+ if (type === "basic") {
771
+ const entries = Object.fromEntries((auth.basic ?? []).map((e) => [e.key, e.value]));
772
+ return { type: "basic", username: entries.username ?? "", password: entries.password ?? "" };
773
+ }
774
+ if (type === "apikey") {
775
+ const entries = Object.fromEntries((auth.apikey ?? []).map((e) => [e.key, e.value]));
776
+ return {
777
+ type: "apikey",
778
+ apiKeyName: entries.key ?? "X-API-Key",
779
+ apiKeyValue: entries.value ?? "",
780
+ apiKeyIn: entries.in ?? "header"
781
+ };
782
+ }
783
+ return { type: "none" };
784
+ }
785
+ function translateMaybe(src, format) {
786
+ return src !== void 0 ? translateScript(src, format) : void 0;
787
+ }
788
+ function parseRequest(item, collectionAuth) {
789
+ const req = typeof item.request === "string" ? { url: item.request, method: "GET" } : item.request ?? {};
790
+ return {
791
+ id: uuid.v4(),
792
+ name: item.name ?? "Request",
793
+ method: (req.method ?? "GET").toUpperCase(),
794
+ url: parseUrl(req.url),
795
+ headers: parseHeaders$1(req.header),
796
+ params: parseParams$1(req.url),
797
+ auth: parseAuth$1(req.auth ?? collectionAuth),
798
+ body: parseBody$1(req.body),
799
+ description: req.description ?? "",
800
+ preRequestScript: translateMaybe(parseScript(item.event, "prerequest"), "postman"),
801
+ postRequestScript: translateMaybe(parseScript(item.event, "test"), "postman"),
802
+ meta: {}
803
+ };
804
+ }
805
+ function parseScript(events, listen) {
806
+ if (!Array.isArray(events)) return void 0;
807
+ const event = events.find((e) => e.listen === listen);
808
+ const exec = event?.script?.exec;
809
+ if (!exec) return void 0;
810
+ const src = Array.isArray(exec) ? exec.join("\n") : String(exec);
811
+ return src.trim() || void 0;
812
+ }
813
+ function parseFolder(item, requests, collectionAuth) {
814
+ const folder = {
815
+ id: uuid.v4(),
816
+ name: item.name ?? "Folder",
817
+ description: item.description ?? "",
818
+ folders: [],
819
+ requestIds: []
820
+ };
821
+ for (const child of item.item ?? []) {
822
+ if (Array.isArray(child.item)) {
823
+ folder.folders.push(parseFolder(child, requests, collectionAuth));
824
+ } else {
825
+ const req = parseRequest(child, collectionAuth);
826
+ requests[req.id] = req;
827
+ folder.requestIds.push(req.id);
828
+ }
829
+ }
830
+ return folder;
831
+ }
832
+ async function importPostman(filePath) {
833
+ const raw = await promises.readFile(filePath, "utf8");
834
+ const data = JSON.parse(raw);
835
+ const info = data.info ?? {};
836
+ const collectionAuth = data.auth;
837
+ const requests = {};
838
+ const rootFolder = {
839
+ id: uuid.v4(),
840
+ name: "root",
841
+ description: "",
842
+ folders: [],
843
+ requestIds: []
844
+ };
845
+ for (const item of data.item ?? []) {
846
+ if (Array.isArray(item.item)) {
847
+ rootFolder.folders.push(parseFolder(item, requests, collectionAuth));
848
+ } else {
849
+ const req = parseRequest(item, collectionAuth);
850
+ requests[req.id] = req;
851
+ rootFolder.requestIds.push(req.id);
852
+ }
853
+ }
854
+ return {
855
+ version: "1.0",
856
+ id: uuid.v4(),
857
+ name: info.name ?? "Imported Collection",
858
+ description: info.description ?? "",
859
+ rootFolder,
860
+ requests
861
+ };
862
+ }
863
+ async function loadSpec$1(filePath) {
864
+ const raw = await promises.readFile(filePath, "utf8");
865
+ if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
866
+ return jsYaml.load(raw);
867
+ }
868
+ return JSON.parse(raw);
869
+ }
870
+ async function loadSpecFromUrl(url) {
871
+ const resp = await undici.fetch(url);
872
+ if (!resp.ok) throw new Error(`HTTP ${resp.status} fetching ${url}`);
873
+ const text = await resp.text();
874
+ const ct = resp.headers.get("content-type") ?? "";
875
+ if (ct.includes("yaml") || url.endsWith(".yaml") || url.endsWith(".yml")) {
876
+ return jsYaml.load(text);
877
+ }
878
+ return JSON.parse(text);
879
+ }
880
+ function resolveRef$1(spec, ref) {
881
+ const parts = ref.replace(/^#\//, "").split("/");
882
+ return parts.reduce((obj, key) => obj?.[decodeURIComponent(key.replace(/~1/g, "/").replace(/~0/g, "~"))], spec);
883
+ }
884
+ function resolve(spec, obj, seen = /* @__PURE__ */ new Set()) {
885
+ if (!obj || typeof obj !== "object") return obj;
886
+ if (seen.has(obj)) return {};
887
+ if (Array.isArray(obj)) {
888
+ seen.add(obj);
889
+ return obj.map((item) => resolve(spec, item, seen));
890
+ }
891
+ if ("$ref" in obj) {
892
+ const target = resolveRef$1(spec, obj.$ref);
893
+ if (!target || seen.has(target)) return {};
894
+ return resolve(spec, target, seen);
895
+ }
896
+ seen.add(obj);
897
+ return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, resolve(spec, v, seen)]));
898
+ }
899
+ function schemaToExample(schema) {
900
+ if (!schema) return {};
901
+ if ("example" in schema) return schema.example;
902
+ if ("default" in schema) return schema.default;
903
+ switch (schema.type) {
904
+ case "object": {
905
+ const props = schema.properties ?? {};
906
+ return Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToExample(v)]));
907
+ }
908
+ case "array":
909
+ return [schemaToExample(schema.items ?? {})];
910
+ case "string":
911
+ return "string";
912
+ case "integer":
913
+ return 0;
914
+ case "number":
915
+ return 0;
916
+ case "boolean":
917
+ return true;
918
+ default:
919
+ return null;
920
+ }
921
+ }
922
+ function buildBody(operation, spec) {
923
+ const content = resolve(spec, operation.requestBody?.content ?? {});
924
+ if ("application/json" in content) {
925
+ const schema = resolve(spec, content["application/json"].schema ?? {});
926
+ const example = schemaToExample(schema);
927
+ return { mode: "json", json: JSON.stringify(example, null, 2) };
928
+ }
929
+ return { mode: "none" };
930
+ }
931
+ function buildParams(operation) {
932
+ return (operation.parameters ?? []).filter((p) => p.in === "query").map((p) => ({
933
+ key: p.name,
934
+ value: String(schemaToExample(p.schema ?? {}) ?? ""),
935
+ enabled: p.required ?? false,
936
+ description: p.description ?? ""
937
+ }));
938
+ }
939
+ function buildHeaders(operation) {
940
+ return (operation.parameters ?? []).filter((p) => p.in === "header").map((p) => ({
941
+ key: p.name,
942
+ value: "",
943
+ enabled: true,
944
+ description: p.description ?? ""
945
+ }));
946
+ }
947
+ function buildAuth(security, securitySchemes) {
948
+ for (const req of security ?? []) {
949
+ for (const schemeName of Object.keys(req)) {
950
+ const scheme = securitySchemes[schemeName];
951
+ if (!scheme) continue;
952
+ if (scheme.type === "http") {
953
+ if (scheme.scheme === "bearer") return { type: "bearer", token: "" };
954
+ if (scheme.scheme === "basic") return { type: "basic", username: "", password: "" };
955
+ }
956
+ if (scheme.type === "apiKey") {
957
+ return {
958
+ type: "apikey",
959
+ apiKeyName: scheme.name ?? "X-API-Key",
960
+ apiKeyValue: "",
961
+ apiKeyIn: scheme.in ?? "header"
962
+ };
963
+ }
964
+ }
965
+ }
966
+ return { type: "none" };
967
+ }
968
+ const HTTP_METHODS$1 = ["get", "post", "put", "patch", "delete", "head", "options"];
969
+ function buildCollection(spec) {
970
+ const info = spec.info ?? {};
971
+ const servers = spec.servers ?? [{}];
972
+ const baseUrl = servers[0]?.url ?? "";
973
+ const securitySchemes = resolve(spec, spec.components?.securitySchemes ?? {});
974
+ const globalSecurity = spec.security ?? [];
975
+ const requests = {};
976
+ const foldersByTag = {};
977
+ for (const [pathStr, pathItem] of Object.entries(spec.paths ?? {})) {
978
+ const resolved = resolve(spec, pathItem);
979
+ const pathLevelParams = resolved.parameters ?? [];
980
+ for (const method of HTTP_METHODS$1) {
981
+ const operation = resolved[method];
982
+ if (!operation) continue;
983
+ const tags = operation.tags?.length ? operation.tags : ["default"];
984
+ const tag = tags[0];
985
+ const allParams = [...pathLevelParams, ...operation.parameters ?? []];
986
+ const opWithParams = { ...operation, parameters: allParams };
987
+ const security = operation.security ?? globalSecurity;
988
+ const req = {
989
+ id: uuid.v4(),
990
+ name: operation.summary ?? operation.operationId ?? `${method.toUpperCase()} ${pathStr}`,
991
+ method: method.toUpperCase(),
992
+ url: `${baseUrl}${pathStr}`,
993
+ headers: buildHeaders(opWithParams),
994
+ params: buildParams(opWithParams),
995
+ auth: buildAuth(security, securitySchemes),
996
+ body: buildBody(opWithParams, spec),
997
+ description: operation.description ?? "",
998
+ meta: { tags }
999
+ };
1000
+ requests[req.id] = req;
1001
+ if (!foldersByTag[tag]) {
1002
+ foldersByTag[tag] = { id: uuid.v4(), name: tag, description: "", folders: [], requestIds: [] };
1003
+ }
1004
+ foldersByTag[tag].requestIds.push(req.id);
1005
+ }
1006
+ }
1007
+ return {
1008
+ version: "1.0",
1009
+ id: uuid.v4(),
1010
+ name: info.title ?? "Imported API",
1011
+ description: info.description ?? "",
1012
+ rootFolder: { id: uuid.v4(), name: "root", description: "", folders: Object.values(foldersByTag), requestIds: [] },
1013
+ requests
1014
+ };
1015
+ }
1016
+ async function importOpenApi(filePath) {
1017
+ return buildCollection(await loadSpec$1(filePath));
1018
+ }
1019
+ async function importOpenApiFromUrl(url) {
1020
+ return buildCollection(await loadSpecFromUrl(url));
1021
+ }
1022
+ function parseHeaders(headers) {
1023
+ return (headers ?? []).map((h) => ({
1024
+ key: h.name ?? "",
1025
+ value: h.value ?? "",
1026
+ enabled: !h.disabled,
1027
+ description: h.description ?? ""
1028
+ }));
1029
+ }
1030
+ function parseParams(params) {
1031
+ return (params ?? []).map((p) => ({
1032
+ key: p.name ?? "",
1033
+ value: p.value ?? "",
1034
+ enabled: !p.disabled,
1035
+ description: p.description ?? ""
1036
+ }));
1037
+ }
1038
+ function parseBody(body) {
1039
+ if (!body) return { mode: "none" };
1040
+ const mime = body.mimeType ?? "";
1041
+ if (mime.includes("json") || mime === "application/json") {
1042
+ return { mode: "json", json: body.text ?? "{}" };
1043
+ }
1044
+ if (mime === "application/graphql") {
1045
+ return {
1046
+ mode: "graphql",
1047
+ graphql: { query: body.text ?? "", variables: "{}" }
1048
+ };
1049
+ }
1050
+ if (mime === "application/x-www-form-urlencoded") {
1051
+ return {
1052
+ mode: "form",
1053
+ form: (body.params ?? []).map((p) => ({
1054
+ key: p.name ?? "",
1055
+ value: p.value ?? "",
1056
+ enabled: !p.disabled
1057
+ }))
1058
+ };
1059
+ }
1060
+ if (body.text) {
1061
+ return { mode: "raw", raw: body.text, rawContentType: mime || "text/plain" };
1062
+ }
1063
+ return { mode: "none" };
1064
+ }
1065
+ function parseAuth(auth) {
1066
+ if (!auth || !auth.type || auth.type === "none") return { type: "none" };
1067
+ if (auth.type === "bearer") {
1068
+ return { type: "bearer", token: auth.token ?? "" };
1069
+ }
1070
+ if (auth.type === "basic") {
1071
+ return { type: "basic", username: auth.username ?? "", password: auth.password ?? "" };
1072
+ }
1073
+ if (auth.type === "apikey") {
1074
+ return {
1075
+ type: "apikey",
1076
+ apiKeyName: auth.key ?? "X-API-Key",
1077
+ apiKeyValue: auth.value ?? "",
1078
+ apiKeyIn: auth.addTo === "queryParams" ? "query" : "header"
1079
+ };
1080
+ }
1081
+ return { type: "none" };
1082
+ }
1083
+ async function importInsomnia(filePath) {
1084
+ const raw = await promises.readFile(filePath, "utf8");
1085
+ const data = JSON.parse(raw);
1086
+ const resources = data.resources ?? [];
1087
+ const workspace = resources.find((r) => r._type === "workspace") ?? {};
1088
+ const workspaceId = workspace._id ?? "";
1089
+ const requests = {};
1090
+ const folderById = {};
1091
+ const rootFolder = { id: uuid.v4(), name: "root", description: "", folders: [], requestIds: [] };
1092
+ for (const r of resources) {
1093
+ if (r._type !== "request_group") continue;
1094
+ folderById[r._id] = {
1095
+ id: uuid.v4(),
1096
+ name: r.name ?? "Folder",
1097
+ description: r.description ?? "",
1098
+ folders: [],
1099
+ requestIds: []
1100
+ };
1101
+ }
1102
+ for (const r of resources) {
1103
+ if (r._type !== "request") continue;
1104
+ const req = {
1105
+ id: uuid.v4(),
1106
+ name: r.name ?? "Request",
1107
+ method: (r.method ?? "GET").toUpperCase(),
1108
+ url: r.url ?? "",
1109
+ headers: parseHeaders(r.headers),
1110
+ params: parseParams(r.parameters),
1111
+ auth: parseAuth(r.authentication),
1112
+ body: parseBody(r.body),
1113
+ description: r.description ?? "",
1114
+ preRequestScript: r.preRequestScript ? translateScript(String(r.preRequestScript).trim(), "insomnia") || void 0 : void 0,
1115
+ postRequestScript: r.afterResponseScript ? translateScript(String(r.afterResponseScript).trim(), "insomnia") || void 0 : void 0,
1116
+ meta: {}
1117
+ };
1118
+ requests[req.id] = req;
1119
+ if (folderById[r.parentId]) {
1120
+ folderById[r.parentId].requestIds.push(req.id);
1121
+ } else {
1122
+ rootFolder.requestIds.push(req.id);
1123
+ }
1124
+ }
1125
+ for (const r of resources) {
1126
+ if (r._type !== "request_group") continue;
1127
+ const folder = folderById[r._id];
1128
+ if (!folder) continue;
1129
+ const isTopLevel = r.parentId === workspaceId || !folderById[r.parentId];
1130
+ if (isTopLevel) {
1131
+ rootFolder.folders.push(folder);
1132
+ } else {
1133
+ folderById[r.parentId].folders.push(folder);
1134
+ }
1135
+ }
1136
+ return {
1137
+ version: "1.0",
1138
+ id: uuid.v4(),
1139
+ name: workspace.name ?? "Imported Collection",
1140
+ description: workspace.description ?? "",
1141
+ rootFolder,
1142
+ requests
1143
+ };
1144
+ }
1145
+ function extractBlock(content, blockName) {
1146
+ const lines = content.split("\n");
1147
+ for (let i = 0; i < lines.length; i++) {
1148
+ if (lines[i].trim() === `${blockName} {`) {
1149
+ let depth = 1;
1150
+ const blockLines = [];
1151
+ i++;
1152
+ while (i < lines.length && depth > 0) {
1153
+ const trimmed = lines[i].trim();
1154
+ if (trimmed === "{") depth++;
1155
+ if (trimmed === "}") {
1156
+ depth--;
1157
+ if (depth === 0) break;
1158
+ }
1159
+ blockLines.push(lines[i]);
1160
+ i++;
1161
+ }
1162
+ return blockLines.join("\n");
1163
+ }
1164
+ }
1165
+ return null;
1166
+ }
1167
+ function parseKv(blockContent) {
1168
+ if (!blockContent) return {};
1169
+ const result = {};
1170
+ for (const line of blockContent.split("\n")) {
1171
+ const trimmed = line.trim();
1172
+ if (!trimmed || trimmed.startsWith("#")) continue;
1173
+ const colonIdx = trimmed.indexOf(":");
1174
+ if (colonIdx > 0) {
1175
+ const key = trimmed.slice(0, colonIdx).trim();
1176
+ const val = trimmed.slice(colonIdx + 1).trim();
1177
+ if (key) result[key] = val;
1178
+ }
1179
+ }
1180
+ return result;
1181
+ }
1182
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
1183
+ function parseBruFile(content, fileName) {
1184
+ const meta = parseKv(extractBlock(content, "meta"));
1185
+ let method = "GET";
1186
+ let methodAttrs = {};
1187
+ for (const m of HTTP_METHODS) {
1188
+ const block = extractBlock(content, m);
1189
+ if (block !== null) {
1190
+ method = m.toUpperCase();
1191
+ methodAttrs = parseKv(block);
1192
+ break;
1193
+ }
1194
+ }
1195
+ const url = methodAttrs["url"] ?? "";
1196
+ const bodyMode = methodAttrs["body"] ?? "none";
1197
+ const authMode = methodAttrs["auth"] ?? "none";
1198
+ const headers = Object.entries(parseKv(extractBlock(content, "headers"))).map(([k, v]) => ({
1199
+ key: k,
1200
+ value: v,
1201
+ enabled: true
1202
+ }));
1203
+ const params = Object.entries(parseKv(extractBlock(content, "params:query"))).map(([k, v]) => ({
1204
+ key: k,
1205
+ value: v,
1206
+ enabled: true
1207
+ }));
1208
+ let body = { mode: "none" };
1209
+ if (bodyMode === "json") {
1210
+ const jsonContent = extractBlock(content, "body:json") ?? "";
1211
+ body = { mode: "json", json: jsonContent.trim() };
1212
+ } else if (bodyMode === "text") {
1213
+ const rawContent = extractBlock(content, "body:text") ?? "";
1214
+ body = { mode: "raw", raw: rawContent.trim(), rawContentType: "text/plain" };
1215
+ } else if (bodyMode === "formUrlEncoded") {
1216
+ const form = Object.entries(parseKv(extractBlock(content, "body:form-urlencoded"))).map(([k, v]) => ({
1217
+ key: k,
1218
+ value: v,
1219
+ enabled: true
1220
+ }));
1221
+ body = { mode: "form", form };
1222
+ } else if (bodyMode === "graphql") {
1223
+ const gqlContent = extractBlock(content, "body:graphql") ?? "";
1224
+ body = { mode: "graphql", graphql: { query: gqlContent.trim(), variables: "{}" } };
1225
+ }
1226
+ let auth = { type: "none" };
1227
+ if (authMode === "bearer") {
1228
+ const bearerAttrs = parseKv(extractBlock(content, "auth:bearer"));
1229
+ auth = { type: "bearer", token: bearerAttrs["token"] ?? "" };
1230
+ } else if (authMode === "basic") {
1231
+ const basicAttrs = parseKv(extractBlock(content, "auth:basic"));
1232
+ auth = { type: "basic", username: basicAttrs["username"] ?? "", password: basicAttrs["password"] ?? "" };
1233
+ } else if (authMode === "apikey") {
1234
+ const apiKeyAttrs = parseKv(extractBlock(content, "auth:apikey"));
1235
+ auth = {
1236
+ type: "apikey",
1237
+ apiKeyName: apiKeyAttrs["key"] ?? "X-API-Key",
1238
+ apiKeyValue: apiKeyAttrs["value"] ?? "",
1239
+ apiKeyIn: apiKeyAttrs["in"] === "query" ? "query" : "header"
1240
+ };
1241
+ }
1242
+ return {
1243
+ id: uuid.v4(),
1244
+ name: meta["name"] ?? path.basename(fileName, ".bru"),
1245
+ method,
1246
+ url,
1247
+ headers,
1248
+ params,
1249
+ auth,
1250
+ body,
1251
+ description: "",
1252
+ preRequestScript: extractBlock(content, "script:pre-request") ? translateScript(extractBlock(content, "script:pre-request").trim(), "bruno") || void 0 : void 0,
1253
+ postRequestScript: extractBlock(content, "script:post-response") ? translateScript(extractBlock(content, "script:post-response").trim(), "bruno") || void 0 : void 0,
1254
+ meta: { seq: meta["seq"] !== void 0 ? Number(meta["seq"]) : 0 }
1255
+ };
1256
+ }
1257
+ async function scanDir(dirPath, name, requests, ignore) {
1258
+ const folder = { id: uuid.v4(), name, description: "", folders: [], requestIds: [] };
1259
+ const entries = await promises.readdir(dirPath);
1260
+ const bruRequests = [];
1261
+ for (const entry of entries) {
1262
+ if (ignore.has(entry)) continue;
1263
+ const fullPath = path.join(dirPath, entry);
1264
+ const s = await promises.stat(fullPath);
1265
+ if (s.isDirectory()) {
1266
+ const sub = await scanDir(fullPath, entry, requests, ignore);
1267
+ if (sub.folders.length > 0 || sub.requestIds.length > 0) folder.folders.push(sub);
1268
+ } else if (entry.endsWith(".bru")) {
1269
+ const content = await promises.readFile(fullPath, "utf8");
1270
+ const req = parseBruFile(content, entry);
1271
+ requests[req.id] = req;
1272
+ bruRequests.push(req);
1273
+ }
1274
+ }
1275
+ bruRequests.sort((a, b) => (a.meta?.["seq"] ?? 0) - (b.meta?.["seq"] ?? 0));
1276
+ folder.requestIds = bruRequests.map((r) => r.id);
1277
+ return folder;
1278
+ }
1279
+ async function importBruno(filePath) {
1280
+ const raw = await promises.readFile(filePath, "utf8");
1281
+ const config = JSON.parse(raw);
1282
+ const dirPath = filePath.slice(0, filePath.lastIndexOf("/") + 1) || ".";
1283
+ const ignore = /* @__PURE__ */ new Set(["node_modules", ".git", ...config.ignore ?? []]);
1284
+ const requests = {};
1285
+ const rootFolder = await scanDir(dirPath, "root", requests, ignore);
1286
+ return {
1287
+ version: "1.0",
1288
+ id: uuid.v4(),
1289
+ name: config.name ?? "Imported Collection",
1290
+ description: "",
1291
+ rootFolder,
1292
+ requests
1293
+ };
1294
+ }
1295
+ function registerImportHandlers(ipc) {
1296
+ ipc.handle("import:postman", async () => {
1297
+ const result = await electron.dialog.showOpenDialog({
1298
+ title: "Import Postman Collection",
1299
+ filters: [{ name: "JSON", extensions: ["json"] }],
1300
+ properties: ["openFile"]
1301
+ });
1302
+ if (result.canceled || !result.filePaths[0]) return null;
1303
+ return importPostman(result.filePaths[0]);
1304
+ });
1305
+ ipc.handle("import:openapi", async () => {
1306
+ const result = await electron.dialog.showOpenDialog({
1307
+ title: "Import OpenAPI Definition",
1308
+ filters: [{ name: "OpenAPI", extensions: ["json", "yaml", "yml"] }],
1309
+ properties: ["openFile"]
1310
+ });
1311
+ if (result.canceled || !result.filePaths[0]) return null;
1312
+ return importOpenApi(result.filePaths[0]);
1313
+ });
1314
+ ipc.handle("import:openapi-url", async (_event, url) => {
1315
+ return importOpenApiFromUrl(url);
1316
+ });
1317
+ ipc.handle("import:insomnia", async () => {
1318
+ const result = await electron.dialog.showOpenDialog({
1319
+ title: "Import Insomnia Collection",
1320
+ filters: [{ name: "JSON", extensions: ["json"] }],
1321
+ properties: ["openFile"]
1322
+ });
1323
+ if (result.canceled || !result.filePaths[0]) return null;
1324
+ return importInsomnia(result.filePaths[0]);
1325
+ });
1326
+ ipc.handle("import:bruno", async () => {
1327
+ const result = await electron.dialog.showOpenDialog({
1328
+ title: "Import Bruno Collection",
1329
+ filters: [{ name: "Bruno Collection", extensions: ["json"] }],
1330
+ properties: ["openFile"]
1331
+ });
1332
+ if (result.canceled || !result.filePaths[0]) return null;
1333
+ return importBruno(result.filePaths[0]);
1334
+ });
1335
+ }
1336
+ function safeName(name) {
1337
+ return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1338
+ }
1339
+ function robotVar(key) {
1340
+ return "${" + key.replace(/\W+/g, "_").toUpperCase() + "}";
1341
+ }
1342
+ function envVar(key) {
1343
+ return "%{" + key.replace(/\W+/g, "_").toUpperCase() + "}";
1344
+ }
1345
+ function interpolate(value, vars) {
1346
+ return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
1347
+ const v = vars.get(key.trim());
1348
+ return v?.secret ? envVar(key.trim()) : robotVar(key.trim());
1349
+ });
1350
+ }
1351
+ function buildNameMap$2(root, requests) {
1352
+ const map = /* @__PURE__ */ new Map();
1353
+ const used = /* @__PURE__ */ new Set();
1354
+ function visit(folder) {
1355
+ for (const id of folder.requestIds) {
1356
+ const req = requests[id];
1357
+ if (!req) continue;
1358
+ const base = safeName(req.name);
1359
+ let name = base;
1360
+ if (used.has(name)) {
1361
+ let i = 2;
1362
+ while (used.has(`${base} ${i}`)) i++;
1363
+ name = `${base} ${i}`;
1364
+ }
1365
+ used.add(name);
1366
+ map.set(id, name);
1367
+ }
1368
+ for (const sub of folder.folders) visit(sub);
1369
+ }
1370
+ visit(root);
1371
+ return map;
1372
+ }
1373
+ function buildVariablesFile(environment) {
1374
+ const lines = ["*** Variables ***"];
1375
+ if (!environment) {
1376
+ lines.push("# No environment — add your variables here");
1377
+ lines.push("${BASE_URL} http://localhost:8080");
1378
+ return lines.join("\n") + "\n";
1379
+ }
1380
+ for (const v of environment.variables) {
1381
+ if (!v.enabled) continue;
1382
+ if (v.secret) {
1383
+ lines.push(`# ${envVar(v.key)} — stored in OS keychain, never hardcoded`);
1384
+ } else {
1385
+ lines.push(`${robotVar(v.key)} ${v.value}`);
1386
+ }
1387
+ }
1388
+ return lines.join("\n") + "\n";
1389
+ }
1390
+ function jsonToRfDictPairs(json, vars) {
1391
+ try {
1392
+ const parsed = JSON.parse(json);
1393
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
1394
+ const isFlat = Object.values(parsed).every((v) => typeof v !== "object" || v === null);
1395
+ if (!isFlat) return null;
1396
+ return Object.entries(parsed).map(([k, v]) => `${k}=${interpolate(String(v ?? ""), vars)}`).join(" ");
1397
+ } catch {
1398
+ return null;
1399
+ }
1400
+ }
1401
+ function buildKeywordsFile(collection, varMap, nameMap) {
1402
+ const lines = [
1403
+ "*** Settings ***",
1404
+ "Library RequestsLibrary",
1405
+ "Resource variables.resource",
1406
+ "",
1407
+ "*** Keywords ***"
1408
+ ];
1409
+ function processFolder(folder) {
1410
+ for (const reqId of folder.requestIds) {
1411
+ const req = collection.requests[reqId];
1412
+ if (!req) continue;
1413
+ const kwName = nameMap.get(reqId);
1414
+ const url = interpolate(req.url, varMap);
1415
+ lines.push(kwName);
1416
+ lines.push(` [Documentation] ${req.description || req.name}`);
1417
+ const headerPairs = [];
1418
+ const { auth } = req;
1419
+ if (auth.type === "bearer") {
1420
+ const ref = auth.tokenSecretRef ?? "API_TOKEN";
1421
+ headerPairs.push(`Authorization=Bearer ${envVar(ref)}`);
1422
+ } else if (auth.type === "basic") {
1423
+ const passRef = auth.passwordSecretRef ?? "API_PASSWORD";
1424
+ const user = auth.username ?? "";
1425
+ lines.push(` \${credentials}= Evaluate base64.b64encode(f"${user}:${envVar(passRef)}".encode()).decode() base64`);
1426
+ headerPairs.push(`Authorization=Basic \${credentials}`);
1427
+ } else if (auth.type === "apikey" && auth.apiKeyIn === "header") {
1428
+ const keyRef = auth.apiKeySecretRef ?? "API_KEY";
1429
+ const keyName = auth.apiKeyName ?? "X-API-Key";
1430
+ headerPairs.push(`${keyName}=${envVar(keyRef)}`);
1431
+ }
1432
+ for (const h of req.headers.filter((h2) => h2.enabled && h2.key)) {
1433
+ headerPairs.push(`${h.key}=${interpolate(h.value, varMap)}`);
1434
+ }
1435
+ if (headerPairs.length) {
1436
+ lines.push(` VAR &{headers} ${headerPairs.join(" ")}`);
1437
+ }
1438
+ const enabledParams = req.params.filter((p) => p.enabled && p.key);
1439
+ if (enabledParams.length) {
1440
+ const pairs = enabledParams.map((p) => `${p.key}=${interpolate(p.value, varMap)}`).join(" ");
1441
+ lines.push(` VAR &{params} ${pairs}`);
1442
+ }
1443
+ const { body } = req;
1444
+ const hasBody = body.mode !== "none" && !["GET", "HEAD"].includes(req.method);
1445
+ if (hasBody && body.mode === "json" && body.json) {
1446
+ const bodyPairs = jsonToRfDictPairs(body.json, varMap);
1447
+ if (bodyPairs !== null) {
1448
+ lines.push(` VAR &{body} ${bodyPairs}`);
1449
+ } else {
1450
+ lines.push(` VAR \${body} ${interpolate(body.json, varMap)}`);
1451
+ }
1452
+ }
1453
+ const method = req.method.charAt(0) + req.method.slice(1).toLowerCase();
1454
+ const callArgs = [];
1455
+ if (headerPairs.length) callArgs.push("headers=${headers}");
1456
+ if (enabledParams.length) callArgs.push("params=${params}");
1457
+ if (hasBody && body.mode === "json") callArgs.push("json=${body}");
1458
+ lines.push(` \${response}= ${method} ${url}`);
1459
+ if (callArgs.length) {
1460
+ lines.push(` ... ${callArgs.join(" ")}`);
1461
+ }
1462
+ lines.push(` RETURN \${response}`);
1463
+ lines.push("");
1464
+ }
1465
+ for (const sub of folder.folders) processFolder(sub);
1466
+ }
1467
+ processFolder(collection.rootFolder);
1468
+ return lines.join("\n");
1469
+ }
1470
+ function buildTestSuite(collection, environment, nameMap) {
1471
+ const colName = safeName(collection.name);
1472
+ const envName = environment?.name ?? "default";
1473
+ const lines = [
1474
+ "*** Settings ***",
1475
+ "Resource ../resources/api_keywords.resource",
1476
+ "",
1477
+ `Suite Setup Log Running ${colName} against ${envName} environment`,
1478
+ "",
1479
+ "*** Test Cases ***"
1480
+ ];
1481
+ function processFolder(folder) {
1482
+ for (const reqId of folder.requestIds) {
1483
+ const req = collection.requests[reqId];
1484
+ if (!req) continue;
1485
+ const kwName = nameMap.get(reqId);
1486
+ lines.push(kwName);
1487
+ lines.push(` [Documentation] ${req.description || req.name}`);
1488
+ lines.push(` \${response}= ${kwName}`);
1489
+ lines.push(` Status Should Be 200 \${response}`);
1490
+ lines.push("");
1491
+ }
1492
+ for (const sub of folder.folders) processFolder(sub);
1493
+ }
1494
+ processFolder(collection.rootFolder);
1495
+ return lines.join("\n");
1496
+ }
1497
+ function renderTree$5(paths) {
1498
+ const root = {};
1499
+ for (const p of [...paths].sort()) {
1500
+ let cur = root;
1501
+ for (const part of p.split("/")) {
1502
+ cur = cur[part] ??= {};
1503
+ }
1504
+ }
1505
+ function render(node, prefix = "") {
1506
+ const entries = Object.entries(node);
1507
+ return entries.flatMap(([name, children], i) => {
1508
+ const last = i === entries.length - 1;
1509
+ const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
1510
+ if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
1511
+ return lines;
1512
+ });
1513
+ }
1514
+ return [".", ...render(root)].join("\n");
1515
+ }
1516
+ function buildReadme$5(collectionName, filePaths) {
1517
+ const tree = renderTree$5(filePaths);
1518
+ return `# ${collectionName} — API Tests (Robot Framework)
1519
+
1520
+ ## Project structure
1521
+
1522
+ \`\`\`
1523
+ ${tree}
1524
+ \`\`\`
1525
+
1526
+ > Secrets are read from OS environment variables (e.g. \`%{API_TOKEN}\`).
1527
+ > Never hardcode secrets — export them in your shell or CI environment.
1528
+
1529
+ ## Setup
1530
+
1531
+ \`\`\`sh
1532
+ pip install -r requirements.txt
1533
+
1534
+ # Run all tests
1535
+ robot tests/
1536
+
1537
+ # Override base URL
1538
+ BASE_URL=https://staging.example.com robot tests/
1539
+ \`\`\`
1540
+ `;
1541
+ }
1542
+ function generateRobotFramework(collection, environment) {
1543
+ const varMap = new Map(
1544
+ (environment?.variables ?? []).map((v) => [v.key, v])
1545
+ );
1546
+ const nameMap = buildNameMap$2(collection.rootFolder, collection.requests);
1547
+ const slug2 = collection.name.replace(/\W+/g, "_").toLowerCase();
1548
+ const contentFiles = [
1549
+ { path: "resources/variables.resource", content: buildVariablesFile(environment) },
1550
+ { path: "resources/api_keywords.resource", content: buildKeywordsFile(collection, varMap, nameMap) },
1551
+ { path: `tests/test_${slug2}.robot`, content: buildTestSuite(collection, environment, nameMap) }
1552
+ ];
1553
+ const allPaths = ["requirements.txt", ...contentFiles.map((f) => f.path)];
1554
+ return [
1555
+ { path: "README.md", content: buildReadme$5(collection.name, allPaths) },
1556
+ { path: "requirements.txt", content: "robotframework\nrobotframework-requests\n" },
1557
+ ...contentFiles
1558
+ ];
1559
+ }
1560
+ function slug$3(name) {
1561
+ return name.replace(/\W+/g, "-").toLowerCase().replace(/^-|-$/g, "");
1562
+ }
1563
+ function toEnvVar$3(key) {
1564
+ return key.replace(/\W+/g, "_").toUpperCase();
1565
+ }
1566
+ function interpolatePath$1(value) {
1567
+ return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => `\${process.env.${toEnvVar$3(key.trim())} ?? ''}`);
1568
+ }
1569
+ function buildNameMap$1(folder, requests) {
1570
+ const map = /* @__PURE__ */ new Map();
1571
+ const used = /* @__PURE__ */ new Set();
1572
+ for (const id of folder.requestIds) {
1573
+ const req = requests[id];
1574
+ if (!req) continue;
1575
+ const base = req.name;
1576
+ let name = base;
1577
+ if (used.has(name)) {
1578
+ let i = 2;
1579
+ while (used.has(`${base} ${i}`)) i++;
1580
+ name = `${base} ${i}`;
1581
+ }
1582
+ used.add(name);
1583
+ map.set(id, name);
1584
+ }
1585
+ return map;
1586
+ }
1587
+ function renderJsValue$1(value, indent) {
1588
+ const next = indent + " ";
1589
+ if (value === null) return "null";
1590
+ if (typeof value === "boolean" || typeof value === "number") return String(value);
1591
+ if (typeof value === "string") {
1592
+ if (value.includes("{{")) {
1593
+ const s = value.replace(/\{\{([^}]+)\}\}/g, (_, k) => `\${process.env.${toEnvVar$3(k.trim())} ?? ''}`);
1594
+ return "`" + s + "`";
1595
+ }
1596
+ return JSON.stringify(value);
1597
+ }
1598
+ if (Array.isArray(value)) {
1599
+ if (!value.length) return "[]";
1600
+ return `[
1601
+ ${value.map((v) => next + renderJsValue$1(v, next)).join(",\n")},
1602
+ ${indent}]`;
1603
+ }
1604
+ if (typeof value === "object") {
1605
+ const entries = Object.entries(value);
1606
+ if (!entries.length) return "{}";
1607
+ return `{
1608
+ ${entries.map(([k, v]) => `${next}${k}: ${renderJsValue$1(v, next)}`).join(",\n")},
1609
+ ${indent}}`;
1610
+ }
1611
+ return JSON.stringify(value);
1612
+ }
1613
+ function buildPlaywrightConfig$1(environment) {
1614
+ const baseUrl = environment?.variables.find(
1615
+ (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
1616
+ )?.value ?? "http://localhost:3000";
1617
+ return `import { defineConfig } from '@playwright/test'
1618
+ import * as dotenv from 'dotenv'
1619
+
1620
+ dotenv.config({ path: '.env.local' });
1621
+
1622
+ export default defineConfig({
1623
+ testDir: './tests',
1624
+ fullyParallel: true,
1625
+ reporter: 'html',
1626
+ use: {
1627
+ baseURL: process.env.BASE_URL ?? '${baseUrl}',
1628
+ extraHTTPHeaders: { Accept: 'application/json' },
1629
+ },
1630
+ })
1631
+ `;
1632
+ }
1633
+ function buildSpec$1(folderName, folder, requests, nameMap) {
1634
+ const tests = [];
1635
+ for (const reqId of folder.requestIds) {
1636
+ const req = requests[reqId];
1637
+ if (!req) continue;
1638
+ const testName = nameMap.get(reqId) ?? req.name;
1639
+ const method = req.method.toLowerCase();
1640
+ const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
1641
+ const pathExpr = path2.includes("{{") ? "`" + interpolatePath$1(path2) + "`" : `'${path2}'`;
1642
+ const optionParts = [];
1643
+ const headerEntries = [];
1644
+ const { auth } = req;
1645
+ if (auth.type === "bearer") {
1646
+ const ref = auth.tokenSecretRef ?? "API_TOKEN";
1647
+ headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar$3(ref)} ?? ''}\``);
1648
+ } else if (auth.type === "apikey" && auth.apiKeyIn === "header") {
1649
+ const ref = auth.apiKeySecretRef ?? "API_KEY";
1650
+ const name = auth.apiKeyName ?? "X-API-Key";
1651
+ headerEntries.push(`'${name}': \`\${process.env.${toEnvVar$3(ref)} ?? ''}\``);
1652
+ }
1653
+ for (const h of req.headers.filter((h2) => h2.enabled && h2.key)) {
1654
+ headerEntries.push(`'${h.key}': \`${interpolatePath$1(h.value)}\``);
1655
+ }
1656
+ if (headerEntries.length) {
1657
+ optionParts.push(` headers: {
1658
+ ${headerEntries.join(",\n ")},
1659
+ }`);
1660
+ }
1661
+ const enabledParams = req.params.filter((p) => p.enabled && p.key);
1662
+ if (enabledParams.length) {
1663
+ const pairs = enabledParams.map((p) => `'${p.key}': '${interpolatePath$1(p.value)}'`).join(", ");
1664
+ optionParts.push(` params: { ${pairs} }`);
1665
+ }
1666
+ const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
1667
+ if (hasBody && req.body.mode === "json" && req.body.json) {
1668
+ try {
1669
+ const rendered = renderJsValue$1(JSON.parse(req.body.json), " ");
1670
+ optionParts.push(` data: ${rendered}`);
1671
+ } catch {
1672
+ optionParts.push(` data: \`${interpolatePath$1(req.body.json)}\``);
1673
+ }
1674
+ }
1675
+ const optionsStr = optionParts.length ? `, {
1676
+ ${optionParts.join(",\n")},
1677
+ }` : "";
1678
+ tests.push([
1679
+ ` test('${testName}', async ({ request }) => {`,
1680
+ ` const response = await request.${method}(${pathExpr}${optionsStr});`,
1681
+ ` expect(response.ok()).toBeTruthy();`,
1682
+ ` });`
1683
+ ].join("\n"));
1684
+ }
1685
+ return `import { test, expect } from '@playwright/test'
1686
+
1687
+ test.describe('${folderName}', () => {
1688
+
1689
+ ${tests.join("\n\n")}
1690
+
1691
+ })
1692
+ `;
1693
+ }
1694
+ function buildPackageJson$3(collectionName) {
1695
+ const name = collectionName.replace(/\W+/g, "-").toLowerCase();
1696
+ return JSON.stringify({
1697
+ name: `${name}-api-tests`,
1698
+ version: "1.0.0",
1699
+ private: true,
1700
+ scripts: {
1701
+ test: "playwright test",
1702
+ "test:report": "playwright show-report"
1703
+ },
1704
+ devDependencies: {
1705
+ "@playwright/test": "^1.44.0",
1706
+ dotenv: "^16.4.0",
1707
+ typescript: "^5.4.0"
1708
+ }
1709
+ }, null, 2) + "\n";
1710
+ }
1711
+ function renderTree$4(paths) {
1712
+ const root = {};
1713
+ for (const p of [...paths].sort()) {
1714
+ let cur = root;
1715
+ for (const part of p.split("/")) {
1716
+ cur = cur[part] ??= {};
1717
+ }
1718
+ }
1719
+ function render(node, prefix = "") {
1720
+ const entries = Object.entries(node);
1721
+ return entries.flatMap(([name, children], i) => {
1722
+ const last = i === entries.length - 1;
1723
+ const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
1724
+ if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
1725
+ return lines;
1726
+ });
1727
+ }
1728
+ return [".", ...render(root)].join("\n");
1729
+ }
1730
+ function buildReadme$4(collectionName, filePaths) {
1731
+ const tree = renderTree$4([...filePaths, ".env.local"]);
1732
+ return `# ${collectionName} — API Tests (Playwright TypeScript)
1733
+
1734
+ ## Project structure
1735
+
1736
+ \`\`\`
1737
+ ${tree}
1738
+ \`\`\`
1739
+
1740
+ > \`.env.local\` is git-ignored — fill in your secrets before running.
1741
+
1742
+ ## Setup
1743
+
1744
+ \`\`\`sh
1745
+ npm install
1746
+ npx playwright install --with-deps chromium
1747
+ npm test
1748
+ \`\`\`
1749
+ `;
1750
+ }
1751
+ function generatePlaywright(collection, environment) {
1752
+ const files = [];
1753
+ function processFolder(folder, name) {
1754
+ if (folder.requestIds.length > 0) {
1755
+ const nameMap = buildNameMap$1(folder, collection.requests);
1756
+ files.push({ path: `tests/${slug$3(name)}.spec.ts`, content: buildSpec$1(name, folder, collection.requests, nameMap) });
1757
+ }
1758
+ for (const sub of folder.folders) processFolder(sub, sub.name);
1759
+ }
1760
+ if (collection.rootFolder.requestIds.length > 0) processFolder(collection.rootFolder, collection.name);
1761
+ for (const sub of collection.rootFolder.folders) processFolder(sub, sub.name);
1762
+ const scaffoldPaths = ["package.json", "playwright.config.ts", ...files.map((f) => f.path)];
1763
+ files.unshift(
1764
+ { path: "package.json", content: buildPackageJson$3(collection.name) },
1765
+ { path: "playwright.config.ts", content: buildPlaywrightConfig$1(environment) },
1766
+ { path: "README.md", content: buildReadme$4(collection.name, scaffoldPaths) }
1767
+ );
1768
+ return files;
1769
+ }
1770
+ function slug$2(name) {
1771
+ return name.replace(/\W+/g, "-").toLowerCase().replace(/^-|-$/g, "");
1772
+ }
1773
+ function toEnvVar$2(key) {
1774
+ return key.replace(/\W+/g, "_").toUpperCase();
1775
+ }
1776
+ function interpolatePath(value) {
1777
+ return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => `\${process.env.${toEnvVar$2(key.trim())} ?? ''}`);
1778
+ }
1779
+ function buildNameMap(folder, requests) {
1780
+ const map = /* @__PURE__ */ new Map();
1781
+ const used = /* @__PURE__ */ new Set();
1782
+ for (const id of folder.requestIds) {
1783
+ const req = requests[id];
1784
+ if (!req) continue;
1785
+ const base = req.name;
1786
+ let name = base;
1787
+ if (used.has(name)) {
1788
+ let i = 2;
1789
+ while (used.has(`${base} ${i}`)) i++;
1790
+ name = `${base} ${i}`;
1791
+ }
1792
+ used.add(name);
1793
+ map.set(id, name);
1794
+ }
1795
+ return map;
1796
+ }
1797
+ function renderJsValue(value, indent) {
1798
+ const next = indent + " ";
1799
+ if (value === null) return "null";
1800
+ if (typeof value === "boolean" || typeof value === "number") return String(value);
1801
+ if (typeof value === "string") {
1802
+ if (value.includes("{{")) {
1803
+ const s = value.replace(/\{\{([^}]+)\}\}/g, (_, k) => `\${process.env.${toEnvVar$2(k.trim())} ?? ''}`);
1804
+ return "`" + s + "`";
1805
+ }
1806
+ return JSON.stringify(value);
1807
+ }
1808
+ if (Array.isArray(value)) {
1809
+ if (!value.length) return "[]";
1810
+ return `[
1811
+ ${value.map((v) => next + renderJsValue(v, next)).join(",\n")},
1812
+ ${indent}]`;
1813
+ }
1814
+ if (typeof value === "object") {
1815
+ const entries = Object.entries(value);
1816
+ if (!entries.length) return "{}";
1817
+ return `{
1818
+ ${entries.map(([k, v]) => `${next}${k}: ${renderJsValue(v, next)}`).join(",\n")},
1819
+ ${indent}}`;
1820
+ }
1821
+ return JSON.stringify(value);
1822
+ }
1823
+ function buildPlaywrightConfig(environment) {
1824
+ const baseUrl = environment?.variables.find(
1825
+ (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
1826
+ )?.value ?? "http://localhost:3000";
1827
+ return `// @ts-check
1828
+ const { defineConfig } = require('@playwright/test');
1829
+ require('dotenv').config({ path: '.env.local' });
1830
+
1831
+ module.exports = defineConfig({
1832
+ testDir: './tests',
1833
+ fullyParallel: true,
1834
+ reporter: 'html',
1835
+ use: {
1836
+ baseURL: process.env.BASE_URL ?? '${baseUrl}',
1837
+ extraHTTPHeaders: { Accept: 'application/json' },
1838
+ },
1839
+ });
1840
+ `;
1841
+ }
1842
+ function buildSpec(folderName, folder, requests, nameMap) {
1843
+ const tests = [];
1844
+ for (const reqId of folder.requestIds) {
1845
+ const req = requests[reqId];
1846
+ if (!req) continue;
1847
+ const testName = nameMap.get(reqId) ?? req.name;
1848
+ const method = req.method.toLowerCase();
1849
+ const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
1850
+ const pathExpr = path2.includes("{{") ? "`" + interpolatePath(path2) + "`" : `'${path2}'`;
1851
+ const optionParts = [];
1852
+ const headerEntries = [];
1853
+ const { auth } = req;
1854
+ if (auth.type === "bearer") {
1855
+ const ref = auth.tokenSecretRef ?? "API_TOKEN";
1856
+ headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar$2(ref)} ?? ''}\``);
1857
+ } else if (auth.type === "apikey" && auth.apiKeyIn === "header") {
1858
+ const ref = auth.apiKeySecretRef ?? "API_KEY";
1859
+ const name = auth.apiKeyName ?? "X-API-Key";
1860
+ headerEntries.push(`'${name}': \`\${process.env.${toEnvVar$2(ref)} ?? ''}\``);
1861
+ }
1862
+ for (const h of req.headers.filter((h2) => h2.enabled && h2.key)) {
1863
+ headerEntries.push(`'${h.key}': \`${interpolatePath(h.value)}\``);
1864
+ }
1865
+ if (headerEntries.length) {
1866
+ optionParts.push(` headers: {
1867
+ ${headerEntries.join(",\n ")},
1868
+ }`);
1869
+ }
1870
+ const enabledParams = req.params.filter((p) => p.enabled && p.key);
1871
+ if (enabledParams.length) {
1872
+ const pairs = enabledParams.map((p) => `'${p.key}': '${interpolatePath(p.value)}'`).join(", ");
1873
+ optionParts.push(` params: { ${pairs} }`);
1874
+ }
1875
+ const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
1876
+ if (hasBody && req.body.mode === "json" && req.body.json) {
1877
+ try {
1878
+ const rendered = renderJsValue(JSON.parse(req.body.json), " ");
1879
+ optionParts.push(` data: ${rendered}`);
1880
+ } catch {
1881
+ optionParts.push(` data: \`${interpolatePath(req.body.json)}\``);
1882
+ }
1883
+ }
1884
+ const optionsStr = optionParts.length ? `, {
1885
+ ${optionParts.join(",\n")},
1886
+ }` : "";
1887
+ tests.push([
1888
+ ` test('${testName}', async ({ request }) => {`,
1889
+ ` const response = await request.${method}(${pathExpr}${optionsStr});`,
1890
+ ` expect(response.ok()).toBeTruthy();`,
1891
+ ` });`
1892
+ ].join("\n"));
1893
+ }
1894
+ return `const { test, expect } = require('@playwright/test');
1895
+
1896
+ test.describe('${folderName}', () => {
1897
+
1898
+ ${tests.join("\n\n")}
1899
+
1900
+ });
1901
+ `;
1902
+ }
1903
+ function buildPackageJson$2(collectionName) {
1904
+ const name = collectionName.replace(/\W+/g, "-").toLowerCase();
1905
+ return JSON.stringify({
1906
+ name: `${name}-api-tests`,
1907
+ version: "1.0.0",
1908
+ private: true,
1909
+ scripts: {
1910
+ test: "playwright test",
1911
+ "test:report": "playwright show-report"
1912
+ },
1913
+ devDependencies: {
1914
+ "@playwright/test": "^1.44.0",
1915
+ dotenv: "^16.4.0"
1916
+ }
1917
+ }, null, 2) + "\n";
1918
+ }
1919
+ function renderTree$3(paths) {
1920
+ const root = {};
1921
+ for (const p of [...paths].sort()) {
1922
+ let cur = root;
1923
+ for (const part of p.split("/")) {
1924
+ cur = cur[part] ??= {};
1925
+ }
1926
+ }
1927
+ function render(node, prefix = "") {
1928
+ const entries = Object.entries(node);
1929
+ return entries.flatMap(([name, children], i) => {
1930
+ const last = i === entries.length - 1;
1931
+ const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
1932
+ if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
1933
+ return lines;
1934
+ });
1935
+ }
1936
+ return [".", ...render(root)].join("\n");
1937
+ }
1938
+ function buildReadme$3(collectionName, filePaths) {
1939
+ const tree = renderTree$3([...filePaths, ".env.local"]);
1940
+ return `# ${collectionName} — API Tests (Playwright JavaScript)
1941
+
1942
+ ## Project structure
1943
+
1944
+ \`\`\`
1945
+ ${tree}
1946
+ \`\`\`
1947
+
1948
+ > \`.env.local\` is git-ignored — fill in your secrets before running.
1949
+
1950
+ ## Setup
1951
+
1952
+ \`\`\`sh
1953
+ npm install
1954
+ npx playwright install --with-deps chromium
1955
+ npm test
1956
+ \`\`\`
1957
+ `;
1958
+ }
1959
+ function generatePlaywrightJs(collection, environment) {
1960
+ const files = [];
1961
+ function processFolder(folder, name) {
1962
+ if (folder.requestIds.length > 0) {
1963
+ const nameMap = buildNameMap(folder, collection.requests);
1964
+ files.push({ path: `tests/${slug$2(name)}.spec.js`, content: buildSpec(name, folder, collection.requests, nameMap) });
1965
+ }
1966
+ for (const sub of folder.folders) processFolder(sub, sub.name);
1967
+ }
1968
+ if (collection.rootFolder.requestIds.length > 0) processFolder(collection.rootFolder, collection.name);
1969
+ for (const sub of collection.rootFolder.folders) processFolder(sub, sub.name);
1970
+ const scaffoldPaths = ["package.json", "playwright.config.js", ...files.map((f) => f.path)];
1971
+ files.unshift(
1972
+ { path: "package.json", content: buildPackageJson$2(collection.name) },
1973
+ { path: "playwright.config.js", content: buildPlaywrightConfig(environment) },
1974
+ { path: "README.md", content: buildReadme$3(collection.name, scaffoldPaths) }
1975
+ );
1976
+ return files;
1977
+ }
1978
+ function slug$1(name) {
1979
+ return name.replace(/\W+/g, "-").toLowerCase().replace(/^-|-$/g, "");
1980
+ }
1981
+ function toEnvVar$1(key) {
1982
+ return key.replace(/\W+/g, "_").toUpperCase();
1983
+ }
1984
+ function interpolateValue$1(value) {
1985
+ return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => `\${process.env.${toEnvVar$1(key.trim())} ?? ''}`);
1986
+ }
1987
+ function buildJestConfig$1() {
1988
+ return `import type { Config } from 'jest'
1989
+
1990
+ const config: Config = {
1991
+ preset: 'ts-jest',
1992
+ testEnvironment: 'node',
1993
+ testMatch: ['**/tests/**/*.test.ts'],
1994
+ setupFiles: ['dotenv/config'],
1995
+ }
1996
+
1997
+ export default config
1998
+ `;
1999
+ }
2000
+ function buildClient$1(environment) {
2001
+ const baseUrl = environment?.variables.find(
2002
+ (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
2003
+ )?.value ?? "http://localhost:3000";
2004
+ const secretVars = environment?.variables.filter((v) => v.secret && v.secretRef) ?? [];
2005
+ const envComments = secretVars.map((v) => `# ${toEnvVar$1(v.key)}=<from keychain>`).join("\n");
2006
+ return `import supertest from 'supertest'
2007
+ import * as dotenv from 'dotenv'
2008
+
2009
+ dotenv.config({ path: '.env.local' });
2010
+
2011
+ // Add secret env vars to .env.local:
2012
+ ${envComments ? `// ${envComments.replace(/\n/g, "\n// ")}
2013
+ ` : ""}export const BASE_URL = process.env.BASE_URL ?? '${baseUrl}';
2014
+
2015
+ export const api = supertest(BASE_URL);
2016
+ `;
2017
+ }
2018
+ function buildTestFile$1(folderName, folder, requests) {
2019
+ const tests = [];
2020
+ const used = /* @__PURE__ */ new Set();
2021
+ const nameMap = /* @__PURE__ */ new Map();
2022
+ for (const id of folder.requestIds) {
2023
+ const req = requests[id];
2024
+ if (!req) continue;
2025
+ const base = req.name;
2026
+ let name = base;
2027
+ if (used.has(name)) {
2028
+ let i = 2;
2029
+ while (used.has(`${base} ${i}`)) i++;
2030
+ name = `${base} ${i}`;
2031
+ }
2032
+ used.add(name);
2033
+ nameMap.set(id, name);
2034
+ }
2035
+ for (const reqId of folder.requestIds) {
2036
+ const req = requests[reqId];
2037
+ if (!req) continue;
2038
+ const method = req.method.toLowerCase();
2039
+ const path2 = interpolateValue$1(req.url.replace(/^https?:\/\/[^/]+/, "") || "/");
2040
+ const enabledHeaders = req.headers.filter((h) => h.enabled && h.key);
2041
+ const enabledParams = req.params.filter((p) => p.enabled && p.key);
2042
+ const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
2043
+ const lines = [];
2044
+ lines.push(` it('${nameMap.get(reqId)}', async () => {`);
2045
+ lines.push(` const res = await api`);
2046
+ lines.push(` .${method}(\`${path2}\`)`);
2047
+ for (const h of enabledHeaders) {
2048
+ lines.push(` .set('${h.key}', \`${interpolateValue$1(h.value)}\`)`);
2049
+ }
2050
+ if (enabledParams.length) {
2051
+ const pairs = enabledParams.map((p) => `${p.key}: \`${interpolateValue$1(p.value)}\``).join(", ");
2052
+ lines.push(` .query({ ${pairs} })`);
2053
+ }
2054
+ if (hasBody) {
2055
+ if (req.body.mode === "json") {
2056
+ lines.push(` .send(${req.body.json ?? "{}"})`);
2057
+ } else if (req.body.mode === "form" && req.body.form) {
2058
+ const pairs = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${p.key}: \`${interpolateValue$1(p.value)}\``).join(", ");
2059
+ lines.push(` .type('form')`);
2060
+ lines.push(` .send({ ${pairs} })`);
2061
+ }
2062
+ }
2063
+ lines[lines.length - 1] += ";";
2064
+ lines.push(``);
2065
+ lines.push(` expect(res.status).toBe(200);`);
2066
+ lines.push(` // expect(res.body).toMatchObject({});`);
2067
+ lines.push(` })`);
2068
+ tests.push(lines.join("\n"));
2069
+ }
2070
+ return `import { api } from '../helpers/api-client'
2071
+
2072
+ describe('${folderName}', () => {
2073
+ ${tests.join("\n\n")}
2074
+ })
2075
+ `;
2076
+ }
2077
+ function buildPackageJson$1(collectionName) {
2078
+ const name = collectionName.replace(/\W+/g, "-").toLowerCase();
2079
+ return JSON.stringify({
2080
+ name: `${name}-api-tests`,
2081
+ version: "1.0.0",
2082
+ private: true,
2083
+ scripts: { test: "jest" },
2084
+ devDependencies: {
2085
+ "@types/jest": "^29.5.0",
2086
+ "@types/supertest": "^6.0.0",
2087
+ dotenv: "^16.4.0",
2088
+ jest: "^29.7.0",
2089
+ supertest: "^7.0.0",
2090
+ "ts-jest": "^29.1.0",
2091
+ "ts-node": "^10.9.2",
2092
+ typescript: "^5.4.0"
2093
+ }
2094
+ }, null, 2) + "\n";
2095
+ }
2096
+ function buildTsConfig() {
2097
+ return JSON.stringify({
2098
+ compilerOptions: {
2099
+ target: "ES2020",
2100
+ module: "commonjs",
2101
+ strict: true,
2102
+ esModuleInterop: true,
2103
+ outDir: "dist"
2104
+ },
2105
+ include: ["**/*.ts"],
2106
+ exclude: ["node_modules", "dist"]
2107
+ }, null, 2) + "\n";
2108
+ }
2109
+ function renderTree$2(paths) {
2110
+ const root = {};
2111
+ for (const p of [...paths].sort()) {
2112
+ let cur = root;
2113
+ for (const part of p.split("/")) {
2114
+ cur = cur[part] ??= {};
2115
+ }
2116
+ }
2117
+ function render(node, prefix = "") {
2118
+ const entries = Object.entries(node);
2119
+ return entries.flatMap(([name, children], i) => {
2120
+ const last = i === entries.length - 1;
2121
+ const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
2122
+ if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
2123
+ return lines;
2124
+ });
2125
+ }
2126
+ return [".", ...render(root)].join("\n");
2127
+ }
2128
+ function buildReadme$2(collectionName, filePaths) {
2129
+ const tree = renderTree$2([...filePaths, ".env.local"]);
2130
+ return `# ${collectionName} — API Tests (Supertest + Jest TypeScript)
2131
+
2132
+ ## Project structure
2133
+
2134
+ \`\`\`
2135
+ ${tree}
2136
+ \`\`\`
2137
+
2138
+ > \`.env.local\` is git-ignored — fill in your secrets before running.
2139
+
2140
+ ## Setup
2141
+
2142
+ \`\`\`sh
2143
+ npm install
2144
+ npm test
2145
+ \`\`\`
2146
+ `;
2147
+ }
2148
+ function generateSupertestTs(collection, environment) {
2149
+ const files = [
2150
+ { path: "jest.config.ts", content: buildJestConfig$1() },
2151
+ { path: "helpers/api-client.ts", content: buildClient$1(environment) }
2152
+ ];
2153
+ function processFolder(folder, name) {
2154
+ if (folder.requestIds.length > 0) {
2155
+ files.push({
2156
+ path: `tests/${slug$1(name)}.test.ts`,
2157
+ content: buildTestFile$1(name, folder, collection.requests)
2158
+ });
2159
+ }
2160
+ for (const sub of folder.folders) {
2161
+ processFolder(sub, sub.name);
2162
+ }
2163
+ }
2164
+ if (collection.rootFolder.requestIds.length > 0) {
2165
+ processFolder(collection.rootFolder, collection.name);
2166
+ }
2167
+ for (const sub of collection.rootFolder.folders) {
2168
+ processFolder(sub, sub.name);
2169
+ }
2170
+ const scaffoldPaths = ["package.json", "tsconfig.json", ...files.map((f) => f.path)];
2171
+ files.unshift(
2172
+ { path: "package.json", content: buildPackageJson$1(collection.name) },
2173
+ { path: "tsconfig.json", content: buildTsConfig() },
2174
+ { path: "README.md", content: buildReadme$2(collection.name, scaffoldPaths) }
2175
+ );
2176
+ return files;
2177
+ }
2178
+ function slug(name) {
2179
+ return name.replace(/\W+/g, "-").toLowerCase().replace(/^-|-$/g, "");
2180
+ }
2181
+ function toEnvVar(key) {
2182
+ return key.replace(/\W+/g, "_").toUpperCase();
2183
+ }
2184
+ function interpolateValue(value) {
2185
+ return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => `\${process.env.${toEnvVar(key.trim())} ?? ''}`);
2186
+ }
2187
+ function buildJestConfig() {
2188
+ return `/** @type {import('jest').Config} */
2189
+ module.exports = {
2190
+ testEnvironment: 'node',
2191
+ testMatch: ['**/tests/**/*.test.js'],
2192
+ setupFiles: ['dotenv/config'],
2193
+ }
2194
+ `;
2195
+ }
2196
+ function buildClient(environment) {
2197
+ const baseUrl = environment?.variables.find(
2198
+ (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
2199
+ )?.value ?? "http://localhost:3000";
2200
+ const secretVars = environment?.variables.filter((v) => v.secret && v.secretRef) ?? [];
2201
+ const envComments = secretVars.map((v) => `# ${toEnvVar(v.key)}=<from keychain>`).join("\n");
2202
+ return `const supertest = require('supertest');
2203
+ require('dotenv').config({ path: '.env.local' });
2204
+
2205
+ // Add secret env vars to .env.local:
2206
+ ${envComments ? `// ${envComments.replace(/\n/g, "\n// ")}
2207
+ ` : ""}const BASE_URL = process.env.BASE_URL ?? '${baseUrl}';
2208
+
2209
+ module.exports.api = supertest(BASE_URL);
2210
+ `;
2211
+ }
2212
+ function buildTestFile(folderName, folder, requests) {
2213
+ const tests = [];
2214
+ const used = /* @__PURE__ */ new Set();
2215
+ const nameMap = /* @__PURE__ */ new Map();
2216
+ for (const id of folder.requestIds) {
2217
+ const req = requests[id];
2218
+ if (!req) continue;
2219
+ const base = req.name;
2220
+ let name = base;
2221
+ if (used.has(name)) {
2222
+ let i = 2;
2223
+ while (used.has(`${base} ${i}`)) i++;
2224
+ name = `${base} ${i}`;
2225
+ }
2226
+ used.add(name);
2227
+ nameMap.set(id, name);
2228
+ }
2229
+ for (const reqId of folder.requestIds) {
2230
+ const req = requests[reqId];
2231
+ if (!req) continue;
2232
+ const method = req.method.toLowerCase();
2233
+ const path2 = interpolateValue(req.url.replace(/^https?:\/\/[^/]+/, "") || "/");
2234
+ const enabledHeaders = req.headers.filter((h) => h.enabled && h.key);
2235
+ const enabledParams = req.params.filter((p) => p.enabled && p.key);
2236
+ const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
2237
+ const lines = [];
2238
+ lines.push(` it('${nameMap.get(reqId)}', async () => {`);
2239
+ lines.push(` const res = await api`);
2240
+ lines.push(` .${method}(\`${path2}\`)`);
2241
+ for (const h of enabledHeaders) {
2242
+ lines.push(` .set('${h.key}', \`${interpolateValue(h.value)}\`)`);
2243
+ }
2244
+ if (enabledParams.length) {
2245
+ const pairs = enabledParams.map((p) => `${p.key}: \`${interpolateValue(p.value)}\``).join(", ");
2246
+ lines.push(` .query({ ${pairs} })`);
2247
+ }
2248
+ if (hasBody) {
2249
+ if (req.body.mode === "json") {
2250
+ lines.push(` .send(${req.body.json ?? "{}"})`);
2251
+ } else if (req.body.mode === "form" && req.body.form) {
2252
+ const pairs = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${p.key}: \`${interpolateValue(p.value)}\``).join(", ");
2253
+ lines.push(` .type('form')`);
2254
+ lines.push(` .send({ ${pairs} })`);
2255
+ }
2256
+ }
2257
+ lines[lines.length - 1] += ";";
2258
+ lines.push(``);
2259
+ lines.push(` expect(res.status).toBe(200);`);
2260
+ lines.push(` // expect(res.body).toMatchObject({});`);
2261
+ lines.push(` })`);
2262
+ tests.push(lines.join("\n"));
2263
+ }
2264
+ return `const { api } = require('../helpers/api-client');
2265
+
2266
+ describe('${folderName}', () => {
2267
+ ${tests.join("\n\n")}
2268
+ })
2269
+ `;
2270
+ }
2271
+ function buildPackageJson(collectionName) {
2272
+ const name = collectionName.replace(/\W+/g, "-").toLowerCase();
2273
+ return JSON.stringify({
2274
+ name: `${name}-api-tests`,
2275
+ version: "1.0.0",
2276
+ private: true,
2277
+ scripts: { test: "jest" },
2278
+ devDependencies: {
2279
+ dotenv: "^16.4.0",
2280
+ jest: "^29.7.0",
2281
+ supertest: "^7.0.0"
2282
+ }
2283
+ }, null, 2) + "\n";
2284
+ }
2285
+ function renderTree$1(paths) {
2286
+ const root = {};
2287
+ for (const p of [...paths].sort()) {
2288
+ let cur = root;
2289
+ for (const part of p.split("/")) {
2290
+ cur = cur[part] ??= {};
2291
+ }
2292
+ }
2293
+ function render(node, prefix = "") {
2294
+ const entries = Object.entries(node);
2295
+ return entries.flatMap(([name, children], i) => {
2296
+ const last = i === entries.length - 1;
2297
+ const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
2298
+ if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
2299
+ return lines;
2300
+ });
2301
+ }
2302
+ return [".", ...render(root)].join("\n");
2303
+ }
2304
+ function buildReadme$1(collectionName, filePaths) {
2305
+ const tree = renderTree$1([...filePaths, ".env.local"]);
2306
+ return `# ${collectionName} — API Tests (Supertest + Jest JavaScript)
2307
+
2308
+ ## Project structure
2309
+
2310
+ \`\`\`
2311
+ ${tree}
2312
+ \`\`\`
2313
+
2314
+ > \`.env.local\` is git-ignored — fill in your secrets before running.
2315
+
2316
+ ## Setup
2317
+
2318
+ \`\`\`sh
2319
+ npm install
2320
+ npm test
2321
+ \`\`\`
2322
+ `;
2323
+ }
2324
+ function generateSupertestJs(collection, environment) {
2325
+ const files = [
2326
+ { path: "jest.config.js", content: buildJestConfig() },
2327
+ { path: "helpers/api-client.js", content: buildClient(environment) }
2328
+ ];
2329
+ function processFolder(folder, name) {
2330
+ if (folder.requestIds.length > 0) {
2331
+ files.push({
2332
+ path: `tests/${slug(name)}.test.js`,
2333
+ content: buildTestFile(name, folder, collection.requests)
2334
+ });
2335
+ }
2336
+ for (const sub of folder.folders) {
2337
+ processFolder(sub, sub.name);
2338
+ }
2339
+ }
2340
+ if (collection.rootFolder.requestIds.length > 0) {
2341
+ processFolder(collection.rootFolder, collection.name);
2342
+ }
2343
+ for (const sub of collection.rootFolder.folders) {
2344
+ processFolder(sub, sub.name);
2345
+ }
2346
+ const scaffoldPaths = ["package.json", ...files.map((f) => f.path)];
2347
+ files.unshift(
2348
+ { path: "package.json", content: buildPackageJson(collection.name) },
2349
+ { path: "README.md", content: buildReadme$1(collection.name, scaffoldPaths) }
2350
+ );
2351
+ return files;
2352
+ }
2353
+ function javaClass(name) {
2354
+ return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
2355
+ }
2356
+ function javaMethod(name) {
2357
+ const parts = name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean);
2358
+ return parts[0].toLowerCase() + parts.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
2359
+ }
2360
+ function toEnvConst(key) {
2361
+ return key.replace(/\W+/g, "_").toUpperCase();
2362
+ }
2363
+ function interpolateJava(value) {
2364
+ return '"' + value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
2365
+ const envKey = toEnvConst(key.trim());
2366
+ return `" + System.getenv("${envKey}") + "`;
2367
+ }) + '"';
2368
+ }
2369
+ function buildPom(collectionName) {
2370
+ const artifact = collectionName.replace(/\W+/g, "-").toLowerCase();
2371
+ return `<?xml version="1.0" encoding="UTF-8"?>
2372
+ <project xmlns="http://maven.apache.org/POM/4.0.0"
2373
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2374
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
2375
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
2376
+ <modelVersion>4.0.0</modelVersion>
2377
+
2378
+ <groupId>com.example.api</groupId>
2379
+ <artifactId>${artifact}-tests</artifactId>
2380
+ <version>1.0.0-SNAPSHOT</version>
2381
+ <packaging>jar</packaging>
2382
+
2383
+ <properties>
2384
+ <java.version>17</java.version>
2385
+ <maven.compiler.source>\${java.version}</maven.compiler.source>
2386
+ <maven.compiler.target>\${java.version}</maven.compiler.target>
2387
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
2388
+ <rest-assured.version>5.4.0</rest-assured.version>
2389
+ <junit.version>5.10.2</junit.version>
2390
+ </properties>
2391
+
2392
+ <dependencies>
2393
+ <!-- REST Assured -->
2394
+ <dependency>
2395
+ <groupId>io.rest-assured</groupId>
2396
+ <artifactId>rest-assured</artifactId>
2397
+ <version>\${rest-assured.version}</version>
2398
+ <scope>test</scope>
2399
+ </dependency>
2400
+ <dependency>
2401
+ <groupId>io.rest-assured</groupId>
2402
+ <artifactId>json-schema-validator</artifactId>
2403
+ <version>\${rest-assured.version}</version>
2404
+ <scope>test</scope>
2405
+ </dependency>
2406
+
2407
+ <!-- JUnit 5 -->
2408
+ <dependency>
2409
+ <groupId>org.junit.jupiter</groupId>
2410
+ <artifactId>junit-jupiter</artifactId>
2411
+ <version>\${junit.version}</version>
2412
+ <scope>test</scope>
2413
+ </dependency>
2414
+
2415
+ <!-- Hamcrest -->
2416
+ <dependency>
2417
+ <groupId>org.hamcrest</groupId>
2418
+ <artifactId>hamcrest</artifactId>
2419
+ <version>2.2</version>
2420
+ <scope>test</scope>
2421
+ </dependency>
2422
+ </dependencies>
2423
+
2424
+ <build>
2425
+ <plugins>
2426
+ <plugin>
2427
+ <groupId>org.apache.maven.plugins</groupId>
2428
+ <artifactId>maven-surefire-plugin</artifactId>
2429
+ <version>3.2.5</version>
2430
+ </plugin>
2431
+ </plugins>
2432
+ </build>
2433
+ </project>
2434
+ `;
2435
+ }
2436
+ function buildBaseTest(environment) {
2437
+ const baseUrl = environment?.variables.find(
2438
+ (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
2439
+ )?.value ?? "http://localhost:8080";
2440
+ const secretVars = environment?.variables.filter((v) => v.secret && v.secretRef) ?? [];
2441
+ const secretComments = secretVars.map(
2442
+ (v) => ` * - ${toEnvConst(v.key)}=<value from keychain>`
2443
+ ).join("\n");
2444
+ return `package com.example.api;
2445
+
2446
+ import io.restassured.RestAssured;
2447
+ import io.restassured.builder.RequestSpecBuilder;
2448
+ import io.restassured.http.ContentType;
2449
+ import io.restassured.specification.RequestSpecification;
2450
+ import org.junit.jupiter.api.BeforeAll;
2451
+
2452
+ /**
2453
+ * Base class for all API tests.
2454
+ *
2455
+ * Set the following environment variables before running:
2456
+ ${secretComments ? secretComments + "\n" : ""} * - BASE_URL (optional, default: ${baseUrl})
2457
+ */
2458
+ public class BaseTest {
2459
+
2460
+ protected static RequestSpecification requestSpec;
2461
+
2462
+ @BeforeAll
2463
+ static void setupRestAssured() {
2464
+ String baseUrl = System.getenv("BASE_URL") != null
2465
+ ? System.getenv("BASE_URL")
2466
+ : "${baseUrl}";
2467
+
2468
+ requestSpec = new RequestSpecBuilder()
2469
+ .setBaseUri(baseUrl)
2470
+ .setContentType(ContentType.JSON)
2471
+ .setAccept(ContentType.JSON)
2472
+ .build();
2473
+
2474
+ RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
2475
+ }
2476
+ }
2477
+ `;
2478
+ }
2479
+ function buildTestClass(folderName, folder, requests) {
2480
+ const className = javaClass(folderName) + "Test";
2481
+ const methods = [];
2482
+ const usedNames = /* @__PURE__ */ new Set();
2483
+ const nameMap = /* @__PURE__ */ new Map();
2484
+ for (const reqId of folder.requestIds) {
2485
+ const req = requests[reqId];
2486
+ if (!req) continue;
2487
+ const base = javaMethod(req.name);
2488
+ let name = base;
2489
+ if (usedNames.has(name)) {
2490
+ let i = 2;
2491
+ while (usedNames.has(`${base}${i}`)) i++;
2492
+ name = `${base}${i}`;
2493
+ }
2494
+ usedNames.add(name);
2495
+ nameMap.set(reqId, name);
2496
+ }
2497
+ for (const reqId of folder.requestIds) {
2498
+ const req = requests[reqId];
2499
+ if (!req) continue;
2500
+ const methodName = nameMap.get(reqId);
2501
+ const method = req.method.toLowerCase();
2502
+ const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
2503
+ const javaPath = interpolateJava(path2);
2504
+ const enabledHeaders = req.headers.filter((h) => h.enabled && h.key);
2505
+ const enabledParams = req.params.filter((p) => p.enabled && p.key);
2506
+ const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
2507
+ const lines = [];
2508
+ lines.push(` @Test`);
2509
+ lines.push(` public void ${methodName}() {`);
2510
+ lines.push(` given()`);
2511
+ lines.push(` .spec(requestSpec)`);
2512
+ for (const h of enabledHeaders) {
2513
+ lines.push(` .header("${h.key}", ${interpolateJava(h.value)})`);
2514
+ }
2515
+ for (const p of enabledParams) {
2516
+ lines.push(` .queryParam("${p.key}", ${interpolateJava(p.value)})`);
2517
+ }
2518
+ if (hasBody) {
2519
+ if (req.body.mode === "json" && req.body.json) {
2520
+ const escaped = req.body.json.replace(/"/g, '\\"').replace(/\n/g, "\\n");
2521
+ lines.push(` .body("${escaped}")`);
2522
+ }
2523
+ }
2524
+ lines.push(` .when()`);
2525
+ lines.push(` .${method}(${javaPath})`);
2526
+ lines.push(` .then()`);
2527
+ lines.push(` .statusCode(200);`);
2528
+ lines.push(` // .body("field", equalTo("value"));`);
2529
+ lines.push(` }`);
2530
+ methods.push(lines.join("\n"));
2531
+ }
2532
+ return `package com.example.api;
2533
+
2534
+ import org.junit.jupiter.api.Test;
2535
+
2536
+ import static io.restassured.RestAssured.given;
2537
+ import static org.hamcrest.Matchers.*;
2538
+
2539
+ public class ${className} extends BaseTest {
2540
+
2541
+ ${methods.join("\n\n")}
2542
+ }
2543
+ `;
2544
+ }
2545
+ function renderTree(paths) {
2546
+ const root = {};
2547
+ for (const p of [...paths].sort()) {
2548
+ let cur = root;
2549
+ for (const part of p.split("/")) {
2550
+ cur = cur[part] ??= {};
2551
+ }
2552
+ }
2553
+ function render(node, prefix = "") {
2554
+ const entries = Object.entries(node);
2555
+ return entries.flatMap(([name, children], i) => {
2556
+ const last = i === entries.length - 1;
2557
+ const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
2558
+ if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
2559
+ return lines;
2560
+ });
2561
+ }
2562
+ return [".", ...render(root)].join("\n");
2563
+ }
2564
+ function buildReadme(collectionName, filePaths) {
2565
+ const tree = renderTree(filePaths);
2566
+ return `# ${collectionName} — API Tests (REST Assured + JUnit 5)
2567
+
2568
+ ## Project structure
2569
+
2570
+ \`\`\`
2571
+ ${tree}
2572
+ \`\`\`
2573
+
2574
+ ## Setup
2575
+
2576
+ Requires Java 17+ and Maven 3.8+.
2577
+
2578
+ \`\`\`sh
2579
+ # Run all tests
2580
+ mvn test
2581
+
2582
+ # Pass secrets as env vars
2583
+ BASE_URL=https://api.example.com mvn test
2584
+ \`\`\`
2585
+ `;
2586
+ }
2587
+ function generateRestAssured(collection, environment) {
2588
+ const files = [
2589
+ { path: "pom.xml", content: buildPom(collection.name) },
2590
+ { path: "src/test/java/com/example/api/BaseTest.java", content: buildBaseTest(environment) }
2591
+ ];
2592
+ function processFolder(folder, name) {
2593
+ if (folder.requestIds.length > 0) {
2594
+ const className = javaClass(name) + "Test";
2595
+ files.push({
2596
+ path: `src/test/java/com/example/api/${className}.java`,
2597
+ content: buildTestClass(name, folder, collection.requests)
2598
+ });
2599
+ }
2600
+ for (const sub of folder.folders) {
2601
+ processFolder(sub, sub.name);
2602
+ }
2603
+ }
2604
+ if (collection.rootFolder.requestIds.length > 0) {
2605
+ processFolder(collection.rootFolder, collection.name);
2606
+ }
2607
+ for (const sub of collection.rootFolder.folders) {
2608
+ processFolder(sub, sub.name);
2609
+ }
2610
+ files.unshift({ path: "README.md", content: buildReadme(collection.name, files.map((f) => f.path)) });
2611
+ return files;
2612
+ }
2613
+ function registerGenerateHandlers(ipc) {
2614
+ ipc.handle("generate:code", (_e, opts) => {
2615
+ const { collection, environment, target } = opts;
2616
+ switch (target) {
2617
+ case "robot_framework":
2618
+ return generateRobotFramework(collection, environment);
2619
+ case "playwright_ts":
2620
+ return generatePlaywright(collection, environment);
2621
+ case "playwright_js":
2622
+ return generatePlaywrightJs(collection, environment);
2623
+ case "supertest_ts":
2624
+ return generateSupertestTs(collection, environment);
2625
+ case "supertest_js":
2626
+ return generateSupertestJs(collection, environment);
2627
+ case "rest_assured":
2628
+ return generateRestAssured(collection, environment);
2629
+ default:
2630
+ throw new Error(`Unknown target: ${target}`);
2631
+ }
2632
+ });
2633
+ ipc.handle("generate:save", async (_e, files, outputDir) => {
2634
+ for (const file of files) {
2635
+ const fullPath = path.join(outputDir, file.path);
2636
+ await promises.mkdir(path.dirname(fullPath), { recursive: true });
2637
+ await promises.writeFile(fullPath, file.content, "utf8");
2638
+ }
2639
+ });
2640
+ ipc.handle("generate:saveZip", async (_e, files, collectionName, target) => {
2641
+ const colSlug = collectionName.replace(/\W+/g, "-").toLowerCase();
2642
+ const targetSlug = target.replace(/_/g, "-");
2643
+ const defaultName = `${colSlug}-${targetSlug}.zip`;
2644
+ const { canceled, filePath } = await electron.dialog.showSaveDialog({
2645
+ title: "Save as ZIP",
2646
+ defaultPath: defaultName,
2647
+ filters: [{ name: "ZIP Archive", extensions: ["zip"] }]
2648
+ });
2649
+ if (canceled || !filePath) return false;
2650
+ const zip = new JSZip();
2651
+ for (const file of files) zip.file(file.path, file.content);
2652
+ const buffer = await zip.generateAsync({ type: "nodebuffer" });
2653
+ await promises.writeFile(filePath, buffer);
2654
+ return true;
2655
+ });
2656
+ }
2657
+ async function buildDispatcher(proxy, tls) {
2658
+ const connectOpts = {};
2659
+ let hasTls = false;
2660
+ if (tls) {
2661
+ hasTls = true;
2662
+ if (tls.rejectUnauthorized !== void 0) connectOpts["rejectUnauthorized"] = tls.rejectUnauthorized;
2663
+ if (tls.caCertPath) {
2664
+ try {
2665
+ connectOpts["ca"] = await promises.readFile(tls.caCertPath);
2666
+ } catch {
2667
+ }
2668
+ }
2669
+ if (tls.clientCertPath) {
2670
+ try {
2671
+ connectOpts["cert"] = await promises.readFile(tls.clientCertPath);
2672
+ } catch {
2673
+ }
2674
+ }
2675
+ if (tls.clientKeyPath) {
2676
+ try {
2677
+ connectOpts["key"] = await promises.readFile(tls.clientKeyPath);
2678
+ } catch {
2679
+ }
2680
+ }
2681
+ }
2682
+ if (proxy?.url) {
2683
+ const proxyUri = proxy.auth ? proxy.url.replace("://", `://${encodeURIComponent(proxy.auth.username)}:${encodeURIComponent(proxy.auth.password)}@`) : proxy.url;
2684
+ return new undici.ProxyAgent({
2685
+ uri: proxyUri,
2686
+ ...hasTls ? { connect: connectOpts } : {}
2687
+ });
2688
+ }
2689
+ if (hasTls) return new undici.Agent({ connect: connectOpts });
2690
+ return void 0;
2691
+ }
2692
+ async function executeOne(req, collectionVars, envVars, globals, localVars, dispatcher, piiMaskPatterns) {
2693
+ const base = {
2694
+ requestId: req.id,
2695
+ name: req.name,
2696
+ method: req.method,
2697
+ resolvedUrl: "",
2698
+ status: "running"
2699
+ };
2700
+ let vars = scriptRunner.mergeVars(envVars, collectionVars, globals, localVars);
2701
+ let updatedEnvVars = { ...envVars };
2702
+ let updatedCollectionVars = { ...collectionVars };
2703
+ let updatedGlobals = { ...globals };
2704
+ let preScriptError;
2705
+ if (req.preRequestScript?.trim()) {
2706
+ const r = await scriptRunner.runScript(req.preRequestScript, {
2707
+ envVars: { ...envVars },
2708
+ collectionVars: { ...collectionVars },
2709
+ globals: { ...globals },
2710
+ localVars: {}
2711
+ });
2712
+ preScriptError = r.error;
2713
+ localVars = r.updatedLocalVars;
2714
+ updatedEnvVars = r.updatedEnvVars;
2715
+ updatedCollectionVars = r.updatedCollectionVars;
2716
+ updatedGlobals = r.updatedGlobals;
2717
+ scriptRunner.patchGlobals(r.updatedGlobals);
2718
+ await scriptRunner.persistGlobals();
2719
+ vars = scriptRunner.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars);
2720
+ }
2721
+ const resolvedUrl = scriptRunner.buildUrl(req.url, req.params, vars);
2722
+ base.resolvedUrl = resolvedUrl;
2723
+ const start = Date.now();
2724
+ try {
2725
+ if (req.auth.type === "oauth2") {
2726
+ const now = Date.now();
2727
+ const tokenMissing = !req.auth.oauth2CachedToken;
2728
+ const tokenExpired = req.auth.oauth2TokenExpiry ? req.auth.oauth2TokenExpiry <= now + 5e3 : true;
2729
+ if (tokenMissing || tokenExpired) {
2730
+ const result = await fetchOAuth2Token(req.auth, vars);
2731
+ req.auth.oauth2CachedToken = result.accessToken;
2732
+ req.auth.oauth2TokenExpiry = result.expiresAt;
2733
+ }
2734
+ }
2735
+ const authHeaders = await buildAuthHeaders(req.auth, vars);
2736
+ const headers = new undici.Headers();
2737
+ for (const h of req.headers) {
2738
+ if (h.enabled && h.key) headers.set(scriptRunner.interpolate(h.key, vars), scriptRunner.interpolate(h.value, vars));
2739
+ }
2740
+ for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
2741
+ let body;
2742
+ if (req.body.mode === "json" && req.body.json) {
2743
+ body = scriptRunner.interpolate(req.body.json, vars);
2744
+ if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
2745
+ } else if (req.body.mode === "raw" && req.body.raw) {
2746
+ body = scriptRunner.interpolate(req.body.raw, vars);
2747
+ if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
2748
+ }
2749
+ const methodHasBody = !["GET", "HEAD"].includes(req.method);
2750
+ const doFetch = (h) => undici.fetch(resolvedUrl, {
2751
+ method: req.method,
2752
+ headers: h,
2753
+ body: methodHasBody ? body : void 0,
2754
+ dispatcher
2755
+ });
2756
+ let fetchResp;
2757
+ if (req.auth.type === "ntlm") {
2758
+ await performNtlmRequest(resolvedUrl, req.method, req.auth, vars);
2759
+ fetchResp = await doFetch(headers);
2760
+ } else if (req.auth.type === "digest") {
2761
+ const probeFetch = (url, init) => undici.fetch(url, {
2762
+ ...init,
2763
+ dispatcher
2764
+ });
2765
+ const digestHeader = await performDigestAuth(resolvedUrl, req.method, req.auth, vars, probeFetch);
2766
+ if (digestHeader) headers.set("Authorization", digestHeader);
2767
+ fetchResp = await doFetch(headers);
2768
+ } else {
2769
+ fetchResp = await doFetch(headers);
2770
+ }
2771
+ const responseBody = await fetchResp.text();
2772
+ const durationMs = Date.now() - start;
2773
+ const rawRespHeaders = {};
2774
+ fetchResp.headers.forEach((v, k) => {
2775
+ rawRespHeaders[k] = v;
2776
+ });
2777
+ const maskedBody = maskPii(responseBody, piiMaskPatterns);
2778
+ const maskedHeaders = maskHeaders(rawRespHeaders, piiMaskPatterns);
2779
+ const response = {
2780
+ status: fetchResp.status,
2781
+ statusText: fetchResp.statusText,
2782
+ headers: maskedHeaders,
2783
+ body: maskedBody,
2784
+ bodySize: Buffer.byteLength(responseBody, "utf8"),
2785
+ durationMs
2786
+ };
2787
+ let testResults = [];
2788
+ let consoleOutput = [];
2789
+ let postScriptError;
2790
+ if (req.postRequestScript?.trim()) {
2791
+ const r = await scriptRunner.runScript(req.postRequestScript, {
2792
+ envVars: updatedEnvVars,
2793
+ collectionVars: updatedCollectionVars,
2794
+ globals: updatedGlobals,
2795
+ localVars,
2796
+ response
2797
+ });
2798
+ testResults = r.testResults;
2799
+ consoleOutput = r.consoleOutput;
2800
+ postScriptError = r.error;
2801
+ updatedEnvVars = r.updatedEnvVars;
2802
+ updatedCollectionVars = r.updatedCollectionVars;
2803
+ updatedGlobals = r.updatedGlobals;
2804
+ scriptRunner.patchGlobals(r.updatedGlobals);
2805
+ await scriptRunner.persistGlobals();
2806
+ }
2807
+ const allPassed = testResults.every((t) => t.passed);
2808
+ const status = postScriptError ? "error" : testResults.length > 0 ? allPassed ? "passed" : "failed" : "passed";
2809
+ const sentHeaders = {};
2810
+ headers.forEach((v, k) => {
2811
+ sentHeaders[k] = v;
2812
+ });
2813
+ return {
2814
+ result: {
2815
+ ...base,
2816
+ status,
2817
+ httpStatus: fetchResp.status,
2818
+ durationMs,
2819
+ testResults,
2820
+ consoleOutput,
2821
+ preScriptError,
2822
+ postScriptError,
2823
+ sentRequest: { headers: sentHeaders, body: body ?? void 0 },
2824
+ receivedResponse: {
2825
+ status: fetchResp.status,
2826
+ statusText: fetchResp.statusText,
2827
+ headers: maskedHeaders,
2828
+ body: maskedBody
2829
+ }
2830
+ },
2831
+ updatedEnvVars,
2832
+ updatedCollectionVars,
2833
+ updatedGlobals
2834
+ };
2835
+ } catch (err) {
2836
+ return {
2837
+ result: {
2838
+ ...base,
2839
+ status: "error",
2840
+ durationMs: Date.now() - start,
2841
+ error: err instanceof Error ? err.message : String(err),
2842
+ preScriptError
2843
+ },
2844
+ updatedEnvVars,
2845
+ updatedCollectionVars,
2846
+ updatedGlobals
2847
+ };
2848
+ }
2849
+ }
2850
+ const sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
2851
+ function registerRunnerHandler(ipc) {
2852
+ ipc.handle("runner:start", async (event, payload) => {
2853
+ const { items, environment, globals: payloadGlobals, proxy, tls, piiMaskPatterns = [], requestDelay = 0 } = payload;
2854
+ const envVars = await scriptRunner.buildEnvVars(environment);
2855
+ const liveGlobals = scriptRunner.getGlobals();
2856
+ const globals = { ...payloadGlobals, ...liveGlobals };
2857
+ const dispatcher = await buildDispatcher(proxy, tls);
2858
+ const summary = { total: items.length, passed: 0, failed: 0, errors: 0, durationMs: 0 };
2859
+ const totalStart = Date.now();
2860
+ let runEnvVars = { ...envVars };
2861
+ let runCollectionVars = {};
2862
+ let runGlobals = { ...globals };
2863
+ for (const item of items) {
2864
+ const runningUpdate = { status: "running", iterationLabel: item.iterationLabel };
2865
+ event.sender.send("runner:progress", { requestId: item.request.id, ...runningUpdate });
2866
+ const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals } = await executeOne(
2867
+ item.request,
2868
+ { ...item.collectionVars, ...runCollectionVars },
2869
+ runEnvVars,
2870
+ runGlobals,
2871
+ item.dataRow ?? {},
2872
+ dispatcher,
2873
+ piiMaskPatterns
2874
+ );
2875
+ runEnvVars = updatedEnvVars;
2876
+ runCollectionVars = updatedCollectionVars;
2877
+ runGlobals = updatedGlobals;
2878
+ if (result.status === "passed") summary.passed++;
2879
+ else if (result.status === "failed") summary.failed++;
2880
+ else summary.errors++;
2881
+ event.sender.send("runner:progress", { ...result, iterationLabel: item.iterationLabel });
2882
+ if (requestDelay > 0 && item !== items[items.length - 1]) {
2883
+ await sleep(requestDelay);
2884
+ }
2885
+ }
2886
+ summary.durationMs = Date.now() - totalStart;
2887
+ return summary;
2888
+ });
2889
+ }
2890
+ function registerMockHandlers(ipc) {
2891
+ ipc.handle("mock:start", async (e, server) => {
2892
+ mockServer.setHitCallback((hit) => e.sender.send("mock:hit", hit));
2893
+ await mockServer.startMock(server);
2894
+ });
2895
+ ipc.handle("mock:stop", async (_e, id) => {
2896
+ await mockServer.stopMock(id);
2897
+ });
2898
+ ipc.handle("mock:isRunning", (_e, id) => mockServer.isRunning(id));
2899
+ ipc.handle("mock:updateRoutes", (_e, id, routes) => {
2900
+ mockServer.updateMockRoutes(id, routes);
2901
+ });
2902
+ ipc.handle("mock:runningIds", () => mockServer.getRunningIds());
2903
+ ipc.handle("file:saveMock", async (_e, relPath, server) => {
2904
+ const wsDir = getWorkspaceDir();
2905
+ if (!wsDir) throw new Error("No workspace open");
2906
+ const fullPath = path.join(wsDir, relPath);
2907
+ await promises.mkdir(path.dirname(fullPath), { recursive: true });
2908
+ await promises.writeFile(fullPath, JSON.stringify(server, null, 2), "utf8");
2909
+ });
2910
+ ipc.handle("file:loadMock", async (_e, relPath) => {
2911
+ const wsDir = getWorkspaceDir();
2912
+ if (!wsDir) throw new Error("No workspace open");
2913
+ const raw = await promises.readFile(path.join(wsDir, relPath), "utf8");
2914
+ return JSON.parse(raw);
2915
+ });
2916
+ }
2917
+ function registerOAuth2Handlers(ipc) {
2918
+ ipc.handle("oauth2:startFlow", async (_e, auth, vars) => {
2919
+ const port = auth.oauth2RedirectPort ?? 9876;
2920
+ const redirectUri = `http://localhost:${port}/callback`;
2921
+ const authUrl = scriptRunner.interpolate(auth.oauth2AuthUrl ?? "", vars);
2922
+ const tokenUrl = scriptRunner.interpolate(auth.oauth2TokenUrl ?? "", vars);
2923
+ const clientId = scriptRunner.interpolate(auth.oauth2ClientId ?? "", vars);
2924
+ let clientSecret = auth.oauth2ClientSecret ?? "";
2925
+ if (!clientSecret && auth.oauth2ClientSecretRef) {
2926
+ clientSecret = await scriptRunner.getSecret(auth.oauth2ClientSecretRef) ?? "";
2927
+ }
2928
+ clientSecret = scriptRunner.interpolate(clientSecret, vars);
2929
+ if (!authUrl) throw new Error("OAuth 2.0: authUrl is required for authorization_code flow.");
2930
+ if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required for authorization_code flow.");
2931
+ if (!clientId) throw new Error("OAuth 2.0: clientId is required.");
2932
+ const state = Math.random().toString(36).slice(2);
2933
+ const authUrlFull = new URL(authUrl);
2934
+ authUrlFull.searchParams.set("response_type", "code");
2935
+ authUrlFull.searchParams.set("client_id", clientId);
2936
+ authUrlFull.searchParams.set("redirect_uri", redirectUri);
2937
+ authUrlFull.searchParams.set("state", state);
2938
+ if (auth.oauth2Scopes) authUrlFull.searchParams.set("scope", auth.oauth2Scopes);
2939
+ const code = await new Promise((resolve2, reject) => {
2940
+ const server = http.createServer((req, res) => {
2941
+ const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
2942
+ if (!reqUrl.pathname.startsWith("/callback")) {
2943
+ res.writeHead(404);
2944
+ res.end();
2945
+ return;
2946
+ }
2947
+ const returnedState = reqUrl.searchParams.get("state");
2948
+ const returnedCode = reqUrl.searchParams.get("code");
2949
+ const error = reqUrl.searchParams.get("error");
2950
+ res.writeHead(200, { "Content-Type": "text/html" });
2951
+ res.end("<html><body><p>Authorization complete. You may close this tab.</p></body></html>");
2952
+ server.close();
2953
+ if (error) {
2954
+ reject(new Error(`OAuth 2.0 authorization error: ${error}`));
2955
+ return;
2956
+ }
2957
+ if (returnedState !== state) {
2958
+ reject(new Error("OAuth 2.0: state mismatch."));
2959
+ return;
2960
+ }
2961
+ if (!returnedCode) {
2962
+ reject(new Error("OAuth 2.0: no code in callback."));
2963
+ return;
2964
+ }
2965
+ resolve2(returnedCode);
2966
+ });
2967
+ server.on("error", reject);
2968
+ server.listen(port, "127.0.0.1", () => {
2969
+ electron.shell.openExternal(authUrlFull.toString()).catch(reject);
2970
+ });
2971
+ setTimeout(() => {
2972
+ server.close();
2973
+ reject(new Error("OAuth 2.0 authorization timed out (5 min)."));
2974
+ }, 5 * 60 * 1e3);
2975
+ });
2976
+ const { fetch: nodeFetch } = await import("undici");
2977
+ const params = new URLSearchParams();
2978
+ params.set("grant_type", "authorization_code");
2979
+ params.set("code", code);
2980
+ params.set("redirect_uri", redirectUri);
2981
+ params.set("client_id", clientId);
2982
+ if (clientSecret) params.set("client_secret", clientSecret);
2983
+ const resp = await nodeFetch(tokenUrl, {
2984
+ method: "POST",
2985
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2986
+ body: params.toString()
2987
+ });
2988
+ if (!resp.ok) {
2989
+ const body = await resp.text();
2990
+ throw new Error(`OAuth 2.0 token exchange failed (${resp.status}): ${body}`);
2991
+ }
2992
+ const json = await resp.json();
2993
+ const accessToken = String(json["access_token"] ?? "");
2994
+ if (!accessToken) throw new Error("OAuth 2.0: token response missing access_token.");
2995
+ const expiresIn = Number(json["expires_in"] ?? 3600);
2996
+ return {
2997
+ accessToken,
2998
+ expiresAt: Date.now() + expiresIn * 1e3,
2999
+ refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
3000
+ };
3001
+ });
3002
+ ipc.handle("oauth2:refreshToken", async (_e, auth, vars, refreshToken) => {
3003
+ const tokenUrl = scriptRunner.interpolate(auth.oauth2TokenUrl ?? "", vars);
3004
+ const clientId = scriptRunner.interpolate(auth.oauth2ClientId ?? "", vars);
3005
+ let clientSecret = auth.oauth2ClientSecret ?? "";
3006
+ if (!clientSecret && auth.oauth2ClientSecretRef) {
3007
+ clientSecret = await scriptRunner.getSecret(auth.oauth2ClientSecretRef) ?? "";
3008
+ }
3009
+ clientSecret = scriptRunner.interpolate(clientSecret, vars);
3010
+ if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required for refresh.");
3011
+ const { fetch: nodeFetch } = await import("undici");
3012
+ const params = new URLSearchParams();
3013
+ params.set("grant_type", "refresh_token");
3014
+ params.set("refresh_token", refreshToken);
3015
+ params.set("client_id", clientId);
3016
+ if (clientSecret) params.set("client_secret", clientSecret);
3017
+ const resp = await nodeFetch(tokenUrl, {
3018
+ method: "POST",
3019
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3020
+ body: params.toString()
3021
+ });
3022
+ if (!resp.ok) {
3023
+ const body = await resp.text();
3024
+ throw new Error(`OAuth 2.0 token refresh failed (${resp.status}): ${body}`);
3025
+ }
3026
+ const json = await resp.json();
3027
+ const accessToken = String(json["access_token"] ?? "");
3028
+ if (!accessToken) throw new Error("OAuth 2.0: refresh response missing access_token.");
3029
+ const expiresIn = Number(json["expires_in"] ?? 3600);
3030
+ return {
3031
+ accessToken,
3032
+ expiresAt: Date.now() + expiresIn * 1e3,
3033
+ refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : refreshToken
3034
+ };
3035
+ });
3036
+ }
3037
+ const connections = /* @__PURE__ */ new Map();
3038
+ function closeAllWsConnections() {
3039
+ for (const [, ws] of connections) {
3040
+ try {
3041
+ ws.close();
3042
+ } catch {
3043
+ }
3044
+ }
3045
+ connections.clear();
3046
+ }
3047
+ function registerWsHandlers(ipc) {
3048
+ ipc.handle("ws:connect", async (event, requestId, url, headers) => {
3049
+ const existing = connections.get(requestId);
3050
+ if (existing) {
3051
+ existing.close();
3052
+ connections.delete(requestId);
3053
+ }
3054
+ event.sender.send("ws:status", { requestId, status: "connecting" });
3055
+ const ws = new WebSocket(url, { headers });
3056
+ connections.set(requestId, ws);
3057
+ ws.on("open", () => {
3058
+ event.sender.send("ws:status", { requestId, status: "connected" });
3059
+ });
3060
+ ws.on("message", (data) => {
3061
+ const message = {
3062
+ id: uuid.v4(),
3063
+ direction: "received",
3064
+ data: data.toString(),
3065
+ timestamp: Date.now()
3066
+ };
3067
+ event.sender.send("ws:message", { requestId, message });
3068
+ });
3069
+ ws.on("error", (err) => {
3070
+ event.sender.send("ws:status", { requestId, status: "error", error: err.message });
3071
+ connections.delete(requestId);
3072
+ });
3073
+ ws.on("close", () => {
3074
+ event.sender.send("ws:status", { requestId, status: "disconnected" });
3075
+ connections.delete(requestId);
3076
+ });
3077
+ });
3078
+ ipc.handle("ws:send", async (_event, requestId, data) => {
3079
+ const ws = connections.get(requestId);
3080
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
3081
+ throw new Error("WebSocket is not connected");
3082
+ }
3083
+ ws.send(data);
3084
+ });
3085
+ ipc.handle("ws:disconnect", async (_event, requestId) => {
3086
+ const ws = connections.get(requestId);
3087
+ if (ws) {
3088
+ ws.close();
3089
+ connections.delete(requestId);
3090
+ }
3091
+ });
3092
+ }
3093
+ function fetchUrl(url, headers = {}) {
3094
+ return new Promise((resolve2, reject) => {
3095
+ const lib = url.startsWith("https") ? https : http;
3096
+ const req = lib.get(url, { headers }, (res) => {
3097
+ const chunks = [];
3098
+ res.on("data", (chunk) => chunks.push(chunk));
3099
+ res.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
3100
+ res.on("error", reject);
3101
+ });
3102
+ req.on("error", reject);
3103
+ req.setTimeout(15e3, () => {
3104
+ req.destroy();
3105
+ reject(new Error("WSDL fetch timed out"));
3106
+ });
3107
+ });
3108
+ }
3109
+ function parseWsdl(wsdlText) {
3110
+ const nsMatch = wsdlText.match(/targetNamespace\s*=\s*["']([^"']+)["']/);
3111
+ const targetNamespace = nsMatch ? nsMatch[1] : "";
3112
+ const operationRegex = /<(?:wsdl:)?operation\s+name\s*=\s*["']([^"']+)["']/g;
3113
+ const operationNames = /* @__PURE__ */ new Set();
3114
+ let m;
3115
+ while ((m = operationRegex.exec(wsdlText)) !== null) {
3116
+ operationNames.add(m[1]);
3117
+ }
3118
+ const soapActionMap = {};
3119
+ const bindingOpRegex = /<(?:wsdl:)?operation\s+name\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)<\/(?:wsdl:)?operation>/g;
3120
+ while ((m = bindingOpRegex.exec(wsdlText)) !== null) {
3121
+ const opName = m[1];
3122
+ const block = m[2];
3123
+ const saMatch = block.match(/soapAction\s*=\s*["']([^"']*)["']/);
3124
+ if (saMatch) soapActionMap[opName] = saMatch[1];
3125
+ }
3126
+ const operations = [];
3127
+ for (const name of operationNames) {
3128
+ const soapAction = soapActionMap[name];
3129
+ const inputTemplate = buildEnvelopeTemplate(name, targetNamespace);
3130
+ operations.push({ name, soapAction, inputTemplate });
3131
+ }
3132
+ return { operations, targetNamespace };
3133
+ }
3134
+ function buildEnvelopeTemplate(operationName, namespace) {
3135
+ return `<?xml version="1.0" encoding="utf-8"?>
3136
+ <soap:Envelope
3137
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
3138
+ xmlns:tns="${namespace}">
3139
+ <soap:Header/>
3140
+ <soap:Body>
3141
+ <tns:${operationName}>
3142
+ <!-- Add parameters here -->
3143
+ </tns:${operationName}>
3144
+ </soap:Body>
3145
+ </soap:Envelope>`;
3146
+ }
3147
+ function registerSoapHandlers(ipc) {
3148
+ ipc.handle("wsdl:fetch", async (_event, url, extraHeaders = {}) => {
3149
+ const wsdlText = await fetchUrl(url, extraHeaders);
3150
+ return parseWsdl(wsdlText);
3151
+ });
3152
+ }
3153
+ function escMd(s) {
3154
+ return s.replace(/[|\\`*_{}[\]()#+\-.!]/g, (c) => `\\${c}`);
3155
+ }
3156
+ function requestToMarkdown(req) {
3157
+ const lines = [];
3158
+ const methodLabel = req.protocol === "websocket" ? "WS" : req.method;
3159
+ lines.push(`#### ${methodLabel} ${escMd(req.name)}`);
3160
+ lines.push("");
3161
+ if (req.description?.trim()) {
3162
+ lines.push(req.description.trim());
3163
+ lines.push("");
3164
+ }
3165
+ lines.push("**URL**");
3166
+ lines.push("```");
3167
+ lines.push(req.url || "(no url)");
3168
+ lines.push("```");
3169
+ lines.push("");
3170
+ const enabledParams = req.params.filter((p) => p.enabled && p.key);
3171
+ if (enabledParams.length) {
3172
+ lines.push("**Query Parameters**");
3173
+ lines.push("");
3174
+ lines.push("| Key | Value | Description |");
3175
+ lines.push("|-----|-------|-------------|");
3176
+ for (const p of enabledParams) {
3177
+ lines.push(`| ${escMd(p.key)} | ${escMd(p.value)} | ${escMd(p.description ?? "")} |`);
3178
+ }
3179
+ lines.push("");
3180
+ }
3181
+ const enabledHeaders = req.headers.filter((h) => h.enabled && h.key);
3182
+ if (enabledHeaders.length) {
3183
+ lines.push("**Headers**");
3184
+ lines.push("");
3185
+ lines.push("| Key | Value |");
3186
+ lines.push("|-----|-------|");
3187
+ for (const h of enabledHeaders) {
3188
+ lines.push(`| ${escMd(h.key)} | ${escMd(h.value)} |`);
3189
+ }
3190
+ lines.push("");
3191
+ }
3192
+ if (req.auth.type !== "none") {
3193
+ lines.push(`**Auth**: ${req.auth.type}`);
3194
+ lines.push("");
3195
+ }
3196
+ const mode = req.body.mode;
3197
+ if (mode === "json" && req.body.json?.trim()) {
3198
+ lines.push("**Body** (JSON)");
3199
+ lines.push("```json");
3200
+ lines.push(req.body.json.trim());
3201
+ lines.push("```");
3202
+ lines.push("");
3203
+ } else if (mode === "raw" && req.body.raw?.trim()) {
3204
+ const ct = req.body.rawContentType ?? "text";
3205
+ lines.push(`**Body** (${ct})`);
3206
+ lines.push("```");
3207
+ lines.push(req.body.raw.trim());
3208
+ lines.push("```");
3209
+ lines.push("");
3210
+ } else if (mode === "graphql" && req.body.graphql?.query?.trim()) {
3211
+ lines.push("**Body** (GraphQL)");
3212
+ lines.push("```graphql");
3213
+ lines.push(req.body.graphql.query.trim());
3214
+ lines.push("```");
3215
+ lines.push("");
3216
+ } else if (mode === "soap" && req.body.soap?.envelope?.trim()) {
3217
+ lines.push("**Body** (SOAP)");
3218
+ lines.push("```xml");
3219
+ lines.push(req.body.soap.envelope.trim());
3220
+ lines.push("```");
3221
+ lines.push("");
3222
+ }
3223
+ return lines.join("\n");
3224
+ }
3225
+ function folderToMarkdown(folder, requests, depth) {
3226
+ const lines = [];
3227
+ const heading = "#".repeat(depth);
3228
+ if (folder.name !== "root") {
3229
+ lines.push(`${heading} ${escMd(folder.name)}`);
3230
+ lines.push("");
3231
+ if (folder.description?.trim()) {
3232
+ lines.push(folder.description.trim());
3233
+ lines.push("");
3234
+ }
3235
+ }
3236
+ for (const reqId of folder.requestIds) {
3237
+ const req = requests[reqId];
3238
+ if (req) {
3239
+ lines.push(requestToMarkdown(req));
3240
+ }
3241
+ }
3242
+ for (const sub of folder.folders) {
3243
+ lines.push(folderToMarkdown(sub, requests, depth + 1));
3244
+ }
3245
+ return lines.join("\n");
3246
+ }
3247
+ function generateMarkdown(payload) {
3248
+ const lines = [];
3249
+ lines.push("# API Documentation");
3250
+ lines.push("");
3251
+ for (const { collection, requests } of payload.collections) {
3252
+ lines.push(`## ${escMd(collection.name)}`);
3253
+ lines.push("");
3254
+ if (collection.description?.trim()) {
3255
+ lines.push(collection.description.trim());
3256
+ lines.push("");
3257
+ }
3258
+ lines.push(folderToMarkdown(collection.rootFolder, requests, 3));
3259
+ }
3260
+ return lines.join("\n");
3261
+ }
3262
+ function escHtml(s) {
3263
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3264
+ }
3265
+ const METHOD_COLORS = {
3266
+ GET: "#34d399",
3267
+ POST: "#60a5fa",
3268
+ PUT: "#fbbf24",
3269
+ PATCH: "#fb923c",
3270
+ DELETE: "#f87171",
3271
+ HEAD: "#6aa3c8",
3272
+ OPTIONS: "#9ca3af",
3273
+ WS: "#22d3ee"
3274
+ };
3275
+ function requestToHtml(req) {
3276
+ const methodLabel = req.protocol === "websocket" ? "WS" : req.method;
3277
+ const color = METHOD_COLORS[methodLabel] ?? "#9ca3af";
3278
+ let html = `<div class="request">`;
3279
+ html += `<h4><span class="method" style="color:${color}">${escHtml(methodLabel)}</span> ${escHtml(req.name)}</h4>`;
3280
+ if (req.description?.trim()) {
3281
+ html += `<p class="desc">${escHtml(req.description.trim())}</p>`;
3282
+ }
3283
+ html += `<div class="label">URL</div><pre><code>${escHtml(req.url || "(no url)")}</code></pre>`;
3284
+ const enabledParams = req.params.filter((p) => p.enabled && p.key);
3285
+ if (enabledParams.length) {
3286
+ html += `<div class="label">Query Parameters</div><table><thead><tr><th>Key</th><th>Value</th><th>Description</th></tr></thead><tbody>`;
3287
+ for (const p of enabledParams) {
3288
+ html += `<tr><td>${escHtml(p.key)}</td><td>${escHtml(p.value)}</td><td>${escHtml(p.description ?? "")}</td></tr>`;
3289
+ }
3290
+ html += `</tbody></table>`;
3291
+ }
3292
+ const enabledHeaders = req.headers.filter((h) => h.enabled && h.key);
3293
+ if (enabledHeaders.length) {
3294
+ html += `<div class="label">Headers</div><table><thead><tr><th>Key</th><th>Value</th></tr></thead><tbody>`;
3295
+ for (const h of enabledHeaders) {
3296
+ html += `<tr><td>${escHtml(h.key)}</td><td>${escHtml(h.value)}</td></tr>`;
3297
+ }
3298
+ html += `</tbody></table>`;
3299
+ }
3300
+ if (req.auth.type !== "none") {
3301
+ html += `<div class="label">Auth</div><p class="auth-type">${escHtml(req.auth.type)}</p>`;
3302
+ }
3303
+ const mode = req.body.mode;
3304
+ if (mode === "json" && req.body.json?.trim()) {
3305
+ html += `<div class="label">Body (JSON)</div><pre><code class="lang-json">${escHtml(req.body.json.trim())}</code></pre>`;
3306
+ } else if (mode === "raw" && req.body.raw?.trim()) {
3307
+ html += `<div class="label">Body (${escHtml(req.body.rawContentType ?? "text")})</div><pre><code>${escHtml(req.body.raw.trim())}</code></pre>`;
3308
+ } else if (mode === "graphql" && req.body.graphql?.query?.trim()) {
3309
+ html += `<div class="label">Body (GraphQL)</div><pre><code>${escHtml(req.body.graphql.query.trim())}</code></pre>`;
3310
+ } else if (mode === "soap" && req.body.soap?.envelope?.trim()) {
3311
+ html += `<div class="label">Body (SOAP)</div><pre><code>${escHtml(req.body.soap.envelope.trim())}</code></pre>`;
3312
+ }
3313
+ html += `</div>`;
3314
+ return html;
3315
+ }
3316
+ function folderToHtml(folder, requests, depth) {
3317
+ let html = "";
3318
+ const tag = `h${Math.min(depth, 6)}`;
3319
+ if (folder.name !== "root") {
3320
+ html += `<${tag} class="folder-heading">${escHtml(folder.name)}</${tag}>`;
3321
+ if (folder.description?.trim()) {
3322
+ html += `<p class="folder-desc">${escHtml(folder.description.trim())}</p>`;
3323
+ }
3324
+ }
3325
+ for (const reqId of folder.requestIds) {
3326
+ const req = requests[reqId];
3327
+ if (req) html += requestToHtml(req);
3328
+ }
3329
+ for (const sub of folder.folders) {
3330
+ html += folderToHtml(sub, requests, depth + 1);
3331
+ }
3332
+ return html;
3333
+ }
3334
+ function generateHtml(payload) {
3335
+ let body = "";
3336
+ for (const { collection, requests } of payload.collections) {
3337
+ body += `<section class="collection"><h2>${escHtml(collection.name)}</h2>`;
3338
+ if (collection.description?.trim()) {
3339
+ body += `<p class="collection-desc">${escHtml(collection.description.trim())}</p>`;
3340
+ }
3341
+ body += folderToHtml(collection.rootFolder, requests, 3);
3342
+ body += `</section>`;
3343
+ }
3344
+ return `<!DOCTYPE html>
3345
+ <html lang="en">
3346
+ <head>
3347
+ <meta charset="UTF-8" />
3348
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
3349
+ <title>API Documentation</title>
3350
+ <style>
3351
+ *, *::before, *::after { box-sizing: border-box; }
3352
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; color: #e2e8f0; margin: 0; padding: 2rem; line-height: 1.6; }
3353
+ h1 { font-size: 2rem; color: #f8fafc; border-bottom: 2px solid #1e293b; padding-bottom: .5rem; }
3354
+ h2 { font-size: 1.5rem; color: #f1f5f9; margin-top: 2.5rem; border-bottom: 1px solid #1e293b; padding-bottom: .4rem; }
3355
+ h3 { font-size: 1.2rem; color: #cbd5e1; margin-top: 2rem; }
3356
+ h4 { font-size: 1rem; margin: 0 0 .5rem; display: flex; align-items: center; gap: .5rem; color: #f1f5f9; }
3357
+ .method { font-weight: 700; font-family: monospace; font-size: .85rem; }
3358
+ .collection { margin-bottom: 3rem; }
3359
+ .collection-desc, .folder-desc, .desc { color: #94a3b8; font-size: .9rem; margin: .25rem 0 1rem; }
3360
+ .request { background: #1e2433; border: 1px solid #2d3748; border-radius: 8px; padding: 1rem 1.25rem; margin: 1rem 0; }
3361
+ .label { font-size: .7rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: #64748b; margin: .75rem 0 .3rem; }
3362
+ pre { background: #131924; border: 1px solid #2d3748; border-radius: 6px; padding: .75rem 1rem; overflow-x: auto; margin: 0; }
3363
+ code { font-family: 'JetBrains Mono', 'Fira Mono', monospace; font-size: .8rem; color: #a5f3fc; }
3364
+ table { border-collapse: collapse; width: 100%; font-size: .85rem; margin: .25rem 0; }
3365
+ th { background: #131924; color: #64748b; font-weight: 600; text-align: left; padding: .4rem .6rem; border: 1px solid #2d3748; }
3366
+ td { padding: .35rem .6rem; border: 1px solid #2d3748; color: #cbd5e1; font-family: monospace; font-size: .8rem; }
3367
+ .auth-type { background: #1e2433; border: 1px solid #2d3748; border-radius: 4px; display: inline-block; padding: .2rem .6rem; font-size: .8rem; color: #a5f3fc; margin: 0; }
3368
+ .folder-heading { color: #94a3b8; font-size: 1.05rem; }
3369
+ </style>
3370
+ </head>
3371
+ <body>
3372
+ <h1>API Documentation</h1>
3373
+ ${body}
3374
+ </body>
3375
+ </html>`;
3376
+ }
3377
+ function registerDocsHandlers(ipc) {
3378
+ ipc.handle("docs:generate", async (_event, payload) => {
3379
+ if (payload.format === "html") {
3380
+ return generateHtml(payload);
3381
+ }
3382
+ return generateMarkdown(payload);
3383
+ });
3384
+ }
3385
+ const ajv$1 = new Ajv({ allErrors: true, strict: false });
3386
+ function validateConsumerResponse(contract, actualStatus, actualHeaders, bodyText) {
3387
+ const violations = [];
3388
+ if (contract.statusCode !== void 0 && actualStatus !== contract.statusCode) {
3389
+ violations.push({
3390
+ type: "status_mismatch",
3391
+ message: `Expected status ${contract.statusCode}, got ${actualStatus}`,
3392
+ expected: String(contract.statusCode),
3393
+ actual: String(actualStatus)
3394
+ });
3395
+ }
3396
+ for (const expected of contract.headers ?? []) {
3397
+ if (!expected.required) continue;
3398
+ const actual = actualHeaders[expected.key.toLowerCase()];
3399
+ if (actual === void 0) {
3400
+ violations.push({
3401
+ type: "missing_header",
3402
+ message: `Required header "${expected.key}" is absent`,
3403
+ expected: expected.value || "(any)",
3404
+ actual: "(absent)"
3405
+ });
3406
+ } else if (expected.value && actual.split(";")[0].trim().toLowerCase() !== expected.value.split(";")[0].trim().toLowerCase()) {
3407
+ violations.push({
3408
+ type: "missing_header",
3409
+ message: `Header "${expected.key}" has unexpected value`,
3410
+ expected: expected.value,
3411
+ actual
3412
+ });
3413
+ }
3414
+ }
3415
+ if (contract.bodySchema?.trim()) {
3416
+ let schema;
3417
+ try {
3418
+ schema = JSON.parse(contract.bodySchema);
3419
+ } catch {
3420
+ violations.push({
3421
+ type: "schema_violation",
3422
+ message: "Contract bodySchema is not valid JSON"
3423
+ });
3424
+ return violations;
3425
+ }
3426
+ let data;
3427
+ try {
3428
+ data = JSON.parse(bodyText);
3429
+ } catch {
3430
+ violations.push({
3431
+ type: "schema_violation",
3432
+ message: "Response body is not valid JSON — cannot validate against schema"
3433
+ });
3434
+ return violations;
3435
+ }
3436
+ try {
3437
+ const validate = ajv$1.compile(schema);
3438
+ if (!validate(data)) {
3439
+ for (const err of validate.errors ?? []) {
3440
+ violations.push({
3441
+ type: "schema_violation",
3442
+ message: err.message ?? "Schema violation",
3443
+ path: err.instancePath || "/"
3444
+ });
3445
+ }
3446
+ }
3447
+ } catch (e) {
3448
+ violations.push({
3449
+ type: "schema_violation",
3450
+ message: `Schema compile error: ${e instanceof Error ? e.message : String(e)}`
3451
+ });
3452
+ }
3453
+ }
3454
+ return violations;
3455
+ }
3456
+ async function executeContract(req, vars) {
3457
+ const url = scriptRunner.buildUrl(req.url, req.params, vars);
3458
+ const start = Date.now();
3459
+ try {
3460
+ const headers = new undici.Headers();
3461
+ for (const h of req.headers) {
3462
+ if (h.enabled && h.key) headers.set(scriptRunner.interpolate(h.key, vars), scriptRunner.interpolate(h.value, vars));
3463
+ }
3464
+ const authHeaders = await buildAuthHeaders(req.auth, vars);
3465
+ for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
3466
+ let body;
3467
+ if (req.body.mode === "json" && req.body.json) {
3468
+ body = scriptRunner.interpolate(req.body.json, vars);
3469
+ if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
3470
+ } else if (req.body.mode === "raw" && req.body.raw) {
3471
+ body = scriptRunner.interpolate(req.body.raw, vars);
3472
+ }
3473
+ const resp = await undici.fetch(url, {
3474
+ method: req.method,
3475
+ headers,
3476
+ body: !["GET", "HEAD"].includes(req.method) ? body : void 0
3477
+ });
3478
+ const bodyText = await resp.text();
3479
+ const rawHeaders = {};
3480
+ resp.headers.forEach((v, k) => {
3481
+ rawHeaders[k] = v;
3482
+ });
3483
+ const violations = validateConsumerResponse(req.contract, resp.status, rawHeaders, bodyText);
3484
+ return {
3485
+ requestId: req.id,
3486
+ requestName: req.name,
3487
+ method: req.method,
3488
+ url,
3489
+ passed: violations.length === 0,
3490
+ violations,
3491
+ durationMs: Date.now() - start,
3492
+ actualStatus: resp.status
3493
+ };
3494
+ } catch (err) {
3495
+ return {
3496
+ requestId: req.id,
3497
+ requestName: req.name,
3498
+ method: req.method,
3499
+ url,
3500
+ passed: false,
3501
+ violations: [{
3502
+ type: "status_mismatch",
3503
+ message: `Request failed: ${err instanceof Error ? err.message : String(err)}`
3504
+ }],
3505
+ durationMs: Date.now() - start
3506
+ };
3507
+ }
3508
+ }
3509
+ async function runConsumerContracts(requests, envVars, collectionVars = {}) {
3510
+ const vars = { ...envVars, ...collectionVars };
3511
+ const contractRequests = requests.filter(
3512
+ (r) => r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
3513
+ );
3514
+ const start = Date.now();
3515
+ const results = await Promise.all(contractRequests.map((r) => executeContract(r, vars)));
3516
+ const passed = results.filter((r) => r.passed).length;
3517
+ return {
3518
+ mode: "consumer",
3519
+ total: results.length,
3520
+ passed,
3521
+ failed: results.length - passed,
3522
+ results,
3523
+ durationMs: Date.now() - start
3524
+ };
3525
+ }
3526
+ const ajv = new Ajv({ allErrors: true, strict: false });
3527
+ async function loadSpec(specUrl, specPath) {
3528
+ if (specUrl) {
3529
+ const resp = await undici.fetch(specUrl);
3530
+ if (!resp.ok) throw new Error(`HTTP ${resp.status} loading spec from ${specUrl}`);
3531
+ const text = await resp.text();
3532
+ const ct = resp.headers.get("content-type") ?? "";
3533
+ return ct.includes("yaml") || specUrl.endsWith(".yaml") || specUrl.endsWith(".yml") ? jsYaml.load(text) : JSON.parse(text);
3534
+ }
3535
+ if (specPath) {
3536
+ const raw = await promises.readFile(specPath, "utf8");
3537
+ return specPath.endsWith(".yaml") || specPath.endsWith(".yml") ? jsYaml.load(raw) : JSON.parse(raw);
3538
+ }
3539
+ throw new Error("Either specUrl or specPath must be provided");
3540
+ }
3541
+ function resolveRef(spec, ref) {
3542
+ const parts = ref.replace(/^#\//, "").split("/");
3543
+ return parts.reduce((obj, key) => obj?.[key], spec);
3544
+ }
3545
+ function resolveSchema(spec, obj, seen = /* @__PURE__ */ new Set()) {
3546
+ if (!obj || typeof obj !== "object") return obj;
3547
+ if (seen.has(obj)) return {};
3548
+ if (Array.isArray(obj)) {
3549
+ seen.add(obj);
3550
+ return obj.map((i) => resolveSchema(spec, i, seen));
3551
+ }
3552
+ const o = obj;
3553
+ if ("$ref" in o) {
3554
+ const target = resolveRef(spec, o["$ref"]);
3555
+ return resolveSchema(spec, target, seen);
3556
+ }
3557
+ seen.add(obj);
3558
+ return Object.fromEntries(Object.entries(o).map(([k, v]) => [k, resolveSchema(spec, v, seen)]));
3559
+ }
3560
+ function getServerBases(spec) {
3561
+ const servers = spec["servers"] ?? [];
3562
+ if (!servers.length) return [""];
3563
+ return servers.map((s) => {
3564
+ const raw = s.url ?? "";
3565
+ try {
3566
+ return new URL(raw).pathname.replace(/\/$/, "");
3567
+ } catch {
3568
+ return raw.replace(/\/$/, "");
3569
+ }
3570
+ });
3571
+ }
3572
+ function urlPathname(raw, baseUrl) {
3573
+ try {
3574
+ const pathname = new URL(raw).pathname;
3575
+ if (baseUrl) {
3576
+ try {
3577
+ const basePath = new URL(baseUrl).pathname.replace(/\/$/, "");
3578
+ if (basePath && pathname.startsWith(basePath)) return pathname.slice(basePath.length) || "/";
3579
+ } catch {
3580
+ }
3581
+ }
3582
+ return pathname;
3583
+ } catch {
3584
+ return raw.split("?")[0];
3585
+ }
3586
+ }
3587
+ function pathTemplateToRegex(base, template) {
3588
+ const combined = (base + template).replace(/\/+/g, "/");
3589
+ const pattern = combined.replace(/\{[^}]+\}/g, "[^/]+");
3590
+ return new RegExp("^" + pattern + "/?$");
3591
+ }
3592
+ function findOperation(spec, method, reqUrl, requestBaseUrl) {
3593
+ const bases = getServerBases(spec);
3594
+ const pathname = urlPathname(reqUrl, requestBaseUrl);
3595
+ const paths = spec["paths"] ?? {};
3596
+ for (const [template, pathItem] of Object.entries(paths)) {
3597
+ const resolved = resolveSchema(spec, pathItem);
3598
+ for (const base of bases) {
3599
+ if (pathTemplateToRegex(base, template).test(pathname)) {
3600
+ const op = resolved[method.toLowerCase()];
3601
+ return op ? { pathTemplate: template, operation: op } : null;
3602
+ }
3603
+ }
3604
+ }
3605
+ return null;
3606
+ }
3607
+ function validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl) {
3608
+ const violations = [];
3609
+ const vars = envVars;
3610
+ const url = req.url.replace(/\{\{([^}]+)\}\}/g, (_, k) => vars[k] ?? `{{${k}}}`);
3611
+ const match = findOperation(spec, req.method, url, requestBaseUrl);
3612
+ if (!match) {
3613
+ violations.push({
3614
+ type: "unknown_path",
3615
+ message: `No operation found in spec for ${req.method} ${url}`
3616
+ });
3617
+ return violations;
3618
+ }
3619
+ const { operation } = match;
3620
+ if (req.body.mode === "json" && req.body.json?.trim()) {
3621
+ const requestBody = resolveSchema(spec, operation["requestBody"]);
3622
+ const content = requestBody?.["content"] ?? {};
3623
+ const jsonContent = content["application/json"];
3624
+ if (jsonContent?.["schema"]) {
3625
+ try {
3626
+ const data = JSON.parse(scriptRunner.interpolate(req.body.json, vars));
3627
+ const schema = resolveSchema(spec, jsonContent["schema"]);
3628
+ const validate = ajv.compile(schema);
3629
+ if (!validate(data)) {
3630
+ for (const err of validate.errors ?? []) {
3631
+ violations.push({
3632
+ type: "request_body_invalid",
3633
+ message: err.message ?? "Request body schema violation",
3634
+ path: err.instancePath || "/"
3635
+ });
3636
+ }
3637
+ }
3638
+ } catch (e) {
3639
+ violations.push({
3640
+ type: "request_body_invalid",
3641
+ message: `Could not validate request body: ${e instanceof Error ? e.message : String(e)}`
3642
+ });
3643
+ }
3644
+ }
3645
+ }
3646
+ const parameters = resolveSchema(spec, operation["parameters"] ?? []);
3647
+ for (const param of parameters) {
3648
+ const p = param;
3649
+ if (p["in"] === "query" && p["required"] === true) {
3650
+ const name = p["name"];
3651
+ if (!req.params.some((kv) => kv.enabled && kv.key === name)) {
3652
+ violations.push({
3653
+ type: "request_body_invalid",
3654
+ message: `Required query parameter "${name}" is missing`,
3655
+ path: `query.${name}`
3656
+ });
3657
+ }
3658
+ }
3659
+ }
3660
+ return violations;
3661
+ }
3662
+ async function runProviderVerification(requests, envVars, specUrl, specPath, requestBaseUrl) {
3663
+ const spec = await loadSpec(specUrl, specPath);
3664
+ const start = Date.now();
3665
+ const results = requests.map((req) => {
3666
+ const violations = validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl);
3667
+ const url = req.url.replace(/\{\{([^}]+)\}\}/g, (_, k) => envVars[k] ?? `{{${k}}}`);
3668
+ return {
3669
+ requestId: req.id,
3670
+ requestName: req.name,
3671
+ method: req.method,
3672
+ url,
3673
+ passed: violations.length === 0,
3674
+ violations
3675
+ };
3676
+ });
3677
+ const passed = results.filter((r) => r.passed).length;
3678
+ return {
3679
+ mode: "provider",
3680
+ total: results.length,
3681
+ passed,
3682
+ failed: results.length - passed,
3683
+ results,
3684
+ durationMs: Date.now() - start
3685
+ };
3686
+ }
3687
+ function checkSchemaCompatibility(consumerSchema, providerSchema, path2 = "") {
3688
+ const violations = [];
3689
+ if (!consumerSchema || !providerSchema) return violations;
3690
+ const cType = consumerSchema["type"];
3691
+ const pType = providerSchema["type"];
3692
+ if (cType && pType && cType !== pType) {
3693
+ const ok = cType === "integer" && pType === "number" || cType === "number" && pType === "integer";
3694
+ if (!ok) {
3695
+ violations.push({
3696
+ type: "schema_incompatible",
3697
+ message: `Type mismatch${path2 ? ` at "${path2}"` : ""}: consumer expects "${cType}", provider offers "${pType}"`,
3698
+ path: path2 || "/",
3699
+ expected: cType,
3700
+ actual: pType
3701
+ });
3702
+ return violations;
3703
+ }
3704
+ }
3705
+ if (cType === "array" || Array.isArray(consumerSchema["items"])) {
3706
+ const cItems = consumerSchema["items"];
3707
+ const pItems = providerSchema["items"];
3708
+ if (cItems && pItems) {
3709
+ violations.push(...checkSchemaCompatibility(cItems, pItems, path2 ? `${path2}[]` : "[]"));
3710
+ }
3711
+ return violations;
3712
+ }
3713
+ if (cType === "object" || consumerSchema["properties"]) {
3714
+ const cProps = consumerSchema["properties"] ?? {};
3715
+ const pProps = providerSchema["properties"] ?? {};
3716
+ const cRequired = consumerSchema["required"] ?? [];
3717
+ for (const field of cRequired) {
3718
+ const fieldPath = path2 ? `${path2}.${field}` : field;
3719
+ if (!(field in pProps)) {
3720
+ violations.push({
3721
+ type: "schema_incompatible",
3722
+ message: `Consumer requires field "${fieldPath}" which is not defined in provider schema`,
3723
+ path: fieldPath,
3724
+ expected: "(defined)",
3725
+ actual: "(absent)"
3726
+ });
3727
+ }
3728
+ }
3729
+ for (const [field, cPropSchema] of Object.entries(cProps)) {
3730
+ if (field in pProps) {
3731
+ const fieldPath = path2 ? `${path2}.${field}` : field;
3732
+ violations.push(...checkSchemaCompatibility(
3733
+ cPropSchema,
3734
+ pProps[field],
3735
+ fieldPath
3736
+ ));
3737
+ }
3738
+ }
3739
+ }
3740
+ return violations;
3741
+ }
3742
+ function getProviderResponseSchema(spec, req, envVars, statusCode, requestBaseUrl) {
3743
+ const url = req.url.replace(/\{\{([^}]+)\}\}/g, (_, k) => envVars[k] ?? `{{${k}}}`);
3744
+ const match = findOperation(spec, req.method, url, requestBaseUrl);
3745
+ if (!match) return null;
3746
+ const responses = match.operation["responses"] ?? {};
3747
+ const candidates = [String(statusCode), `${String(statusCode)[0]}xx`, "2XX", "2xx", "default"];
3748
+ for (const candidate of candidates) {
3749
+ const resp = responses[candidate];
3750
+ if (resp) {
3751
+ const resolved = resolveSchema(spec, resp);
3752
+ const content = resolved["content"] ?? {};
3753
+ const json = content["application/json"];
3754
+ if (json?.["schema"]) {
3755
+ return resolveSchema(spec, json["schema"]);
3756
+ }
3757
+ }
3758
+ }
3759
+ return null;
3760
+ }
3761
+ async function executeRequest(req, vars) {
3762
+ const url = scriptRunner.buildUrl(req.url, req.params, vars);
3763
+ const start = Date.now();
3764
+ try {
3765
+ const headers = new undici.Headers();
3766
+ for (const h of req.headers) {
3767
+ if (h.enabled && h.key) headers.set(scriptRunner.interpolate(h.key, vars), scriptRunner.interpolate(h.value, vars));
3768
+ }
3769
+ const authHeaders = await buildAuthHeaders(req.auth, vars);
3770
+ for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
3771
+ let body;
3772
+ if (req.body.mode === "json" && req.body.json) {
3773
+ body = scriptRunner.interpolate(req.body.json, vars);
3774
+ if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
3775
+ }
3776
+ const resp = await undici.fetch(url, {
3777
+ method: req.method,
3778
+ headers,
3779
+ body: !["GET", "HEAD"].includes(req.method) ? body : void 0
3780
+ });
3781
+ const bodyText = await resp.text();
3782
+ const rawHdrs = {};
3783
+ resp.headers.forEach((v, k) => {
3784
+ rawHdrs[k] = v;
3785
+ });
3786
+ return { status: resp.status, headers: rawHdrs, body: bodyText, durationMs: Date.now() - start };
3787
+ } catch (err) {
3788
+ return err instanceof Error ? err : new Error(String(err));
3789
+ }
3790
+ }
3791
+ async function runBidirectional(requests, envVars, collectionVars = {}, specUrl, specPath, requestBaseUrl) {
3792
+ const spec = await loadSpec(specUrl, specPath);
3793
+ const vars = { ...envVars, ...collectionVars };
3794
+ const start = Date.now();
3795
+ const contractRequests = requests.filter(
3796
+ (r) => r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
3797
+ );
3798
+ const results = await Promise.all(contractRequests.map(async (req) => {
3799
+ const url = scriptRunner.buildUrl(req.url, req.params, vars);
3800
+ const violations = [];
3801
+ const expectedStatus = req.contract.statusCode ?? 200;
3802
+ const consumerSchema = req.contract.bodySchema ? (() => {
3803
+ try {
3804
+ return JSON.parse(req.contract.bodySchema);
3805
+ } catch {
3806
+ return null;
3807
+ }
3808
+ })() : null;
3809
+ if (consumerSchema) {
3810
+ const providerSchema = getProviderResponseSchema(spec, req, vars, expectedStatus, requestBaseUrl);
3811
+ if (!providerSchema) {
3812
+ violations.push({
3813
+ type: "schema_incompatible",
3814
+ message: `No response schema found in spec for ${req.method} ${url} → ${expectedStatus}`
3815
+ });
3816
+ } else {
3817
+ violations.push(...checkSchemaCompatibility(consumerSchema, providerSchema));
3818
+ }
3819
+ }
3820
+ const result = await executeRequest(req, vars);
3821
+ if (result instanceof Error) {
3822
+ violations.push({ type: "status_mismatch", message: `Request failed: ${result.message}` });
3823
+ return { requestId: req.id, requestName: req.name, method: req.method, url, passed: false, violations };
3824
+ }
3825
+ const liveViolations = validateConsumerResponse(
3826
+ req.contract,
3827
+ result.status,
3828
+ result.headers,
3829
+ result.body
3830
+ );
3831
+ violations.push(...liveViolations);
3832
+ return {
3833
+ requestId: req.id,
3834
+ requestName: req.name,
3835
+ method: req.method,
3836
+ url,
3837
+ passed: violations.length === 0,
3838
+ violations,
3839
+ durationMs: result.durationMs,
3840
+ actualStatus: result.status
3841
+ };
3842
+ }));
3843
+ const passed = results.filter((r) => r.passed).length;
3844
+ return {
3845
+ mode: "bidirectional",
3846
+ total: results.length,
3847
+ passed,
3848
+ failed: results.length - passed,
3849
+ results,
3850
+ durationMs: Date.now() - start
3851
+ };
3852
+ }
3853
+ function inferSchema(data) {
3854
+ if (data === null || data === void 0) return { type: "null" };
3855
+ if (typeof data === "boolean") return { type: "boolean" };
3856
+ if (typeof data === "number") {
3857
+ return Number.isInteger(data) ? { type: "integer" } : { type: "number" };
3858
+ }
3859
+ if (typeof data === "string") return { type: "string" };
3860
+ if (Array.isArray(data)) {
3861
+ if (data.length === 0) return { type: "array", items: {} };
3862
+ return { type: "array", items: inferSchema(data[0]) };
3863
+ }
3864
+ if (typeof data === "object") {
3865
+ const obj = data;
3866
+ const properties = {};
3867
+ const required = [];
3868
+ for (const [key, value] of Object.entries(obj)) {
3869
+ properties[key] = inferSchema(value);
3870
+ if (value !== null && value !== void 0) required.push(key);
3871
+ }
3872
+ const schema = { type: "object", properties };
3873
+ if (required.length > 0) schema["required"] = required;
3874
+ return schema;
3875
+ }
3876
+ return {};
3877
+ }
3878
+ function inferSchemaFromJson(json) {
3879
+ try {
3880
+ return inferSchema(JSON.parse(json));
3881
+ } catch {
3882
+ return null;
3883
+ }
3884
+ }
3885
+ function registerContractHandlers(ipc) {
3886
+ ipc.handle("contract:run", async (_e, payload) => {
3887
+ const { mode, requests, envVars, collectionVars = {}, specUrl, specPath, requestBaseUrl } = payload;
3888
+ switch (mode) {
3889
+ case "consumer":
3890
+ return runConsumerContracts(requests, envVars, collectionVars);
3891
+ case "provider":
3892
+ return runProviderVerification(requests, envVars, specUrl, specPath, requestBaseUrl);
3893
+ case "bidirectional":
3894
+ return runBidirectional(requests, envVars, collectionVars, specUrl, specPath, requestBaseUrl);
3895
+ }
3896
+ });
3897
+ ipc.handle("contract:inferSchema", (_e, jsonBody) => {
3898
+ const schema = inferSchemaFromJson(jsonBody);
3899
+ return schema ? JSON.stringify(schema, null, 2) : null;
3900
+ });
3901
+ }
3902
+ function createSplashWindow() {
3903
+ const splash = new electron.BrowserWindow({
3904
+ width: 420,
3905
+ height: 280,
3906
+ frame: false,
3907
+ resizable: false,
3908
+ movable: true,
3909
+ center: true,
3910
+ skipTaskbar: true,
3911
+ alwaysOnTop: true,
3912
+ backgroundColor: "#1e1b2e",
3913
+ webPreferences: { contextIsolation: true }
3914
+ });
3915
+ const splashPath = electron.app.isPackaged ? path.join(process.resourcesPath, "splash.html") : path.join(electron.app.getAppPath(), "resources/splash.html");
3916
+ splash.loadFile(splashPath);
3917
+ return splash;
3918
+ }
3919
+ function loadAppIcon() {
3920
+ const pngCandidates = [
3921
+ path.join(electron.app.getAppPath(), "build/icon.png"),
3922
+ path.join(electron.app.getAppPath(), "resources/icon.png"),
3923
+ path.join(__dirname, "../../build/icon.png")
3924
+ ];
3925
+ for (const p of pngCandidates) {
3926
+ if (fs.existsSync(p)) return electron.nativeImage.createFromPath(p);
3927
+ }
3928
+ const svgCandidates = [
3929
+ path.join(electron.app.getAppPath(), "resources/icon.svg"),
3930
+ path.join(__dirname, "../../resources/icon.svg")
3931
+ ];
3932
+ for (const p of svgCandidates) {
3933
+ if (fs.existsSync(p)) {
3934
+ const dataUrl = "data:image/svg+xml;base64," + fs.readFileSync(p).toString("base64");
3935
+ return electron.nativeImage.createFromDataURL(dataUrl);
3936
+ }
3937
+ }
3938
+ return void 0;
3939
+ }
3940
+ function createWindow() {
3941
+ const splash = createSplashWindow();
3942
+ const appIcon = loadAppIcon();
3943
+ if (appIcon && process.platform === "darwin" && electron.app.dock) {
3944
+ electron.app.dock.setIcon(appIcon);
3945
+ }
3946
+ const win = new electron.BrowserWindow({
3947
+ width: 1440,
3948
+ height: 900,
3949
+ minWidth: 900,
3950
+ minHeight: 600,
3951
+ titleBarStyle: "hiddenInset",
3952
+ backgroundColor: "#1e1b2e",
3953
+ icon: appIcon,
3954
+ show: false,
3955
+ webPreferences: {
3956
+ preload: path.join(__dirname, "../preload/index.js"),
3957
+ contextIsolation: true,
3958
+ nodeIntegration: false,
3959
+ sandbox: false
3960
+ }
3961
+ });
3962
+ if (process.env["ELECTRON_RENDERER_URL"]) {
3963
+ win.loadURL(process.env["ELECTRON_RENDERER_URL"]);
3964
+ } else {
3965
+ win.loadFile(path.join(__dirname, "../renderer/index.html"));
3966
+ }
3967
+ win.webContents.once("did-finish-load", () => {
3968
+ setTimeout(() => {
3969
+ splash.close();
3970
+ win.show();
3971
+ }, 1200);
3972
+ });
3973
+ win.webContents.on("before-input-event", (_e, input) => {
3974
+ if (input.type !== "keyDown") return;
3975
+ const devToolsShortcut = input.key === "F12" || input.key === "I" && input.shift && (input.meta || input.control);
3976
+ if (devToolsShortcut) win.webContents.toggleDevTools();
3977
+ });
3978
+ }
3979
+ electron.app.whenReady().then(() => {
3980
+ registerFileHandlers(electron.ipcMain);
3981
+ registerRequestHandler(electron.ipcMain);
3982
+ scriptRunner.registerSecretHandlers(electron.ipcMain);
3983
+ registerImportHandlers(electron.ipcMain);
3984
+ registerGenerateHandlers(electron.ipcMain);
3985
+ registerRunnerHandler(electron.ipcMain);
3986
+ registerMockHandlers(electron.ipcMain);
3987
+ registerOAuth2Handlers(electron.ipcMain);
3988
+ registerWsHandlers(electron.ipcMain);
3989
+ registerSoapHandlers(electron.ipcMain);
3990
+ registerDocsHandlers(electron.ipcMain);
3991
+ registerContractHandlers(electron.ipcMain);
3992
+ createWindow();
3993
+ electron.app.on("activate", () => {
3994
+ if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
3995
+ });
3996
+ });
3997
+ electron.app.on("window-all-closed", () => {
3998
+ if (process.platform !== "darwin") electron.app.quit();
3999
+ });
4000
+ electron.app.on("before-quit", async () => {
4001
+ closeAllWsConnections();
4002
+ await mockServer.stopAll();
4003
+ });