@truenas/api-client 3.0.2 → 3.0.4
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/README.md +92 -7
- package/dist/index.cjs +57 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +138 -8
- package/dist/index.d.ts +138 -8
- package/dist/index.js +57 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,21 +82,106 @@ client.api.events('app.query').subscribe(event => {
|
|
|
82
82
|
});
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### Reaching an appliance over http
|
|
86
|
+
|
|
87
|
+
By default the client discovers over `https://` and connects over `wss://`,
|
|
88
|
+
which is what an appliance serves. An appliance reached without TLS needs
|
|
89
|
+
`protocol`:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import type { ApplianceProtocol } from '@truenas/api-client';
|
|
93
|
+
|
|
94
|
+
const protocol: ApplianceProtocol =
|
|
95
|
+
location.protocol === 'http:' ? 'http:' : 'https:';
|
|
96
|
+
|
|
97
|
+
const client = await createTrueNasClient({
|
|
98
|
+
uuid, hostnames: [location.host], enabled: true, protocol,
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
It selects both halves of the transport — `https:` gives `https` discovery and a
|
|
103
|
+
`wss` socket, `http:` gives `http` and `ws` — and defaults to `https:`, so
|
|
104
|
+
existing callers are unaffected.
|
|
105
|
+
|
|
106
|
+
`protocol` describes the **appliance**, not the page. Reading it from
|
|
107
|
+
`location.protocol` is right when the appliance serves the page, which is the
|
|
108
|
+
same-origin case this exists for. A page served from somewhere else — a dev
|
|
109
|
+
server on `http://localhost:5173` talking to an https appliance — must pass what
|
|
110
|
+
the *appliance* uses. Getting it wrong breaks both halves but reports only one:
|
|
111
|
+
discovery's `fetch` follows the redirect and looks fine, while the socket opens
|
|
112
|
+
`ws://`, meets the same redirect, and fails the handshake without naming the
|
|
113
|
+
scheme.
|
|
114
|
+
|
|
115
|
+
Omitting it against a plaintext appliance fails the other way, and more quietly.
|
|
116
|
+
Discovery tries `https://`, `fetch` rejects, and the factory cannot tell that
|
|
117
|
+
apart from the CORS block that v25.10.0 has on `/api/versions` — so it takes the
|
|
118
|
+
fallback and hands back a client pinned to `v25.10.0` on `/api/v25.10.0`, with
|
|
119
|
+
only a `logger.warn` to say so. Against a v26 or v27 box that is a wrong-version
|
|
120
|
+
client that looks configured. If the appliance is plaintext, say so.
|
|
121
|
+
|
|
122
|
+
Narrow rather than cast: `location.protocol` is a `string`, and it is genuinely
|
|
123
|
+
`file:` for a locally-opened page or `chrome-extension:` in an extension. Both
|
|
124
|
+
halves fall back to the encrypted scheme for anything off-contract, so a bad
|
|
125
|
+
value cannot downgrade the transport — but the compiler will not stop you
|
|
126
|
+
asserting one into this option, and it will not be the value you meant.
|
|
127
|
+
|
|
85
128
|
### Naming a version
|
|
86
129
|
|
|
87
|
-
|
|
88
|
-
`createTrueNasClient`
|
|
89
|
-
understates a newer server rather than promising methods it lacks.
|
|
90
|
-
|
|
130
|
+
By default the version is discovered at runtime while the types are fixed at
|
|
131
|
+
compile time, and `createTrueNasClient` assumes the oldest supported version —
|
|
132
|
+
which understates a newer server rather than promising methods it lacks. There
|
|
133
|
+
are two ways to reach the rest, and they differ in more than syntax.
|
|
134
|
+
|
|
135
|
+
**Assert the surface** when you do not know the version but intend to write
|
|
136
|
+
against a particular one:
|
|
91
137
|
|
|
92
138
|
```typescript
|
|
93
139
|
const client = await createTrueNasClient<ApiDirectoryV26_0_0>(opts);
|
|
94
140
|
client.api.query('container.query'); // v26-only, reachable
|
|
95
141
|
```
|
|
96
142
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
143
|
+
Discovery still runs and still decides which client is built. The type argument
|
|
144
|
+
is a claim about the server, not a guarantee — the client you get is whichever
|
|
145
|
+
version discovery found, so a wrong claim fails at runtime.
|
|
146
|
+
|
|
147
|
+
**State the version** when you already know it — a UI served by the appliance,
|
|
148
|
+
a harness against a pinned image:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const client = await createTrueNasClient({
|
|
152
|
+
uuid, hostnames, enabled: true, version: 'v27.0.0',
|
|
153
|
+
});
|
|
154
|
+
client.api.query('container.query'); // typed v27, derived from the string
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
This skips discovery entirely: no `GET /api/versions`, no CORS fallback. The
|
|
158
|
+
surface is *derived* rather than asserted, so there is no type argument to get
|
|
159
|
+
wrong, and a version the package ships no types for does not compile.
|
|
160
|
+
|
|
161
|
+
It is the stronger claim of the two, because the version also selects the
|
|
162
|
+
websocket path. Naming `v27.0.0` at a v26 appliance connects on `/api/v27.0.0`
|
|
163
|
+
with v27 types over a v26 server, and discovery cannot correct it — declining
|
|
164
|
+
discovery is the point.
|
|
165
|
+
|
|
166
|
+
The derivation needs the version to be literal at the call site. Passing a type
|
|
167
|
+
argument as well, forwarding `version` through a wrapper, or annotating the
|
|
168
|
+
options object as `CreateClientOptions` all compile, all connect to the version
|
|
169
|
+
you named, and all type as the default surface instead. That
|
|
170
|
+
errs safely — understated types fail at the method call, not at runtime — but
|
|
171
|
+
silently, so keep the literal where the call is.
|
|
172
|
+
|
|
173
|
+
Compatibility is still checked, and two kinds of refusal reach a caller. A string that
|
|
174
|
+
is not a supported version — reachable only from JavaScript — throws a plain
|
|
175
|
+
`Error` naming the ones that are. A supported version this build has no client
|
|
176
|
+
for throws `VersionTooNewError`, the same type discovery raises; that happens
|
|
177
|
+
when types have been generated for a release before its client was written.
|
|
178
|
+
There is no `VersionTooOldError` here, because the oldest version you can name
|
|
179
|
+
is the oldest one supported.
|
|
180
|
+
|
|
181
|
+
Operations that must work across versions belong on `client.ops`. On the
|
|
182
|
+
discovery route that resolves against whatever the appliance turned out to be.
|
|
183
|
+
On the named route it cannot: the client class is picked from the version you
|
|
184
|
+
stated, so `ops` is that version's mappings whether or not the server agrees.
|
|
100
185
|
|
|
101
186
|
## Documentation
|
|
102
187
|
|
package/dist/index.cjs
CHANGED
|
@@ -694,11 +694,19 @@ var TrueNasSocket = class {
|
|
|
694
694
|
}
|
|
695
695
|
};
|
|
696
696
|
|
|
697
|
+
// src/types/transport.type.ts
|
|
698
|
+
function httpScheme(protocol) {
|
|
699
|
+
return protocol === "http:" ? "http:" : "https:";
|
|
700
|
+
}
|
|
701
|
+
function socketScheme(protocol) {
|
|
702
|
+
return protocol === "http:" ? "ws:" : "wss:";
|
|
703
|
+
}
|
|
704
|
+
|
|
697
705
|
// src/connection/truenas-connection.ts
|
|
698
706
|
var tenSeconds = 10 * 1e3;
|
|
699
707
|
var twentySeconds = 20 * 1e3;
|
|
700
708
|
var TrueNasConnection = class {
|
|
701
|
-
constructor(initialEnabled, hostnames, systemUuid, websocketPath, systemName, retryDelay = tenSeconds, maxRetry = 3, logger = noopLogger) {
|
|
709
|
+
constructor(initialEnabled, hostnames, systemUuid, websocketPath, systemName, retryDelay = tenSeconds, maxRetry = 3, logger = noopLogger, protocol = "https:") {
|
|
702
710
|
this.hostnames = hostnames;
|
|
703
711
|
this.systemUuid = systemUuid;
|
|
704
712
|
this.websocketPath = websocketPath;
|
|
@@ -706,6 +714,7 @@ var TrueNasConnection = class {
|
|
|
706
714
|
this.retryDelay = retryDelay;
|
|
707
715
|
this.maxRetry = maxRetry;
|
|
708
716
|
this.logger = logger;
|
|
717
|
+
this.protocol = protocol;
|
|
709
718
|
// compatibility properties
|
|
710
719
|
this.opened = new rxjs.BehaviorSubject(false);
|
|
711
720
|
this.closed = new rxjs.Subject();
|
|
@@ -902,7 +911,7 @@ var TrueNasConnection = class {
|
|
|
902
911
|
* error if the connection is never established and will not complete until unsubscribed from or closed.
|
|
903
912
|
*/
|
|
904
913
|
createSocket(hostname) {
|
|
905
|
-
const url =
|
|
914
|
+
const url = `${socketScheme(this.protocol)}//${hostname}${this.websocketPath}`;
|
|
906
915
|
let hasOpened = false;
|
|
907
916
|
return new rxjs.Observable((subscriber) => {
|
|
908
917
|
const ws = new TrueNasSocket({
|
|
@@ -3001,13 +3010,14 @@ function getWebSocketPath(version) {
|
|
|
3001
3010
|
|
|
3002
3011
|
// src/client/truenas-api-client.ts
|
|
3003
3012
|
var TrueNasApiClient = class {
|
|
3004
|
-
constructor(uuid, hostnames, version, enabled, systemName, logger = noopLogger) {
|
|
3013
|
+
constructor(uuid, hostnames, version, enabled, systemName, logger = noopLogger, protocol = "https:") {
|
|
3005
3014
|
this.uuid = uuid;
|
|
3006
3015
|
this.hostnames = hostnames;
|
|
3007
3016
|
this.version = version;
|
|
3008
3017
|
this.enabled = enabled;
|
|
3009
3018
|
this.systemName = systemName;
|
|
3010
3019
|
this.logger = logger;
|
|
3020
|
+
this.protocol = protocol;
|
|
3011
3021
|
this.connection = this.createConnection();
|
|
3012
3022
|
this.authenticator = this.createAuthenticator();
|
|
3013
3023
|
this.api = this.createApi();
|
|
@@ -3050,7 +3060,8 @@ var TrueNasApiClient = class {
|
|
|
3050
3060
|
// retryDelay (use default)
|
|
3051
3061
|
void 0,
|
|
3052
3062
|
// maxRetry (use default)
|
|
3053
|
-
this.logger
|
|
3063
|
+
this.logger,
|
|
3064
|
+
this.protocol
|
|
3054
3065
|
);
|
|
3055
3066
|
}
|
|
3056
3067
|
/**
|
|
@@ -3444,14 +3455,18 @@ function hasErrorName(error, expected) {
|
|
|
3444
3455
|
return typeof error === "object" && error !== null && "name" in error && error.name === expected;
|
|
3445
3456
|
}
|
|
3446
3457
|
var VersionDiscovery = class {
|
|
3447
|
-
constructor(logger = noopLogger) {
|
|
3458
|
+
constructor(logger = noopLogger, protocol = "https:") {
|
|
3448
3459
|
this.logger = logger;
|
|
3460
|
+
this.protocol = protocol;
|
|
3449
3461
|
this.versionCache = /* @__PURE__ */ new Map();
|
|
3450
3462
|
}
|
|
3463
|
+
versionsUrl(hostname) {
|
|
3464
|
+
return `${httpScheme(this.protocol)}//${hostname}/api/versions`;
|
|
3465
|
+
}
|
|
3451
3466
|
/**
|
|
3452
3467
|
* Discovers the API version for a given hostname.
|
|
3453
3468
|
*
|
|
3454
|
-
* Makes a GET request to `
|
|
3469
|
+
* Makes a GET request to `{protocol}//{hostname}/api/versions` and returns the latest
|
|
3455
3470
|
* compatible version. Results are cached per hostname; the cache entry is removed
|
|
3456
3471
|
* on failure so the next call retries.
|
|
3457
3472
|
*
|
|
@@ -3465,8 +3480,10 @@ var VersionDiscovery = class {
|
|
|
3465
3480
|
this.logger.info("Version discovery cache hit", { hostname });
|
|
3466
3481
|
return cached;
|
|
3467
3482
|
}
|
|
3468
|
-
|
|
3469
|
-
|
|
3483
|
+
this.logger.info("Starting version discovery", {
|
|
3484
|
+
hostname,
|
|
3485
|
+
url: this.versionsUrl(hostname)
|
|
3486
|
+
});
|
|
3470
3487
|
const discovery$ = rxjs.defer(() => rxjs.from(this.fetchVersions(hostname))).pipe(
|
|
3471
3488
|
operators.map((versionStrings) => this.selectVersion(hostname, versionStrings)),
|
|
3472
3489
|
operators.catchError((error) => {
|
|
@@ -3504,7 +3521,7 @@ var VersionDiscovery = class {
|
|
|
3504
3521
|
* misfile as a network error. Validating here keeps it an `InvalidVersionResponseError`.
|
|
3505
3522
|
*/
|
|
3506
3523
|
async fetchVersions(hostname) {
|
|
3507
|
-
const url =
|
|
3524
|
+
const url = this.versionsUrl(hostname);
|
|
3508
3525
|
const controller = new AbortController();
|
|
3509
3526
|
const timer2 = setTimeout(() => controller.abort(), discoveryTimeoutMs);
|
|
3510
3527
|
try {
|
|
@@ -3644,12 +3661,40 @@ async function createTrueNasClient(opts) {
|
|
|
3644
3661
|
`Cannot create client for system ${uuid}: hostnames array is empty`
|
|
3645
3662
|
);
|
|
3646
3663
|
}
|
|
3647
|
-
const versionDiscovery = new VersionDiscovery(logger);
|
|
3648
3664
|
logger.info("Creating versioned API client", {
|
|
3649
3665
|
uuid: uuid.slice(0, 8),
|
|
3650
3666
|
hostnames: hostnames.join(", "),
|
|
3651
3667
|
systemName
|
|
3652
3668
|
});
|
|
3669
|
+
if (opts.version !== void 0) {
|
|
3670
|
+
if (!SUPPORTED_API_VERSIONS.includes(opts.version)) {
|
|
3671
|
+
throw new Error(
|
|
3672
|
+
`Cannot create client for system ${uuid}: '${opts.version}' is not a version this package ships types for. Supported: ${SUPPORTED_API_VERSIONS.join(", ")}.`
|
|
3673
|
+
);
|
|
3674
|
+
}
|
|
3675
|
+
const known = parseApiVersion(opts.version);
|
|
3676
|
+
if (!known) {
|
|
3677
|
+
throw new Error(
|
|
3678
|
+
`Cannot create client for system ${uuid}: supported version '${opts.version}' failed to parse.`
|
|
3679
|
+
);
|
|
3680
|
+
}
|
|
3681
|
+
const compatibility = checkVersionCompatibility(known);
|
|
3682
|
+
if (compatibility === "too-new" /* TooNew */) {
|
|
3683
|
+
throw new VersionTooNewError(hostnames[0], [known.version]);
|
|
3684
|
+
}
|
|
3685
|
+
if (compatibility !== "compatible" /* Compatible */) {
|
|
3686
|
+
throw new Error(
|
|
3687
|
+
`Cannot create client for system ${uuid}: the supported version range is not usable (${apiVersionConfig.MIN_SUPPORTED_VERSION}..${apiVersionConfig.MAX_SUPPORTED_VERSION}).`
|
|
3688
|
+
);
|
|
3689
|
+
}
|
|
3690
|
+
logger.info("API version supplied by the caller, skipping discovery", {
|
|
3691
|
+
uuid: uuid.slice(0, 8),
|
|
3692
|
+
version: known.version,
|
|
3693
|
+
websocketPath: known.websocketPath
|
|
3694
|
+
});
|
|
3695
|
+
return instantiateClientForVersion(known, opts, logger);
|
|
3696
|
+
}
|
|
3697
|
+
const versionDiscovery = new VersionDiscovery(logger, opts.protocol);
|
|
3653
3698
|
let version;
|
|
3654
3699
|
try {
|
|
3655
3700
|
const winner = await discoverVersionFromAnyHostname(
|
|
@@ -3758,7 +3803,8 @@ function instantiateClientForVersion(version, opts, logger) {
|
|
|
3758
3803
|
version,
|
|
3759
3804
|
enabled,
|
|
3760
3805
|
systemName,
|
|
3761
|
-
logger
|
|
3806
|
+
logger,
|
|
3807
|
+
opts.protocol
|
|
3762
3808
|
);
|
|
3763
3809
|
}
|
|
3764
3810
|
function errorMessageOrDefault(error, fallback) {
|