@testsmith/api-spector 0.3.4 → 0.3.6
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 +11 -7
- package/out/main/agents.js +1 -1
- package/out/main/chunks/environments-iM3SUM-4.js +28 -0
- package/out/main/chunks/{handle-C0IQL-Vl.js → handle-BtRMtQJg.js} +3 -1
- package/out/main/chunks/{import-C5Ngq8hk.js → import-CZxL6J96.js} +4 -4
- package/out/main/chunks/{recorder-DFxJgn9c.js → recorder-0Ij921El.js} +1 -1
- package/out/main/chunks/request-collection-CBuPjBwa.js +118 -0
- package/out/main/chunks/{request-collection-8TOVNXE0.js → request-exec-DbNHbA8x.js} +602 -146
- package/out/main/chunks/snapshots-rV5tPwAd.js +1658 -0
- package/out/main/chunks/{soap-handler-B9x_YCtj.js → soap-handler-CqXVreNa.js} +3 -3
- package/out/main/contract.js +508 -91
- package/out/main/index.js +158 -87
- package/out/main/record.js +3 -3
- package/out/main/runner.js +23 -20
- package/out/main/wsdl.js +4 -4
- package/out/preload/index.js +5 -1
- package/out/renderer/assets/index-DWiG0dQo.css +2 -0
- package/out/renderer/assets/{index-BjjOh-E5.js → index-htBErIFG.js} +915 -176
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/readme.md +2 -2
- package/out/main/chunks/auth-builder-CUs9yzOF.js +0 -623
- package/out/main/chunks/snapshots-UFd3XgSS.js +0 -928
- package/out/renderer/assets/index-CjvjKHtF.css +0 -2
|
@@ -23,14 +23,15 @@ 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
|
|
27
|
-
const vm = require("vm");
|
|
26
|
+
const handle = require("./handle-BtRMtQJg.js");
|
|
28
27
|
const crypto = require("crypto");
|
|
28
|
+
const path = require("path");
|
|
29
29
|
const dayjs = require("dayjs");
|
|
30
|
+
const vm = require("vm");
|
|
30
31
|
const tv4 = require("tv4");
|
|
31
32
|
const jsonpathPlus = require("jsonpath-plus");
|
|
32
33
|
const xmldom = require("@xmldom/xmldom");
|
|
33
|
-
const
|
|
34
|
+
const http = require("http");
|
|
34
35
|
const Ajv = require("ajv");
|
|
35
36
|
function _interopNamespaceDefault(e) {
|
|
36
37
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
@@ -48,8 +49,8 @@ function _interopNamespaceDefault(e) {
|
|
|
48
49
|
n.default = e;
|
|
49
50
|
return Object.freeze(n);
|
|
50
51
|
}
|
|
51
|
-
const vm__namespace = /* @__PURE__ */ _interopNamespaceDefault(vm);
|
|
52
52
|
const crypto__namespace = /* @__PURE__ */ _interopNamespaceDefault(crypto);
|
|
53
|
+
const vm__namespace = /* @__PURE__ */ _interopNamespaceDefault(vm);
|
|
53
54
|
let globals = {};
|
|
54
55
|
let currentDir = null;
|
|
55
56
|
function globalsPath(dir) {
|
|
@@ -78,6 +79,171 @@ function setGlobals(next) {
|
|
|
78
79
|
function patchGlobals(patch) {
|
|
79
80
|
globals = { ...globals, ...patch };
|
|
80
81
|
}
|
|
82
|
+
const MASTER_KEY_ENV = "API_SPECTOR_MASTER_KEY";
|
|
83
|
+
let secretStore = {};
|
|
84
|
+
let secretStorePath = null;
|
|
85
|
+
async function initSecretStore(userDataPath) {
|
|
86
|
+
secretStorePath = path.join(userDataPath, "secrets.json");
|
|
87
|
+
try {
|
|
88
|
+
const raw = await promises.readFile(secretStorePath, "utf8");
|
|
89
|
+
secretStore = JSON.parse(raw);
|
|
90
|
+
} catch {
|
|
91
|
+
secretStore = {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function persistSecretStore() {
|
|
95
|
+
if (!secretStorePath) return;
|
|
96
|
+
await promises.writeFile(secretStorePath, JSON.stringify(secretStore, null, 2), "utf8");
|
|
97
|
+
}
|
|
98
|
+
function getSafeStorage() {
|
|
99
|
+
try {
|
|
100
|
+
const { safeStorage } = require("electron");
|
|
101
|
+
if (typeof safeStorage?.isEncryptionAvailable === "function") return safeStorage;
|
|
102
|
+
return null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function registerSecretHandlers(ipc) {
|
|
108
|
+
handle.handleIpc(ipc, handle.IPC.secret.checkMasterKey, () => {
|
|
109
|
+
return { set: Boolean(process.env[MASTER_KEY_ENV]) };
|
|
110
|
+
});
|
|
111
|
+
handle.handleIpc(ipc, handle.IPC.secret.setMasterKey, (_e, value) => {
|
|
112
|
+
process.env[MASTER_KEY_ENV] = value;
|
|
113
|
+
});
|
|
114
|
+
handle.handleIpc(ipc, handle.IPC.secret.set, async (_e, ref, value) => {
|
|
115
|
+
const ss = getSafeStorage();
|
|
116
|
+
if (!ss || !ss.isEncryptionAvailable()) {
|
|
117
|
+
throw new Error("OS encryption is not available - set the secret via environment variable instead");
|
|
118
|
+
}
|
|
119
|
+
secretStore[ref] = ss.encryptString(value).toString("base64");
|
|
120
|
+
await persistSecretStore();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function decryptSecret(encrypted, salt, iv, password) {
|
|
124
|
+
const saltBuf = Buffer.from(salt, "base64");
|
|
125
|
+
const ivBuf = Buffer.from(iv, "base64");
|
|
126
|
+
const encBuf = Buffer.from(encrypted, "base64");
|
|
127
|
+
const key = crypto.pbkdf2Sync(password, saltBuf, 1e5, 32, "sha256");
|
|
128
|
+
const authTag = encBuf.subarray(encBuf.length - 16);
|
|
129
|
+
const ciphertext = encBuf.subarray(0, encBuf.length - 16);
|
|
130
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, ivBuf);
|
|
131
|
+
decipher.setAuthTag(authTag);
|
|
132
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
133
|
+
}
|
|
134
|
+
async function getSecret(ref) {
|
|
135
|
+
const stored = secretStore[ref];
|
|
136
|
+
if (stored) {
|
|
137
|
+
const ss = getSafeStorage();
|
|
138
|
+
if (ss && ss.isEncryptionAvailable()) {
|
|
139
|
+
try {
|
|
140
|
+
return ss.decryptString(Buffer.from(stored, "base64"));
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return process.env[ref] ?? null;
|
|
146
|
+
}
|
|
147
|
+
let _fakerCache$1 = null;
|
|
148
|
+
async function getFaker$1() {
|
|
149
|
+
if (!_fakerCache$1) _fakerCache$1 = await import("@faker-js/faker");
|
|
150
|
+
return _fakerCache$1.faker;
|
|
151
|
+
}
|
|
152
|
+
let _exprContext = null;
|
|
153
|
+
async function buildDynamicVars() {
|
|
154
|
+
const faker = await getFaker$1();
|
|
155
|
+
const now = dayjs();
|
|
156
|
+
_exprContext = { faker, dayjs };
|
|
157
|
+
return {
|
|
158
|
+
$uuid: faker.string.uuid(),
|
|
159
|
+
$timestamp: String(Date.now()),
|
|
160
|
+
$isoTimestamp: now.toISOString(),
|
|
161
|
+
$randomInt: String(faker.number.int({ min: 0, max: 1e3 })),
|
|
162
|
+
$randomFloat: String(faker.number.float({ min: 0, max: 1e3, fractionDigits: 2 })),
|
|
163
|
+
$randomBoolean: String(faker.datatype.boolean()),
|
|
164
|
+
$randomEmail: faker.internet.email(),
|
|
165
|
+
$randomUsername: faker.internet.username(),
|
|
166
|
+
$randomPassword: faker.internet.password(),
|
|
167
|
+
$randomFullName: faker.person.fullName(),
|
|
168
|
+
$randomFirstName: faker.person.firstName(),
|
|
169
|
+
$randomLastName: faker.person.lastName(),
|
|
170
|
+
$randomWord: faker.lorem.word(),
|
|
171
|
+
$randomPhrase: faker.lorem.sentence(),
|
|
172
|
+
$randomUrl: faker.internet.url(),
|
|
173
|
+
$randomIp: faker.internet.ip(),
|
|
174
|
+
$randomHexColor: faker.color.rgb({ format: "hex", casing: "lower" })
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function interpolate(str, vars) {
|
|
178
|
+
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
179
|
+
const trimmed = key.trim();
|
|
180
|
+
if (trimmed in vars) return vars[trimmed];
|
|
181
|
+
if (_exprContext && (trimmed.includes(".") || trimmed.includes("("))) {
|
|
182
|
+
try {
|
|
183
|
+
const result = vm__namespace.runInNewContext(trimmed, _exprContext);
|
|
184
|
+
if (result !== void 0 && result !== null) return String(result);
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return match;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function buildUrl(baseUrl, params, vars) {
|
|
192
|
+
const templateTokens = /* @__PURE__ */ new Set();
|
|
193
|
+
baseUrl.replace(/\{\{([^}]+)\}\}/g, (_m, name) => {
|
|
194
|
+
templateTokens.add(String(name).trim());
|
|
195
|
+
return "";
|
|
196
|
+
});
|
|
197
|
+
const enabled = (params ?? []).filter((p) => p.enabled && p.key);
|
|
198
|
+
const pathRows = [];
|
|
199
|
+
const queryRows = [];
|
|
200
|
+
for (const p of enabled) {
|
|
201
|
+
const isPath = p.paramType === "path" || templateTokens.has(p.key);
|
|
202
|
+
if (isPath) pathRows.push(p);
|
|
203
|
+
else queryRows.push(p);
|
|
204
|
+
}
|
|
205
|
+
const mergedVars = pathRows.length ? {
|
|
206
|
+
...vars,
|
|
207
|
+
...Object.fromEntries(pathRows.map((p) => [p.key, interpolate(p.value, vars)]))
|
|
208
|
+
} : vars;
|
|
209
|
+
const url = interpolate(baseUrl, mergedVars);
|
|
210
|
+
if (!queryRows.length) return url;
|
|
211
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
212
|
+
const qs = queryRows.map((p) => `${encodeURIComponent(interpolate(p.key, vars))}=${encodeURIComponent(interpolate(p.value, vars))}`).join("&");
|
|
213
|
+
return url + sep + qs;
|
|
214
|
+
}
|
|
215
|
+
async function buildEnvVars(environment) {
|
|
216
|
+
const vars = {};
|
|
217
|
+
if (!environment) return vars;
|
|
218
|
+
const masterKey = process.env["API_SPECTOR_MASTER_KEY"];
|
|
219
|
+
for (const v of environment.variables) {
|
|
220
|
+
if (!v.enabled) continue;
|
|
221
|
+
if (v.envRef) {
|
|
222
|
+
const envValue = process.env[v.envRef];
|
|
223
|
+
if (envValue !== void 0) vars[v.key] = envValue;
|
|
224
|
+
} else if (v.secret && v.secretEncrypted && v.secretSalt && v.secretIv) {
|
|
225
|
+
if (masterKey) {
|
|
226
|
+
try {
|
|
227
|
+
vars[v.key] = decryptSecret(v.secretEncrypted, v.secretSalt, v.secretIv, masterKey);
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (vars[v.key] === void 0 && process.env[v.key] !== void 0) {
|
|
232
|
+
vars[v.key] = process.env[v.key];
|
|
233
|
+
}
|
|
234
|
+
} else if (v.secret) {
|
|
235
|
+
if (process.env[v.key] !== void 0) {
|
|
236
|
+
vars[v.key] = process.env[v.key];
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
vars[v.key] = v.value;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return vars;
|
|
243
|
+
}
|
|
244
|
+
function mergeVars(envVars, collectionVars, globals2, localVars = {}, dynamicVars = {}) {
|
|
245
|
+
return { ...dynamicVars, ...globals2, ...collectionVars, ...envVars, ...localVars };
|
|
246
|
+
}
|
|
81
247
|
function xmlFindAll(node, tag, nth) {
|
|
82
248
|
const results = [];
|
|
83
249
|
const siblings = Array.from(node.childNodes).filter((c) => c.nodeType === 1 && c.tagName === tag);
|
|
@@ -469,6 +635,404 @@ function normalizeProxyInput(input) {
|
|
|
469
635
|
function ensureScheme(value) {
|
|
470
636
|
return /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `http://${value}`;
|
|
471
637
|
}
|
|
638
|
+
const SIGNATURE = Buffer.from("NTLMSSP\0", "latin1");
|
|
639
|
+
const F_UNICODE = 1;
|
|
640
|
+
const F_OEM = 2;
|
|
641
|
+
const F_REQUEST_TARGET = 4;
|
|
642
|
+
const F_NTLM = 512;
|
|
643
|
+
const F_ALWAYS_SIGN = 32768;
|
|
644
|
+
const F_EXT_SESSION = 524288;
|
|
645
|
+
const TYPE1_FLAGS = F_UNICODE | F_OEM | F_REQUEST_TARGET | F_NTLM | F_ALWAYS_SIGN | F_EXT_SESSION;
|
|
646
|
+
const TYPE3_FLAGS = F_UNICODE | F_NTLM | F_ALWAYS_SIGN | F_EXT_SESSION;
|
|
647
|
+
function rotl(x, n) {
|
|
648
|
+
return (x << n | x >>> 32 - n) >>> 0;
|
|
649
|
+
}
|
|
650
|
+
function md4(msg) {
|
|
651
|
+
const len = msg.length;
|
|
652
|
+
const bitLen = len * 8;
|
|
653
|
+
const padLen = (56 - (len + 1) % 64 + 64) % 64;
|
|
654
|
+
const total = len + 1 + padLen + 8;
|
|
655
|
+
const buf = Buffer.alloc(total);
|
|
656
|
+
msg.copy(buf, 0);
|
|
657
|
+
buf[len] = 128;
|
|
658
|
+
buf.writeUInt32LE(bitLen >>> 0, total - 8);
|
|
659
|
+
buf.writeUInt32LE(Math.floor(bitLen / 4294967296) >>> 0, total - 4);
|
|
660
|
+
let a = 1732584193, b = 4023233417, c = 2562383102, d = 271733878;
|
|
661
|
+
const X = new Array(16);
|
|
662
|
+
const F = (x, y, z) => x & y | ~x & z;
|
|
663
|
+
const G = (x, y, z) => x & y | x & z | y & z;
|
|
664
|
+
const H = (x, y, z) => x ^ y ^ z;
|
|
665
|
+
const FF = (aa, bb, cc, dd, k, s) => rotl(aa + F(bb, cc, dd) + X[k] >>> 0, s);
|
|
666
|
+
const GG = (aa, bb, cc, dd, k, s) => rotl(aa + G(bb, cc, dd) + X[k] + 1518500249 >>> 0, s);
|
|
667
|
+
const HH = (aa, bb, cc, dd, k, s) => rotl(aa + H(bb, cc, dd) + X[k] + 1859775393 >>> 0, s);
|
|
668
|
+
for (let i = 0; i < total; i += 64) {
|
|
669
|
+
for (let j = 0; j < 16; j++) X[j] = buf.readUInt32LE(i + j * 4);
|
|
670
|
+
const aa = a, bb = b, cc = c, dd = d;
|
|
671
|
+
for (let k = 0; k < 16; k += 4) {
|
|
672
|
+
a = FF(a, b, c, d, k, 3);
|
|
673
|
+
d = FF(d, a, b, c, k + 1, 7);
|
|
674
|
+
c = FF(c, d, a, b, k + 2, 11);
|
|
675
|
+
b = FF(b, c, d, a, k + 3, 19);
|
|
676
|
+
}
|
|
677
|
+
for (let k = 0; k < 4; k++) {
|
|
678
|
+
a = GG(a, b, c, d, k, 3);
|
|
679
|
+
d = GG(d, a, b, c, k + 4, 5);
|
|
680
|
+
c = GG(c, d, a, b, k + 8, 9);
|
|
681
|
+
b = GG(b, c, d, a, k + 12, 13);
|
|
682
|
+
}
|
|
683
|
+
const order = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15];
|
|
684
|
+
for (let k = 0; k < 16; k += 4) {
|
|
685
|
+
a = HH(a, b, c, d, order[k], 3);
|
|
686
|
+
d = HH(d, a, b, c, order[k + 1], 9);
|
|
687
|
+
c = HH(c, d, a, b, order[k + 2], 11);
|
|
688
|
+
b = HH(b, c, d, a, order[k + 3], 15);
|
|
689
|
+
}
|
|
690
|
+
a = a + aa >>> 0;
|
|
691
|
+
b = b + bb >>> 0;
|
|
692
|
+
c = c + cc >>> 0;
|
|
693
|
+
d = d + dd >>> 0;
|
|
694
|
+
}
|
|
695
|
+
const out = Buffer.alloc(16);
|
|
696
|
+
out.writeUInt32LE(a, 0);
|
|
697
|
+
out.writeUInt32LE(b, 4);
|
|
698
|
+
out.writeUInt32LE(c, 8);
|
|
699
|
+
out.writeUInt32LE(d, 12);
|
|
700
|
+
return out;
|
|
701
|
+
}
|
|
702
|
+
const utf16le = (s) => Buffer.from(s, "utf16le");
|
|
703
|
+
const hmacMd5 = (key, data) => crypto.createHmac("md5", key).update(data).digest();
|
|
704
|
+
function ntowfv2(user, domain, password) {
|
|
705
|
+
const ntHash = md4(utf16le(password));
|
|
706
|
+
return hmacMd5(ntHash, utf16le(user.toUpperCase() + domain));
|
|
707
|
+
}
|
|
708
|
+
function filetime(unixMs) {
|
|
709
|
+
const ticks = (BigInt(unixMs) + 11644473600000n) * 10000n;
|
|
710
|
+
const buf = Buffer.alloc(8);
|
|
711
|
+
buf.writeBigUInt64LE(ticks);
|
|
712
|
+
return buf;
|
|
713
|
+
}
|
|
714
|
+
function createType1Message() {
|
|
715
|
+
const msg = Buffer.alloc(32);
|
|
716
|
+
SIGNATURE.copy(msg, 0);
|
|
717
|
+
msg.writeUInt32LE(1, 8);
|
|
718
|
+
msg.writeUInt32LE(TYPE1_FLAGS, 12);
|
|
719
|
+
msg.writeUInt32LE(32, 16);
|
|
720
|
+
msg.writeUInt32LE(32, 24);
|
|
721
|
+
return msg.toString("base64");
|
|
722
|
+
}
|
|
723
|
+
function decodeType2Message(token) {
|
|
724
|
+
const buf = Buffer.isBuffer(token) ? token : Buffer.from(token, "base64");
|
|
725
|
+
if (buf.length < 32 || !buf.subarray(0, 8).equals(SIGNATURE)) {
|
|
726
|
+
throw new Error("Invalid NTLM Type 2 message");
|
|
727
|
+
}
|
|
728
|
+
const flags = buf.readUInt32LE(20);
|
|
729
|
+
const serverChallenge = Buffer.from(buf.subarray(24, 32));
|
|
730
|
+
let targetInfo = Buffer.alloc(0);
|
|
731
|
+
if (buf.length >= 48) {
|
|
732
|
+
const tiLen = buf.readUInt16LE(40);
|
|
733
|
+
const tiOff = buf.readUInt32LE(44);
|
|
734
|
+
if (tiLen > 0 && tiOff + tiLen <= buf.length) {
|
|
735
|
+
targetInfo = Buffer.from(buf.subarray(tiOff, tiOff + tiLen));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return { serverChallenge, targetInfo, flags };
|
|
739
|
+
}
|
|
740
|
+
function computeNtlmV2Response(opts) {
|
|
741
|
+
const responseKey = ntowfv2(opts.user, opts.domain, opts.password);
|
|
742
|
+
const blob = Buffer.concat([
|
|
743
|
+
Buffer.from([1, 1, 0, 0]),
|
|
744
|
+
// RespType + HiRespType + reserved
|
|
745
|
+
Buffer.from([0, 0, 0, 0]),
|
|
746
|
+
opts.timestamp,
|
|
747
|
+
opts.clientChallenge,
|
|
748
|
+
Buffer.from([0, 0, 0, 0]),
|
|
749
|
+
opts.targetInfo,
|
|
750
|
+
Buffer.from([0, 0, 0, 0])
|
|
751
|
+
]);
|
|
752
|
+
const ntProof = hmacMd5(responseKey, Buffer.concat([opts.serverChallenge, blob]));
|
|
753
|
+
const ntResponse = Buffer.concat([ntProof, blob]);
|
|
754
|
+
const lmProof = hmacMd5(responseKey, Buffer.concat([opts.serverChallenge, opts.clientChallenge]));
|
|
755
|
+
const lmResponse = Buffer.concat([lmProof, opts.clientChallenge]);
|
|
756
|
+
return { ntResponse, lmResponse, ntProof };
|
|
757
|
+
}
|
|
758
|
+
function createType3Message(opts) {
|
|
759
|
+
const domain = opts.domain ?? "";
|
|
760
|
+
const workstation = opts.workstation ?? "";
|
|
761
|
+
const clientChallenge = opts.clientChallenge ?? crypto.randomBytes(8);
|
|
762
|
+
const timestamp = filetime(opts.timestamp ?? Date.now());
|
|
763
|
+
const { ntResponse, lmResponse } = computeNtlmV2Response({
|
|
764
|
+
user: opts.user,
|
|
765
|
+
domain,
|
|
766
|
+
password: opts.password,
|
|
767
|
+
serverChallenge: opts.challenge.serverChallenge,
|
|
768
|
+
targetInfo: opts.challenge.targetInfo,
|
|
769
|
+
clientChallenge,
|
|
770
|
+
timestamp
|
|
771
|
+
});
|
|
772
|
+
const domainBuf = utf16le(domain);
|
|
773
|
+
const userBuf = utf16le(opts.user);
|
|
774
|
+
const wsBuf = utf16le(workstation);
|
|
775
|
+
const HEADER = 64;
|
|
776
|
+
const payload = Buffer.concat([lmResponse, ntResponse, domainBuf, userBuf, wsBuf]);
|
|
777
|
+
const msg = Buffer.alloc(HEADER + payload.length);
|
|
778
|
+
SIGNATURE.copy(msg, 0);
|
|
779
|
+
msg.writeUInt32LE(3, 8);
|
|
780
|
+
let off = HEADER;
|
|
781
|
+
const writeField = (pos, buf) => {
|
|
782
|
+
msg.writeUInt16LE(buf.length, pos);
|
|
783
|
+
msg.writeUInt16LE(buf.length, pos + 2);
|
|
784
|
+
msg.writeUInt32LE(buf.length ? off : HEADER, pos + 4);
|
|
785
|
+
buf.copy(msg, off);
|
|
786
|
+
off += buf.length;
|
|
787
|
+
};
|
|
788
|
+
writeField(12, lmResponse);
|
|
789
|
+
writeField(20, ntResponse);
|
|
790
|
+
writeField(28, domainBuf);
|
|
791
|
+
writeField(36, userBuf);
|
|
792
|
+
writeField(44, wsBuf);
|
|
793
|
+
msg.writeUInt16LE(0, 52);
|
|
794
|
+
msg.writeUInt16LE(0, 54);
|
|
795
|
+
msg.writeUInt32LE(HEADER, 56);
|
|
796
|
+
msg.writeUInt32LE(TYPE3_FLAGS, 60);
|
|
797
|
+
return msg.toString("base64");
|
|
798
|
+
}
|
|
799
|
+
async function buildAuthHeaders(auth, vars) {
|
|
800
|
+
const headers = {};
|
|
801
|
+
if (auth.type === "bearer") {
|
|
802
|
+
let token = auth.token ?? "";
|
|
803
|
+
if (!token && auth.tokenSecretRef) token = await getSecret(auth.tokenSecretRef) ?? "";
|
|
804
|
+
token = interpolate(token, vars);
|
|
805
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
806
|
+
}
|
|
807
|
+
if (auth.type === "basic") {
|
|
808
|
+
let password = auth.password ?? "";
|
|
809
|
+
if (!password && auth.passwordSecretRef) password = await getSecret(auth.passwordSecretRef) ?? "";
|
|
810
|
+
password = interpolate(password, vars);
|
|
811
|
+
const username = interpolate(auth.username ?? "", vars);
|
|
812
|
+
headers["Authorization"] = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
813
|
+
}
|
|
814
|
+
if (auth.type === "apikey" && auth.apiKeyIn === "header") {
|
|
815
|
+
let value = auth.apiKeyValue ?? "";
|
|
816
|
+
if (!value && auth.apiKeySecretRef) value = await getSecret(auth.apiKeySecretRef) ?? "";
|
|
817
|
+
value = interpolate(value, vars);
|
|
818
|
+
headers[auth.apiKeyName ?? "X-API-Key"] = value;
|
|
819
|
+
}
|
|
820
|
+
if (auth.type === "oauth2") {
|
|
821
|
+
const now = Date.now();
|
|
822
|
+
if (auth.oauth2CachedToken && auth.oauth2TokenExpiry && auth.oauth2TokenExpiry > now + 5e3) {
|
|
823
|
+
headers["Authorization"] = `Bearer ${auth.oauth2CachedToken}`;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return headers;
|
|
827
|
+
}
|
|
828
|
+
async function buildApiKeyParam(auth, vars) {
|
|
829
|
+
if (auth.type !== "apikey" || auth.apiKeyIn !== "query") return null;
|
|
830
|
+
let value = auth.apiKeyValue ?? "";
|
|
831
|
+
if (!value && auth.apiKeySecretRef) value = await getSecret(auth.apiKeySecretRef) ?? "";
|
|
832
|
+
value = interpolate(value, vars);
|
|
833
|
+
return { key: auth.apiKeyName ?? "apikey", value };
|
|
834
|
+
}
|
|
835
|
+
function parseDigestChallenge(wwwAuth) {
|
|
836
|
+
const extract = (key) => {
|
|
837
|
+
const m = new RegExp(`${key}="([^"]*)"`, "i").exec(wwwAuth);
|
|
838
|
+
return m ? m[1] : "";
|
|
839
|
+
};
|
|
840
|
+
const extractUnquoted = (key) => {
|
|
841
|
+
const m = new RegExp(`${key}=([^,\\s]+)`, "i").exec(wwwAuth);
|
|
842
|
+
return m ? m[1] : "";
|
|
843
|
+
};
|
|
844
|
+
return {
|
|
845
|
+
realm: extract("realm"),
|
|
846
|
+
nonce: extract("nonce"),
|
|
847
|
+
qop: extract("qop") || extractUnquoted("qop") || void 0,
|
|
848
|
+
algorithm: extract("algorithm") || extractUnquoted("algorithm") || "MD5",
|
|
849
|
+
opaque: extract("opaque") || void 0
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function md5(s) {
|
|
853
|
+
return crypto.createHash("md5").update(s).digest("hex");
|
|
854
|
+
}
|
|
855
|
+
function buildDigestAuthHeader(challenge, username, password, method, uri) {
|
|
856
|
+
const { realm, nonce, qop, algorithm, opaque } = challenge;
|
|
857
|
+
const algo = (algorithm ?? "MD5").toUpperCase();
|
|
858
|
+
const ha1 = algo === "MD5-SESS" ? md5(`${md5(`${username}:${realm}:${password}`)}:${nonce}:`) : md5(`${username}:${realm}:${password}`);
|
|
859
|
+
const ha2 = md5(`${method}:${uri}`);
|
|
860
|
+
let response;
|
|
861
|
+
let nc;
|
|
862
|
+
let cnonce;
|
|
863
|
+
if (qop === "auth" || qop === "auth-int") {
|
|
864
|
+
nc = "00000001";
|
|
865
|
+
cnonce = crypto.randomBytes(8).toString("hex");
|
|
866
|
+
response = md5(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`);
|
|
867
|
+
} else {
|
|
868
|
+
response = md5(`${ha1}:${nonce}:${ha2}`);
|
|
869
|
+
}
|
|
870
|
+
let header = `Digest username="${username}", realm="${realm}", nonce="${nonce}", uri="${uri}", response="${response}"`;
|
|
871
|
+
if (qop) header += `, qop=${qop}`;
|
|
872
|
+
if (nc) header += `, nc=${nc}`;
|
|
873
|
+
if (cnonce) header += `, cnonce="${cnonce}"`;
|
|
874
|
+
if (opaque) header += `, opaque="${opaque}"`;
|
|
875
|
+
if (algo !== "MD5") header += `, algorithm=${algo}`;
|
|
876
|
+
return header;
|
|
877
|
+
}
|
|
878
|
+
async function performDigestAuth(url, method, auth, vars, fetchFn) {
|
|
879
|
+
const probeResp = await fetchFn(url, { method, headers: {} });
|
|
880
|
+
if (probeResp.status !== 401) return null;
|
|
881
|
+
const wwwAuth = probeResp.headers.get("www-authenticate") ?? "";
|
|
882
|
+
if (!wwwAuth.toLowerCase().startsWith("digest")) return null;
|
|
883
|
+
const challenge = parseDigestChallenge(wwwAuth);
|
|
884
|
+
let password = auth.password ?? "";
|
|
885
|
+
if (!password && auth.passwordSecretRef) password = await getSecret(auth.passwordSecretRef) ?? "";
|
|
886
|
+
password = interpolate(password, vars);
|
|
887
|
+
const username = interpolate(auth.username ?? "", vars);
|
|
888
|
+
let uri = "/";
|
|
889
|
+
try {
|
|
890
|
+
uri = new URL(url).pathname + (new URL(url).search ?? "");
|
|
891
|
+
} catch {
|
|
892
|
+
}
|
|
893
|
+
return buildDigestAuthHeader(challenge, username, password, method, uri);
|
|
894
|
+
}
|
|
895
|
+
function extractNtlmChallenge(value) {
|
|
896
|
+
if (!value) return null;
|
|
897
|
+
const values = Array.isArray(value) ? value : [value];
|
|
898
|
+
for (const v of values) {
|
|
899
|
+
for (const part of v.split(",")) {
|
|
900
|
+
const m = /^\s*NTLM\s+(.+)\s*$/i.exec(part);
|
|
901
|
+
if (m) return m[1].trim();
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
async function buildNtlmConnectOpts(tls) {
|
|
907
|
+
if (!tls) return void 0;
|
|
908
|
+
const connect = {};
|
|
909
|
+
if (tls.rejectUnauthorized !== void 0) connect["rejectUnauthorized"] = tls.rejectUnauthorized;
|
|
910
|
+
if (tls.caCertPath) {
|
|
911
|
+
try {
|
|
912
|
+
connect["ca"] = await promises.readFile(tls.caCertPath);
|
|
913
|
+
} catch {
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (tls.clientCertPath) {
|
|
917
|
+
try {
|
|
918
|
+
connect["cert"] = await promises.readFile(tls.clientCertPath);
|
|
919
|
+
} catch {
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (tls.clientKeyPath) {
|
|
923
|
+
try {
|
|
924
|
+
connect["key"] = await promises.readFile(tls.clientKeyPath);
|
|
925
|
+
} catch {
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return Object.keys(connect).length ? connect : void 0;
|
|
929
|
+
}
|
|
930
|
+
async function performNtlmRequest(opts) {
|
|
931
|
+
if (opts.proxy?.url) {
|
|
932
|
+
throw new Error("NTLM authentication through a proxy is not supported. Disable the proxy for this request, or target the server directly.");
|
|
933
|
+
}
|
|
934
|
+
const { Client } = await import("undici");
|
|
935
|
+
let password = opts.auth.password ?? "";
|
|
936
|
+
if (!password && opts.auth.passwordSecretRef) password = await getSecret(opts.auth.passwordSecretRef) ?? "";
|
|
937
|
+
password = interpolate(password, opts.vars);
|
|
938
|
+
const username = interpolate(opts.auth.username ?? "", opts.vars);
|
|
939
|
+
const domain = interpolate(opts.auth.ntlmDomain ?? "", opts.vars);
|
|
940
|
+
const workstation = interpolate(opts.auth.ntlmWorkstation ?? "", opts.vars);
|
|
941
|
+
const parsed = new URL(opts.url);
|
|
942
|
+
const origin = `${parsed.protocol}//${parsed.host}`;
|
|
943
|
+
const path2 = parsed.pathname + parsed.search;
|
|
944
|
+
const connect = await buildNtlmConnectOpts(opts.tls);
|
|
945
|
+
const client = new Client(origin, { pipelining: 1, ...connect ? { connect } : {} });
|
|
946
|
+
const toAdapter = (statusCode, headers, bodyText) => {
|
|
947
|
+
const entries = [];
|
|
948
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
949
|
+
if (v === void 0) continue;
|
|
950
|
+
entries.push([k, Array.isArray(v) ? v.join(", ") : String(v)]);
|
|
951
|
+
}
|
|
952
|
+
return {
|
|
953
|
+
status: statusCode,
|
|
954
|
+
statusText: http.STATUS_CODES[statusCode] ?? "",
|
|
955
|
+
headers: { forEach: (cb) => entries.forEach(([k, v]) => cb(v, k)) },
|
|
956
|
+
text: () => Promise.resolve(bodyText)
|
|
957
|
+
};
|
|
958
|
+
};
|
|
959
|
+
try {
|
|
960
|
+
const negotiate = await client.request({
|
|
961
|
+
path: path2,
|
|
962
|
+
method: opts.method,
|
|
963
|
+
headers: { authorization: `NTLM ${createType1Message()}` }
|
|
964
|
+
});
|
|
965
|
+
await negotiate.body.text();
|
|
966
|
+
const challengeToken = extractNtlmChallenge(negotiate.headers["www-authenticate"]);
|
|
967
|
+
if (negotiate.statusCode !== 401 || !challengeToken) {
|
|
968
|
+
const second = await client.request({ path: path2, method: opts.method, headers: opts.baseHeaders, body: opts.body });
|
|
969
|
+
const bodyText2 = await second.body.text();
|
|
970
|
+
return toAdapter(second.statusCode, second.headers, bodyText2);
|
|
971
|
+
}
|
|
972
|
+
const challenge = decodeType2Message(challengeToken);
|
|
973
|
+
const type3 = createType3Message({ user: username, password, domain, workstation, challenge });
|
|
974
|
+
const authed = await client.request({
|
|
975
|
+
path: path2,
|
|
976
|
+
method: opts.method,
|
|
977
|
+
headers: { ...opts.baseHeaders, authorization: `NTLM ${type3}` },
|
|
978
|
+
body: opts.body
|
|
979
|
+
});
|
|
980
|
+
const bodyText = await authed.body.text();
|
|
981
|
+
return toAdapter(authed.statusCode, authed.headers, bodyText);
|
|
982
|
+
} finally {
|
|
983
|
+
await client.close().catch(() => {
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
async function fetchOAuth2Token(auth, vars) {
|
|
988
|
+
const flow = auth.oauth2Flow ?? "client_credentials";
|
|
989
|
+
if (flow === "authorization_code") {
|
|
990
|
+
throw new Error("authorization_code flow requires the oauth2:startFlow IPC call from the renderer.");
|
|
991
|
+
}
|
|
992
|
+
if (flow === "implicit") {
|
|
993
|
+
throw new Error("implicit flow cannot be performed server-side - tokens must be obtained via the browser redirect.");
|
|
994
|
+
}
|
|
995
|
+
const tokenUrl = interpolate(auth.oauth2TokenUrl ?? "", vars);
|
|
996
|
+
if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required.");
|
|
997
|
+
const clientId = interpolate(auth.oauth2ClientId ?? "", vars);
|
|
998
|
+
let clientSecret = auth.oauth2ClientSecret ?? "";
|
|
999
|
+
if (!clientSecret && auth.oauth2ClientSecretRef) {
|
|
1000
|
+
clientSecret = await getSecret(auth.oauth2ClientSecretRef) ?? "";
|
|
1001
|
+
}
|
|
1002
|
+
clientSecret = interpolate(clientSecret, vars);
|
|
1003
|
+
const params = new URLSearchParams();
|
|
1004
|
+
params.set("grant_type", flow === "password" ? "password" : "client_credentials");
|
|
1005
|
+
params.set("client_id", clientId);
|
|
1006
|
+
params.set("client_secret", clientSecret);
|
|
1007
|
+
if (auth.oauth2Scopes) params.set("scope", auth.oauth2Scopes);
|
|
1008
|
+
if (flow === "password") {
|
|
1009
|
+
let password = auth.password ?? "";
|
|
1010
|
+
if (!password && auth.passwordSecretRef) password = await getSecret(auth.passwordSecretRef) ?? "";
|
|
1011
|
+
password = interpolate(password, vars);
|
|
1012
|
+
params.set("username", interpolate(auth.username ?? "", vars));
|
|
1013
|
+
params.set("password", password);
|
|
1014
|
+
}
|
|
1015
|
+
const { fetch: nodeFetch } = await import("undici");
|
|
1016
|
+
const resp = await nodeFetch(tokenUrl, {
|
|
1017
|
+
method: "POST",
|
|
1018
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1019
|
+
body: params.toString()
|
|
1020
|
+
});
|
|
1021
|
+
if (!resp.ok) {
|
|
1022
|
+
const body = await resp.text();
|
|
1023
|
+
throw new Error(`OAuth 2.0 token request failed (${resp.status}): ${body}`);
|
|
1024
|
+
}
|
|
1025
|
+
const json = await resp.json();
|
|
1026
|
+
const accessToken = String(json["access_token"] ?? "");
|
|
1027
|
+
if (!accessToken) throw new Error("OAuth 2.0: token response missing access_token.");
|
|
1028
|
+
const expiresIn = Number(json["expires_in"] ?? 3600);
|
|
1029
|
+
const expiresAt = Date.now() + expiresIn * 1e3;
|
|
1030
|
+
return {
|
|
1031
|
+
accessToken,
|
|
1032
|
+
expiresAt,
|
|
1033
|
+
refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
472
1036
|
function maskPii(data, patterns) {
|
|
473
1037
|
if (!patterns.length) return data;
|
|
474
1038
|
try {
|
|
@@ -529,7 +1093,7 @@ function buildSchemaTestResults(schemaText, body) {
|
|
|
529
1093
|
return [{
|
|
530
1094
|
name: "[schema] body matches schema",
|
|
531
1095
|
passed: false,
|
|
532
|
-
error: "Response body is not valid JSON
|
|
1096
|
+
error: "Response body is not valid JSON - cannot validate against schema"
|
|
533
1097
|
}];
|
|
534
1098
|
}
|
|
535
1099
|
let validate;
|
|
@@ -633,21 +1197,21 @@ async function buildDispatcher(proxy, tls) {
|
|
|
633
1197
|
function buildBodyAndApplyHeaders(req, vars, headers) {
|
|
634
1198
|
let body;
|
|
635
1199
|
if (req.body.mode === "json" && req.body.json) {
|
|
636
|
-
body =
|
|
1200
|
+
body = interpolate(req.body.json, vars);
|
|
637
1201
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
638
1202
|
} else if (req.body.mode === "form" && req.body.form) {
|
|
639
|
-
body = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${encodeURIComponent(
|
|
1203
|
+
body = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${encodeURIComponent(interpolate(p.key, vars))}=${encodeURIComponent(interpolate(p.value, vars))}`).join("&");
|
|
640
1204
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/x-www-form-urlencoded");
|
|
641
1205
|
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
642
|
-
body =
|
|
1206
|
+
body = interpolate(req.body.raw, vars);
|
|
643
1207
|
if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
|
|
644
1208
|
} else if (req.body.mode === "graphql" && req.body.graphql) {
|
|
645
1209
|
const gql = req.body.graphql;
|
|
646
|
-
const gqlBody = { query:
|
|
1210
|
+
const gqlBody = { query: interpolate(gql.query, vars) };
|
|
647
1211
|
const rawVars = gql.variables?.trim();
|
|
648
1212
|
if (rawVars) {
|
|
649
1213
|
try {
|
|
650
|
-
gqlBody.variables = JSON.parse(
|
|
1214
|
+
gqlBody.variables = JSON.parse(interpolate(rawVars, vars));
|
|
651
1215
|
} catch {
|
|
652
1216
|
}
|
|
653
1217
|
}
|
|
@@ -655,7 +1219,7 @@ function buildBodyAndApplyHeaders(req, vars, headers) {
|
|
|
655
1219
|
body = JSON.stringify(gqlBody);
|
|
656
1220
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
657
1221
|
} else if (req.body.mode === "soap" && req.body.soap) {
|
|
658
|
-
body =
|
|
1222
|
+
body = interpolate(req.body.soap.envelope, vars);
|
|
659
1223
|
if (!headers.has("content-type")) headers.set("Content-Type", "text/xml; charset=utf-8");
|
|
660
1224
|
if (req.body.soap.soapAction && !headers.has("soapaction")) {
|
|
661
1225
|
headers.set("SOAPAction", req.body.soap.soapAction);
|
|
@@ -671,13 +1235,13 @@ async function performHttpExchange(opts) {
|
|
|
671
1235
|
const tokenMissing = !req.auth.oauth2CachedToken;
|
|
672
1236
|
const tokenExpired = req.auth.oauth2TokenExpiry ? req.auth.oauth2TokenExpiry <= now + 5e3 : true;
|
|
673
1237
|
if (tokenMissing || tokenExpired) {
|
|
674
|
-
const result = await
|
|
1238
|
+
const result = await fetchOAuth2Token(req.auth, vars);
|
|
675
1239
|
req.auth.oauth2CachedToken = result.accessToken;
|
|
676
1240
|
req.auth.oauth2TokenExpiry = result.expiresAt;
|
|
677
1241
|
}
|
|
678
1242
|
}
|
|
679
|
-
const authHeaders = await
|
|
680
|
-
const apiKeyParam = await
|
|
1243
|
+
const authHeaders = await buildAuthHeaders(req.auth, vars);
|
|
1244
|
+
const apiKeyParam = await buildApiKeyParam(req.auth, vars);
|
|
681
1245
|
let finalUrl = resolvedUrl;
|
|
682
1246
|
if (apiKeyParam) {
|
|
683
1247
|
const sep = finalUrl.includes("?") ? "&" : "?";
|
|
@@ -685,7 +1249,7 @@ async function performHttpExchange(opts) {
|
|
|
685
1249
|
}
|
|
686
1250
|
const headers = new undici.Headers();
|
|
687
1251
|
for (const h of req.headers) {
|
|
688
|
-
if (h.enabled && h.key) headers.set(
|
|
1252
|
+
if (h.enabled && h.key) headers.set(interpolate(h.key, vars), interpolate(h.value, vars));
|
|
689
1253
|
}
|
|
690
1254
|
for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
|
|
691
1255
|
const body = buildBodyAndApplyHeaders(req, vars, headers);
|
|
@@ -709,7 +1273,7 @@ async function performHttpExchange(opts) {
|
|
|
709
1273
|
let fetchResp;
|
|
710
1274
|
if (req.auth.type === "ntlm") {
|
|
711
1275
|
sentHeaders = captureSent(headers);
|
|
712
|
-
fetchResp = await
|
|
1276
|
+
fetchResp = await performNtlmRequest({
|
|
713
1277
|
url: finalUrl,
|
|
714
1278
|
method: req.method,
|
|
715
1279
|
auth: req.auth,
|
|
@@ -724,7 +1288,7 @@ async function performHttpExchange(opts) {
|
|
|
724
1288
|
...init,
|
|
725
1289
|
dispatcher
|
|
726
1290
|
});
|
|
727
|
-
const digestHeader = await
|
|
1291
|
+
const digestHeader = await performDigestAuth(finalUrl, req.method, req.auth, vars, probeFetch);
|
|
728
1292
|
if (digestHeader) headers.set("Authorization", digestHeader);
|
|
729
1293
|
sentHeaders = captureSent(headers);
|
|
730
1294
|
fetchResp = await doFetch(headers);
|
|
@@ -758,7 +1322,7 @@ function syntheticHttpFailure(status, statusText) {
|
|
|
758
1322
|
return {
|
|
759
1323
|
name: `HTTP status ${status} ${statusText}`.trim(),
|
|
760
1324
|
passed: false,
|
|
761
|
-
error: `Request returned ${status}
|
|
1325
|
+
error: `Request returned ${status} - no assertion was defined to verify the status code.`
|
|
762
1326
|
};
|
|
763
1327
|
}
|
|
764
1328
|
async function executeRunnerRequest(opts) {
|
|
@@ -772,14 +1336,14 @@ async function executeRunnerRequest(opts) {
|
|
|
772
1336
|
resolvedUrl: req.url,
|
|
773
1337
|
status: "running"
|
|
774
1338
|
};
|
|
775
|
-
const dynamicVars = await
|
|
776
|
-
let vars =
|
|
1339
|
+
const dynamicVars = await buildDynamicVars();
|
|
1340
|
+
let vars = mergeVars(envVars, collectionVars, globals2, localVars, dynamicVars);
|
|
777
1341
|
let updatedEnvVars = { ...envVars };
|
|
778
1342
|
let updatedCollectionVars = { ...collectionVars };
|
|
779
1343
|
let updatedGlobals = { ...globals2 };
|
|
780
1344
|
let preScriptError;
|
|
781
1345
|
if (req.preRequestScript?.trim()) {
|
|
782
|
-
const r = await runScript(
|
|
1346
|
+
const r = await runScript(interpolate(req.preRequestScript, vars), {
|
|
783
1347
|
envVars: { ...envVars },
|
|
784
1348
|
collectionVars: { ...collectionVars },
|
|
785
1349
|
globals: { ...globals2 },
|
|
@@ -794,9 +1358,9 @@ async function executeRunnerRequest(opts) {
|
|
|
794
1358
|
patchGlobals(r.updatedGlobals);
|
|
795
1359
|
await persistGlobals();
|
|
796
1360
|
onScriptOutput?.("pre", r.consoleOutput, r.error);
|
|
797
|
-
vars =
|
|
1361
|
+
vars = mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
|
|
798
1362
|
}
|
|
799
|
-
const resolvedUrl =
|
|
1363
|
+
const resolvedUrl = buildUrl(req.url, req.params, vars);
|
|
800
1364
|
base.resolvedUrl = resolvedUrl;
|
|
801
1365
|
const start = Date.now();
|
|
802
1366
|
try {
|
|
@@ -817,7 +1381,7 @@ async function executeRunnerRequest(opts) {
|
|
|
817
1381
|
let consoleOutput = [];
|
|
818
1382
|
let postScriptError;
|
|
819
1383
|
if (req.postRequestScript?.trim()) {
|
|
820
|
-
const r = await runScript(
|
|
1384
|
+
const r = await runScript(interpolate(req.postRequestScript, vars), {
|
|
821
1385
|
envVars: updatedEnvVars,
|
|
822
1386
|
collectionVars: updatedCollectionVars,
|
|
823
1387
|
globals: updatedGlobals,
|
|
@@ -890,25 +1454,25 @@ class HookSkipTracker {
|
|
|
890
1454
|
if (isHook) {
|
|
891
1455
|
if (hookType === "beforeAll") {
|
|
892
1456
|
if ((scopeAncestors ?? []).some((id) => this.failedScopes.has(id))) {
|
|
893
|
-
return "Skipped
|
|
1457
|
+
return "Skipped - outer scope hook failed";
|
|
894
1458
|
}
|
|
895
1459
|
} else if (hookType === "before") {
|
|
896
1460
|
const allScopes2 = [...scopeAncestors ?? [], scopeId].filter(Boolean);
|
|
897
1461
|
if (allScopes2.some((id) => this.failedScopes.has(id))) {
|
|
898
|
-
return "Skipped
|
|
1462
|
+
return "Skipped - scope hook failed";
|
|
899
1463
|
}
|
|
900
1464
|
if (mainRequestId && this.skipRequests.has(mainRequestId)) {
|
|
901
|
-
return "Skipped
|
|
1465
|
+
return "Skipped - before hook failed";
|
|
902
1466
|
}
|
|
903
1467
|
}
|
|
904
1468
|
return void 0;
|
|
905
1469
|
}
|
|
906
1470
|
const allScopes = [...scopeAncestors ?? [], scopeId].filter(Boolean);
|
|
907
1471
|
if (allScopes.some((id) => this.failedScopes.has(id))) {
|
|
908
|
-
return "Skipped
|
|
1472
|
+
return "Skipped - beforeAll hook failed";
|
|
909
1473
|
}
|
|
910
1474
|
if (this.skipRequests.has(item.request.id)) {
|
|
911
|
-
return "Skipped
|
|
1475
|
+
return "Skipped - before hook failed";
|
|
912
1476
|
}
|
|
913
1477
|
return void 0;
|
|
914
1478
|
}
|
|
@@ -925,135 +1489,27 @@ class HookSkipTracker {
|
|
|
925
1489
|
}
|
|
926
1490
|
}
|
|
927
1491
|
}
|
|
928
|
-
function makeHook(req, collectionVars, hookType, scopeId, scopeAncestors, scopePath, mainRequestId) {
|
|
929
|
-
return { request: req, collectionVars, isHook: true, hookType, scopeId, scopeAncestors, scopePath, mainRequestId };
|
|
930
|
-
}
|
|
931
|
-
function buildFolderPlan(folder, requests, collectionVars, filterTags, scopeId, ancestorIds, parentPath, wrappers, isRoot) {
|
|
932
|
-
const result = [];
|
|
933
|
-
const scopePath = isRoot ? [] : [...parentPath, folder.name];
|
|
934
|
-
const folderReqs = folder.requestIds.map((id) => requests[id]).filter((r) => r && !r.disabled);
|
|
935
|
-
const beforeAllHooks = folderReqs.filter((r) => r.hookType === "beforeAll");
|
|
936
|
-
const beforeHooks = folderReqs.filter((r) => r.hookType === "before");
|
|
937
|
-
const afterHooks = folderReqs.filter((r) => r.hookType === "after");
|
|
938
|
-
const afterAllHooks = folderReqs.filter((r) => r.hookType === "afterAll");
|
|
939
|
-
const regularReqs = folderReqs.filter((r) => !r.hookType);
|
|
940
|
-
const myWrapper = { scopeId, ancestors: ancestorIds, scopePath, before: beforeHooks, after: afterHooks };
|
|
941
|
-
const allWrappers = [...wrappers, myWrapper];
|
|
942
|
-
for (const req of beforeAllHooks) {
|
|
943
|
-
result.push(makeHook(req, collectionVars, "beforeAll", scopeId, ancestorIds, scopePath));
|
|
944
|
-
}
|
|
945
|
-
for (const req of regularReqs) {
|
|
946
|
-
const tags = req.meta?.tags ?? [];
|
|
947
|
-
if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
|
|
948
|
-
for (const w of allWrappers) {
|
|
949
|
-
for (const hookReq of w.before) {
|
|
950
|
-
result.push(makeHook(hookReq, collectionVars, "before", w.scopeId, w.ancestors, w.scopePath, req.id));
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
result.push({ request: req, collectionVars, scopeId, scopeAncestors: ancestorIds, scopePath });
|
|
954
|
-
for (const w of [...allWrappers].reverse()) {
|
|
955
|
-
for (const hookReq of w.after) {
|
|
956
|
-
result.push(makeHook(hookReq, collectionVars, "after", w.scopeId, w.ancestors, w.scopePath, req.id));
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
for (const sub of folder.folders) {
|
|
961
|
-
const folderTags = sub.tags ?? [];
|
|
962
|
-
const effectiveFilter = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
|
|
963
|
-
result.push(...buildFolderPlan(
|
|
964
|
-
sub,
|
|
965
|
-
requests,
|
|
966
|
-
collectionVars,
|
|
967
|
-
effectiveFilter,
|
|
968
|
-
sub.id,
|
|
969
|
-
[...ancestorIds, scopeId],
|
|
970
|
-
scopePath,
|
|
971
|
-
allWrappers,
|
|
972
|
-
false
|
|
973
|
-
));
|
|
974
|
-
}
|
|
975
|
-
for (const req of afterAllHooks) {
|
|
976
|
-
result.push(makeHook(req, collectionVars, "afterAll", scopeId, ancestorIds, scopePath));
|
|
977
|
-
}
|
|
978
|
-
return result;
|
|
979
|
-
}
|
|
980
|
-
function folderPathTo(root, requestId) {
|
|
981
|
-
if (root.requestIds.includes(requestId)) return [root];
|
|
982
|
-
for (const sub of root.folders) {
|
|
983
|
-
const path2 = folderPathTo(sub, requestId);
|
|
984
|
-
if (path2.length > 0) return [root, ...path2];
|
|
985
|
-
}
|
|
986
|
-
return [];
|
|
987
|
-
}
|
|
988
|
-
function getAllApplicableHooks(folderId, collection) {
|
|
989
|
-
function chainToFolder(root, targetId) {
|
|
990
|
-
if (root.id === targetId) return [root];
|
|
991
|
-
for (const sub of root.folders) {
|
|
992
|
-
const chain2 = chainToFolder(sub, targetId);
|
|
993
|
-
if (chain2.length) return [root, ...chain2];
|
|
994
|
-
}
|
|
995
|
-
return [];
|
|
996
|
-
}
|
|
997
|
-
const chain = chainToFolder(collection.rootFolder, folderId);
|
|
998
|
-
const beforeAll = [];
|
|
999
|
-
const before = [];
|
|
1000
|
-
const after = [];
|
|
1001
|
-
const afterAll = [];
|
|
1002
|
-
for (const folder of chain) {
|
|
1003
|
-
const reqs = folder.requestIds.map((id) => collection.requests[id]).filter((r) => r && !r.disabled);
|
|
1004
|
-
beforeAll.push(...reqs.filter((r) => r.hookType === "beforeAll"));
|
|
1005
|
-
before.push(...reqs.filter((r) => r.hookType === "before"));
|
|
1006
|
-
}
|
|
1007
|
-
for (const folder of [...chain].reverse()) {
|
|
1008
|
-
const reqs = folder.requestIds.map((id) => collection.requests[id]).filter((r) => r && !r.disabled);
|
|
1009
|
-
after.push(...reqs.filter((r) => r.hookType === "after"));
|
|
1010
|
-
afterAll.push(...reqs.filter((r) => r.hookType === "afterAll"));
|
|
1011
|
-
}
|
|
1012
|
-
return { beforeAll, before, after, afterAll };
|
|
1013
|
-
}
|
|
1014
|
-
function resolveInheritedAuthAndHeaders(requestId, collection) {
|
|
1015
|
-
let inheritedAuth = collection.auth && collection.auth.type !== "none" ? collection.auth : null;
|
|
1016
|
-
let inheritedHeaders = collection.headers?.filter((h) => h.enabled && h.key) ?? [];
|
|
1017
|
-
const path2 = folderPathTo(collection.rootFolder, requestId);
|
|
1018
|
-
for (const folder of path2) {
|
|
1019
|
-
if (folder.auth && folder.auth.type !== "none") inheritedAuth = folder.auth;
|
|
1020
|
-
if (folder.headers?.length) {
|
|
1021
|
-
inheritedHeaders = [...inheritedHeaders, ...folder.headers.filter((h) => h.enabled && h.key)];
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
return { auth: inheritedAuth, headers: inheritedHeaders };
|
|
1025
|
-
}
|
|
1026
|
-
function buildRunPlan(collection, folderId, filterTags) {
|
|
1027
|
-
const collectionVars = collection.collectionVariables ?? {};
|
|
1028
|
-
{
|
|
1029
|
-
return buildFolderPlan(
|
|
1030
|
-
collection.rootFolder,
|
|
1031
|
-
collection.requests,
|
|
1032
|
-
collectionVars,
|
|
1033
|
-
filterTags,
|
|
1034
|
-
collection.rootFolder.id,
|
|
1035
|
-
[],
|
|
1036
|
-
[],
|
|
1037
|
-
[],
|
|
1038
|
-
true
|
|
1039
|
-
);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
1492
|
exports.HookSkipTracker = HookSkipTracker;
|
|
1043
1493
|
exports.applyRequestDefaults = applyRequestDefaults;
|
|
1494
|
+
exports.buildAuthHeaders = buildAuthHeaders;
|
|
1044
1495
|
exports.buildDispatcher = buildDispatcher;
|
|
1496
|
+
exports.buildDynamicVars = buildDynamicVars;
|
|
1497
|
+
exports.buildEnvVars = buildEnvVars;
|
|
1045
1498
|
exports.buildProxyUri = buildProxyUri;
|
|
1046
|
-
exports.buildRunPlan = buildRunPlan;
|
|
1047
1499
|
exports.buildSchemaTestResults = buildSchemaTestResults;
|
|
1500
|
+
exports.buildUrl = buildUrl;
|
|
1048
1501
|
exports.executeRunnerRequest = executeRunnerRequest;
|
|
1049
|
-
exports.getAllApplicableHooks = getAllApplicableHooks;
|
|
1050
1502
|
exports.getGlobals = getGlobals;
|
|
1503
|
+
exports.getSecret = getSecret;
|
|
1504
|
+
exports.initSecretStore = initSecretStore;
|
|
1505
|
+
exports.interpolate = interpolate;
|
|
1051
1506
|
exports.loadGlobals = loadGlobals;
|
|
1052
1507
|
exports.maskHeaders = maskHeaders;
|
|
1053
1508
|
exports.maskPii = maskPii;
|
|
1509
|
+
exports.mergeVars = mergeVars;
|
|
1054
1510
|
exports.patchGlobals = patchGlobals;
|
|
1055
1511
|
exports.performHttpExchange = performHttpExchange;
|
|
1056
1512
|
exports.persistGlobals = persistGlobals;
|
|
1057
|
-
exports.
|
|
1513
|
+
exports.registerSecretHandlers = registerSecretHandlers;
|
|
1058
1514
|
exports.runScript = runScript;
|
|
1059
1515
|
exports.setGlobals = setGlobals;
|