@testsmith/api-spector 0.0.1 → 0.0.3
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/out/main/chunks/{script-runner-Ci5t2-bo.js → script-runner-DIevRMmJ.js} +43 -0
- package/out/main/index.js +3 -2
- package/out/main/mock.js +1 -1
- package/out/main/runner.js +1 -1
- package/out/preload/index.js +2 -0
- package/out/renderer/assets/{index-D2g8SYEA.js → index-CHCrooIl.js} +28 -23
- package/out/renderer/index.html +1 -1
- package/package.json +2 -3
- package/readme.md +4 -4
|
@@ -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-
|
|
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
|
}
|
package/out/main/runner.js
CHANGED
|
@@ -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-
|
|
6
|
+
const scriptRunner = require("./chunks/script-runner-DIevRMmJ.js");
|
|
7
7
|
require("crypto");
|
|
8
8
|
require("vm");
|
|
9
9
|
require("dayjs");
|
package/out/preload/index.js
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
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
|
-
|
|
35109
|
-
}, [gql.
|
|
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
|
|
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
|
|
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-
|
|
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(
|
|
46681
|
-
setSelectedEnvId(
|
|
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 (
|
|
46691
|
-
items2 =
|
|
46695
|
+
if (ds.rows.length > 0) {
|
|
46696
|
+
items2 = ds.rows.flatMap((row, ri2) => {
|
|
46692
46697
|
const dataRow = {};
|
|
46693
|
-
|
|
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}/${
|
|
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-
|
|
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
|
|
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-
|
|
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)) {
|
package/out/renderer/index.html
CHANGED
|
@@ -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-
|
|
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,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testsmith/api-spector",
|
|
3
3
|
"productName": "api Spector",
|
|
4
|
-
"version": "0.0.
|
|
5
|
-
"description": "Local-first API testing tool
|
|
4
|
+
"version": "0.0.3",
|
|
5
|
+
"description": "Local-first API testing tool to inspect, test and mock APIs",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/testsmith-io/api-spector"
|
|
@@ -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",
|
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# api Spector
|
|
2
2
|
|
|
3
|
-
Local-first API testing tool. Inspect, test and mock APIs
|
|
3
|
+
Local-first API testing tool. Inspect, test and mock APIs. Secrets stay on your machine.
|
|
4
4
|
|
|
5
5
|
- GUI built with Electron + React
|
|
6
6
|
- CLI for running tests and mock servers in CI/CD pipelines
|
|
@@ -57,7 +57,7 @@ Keeps running until `Ctrl+C`.
|
|
|
57
57
|
|
|
58
58
|
## Workspaces
|
|
59
59
|
|
|
60
|
-
A workspace is a `.spector` file that references your collections, environments and mock servers. Safe to commit to Git
|
|
60
|
+
A workspace is a `.spector` file that references your collections, environments and mock servers. Safe to commit to Git. Secrets are encrypted, not stored in plain text.
|
|
61
61
|
|
|
62
62
|
```
|
|
63
63
|
my-project/
|
|
@@ -78,12 +78,12 @@ Secret variables are encrypted with AES-256-GCM using a master password. Set `AP
|
|
|
78
78
|
export API_SPECTOR_MASTER_KEY="your-password"
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
**Windows
|
|
81
|
+
**Windows (PowerShell profile):**
|
|
82
82
|
```powershell
|
|
83
83
|
$env:API_SPECTOR_MASTER_KEY = "your-password"
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
**Windows
|
|
86
|
+
**Windows (Command Prompt, permanent):**
|
|
87
87
|
```cmd
|
|
88
88
|
setx API_SPECTOR_MASTER_KEY "your-password"
|
|
89
89
|
```
|