@testsmith/api-spector 0.3.8 → 0.4.0

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.
@@ -54,6 +54,7 @@ const IPC = {
54
54
  insomnia: "import:insomnia",
55
55
  bruno: "import:bruno",
56
56
  http: "import:http",
57
+ spector: "import:spector",
57
58
  openapiSchemas: "import:openapi-schemas",
58
59
  openapiSchemasUrl: "import:openapi-schemas-url"
59
60
  },
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const uuid = require("uuid");
4
- const soapHandler = require("./soap-handler-h0dWIjOK.js");
5
- require("./handle-BCnNIZZr.js");
4
+ const soapHandler = require("./soap-handler-B1Huudj7.js");
5
+ require("./handle-Coax8Nz1.js");
6
6
  require("https");
7
7
  require("http");
8
8
  require("@xmldom/xmldom");
@@ -85,6 +85,25 @@ function getAllApplicableHooks(folderId, collection) {
85
85
  }
86
86
  return { beforeAll, before, after, afterAll };
87
87
  }
88
+ function authIsConfigured(auth) {
89
+ if (!auth || auth.type === "none") return false;
90
+ const set = (s) => !!(s && s.trim());
91
+ switch (auth.type) {
92
+ case "bearer":
93
+ return set(auth.token) || set(auth.tokenSecretRef);
94
+ case "basic":
95
+ case "digest":
96
+ return set(auth.username) || set(auth.password) || set(auth.passwordSecretRef);
97
+ case "ntlm":
98
+ return set(auth.username) || set(auth.password) || set(auth.passwordSecretRef);
99
+ case "apikey":
100
+ return set(auth.apiKeyValue) || set(auth.apiKeySecretRef);
101
+ case "oauth2":
102
+ return set(auth.oauth2TokenUrl) || set(auth.oauth2ClientId) || set(auth.oauth2CachedToken);
103
+ default:
104
+ return true;
105
+ }
106
+ }
88
107
  function resolveInheritedAuthAndHeaders(requestId, collection) {
89
108
  let inheritedAuth = collection.auth && collection.auth.type !== "none" ? collection.auth : null;
90
109
  let inheritedHeaders = collection.headers?.filter((h) => h.enabled && h.key) ?? [];
@@ -113,6 +132,7 @@ function buildRunPlan(collection, folderId, filterTags) {
113
132
  );
114
133
  }
115
134
  }
135
+ exports.authIsConfigured = authIsConfigured;
116
136
  exports.buildRunPlan = buildRunPlan;
117
137
  exports.getAllApplicableHooks = getAllApplicableHooks;
118
138
  exports.resolveInheritedAuthAndHeaders = resolveInheritedAuthAndHeaders;
@@ -23,7 +23,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
  const undici = require("undici");
25
25
  const promises = require("fs/promises");
26
- const handle = require("./handle-BCnNIZZr.js");
26
+ const handle = require("./handle-Coax8Nz1.js");
27
27
  const crypto = require("crypto");
28
28
  const path = require("path");
29
29
  const dayjs = require("dayjs");
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  const undici = require("undici");
3
- const requestExec = require("./request-exec-AQD6cEgT.js");
3
+ const requestExec = require("./request-exec-C0DnGKdd.js");
4
4
  const promises = require("fs/promises");
5
5
  const path = require("path");
6
6
  const Ajv = require("ajv");
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const handle = require("./handle-BCnNIZZr.js");
2
+ const handle = require("./handle-Coax8Nz1.js");
3
3
  const https = require("https");
4
4
  const http = require("http");
5
5
  const xmldom = require("@xmldom/xmldom");
