Node.js client for Deluge's binary RPC socket API. Works against Deluge 1.x (protocol v0) and 2.x (protocol v1), auto-detects which one the daemon speaks.
bun add deluge-rpc-socket
# or
npm install deluge-rpc-socketESM-only. Requires Bun ≥ 1.3 or Node ≥ 22.
import DelugeRPC, { isRPCError } from 'deluge-rpc-socket';
import { connect } from 'tls';
const socket = connect(58846, {
// Deluge daemons usually use a self-signed certificate
rejectUnauthorized: false,
});
const rpc = DelugeRPC(socket);
// Asynchronous events from the daemon (torrent state changes, etc.)
rpc.events.on('delugeEvent', console.log);
// Non-fatal decoding errors — something's wrong with the wire format
rpc.events.on('decodingError', console.log);
// Wait for socket.on('secureConnect', ...) before issuing RPC calls
async function login(username: string, password: string): Promise<boolean> {
const { result, sent } = rpc.daemon.login(username, password);
try {
await sent;
} catch {
console.log('Login message not sent');
return false;
}
const res = await result;
if (isRPCError(res)) {
console.log('Login error:', res.error);
return false;
}
return true;
}
async function addTorrent(url: string): Promise<boolean> {
const { result, sent } = rpc.core.addTorrentUrl(url);
await sent;
const res = await result;
return !isRPCError(res);
}Deluge 2.x's daemon.login raises IncompatibleClient if the request
doesn't include a client_version kwarg. This library always sends one;
the default is its own package version. Override it if you need to
impersonate a specific Deluge client:
const rpc = DelugeRPC(socket, { clientVersion: '2.1.1' });All arguments to API functions at any depth can be promises — they are awaited before being sent.
Deluge's wire format uses snake_case. Named option arguments accept
camelCase and are converted automatically. Responses are also converted
to camelCase by default; opt out with camelCaseResponses: false.
- Fixed: Deluge 2.x (protocol v1) responses were decoded five bytes short.
The receive path ended the payload slice at
payloadLengthrather than atpacketLength, andBuffer.slicetakes an end offset rather than a count. The lost bytes are the Adler-32 trailer plus the final deflate byte, which usually holds only the end-of-block code and padding — so most responses still decoded and the bug stayed hidden. When that byte carried real symbol bits the inflated output came up short and rencode threwTried to access data[N] but data len is: N. Reported and fixed by @dkacperski97 in #16. - Fixed: A packet that fails to decode is now reported via the
decodingErrorevent and skipped, instead of throwing out of the socketdatahandler as an uncaught exception that took the process down. Packets behind the bad one still parse. - Tests: First coverage of the protocol v1 receive path, replaying a real frame captured off a Deluge 2.2 daemon.
Promoted from 1.0.0-alpha after consumer validation against a real
Deluge 2.x daemon. No functional changes from the alpha.
- Toolchain: Bun-first; ESM; TypeScript 6; dropped yarn/ts-jest/ts-node/coveralls/cspell.
- Deps: Bumped
python-rencodeto^2.0.0andsmallest-power-of-twoto^2.0.0. - Tests: Ported from jest to
bun:test. Integration tests against a real daemon now gate onDELUGE1_PORT(orDELUGE_PORT) and are skipped in CI. - CI: New bun-based
ci.ymlandpublish.yml; publishing uses npm Trusted Publishing (OIDC, with provenance). Pre-release versions go to thenextdist-tag. - Breaking: ESM-only, no CommonJS
require. The runtime API is unchanged fromv0.5.0; migration is just the import switch.
- Fixed
IncompatibleClientwhen logging in to Deluge 2.x daemons by sending the requiredclient_versionkwarg (it was never sent in v0.4.0). Added aclientVersionfactory option. - Replaced the broken legacy publish job with a tag-triggered Trusted Publishing workflow.
See git tags.
bun install
bun run lint # type-check
bun test src # unit tests
bun run build # tsc → dist/Integration tests against a real Deluge daemon:
DELUGE1_PORT=58846 DELUGE1_HOST=localhost bun test src