@testsmith/api-spector 0.0.7 → 0.0.9

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.
@@ -0,0 +1,1041 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ const undici = require("undici");
25
+ const promises = require("fs/promises");
26
+ const crypto = require("crypto");
27
+ const path = require("path");
28
+ const dayjs = require("dayjs");
29
+ const vm = require("vm");
30
+ const tv4 = require("tv4");
31
+ const jsonpathPlus = require("jsonpath-plus");
32
+ const xmldom = require("@xmldom/xmldom");
33
+ function _interopNamespaceDefault(e) {
34
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
35
+ if (e) {
36
+ for (const k in e) {
37
+ if (k !== "default") {
38
+ const d = Object.getOwnPropertyDescriptor(e, k);
39
+ Object.defineProperty(n, k, d.get ? d : {
40
+ enumerable: true,
41
+ get: () => e[k]
42
+ });
43
+ }
44
+ }
45
+ }
46
+ n.default = e;
47
+ return Object.freeze(n);
48
+ }
49
+ const vm__namespace = /* @__PURE__ */ _interopNamespaceDefault(vm);
50
+ let globals = {};
51
+ let currentDir = null;
52
+ function globalsPath(dir) {
53
+ return path.join(dir, "globals.json");
54
+ }
55
+ async function loadGlobals(workspaceDir) {
56
+ currentDir = workspaceDir;
57
+ try {
58
+ const raw = await promises.readFile(globalsPath(workspaceDir), "utf8");
59
+ globals = JSON.parse(raw);
60
+ } catch {
61
+ globals = {};
62
+ }
63
+ return { ...globals };
64
+ }
65
+ async function persistGlobals() {
66
+ if (!currentDir) return;
67
+ await promises.writeFile(globalsPath(currentDir), JSON.stringify(globals, null, 2), "utf8");
68
+ }
69
+ function getGlobals() {
70
+ return { ...globals };
71
+ }
72
+ function setGlobals(next) {
73
+ globals = { ...next };
74
+ }
75
+ function patchGlobals(patch) {
76
+ globals = { ...globals, ...patch };
77
+ }
78
+ const MASTER_KEY_ENV = "API_SPECTOR_MASTER_KEY";
79
+ let secretStore = {};
80
+ let secretStorePath = null;
81
+ async function initSecretStore(userDataPath) {
82
+ secretStorePath = path.join(userDataPath, "secrets.json");
83
+ try {
84
+ const raw = await promises.readFile(secretStorePath, "utf8");
85
+ secretStore = JSON.parse(raw);
86
+ } catch {
87
+ secretStore = {};
88
+ }
89
+ }
90
+ async function persistSecretStore() {
91
+ if (!secretStorePath) return;
92
+ await promises.writeFile(secretStorePath, JSON.stringify(secretStore, null, 2), "utf8");
93
+ }
94
+ function getSafeStorage() {
95
+ try {
96
+ const { safeStorage } = require("electron");
97
+ if (typeof safeStorage?.isEncryptionAvailable === "function") return safeStorage;
98
+ return null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ function registerSecretHandlers(ipc) {
104
+ ipc.handle("secret:checkMasterKey", () => {
105
+ return { set: Boolean(process.env[MASTER_KEY_ENV]) };
106
+ });
107
+ ipc.handle("secret:setMasterKey", (_e, value) => {
108
+ process.env[MASTER_KEY_ENV] = value;
109
+ });
110
+ ipc.handle("secret:set", async (_e, ref, value) => {
111
+ const ss = getSafeStorage();
112
+ if (!ss || !ss.isEncryptionAvailable()) {
113
+ throw new Error("OS encryption is not available — set the secret via environment variable instead");
114
+ }
115
+ secretStore[ref] = ss.encryptString(value).toString("base64");
116
+ await persistSecretStore();
117
+ });
118
+ }
119
+ function decryptSecret(encrypted, salt, iv, password) {
120
+ const saltBuf = Buffer.from(salt, "base64");
121
+ const ivBuf = Buffer.from(iv, "base64");
122
+ const encBuf = Buffer.from(encrypted, "base64");
123
+ const key = crypto.pbkdf2Sync(password, saltBuf, 1e5, 32, "sha256");
124
+ const authTag = encBuf.subarray(encBuf.length - 16);
125
+ const ciphertext = encBuf.subarray(0, encBuf.length - 16);
126
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, ivBuf);
127
+ decipher.setAuthTag(authTag);
128
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
129
+ }
130
+ async function getSecret(ref) {
131
+ const stored = secretStore[ref];
132
+ if (stored) {
133
+ const ss = getSafeStorage();
134
+ if (ss && ss.isEncryptionAvailable()) {
135
+ try {
136
+ return ss.decryptString(Buffer.from(stored, "base64"));
137
+ } catch {
138
+ }
139
+ }
140
+ }
141
+ return process.env[ref] ?? null;
142
+ }
143
+ let _fakerCache$1 = null;
144
+ async function getFaker$1() {
145
+ if (!_fakerCache$1) _fakerCache$1 = await import("@faker-js/faker");
146
+ return _fakerCache$1.faker;
147
+ }
148
+ let _exprContext = null;
149
+ async function buildDynamicVars() {
150
+ const faker = await getFaker$1();
151
+ const now = dayjs();
152
+ _exprContext = { faker, dayjs };
153
+ return {
154
+ $uuid: faker.string.uuid(),
155
+ $timestamp: String(Date.now()),
156
+ $isoTimestamp: now.toISOString(),
157
+ $randomInt: String(faker.number.int({ min: 0, max: 1e3 })),
158
+ $randomFloat: String(faker.number.float({ min: 0, max: 1e3, fractionDigits: 2 })),
159
+ $randomBoolean: String(faker.datatype.boolean()),
160
+ $randomEmail: faker.internet.email(),
161
+ $randomUsername: faker.internet.username(),
162
+ $randomPassword: faker.internet.password(),
163
+ $randomFullName: faker.person.fullName(),
164
+ $randomFirstName: faker.person.firstName(),
165
+ $randomLastName: faker.person.lastName(),
166
+ $randomWord: faker.lorem.word(),
167
+ $randomPhrase: faker.lorem.sentence(),
168
+ $randomUrl: faker.internet.url(),
169
+ $randomIp: faker.internet.ip(),
170
+ $randomHexColor: faker.color.rgb({ format: "hex", casing: "lower" })
171
+ };
172
+ }
173
+ function interpolate(str, vars) {
174
+ return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
175
+ const trimmed = key.trim();
176
+ if (trimmed in vars) return vars[trimmed];
177
+ if (_exprContext && (trimmed.includes(".") || trimmed.includes("("))) {
178
+ try {
179
+ const result = vm__namespace.runInNewContext(trimmed, _exprContext);
180
+ if (result !== void 0 && result !== null) return String(result);
181
+ } catch {
182
+ }
183
+ }
184
+ return match;
185
+ });
186
+ }
187
+ function buildUrl(baseUrl, params, vars) {
188
+ const url = interpolate(baseUrl, vars);
189
+ const enabled = params.filter((p) => p.enabled && p.key);
190
+ if (!enabled.length) return url;
191
+ const sep = url.includes("?") ? "&" : "?";
192
+ const qs = enabled.map((p) => `${encodeURIComponent(interpolate(p.key, vars))}=${encodeURIComponent(interpolate(p.value, vars))}`).join("&");
193
+ return url + sep + qs;
194
+ }
195
+ async function buildEnvVars(environment) {
196
+ const vars = {};
197
+ if (!environment) return vars;
198
+ const masterKey = process.env["API_SPECTOR_MASTER_KEY"];
199
+ for (const v of environment.variables) {
200
+ if (!v.enabled) continue;
201
+ if (v.envRef) {
202
+ const envValue = process.env[v.envRef];
203
+ if (envValue !== void 0) vars[v.key] = envValue;
204
+ } else if (v.secret && v.secretEncrypted && v.secretSalt && v.secretIv) {
205
+ if (masterKey) {
206
+ try {
207
+ vars[v.key] = decryptSecret(v.secretEncrypted, v.secretSalt, v.secretIv, masterKey);
208
+ } catch {
209
+ }
210
+ }
211
+ } else {
212
+ vars[v.key] = v.value;
213
+ }
214
+ }
215
+ return vars;
216
+ }
217
+ function mergeVars(envVars, collectionVars, globals2, localVars = {}, dynamicVars = {}) {
218
+ return { ...dynamicVars, ...globals2, ...collectionVars, ...envVars, ...localVars };
219
+ }
220
+ function xmlFindAll(node, tag, nth) {
221
+ const results = [];
222
+ const siblings = Array.from(node.childNodes).filter((c) => c.nodeType === 1 && c.tagName === tag);
223
+ if (siblings[nth]) results.push(siblings[nth]);
224
+ for (const child of Array.from(node.childNodes)) {
225
+ if (child.nodeType === 1) results.push(...xmlFindAll(child, tag, nth));
226
+ }
227
+ return results;
228
+ }
229
+ function xmlQuerySelector(root, selector) {
230
+ const parts = selector.trim().split(/\s*>\s*/);
231
+ const m0 = parts[0].match(/^([A-Za-z0-9_:.-]+?)(?::nth-of-type\((\d+)\))?$/);
232
+ if (!m0) return null;
233
+ let candidates = xmlFindAll(root, m0[1], m0[2] ? parseInt(m0[2]) - 1 : 0);
234
+ for (let i = 1; i < parts.length; i++) {
235
+ const m = parts[i].match(/^([A-Za-z0-9_:.-]+?)(?::nth-of-type\((\d+)\))?$/);
236
+ if (!m) return null;
237
+ const tag = m[1];
238
+ const idx = m[2] ? parseInt(m[2]) - 1 : 0;
239
+ const next = [];
240
+ for (const node of candidates) {
241
+ const children = Array.from(node.childNodes).filter((c) => c.nodeType === 1 && c.tagName === tag);
242
+ if (children[idx]) next.push(children[idx]);
243
+ }
244
+ candidates = next;
245
+ }
246
+ return candidates[0] ?? null;
247
+ }
248
+ function makeSandboxDOMParser() {
249
+ return {
250
+ parseFromString(str, mime) {
251
+ const doc = new xmldom.DOMParser().parseFromString(str, mime);
252
+ doc.querySelector = (sel) => xmlQuerySelector(doc, sel);
253
+ return doc;
254
+ }
255
+ };
256
+ }
257
+ let _fakerCache = null;
258
+ async function getFaker() {
259
+ if (!_fakerCache) _fakerCache = await import("@faker-js/faker");
260
+ return _fakerCache.faker;
261
+ }
262
+ function makeAsserter(value, negated = false) {
263
+ function doAssert(condition, msg) {
264
+ const passed = negated ? !condition : condition;
265
+ if (!passed) throw new AssertionError(msg);
266
+ }
267
+ const asserter = {};
268
+ const chainer = { get: () => asserter };
269
+ for (const key of ["to", "be", "been", "have", "that", "and", "is", "deep"]) {
270
+ Object.defineProperty(asserter, key, chainer);
271
+ }
272
+ Object.defineProperty(asserter, "not", { get: () => makeAsserter(value, !negated) });
273
+ Object.defineProperty(asserter, "ok", { get: () => {
274
+ doAssert(Boolean(value), `Expected ${JSON.stringify(value)} to be truthy`);
275
+ return asserter;
276
+ } });
277
+ Object.defineProperty(asserter, "true", { get: () => {
278
+ doAssert(value === true, `Expected ${JSON.stringify(value)} to be true`);
279
+ return asserter;
280
+ } });
281
+ Object.defineProperty(asserter, "false", { get: () => {
282
+ doAssert(value === false, `Expected ${JSON.stringify(value)} to be false`);
283
+ return asserter;
284
+ } });
285
+ Object.defineProperty(asserter, "null", { get: () => {
286
+ doAssert(value === null, `Expected ${JSON.stringify(value)} to be null`);
287
+ return asserter;
288
+ } });
289
+ Object.defineProperty(asserter, "undefined", { get: () => {
290
+ doAssert(value === void 0, `Expected value to be undefined`);
291
+ return asserter;
292
+ } });
293
+ asserter.equal = (expected) => {
294
+ doAssert(
295
+ value === expected,
296
+ `Expected ${JSON.stringify(value)} to ${negated ? "not " : ""}equal ${JSON.stringify(expected)}`
297
+ );
298
+ return asserter;
299
+ };
300
+ asserter.eq = asserter.equal;
301
+ asserter.eql = (expected) => {
302
+ doAssert(
303
+ JSON.stringify(value) === JSON.stringify(expected),
304
+ `Expected deep equal: ${JSON.stringify(value)} ${negated ? "!=" : "=="} ${JSON.stringify(expected)}`
305
+ );
306
+ return asserter;
307
+ };
308
+ asserter.include = (substr) => {
309
+ if (typeof value === "string") {
310
+ doAssert(
311
+ value.includes(String(substr)),
312
+ `Expected "${value}" to ${negated ? "not " : ""}include "${substr}"`
313
+ );
314
+ } else if (Array.isArray(value)) {
315
+ doAssert(
316
+ value.includes(substr),
317
+ `Expected array to ${negated ? "not " : ""}include ${JSON.stringify(substr)}`
318
+ );
319
+ }
320
+ return asserter;
321
+ };
322
+ asserter.contain = asserter.include;
323
+ asserter.property = (name, expected) => {
324
+ doAssert(
325
+ value != null && name in Object(value),
326
+ `Expected object to ${negated ? "not " : ""}have property "${name}"`
327
+ );
328
+ if (expected !== void 0) {
329
+ doAssert(
330
+ value[name] === expected,
331
+ `Expected property "${name}" to equal ${JSON.stringify(expected)}`
332
+ );
333
+ }
334
+ return asserter;
335
+ };
336
+ asserter.a = (type) => {
337
+ const actual = Array.isArray(value) ? "array" : typeof value;
338
+ doAssert(
339
+ actual === type,
340
+ `Expected ${JSON.stringify(value)} to ${negated ? "not " : ""}be a ${type}`
341
+ );
342
+ return asserter;
343
+ };
344
+ asserter.above = (n) => {
345
+ doAssert(value > n, `Expected ${value} to ${negated ? "not " : ""}be above ${n}`);
346
+ return asserter;
347
+ };
348
+ asserter.below = (n) => {
349
+ doAssert(value < n, `Expected ${value} to ${negated ? "not " : ""}be below ${n}`);
350
+ return asserter;
351
+ };
352
+ asserter.least = (n) => {
353
+ doAssert(value >= n, `Expected ${value} to ${negated ? "not " : ""}be at least ${n}`);
354
+ return asserter;
355
+ };
356
+ asserter.most = (n) => {
357
+ doAssert(value <= n, `Expected ${value} to ${negated ? "not " : ""}be at most ${n}`);
358
+ return asserter;
359
+ };
360
+ asserter.gt = asserter.above;
361
+ asserter.gte = asserter.least;
362
+ asserter.lt = asserter.below;
363
+ asserter.lte = asserter.most;
364
+ asserter.oneOf = (list) => {
365
+ doAssert(
366
+ list.includes(value),
367
+ `Expected ${JSON.stringify(value)} to ${negated ? "not " : ""}be one of ${JSON.stringify(list)}`
368
+ );
369
+ return asserter;
370
+ };
371
+ asserter.lengthOf = (n) => {
372
+ const len = value.length;
373
+ doAssert(
374
+ len === n,
375
+ `Expected length ${len} to ${negated ? "not " : ""}equal ${n}`
376
+ );
377
+ return asserter;
378
+ };
379
+ asserter.match = (re) => {
380
+ doAssert(
381
+ re.test(String(value)),
382
+ `Expected "${value}" to ${negated ? "not " : ""}match ${re}`
383
+ );
384
+ return asserter;
385
+ };
386
+ return asserter;
387
+ }
388
+ class AssertionError extends Error {
389
+ constructor(message) {
390
+ super(message);
391
+ this.name = "AssertionError";
392
+ }
393
+ }
394
+ function buildAt(ctx, testResults, consoleOutput) {
395
+ const { envVars, collectionVars, globals: globals2, localVars } = ctx;
396
+ function makeVarScope(store, scopeName) {
397
+ return {
398
+ get: (key) => store[key] ?? null,
399
+ set: (key, value) => {
400
+ store[key] = String(value);
401
+ consoleOutput.push(`[set] ${scopeName}.${key} = ${JSON.stringify(String(value))}`);
402
+ },
403
+ clear: (key) => {
404
+ delete store[key];
405
+ consoleOutput.push(`[set] ${scopeName}.${key} cleared`);
406
+ },
407
+ has: (key) => key in store,
408
+ toObject: () => ({ ...store })
409
+ };
410
+ }
411
+ const sp = {
412
+ // Variable scopes
413
+ variables: makeVarScope(localVars, "variables"),
414
+ environment: makeVarScope(envVars, "environment"),
415
+ collectionVariables: makeVarScope(collectionVars, "collectionVariables"),
416
+ globals: makeVarScope(globals2, "globals"),
417
+ // Convenience: get/set across all scopes (local wins)
418
+ variables_get: (key) => localVars[key] ?? envVars[key] ?? collectionVars[key] ?? globals2[key] ?? null,
419
+ variables_set: (key, value) => {
420
+ localVars[key] = String(value);
421
+ },
422
+ // Test runner
423
+ test: (name, fn) => {
424
+ try {
425
+ fn();
426
+ testResults.push({ name, passed: true });
427
+ } catch (err) {
428
+ testResults.push({
429
+ name,
430
+ passed: false,
431
+ error: err instanceof Error ? err.message : String(err)
432
+ });
433
+ }
434
+ },
435
+ // Expect / assertions
436
+ expect: (value) => makeAsserter(value, false),
437
+ // JSONPath query: sp.jsonPath(data, '$.store.book[?(@.price < 10)].title')
438
+ jsonPath: (data, expr) => jsonpathPlus.JSONPath({ path: expr, json: data })
439
+ };
440
+ if (ctx.response) {
441
+ const resp = ctx.response;
442
+ let parsedJson = void 0;
443
+ sp.response = {
444
+ code: resp.status,
445
+ status: `${resp.status} ${resp.statusText}`,
446
+ statusText: resp.statusText,
447
+ responseTime: resp.durationMs,
448
+ responseSize: resp.bodySize,
449
+ headers: {
450
+ get: (name) => resp.headers[name.toLowerCase()] ?? null,
451
+ toObject: () => resp.headers
452
+ },
453
+ json: () => {
454
+ if (parsedJson === void 0) parsedJson = JSON.parse(resp.body);
455
+ return parsedJson;
456
+ },
457
+ text: () => resp.body,
458
+ xmlText: (selector) => {
459
+ const doc = new xmldom.DOMParser().parseFromString(resp.body, "text/xml");
460
+ const node = xmlQuerySelector(doc, selector);
461
+ return node ? String(node.textContent ?? "").trim() : null;
462
+ },
463
+ to: {
464
+ have: {
465
+ status: (code) => {
466
+ if (resp.status !== code) throw new AssertionError(`Expected status ${resp.status} to be ${code}`);
467
+ }
468
+ }
469
+ }
470
+ };
471
+ }
472
+ return sp;
473
+ }
474
+ async function runScript(code, ctx, timeoutMs = 5e3) {
475
+ const faker = await getFaker();
476
+ const testResults = [];
477
+ const consoleOutput = [];
478
+ const envVarsCopy = { ...ctx.envVars };
479
+ const collectionCopy = { ...ctx.collectionVars };
480
+ const globalsCopy = { ...ctx.globals };
481
+ const localVarsCopy = { ...ctx.localVars };
482
+ const scriptCtx = {
483
+ envVars: envVarsCopy,
484
+ collectionVars: collectionCopy,
485
+ globals: globalsCopy,
486
+ localVars: localVarsCopy,
487
+ response: ctx.response
488
+ };
489
+ const sp = buildAt(scriptCtx, testResults, consoleOutput);
490
+ const captureConsole = {
491
+ log: (...args) => consoleOutput.push(args.map(String).join(" ")),
492
+ warn: (...args) => consoleOutput.push("[warn] " + args.map(String).join(" ")),
493
+ error: (...args) => consoleOutput.push("[error] " + args.map(String).join(" ")),
494
+ info: (...args) => consoleOutput.push("[info] " + args.map(String).join(" "))
495
+ };
496
+ const sandbox = {
497
+ sp,
498
+ DOMParser: makeSandboxDOMParser,
499
+ dayjs,
500
+ faker,
501
+ tv4,
502
+ console: captureConsole,
503
+ JSON,
504
+ Math,
505
+ Date,
506
+ parseInt,
507
+ parseFloat,
508
+ isNaN,
509
+ isFinite,
510
+ encodeURIComponent,
511
+ decodeURIComponent,
512
+ btoa: (s) => Buffer.from(s, "binary").toString("base64"),
513
+ atob: (s) => Buffer.from(s, "base64").toString("binary"),
514
+ setTimeout: void 0,
515
+ // not available in sync vm context
516
+ setInterval: void 0
517
+ };
518
+ try {
519
+ vm__namespace.runInNewContext(code, sandbox, { timeout: timeoutMs, filename: "script.js" });
520
+ } catch (err) {
521
+ const message = err instanceof Error ? err.message : String(err);
522
+ return {
523
+ testResults,
524
+ consoleOutput,
525
+ updatedEnvVars: envVarsCopy,
526
+ updatedCollectionVars: collectionCopy,
527
+ updatedGlobals: globalsCopy,
528
+ updatedLocalVars: localVarsCopy,
529
+ error: message
530
+ };
531
+ }
532
+ return {
533
+ testResults,
534
+ consoleOutput,
535
+ updatedEnvVars: envVarsCopy,
536
+ updatedCollectionVars: collectionCopy,
537
+ updatedGlobals: globalsCopy,
538
+ updatedLocalVars: localVarsCopy
539
+ };
540
+ }
541
+ async function buildAuthHeaders(auth, vars) {
542
+ const headers = {};
543
+ if (auth.type === "bearer") {
544
+ let token = auth.token ?? "";
545
+ if (!token && auth.tokenSecretRef) token = await getSecret(auth.tokenSecretRef) ?? "";
546
+ token = interpolate(token, vars);
547
+ if (token) headers["Authorization"] = `Bearer ${token}`;
548
+ }
549
+ if (auth.type === "basic") {
550
+ let password = auth.password ?? "";
551
+ if (!password && auth.passwordSecretRef) password = await getSecret(auth.passwordSecretRef) ?? "";
552
+ password = interpolate(password, vars);
553
+ const username = interpolate(auth.username ?? "", vars);
554
+ headers["Authorization"] = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
555
+ }
556
+ if (auth.type === "apikey" && auth.apiKeyIn === "header") {
557
+ let value = auth.apiKeyValue ?? "";
558
+ if (!value && auth.apiKeySecretRef) value = await getSecret(auth.apiKeySecretRef) ?? "";
559
+ value = interpolate(value, vars);
560
+ headers[auth.apiKeyName ?? "X-API-Key"] = value;
561
+ }
562
+ if (auth.type === "oauth2") {
563
+ const now = Date.now();
564
+ if (auth.oauth2CachedToken && auth.oauth2TokenExpiry && auth.oauth2TokenExpiry > now + 5e3) {
565
+ headers["Authorization"] = `Bearer ${auth.oauth2CachedToken}`;
566
+ }
567
+ }
568
+ return headers;
569
+ }
570
+ async function buildApiKeyParam(auth, vars) {
571
+ if (auth.type !== "apikey" || auth.apiKeyIn !== "query") return null;
572
+ let value = auth.apiKeyValue ?? "";
573
+ if (!value && auth.apiKeySecretRef) value = await getSecret(auth.apiKeySecretRef) ?? "";
574
+ value = interpolate(value, vars);
575
+ return { key: auth.apiKeyName ?? "apikey", value };
576
+ }
577
+ function parseDigestChallenge(wwwAuth) {
578
+ const extract = (key) => {
579
+ const m = new RegExp(`${key}="([^"]*)"`, "i").exec(wwwAuth);
580
+ return m ? m[1] : "";
581
+ };
582
+ const extractUnquoted = (key) => {
583
+ const m = new RegExp(`${key}=([^,\\s]+)`, "i").exec(wwwAuth);
584
+ return m ? m[1] : "";
585
+ };
586
+ return {
587
+ realm: extract("realm"),
588
+ nonce: extract("nonce"),
589
+ qop: extract("qop") || extractUnquoted("qop") || void 0,
590
+ algorithm: extract("algorithm") || extractUnquoted("algorithm") || "MD5",
591
+ opaque: extract("opaque") || void 0
592
+ };
593
+ }
594
+ function md5(s) {
595
+ return crypto.createHash("md5").update(s).digest("hex");
596
+ }
597
+ function buildDigestAuthHeader(challenge, username, password, method, uri) {
598
+ const { realm, nonce, qop, algorithm, opaque } = challenge;
599
+ const algo = (algorithm ?? "MD5").toUpperCase();
600
+ const ha1 = algo === "MD5-SESS" ? md5(`${md5(`${username}:${realm}:${password}`)}:${nonce}:`) : md5(`${username}:${realm}:${password}`);
601
+ const ha2 = md5(`${method}:${uri}`);
602
+ let response;
603
+ let nc;
604
+ let cnonce;
605
+ if (qop === "auth" || qop === "auth-int") {
606
+ nc = "00000001";
607
+ cnonce = crypto.randomBytes(8).toString("hex");
608
+ response = md5(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`);
609
+ } else {
610
+ response = md5(`${ha1}:${nonce}:${ha2}`);
611
+ }
612
+ let header = `Digest username="${username}", realm="${realm}", nonce="${nonce}", uri="${uri}", response="${response}"`;
613
+ if (qop) header += `, qop=${qop}`;
614
+ if (nc) header += `, nc=${nc}`;
615
+ if (cnonce) header += `, cnonce="${cnonce}"`;
616
+ if (opaque) header += `, opaque="${opaque}"`;
617
+ if (algo !== "MD5") header += `, algorithm=${algo}`;
618
+ return header;
619
+ }
620
+ async function performDigestAuth(url, method, auth, vars, fetchFn) {
621
+ const probeResp = await fetchFn(url, { method, headers: {} });
622
+ if (probeResp.status !== 401) return null;
623
+ const wwwAuth = probeResp.headers.get("www-authenticate") ?? "";
624
+ if (!wwwAuth.toLowerCase().startsWith("digest")) return null;
625
+ const challenge = parseDigestChallenge(wwwAuth);
626
+ let password = auth.password ?? "";
627
+ if (!password && auth.passwordSecretRef) password = await getSecret(auth.passwordSecretRef) ?? "";
628
+ password = interpolate(password, vars);
629
+ const username = interpolate(auth.username ?? "", vars);
630
+ let uri = "/";
631
+ try {
632
+ uri = new URL(url).pathname + (new URL(url).search ?? "");
633
+ } catch {
634
+ }
635
+ return buildDigestAuthHeader(challenge, username, password, method, uri);
636
+ }
637
+ async function performNtlmRequest(_url, _method, _auth, _vars) {
638
+ throw new Error(
639
+ 'NTLM auth is not yet implemented. Add "httpntlm" to package.json dependencies and implement performNtlmRequest in auth-builder.ts.'
640
+ );
641
+ }
642
+ async function fetchOAuth2Token(auth, vars) {
643
+ const flow = auth.oauth2Flow ?? "client_credentials";
644
+ if (flow === "authorization_code") {
645
+ throw new Error("authorization_code flow requires the oauth2:startFlow IPC call from the renderer.");
646
+ }
647
+ if (flow === "implicit") {
648
+ throw new Error("implicit flow cannot be performed server-side — tokens must be obtained via the browser redirect.");
649
+ }
650
+ const tokenUrl = interpolate(auth.oauth2TokenUrl ?? "", vars);
651
+ if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required.");
652
+ const clientId = interpolate(auth.oauth2ClientId ?? "", vars);
653
+ let clientSecret = auth.oauth2ClientSecret ?? "";
654
+ if (!clientSecret && auth.oauth2ClientSecretRef) {
655
+ clientSecret = await getSecret(auth.oauth2ClientSecretRef) ?? "";
656
+ }
657
+ clientSecret = interpolate(clientSecret, vars);
658
+ const params = new URLSearchParams();
659
+ params.set("grant_type", flow === "password" ? "password" : "client_credentials");
660
+ params.set("client_id", clientId);
661
+ params.set("client_secret", clientSecret);
662
+ if (auth.oauth2Scopes) params.set("scope", auth.oauth2Scopes);
663
+ if (flow === "password") {
664
+ let password = auth.password ?? "";
665
+ if (!password && auth.passwordSecretRef) password = await getSecret(auth.passwordSecretRef) ?? "";
666
+ password = interpolate(password, vars);
667
+ params.set("username", interpolate(auth.username ?? "", vars));
668
+ params.set("password", password);
669
+ }
670
+ const { fetch: nodeFetch } = await import("undici");
671
+ const resp = await nodeFetch(tokenUrl, {
672
+ method: "POST",
673
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
674
+ body: params.toString()
675
+ });
676
+ if (!resp.ok) {
677
+ const body = await resp.text();
678
+ throw new Error(`OAuth 2.0 token request failed (${resp.status}): ${body}`);
679
+ }
680
+ const json = await resp.json();
681
+ const accessToken = String(json["access_token"] ?? "");
682
+ if (!accessToken) throw new Error("OAuth 2.0: token response missing access_token.");
683
+ const expiresIn = Number(json["expires_in"] ?? 3600);
684
+ const expiresAt = Date.now() + expiresIn * 1e3;
685
+ return {
686
+ accessToken,
687
+ expiresAt,
688
+ refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
689
+ };
690
+ }
691
+ function maskPii(data, patterns) {
692
+ if (!patterns.length) return data;
693
+ try {
694
+ const obj = JSON.parse(data);
695
+ const masked = maskObject(obj, patterns);
696
+ return JSON.stringify(masked);
697
+ } catch {
698
+ return data;
699
+ }
700
+ }
701
+ function maskObject(obj, patterns) {
702
+ if (Array.isArray(obj)) return obj.map((item) => maskObject(item, patterns));
703
+ if (obj && typeof obj === "object") {
704
+ const result = {};
705
+ for (const [k, v] of Object.entries(obj)) {
706
+ if (patterns.some((p) => k.toLowerCase().includes(p.toLowerCase()))) {
707
+ result[k] = "[REDACTED]";
708
+ } else {
709
+ result[k] = maskObject(v, patterns);
710
+ }
711
+ }
712
+ return result;
713
+ }
714
+ return obj;
715
+ }
716
+ function maskHeaders(headers, patterns) {
717
+ if (!patterns.length) return headers;
718
+ const alwaysMask = ["authorization", "cookie", "set-cookie"];
719
+ const result = {};
720
+ for (const [k, v] of Object.entries(headers)) {
721
+ const lower = k.toLowerCase();
722
+ if (alwaysMask.includes(lower) || patterns.some((p) => lower.includes(p.toLowerCase()))) {
723
+ result[k] = "[REDACTED]";
724
+ } else {
725
+ result[k] = v;
726
+ }
727
+ }
728
+ return result;
729
+ }
730
+ async function buildDispatcher(proxy, tls) {
731
+ const connectOpts = {};
732
+ let hasTls = false;
733
+ if (tls) {
734
+ hasTls = true;
735
+ if (tls.rejectUnauthorized !== void 0) {
736
+ connectOpts["rejectUnauthorized"] = tls.rejectUnauthorized;
737
+ }
738
+ if (tls.caCertPath) {
739
+ try {
740
+ connectOpts["ca"] = await promises.readFile(tls.caCertPath);
741
+ } catch {
742
+ }
743
+ }
744
+ if (tls.clientCertPath) {
745
+ try {
746
+ connectOpts["cert"] = await promises.readFile(tls.clientCertPath);
747
+ } catch {
748
+ }
749
+ }
750
+ if (tls.clientKeyPath) {
751
+ try {
752
+ connectOpts["key"] = await promises.readFile(tls.clientKeyPath);
753
+ } catch {
754
+ }
755
+ }
756
+ }
757
+ if (proxy?.url) {
758
+ const proxyUri = proxy.auth ? proxy.url.replace("://", `://${encodeURIComponent(proxy.auth.username)}:${encodeURIComponent(proxy.auth.password)}@`) : proxy.url;
759
+ return new undici.ProxyAgent({
760
+ uri: proxyUri,
761
+ ...hasTls ? { connect: connectOpts } : {}
762
+ });
763
+ }
764
+ if (hasTls) {
765
+ return new undici.Agent({ connect: connectOpts });
766
+ }
767
+ return void 0;
768
+ }
769
+ function registerRequestHandler(ipc) {
770
+ ipc.handle("request:send", async (_e, payload) => {
771
+ const {
772
+ request: req,
773
+ environment,
774
+ collectionVars,
775
+ globals: payloadGlobals,
776
+ proxy,
777
+ tls,
778
+ piiMaskPatterns = []
779
+ } = payload;
780
+ const start = Date.now();
781
+ const liveGlobals = getGlobals();
782
+ const mergedGlobals = { ...payloadGlobals, ...liveGlobals };
783
+ const envVars = await buildEnvVars(environment);
784
+ let localVars = {};
785
+ const decryptionWarnings = [];
786
+ if (environment) {
787
+ const masterKeySet = Boolean(process.env["API_SPECTOR_MASTER_KEY"]);
788
+ for (const v of environment.variables) {
789
+ if (!v.enabled || !v.secret || !v.secretEncrypted) continue;
790
+ if (!masterKeySet) {
791
+ decryptionWarnings.push(`[warn] Secret "${v.key}" was not decrypted: API_SPECTOR_MASTER_KEY is not set. Use the master password modal or export the variable in your shell.`);
792
+ } else if (envVars[v.key] === void 0) {
793
+ decryptionWarnings.push(`[warn] Secret "${v.key}" could not be decrypted: wrong password or corrupted data.`);
794
+ }
795
+ }
796
+ }
797
+ const dynamicVars = await buildDynamicVars();
798
+ let vars = mergeVars(envVars, collectionVars, mergedGlobals, localVars, dynamicVars);
799
+ let preScriptMeta = { consoleOutput: [] };
800
+ let updatedCollectionVars = { ...collectionVars };
801
+ let updatedEnvVars = { ...envVars };
802
+ let updatedGlobals = { ...mergedGlobals };
803
+ if (req.preRequestScript?.trim()) {
804
+ const result = await runScript(req.preRequestScript, {
805
+ envVars: { ...envVars },
806
+ collectionVars: { ...collectionVars },
807
+ globals: { ...mergedGlobals },
808
+ localVars: {}
809
+ });
810
+ preScriptMeta = { error: result.error, consoleOutput: result.consoleOutput };
811
+ localVars = result.updatedLocalVars;
812
+ updatedEnvVars = result.updatedEnvVars;
813
+ updatedCollectionVars = result.updatedCollectionVars;
814
+ updatedGlobals = result.updatedGlobals;
815
+ patchGlobals(result.updatedGlobals);
816
+ await persistGlobals();
817
+ vars = mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
818
+ }
819
+ let response;
820
+ let sentRequest = { method: req.method, url: "", headers: {} };
821
+ const resolvedUrl = buildUrl(req.url, req.params, vars);
822
+ const secretValues = /* @__PURE__ */ new Set();
823
+ if (environment) {
824
+ for (const v of environment.variables) {
825
+ if (!v.enabled) continue;
826
+ if ((v.secret || v.envRef) && envVars[v.key]) {
827
+ secretValues.add(envVars[v.key]);
828
+ }
829
+ }
830
+ }
831
+ function redactSecrets(s) {
832
+ if (!secretValues.size) return s;
833
+ let result = s;
834
+ for (const secret of secretValues) {
835
+ if (secret) result = result.split(secret).join("[*****]");
836
+ }
837
+ return result;
838
+ }
839
+ function redactSentRequest(sr) {
840
+ const headers = {};
841
+ for (const [k, v] of Object.entries(sr.headers)) {
842
+ headers[k] = redactSecrets(v);
843
+ }
844
+ return {
845
+ method: sr.method,
846
+ url: redactSecrets(sr.url),
847
+ headers,
848
+ body: sr.body !== void 0 ? redactSecrets(sr.body) : void 0
849
+ };
850
+ }
851
+ try {
852
+ const dispatcher = await buildDispatcher(proxy, tls);
853
+ if (req.auth.type === "oauth2") {
854
+ const now = Date.now();
855
+ const tokenMissing = !req.auth.oauth2CachedToken;
856
+ const tokenExpired = req.auth.oauth2TokenExpiry ? req.auth.oauth2TokenExpiry <= now + 5e3 : true;
857
+ if (tokenMissing || tokenExpired) {
858
+ const result = await fetchOAuth2Token(req.auth, vars);
859
+ req.auth.oauth2CachedToken = result.accessToken;
860
+ req.auth.oauth2TokenExpiry = result.expiresAt;
861
+ }
862
+ }
863
+ const authHeaders = await buildAuthHeaders(req.auth, vars);
864
+ const apiKeyParam = await buildApiKeyParam(req.auth, vars);
865
+ let finalUrl = resolvedUrl;
866
+ if (apiKeyParam) {
867
+ const sep = finalUrl.includes("?") ? "&" : "?";
868
+ finalUrl += `${sep}${encodeURIComponent(apiKeyParam.key)}=${encodeURIComponent(apiKeyParam.value)}`;
869
+ }
870
+ const buildHeaders = () => {
871
+ const h = new undici.Headers();
872
+ for (const header of req.headers) {
873
+ if (header.enabled && header.key) {
874
+ h.set(interpolate(header.key, vars), interpolate(header.value, vars));
875
+ }
876
+ }
877
+ for (const [k, v] of Object.entries(authHeaders)) h.set(k, v);
878
+ return h;
879
+ };
880
+ let body;
881
+ if (req.body.mode === "json" && req.body.json) {
882
+ body = interpolate(req.body.json, vars);
883
+ } else if (req.body.mode === "form" && req.body.form) {
884
+ body = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${encodeURIComponent(interpolate(p.key, vars))}=${encodeURIComponent(interpolate(p.value, vars))}`).join("&");
885
+ } else if (req.body.mode === "raw" && req.body.raw) {
886
+ body = interpolate(req.body.raw, vars);
887
+ } else if (req.body.mode === "graphql" && req.body.graphql) {
888
+ const gql = req.body.graphql;
889
+ const gqlBody = { query: interpolate(gql.query, vars) };
890
+ const rawVars = gql.variables?.trim();
891
+ if (rawVars) {
892
+ try {
893
+ gqlBody.variables = JSON.parse(interpolate(rawVars, vars));
894
+ } catch {
895
+ }
896
+ }
897
+ if (gql.operationName?.trim()) gqlBody.operationName = gql.operationName.trim();
898
+ body = JSON.stringify(gqlBody);
899
+ } else if (req.body.mode === "soap" && req.body.soap) {
900
+ const soap = req.body.soap;
901
+ body = interpolate(soap.envelope, vars);
902
+ }
903
+ const methodHasBody = !["GET", "HEAD"].includes(req.method);
904
+ const doFetch = async (overrideHeaders) => {
905
+ const h = overrideHeaders ?? buildHeaders();
906
+ if (body !== void 0) {
907
+ if (!h.has("content-type")) {
908
+ if (req.body.mode === "json" || req.body.mode === "graphql") h.set("Content-Type", "application/json");
909
+ else if (req.body.mode === "form") h.set("Content-Type", "application/x-www-form-urlencoded");
910
+ else if (req.body.mode === "raw") h.set("Content-Type", req.body.rawContentType ?? "text/plain");
911
+ else if (req.body.mode === "soap") h.set("Content-Type", "text/xml; charset=utf-8");
912
+ }
913
+ if (req.body.mode === "soap" && req.body.soap?.soapAction && !h.has("soapaction")) {
914
+ h.set("SOAPAction", req.body.soap.soapAction);
915
+ }
916
+ }
917
+ const capturedHeaders = {};
918
+ h.forEach((value, key) => {
919
+ capturedHeaders[key] = value;
920
+ });
921
+ sentRequest = { method: req.method, url: finalUrl, headers: capturedHeaders, body: methodHasBody ? body : void 0 };
922
+ return undici.fetch(finalUrl, {
923
+ method: req.method,
924
+ headers: h,
925
+ body: methodHasBody ? body : void 0,
926
+ dispatcher
927
+ });
928
+ };
929
+ let fetchResp;
930
+ if (req.auth.type === "ntlm") {
931
+ await performNtlmRequest(finalUrl, req.method, req.auth, vars);
932
+ fetchResp = await doFetch();
933
+ } else if (req.auth.type === "digest") {
934
+ const probeFetch = async (url, init) => {
935
+ return undici.fetch(url, {
936
+ ...init,
937
+ dispatcher
938
+ });
939
+ };
940
+ const digestHeader = await performDigestAuth(finalUrl, req.method, req.auth, vars, probeFetch);
941
+ const h = buildHeaders();
942
+ if (digestHeader) h.set("Authorization", digestHeader);
943
+ fetchResp = await doFetch(h);
944
+ } else {
945
+ fetchResp = await doFetch();
946
+ }
947
+ const responseBody = await fetchResp.text();
948
+ const durationMs = Date.now() - start;
949
+ const rawResponseHeaders = {};
950
+ fetchResp.headers.forEach((value, key) => {
951
+ rawResponseHeaders[key] = value;
952
+ });
953
+ const maskedBody = maskPii(responseBody, piiMaskPatterns);
954
+ const maskedHeaders = maskHeaders(rawResponseHeaders, piiMaskPatterns);
955
+ response = {
956
+ status: fetchResp.status,
957
+ statusText: fetchResp.statusText,
958
+ headers: maskedHeaders,
959
+ body: maskedBody,
960
+ bodySize: Buffer.byteLength(responseBody, "utf8"),
961
+ durationMs
962
+ };
963
+ } catch (err) {
964
+ response = {
965
+ status: 0,
966
+ statusText: "Error",
967
+ headers: {},
968
+ body: "",
969
+ bodySize: 0,
970
+ durationMs: Date.now() - start,
971
+ error: err instanceof Error ? err.message : String(err)
972
+ };
973
+ }
974
+ let postTestResults = [];
975
+ let postConsole = [];
976
+ let postError;
977
+ if (req.postRequestScript?.trim() && !response.error) {
978
+ const result = await runScript(req.postRequestScript, {
979
+ envVars: { ...updatedEnvVars },
980
+ collectionVars: { ...updatedCollectionVars },
981
+ globals: { ...updatedGlobals },
982
+ localVars: { ...localVars },
983
+ response
984
+ });
985
+ postTestResults = result.testResults;
986
+ postConsole = result.consoleOutput;
987
+ postError = result.error;
988
+ updatedEnvVars = result.updatedEnvVars;
989
+ updatedCollectionVars = result.updatedCollectionVars;
990
+ updatedGlobals = result.updatedGlobals;
991
+ patchGlobals(result.updatedGlobals);
992
+ await persistGlobals();
993
+ }
994
+ const scriptResult = {
995
+ testResults: postTestResults,
996
+ consoleOutput: [...decryptionWarnings, ...preScriptMeta.consoleOutput, ...postConsole],
997
+ updatedEnvVars,
998
+ updatedCollectionVars,
999
+ updatedGlobals,
1000
+ resolvedUrl,
1001
+ preScriptError: preScriptMeta.error,
1002
+ postScriptError: postError
1003
+ };
1004
+ return { response, scriptResult, sentRequest: redactSentRequest(sentRequest) };
1005
+ });
1006
+ ipc.handle("script:run-hook", async (_e, payload) => {
1007
+ const { script, envVars, collectionVars, globals: globals2 } = payload;
1008
+ const result = await runScript(script, { envVars, collectionVars, globals: globals2, localVars: {} });
1009
+ patchGlobals(result.updatedGlobals);
1010
+ await persistGlobals();
1011
+ return {
1012
+ updatedEnvVars: result.updatedEnvVars,
1013
+ updatedCollectionVars: result.updatedCollectionVars,
1014
+ updatedGlobals: result.updatedGlobals,
1015
+ consoleOutput: result.consoleOutput,
1016
+ error: result.error
1017
+ };
1018
+ });
1019
+ }
1020
+ exports.buildAuthHeaders = buildAuthHeaders;
1021
+ exports.buildDispatcher = buildDispatcher;
1022
+ exports.buildDynamicVars = buildDynamicVars;
1023
+ exports.buildEnvVars = buildEnvVars;
1024
+ exports.buildUrl = buildUrl;
1025
+ exports.fetchOAuth2Token = fetchOAuth2Token;
1026
+ exports.getGlobals = getGlobals;
1027
+ exports.getSecret = getSecret;
1028
+ exports.initSecretStore = initSecretStore;
1029
+ exports.interpolate = interpolate;
1030
+ exports.loadGlobals = loadGlobals;
1031
+ exports.maskHeaders = maskHeaders;
1032
+ exports.maskPii = maskPii;
1033
+ exports.mergeVars = mergeVars;
1034
+ exports.patchGlobals = patchGlobals;
1035
+ exports.performDigestAuth = performDigestAuth;
1036
+ exports.performNtlmRequest = performNtlmRequest;
1037
+ exports.persistGlobals = persistGlobals;
1038
+ exports.registerRequestHandler = registerRequestHandler;
1039
+ exports.registerSecretHandlers = registerSecretHandlers;
1040
+ exports.runScript = runScript;
1041
+ exports.setGlobals = setGlobals;