@@ -315,7 +315,7 @@ function registerSoapHandlers(ipc) {
315
315
  handle.handleIpc(ipc, handle.IPC.wsdl.import, async (_event, opts) => {
316
316
  const { validateWsdlImport } = await Promise.resolve().then(() => require("./ipc-validate-k6KI8adf.js"));
317
317
  validateWsdlImport(opts);
318
- const { importWsdl } = await Promise.resolve().then(() => require("./import-DUKaMYGO.js"));
318
+ const { importWsdl } = await Promise.resolve().then(() => require("./import-Bilo697Z.js"));
319
319
  const wsdlText = opts.xml ?? (opts.url ? await fetchUrl(opts.url) : "");
320
320
  if (!wsdlText) throw new Error("wsdl:import requires either `url` or `xml`");
321
321
  return importWsdl(wsdlText, { name: opts.name, existingMockPorts: opts.existingMockPorts });
@@ -24,14 +24,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
  const promises = require("fs/promises");
26
26
  const path = require("path");
27
- const snapshots = require("./chunks/snapshots-NGkGQNZ4.js");
27
+ const snapshots = require("./chunks/snapshots-CatB4zLh.js");
28
28
  const crypto = require("crypto");
29
29
  const undici = require("undici");
30
30
  const os = require("os");
31
31
  const cliCommon = require("./chunks/cli-common-CDQY1erJ.js");
32
32
  const environments = require("./chunks/environments-iM3SUM-4.js");
33
- require("./chunks/request-exec-AQD6cEgT.js");
34
- require("./chunks/handle-BCnNIZZr.js");
33
+ require("./chunks/request-exec-C0DnGKdd.js");
34
+ require("./chunks/handle-Coax8Nz1.js");
35
35
  require("dayjs");
36
36
  require("vm");
37
37
  require("tv4");
package/out/main/index.js CHANGED
@@ -22,24 +22,24 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
  const electron = require("electron");
25
- const handle = require("./chunks/handle-BCnNIZZr.js");
25
+ const handle = require("./chunks/handle-Coax8Nz1.js");
26
26
  const path = require("path");
27
27
  const fs = require("fs");
28
28
  const promises = require("fs/promises");
29
- const requestExec = require("./chunks/request-exec-AQD6cEgT.js");
29
+ const crypto = require("crypto");
30
+ const requestExec = require("./chunks/request-exec-C0DnGKdd.js");
30
31
  const ipcValidate = require("./chunks/ipc-validate-k6KI8adf.js");
31
32
  const uuid = require("uuid");
32
33
  const jsYaml = require("js-yaml");
33
34
  const undici = require("undici");
34
35
  const JSZip = require("jszip");
35
- const requestCollection = require("./chunks/request-collection-CBuPjBwa.js");
36
+ const requestCollection = require("./chunks/request-collection-CJoXpvOw.js");
36
37
  const mockServer = require("./chunks/mock-server-DSLR2ulH.js");
37
38
  const http = require("http");
38
39
  const WebSocket = require("ws");
39
- const soapHandler = require("./chunks/soap-handler-h0dWIjOK.js");
40
+ const soapHandler = require("./chunks/soap-handler-B1Huudj7.js");
40
41
  const os = require("os");
41
- const crypto = require("crypto");
42
- const snapshots = require("./chunks/snapshots-NGkGQNZ4.js");
42
+ const snapshots = require("./chunks/snapshots-CatB4zLh.js");
43
43
  const simpleGit = require("simple-git");
44
44
  const recorder = require("./chunks/recorder-0Ij921El.js");
45
45
  require("dayjs");
@@ -224,12 +224,27 @@ function registerFileHandlers(ipc) {
224
224
  });
225
225
  if (result.canceled || !result.filePaths[0]) return null;
226
226
  const wsPath = result.filePaths[0];
227
+ const raw = await promises.readFile(wsPath, "utf8");
228
+ const parsed = JSON.parse(raw);
229
+ if (looksLikeCollection(parsed)) {
230
+ const { response } = await electron.dialog.showMessageBox({
231
+ type: "question",
232
+ buttons: ["Create Workspace", "Cancel"],
233
+ defaultId: 0,
234
+ cancelId: 1,
235
+ message: "This is a collection, not a workspace.",
236
+ detail: `API Spector opens workspaces, which contain collections.
237
+
238
+ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The collection is moved into a collections/ folder and this file becomes the workspace.`
239
+ });
240
+ if (response !== 0) return null;
241
+ return wrapCollectionAsWorkspace(wsPath, parsed);
242
+ }
227
243
  workspaceDir = path.dirname(wsPath);
228
244
  workspaceFile = wsPath;
229
245
  await requestExec.loadGlobals(workspaceDir);
230
246
  await saveLastWorkspacePath(wsPath);
231
- const raw = await promises.readFile(wsPath, "utf8");
232
- return { workspace: JSON.parse(raw), workspacePath: wsPath };
247
+ return { workspace: parsed, workspacePath: wsPath };
233
248
  });
