@truenas/api-client 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2389,8 +2389,11 @@ var apiVersionConfig = {
2389
2389
  * - DNS lookup failure
2390
2390
  * - Server unreachable
2391
2391
  *
2392
- * Using this fallback means systems with genuine network issues will still
2393
- * attempt connection (and fail during WebSocket handshake instead of immediately).
2392
+ * Which is why it is no longer reached on all of them. A discovery failure is
2393
+ * now put to a reachability probe and a second attempt first, and only an
2394
+ * appliance that answers the probe and still will not serve `/api/versions`
2395
+ * is assumed to be this version. One that answers nothing is reported as
2396
+ * unreachable rather than pinned to a version nobody confirmed.
2394
2397
  */
2395
2398
  FALLBACK_VERSION: "v25.10.0"
2396
2399
  };
@@ -3753,6 +3756,12 @@ var InvalidVersionResponseError = class extends VersionDiscoveryError {
3753
3756
  }
3754
3757
  };
3755
3758
  var discoveryTimeoutMs = 5e3;
3759
+ var probeTimeoutMs = 5e3;
3760
+ function corsIsEnforced() {
3761
+ const hasDocument = typeof window !== "undefined" && typeof window.document !== "undefined";
3762
+ const inWorker = typeof globalThis.WorkerGlobalScope !== "undefined";
3763
+ return hasDocument || inWorker;
3764
+ }
3756
3765
  function hasErrorName(error, expected) {
3757
3766
  return typeof error === "object" && error !== null && "name" in error && error.name === expected;
3758
3767
  }
