@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 +116 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -10
- package/dist/index.d.ts +40 -10
- package/dist/index.js +116 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -30429,9 +30429,10 @@ interface CreateClientOptions {
|
|
|
30429
30429
|
*
|
|
30430
30430
|
* Omitting it against a plaintext appliance is the quieter failure and the
|
|
30431
30431
|
* likelier one, since it is the case this option exists for. Discovery tries
|
|
30432
|
-
* `https
|
|
30433
|
-
* v25.10.0 has on `/api/versions
|
|
30434
|
-
*
|
|
30432
|
+
* `https` and `fetch` rejects, which reads exactly like the CORS block
|
|
30433
|
+
* v25.10.0 has on `/api/versions`. The reachability probe now runs on the same
|
|
30434
|
+
* scheme and fails the same way, so the mismatch is reported as unreachable
|
|
30435
|
+
* rather than quietly answered with a v25.10.0 client.
|
|
30435
30436
|
*/
|
|
30436
30437
|
protocol?: ApplianceProtocol;
|
|
30437
30438
|
}
|
|
@@ -30545,11 +30546,18 @@ interface CreateClientOptions {
|
|
|
30545
30546
|
* @returns a Promise that resolves with the created client, or rejects with a
|
|
30546
30547
|
* {@link VersionDiscoveryError} subclass (or a client-selection error).
|
|
30547
30548
|
* Rejects if version discovery on all hostnames *fails* and is not recoverable.
|
|
30548
|
-
*
|
|
30549
|
-
*
|
|
30550
|
-
*
|
|
30551
|
-
*
|
|
30552
|
-
*
|
|
30549
|
+
*
|
|
30550
|
+
* A `VersionDiscoveryNetworkError` is not by itself a verdict: `fetch` reports
|
|
30551
|
+
* a CORS refusal, an absent appliance, a bad name and the wrong scheme
|
|
30552
|
+
* identically. So that failure opens a reachability probe and a second
|
|
30553
|
+
* discovery attempt. A retry that succeeds is used; one that fails with a
|
|
30554
|
+
* specific error raises that instead; an appliance that answers nothing
|
|
30555
|
+
* rejects with the network error. Only one that answers the probe and still
|
|
30556
|
+
* will not serve `/api/versions` — which is what 25.10.0 looks like from a
|
|
30557
|
+
* browser — falls back to `FALLBACK_VERSION`.
|
|
30558
|
+
*
|
|
30559
|
+
* A network error alongside a version-compatibility error or a 404 does
|
|
30560
|
+
* *not* reach any of that — see `selectRepresentativeFailure`.
|
|
30553
30561
|
*/
|
|
30554
30562
|
declare function createTrueNasClient<V extends SupportedApiVersion>(opts: CreateClientOptions & {
|
|
30555
30563
|
version: V;
|
|
@@ -30694,6 +30702,13 @@ declare class TrueNasApiClientV27 extends TrueNasApiClient<ApiDirectory> {
|
|
|
30694
30702
|
protected createOperations(): OperationMappings;
|
|
30695
30703
|
}
|
|
30696
30704
|
|
|
30705
|
+
/**
|
|
30706
|
+
* What a reachability probe found. Three states rather than two: "we asked and
|
|
30707
|
+
* got silence" and "there was no way to ask" refuse the CORS fallback for
|
|
30708
|
+
* different reasons, and collapsing them into one `false` is the conflation
|
|
30709
|
+
* this whole change exists to undo.
|
|
30710
|
+
*/
|
|
30711
|
+
type Reachability = 'reachable' | 'silent' | 'cannot-ask';
|
|
30697
30712
|
/**
|
|
30698
30713
|
* Discovers available API versions from TrueNAS systems.
|
|
30699
30714
|
*
|
|
@@ -30708,8 +30723,10 @@ declare class TrueNasApiClientV27 extends TrueNasApiClient<ApiDirectory> {
|
|
|
30708
30723
|
* service. Because `fetch` resolves (rather than rejects) on non-2xx responses and
|
|
30709
30724
|
* throws a `TypeError` on network/CORS/unreachable failures, the error contract
|
|
30710
30725
|
* differs from the original: a network/CORS/unreachable failure surfaces as a
|
|
30711
|
-
* {@link VersionDiscoveryNetworkError}
|
|
30712
|
-
*
|
|
30726
|
+
* {@link VersionDiscoveryNetworkError} (replacing the old
|
|
30727
|
+
* `HttpErrorResponse.status === 0` check). That error names a symptom with
|
|
30728
|
+
* several causes, so it opens the client factory's disambiguation rather than
|
|
30729
|
+
* deciding it — see {@link VersionDiscovery.probeReachable}.
|
|
30713
30730
|
*/
|
|
30714
30731
|
declare class VersionDiscovery {
|
|
30715
30732
|
private readonly logger;
|
|
@@ -30717,6 +30734,19 @@ declare class VersionDiscovery {
|
|
|
30717
30734
|
private versionCache;
|
|
30718
30735
|
constructor(logger?: Logger, protocol?: ApplianceProtocol);
|
|
30719
30736
|
private versionsUrl;
|
|
30737
|
+
/**
|
|
30738
|
+
* Whether the appliance answers at all, asked in a way CORS cannot block.
|
|
30739
|
+
*
|
|
30740
|
+
* A `no-cors` request yields a response the page may not read, but its
|
|
30741
|
+
* *arrival* is the whole answer here: something served it. That separates the
|
|
30742
|
+
* two failures `fetch` reports identically — a box that refused to share its
|
|
30743
|
+
* versions with this origin, and a box that is not there.
|
|
30744
|
+
*
|
|
30745
|
+
* `cannot-ask` where CORS is not enforced: nothing is blocking anything, so a
|
|
30746
|
+
* failed fetch already means unreachable and a probe would only repeat what
|
|
30747
|
+
* discovery just found. It is not a quieter way of saying "no".
|
|
30748
|
+
*/
|
|
30749
|
+
probeReachable(hostname: string): Promise<Reachability>;
|
|
30720
30750
|
/**
|
|
30721
30751
|
* Discovers the API version for a given hostname.
|
|
30722
30752
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -30429,9 +30429,10 @@ interface CreateClientOptions {
|
|
|
30429
30429
|
*
|
|
30430
30430
|
* Omitting it against a plaintext appliance is the quieter failure and the
|
|
30431
30431
|
* likelier one, since it is the case this option exists for. Discovery tries
|
|
30432
|
-
* `https
|
|
30433
|
-
* v25.10.0 has on `/api/versions
|
|
30434
|
-
*
|
|
30432
|
+
* `https` and `fetch` rejects, which reads exactly like the CORS block
|
|
30433
|
+
* v25.10.0 has on `/api/versions`. The reachability probe now runs on the same
|
|
30434
|
+
* scheme and fails the same way, so the mismatch is reported as unreachable
|
|
30435
|
+
* rather than quietly answered with a v25.10.0 client.
|
|
30435
30436
|
*/
|
|
30436
30437
|
protocol?: ApplianceProtocol;
|
|
30437
30438
|
}
|
|
@@ -30545,11 +30546,18 @@ interface CreateClientOptions {
|
|
|
30545
30546
|
* @returns a Promise that resolves with the created client, or rejects with a
|
|
30546
30547
|
* {@link VersionDiscoveryError} subclass (or a client-selection error).
|
|
30547
30548
|
* Rejects if version discovery on all hostnames *fails* and is not recoverable.
|
|
30548
|
-
*
|
|
30549
|
-
*
|
|
30550
|
-
*
|
|
30551
|
-
*
|
|
30552
|
-
*
|
|
30549
|
+
*
|
|
30550
|
+
* A `VersionDiscoveryNetworkError` is not by itself a verdict: `fetch` reports
|
|
30551
|
+
* a CORS refusal, an absent appliance, a bad name and the wrong scheme
|
|
30552
|
+
* identically. So that failure opens a reachability probe and a second
|
|
30553
|
+
* discovery attempt. A retry that succeeds is used; one that fails with a
|
|
30554
|
+
* specific error raises that instead; an appliance that answers nothing
|
|
30555
|
+
* rejects with the network error. Only one that answers the probe and still
|
|
30556
|
+
* will not serve `/api/versions` — which is what 25.10.0 looks like from a
|
|
30557
|
+
* browser — falls back to `FALLBACK_VERSION`.
|
|
30558
|
+
*
|
|
30559
|
+
* A network error alongside a version-compatibility error or a 404 does
|
|
30560
|
+
* *not* reach any of that — see `selectRepresentativeFailure`.
|
|
30553
30561
|
*/
|
|
30554
30562
|
declare function createTrueNasClient<V extends SupportedApiVersion>(opts: CreateClientOptions & {
|
|
30555
30563
|
version: V;
|
|
@@ -30694,6 +30702,13 @@ declare class TrueNasApiClientV27 extends TrueNasApiClient<ApiDirectory> {
|
|
|
30694
30702
|
protected createOperations(): OperationMappings;
|
|
30695
30703
|
}
|
|
30696
30704
|
|
|
30705
|
+
/**
|
|
30706
|
+
* What a reachability probe found. Three states rather than two: "we asked and
|
|
30707
|
+
* got silence" and "there was no way to ask" refuse the CORS fallback for
|
|
30708
|
+
* different reasons, and collapsing them into one `false` is the conflation
|
|
30709
|
+
* this whole change exists to undo.
|
|
30710
|
+
*/
|
|
30711
|
+
type Reachability = 'reachable' | 'silent' | 'cannot-ask';
|
|
30697
30712
|
/**
|
|
30698
30713
|
* Discovers available API versions from TrueNAS systems.
|
|
30699
30714
|
*
|
|
@@ -30708,8 +30723,10 @@ declare class TrueNasApiClientV27 extends TrueNasApiClient<ApiDirectory> {
|
|
|
30708
30723
|
* service. Because `fetch` resolves (rather than rejects) on non-2xx responses and
|
|
30709
30724
|
* throws a `TypeError` on network/CORS/unreachable failures, the error contract
|
|
30710
30725
|
* differs from the original: a network/CORS/unreachable failure surfaces as a
|
|
30711
|
-
* {@link VersionDiscoveryNetworkError}
|
|
30712
|
-
*
|
|
30726
|
+
* {@link VersionDiscoveryNetworkError} (replacing the old
|
|
30727
|
+
* `HttpErrorResponse.status === 0` check). That error names a symptom with
|
|
30728
|
+
* several causes, so it opens the client factory's disambiguation rather than
|
|
30729
|
+
* deciding it — see {@link VersionDiscovery.probeReachable}.
|
|
30713
30730
|
*/
|
|
30714
30731
|
declare class VersionDiscovery {
|
|
30715
30732
|
private readonly logger;
|
|
@@ -30717,6 +30734,19 @@ declare class VersionDiscovery {
|
|
|
30717
30734
|
private versionCache;
|
|
30718
30735
|
constructor(logger?: Logger, protocol?: ApplianceProtocol);
|
|
30719
30736
|
private versionsUrl;
|
|
30737
|
+
/**
|
|
30738
|
+
* Whether the appliance answers at all, asked in a way CORS cannot block.
|
|
30739
|
+
*
|
|
30740
|
+
* A `no-cors` request yields a response the page may not read, but its
|
|
30741
|
+
* *arrival* is the whole answer here: something served it. That separates the
|
|
30742
|
+
* two failures `fetch` reports identically — a box that refused to share its
|
|
30743
|
+
* versions with this origin, and a box that is not there.
|
|
30744
|
+
*
|
|
30745
|
+
* `cannot-ask` where CORS is not enforced: nothing is blocking anything, so a
|
|
30746
|
+
* failed fetch already means unreachable and a probe would only repeat what
|
|
30747
|
+
* discovery just found. It is not a quieter way of saying "no".
|
|
30748
|
+
*/
|
|
30749
|
+
probeReachable(hostname: string): Promise<Reachability>;
|
|
30720
30750
|
/**
|
|
30721
30751
|
* Discovers the API version for a given hostname.
|
|
30722
30752
|
*
|
package/dist/index.js
CHANGED
|
@@ -2387,8 +2387,11 @@ var apiVersionConfig = {
|
|
|
2387
2387
|
* - DNS lookup failure
|
|
2388
2388
|
* - Server unreachable
|
|
2389
2389
|
*
|
|
2390
|
-
*
|
|
2391
|
-
*
|
|
2390
|
+
* Which is why it is no longer reached on all of them. A discovery failure is
|
|
2391
|
+
* now put to a reachability probe and a second attempt first, and only an
|
|
2392
|
+
* appliance that answers the probe and still will not serve `/api/versions`
|
|
2393
|
+
* is assumed to be this version. One that answers nothing is reported as
|
|
2394
|
+
* unreachable rather than pinned to a version nobody confirmed.
|
|
2392
2395
|
*/
|
|
2393
2396
|
FALLBACK_VERSION: "v25.10.0"
|
|
2394
2397
|
};
|
|
@@ -3751,6 +3754,12 @@ var InvalidVersionResponseError = class extends VersionDiscoveryError {
|
|
|
3751
3754
|
}
|
|
3752
3755
|
};
|
|
3753
3756
|
var discoveryTimeoutMs = 5e3;
|
|
3757
|
+
var probeTimeoutMs = 5e3;
|
|
3758
|
+
function corsIsEnforced() {
|
|
3759
|
+
const hasDocument = typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
3760
|
+
const inWorker = typeof globalThis.WorkerGlobalScope !== "undefined";
|
|
3761
|
+
return hasDocument || inWorker;
|
|
3762
|
+
}
|
|
3754
3763
|
function hasErrorName(error, expected) {
|
|
3755
3764
|
return typeof error === "object" && error !== null && "name" in error && error.name === expected;
|
|
3756
3765
|
}
|
|
@@ -3763,6 +3772,42 @@ var VersionDiscovery = class {
|
|
|
3763
3772
|
versionsUrl(hostname) {
|
|
3764
3773
|
return `${httpScheme(this.protocol)}//${hostname}/api/versions`;
|
|
3765
3774
|
}
|
|
3775
|
+
/**
|
|
3776
|
+
* Whether the appliance answers at all, asked in a way CORS cannot block.
|
|
3777
|
+
*
|
|
3778
|
+
* A `no-cors` request yields a response the page may not read, but its
|
|
3779
|
+
* *arrival* is the whole answer here: something served it. That separates the
|
|
3780
|
+
* two failures `fetch` reports identically — a box that refused to share its
|
|
3781
|
+
* versions with this origin, and a box that is not there.
|
|
3782
|
+
*
|
|
3783
|
+
* `cannot-ask` where CORS is not enforced: nothing is blocking anything, so a
|
|
3784
|
+
* failed fetch already means unreachable and a probe would only repeat what
|
|
3785
|
+
* discovery just found. It is not a quieter way of saying "no".
|
|
3786
|
+
*/
|
|
3787
|
+
async probeReachable(hostname) {
|
|
3788
|
+
if (!corsIsEnforced()) return "cannot-ask";
|
|
3789
|
+
const url = this.versionsUrl(hostname);
|
|
3790
|
+
const controller = new AbortController();
|
|
3791
|
+
const timer2 = setTimeout(() => controller.abort(), probeTimeoutMs);
|
|
3792
|
+
try {
|
|
3793
|
+
await fetch(url, {
|
|
3794
|
+
mode: "no-cors",
|
|
3795
|
+
cache: "no-store",
|
|
3796
|
+
signal: controller.signal
|
|
3797
|
+
});
|
|
3798
|
+
this.logger.info("Reachability probe answered", { hostname, url });
|
|
3799
|
+
return "reachable";
|
|
3800
|
+
} catch (error) {
|
|
3801
|
+
this.logger.warn("Reachability probe got no answer", {
|
|
3802
|
+
hostname,
|
|
3803
|
+
url,
|
|
3804
|
+
error
|
|
3805
|
+
});
|
|
3806
|
+
return "silent";
|
|
3807
|
+
} finally {
|
|
3808
|
+
clearTimeout(timer2);
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3766
3811
|
/**
|
|
3767
3812
|
* Discovers the API version for a given hostname.
|
|
3768
3813
|
*
|
|
@@ -4022,30 +4067,81 @@ async function createTrueNasClient(opts) {
|
|
|
4022
4067
|
});
|
|
4023
4068
|
throw error;
|
|
4024
4069
|
}
|
|
4025
|
-
const
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
logger.error("Invalid fallback version configuration", {
|
|
4029
|
-
uuid: uuid.slice(0, 8),
|
|
4030
|
-
hostnames: hostnames.join(", "),
|
|
4031
|
-
fallbackVersion: fallbackVersionString
|
|
4032
|
-
});
|
|
4033
|
-
throw error;
|
|
4070
|
+
const reachable = await probeAnyHostname(hostnames, versionDiscovery);
|
|
4071
|
+
if (reachable === "silent") {
|
|
4072
|
+
await sleep(unreachableRetryDelayMs);
|
|
4034
4073
|
}
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4074
|
+
try {
|
|
4075
|
+
const retryWinner = await discoverVersionFromAnyHostname(
|
|
4076
|
+
hostnames,
|
|
4077
|
+
versionDiscovery
|
|
4078
|
+
);
|
|
4079
|
+
logger.info("Version discovery succeeded on retry", {
|
|
4038
4080
|
uuid: uuid.slice(0, 8),
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4081
|
+
hostname: retryWinner.hostname,
|
|
4082
|
+
version: retryWinner.version.version,
|
|
4083
|
+
firstError: errorMessage
|
|
4084
|
+
});
|
|
4085
|
+
version = retryWinner.version;
|
|
4086
|
+
} catch (retryError) {
|
|
4087
|
+
if (!(retryError instanceof VersionDiscoveryNetworkError)) {
|
|
4088
|
+
throw retryError;
|
|
4043
4089
|
}
|
|
4044
|
-
|
|
4045
|
-
|
|
4090
|
+
if (reachable !== "reachable") {
|
|
4091
|
+
logger.error(
|
|
4092
|
+
"Version discovery failed and the appliance did not answer a reachability probe; not assuming a version",
|
|
4093
|
+
{
|
|
4094
|
+
uuid: uuid.slice(0, 8),
|
|
4095
|
+
hostnames: hostnames.join(", "),
|
|
4096
|
+
probe: reachable === "silent" ? "no answer" : "not applicable",
|
|
4097
|
+
error: errorMessageOrDefault(retryError, "Unknown error")
|
|
4098
|
+
}
|
|
4099
|
+
);
|
|
4100
|
+
throw retryError;
|
|
4101
|
+
}
|
|
4102
|
+
const fallbackVersionString = apiVersionConfig.FALLBACK_VERSION;
|
|
4103
|
+
const fallbackVersion = parseApiVersion(fallbackVersionString);
|
|
4104
|
+
if (!fallbackVersion) {
|
|
4105
|
+
logger.error("Invalid fallback version configuration", {
|
|
4106
|
+
uuid: uuid.slice(0, 8),
|
|
4107
|
+
hostnames: hostnames.join(", "),
|
|
4108
|
+
fallbackVersion: fallbackVersionString
|
|
4109
|
+
});
|
|
4110
|
+
throw retryError;
|
|
4111
|
+
}
|
|
4112
|
+
logger.warn(
|
|
4113
|
+
"Appliance is reachable but version discovery is still blocked; falling back to assumed version",
|
|
4114
|
+
{
|
|
4115
|
+
uuid: uuid.slice(0, 8),
|
|
4116
|
+
hostnames: hostnames.join(", "),
|
|
4117
|
+
fallbackVersion: fallbackVersionString,
|
|
4118
|
+
originalError: errorMessage,
|
|
4119
|
+
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."
|
|
4120
|
+
}
|
|
4121
|
+
);
|
|
4122
|
+
version = fallbackVersion;
|
|
4123
|
+
}
|
|
4046
4124
|
}
|
|
4047
4125
|
return instantiateClientForVersion(version, opts, logger);
|
|
4048
4126
|
}
|
|
4127
|
+
var unreachableRetryDelayMs = 2500;
|
|
4128
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4129
|
+
async function probeAnyHostname(hostnames, versionDiscovery) {
|
|
4130
|
+
const probes = hostnames.map(
|
|
4131
|
+
(hostname) => versionDiscovery.probeReachable(hostname)
|
|
4132
|
+
);
|
|
4133
|
+
const remaining = new Set(probes);
|
|
4134
|
+
let sawSilence = false;
|
|
4135
|
+
while (remaining.size > 0) {
|
|
4136
|
+
const settled = await Promise.race(
|
|
4137
|
+
[...remaining].map((probe) => probe.then((result) => ({ probe, result })))
|
|
4138
|
+
);
|
|
4139
|
+
if (settled.result === "reachable") return "reachable";
|
|
4140
|
+
if (settled.result === "silent") sawSilence = true;
|
|
4141
|
+
remaining.delete(settled.probe);
|
|
4142
|
+
}
|
|
4143
|
+
return sawSilence ? "silent" : "cannot-ask";
|
|
4144
|
+
}
|
|
4049
4145
|
async function discoverVersionFromAnyHostname(hostnames, versionDiscovery) {
|
|
4050
4146
|
const attempts = hostnames.map(
|
|
4051
4147
|
(hostname) => firstValueFrom(versionDiscovery.discoverVersion(hostname)).then(
|