234
249
  handle.handleIpc(ipc, handle.IPC.file.newWorkspace, async () => {
235
250
  const startDir = dialogStartDir();
@@ -390,6 +405,30 @@ async function tryOpenWorkspaceInDir(dir) {
390
405
  function getWorkspaceDir() {
391
406
  return workspaceDir;
392
407
  }
408
+ function looksLikeCollection(obj) {
409
+ if (!obj || typeof obj !== "object") return false;
410
+ const o = obj;
411
+ return !Array.isArray(o["collections"]) && (!!o["rootFolder"] || !!o["requests"]);
412
+ }
413
+ async function wrapCollectionAsWorkspace(collFilePath, collection) {
414
+ const dir = path.dirname(collFilePath);
415
+ const base = path.basename(collFilePath).replace(/\.(spector|json)$/i, "");
416
+ const safe = base.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "collection";
417
+ const collRel = `collections/${safe}.json`;
418
+ if (!collection.id) collection.id = crypto.randomUUID();
419
+ await promises.mkdir(path.join(dir, "collections"), { recursive: true });
420
+ await atomicWrite(path.join(dir, collRel), JSON.stringify(collection, null, 2));
421
+ const ws = { version: "1.0", collections: [collRel], environments: [], activeEnvironmentId: null };
422
+ await ensureGitignore(dir);
423
+ await ensureVscodeFileAssociation(dir);
424
+ await ensureReadme(dir, path.basename(collFilePath));
425
+ await atomicWrite(collFilePath, JSON.stringify(ws, null, 2));
426
+ workspaceDir = dir;
427
+ workspaceFile = collFilePath;
428
+ await requestExec.loadGlobals(dir);
429
+ await saveLastWorkspacePath(collFilePath);
430
+ return { workspace: ws, workspacePath: collFilePath };
431
+ }
393
432
  function asObject(value) {
394
433
  return value && typeof value === "object" ? value : null;
395
434
  }
@@ -1593,6 +1632,30 @@ function registerImportHandlers(ipc) {
1593
1632
  if (result.canceled || !result.filePaths[0]) return null;
1594
1633
  return importHttpFile(result.filePaths[0]);
1595
1634
  });
1635
+ handle.handleIpc(ipc, handle.IPC.import.spector, async () => {
1636
+ const result = await electron.dialog.showOpenDialog({
1637
+ title: "Import API Spector Collection",
1638
+ filters: [{ name: "API Spector Collection", extensions: ["json", "spector"] }],
1639
+ properties: ["openFile"]
1640
+ });
1641
+ if (result.canceled || !result.filePaths[0]) return null;
1642
+ const raw = await promises.readFile(result.filePaths[0], "utf8");
1643
+ let parsed;
1644
+ try {
1645
+ parsed = JSON.parse(raw);
1646
+ } catch {
1647
+ throw new Error("Not a valid JSON file.");
1648
+ }
1649
+ if (Array.isArray(parsed["collections"])) {
1650
+ throw new Error("This is a workspace file, not a collection. Open it with File > Open Workspace instead.");
1651
+ }
1652
+ if (!parsed["rootFolder"] || !parsed["requests"]) {
1653
+ throw new Error("This does not look like an API Spector collection (no rootFolder / requests).");
1654
+ }
1655
+ const collection = parsed;
1656
+ collection.id = crypto.randomUUID();
1657
+ return collection;
1658
+ });
1596
1659
  handle.handleIpc(ipc, handle.IPC.import.openapiSchemas, async () => {
1597
1660
  const result = await electron.dialog.showOpenDialog({
1598
1661
  title: "Load OpenAPI spec for schema sync",
@@ -1719,7 +1782,7 @@ function javaClass(name) {
1719
1782
  return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
1720
1783
  }
1721
1784
  function resolveEffectiveAuth(req, inherited) {
1722
- return req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
1785
+ return requestCollection.authIsConfigured(req.auth) ? req.auth : inherited.auth ?? req.auth;
1723
1786
  }
1724
1787
  function mergeHeaders(req, inherited) {
1725
1788
  return [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
@@ -2,12 +2,12 @@
2
2
  "use strict";
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
- const requestExec = require("./chunks/request-exec-AQD6cEgT.js");
6
- const requestCollection = require("./chunks/request-collection-CBuPjBwa.js");
5
+ const requestExec = require("./chunks/request-exec-C0DnGKdd.js");
6
+ const requestCollection = require("./chunks/request-collection-CJoXpvOw.js");
7
7
  const environments = require("./chunks/environments-iM3SUM-4.js");
8
8
  const cliCommon = require("./chunks/cli-common-CDQY1erJ.js");
9
9
  require("undici");
10
- require("./chunks/handle-BCnNIZZr.js");
10
+ require("./chunks/handle-Coax8Nz1.js");
11
11
  require("crypto");
12
12
  require("dayjs");
13
13
  require("vm");
@@ -396,7 +396,7 @@ async function main() {
396
396
  } else if (!envName && workspace.settings?.defaultEnvironment && !env) {
397
397
  console.warn(cliCommon.color(`Warning: default environment "${workspace.settings.defaultEnvironment}" not found. Running without environment.`, cliCommon.C.yellow));
398
398
  }
399
- const version = `v${"0.3.8"}`;
399
+ const version = `v${"0.4.0"}`;
400
400
  console.log("");
401
401
  console.log(cliCommon.color(" API Test Runner" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
402
402
  console.log(cliCommon.color(` Workspace: ${wsPath}`, cliCommon.C.gray));
@@ -448,7 +448,7 @@ async function main() {
448
448
  for (const item of items) {
449
449
  const req = item.request;
450
450
  const inherited = requestCollection.resolveInheritedAuthAndHeaders(req.id, col);
451
- if (req.auth.type === "none" && inherited.auth && inherited.auth.type !== "none") {
451
+ if (!requestCollection.authIsConfigured(req.auth) && inherited.auth && inherited.auth.type !== "none") {
452
452
  req.auth = inherited.auth;
453
453
  }
454
454
  const inheritedHeaders = inherited.headers.filter((h) => h.enabled && h.key);
package/out/main/wsdl.js CHANGED
@@ -4,10 +4,10 @@ const promises = require("fs/promises");
4
4
  const path = require("path");
5
5
  const https = require("https");
6
6
  const http = require("http");
7
- const soapHandler = require("./chunks/soap-handler-h0dWIjOK.js");
8
- const _import = require("./chunks/import-DUKaMYGO.js");
7
+ const soapHandler = require("./chunks/soap-handler-B1Huudj7.js");
8
+ const _import = require("./chunks/import-Bilo697Z.js");
9
9
  const cliCommon = require("./chunks/cli-common-CDQY1erJ.js");
10
- require("./chunks/handle-BCnNIZZr.js");
10
+ require("./chunks/handle-Coax8Nz1.js");
11
11
  require("@xmldom/xmldom");
12
12
  require("uuid");
13
13
  function fetchUrl(url) {
@@ -55,6 +55,7 @@ const IPC = {
55
55
  insomnia: "import:insomnia",
56
56
  bruno: "import:bruno",
57
57
  http: "import:http",
58
+ spector: "import:spector",
58
59
  openapiSchemas: "import:openapi-schemas",
59
60
  openapiSchemasUrl: "import:openapi-schemas-url"
60
61
  },
@@ -198,6 +199,7 @@ const api = {
198
199
  importInsomnia: () => electron.ipcRenderer.invoke(IPC.import.insomnia),
199
200
  importBruno: () => electron.ipcRenderer.invoke(IPC.import.bruno),
200
201
  importHttpFile: () => electron.ipcRenderer.invoke(IPC.import.http),
202
+ importSpectorCollection: () => electron.ipcRenderer.invoke(IPC.import.spector),
201
203
  /** Extract response-body schemas from an OpenAPI spec file (no full import). */
202
204
  extractOpenApiSchemas: () => electron.ipcRenderer.invoke(IPC.import.openapiSchemas),
203
205
  /** Extract response-body schemas from an OpenAPI spec URL (no full import). */
@@ -14350,14 +14350,14 @@ function useWorkspaceLoader() {
14350
14350
  } else {
14351
14351
  useStore.getState().setHistory([]);
14352
14352
  }
14353
- for (const colPath of ws2.collections) {
14353
+ for (const colPath of ws2.collections ?? []) {
14354
14354
  try {
14355
14355
  const col = await electron$r.loadCollection(colPath);
14356
14356
  loadCollection(colPath, col);
14357
14357
  } catch {
14358
14358
  }
14359
14359
  }
14360
- for (const envPath of ws2.environments) {
14360
+ for (const envPath of ws2.environments ?? []) {
14361
14361
  try {
14362
14362
  const env = await electron$r.loadEnvironment(envPath);
14363
14363
  loadEnvironment(envPath, env);
@@ -14388,7 +14388,7 @@ function useWorkspaceLoader() {
14388
14388
  for (const { relPath, snapshot } of snapshots) loadContractSnapshot(relPath, snapshot);
14389
14389
  } catch {
14390
14390
  }
14391
- if (ws2.collections.length > 0) {
14391
+ if ((ws2.collections ?? []).length > 0) {
14392
14392
  try {
14393
14393
  const firstCol = await electron$r.loadCollection(ws2.collections[0]);
14394
14394
  setActiveCollection(firstCol.id);
@@ -35468,7 +35468,11 @@ function AuthEditor({
35468
35468
  /* @__PURE__ */ jsxRuntimeExports.jsx(
35469
35469
  "button",
35470
35470
  {
35471
- onClick: () => secrets.saveSecret(auth.passwordSecretRef ?? "NTLM_PASSWORD"),
35471
+ onClick: () => {
35472
+ const ref2 = auth.passwordSecretRef ?? "NTLM_PASSWORD";
35473
+ onChange({ passwordSecretRef: ref2 });
35474
+ void secrets.saveSecret(ref2);
35475
+ },
35472
35476
  className: "px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded transition-colors",
35473
35477
  children: secrets.saved ? "✓" : "Save"
35474
35478
  }
@@ -35581,7 +35585,11 @@ function AuthEditor({
35581
35585
  /* @__PURE__ */ jsxRuntimeExports.jsx(
35582
35586
  "button",
35583
35587
  {
35584
- onClick: () => secrets.saveSecret(auth.apiKeySecretRef ?? "API_KEY"),
35588
+ onClick: () => {
35589
+ const ref2 = auth.apiKeySecretRef ?? "API_KEY";
35590
+ onChange({ apiKeySecretRef: ref2 });
35591
+ void secrets.saveSecret(ref2);
35592
+ },
35585
35593
  className: "px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded transition-colors",
35586
35594
  children: secrets.saved ? "✓" : "Save"
35587
35595
  }
@@ -35640,55 +35648,88 @@ function BasicCredentialsFields({
35640
35648
  label,
35641
35649
  note
35642
35650
  }) {
35651
+ const [keychainOpen, setKeychainOpen] = reactExports.useState(!!auth.passwordSecretRef);
35643
35652
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5", children: [
35644
35653
  label && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-500 text-[10px] uppercase tracking-wide", children: label }),
35645
35654
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
35646
35655
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [
35647
35656
  /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-surface-400", children: "Username" }),
35648
35657
  /* @__PURE__ */ jsxRuntimeExports.jsx(
35649
- "input",
35658
+ VarInput,
35650
35659
  {
35651
35660
  value: auth.username ?? "",
35652
- onChange: (e) => setAuth({ username: e.target.value }),
35661
+ onChange: (v) => setAuth({ username: v }),
35662
+ placeholder: "username",
35653
35663
  className: "mt-1 w-full bg-surface-800 border border-surface-700 rounded px-2 py-1 focus:outline-none focus:border-blue-500"
35654
35664
  }
35655
35665
  )
35656
35666
  ] }),
35657
35667
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [
35658
- /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-surface-400", children: "Password" }),
35659
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1 mt-1", children: [
35660
- /* @__PURE__ */ jsxRuntimeExports.jsx(
35661
- "input",
35662
- {
35663
- type: "password",
35664
- value: secretValue,
35665
- onChange: (e) => setSecretValue(e.target.value),
35666
- placeholder: auth.passwordSecretRef ? `Stored as "${auth.passwordSecretRef}"` : "Password",
35667
- className: "flex-1 bg-surface-800 border border-surface-700 rounded px-2 py-1 focus:outline-none focus:border-blue-500 font-mono"
35668
- }
35669
- ),
35670
- /* @__PURE__ */ jsxRuntimeExports.jsx(
35671
- "button",
35672
- {
35673
- onClick: () => saveSecret(auth.passwordSecretRef ?? "API_PASSWORD"),
35674
- className: "px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded transition-colors",
35675
- children: saved ? "✓" : "Save"
35676
- }
35677
- )
35678
- ] })
35668
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "text-surface-400", children: [
35669
+ "Password",
35670
+ " ",
35671
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-surface-500 text-[10px]", children: [
35672
+ "- supports ",
35673
+ "{{variables}}"
35674
+ ] })
35675
+ ] }),
35676
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35677
+ VarInput,
35678
+ {
35679
+ value: auth.password ?? "",
35680
+ onChange: (v) => setAuth({ password: v }),
35681
+ placeholder: "password or API token",
35682
+ className: "mt-1 w-full bg-surface-800 border border-surface-700 rounded px-2 py-1 focus:outline-none focus:border-blue-500 font-mono"
35683
+ }
35684
+ )
35679
35685
  ] })
35680
35686
  ] }),
35681
- /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-surface-400 text-[10px]", children: [
35682
- "Password stored in OS keychain as",
35683
- " ",
35684
- /* @__PURE__ */ jsxRuntimeExports.jsx(
35685
- "input",
35686
- {
35687
- value: auth.passwordSecretRef ?? "API_PASSWORD",
35688
- onChange: (e) => setAuth({ passwordSecretRef: e.target.value }),
35689
- className: "inline bg-transparent border-b border-surface-700 focus:outline-none focus:border-blue-500 w-24"
35690
- }
35691
- )
35687
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
35688
+ "button",
35689
+ {
35690
+ onClick: () => setKeychainOpen((o) => !o),
35691
+ className: "text-[10px] text-surface-500 hover:text-surface-300 text-left transition-colors w-fit",
35692
+ children: [
35693
+ keychainOpen ? "▾" : "",
35694
+ " Store password in OS keychain instead"
35695
+ ]
35696
+ }
35697
+ ),
35698
+ keychainOpen && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1 pl-3 border-l border-surface-800", children: [
35699
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-500 text-[10px]", children: "Encrypt the password in your OS keychain rather than saving it with the request. Leave the Password field above empty to use the keychain value." }),
35700
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
35701
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35702
+ "input",
35703
+ {
35704
+ type: "password",
35705
+ value: secretValue,
35706
+ onChange: (e) => setSecretValue(e.target.value),
35707
+ placeholder: auth.passwordSecretRef ? `Stored as "${auth.passwordSecretRef}"` : "Password",
35708
+ className: "flex-1 bg-surface-800 border border-surface-700 rounded px-2 py-1 focus:outline-none focus:border-blue-500 font-mono"
35709
+ }
35710
+ ),
35711
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35712
+ "input",
35713
+ {
35714
+ value: auth.passwordSecretRef ?? "API_PASSWORD",
35715
+ onChange: (e) => setAuth({ passwordSecretRef: e.target.value }),
35716
+ placeholder: "Keychain key",
35717
+ className: "w-32 bg-surface-800 border border-surface-700 rounded px-2 py-1 focus:outline-none focus:border-blue-500"
35718
+ }
35719
+ ),
35720
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35721
+ "button",
35722
+ {
35723
+ onClick: () => {
35724
+ const ref2 = auth.passwordSecretRef ?? "API_PASSWORD";
35725
+ setAuth({ passwordSecretRef: ref2 });
35726
+ void saveSecret(ref2);
35727
+ },
35728
+ className: "px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded transition-colors",
35729
+ children: saved ? "✓" : "Save"
35730
+ }
35731
+ )
35732
+ ] })
35692
35733
  ] }),
35693
35734
  note && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-600 text-[10px]", children: note })
35694
35735
  ] });
