@testsmith/api-spector 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,6 +73,30 @@ function patchGlobals(patch) {
73
73
  globals = { ...globals, ...patch };
74
74
  }
75
75
  const MASTER_KEY_ENV = "API_SPECTOR_MASTER_KEY";
76
+ let secretStore = {};
77
+ let secretStorePath = null;
78
+ async function initSecretStore(userDataPath) {
79
+ secretStorePath = path.join(userDataPath, "secrets.json");
80
+ try {
81
+ const raw = await promises.readFile(secretStorePath, "utf8");
82
+ secretStore = JSON.parse(raw);
83
+ } catch {
84
+ secretStore = {};
85
+ }
86
+ }
87
+ async function persistSecretStore() {
88
+ if (!secretStorePath) return;
89
+ await promises.writeFile(secretStorePath, JSON.stringify(secretStore, null, 2), "utf8");
90
+ }
91
+ function getSafeStorage() {
92
+ try {
93
+ const { safeStorage } = require("electron");
94
+ if (typeof safeStorage?.isEncryptionAvailable === "function") return safeStorage;
95
+ return null;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
76
100
  function registerSecretHandlers(ipc) {
77
101
  ipc.handle("secret:checkMasterKey", () => {
78
102
  return { set: Boolean(process.env[MASTER_KEY_ENV]) };
@@ -80,6 +104,14 @@ function registerSecretHandlers(ipc) {
80
104
  ipc.handle("secret:setMasterKey", (_e, value) => {
81
105
  process.env[MASTER_KEY_ENV] = value;
82
106
  });
107
+ ipc.handle("secret:set", async (_e, ref, value) => {
108
+ const ss = getSafeStorage();
109
+ if (!ss || !ss.isEncryptionAvailable()) {
110
+ throw new Error("OS encryption is not available — set the secret via environment variable instead");
111
+ }
112
+ secretStore[ref] = ss.encryptString(value).toString("base64");
113
+ await persistSecretStore();
114
+ });
83
115
  }
84
116
  function decryptSecret(encrypted, salt, iv, password) {
85
117
  const saltBuf = Buffer.from(salt, "base64");
@@ -93,6 +125,16 @@ function decryptSecret(encrypted, salt, iv, password) {
93
125
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
94
126
  }
95
127
  async function getSecret(ref) {
128
+ const stored = secretStore[ref];
129
+ if (stored) {
130
+ const ss = getSafeStorage();
131
+ if (ss && ss.isEncryptionAvailable()) {
132
+ try {
133
+ return ss.decryptString(Buffer.from(stored, "base64"));
134
+ } catch {
135
+ }
136
+ }
137
+ }
96
138
  return process.env[ref] ?? null;
97
139
  }
98
140
  function interpolate(str, vars) {
@@ -389,6 +431,7 @@ exports.buildEnvVars = buildEnvVars;
389
431
  exports.buildUrl = buildUrl;
390
432
  exports.getGlobals = getGlobals;
391
433
  exports.getSecret = getSecret;
434
+ exports.initSecretStore = initSecretStore;
392
435
  exports.interpolate = interpolate;
393
436
  exports.loadGlobals = loadGlobals;
394
437
  exports.mergeVars = mergeVars;
package/out/main/index.js CHANGED
@@ -25,7 +25,7 @@ const electron = require("electron");
25
25
  const path = require("path");
26
26
  const fs = require("fs");
27
27
  const promises = require("fs/promises");
28
- const scriptRunner = require("./chunks/script-runner-Ci5t2-bo.js");
28
+ const scriptRunner = require("./chunks/script-runner-DIevRMmJ.js");
29
29
  const undici = require("undici");
30
30
  const crypto = require("crypto");
31
31
  const uuid = require("uuid");
@@ -3976,7 +3976,8 @@ function createWindow() {
3976
3976
  if (devToolsShortcut) win.webContents.toggleDevTools();
3977
3977
  });
3978
3978
  }
3979
- electron.app.whenReady().then(() => {
3979
+ electron.app.whenReady().then(async () => {
3980
+ await scriptRunner.initSecretStore(electron.app.getPath("userData"));
3980
3981
  registerFileHandlers(electron.ipcMain);
3981
3982
  registerRequestHandler(electron.ipcMain);
3982
3983
  scriptRunner.registerSecretHandlers(electron.ipcMain);
package/out/main/mock.js CHANGED
@@ -108,7 +108,7 @@ async function main() {
108
108
  started++;
109
109
  } catch (e) {
110
110
  console.error(
111
- color(" ✗", C.red, C.bold) + ` ${mock.name} ` + color(e.message, C.red)
111
+ color(" ✗", C.red, C.bold) + ` ${mock.name} ` + color(e instanceof Error ? e.message : String(e), C.red)
112
112
  );
113
113
  }
114
114
  }
@@ -3,7 +3,7 @@
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
5
  const undici = require("undici");
6
- const scriptRunner = require("./chunks/script-runner-Ci5t2-bo.js");
6
+ const scriptRunner = require("./chunks/script-runner-DIevRMmJ.js");
7
7
  require("crypto");
8
8
  require("vm");
9
9
  require("dayjs");
@@ -16,6 +16,8 @@ const api = {
16
16
  // ─── Secrets (encrypted, master-key-based) ───────────────────────────────
17
17
  checkMasterKey: () => electron.ipcRenderer.invoke("secret:checkMasterKey"),
18
18
  setMasterKey: (value) => electron.ipcRenderer.invoke("secret:setMasterKey", value),
19
+ /** Save a named secret to the OS keychain (safeStorage). */
20
+ setSecret: (ref, value) => electron.ipcRenderer.invoke("secret:set", ref, value),
19
21
  // ─── Globals ──────────────────────────────────────────────────────────────
20
22
  getGlobals: () => electron.ipcRenderer.invoke("globals:get"),
21
23
  setGlobals: (globals) => electron.ipcRenderer.invoke("globals:set", globals),
@@ -8506,7 +8506,7 @@ function useAutoSave() {
8506
8506
  return () => {
8507
8507
  if (colTimerRef.current) clearTimeout(colTimerRef.current);
8508
8508
  };
8509
- }, [collections]);
8509
+ }, [collections, markCollectionClean]);
8510
8510
  reactExports.useEffect(() => {
8511
8511
  if (!workspace) return;
8512
8512
  if (wsTimerRef.current) clearTimeout(wsTimerRef.current);
@@ -8527,7 +8527,7 @@ function useWorkspaceLoader() {
8527
8527
  const loadEnvironment = useStore((s) => s.loadEnvironment);
8528
8528
  const loadMock = useStore((s) => s.loadMock);
8529
8529
  const setActiveCollection = useStore((s) => s.setActiveCollection);
8530
- async function applyWorkspace(ws2, path) {
8530
+ const applyWorkspace = reactExports.useCallback(async (ws2, path) => {
8531
8531
  useStore.setState({
8532
8532
  workspace: ws2,
8533
8533
  workspacePath: path,
@@ -8566,7 +8566,7 @@ function useWorkspaceLoader() {
8566
8566
  } catch {
8567
8567
  }
8568
8568
  }
8569
- }
8569
+ }, [loadCollection, loadEnvironment, loadMock, setActiveCollection]);
8570
8570
  return { applyWorkspace };
8571
8571
  }
8572
8572
  const METHOD_COLORS$1 = {
@@ -9217,7 +9217,7 @@ function CollectionTree() {
9217
9217
  const updateRequestTags = useStore((s) => s.updateRequestTags);
9218
9218
  const openRunner = useStore((s) => s.openRunner);
9219
9219
  const colList = Object.values(collections);
9220
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full select-none", children: [
9220
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col flex-1 min-h-0 select-none", children: [
9221
9221
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-3 py-2 text-xs font-semibold text-surface-400 uppercase tracking-wider flex items-center justify-between flex-shrink-0", children: [
9222
9222
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Collections" }),
9223
9223
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -35105,8 +35105,8 @@ function GraphQLEditor({ request, onChange }) {
35105
35105
  onChange({ body: { ...request.body, graphql: { ...gql, ...patch } } });
35106
35106
  }
35107
35107
  const handleInsert = reactExports.useCallback((snippet2) => {
35108
- updateGql({ query: insertSnippet(gql.query, snippet2) });
35109
- }, [gql.query]);
35108
+ onChange({ body: { ...request.body, graphql: { ...gql, query: insertSnippet(gql.query, snippet2) } } });
35109
+ }, [gql, onChange, request.body]);
35110
35110
  async function loadSchema() {
35111
35111
  const url = request.url.trim();
35112
35112
  if (!url) return;
@@ -35122,7 +35122,7 @@ function GraphQLEditor({ request, onChange }) {
35122
35122
  setSchemaState("idle");
35123
35123
  setShowExplorer(true);
35124
35124
  } catch (e) {
35125
- setSchemaError(e.message ?? String(e));
35125
+ setSchemaError(e instanceof Error ? e.message : String(e));
35126
35126
  setSchemaState("error");
35127
35127
  }
35128
35128
  }
@@ -43416,7 +43416,7 @@ function WebSocketPanel({ request }) {
43416
43416
  return () => {
43417
43417
  electron$g.offWsEvents();
43418
43418
  };
43419
- }, []);
43419
+ }, [addWsMessage, setWsStatus]);
43420
43420
  reactExports.useEffect(() => {
43421
43421
  logEndRef.current?.scrollIntoView({ behavior: "smooth" });
43422
43422
  }, [conn.messages.length]);
@@ -44615,7 +44615,7 @@ function GeneratorPanel() {
44615
44615
  setFiles(generated);
44616
44616
  setSelectedFile(generated[0]?.path ?? null);
44617
44617
  } catch (e) {
44618
- setError(e?.message ?? String(e));
44618
+ setError(e instanceof Error ? e.message : String(e));
44619
44619
  } finally {
44620
44620
  setGenerating(false);
44621
44621
  }
@@ -44770,7 +44770,7 @@ function HistoryPanel() {
44770
44770
  setTabResponse(activeTabId, entry.response, entry.scriptResult ?? null);
44771
44771
  }
44772
44772
  }
44773
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full", children: [
44773
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col flex-1 min-h-0", children: [
44774
44774
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-2 py-2 border-b border-surface-800 flex gap-1.5 flex-shrink-0", children: [
44775
44775
  /* @__PURE__ */ jsxRuntimeExports.jsx(
44776
44776
  "input",
@@ -46676,27 +46676,32 @@ function RunnerModal() {
46676
46676
  const iterCount = dataSet.rows.length;
46677
46677
  const availableTags = collectionId ? allTagsIn(collectionId, folderId) : [];
46678
46678
  const progressIdxRef = reactExports.useRef(0);
46679
+ const initFilterTagsRef = reactExports.useRef(runnerModal.filterTags);
46680
+ initFilterTagsRef.current = runnerModal.filterTags;
46681
+ const initEnvIdRef = reactExports.useRef(activeEnvId);
46682
+ initEnvIdRef.current = activeEnvId;
46679
46683
  reactExports.useEffect(() => {
46680
- setFilterTags(runnerModal.filterTags);
46681
- setSelectedEnvId(activeEnvId ?? "");
46684
+ setFilterTags(initFilterTagsRef.current);
46685
+ setSelectedEnvId(initEnvIdRef.current ?? "");
46682
46686
  setSummary(null);
46683
46687
  progressIdxRef.current = 0;
46684
46688
  }, [runnerModal.open]);
46685
46689
  const toggleTag = (tag) => setFilterTags((prev) => prev.includes(tag) ? prev.filter((t2) => t2 !== tag) : [...prev, tag]);
46686
46690
  const run = reactExports.useCallback(async () => {
46691
+ const ds = colEntry?.data.dataSet ?? { columns: [], rows: [] };
46687
46692
  const baseItems = collectionId ? collectRequests(collectionId, folderId, filterTags) : [];
46688
46693
  if (baseItems.length === 0) return;
46689
46694
  let items2;
46690
- if (dataSet.rows.length > 0) {
46691
- items2 = dataSet.rows.flatMap((row, ri2) => {
46695
+ if (ds.rows.length > 0) {
46696
+ items2 = ds.rows.flatMap((row, ri2) => {
46692
46697
  const dataRow = {};
46693
- dataSet.columns.forEach((col, ci2) => {
46698
+ ds.columns.forEach((col, ci2) => {
46694
46699
  dataRow[col] = row[ci2] ?? "";
46695
46700
  });
46696
46701
  return baseItems.map((item) => ({
46697
46702
  ...item,
46698
46703
  dataRow,
46699
- iterationLabel: `${ri2 + 1}/${dataSet.rows.length}`
46704
+ iterationLabel: `${ri2 + 1}/${ds.rows.length}`
46700
46705
  }));
46701
46706
  });
46702
46707
  } else {
@@ -46734,7 +46739,7 @@ function RunnerModal() {
46734
46739
  electron$4.offRunProgress();
46735
46740
  setRunnerRunning(false);
46736
46741
  }
46737
- }, [collectionId, folderId, filterTags, selectedEnvId, environments, globals, colEntry, requestDelay]);
46742
+ }, [collectionId, folderId, filterTags, selectedEnvId, environments, globals, colEntry, requestDelay, workspaceSettings, setRunnerResults, patchRunnerResult, setRunnerRunning]);
46738
46743
  if (!runnerModal.open) return null;
46739
46744
  const envName = selectedEnvId ? environments[selectedEnvId]?.data.name ?? null : null;
46740
46745
  function copyCI(key, content2) {
@@ -47199,7 +47204,7 @@ function MockPanel() {
47199
47204
  } catch {
47200
47205
  }
47201
47206
  }
47202
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full", children: [
47207
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col flex-1 min-h-0", children: [
47203
47208
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-3 py-2 flex items-center justify-between border-b border-surface-800 flex-shrink-0", children: [
47204
47209
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-semibold uppercase tracking-widest text-surface-600", children: "Mock Servers" }),
47205
47210
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -47539,7 +47544,7 @@ function MockDetailPanel({ mockId }) {
47539
47544
  setRunning(mock.id, true);
47540
47545
  }
47541
47546
  } catch (e) {
47542
- setError(e.message ?? String(e));
47547
+ setError(e instanceof Error ? e.message : String(e));
47543
47548
  }
47544
47549
  }
47545
47550
  function addRoute() {
@@ -47749,7 +47754,7 @@ function ContractPanel() {
47749
47754
  setRunning(false);
47750
47755
  }
47751
47756
  }
47752
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [
47757
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col flex-1 min-h-0 overflow-hidden", children: [
47753
47758
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3 px-3 py-3 border-b border-surface-800 flex-shrink-0", children: [
47754
47759
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-1 bg-surface-800 rounded-lg p-0.5", children: ["consumer", "provider", "bidirectional"].map((m2) => /* @__PURE__ */ jsxRuntimeExports.jsx(
47755
47760
  "button",
@@ -48137,11 +48142,11 @@ function App() {
48137
48142
  electron.getLastWorkspace().then((result) => {
48138
48143
  if (result) applyWorkspace(result.workspace, result.workspacePath);
48139
48144
  });
48140
- }, []);
48145
+ }, [applyWorkspace]);
48141
48146
  reactExports.useEffect(() => {
48142
48147
  electron.onMockHit(addMockHit);
48143
48148
  return () => electron.offMockHit();
48144
- }, []);
48149
+ }, [addMockHit]);
48145
48150
  reactExports.useEffect(() => {
48146
48151
  electron.onWsMessage(({ requestId, message }) => {
48147
48152
  addWsMessage(requestId, message);
@@ -48150,7 +48155,7 @@ function App() {
48150
48155
  setWsStatus(requestId, status, error2);
48151
48156
  });
48152
48157
  return () => electron.offWsEvents();
48153
- }, []);
48158
+ }, [addWsMessage, setWsStatus]);
48154
48159
  reactExports.useEffect(() => {
48155
48160
  function handleKeyDown(e) {
48156
48161
  if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>api Spector</title>
7
- <script type="module" crossorigin src="./assets/index-D2g8SYEA.js"></script>
7
+ <script type="module" crossorigin src="./assets/index-CHCrooIl.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="./assets/index-B_l1FCkO.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@testsmith/api-spector",
3
3
  "productName": "api Spector",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "description": "Local-first API testing tool — inspect, test and mock APIs",
6
6
  "repository": {
7
7
  "type": "git",
@@ -73,7 +73,6 @@
73
73
  "immer": "^10.1.1",
74
74
  "js-yaml": "^4.1.1",
75
75
  "jszip": "^3.10.1",
76
- "keytar": "^7.9.0",
77
76
  "node-soap": "^1.0.0",
78
77
  "react": "^18.3.1",
79
78
  "react-dom": "^18.3.1",