@testsmith/api-spector 0.7.3 → 0.7.5

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.
@@ -3,7 +3,7 @@ const promises = require("node:fs/promises");
3
3
  const path = require("node:path");
4
4
  const crypto = require("node:crypto");
5
5
  const undici = require("undici");
6
- const requestExec = require("./request-exec-CcLHtkwG.js");
6
+ const requestExec = require("./request-exec-DHxjokyP.js");
7
7
  const Ajv = require("ajv");
8
8
  const jsYaml = require("js-yaml");
9
9
  require("node:http");
@@ -197,6 +197,13 @@ function parseBody(body) {
197
197
  return body;
198
198
  }
199
199
  }
200
+ const MAX_CAPTURED_BODY = 2e4;
201
+ function clipBody(body) {
202
+ if (body == null) return { clipped: false };
203
+ if (body.length <= MAX_CAPTURED_BODY) return { text: body, clipped: false };
204
+ return { text: body.slice(0, MAX_CAPTURED_BODY) + `
205
+ … [truncated ${body.length - MAX_CAPTURED_BODY} more chars]`, clipped: true };
206
+ }
200
207
  async function runFlow(flow, deps, options = {}) {
201
208
  const startedAt = Date.now();
202
209
  const maxSteps = deps.maxSteps ?? 1e4;
@@ -396,6 +403,8 @@ async function runFlow(flow, deps, options = {}) {
396
403
  ctx.response = r.response;
397
404
  const ok = r.status === "passed" || r.status === "skipped";
398
405
  const reqStatus = r.status === "skipped" ? "done" : r.status === "pending" ? "running" : r.status;
406
+ const reqBody = clipBody(r.sentRequest?.body);
407
+ const resBody = clipBody(r.response?.body);
399
408
  record(block, reqStatus, {
400
409
  iteration,
401
410
  httpStatus: r.httpStatus,
@@ -403,7 +412,16 @@ async function runFlow(flow, deps, options = {}) {
403
412
  error: r.error,
404
413
  testResults: r.testResults,
405
414
  method: r.method,
406
- resolvedUrl: r.resolvedUrl
415
+ resolvedUrl: r.resolvedUrl,
416
+ http: {
417
+ reqHeaders: r.sentRequest?.headers,
418
+ reqBody: reqBody.text,
419
+ statusText: r.response?.statusText,
420
+ resHeaders: r.response?.headers,
421
+ resBody: resBody.text,
422
+ bodySize: r.response?.bodySize,
423
+ truncated: reqBody.clipped || resBody.clipped
424
+ }
407
425
  });
408
426
  return [ok ? "success" : "fail"];
409
427
  }
@@ -114,7 +114,7 @@ function setSecretsConfig(settings) {
114
114
  function getSecretsConfig() {
115
115
  return current;
116
116
  }
117
- function env$3(...names) {
117
+ function env$4(...names) {
118
118
  for (const n of names) {
119
119
  const v = process.env[n];
120
120
  if (v !== void 0 && v !== "") return v;
@@ -128,22 +128,22 @@ function readFileTrim(path2) {
128
128
  return void 0;
129
129
  }
130
130
  }
131
- function resolveConn$3() {
131
+ function resolveConn$4() {
132
132
  const cfg = getSecretsConfig()?.vault ?? {};
133
- const address = env$3("VAULT_ADDR", "API_SPECTOR_VAULT_ADDR") ?? cfg.address;
133
+ const address = env$4("VAULT_ADDR", "API_SPECTOR_VAULT_ADDR") ?? cfg.address;
134
134
  if (!address) {
135
135
  throw new Error("Vault is not configured: set VAULT_ADDR (or settings.secrets.vault.address)");
136
136
  }
137
- const token = env$3("VAULT_TOKEN", "API_SPECTOR_VAULT_TOKEN") ?? readFileTrim(path.join(os.homedir(), ".vault-token"));
138
- const roleId = env$3("VAULT_ROLE_ID", "API_SPECTOR_VAULT_ROLE_ID") ?? cfg.roleId;
139
- const secretId = env$3("VAULT_SECRET_ID", "API_SPECTOR_VAULT_SECRET_ID");
140
- 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);
141
- const jwtRole = env$3("VAULT_JWT_ROLE", "API_SPECTOR_VAULT_JWT_ROLE") ?? cfg.jwtRole;
142
- const explicit = env$3("VAULT_AUTH_METHOD", "API_SPECTOR_VAULT_AUTH_METHOD") ?? cfg.authMethod;
137
+ const token = env$4("VAULT_TOKEN", "API_SPECTOR_VAULT_TOKEN") ?? readFileTrim(path.join(os.homedir(), ".vault-token"));
138
+ const roleId = env$4("VAULT_ROLE_ID", "API_SPECTOR_VAULT_ROLE_ID") ?? cfg.roleId;
139
+ const secretId = env$4("VAULT_SECRET_ID", "API_SPECTOR_VAULT_SECRET_ID");
140
+ const jwt = env$4("VAULT_JWT", "API_SPECTOR_VAULT_JWT") ?? (env$4("VAULT_JWT_PATH", "API_SPECTOR_VAULT_JWT_PATH") ? readFileTrim(env$4("VAULT_JWT_PATH", "API_SPECTOR_VAULT_JWT_PATH")) : void 0);
141
+ const jwtRole = env$4("VAULT_JWT_ROLE", "API_SPECTOR_VAULT_JWT_ROLE") ?? cfg.jwtRole;
142
+ const explicit = env$4("VAULT_AUTH_METHOD", "API_SPECTOR_VAULT_AUTH_METHOD") ?? cfg.authMethod;
143
143
  const authMethod = explicit ?? (token ? "token" : roleId ? "approle" : jwt ? "jwt" : "token");
144
144
  return {
145
145
  address: address.replace(/\/+$/, ""),
146
- namespace: env$3("VAULT_NAMESPACE", "API_SPECTOR_VAULT_NAMESPACE") ?? cfg.namespace,
146
+ namespace: env$4("VAULT_NAMESPACE", "API_SPECTOR_VAULT_NAMESPACE") ?? cfg.namespace,
147
147
  authMethod,
148
148
  token,
149
149
  roleId,
@@ -151,13 +151,13 @@ function resolveConn$3() {
151
151
  jwt,
152
152
  jwtRole,
153
153
  loginMount: cfg.loginMount,
154
- kvVersion: env$3("VAULT_KV_VERSION", "API_SPECTOR_VAULT_KV_VERSION") ?? cfg.kvVersion ?? "auto",
155
- skipVerify: (env$3("VAULT_SKIP_VERIFY", "API_SPECTOR_VAULT_SKIP_VERIFY") ?? "").toLowerCase() === "true" || cfg.skipVerify === true,
156
- caPath: env$3("VAULT_CACERT", "API_SPECTOR_VAULT_CACERT")
154
+ kvVersion: env$4("VAULT_KV_VERSION", "API_SPECTOR_VAULT_KV_VERSION") ?? cfg.kvVersion ?? "auto",
155
+ skipVerify: (env$4("VAULT_SKIP_VERIFY", "API_SPECTOR_VAULT_SKIP_VERIFY") ?? "").toLowerCase() === "true" || cfg.skipVerify === true,
156
+ caPath: env$4("VAULT_CACERT", "API_SPECTOR_VAULT_CACERT")
157
157
  };
158
158
  }
159
- let cachedToken$1 = null;
160
- const secretCache$1 = /* @__PURE__ */ new Map();
159
+ let cachedToken$2 = null;
160
+ const secretCache$2 = /* @__PURE__ */ new Map();
161
161
  let _dispatcher;
162
162
  let _dispatcherKey = "";
163
163
  async function dispatcherFor(conn) {
@@ -200,7 +200,7 @@ async function clientToken(conn) {
200
200
  return conn.token;
201
201
  }
202
202
  const now = Date.now();
203
- if (cachedToken$1 && cachedToken$1.expiresAt > now + 5e3) return cachedToken$1.value;
203
+ if (cachedToken$2 && cachedToken$2.expiresAt > now + 5e3) return cachedToken$2.value;
204
204
  let mount;
205
205
  let body;
206
206
  if (conn.authMethod === "approle") {
@@ -224,7 +224,7 @@ async function clientToken(conn) {
224
224
  const token = json?.auth?.client_token;
225
225
  if (!token) throw new Error("Vault login did not return a client token");
226
226
  const ttl = Number(json?.auth?.lease_duration ?? 0);
227
- cachedToken$1 = { value: token, expiresAt: ttl > 0 ? now + ttl * 1e3 : now + 5 * 6e4 };
227
+ cachedToken$2 = { value: token, expiresAt: ttl > 0 ? now + ttl * 1e3 : now + 5 * 6e4 };
228
228
  return token;
229
229
  }
230
230
  async function readSecret(refBody) {
@@ -237,11 +237,11 @@ async function readSecret(refBody) {
237
237
  if (!path2 || !key) {
238
238
  throw new Error(`Vault reference must be 'vault:<path>#<key>' (got 'vault:${refBody}')`);
239
239
  }
240
- const conn = resolveConn$3();
240
+ const conn = resolveConn$4();
241
241
  const cacheKey = `${conn.address}|${conn.namespace ?? ""}|${path2}`;
242
242
  const now = Date.now();
243
243
  let bag;
244
- const hit = secretCache$1.get(cacheKey);
244
+ const hit = secretCache$2.get(cacheKey);
245
245
  if (hit && hit.expiresAt > now) {
246
246
  bag = hit.bag;
247
247
  } else {
@@ -250,7 +250,7 @@ async function readSecret(refBody) {
250
250
  bag = isV2 ? json?.data?.data : json?.data;
251
251
  const lease = Number(json?.lease_duration ?? 0);
252
252
  if (bag) {
253
- secretCache$1.set(cacheKey, { bag, expiresAt: lease > 0 ? now + lease * 1e3 : now + 5 * 6e4 });
253
+ secretCache$2.set(cacheKey, { bag, expiresAt: lease > 0 ? now + lease * 1e3 : now + 5 * 6e4 });
254
254
  }
255
255
  }
256
256
  if (!bag || !(key in bag)) {
@@ -293,23 +293,23 @@ function signSigV4(input) {
293
293
  Authorization: `AWS4-HMAC-SHA256 Credential=${input.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
294
294
  };
295
295
  }
296
- function env$2(...names) {
296
+ function env$3(...names) {
297
297
  for (const n of names) {
298
298
  const v = process.env[n];
299
299
  if (v !== void 0 && v !== "") return v;
300
300
  }
301
301
  return void 0;
302
302
  }
303
- function resolveConn$2() {
303
+ function resolveConn$3() {
304
304
  const cfg = getSecretsConfig()?.aws ?? {};
305
- const region = env$2("AWS_REGION", "AWS_DEFAULT_REGION", "API_SPECTOR_AWS_REGION") ?? cfg.region;
306
- const accessKeyId = env$2("AWS_ACCESS_KEY_ID");
307
- const secretAccessKey = env$2("AWS_SECRET_ACCESS_KEY");
305
+ const region = env$3("AWS_REGION", "AWS_DEFAULT_REGION", "API_SPECTOR_AWS_REGION") ?? cfg.region;
306
+ const accessKeyId = env$3("AWS_ACCESS_KEY_ID");
307
+ const secretAccessKey = env$3("AWS_SECRET_ACCESS_KEY");
308
308
  if (!region) throw new Error("AWS Secrets Manager: no region (set AWS_REGION or settings.secrets.aws.region)");
309
309
  if (!accessKeyId || !secretAccessKey) {
310
310
  throw new Error("AWS Secrets Manager: no credentials (set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY)");
311
311
  }
312
- return { region, accessKeyId, secretAccessKey, sessionToken: env$2("AWS_SESSION_TOKEN") };
312
+ return { region, accessKeyId, secretAccessKey, sessionToken: env$3("AWS_SESSION_TOKEN") };
313
313
  }
314
314
  const cache = /* @__PURE__ */ new Map();
315
315
  async function getSecretString(conn, secretId) {
@@ -346,12 +346,12 @@ async function getSecretString(conn, secretId) {
346
346
  cache.set(cacheKey, { raw, expiresAt: now + 5 * 6e4 });
347
347
  return raw;
348
348
  }
349
- async function resolve$2(refBody) {
349
+ async function resolve$3(refBody) {
350
350
  const hash = refBody.lastIndexOf("#");
351
351
  const secretId = hash < 0 ? refBody : refBody.slice(0, hash);
352
352
  const key = hash < 0 ? null : refBody.slice(hash + 1);
353
353
  if (!secretId) throw new Error(`AWS reference must be 'aws:<secretId>[#<key>]' (got 'aws:${refBody}')`);
354
- const raw = await getSecretString(resolveConn$2(), secretId);
354
+ const raw = await getSecretString(resolveConn$3(), secretId);
355
355
  if (key === null) return raw;
356
356
  let parsed;
357
357
  try {
@@ -364,30 +364,30 @@ async function resolve$2(refBody) {
364
364
  }
365
365
  const awsSecretsProvider = {
366
366
  scheme: "aws",
367
- resolve: (refBody) => resolve$2(refBody)
367
+ resolve: (refBody) => resolve$3(refBody)
368
368
  };
369
- function env$1(...names) {
369
+ function env$2(...names) {
370
370
  for (const n of names) {
371
371
  const v = process.env[n];
372
372
  if (v !== void 0 && v !== "") return v;
373
373
  }
374
374
  return void 0;
375
375
  }
376
- function resolveConn$1() {
376
+ function resolveConn$2() {
377
377
  const cfg = getSecretsConfig()?.azure ?? {};
378
- const tenantId = env$1("AZURE_TENANT_ID", "API_SPECTOR_AZURE_TENANT_ID") ?? cfg.tenantId;
379
- const clientId = env$1("AZURE_CLIENT_ID", "API_SPECTOR_AZURE_CLIENT_ID") ?? cfg.clientId;
380
- const clientSecret = env$1("AZURE_CLIENT_SECRET", "API_SPECTOR_AZURE_CLIENT_SECRET");
378
+ const tenantId = env$2("AZURE_TENANT_ID", "API_SPECTOR_AZURE_TENANT_ID") ?? cfg.tenantId;
379
+ const clientId = env$2("AZURE_CLIENT_ID", "API_SPECTOR_AZURE_CLIENT_ID") ?? cfg.clientId;
380
+ const clientSecret = env$2("AZURE_CLIENT_SECRET", "API_SPECTOR_AZURE_CLIENT_SECRET");
381
381
  if (!tenantId || !clientId || !clientSecret) {
382
382
  throw new Error("Azure Key Vault: set AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET");
383
383
  }
384
384
  return { tenantId, clientId, clientSecret, defaultVault: cfg.vault };
385
385
  }
386
- let cachedToken = null;
387
- const secretCache = /* @__PURE__ */ new Map();
388
- async function accessToken(conn) {
386
+ let cachedToken$1 = null;
387
+ const secretCache$1 = /* @__PURE__ */ new Map();
388
+ async function accessToken$1(conn) {
389
389
  const now = Date.now();
390
- if (cachedToken && cachedToken.expiresAt > now + 3e4) return cachedToken.value;
390
+ if (cachedToken$1 && cachedToken$1.expiresAt > now + 3e4) return cachedToken$1.value;
391
391
  const form = new URLSearchParams({
392
392
  grant_type: "client_credentials",
393
393
  client_id: conn.clientId,
@@ -407,7 +407,7 @@ async function accessToken(conn) {
407
407
  const token = json?.access_token;
408
408
  if (!token) throw new Error("Azure AD did not return an access token");
409
409
  const ttl = Number(json?.expires_in ?? 0);
410
- cachedToken = { value: token, expiresAt: ttl > 0 ? now + ttl * 1e3 : now + 5 * 6e4 };
410
+ cachedToken$1 = { value: token, expiresAt: ttl > 0 ? now + ttl * 1e3 : now + 5 * 6e4 };
411
411
  return token;
412
412
  }
413
413
  function secretUrl(refBody, defaultVault) {
@@ -430,13 +430,13 @@ function secretUrl(refBody, defaultVault) {
430
430
  const path2 = version ? `secrets/${name}/${version}` : `secrets/${name}`;
431
431
  return `${base}/${path2}?api-version=7.4`;
432
432
  }
433
- async function resolve$1(refBody) {
434
- const conn = resolveConn$1();
433
+ async function resolve$2(refBody) {
434
+ const conn = resolveConn$2();
435
435
  const url = secretUrl(refBody, conn.defaultVault);
436
436
  const now = Date.now();
437
- const hit = secretCache.get(url);
437
+ const hit = secretCache$1.get(url);
438
438
  if (hit && hit.expiresAt > now) return hit.value;
439
- const res = await fetch(url, { headers: { authorization: `Bearer ${await accessToken(conn)}` } });
439
+ const res = await fetch(url, { headers: { authorization: `Bearer ${await accessToken$1(conn)}` } });
440
440
  if (!res.ok) {
441
441
  const text = await res.text().catch(() => "");
442
442
  throw new Error(`Azure Key Vault ${res.status} for '${refBody}': ${text.slice(0, 300)}`);
@@ -444,24 +444,24 @@ async function resolve$1(refBody) {
444
444
  const json = await res.json();
445
445
  const value = json?.value;
446
446
  if (value === void 0) throw new Error(`Azure Key Vault secret '${refBody}' has no value`);
447
- secretCache.set(url, { value, expiresAt: now + 5 * 6e4 });
447
+ secretCache$1.set(url, { value, expiresAt: now + 5 * 6e4 });
448
448
  return value;
449
449
  }
450
450
  const azureKeyVaultProvider = {
451
451
  scheme: "azure",
452
- resolve: (refBody) => resolve$1(refBody)
452
+ resolve: (refBody) => resolve$2(refBody)
453
453
  };
454
- function env(...names) {
454
+ function env$1(...names) {
455
455
  for (const n of names) {
456
456
  const v = process.env[n];
457
457
  if (v !== void 0 && v !== "") return v;
458
458
  }
459
459
  return void 0;
460
460
  }
461
- function resolveConn() {
461
+ function resolveConn$1() {
462
462
  const cfg = getSecretsConfig()?.onePassword ?? {};
463
- const host = env("OP_CONNECT_HOST", "API_SPECTOR_OP_CONNECT_HOST") ?? cfg.connectHost;
464
- const token = env("OP_CONNECT_TOKEN", "API_SPECTOR_OP_CONNECT_TOKEN");
463
+ const host = env$1("OP_CONNECT_HOST", "API_SPECTOR_OP_CONNECT_HOST") ?? cfg.connectHost;
464
+ const token = env$1("OP_CONNECT_TOKEN", "API_SPECTOR_OP_CONNECT_TOKEN");
465
465
  if (!host || !token) {
466
466
  throw new Error("1Password: set OP_CONNECT_HOST and OP_CONNECT_TOKEN (1Password Connect)");
467
467
  }
@@ -500,7 +500,7 @@ async function itemId(conn, vid, item) {
500
500
  idCache.set(cacheKey, id);
501
501
  return id;
502
502
  }
503
- async function resolve(refBody) {
503
+ async function resolve$1(refBody) {
504
504
  const parts = refBody.replace(/^\/\//, "").replace(/^\/+/, "").split("/").filter(Boolean);
505
505
  if (parts.length < 3) {
506
506
  throw new Error(`1Password reference must be 'op://<vault>/<item>/<field>' (got 'op:${refBody}')`);
@@ -508,7 +508,7 @@ async function resolve(refBody) {
508
508
  const [vault, item, ...rest] = parts;
509
509
  const sectionLabel = rest.length >= 2 ? rest[0] : null;
510
510
  const fieldLabel = rest[rest.length - 1];
511
- const conn = resolveConn();
511
+ const conn = resolveConn$1();
512
512
  const vid = await vaultId(conn, vault);
513
513
  const iid = await itemId(conn, vid, item);
514
514
  const cacheKey = `${vid}/${iid}`;
@@ -535,12 +535,158 @@ async function resolve(refBody) {
535
535
  }
536
536
  const onePasswordProvider = {
537
537
  scheme: "op",
538
+ resolve: (refBody) => resolve$1(refBody)
539
+ };
540
+ const TOKEN_URI = "https://oauth2.googleapis.com/token";
541
+ const SCOPE = "https://www.googleapis.com/auth/cloud-platform";
542
+ const API_BASE = "https://secretmanager.googleapis.com/v1";
543
+ function env(...names) {
544
+ for (const n of names) {
545
+ const v = process.env[n];
546
+ if (v !== void 0 && v !== "") return v;
547
+ }
548
+ return void 0;
549
+ }
550
+ function loadServiceAccount() {
551
+ const inline = env("GCP_SERVICE_ACCOUNT_KEY", "GOOGLE_APPLICATION_CREDENTIALS_JSON");
552
+ const path2 = env("GOOGLE_APPLICATION_CREDENTIALS");
553
+ let raw;
554
+ if (inline) {
555
+ raw = inline;
556
+ } else if (path2) {
557
+ try {
558
+ raw = fs.readFileSync(path2, "utf8");
559
+ } catch (e) {
560
+ throw new Error(`GCP Secret Manager: cannot read GOOGLE_APPLICATION_CREDENTIALS '${path2}': ${e.message}`);
561
+ }
562
+ }
563
+ if (!raw) return void 0;
564
+ let key;
565
+ try {
566
+ key = JSON.parse(raw);
567
+ } catch {
568
+ throw new Error("GCP Secret Manager: the service-account key is not valid JSON");
569
+ }
570
+ if (!key.client_email || !key.private_key) {
571
+ throw new Error("GCP Secret Manager: the service-account key is missing client_email or private_key");
572
+ }
573
+ return key;
574
+ }
575
+ function resolveConn() {
576
+ const cfg = getSecretsConfig()?.gcp ?? {};
577
+ const accessToken2 = env("GCP_ACCESS_TOKEN", "GOOGLE_OAUTH_ACCESS_TOKEN");
578
+ const serviceAccount = loadServiceAccount();
579
+ if (!accessToken2 && !serviceAccount) {
580
+ throw new Error(
581
+ "GCP Secret Manager: no credentials (set GCP_ACCESS_TOKEN, or GOOGLE_APPLICATION_CREDENTIALS / GOOGLE_APPLICATION_CREDENTIALS_JSON to a service-account key)"
582
+ );
583
+ }
584
+ const defaultProject = env("GCP_PROJECT", "GOOGLE_CLOUD_PROJECT", "CLOUDSDK_CORE_PROJECT") ?? cfg.project ?? serviceAccount?.project_id;
585
+ return { accessToken: accessToken2, serviceAccount, defaultProject };
586
+ }
587
+ let cachedToken = null;
588
+ const secretCache = /* @__PURE__ */ new Map();
589
+ const b64url = (b) => (Buffer.isBuffer(b) ? b : Buffer.from(b)).toString("base64url");
590
+ async function mintAccessToken(sa) {
591
+ const iat = Math.floor(Date.now() / 1e3);
592
+ const aud = sa.token_uri || TOKEN_URI;
593
+ const header = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
594
+ const claims = b64url(JSON.stringify({ iss: sa.client_email, scope: SCOPE, aud, iat, exp: iat + 3600 }));
595
+ const signingInput = `${header}.${claims}`;
596
+ const signature = b64url(crypto.createSign("RSA-SHA256").update(signingInput).sign(sa.private_key));
597
+ const assertion = `${signingInput}.${signature}`;
598
+ const body = new URLSearchParams({
599
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
600
+ assertion
601
+ });
602
+ const res = await fetch(aud, {
603
+ method: "POST",
604
+ headers: { "content-type": "application/x-www-form-urlencoded" },
605
+ body: body.toString()
606
+ });
607
+ if (!res.ok) {
608
+ const text = await res.text().catch(() => "");
609
+ throw new Error(`GCP token exchange ${res.status}: ${text.slice(0, 300)}`);
610
+ }
611
+ const json = await res.json();
612
+ const token = json?.access_token;
613
+ if (!token) throw new Error("GCP token endpoint did not return an access_token");
614
+ return { token, ttl: Number(json?.expires_in ?? 3600) };
615
+ }
616
+ async function accessToken(conn) {
617
+ if (conn.accessToken) return conn.accessToken;
618
+ const now = Date.now();
619
+ if (cachedToken && cachedToken.expiresAt > now + 3e4) return cachedToken.value;
620
+ const { token, ttl } = await mintAccessToken(conn.serviceAccount);
621
+ cachedToken = { value: token, expiresAt: ttl > 0 ? now + ttl * 1e3 : now + 5 * 6e4 };
622
+ return token;
623
+ }
624
+ function accessUrl(refPath, defaultProject) {
625
+ if (refPath.startsWith("projects/")) {
626
+ const hasVersion = /\/versions\/[^/]+$/.test(refPath);
627
+ const full = hasVersion ? refPath : `${refPath.replace(/\/+$/, "")}/versions/latest`;
628
+ return `${API_BASE}/${full}:access`;
629
+ }
630
+ const parts = refPath.replace(/^\/+/, "").split("/").filter(Boolean);
631
+ let project;
632
+ let secret;
633
+ let version = "latest";
634
+ if (parts.length >= 2) {
635
+ [project, secret, version = "latest"] = parts;
636
+ } else if (parts.length === 1) {
637
+ project = defaultProject;
638
+ secret = parts[0];
639
+ } else {
640
+ throw new Error(`GCP reference must be 'gcp:<project>/<secret>[/<version>]' (got 'gcp:${refPath}')`);
641
+ }
642
+ if (!project) {
643
+ throw new Error(
644
+ `GCP reference 'gcp:${refPath}' has no project and no default is set (set GCP_PROJECT or settings.secrets.gcp.project, or write 'gcp:<project>/<secret>')`
645
+ );
646
+ }
647
+ return `${API_BASE}/projects/${project}/secrets/${secret}/versions/${version}:access`;
648
+ }
649
+ async function resolve(refBody) {
650
+ const hash = refBody.lastIndexOf("#");
651
+ const refPath = hash < 0 ? refBody : refBody.slice(0, hash);
652
+ const key = hash < 0 ? null : refBody.slice(hash + 1);
653
+ const conn = resolveConn();
654
+ const url = accessUrl(refPath, conn.defaultProject);
655
+ const now = Date.now();
656
+ const hit = secretCache.get(url);
657
+ const rawPayload = hit && hit.expiresAt > now ? hit.value : await fetchPayload(conn, url, refBody);
658
+ if (key === null) return rawPayload;
659
+ let parsed;
660
+ try {
661
+ parsed = JSON.parse(rawPayload);
662
+ } catch {
663
+ throw new Error(`GCP secret '${refPath}' is not JSON, so key '${key}' cannot be read (drop the '#${key}')`);
664
+ }
665
+ if (!(key in parsed)) throw new Error(`GCP secret '${refPath}' has no key '${key}'`);
666
+ return String(parsed[key]);
667
+ }
668
+ async function fetchPayload(conn, url, refBody) {
669
+ const res = await fetch(url, { headers: { authorization: `Bearer ${await accessToken(conn)}` } });
670
+ if (!res.ok) {
671
+ const text = await res.text().catch(() => "");
672
+ throw new Error(`GCP Secret Manager ${res.status} for '${refBody}': ${text.slice(0, 300)}`);
673
+ }
674
+ const json = await res.json();
675
+ const data = json?.payload?.data;
676
+ if (data === void 0) throw new Error(`GCP secret '${refBody}' returned no payload data`);
677
+ const value = Buffer.from(data, "base64").toString("utf8");
678
+ secretCache.set(url, { value, expiresAt: Date.now() + 5 * 6e4 });
679
+ return value;
680
+ }
681
+ const gcpSecretManagerProvider = {
682
+ scheme: "gcp",
538
683
  resolve: (refBody) => resolve(refBody)
539
684
  };
540
685
  registerSecretProvider(vaultProvider);
541
686
  registerSecretProvider(awsSecretsProvider);
542
687
  registerSecretProvider(azureKeyVaultProvider);
543
688
  registerSecretProvider(onePasswordProvider);
689
+ registerSecretProvider(gcpSecretManagerProvider);
544
690
  const MASTER_KEY_ENV = "API_SPECTOR_MASTER_KEY";
545
691
  let secretStore = {};
546
692
  let secretStorePath = null;
@@ -741,7 +887,14 @@ async function buildEnvVars(environment) {
741
887
  return vars;
742
888
  }
743
889
  function mergeVars(envVars, collectionVars, globals2, localVars = {}, dynamicVars = {}) {
744
- return { ...dynamicVars, ...globals2, ...collectionVars, ...envVars, ...localVars };
890
+ const out = {};
891
+ for (const src of [dynamicVars, globals2, collectionVars, envVars, localVars]) {
892
+ for (const [k, v] of Object.entries(src ?? {})) {
893
+ if (v == null) continue;
894
+ out[k] = v;
895
+ }
896
+ }
897
+ return out;
745
898
  }
746
899
  function xmlFindAll(node, tag, nth) {
747
900
  const results = [];
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
  const promises = require("node:fs/promises");
4
4
  const path = require("node:path");
5
- const brokerClient = require("./chunks/broker-client-VwFF9sbp.js");
5
+ const brokerClient = require("./chunks/broker-client-D0wYVblb.js");
6
6
  const escape = require("./chunks/escape-DDVAlOvZ.js");
7
7
  const crypto = require("node:crypto");
8
8
  const undici = require("undici");
@@ -11,7 +11,7 @@ const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
11
11
  const node_child_process = require("node:child_process");
12
12
  const jsYaml = require("js-yaml");
13
13
  const environments = require("./chunks/environments-iM3SUM-4.js");
14
- require("./chunks/request-exec-CcLHtkwG.js");
14
+ require("./chunks/request-exec-DHxjokyP.js");
15
15
  require("fs/promises");
16
16
  require("tls");
17
17
  require("./chunks/handle-BY_jaVBT.js");
package/out/main/flow.js CHANGED
@@ -2,10 +2,10 @@
2
2
  "use strict";
3
3
  const promises = require("node:fs/promises");
4
4
  const path = require("node:path");
5
- const requestExec = require("./chunks/request-exec-CcLHtkwG.js");
5
+ const requestExec = require("./chunks/request-exec-DHxjokyP.js");
6
6
  const requestCollection = require("./chunks/request-collection-KGPuvN7o.js");
7
7
  const environments = require("./chunks/environments-iM3SUM-4.js");
8
- const flowEngine = require("./chunks/flow-engine-DuO1iX7c.js");
8
+ const flowEngine = require("./chunks/flow-engine-BVzjz4lJ.js");
9
9
  const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
10
10
  require("undici");
11
11
  require("fs/promises");
@@ -265,7 +265,7 @@ async function main() {
265
265
  const envName = args.environment ?? args.env;
266
266
  const env = environments.selectEnvironment(workspace, environments$1, envName);
267
267
  const verbose = Boolean(args.verbose);
268
- const version = `v${"0.7.3"}`;
268
+ const version = `v${"0.7.5"}`;
269
269
  console.log("");
270
270
  console.log(cliCommon.color(" API Flow Runner" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
271
271
  console.log(cliCommon.color(` Flow: ${flow.name}`, cliCommon.C.gray));
package/out/main/index.js CHANGED
@@ -29,13 +29,13 @@ const promises = require("node:fs/promises");
29
29
  const crypto = require("node:crypto");
30
30
  const JSZip = require("jszip");
31
31
  const dataFiles = require("./chunks/data-files-BYqhsoPe.js");
32
- const requestExec = require("./chunks/request-exec-CcLHtkwG.js");
32
+ const requestExec = require("./chunks/request-exec-DHxjokyP.js");
33
33
  const ipcValidate = require("./chunks/ipc-validate-sqUFqRmE.js");
34
34
  const uuid = require("uuid");
35
35
  const jsYaml = require("js-yaml");
36
36
  const undici = require("undici");
37
37
  const requestCollection = require("./chunks/request-collection-KGPuvN7o.js");
38
- const flowEngine = require("./chunks/flow-engine-DuO1iX7c.js");
38
+ const flowEngine = require("./chunks/flow-engine-BVzjz4lJ.js");
39
39
  const mockServer = require("./chunks/mock-server-C4kTFkod.js");
40
40
  const http = require("node:http");
41
41
  const WebSocket = require("ws");
@@ -44,7 +44,7 @@ const grpc = require("@grpc/grpc-js");
44
44
  const protoLoader = require("@grpc/proto-loader");
45
45
  const soapHandler = require("./chunks/soap-handler-CkoLJHoI.js");
46
46
  const escape = require("./chunks/escape-DDVAlOvZ.js");
47
- const brokerClient = require("./chunks/broker-client-VwFF9sbp.js");
47
+ const brokerClient = require("./chunks/broker-client-D0wYVblb.js");
48
48
  const simpleGit = require("simple-git");
49
49
  const recorder = require("./chunks/recorder-BkO8G_mB.js");
50
50
  const electronUpdater = require("electron-updater");
@@ -5877,7 +5877,19 @@ async function buildFlowDefinition(input) {
5877
5877
  requests[`${ref.collectionId}:${ref.requestId}`] = cloned;
5878
5878
  }
5879
5879
  }
5880
- return { flow: input.flow, flows: input.flows ?? [], requests, envVars, collectionVars, globals: input.globals ?? {} };
5880
+ return {
5881
+ flow: input.flow,
5882
+ flows: input.flows ?? [],
5883
+ requests,
5884
+ envVars,
5885
+ collectionVars,
5886
+ globals: input.globals ?? {},
5887
+ // Record which environment's values were baked (for the cloud UI). null when
5888
+ // no environment was active at push time — a common cause of "works locally,
5889
+ // fails in the cloud" when requests rely on environment variables.
5890
+ environmentName: input.environment?.name ?? null,
5891
+ envVarCount: Object.keys(envVars).length
5892
+ };
5881
5893
  }
5882
5894
  const CLOUD_TOKEN_REF = "cloud:token";
5883
5895
  const CLOUD_ENDPOINT = brokerClient.resolveCloudEndpoint();
@@ -6098,7 +6110,7 @@ function isNewer(a, b) {
6098
6110
  return a2 > b2;
6099
6111
  }
6100
6112
  async function checkForUpdate() {
6101
- const current = "0.7.3";
6113
+ const current = "0.7.5";
6102
6114
  try {
6103
6115
  const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(4e3) });
6104
6116
  if (!res.ok) return null;
package/out/main/lib.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const mockServer = require("./chunks/mock-server-C4kTFkod.js");
4
- const requestExec = require("./chunks/request-exec-CcLHtkwG.js");
5
- const flowEngine = require("./chunks/flow-engine-DuO1iX7c.js");
4
+ const requestExec = require("./chunks/request-exec-DHxjokyP.js");
5
+ const flowEngine = require("./chunks/flow-engine-BVzjz4lJ.js");
6
6
  require("node:http");
7
7
  require("node:crypto");
8
8
  require("node:vm");
@@ -56,7 +56,13 @@ async function runFlowHeadless(input) {
56
56
  testResults: result.testResults,
57
57
  name: result.name,
58
58
  method: result.method,
59
- resolvedUrl: result.resolvedUrl
59
+ resolvedUrl: result.resolvedUrl,
60
+ sentRequest: {
61
+ method: result.method,
62
+ url: result.resolvedUrl,
63
+ headers: result.sentRequest?.headers ?? {},
64
+ body: result.sentRequest?.body
65
+ }
60
66
  };
61
67
  },
62
68
  runScript: async (code, i) => {
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
  const promises = require("node:fs/promises");
4
4
  const path = require("node:path");
5
- const requestExec = require("./chunks/request-exec-CcLHtkwG.js");
5
+ const requestExec = require("./chunks/request-exec-DHxjokyP.js");
6
6
  const escape = require("./chunks/escape-DDVAlOvZ.js");
7
7
  const requestCollection = require("./chunks/request-collection-KGPuvN7o.js");
8
8
  const environments = require("./chunks/environments-iM3SUM-4.js");
@@ -495,7 +495,7 @@ async function main() {
495
495
  } else if (!envName && workspace.settings?.defaultEnvironment && !env) {
496
496
  console.warn(cliCommon.color(`Warning: default environment "${workspace.settings.defaultEnvironment}" not found. Running without environment.`, cliCommon.C.yellow));
497
497
  }
498
- const version = `v${"0.7.3"}`;
498
+ const version = `v${"0.7.5"}`;
499
499
  console.log("");
500
500
  console.log(cliCommon.color(" API Test Runner" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
501
501
  console.log(cliCommon.color(` Workspace: ${wsPath}`, cliCommon.C.gray));
@@ -1,5 +1,5 @@
1
1
  import { l as linter, E as EditorView, L as LanguageSupport, e as LRLanguage, s as styleTags, i as indentNodeProp, f as delimitedIndent, m as foldNodeProp, n as foldInside, S as StateField, p as LRParser, q as StateEffect, u as tags, r as reactExports, j as jsxRuntimeExports, c as ReactCodeMirror, o as oneDark, b as json } from "./codemirror-F6m3Hm1M.js";
2
- import { u as useT, a as useStore, b as useActiveEnvironment } from "./index-aKbixApk.js";
2
+ import { u as useT, a as useStore, b as useActiveEnvironment } from "./index-CUCX3V01.js";
3
3
  import "./ajv-2MAg6kGF.js";
4
4
  function devAssert(condition, message) {
5
5
  const booleanCondition = Boolean(condition);
@@ -1,5 +1,5 @@
1
1
  import { r as reactExports, j as jsxRuntimeExports, c as ReactCodeMirror, o as oneDark, x as xml } from "./codemirror-F6m3Hm1M.js";
2
- import { u as useT, c as btnPrimaryCls, d as commentKeymap } from "./index-aKbixApk.js";
2
+ import { u as useT, c as btnPrimaryCls, d as commentKeymap } from "./index-CUCX3V01.js";
3
3
  import "./ajv-2MAg6kGF.js";
4
4
  const SOAP_11_CONTENT_TYPE = "text/xml; charset=utf-8";
5
5
  const SOAP_12_CONTENT_TYPE = "application/soap+xml; charset=utf-8";