@@ -35758,7 +35799,11 @@ function BearerPanel({
35758
35799
  /* @__PURE__ */ jsxRuntimeExports.jsx(
35759
35800
  "button",
35760
35801
  {
35761
- onClick: () => saveSecret(auth.tokenSecretRef ?? "API_TOKEN"),
35802
+ onClick: () => {
35803
+ const ref2 = auth.tokenSecretRef ?? "API_TOKEN";
35804
+ setAuth({ tokenSecretRef: ref2 });
35805
+ void saveSecret(ref2);
35806
+ },
35762
35807
  className: "px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded transition-colors",
35763
35808
  children: saved ? "✓" : "Save"
35764
35809
  }
@@ -36531,6 +36576,10 @@ function DotsBtn({ items: items2 }) {
36531
36576
  menu && /* @__PURE__ */ jsxRuntimeExports.jsx(ContextMenu, { items: items2, x: menu.x, y: menu.y, onClose: () => setMenu(null) })
36532
36577
  ] });
36533
36578
  }
36579
+ function requestPath(url) {
36580
+ const trimmed = url.trim().replace(/^\{\{[^}]+\}\}/, "").replace(/^https?:\/\/[^/]+/i, "");
36581
+ return trimmed || url.trim();
36582
+ }
36534
36583
  const HOOK_LABELS = {
36535
36584
  beforeAll: "Before All",
36536
36585
  before: "Before",
@@ -36557,6 +36606,7 @@ function RequestRow({
36557
36606
  folderId,
36558
36607
  reqIndex,
36559
36608
  name: name2,
36609
+ url,
36560
36610
  method,
36561
36611
  protocol,
36562
36612
  authType,
@@ -36658,6 +36708,10 @@ function RequestRow({
36658
36708
  }
36659
36709
  )
36660
36710
  ] }),
