@testsmith/api-spector 0.4.7 → 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.
@@ -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-BXx842MN.js");
5
- require("./handle-rimXXdJH.js");
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-rimXXdJH.js");
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.envRef) {
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 hmac = crypto__namespace.createHmac(algorithm, keyBytes).update(counterBuf).digest();
503
- const offset = hmac[hmac.length - 1] & 15;
504
- const binCode = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
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 accessToken = String(json["access_token"] ?? "");
1201
- if (!accessToken) throw new Error("OAuth 2.0: token response missing access_token.");
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, vars, resolvedUrl, dispatcher, proxy, tls, onSent } = opts;
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;