@testsmith/api-spector 0.4.8 → 0.4.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.
- package/out/main/chunks/{handle-rimXXdJH.js → handle-CF2LjTGV.js} +4 -0
- package/out/main/chunks/{import-DcenB5Q_.js → import-ChCkdHOB.js} +2 -2
- package/out/main/chunks/{request-exec-BH-M3KqZ.js → request-exec-DeXgxps8.js} +544 -9
- package/out/main/chunks/{snapshots-CtYxkSLz.js → snapshots-8wLrfQcq.js} +23 -21
- package/out/main/chunks/{soap-handler-BXx842MN.js → soap-handler-pOrJ625E.js} +2 -2
- package/out/main/contract.js +73 -149
- package/out/main/index.js +89 -6
- package/out/main/lib.js +11 -2
- package/out/main/runner.js +9 -4
- package/out/main/wsdl.js +3 -3
- package/out/preload/index.js +6 -0
- package/out/renderer/assets/index-CFvibipL.css +2 -0
- package/out/renderer/assets/{index-CEKgq6t0.js → index-b63nUtxA.js} +255 -35
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/readme.md +20 -0
- package/out/renderer/assets/index-B_xyaTDo.css +0 -2
|
@@ -75,6 +75,10 @@ const IPC = {
|
|
|
75
75
|
startFlow: "oauth2:startFlow",
|
|
76
76
|
refreshToken: "oauth2:refreshToken"
|
|
77
77
|
},
|
|
78
|
+
// ─── HashiCorp Vault (interactive OIDC login) ──────────────────────────────
|
|
79
|
+
vault: {
|
|
80
|
+
oidcLogin: "vault:oidcLogin"
|
|
81
|
+
},
|
|
78
82
|
// ─── Mock servers ──────────────────────────────────────────────────────────
|
|
79
83
|
mock: {
|
|
80
84
|
start: "mock:start",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const uuid = require("uuid");
|
|
4
|
-
const soapHandler = require("./soap-handler-
|
|
5
|
-
require("./handle-
|
|
4
|
+
const soapHandler = require("./soap-handler-pOrJ625E.js");
|
|
5
|
+
require("./handle-CF2LjTGV.js");
|
|
6
6
|
require("https");
|
|
7
7
|
require("http");
|
|
8
8
|
require("@xmldom/xmldom");
|
|
@@ -24,7 +24,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
const undici = require("undici");
|
|
25
25
|
const promises = require("fs/promises");
|
|
26
26
|
const nodeTls = require("tls");
|
|
27
|
-
const handle = require("./handle-
|
|
27
|
+
const handle = require("./handle-CF2LjTGV.js");
|
|
28
|
+
const node_fs = require("node:fs");
|
|
29
|
+
const node_os = require("node:os");
|
|
30
|
+
const node_path = require("node:path");
|
|
31
|
+
const node_crypto = require("node:crypto");
|
|
28
32
|
const crypto = require("crypto");
|
|
29
33
|
const path = require("path");
|
|
30
34
|
const dayjs = require("dayjs");
|
|
@@ -81,6 +85,463 @@ function setGlobals(next) {
|
|
|
81
85
|
function patchGlobals(patch) {
|
|
82
86
|
globals = { ...globals, ...patch };
|
|
83
87
|
}
|
|
88
|
+
const providers = /* @__PURE__ */ new Map();
|
|
89
|
+
function registerSecretProvider(provider) {
|
|
90
|
+
providers.set(provider.scheme.toLowerCase(), provider);
|
|
91
|
+
}
|
|
92
|
+
function registeredSchemes() {
|
|
93
|
+
return [...providers.keys()];
|
|
94
|
+
}
|
|
95
|
+
function schemeOf(ref) {
|
|
96
|
+
const m = /^([a-z][a-z0-9+.-]*):/i.exec(ref);
|
|
97
|
+
if (!m) return null;
|
|
98
|
+
const scheme = m[1].toLowerCase();
|
|
99
|
+
return providers.has(scheme) ? scheme : null;
|
|
100
|
+
}
|
|
101
|
+
function hasSecretScheme(ref) {
|
|
102
|
+
return schemeOf(ref) !== null;
|
|
103
|
+
}
|
|
104
|
+
async function resolveExternalSecret(ref, ctx = {}) {
|
|
105
|
+
const scheme = schemeOf(ref);
|
|
106
|
+
if (!scheme) return null;
|
|
107
|
+
const provider = providers.get(scheme);
|
|
108
|
+
const refBody = ref.slice(scheme.length + 1);
|
|
109
|
+
return provider.resolve(refBody, ctx);
|
|
110
|
+
}
|
|
111
|
+
let current;
|
|
112
|
+
function setSecretsConfig(settings) {
|
|
113
|
+
current = settings;
|
|
114
|
+
}
|
|
115
|
+
function getSecretsConfig() {
|
|
116
|
+
return current;
|
|
117
|
+
}
|
|
118
|
+
function env$3(...names) {
|
|
119
|
+
for (const n of names) {
|
|
120
|
+
const v = process.env[n];
|
|
121
|
+
if (v !== void 0 && v !== "") return v;
|
|
122
|
+
}
|
|
123
|
+
return void 0;
|
|
124
|
+
}
|
|
125
|
+
function readFileTrim(path2) {
|
|
126
|
+
try {
|
|
127
|
+
return node_fs.readFileSync(path2, "utf8").trim() || void 0;
|
|
128
|
+
} catch {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function resolveConn$3() {
|
|
133
|
+
const cfg = getSecretsConfig()?.vault ?? {};
|
|
134
|
+
const address = env$3("VAULT_ADDR", "API_SPECTOR_VAULT_ADDR") ?? cfg.address;
|
|
135
|
+
if (!address) {
|
|
136
|
+
throw new Error("Vault is not configured: set VAULT_ADDR (or settings.secrets.vault.address)");
|
|
137
|
+
}
|
|
138
|
+
const token = env$3("VAULT_TOKEN", "API_SPECTOR_VAULT_TOKEN") ?? readFileTrim(node_path.join(node_os.homedir(), ".vault-token"));
|
|
139
|
+
const roleId = env$3("VAULT_ROLE_ID", "API_SPECTOR_VAULT_ROLE_ID") ?? cfg.roleId;
|
|
140
|
+
const secretId = env$3("VAULT_SECRET_ID", "API_SPECTOR_VAULT_SECRET_ID");
|
|
141
|
+
const jwt = env$3("VAULT_JWT", "API_SPECTOR_VAULT_JWT") ?? (env$3("VAULT_JWT_PATH", "API_SPECTOR_VAULT_JWT_PATH") ? readFileTrim(env$3("VAULT_JWT_PATH", "API_SPECTOR_VAULT_JWT_PATH")) : void 0);
|
|
142
|
+
const jwtRole = env$3("VAULT_JWT_ROLE", "API_SPECTOR_VAULT_JWT_ROLE") ?? cfg.jwtRole;
|
|
143
|
+
const explicit = env$3("VAULT_AUTH_METHOD", "API_SPECTOR_VAULT_AUTH_METHOD") ?? cfg.authMethod;
|
|
144
|
+
const authMethod = explicit ?? (token ? "token" : roleId ? "approle" : jwt ? "jwt" : "token");
|
|
145
|
+
return {
|
|
146
|
+
address: address.replace(/\/+$/, ""),
|
|
147
|
+
namespace: env$3("VAULT_NAMESPACE", "API_SPECTOR_VAULT_NAMESPACE") ?? cfg.namespace,
|
|
148
|
+
authMethod,
|
|
149
|
+
token,
|
|
150
|
+
roleId,
|
|
151
|
+
secretId,
|
|
152
|
+
jwt,
|
|
153
|
+
jwtRole,
|
|
154
|
+
loginMount: cfg.loginMount,
|
|
155
|
+
kvVersion: env$3("VAULT_KV_VERSION", "API_SPECTOR_VAULT_KV_VERSION") ?? cfg.kvVersion ?? "auto",
|
|
156
|
+
skipVerify: (env$3("VAULT_SKIP_VERIFY", "API_SPECTOR_VAULT_SKIP_VERIFY") ?? "").toLowerCase() === "true" || cfg.skipVerify === true,
|
|
157
|
+
caPath: env$3("VAULT_CACERT", "API_SPECTOR_VAULT_CACERT")
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
let cachedToken$1 = null;
|
|
161
|
+
const secretCache$1 = /* @__PURE__ */ new Map();
|
|
162
|
+
let _dispatcher;
|
|
163
|
+
let _dispatcherKey = "";
|
|
164
|
+
async function dispatcherFor(conn) {
|
|
165
|
+
if (!conn.skipVerify && !conn.caPath) return void 0;
|
|
166
|
+
const key = `${conn.skipVerify}|${conn.caPath ?? ""}`;
|
|
167
|
+
if (_dispatcher && _dispatcherKey === key) return _dispatcher;
|
|
168
|
+
const { Agent } = await import("undici");
|
|
169
|
+
const connect = {};
|
|
170
|
+
if (conn.skipVerify) connect.rejectUnauthorized = false;
|
|
171
|
+
if (conn.caPath) {
|
|
172
|
+
const ca = readFileTrim(conn.caPath);
|
|
173
|
+
if (ca) connect.ca = ca;
|
|
174
|
+
}
|
|
175
|
+
_dispatcher = new Agent({ connect });
|
|
176
|
+
_dispatcherKey = key;
|
|
177
|
+
return _dispatcher;
|
|
178
|
+
}
|
|
179
|
+
async function vaultFetch(conn, path2, init = {}) {
|
|
180
|
+
const headers = { ...init.headers };
|
|
181
|
+
if (conn.namespace) headers["X-Vault-Namespace"] = conn.namespace;
|
|
182
|
+
if (init.token) headers["X-Vault-Token"] = init.token;
|
|
183
|
+
const dispatcher = await dispatcherFor(conn);
|
|
184
|
+
const res = await fetch(`${conn.address}/v1/${path2}`, {
|
|
185
|
+
...init,
|
|
186
|
+
headers,
|
|
187
|
+
// undici honours `dispatcher`; the field is not in the DOM RequestInit type.
|
|
188
|
+
...dispatcher ? { dispatcher } : {}
|
|
189
|
+
});
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
const body = await res.text().catch(() => "");
|
|
192
|
+
throw new Error(`Vault ${res.status} on /v1/${path2}: ${body.slice(0, 300)}`);
|
|
193
|
+
}
|
|
194
|
+
return res.json();
|
|
195
|
+
}
|
|
196
|
+
async function clientToken(conn) {
|
|
197
|
+
if (conn.authMethod === "token") {
|
|
198
|
+
if (!conn.token) {
|
|
199
|
+
throw new Error("Vault token auth selected but no token found (set VAULT_TOKEN or run `vault login`)");
|
|
200
|
+
}
|
|
201
|
+
return conn.token;
|
|
202
|
+
}
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
if (cachedToken$1 && cachedToken$1.expiresAt > now + 5e3) return cachedToken$1.value;
|
|
205
|
+
let mount;
|
|
206
|
+
let body;
|
|
207
|
+
if (conn.authMethod === "approle") {
|
|
208
|
+
if (!conn.roleId || !conn.secretId) {
|
|
209
|
+
throw new Error("Vault AppRole auth needs VAULT_ROLE_ID and VAULT_SECRET_ID");
|
|
210
|
+
}
|
|
211
|
+
mount = conn.loginMount ?? "approle";
|
|
212
|
+
body = { role_id: conn.roleId, secret_id: conn.secretId };
|
|
213
|
+
} else {
|
|
214
|
+
if (!conn.jwt || !conn.jwtRole) {
|
|
215
|
+
throw new Error("Vault JWT/OIDC auth needs a JWT (VAULT_JWT or VAULT_JWT_PATH) and a role (VAULT_JWT_ROLE)");
|
|
216
|
+
}
|
|
217
|
+
mount = conn.loginMount ?? "jwt";
|
|
218
|
+
body = { role: conn.jwtRole, jwt: conn.jwt };
|
|
219
|
+
}
|
|
220
|
+
const json = await vaultFetch(conn, `auth/${mount}/login`, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: { "content-type": "application/json" },
|
|
223
|
+
body: JSON.stringify(body)
|
|
224
|
+
});
|
|
225
|
+
const token = json?.auth?.client_token;
|
|
226
|
+
if (!token) throw new Error("Vault login did not return a client token");
|
|
227
|
+
const ttl = Number(json?.auth?.lease_duration ?? 0);
|
|
228
|
+
cachedToken$1 = { value: token, expiresAt: ttl > 0 ? now + ttl * 1e3 : now + 5 * 6e4 };
|
|
229
|
+
return token;
|
|
230
|
+
}
|
|
231
|
+
async function readSecret(refBody) {
|
|
232
|
+
const hash = refBody.lastIndexOf("#");
|
|
233
|
+
if (hash < 0) {
|
|
234
|
+
throw new Error(`Vault reference must be 'vault:<path>#<key>' (got 'vault:${refBody}')`);
|
|
235
|
+
}
|
|
236
|
+
const path2 = refBody.slice(0, hash).replace(/^\/+/, "");
|
|
237
|
+
const key = refBody.slice(hash + 1);
|
|
238
|
+
if (!path2 || !key) {
|
|
239
|
+
throw new Error(`Vault reference must be 'vault:<path>#<key>' (got 'vault:${refBody}')`);
|
|
240
|
+
}
|
|
241
|
+
const conn = resolveConn$3();
|
|
242
|
+
const cacheKey = `${conn.address}|${conn.namespace ?? ""}|${path2}`;
|
|
243
|
+
const now = Date.now();
|
|
244
|
+
let bag;
|
|
245
|
+
const hit = secretCache$1.get(cacheKey);
|
|
246
|
+
if (hit && hit.expiresAt > now) {
|
|
247
|
+
bag = hit.bag;
|
|
248
|
+
} else {
|
|
249
|
+
const json = await vaultFetch(conn, path2, { token: await clientToken(conn) });
|
|
250
|
+
const isV2 = conn.kvVersion === "2" || conn.kvVersion === "auto" && json?.data?.data !== void 0 && json?.data?.metadata !== void 0;
|
|
251
|
+
bag = isV2 ? json?.data?.data : json?.data;
|
|
252
|
+
const lease = Number(json?.lease_duration ?? 0);
|
|
253
|
+
if (bag) {
|
|
254
|
+
secretCache$1.set(cacheKey, { bag, expiresAt: lease > 0 ? now + lease * 1e3 : now + 5 * 6e4 });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!bag || !(key in bag)) {
|
|
258
|
+
throw new Error(`Vault secret at '${path2}' has no key '${key}'`);
|
|
259
|
+
}
|
|
260
|
+
return String(bag[key]);
|
|
261
|
+
}
|
|
262
|
+
const vaultProvider = {
|
|
263
|
+
scheme: "vault",
|
|
264
|
+
resolve: (refBody) => readSecret(refBody)
|
|
265
|
+
};
|
|
266
|
+
function hmac(key, data) {
|
|
267
|
+
return node_crypto.createHmac("sha256", key).update(data, "utf8").digest();
|
|
268
|
+
}
|
|
269
|
+
function sha256hex(data) {
|
|
270
|
+
return node_crypto.createHash("sha256").update(data, "utf8").digest("hex");
|
|
271
|
+
}
|
|
272
|
+
function signSigV4(input) {
|
|
273
|
+
const amzDate = input.now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
274
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
275
|
+
const headers = { ...input.headers, host: input.host, "x-amz-date": amzDate };
|
|
276
|
+
if (input.sessionToken) headers["x-amz-security-token"] = input.sessionToken;
|
|
277
|
+
const lower = {};
|
|
278
|
+
for (const k of Object.keys(headers)) lower[k.toLowerCase()] = headers[k].trim().replace(/\s+/g, " ");
|
|
279
|
+
const sortedKeys = Object.keys(lower).sort();
|
|
280
|
+
const canonicalHeaders = sortedKeys.map((k) => `${k}:${lower[k]}
|
|
281
|
+
`).join("");
|
|
282
|
+
const signedHeaders = sortedKeys.join(";");
|
|
283
|
+
const payloadHash = sha256hex(input.body);
|
|
284
|
+
const canonicalRequest = [input.method, input.path, input.query ?? "", canonicalHeaders, signedHeaders, payloadHash].join("\n");
|
|
285
|
+
const scope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
|
|
286
|
+
const stringToSign = ["AWS4-HMAC-SHA256", amzDate, scope, sha256hex(canonicalRequest)].join("\n");
|
|
287
|
+
const kDate = hmac(`AWS4${input.secretAccessKey}`, dateStamp);
|
|
288
|
+
const kRegion = hmac(kDate, input.region);
|
|
289
|
+
const kService = hmac(kRegion, input.service);
|
|
290
|
+
const kSigning = hmac(kService, "aws4_request");
|
|
291
|
+
const signature = node_crypto.createHmac("sha256", kSigning).update(stringToSign, "utf8").digest("hex");
|
|
292
|
+
return {
|
|
293
|
+
...headers,
|
|
294
|
+
Authorization: `AWS4-HMAC-SHA256 Credential=${input.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function env$2(...names) {
|
|
298
|
+
for (const n of names) {
|
|
299
|
+
const v = process.env[n];
|
|
300
|
+
if (v !== void 0 && v !== "") return v;
|
|
301
|
+
}
|
|
302
|
+
return void 0;
|
|
303
|
+
}
|
|
304
|
+
function resolveConn$2() {
|
|
305
|
+
const cfg = getSecretsConfig()?.aws ?? {};
|
|
306
|
+
const region = env$2("AWS_REGION", "AWS_DEFAULT_REGION", "API_SPECTOR_AWS_REGION") ?? cfg.region;
|
|
307
|
+
const accessKeyId = env$2("AWS_ACCESS_KEY_ID");
|
|
308
|
+
const secretAccessKey = env$2("AWS_SECRET_ACCESS_KEY");
|
|
309
|
+
if (!region) throw new Error("AWS Secrets Manager: no region (set AWS_REGION or settings.secrets.aws.region)");
|
|
310
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
311
|
+
throw new Error("AWS Secrets Manager: no credentials (set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY)");
|
|
312
|
+
}
|
|
313
|
+
return { region, accessKeyId, secretAccessKey, sessionToken: env$2("AWS_SESSION_TOKEN") };
|
|
314
|
+
}
|
|
315
|
+
const cache = /* @__PURE__ */ new Map();
|
|
316
|
+
async function getSecretString(conn, secretId) {
|
|
317
|
+
const cacheKey = `${conn.region}|${secretId}`;
|
|
318
|
+
const now = Date.now();
|
|
319
|
+
const hit = cache.get(cacheKey);
|
|
320
|
+
if (hit && hit.expiresAt > now) return hit.raw;
|
|
321
|
+
const host = `secretsmanager.${conn.region}.amazonaws.com`;
|
|
322
|
+
const body = JSON.stringify({ SecretId: secretId });
|
|
323
|
+
const headers = signSigV4({
|
|
324
|
+
method: "POST",
|
|
325
|
+
host,
|
|
326
|
+
path: "/",
|
|
327
|
+
headers: {
|
|
328
|
+
"content-type": "application/x-amz-json-1.1",
|
|
329
|
+
"x-amz-target": "secretsmanager.GetSecretValue"
|
|
330
|
+
},
|
|
331
|
+
body,
|
|
332
|
+
service: "secretsmanager",
|
|
333
|
+
region: conn.region,
|
|
334
|
+
accessKeyId: conn.accessKeyId,
|
|
335
|
+
secretAccessKey: conn.secretAccessKey,
|
|
336
|
+
sessionToken: conn.sessionToken,
|
|
337
|
+
now: new Date(now)
|
|
338
|
+
});
|
|
339
|
+
const res = await fetch(`https://${host}/`, { method: "POST", headers, body });
|
|
340
|
+
if (!res.ok) {
|
|
341
|
+
const text = await res.text().catch(() => "");
|
|
342
|
+
throw new Error(`AWS Secrets Manager ${res.status} for '${secretId}': ${text.slice(0, 300)}`);
|
|
343
|
+
}
|
|
344
|
+
const json = await res.json();
|
|
345
|
+
const raw = json?.SecretString;
|
|
346
|
+
if (raw === void 0) throw new Error(`AWS secret '${secretId}' has no SecretString (binary secrets are not supported)`);
|
|
347
|
+
cache.set(cacheKey, { raw, expiresAt: now + 5 * 6e4 });
|
|
348
|
+
return raw;
|
|
349
|
+
}
|
|
350
|
+
async function resolve$2(refBody) {
|
|
351
|
+
const hash = refBody.lastIndexOf("#");
|
|
352
|
+
const secretId = hash < 0 ? refBody : refBody.slice(0, hash);
|
|
353
|
+
const key = hash < 0 ? null : refBody.slice(hash + 1);
|
|
354
|
+
if (!secretId) throw new Error(`AWS reference must be 'aws:<secretId>[#<key>]' (got 'aws:${refBody}')`);
|
|
355
|
+
const raw = await getSecretString(resolveConn$2(), secretId);
|
|
356
|
+
if (key === null) return raw;
|
|
357
|
+
let parsed;
|
|
358
|
+
try {
|
|
359
|
+
parsed = JSON.parse(raw);
|
|
360
|
+
} catch {
|
|
361
|
+
throw new Error(`AWS secret '${secretId}' is not JSON, so key '${key}' cannot be read (drop the '#${key}')`);
|
|
362
|
+
}
|
|
363
|
+
if (!(key in parsed)) throw new Error(`AWS secret '${secretId}' has no key '${key}'`);
|
|
364
|
+
return String(parsed[key]);
|
|
365
|
+
}
|
|
366
|
+
const awsSecretsProvider = {
|
|
367
|
+
scheme: "aws",
|
|
368
|
+
resolve: (refBody) => resolve$2(refBody)
|
|
369
|
+
};
|
|
370
|
+
function env$1(...names) {
|
|
371
|
+
for (const n of names) {
|
|
372
|
+
const v = process.env[n];
|
|
373
|
+
if (v !== void 0 && v !== "") return v;
|
|
374
|
+
}
|
|
375
|
+
return void 0;
|
|
376
|
+
}
|
|
377
|
+
function resolveConn$1() {
|
|
378
|
+
const cfg = getSecretsConfig()?.azure ?? {};
|
|
379
|
+
const tenantId = env$1("AZURE_TENANT_ID", "API_SPECTOR_AZURE_TENANT_ID") ?? cfg.tenantId;
|
|
380
|
+
const clientId = env$1("AZURE_CLIENT_ID", "API_SPECTOR_AZURE_CLIENT_ID") ?? cfg.clientId;
|
|
381
|
+
const clientSecret = env$1("AZURE_CLIENT_SECRET", "API_SPECTOR_AZURE_CLIENT_SECRET");
|
|
382
|
+
if (!tenantId || !clientId || !clientSecret) {
|
|
383
|
+
throw new Error("Azure Key Vault: set AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET");
|
|
384
|
+
}
|
|
385
|
+
return { tenantId, clientId, clientSecret, defaultVault: cfg.vault };
|
|
386
|
+
}
|
|
387
|
+
let cachedToken = null;
|
|
388
|
+
const secretCache = /* @__PURE__ */ new Map();
|
|
389
|
+
async function accessToken(conn) {
|
|
390
|
+
const now = Date.now();
|
|
391
|
+
if (cachedToken && cachedToken.expiresAt > now + 3e4) return cachedToken.value;
|
|
392
|
+
const form = new URLSearchParams({
|
|
393
|
+
grant_type: "client_credentials",
|
|
394
|
+
client_id: conn.clientId,
|
|
395
|
+
client_secret: conn.clientSecret,
|
|
396
|
+
scope: "https://vault.azure.net/.default"
|
|
397
|
+
});
|
|
398
|
+
const res = await fetch(`https://login.microsoftonline.com/${conn.tenantId}/oauth2/v2.0/token`, {
|
|
399
|
+
method: "POST",
|
|
400
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
401
|
+
body: form.toString()
|
|
402
|
+
});
|
|
403
|
+
if (!res.ok) {
|
|
404
|
+
const text = await res.text().catch(() => "");
|
|
405
|
+
throw new Error(`Azure AD token ${res.status}: ${text.slice(0, 300)}`);
|
|
406
|
+
}
|
|
407
|
+
const json = await res.json();
|
|
408
|
+
const token = json?.access_token;
|
|
409
|
+
if (!token) throw new Error("Azure AD did not return an access token");
|
|
410
|
+
const ttl = Number(json?.expires_in ?? 0);
|
|
411
|
+
cachedToken = { value: token, expiresAt: ttl > 0 ? now + ttl * 1e3 : now + 5 * 6e4 };
|
|
412
|
+
return token;
|
|
413
|
+
}
|
|
414
|
+
function secretUrl(refBody, defaultVault) {
|
|
415
|
+
if (/^https?:\/\//i.test(refBody)) {
|
|
416
|
+
return refBody.split("#")[0];
|
|
417
|
+
}
|
|
418
|
+
const parts = refBody.replace(/^\/+/, "").split("/");
|
|
419
|
+
let vault;
|
|
420
|
+
let name;
|
|
421
|
+
let version = "";
|
|
422
|
+
if (parts.length >= 2) {
|
|
423
|
+
[vault, name, version = ""] = parts;
|
|
424
|
+
} else if (parts.length === 1 && defaultVault) {
|
|
425
|
+
vault = defaultVault;
|
|
426
|
+
name = parts[0];
|
|
427
|
+
} else {
|
|
428
|
+
throw new Error(`Azure reference must be 'azure:<vault>/<secret>[/<version>]' (got 'azure:${refBody}')`);
|
|
429
|
+
}
|
|
430
|
+
const base = /^https?:\/\//i.test(vault) ? vault.replace(/\/+$/, "") : `https://${vault}.vault.azure.net`;
|
|
431
|
+
const path2 = version ? `secrets/${name}/${version}` : `secrets/${name}`;
|
|
432
|
+
return `${base}/${path2}?api-version=7.4`;
|
|
433
|
+
}
|
|
434
|
+
async function resolve$1(refBody) {
|
|
435
|
+
const conn = resolveConn$1();
|
|
436
|
+
const url = secretUrl(refBody, conn.defaultVault);
|
|
437
|
+
const now = Date.now();
|
|
438
|
+
const hit = secretCache.get(url);
|
|
439
|
+
if (hit && hit.expiresAt > now) return hit.value;
|
|
440
|
+
const res = await fetch(url, { headers: { authorization: `Bearer ${await accessToken(conn)}` } });
|
|
441
|
+
if (!res.ok) {
|
|
442
|
+
const text = await res.text().catch(() => "");
|
|
443
|
+
throw new Error(`Azure Key Vault ${res.status} for '${refBody}': ${text.slice(0, 300)}`);
|
|
444
|
+
}
|
|
445
|
+
const json = await res.json();
|
|
446
|
+
const value = json?.value;
|
|
447
|
+
if (value === void 0) throw new Error(`Azure Key Vault secret '${refBody}' has no value`);
|
|
448
|
+
secretCache.set(url, { value, expiresAt: now + 5 * 6e4 });
|
|
449
|
+
return value;
|
|
450
|
+
}
|
|
451
|
+
const azureKeyVaultProvider = {
|
|
452
|
+
scheme: "azure",
|
|
453
|
+
resolve: (refBody) => resolve$1(refBody)
|
|
454
|
+
};
|
|
455
|
+
function env(...names) {
|
|
456
|
+
for (const n of names) {
|
|
457
|
+
const v = process.env[n];
|
|
458
|
+
if (v !== void 0 && v !== "") return v;
|
|
459
|
+
}
|
|
460
|
+
return void 0;
|
|
461
|
+
}
|
|
462
|
+
function resolveConn() {
|
|
463
|
+
const cfg = getSecretsConfig()?.onePassword ?? {};
|
|
464
|
+
const host = env("OP_CONNECT_HOST", "API_SPECTOR_OP_CONNECT_HOST") ?? cfg.connectHost;
|
|
465
|
+
const token = env("OP_CONNECT_TOKEN", "API_SPECTOR_OP_CONNECT_TOKEN");
|
|
466
|
+
if (!host || !token) {
|
|
467
|
+
throw new Error("1Password: set OP_CONNECT_HOST and OP_CONNECT_TOKEN (1Password Connect)");
|
|
468
|
+
}
|
|
469
|
+
return { host: host.replace(/\/+$/, ""), token };
|
|
470
|
+
}
|
|
471
|
+
const itemCache = /* @__PURE__ */ new Map();
|
|
472
|
+
const idCache = /* @__PURE__ */ new Map();
|
|
473
|
+
async function opGet(conn, path2) {
|
|
474
|
+
const res = await fetch(`${conn.host}${path2}`, { headers: { authorization: `Bearer ${conn.token}` } });
|
|
475
|
+
if (!res.ok) {
|
|
476
|
+
const text = await res.text().catch(() => "");
|
|
477
|
+
throw new Error(`1Password Connect ${res.status} on ${path2}: ${text.slice(0, 300)}`);
|
|
478
|
+
}
|
|
479
|
+
return res.json();
|
|
480
|
+
}
|
|
481
|
+
const isId = (s) => /^[a-z0-9]{26}$/i.test(s);
|
|
482
|
+
async function vaultId(conn, vault) {
|
|
483
|
+
if (isId(vault)) return vault;
|
|
484
|
+
const cacheKey = `vaults:${vault}`;
|
|
485
|
+
const cached = idCache.get(cacheKey);
|
|
486
|
+
if (cached) return cached;
|
|
487
|
+
const list = await opGet(conn, `/v1/vaults?filter=${encodeURIComponent(`name eq "${vault}"`)}`);
|
|
488
|
+
const id = Array.isArray(list) && list[0]?.id;
|
|
489
|
+
if (!id) throw new Error(`1Password: vault '${vault}' not found`);
|
|
490
|
+
idCache.set(cacheKey, id);
|
|
491
|
+
return id;
|
|
492
|
+
}
|
|
493
|
+
async function itemId(conn, vid, item) {
|
|
494
|
+
if (isId(item)) return item;
|
|
495
|
+
const cacheKey = `items:${vid}:${item}`;
|
|
496
|
+
const cached = idCache.get(cacheKey);
|
|
497
|
+
if (cached) return cached;
|
|
498
|
+
const list = await opGet(conn, `/v1/vaults/${vid}/items?filter=${encodeURIComponent(`title eq "${item}"`)}`);
|
|
499
|
+
const id = Array.isArray(list) && list[0]?.id;
|
|
500
|
+
if (!id) throw new Error(`1Password: item '${item}' not found in vault`);
|
|
501
|
+
idCache.set(cacheKey, id);
|
|
502
|
+
return id;
|
|
503
|
+
}
|
|
504
|
+
async function resolve(refBody) {
|
|
505
|
+
const parts = refBody.replace(/^\/\//, "").replace(/^\/+/, "").split("/").filter(Boolean);
|
|
506
|
+
if (parts.length < 3) {
|
|
507
|
+
throw new Error(`1Password reference must be 'op://<vault>/<item>/<field>' (got 'op:${refBody}')`);
|
|
508
|
+
}
|
|
509
|
+
const [vault, item, ...rest] = parts;
|
|
510
|
+
const sectionLabel = rest.length >= 2 ? rest[0] : null;
|
|
511
|
+
const fieldLabel = rest[rest.length - 1];
|
|
512
|
+
const conn = resolveConn();
|
|
513
|
+
const vid = await vaultId(conn, vault);
|
|
514
|
+
const iid = await itemId(conn, vid, item);
|
|
515
|
+
const cacheKey = `${vid}/${iid}`;
|
|
516
|
+
const now = Date.now();
|
|
517
|
+
let fields;
|
|
518
|
+
const hit = itemCache.get(cacheKey);
|
|
519
|
+
if (hit && hit.expiresAt > now) {
|
|
520
|
+
fields = hit.fields;
|
|
521
|
+
} else {
|
|
522
|
+
const doc = await opGet(conn, `/v1/vaults/${vid}/items/${iid}`);
|
|
523
|
+
fields = Array.isArray(doc?.fields) ? doc.fields : [];
|
|
524
|
+
itemCache.set(cacheKey, { fields, expiresAt: now + 5 * 6e4 });
|
|
525
|
+
}
|
|
526
|
+
const match = fields.find((f) => {
|
|
527
|
+
const labelOk = String(f?.label ?? "").toLowerCase() === fieldLabel.toLowerCase() || f?.id === fieldLabel;
|
|
528
|
+
if (!labelOk) return false;
|
|
529
|
+
if (!sectionLabel) return true;
|
|
530
|
+
return String(f?.section?.label ?? "").toLowerCase() === sectionLabel.toLowerCase();
|
|
531
|
+
});
|
|
532
|
+
if (!match || match.value === void 0) {
|
|
533
|
+
throw new Error(`1Password: field '${fieldLabel}' not found on item '${item}'`);
|
|
534
|
+
}
|
|
535
|
+
return String(match.value);
|
|
536
|
+
}
|
|
537
|
+
const onePasswordProvider = {
|
|
538
|
+
scheme: "op",
|
|
539
|
+
resolve: (refBody) => resolve(refBody)
|
|
540
|
+
};
|
|
541
|
+
registerSecretProvider(vaultProvider);
|
|
542
|
+
registerSecretProvider(awsSecretsProvider);
|
|
543
|
+
registerSecretProvider(azureKeyVaultProvider);
|
|
544
|
+
registerSecretProvider(onePasswordProvider);
|
|
84
545
|
const MASTER_KEY_ENV = "API_SPECTOR_MASTER_KEY";
|
|
85
546
|
let secretStore = {};
|
|
86
547
|
let secretStorePath = null;
|
|
@@ -134,6 +595,9 @@ function decryptSecret(encrypted, salt, iv, password) {
|
|
|
134
595
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
135
596
|
}
|
|
136
597
|
async function getSecret(ref) {
|
|
598
|
+
if (hasSecretScheme(ref)) {
|
|
599
|
+
return resolveExternalSecret(ref);
|
|
600
|
+
}
|
|
137
601
|
const stored = secretStore[ref];
|
|
138
602
|
if (stored) {
|
|
139
603
|
const ss = getSafeStorage();
|
|
@@ -214,13 +678,33 @@ function buildUrl(baseUrl, params, vars) {
|
|
|
214
678
|
const qs = queryRows.map((p) => `${encodeURIComponent(interpolate(p.key, vars))}=${encodeURIComponent(interpolate(p.value, vars))}`).join("&");
|
|
215
679
|
return url + sep + qs;
|
|
216
680
|
}
|
|
681
|
+
function rebaseUrl(fullUrl, baseUrl) {
|
|
682
|
+
if (!baseUrl) return fullUrl;
|
|
683
|
+
try {
|
|
684
|
+
const orig = new URL(fullUrl, "http://placeholder.invalid");
|
|
685
|
+
const base = new URL(baseUrl);
|
|
686
|
+
const basePath = base.pathname.replace(/\/$/, "");
|
|
687
|
+
base.pathname = (basePath + orig.pathname).replace(/\/{2,}/g, "/");
|
|
688
|
+
base.search = orig.search;
|
|
689
|
+
return base.toString();
|
|
690
|
+
} catch {
|
|
691
|
+
return fullUrl;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
217
694
|
async function buildEnvVars(environment) {
|
|
218
695
|
const vars = {};
|
|
219
696
|
if (!environment) return vars;
|
|
220
697
|
const masterKey = process.env["API_SPECTOR_MASTER_KEY"];
|
|
221
698
|
for (const v of environment.variables) {
|
|
222
699
|
if (!v.enabled) continue;
|
|
223
|
-
if (v.
|
|
700
|
+
if (v.secretRef && hasSecretScheme(v.secretRef)) {
|
|
701
|
+
try {
|
|
702
|
+
const resolved = await resolveExternalSecret(v.secretRef);
|
|
703
|
+
if (resolved !== null) vars[v.key] = resolved;
|
|
704
|
+
} catch (err) {
|
|
705
|
+
console.warn(`[secrets] could not resolve ${v.key} (${v.secretRef}): ${err.message}`);
|
|
706
|
+
}
|
|
707
|
+
} else if (v.envRef) {
|
|
224
708
|
const envValue = process.env[v.envRef];
|
|
225
709
|
if (envValue !== void 0) vars[v.key] = envValue;
|
|
226
710
|
} else if (v.secret && v.secretEncrypted && v.secretSalt && v.secretIv) {
|
|
@@ -499,9 +983,9 @@ function buildAt(ctx, testResults, consoleOutput) {
|
|
|
499
983
|
const counterBuf = Buffer.alloc(8);
|
|
500
984
|
counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
|
|
501
985
|
counterBuf.writeUInt32BE(counter >>> 0, 4);
|
|
502
|
-
const
|
|
503
|
-
const offset =
|
|
504
|
-
const binCode = (
|
|
986
|
+
const hmac2 = crypto__namespace.createHmac(algorithm, keyBytes).update(counterBuf).digest();
|
|
987
|
+
const offset = hmac2[hmac2.length - 1] & 15;
|
|
988
|
+
const binCode = (hmac2[offset] & 127) << 24 | (hmac2[offset + 1] & 255) << 16 | (hmac2[offset + 2] & 255) << 8 | hmac2[offset + 3] & 255;
|
|
505
989
|
return String(binCode % 10 ** digits).padStart(digits, "0");
|
|
506
990
|
}
|
|
507
991
|
};
|
|
@@ -1197,12 +1681,12 @@ async function fetchOAuth2Token(auth, vars) {
|
|
|
1197
1681
|
throw new Error(`OAuth 2.0 token request failed (${resp.status}): ${body}`);
|
|
1198
1682
|
}
|
|
1199
1683
|
const json = await resp.json();
|
|
1200
|
-
const
|
|
1201
|
-
if (!
|
|
1684
|
+
const accessToken2 = String(json["access_token"] ?? "");
|
|
1685
|
+
if (!accessToken2) throw new Error("OAuth 2.0: token response missing access_token.");
|
|
1202
1686
|
const expiresIn = Number(json["expires_in"] ?? 3600);
|
|
1203
1687
|
const expiresAt = Date.now() + expiresIn * 1e3;
|
|
1204
1688
|
return {
|
|
1205
|
-
accessToken,
|
|
1689
|
+
accessToken: accessToken2,
|
|
1206
1690
|
expiresAt,
|
|
1207
1691
|
refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
|
|
1208
1692
|
};
|
|
@@ -1433,7 +1917,10 @@ function buildBodyAndApplyHeaders(req, vars, headers) {
|
|
|
1433
1917
|
return body;
|
|
1434
1918
|
}
|
|
1435
1919
|
async function performHttpExchange(opts) {
|
|
1436
|
-
const { req,
|
|
1920
|
+
const { req, resolvedUrl, dispatcher, proxy, tls, onSent } = opts;
|
|
1921
|
+
let vars = opts.vars;
|
|
1922
|
+
const inlineSecrets = await resolveInlineSecrets(req);
|
|
1923
|
+
if (Object.keys(inlineSecrets).length) vars = { ...vars, ...inlineSecrets };
|
|
1437
1924
|
const start = Date.now();
|
|
1438
1925
|
if (req.auth.type === "oauth2") {
|
|
1439
1926
|
const now = Date.now();
|
|
@@ -1547,6 +2034,29 @@ function syntheticHttpFailure(status, statusText) {
|
|
|
1547
2034
|
error: `Request returned ${status} - no assertion was defined to verify the status code.`
|
|
1548
2035
|
};
|
|
1549
2036
|
}
|
|
2037
|
+
async function resolveInlineSecrets(req) {
|
|
2038
|
+
const haystack = [
|
|
2039
|
+
req.url ?? "",
|
|
2040
|
+
JSON.stringify(req.params ?? []),
|
|
2041
|
+
JSON.stringify(req.headers ?? []),
|
|
2042
|
+
JSON.stringify(req.body ?? {}),
|
|
2043
|
+
JSON.stringify(req.auth ?? {}),
|
|
2044
|
+
req.preRequestScript ?? "",
|
|
2045
|
+
req.postRequestScript ?? ""
|
|
2046
|
+
].join("\n");
|
|
2047
|
+
const refs = /* @__PURE__ */ new Set();
|
|
2048
|
+
for (const m of haystack.matchAll(/\{\{\s*([^}]+?)\s*\}\}/g)) {
|
|
2049
|
+
const inner = m[1].trim();
|
|
2050
|
+
if (hasSecretScheme(inner)) refs.add(inner);
|
|
2051
|
+
}
|
|
2052
|
+
if (refs.size === 0) return {};
|
|
2053
|
+
const out = {};
|
|
2054
|
+
await Promise.all([...refs].map(async (ref) => {
|
|
2055
|
+
const value = await resolveExternalSecret(ref);
|
|
2056
|
+
if (value !== null) out[ref] = value;
|
|
2057
|
+
}));
|
|
2058
|
+
return out;
|
|
2059
|
+
}
|
|
1550
2060
|
async function executeRunnerRequest(opts) {
|
|
1551
2061
|
const { req, collectionVars, envVars, globals: globals2, dispatcher, piiMaskPatterns, proxy, tls, onScriptOutput } = opts;
|
|
1552
2062
|
let { localVars } = opts;
|
|
@@ -1564,6 +2074,24 @@ async function executeRunnerRequest(opts) {
|
|
|
1564
2074
|
let updatedCollectionVars = { ...collectionVars };
|
|
1565
2075
|
let updatedGlobals = { ...globals2 };
|
|
1566
2076
|
let preScriptError;
|
|
2077
|
+
let inlineSecrets = {};
|
|
2078
|
+
try {
|
|
2079
|
+
inlineSecrets = await resolveInlineSecrets(req);
|
|
2080
|
+
} catch (err) {
|
|
2081
|
+
return {
|
|
2082
|
+
result: {
|
|
2083
|
+
...base,
|
|
2084
|
+
status: "error",
|
|
2085
|
+
durationMs: 0,
|
|
2086
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2087
|
+
},
|
|
2088
|
+
updatedEnvVars,
|
|
2089
|
+
updatedCollectionVars,
|
|
2090
|
+
updatedGlobals,
|
|
2091
|
+
updatedLocalVars: localVars
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
if (Object.keys(inlineSecrets).length) vars = { ...vars, ...inlineSecrets };
|
|
1567
2095
|
if (req.preRequestScript?.trim()) {
|
|
1568
2096
|
const r = await runScript(interpolate(req.preRequestScript, vars), {
|
|
1569
2097
|
envVars: { ...envVars },
|
|
@@ -1581,6 +2109,7 @@ async function executeRunnerRequest(opts) {
|
|
|
1581
2109
|
await persistGlobals();
|
|
1582
2110
|
onScriptOutput?.("pre", r.consoleOutput, r.error);
|
|
1583
2111
|
vars = mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
|
|
2112
|
+
if (Object.keys(inlineSecrets).length) vars = { ...vars, ...inlineSecrets };
|
|
1584
2113
|
}
|
|
1585
2114
|
const resolvedUrl = buildUrl(req.url, req.params, vars);
|
|
1586
2115
|
base.resolvedUrl = resolvedUrl;
|
|
@@ -1725,6 +2254,7 @@ exports.deriveRunStatus = deriveRunStatus;
|
|
|
1725
2254
|
exports.executeRunnerRequest = executeRunnerRequest;
|
|
1726
2255
|
exports.getGlobals = getGlobals;
|
|
1727
2256
|
exports.getSecret = getSecret;
|
|
2257
|
+
exports.hasSecretScheme = hasSecretScheme;
|
|
1728
2258
|
exports.initSecretStore = initSecretStore;
|
|
1729
2259
|
exports.interpolate = interpolate;
|
|
1730
2260
|
exports.loadGlobals = loadGlobals;
|
|
@@ -1734,7 +2264,12 @@ exports.mergeVars = mergeVars;
|
|
|
1734
2264
|
exports.patchGlobals = patchGlobals;
|
|
1735
2265
|
exports.performHttpExchange = performHttpExchange;
|
|
1736
2266
|
exports.persistGlobals = persistGlobals;
|
|
2267
|
+
exports.rebaseUrl = rebaseUrl;
|
|
1737
2268
|
exports.registerSecretHandlers = registerSecretHandlers;
|
|
2269
|
+
exports.registerSecretProvider = registerSecretProvider;
|
|
2270
|
+
exports.registeredSchemes = registeredSchemes;
|
|
2271
|
+
exports.resolveExternalSecret = resolveExternalSecret;
|
|
1738
2272
|
exports.runScript = runScript;
|
|
1739
2273
|
exports.setGlobals = setGlobals;
|
|
2274
|
+
exports.setSecretsConfig = setSecretsConfig;
|
|
1740
2275
|
exports.syntheticHttpFailure = syntheticHttpFailure;
|