36711
+ (() => {
36712
+ const path = requestPath(url);
36713
+ return path && path !== name2 ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-[10px] text-surface-500 font-mono truncate leading-tight", title: url, children: path }) : null;
36714
+ })(),
36661
36715
  (tags2.length > 0 || addingTag) && /* @__PURE__ */ jsxRuntimeExports.jsx(
36662
36716
  TagChips,
36663
36717
  {
@@ -37241,6 +37295,7 @@ function FolderContents({
37241
37295
  folderId: folder.id,
37242
37296
  reqIndex,
37243
37297
  name: req.name,
37298
+ url: req.url,
37244
37299
  method: req.method,
37245
37300
  protocol: req.protocol,
37246
37301
  authType: req.auth.type,
@@ -37358,6 +37413,25 @@ function getHooksForRequest(requestId, collection) {
37358
37413
  }
37359
37414
  return { before, after: afterReversed.reverse() };
37360
37415
  }
37416
+ function authIsConfigured(auth) {
37417
+ if (!auth || auth.type === "none") return false;
37418
+ const set2 = (s) => !!(s && s.trim());
37419
+ switch (auth.type) {
37420
+ case "bearer":
37421
+ return set2(auth.token) || set2(auth.tokenSecretRef);
37422
+ case "basic":
37423
+ case "digest":
37424
+ return set2(auth.username) || set2(auth.password) || set2(auth.passwordSecretRef);
37425
+ case "ntlm":
37426
+ return set2(auth.username) || set2(auth.password) || set2(auth.passwordSecretRef);
37427
+ case "apikey":
37428
+ return set2(auth.apiKeyValue) || set2(auth.apiKeySecretRef);
37429
+ case "oauth2":
37430
+ return set2(auth.oauth2TokenUrl) || set2(auth.oauth2ClientId) || set2(auth.oauth2CachedToken);
37431
+ default:
37432
+ return true;
37433
+ }
37434
+ }
37361
37435
  function resolveInheritedAuthAndHeaders(requestId, collection) {
37362
37436
  let inheritedAuth = collection.auth && collection.auth.type !== "none" ? collection.auth : null;
37363
37437
  let inheritedHeaders = collection.headers?.filter((h) => h.enabled && h.key) ?? [];
@@ -55886,7 +55960,11 @@ function AuthTab({ request, onChange }) {
55886
55960
  /* @__PURE__ */ jsxRuntimeExports.jsx(
55887
55961
  "button",
55888
55962
  {
55889
- onClick: () => saveSecret(auth.oauth2ClientSecretRef ?? "OAUTH2_CLIENT_SECRET"),
55963
+ onClick: () => {
55964
+ const ref2 = auth.oauth2ClientSecretRef ?? "OAUTH2_CLIENT_SECRET";
55965
+ setAuth({ oauth2ClientSecretRef: ref2 });
55966
+ void saveSecret(ref2);
55967
+ },
55890
55968
  className: "px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded transition-colors",
55891
55969
  children: saved ? "✓" : "Save"
55892
55970
  }
@@ -63829,7 +63907,7 @@ function RequestBuilder({ request }) {
63829
63907
  };
63830
63908
  let liveGlobals = { ...globals };
63831
63909
  const inherited = useStore.getState().getInheritedAuthAndHeaders(request.id);
63832
- const mergedAuth = request.auth.type !== "none" ? request.auth : inherited.auth ?? request.auth;
63910
+ const mergedAuth = authIsConfigured(request.auth) ? request.auth : inherited.auth ?? request.auth;
63833
63911
  const mergedHeaders = [
63834
63912
  ...inherited.headers.filter((h) => h.enabled),
63835
63913
  ...request.headers
@@ -67233,7 +67311,8 @@ const OPTIONS = [
67233
67311
  { id: "openapi", label: "OpenAPI", description: "JSON or YAML (v3.x)", supportsUrl: true },
67234
67312
  { id: "insomnia", label: "Insomnia", description: "Export v4 JSON" },
67235
67313
  { id: "bruno", label: "Bruno", description: "bruno.json collection file" },
67236
- { id: "http", label: "HTTP file", description: ".http / .rest (REST Client)" }
67314
+ { id: "http", label: "HTTP file", description: ".http / .rest (REST Client)" },
67315
+ { id: "spector", label: "API Spector", description: "An existing .spector / .json collection" }
67237
67316
  ];
67238
67317
  function listEndpoints(col) {
67239
67318
  const result = [];
@@ -67304,6 +67383,7 @@ function ImportModal({ onImport, onClose }) {
67304
67383
  if (opt2.id === "insomnia") col = await electron$9.importInsomnia();
67305
67384
  if (opt2.id === "bruno") col = await electron$9.importBruno();
67306
67385
  if (opt2.id === "http") col = await electron$9.importHttpFile();
67386
+ if (opt2.id === "spector") col = await electron$9.importSpectorCollection();
67307
67387
  if (!col) {
67308
67388
  setLoading(false);
67309
67389
  return;
@@ -68506,7 +68586,7 @@ function RunnerModal() {
68506
68586
  items2 = items2.map((item) => {
68507
68587
  const inherited = resolveInheritedAuthAndHeaders(item.request.id, col);
68508
68588
  const req = JSON.parse(JSON.stringify(item.request));
68509
- if (req.auth?.type === "none" && inherited.auth && inherited.auth.type !== "none") {
68589
+ if (!authIsConfigured(req.auth) && inherited.auth && inherited.auth.type !== "none") {
68510
68590
  req.auth = inherited.auth;
68511
68591
  }
68512
68592
  const inheritedHeaders = inherited.headers.filter((h) => h.enabled && h.key);
@@ -72054,7 +72134,7 @@ function App() {
72054
72134
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#6aa3c8" }, children: "Spector" }),
72055
72135
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-[10px] font-normal opacity-50", children: [
72056
72136
  "v",
72057
- "0.3.8"
72137
+ "0.4.0"
72058
72138
  ] }),
72059
72139
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mx-2 opacity-30", children: "·" }),
72060
72140
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -5,7 +5,7 @@
5
5
  <meta charset="UTF-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>API Spector</title>
8
- <script type="module" crossorigin src="./assets/index-BK58GuOI.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-BtkRge9R.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="./assets/index-B6kFQEbI.css">
10
10
  </head>
11
11
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@testsmith/api-spector",
3
3
  "productName": "API Spector",
4
- "version": "0.3.8",
4
+ "version": "0.4.0",
5
5
  "description": "Local-first API testing tool to inspect, test and mock APIs",
6
6
  "repository": {
7
7
  "type": "git",