@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 +42 -33
- package/out/main/chunks/auth-builder-B7-LgcGr.js +373 -0
- package/out/main/chunks/import-C9qdkBCH.js +154 -0
- package/out/main/chunks/ipc-validate-CscN4HfG.js +94 -0
- package/out/main/chunks/{mock-server-Cx-xG4pJ.js → mock-server-DmdvwCgj.js} +4 -0
- package/out/main/chunks/{request-collection-Dx0ZqB54.js → request-collection-DIsjTggj.js} +25 -349
- package/out/main/chunks/snapshots-C7YbGHM7.js +588 -0
- package/out/main/chunks/soap-handler-Cpj-JwyA.js +326 -0
- package/out/main/contract.js +210 -0
- package/out/main/index.js +398 -592
- package/out/main/mock.js +1 -1
- package/out/main/runner.js +20 -18
- package/out/main/wsdl.js +174 -0
- package/out/preload/index.js +12 -0
- package/out/renderer/assets/{index-C_vjoxxA.js → index-BC1srylp.js} +1374 -647
- package/out/renderer/assets/index-DfkLUeA1.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +6 -1
- package/out/renderer/assets/index-DVaubmCJ.css +0 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const Ajv = require("ajv");
|
|
4
|
+
const ajv = new Ajv({
|
|
5
|
+
allErrors: true,
|
|
6
|
+
strict: false,
|
|
7
|
+
coerceTypes: false,
|
|
8
|
+
allowUnionTypes: true
|
|
9
|
+
});
|
|
10
|
+
function compile(schema) {
|
|
11
|
+
const fn = ajv.compile(schema);
|
|
12
|
+
return function validate(data) {
|
|
13
|
+
if (!fn(data)) {
|
|
14
|
+
const err = (fn.errors ?? []).map((e) => `${e.instancePath || "<root>"} ${e.message ?? "invalid"}`).join("; ");
|
|
15
|
+
throw new Error(`Invalid IPC payload: ${err || "unknown"}`);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const apiRequestSchema = {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
id: { type: "string" },
|
|
23
|
+
name: { type: "string" },
|
|
24
|
+
method: { type: "string" },
|
|
25
|
+
url: { type: "string" },
|
|
26
|
+
headers: { type: "array" },
|
|
27
|
+
params: { type: "array" },
|
|
28
|
+
auth: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: { type: { type: "string" } },
|
|
31
|
+
required: ["type"],
|
|
32
|
+
additionalProperties: true
|
|
33
|
+
},
|
|
34
|
+
body: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: { mode: { type: "string" } },
|
|
37
|
+
required: ["mode"],
|
|
38
|
+
additionalProperties: true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
required: ["id", "name", "method", "url", "headers", "params"],
|
|
42
|
+
additionalProperties: true
|
|
43
|
+
};
|
|
44
|
+
const stringMap = {
|
|
45
|
+
type: "object",
|
|
46
|
+
additionalProperties: { type: "string" }
|
|
47
|
+
};
|
|
48
|
+
const validateSendRequestPayload = compile({
|
|
49
|
+
type: "object",
|
|
50
|
+
properties: {
|
|
51
|
+
request: apiRequestSchema,
|
|
52
|
+
collectionVars: stringMap,
|
|
53
|
+
globals: stringMap,
|
|
54
|
+
piiMaskPatterns: { type: "array", items: { type: "string" } }
|
|
55
|
+
// `environment`, `proxy`, `tls` are accepted as anything — they're
|
|
56
|
+
// covered by the IPC handler's own type-narrow + defaults.
|
|
57
|
+
},
|
|
58
|
+
required: ["request"],
|
|
59
|
+
additionalProperties: true
|
|
60
|
+
});
|
|
61
|
+
const validateContractRunPayload = compile({
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
mode: { type: "string", enum: ["consumer", "provider", "bidirectional"] },
|
|
65
|
+
requests: { type: "array", items: apiRequestSchema },
|
|
66
|
+
envVars: stringMap,
|
|
67
|
+
collectionVars: stringMap,
|
|
68
|
+
specUrl: { type: "string" },
|
|
69
|
+
specPath: { type: "string" },
|
|
70
|
+
specSnapshotRelPath: { type: "string" },
|
|
71
|
+
requestBaseUrl: { type: "string" }
|
|
72
|
+
},
|
|
73
|
+
required: ["mode", "requests"],
|
|
74
|
+
additionalProperties: true
|
|
75
|
+
});
|
|
76
|
+
const validateWsdlFetchUrl = (url) => {
|
|
77
|
+
if (typeof url !== "string" || !url.trim()) throw new Error("Invalid IPC payload: url must be a non-empty string");
|
|
78
|
+
if (!/^https?:\/\//i.test(url)) throw new Error("Invalid IPC payload: url must start with http:// or https://");
|
|
79
|
+
};
|
|
80
|
+
const validateWsdlImport = compile({
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
url: { type: "string" },
|
|
84
|
+
xml: { type: "string" },
|
|
85
|
+
name: { type: "string" },
|
|
86
|
+
extraHeaders: stringMap,
|
|
87
|
+
existingMockPorts: { type: "array", items: { type: "number" } }
|
|
88
|
+
},
|
|
89
|
+
additionalProperties: true
|
|
90
|
+
});
|
|
91
|
+
exports.validateContractRunPayload = validateContractRunPayload;
|
|
92
|
+
exports.validateSendRequestPayload = validateSendRequestPayload;
|
|
93
|
+
exports.validateWsdlFetchUrl = validateWsdlFetchUrl;
|
|
94
|
+
exports.validateWsdlImport = validateWsdlImport;
|
|
@@ -169,6 +169,10 @@ async function handleRequest(serverId, req, res, reqStart) {
|
|
|
169
169
|
vm__namespace.runInNewContext(route.script, {
|
|
170
170
|
request: requestCtx,
|
|
171
171
|
response: responseDraft,
|
|
172
|
+
// Free-form per-route data the importer/user can stash (e.g. SOAP
|
|
173
|
+
// envelopes by operation, lookup tables, fixtures). Frozen so a script
|
|
174
|
+
// can't mutate it across calls.
|
|
175
|
+
metadata: route.metadata ? Object.freeze({ ...route.metadata }) : {},
|
|
172
176
|
faker: faker2,
|
|
173
177
|
dayjs,
|
|
174
178
|
console: { log: (...args) => console.log("[mock-script]", ...args) }
|
|
@@ -23,14 +23,16 @@ 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 authBuilder = require("./auth-builder-B7-LgcGr.js");
|
|
27
|
+
const vm = require("vm");
|
|
26
28
|
const crypto = require("crypto");
|
|
27
|
-
const path = require("path");
|
|
28
29
|
const dayjs = require("dayjs");
|
|
29
|
-
const vm = require("vm");
|
|
30
30
|
const tv4 = require("tv4");
|
|
31
31
|
const jsonpathPlus = require("jsonpath-plus");
|
|
32
32
|
const xmldom = require("@xmldom/xmldom");
|
|
33
|
+
const path = require("path");
|
|
33
34
|
const Ajv = require("ajv");
|
|
35
|
+
const ipcValidate = require("./ipc-validate-CscN4HfG.js");
|
|
34
36
|
function _interopNamespaceDefault(e) {
|
|
35
37
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
36
38
|
if (e) {
|
|
@@ -47,8 +49,8 @@ function _interopNamespaceDefault(e) {
|
|
|
47
49
|
n.default = e;
|
|
48
50
|
return Object.freeze(n);
|
|
49
51
|
}
|
|
50
|
-
const crypto__namespace = /* @__PURE__ */ _interopNamespaceDefault(crypto);
|
|
51
52
|
const vm__namespace = /* @__PURE__ */ _interopNamespaceDefault(vm);
|
|
53
|
+
const crypto__namespace = /* @__PURE__ */ _interopNamespaceDefault(crypto);
|
|
52
54
|
let globals = {};
|
|
53
55
|
let currentDir = null;
|
|
54
56
|
function globalsPath(dir) {
|
|
@@ -77,171 +79,6 @@ function setGlobals(next) {
|
|
|
77
79
|
function patchGlobals(patch) {
|
|
78
80
|
globals = { ...globals, ...patch };
|
|
79
81
|
}
|
|
80
|
-
const MASTER_KEY_ENV = "API_SPECTOR_MASTER_KEY";
|
|
81
|
-
let secretStore = {};
|
|
82
|
-
let secretStorePath = null;
|
|
83
|
-
async function initSecretStore(userDataPath) {
|
|
84
|
-
secretStorePath = path.join(userDataPath, "secrets.json");
|
|
85
|
-
try {
|
|
86
|
-
const raw = await promises.readFile(secretStorePath, "utf8");
|
|
87
|
-
secretStore = JSON.parse(raw);
|
|
88
|
-
} catch {
|
|
89
|
-
secretStore = {};
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
async function persistSecretStore() {
|
|
93
|
-
if (!secretStorePath) return;
|
|
94
|
-
await promises.writeFile(secretStorePath, JSON.stringify(secretStore, null, 2), "utf8");
|
|
95
|
-
}
|
|
96
|
-
function getSafeStorage() {
|
|
97
|
-
try {
|
|
98
|
-
const { safeStorage } = require("electron");
|
|
99
|
-
if (typeof safeStorage?.isEncryptionAvailable === "function") return safeStorage;
|
|
100
|
-
return null;
|
|
101
|
-
} catch {
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
function registerSecretHandlers(ipc) {
|
|
106
|
-
ipc.handle("secret:checkMasterKey", () => {
|
|
107
|
-
return { set: Boolean(process.env[MASTER_KEY_ENV]) };
|
|
108
|
-
});
|
|
109
|
-
ipc.handle("secret:setMasterKey", (_e, value) => {
|
|
110
|
-
process.env[MASTER_KEY_ENV] = value;
|
|
111
|
-
});
|
|
112
|
-
ipc.handle("secret:set", async (_e, ref, value) => {
|
|
113
|
-
const ss = getSafeStorage();
|
|
114
|
-
if (!ss || !ss.isEncryptionAvailable()) {
|
|
115
|
-
throw new Error("OS encryption is not available — set the secret via environment variable instead");
|
|
116
|
-
}
|
|
117
|
-
secretStore[ref] = ss.encryptString(value).toString("base64");
|
|
118
|
-
await persistSecretStore();
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
function decryptSecret(encrypted, salt, iv, password) {
|
|
122
|
-
const saltBuf = Buffer.from(salt, "base64");
|
|
123
|
-
const ivBuf = Buffer.from(iv, "base64");
|
|
124
|
-
const encBuf = Buffer.from(encrypted, "base64");
|
|
125
|
-
const key = crypto.pbkdf2Sync(password, saltBuf, 1e5, 32, "sha256");
|
|
126
|
-
const authTag = encBuf.subarray(encBuf.length - 16);
|
|
127
|
-
const ciphertext = encBuf.subarray(0, encBuf.length - 16);
|
|
128
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", key, ivBuf);
|
|
129
|
-
decipher.setAuthTag(authTag);
|
|
130
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
131
|
-
}
|
|
132
|
-
async function getSecret(ref) {
|
|
133
|
-
const stored = secretStore[ref];
|
|
134
|
-
if (stored) {
|
|
135
|
-
const ss = getSafeStorage();
|
|
136
|
-
if (ss && ss.isEncryptionAvailable()) {
|
|
137
|
-
try {
|
|
138
|
-
return ss.decryptString(Buffer.from(stored, "base64"));
|
|
139
|
-
} catch {
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return process.env[ref] ?? null;
|
|
144
|
-
}
|
|
145
|
-
let _fakerCache$1 = null;
|
|
146
|
-
async function getFaker$1() {
|
|
147
|
-
if (!_fakerCache$1) _fakerCache$1 = await import("@faker-js/faker");
|
|
148
|
-
return _fakerCache$1.faker;
|
|
149
|
-
}
|
|
150
|
-
let _exprContext = null;
|
|
151
|
-
async function buildDynamicVars() {
|
|
152
|
-
const faker = await getFaker$1();
|
|
153
|
-
const now = dayjs();
|
|
154
|
-
_exprContext = { faker, dayjs };
|
|
155
|
-
return {
|
|
156
|
-
$uuid: faker.string.uuid(),
|
|
157
|
-
$timestamp: String(Date.now()),
|
|
158
|
-
$isoTimestamp: now.toISOString(),
|
|
159
|
-
$randomInt: String(faker.number.int({ min: 0, max: 1e3 })),
|
|
160
|
-
$randomFloat: String(faker.number.float({ min: 0, max: 1e3, fractionDigits: 2 })),
|
|
161
|
-
$randomBoolean: String(faker.datatype.boolean()),
|
|
162
|
-
$randomEmail: faker.internet.email(),
|
|
163
|
-
$randomUsername: faker.internet.username(),
|
|
164
|
-
$randomPassword: faker.internet.password(),
|
|
165
|
-
$randomFullName: faker.person.fullName(),
|
|
166
|
-
$randomFirstName: faker.person.firstName(),
|
|
167
|
-
$randomLastName: faker.person.lastName(),
|
|
168
|
-
$randomWord: faker.lorem.word(),
|
|
169
|
-
$randomPhrase: faker.lorem.sentence(),
|
|
170
|
-
$randomUrl: faker.internet.url(),
|
|
171
|
-
$randomIp: faker.internet.ip(),
|
|
172
|
-
$randomHexColor: faker.color.rgb({ format: "hex", casing: "lower" })
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
function interpolate(str, vars) {
|
|
176
|
-
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
177
|
-
const trimmed = key.trim();
|
|
178
|
-
if (trimmed in vars) return vars[trimmed];
|
|
179
|
-
if (_exprContext && (trimmed.includes(".") || trimmed.includes("("))) {
|
|
180
|
-
try {
|
|
181
|
-
const result = vm__namespace.runInNewContext(trimmed, _exprContext);
|
|
182
|
-
if (result !== void 0 && result !== null) return String(result);
|
|
183
|
-
} catch {
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return match;
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
function buildUrl(baseUrl, params, vars) {
|
|
190
|
-
const templateTokens = /* @__PURE__ */ new Set();
|
|
191
|
-
baseUrl.replace(/\{\{([^}]+)\}\}/g, (_m, name) => {
|
|
192
|
-
templateTokens.add(String(name).trim());
|
|
193
|
-
return "";
|
|
194
|
-
});
|
|
195
|
-
const enabled = (params ?? []).filter((p) => p.enabled && p.key);
|
|
196
|
-
const pathRows = [];
|
|
197
|
-
const queryRows = [];
|
|
198
|
-
for (const p of enabled) {
|
|
199
|
-
const isPath = p.paramType === "path" || templateTokens.has(p.key);
|
|
200
|
-
if (isPath) pathRows.push(p);
|
|
201
|
-
else queryRows.push(p);
|
|
202
|
-
}
|
|
203
|
-
const mergedVars = pathRows.length ? {
|
|
204
|
-
...vars,
|
|
205
|
-
...Object.fromEntries(pathRows.map((p) => [p.key, interpolate(p.value, vars)]))
|
|
206
|
-
} : vars;
|
|
207
|
-
const url = interpolate(baseUrl, mergedVars);
|
|
208
|
-
if (!queryRows.length) return url;
|
|
209
|
-
const sep = url.includes("?") ? "&" : "?";
|
|
210
|
-
const qs = queryRows.map((p) => `${encodeURIComponent(interpolate(p.key, vars))}=${encodeURIComponent(interpolate(p.value, vars))}`).join("&");
|
|
211
|
-
return url + sep + qs;
|
|
212
|
-
}
|
|
213
|
-
async function buildEnvVars(environment) {
|
|
214
|
-
const vars = {};
|
|
215
|
-
if (!environment) return vars;
|
|
216
|
-
const masterKey = process.env["API_SPECTOR_MASTER_KEY"];
|
|
217
|
-
for (const v of environment.variables) {
|
|
218
|
-
if (!v.enabled) continue;
|
|
219
|
-
if (v.envRef) {
|
|
220
|
-
const envValue = process.env[v.envRef];
|
|
221
|
-
if (envValue !== void 0) vars[v.key] = envValue;
|
|
222
|
-
} else if (v.secret && v.secretEncrypted && v.secretSalt && v.secretIv) {
|
|
223
|
-
if (masterKey) {
|
|
224
|
-
try {
|
|
225
|
-
vars[v.key] = decryptSecret(v.secretEncrypted, v.secretSalt, v.secretIv, masterKey);
|
|
226
|
-
} catch {
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (vars[v.key] === void 0 && process.env[v.key] !== void 0) {
|
|
230
|
-
vars[v.key] = process.env[v.key];
|
|
231
|
-
}
|
|
232
|
-
} else if (v.secret) {
|
|
233
|
-
if (process.env[v.key] !== void 0) {
|
|
234
|
-
vars[v.key] = process.env[v.key];
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
vars[v.key] = v.value;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return vars;
|
|
241
|
-
}
|
|
242
|
-
function mergeVars(envVars, collectionVars, globals2, localVars = {}, dynamicVars = {}) {
|
|
243
|
-
return { ...dynamicVars, ...globals2, ...collectionVars, ...envVars, ...localVars };
|
|
244
|
-
}
|
|
245
82
|
function xmlFindAll(node, tag, nth) {
|
|
246
83
|
const results = [];
|
|
247
84
|
const siblings = Array.from(node.childNodes).filter((c) => c.nodeType === 1 && c.tagName === tag);
|
|
@@ -600,156 +437,6 @@ async function runScript(code, ctx, timeoutMs = 5e3) {
|
|
|
600
437
|
updatedLocalVars: localVarsCopy
|
|
601
438
|
};
|
|
602
439
|
}
|
|
603
|
-
async function buildAuthHeaders(auth, vars) {
|
|
604
|
-
const headers = {};
|
|
605
|
-
if (auth.type === "bearer") {
|
|
606
|
-
let token = auth.token ?? "";
|
|
607
|
-
if (!token && auth.tokenSecretRef) token = await getSecret(auth.tokenSecretRef) ?? "";
|
|
608
|
-
token = interpolate(token, vars);
|
|
609
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
610
|
-
}
|
|
611
|
-
if (auth.type === "basic") {
|
|
612
|
-
let password = auth.password ?? "";
|
|
613
|
-
if (!password && auth.passwordSecretRef) password = await getSecret(auth.passwordSecretRef) ?? "";
|
|
614
|
-
password = interpolate(password, vars);
|
|
615
|
-
const username = interpolate(auth.username ?? "", vars);
|
|
616
|
-
headers["Authorization"] = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
617
|
-
}
|
|
618
|
-
if (auth.type === "apikey" && auth.apiKeyIn === "header") {
|
|
619
|
-
let value = auth.apiKeyValue ?? "";
|
|
620
|
-
if (!value && auth.apiKeySecretRef) value = await getSecret(auth.apiKeySecretRef) ?? "";
|
|
621
|
-
value = interpolate(value, vars);
|
|
622
|
-
headers[auth.apiKeyName ?? "X-API-Key"] = value;
|
|
623
|
-
}
|
|
624
|
-
if (auth.type === "oauth2") {
|
|
625
|
-
const now = Date.now();
|
|
626
|
-
if (auth.oauth2CachedToken && auth.oauth2TokenExpiry && auth.oauth2TokenExpiry > now + 5e3) {
|
|
627
|
-
headers["Authorization"] = `Bearer ${auth.oauth2CachedToken}`;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
return headers;
|
|
631
|
-
}
|
|
632
|
-
async function buildApiKeyParam(auth, vars) {
|
|
633
|
-
if (auth.type !== "apikey" || auth.apiKeyIn !== "query") return null;
|
|
634
|
-
let value = auth.apiKeyValue ?? "";
|
|
635
|
-
if (!value && auth.apiKeySecretRef) value = await getSecret(auth.apiKeySecretRef) ?? "";
|
|
636
|
-
value = interpolate(value, vars);
|
|
637
|
-
return { key: auth.apiKeyName ?? "apikey", value };
|
|
638
|
-
}
|
|
639
|
-
function parseDigestChallenge(wwwAuth) {
|
|
640
|
-
const extract = (key) => {
|
|
641
|
-
const m = new RegExp(`${key}="([^"]*)"`, "i").exec(wwwAuth);
|
|
642
|
-
return m ? m[1] : "";
|
|
643
|
-
};
|
|
644
|
-
const extractUnquoted = (key) => {
|
|
645
|
-
const m = new RegExp(`${key}=([^,\\s]+)`, "i").exec(wwwAuth);
|
|
646
|
-
return m ? m[1] : "";
|
|
647
|
-
};
|
|
648
|
-
return {
|
|
649
|
-
realm: extract("realm"),
|
|
650
|
-
nonce: extract("nonce"),
|
|
651
|
-
qop: extract("qop") || extractUnquoted("qop") || void 0,
|
|
652
|
-
algorithm: extract("algorithm") || extractUnquoted("algorithm") || "MD5",
|
|
653
|
-
opaque: extract("opaque") || void 0
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
function md5(s) {
|
|
657
|
-
return crypto.createHash("md5").update(s).digest("hex");
|
|
658
|
-
}
|
|
659
|
-
function buildDigestAuthHeader(challenge, username, password, method, uri) {
|
|
660
|
-
const { realm, nonce, qop, algorithm, opaque } = challenge;
|
|
661
|
-
const algo = (algorithm ?? "MD5").toUpperCase();
|
|
662
|
-
const ha1 = algo === "MD5-SESS" ? md5(`${md5(`${username}:${realm}:${password}`)}:${nonce}:`) : md5(`${username}:${realm}:${password}`);
|
|
663
|
-
const ha2 = md5(`${method}:${uri}`);
|
|
664
|
-
let response;
|
|
665
|
-
let nc;
|
|
666
|
-
let cnonce;
|
|
667
|
-
if (qop === "auth" || qop === "auth-int") {
|
|
668
|
-
nc = "00000001";
|
|
669
|
-
cnonce = crypto.randomBytes(8).toString("hex");
|
|
670
|
-
response = md5(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`);
|
|
671
|
-
} else {
|
|
672
|
-
response = md5(`${ha1}:${nonce}:${ha2}`);
|
|
673
|
-
}
|
|
674
|
-
let header = `Digest username="${username}", realm="${realm}", nonce="${nonce}", uri="${uri}", response="${response}"`;
|
|
675
|
-
if (qop) header += `, qop=${qop}`;
|
|
676
|
-
if (nc) header += `, nc=${nc}`;
|
|
677
|
-
if (cnonce) header += `, cnonce="${cnonce}"`;
|
|
678
|
-
if (opaque) header += `, opaque="${opaque}"`;
|
|
679
|
-
if (algo !== "MD5") header += `, algorithm=${algo}`;
|
|
680
|
-
return header;
|
|
681
|
-
}
|
|
682
|
-
async function performDigestAuth(url, method, auth, vars, fetchFn) {
|
|
683
|
-
const probeResp = await fetchFn(url, { method, headers: {} });
|
|
684
|
-
if (probeResp.status !== 401) return null;
|
|
685
|
-
const wwwAuth = probeResp.headers.get("www-authenticate") ?? "";
|
|
686
|
-
if (!wwwAuth.toLowerCase().startsWith("digest")) return null;
|
|
687
|
-
const challenge = parseDigestChallenge(wwwAuth);
|
|
688
|
-
let password = auth.password ?? "";
|
|
689
|
-
if (!password && auth.passwordSecretRef) password = await getSecret(auth.passwordSecretRef) ?? "";
|
|
690
|
-
password = interpolate(password, vars);
|
|
691
|
-
const username = interpolate(auth.username ?? "", vars);
|
|
692
|
-
let uri = "/";
|
|
693
|
-
try {
|
|
694
|
-
uri = new URL(url).pathname + (new URL(url).search ?? "");
|
|
695
|
-
} catch {
|
|
696
|
-
}
|
|
697
|
-
return buildDigestAuthHeader(challenge, username, password, method, uri);
|
|
698
|
-
}
|
|
699
|
-
async function performNtlmRequest(_url, _method, _auth, _vars) {
|
|
700
|
-
throw new Error(
|
|
701
|
-
'NTLM auth is not yet implemented. Add "httpntlm" to package.json dependencies and implement performNtlmRequest in auth-builder.ts.'
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
async function fetchOAuth2Token(auth, vars) {
|
|
705
|
-
const flow = auth.oauth2Flow ?? "client_credentials";
|
|
706
|
-
if (flow === "authorization_code") {
|
|
707
|
-
throw new Error("authorization_code flow requires the oauth2:startFlow IPC call from the renderer.");
|
|
708
|
-
}
|
|
709
|
-
if (flow === "implicit") {
|
|
710
|
-
throw new Error("implicit flow cannot be performed server-side — tokens must be obtained via the browser redirect.");
|
|
711
|
-
}
|
|
712
|
-
const tokenUrl = interpolate(auth.oauth2TokenUrl ?? "", vars);
|
|
713
|
-
if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required.");
|
|
714
|
-
const clientId = interpolate(auth.oauth2ClientId ?? "", vars);
|
|
715
|
-
let clientSecret = auth.oauth2ClientSecret ?? "";
|
|
716
|
-
if (!clientSecret && auth.oauth2ClientSecretRef) {
|
|
717
|
-
clientSecret = await getSecret(auth.oauth2ClientSecretRef) ?? "";
|
|
718
|
-
}
|
|
719
|
-
clientSecret = interpolate(clientSecret, vars);
|
|
720
|
-
const params = new URLSearchParams();
|
|
721
|
-
params.set("grant_type", flow === "password" ? "password" : "client_credentials");
|
|
722
|
-
params.set("client_id", clientId);
|
|
723
|
-
params.set("client_secret", clientSecret);
|
|
724
|
-
if (auth.oauth2Scopes) params.set("scope", auth.oauth2Scopes);
|
|
725
|
-
if (flow === "password") {
|
|
726
|
-
let password = auth.password ?? "";
|
|
727
|
-
if (!password && auth.passwordSecretRef) password = await getSecret(auth.passwordSecretRef) ?? "";
|
|
728
|
-
password = interpolate(password, vars);
|
|
729
|
-
params.set("username", interpolate(auth.username ?? "", vars));
|
|
730
|
-
params.set("password", password);
|
|
731
|
-
}
|
|
732
|
-
const { fetch: nodeFetch } = await import("undici");
|
|
733
|
-
const resp = await nodeFetch(tokenUrl, {
|
|
734
|
-
method: "POST",
|
|
735
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
736
|
-
body: params.toString()
|
|
737
|
-
});
|
|
738
|
-
if (!resp.ok) {
|
|
739
|
-
const body = await resp.text();
|
|
740
|
-
throw new Error(`OAuth 2.0 token request failed (${resp.status}): ${body}`);
|
|
741
|
-
}
|
|
742
|
-
const json = await resp.json();
|
|
743
|
-
const accessToken = String(json["access_token"] ?? "");
|
|
744
|
-
if (!accessToken) throw new Error("OAuth 2.0: token response missing access_token.");
|
|
745
|
-
const expiresIn = Number(json["expires_in"] ?? 3600);
|
|
746
|
-
const expiresAt = Date.now() + expiresIn * 1e3;
|
|
747
|
-
return {
|
|
748
|
-
accessToken,
|
|
749
|
-
expiresAt,
|
|
750
|
-
refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
440
|
function buildProxyUri(proxy) {
|
|
754
441
|
const raw = proxy.url.trim();
|
|
755
442
|
if (!raw) throw new Error("Proxy URL is empty");
|
|
@@ -956,6 +643,7 @@ async function buildDispatcher(proxy, tls) {
|
|
|
956
643
|
}
|
|
957
644
|
function registerRequestHandler(ipc) {
|
|
958
645
|
ipc.handle("request:send", async (_e, payload) => {
|
|
646
|
+
ipcValidate.validateSendRequestPayload(payload);
|
|
959
647
|
const {
|
|
960
648
|
request: req,
|
|
961
649
|
environment,
|
|
@@ -972,7 +660,7 @@ function registerRequestHandler(ipc) {
|
|
|
972
660
|
const start = Date.now();
|
|
973
661
|
const liveGlobals = getGlobals();
|
|
974
662
|
const mergedGlobals = { ...payloadGlobals, ...liveGlobals };
|
|
975
|
-
const envVars = await buildEnvVars(environment);
|
|
663
|
+
const envVars = await authBuilder.buildEnvVars(environment);
|
|
976
664
|
let localVars = {};
|
|
977
665
|
const decryptionWarnings = [];
|
|
978
666
|
if (environment) {
|
|
@@ -986,14 +674,14 @@ function registerRequestHandler(ipc) {
|
|
|
986
674
|
}
|
|
987
675
|
}
|
|
988
676
|
}
|
|
989
|
-
const dynamicVars = await buildDynamicVars();
|
|
990
|
-
let vars = mergeVars(envVars, collectionVars, mergedGlobals, localVars, dynamicVars);
|
|
677
|
+
const dynamicVars = await authBuilder.buildDynamicVars();
|
|
678
|
+
let vars = authBuilder.mergeVars(envVars, collectionVars, mergedGlobals, localVars, dynamicVars);
|
|
991
679
|
let preScriptMeta = { consoleOutput: [] };
|
|
992
680
|
let updatedCollectionVars = { ...collectionVars };
|
|
993
681
|
let updatedEnvVars = { ...envVars };
|
|
994
682
|
let updatedGlobals = { ...mergedGlobals };
|
|
995
683
|
if (req.preRequestScript?.trim()) {
|
|
996
|
-
const result = await runScript(interpolate(req.preRequestScript, vars), {
|
|
684
|
+
const result = await runScript(authBuilder.interpolate(req.preRequestScript, vars), {
|
|
997
685
|
envVars: { ...envVars },
|
|
998
686
|
collectionVars: { ...collectionVars },
|
|
999
687
|
globals: { ...mergedGlobals },
|
|
@@ -1006,11 +694,11 @@ function registerRequestHandler(ipc) {
|
|
|
1006
694
|
updatedGlobals = result.updatedGlobals;
|
|
1007
695
|
patchGlobals(result.updatedGlobals);
|
|
1008
696
|
await persistGlobals();
|
|
1009
|
-
vars = mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
|
|
697
|
+
vars = authBuilder.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
|
|
1010
698
|
}
|
|
1011
699
|
let response;
|
|
1012
700
|
let sentRequest = { method: req.method, url: "", headers: {} };
|
|
1013
|
-
const resolvedUrl = buildUrl(req.url, req.params, vars);
|
|
701
|
+
const resolvedUrl = authBuilder.buildUrl(req.url, req.params, vars);
|
|
1014
702
|
const secretValues = /* @__PURE__ */ new Set();
|
|
1015
703
|
if (environment) {
|
|
1016
704
|
for (const v of environment.variables) {
|
|
@@ -1047,13 +735,13 @@ function registerRequestHandler(ipc) {
|
|
|
1047
735
|
const tokenMissing = !req.auth.oauth2CachedToken;
|
|
1048
736
|
const tokenExpired = req.auth.oauth2TokenExpiry ? req.auth.oauth2TokenExpiry <= now + 5e3 : true;
|
|
1049
737
|
if (tokenMissing || tokenExpired) {
|
|
1050
|
-
const result = await fetchOAuth2Token(req.auth, vars);
|
|
738
|
+
const result = await authBuilder.fetchOAuth2Token(req.auth, vars);
|
|
1051
739
|
req.auth.oauth2CachedToken = result.accessToken;
|
|
1052
740
|
req.auth.oauth2TokenExpiry = result.expiresAt;
|
|
1053
741
|
}
|
|
1054
742
|
}
|
|
1055
|
-
const authHeaders = await buildAuthHeaders(req.auth, vars);
|
|
1056
|
-
const apiKeyParam = await buildApiKeyParam(req.auth, vars);
|
|
743
|
+
const authHeaders = await authBuilder.buildAuthHeaders(req.auth, vars);
|
|
744
|
+
const apiKeyParam = await authBuilder.buildApiKeyParam(req.auth, vars);
|
|
1057
745
|
let finalUrl = resolvedUrl;
|
|
1058
746
|
if (apiKeyParam) {
|
|
1059
747
|
const sep = finalUrl.includes("?") ? "&" : "?";
|
|
@@ -1063,7 +751,7 @@ function registerRequestHandler(ipc) {
|
|
|
1063
751
|
const h = new undici.Headers();
|
|
1064
752
|
for (const header of req.headers) {
|
|
1065
753
|
if (header.enabled && header.key) {
|
|
1066
|
-
h.set(interpolate(header.key, vars), interpolate(header.value, vars));
|
|
754
|
+
h.set(authBuilder.interpolate(header.key, vars), authBuilder.interpolate(header.value, vars));
|
|
1067
755
|
}
|
|
1068
756
|
}
|
|
1069
757
|
for (const [k, v] of Object.entries(authHeaders)) h.set(k, v);
|
|
@@ -1071,18 +759,18 @@ function registerRequestHandler(ipc) {
|
|
|
1071
759
|
};
|
|
1072
760
|
let body;
|
|
1073
761
|
if (req.body.mode === "json" && req.body.json) {
|
|
1074
|
-
body = interpolate(req.body.json, vars);
|
|
762
|
+
body = authBuilder.interpolate(req.body.json, vars);
|
|
1075
763
|
} else if (req.body.mode === "form" && req.body.form) {
|
|
1076
|
-
body = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${encodeURIComponent(interpolate(p.key, vars))}=${encodeURIComponent(interpolate(p.value, vars))}`).join("&");
|
|
764
|
+
body = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${encodeURIComponent(authBuilder.interpolate(p.key, vars))}=${encodeURIComponent(authBuilder.interpolate(p.value, vars))}`).join("&");
|
|
1077
765
|
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
1078
|
-
body = interpolate(req.body.raw, vars);
|
|
766
|
+
body = authBuilder.interpolate(req.body.raw, vars);
|
|
1079
767
|
} else if (req.body.mode === "graphql" && req.body.graphql) {
|
|
1080
768
|
const gql = req.body.graphql;
|
|
1081
|
-
const gqlBody = { query: interpolate(gql.query, vars) };
|
|
769
|
+
const gqlBody = { query: authBuilder.interpolate(gql.query, vars) };
|
|
1082
770
|
const rawVars = gql.variables?.trim();
|
|
1083
771
|
if (rawVars) {
|
|
1084
772
|
try {
|
|
1085
|
-
gqlBody.variables = JSON.parse(interpolate(rawVars, vars));
|
|
773
|
+
gqlBody.variables = JSON.parse(authBuilder.interpolate(rawVars, vars));
|
|
1086
774
|
} catch {
|
|
1087
775
|
}
|
|
1088
776
|
}
|
|
@@ -1090,7 +778,7 @@ function registerRequestHandler(ipc) {
|
|
|
1090
778
|
body = JSON.stringify(gqlBody);
|
|
1091
779
|
} else if (req.body.mode === "soap" && req.body.soap) {
|
|
1092
780
|
const soap = req.body.soap;
|
|
1093
|
-
body = interpolate(soap.envelope, vars);
|
|
781
|
+
body = authBuilder.interpolate(soap.envelope, vars);
|
|
1094
782
|
}
|
|
1095
783
|
const methodHasBody = !["GET", "HEAD"].includes(req.method);
|
|
1096
784
|
const doFetch = async (overrideHeaders) => {
|
|
@@ -1120,7 +808,7 @@ function registerRequestHandler(ipc) {
|
|
|
1120
808
|
};
|
|
1121
809
|
let fetchResp;
|
|
1122
810
|
if (req.auth.type === "ntlm") {
|
|
1123
|
-
await performNtlmRequest(finalUrl, req.method, req.auth, vars);
|
|
811
|
+
await authBuilder.performNtlmRequest(finalUrl, req.method, req.auth, vars);
|
|
1124
812
|
fetchResp = await doFetch();
|
|
1125
813
|
} else if (req.auth.type === "digest") {
|
|
1126
814
|
const probeFetch = async (url, init) => {
|
|
@@ -1129,7 +817,7 @@ function registerRequestHandler(ipc) {
|
|
|
1129
817
|
dispatcher
|
|
1130
818
|
});
|
|
1131
819
|
};
|
|
1132
|
-
const digestHeader = await performDigestAuth(finalUrl, req.method, req.auth, vars, probeFetch);
|
|
820
|
+
const digestHeader = await authBuilder.performDigestAuth(finalUrl, req.method, req.auth, vars, probeFetch);
|
|
1133
821
|
const h = buildHeaders();
|
|
1134
822
|
if (digestHeader) h.set("Authorization", digestHeader);
|
|
1135
823
|
fetchResp = await doFetch(h);
|
|
@@ -1176,7 +864,7 @@ function registerRequestHandler(ipc) {
|
|
|
1176
864
|
let postConsole = [];
|
|
1177
865
|
let postError;
|
|
1178
866
|
if (req.postRequestScript?.trim() && !response.error) {
|
|
1179
|
-
const result = await runScript(interpolate(req.postRequestScript, vars), {
|
|
867
|
+
const result = await runScript(authBuilder.interpolate(req.postRequestScript, vars), {
|
|
1180
868
|
envVars: { ...updatedEnvVars },
|
|
1181
869
|
collectionVars: { ...updatedCollectionVars },
|
|
1182
870
|
globals: { ...updatedGlobals },
|
|
@@ -1291,29 +979,17 @@ function resolveInheritedAuthAndHeaders(requestId, collection) {
|
|
|
1291
979
|
}
|
|
1292
980
|
return { auth: inheritedAuth, headers: inheritedHeaders };
|
|
1293
981
|
}
|
|
1294
|
-
exports.buildAuthHeaders = buildAuthHeaders;
|
|
1295
982
|
exports.buildDispatcher = buildDispatcher;
|
|
1296
|
-
exports.buildDynamicVars = buildDynamicVars;
|
|
1297
|
-
exports.buildEnvVars = buildEnvVars;
|
|
1298
983
|
exports.buildSchemaTestResults = buildSchemaTestResults;
|
|
1299
|
-
exports.buildUrl = buildUrl;
|
|
1300
984
|
exports.collectTagged = collectTagged;
|
|
1301
|
-
exports.fetchOAuth2Token = fetchOAuth2Token;
|
|
1302
985
|
exports.getAllApplicableHooks = getAllApplicableHooks;
|
|
1303
986
|
exports.getGlobals = getGlobals;
|
|
1304
|
-
exports.getSecret = getSecret;
|
|
1305
|
-
exports.initSecretStore = initSecretStore;
|
|
1306
|
-
exports.interpolate = interpolate;
|
|
1307
987
|
exports.loadGlobals = loadGlobals;
|
|
1308
988
|
exports.maskHeaders = maskHeaders;
|
|
1309
989
|
exports.maskPii = maskPii;
|
|
1310
|
-
exports.mergeVars = mergeVars;
|
|
1311
990
|
exports.patchGlobals = patchGlobals;
|
|
1312
|
-
exports.performDigestAuth = performDigestAuth;
|
|
1313
|
-
exports.performNtlmRequest = performNtlmRequest;
|
|
1314
991
|
exports.persistGlobals = persistGlobals;
|
|
1315
992
|
exports.registerRequestHandler = registerRequestHandler;
|
|
1316
|
-
exports.registerSecretHandlers = registerSecretHandlers;
|
|
1317
993
|
exports.resolveInheritedAuthAndHeaders = resolveInheritedAuthAndHeaders;
|
|
1318
994
|
exports.runScript = runScript;
|
|
1319
995
|
exports.setGlobals = setGlobals;
|