Sliver-script is a TypeScript/JavaScript client library for Sliver, it can be used to automate any operator interaction with Sliver. Sliver-script uses existing Sliver client configuration files and connects to servers using gRPC over Mutual-TLS. It also provides RxJS abstractions for easy interactions with real-time components.
This library targets modern Sliver protobuf/gRPC APIs and provides a strongly-typed TypeScript-first client.
The standalone package lineage and generic adaptations are pinned in integration.lock.json. The canonical Sliver/protobuf source and generated artifacts are pinned in protobuf.lock.json, which the integration lock cross-references. Both locks describe only this package and its upstream protocol inputs. The package exposes explicit typed convenience methods and no method-name/request-object dispatcher. For compatibility it still exports the generated rpcpb namespace and the existing statically typed SliverClient.rpc getter; trusted applications must impose their own narrower capability boundary.
Node v24 or later is required for this package, and it can be installed via npm:
npm install sliver-script
npm run test:e2e compiles the server from the pinned Sliver submodule, creates
an isolated multiplayer profile with the server CLI, starts the native server,
and runs the grouped TypeScript client tests against it. GitHub Actions runs the
same path on Linux/amd64, Windows/amd64, and macOS/arm64. See
e2e/README.md for the implemented groups and expansion plan.
Operator connections use direct mTLS. Configurations containing a WireGuard operator profile are rejected before credentials or channels are created. WireGuard listener and implant C2 APIs remain available.
The client provides stateful TCP port-forward, reverse-port-forward, and SOCKS5
handles. Local port forwards and SOCKS5 listeners are owned by the
SliverClient and close when it disconnects. Reverse port forwards are owned by
the teamserver: their handles become detached on disconnect and reconcile with
the authoritative server inventory after the client reconnects.
const portForward = await client.startPortForward(sessionId, {
bind: { host: "127.0.0.1", port: 0 }, // zero selects a free local port
target: { host: "intranet.example", port: 443 },
})
const socks = await client.startSocks5Proxy(sessionId, {
bind: { host: "127.0.0.1", port: 0 },
authentication: { username: "operator", password: process.env.SOCKS_PASSWORD! },
})
const reverse = await client.startReversePortForward(sessionId, {
bind: { host: "127.0.0.1", port: 8080 }, // opened by the implant
target: { host: "127.0.0.1", port: 3000 }, // opened by the teamserver
})
portForward.connection$.subscribe((connection) => {
console.log(connection.status, connection.bytesToTarget, connection.bytesFromTarget)
})
await Promise.all([portForward.close(), socks.close(), reverse.close()])Local listeners expose their actual bound address, immutable state snapshots,
state and per-connection observables, bounded connection/buffer controls, and
idempotent close(). Use listPortForwards() / stopPortForward(id) and
listSocks5Proxies() / stopSocks5Proxy(id) for client-owned inventory. Use
listReversePortForwards() / stopReversePortForward() for server-owned
inventory, including safe cleanup of legacy listeners whose target metadata is
not trusted. Sliver's generic forwarding protocol currently supports TCP; the
application traffic carried over it can be HTTP, RDP, or any other TCP protocol.
These tunnels use full-close TCP semantics: a local FIN retires both directions,
so protocols that send a delayed response only after the client half-closes are
not supported. A SOCKS connection's open event means its Sliver lifecycle
stream is established; SOCKS authentication and the target CONNECT exchange
still occur afterward. Authentication and CONNECT rejections are returned in
the SOCKS wire replies and follow the ordinary connection-close lifecycle; the
handle does not parse these negotiation results. A protocol-error event means
malformed Sliver SOCKS framing, such as invalid sequence or acknowledgement data.
Generated TypeScript under src/pb is locked to Sliver commit bbb20155b7a18d4906ec936566bf0dc61fe38f35, protoc 35.1, and ts-proto 2.12.3. The generator reads only the pinned sliver submodule; it never selects a neighboring checkout.
From a Git source checkout, install protoc 35.1 after npm ci, initialize the pinned submodule, and run:
git submodule update --init --recursive
npm run protobuf:checknpm run protobuf:generate rewrites the five checked-in protobuf modules only after the source commit, source tree, input hashes, descriptor hash, generator version, and generated byte hashes all match protobuf.lock.json.
That command is restorative, not an upgrade command. Advancing Sliver, protoc, or ts-proto requires a deliberate maintainer review that regenerates and updates every source, descriptor, and output hash in the lock together.
The npm tarball carries those generated TypeScript modules and their provenance lock, but deliberately excludes the Sliver submodule and therefore is not a self-contained protobuf regeneration checkout.
Before preparing a package, run npm run audit:all and npm run verify. Verification audits the exact packed runtime dependency graph, then executes unit tests, protobuf checks, a clean TypeScript build, npm pack --dry-run, and CommonJS, ESM, and TypeScript NodeNext smoke tests against the packed tarball in a temporary consumer. The packed tarball includes the TypeScript source and locked protobuf provenance in addition to compiled JavaScript and declarations.
Pushing a version tag such as v2.0.0 starts the npm publishing workflow. Both
Build Check and the full native E2E matrix must pass for that commit before the
validated tarball can be published. Stable versions use npm's latest channel;
prereleases such as v2.0.0-rc.1 use next.
The workflow also supports a manual dry-run before creating a tag. See
RELEASING.md
for npm trusted publisher setup, dry-runs, release steps, and retry behavior.
import { SliverClient, ParseConfigFile } from 'sliver-script'
(async function() {
const config = await ParseConfigFile('./localhost.cfg')
const client = new SliverClient(config)
await client.connect()
const version = await client.getVersion()
console.log(version)
const sessions = await client.sessions()
console.log(`Sessions: ${sessions.length}`)
await client.disconnect()
})()import { SliverClient, ParseConfigFile } from 'sliver-script'
(async function() {
const config = await ParseConfigFile('./localhost.cfg')
const client = new SliverClient(config)
await client.connect()
client.event$.subscribe((event) => {
console.log(event)
})
})()import { SliverClient, ParseConfigFile } from 'sliver-script'
(async function() {
const config = await ParseConfigFile('./localhost.cfg')
const client = new SliverClient(config);
await client.connect()
console.log('Waiting for new sessions ...')
client.session$.subscribe(async (event) => {
console.log(`New session #${event.Session.ID}!`)
const session = client.interactSession(event.Session.ID)
const ls = await session.ls('.')
console.log(`Path: ${ls.Path}`)
ls.Files.forEach(file => {
console.log(`Name: ${file.Name} (Size: ${file.Size})`)
})
})
})()const sliver = require('sliver-script');
;(async function() {
const config = await sliver.ParseConfigFile('./localhost.cfg')
const client = new sliver.SliverClient(config)
await client.connect()
console.log('Waiting for new sessions ...')
client.session$.subscribe(async (event) => {
console.log(`New session #${event.Session.ID}!`)
const session = client.interactSession(event.Session.ID)
const ls = await session.ls('.')
console.log(`Path: ${ls.Path}`)
ls.Files.forEach(file => {
console.log(`Name: ${file.Name} (Size: ${file.Size})`)
})
})
})()