@@ -3765,6 +3774,42 @@ var VersionDiscovery = class {
3765
3774
  versionsUrl(hostname) {
3766
3775
  return `${httpScheme(this.protocol)}//${hostname}/api/versions`;
3767
3776
  }
3777
+ /**
3778
+ * Whether the appliance answers at all, asked in a way CORS cannot block.
3779
+ *
3780
+ * A `no-cors` request yields a response the page may not read, but its
3781
+ * *arrival* is the whole answer here: something served it. That separates the
3782
+ * two failures `fetch` reports identically — a box that refused to share its
3783
+ * versions with this origin, and a box that is not there.
3784
+ *
3785
+ * `cannot-ask` where CORS is not enforced: nothing is blocking anything, so a
3786
+ * failed fetch already means unreachable and a probe would only repeat what
3787
+ * discovery just found. It is not a quieter way of saying "no".
3788
+ */
3789
+ async probeReachable(hostname) {
3790
+ if (!corsIsEnforced()) return "cannot-ask";
3791
+ const url = this.versionsUrl(hostname);
3792
+ const controller = new AbortController();
3793
+ const timer2 = setTimeout(() => controller.abort(), probeTimeoutMs);
3794
+ try {
3795
+ await fetch(url, {
3796
+ mode: "no-cors",
3797
+ cache: "no-store",
3798
+ signal: controller.signal
3799
+ });
3800
+ this.logger.info("Reachability probe answered", { hostname, url });
3801
+ return "reachable";
3802
+ } catch (error) {
3803
+ this.logger.warn("Reachability probe got no answer", {
3804
+ hostname,
3805
+ url,
3806
+ error
3807
+ });
3808
+ return "silent";
3809
+ } finally {
3810
+ clearTimeout(timer2);
3811
+ }
3812
+ }
3768
3813
  /**
3769
3814
  * Discovers the API version for a given hostname.
3770
3815
  *
@@ -4024,30 +4069,81 @@ async function createTrueNasClient(opts) {
4024
4069
  });
4025
4070
  throw error;
4026
4071
  }
4027
- const fallbackVersionString = apiVersionConfig.FALLBACK_VERSION;
4028
- const fallbackVersion = parseApiVersion(fallbackVersionString);
4029
- if (!fallbackVersion) {
4030
- logger.error("Invalid fallback version configuration", {
4031
- uuid: uuid.slice(0, 8),
4032
- hostnames: hostnames.join(", "),
4033
- fallbackVersion: fallbackVersionString
4034
- });
4035
- throw error;
4072
+ const reachable = await probeAnyHostname(hostnames, versionDiscovery);
4073
+ if (reachable === "silent") {
4074
+ await sleep(unreachableRetryDelayMs);
4036
4075
  }
4037
- logger.warn(
4038
- "Version discovery failed with a network error (possible CORS or network issue), falling back to assumed version",
4039
- {
4076
+ try {
4077
+ const retryWinner = await discoverVersionFromAnyHostname(
4078
+ hostnames,
4079
+ versionDiscovery
4080
+ );
4081
+ logger.info("Version discovery succeeded on retry", {
4040
4082
  uuid: uuid.slice(0, 8),
4041
- hostnames: hostnames.join(", "),
4042
- fallbackVersion: fallbackVersionString,
4043
- originalError: errorMessage,
4044
- warning: "A network error has multiple causes (CORS, network down, DNS failure). The connection may still fail during the WebSocket handshake."
4083
+ hostname: retryWinner.hostname,
4084
+ version: retryWinner.version.version,
4085
+ firstError: errorMessage
4086
+ });
4087
+ version = retryWinner.version;
4088
+ } catch (retryError) {
4089
+ if (!(retryError instanceof VersionDiscoveryNetworkError)) {
4090
+ throw retryError;
4045
4091
  }
4046
- );
4047
- version = fallbackVersion;
4092
+ if (reachable !== "reachable") {
4093
+ logger.error(
4094
+ "Version discovery failed and the appliance did not answer a reachability probe; not assuming a version",
4095
+ {
4096
+ uuid: uuid.slice(0, 8),
4097
+ hostnames: hostnames.join(", "),
4098
+ probe: reachable === "silent" ? "no answer" : "not applicable",
4099
+ error: errorMessageOrDefault(retryError, "Unknown error")
4100
+ }
4101
+ );
4102
+ throw retryError;
4103
+ }
4104
+ const fallbackVersionString = apiVersionConfig.FALLBACK_VERSION;
4105
+ const fallbackVersion = parseApiVersion(fallbackVersionString);
4106
+ if (!fallbackVersion) {
4107
+ logger.error("Invalid fallback version configuration", {
4108
+ uuid: uuid.slice(0, 8),
4109
+ hostnames: hostnames.join(", "),
4110
+ fallbackVersion: fallbackVersionString
4111
+ });
4112
+ throw retryError;
4113
+ }
4114
+ logger.warn(
4115
+ "Appliance is reachable but version discovery is still blocked; falling back to assumed version",
4116
+ {
4117
+ uuid: uuid.slice(0, 8),
4118
+ hostnames: hostnames.join(", "),
4119
+ fallbackVersion: fallbackVersionString,
4120
+ originalError: errorMessage,
4121
+ warning: "The appliance answered a probe but not /api/versions, which is what v25.10.0 looks like from a browser. If it is newer than that, this client is now pinned to the wrong surface."
4122
+ }
4123
+ );
4124
+ version = fallbackVersion;
4125
+ }
4048
4126
  }
4049
4127
  return instantiateClientForVersion(version, opts, logger);
4050
4128
  }
4129
+ var unreachableRetryDelayMs = 2500;
4130
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4131
+ async function probeAnyHostname(hostnames, versionDiscovery) {
4132
+ const probes = hostnames.map(
4133
+ (hostname) => versionDiscovery.probeReachable(hostname)
4134
+ );
4135
+ const remaining = new Set(probes);
4136
+ let sawSilence = false;
4137
+ while (remaining.size > 0) {
4138
+ const settled = await Promise.race(
4139
+ [...remaining].map((probe) => probe.then((result) => ({ probe, result })))
4140
+ );
4141
+ if (settled.result === "reachable") return "reachable";
4142
+ if (settled.result === "silent") sawSilence = true;
4143
+ remaining.delete(settled.probe);
4144
+ }
4145
+ return sawSilence ? "silent" : "cannot-ask";
4146
+ }
4051
4147
  async function discoverVersionFromAnyHostname(hostnames, versionDiscovery) {
4052
4148
  const attempts = hostnames.map(
4053
4149
  (hostname) => rxjs.firstValueFrom(versionDiscovery.discoverVersion(hostname)).then(