@truenas/api-client 6.0.1 → 6.0.2
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 +144 -217
- package/dist/chunk-7QVRLQIZ.js +4211 -0
- package/dist/chunk-7QVRLQIZ.js.map +1 -0
- package/dist/chunk-PQIVBXN3.cjs +4251 -0
- package/dist/chunk-PQIVBXN3.cjs.map +1 -0
- package/dist/index.cjs +130 -4343
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -31436
- package/dist/index.d.ts +29 -31436
- package/dist/index.js +1 -4324
- package/dist/index.js.map +1 -1
- package/dist/testing/index.cjs +729 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +483 -0
- package/dist/testing/index.d.ts +483 -0
- package/dist/testing/index.js +716 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/truenas-auth-mechanism.enum-DikfjVxa.d.cts +30936 -0
- package/dist/truenas-auth-mechanism.enum-DikfjVxa.d.ts +30936 -0
- package/package.json +23 -2
package/README.md
CHANGED
|
@@ -1,280 +1,207 @@
|
|
|
1
1
|
# @truenas/api-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The TypeScript client for the TrueNAS API. Every method, parameter and response
|
|
4
|
+
middleware exposes is checked at compile time, and every request travels over
|
|
5
|
+
one self-healing WebSocket.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
```typescript
|
|
8
|
+
const users = await firstValueFrom(
|
|
9
|
+
client.api.query('user.query', [['builtin', '=', false]], { select: ['id', 'username'] })
|
|
10
|
+
);
|
|
11
|
+
// users: Pick<UserEntry, 'id' | 'username'>[]
|
|
12
|
+
```
|
|
7
13
|
|
|
8
|
-
##
|
|
14
|
+
## Why this client
|
|
15
|
+
|
|
16
|
+
- **Typed from the source.** Types are generated from `middlewared --dump-api`
|
|
17
|
+
for every supported release. A misspelled method, a missing parameter or a
|
|
18
|
+
filter on a field that doesn't exist fails the build, not the user.
|
|
19
|
+
- **Version-aware.** The client asks the appliance which API it speaks and
|
|
20
|
+
builds the matching implementation. API versions v25.10.0 through v27.0.0
|
|
21
|
+
are supported side by side.
|
|
22
|
+
- **One call across releases.** `client.ops` keeps working when middleware
|
|
23
|
+
renames or reshapes an endpoint between versions.
|
|
24
|
+
- **Queries that know their shape.** List, single entry or count is chosen by
|
|
25
|
+
the method you call, and `select` narrows the result to the fields you asked
|
|
26
|
+
for.
|
|
27
|
+
- **Jobs and events as streams.** Jobs report progress until they finish.
|
|
28
|
+
Collection subscriptions deliver typed changes and come back after a
|
|
29
|
+
reconnect.
|
|
30
|
+
- **Built to stay connected.** It races every hostname you give it, retries,
|
|
31
|
+
keeps the socket alive, holds requests made while it reconnects, and logs
|
|
32
|
+
password and API-key sessions back in.
|
|
33
|
+
- **Framework-agnostic.** Plain RxJS, no framework. ESM and CommonJS, Node 22+
|
|
34
|
+
or any modern browser, and your own logger.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @truenas/api-client rxjs
|
|
40
|
+
```
|
|
9
41
|
|
|
10
|
-
|
|
11
|
-
implementation (e.g. the [`ws`](https://www.npmjs.com/package/ws) package) via the socket config.
|
|
12
|
-
- **`rxjs` ^7.8** is a peer dependency — the consuming project provides it.
|
|
42
|
+
`rxjs` ^7.8 is a peer dependency.
|
|
13
43
|
|
|
14
|
-
##
|
|
44
|
+
## Quick start
|
|
15
45
|
|
|
16
46
|
```typescript
|
|
17
47
|
import { createTrueNasClient } from '@truenas/api-client';
|
|
48
|
+
import { firstValueFrom } from 'rxjs';
|
|
18
49
|
|
|
19
50
|
const client = await createTrueNasClient({
|
|
20
|
-
uuid:
|
|
21
|
-
hostnames: ['truenas.local'],
|
|
51
|
+
uuid: systemUuid,
|
|
52
|
+
hostnames: ['truenas.local', '192.168.1.50'],
|
|
22
53
|
enabled: true,
|
|
23
54
|
});
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
`createTrueNasClient` does not take credentials, so log in before calling
|
|
27
|
-
anything — middleware refuses an unauthenticated call, and `authenticated$`
|
|
28
|
-
only turns true once one of these resolves:
|
|
29
55
|
|
|
30
|
-
```typescript
|
|
31
56
|
await firstValueFrom(
|
|
32
|
-
client.authenticator.loginWithApiKey({ username, key })
|
|
57
|
+
client.authenticator.loginWithApiKey({ username: 'admin', key: apiKey })
|
|
33
58
|
);
|
|
34
|
-
|
|
59
|
+
|
|
60
|
+
const info = await firstValueFrom(client.api.call('system.info'));
|
|
61
|
+
console.log(`${info.hostname} runs ${info.version}`);
|
|
62
|
+
|
|
63
|
+
client.close();
|
|
35
64
|
```
|
|
36
65
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
Every request is a cold `Observable`: nothing is sent until you subscribe, so
|
|
67
|
+
requests compose with the rest of RxJS. `firstValueFrom` turns one into a
|
|
68
|
+
promise.
|
|
69
|
+
|
|
70
|
+
## Logging in
|
|
71
|
+
|
|
72
|
+
| Method | Use it for |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `loginWithUserPass(username, password)` | Interactive logins. Resolves with `response_type: 'OTP_REQUIRED'` when two-factor is on. |
|
|
75
|
+
| `loginWithOtp(code)` | The second step of a two-factor login. |
|
|
76
|
+
| `loginWithApiKey({ username, key })` | Services and scripts. |
|
|
77
|
+
| `loginWithToken(token)` | Opening another session from a previous login's `reconnect_token`. |
|
|
78
|
+
|
|
79
|
+
All four live on `client.authenticator`. Password and API-key sessions log back
|
|
80
|
+
in by themselves after a reconnect. `authenticated$` tracks the session,
|
|
81
|
+
`logout()` ends it, and a rejected login errors with an `AuthError` carrying an
|
|
82
|
+
`AuthErrorCode`.
|
|
83
|
+
|
|
84
|
+
## Calls
|
|
41
85
|
|
|
42
86
|
```typescript
|
|
43
|
-
client.api.call('system.info');
|
|
44
|
-
client.api.call('alert.dismiss', ['uuid-1']);
|
|
45
|
-
client.api.call('nope.nope');
|
|
87
|
+
client.api.call('system.info'); // Observable<SystemInfoResult>
|
|
88
|
+
client.api.call('alert.dismiss', ['uuid-1']); // params checked against the method
|
|
89
|
+
client.api.call('nope.nope'); // ✗ compile error
|
|
46
90
|
```
|
|
47
91
|
|
|
48
|
-
|
|
49
|
-
the same endpoint returns a list, one entry, or a count. Which you get is
|
|
50
|
-
chosen by the verb, so there is nothing to narrow:
|
|
92
|
+
A failed call errors with middleware's own message.
|
|
51
93
|
|
|
52
|
-
|
|
53
|
-
client.api.query('user.query', [['uid', '>', 1000]]); // UserEntry[]
|
|
54
|
-
client.api.queryOne('user.query', [['id', '=', 1]]); // UserEntry
|
|
55
|
-
client.api.queryCount('user.query'); // number
|
|
94
|
+
## Queries
|
|
56
95
|
|
|
57
|
-
|
|
58
|
-
|
|
96
|
+
```typescript
|
|
97
|
+
client.api.query('user.query', [['uid', '>', 1000]]); // UserEntry[]
|
|
98
|
+
client.api.query('user.query', [], { select: ['id', 'username'] }); // Pick<UserEntry, 'id' | 'username'>[]
|
|
99
|
+
client.api.queryOne('user.query', [['username', '=', 'root']]); // UserEntry
|
|
100
|
+
client.api.queryCount('user.query'); // number
|
|
101
|
+
client.api.query('user.query', [['uidd', '>', 1000]]); // ✗ no such field
|
|
59
102
|
```
|
|
60
103
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
degrades to `Partial<E>[]`.
|
|
104
|
+
When you build options in a variable, write `satisfies QueryListOptions<…>`
|
|
105
|
+
rather than a type annotation, so the result keeps its precise type.
|
|
64
106
|
|
|
65
|
-
|
|
66
|
-
not appear in the call directory. `job` starts one and follows it to
|
|
67
|
-
completion, typing the result from the job directory:
|
|
107
|
+
## Jobs
|
|
68
108
|
|
|
69
109
|
```typescript
|
|
70
|
-
client.api.job('pool.dataset.export_key', ['tank/
|
|
71
|
-
.
|
|
110
|
+
client.api.job('pool.dataset.export_key', ['tank/encrypted']).subscribe(job => {
|
|
111
|
+
progress.set(job.progress.percent ?? 0);
|
|
112
|
+
|
|
113
|
+
if (job.state === JobState.Success) save(job.result);
|
|
114
|
+
if (job.state === JobState.Failed) report(job.error);
|
|
115
|
+
});
|
|
72
116
|
```
|
|
73
117
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
118
|
+
The stream emits each state and completes when the job finishes. A failed job
|
|
119
|
+
completes too, with `state` and `error` set.
|
|
120
|
+
|
|
121
|
+
## Events
|
|
77
122
|
|
|
78
123
|
```typescript
|
|
79
|
-
client.api.events('app.query').subscribe(
|
|
80
|
-
if (
|
|
81
|
-
|
|
124
|
+
client.api.events('app.query').subscribe(change => {
|
|
125
|
+
if (change.msg === 'removed') return removeRow(change.id);
|
|
126
|
+
upsertRow(change.fields);
|
|
82
127
|
});
|
|
83
128
|
```
|
|
84
129
|
|
|
85
|
-
|
|
130
|
+
Changes are typed per collection and discriminated on `msg`. One server
|
|
131
|
+
subscription is shared by every subscriber and re-registered after each
|
|
132
|
+
reconnect.
|
|
86
133
|
|
|
87
|
-
|
|
88
|
-
which is what an appliance serves. An appliance reached without TLS needs
|
|
89
|
-
`protocol`:
|
|
134
|
+
## Working across versions
|
|
90
135
|
|
|
91
|
-
|
|
92
|
-
|
|
136
|
+
Without a hint, the client is typed as the oldest supported version, since that
|
|
137
|
+
is a safe floor against any appliance. Reach newer methods one of two ways:
|
|
93
138
|
|
|
94
|
-
|
|
95
|
-
|
|
139
|
+
```typescript
|
|
140
|
+
// Discover the version, and write against v26:
|
|
141
|
+
const client = await createTrueNasClient<ApiDirectoryV26_0_0>(options);
|
|
142
|
+
client.api.query('container.query');
|
|
96
143
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
144
|
+
// Or skip discovery, and let the types follow the version:
|
|
145
|
+
const pinned = await createTrueNasClient({ ...options, version: 'v27.0.0' });
|
|
146
|
+
pinned.api.query('container.query');
|
|
100
147
|
```
|
|
101
148
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
128
|
-
### Naming a version
|
|
129
|
-
|
|
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:
|
|
149
|
+
A type argument is a claim about the appliance. Discovery still decides which
|
|
150
|
+
client you get. A named `version` skips discovery and also selects the
|
|
151
|
+
WebSocket endpoint, so name the version the appliance actually runs. The types
|
|
152
|
+
follow it when the version is a literal.
|
|
153
|
+
|
|
154
|
+
For code that must run against any supported release, use `client.ops`. It
|
|
155
|
+
offers the same calls on every version and maps them onto whatever that version
|
|
156
|
+
provides:
|
|
137
157
|
|
|
138
158
|
```typescript
|
|
139
|
-
|
|
140
|
-
client.
|
|
159
|
+
client.ops.containerQuery(); // Observable<Container[]>
|
|
160
|
+
client.ops.containerStop(id, { force: true }); // Observable<Job | null>
|
|
161
|
+
client.ops.smbStatus({ infoLevel: 'SESSIONS' });
|
|
141
162
|
```
|
|
142
163
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
version discovery found, so a wrong claim fails at runtime.
|
|
164
|
+
Per-version details, such as permissions that differ between releases, are in
|
|
165
|
+
the `OperationMappings` reference.
|
|
146
166
|
|
|
147
|
-
|
|
148
|
-
a harness against a pinned image:
|
|
167
|
+
## Connection
|
|
149
168
|
|
|
150
169
|
```typescript
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
client.api.query('container.query'); // typed v27, derived from the string
|
|
170
|
+
client.connection.opened.subscribe(open => indicator.set(open));
|
|
171
|
+
client.connection.setEnabled(false); // disconnect; true connects again
|
|
172
|
+
client.close(); // disconnect for good
|
|
155
173
|
```
|
|
156
174
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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.
|
|
185
|
-
|
|
186
|
-
`ops` is deliberately flat — `OperationMappings`, not parameterized by the
|
|
187
|
-
directory — so every operation is callable whether or not you pinned a version.
|
|
188
|
-
That is what lets an operation paper over a difference the type system would
|
|
189
|
-
otherwise force the caller to handle.
|
|
190
|
-
|
|
191
|
-
```ts
|
|
192
|
-
// The same call on every supported version.
|
|
193
|
-
const sessions = await firstValueFrom(
|
|
194
|
-
client.ops.smbStatus({ infoLevel: 'SESSIONS' })
|
|
195
|
-
);
|
|
175
|
+
An appliance served without TLS needs `protocol: 'http:'`, which switches
|
|
176
|
+
discovery to `http` and the socket to `ws`. It describes the appliance, not the
|
|
177
|
+
page, and defaults to `https:`.
|
|
196
178
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
client.ops.smbStatus({
|
|
200
|
-
infoLevel: 'SESSIONS',
|
|
201
|
-
options: { count: true },
|
|
202
|
-
statusOptions: { fast: true },
|
|
203
|
-
})
|
|
204
|
-
);
|
|
205
|
-
```
|
|
179
|
+
Pass `logger: consoleLogger`, or any object implementing `Logger`, to see what
|
|
180
|
+
the client is doing.
|
|
206
181
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
one method name, one argument tuple and one return type, checked against
|
|
215
|
-
middleware source instead of against the dump.
|
|
216
|
-
|
|
217
|
-
Three consequences worth knowing before you reach for it.
|
|
218
|
-
|
|
219
|
-
**It needs a full-admin session on v25.10.** Being private there decides
|
|
220
|
-
authorization, not just documentation. `smb.status` declares no roles on
|
|
221
|
-
v25.10, and middleware role-registers a method only `if roles:` — so it is in no
|
|
222
|
-
role's allowlist, and only a **non-STIG full-admin** session, whose allowlist is
|
|
223
|
-
the wildcard `{ method: '*', resource: '*' }`, reaches it. A session holding
|
|
224
|
-
exactly `SHARING_SMB_READ` is refused with `EACCES` on v25.10 and succeeds on
|
|
225
|
-
v26+, where the method carries that role. Under STIG, full admin is expanded to
|
|
226
|
-
the union of its roles' allowlists rather than the wildcard, so it is refused on
|
|
227
|
-
v25.10 as well. The client does not pre-empt any of this: you get middleware's
|
|
228
|
-
own error, on the version where it applies.
|
|
229
|
-
|
|
230
|
-
**It logs server-side on every dispatch**, which matters if you poll it. v25.10
|
|
231
|
-
is in maintenance and that cost is accepted there, but it is not a pattern to
|
|
232
|
-
extend.
|
|
233
|
-
|
|
234
|
-
**The result is the union middleware sends** — which arm arrives is decided by
|
|
235
|
-
the options, not the info level — so callers narrow it themselves.
|
|
236
|
-
|
|
237
|
-
These types are deliberately not the generated ones. `SmbStatusOptions` is not
|
|
238
|
-
v26's `SMBStatusOptions`, and `SmbStatusResponse` is not that method's generated
|
|
239
|
-
response: the generated types describe one version's dump, while these describe
|
|
240
|
-
the contract both versions honour. They are named `Smb…` rather than `SMB…` so
|
|
241
|
-
the two cannot be confused at a call site — and `SmbStatusOptions` is the one
|
|
242
|
-
pair where the names sit closest, since it is the fourth positional argument on
|
|
243
|
-
both versions.
|
|
182
|
+
## Errors
|
|
183
|
+
|
|
184
|
+
- **Calls** error with middleware's message.
|
|
185
|
+
- **Logins** error with `AuthError`. Check `code` against `AuthErrorCode`.
|
|
186
|
+
- **`createTrueNasClient`** rejects with a `VersionDiscoveryError` subclass, such
|
|
187
|
+
as `VersionTooOldError`, `VersionTooNewError` or
|
|
188
|
+
`VersionDiscoveryNetworkError`.
|
|
244
189
|
|
|
245
190
|
## Documentation
|
|
246
191
|
|
|
247
|
-
The API reference is
|
|
248
|
-
|
|
249
|
-
<https://truenas.github.io/api-client-ts/>
|
|
250
|
-
|
|
251
|
-
```bash
|
|
252
|
-
yarn docs # generate locally into docs/ (gitignored)
|
|
253
|
-
yarn docs:check # validate doc comments without rendering (run in CI)
|
|
254
|
-
```
|
|
192
|
+
The full API reference is at <https://truenas.github.io/api-client-ts/>,
|
|
193
|
+
generated from the source with each release.
|
|
255
194
|
|
|
256
195
|
## Development
|
|
257
196
|
|
|
258
197
|
```bash
|
|
259
|
-
corepack enable # once,
|
|
198
|
+
corepack enable # once, for Yarn 4
|
|
260
199
|
yarn install
|
|
261
|
-
yarn build #
|
|
262
|
-
yarn typecheck #
|
|
263
|
-
yarn test
|
|
264
|
-
yarn lint
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
## Layout
|
|
268
|
-
|
|
269
|
-
Sources live under `src/`, grouped by role:
|
|
270
|
-
|
|
271
|
-
```
|
|
272
|
-
src/
|
|
273
|
-
connection/ api/ auth/ client/ # the WebSocket client, split by responsibility
|
|
274
|
-
types/ enums/ utils/ config/ errors/
|
|
275
|
-
logger.ts factory.ts version-discovery.ts index.ts
|
|
200
|
+
yarn build # ESM + CJS + .d.ts into dist/
|
|
201
|
+
yarn typecheck # sources, specs and scripts
|
|
202
|
+
yarn test
|
|
203
|
+
yarn lint
|
|
204
|
+
yarn generate:api # regenerate src/generated from middleware dumps
|
|
276
205
|
```
|
|
277
206
|
|
|
278
|
-
|
|
279
|
-
build-time convenience only — it is inlined away during bundling and never reaches consumers; the public
|
|
280
|
-
API is solely what `src/index.ts` (the barrel) re-exports.
|
|
207
|
+
Releases are automated from commit titles. See [RELEASING.md](RELEASING.md).
|