@testsmith/api-spector 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -6,15 +6,34 @@ const { spawn } = require('child_process')
6
6
 
7
7
  const [, , cmd = 'ui', ...rest] = process.argv
8
8
 
9
+ // ─── Command dispatch ────────────────────────────────────────────────────────
10
+ //
11
+ // Each entry maps a top-level command to the bundled JS file that handles it.
12
+ // `entrypoint: null` is reserved for `ui`, which spawns electron itself rather
13
+ // than a node script. Adding a new CLI surface is one line here + one entry in
14
+ // electron.vite.config.ts.
15
+
16
+ const COMMANDS = {
17
+ ui: { entrypoint: null, runner: 'electron' },
18
+ run: { entrypoint: 'runner.js', runner: 'node' },
19
+ mock: { entrypoint: 'mock.js', runner: 'node' },
20
+ record: { entrypoint: 'record.js', runner: 'node' },
21
+ agents: { entrypoint: 'agents.js', runner: 'node' },
22
+ contract: { entrypoint: 'contract.js',runner: 'node' },
23
+ wsdl: { entrypoint: 'wsdl.js', runner: 'node' },
24
+ }
25
+
9
26
  function printHelp() {
10
27
  console.log('')
11
28
  console.log(' API Spector — local-first API testing tool')
12
29
  console.log('')
13
30
  console.log(' Usage:')
14
31
  console.log(' api-spector ui Launch the app')
15
- console.log(' api-spector run --workspace <path> Run tests from CLI')
16
- console.log(' api-spector mock --workspace <path> Start mock servers from CLI')
17
- console.log(' api-spector record --upstream <url> Record API traffic as mock stubs')
32
+ console.log(' api-spector run --workspace <path> Run tests from CLI')
33
+ console.log(' api-spector mock --workspace <path> Start mock servers from CLI')
34
+ console.log(' api-spector record --upstream <url> Record API traffic as mock stubs')
35
+ console.log(' api-spector contract list|run Manage & run pinned contract snapshots')
36
+ console.log(' api-spector wsdl describe|import-* Inspect a WSDL or import as collection/mock')
18
37
  console.log('')
19
38
  console.log(' Options:')
20
39
  console.log(' api-spector agents init <name> Initialize AI agent files')
@@ -33,44 +52,34 @@ function printHelp() {
33
52
  if (cmd === '--help' || cmd === '-h') {
34
53
  printHelp()
35
54
  process.exit(0)
36
- } else if (cmd === 'ui') {
55
+ }
56
+
57
+ const command = COMMANDS[cmd]
58
+ if (!command) {
59
+ console.error(`API Spector — unknown command: "${cmd}"`)
60
+ printHelp()
61
+ process.exit(1)
62
+ }
63
+
64
+ // ui: spawn electron with the app dir
65
+ if (command.runner === 'electron') {
37
66
  const electron = require('electron')
38
67
  const appDir = path.join(__dirname, '..')
68
+ // Forward the user's cwd so the main process can decide whether to open a
69
+ // workspace in this folder, or fall through to the welcome screen. Without
70
+ // this, the app would always auto-load the previously-opened workspace
71
+ // even when launched from an empty/different directory.
39
72
  const proc = spawn(String(electron), [appDir, ...rest], {
40
73
  stdio: 'inherit',
41
- env: process.env,
42
- })
43
- proc.on('close', code => process.exit(code ?? 0))
44
- } else if (cmd === 'run') {
45
- const runnerPath = path.join(__dirname, '..', 'out', 'main', 'runner.js')
46
- const proc = spawn(process.execPath, [runnerPath, ...rest], {
47
- stdio: 'inherit',
48
- env: process.env,
49
- })
50
- proc.on('close', code => process.exit(code ?? 0))
51
- } else if (cmd === 'mock') {
52
- const mockPath = path.join(__dirname, '..', 'out', 'main', 'mock.js')
53
- const proc = spawn(process.execPath, [mockPath, ...rest], {
54
- stdio: 'inherit',
55
- env: process.env,
56
- })
57
- proc.on('close', code => process.exit(code ?? 0))
58
- } else if (cmd === 'record') {
59
- const recordPath = path.join(__dirname, '..', 'out', 'main', 'record.js')
60
- const proc = spawn(process.execPath, [recordPath, ...rest], {
61
- stdio: 'inherit',
62
- env: process.env,
74
+ env: { ...process.env, API_SPECTOR_LAUNCH_CWD: process.cwd() },
63
75
  })
64
76
  proc.on('close', code => process.exit(code ?? 0))
65
- } else if (cmd === 'agents') {
66
- const agentsPath = path.join(__dirname, '..', 'out', 'main', 'agents.js')
67
- const proc = spawn(process.execPath, [agentsPath, ...rest], {
77
+ } else {
78
+ // node-runnable bundles in out/main/<entrypoint>
79
+ const target = path.join(__dirname, '..', 'out', 'main', command.entrypoint)
80
+ const proc = spawn(process.execPath, [target, ...rest], {
68
81
  stdio: 'inherit',
69
82
  env: process.env,
70
83
  })
71
84
  proc.on('close', code => process.exit(code ?? 0))
72
- } else {
73
- console.error(`API Spector — unknown command: "${cmd}"`)
74
- printHelp()
75
- process.exit(1)
76
85
  }
@@ -0,0 +1,373 @@
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 crypto = require("crypto");
25
+ const promises = require("fs/promises");
26
+ const path = require("path");
27
+ const dayjs = require("dayjs");
28
+ const vm = require("vm");
29
+ function _interopNamespaceDefault(e) {
30
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
31
+ if (e) {
32
+ for (const k in e) {
33
+ if (k !== "default") {
34
+ const d = Object.getOwnPropertyDescriptor(e, k);
35
+ Object.defineProperty(n, k, d.get ? d : {
36
+ enumerable: true,
37
+ get: () => e[k]
38
+ });
39
+ }
40
+ }
41
+ }
42
+ n.default = e;
43
+ return Object.freeze(n);
44
+ }
45
+ const vm__namespace = /* @__PURE__ */ _interopNamespaceDefault(vm);
46
+ const MASTER_KEY_ENV = "API_SPECTOR_MASTER_KEY";
47
+ let secretStore = {};
48
+ let secretStorePath = null;
49
+ async function initSecretStore(userDataPath) {
50
+ secretStorePath = path.join(userDataPath, "secrets.json");
51
+ try {
52
+ const raw = await promises.readFile(secretStorePath, "utf8");
53
+ secretStore = JSON.parse(raw);
54
+ } catch {
55
+ secretStore = {};
56
+ }
57
+ }
58
+ async function persistSecretStore() {
59
+ if (!secretStorePath) return;
60
+ await promises.writeFile(secretStorePath, JSON.stringify(secretStore, null, 2), "utf8");
61
+ }
62
+ function getSafeStorage() {
63
+ try {
64
+ const { safeStorage } = require("electron");
65
+ if (typeof safeStorage?.isEncryptionAvailable === "function") return safeStorage;
66
+ return null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ function registerSecretHandlers(ipc) {
72
+ ipc.handle("secret:checkMasterKey", () => {
73
+ return { set: Boolean(process.env[MASTER_KEY_ENV]) };
74
+ });
75
+ ipc.handle("secret:setMasterKey", (_e, value) => {
76
+ process.env[MASTER_KEY_ENV] = value;
77
+ });
78
+ ipc.handle("secret:set", async (_e, ref, value) => {
79
+ const ss = getSafeStorage();
80
+ if (!ss || !ss.isEncryptionAvailable()) {
81
+ throw new Error("OS encryption is not available — set the secret via environment variable instead");
82
+ }
83
+ secretStore[ref] = ss.encryptString(value).toString("base64");
84
+ await persistSecretStore();
85
+ });
86
+ }
87
+ function decryptSecret(encrypted, salt, iv, password) {
88
+ const saltBuf = Buffer.from(salt, "base64");
89
+ const ivBuf = Buffer.from(iv, "base64");
90
+ const encBuf = Buffer.from(encrypted, "base64");
91
+ const key = crypto.pbkdf2Sync(password, saltBuf, 1e5, 32, "sha256");
92
+ const authTag = encBuf.subarray(encBuf.length - 16);
93
+ const ciphertext = encBuf.subarray(0, encBuf.length - 16);
94
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, ivBuf);
95
+ decipher.setAuthTag(authTag);
96
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
97
+ }
98
+ async function getSecret(ref) {
99
+ const stored = secretStore[ref];
100
+ if (stored) {
101
+ const ss = getSafeStorage();
102
+ if (ss && ss.isEncryptionAvailable()) {
103
+ try {
104
+ return ss.decryptString(Buffer.from(stored, "base64"));
105
+ } catch {
106
+ }
107
+ }
108
+ }
109
+ return process.env[ref] ?? null;
110
+ }
111
+ let _fakerCache = null;
112
+ async function getFaker() {
113
+ if (!_fakerCache) _fakerCache = await import("@faker-js/faker");
114
+ return _fakerCache.faker;
115
+ }
116
+ let _exprContext = null;
117
+ async function buildDynamicVars() {
118
+ const faker = await getFaker();
119
+ const now = dayjs();
120
+ _exprContext = { faker, dayjs };
121
+ return {
122
+ $uuid: faker.string.uuid(),
123
+ $timestamp: String(Date.now()),
124
+ $isoTimestamp: now.toISOString(),
125
+ $randomInt: String(faker.number.int({ min: 0, max: 1e3 })),
126
+ $randomFloat: String(faker.number.float({ min: 0, max: 1e3, fractionDigits: 2 })),
127
+ $randomBoolean: String(faker.datatype.boolean()),
128
+ $randomEmail: faker.internet.email(),
129
+ $randomUsername: faker.internet.username(),
130
+ $randomPassword: faker.internet.password(),
131
+ $randomFullName: faker.person.fullName(),
132
+ $randomFirstName: faker.person.firstName(),
133
+ $randomLastName: faker.person.lastName(),
134
+ $randomWord: faker.lorem.word(),
135
+ $randomPhrase: faker.lorem.sentence(),
136
+ $randomUrl: faker.internet.url(),
137
+ $randomIp: faker.internet.ip(),
138
+ $randomHexColor: faker.color.rgb({ format: "hex", casing: "lower" })
139
+ };
140
+ }
141
+ function interpolate(str, vars) {
142
+ return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
143
+ const trimmed = key.trim();
144
+ if (trimmed in vars) return vars[trimmed];
145
+ if (_exprContext && (trimmed.includes(".") || trimmed.includes("("))) {
146
+ try {
147
+ const result = vm__namespace.runInNewContext(trimmed, _exprContext);
148
+ if (result !== void 0 && result !== null) return String(result);
149
+ } catch {
150
+ }
151
+ }
152
+ return match;
153
+ });
154
+ }
155
+ function buildUrl(baseUrl, params, vars) {
156
+ const templateTokens = /* @__PURE__ */ new Set();
157
+ baseUrl.replace(/\{\{([^}]+)\}\}/g, (_m, name) => {
158
+ templateTokens.add(String(name).trim());
159
+ return "";
160
+ });
161
+ const enabled = (params ?? []).filter((p) => p.enabled && p.key);
162
+ const pathRows = [];
163
+ const queryRows = [];
164
+ for (const p of enabled) {
165
+ const isPath = p.paramType === "path" || templateTokens.has(p.key);
166
+ if (isPath) pathRows.push(p);
167
+ else queryRows.push(p);
168
+ }
169
+ const mergedVars = pathRows.length ? {
170
+ ...vars,
171
+ ...Object.fromEntries(pathRows.map((p) => [p.key, interpolate(p.value, vars)]))
172
+ } : vars;
173
+ const url = interpolate(baseUrl, mergedVars);
174
+ if (!queryRows.length) return url;
175
+ const sep = url.includes("?") ? "&" : "?";
176
+ const qs = queryRows.map((p) => `${encodeURIComponent(interpolate(p.key, vars))}=${encodeURIComponent(interpolate(p.value, vars))}`).join("&");
177
+ return url + sep + qs;
178
+ }
179
+ async function buildEnvVars(environment) {
180
+ const vars = {};
181
+ if (!environment) return vars;
182
+ const masterKey = process.env["API_SPECTOR_MASTER_KEY"];
183
+ for (const v of environment.variables) {
184
+ if (!v.enabled) continue;
185
+ if (v.envRef) {
186
+ const envValue = process.env[v.envRef];
187
+ if (envValue !== void 0) vars[v.key] = envValue;
188
+ } else if (v.secret && v.secretEncrypted && v.secretSalt && v.secretIv) {
189
+ if (masterKey) {
190
+ try {
191
+ vars[v.key] = decryptSecret(v.secretEncrypted, v.secretSalt, v.secretIv, masterKey);
192
+ } catch {
193
+ }
194
+ }
195
+ if (vars[v.key] === void 0 && process.env[v.key] !== void 0) {
196
+ vars[v.key] = process.env[v.key];
197
+ }
198
+ } else if (v.secret) {
199
+ if (process.env[v.key] !== void 0) {
200
+ vars[v.key] = process.env[v.key];
201
+ }
202
+ } else {
203
+ vars[v.key] = v.value;
204
+ }
205
+ }
206
+ return vars;
207
+ }
208
+ function mergeVars(envVars, collectionVars, globals, localVars = {}, dynamicVars = {}) {
209
+ return { ...dynamicVars, ...globals, ...collectionVars, ...envVars, ...localVars };
210
+ }
211
+ async function buildAuthHeaders(auth, vars) {
212
+ const headers = {};
213
+ if (auth.type === "bearer") {
214
+ let token = auth.token ?? "";
215
+ if (!token && auth.tokenSecretRef) token = await getSecret(auth.tokenSecretRef) ?? "";
216
+ token = interpolate(token, vars);
217
+ if (token) headers["Authorization"] = `Bearer ${token}`;
218
+ }
219
+ if (auth.type === "basic") {
220
+ let password = auth.password ?? "";
221
+ if (!password && auth.passwordSecretRef) password = await getSecret(auth.passwordSecretRef) ?? "";
222
+ password = interpolate(password, vars);
223
+ const username = interpolate(auth.username ?? "", vars);
224
+ headers["Authorization"] = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
225
+ }
226
+ if (auth.type === "apikey" && auth.apiKeyIn === "header") {
227
+ let value = auth.apiKeyValue ?? "";
228
+ if (!value && auth.apiKeySecretRef) value = await getSecret(auth.apiKeySecretRef) ?? "";
229
+ value = interpolate(value, vars);
230
+ headers[auth.apiKeyName ?? "X-API-Key"] = value;
231
+ }
232
+ if (auth.type === "oauth2") {
233
+ const now = Date.now();
234
+ if (auth.oauth2CachedToken && auth.oauth2TokenExpiry && auth.oauth2TokenExpiry > now + 5e3) {
235
+ headers["Authorization"] = `Bearer ${auth.oauth2CachedToken}`;
236
+ }
237
+ }
238
+ return headers;
239
+ }
240
+ async function buildApiKeyParam(auth, vars) {
241
+ if (auth.type !== "apikey" || auth.apiKeyIn !== "query") return null;
242
+ let value = auth.apiKeyValue ?? "";
243
+ if (!value && auth.apiKeySecretRef) value = await getSecret(auth.apiKeySecretRef) ?? "";
244
+ value = interpolate(value, vars);
245
+ return { key: auth.apiKeyName ?? "apikey", value };
246
+ }
247
+ function parseDigestChallenge(wwwAuth) {
248
+ const extract = (key) => {
249
+ const m = new RegExp(`${key}="([^"]*)"`, "i").exec(wwwAuth);
250
+ return m ? m[1] : "";
251
+ };
252
+ const extractUnquoted = (key) => {
253
+ const m = new RegExp(`${key}=([^,\\s]+)`, "i").exec(wwwAuth);
254
+ return m ? m[1] : "";
255
+ };
256
+ return {
257
+ realm: extract("realm"),
258
+ nonce: extract("nonce"),
259
+ qop: extract("qop") || extractUnquoted("qop") || void 0,
260
+ algorithm: extract("algorithm") || extractUnquoted("algorithm") || "MD5",
261
+ opaque: extract("opaque") || void 0
262
+ };
263
+ }
264
+ function md5(s) {
265
+ return crypto.createHash("md5").update(s).digest("hex");
266
+ }
267
+ function buildDigestAuthHeader(challenge, username, password, method, uri) {
268
+ const { realm, nonce, qop, algorithm, opaque } = challenge;
269
+ const algo = (algorithm ?? "MD5").toUpperCase();
270
+ const ha1 = algo === "MD5-SESS" ? md5(`${md5(`${username}:${realm}:${password}`)}:${nonce}:`) : md5(`${username}:${realm}:${password}`);
271
+ const ha2 = md5(`${method}:${uri}`);
272
+ let response;
273
+ let nc;
274
+ let cnonce;
275
+ if (qop === "auth" || qop === "auth-int") {
276
+ nc = "00000001";
277
+ cnonce = crypto.randomBytes(8).toString("hex");
278
+ response = md5(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`);
279
+ } else {
280
+ response = md5(`${ha1}:${nonce}:${ha2}`);
281
+ }
282
+ let header = `Digest username="${username}", realm="${realm}", nonce="${nonce}", uri="${uri}", response="${response}"`;
283
+ if (qop) header += `, qop=${qop}`;
284
+ if (nc) header += `, nc=${nc}`;
285
+ if (cnonce) header += `, cnonce="${cnonce}"`;
286
+ if (opaque) header += `, opaque="${opaque}"`;
287
+ if (algo !== "MD5") header += `, algorithm=${algo}`;
288
+ return header;
289
+ }
290
+ async function performDigestAuth(url, method, auth, vars, fetchFn) {
291
+ const probeResp = await fetchFn(url, { method, headers: {} });
292
+ if (probeResp.status !== 401) return null;
293
+ const wwwAuth = probeResp.headers.get("www-authenticate") ?? "";
294
+ if (!wwwAuth.toLowerCase().startsWith("digest")) return null;
295
+ const challenge = parseDigestChallenge(wwwAuth);
296
+ let password = auth.password ?? "";
297
+ if (!password && auth.passwordSecretRef) password = await getSecret(auth.passwordSecretRef) ?? "";
298
+ password = interpolate(password, vars);
299
+ const username = interpolate(auth.username ?? "", vars);
300
+ let uri = "/";
301
+ try {
302
+ uri = new URL(url).pathname + (new URL(url).search ?? "");
303
+ } catch {
304
+ }
305
+ return buildDigestAuthHeader(challenge, username, password, method, uri);
306
+ }
307
+ async function performNtlmRequest(_url, _method, _auth, _vars) {
308
+ throw new Error(
309
+ 'NTLM auth is not yet implemented. Add "httpntlm" to package.json dependencies and implement performNtlmRequest in auth-builder.ts.'
310
+ );
311
+ }
312
+ async function fetchOAuth2Token(auth, vars) {
313
+ const flow = auth.oauth2Flow ?? "client_credentials";
314
+ if (flow === "authorization_code") {
315
+ throw new Error("authorization_code flow requires the oauth2:startFlow IPC call from the renderer.");
316
+ }
317
+ if (flow === "implicit") {
318
+ throw new Error("implicit flow cannot be performed server-side — tokens must be obtained via the browser redirect.");
319
+ }
320
+ const tokenUrl = interpolate(auth.oauth2TokenUrl ?? "", vars);
321
+ if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required.");
322
+ const clientId = interpolate(auth.oauth2ClientId ?? "", vars);
323
+ let clientSecret = auth.oauth2ClientSecret ?? "";
324
+ if (!clientSecret && auth.oauth2ClientSecretRef) {
325
+ clientSecret = await getSecret(auth.oauth2ClientSecretRef) ?? "";
326
+ }
327
+ clientSecret = interpolate(clientSecret, vars);
328
+ const params = new URLSearchParams();
329
+ params.set("grant_type", flow === "password" ? "password" : "client_credentials");
330
+ params.set("client_id", clientId);
331
+ params.set("client_secret", clientSecret);
332
+ if (auth.oauth2Scopes) params.set("scope", auth.oauth2Scopes);
333
+ if (flow === "password") {
334
+ let password = auth.password ?? "";
335
+ if (!password && auth.passwordSecretRef) password = await getSecret(auth.passwordSecretRef) ?? "";
336
+ password = interpolate(password, vars);
337
+ params.set("username", interpolate(auth.username ?? "", vars));
338
+ params.set("password", password);
339
+ }
340
+ const { fetch: nodeFetch } = await import("undici");
341
+ const resp = await nodeFetch(tokenUrl, {
342
+ method: "POST",
343
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
344
+ body: params.toString()
345
+ });
346
+ if (!resp.ok) {
347
+ const body = await resp.text();
348
+ throw new Error(`OAuth 2.0 token request failed (${resp.status}): ${body}`);
349
+ }
350
+ const json = await resp.json();
351
+ const accessToken = String(json["access_token"] ?? "");
352
+ if (!accessToken) throw new Error("OAuth 2.0: token response missing access_token.");
353
+ const expiresIn = Number(json["expires_in"] ?? 3600);
354
+ const expiresAt = Date.now() + expiresIn * 1e3;
355
+ return {
356
+ accessToken,
357
+ expiresAt,
358
+ refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
359
+ };
360
+ }
361
+ exports.buildApiKeyParam = buildApiKeyParam;
362
+ exports.buildAuthHeaders = buildAuthHeaders;
363
+ exports.buildDynamicVars = buildDynamicVars;
364
+ exports.buildEnvVars = buildEnvVars;
365
+ exports.buildUrl = buildUrl;
366
+ exports.fetchOAuth2Token = fetchOAuth2Token;
367
+ exports.getSecret = getSecret;
368
+ exports.initSecretStore = initSecretStore;
369
+ exports.interpolate = interpolate;
370
+ exports.mergeVars = mergeVars;
371
+ exports.performDigestAuth = performDigestAuth;
372
+ exports.performNtlmRequest = performNtlmRequest;
373
+ exports.registerSecretHandlers = registerSecretHandlers;
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const uuid = require("uuid");
4
+ const soapHandler = require("./soap-handler-Cpj-JwyA.js");
5
+ require("https");
6
+ require("http");
7
+ require("@xmldom/xmldom");
8
+ const SOAP_11_CONTENT_TYPE = "text/xml; charset=utf-8";
9
+ const SOAP_12_CONTENT_TYPE = "application/soap+xml; charset=utf-8";
10
+ function contentTypeForSoap(version) {
11
+ return version === "1.2" ? SOAP_12_CONTENT_TYPE : SOAP_11_CONTENT_TYPE;
12
+ }
13
+ function withContentType(headers, value) {
14
+ const idx = headers.findIndex((h) => h.key.toLowerCase() === "content-type");
15
+ const next = { key: "Content-Type", value, enabled: true };
16
+ if (idx === -1) return [...headers, next];
17
+ return headers.map((h, i) => i === idx ? { ...h, value, enabled: true } : h);
18
+ }
19
+ function buildRequestsFromWsdl(parsed) {
20
+ const out = [];
21
+ const byName = /* @__PURE__ */ new Map();
22
+ for (const op of parsed.operations) {
23
+ const existing = byName.get(op.name);
24
+ if (!existing || existing.soapVersion === "1.2" && op.soapVersion === "1.1") byName.set(op.name, op);
25
+ }
26
+ for (const op of byName.values()) {
27
+ const ct = contentTypeForSoap(op.soapVersion);
28
+ const req = {
29
+ id: uuid.v4(),
30
+ name: op.name,
31
+ method: "POST",
32
+ url: op.endpoint ?? "",
33
+ headers: withContentType([], ct),
34
+ params: [],
35
+ auth: { type: "none" },
36
+ protocol: "soap",
37
+ body: {
38
+ mode: "soap",
39
+ soap: {
40
+ wsdlUrl: "",
41
+ operationName: op.name,
42
+ soapAction: op.soapAction ?? "",
43
+ envelope: op.inputTemplate
44
+ }
45
+ },
46
+ description: op.soapAction ? `SOAPAction: ${op.soapAction}` : void 0
47
+ };
48
+ out.push(req);
49
+ }
50
+ return out;
51
+ }
52
+ function buildCollectionFromWsdl(name, parsed) {
53
+ const requests = buildRequestsFromWsdl(parsed);
54
+ const requestMap = {};
55
+ for (const r of requests) requestMap[r.id] = r;
56
+ const collection = {
57
+ version: "1.0",
58
+ id: uuid.v4(),
59
+ name,
60
+ description: `Imported from WSDL — ${parsed.endpoints[0]?.address ?? parsed.targetNamespace}`,
61
+ rootFolder: {
62
+ id: uuid.v4(),
63
+ name: "root",
64
+ description: "",
65
+ folders: [],
66
+ requestIds: requests.map((r) => r.id)
67
+ },
68
+ requests: requestMap
69
+ };
70
+ return { collection, requestIds: requests.map((r) => r.id) };
71
+ }
72
+ function pathFromUrl(url) {
73
+ try {
74
+ return new URL(url).pathname || "/";
75
+ } catch {
76
+ return "/";
77
+ }
78
+ }
79
+ function pickFreePort(existing) {
80
+ let p = 3900;
81
+ while (existing.includes(p)) p++;
82
+ return p;
83
+ }
84
+ function buildMockFromWsdl(name, parsed, existingPorts = []) {
85
+ const opMap = {};
86
+ const seen = /* @__PURE__ */ new Set();
87
+ for (const op of parsed.operations) {
88
+ if (seen.has(op.name)) continue;
89
+ seen.add(op.name);
90
+ opMap[op.name] = soapHandler.buildResponseEnvelope(op.name, parsed.targetNamespace, op.soapVersion);
91
+ }
92
+ const pathsByVersion = /* @__PURE__ */ new Map();
93
+ for (const ep of parsed.endpoints) {
94
+ pathsByVersion.set(pathFromUrl(ep.address), ep.soapVersion);
95
+ }
96
+ if (pathsByVersion.size === 0) pathsByVersion.set("/", "1.1");
97
+ const routes = [];
98
+ for (const [routePath, version] of pathsByVersion.entries()) {
99
+ routes.push({
100
+ id: uuid.v4(),
101
+ method: "POST",
102
+ path: routePath,
103
+ statusCode: 200,
104
+ headers: { "Content-Type": contentTypeForSoap(version) },
105
+ body: "",
106
+ description: `SOAP ${version} — dispatched per operation`,
107
+ script: soapHandler.buildMockDispatchScript(),
108
+ // Externalized so workspace JSON stays compact: the dispatch script reads
109
+ // these via the script-runner's `metadata` context binding instead of
110
+ // baking each envelope as a JS string literal.
111
+ metadata: { soapEnvelopes: opMap, soapVersion: version }
112
+ });
113
+ }
114
+ return {
115
+ version: "1.0",
116
+ id: uuid.v4(),
117
+ name,
118
+ port: pickFreePort(existingPorts),
119
+ routes
120
+ };
121
+ }
122
+ function importWsdl(wsdlText, opts = {}) {
123
+ const parsed = soapHandler.parseWsdl(wsdlText);
124
+ const baseName = opts.name?.trim() || (() => {
125
+ try {
126
+ return new URL(parsed.endpoints[0]?.address ?? "").hostname || "WSDL service";
127
+ } catch {
128
+ return "WSDL service";
129
+ }
130
+ })();
131
+ const { collection } = buildCollectionFromWsdl(baseName, parsed);
132
+ const mock = buildMockFromWsdl(`${baseName} (mock)`, parsed, opts.existingMockPorts ?? []);
133
+ return { parsed, collection, mock };
134
+ }
135
+ function defaultCollectionRelPath(ws, collection) {
136
+ const safe = collection.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "wsdl";
137
+ let i = 0;
138
+ let candidate = `collections/${safe}.json`;
139
+ const existing = new Set(ws.collections);
140
+ while (existing.has(candidate)) {
141
+ i++;
142
+ candidate = `collections/${safe}-${i}.json`;
143
+ }
144
+ return candidate;
145
+ }
146
+ function defaultMockRelPath(mock) {
147
+ return `mocks/${mock.id}.mock.json`;
148
+ }
149
+ exports.buildCollectionFromWsdl = buildCollectionFromWsdl;
150
+ exports.buildMockFromWsdl = buildMockFromWsdl;
151
+ exports.buildRequestsFromWsdl = buildRequestsFromWsdl;
152
+ exports.defaultCollectionRelPath = defaultCollectionRelPath;
153
+ exports.defaultMockRelPath = defaultMockRelPath;
154
+ exports.importWsdl = importWsdl;