Framework-agnostic TypeScript client for the TrueNAS JSON-RPC 2.0 WebSocket API.
Status: early extraction in progress. The client is being pulled out of the TrueNAS Connect UI into this standalone package.
- Node ≥ 22 (provides a global
WebSocket) or a browser. On older Node, supply aWebSocketimplementation (e.g. thewspackage) via the socket config. rxjs^7.8 is a peer dependency — the consuming project provides it.
import { createTrueNasClient } from '@truenas/api-client';
const client = await createTrueNasClient({
uuid: 'system-uuid',
hostnames: ['truenas.local'],
enabled: true,
});createTrueNasClient does not take credentials, so log in before calling
anything — middleware refuses an unauthenticated call, and authenticated$
only turns true once one of these resolves:
await firstValueFrom(
client.authenticator.loginWithApiKey({ username, key })
);
// or client.authenticator.loginWithUserPass(username, password)Everything below hangs off client.api, and every method name it accepts comes
from types generated from middlewared --dump-api. A name the declared version
does not have is a compile error, and params and responses come from the same
source — there is no list of endpoint constants to import.
client.api.call('system.info'); // SystemInfoResult
client.api.call('alert.dismiss', ['uuid-1']); // params required
client.api.call('nope.nope'); // ✗ compile errorQueries. Middleware's .query methods are polymorphic in their options —
the same endpoint returns a list, one entry, or a count. Which you get is
chosen by the verb, so there is nothing to narrow:
client.api.query('user.query', [['uid', '>', 1000]]); // UserEntry[]
client.api.queryOne('user.query', [['id', '=', 1]]); // UserEntry
client.api.queryCount('user.query'); // number
client.api.query('user.query', [], { select: ['id', 'username'] });
// Pick<UserEntry, 'id' | 'username'>[]Use satisfies rather than an annotation when building options into a
variable — an annotated QueryListOptions<E> widens select, and the result
degrades to Partial<E>[].
Jobs. A separate key space from call: app.start runs as a job and does
not appear in the call directory. job starts one and follows it to
completion, typing the result from the job directory:
client.api.job('pool.dataset.export_key', ['tank/enc'])
.subscribe(job => report(job.progress.percent)); // Job<string | null>Events. Emits the change as a union discriminated on msg. Narrowing is
load-bearing: a removal carries an id and no fields in almost every
collection.
client.api.events('app.query').subscribe(event => {
if (event.msg === 'removed') return drop(event.id);
render(event.fields);
});The version is discovered at runtime; the types are fixed at compile time.
createTrueNasClient defaults to the oldest supported version, which
understates a newer server rather than promising methods it lacks. Name a
version to reach the rest:
const client = await createTrueNasClient<ApiDirectoryV26_0_0>(opts);
client.api.query('container.query'); // v26-only, reachableThat is a claim about the server, not a guarantee — the client you get is
whichever version discovery found. Operations that must work across versions
belong on client.ops, which resolves them at runtime.
The API reference is generated from the TSDoc comments in the source with TypeDoc and published to GitHub Pages with each npm release: https://truenas.github.io/api-client-ts/
yarn docs # generate locally into docs/ (gitignored)
yarn docs:check # validate doc comments without rendering (run in CI)corepack enable # once, to enable Yarn 4
yarn install
yarn build # bundle to dist/ (ESM + CJS + .d.ts) via tsup
yarn typecheck # tsc --noEmit
yarn test # vitest
yarn lint # eslintSources live under src/, grouped by role:
src/
connection/ api/ auth/ client/ # the WebSocket client, split by responsibility
types/ enums/ utils/ config/ errors/
logger.ts factory.ts version-discovery.ts index.ts
Internal modules import each other through the @/* path alias (@/* → src/*). The alias is a
build-time convenience only — it is inlined away during bundling and never reaches consumers; the public
API is solely what src/index.ts (the barrel) re-exports.