wayfarer-cli 2.1.0 → 2.2.0

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.
@@ -7493,9 +7493,9 @@ function isTextType(mediaType) {
7493
7493
  const subtype = type.slice(type.indexOf("/") + 1);
7494
7494
  return type.startsWith("text/") || subtype === "xml" || subtype.startsWith("xml-") || subtype.endsWith("+xml") || TEXT_WORDS.some((word) => type.includes(word));
7495
7495
  }
7496
- function decodeResponseBody(bytes, contentType2) {
7496
+ function readBody(bytes, contentType2) {
7497
7497
  if (!bytes || bytes.byteLength === 0) {
7498
- return null;
7498
+ return { value: null };
7499
7499
  }
7500
7500
  const type = (contentType2 ?? "").trim();
7501
7501
  const mediaType = type.split(";")[0].trim();
@@ -7505,21 +7505,21 @@ function decodeResponseBody(bytes, contentType2) {
7505
7505
  text3 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
7506
7506
  } catch (error) {
7507
7507
  if (!(error instanceof TypeError)) throw error;
7508
- return new BinaryBody(bytes, "");
7508
+ return { value: new BinaryBody(bytes, "") };
7509
7509
  }
7510
7510
  } else if (JSON_TYPE.test(mediaType) || isTextType(mediaType)) {
7511
7511
  text3 = decodeText(bytes, charsetOf(type));
7512
7512
  } else {
7513
- return new BinaryBody(bytes, mediaType);
7513
+ return { value: new BinaryBody(bytes, mediaType) };
7514
7514
  }
7515
7515
  const trimmed = text3.trim();
7516
7516
  if (JSON_TYPE.test(mediaType) || trimmed.startsWith("{") || trimmed.startsWith("[")) {
7517
7517
  const parsed2 = parseJson(text3);
7518
7518
  if (parsed2.ok && parsed2.value !== null && (JSON_TYPE.test(mediaType) || typeof parsed2.value === "object")) {
7519
- return parsed2.value;
7519
+ return { value: parsed2.value, text: text3 };
7520
7520
  }
7521
7521
  }
7522
- return text3;
7522
+ return { value: text3, text: text3 };
7523
7523
  }
7524
7524
  function charsetOf(contentType2) {
7525
7525
  const match = /;\s*charset\s*=\s*"?([^";\s]+)"?/i.exec(contentType2);
@@ -7533,12 +7533,12 @@ function decodeText(bytes, charset) {
7533
7533
  return new TextDecoder("utf-8").decode(bytes);
7534
7534
  }
7535
7535
  }
7536
- function decodeEnvelope(envelope) {
7536
+ function readEnvelope(envelope) {
7537
7537
  const contentType2 = envelope.headers.find(([name]) => name.toLowerCase() === "content-type")?.[1];
7538
7538
  if (envelope.body instanceof Blob) {
7539
- return new BinaryBody(envelope.body, (contentType2 ?? "").split(";")[0].trim());
7539
+ return { value: new BinaryBody(envelope.body, (contentType2 ?? "").split(";")[0].trim()) };
7540
7540
  }
7541
- return decodeResponseBody(envelope.body, contentType2);
7541
+ return readBody(envelope.body, contentType2);
7542
7542
  }
7543
7543
 
7544
7544
  // ../core/src/id.ts
@@ -7589,15 +7589,16 @@ function buildAuthHeaders(auth3) {
7589
7589
  return { Authorization: `Bearer ${auth3.token}` };
7590
7590
  }
7591
7591
  if (auth3.type === "basic" && auth3.username) {
7592
- const bytes = new TextEncoder().encode(`${auth3.username}:${auth3.password}`);
7593
- const encoded = btoa(String.fromCharCode(...bytes));
7594
- return { Authorization: `Basic ${encoded}` };
7592
+ return { Authorization: basicCredentials(auth3.username, auth3.password) };
7595
7593
  }
7596
7594
  if (auth3.type === "apikey" && auth3.key && auth3.in === "header") {
7597
7595
  return Object.fromEntries([[auth3.key, auth3.value]]);
7598
7596
  }
7599
7597
  return {};
7600
7598
  }
