NestJS module for the Telemt Control API.
Wraps @aezakmiproject/telemt-sdk in an injectable provider: one configured TelemtAPI per registration, resolved through Nest DI, with sync and async registration and an opt-in helper that turns the SDK's no-throw envelope into a thrown exception.
Requires Node.js 22.12+. @nestjs/common v12 is ESM-only while this package is published as CommonJS, so it relies on require(esm), which is unflagged only from that version.
This project is an independent open-source client. It is not affiliated with Telegram or the Telemt authors.
This package declares its runtime dependencies as peers, so install the SDK alongside it:
npm install @aezakmiproject/telemt-sdk-nest @aezakmiproject/telemt-sdk
# or
pnpm add @aezakmiproject/telemt-sdk-nest @aezakmiproject/telemt-sdkThe full peer set is:
| Peer | Range | Notes |
|---|---|---|
@aezakmiproject/telemt-sdk |
^1.2.0 |
The client this module wraps — install it explicitly |
@nestjs/common |
^12.0.0 |
Already present in any Nest application |
reflect-metadata |
^0.2.0 |
Already present in any Nest application |
Keeping the SDK a peer means your application owns its version, and the module does not pin a second copy of the client into your tree.
Register the module once, then inject TelemtService anywhere.
import { Module } from '@nestjs/common';
import { TelemtModule } from '@aezakmiproject/telemt-sdk-nest';
@Module({
imports: [
TelemtModule.forRoot({
apiUrl: 'http://127.0.0.1:9091',
auth: 'telemt-sdk-dev-token', // exact value of [server.api].auth_header
}),
],
})
export class AppModule {}import { Injectable } from '@nestjs/common';
import { TelemtService } from '@aezakmiproject/telemt-sdk-nest';
@Injectable()
export class UsersReport {
constructor(private readonly telemt: TelemtService) {}
async listUsernames(): Promise<string[]> {
const users = await this.telemt.unwrap(this.telemt.users.getAll());
return users.map((user) => user.username);
}
}auth is sent verbatim as the Authorization header. Telemt does a constant-time string comparison rather than Bearer/OAuth parsing — do not prefix Bearer unless that prefix is literally part of auth_header.
Use forRootAsync when the options come from config, a secret store, or anything else resolved at boot. Exactly one of useFactory, useClass, or useExisting is required; passing none throws at module-construction time.
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TelemtModule } from '@aezakmiproject/telemt-sdk-nest';
TelemtModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
apiUrl: config.getOrThrow<string>('TELEMT_API_URL'),
auth: config.getOrThrow<string>('TELEMT_AUTH'),
}),
});The factory may be async; the client is not constructed until it resolves.
import { Injectable } from '@nestjs/common';
import {
TelemtModule,
type TelemtModuleOptions,
type TelemtOptionsFactory,
} from '@aezakmiproject/telemt-sdk-nest';
@Injectable()
export class TelemtConfig implements TelemtOptionsFactory {
async createTelemtOptions(): Promise<TelemtModuleOptions> {
return { apiUrl: process.env.TELEMT_API_URL!, auth: await readSecret() };
}
}
TelemtModule.forRootAsync({ useClass: TelemtConfig });useClass is instantiated by this module, so the class does not need to be provided anywhere else.
Reuse a provider the host application already owns, rather than getting a second instance:
TelemtModule.forRootAsync({
imports: [TelemtConfigModule], // must export TelemtConfig
useExisting: TelemtConfig,
});Thin, stateless facade over the underlying TelemtAPI. Each getter forwards to the client on every access.
| Member | What it covers |
|---|---|
users |
CRUD, enable/disable, rotate secret, reset quota |
config |
Read / merge-patch config.toml |
system |
Build info, reload, waitForReload |
health |
Liveness and readiness |
stats |
Counters, upstreams, DCs, ME writers |
runtime |
Gates, ME pool/quality, events, TLS fingerprints |
security |
API posture and IP whitelist |
limits |
Effective timeouts / pool / per-user limits |
web |
WEB-proxy session control — requires Telemt 3.5.1+ |
client |
The raw TelemtAPI, for anything not surfaced above |
unwrap(res) |
Returns data, or throws TelemtApiException |
web is version-gated: on a Telemt older than 3.5.1 its calls come back as a failed
ISdkResponse rather than throwing, like every other SDK call.
The full method list and request/response types live in the SDK's API reference.
SDK methods never throw. Every call returns a flat envelope, and transport failures arrive there too, under an sdk_* code:
interface ISdkResponse<T> {
isOk: boolean;
data?: T;
code?: TelemtErrorCode;
message?: string;
revision?: string; // success only — SHA-256 of config.toml
requestId?: number; // Telemt-side errors only
}Checking isOk by hand keeps that behaviour:
const res = await this.telemt.users.getAll();
if (!res.isOk) {
throw new Error(`${res.code}: ${res.message}`);
}
// `data` is still `UserInfo[] | undefined` here — see the narrowing note below
return (res.data ?? []).map((user) => user.username);ISdkResponse is not a discriminated union, so testing isOk does not narrow data; under strict you still have to handle the undefined. unwrap exists to collapse that:
const users = await this.telemt.unwrap(this.telemt.users.getAll());
// users is UserInfo[] — the failure branch has already thrown, and it narrowsIt accepts either a response or a promise of one, and on isOk: false throws TelemtApiException:
class TelemtApiException extends Error {
readonly code?: string; // server or `sdk_*` code
readonly requestId?: number; // Telemt-side errors only, absent on transport failures
}Note that unwrap throws whenever isOk is false, even if the response also carries partial data.
A 202 from a user mutation is still isOk: true — the write is on disk, but check UserInfo.in_runtime (or call system.reload()) before treating the user as live.
Each registration builds its own client, so registering the module in two places gives two independent TelemtAPI instances. The module is not global: every consuming module must import it, and only TelemtService is exported — TELEMT_CLIENT and TELEMT_MODULE_OPTIONS stay internal to the module.
pnpm install
pnpm test # vitest, unit tests
pnpm test:cov # with V8 coverage
pnpm typecheck # tsc, sources + specs
pnpm build # tsc -> dist/Tests run on Vitest rather than Jest because this project pins typescript@7 (the native compiler), which no longer ships the JS compiler API ts-jest needs. See docs/decisions.md.