7599
+ function basicCredentials(user, password) {
7600
+ return `Basic ${btoa(String.fromCharCode(...new TextEncoder().encode(`${user}:${password}`)))}`;
7601
+ }
7601
7602
  function buildAuthQueryParam(auth3) {
7602
7603
  if (auth3.type === "apikey" && auth3.key && auth3.in === "query") {
7603
7604
  return { key: auth3.key, value: auth3.value };
@@ -7662,86 +7663,117 @@ function credentialsOf(auth3) {
7662
7663
  }
7663
7664
  }
7664
7665
 
7665
- // ../core/src/auth/sigv4.ts
7666
+ // ../core/src/vault/vault-crypto.ts
7667
+ var KDF_ITERATIONS = 6e5;
7668
+ var MAX_KDF_ITERATIONS = 1e7;
7669
+ var IV_BYTES = 12;
7670
+ var AES = { name: "AES-GCM", length: 256 };
7671
+ var VAULT_FILE_FORMAT = "wayfarer/vault";
7672
+ function fromBase64Url(text3) {
7673
+ const binary = atob(text3.replace(/-/g, "+").replace(/_/g, "/"));
7674
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
7675
+ }
7666
7676
  var utf8 = (text3) => new TextEncoder().encode(text3);
7667
- var sha256 = async (data) => new Uint8Array(await crypto.subtle.digest("SHA-256", data));
7668
- var hmac = async (key, data) => new Uint8Array(
7669
- await crypto.subtle.sign(
7670
- "HMAC",
7671
- await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]),
7672
- utf8(data)
7673
- )
7674
- );
7675
- var hex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
7676
- var encode = (text3) => encodeURIComponent(text3).replace(/[!'()*]/g, (mark) => `%${mark.charCodeAt(0).toString(16).toUpperCase()}`);
7677
- var decode = (text3) => {
7677
+ async function deriveKek(passphrase, kdf) {
7678
+ if (!crypto.subtle) {
7679
+ throw new Error("The vault needs a secure page: open Wayfarer over https or on localhost.");
7680
+ }
7681
+ const material = await crypto.subtle.importKey("raw", utf8(passphrase), "PBKDF2", false, ["deriveKey"]);
7682
+ return crypto.subtle.deriveKey(
7683
+ { name: "PBKDF2", salt: fromBase64Url(kdf.salt), iterations: kdf.iterations, hash: "SHA-256" },
7684
+ material,
7685
+ AES,
7686
+ false,
7687
+ ["wrapKey", "unwrapKey"]
7688
+ );
7689
+ }
7690
+ async function unwrap(record, passphrase, extractable) {
7691
+ const bytes = fromBase64Url(record.wrappedDek);
7692
+ const kek = await deriveKek(passphrase, record.kdf);
7678
7693
  try {
7679
- return decodeURIComponent(text3);
7694
+ return await crypto.subtle.unwrapKey(
7695
+ "raw",
7696
+ bytes.slice(IV_BYTES),
7697
+ kek,
7698
+ { name: "AES-GCM", iv: bytes.slice(0, IV_BYTES) },
7699
+ AES,
7700
+ extractable,
7701
+ ["encrypt", "decrypt"]
7702
+ );
7680
7703
  } catch (error) {
7681
- if (!(error instanceof URIError)) throw error;
7682
- return text3;
7704
+ if (error instanceof Error && error.name === "OperationError") return null;
7705
+ throw error;
7683
7706
  }
7684
- };
7685
- async function signV4(request, credentials, at) {
7686
- const url = new URL(request.url);
7687
- const s3 = credentials.service === "s3";
7688
- const segments = url.pathname.split("/");
7689
- const kept = segments.filter(Boolean);
7690
- const path = s3 ? segments.map((segment) => encode(decode(segment))).join("/") : `/${kept.map(encode).join("/")}${kept.length && url.pathname.endsWith("/") ? "/" : ""}`;
7691
- const query = url.search.slice(1).split("&").filter(Boolean).map((pair) => {
7692
- const cut = pair.indexOf("=");
7693
- return [
7694
- encode(decode(cut === -1 ? pair : pair.slice(0, cut))),
7695
- encode(decode(cut === -1 ? "" : pair.slice(cut + 1)))
7696
- ];
7697
- }).sort(
7698
- ([keyA, valueA], [keyB, valueB]) => keyA === keyB ? valueA < valueB ? -1 : valueA > valueB ? 1 : 0 : keyA < keyB ? -1 : 1
7699
- ).map(([key2, value]) => `${key2}=${value}`).join("&");
7700
- const date = at.toISOString().replace(/[-:]|\.\d{3}/g, "");
7701
- const day = date.slice(0, 8);
7702
- const body3 = typeof request.body === "string" ? utf8(request.body) : new Uint8Array(request.body ?? []);
7703
- const bodyHash = hex(await sha256(body3));
7704
- const added2 = [
7705
- ["X-Amz-Date", date],
7706
- ...credentials.sessionToken ? [["X-Amz-Security-Token", credentials.sessionToken]] : [],
7707
- ...s3 ? [["X-Amz-Content-Sha256", bodyHash]] : []
7708
- ];
7709
- const byName = /* @__PURE__ */ new Map();
7710
- const own3 = request.headers.filter(
7711
- ([name]) => !added2.some(([written]) => written.toLowerCase() === name.toLowerCase()) && name.toLowerCase() !== "host"
7707
+ }
7708
+ function unwrapDek(record, passphrase) {
7709
+ return unwrap(record, passphrase, false);
7710
+ }
7711
+ async function decryptSecret(dek, id, envelope) {
7712
+ const plain = await crypto.subtle.decrypt(
7713
+ { name: "AES-GCM", iv: fromBase64Url(envelope.iv), additionalData: utf8(id) },
7714
+ dek,
7715
+ fromBase64Url(envelope.ct)
7712
7716
  );
7713
- for (const [name, value] of [["Host", url.host], ...own3, ...added2]) {
7714
- const key2 = name.toLowerCase();
7715
- byName.set(key2, [...byName.get(key2) ?? [], value.trim().replace(/\s+/g, " ")]);
7717
+ return new TextDecoder().decode(plain);
7718
+ }
7719
+ function field(value, key) {
7720
+ return typeof value === "object" && value !== null && Object.hasOwn(value, key) ? value[key] : void 0;
7721
+ }
7722
+ var isBase64Url = (value) => typeof value === "string" && /^[\w-]+$/.test(value);
7723
+ function validateVaultFile(value) {
7724
+ const issues = [];
7725
+ const fail = (path, message) => issues.push({ path, message });
7726
+ if (typeof value !== "object" || value === null) {
7727
+ fail("$", "Value must be an object.");
7728
+ return issues;
7716
7729
  }
7717
- const names = [...byName.keys()].sort();
7718
- const signedHeaders = names.join(";");
7719
- const canonicalRequest = [
7720
- request.method,
7721
- path,
7722
- query,
7723
- ...names.map((name) => `${name}:${(byName.get(name) ?? []).join(",")}`),
7724
- "",
7725
- signedHeaders,
7726
- bodyHash
7727
- ].join("\n");
7728
- const scope = `${day}/${credentials.region}/${credentials.service}/aws4_request`;
7729
- const stringToSign = ["AWS4-HMAC-SHA256", date, scope, hex(await sha256(utf8(canonicalRequest)))].join("\n");
7730
- let key = utf8(`AWS4${credentials.secretKey}`);
7731
- for (const part of [day, credentials.region, credentials.service, "aws4_request"]) key = await hmac(key, part);
7732
- const signature = hex(await hmac(key, stringToSign));
7733
- return {
7734
- headers: [
7735
- ...added2,
7736
- [
7737
- "Authorization",
7738
- `AWS4-HMAC-SHA256 Credential=${credentials.accessKey}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
7739
- ]
7740
- ],
7741
- canonicalRequest,
7742
- stringToSign,
7743
- signature
7744
- };
7730
+ if (field(value, "$id") !== VAULT_FILE_FORMAT) {
7731
+ fail("$id", `Not a Wayfarer vault file: "$id" must be "${VAULT_FILE_FORMAT}".`);
7732
+ return issues;
7733
+ }
7734
+ const vault = field(value, "vault");
7735
+ if (typeof vault !== "object" || vault === null) {
7736
+ fail("vault", "Value must be an object.");
7737
+ } else {
7738
+ const kdf = field(vault, "kdf");
7739
+ if (typeof kdf !== "object" || kdf === null) {
7740
+ fail("vault.kdf", "Value must be an object.");
7741
+ } else {
7742
+ if (field(kdf, "alg") !== "PBKDF2-SHA256") fail("vault.kdf.alg", 'Value must be "PBKDF2-SHA256".');
7743
+ const iterations = field(kdf, "iterations");
7744
+ if (!(Number.isInteger(iterations) && iterations >= KDF_ITERATIONS && iterations <= MAX_KDF_ITERATIONS)) {
7745
+ fail("vault.kdf.iterations", `Value must be a whole number from ${KDF_ITERATIONS} to ${MAX_KDF_ITERATIONS}.`);
7746
+ }
7747
+ if (!isBase64Url(field(kdf, "salt"))) fail("vault.kdf.salt", "Value must be base64url text.");
7748
+ }
7749
+ if (!isBase64Url(field(vault, "wrappedDek"))) fail("vault.wrappedDek", "Value must be base64url text.");
7750
+ }
7751
+ const secrets = field(value, "secrets");
7752
+ if (!Array.isArray(secrets)) {
7753
+ fail("secrets", "Value must be an array.");
7754
+ return issues;
7755
+ }
7756
+ secrets.forEach((secret, index) => {
7757
+ const path = `secrets[${index}]`;
7758
+ if (typeof secret !== "object" || secret === null) {
7759
+ fail(path, "Value must be an object.");
7760
+ return;
7761
+ }
7762
+ const id = field(secret, "id");
7763
+ if (typeof id !== "string" || !id) fail(`${path}.id`, "Value must be a non-empty string.");
7764
+ if (typeof field(secret, "name") !== "string") fail(`${path}.name`, "Value must be a string.");
7765
+ const environmentId = field(secret, "environmentId");
7766
+ if (environmentId !== void 0 && typeof environmentId !== "string")
7767
+ fail(`${path}.environmentId`, "Value must be a string.");
7768
+ const envelope = field(secret, "envelope");
7769
+ if (typeof envelope !== "object" || envelope === null) {
7770
+ fail(`${path}.envelope`, "Value must be an object.");
7771
+ return;
7772
+ }
7773
+ if (!isBase64Url(field(envelope, "iv"))) fail(`${path}.envelope.iv`, "Value must be base64url text.");
7774
+ if (!isBase64Url(field(envelope, "ct"))) fail(`${path}.envelope.ct`, "Value must be base64url text.");
7775
+ });
7776
+ return issues;
7745
7777
  }
7746
7778
 
7747
7779
  // ../core/src/auth/oauth2.ts
@@ -7772,12 +7804,42 @@ function folderChain(folders, folderId) {
7772
7804
  }
7773
7805
  return chain;
7774
7806
  }
7775
- function childrenInOrder(tree, parent) {
7776
- const entries = [
7777
- ...tree.folders.filter((folder) => folder.parentFolderId === parent).map((folder) => ({ order: folder.order, kind: 0, id: folder.id, child: { folder } })),
7778
- ...tree.requests.filter((request) => request.folderId === parent).map((request) => ({ order: request.order, kind: 1, id: request.id, child: { request } }))
7807
+ function childrenInOrder(tree) {
7808
+ const byParent = /* @__PURE__ */ new Map();
7809
+ const add = (parent, entry) => {
7810
+ const siblings = byParent.get(parent);
7811
+ if (siblings) siblings.push(entry);
7812
+ else byParent.set(parent, [entry]);
7813
+ };
7814
+ for (const folder of tree.folders)
7815
+ add(folder.parentFolderId, { order: folder.order, kind: 0, id: folder.id, child: { folder } });
7816
+ for (const request of tree.requests)
7817
+ add(request.folderId, { order: request.order, kind: 1, id: request.id, child: { request } });
7818
+ const sorted = new Map(
7819
+ [...byParent].map(([parent, entries]) => [
7820
+ parent,
7821
+ entries.sort((a, b) => a.order - b.order || a.kind - b.kind || a.id.localeCompare(b.id)).map((entry) => entry.child)
7822
+ ])
7823
+ );
7824
+ return (parent) => sorted.get(parent) ?? [];
7825
+ }
7826
+ function treeInOrder(tree) {
7827
+ const entered = /* @__PURE__ */ new Set();
7828
+ const childrenOf = childrenInOrder(tree);
7829
+ const walk2 = (parent) => childrenOf(parent).flatMap((child) => {
7830
+ if (child.request !== void 0) return [{ kind: "request", request: child.request }];
7831
+ const { folder } = child;
7832
+ if (!folder || entered.has(folder.id)) return [];
7833
+ entered.add(folder.id);
7834
+ return [{ kind: "folder", folder }, ...walk2(folder.id)];
7835
+ });
7836
+ const listed = walk2(void 0);
7837
+ const seen = new Set(listed.map((item) => item.kind === "request" ? item.request : item.folder));
7838
+ return [
7839
+ ...listed,
7840
+ ...tree.folders.filter((folder) => !seen.has(folder)).map((folder) => ({ kind: "folder", folder })),
7841
+ ...tree.requests.filter((request) => !seen.has(request)).map((request) => ({ kind: "request", request }))
7779
7842
  ];
7780
- return entries.sort((a, b) => a.order - b.order || a.kind - b.kind || a.id.localeCompare(b.id)).map((entry) => entry.child);
7781
7843
  }
7782
7844
  function ancestorsOf(collection, folders, folderId) {
7783
7845
  const named2 = (kind, { name, variables: variables2, auth: auth3, scripts: scripts2 }) => ({
@@ -7844,7 +7906,7 @@ function emptyRequest() {
7844
7906
  function isObject(value) {
7845
7907
  return typeof value === "object" && value !== null && !Array.isArray(value);
7846
7908
  }
7847
- function field(object, key) {
7909
+ function field2(object, key) {
7848
7910
  return Object.hasOwn(object, key) ? object[key] : void 0;
7849
7911
  }
7850
7912
  var Checker = class {
@@ -7858,12 +7920,12 @@ var Checker = class {
7858
7920
  return null;
7859
7921
  }
7860
7922
  string(object, key, path, optional = false) {
7861
- const value = field(object, key);
7923
+ const value = field2(object, key);
7862
7924
  if (typeof value === "string" || optional && value === void 0) return;
7863
7925
  this.fail(`${path}.${key}`, "Value must be a string.");
7864
7926
  }
7865
7927
  oneOf(object, key, path, allowed, optional = false) {
7866
- const value = field(object, key);
7928
+ const value = field2(object, key);
7867
7929
  if (optional && value === void 0 || typeof value === "string" && allowed.includes(value)) return;
7868
7930
  this.fail(`${path}.${key}`, `Value must be one of ${allowed.join(", ")}.`);
7869
7931
  }
@@ -7879,7 +7941,7 @@ var Checker = class {
7879
7941
  });
7880
7942
  }
7881
7943
  enabled(row, rowPath) {
7882
- if (typeof field(row, "enabled") !== "boolean") {
7944
+ if (typeof field2(row, "enabled") !== "boolean") {
7883
7945
  this.fail(`${rowPath}.enabled`, "Value must be true or false.");
7884
7946
  }
7885
7947
  }
@@ -7888,7 +7950,7 @@ var Checker = class {
7888
7950
  this.string(row, "key", rowPath);
7889
7951
  this.string(row, "value", rowPath);
7890
7952
  this.enabled(row, rowPath);
7891
- if (Object.hasOwn(row, "json") && (field(row, "json") !== true || !isJsonText(field(row, "value"))))
7953
+ if (Object.hasOwn(row, "json") && (field2(row, "json") !== true || !isJsonText(field2(row, "value"))))
7892
7954
  this.fail(`${rowPath}.json`, "Value must be true, on a value that is JSON.");
7893
7955
  });
7894
7956
  }
@@ -7897,7 +7959,7 @@ var Checker = class {
7897
7959
  const auth3 = this.object(value, path);
7898
7960
  if (!auth3) return;
7899
7961
  this.oneOf(auth3, "type", path, inherit ? AUTH_TYPES : AUTH_TYPES.filter((type2) => type2 !== "inherit"));
7900
- const type = field(auth3, "type");
7962
+ const type = field2(auth3, "type");
7901
7963
  if (type === "bearer") {
7902
7964
  this.string(auth3, "token", path);
7903
7965
  } else if (type === "basic") {
@@ -7914,7 +7976,7 @@ var Checker = class {
7914
7976
  for (const key of ["authUrl", "tokenUrl", "clientId", "clientSecret", "scope", "username", "password"])
7915
7977
  this.string(auth3, key, path);
7916
7978
  this.oneOf(auth3, "clientAuth", path, ["header", "body"]);
7917
- if (typeof field(auth3, "retryOn401") !== "boolean")
7979
+ if (typeof field2(auth3, "retryOn401") !== "boolean")
7918
7980
  this.fail(`${path}.retryOn401`, "Value must be true or false.");
7919
7981
  }
7920
7982
  }
@@ -7938,9 +8000,9 @@ function validateInherited(value, path, kind) {
7938
8000
  const check = new Checker();
7939
8001
  const node = check.object(value, path);
7940
8002
  if (!node) return check.issues;
7941
- check.auth(field(node, "auth"), `${path}.auth`, kind === "folder");
7942
- check.scripts(field(node, "scripts"), `${path}.scripts`);
7943
- if (kind === "folder") check.rows(field(node, "variables"), `${path}.variables`);
8003
+ check.auth(field2(node, "auth"), `${path}.auth`, kind === "folder");
8004
+ check.scripts(field2(node, "scripts"), `${path}.scripts`);
8005
+ if (kind === "folder") check.rows(field2(node, "variables"), `${path}.variables`);
7944
8006
  else check.oneOf(node, "route", path, ["auto", "direct", "bridge"], true);
7945
8007
  return check.issues;
7946
8008
  }
@@ -7948,75 +8010,75 @@ function validateRequestContent(value, path) {
7948
8010
  const check = new Checker();
7949
8011
  const request = check.object(value, path);
7950
8012
  if (!request) return check.issues;
7951
- const method = field(request, "method");
8013
+ const method = field2(request, "method");
7952
8014
  if (typeof method !== "string" || !isHttpMethod(method)) {
7953
8015
  check.fail(`${path}.method`, "Value must be an HTTP method: one word of at most 32 characters, in upper case.");
7954
8016
  }
7955
- if (typeof field(request, "url") !== "string" || !field(request, "url").trim()) {
8017
+ if (typeof field2(request, "url") !== "string" || !field2(request, "url").trim()) {
7956
8018
  check.fail(`${path}.url`, "Value must be a non-empty string.");
7957
8019
  }
7958
- check.rows(field(request, "params"), `${path}.params`);
7959
- check.rows(field(request, "headers"), `${path}.headers`);
7960
- const body3 = check.object(field(request, "body"), `${path}.body`);
8020
+ check.rows(field2(request, "params"), `${path}.params`);
8021
+ check.rows(field2(request, "headers"), `${path}.headers`);
8022
+ const body3 = check.object(field2(request, "body"), `${path}.body`);
7961
8023
  if (body3) {
7962
- const mode = field(body3, "mode");
8024
+ const mode = field2(body3, "mode");
7963
8025
  check.oneOf(body3, "mode", `${path}.body`, BODY_MODES);
7964
- const part = (name) => field(body3, name) !== void 0 || mode === name ? name : null;
8026
+ const part = (name) => field2(body3, name) !== void 0 || mode === name ? name : null;
7965
8027
  if (part("raw")) {
7966
- const raw = check.object(field(body3, "raw"), `${path}.body.raw`);
8028
+ const raw = check.object(field2(body3, "raw"), `${path}.body.raw`);
7967
8029
  if (raw) {
7968
8030
  check.oneOf(raw, "language", `${path}.body.raw`, RAW_LANGUAGES);
7969
8031
  check.string(raw, "text", `${path}.body.raw`);
7970
8032
  }
7971
8033
  }
7972
8034
  if (part("urlencoded")) {
7973
- check.rows(field(body3, "urlencoded"), `${path}.body.urlencoded`);
8035
+ check.rows(field2(body3, "urlencoded"), `${path}.body.urlencoded`);
7974
8036
  }
7975
8037
  if (part("multipart")) {
7976
- check.list(field(body3, "multipart"), `${path}.body.multipart`, (item, itemPath) => {
8038
+ check.list(field2(body3, "multipart"), `${path}.body.multipart`, (item, itemPath) => {
7977
8039
  check.string(item, "key", itemPath);
7978
8040
  check.enabled(item, itemPath);
7979
8041
  check.oneOf(item, "kind", itemPath, ["text", "file"]);
7980
- if (field(item, "kind") === "text") check.string(item, "value", itemPath);
7981
- if (field(item, "kind") === "file") check.fileRef(item, itemPath);
8042
+ if (field2(item, "kind") === "text") check.string(item, "value", itemPath);
8043
+ if (field2(item, "kind") === "file") check.fileRef(item, itemPath);
7982
8044
  });
7983
8045
  }
7984
8046
  if (part("graphql")) {
7985
- const graphql = check.object(field(body3, "graphql"), `${path}.body.graphql`);
8047
+ const graphql = check.object(field2(body3, "graphql"), `${path}.body.graphql`);
7986
8048
  if (graphql) {
7987
8049
  check.string(graphql, "query", `${path}.body.graphql`);
7988
8050
  check.string(graphql, "variables", `${path}.body.graphql`);
7989
8051
  }
7990
8052
  }
7991
- if (field(body3, "binary") !== void 0) {
7992
- const binary = check.object(field(body3, "binary"), `${path}.body.binary`);
8053
+ if (field2(body3, "binary") !== void 0) {
8054
+ const binary = check.object(field2(body3, "binary"), `${path}.body.binary`);
7993
8055
  if (binary) {
7994
8056
  check.fileRef(binary, `${path}.body.binary`);
7995
8057
  check.string(binary, "contentType", `${path}.body.binary`, true);
7996
8058
  }
7997
8059
  }
7998
8060
  }
7999
- check.auth(field(request, "auth"), `${path}.auth`, true);
8000
- check.scripts(field(request, "scripts"), `${path}.scripts`);
8001
- check.list(field(request, "tests"), `${path}.tests`, (test, testPath) => {
8061
+ check.auth(field2(request, "auth"), `${path}.auth`, true);
8062
+ check.scripts(field2(request, "scripts"), `${path}.scripts`);
8063
+ check.list(field2(request, "tests"), `${path}.tests`, (test, testPath) => {
8002
8064
  check.string(test, "id", testPath);
8003
8065
  check.oneOf(test, "target", testPath, ASSERTION_TARGETS);
8004
8066
  check.oneOf(test, "operator", testPath, ASSERTION_OPERATORS);
8005
8067
  check.string(test, "key", testPath, true);
8006
8068
  check.string(test, "expected", testPath, true);
8007
8069
  });
8008
- const settings = check.object(field(request, "settings"), `${path}.settings`);
8070
+ const settings = check.object(field2(request, "settings"), `${path}.settings`);
8009
8071
  if (settings) {
8010
- const timeout = field(settings, "timeoutMs");
8072
+ const timeout = field2(settings, "timeoutMs");
8011
8073
  if (timeout !== void 0 && !(typeof timeout === "number" && Number.isFinite(timeout) && timeout >= 0)) {
8012
8074
  check.fail(`${path}.settings.timeoutMs`, "Value must be a number, 0 or more.");
8013
8075
  }
8014
- const follow = field(settings, "followRedirects");
8076
+ const follow = field2(settings, "followRedirects");
8015
8077
  if (follow !== void 0 && typeof follow !== "boolean") {
8016
8078
  check.fail(`${path}.settings.followRedirects`, "Value must be true or false.");
8017
8079
  }
8018
8080
  check.oneOf(settings, "route", `${path}.settings`, ["auto", "direct", "bridge"], true);
8019
- const insecure = field(settings, "tlsInsecure");
8081
+ const insecure = field2(settings, "tlsInsecure");
8020
8082
  if (insecure !== void 0 && typeof insecure !== "boolean") {
8021
8083
  check.fail(`${path}.settings.tlsInsecure`, "Value must be true or false.");
8022
8084
  }
@@ -8110,21 +8172,10 @@ function buildPlan(payload, duplicate) {
8110
8172
  action
8111
8173
  }
8112
8174
  ];
8113
- for (const folder of payload.folders) {
8114
- entries.push({
8115
- type: "folder",
8116
- name: folder.name,
8117
- id: folder.meta.id,
8118
- action
8119
- });
8120
- }
8121
- for (const request of payload.requests) {
8122
- entries.push({
8123
- type: "request",
8124
- name: request.name || request.url,
8125
- id: request.meta.id,
8126
- action
8127
- });
8175
+ for (const child of treeInOrder(payload)) {
8176
+ entries.push(
8177
+ child.kind === "request" ? { type: "request", name: child.request.name || child.request.url, id: child.request.meta.id, action } : { type: "folder", name: child.folder.name, id: child.folder.meta.id, action }
8178
+ );
8128
8179
  }
8129
8180
  return entries;
8130
8181
  }
@@ -8532,119 +8583,6 @@ function scriptVariables(rows2) {
8532
8583
  return [...byName.values()];
8533
8584
  }
8534
8585
 
8535
- // ../core/src/vault/vault-crypto.ts
8536
- var KDF_ITERATIONS = 6e5;
8537
- var MAX_KDF_ITERATIONS = 1e7;
8538
- var IV_BYTES = 12;
8539
- var AES = { name: "AES-GCM", length: 256 };
8540
- var VAULT_FILE_FORMAT = "wayfarer/vault";
8541
- function fromBase64Url(text3) {
8542
- const binary = atob(text3.replace(/-/g, "+").replace(/_/g, "/"));
8543
- return Uint8Array.from(binary, (char) => char.charCodeAt(0));
8544
- }
8545
- var utf82 = (text3) => new TextEncoder().encode(text3);
8546
- async function deriveKek(passphrase, kdf) {
8547
- if (!crypto.subtle) {
8548
- throw new Error("The vault needs a secure page: open Wayfarer over https or on localhost.");
8549
- }
8550
- const material = await crypto.subtle.importKey("raw", utf82(passphrase), "PBKDF2", false, ["deriveKey"]);
8551
- return crypto.subtle.deriveKey(
8552
- { name: "PBKDF2", salt: fromBase64Url(kdf.salt), iterations: kdf.iterations, hash: "SHA-256" },
8553
- material,
8554
- AES,
8555
- false,
8556
- ["wrapKey", "unwrapKey"]
8557
- );
8558
- }
8559
- async function unwrap(record, passphrase, extractable) {
8560
- const bytes = fromBase64Url(record.wrappedDek);
8561
- const kek = await deriveKek(passphrase, record.kdf);
8562
- try {
8563
- return await crypto.subtle.unwrapKey(
8564
- "raw",
8565
- bytes.slice(IV_BYTES),
8566
- kek,
8567
- { name: "AES-GCM", iv: bytes.slice(0, IV_BYTES) },
8568
- AES,
8569
- extractable,
8570
- ["encrypt", "decrypt"]
8571
- );
8572
- } catch (error) {
8573
- if (error instanceof Error && error.name === "OperationError") return null;
8574
- throw error;
8575
- }
8576
- }
8577
- function unwrapDek(record, passphrase) {
8578
- return unwrap(record, passphrase, false);
8579
- }
8580
- async function decryptSecret(dek, id, envelope) {
8581
- const plain = await crypto.subtle.decrypt(
8582
- { name: "AES-GCM", iv: fromBase64Url(envelope.iv), additionalData: utf82(id) },
8583
- dek,
8584
- fromBase64Url(envelope.ct)
8585
- );
8586
- return new TextDecoder().decode(plain);
8587
- }
8588
- function field2(value, key) {
8589
- return typeof value === "object" && value !== null && Object.hasOwn(value, key) ? value[key] : void 0;
8590
- }
8591
- var isBase64Url = (value) => typeof value === "string" && /^[\w-]+$/.test(value);
8592
- function validateVaultFile(value) {
8593
- const issues = [];
8594
- const fail = (path, message) => issues.push({ path, message });
8595
- if (typeof value !== "object" || value === null) {
8596
- fail("$", "Value must be an object.");
8597
- return issues;
8598
- }
8599
- if (field2(value, "$id") !== VAULT_FILE_FORMAT) {
8600
- fail("$id", `Not a Wayfarer vault file: "$id" must be "${VAULT_FILE_FORMAT}".`);
8601
- return issues;
8602
- }
8603
- const vault = field2(value, "vault");
8604
- if (typeof vault !== "object" || vault === null) {
8605
- fail("vault", "Value must be an object.");
8606
- } else {
8607
- const kdf = field2(vault, "kdf");
8608
- if (typeof kdf !== "object" || kdf === null) {
8609
- fail("vault.kdf", "Value must be an object.");
8610
- } else {
8611
- if (field2(kdf, "alg") !== "PBKDF2-SHA256") fail("vault.kdf.alg", 'Value must be "PBKDF2-SHA256".');
8612
- const iterations = field2(kdf, "iterations");
8613
- if (!(Number.isInteger(iterations) && iterations >= KDF_ITERATIONS && iterations <= MAX_KDF_ITERATIONS)) {
8614
- fail("vault.kdf.iterations", `Value must be a whole number from ${KDF_ITERATIONS} to ${MAX_KDF_ITERATIONS}.`);
8615
- }
8616
- if (!isBase64Url(field2(kdf, "salt"))) fail("vault.kdf.salt", "Value must be base64url text.");
8617
- }
8618
- if (!isBase64Url(field2(vault, "wrappedDek"))) fail("vault.wrappedDek", "Value must be base64url text.");
8619
- }
8620
- const secrets = field2(value, "secrets");
8621
- if (!Array.isArray(secrets)) {
8622
- fail("secrets", "Value must be an array.");
8623
- return issues;
8624
- }
8625
- secrets.forEach((secret, index) => {
8626
- const path = `secrets[${index}]`;
8627
- if (typeof secret !== "object" || secret === null) {
8628
- fail(path, "Value must be an object.");
8629
- return;
8630
- }
8631
- const id = field2(secret, "id");
8632
- if (typeof id !== "string" || !id) fail(`${path}.id`, "Value must be a non-empty string.");
8633
- if (typeof field2(secret, "name") !== "string") fail(`${path}.name`, "Value must be a string.");
8634
- const environmentId = field2(secret, "environmentId");
8635
- if (environmentId !== void 0 && typeof environmentId !== "string")
8636
- fail(`${path}.environmentId`, "Value must be a string.");
8637
- const envelope = field2(secret, "envelope");
8638
- if (typeof envelope !== "object" || envelope === null) {
8639
- fail(`${path}.envelope`, "Value must be an object.");
8640
- return;
8641
- }
8642
- if (!isBase64Url(field2(envelope, "iv"))) fail(`${path}.envelope.iv`, "Value must be base64url text.");
8643
- if (!isBase64Url(field2(envelope, "ct"))) fail(`${path}.envelope.ct`, "Value must be base64url text.");
8644
- });
8645
- return issues;
8646
- }
8647
-
8648
8586
  // ../core/src/variables/resolver.ts
8649
8587
  var VARIABLE_SCOPES = ["local", "data", "environment", "folder", "collection", "global"];
8650
8588
  var MAX_VARIABLE_DEPTH = 10;
@@ -8904,13 +8842,15 @@ var SseParser = class {
8904
8842
  // ../core/src/transport/transport.ts
8905
8843
  var STREAM_BODY_BYTES = 1024 * 1024;
8906
8844
  var TransportError = class extends Error {
8907
- constructor(kind, message, status) {
8845
+ constructor(kind, message, status, responded = false) {
8908
8846
  super(message);
8909
8847
  this.kind = kind;
8910
8848
  this.status = status;
8849
+ this.responded = responded;
8911
8850
  }
8912
8851
  kind;
8913
8852
  status;
8853
+ responded;
8914
8854
  name = "TransportError";
8915
8855
  };
8916
8856
  var DISPLAY_CAP_BYTES = 50 * 1024 * 1024;
@@ -8920,10 +8860,13 @@ async function withDeadline(options, run) {
8920
8860
  const cancel = () => controller.abort();
8921
8861
  if (options.signal.aborted) cancel();
8922
8862
  options.signal.addEventListener("abort", cancel);
8923
- const timer = options.timeoutMs > 0 ? setTimeout(() => {
8924
- timedOut = true;
8925
- controller.abort();
8926
- }, options.timeoutMs) : void 0;
8863
+ const timer = options.timeoutMs > 0 ? setTimeout(
8864
+ () => {
8865
+ timedOut = true;
8866
+ controller.abort();
8867
+ },
8868
+ Math.min(options.timeoutMs, 2 ** 31 - 1)
8869
+ ) : void 0;
8927
8870
  try {
8928
8871
  return await run(controller.signal, () => clearTimeout(timer));
8929
8872
  } catch (cause) {
@@ -8940,7 +8883,7 @@ async function withDeadline(options, run) {
8940
8883
  options.signal.removeEventListener("abort", cancel);
8941
8884
  }
8942
8885
  }
8943
- async function readBody(response, cap = DISPLAY_CAP_BYTES) {
8886
+ async function readBody2(response, cap = DISPLAY_CAP_BYTES) {
8944
8887
  if (!response.body) return response.arrayBuffer();
8945
8888
  const reader2 = response.body.getReader();
8946
8889
  let chunks = [];
@@ -8974,7 +8917,7 @@ async function readEventStream(response, signal, onEvent) {
8974
8917
  const kept = new Uint8Array(STREAM_BODY_BYTES);
8975
8918
  let size = 0;
8976
8919
  let truncated = false;
8977
- let stopped = false;
8920
+ let stopped2 = false;
8978
8921
  const reader2 = response.body?.getReader();
8979
8922
  try {
8980
8923
  for (; ; ) {
@@ -8989,9 +8932,9 @@ async function readEventStream(response, signal, onEvent) {
8989
8932
  }
8990
8933
  } catch (error) {
8991
8934
  if (!signal.aborted) throw error;
8992
- stopped = true;
8935
+ stopped2 = true;
8993
8936
  }
8994
- return { body: kept.slice(0, size).buffer, stream: { stopped, ...truncated && { truncated } } };
8937
+ return { body: kept.slice(0, size).buffer, stream: { stopped: stopped2, ...truncated && { truncated } } };
8995
8938
  }
8996
8939
  function networkMessage(cause) {
8997
8940
  if (!(cause instanceof Error)) return String(cause);
@@ -9008,7 +8951,8 @@ var FetchTransport = class {
9008
8951
  fetchFn;
9009
8952
  displayCap;
9010
8953
  send(request, options) {
9011
- return withDeadline(options, async (signal, arrived) => {
8954
+ let responded = false;
8955
+ const sent = withDeadline(options, async (signal, arrived) => {
9012
8956
  const response = await this.fetchFn(request.url, {
9013
8957
  method: request.method,
9014
8958
  headers: request.headers,
@@ -9019,9 +8963,10 @@ var FetchTransport = class {
9019
8963
  redirect: "follow",
9020
8964
  signal
9021
8965
  });
8966
+ responded = true;
9022
8967
  const { onEvent } = options;
9023
8968
  const events = onEvent && /^text\/event-stream\b/i.test(response.headers.get("content-type") ?? "") ? (arrived(), await readEventStream(response, signal, onEvent)) : void 0;
9024
- const body3 = events?.body ?? await readBody(response, this.displayCap);
8969
+ const body3 = events?.body ?? await readBody2(response, this.displayCap);
9025
8970
  return {
9026
8971
  status: response.status,
9027
8972
  statusText: response.statusText,
@@ -9034,6 +8979,11 @@ var FetchTransport = class {
9034
8979
  ...events && { stream: events.stream }
9035
8980
  };
9036
8981
  });
8982
+ return sent.catch((error) => {
8983
+ if (responded && error instanceof TransportError && error.kind === "network")
8984
+ throw new TransportError("network", error.message, void 0, true);
8985
+ throw error;
8986
+ });
9037
8987
  }
9038
8988
  };
9039
8989
 
@@ -9736,7 +9686,7 @@ var _HMAC = class {
9736
9686
  this.iHash.destroy();
9737
9687
  }
9738
9688
  };
9739
- var hmac2 = /* @__PURE__ */ (() => {
9689
+ var hmac = /* @__PURE__ */ (() => {
9740
9690
  const hmac_ = ((hash, key, message) => new _HMAC(hash, key).update(message).digest());
9741
9691
  hmac_.create = (hash, key) => new _HMAC(hash, key);
9742
9692
  return hmac_;
@@ -10180,13 +10130,13 @@ var _SHA256 = class extends SHA2_32B {
10180
10130
  super(32, SHA256_IV);
10181
10131
  }
10182
10132
  };
10183
- var sha2562 = /* @__PURE__ */ createHasher(
10133
+ var sha256 = /* @__PURE__ */ createHasher(
10184
10134
  () => new _SHA256(),
10185
10135
  /* @__PURE__ */ oidNist(1)
10186
10136
  );
10187
10137
 
10188
10138
  // ../core/src/scripting/vm-crypto.ts
10189
- var HASHES = { MD5: md5, SHA1: sha1, SHA256: sha2562 };
10139
+ var HASHES = { MD5: md5, SHA1: sha1, SHA256: sha256 };
10190
10140
  var isVmHash = (name) => Object.hasOwn(HASHES, name);
10191
10141
  var VM_ENCODINGS = ["utf8", "hex", "base64"];
10192
10142
  function fromWords(json) {
@@ -10212,7 +10162,7 @@ function vmHash(name, format, payload) {
10212
10162
  return toWords(HASHES[name](bytesOf(format, payload)));
10213
10163
  }
10214
10164
  function vmHmac(name, keyFormat, key, format, payload) {
10215
- return toWords(hmac2(HASHES[name], bytesOf(keyFormat, key), bytesOf(format, payload)));
10165
+ return toWords(hmac(HASHES[name], bytesOf(keyFormat, key), bytesOf(format, payload)));
10216
10166
  }
10217
10167
  function vmEncode(encoding, text3) {
10218
10168
  if (encoding === "utf8") return toWords(new TextEncoder().encode(text3));
@@ -10351,8 +10301,17 @@ async function evaluate(vm, runtime, source, context, state, deadline, limits, s
10351
10301
  if (Date.now() >= deadline.at) return { message: "", limit: "timeout" };
10352
10302
  const kind = vm.typeof(error);
10353
10303
  if (kind === "string") return { message: vm.getString(error) };
10304
+ if (kind === "number" || kind === "boolean" || kind === "undefined") return { message: String(vm.dump(error)) };
10354
10305
  if (kind !== "object") return { message: "Script failed." };
10355
- if (vm.sameValue(error, vm.null)) return { message: "", limit: "memory" };
10306
+ if (vm.sameValue(error, vm.null)) {
10307
+ const probe = vm.evalCode("({})");
10308
+ if (probe.error) {
10309
+ probe.error.dispose();
10310
+ return { message: "", limit: "memory" };
10311
+ }
10312
+ probe.value.dispose();
10313
+ return { message: "null" };
10314
+ }
10356
10315
  const name = property(vm, error, "name");
10357
10316
  const message = property(vm, error, "message") ?? "Script failed.";
10358
10317
  if (name === "InternalError" && message === "out of memory") return { message, limit: "memory" };
@@ -10683,32 +10642,6 @@ function property(vm, object, name) {
10683
10642
  }
10684
10643
  }
10685
10644
 
10686
- // ../core/src/scripting/request-view.ts
10687
- function scriptRequestOf(content) {
10688
- const rows2 = (list4) => list4.filter((row) => row.enabled && row.key).map((row) => [row.key, row.value]);
10689
- const { body: body3 } = content;
10690
- const seen = body3.mode === "raw" ? { mode: "raw", raw: body3.raw?.text ?? "" } : body3.mode === "graphql" ? { mode: "raw", raw: graphqlText(body3.graphql?.query ?? "", body3.graphql?.variables ?? "") } : body3.mode === "urlencoded" ? { mode: "urlencoded", urlencoded: rows2(body3.urlencoded ?? []) } : { mode: body3.mode };
10691
- return { method: content.method, url: content.url, headers: rows2(content.headers), body: seen };
10692
- }
10693
- function withScriptRequest(content, request) {
10694
- const rows2 = (pairs2) => pairs2.map(([key, value]) => ({ key, value, enabled: true }));
10695
- const untouched = content.body.mode === "graphql" && request.body.mode === "raw" && request.body.raw === graphqlText(content.body.graphql?.query ?? "", content.body.graphql?.variables ?? "");
10696
- const body3 = untouched ? content.body : request.body.mode === "raw" ? {
10697
- ...content.body,
10698
- mode: "raw",
10699
- raw: {
10700
- language: content.body.mode === "graphql" ? "json" : content.body.raw?.language ?? "text",
10701
- text: request.body.raw
10702
- }
10703
- } : request.body.mode === "urlencoded" ? { ...content.body, mode: "urlencoded", urlencoded: rows2(request.body.urlencoded) } : content.body;
10704
- return { ...content, method: request.method, url: request.url, headers: rows2(request.headers), body: body3 };
10705
- }
10706
- function sentScriptRequest(content, sent) {
10707
- const { mode } = content.body;
10708
- const body3 = mode === "multipart" || mode === "binary" ? { mode } : sent.body === void 0 ? { mode: "none" } : mode === "urlencoded" ? { mode, urlencoded: [...new URLSearchParams(sent.body)] } : { mode: "raw", raw: sent.body };
10709
- return { method: sent.method, url: sent.url, headers: sent.headers, body: body3 };
10710
- }
10711
-
10712
10645
  // ../core/src/scripting/trust.ts
10713
10646
  function scriptsOf(holder) {
10714
10647
  return [holder.scripts.pre, holder.scripts.post].filter((script) => script.trim());
@@ -10720,7 +10653,8 @@ function runFailed(result) {
10720
10653
  }
10721
10654
  function requestsInOrder(tree, folderId) {
10722
10655
  const entered = /* @__PURE__ */ new Set();
10723
- const inOrder = (parent) => childrenInOrder(tree, parent).flatMap((child) => {
10656
+ const childrenOf = childrenInOrder(tree);
10657
+ const inOrder = (parent) => childrenOf(parent).flatMap((child) => {
10724
10658
  if (child.request) return [child.request];
10725
10659
  if (entered.has(child.folder.id)) return [];
10726
10660
  entered.add(child.folder.id);
@@ -10804,26 +10738,25 @@ var SECRET_PLACEHOLDER = /\{\{\s*\$secret\./;
10804
10738
  function containsSecretPlaceholder(request) {
10805
10739
  const wire = [
10806
10740
  request.url,
10807
- percentDecoded(request.url),
10741
+ formDecoded(request.url),
10808
10742
  JSON.stringify(request.headers),
10809
10743
  ...request.headers.map(([, value]) => decodeBasicCredentials(value)),
10810
10744
  // An OAuth 2.0 token request names its client in Basic credentials that are form-encoded first (RFC 6749, 2.3.1).
10811
- ...request.headers.map(([, value]) => percentDecoded(decodeBasicCredentials(value))),
10745
+ ...request.headers.map(([, value]) => formDecoded(decodeBasicCredentials(value))),
10812
10746
  ...bodyTexts(request.body)
10813
10747
  ];
10814
10748
  return wire.some((text3) => SECRET_PLACEHOLDER.test(text3));
10815
10749
  }
10816
- function percentDecoded(text3) {
10817
- try {
10818
- return decodeURIComponent(text3);
10819
- } catch (error) {
10820
- if (!(error instanceof URIError)) throw error;
10821
- return "";
10822
- }
10750
+ function formDecoded(text3) {
10751
+ return text3.replaceAll("+", " ").replace(
10752
+ /(?:%[0-9a-f]{2})+/gi,
10753
+ (run) => utf82.decode(Uint8Array.from(run.slice(1).split("%"), (hex2) => parseInt(hex2, 16)))
10754
+ );
10823
10755
  }
10756
+ var utf82 = new TextDecoder();
10824
10757
  function bodyTexts(body3) {
10825
10758
  if (typeof body3 === "string") {
10826
- return [body3, percentDecoded(body3.replaceAll("+", " "))];
10759
+ return [body3, formDecoded(body3)];
10827
10760
  }
10828
10761
  if (body3 instanceof FormData) {
10829
10762
  return [...body3].flatMap(([name, value]) => [name, typeof value === "string" ? value : value.name]);
@@ -10841,6 +10774,32 @@ function decodeBasicCredentials(headerValue) {
10841
10774
  }
10842
10775
  }
10843
10776
 
10777
+ // ../core/src/scripting/request-view.ts
10778
+ function scriptRequestOf(content) {
10779
+ const rows2 = (list4) => list4.filter((row) => row.enabled && row.key).map((row) => [row.key, row.value]);
10780
+ const { body: body3 } = content;
10781
+ const seen = body3.mode === "raw" ? { mode: "raw", raw: body3.raw?.text ?? "" } : body3.mode === "graphql" ? { mode: "raw", raw: graphqlText(body3.graphql?.query ?? "", body3.graphql?.variables ?? "") } : body3.mode === "urlencoded" ? { mode: "urlencoded", urlencoded: rows2(body3.urlencoded ?? []) } : { mode: body3.mode };
10782
+ return { method: content.method, url: content.url, headers: rows2(content.headers), body: seen };
10783
+ }
10784
+ function withScriptRequest(content, request) {
10785
+ const rows2 = (pairs2) => pairs2.map(([key, value]) => ({ key, value, enabled: true }));
10786
+ const untouched = content.body.mode === "graphql" && request.body.mode === "raw" && request.body.raw === graphqlText(content.body.graphql?.query ?? "", content.body.graphql?.variables ?? "");
10787
+ const body3 = untouched ? content.body : request.body.mode === "raw" ? {
10788
+ ...content.body,
10789
+ mode: "raw",
10790
+ raw: {
10791
+ language: content.body.mode === "graphql" ? "json" : content.body.raw?.language ?? "text",
10792
+ text: request.body.raw
10793
+ }
10794
+ } : request.body.mode === "urlencoded" ? { ...content.body, mode: "urlencoded", urlencoded: rows2(request.body.urlencoded) } : content.body;
10795
+ return { ...content, method: request.method, url: request.url, headers: rows2(request.headers), body: body3 };
10796
+ }
10797
+ function sentScriptRequest(content, sent) {
10798
+ const { mode } = content.body;
10799
+ const body3 = mode === "multipart" || mode === "binary" ? { mode } : sent.body === void 0 ? { mode: "none" } : mode === "urlencoded" ? { mode, urlencoded: [...new URLSearchParams(sent.body)] } : { mode: "raw", raw: sent.body };
10800
+ return { method: sent.method, url: sent.url, headers: sent.headers, body: body3 };
10801
+ }
10802
+
10844
10803
  // ../core/src/send/assertions.ts
10845
10804
  function runAssertions(assertions, response) {
10846
10805
  return assertions.map((assertion) => evaluate2(assertion, response));
@@ -11017,7 +10976,7 @@ function sentBodyPreview(body3) {
11017
10976
  return `@file ${file(body3)}`;
11018
10977
  }
11019
10978
  function scriptResponse(response) {
11020
- const body3 = bodyText(response.body);
10979
+ const body3 = response.text ?? bodyText(response.body);
11021
10980
  return {
11022
10981
  code: response.statusCode,
11023
10982
  status: response.statusText,
@@ -11097,6 +11056,7 @@ async function exchange(spec, ports) {
11097
11056
  scriptExtras(ports, spec, "prerequest", scriptRequestOf(template))
11098
11057
  );
11099
11058
  timings.preScriptMs += Math.round(performance.now() - scriptStarted);
11059
+ stopped(spec);
11100
11060
  preRuns.push({ name, result });
11101
11061
  const { changes } = result;
11102
11062
  restoreSecrets.push(await applyChanges(ports, spec, changes, notes));
@@ -11121,6 +11081,7 @@ async function exchange(spec, ports) {
11121
11081
  );
11122
11082
  }
11123
11083
  }
11084
+ stopped(spec);
11124
11085
  let request;
11125
11086
  try {
11126
11087
  const built2 = spec.buildRequest(template);
@@ -11154,6 +11115,7 @@ async function exchange(spec, ports) {
11154
11115
  };
11155
11116
  const transmit = async () => {
11156
11117
  startedAt = performance.now();
11118
+ delete policy.startedAt;
11157
11119
  sentCookies = void 0;
11158
11120
  try {
11159
11121
  return await ports.transport.send(
@@ -11189,7 +11151,7 @@ async function exchange(spec, ports) {
11189
11151
  outcome = await transmit();
11190
11152
  }
11191
11153
  }
11192
- const durationMs = Math.round(performance.now() - startedAt);
11154
+ const durationMs = Math.round(performance.now() - (policy.startedAt ?? startedAt));
11193
11155
  timings.requestMs = durationMs;
11194
11156
  ports.watch?.received(request.url);
11195
11157
  const shaped = outcome instanceof TransportError ? shapeFailure(outcome, ports.page, ports.online?.()) : shapeResponse(outcome, ports.page);
@@ -11214,6 +11176,7 @@ async function exchange(spec, ports) {
11214
11176
  statusCode: shaped.response.statusCode ?? 0,
11215
11177
  statusText: shaped.response.statusText ?? "",
11216
11178
  body: shaped.body,
11179
+ ...shaped.text !== void 0 && { text: shaped.text },
11217
11180
  headers: shaped.headers,
11218
11181
  durationMs,
11219
11182
  sizeBytes: outcome instanceof TransportError ? void 0 : outcome.sizes.decoded
@@ -11284,6 +11247,9 @@ async function exchange(spec, ports) {
11284
11247
  ...nextRequest !== void 0 && { nextRequest }
11285
11248
  };
11286
11249
  }
11250
+ function stopped(spec) {
11251
+ if (spec.signal?.aborted) throw new SendBlockedError("You stopped the request. Nothing was sent.");
11252
+ }
11287
11253
  function firstSentence(text3) {
11288
11254
  const match = /^.*?[.!?](?=\s|$)/s.exec(text3.trim());
11289
11255
  return (match ? match[0] : text3).trim();
@@ -11319,7 +11285,7 @@ function recordOf(history, raw) {
11319
11285
  };
11320
11286
  }
11321
11287
  function shapeResponse(envelope, page) {
11322
- const body3 = decodeEnvelope(envelope);
11288
+ const { value: body3, text: sentText } = readEnvelope(envelope);
11323
11289
  const isError = envelope.status < 200 || envelope.status >= 300;
11324
11290
  const shown = body3;
11325
11291
  const bodyIsJson = isJsonPayload(shown);
@@ -11328,6 +11294,7 @@ function shapeResponse(envelope, page) {
11328
11294
  const contentLength = Number(header("content-length") || NaN);
11329
11295
  return {
11330
11296
  body: body3,
11297
+ ...sentText !== void 0 && { text: sentText },
11331
11298
  headers: Object.fromEntries(envelope.headers),
11332
11299
  headerList: envelope.headers,
11333
11300
  bodyText: body3 === void 0 || body3 === null || body3 instanceof BinaryBody ? void 0 : text3,
@@ -11423,18 +11390,25 @@ async function sendForScript(ports, request, signal, route = ports.transport.rou
11423
11390
  );
11424
11391
  }
11425
11392
  const startedAt = performance.now();
11393
+ const policy = { route };
11426
11394
  const envelope = await ports.transport.send(
11427
11395
  request,
11428
- { signal: signal ?? new AbortController().signal, timeoutMs: ports.timeoutMs() },
11429
- { route }
11396
+ {
11397
+ signal: signal ?? new AbortController().signal,
11398
+ timeoutMs: ports.timeoutMs(),
11399
+ // Followed on every route, as a browser follows it: the vault is not read for this request, so no secret is held to its host.
11400
+ redirects: { follow: true, stayOnHost: false }
11401
+ },
11402
+ policy
11430
11403
  );
11431
11404
  const shaped = shapeResponse(envelope, ports.page);
11432
11405
  return scriptResponse({
11433
11406
  statusCode: envelope.status,
11434
11407
  statusText: envelope.statusText,
11435
11408
  body: shaped.body,
11409
+ ...shaped.text !== void 0 && { text: shaped.text },
11436
11410
  headers: shaped.headers,
11437
- durationMs: Math.round(performance.now() - startedAt),
11411
+ durationMs: Math.round(performance.now() - (policy.startedAt ?? startedAt)),
11438
11412
  sizeBytes: envelope.sizes.decoded
11439
11413
  });
11440
11414
  }
@@ -11521,6 +11495,88 @@ function stringifyPayload(payload) {
11521
11495
  return stringifyJson(payload, 4) ?? String(payload);
11522
11496
  }
11523
11497
 
11498
+ // ../core/src/auth/sigv4.ts
11499
+ var utf83 = (text3) => new TextEncoder().encode(text3);
11500
+ var sha2562 = async (data) => new Uint8Array(await crypto.subtle.digest("SHA-256", data));
11501
+ var hmac2 = async (key, data) => new Uint8Array(
11502
+ await crypto.subtle.sign(
11503
+ "HMAC",
11504
+ await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]),
11505
+ utf83(data)
11506
+ )
11507
+ );
11508
+ var hex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
11509
+ var encode = (text3) => encodeURIComponent(text3).replace(/[!'()*]/g, (mark) => `%${mark.charCodeAt(0).toString(16).toUpperCase()}`);
11510
+ var decode = (text3) => {
11511
+ try {
11512
+ return decodeURIComponent(text3);
11513
+ } catch (error) {
11514
+ if (!(error instanceof URIError)) throw error;
11515
+ return text3;
11516
+ }
11517
+ };
11518
+ async function signV4(request, credentials, at) {
11519
+ const url = new URL(request.url);
11520
+ const s3 = credentials.service === "s3";
11521
+ const segments = url.pathname.split("/");
11522
+ const kept = segments.filter(Boolean);
11523
+ const path = s3 ? segments.map((segment) => encode(decode(segment))).join("/") : `/${kept.map(encode).join("/")}${kept.length && url.pathname.endsWith("/") ? "/" : ""}`;
11524
+ const query = url.search.slice(1).split("&").filter(Boolean).map((pair) => {
11525
+ const cut = pair.indexOf("=");
11526
+ return [
11527
+ encode(decode(cut === -1 ? pair : pair.slice(0, cut))),
11528
+ encode(decode(cut === -1 ? "" : pair.slice(cut + 1)))
11529
+ ];
11530
+ }).sort(
11531
+ ([keyA, valueA], [keyB, valueB]) => keyA === keyB ? valueA < valueB ? -1 : valueA > valueB ? 1 : 0 : keyA < keyB ? -1 : 1
11532
+ ).map(([key2, value]) => `${key2}=${value}`).join("&");
11533
+ const date = at.toISOString().replace(/[-:]|\.\d{3}/g, "");
11534
+ const day = date.slice(0, 8);
11535
+ const body3 = typeof request.body === "string" ? utf83(request.body) : new Uint8Array(request.body ?? []);
11536
+ const bodyHash = hex(await sha2562(body3));
11537
+ const added2 = [
11538
+ ["X-Amz-Date", date],
11539
+ ...credentials.sessionToken ? [["X-Amz-Security-Token", credentials.sessionToken]] : [],
11540
+ ...s3 ? [["X-Amz-Content-Sha256", bodyHash]] : []
11541
+ ];
11542
+ const byName = /* @__PURE__ */ new Map();
11543
+ const own3 = request.headers.filter(
11544
+ ([name]) => !added2.some(([written]) => written.toLowerCase() === name.toLowerCase()) && name.toLowerCase() !== "host"
11545
+ );
11546
+ for (const [name, value] of [["Host", url.host], ...own3, ...added2]) {
11547
+ const key2 = name.toLowerCase();
11548
+ byName.set(key2, [...byName.get(key2) ?? [], value.trim().replace(/\s+/g, " ")]);
11549
+ }
11550
+ const names = [...byName.keys()].sort();
11551
+ const signedHeaders = names.join(";");
11552
+ const canonicalRequest = [
11553
+ request.method,
11554
+ path,
11555
+ query,
11556
+ ...names.map((name) => `${name}:${(byName.get(name) ?? []).join(",")}`),
11557
+ "",
11558
+ signedHeaders,
11559
+ bodyHash
11560
+ ].join("\n");
11561
+ const scope = `${day}/${credentials.region}/${credentials.service}/aws4_request`;
11562
+ const stringToSign = ["AWS4-HMAC-SHA256", date, scope, hex(await sha2562(utf83(canonicalRequest)))].join("\n");
11563
+ let key = utf83(`AWS4${credentials.secretKey}`);
11564
+ for (const part of [day, credentials.region, credentials.service, "aws4_request"]) key = await hmac2(key, part);
11565
+ const signature = hex(await hmac2(key, stringToSign));
11566
+ return {
11567
+ headers: [
11568
+ ...added2,
11569
+ [
11570
+ "Authorization",
11571
+ `AWS4-HMAC-SHA256 Credential=${credentials.accessKey}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
11572
+ ]
11573
+ ],
11574
+ canonicalRequest,
11575
+ stringToSign,
11576
+ signature
11577
+ };
11578
+ }
11579
+
11524
11580
  // ../core/src/send/build.ts
11525
11581
  var UnresolvedVariablesError = class extends SendBlockedError {
11526
11582
  constructor(names) {
@@ -11544,6 +11600,14 @@ function normalizeUrl(text3) {
11544
11600
  const address2 = text3.replace(/[\t\n\r]/g, encodeURIComponent);
11545
11601
  return hasExplicitScheme(address2) ? address2 : `https://${address2}`;
11546
11602
  }
11603
+ function validateUrl(text3) {
11604
+ if (!text3) return false;
11605
+ const parsed2 = URL.parse(normalizeUrl(text3));
11606
+ if (!parsed2 || parsed2.protocol !== "http:" && parsed2.protocol !== "https:" || !parsed2.hostname) return false;
11607
+ if (hasExplicitScheme(text3)) return true;
11608
+ const { hostname } = parsed2;
11609
+ return hostname === "localhost" || hostname.includes(".") || hostname.includes(":") || !!parsed2.port;
11610
+ }
11547
11611
  function appendQueryParam(url, key, value) {
11548
11612
  const parsed2 = URL.parse(normalizeUrl(url));
11549
11613
  if (!parsed2) {
@@ -11598,12 +11662,18 @@ function buildRequest(content, context) {
11598
11662
  if (resolver.unresolved.size && context.blockUnresolved) {
11599
11663
  throw new UnresolvedVariablesError([...resolver.unresolved]);
11600
11664
  }
11665
+ if (!validateUrl(resolver.resolve(content.url.trim()))) {
11666
+ throw new SendBlockedError(
11667
+ "This is not an address a request can go to. Write it as https://host/path. Nothing was sent."
11668
+ );
11669
+ }
11601
11670
  return request;
11602
11671
  };
11603
11672
  return built2 instanceof Promise ? built2.then(check) : check(built2);
11604
11673
  };
11605
- if (!probe.lockedSecrets.size) return checked(new VariableResolver(scopes()));
11606
- const withSecrets = () => context.readSecrets([...probe.lockedSecrets]).then((secrets) => checked(new VariableResolver(scopes(), (id) => secrets.get(id))));
11674
+ const oauth = sentAuth(content.auth, context.ancestors).type === "oauth2";
11675
+ if (!probe.lockedSecrets.size && !oauth) return checked(new VariableResolver(scopes()));
11676
+ const withSecrets = () => probe.lockedSecrets.size ? context.readSecrets([...probe.lockedSecrets]).then((secrets) => checked(new VariableResolver(scopes(), (id) => secrets.get(id)))) : checked(new VariableResolver(scopes()));
11607
11677
  const from = composed && hostOf(composed.url, composed.scopes);
11608
11678
  const to = hostOf(endpointText, scopes());
11609
11679
  if (from === void 0 || from === to || composed?.accepted?.has(to)) return withSecrets();
@@ -12105,6 +12175,7 @@ var VALUE_OPTIONS = {
12105
12175
  "--data-raw": "data-raw",
12106
12176
  "--data-binary": "data-binary",
12107
12177
  "--data-urlencode": "data-urlencode",
12178
+ "--json": "json",
12108
12179
  "-F": "form",
12109
12180
  "--form": "form",
12110
12181
  "--form-string": "form-string",
@@ -12210,7 +12281,16 @@ var SKIPPED_VALUE_OPTIONS = /* @__PURE__ */ new Set([
12210
12281
  "--trace-ascii",
12211
12282
  "--stderr",
12212
12283
  "--proto",
12213
- "--proto-default"
12284
+ "--proto-default",
12285
+ "--url-query",
12286
+ "--proxy-header",
12287
+ "--socks5",
12288
+ "--socks5-hostname",
12289
+ "--preproxy",
12290
+ "--request-target",
12291
+ "--doh-url",
12292
+ "--output-dir",
12293
+ "--netrc-file"
12214
12294
  ]);
12215
12295
  var formEncode = (text3) => new URLSearchParams([[text3, ""]]).toString().slice(0, -1);
12216
12296
  var contentType = (headers2) => headers2.filter(([name]) => name.toLowerCase() === "content-type").at(-1)?.[1];
@@ -12252,7 +12332,7 @@ function parseCurl(text3) {
12252
12332
  const urls = [];
12253
12333
  let method;
12254
12334
  let basic;
12255
- let [get, head, binaryOnly] = [false, false, true];
12335
+ let [get, head, binaryOnly, json] = [false, false, true, false];
12256
12336
  const take = (kind, value) => {
12257
12337
  switch (kind) {
12258
12338
  case "method":
@@ -12267,7 +12347,9 @@ function parseCurl(text3) {
12267
12347
  }
12268
12348
  case "data":
12269
12349
  case "data-binary":
12270
- if (kind === "data") binaryOnly = false;
12350
+ case "json":
12351
+ if (kind === "json") json = true;
12352
+ if (kind !== "data-binary") binaryOnly = false;
12271
12353
  data.push(value.startsWith("@") ? { file: value.slice(1) } : { text: value });
12272
12354
  break;
12273
12355
  case "data-raw":
@@ -12346,6 +12428,11 @@ function parseCurl(text3) {
12346
12428
  `The command names ${urls.length} addresses. The first is used; the others were left out: ${urls.slice(1).join(", ")}.`
12347
12429
  );
12348
12430
  let url = urls[0];
12431
+ if (json) {
12432
+ for (const name of ["Content-Type", "Accept"])
12433
+ if (!headers2.some(([named2]) => named2.toLowerCase() === name.toLowerCase()))
12434
+ headers2.push([name, "application/json"]);
12435
+ }
12349
12436
  const type = (contentType(headers2) ?? "").toLowerCase();
12350
12437
  let body3 = { mode: "none" };
12351
12438
  const files = data.flatMap((datum) => "file" in datum ? [datum.file] : []);
@@ -12362,7 +12449,9 @@ function parseCurl(text3) {
12362
12449
  } else if (data.length) {
12363
12450
  if (files.length) warnings.push(`The data of the file ${files.join(", ")} was left out: the file was not read.`);
12364
12451
  const wire = data.flatMap((datum) => "text" in datum ? [datum.text] : []).join("&");
12365
- body3 = !type || type.startsWith("application/x-www-form-urlencoded") ? { mode: "urlencoded", fields: [...new URLSearchParams(wire)] } : { mode: "raw", text: wire };
12452
+ const pairs2 = wire.split("&").every((piece) => /^[\w.~%+\-[\]]*(=|$)/.test(piece));
12453
+ if (!type && !pairs2) headers2.push(["Content-Type", "application/x-www-form-urlencoded"]);
12454
+ body3 = !type && pairs2 || type.startsWith("application/x-www-form-urlencoded") ? { mode: "urlencoded", fields: [...new URLSearchParams(wire)] } : { mode: "raw", text: wire };
12366
12455
  }
12367
12456
  method ??= head ? "HEAD" : body3.mode === "none" ? "GET" : "POST";
12368
12457
  if (!isHttpMethod(method))
@@ -13197,7 +13286,9 @@ function format4(value) {
13197
13286
  const children = /* @__PURE__ */ new Map();
13198
13287
  for (const resource of resources) {
13199
13288
  const parent = text2(field3(resource, "parentId"));
13200
- children.set(parent, [...children.get(parent) ?? [], resource]);
13289
+ const siblings = children.get(parent);
13290
+ if (siblings) siblings.push(resource);
13291
+ else children.set(parent, [resource]);
13201
13292
  }
13202
13293
  for (const list4 of children.values()) list4.sort((a, b) => sortKey(a) - sortKey(b));
13203
13294
  const under = (parent, type) => parent === "" ? [] : (children.get(parent) ?? []).filter((resource) => kind(resource) === type);
@@ -13667,13 +13758,17 @@ var NOT_READABLE = "The file is neither JSON nor YAML that can be read, so it is
13667
13758
  function parseYaml(text3) {
13668
13759
  if (text3.length > MAX_YAML_CHARS) throw new ImportError(YAML_TOO_LARGE);
13669
13760
  try {
13670
- return (0, import_yaml.parse)(text3, {
13671
- maxAliasCount: MAX_YAML_ALIASES,
13672
- schema: "core",
13673
- logLevel: "error",
13674
- uniqueKeys: false,
13675
- prettyErrors: false
13676
- });
13761
+ return JSON.parse(
13762
+ JSON.stringify(
13763
+ (0, import_yaml.parse)(text3, {
13764
+ maxAliasCount: MAX_YAML_ALIASES,
13765
+ schema: "core",
13766
+ logLevel: "error",
13767
+ uniqueKeys: false,
13768
+ prettyErrors: false
13769
+ })
13770
+ )
13771
+ );
13677
13772
  } catch (error) {
13678
13773
  throw new ImportError(error instanceof Error && /alias count/i.test(error.message) ? YAML_ALIASES : NOT_READABLE);
13679
13774
  }
@@ -13782,7 +13877,13 @@ function added(payload) {
13782
13877
  return { payload: checked, plan };
13783
13878
  }
13784
13879
  function importText(text3, options = {}) {
13785
- const imported = readText(text3, options);
13880
+ let imported;
13881
+ try {
13882
+ imported = readText(text3, options);
13883
+ } catch (error) {
13884
+ if (error instanceof RangeError) throw new ImportError("The file is nested too deeply to read.");
13885
+ throw error;
13886
+ }
13786
13887
  return {
13787
13888
  ...imported,
13788
13889
  report: {
@@ -14053,7 +14154,7 @@ function runReportHtml(report, info) {
14053
14154
  :root { color-scheme: light dark; ${palette(REPORT_COLOURS.light)} }
14054
14155
  @media (prefers-color-scheme: dark) { :root { ${palette(REPORT_COLOURS.dark)} } }
14055
14156
  body { margin: 0; padding: 24px 16px; background: var(--canvas); color: var(--primary);
14056
- font: 14px/1.5 "Atkinson Hyperlegible Next", system-ui, sans-serif; }
14157
+ font: 14px/1.5 Figtree, system-ui, sans-serif; }
14057
14158
  main { max-width: 1100px; margin: 0 auto; }
14058
14159
  h1 { margin: 0 0 4px; font-size: 1.375rem; font-weight: 600; }
14059
14160
  .meta, .counts { margin: 0 0 16px; color: var(--secondary); }
@@ -14262,6 +14363,8 @@ Options:
14262
14363
  -e, --environment <file> The variables: a Wayfarer environments file or a
14263
14364
  Postman environment. A workspace backup brings
14264
14365
  its active environment unless one is given.
14366
+ --environment-name <name>
14367
+ Which environment of a file that holds several.
14265
14368
  -d, --data <file> A data file, CSV or JSON: one iteration per row.
14266
14369
  -n, --iterations <n> How many iterations (default: one per row of the
14267
14370
  data file, or 1).
@@ -14272,8 +14375,9 @@ Options:
14272
14375
  in --out.
14273
14376
  --out <dir> Where the reports are written (default: here).
14274
14377
  --bail Stop at the first failure.
14275
- --timeout <ms> Give up on a request after this long (default: no
14276
- limit).
14378
+ --timeout <ms> Give up on a request after this long, at most
14379
+ 300000 (default 300000: Node's fetch waits no
14380
+ longer for a response).
14277
14381
  --insecure Do not verify servers' TLS certificates.
14278
14382
  --allow-scripts Run the file's scripts. A file's scripts are not
14279
14383
  reviewed, so a collection that has any is not run
@@ -14284,8 +14388,10 @@ Options:
14284
14388
  -v, --version
14285
14389
  -h, --help
14286
14390
 
14287
- Exit codes: 0 every request answered and every test passed; 1 a test failed
14288
- or a request got no response; 2 nothing was run (see the message).`;
14391
+ Exit codes: 0 every request answered and every test passed; 1 a test failed,
14392
+ a request got no response, or a report could not be written; 2 nothing was
14393
+ run (see the message).`;
14394
+ var NODE_FETCH_TIMEOUT_MS = 3e5;
14289
14395
  var Refusal = class extends Error {
14290
14396
  };
14291
14397
  async function readText2(file, what) {
@@ -14297,7 +14403,7 @@ async function readText2(file, what) {
14297
14403
  );
14298
14404
  }
14299
14405
  }
14300
- function pick(list4, name, what, file) {
14406
+ function pick(list4, name, what, flag2, file) {
14301
14407
  if (name !== void 0) {
14302
14408
  const found = list4.find((entry) => entry.name === name);
14303
14409
  if (!found)
@@ -14308,7 +14414,7 @@ function pick(list4, name, what, file) {
14308
14414
  }
14309
14415
  if (list4.length > 1)
14310
14416
  throw new Refusal(
14311
- `${file} holds ${list4.length} ${what}s: name one with --${what} (${list4.map((entry) => `"${entry.name}"`).join(", ")}).`
14417
+ `${file} holds ${list4.length} ${what}s: name one with --${flag2} (${list4.map((entry) => `"${entry.name}"`).join(", ")}).`
14312
14418
  );
14313
14419
  const [only] = list4;
14314
14420
  if (!only) throw new Refusal(`${file} holds no ${what}.`);
@@ -14334,6 +14440,7 @@ async function readSource(file, collection) {
14334
14440
  collections.map(({ payload }) => ({ name: payload.collection.name, payload })),
14335
14441
  collection,
14336
14442
  "collection",
14443
+ "collection",
14337
14444
  file
14338
14445
  ).payload,
14339
14446
  globals: []
@@ -14348,7 +14455,7 @@ async function readSource(file, collection) {
14348
14455
  throw new Refusal(`${file} is not a workspace backup this version reads:
14349
14456
  ${errors.join("\n ")}`);
14350
14457
  const { stores } = parsed2.ok ? parsed2.value : {};
14351
- const chosen = pick(stores.collections, collection, "collection", file);
14458
+ const chosen = pick(stores.collections, collection, "collection", "collection", file);
14352
14459
  const source = {
14353
14460
  $id: "wayfarer/collection",
14354
14461
  meta: chosen.meta,
@@ -14370,11 +14477,11 @@ async function readSource(file, collection) {
14370
14477
  }
14371
14478
  };
14372
14479
  }
14373
- async function readEnvironment(file) {
14480
+ async function readEnvironment(file, name) {
14374
14481
  try {
14375
14482
  const { environments } = importText(await readText2(file, "environment"));
14376
- const { name, vars } = pick(environments, void 0, "environment", file);
14377
- return { name, vars };
14483
+ const chosen = pick(environments, name, "environment", "environment-name", file);
14484
+ return { name: chosen.name, vars: chosen.vars };
14378
14485
  } catch (error) {
14379
14486
  if (error instanceof Error && error.name === "ImportError") throw new Refusal(`${file}: ${error.message}`);
14380
14487
  throw error;
@@ -14420,6 +14527,7 @@ async function main(argv) {
14420
14527
  allowPositionals: true,
14421
14528
  options: {
14422
14529
  environment: { type: "string", short: "e" },
14530
+ "environment-name": { type: "string" },
14423
14531
  data: { type: "string", short: "d" },
14424
14532
  iterations: { type: "string", short: "n" },
14425
14533
  collection: { type: "string" },
@@ -14435,7 +14543,7 @@ async function main(argv) {
14435
14543
  }
14436
14544
  });
14437
14545
  if (values.version) {
14438
- console.log("2.1.0");
14546
+ console.log("2.2.0");
14439
14547
  return 0;
14440
14548
  }
14441
14549
  const [command, file, ...rest] = positionals;
@@ -14459,6 +14567,8 @@ ${USAGE}`);
14459
14567
  const iterations = number(values.iterations, "iterations");
14460
14568
  if (iterations === 0) throw new Refusal("--iterations takes 1 or more.");
14461
14569
  const timeoutMs = number(values.timeout, "timeout") ?? 0;
14570
+ if (timeoutMs > NODE_FETCH_TIMEOUT_MS)
14571
+ throw new Refusal(`--timeout takes at most ${NODE_FETCH_TIMEOUT_MS}: Node's fetch waits no longer for a response.`);
14462
14572
  const read = await readSource(file, values.collection);
14463
14573
  const holders = scriptHolders(read.source);
14464
14574
  if (holders.length && !values["allow-scripts"]) {
@@ -14466,7 +14576,9 @@ ${USAGE}`);
14466
14576
  `This collection has scripts, in ${holders.join(", ")}. A file's scripts have not been reviewed: read them, and run again with --allow-scripts only if you trust them. Nothing was sent.`
14467
14577
  );
14468
14578
  }
14469
- const environment2 = values.environment === void 0 ? read.environment : await readEnvironment(values.environment);
14579
+ if (values["environment-name"] !== void 0 && values.environment === void 0)
14580
+ throw new Refusal("--environment-name names an environment of the file given with --environment.");
14581
+ const environment2 = values.environment === void 0 ? read.environment : await readEnvironment(values.environment, values["environment-name"]);
14470
14582
  const vault = values["vault-file"] === void 0 ? read.vault : await readVaultFile(values["vault-file"]);
14471
14583
  let data;
14472
14584
  if (values.data !== void 0) {
@@ -14507,12 +14619,21 @@ ${USAGE}`);
14507
14619
  const info = { name: read.source.collection.name, startedAt };
14508
14620
  if (reporters.has("cli")) console.log(`
14509
14621
  ${summary(report)}`);
14510
- if (reporters.has("json"))
14511
- await writeFile(join(values.out, "wayfarer-run.json"), `${JSON.stringify(runReportJson(report, info), null, 2)}
14512
- `);
14513
- if (reporters.has("junit")) await writeFile(join(values.out, "wayfarer-run.junit.xml"), junitXml(report, info));
14514
- if (reporters.has("html")) await writeFile(join(values.out, "wayfarer-run.html"), runReportHtml(report, info));
14515
- return report.results.some(runFailed) ? 1 : 0;
14622
+ const code = report.results.some(runFailed) ? 1 : 0;
14623
+ try {
14624
+ if (reporters.has("json"))
14625
+ await writeFile(
14626
+ join(values.out, "wayfarer-run.json"),
14627
+ `${JSON.stringify(runReportJson(report, info), null, 2)}
14628
+ `
14629
+ );
14630
+ if (reporters.has("junit")) await writeFile(join(values.out, "wayfarer-run.junit.xml"), junitXml(report, info));
14631
+ if (reporters.has("html")) await writeFile(join(values.out, "wayfarer-run.html"), runReportHtml(report, info));
14632
+ } catch (error) {
14633
+ console.error(`--out ${values.out}: ${error instanceof Error ? error.message : String(error)}`);
14634
+ return 1;
14635
+ }
14636
+ return code;
14516
14637
  }
14517
14638
  main(process.argv.slice(2)).then(
14518
14639
  (code) => process.exitCode = code,